@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,420 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkExecuteGate = checkExecuteGate;
4
+ exports.checkCommitGate = checkCommitGate;
5
+ exports.defaultCoordinatorLedgerFor = defaultCoordinatorLedgerFor;
6
+ const fs = require('node:fs');
7
+ const { createHash } = require('node:crypto');
8
+ const canonicalJsonMod = require("./canonical-json");
9
+ const { sha256Canonical } = canonicalJsonMod;
10
+ const guards = require("./guards");
11
+ const { isRecord } = guards;
12
+ const scope = require("./scope");
13
+ const { makeAllowed, isRuntimeArtifact, readCoordinatorLedger } = scope;
14
+ const validateDocs = require("./validate-docs");
15
+ const { defaultStagedFiles, resolveTaskUnit, unitLeafRel, statusOf, isBlueprintReviewMode } = validateDocs;
16
+ const runtimeState = require("./runtime-state");
17
+ const { verifyLedgerPathFor, readApprovalSnapshot } = runtimeState;
18
+ const validateSections = require("./validate-sections");
19
+ const { VERIFY_SECTION_DEFS, parseSections, collectFindingFailures, EXECUTE_REVIEW_STATUS } = validateSections;
20
+ const schema = require("./schema");
21
+ const { executionKindOf } = schema;
22
+ const verification = require("./verification");
23
+ const { computeSourceDigest } = verification;
24
+ const approvalSnapshot = require("./approval-snapshot");
25
+ const { computeApprovalDigest, diffApprovalParts } = approvalSnapshot;
26
+ const validate_gates_shared_1 = require("./validate-gates-shared");
27
+ /**
28
+ * Git common dir의 verify 원장을 읽는다. evidenceId가 있으면 v2 경로를 쓰고,
29
+ * 없으면 legacy(rel-only) 경로를 연다 — 구 원장 miss와 v2 hit 경계를 같게 유지한다.
30
+ *
31
+ * @param {{ repoRoot?: string, verificationRel?: string, evidenceId?: string, deps?: GateDeps }} opts
32
+ * @returns {VerifyLedgerRecord | null} 파싱된 원장. 부재·파손은 null, 비-Git은 unavailable
33
+ */
34
+ function defaultReadVerifyLedger({ repoRoot, verificationRel, evidenceId, deps, }) {
35
+ const paths = verifyLedgerPathFor({
36
+ repoRoot: repoRoot,
37
+ verificationRel,
38
+ evidenceId,
39
+ // 경로 해석만 위임한다. ledger 파일 읽기는 아래 fsApi가 담당하므로
40
+ // GateDeps.fs(부분 InjectedFs)를 RuntimeDeps로 억지 대입하지 않는다.
41
+ deps: deps
42
+ ? { execFileSync: deps.execFileSync, platform: deps.platform }
43
+ : undefined,
44
+ });
45
+ if (paths.unavailable) {
46
+ return { unavailable: true, reason: paths.reason };
47
+ }
48
+ const fsApi = (deps && deps.fs) || fs;
49
+ if (!paths.ledgerFile || !fsApi.existsSync(paths.ledgerFile))
50
+ return null;
51
+ try {
52
+ const parsed = JSON.parse(fsApi.readFileSync(paths.ledgerFile, 'utf8'));
53
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
54
+ return null;
55
+ return parsed;
56
+ }
57
+ catch (_error) {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * execute/commit G13 — verification.md 하네스 메타와 common-dir 원장을 대조한다.
63
+ * v2는 evidence_id·scope·reused lineage까지 맞아야 통과하고, 손기록 변조를 거절한다.
64
+ *
65
+ * @param {DocLeaf | undefined | null} verificationDoc - verification.md 문서
66
+ * @param {(code: string, message: string, leaf: string) => void} addUnit - 실패 누적
67
+ * @param {GateContext} ctx - repoRoot·deps
68
+ * @returns {void}
69
+ */
70
+ function checkG13(verificationDoc, addUnit, ctx) {
71
+ if (!verificationDoc)
72
+ return;
73
+ const vbody = typeof verificationDoc.body === 'string' ? verificationDoc.body : '';
74
+ const vs = parseSections(vbody, VERIFY_SECTION_DEFS);
75
+ const missingV = ['command', 'evidence'].filter((k) => !vs[k]);
76
+ if (missingV.length) {
77
+ addUnit('G13', `verification.md missing body sections: ${missingV.join(', ')}`, 'verification');
78
+ }
79
+ const vBouncer = verificationDoc.data.bouncer;
80
+ const evidence = vBouncer && vBouncer.verification;
81
+ const scope = evidence && evidence.scope;
82
+ const validEvidence = evidence
83
+ && typeof evidence.command === 'string'
84
+ && evidence.command.trim()
85
+ && typeof evidence.ran_at === 'string'
86
+ && evidence.ran_at.trim()
87
+ && evidence.exit_code === 0
88
+ && typeof evidence.output_tail === 'string'
89
+ && typeof evidence.evidence_id === 'string'
90
+ && /^[a-f0-9]{64}$/.test(evidence.evidence_id)
91
+ && isRecord(evidence.identity)
92
+ && isRecord(scope)
93
+ && (scope.kind === 'task' || scope.kind === 'wave' || scope.kind === 'terminal')
94
+ && typeof scope.key === 'string'
95
+ && scope.key.trim()
96
+ && typeof evidence.reused === 'boolean'
97
+ && (evidence.reused === false
98
+ || (typeof evidence.reused_from === 'string' && /^[a-f0-9]{64}$/.test(evidence.reused_from)));
99
+ if (!validEvidence) {
100
+ addUnit('G13', 'verification.md missing successful harness verification metadata', 'verification');
101
+ return;
102
+ }
103
+ // evidence_id는 identity의 content-addressed 해시여야 한다. 원장과 숫자만
104
+ // 맞추고 identity를 손기록하면 다른 입력의 성공을 위조할 수 있다.
105
+ const expectedEvidenceId = sha256Canonical(evidence.identity);
106
+ if (evidence.evidence_id !== expectedEvidenceId) {
107
+ addUnit('G13', 'verification.md evidence_id does not match canonical identity hash', 'verification');
108
+ return;
109
+ }
110
+ if (!vs.command.includes(`\`${evidence.command}\``)
111
+ || !vs.evidence.includes('Exit code: 0')) {
112
+ addUnit('G13', 'verification.md body does not match harness verification metadata', 'verification');
113
+ }
114
+ // 프론트매터만 맞으면 에이전트 Write로 통과하던 구멍. 원장은 git common dir
115
+ // 아래 하네스 전용이라, 문서와 대조하지 않으면 `/bouncer-commit` 직접 호출도
116
+ // status: passed 손기록으로 열린다.
117
+ const deps = ctx && ctx.deps;
118
+ const reader = (deps && deps.readVerifyLedger) || defaultReadVerifyLedger;
119
+ const record = reader({
120
+ repoRoot: ctx && ctx.repoRoot,
121
+ verificationRel: verificationDoc.rel,
122
+ evidenceId: evidence.evidence_id,
123
+ deps,
124
+ });
125
+ if (record && record.unavailable) {
126
+ addUnit('G13', `verification.md verify ledger unavailable (${record.reason || 'Git common directory unavailable'})`, 'verification');
127
+ return;
128
+ }
129
+ if (!record) {
130
+ addUnit('G13', 'verification.md missing harness verify ledger record', 'verification');
131
+ return;
132
+ }
133
+ if (record.command !== evidence.command
134
+ || record.ran_at !== evidence.ran_at
135
+ || record.exit_code !== evidence.exit_code
136
+ || record.evidence_id !== evidence.evidence_id
137
+ || record.reused !== evidence.reused
138
+ || !(0, validate_gates_shared_1.sameCanonical)(record.identity, evidence.identity)
139
+ || !(0, validate_gates_shared_1.sameCanonical)(record.scope, evidence.scope)
140
+ || (evidence.reused
141
+ ? record.reused_from !== evidence.reused_from
142
+ : record.reused_from !== undefined && record.reused_from !== null)) {
143
+ addUnit('G13', 'verification.md harness metadata does not match verify ledger', 'verification');
144
+ return;
145
+ }
146
+ const outputSha = createHash('sha256').update(String(evidence.output_tail), 'utf8').digest('hex');
147
+ if (record.output_sha !== outputSha) {
148
+ addUnit('G13', 'verification.md output_tail does not match verify ledger output_sha', 'verification');
149
+ }
150
+ }
151
+ /**
152
+ * commit G23 — G13이 통과한 원장의 source_digest·identity.head를 현재
153
+ * checkout과 대조한다. 필드가 없는 구 원장과 repoRoot 비문자열은 건너뛰어
154
+ * 이 변경 전 증거가 execute 경로를 막지 않게 한다.
155
+ *
156
+ * @param {DocLeaf | undefined | null} verificationDoc - verification.md 문서
157
+ * @param {(code: string, message: string, leaf: string) => void} addUnit - 실패 누적
158
+ * @param {GateContext} ctx - repoRoot·deps
159
+ * @param {FailureEntry[]} failures - G13이 이미 있으면 대조하지 않는다
160
+ * @returns {void}
161
+ */
162
+ function checkG23(verificationDoc, addUnit, ctx, failures) {
163
+ if (!verificationDoc)
164
+ return;
165
+ if (failures.some((entry) => entry.code === 'G13'))
166
+ return;
167
+ const repoRoot = ctx && ctx.repoRoot;
168
+ if (typeof repoRoot !== 'string')
169
+ return;
170
+ const deps = ctx && ctx.deps;
171
+ const reader = (deps && deps.readVerifyLedger) || defaultReadVerifyLedger;
172
+ const vBouncer = verificationDoc.data.bouncer;
173
+ const evidence = vBouncer && vBouncer.verification;
174
+ const record = reader({
175
+ repoRoot,
176
+ verificationRel: verificationDoc.rel,
177
+ evidenceId: evidence && typeof evidence.evidence_id === 'string' ? evidence.evidence_id : undefined,
178
+ deps,
179
+ });
180
+ if (!record || record.unavailable)
181
+ return;
182
+ if (typeof record.source_digest !== 'string')
183
+ return;
184
+ const identity = isRecord(record.identity) ? record.identity : undefined;
185
+ const recordedHead = identity && typeof identity.head === 'string' ? identity.head : '';
186
+ try {
187
+ const headResult = (0, validate_gates_shared_1.gitExec)(repoRoot, ['rev-parse', 'HEAD'], deps && deps.exec);
188
+ if (headResult.status !== 0) {
189
+ addUnit('G23', `verification freshness check failed (${(0, validate_gates_shared_1.firstStderrLine)(headResult.stderr) || 'git rev-parse HEAD failed'})`, 'verification');
190
+ }
191
+ else if (headResult.stdout.trim() !== recordedHead) {
192
+ addUnit('G23', 'verification evidence is stale: HEAD moved after verify', 'verification');
193
+ }
194
+ }
195
+ catch (error) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ addUnit('G23', `verification freshness check failed (${message.split(/\r?\n/)[0] || 'HEAD check failed'})`, 'verification');
198
+ }
199
+ try {
200
+ const digestFn = (deps && typeof deps.sourceDigest === 'function')
201
+ ? deps.sourceDigest
202
+ : computeSourceDigest;
203
+ const currentDigest = digestFn(repoRoot);
204
+ if (currentDigest !== record.source_digest) {
205
+ addUnit('G23', 'verification evidence is stale: sources changed after verify', 'verification');
206
+ }
207
+ }
208
+ catch (error) {
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ addUnit('G23', `verification freshness check failed (${message.split(/\r?\n/)[0] || 'source digest failed'})`, 'verification');
211
+ }
212
+ }
213
+ function defaultReadApprovalSnapshot({ repoRoot, blueprintDir }) {
214
+ return readApprovalSnapshot({ repoRoot, blueprintDir });
215
+ }
216
+ function defaultCoordinatorLedgerFor({ repoRoot, blueprint }) {
217
+ const result = readCoordinatorLedger({ repoRoot, blueprint });
218
+ if (result && result.ok === true)
219
+ return result.ledger;
220
+ return null;
221
+ }
222
+ /**
223
+ * execute·commit G24 — 활성화 시점 승인 digest와 현재 digest를 대조한다.
224
+ * 승인 파일이 없거나 coordinator 원장이 있으면 건너뛴다. 이 변경 전 활성화와
225
+ * coordinator가 `reviseTaskScope`로 범위를 고치는 경로를 G24로 막지 않기 위함이다.
226
+ *
227
+ * @param {FailureEntry[]} failures - G24를 누적할 배열
228
+ * @param {GateContext} ctx - repoRoot·blueprintDir·deps
229
+ * @param {BlueprintRels} rels - 실패 file 경로 (blueprint 단위라 blueprintIndex)
230
+ * @returns {void}
231
+ */
232
+ function checkG24(failures, ctx, rels) {
233
+ const repoRoot = ctx && ctx.repoRoot;
234
+ if (typeof repoRoot !== 'string')
235
+ return;
236
+ const blueprintDir = ctx && ctx.blueprintDir;
237
+ if (typeof blueprintDir !== 'string')
238
+ return;
239
+ const deps = ctx && ctx.deps;
240
+ const ledgerFn = (deps && deps.coordinatorLedgerFor) || defaultCoordinatorLedgerFor;
241
+ const ledger = ledgerFn({ repoRoot, blueprint: blueprintDir });
242
+ if (ledger && typeof ledger === 'object')
243
+ return;
244
+ const reader = (deps && deps.readApprovalSnapshot) || defaultReadApprovalSnapshot;
245
+ const approval = reader({ repoRoot, blueprintDir });
246
+ if (approval == null)
247
+ return;
248
+ const file = rels.blueprintIndex;
249
+ if (approval.ok === false) {
250
+ failures.push({
251
+ code: 'G24',
252
+ message: `approval snapshot is unreadable (${approval.path})`,
253
+ file,
254
+ });
255
+ return;
256
+ }
257
+ const current = computeApprovalDigest({ repoRoot, blueprintDir });
258
+ if (current.digest === approval.snapshot.digest)
259
+ return;
260
+ const names = diffApprovalParts(approval.snapshot.parts, current.parts);
261
+ failures.push({
262
+ code: 'G24',
263
+ message: `approved scope changed after activation: ${names.join(', ')}`,
264
+ file,
265
+ });
266
+ }
267
+ /**
268
+ * execute gate 판정. G 코드·메시지·순서는 분리 전과 같다.
269
+ *
270
+ * @param {GateStageCtx} stage - add 클로저와 문서 묶음
271
+ * @returns {void}
272
+ */
273
+ function checkExecuteGate(stage) {
274
+ const { docs, rels, failures } = stage;
275
+ const ctx = (0, validate_gates_shared_1.toGateContext)(stage);
276
+ // docs.tasks(첫 문서 호환 필드)는 쓰지 않는다 — 포인터 대상 묶음만 판정.
277
+ // ctx.taskUnit이 없으면 단위 테스트용으로 평탄 docs에서 합성.
278
+ const taskUnit = (ctx && ctx.taskUnit) || resolveTaskUnit(docs, {});
279
+ const tasksDoc = taskUnit && taskUnit.tasks;
280
+ const verificationDoc = taskUnit && taskUnit.verification;
281
+ const reviewDoc = taskUnit && taskUnit.review;
282
+ const tasksData = (0, validate_gates_shared_1.asData)(tasksDoc);
283
+ const tasksBouncer = tasksData && tasksData.bouncer;
284
+ const isVerificationTask = executionKindOf(tasksBouncer) === 'verification';
285
+ const addUnit = (code, message, leaf) => failures.push({
286
+ code,
287
+ message,
288
+ file: unitLeafRel(taskUnit, leaf, rels[leaf]),
289
+ });
290
+ const expectedTaskStatus = isVerificationTask ? 'integrated' : 'verified';
291
+ if (statusOf(tasksDoc) !== expectedTaskStatus) {
292
+ addUnit('G6', `tasks.status != ${expectedTaskStatus}`, 'tasks');
293
+ }
294
+ if (statusOf(verificationDoc) !== 'passed') {
295
+ addUnit('G7', 'verification.status != passed', 'verification');
296
+ }
297
+ const reviewBouncer = reviewDoc
298
+ ? reviewDoc.data.bouncer
299
+ : undefined;
300
+ const review = reviewBouncer ? reviewBouncer.review : undefined;
301
+ const reviewOk = statusOf(reviewDoc) === 'accepted' || (review && review.required === false);
302
+ const blueprintReviewMode = isBlueprintReviewMode(docs.blueprintIndex);
303
+ // 모드에서는 루트 리뷰만 본다. 남은 task review.md를 읽으면 G8/G14가
304
+ // 마감과 다른 문서를 근거로 실패한다.
305
+ if (!blueprintReviewMode && !isVerificationTask && !reviewOk) {
306
+ addUnit('G8', 'review not accepted and review.required != false', 'review');
307
+ }
308
+ checkG13(verificationDoc, addUnit, ctx);
309
+ const reviewMetaBouncer = reviewDoc
310
+ ? reviewDoc.data.bouncer
311
+ : undefined;
312
+ const reviewMeta = reviewMetaBouncer
313
+ ? reviewMetaBouncer.review
314
+ : undefined;
315
+ const reviewSkipped = reviewMeta && reviewMeta.required === false;
316
+ // G14는 execute status(deferred 포함)를 넘겨 실질 판정(열린 finding·must_fix, 깨진 enum)만 남긴다.
317
+ // 형식 위반은 review record·accept-risk가 기록 시점에 거절하므로 gate는 막지 않는다.
318
+ // G8의 accepted/required 판정은 그대로 둔다.
319
+ if (!blueprintReviewMode && !isVerificationTask && reviewDoc && !reviewSkipped) {
320
+ for (const message of collectFindingFailures({
321
+ body: reviewDoc.body,
322
+ findings: reviewMeta && reviewMeta.findings,
323
+ rounds: reviewMeta && reviewMeta.rounds,
324
+ sectionLabel: 'review.md',
325
+ findingLabel: 'review',
326
+ allowedStatuses: EXECUTE_REVIEW_STATUS,
327
+ reviewStatus: statusOf(reviewDoc),
328
+ }).filter(validate_gates_shared_1.isBlockingReviewFailure)) {
329
+ addUnit('G14', message, 'review');
330
+ }
331
+ }
332
+ checkG24(failures, ctx, rels);
333
+ return;
334
+ }
335
+ /**
336
+ * commit gate 판정. G 코드·메시지·순서는 분리 전과 같다.
337
+ *
338
+ * @param {GateStageCtx} stage - add 클로저와 문서 묶음
339
+ * @returns {void}
340
+ */
341
+ function checkCommitGate(stage) {
342
+ const { docs, rels, failures } = stage;
343
+ const repoRoot = stage.repoRoot;
344
+ const blueprintDir = stage.blueprintDir;
345
+ const deps = stage.deps;
346
+ const ctx = (0, validate_gates_shared_1.toGateContext)(stage);
347
+ // G9는 폐기됨 — 번호만 비워 둠.
348
+ // G15 (explain comprehension / diff_sha)는 폐기됨 — 번호만 비워 둠.
349
+ const taskUnit = (ctx && ctx.taskUnit) || resolveTaskUnit(docs, {
350
+ repoRoot, blueprintDir,
351
+ });
352
+ const tasksDoc = taskUnit && taskUnit.tasks;
353
+ const verificationDoc = taskUnit && taskUnit.verification;
354
+ const reviewDoc = taskUnit && taskUnit.review;
355
+ const addUnit = (code, message, leaf) => failures.push({
356
+ code,
357
+ message,
358
+ file: unitLeafRel(taskUnit, leaf, rels[leaf]),
359
+ });
360
+ if (statusOf(tasksDoc) !== 'verified') {
361
+ addUnit('G6', 'tasks.status != verified', 'tasks');
362
+ }
363
+ if (statusOf(verificationDoc) !== 'passed') {
364
+ addUnit('G7', 'verification.status != passed', 'verification');
365
+ }
366
+ const commitReviewBouncer = reviewDoc
367
+ ? reviewDoc.data.bouncer
368
+ : undefined;
369
+ const review = commitReviewBouncer
370
+ ? commitReviewBouncer.review
371
+ : undefined;
372
+ const reviewOk = statusOf(reviewDoc) === 'accepted' || (review && review.required === false);
373
+ if (!isBlueprintReviewMode(docs.blueprintIndex) && !reviewOk) {
374
+ addUnit('G8', 'review not accepted and review.required != false', 'review');
375
+ }
376
+ checkG13(verificationDoc, addUnit, ctx);
377
+ checkG23(verificationDoc, addUnit, ctx, failures);
378
+ checkG24(failures, ctx, rels);
379
+ // G17은 이미 스테이징된 경로만 본다. working-tree 변경의 out-of-scope는
380
+ // bouncer commit이 따로 막으며, 빈 스테이징은 통과(빈 커밋 방지는 명령 몫).
381
+ const stagedFn = (deps && deps.stagedFiles) || defaultStagedFiles;
382
+ if (typeof repoRoot !== 'string') {
383
+ failures.push({
384
+ code: 'G17',
385
+ message: 'could not read staged files (missing-repo)',
386
+ file: unitLeafRel(taskUnit, 'tasks', rels.tasks),
387
+ });
388
+ return;
389
+ }
390
+ const staged = stagedFn({ repoRoot });
391
+ if (!staged || staged.ok !== true) {
392
+ // `'reason' in`은 객체가 아니면 TypeError. 예전 `staged && staged.reason`은
393
+ // primitive도 git-failed로 G17에 남겼다.
394
+ const stagedFail = staged;
395
+ const reason = stagedFail && stagedFail.reason ? stagedFail.reason : 'git-failed';
396
+ failures.push({
397
+ code: 'G17',
398
+ message: `could not read staged files (${reason})`,
399
+ file: unitLeafRel(taskUnit, 'tasks', rels.tasks),
400
+ });
401
+ return;
402
+ }
403
+ const taskData = (0, validate_gates_shared_1.asData)(tasksDoc);
404
+ const affectedPaths = taskData && taskData.bouncer
405
+ ? taskData.bouncer.affected_paths
406
+ : [];
407
+ const allowed = makeAllowed({ affectedPaths, blueprintDir });
408
+ const files = Array.isArray(staged.files) ? staged.files : [];
409
+ const violations = files
410
+ .filter((f) => !isRuntimeArtifact(f))
411
+ .filter((f) => !allowed(f));
412
+ if (violations.length) {
413
+ failures.push({
414
+ code: 'G17',
415
+ message: `staged path outside affected_paths: ${violations.join(', ')}`,
416
+ file: unitLeafRel(taskUnit, 'tasks', rels.tasks),
417
+ });
418
+ }
419
+ return;
420
+ }
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toGateContext = toGateContext;
4
+ exports.asData = asData;
5
+ exports.gitExec = gitExec;
6
+ exports.lastReviewHead = lastReviewHead;
7
+ exports.firstStderrLine = firstStderrLine;
8
+ exports.sameCanonical = sameCanonical;
9
+ exports.isBlockingReviewFailure = isBlockingReviewFailure;
10
+ const canonicalJsonMod = require("./canonical-json");
11
+ const { canonicalJson } = canonicalJsonMod;
12
+ const { spawnSync } = require('node:child_process');
13
+ /**
14
+ * 단계 함수가 G13·G21처럼 GateContext를 받는 헬퍼를 그대로 부르게 한다.
15
+ * stage 필드를 빼먹으면 repoRoot·deps가 비어 기본 원장 읽기가 건너뛰어질 수 있다.
16
+ *
17
+ * @param {GateStageCtx} stage - runCheckGate가 만든 단계 문맥
18
+ * @returns {GateContext} 기존 헬퍼가 기대하는 문맥
19
+ */
20
+ function toGateContext(stage) {
21
+ return {
22
+ repoRoot: stage.repoRoot,
23
+ blueprintDir: stage.blueprintDir,
24
+ deps: stage.deps,
25
+ taskUnit: stage.taskUnit,
26
+ parseErrors: stage.parseErrors,
27
+ warnings: stage.warnings,
28
+ partialClose: stage.partialClose,
29
+ };
30
+ }
31
+ /**
32
+ * 문서 YAML data를 레코드로 읽는다. data가 null이면 빈 객체로 바꾸지 않는다 —
33
+ * 예전 호출부가 `.bouncer` 접근에서 터지던 실패 형태를 유지해야 한다.
34
+ *
35
+ * @param {DocLeaf | undefined | null} doc - 로드된 문서 또는 부재
36
+ * @returns {Record<string, unknown> | undefined} data. 문서가 없으면 undefined
37
+ */
38
+ function asData(doc) {
39
+ if (!doc)
40
+ return undefined;
41
+ // 호출부가 `doc.data.bouncer`로 바로 들어가던 곳은 그대로 두기 위해
42
+ // 여기서 data를 빈 객체로 바꾸지 않는다. null data는 예전처럼 접근 시 터진다.
43
+ return doc.data;
44
+ }
45
+ /**
46
+ * 주입된 git exec가 없으면 repoRoot에 바인딩한 spawnSync를 쓴다.
47
+ * merge-base --is-ancestor는 status 1이 정상 실패라 execFileSync를 쓰면 안 된다.
48
+ *
49
+ * @param {string} repoRoot - git cwd
50
+ * @param {string[]} args - `git` 뒤 argv
51
+ * @param {GateDeps['exec']} [injected] - 테스트 seam
52
+ * @returns {{ status: number, stdout: string, stderr: string }} git 결과
53
+ */
54
+ function gitExec(repoRoot, args, injected) {
55
+ if (typeof injected === 'function')
56
+ return injected(args);
57
+ const r = spawnSync('git', args, {
58
+ cwd: repoRoot,
59
+ encoding: 'utf8',
60
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
61
+ });
62
+ return {
63
+ status: typeof r.status === 'number' ? r.status : 1,
64
+ stdout: r.stdout || '',
65
+ stderr: r.stderr || '',
66
+ };
67
+ }
68
+ /**
69
+ * 마지막 라운드의 `target.head`. 없거나 빈 값이면 라운드 계약이 없다.
70
+ *
71
+ * @param {unknown} rounds - `bouncer.review.rounds`
72
+ * @returns {string | null} head 또는 null
73
+ */
74
+ function lastReviewHead(rounds) {
75
+ if (!Array.isArray(rounds) || rounds.length === 0)
76
+ return null;
77
+ const last = rounds[rounds.length - 1];
78
+ if (!last || typeof last !== 'object')
79
+ return null;
80
+ const target = last.target;
81
+ if (!target || typeof target !== 'object')
82
+ return null;
83
+ const head = target.head;
84
+ return typeof head === 'string' && head.trim() !== '' ? head : null;
85
+ }
86
+ /**
87
+ * git stderr에서 첫 줄을 골라 G21 메시지에 붙인다. 여러 줄 전체를 넣으면
88
+ * gate 실패 문구가 한 줄 계약을 넘는다.
89
+ *
90
+ * @param {string} stderr - exec 결과 stderr
91
+ * @returns {string} 첫 줄(trim). 비어 있으면 빈 문자열
92
+ */
93
+ function firstStderrLine(stderr) {
94
+ const line = String(stderr).split(/\r?\n/)[0] || '';
95
+ return line.trim();
96
+ }
97
+ /**
98
+ * 객체를 키 정렬 JSON으로 비교한다. G13 identity/scope 대조가 삽입 순서에
99
+ * 흔들리지 않게 한다.
100
+ *
101
+ * @param {unknown} left - 문서 값
102
+ * @param {unknown} right - 원장 값
103
+ * @returns {boolean} 동치면 true
104
+ */
105
+ function sameCanonical(left, right) {
106
+ return canonicalJson(left) === canonicalJson(right);
107
+ }
108
+ /**
109
+ * `collectFindingFailures`가 돌려준 메시지 중 gate가 실패로 남길 실질 판정인지 가린다.
110
+ * 열린 finding·열린 must_fix는 리뷰가 승인될 수 없다는 뜻이고, findings 배열·status·actionability가
111
+ * 깨지면 위 두 판정을 내릴 수 없으므로 fail-closed로 같이 남긴다. 나머지(severity·note·fingerprint·
112
+ * rounds 순서 등) 형식 위반은 기록 시점(`review record`·`accept-risk`)이 이미 거절하므로 gate에서는 뺀다.
113
+ * id 자리는 `.+`다 — id 없는 finding은 `(no id)`(공백 포함)로 찍힌다.
114
+ *
115
+ * @param {string} message - `collectFindingFailures`의 메시지 한 건
116
+ * @returns {boolean} gate 실패로 남겨야 하면 true, 형식 메시지라 뺄 수 있으면 false
117
+ */
118
+ function isBlockingReviewFailure(message) {
119
+ return /^\S+ finding .+ open in accepted review$/.test(message)
120
+ || /^\S+ accepted with open must_fix .+$/.test(message)
121
+ || /^\S+ findings must be an array$/.test(message)
122
+ || /^\S+ finding .+ status invalid: .*$/.test(message)
123
+ || /^\S+ finding .+ actionability (invalid|missing)$/.test(message);
124
+ }
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+ // 게이트별 G 코드 층. 문서 로드(docs)·문서 하나 구조(S)·본문 파싱은 여기 두지
3
+ // 않는다. 승인 범위는 G5가 판정한다(G4는 결번). validate.ts를
4
+ // require하지 않는다(validate → gates → structural, 순환 금지).
5
+ // 단계 본체는 plan·run·finalize 모듈에 두고, 이 파일은 호출 형태와 단계 선택만 맡는다.
6
+ const validate_gates_plan_1 = require("./validate-gates-plan");
7
+ const validate_gates_run_1 = require("./validate-gates-run");
8
+ const validate_gates_finalize_1 = require("./validate-gates-finalize");
9
+ /**
10
+ * 객체 옵션과 위치 인자 두 호출 형태를 받아 runCheckGate로 넘긴다.
11
+ * 기존 소비자는 warnings 부재를 허용한다 — 빈 배열이면 키를 생략한다.
12
+ *
13
+ * @param {string | CheckGateOpts} gate - gate 이름 또는 옵션 객체
14
+ * @param {BlueprintDocs} [docs] - 로드된 문서
15
+ * @param {BlueprintRels} [rels] - 실패 file 경로
16
+ * @param {FailureEntry[]} [failures] - 위치 인자 호출의 누적 배열
17
+ * @param {GateContext} [ctx] - 위치 인자 호출의 문맥
18
+ * @returns {{ failures: FailureEntry[]; warnings?: FailureEntry[] } | void} 객체 호출이면 결과, 아니면 void
19
+ */
20
+ function checkGate(gate, docs, rels, failures, ctx) {
21
+ if (typeof gate === 'object' && gate !== null) {
22
+ const opts = gate;
23
+ const collected = [];
24
+ const warnings = [];
25
+ checkGate(opts.gate, opts.docs || {}, opts.rels, collected, {
26
+ repoRoot: opts.repoRoot,
27
+ blueprintDir: opts.blueprintDir,
28
+ deps: opts.deps,
29
+ taskUnit: opts.taskUnit,
30
+ parseErrors: opts.parseErrors,
31
+ partialClose: opts.partialClose,
32
+ warnings,
33
+ });
34
+ return warnings.length > 0
35
+ ? { failures: collected, warnings }
36
+ : { failures: collected };
37
+ }
38
+ return runCheckGate(gate, docs, rels, failures, ctx || {});
39
+ }
40
+ /**
41
+ * add 클로저를 만들고 gate 이름으로 단계 함수를 고른다. 알 수 없는 이름은 throw한다.
42
+ *
43
+ * @param {string} gate - plan | execute | finalize | commit | partial-close
44
+ * @param {BlueprintDocs} docs - 로드된 문서
45
+ * @param {BlueprintRels} rels - 실패 file 경로
46
+ * @param {FailureEntry[]} failures - 실패 누적 배열
47
+ * @param {GateContext} ctx - repoRoot·deps·taskUnit
48
+ * @returns {void}
49
+ */
50
+ function runCheckGate(gate, docs, rels, failures, ctx) {
51
+ const add = (code, message, fileKey) => failures.push({ code, message, file: rels[fileKey] });
52
+ const stage = {
53
+ add,
54
+ docs,
55
+ rels,
56
+ failures,
57
+ repoRoot: ctx && ctx.repoRoot,
58
+ blueprintDir: ctx && ctx.blueprintDir,
59
+ deps: ctx && ctx.deps,
60
+ taskUnit: ctx && ctx.taskUnit,
61
+ parseErrors: ctx && ctx.parseErrors,
62
+ partialClose: ctx && ctx.partialClose,
63
+ warnings: ctx && ctx.warnings,
64
+ };
65
+ if (gate === 'partial-close') {
66
+ (0, validate_gates_finalize_1.checkPartialCloseGate)(stage);
67
+ return;
68
+ }
69
+ if (gate === 'plan') {
70
+ (0, validate_gates_plan_1.checkPlanGate)(stage);
71
+ return;
72
+ }
73
+ if (gate === 'execute') {
74
+ (0, validate_gates_run_1.checkExecuteGate)(stage);
75
+ return;
76
+ }
77
+ if (gate === 'finalize') {
78
+ (0, validate_gates_finalize_1.checkFinalizeGate)(stage);
79
+ return;
80
+ }
81
+ if (gate === 'commit') {
82
+ (0, validate_gates_run_1.checkCommitGate)(stage);
83
+ return;
84
+ }
85
+ throw new Error(`unknown gate: ${gate}`);
86
+ }
87
+ module.exports = { checkGate, checkPartialCloseEvidence: validate_gates_finalize_1.checkPartialCloseEvidence, checkPlanDraft: validate_gates_plan_1.checkPlanDraft, SECURITY_PATH_TOKENS: validate_gates_plan_1.SECURITY_PATH_TOKENS };