@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,383 @@
1
+ 'use strict';
2
+ const verification_contracts_1 = require("./verification-contracts");
3
+ const execution = require("./verification-execution");
4
+ const { OUTPUT_TAIL_LINES, PASSING_OUTPUT_TAIL_LINES, MAX_VERIFY_OUTPUT_BYTES, executeVerify } = execution;
5
+ const identityModule = require("./verification-identity");
6
+ const { assertVerificationScope, resolveDefaultTaskScope, computeEvidenceIdentity, computeSourceDigest, } = identityModule;
7
+ const evidence = require("./verification-evidence");
8
+ const { recordVerificationResult, findReusableEvidence } = evidence;
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const frontmatter = require("./frontmatter");
12
+ const { readDoc } = frontmatter;
13
+ const render = require("./render");
14
+ const { renderDoc } = render;
15
+ const layout = require("./layout");
16
+ const { isCanonicalBlueprintDir } = layout;
17
+ const time = require("./time");
18
+ const { nowIsoKst } = time;
19
+ const tasksDocs = require("./tasks-docs");
20
+ const { listTasksDocs } = tasksDocs;
21
+ const paths = require("./paths");
22
+ const { toPosix } = paths;
23
+ const config = require("./config");
24
+ const { readConfigResult, readVerifyPolicy, } = config;
25
+ const commandArgv = require("./command-argv");
26
+ const { parseVerifyArgv, isValidVerifyCommand } = commandArgv;
27
+ const schema = require("./schema");
28
+ const { executionKindOf } = schema;
29
+ const seedWorktree = require("./seed-worktree");
30
+ const TASK_DIR_RE = /(?:^|\/)tasks\/(\d{3})(?:\/|$)/;
31
+ /**
32
+ * verify·verification.md 대상 task 문서 목록을 고른다. 명시 `taskId`가 있으면
33
+ * effective task보다 우선하고, 세 자리가 아니거나 목록에 없으면
34
+ * `VERIFY_IDENTITY_INVALID`로 거절한다(형식 오류를 조용히 무시하지 않음).
35
+ *
36
+ * @param {string} repoRoot - 저장소 루트 절대 경로
37
+ * @param {string} blueprintDir - blueprint 상대 경로
38
+ * @param {string} [taskId] - 세 자리 task id. 없으면 lease/pointer effective로 해석
39
+ * @returns {Array<{rel: string, verification?: {rel: string}}>} 대상 문서 엔트리
40
+ */
41
+ function selectVerificationEntries(repoRoot, blueprintDir, { taskId, effectiveTask }) {
42
+ const listing = listTasksDocs({ repoRoot, blueprintDir });
43
+ if (listing.mixed)
44
+ return [];
45
+ // 명시 taskId는 pointer/lease effective보다 우선한다. wave·terminal integrate가
46
+ // 포인터를 옮기지 않고도 그 task의 verify·verification.md만 고르게 한다.
47
+ if (typeof taskId === 'string') {
48
+ // Interface: 세 자리만 허용. 형식이 틀리면 effective로 폴백하지 않는다 —
49
+ // 잘못된 id를 조용히 무시하면 다른 task verify가 돌아 identity를 속인다(RD-002).
50
+ if (!/^\d{3}$/.test(taskId)) {
51
+ throw (0, verification_contracts_1.verificationError)('VERIFY_IDENTITY_INVALID', `taskId ${taskId} is not a three-digit task id`);
52
+ }
53
+ const match = listing.entries.find((entry) => {
54
+ const digits = TASK_DIR_RE.exec(toPosix(entry.rel));
55
+ return digits && digits[1] === taskId;
56
+ });
57
+ // 목록에 없으면 identity 오류 — config.verify로 폴백하면 잘못된 명령을 숨긴다.
58
+ if (!match) {
59
+ throw (0, verification_contracts_1.verificationError)('VERIFY_IDENTITY_INVALID', `taskId ${taskId} is not in the blueprint task listing`);
60
+ }
61
+ return [match];
62
+ }
63
+ // effective task(lease 우선)가 이 blueprint를 가리키고 문서가 살아 있으면
64
+ // 그 문서만. worker에서 lease를 못 확인하면 빈 목록 — pointer로 폴백하지
65
+ // 않아 다른 task 선언을 끌어오지 않는다. 문서가 사라진 경우에만 미지정
66
+ // (전체 walk)으로 폴백한다.
67
+ const effective = effectiveTask;
68
+ if (effective && effective.source === null)
69
+ return [];
70
+ const bp = toPosix(blueprintDir);
71
+ if (effective && typeof effective.path === 'string' && toPosix(effective.blueprint) === bp) {
72
+ const match = listing.entries.find((e) => e.rel === toPosix(effective.path));
73
+ if (match)
74
+ return [match];
75
+ }
76
+ return listing.entries;
77
+ }
78
+ /**
79
+ * blueprint가 `verification_policy: scoped_terminal`을 선언했는지 읽는다.
80
+ * index.md가 없으면(ENOENT) 정책 이전 계획과 같은 legacy로 본다 — 문서 없는
81
+ * fixture·옛 저장소에서 기존 config.verify 경로가 바뀌지 않게 하기 위함이다.
82
+ * 그 밖의 읽기·파싱 오류는 정책을 알 수 없다는 뜻이므로 삼키지 않고 던진다.
83
+ *
84
+ * @param {string} repoRoot - 저장소 루트 절대 경로
85
+ * @param {string} blueprintDir - blueprint 상대 경로
86
+ * @returns {boolean} scoped_terminal이면 true, 부재·legacy·허용 밖 값이면 false
87
+ */
88
+ function isScopedTerminal(repoRoot, blueprintDir) {
89
+ try {
90
+ const { data } = readDoc(path.join(repoRoot, blueprintDir, 'index.md'));
91
+ const bouncer = data ? data.bouncer : undefined;
92
+ return schema.verificationPolicyOf(bouncer) === 'scoped_terminal';
93
+ }
94
+ catch (error) {
95
+ if ((0, verification_contracts_1.errorCode)(error) === 'ENOENT')
96
+ return false;
97
+ throw error;
98
+ }
99
+ }
100
+ /**
101
+ * 문서가 사라졌을 때만 구현 task로 취급하고 다른 읽기 실패는 전파한다.
102
+ * @param {string} repoRoot - 저장소 루트
103
+ * @param {string} rel - task 문서 상대 경로
104
+ * @returns {boolean} verification node이면 true
105
+ */
106
+ function isVerificationKindDoc(repoRoot, rel) {
107
+ try {
108
+ const { data } = readDoc(path.join(repoRoot, rel));
109
+ const bouncer = data ? data.bouncer : undefined;
110
+ return schema.executionKindOf(bouncer) === 'verification';
111
+ }
112
+ catch (error) {
113
+ if ((0, verification_contracts_1.errorCode)(error) === 'ENOENT')
114
+ return false;
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * 활성 task 선언 또는 `config.verify`에서 검증 명령을 고른다. task 필드는
120
+ * 저장소 정책 allowlist로 검사한다. 파손된 config는 기본 목록으로 넘어가지
121
+ * 않고 `VERIFY_CONFIG_INVALID`다. 셸 연산자·미종료 인용은 그보다 먼저
122
+ * `VERIFY_COMMAND_INVALID`로 거절한다.
123
+ *
124
+ * @param {string} repoRoot - 저장소 루트 절대 경로
125
+ * @param {string} repoRoot - 저장소 루트 절대 경로
126
+ * @param {Array<{rel: string}>} entries - 이미 선택된 task 문서 목록
127
+ * @returns {string} 실행할 단일 argv 문자열
128
+ */
129
+ function readVerifyCommandFor(repoRoot, entries, blueprintDir) {
130
+ const configPath = path.join(repoRoot, '.bouncer', 'config.json');
131
+ // blueprint 선언이 있으면 우선합니다. task 문서가 없거나 필드가 없으면
132
+ // 기존 config.verify 경로를 유지합니다. 있지만 유효하지 않은 필드는
133
+ // 조용히 넘어가면 안 됩니다 — plan-time S12 누락을 숨깁니다.
134
+ const scoped = Boolean(blueprintDir) && isScopedTerminal(repoRoot, blueprintDir);
135
+ let selectedEntries = entries;
136
+ if (blueprintDir) {
137
+ // scoped_terminal에서 task를 특정하지 못한 listing 폴백(finalize remainder 등)은
138
+ // earliest 구현 task의 좁은 명령이 아니라 종단 verification의 전체 명령을 골라야
139
+ // 한다. sort는 안정 정렬이라 같은 종류끼리는 기존 번호 순서를 유지한다.
140
+ if (scoped && selectedEntries.length > 1) {
141
+ const rank = (entry) => (isVerificationKindDoc(repoRoot, entry.rel) ? 0 : 1);
142
+ selectedEntries = [...selectedEntries].sort((a, b) => rank(a) - rank(b));
143
+ }
144
+ for (const entry of selectedEntries) {
145
+ try {
146
+ const { data } = readDoc(path.join(repoRoot, entry.rel));
147
+ // `data && data.bouncer && data.bouncer.verify`와 같다. bouncer가 null이면
148
+ // declared가 null로 남아 VERIFY_COMMAND_INVALID로 간다 — undefined로
149
+ // 접으면 config.verify로 폴백되어 S12 누락을 숨긴다.
150
+ const bouncer = data ? data.bouncer : data;
151
+ const declared = bouncer ? bouncer.verify : bouncer;
152
+ if (declared !== undefined) {
153
+ // 셸 연산자·미종료 인용은 config가 깨져 있어도 명령 오류다.
154
+ // 정책을 먼저 보면 VERIFY_CONFIG_INVALID가 그 거절을 가린다.
155
+ if (!parseVerifyArgv(declared)) {
156
+ throw (0, verification_contracts_1.verificationError)('VERIFY_COMMAND_INVALID', 'verify command must be a single executable command');
157
+ }
158
+ const policy = readVerifyPolicy(repoRoot);
159
+ if (policy.ok === false) {
160
+ throw (0, verification_contracts_1.verificationError)('VERIFY_CONFIG_INVALID', `verification config is invalid: ${configPath}`);
161
+ }
162
+ if (!isValidVerifyCommand(declared, policy.allowlist)) {
163
+ throw (0, verification_contracts_1.verificationError)('VERIFY_COMMAND_INVALID', 'verify command must be a single executable command');
164
+ }
165
+ // isValidVerifyCommand가 통과한 값만 문자열이다. 여기서 다시 접지 않는다.
166
+ return declared;
167
+ }
168
+ }
169
+ catch (error) {
170
+ if ((0, verification_contracts_1.errorCode)(error) === 'VERIFY_COMMAND_INVALID')
171
+ throw error;
172
+ if ((0, verification_contracts_1.errorCode)(error) === 'VERIFY_CONFIG_INVALID')
173
+ throw error;
174
+ if ((0, verification_contracts_1.errorCode)(error) === 'VERIFY_IDENTITY_INVALID')
175
+ throw error;
176
+ if ((0, verification_contracts_1.errorCode)(error) !== 'ENOENT')
177
+ throw error;
178
+ }
179
+ }
180
+ }
181
+ // scoped_terminal은 명령 누락을 config.verify(전체 명령)로 메우지 않는다. 구현
182
+ // task가 전체 suite를 몰래 돌리면 책임 분리가 무너지기 때문이다. 문서가 사라졌거나
183
+ // 선언이 없는 경우 모두 여기서 끊는다.
184
+ if (scoped) {
185
+ throw (0, verification_contracts_1.verificationError)('VERIFY_COMMAND_INVALID', 'scoped_terminal requires a task-owned bouncer.verify; config.verify fallback is not allowed');
186
+ }
187
+ // 파일 없음과 깨진 JSON을 한 오류로 합치면 VERIFY_CONFIG_MISSING이
188
+ // 권한·구문 문제를 가린다. 메시지 문자열은 호출자가 경로를 그대로 보게 유지.
189
+ const parsed = readConfigResult(repoRoot);
190
+ // strict가 꺼진 기본 tsc는 `!parsed.ok`로 유니온을 좁히지 못한다.
191
+ // missing/invalid 분기를 유지하려면 리터럴 false와 비교한다.
192
+ if (parsed.ok === false) {
193
+ if (parsed.reason === 'missing') {
194
+ throw (0, verification_contracts_1.verificationError)('VERIFY_CONFIG_MISSING', `verification config missing: ${configPath}`);
195
+ }
196
+ throw (0, verification_contracts_1.verificationError)('VERIFY_CONFIG_INVALID', `verification config is invalid: ${configPath}`);
197
+ }
198
+ const config = parsed.value;
199
+ // JSON.parse 결과는 unknown이다. 객체로 좁히면 null config의 TypeError가
200
+ // VERIFY_CONFIG_INVALID로 바뀌므로, 예전처럼 .verify에 바로 접근한다.
201
+ const verify = config.verify;
202
+ if (typeof verify !== 'string' || verify.trim() === '') {
203
+ throw (0, verification_contracts_1.verificationError)('VERIFY_CONFIG_INVALID', 'config.verify must be a non-empty string');
204
+ }
205
+ return verify;
206
+ }
207
+ /**
208
+ * verify가 통과한 뒤 lease·pointer가 지목한 commit task만 `ready` → `verified`로 올린다.
209
+ * listing fallback이거나 조건이 아니면 쓰지 않는다(오류가 아님). `tasks.md` 읽기·쓰기
210
+ * 예외는 증적 기록 뒤에 그대로 던져, execute 게이트의 기존 catch가 G13으로 보고한다.
211
+ *
212
+ * @param {string} repoRoot - 저장소 루트 절대 경로
213
+ * @param {string} blueprintDir - 이번 실행 blueprint 상대 경로
214
+ * @param {string} entryRel - 이번 실행이 고른 tasks.md 상대 경로
215
+ * @param {EffectiveTask | null} effectiveTask - 호출자가 선택한 현재 task
216
+ * @returns {void} 조건을 만족하면 상태를 쓰고, 아니면 파일을 바꾸지 않는다
217
+ */
218
+ function maybeMarkCommitTaskVerified({ repoRoot, blueprintDir, entryRel, effectiveTask, }) {
219
+ // listing fallback을 전환 대상으로 쓰지 않는다. 묶음이 하나여도 lease·pointer가
220
+ // 없으면 이번 실행이 “정해진 task”가 아니다.
221
+ const effective = effectiveTask;
222
+ if (!effective || effective.source === null || typeof effective.path !== 'string')
223
+ return;
224
+ if (toPosix(effective.blueprint) !== toPosix(blueprintDir))
225
+ return;
226
+ if (toPosix(effective.path) !== toPosix(entryRel))
227
+ return;
228
+ const file = path.join(repoRoot, entryRel);
229
+ const document = readDoc(file);
230
+ const data = document.data;
231
+ const bouncer = (data.bouncer || {});
232
+ data.bouncer = bouncer;
233
+ // 필드 부재는 commit. verification kind와 ready가 아닌 상태(이미 verified 포함)는 둔다.
234
+ if (executionKindOf(bouncer) !== 'commit')
235
+ return;
236
+ if (bouncer.status !== 'ready')
237
+ return;
238
+ bouncer.status = 'verified';
239
+ fs.writeFileSync(file, renderDoc(data, document.body));
240
+ }
241
+ /**
242
+ * 선택된 task의 증적 경로를 우선하고 레거시 blueprint 문서로 폴백한다.
243
+ * @param {string} repoRoot - 기존 호출 형태를 보존하는 저장소 루트
244
+ * @param {string} blueprintDir - blueprint 상대 경로
245
+ * @param {Array} entries - 이미 선택된 task 목록
246
+ * @returns {string} 기록할 증적 문서 상대 경로
247
+ */
248
+ function resolveVerificationRel(repoRoot, blueprintDir, entries) {
249
+ // 호출자가 이미 고른 목록을 따르며, 목록이 비면 레거시 루트 경로를 쓴다.
250
+ if (entries[0] && entries[0].verification && entries[0].verification.rel) {
251
+ return entries[0].verification.rel;
252
+ }
253
+ return `${toPosix(blueprintDir)}/verification.md`;
254
+ }
255
+ /**
256
+ * 활성 task의 검증 명령을 실행하거나, 같은 identity·scope의 성공 원장이 있으면
257
+ * process spawn 없이 재사용 증적을 기록한다. identity 계산 실패는 실행으로
258
+ * 우회하지 않는다. `markTaskVerified`가 true이고 결과가 ok이면, 증적 기록 뒤에
259
+ * lease·pointer commit task의 `ready` → `verified` 전환을 시도한다.
260
+ *
261
+ * @param {object} opts - 실행 옵션
262
+ * @param {string} opts.repoRoot - 저장소 루트 절대 경로
263
+ * @param {string} opts.blueprintDir - blueprint 상대 경로
264
+ * @param {VerificationScope} [opts.scope] - 생략 시 포인터/첫 task의 stable Task ID
265
+ * @param {string} [opts.taskId] - 세 자리. 있으면 그 task의 verify·verification.md
266
+ * @param {VerifyExec} [opts.exec] - 주입 실행기(테스트용)
267
+ * @param {() => Date} [opts.now] - 시각 주입
268
+ * @param {VerificationDeps} [opts.deps] - git/readFile/platform 주입
269
+ * @param {boolean} [opts.markTaskVerified] - true면 통과 후 commit task 상태 전환. 기본 false
270
+ * @returns {{
271
+ * ok: boolean, command: string, exitCode: number, evidenceId: string,
272
+ * reused: boolean, reusedFrom?: string
273
+ * }}
274
+ */
275
+ function runVerification({ repoRoot, blueprintDir, scope: scopeInput, taskId, effectiveTask, exec, now = () => new Date(), deps, markTaskVerified = false, }) {
276
+ // 1. blueprint·명령·문서 존재 — 기존 거절을 identity보다 먼저 유지한다.
277
+ if (!isCanonicalBlueprintDir(blueprintDir)) {
278
+ throw (0, verification_contracts_1.verificationError)('VERIFY_BLUEPRINT_INVALID', 'blueprintDir must be under .bouncer/context/epics');
279
+ }
280
+ if (taskId === undefined && effectiveTask === undefined) {
281
+ throw (0, verification_contracts_1.verificationError)('VERIFY_TARGET_REQUIRED', 'runVerification requires taskId or effectiveTask');
282
+ }
283
+ const selectedEntries = selectVerificationEntries(repoRoot, blueprintDir, { taskId, effectiveTask: effectiveTask ?? null });
284
+ const command = readVerifyCommandFor(repoRoot, selectedEntries, blueprintDir);
285
+ const verificationRel = resolveVerificationRel(repoRoot, blueprintDir, selectedEntries);
286
+ const verificationPath = path.join(repoRoot, verificationRel);
287
+ if (!fs.existsSync(verificationPath)) {
288
+ throw (0, verification_contracts_1.verificationError)('VERIFY_DOCUMENT_MISSING', `verification document missing: ${verificationPath}`);
289
+ }
290
+ // 같은 정책 목록을 executeVerify에 명시한다. 생략하면 cwd를 다시 읽어
291
+ // 두 표면이 어긋날 수 있고, invalid면 기본 목록으로 실행이 이어진다.
292
+ const policy = readVerifyPolicy(repoRoot);
293
+ if (policy.ok === false) {
294
+ throw (0, verification_contracts_1.verificationError)('VERIFY_CONFIG_INVALID', `verification config is invalid: ${path.join(repoRoot, '.bouncer', 'config.json')}`);
295
+ }
296
+ // 2. scope·identity는 spawn 전에 확정한다. 실패는 전용 오류.
297
+ const scope = scopeInput
298
+ ? assertVerificationScope(scopeInput)
299
+ : resolveDefaultTaskScope(repoRoot, blueprintDir, effectiveTask ?? null, selectedEntries);
300
+ const { identity, evidenceId } = computeEvidenceIdentity({
301
+ repoRoot, command, scope, deps,
302
+ });
303
+ // 2b. npm terminal 검증은 직접 의존성 metadata가 빠진 integration에서 실행·재사용·증적
304
+ // 쓰기 전에 거절한다. 일반 테스트 실패(failed 기록)와 구분되는 VERIFY_DEPENDENCIES_MISSING이며,
305
+ // 기존 verification.md·원장 bytes를 건드리지 않는다. 비npm 명령과 검사 불가(workspaces 등)는
306
+ // 현행 경로를 유지하고, 출력 문자열이나 비0 종료로 의존성 부족을 추측하지 않는다.
307
+ if (scope.kind === 'terminal' && parseVerifyArgv(command)?.[0] === 'npm') {
308
+ const missing = seedWorktree.missingDirectDependencies(repoRoot);
309
+ if (missing !== null && missing.length > 0) {
310
+ throw (0, verification_contracts_1.verificationError)('VERIFY_DEPENDENCIES_MISSING', `direct dependencies are not installed in ${repoRoot}: ${missing.join(', ')}`);
311
+ }
312
+ }
313
+ // 3. 성공 v2 원장 hit면 exec를 건너뛰고 원본 ran_at·output을 재기록한다.
314
+ const selected = selectedEntries[0];
315
+ /**
316
+ * 증적 기록이 끝난 뒤에만 상태를 쓴다. 다음 dirty_digest는 달라져 재사용을 놓친다.
317
+ *
318
+ * @param {VerifyFinishResult} result - 이번 실행 또는 재사용 결과
319
+ * @returns {VerifyFinishResult} 호출자에게 그대로 돌려줄 결과
320
+ */
321
+ const finish = (result) => {
322
+ if (markTaskVerified && result.ok && selected && selected.rel) {
323
+ maybeMarkCommitTaskVerified({
324
+ repoRoot,
325
+ blueprintDir,
326
+ entryRel: selected.rel,
327
+ effectiveTask: effectiveTask ?? null,
328
+ });
329
+ }
330
+ return result;
331
+ };
332
+ const existing = findReusableEvidence(repoRoot, verificationRel, identity, evidenceId);
333
+ if (existing) {
334
+ recordVerificationResult({
335
+ repoRoot, verificationRel, command, ranAt: existing.ran_at, exitCode: 0,
336
+ output: typeof existing.output_tail === 'string' ? existing.output_tail : '',
337
+ evidenceId, identity, scope, reused: true, reusedFrom: existing.evidence_id, deps,
338
+ });
339
+ return finish({
340
+ ok: true, command, exitCode: 0, evidenceId, reused: true, reusedFrom: existing.evidence_id,
341
+ });
342
+ }
343
+ // 4. cache miss — 실제 명령을 한 번 실행하고 v2 증적을 남긴다.
344
+ const execution = executeVerify(command, {
345
+ cwd: repoRoot,
346
+ exec,
347
+ allowlist: policy.allowlist,
348
+ });
349
+ const ranAt = nowIsoKst(now());
350
+ recordVerificationResult({
351
+ repoRoot,
352
+ verificationRel,
353
+ command,
354
+ ranAt,
355
+ exitCode: execution.exitCode,
356
+ output: execution.output,
357
+ evidenceId,
358
+ identity,
359
+ scope,
360
+ reused: false,
361
+ deps,
362
+ });
363
+ return finish({
364
+ ok: execution.ok,
365
+ command,
366
+ exitCode: execution.exitCode,
367
+ evidenceId,
368
+ reused: false,
369
+ });
370
+ }
371
+ module.exports = {
372
+ OUTPUT_TAIL_LINES,
373
+ PASSING_OUTPUT_TAIL_LINES,
374
+ MAX_VERIFY_OUTPUT_BYTES,
375
+ parseVerifyArgv,
376
+ isValidVerifyCommand,
377
+ selectVerificationEntries,
378
+ readVerifyCommandFor,
379
+ executeVerify,
380
+ recordVerificationResult,
381
+ runVerification,
382
+ computeSourceDigest,
383
+ };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ // scripts/spawn-win.js와 같은 규칙이다. runtime 패키지에는 scripts/가 들어가지 않아 복제한다.
5
+ // npm/npx는 Windows에서 .cmd shim이라 shell 없이 spawn하면 EINVAL(Node의 CVE-2024-27980 대응),
6
+ // 확장자 없이 부르면 ENOENT가 난다. 그래서 node 옆의 npm CLI 스크립트를 node로 직접 실행한다.
7
+ const CLI_FILES = { npm: 'npm-cli.js', npx: 'npx-cli.js' };
8
+ /**
9
+ * spawn 대상이 npm/npx이고 Windows이면 node로 npm CLI 스크립트를 직접 실행하는 호출로 바꾼다.
10
+ * 그 밖의 명령과 플랫폼은 그대로 돌려준다. CLI 스크립트를 찾지 못하면 `.cmd`를 shell로 실행하는
11
+ * 호출로 되돌린다.
12
+ *
13
+ * @param {string} command - 실행 파일 이름. `npm`, `npm.cmd`, `npx` 모두 받는다
14
+ * @param {string[]} args - 인자
15
+ * @param {ResolveOptions} [opts] - 테스트용 주입값. 생략하면 현재 프로세스 값을 쓴다
16
+ * @returns {SpawnCall} spawn에 넘길 호출
17
+ */
18
+ function resolveSpawn(command, args, opts = {}) {
19
+ const platform = opts.platform || process.platform;
20
+ const tool = String(command).replace(/\.cmd$/i, '');
21
+ if (platform !== 'win32' || !Object.hasOwn(CLI_FILES, tool)) {
22
+ return { command, args, shell: false };
23
+ }
24
+ const execPath = opts.execPath || process.execPath;
25
+ const env = opts.env || process.env;
26
+ const exists = opts.exists || fs.existsSync;
27
+ const candidates = [];
28
+ const viaNpm = env.npm_execpath;
29
+ if (tool === 'npm' && typeof viaNpm === 'string' && /npm-cli\.js$/i.test(viaNpm)) {
30
+ candidates.push(viaNpm);
31
+ }
32
+ candidates.push(path.win32.join(path.win32.dirname(execPath), 'node_modules', 'npm', 'bin', CLI_FILES[tool]));
33
+ const cli = candidates.find((file) => exists(file));
34
+ if (cli)
35
+ return { command: execPath, args: [cli, ...args], shell: false };
36
+ return { command: `${tool}.cmd`, args, shell: true };
37
+ }
38
+ module.exports = { resolveSpawn };
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@cheongfish/bouncer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.13",
4
+ "license": "Apache-2.0",
5
+ "description": "Deterministic runtime for the Bouncer plugin",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/cheongfish/bouncer.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "engines": {
15
+ "node": ">=24"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "dist/",
20
+ "vendor/",
21
+ "resources/",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "bin": {
26
+ "bouncer": "bin/bouncer",
27
+ "bouncer-root": "bin/bouncer-root"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json && node bin/build-stamp.js --write",
31
+ "postinstall": "node bin/register-path.js",
32
+ "prepack": "npm run build"
33
+ }
34
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema_version": 1,
3
+ "package": "graphifyy",
4
+ "install_spec": "graphifyy==0.9.56",
5
+ "cli_version": "0.9.56",
6
+ "graph_schema_version": "1",
7
+ "python_minimum": "3.10",
8
+ "skill_version": "0.9.56"
9
+ }
@@ -0,0 +1,23 @@
1
+ # Vendored dependencies
2
+
3
+ Claude Code installs plugins by cloning them; it never runs `npm install`.
4
+ Anything the CLI or the hooks require at runtime must live in the repository,
5
+ so `js-yaml` is vendored here instead of declared as a dependency.
6
+
7
+ | File | Package | Version | License |
8
+ | --- | --- | --- | --- |
9
+ | `js-yaml.js` | [js-yaml](https://github.com/nodeca/js-yaml) | 4.3.2 | MIT |
10
+
11
+ `js-yaml.js` is the unmodified `dist/js-yaml.js` UMD bundle from the published
12
+ package. Claude Code never runs `npm install`, so this copy is the runtime; it
13
+ must stay byte-identical to `node_modules/js-yaml/dist/js-yaml.js` at the same
14
+ version. To refresh it:
15
+
16
+ ```bash
17
+ npm install --no-save js-yaml@<version>
18
+ cp node_modules/js-yaml/dist/js-yaml.js scripts/vendor/js-yaml.js
19
+ # LICENSE는 4.3.2 MIT 전문이 기존 파일과 같으면 다시 쓰지 않는다.
20
+ cp node_modules/js-yaml/LICENSE scripts/vendor/js-yaml.LICENSE
21
+ node --test test/distribution.test.js
22
+ npm run verify:security
23
+ ```
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (C) 2011-2015 by Vitaly Puzrin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.