Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
1. 日本語で話してください
2. 質問はAskUserQuestion(`request_user_input`)を使ってください
14 changes: 7 additions & 7 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ JSX を再帰的に ANSI 文字列へ変換するだけで足りる。Ink は yo
`(default: name="world", --loud=false)` の形で添える。一覧は「どれを打つか」
だけでなく「打たないと何が起きるか」を選ぶ場面でもあり、`--help` まで降りないと
既定値が分からないのは暗黙の振る舞いになる。hidden なオプションは出さない。
`summarizeSpec()` が組み立て、test/runtime/help.test.tsx が固定する。
`summarizeSpec()` が組み立て、test/features/conventions/help/help.test.tsx が固定する。

## ADR 9: 型はビルド時の codegen で配る

Expand Down Expand Up @@ -424,7 +424,7 @@ layout に到達した `<Dynamic>` は誤りとして落とす。
島の無いドキュメントは従来どおり 1 回書きのまま。

レビュー (PR #16) で固めた個々の契約 (評価の順序、repaint のライフサイクル、
シグナルでの後始末など) は、文章ではなく test/renderer/present.test.tsx に
シグナルでの後始末など) は、文章ではなく test/core/renderer/present.test.tsx に
置いてある (ADR 15)。破ればテストが気づかせてくれる。

**代償と割り切り**: リサイズ (SIGWINCH) は折り返しの変化で前の領域を正確に
Expand Down Expand Up @@ -1075,7 +1075,7 @@ React が要素を `Symbol.for('react.element')` で見分けるのと同じ手
stderr に足す (`--json` では `error.trace`)。フラグではなく環境変数にしたのは、
`--verbose` / `--debug` は利用者が自分のオプションとして宣言しがちな名前で
(README の middleware の例、ADR 11)、予約すると既存の宣言を壊すため。
test/runtime/handle-error.test.tsx が「包んでも場所が潰れない」ことを固定する。
test/features/inherited/error/handle-error.test.tsx が「包んでも場所が潰れない」ことを固定する。

**旧い印は 1 年付け続け、見続ける** (ADR 20)。新バージョンが投げたエラーを
旧バージョンが受ける (逆も) 場面があるため、新しいインスタンスは旧いシンボルも
Expand Down Expand Up @@ -1131,7 +1131,7 @@ TypeScript を依存に足すほどの仕事ではない
(ADR 5 の「宣言は import して呼ぶ」と同じ節約)。

**自分の書き換えで watch が 1 回余分に回る**が、2 回目は差分が無いので止まる。
test/build/annotate.test.ts が「注釈が無いときだけ足す」「import を足す」
test/core/build/annotate.test.ts が「注釈が無いときだけ足す」「import を足す」
「既にあれば触らない」を固定する。

## ADR 45: 実行例は `example.tsx` で宣言する。`decopin docs` はそれだけを実行する
Expand Down Expand Up @@ -1238,7 +1238,7 @@ test/intent/returns/returns.test.ts が「型が配られていること」
`INHERITED_FILES`・`ROOT_ONLY_FILES`) と継承の解決を読むので、core には
置けない (ADR 41)。`cli/docs/document.ts` (ADR 45) と同じ形。

test/build/tree.test.ts が木の形を、test/intent/build/build.test.ts の
test/cli/build/tree.test.ts が木の形を、test/intent/build/build.test.ts の
`shows-what-each-command-is-made-of` が CLI を通した出力を固定する。

---
Expand Down Expand Up @@ -1281,10 +1281,10 @@ Intent の断片が残り、6 つの Behavior が ✓ で出たことがある (

**新しいサブコマンド (`src/cli/<name>/`) は Intent から書く**。既存の機能は
触るときに回収する (Intent Recovery)。**すべてのテストを Evidence にはしない**。
`test/build/` のように、Behavior ではなく実装の形を見るテストは普通のテストの
`test/core/build/` のように、Behavior ではなく実装の形を見るテストは普通のテストの
まま隣に置く。

**結末を見ているテストは、動かさずに分担させる**。`test/runtime/handle-error.test.tsx`
**結末を見ているテストは、動かさずに分担させる**。`test/features/inherited/error/handle-error.test.tsx`
のように規約の隣にあるテストは、`describeBehavior` で包んで
`report(impl, { partial: true })` を末尾に置くだけで Evidence になる。分担する
Intent は元のファイルも `partial` にする — 非 partial の `report()` が先に走ると、
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"site:dev": "bun scripts/serve-site.ts",
"ci": "bun scripts/ci.tsx",
"build:package": "bun scripts/build-package.ts",
"version:next": "bun scripts/next-version.ts"
"version:next": "bun scripts/next-version.ts",
"check:layout": "bun scripts/check-test-layout.ts"
},
"dependencies": {
"valibot": "^1.5.0"
Expand Down
77 changes: 77 additions & 0 deletions scripts/check-test-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bun
/**
* src と test のフォルダ構成が揃っているかを調べる。
*
* bun scripts/check-test-layout.ts
*
* ソースファイル (.ts / .tsx) を直に持つ src 配下のフォルダごとに、
* test 側の同じ相対パスにフォルダがあり、その中にテストファイル
* (*.test.ts / *.test.tsx) が 1 つ以上あることを求める。
*
* src/cli/build/*.ts → test/cli/build/*.test.ts
*
* src 直下 (公開 API の入口) は対象外。欠けがあれば一覧を出して exit 1。
*/
import { existsSync, readdirSync } from 'node:fs';
import { join, relative } from 'node:path';

const ROOT = join(import.meta.dir, '..');
const SRC = join(ROOT, 'src');
const TEST = join(ROOT, 'test');

const SOURCE = /\.tsx?$/;
const TEST_FILE = /\.test\.tsx?$/;

/** dir 以下のフォルダを再帰で集める (dir 自身も含む) */
function directories(dir: string): string[] {
const entries = readdirSync(dir, { withFileTypes: true });
return [
dir,
...entries
.filter((entry) => entry.isDirectory())
.flatMap((entry) => directories(join(dir, entry.name))),
];
}

/** dir 直下のファイル名 (無ければ空) */
function files(dir: string): string[] {
try {
return readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
} catch {
return [];
}
}

type Problem = { path: string; reason: 'no-dir' | 'no-test' };

function check(): Problem[] {
return directories(SRC)
.filter((dir) => dir !== SRC)
.filter((dir) => files(dir).some((name) => SOURCE.test(name)))
.flatMap((dir): Problem[] => {
const path = relative(SRC, dir);
const testDir = join(TEST, path);
if (!existsSync(testDir)) return [{ path, reason: 'no-dir' }];
return files(testDir).some((name) => TEST_FILE.test(name))
? []
: [{ path, reason: 'no-test' }];
})
.sort((a, b) => a.path.localeCompare(b.path));
}

const problems = check();

if (problems.length === 0) {
console.log('✓ src と test の構成は揃っています');
process.exit(0);
}

for (const { path, reason } of problems) {
const detail =
reason === 'no-dir' ? 'フォルダがありません' : 'テストファイルがありません';
console.error(`✗ test/${path} (${detail}, 対応: src/${path})`);
}
console.error(`\n${problems.length} 件のフォルダが揃っていません`);
process.exit(1);
Loading
Loading