@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,388 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readReviewEvidence = readReviewEvidence;
4
+ exports.readVerification = readVerification;
5
+ exports.readReview = readReview;
6
+ exports.resolvePrBase = resolvePrBase;
7
+ exports.readDigestDocument = readDigestDocument;
8
+ exports.digestPathExists = digestPathExists;
9
+ exports.readBlueprintIndex = readBlueprintIndex;
10
+ exports.readTaskDocument = readTaskDocument;
11
+ exports.collectTaskDocuments = collectTaskDocuments;
12
+ exports.readDigestRegistry = readDigestRegistry;
13
+ exports.readDigestConfig = readDigestConfig;
14
+ exports.collectDigestGit = collectDigestGit;
15
+ exports.collectDigestStart = collectDigestStart;
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const { spawnSync } = require('node:child_process');
19
+ const scope = require("./scope");
20
+ const { readCoordinatorLedger } = scope;
21
+ const tasksDocs = require("./tasks-docs");
22
+ const { listTasksDocs } = tasksDocs;
23
+ const currentMod = require("./current");
24
+ const { readCurrent } = currentMod;
25
+ const paths = require("./paths");
26
+ const { toPosix } = paths;
27
+ const frontmatter = require("./frontmatter");
28
+ const { readDoc } = frontmatter;
29
+ const configMod = require("./config");
30
+ const { readConfig } = configMod;
31
+ const registryMod = require("./epic-registry");
32
+ const { readEpicRegistry } = registryMod;
33
+ const taskCommits = require("./task-commits");
34
+ const { resolveTaskCommits } = taskCommits;
35
+ const comprehension = require("./comprehension");
36
+ const { computeDiffSha } = comprehension;
37
+ const gitOps = require("./git-ops");
38
+ const { resolveCheckoutBranch } = gitOps;
39
+ const schema = require("./schema");
40
+ const { executionKindOf } = schema;
41
+ function asRecord(value) {
42
+ return value && typeof value === 'object' && !Array.isArray(value)
43
+ ? value
44
+ : {};
45
+ }
46
+ /**
47
+ * review frontmatter 파싱은 실패할 때만 null로 접어 기존 읽기 전용 계약을 유지한다.
48
+ *
49
+ * @param {string} absPath - 읽을 review.md 절대 경로
50
+ * @param {string} relPath - 결과에 기록할 상대 경로
51
+ * @returns {{ path: string, rounds: number, target_digest: string | null } | null}
52
+ */
53
+ function readReviewEvidence(absPath, relPath) {
54
+ if (!fs.existsSync(absPath))
55
+ return null;
56
+ try {
57
+ const { data } = readDoc(absPath);
58
+ const review = asRecord(asRecord(asRecord(data).bouncer).review);
59
+ const rounds = Array.isArray(review.rounds) ? review.rounds : [];
60
+ const last = asRecord(rounds[rounds.length - 1]);
61
+ const perspective = asRecord(Array.isArray(last.perspectives) ? last.perspectives[0] : null);
62
+ const candidates = [last.target_digest, asRecord(last.target).digest, perspective.target_digest];
63
+ const digest = candidates.find((v) => typeof v === 'string' && v !== '');
64
+ return { path: relPath, rounds: rounds.length, target_digest: digest ?? null };
65
+ }
66
+ catch (_error) {
67
+ // YAML/frontmatter 파싱 실패는 리뷰 기록 없음과 같다 — digest가 throw하지 않는다.
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * verification frontmatter를 원문 로그 없이 요약한다.
73
+ *
74
+ * @param {string} absPath - 읽을 verification.md 절대 경로
75
+ * @returns {TaskDigest['verification']} 파일 부재·파싱 실패면 null
76
+ */
77
+ function readVerification(absPath) {
78
+ if (!fs.existsSync(absPath))
79
+ return null;
80
+ try {
81
+ const { data } = readDoc(absPath);
82
+ const bouncer = asRecord(asRecord(data).bouncer);
83
+ const meta = asRecord(bouncer.verification);
84
+ return {
85
+ status: typeof bouncer.status === 'string' ? bouncer.status : 'unknown',
86
+ command: typeof meta.command === 'string' ? meta.command : null,
87
+ exit_code: typeof meta.exit_code === 'number' ? meta.exit_code : null,
88
+ evidence_id: typeof meta.evidence_id === 'string' ? meta.evidence_id : null,
89
+ reused: typeof meta.reused === 'boolean' ? meta.reused : null,
90
+ };
91
+ }
92
+ catch (_error) {
93
+ // YAML/frontmatter 파싱 실패는 증적 없음과 같다 — digest가 문서 오류로 throw하지 않는다.
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * review frontmatter의 required·findings를 digest용으로 요약한다.
99
+ *
100
+ * @param {string | null} absPath - 읽을 review.md 절대 경로. 없으면 null
101
+ * @returns {TaskDigest['review']} 파일 부재·파싱 실패면 null
102
+ */
103
+ function readReview(absPath) {
104
+ if (!absPath || !fs.existsSync(absPath))
105
+ return null;
106
+ try {
107
+ const { data } = readDoc(absPath);
108
+ const review = asRecord(asRecord(asRecord(data).bouncer).review);
109
+ const rawFindings = Array.isArray(review.findings) ? review.findings : [];
110
+ const findings = [];
111
+ for (const raw of rawFindings) {
112
+ const row = asRecord(raw);
113
+ if (typeof row.id !== 'string' || !row.id)
114
+ continue;
115
+ const finding = {
116
+ id: row.id,
117
+ severity: typeof row.severity === 'string' ? row.severity : 'nit',
118
+ status: typeof row.status === 'string' ? row.status : 'resolved',
119
+ };
120
+ if (typeof row.note === 'string' && row.note)
121
+ finding.note = row.note;
122
+ if (typeof row.actionability === 'string' && row.actionability) {
123
+ finding.actionability = row.actionability;
124
+ }
125
+ findings.push(finding);
126
+ }
127
+ return { required: review.required === false ? false : true, findings };
128
+ }
129
+ catch (_error) {
130
+ // YAML/frontmatter 오류는 유효한 finding을 만들 근거가 없으므로 리뷰 부재로 접는다.
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * PR base를 config 설정 또는 origin/HEAD에서 읽는다.
136
+ *
137
+ * @param {string} repoRoot - config를 읽을 checkout
138
+ * @param {GitExec} run - 주입 가능한 git 실행 seam
139
+ * @returns {string | null} 설정된 base, 없으면 null
140
+ */
141
+ function resolvePrBase(repoRoot, run) {
142
+ const root = asRecord(readConfig(repoRoot));
143
+ const pr = asRecord(root.pr);
144
+ if (typeof pr.base === 'string' && pr.base.trim())
145
+ return pr.base.trim();
146
+ if (typeof root.base_branch === 'string' && root.base_branch.trim())
147
+ return root.base_branch.trim();
148
+ const result = run(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']);
149
+ if (result.status !== 0)
150
+ return null;
151
+ const ref = typeof result.stdout === 'string' ? result.stdout.trim() : '';
152
+ if (!ref.startsWith('origin/'))
153
+ return null;
154
+ return ref.slice('origin/'.length) || null;
155
+ }
156
+ /**
157
+ * 단일 frontmatter 문서를 읽는다. 호출부가 실패 위치에 맞는 fallback을 적용한다.
158
+ *
159
+ * @param {string} absPath - 읽을 문서 절대 경로
160
+ * @returns {ReturnType<typeof readDoc>} frontmatter 데이터와 본문
161
+ */
162
+ function readDigestDocument(absPath) {
163
+ return readDoc(absPath);
164
+ }
165
+ /**
166
+ * 수집 단계에서만 필요한 경로 존재 여부를 묻는다.
167
+ *
168
+ * @param {string} absPath - 확인할 절대 경로
169
+ * @returns {boolean} 경로가 존재하면 true
170
+ */
171
+ function digestPathExists(absPath) {
172
+ return fs.existsSync(absPath);
173
+ }
174
+ /**
175
+ * blueprint index를 읽는다. 읽기·frontmatter 실패는 이전의 기본 메타데이터 경로를 유지한다.
176
+ *
177
+ * @param {string} absPath - blueprint index 절대 경로
178
+ * @returns {ReturnType<typeof readDoc> | null} 읽은 문서, 없거나 파싱 실패면 null
179
+ */
180
+ function readBlueprintIndex(absPath) {
181
+ if (!fs.existsSync(absPath))
182
+ return null;
183
+ try {
184
+ return readDoc(absPath);
185
+ }
186
+ catch (_error) {
187
+ // blueprint index의 읽기·YAML 실패는 기존처럼 기본값으로 진행한다.
188
+ return null;
189
+ }
190
+ }
191
+ /**
192
+ * task 문서를 읽어 실패한 항목만 생략한다.
193
+ *
194
+ * @param {string} absPath - tasks.md 절대 경로
195
+ * @returns {ReturnType<typeof readDoc> | null} 읽은 문서, 없거나 파싱 실패면 null
196
+ */
197
+ function readTaskDocument(absPath) {
198
+ if (!fs.existsSync(absPath))
199
+ return null;
200
+ try {
201
+ return readDoc(absPath);
202
+ }
203
+ catch (_error) {
204
+ // 손상 task 한 건만 생략해 나머지 digest를 보존한다.
205
+ return null;
206
+ }
207
+ }
208
+ /**
209
+ * listing의 tasks/verification/review 문서를 한 번씩 읽어 model용 snapshot으로 모은다.
210
+ *
211
+ * @param {object} opts - checkout와 document listing
212
+ * @param {string} opts.checkoutRoot - 문서를 읽을 checkout
213
+ * @param {Listing} opts.listing - task 문서 경로 목록
214
+ * @returns {Array<object>} 파싱 가능한 task와 각 보조 문서의 읽힌 값
215
+ */
216
+ function collectTaskDocuments({ checkoutRoot, listing }) {
217
+ const snapshots = [];
218
+ for (const entry of listing.entries) {
219
+ const taskDoc = readTaskDocument(path.join(checkoutRoot, entry.tasks.rel));
220
+ if (!taskDoc)
221
+ continue;
222
+ const data = taskDoc.data;
223
+ const bouncer = asRecord(asRecord(data).bouncer);
224
+ const kind = executionKindOf(bouncer) || entry.executionKind || 'commit';
225
+ snapshots.push({
226
+ number: entry.number,
227
+ executionKind: kind,
228
+ data,
229
+ body: typeof taskDoc.body === 'string' ? taskDoc.body : '',
230
+ verification: readVerification(path.join(checkoutRoot, entry.verification.rel)),
231
+ review: kind === 'verification'
232
+ ? null
233
+ : readReview(entry.review ? path.join(checkoutRoot, entry.review.rel) : null),
234
+ });
235
+ }
236
+ return snapshots;
237
+ }
238
+ /**
239
+ * task trailer 매칭에 필요한 registry를 읽는다.
240
+ *
241
+ * @param {string} checkoutRoot - 읽을 checkout
242
+ * @returns {ReturnType<typeof readEpicRegistry>} registry 결과
243
+ */
244
+ function readDigestRegistry(checkoutRoot) {
245
+ return readEpicRegistry(checkoutRoot);
246
+ }
247
+ /**
248
+ * PR 초안 생성 직전에 checkout config를 읽는다.
249
+ *
250
+ * @param {string} checkoutRoot - 읽을 checkout
251
+ * @returns {unknown} 현재 config 값
252
+ */
253
+ function readDigestConfig(checkoutRoot) {
254
+ return readConfig(checkoutRoot);
255
+ }
256
+ /**
257
+ * digest에 필요한 trailer, diff, commit 로그와 branch를 같은 Git seam에서 수집한다.
258
+ *
259
+ * @param {object} opts - Git 수집 입력
260
+ * @param {string} opts.checkoutRoot - 대상 integration checkout
261
+ * @param {string} opts.base - 범위 시작 ref
262
+ * @param {string} opts.head - 범위 끝 ref
263
+ * @param {GitExec} opts.run - 모든 Git 명령의 seam
264
+ * @param {string[]} opts.stableIds - trailer를 매칭할 task id
265
+ * @param {Extract<ReturnType<typeof readEpicRegistry>, { status: 'ok' }>['registry'] | null} opts.registry
266
+ * 읽힌 registry 또는 null
267
+ * @param {string | null} opts.integrationBranch - 원장 branch 우선값
268
+ * @returns {object} Git stdout와 검증된 trailer·diff fingerprint
269
+ */
270
+ function collectDigestGit({ checkoutRoot, base, head, run, stableIds, registry, integrationBranch, }) {
271
+ const trailerMap = resolveTaskCommits({ repoRoot: checkoutRoot, base, head, stableIds, exec: run, registry });
272
+ const nameOnly = run(['diff', '--name-only', `${base}..${head}`]);
273
+ const unified = run(['diff', '-U0', `${base}..${head}`]);
274
+ const subjectLog = run(['log', '--format=%h%x00%s', `${base}..${head}`]);
275
+ const numstat = run(['diff', '--numstat', '--no-renames', '-z', `${base}..${head}`]);
276
+ const diff = computeDiffSha({
277
+ repoRoot: checkoutRoot,
278
+ base,
279
+ exec: (args) => {
280
+ const result = run(args);
281
+ return { status: result.status, stdout: result.stdout, stderr: '' };
282
+ },
283
+ });
284
+ return {
285
+ trailerMap,
286
+ nameOnly,
287
+ unified,
288
+ subjectLog,
289
+ numstat,
290
+ diff,
291
+ branch: integrationBranch || resolveCheckoutBranch(checkoutRoot),
292
+ };
293
+ }
294
+ /**
295
+ * checkout에서 git을 읽고 외부 환경 변수로 대화형 입력을 막는다.
296
+ *
297
+ * @param {string} cwd - git 실행 checkout
298
+ * @param {string[]} args - git 뒤 argv
299
+ * @returns {{ status: number, stdout: string }} 호출 seam과 같은 형태의 결과
300
+ */
301
+ function defaultExec(cwd, args) {
302
+ const result = spawnSync('git', args, {
303
+ cwd, encoding: 'utf8', env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
304
+ });
305
+ return {
306
+ status: typeof result.status === 'number' ? result.status : 1,
307
+ stdout: result.stdout || '',
308
+ };
309
+ }
310
+ /**
311
+ * Digest 진입 단계에서 실패 우선순위가 있는 원장·task 목록·base·HEAD를 읽는다.
312
+ * task 문서 부재를 base 오류보다 먼저 판정해 기존 거절 순서를 보존한다.
313
+ *
314
+ * @param {object} opts - 수집에 필요한 경로와 git 실행 seam
315
+ * @param {string} opts.repoRoot - 호출 checkout 및 pointer 기준 경로
316
+ * @param {string} opts.blueprintDir - blueprint 상대 경로
317
+ * @param {GitExec} opts.run - 호출자가 설정한 git 실행 함수
318
+ * @returns {DigestCollection | DigestFail} 수집 snapshot 또는 첫 우선순위 실패
319
+ */
320
+ function collectDigestStart({ repoRoot, blueprintDir, exec, }) {
321
+ const bp = toPosix(blueprintDir);
322
+ const blueprintAbs = path.join(repoRoot, bp);
323
+ if (!fs.existsSync(blueprintAbs))
324
+ return { ok: false, reason: 'blueprint-not-found' };
325
+ const ledgerRead = readCoordinatorLedger({ repoRoot, blueprint: bp });
326
+ if (!ledgerRead.ok && ledgerRead.reason === 'unreadable-ledger') {
327
+ return {
328
+ ok: false,
329
+ reason: 'coordinator-ledger',
330
+ code: 'UNREADABLE_LEDGER',
331
+ ledgerFile: ledgerRead.ledgerFile || null,
332
+ integrationPath: ledgerRead.integrationPath || null,
333
+ };
334
+ }
335
+ const checkoutRoot = ledgerRead.ok && ledgerRead.integrationPath
336
+ && fs.existsSync(ledgerRead.integrationPath)
337
+ ? ledgerRead.integrationPath
338
+ : repoRoot;
339
+ const run = typeof exec === 'function'
340
+ ? exec
341
+ : (args) => defaultExec(checkoutRoot, args);
342
+ const listing = listTasksDocs({ repoRoot: checkoutRoot, blueprintDir: bp });
343
+ if (!listing.entries.some((entry) => fs.existsSync(path.join(checkoutRoot, entry.tasks.rel)))) {
344
+ return { ok: false, reason: 'task-documents-missing' };
345
+ }
346
+ const ledger = ledgerRead.ok ? ledgerRead.ledger : null;
347
+ const ledgerBase = ledger && typeof ledger.base === 'string'
348
+ ? ledger.base
349
+ : null;
350
+ const base = resolveDigestBase({ repoRoot, blueprintDir: bp, ledgerBase });
351
+ if (!base)
352
+ return { ok: false, reason: 'no-base' };
353
+ const headResult = run(['rev-parse', 'HEAD']);
354
+ if (headResult.status !== 0 || !headResult.stdout.trim()) {
355
+ return { ok: false, reason: 'no-base' };
356
+ }
357
+ const head = headResult.stdout.trim();
358
+ const verified = run(['rev-parse', '--verify', `${base}^{commit}`]);
359
+ if (verified.status !== 0)
360
+ return { ok: false, reason: 'no-base' };
361
+ return { ledgerRead, checkoutRoot, listing, ledger, base, head, run };
362
+ }
363
+ /**
364
+ * drive 원장 base 또는 같은 blueprint pointer의 base를 선택한다.
365
+ * pointer 오류는 기존 계약대로 base 부재로 접고 pointer 상태를 수정하지 않는다.
366
+ *
367
+ * @param {object} opts - 저장소와 원장 base
368
+ * @param {string} opts.repoRoot - pointer를 읽을 저장소
369
+ * @param {string} opts.blueprintDir - 요청된 blueprint 상대 경로
370
+ * @param {string | null} opts.ledgerBase - 원장 base. 없으면 pointer를 확인
371
+ * @returns {string | null} 유효한 base 또는 null
372
+ */
373
+ function resolveDigestBase({ repoRoot, blueprintDir, ledgerBase, }) {
374
+ if (typeof ledgerBase === 'string' && ledgerBase.trim())
375
+ return ledgerBase.trim();
376
+ try {
377
+ const pointer = readCurrent({ repoRoot });
378
+ if (pointer && typeof pointer.blueprint === 'string'
379
+ && toPosix(pointer.blueprint) === toPosix(blueprintDir)
380
+ && typeof pointer.base === 'string' && pointer.base.trim()) {
381
+ return pointer.base.trim();
382
+ }
383
+ }
384
+ catch (_error) {
385
+ // CURRENT_AMBIGUOUS 등은 base 부재와 같다 — digest는 pointer를 고치지 않는다.
386
+ }
387
+ return null;
388
+ }