@cheongfish/bouncer 0.0.0-stage → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -2
  3. package/bin/bouncer +26 -0
  4. package/bin/bouncer-root +25 -0
  5. package/bin/build-stamp.js +78 -0
  6. package/bin/register-path.js +71 -0
  7. package/dist/.build-stamp +1 -0
  8. package/dist/approval-snapshot.js +99 -0
  9. package/dist/bouncer-root.js +80 -0
  10. package/dist/canonical-json.js +37 -0
  11. package/dist/cli-commit-guard-command.js +74 -0
  12. package/dist/cli-config-command.js +47 -0
  13. package/dist/cli-context-review-command.js +273 -0
  14. package/dist/cli-current-command.js +374 -0
  15. package/dist/cli-dispatch-command.js +119 -0
  16. package/dist/cli-doc-commands.js +352 -0
  17. package/dist/cli-epic-command.js +130 -0
  18. package/dist/cli-flags.js +50 -0
  19. package/dist/cli-git-commands.js +931 -0
  20. package/dist/cli-intent-command.js +491 -0
  21. package/dist/cli-metrics-command.js +134 -0
  22. package/dist/cli-ontology-command.js +109 -0
  23. package/dist/cli-project-commands.js +580 -0
  24. package/dist/cli-retro-command.js +138 -0
  25. package/dist/cli-review-command.js +251 -0
  26. package/dist/cli-review-dispatch-command.js +338 -0
  27. package/dist/cli-usage.js +72 -0
  28. package/dist/cli.js +70 -0
  29. package/dist/codex-agents.js +189 -0
  30. package/dist/command-argv.js +125 -0
  31. package/dist/commit-guard.js +72 -0
  32. package/dist/commit-hook.js +470 -0
  33. package/dist/commit-message.js +77 -0
  34. package/dist/commit-sha.js +107 -0
  35. package/dist/commit.js +519 -0
  36. package/dist/composite-gates.js +129 -0
  37. package/dist/comprehension.js +127 -0
  38. package/dist/config.js +298 -0
  39. package/dist/context-ids.js +288 -0
  40. package/dist/context-migration-rewrite.js +421 -0
  41. package/dist/context-ontology.js +746 -0
  42. package/dist/context-review-contracts.js +2 -0
  43. package/dist/context-review-projection.js +147 -0
  44. package/dist/context-review-state.js +824 -0
  45. package/dist/context-review-store.js +602 -0
  46. package/dist/context-review-transitions.js +412 -0
  47. package/dist/coordinate-advance.js +253 -0
  48. package/dist/coordinate-next.js +998 -0
  49. package/dist/coordinate-output.js +57 -0
  50. package/dist/coordinator-command-contracts.js +2 -0
  51. package/dist/coordinator-commands.js +1908 -0
  52. package/dist/coordinator-fanin.js +1021 -0
  53. package/dist/coordinator-git.js +121 -0
  54. package/dist/coordinator-input.js +37 -0
  55. package/dist/coordinator-ledger.js +381 -0
  56. package/dist/coordinator-prepare.js +1047 -0
  57. package/dist/coordinator-repair.js +1416 -0
  58. package/dist/coordinator-verification.js +42 -0
  59. package/dist/coordinator-worker.js +388 -0
  60. package/dist/coordinator.js +7 -0
  61. package/dist/cross-drive.js +85 -0
  62. package/dist/current.js +1148 -0
  63. package/dist/dag-shape.js +93 -0
  64. package/dist/doc-shape-contracts.js +241 -0
  65. package/dist/drive-metrics.js +691 -0
  66. package/dist/epic-index.js +347 -0
  67. package/dist/epic-registry.js +453 -0
  68. package/dist/execute-prepare.js +262 -0
  69. package/dist/execution-contracts.js +46 -0
  70. package/dist/execution-dag.js +148 -0
  71. package/dist/finalize-checks.js +177 -0
  72. package/dist/finalize-collect.js +343 -0
  73. package/dist/finalize-comprehension.js +116 -0
  74. package/dist/finalize-digest-collect.js +388 -0
  75. package/dist/finalize-digest-model.js +595 -0
  76. package/dist/finalize-digest.js +164 -0
  77. package/dist/finalize-explain.js +176 -0
  78. package/dist/finalize-pr.js +610 -0
  79. package/dist/finalize-release-main.js +652 -0
  80. package/dist/finalize.js +714 -0
  81. package/dist/frontmatter.js +22 -0
  82. package/dist/git-ops.js +49 -0
  83. package/dist/graph-exec.js +179 -0
  84. package/dist/graph-scope.js +268 -0
  85. package/dist/graphify.js +995 -0
  86. package/dist/guards.js +82 -0
  87. package/dist/import-git.js +78 -0
  88. package/dist/import-history.js +392 -0
  89. package/dist/import-render.js +59 -0
  90. package/dist/import-types.js +2 -0
  91. package/dist/init.js +571 -0
  92. package/dist/intent-bundle.js +936 -0
  93. package/dist/intent-provenance.js +927 -0
  94. package/dist/layout.js +131 -0
  95. package/dist/lease.js +145 -0
  96. package/dist/legacy-comments.js +71 -0
  97. package/dist/migrate-context-slugs.js +262 -0
  98. package/dist/migrate-merge-legacy-epics.js +392 -0
  99. package/dist/migrate-review-scope.js +144 -0
  100. package/dist/migrate-task-layout.js +194 -0
  101. package/dist/path-overlap.js +38 -0
  102. package/dist/paths.js +45 -0
  103. package/dist/plan-approve.js +218 -0
  104. package/dist/plan-inspect.js +575 -0
  105. package/dist/plan-prerequisites.js +197 -0
  106. package/dist/plan-snapshot-contract.js +98 -0
  107. package/dist/plan-snapshot.js +138 -0
  108. package/dist/plugin-root.js +237 -0
  109. package/dist/posix-path.js +14 -0
  110. package/dist/pre-commit-hook.js +170 -0
  111. package/dist/print-dispatch.js +285 -0
  112. package/dist/render.js +8 -0
  113. package/dist/retention-migration.js +595 -0
  114. package/dist/retro-read.js +259 -0
  115. package/dist/retro-sources.js +318 -0
  116. package/dist/review-dispatch.js +1054 -0
  117. package/dist/review-policy.js +28 -0
  118. package/dist/review-record.js +447 -0
  119. package/dist/review-repair-state.js +52 -0
  120. package/dist/review-snapshot.js +659 -0
  121. package/dist/run-detach.js +165 -0
  122. package/dist/run-preflight.js +368 -0
  123. package/dist/runtime-state.js +1038 -0
  124. package/dist/scaffold.js +533 -0
  125. package/dist/schema.js +294 -0
  126. package/dist/scope.js +1073 -0
  127. package/dist/seed-worktree.js +926 -0
  128. package/dist/session-graph.js +337 -0
  129. package/dist/subagents.js +86 -0
  130. package/dist/symbol-index.js +1390 -0
  131. package/dist/task-brief-hash.js +34 -0
  132. package/dist/task-commits.js +124 -0
  133. package/dist/tasks-docs.js +192 -0
  134. package/dist/templates.js +469 -0
  135. package/dist/time.js +21 -0
  136. package/dist/validate-docs.js +302 -0
  137. package/dist/validate-gates-finalize.js +303 -0
  138. package/dist/validate-gates-plan.js +720 -0
  139. package/dist/validate-gates-run.js +420 -0
  140. package/dist/validate-gates-shared.js +124 -0
  141. package/dist/validate-gates.js +87 -0
  142. package/dist/validate-sections.js +654 -0
  143. package/dist/validate-structural.js +347 -0
  144. package/dist/validate.js +417 -0
  145. package/dist/verification-contracts.js +26 -0
  146. package/dist/verification-evidence.js +233 -0
  147. package/dist/verification-execution.js +163 -0
  148. package/dist/verification-identity.js +413 -0
  149. package/dist/verification-target.js +51 -0
  150. package/dist/verification.js +383 -0
  151. package/dist/windows-npm.js +38 -0
  152. package/package.json +32 -4
  153. package/resources/graphify-compat.json +9 -0
  154. package/vendor/README.md +23 -0
  155. package/vendor/js-yaml.LICENSE +21 -0
  156. package/vendor/js-yaml.js +3022 -0
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+ const guards = require("./guards");
3
+ const { isRecord } = guards;
4
+ const frontmatter = require("./frontmatter");
5
+ const { parseFrontmatter } = frontmatter;
6
+ const approvalSnapshot = require("./approval-snapshot");
7
+ const { sha256Canonical } = approvalSnapshot;
8
+ const LIFECYCLE_KEYS = ['status', 'commit_sha'];
9
+ /**
10
+ * task brief의 canonical SHA-256을 계산한다.
11
+ * dispatch·record·intent bundle이 같은 값을 써야, commit이 `status`/`commit_sha`만
12
+ * 찍거나 YAML을 다시 써도 stale-worker-report가 나지 않는다. 본문이나 그 외
13
+ * frontmatter가 바뀌면 해시도 바뀐다.
14
+ *
15
+ * js-yaml은 따옴표 없는 날짜를 Date로 읽는다. canonicalJson은 Date를 객체로
16
+ * 보고 `{}`로 접으므로, 해시 직전에 JSON 왕복으로 ISO 문자열로 고정한다.
17
+ *
18
+ * @param {string} markdown - tasks.md 전체 텍스트(UTF-8)
19
+ * @returns {string} 64자리 소문자 hex
20
+ */
21
+ function taskBriefHash(markdown) {
22
+ const parsed = parseFrontmatter(markdown);
23
+ // 1. 입력 data를 깊은 복사한 뒤에만 lifecycle 키를 지운다. parseFrontmatter
24
+ // 결과를 직접 delete하면 같은 객체를 쥔 호출자가 status를 잃는다.
25
+ const data = structuredClone(parsed.data);
26
+ if (isRecord(data) && isRecord(data.bouncer)) {
27
+ for (const key of LIFECYCLE_KEYS) {
28
+ delete data.bouncer[key];
29
+ }
30
+ }
31
+ // 2. Date → ISO 문자열. 같은 날짜 값이 YAML 인용만 달라도 같은 해시가 된다.
32
+ return sha256Canonical(JSON.parse(JSON.stringify({ data, body: parsed.body })));
33
+ }
34
+ module.exports = { taskBriefHash, LIFECYCLE_KEYS };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+ const { spawnSync } = require('node:child_process');
3
+ const commitSha = require('./commit-sha');
4
+ const { normalizeCommitSha } = commitSha;
5
+ const contextIds = require("./context-ids");
6
+ const { normalizeStableId } = contextIds;
7
+ const TRAILER_RE = /^Bouncer-Task\s*:\s*(.*?)\s*$/;
8
+ // fan-in이 붙인 worker 출처. 40자리 hex가 아니면 매칭하지 않아 workerSha가 null이 된다.
9
+ const WORKER_TRAILER_RE = /^Bouncer-Worker-Commit\s*:\s*([0-9a-f]{40})\s*$/m;
10
+ // `%H%x00%B`는 커밋 사이에 구분 NUL이 없어 다음 SHA가 본문 뒤에 붙는다.
11
+ // SHA\0 위치를 먼저 찾고, 본문은 다음 SHA 직전까지로 자른다.
12
+ const FULL_SHA_NUL_RE = /([0-9a-f]{40})\0/gi;
13
+ /**
14
+ * 기본 git 실행기. 주입 가능 exec와 같은 `{ status, stdout }` 모양만 돌려
15
+ * 테스트 seam이 stderr 유무로 갈라지지 않게 한다.
16
+ *
17
+ * @param {string} repoRoot - git cwd
18
+ * @param {string[]} args - `git` 뒤 argv
19
+ * @returns {{ status: number, stdout: string }}
20
+ */
21
+ function defaultExec(repoRoot, args) {
22
+ const r = spawnSync('git', args, {
23
+ cwd: repoRoot,
24
+ encoding: 'utf8',
25
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
26
+ });
27
+ return {
28
+ status: typeof r.status === 'number' ? r.status : 1,
29
+ stdout: r.stdout || '',
30
+ };
31
+ }
32
+ /**
33
+ * `git log --format=%H%x00%B` 출력을 `{ sha, body }[]`로 쪼갠다.
34
+ * format이 커밋 경계 NUL을 넣지 않으므로 `\0` split만으로는 다음 SHA가
35
+ * 이전 본문에 붙는다 — SHA\0 앵커로만 경계를 잡는다.
36
+ *
37
+ * @param {string} stdout - git log 원문
38
+ * @returns {Array<{ sha: string, body: string }>} 최신순 커밋
39
+ */
40
+ function parseShaBodyLog(stdout) {
41
+ const starts = [];
42
+ FULL_SHA_NUL_RE.lastIndex = 0;
43
+ let match;
44
+ while ((match = FULL_SHA_NUL_RE.exec(stdout)) !== null) {
45
+ starts.push({
46
+ sha: match[1].toLowerCase(),
47
+ bodyStart: match.index + match[0].length,
48
+ shaStart: match.index,
49
+ });
50
+ }
51
+ const out = [];
52
+ for (let i = 0; i < starts.length; i += 1) {
53
+ const end = i + 1 < starts.length ? starts[i + 1].shaStart : stdout.length;
54
+ out.push({
55
+ sha: starts[i].sha,
56
+ body: stdout.slice(starts[i].bodyStart, end),
57
+ });
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * `base..head` 커밋 본문에서 `Bouncer-Task` trailer만 읽어 stable ID → SHA 맵을 만든다.
63
+ * 문서를 읽지 않는다 — digest와 Explain 제목이 같은 trailer 출처를 쓰게 하기 위함이다.
64
+ * 같은 ID가 여러 커밋에 있으면 최신(git log 기본 최신순)만 남긴다.
65
+ *
66
+ * @param {object} opts
67
+ * @param {string} opts.repoRoot - git cwd(finalize checkout)
68
+ * @param {string} opts.base - 범위 시작(미포함)
69
+ * @param {string} opts.head - 범위 끝(포함). 보통 HEAD
70
+ * @param {string[]} opts.stableIds - 찾을 `EPIC-…/BP-…/TASK-…` 목록
71
+ * @param {(args: string[]) => { status: number, stdout: string }} [opts.exec] - `git` 뒤 argv seam
72
+ * @param {object | null} [opts.registry] - 등록부. 있으면 구형·slug trailer를 `normalizeStableId`로 같은 키에 묶는다.
73
+ * 없으면(또는 별칭이 없으면) 정확 일치로 비교해 이관 전 동작과 같다
74
+ * @returns {Map<string, { sha: string, sha8: string, workerSha: string | null }>} 매칭된 ID만 담은 맵.
75
+ * 키는 호출자가 넘긴 `stableIds` 원래 값이다
76
+ */
77
+ function resolveTaskCommits({ repoRoot, base, head, stableIds, exec, registry, }) {
78
+ // 묶음 키: 별칭 표로 정규화한 ID. 정규화 불가(별칭 없음·해석 불가)면 원래 문자열로 비교한다.
79
+ // 같은 task의 구형·slug trailer가 한 키로 모여 "가장 최근 커밋" 규칙이 형식을 가로질러 적용된다.
80
+ const groupKey = (id) => normalizeStableId(id, registry ?? null) ?? id;
81
+ // 묶음 키 → 호출자가 넘긴 원래 ID. 반환 Map은 호출자 값을 키로 써야 호출부 조회가 그대로 맞는다.
82
+ // 호출자가 같은 task를 두 형식으로 함께 넘겨도 둘 다 채우도록 목록으로 둔다.
83
+ const wanted = new Map();
84
+ for (const id of Array.isArray(stableIds) ? stableIds : []) {
85
+ if (typeof id !== 'string' || id === '')
86
+ continue;
87
+ wanted.set(groupKey(id), [...(wanted.get(groupKey(id)) ?? []), id]);
88
+ }
89
+ const out = new Map();
90
+ if (wanted.size === 0)
91
+ return out;
92
+ const run = typeof exec === 'function'
93
+ ? exec
94
+ : (args) => defaultExec(repoRoot, args);
95
+ const logged = run(['log', '--format=%H%x00%B', `${base}..${head}`]);
96
+ if (logged.status !== 0 || !logged.stdout)
97
+ return out;
98
+ for (const record of parseShaBodyLog(logged.stdout)) {
99
+ let taskId = null;
100
+ for (const line of record.body.split('\n')) {
101
+ const trailer = TRAILER_RE.exec(line);
102
+ if (!trailer)
103
+ continue;
104
+ // 같은 본문에 Task trailer가 둘이면 첫 줄만 본다. 상충 값을 고르지 않는다.
105
+ taskId = trailer[1];
106
+ break;
107
+ }
108
+ if (!taskId)
109
+ continue;
110
+ const callerIds = wanted.get(groupKey(taskId));
111
+ if (callerIds === undefined || callerIds.every((id) => out.has(id)))
112
+ continue;
113
+ const sha8 = normalizeCommitSha(record.sha);
114
+ if (!sha8)
115
+ continue;
116
+ const workerSha = WORKER_TRAILER_RE.exec(record.body)?.[1] ?? null;
117
+ for (const id of callerIds) {
118
+ if (!out.has(id))
119
+ out.set(id, { sha: record.sha, sha8, workerSha });
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ module.exports = { resolveTaskCommits };
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ // 레거시 단일/번호 문서와 새 tasks/<NNN>/ 묶음을 한 모듈에서만 판정한다.
5
+ // 다른 모듈이 tasks.md / verification.md / review.md / \d{3} 문자열을
6
+ // 직접 매칭하지 않게 하기 위함 (묶음 basename 정본).
7
+ const LEGACY_TASKS_BASENAME = 'tasks.md';
8
+ const INITIAL_NUMBERED_TASKS_BASENAME = 'tasks-001.md';
9
+ // tasks-1.md · tasks-01.md 는 정본이 아니다 — 세 자리 zero-pad만 인정.
10
+ const NUMBERED_TASKS_RE = /^tasks-(\d{3})\.md$/;
11
+ // 새 레이아웃: <bp>/tasks/<NNN>/ — NNN 아닌 이름(01, foo)은 invalidDirs 로만 보고,
12
+ // 엔트리로 올리지 않는다. 거절 게이트는 002.
13
+ const TASK_DIR_RE = /^(\d{3})$/;
14
+ const TASK_UNIT_BASENAMES = ['tasks.md', 'verification.md', 'review.md'];
15
+ const schema = require("./schema");
16
+ const posixPath = require("./posix-path");
17
+ const { toPosix } = posixPath;
18
+ const { executionKindOf } = schema;
19
+ const frontmatter = require("./frontmatter");
20
+ const { readDoc } = frontmatter;
21
+ function isNumberedTasksBasename(name) {
22
+ return typeof name === 'string' && NUMBERED_TASKS_RE.test(name);
23
+ }
24
+ function isLegacyTasksBasename(name) {
25
+ return name === LEGACY_TASKS_BASENAME;
26
+ }
27
+ /**
28
+ * basename → 문서 kind. paths.parsePathIds 가 FILE_KIND 에
29
+ * verification/review 문자열을 두지 않도록 여기로 모은다.
30
+ * 번호 붙은 레거시 tasks-{NNN}.md 도 tasks 로 본다.
31
+ */
32
+ function unitDocKind(basename) {
33
+ if (typeof basename !== 'string')
34
+ return null;
35
+ if (isNumberedTasksBasename(basename))
36
+ return 'tasks';
37
+ const idx = TASK_UNIT_BASENAMES.indexOf(basename);
38
+ if (idx === 0)
39
+ return 'tasks';
40
+ if (idx === 1)
41
+ return 'verification';
42
+ if (idx === 2)
43
+ return 'review';
44
+ return null;
45
+ }
46
+ /**
47
+ * 모든 task 소비자가 같은 부재=commit 규칙을 쓰도록 frontmatter를 해석한다.
48
+ *
49
+ * @param {unknown} data - tasks.md의 전체 frontmatter 객체
50
+ * @returns {'commit' | 'verification' | null} 유효 종류 또는 잘못된 선언의 null
51
+ */
52
+ function taskExecutionKind(data) {
53
+ const bouncer = data && typeof data === 'object' && !Array.isArray(data)
54
+ ? data.bouncer
55
+ : null;
56
+ return executionKindOf(bouncer);
57
+ }
58
+ /**
59
+ * S5 기대 id (레거시 판정용).
60
+ * - tasks.md → TASKS-{blueprintId} (레거시: 파일 이름에 번호가 없어 blueprint id를 씀)
61
+ * - tasks-{NNN}.md → TASKS-{NNN} (파일 번호가 곧 task id)
62
+ */
63
+ function expectedTasksId(basename, blueprintId) {
64
+ if (typeof basename !== 'string')
65
+ return null;
66
+ const m = NUMBERED_TASKS_RE.exec(basename);
67
+ if (m)
68
+ return `TASKS-${m[1]}`;
69
+ if (basename === LEGACY_TASKS_BASENAME && typeof blueprintId === 'string' && blueprintId) {
70
+ return `TASKS-${blueprintId}`;
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * 새 묶음 문서의 기대 id 셋. number 는 '001' 또는 1 alike — 항상 zero-pad 세 자리로 정규화.
76
+ */
77
+ function expectedTaskDocIds(number) {
78
+ const digits = typeof number === 'number'
79
+ ? String(number).padStart(3, '0')
80
+ : String(number).padStart(3, '0');
81
+ return {
82
+ tasks: `TASKS-${digits}`,
83
+ verification: `VERIFY-${digits}`,
84
+ review: `REVIEW-${digits}`,
85
+ };
86
+ }
87
+ function makeEntry({ dir, number, tasks, verification, review, executionKind, }) {
88
+ return {
89
+ dir,
90
+ number,
91
+ tasks,
92
+ verification,
93
+ ...(review ? { review } : {}),
94
+ rel: tasks.rel,
95
+ id: tasks.id,
96
+ basename: path.posix.basename(tasks.rel),
97
+ executionKind,
98
+ };
99
+ }
100
+ /**
101
+ * blueprint 디렉터리에서 task 묶음 목록을 번호 오름차순으로 돌려준다.
102
+ * 우선순위: tasks/<NNN>/ 디렉터리 엔트리, 그다음 루트 레거시 파일.
103
+ * tasks.md frontmatter는 정규화된 executionKind를 붙이기 위해 한 번 읽는다.
104
+ * 그 외 문서 내용은 읽지 않으며 invalidDirs 거절은 002 게이트가 맡는다.
105
+ */
106
+ function listTasksDocs({ repoRoot, blueprintDir }) {
107
+ const bp = toPosix(blueprintDir);
108
+ const absDir = path.join(repoRoot, bp);
109
+ let names;
110
+ try {
111
+ names = fs.readdirSync(absDir);
112
+ }
113
+ catch (_e) {
114
+ return { entries: [], mixed: false, legacy: false, invalidDirs: [] };
115
+ }
116
+ const entries = [];
117
+ const invalidDirs = [];
118
+ // --- 새 레이아웃: <bp>/tasks/<NNN>/ ---
119
+ const tasksRootAbs = path.join(absDir, 'tasks');
120
+ let taskChildNames = [];
121
+ try {
122
+ taskChildNames = fs.readdirSync(tasksRootAbs);
123
+ }
124
+ catch (_e) {
125
+ // tasks/ 없음 — 레거시만 본다.
126
+ }
127
+ const dirItems = [];
128
+ for (const name of taskChildNames) {
129
+ let st;
130
+ try {
131
+ st = fs.statSync(path.join(tasksRootAbs, name));
132
+ }
133
+ catch (_e) {
134
+ continue;
135
+ }
136
+ if (!st.isDirectory())
137
+ continue;
138
+ const m = TASK_DIR_RE.exec(name);
139
+ if (!m) {
140
+ invalidDirs.push(name);
141
+ continue;
142
+ }
143
+ dirItems.push({ digits: m[1], n: Number(m[1]) });
144
+ }
145
+ dirItems.sort((a, b) => a.n - b.n);
146
+ // 게이트 메시지 안정성 — 발견 순이 아니라 이름 순으로 고정.
147
+ invalidDirs.sort((a, b) => a.localeCompare(b));
148
+ for (const item of dirItems) {
149
+ const dir = `${bp}/tasks/${item.digits}`;
150
+ const ids = expectedTaskDocIds(item.digits);
151
+ const tasksRel = `${dir}/${TASK_UNIT_BASENAMES[0]}`;
152
+ let executionKind = null;
153
+ try {
154
+ executionKind = taskExecutionKind(readDoc(path.join(repoRoot, tasksRel)).data);
155
+ }
156
+ catch (_error) {
157
+ // 파싱 실패는 validate loader의 S0가 경로와 함께 보고한다. listing은
158
+ // null을 운반해 consumer가 부재=commit으로 잘못 정규화하지 않게 한다.
159
+ }
160
+ const verificationRel = `${dir}/${TASK_UNIT_BASENAMES[1]}`;
161
+ entries.push(makeEntry({
162
+ dir,
163
+ number: item.n,
164
+ tasks: { rel: tasksRel, id: ids.tasks },
165
+ verification: { rel: verificationRel, id: ids.verification },
166
+ // verification node에는 review leaf 자체가 없다. verification.md를 review로
167
+ // alias하면 목록 소비자가 존재하지 않는 review 단계를 다시 만들어 낸다.
168
+ ...(executionKind === 'verification' ? {} : {
169
+ review: { rel: `${dir}/${TASK_UNIT_BASENAMES[2]}`, id: ids.review },
170
+ }),
171
+ executionKind,
172
+ }));
173
+ }
174
+ // 하드컷 뒤 구형 루트 파일은 묶음으로 세지 않고 잔존 목록으로만 보고한다.
175
+ // 해석은 migrate task-layout이 하고, validate는 S15로 거절한다.
176
+ const legacyFiles = names.filter((name) => isLegacyTasksBasename(name) || isNumberedTasksBasename(name)).sort();
177
+ return { entries, mixed: false, legacy: legacyFiles.length > 0, legacyFiles, invalidDirs };
178
+ }
179
+ module.exports = {
180
+ LEGACY_TASKS_BASENAME,
181
+ INITIAL_NUMBERED_TASKS_BASENAME,
182
+ NUMBERED_TASKS_RE,
183
+ TASK_DIR_RE,
184
+ TASK_UNIT_BASENAMES,
185
+ isNumberedTasksBasename,
186
+ isLegacyTasksBasename,
187
+ unitDocKind,
188
+ taskExecutionKind,
189
+ expectedTasksId,
190
+ expectedTaskDocIds,
191
+ listTasksDocs,
192
+ };