@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.
- package/LICENSE +202 -0
- package/README.md +31 -2
- package/bin/bouncer +26 -0
- package/bin/bouncer-root +25 -0
- package/bin/build-stamp.js +78 -0
- package/bin/register-path.js +71 -0
- package/dist/.build-stamp +1 -0
- package/dist/approval-snapshot.js +99 -0
- package/dist/bouncer-root.js +80 -0
- package/dist/canonical-json.js +37 -0
- package/dist/cli-commit-guard-command.js +74 -0
- package/dist/cli-config-command.js +47 -0
- package/dist/cli-context-review-command.js +273 -0
- package/dist/cli-current-command.js +374 -0
- package/dist/cli-dispatch-command.js +119 -0
- package/dist/cli-doc-commands.js +352 -0
- package/dist/cli-epic-command.js +130 -0
- package/dist/cli-flags.js +50 -0
- package/dist/cli-git-commands.js +931 -0
- package/dist/cli-intent-command.js +491 -0
- package/dist/cli-metrics-command.js +134 -0
- package/dist/cli-ontology-command.js +109 -0
- package/dist/cli-project-commands.js +580 -0
- package/dist/cli-retro-command.js +138 -0
- package/dist/cli-review-command.js +251 -0
- package/dist/cli-review-dispatch-command.js +338 -0
- package/dist/cli-usage.js +72 -0
- package/dist/cli.js +70 -0
- package/dist/codex-agents.js +189 -0
- package/dist/command-argv.js +125 -0
- package/dist/commit-guard.js +72 -0
- package/dist/commit-hook.js +470 -0
- package/dist/commit-message.js +77 -0
- package/dist/commit-sha.js +107 -0
- package/dist/commit.js +519 -0
- package/dist/composite-gates.js +129 -0
- package/dist/comprehension.js +127 -0
- package/dist/config.js +298 -0
- package/dist/context-ids.js +288 -0
- package/dist/context-migration-rewrite.js +421 -0
- package/dist/context-ontology.js +746 -0
- package/dist/context-review-contracts.js +2 -0
- package/dist/context-review-projection.js +147 -0
- package/dist/context-review-state.js +824 -0
- package/dist/context-review-store.js +602 -0
- package/dist/context-review-transitions.js +412 -0
- package/dist/coordinate-advance.js +253 -0
- package/dist/coordinate-next.js +998 -0
- package/dist/coordinate-output.js +57 -0
- package/dist/coordinator-command-contracts.js +2 -0
- package/dist/coordinator-commands.js +1908 -0
- package/dist/coordinator-fanin.js +1021 -0
- package/dist/coordinator-git.js +121 -0
- package/dist/coordinator-input.js +37 -0
- package/dist/coordinator-ledger.js +381 -0
- package/dist/coordinator-prepare.js +1047 -0
- package/dist/coordinator-repair.js +1416 -0
- package/dist/coordinator-verification.js +42 -0
- package/dist/coordinator-worker.js +388 -0
- package/dist/coordinator.js +7 -0
- package/dist/cross-drive.js +85 -0
- package/dist/current.js +1148 -0
- package/dist/dag-shape.js +93 -0
- package/dist/doc-shape-contracts.js +241 -0
- package/dist/drive-metrics.js +691 -0
- package/dist/epic-index.js +347 -0
- package/dist/epic-registry.js +453 -0
- package/dist/execute-prepare.js +262 -0
- package/dist/execution-contracts.js +46 -0
- package/dist/execution-dag.js +148 -0
- package/dist/finalize-checks.js +177 -0
- package/dist/finalize-collect.js +343 -0
- package/dist/finalize-comprehension.js +116 -0
- package/dist/finalize-digest-collect.js +388 -0
- package/dist/finalize-digest-model.js +595 -0
- package/dist/finalize-digest.js +164 -0
- package/dist/finalize-explain.js +176 -0
- package/dist/finalize-pr.js +610 -0
- package/dist/finalize-release-main.js +652 -0
- package/dist/finalize.js +714 -0
- package/dist/frontmatter.js +22 -0
- package/dist/git-ops.js +49 -0
- package/dist/graph-exec.js +179 -0
- package/dist/graph-scope.js +268 -0
- package/dist/graphify.js +995 -0
- package/dist/guards.js +82 -0
- package/dist/import-git.js +78 -0
- package/dist/import-history.js +392 -0
- package/dist/import-render.js +59 -0
- package/dist/import-types.js +2 -0
- package/dist/init.js +571 -0
- package/dist/intent-bundle.js +936 -0
- package/dist/intent-provenance.js +927 -0
- package/dist/layout.js +131 -0
- package/dist/lease.js +145 -0
- package/dist/legacy-comments.js +71 -0
- package/dist/migrate-context-slugs.js +262 -0
- package/dist/migrate-merge-legacy-epics.js +392 -0
- package/dist/migrate-review-scope.js +144 -0
- package/dist/migrate-task-layout.js +194 -0
- package/dist/path-overlap.js +38 -0
- package/dist/paths.js +45 -0
- package/dist/plan-approve.js +218 -0
- package/dist/plan-inspect.js +575 -0
- package/dist/plan-prerequisites.js +197 -0
- package/dist/plan-snapshot-contract.js +98 -0
- package/dist/plan-snapshot.js +138 -0
- package/dist/plugin-root.js +237 -0
- package/dist/posix-path.js +14 -0
- package/dist/pre-commit-hook.js +170 -0
- package/dist/print-dispatch.js +285 -0
- package/dist/render.js +8 -0
- package/dist/retention-migration.js +595 -0
- package/dist/retro-read.js +259 -0
- package/dist/retro-sources.js +318 -0
- package/dist/review-dispatch.js +1054 -0
- package/dist/review-policy.js +28 -0
- package/dist/review-record.js +447 -0
- package/dist/review-repair-state.js +52 -0
- package/dist/review-snapshot.js +659 -0
- package/dist/run-detach.js +165 -0
- package/dist/run-preflight.js +368 -0
- package/dist/runtime-state.js +1038 -0
- package/dist/scaffold.js +533 -0
- package/dist/schema.js +294 -0
- package/dist/scope.js +1073 -0
- package/dist/seed-worktree.js +926 -0
- package/dist/session-graph.js +337 -0
- package/dist/subagents.js +86 -0
- package/dist/symbol-index.js +1390 -0
- package/dist/task-brief-hash.js +34 -0
- package/dist/task-commits.js +124 -0
- package/dist/tasks-docs.js +192 -0
- package/dist/templates.js +469 -0
- package/dist/time.js +21 -0
- package/dist/validate-docs.js +302 -0
- package/dist/validate-gates-finalize.js +303 -0
- package/dist/validate-gates-plan.js +720 -0
- package/dist/validate-gates-run.js +420 -0
- package/dist/validate-gates-shared.js +124 -0
- package/dist/validate-gates.js +87 -0
- package/dist/validate-sections.js +654 -0
- package/dist/validate-structural.js +347 -0
- package/dist/validate.js +417 -0
- package/dist/verification-contracts.js +26 -0
- package/dist/verification-evidence.js +233 -0
- package/dist/verification-execution.js +163 -0
- package/dist/verification-identity.js +413 -0
- package/dist/verification-target.js +51 -0
- package/dist/verification.js +383 -0
- package/dist/windows-npm.js +38 -0
- package/package.json +32 -4
- package/resources/graphify-compat.json +9 -0
- package/vendor/README.md +23 -0
- package/vendor/js-yaml.LICENSE +21 -0
- package/vendor/js-yaml.js +3022 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultStagedFiles = defaultStagedFiles;
|
|
4
|
+
exports.readOptionalLeaf = readOptionalLeaf;
|
|
5
|
+
exports.loadBlueprintDocs = loadBlueprintDocs;
|
|
6
|
+
exports.resolveTaskUnit = resolveTaskUnit;
|
|
7
|
+
exports.unitLeafRel = unitLeafRel;
|
|
8
|
+
exports.blueprintDocsExist = blueprintDocsExist;
|
|
9
|
+
exports.statusOf = statusOf;
|
|
10
|
+
exports.requiredTaskLeaves = requiredTaskLeaves;
|
|
11
|
+
exports.isBlueprintReviewMode = isBlueprintReviewMode;
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { execFileSync } = require('node:child_process');
|
|
15
|
+
const frontmatter = require("./frontmatter");
|
|
16
|
+
const { readDoc } = frontmatter;
|
|
17
|
+
const paths = require("./paths");
|
|
18
|
+
const { epicDirOf, toPosix } = paths;
|
|
19
|
+
const verificationTarget = require("./verification-target");
|
|
20
|
+
const { resolveVerificationTarget } = verificationTarget;
|
|
21
|
+
const tasksDocs = require("./tasks-docs");
|
|
22
|
+
const { listTasksDocs, TASK_UNIT_BASENAMES, taskExecutionKind, } = tasksDocs;
|
|
23
|
+
function errorMessage(error) {
|
|
24
|
+
// catch 값은 unknown이다. 예전 e.message 접근을 유지해 primitive throw의
|
|
25
|
+
// 메시지는 undefined, null throw는 TypeError가 나게 둔다.
|
|
26
|
+
return error.message;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* commit 게이트 G17용 스테이징 목록. throw하지 않는다 —
|
|
30
|
+
* git 실패·비저장소는 { ok:false }로 올려 게이트가 G17로 보고하게 한다.
|
|
31
|
+
*/
|
|
32
|
+
function defaultStagedFiles({ repoRoot }) {
|
|
33
|
+
try {
|
|
34
|
+
const out = execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
35
|
+
cwd: repoRoot,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
});
|
|
38
|
+
return { ok: true, files: String(out).split('\n').filter(Boolean) };
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const reason = typeof e === 'object' && e !== null && 'message' in e && e.message
|
|
42
|
+
? String(e.message)
|
|
43
|
+
: 'git-failed';
|
|
44
|
+
return { ok: false, reason };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 묶음 leaf는 없어도 된다(S17이 따로 보고). 없으면 undefined를 돌려
|
|
48
|
+
// 호출자가 대체 묶음으로 채우지 못하게 한다. 파싱 실패만 S0으로 남긴다.
|
|
49
|
+
function readOptionalLeaf(repoRoot, rel, parseErrors) {
|
|
50
|
+
if (!rel)
|
|
51
|
+
return undefined;
|
|
52
|
+
const abs = path.join(repoRoot, rel);
|
|
53
|
+
if (!fs.existsSync(abs))
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const { data, body } = readDoc(abs);
|
|
57
|
+
return { data, body, rel };
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
parseErrors.push({ code: 'S0', message: errorMessage(e), file: rel });
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* context-review frontmatter의 closure_target_digest 모양을 검사한다. 부재(과거 문서)는 유효하고,
|
|
66
|
+
* 있으면 64 lowercase hex 문자열이어야 한다.
|
|
67
|
+
*
|
|
68
|
+
* @param {unknown} data - context-review.md frontmatter
|
|
69
|
+
* @returns {boolean} 부재이거나 올바른 hex이면 true
|
|
70
|
+
*/
|
|
71
|
+
function isValidClosureDigestField(data) {
|
|
72
|
+
const bouncer = data?.bouncer;
|
|
73
|
+
const meta = bouncer?.context_review;
|
|
74
|
+
if (meta === null || typeof meta !== 'object')
|
|
75
|
+
return true;
|
|
76
|
+
const digest = meta.closure_target_digest;
|
|
77
|
+
return digest === undefined || (typeof digest === 'string' && /^[0-9a-f]{64}$/.test(digest));
|
|
78
|
+
}
|
|
79
|
+
function loadBlueprintDocs({ repoRoot, blueprintDir }) {
|
|
80
|
+
const bp = toPosix(blueprintDir);
|
|
81
|
+
const tasksListing = listTasksDocs({ repoRoot, blueprintDir });
|
|
82
|
+
const rels = {
|
|
83
|
+
epicIndex: `${epicDirOf(bp)}/index.md`,
|
|
84
|
+
blueprintIndex: `${bp}/index.md`,
|
|
85
|
+
// finalize · execute G6 호환용 대표 경로(첫 task 문서).
|
|
86
|
+
// 묶음이 없을 때도 정본 레이아웃을 가리킨다 — 레거시 루트 basename은
|
|
87
|
+
// migrate task-layout 입력이고, validate는 S15로 거절한다(보고 경로로 쓰지 않음).
|
|
88
|
+
tasks: tasksListing.entries[0]
|
|
89
|
+
? tasksListing.entries[0].rel
|
|
90
|
+
: `${bp}/tasks/001/tasks.md`,
|
|
91
|
+
verification: `${bp}/verification.md`,
|
|
92
|
+
review: `${bp}/review.md`,
|
|
93
|
+
explain: `${bp}/explain.md`,
|
|
94
|
+
// BP 단위 슬롯. context review 기록 검사가 이 문서를 판정한다. 슬롯이 없으면
|
|
95
|
+
// 문서가 로드되지 않아 checkStructural/S19가 이 파일을 보지 못한다.
|
|
96
|
+
contextReview: `${bp}/context-review.md`,
|
|
97
|
+
};
|
|
98
|
+
const docs = {};
|
|
99
|
+
const parseErrors = [];
|
|
100
|
+
const optionalKeys = [
|
|
101
|
+
'epicIndex', 'blueprintIndex', 'verification', 'review', 'explain', 'contextReview',
|
|
102
|
+
];
|
|
103
|
+
for (const key of optionalKeys) {
|
|
104
|
+
const rel = rels[key];
|
|
105
|
+
const abs = path.join(repoRoot, rel);
|
|
106
|
+
if (fs.existsSync(abs)) {
|
|
107
|
+
try {
|
|
108
|
+
const { data, body } = readDoc(abs);
|
|
109
|
+
docs[key] = { data, body, rel };
|
|
110
|
+
// context_review.closure_target_digest는 64 lowercase hex만 구조상 유효하다. 틀린 값은
|
|
111
|
+
// 현재 내용으로 대체 인증하지 않고 구조 오류로 남겨 stale 값이 통과하지 못하게 한다.
|
|
112
|
+
if (key === 'contextReview' && !isValidClosureDigestField(data)) {
|
|
113
|
+
parseErrors.push({
|
|
114
|
+
code: 'S0',
|
|
115
|
+
message: 'bouncer.context_review.closure_target_digest must be 64 lowercase hex',
|
|
116
|
+
file: rel,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
parseErrors.push({ code: 'S0', message: errorMessage(e), file: rel });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const tasksDocs = [];
|
|
126
|
+
for (const entry of tasksListing.entries) {
|
|
127
|
+
const abs = path.join(repoRoot, entry.rel);
|
|
128
|
+
if (!fs.existsSync(abs))
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
const { data, body } = readDoc(abs);
|
|
132
|
+
tasksDocs.push({ data, body, rel: entry.rel });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
parseErrors.push({ code: 'S0', message: errorMessage(e), file: entry.rel });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (tasksDocs.length > 0) {
|
|
139
|
+
// docs.tasks = 첫 문서 호환 필드. execute 게이트는 taskUnit만 본다.
|
|
140
|
+
docs.tasks = tasksDocs[0];
|
|
141
|
+
docs.tasksDocs = tasksDocs;
|
|
142
|
+
}
|
|
143
|
+
// 묶음별 파싱. 파일이 없으면 해당 leaf는 undefined — 대체 묶음으로 채우지 않는다.
|
|
144
|
+
const taskUnits = tasksListing.entries.map((entry) => ({
|
|
145
|
+
number: entry.number,
|
|
146
|
+
dir: entry.dir,
|
|
147
|
+
tasks: readOptionalLeaf(repoRoot, entry.tasks.rel, parseErrors),
|
|
148
|
+
verification: readOptionalLeaf(repoRoot, entry.verification.rel, parseErrors),
|
|
149
|
+
// verification node는 review 단계가 없다. listing의 review 경로는 S17이
|
|
150
|
+
// 열린 일반 bundle 완전성을 검사할 때만 쓰고, resolver payload에는
|
|
151
|
+
// verification.md를 review로 alias하지 않는다.
|
|
152
|
+
review: entry.executionKind === 'verification'
|
|
153
|
+
? undefined
|
|
154
|
+
: readOptionalLeaf(repoRoot, entry.review?.rel, parseErrors),
|
|
155
|
+
}));
|
|
156
|
+
docs.taskUnits = taskUnits;
|
|
157
|
+
// validate.ts의 공통 S17 순회는 모든 종류에 존재하는 tasks/verification만
|
|
158
|
+
// 맡는다. commit 전용 review 누락은 execution kind를 아는 이 loader에서
|
|
159
|
+
// 추가해, verification entry에 가짜 review 경로를 싣지 않아도 기존 완전성
|
|
160
|
+
// 검사가 유지되게 한다. blueprint 리뷰 모드는 task review leaf를 요구하지
|
|
161
|
+
// 않는다 — 판정 근거는 루트 review.md 하나다.
|
|
162
|
+
const blueprintStatus = statusOf(docs.blueprintIndex);
|
|
163
|
+
const reviewMode = isBlueprintReviewMode(docs.blueprintIndex);
|
|
164
|
+
for (const entry of tasksListing.entries) {
|
|
165
|
+
for (const leaf of requiredTaskLeaves(blueprintStatus, entry.executionKind, reviewMode)) {
|
|
166
|
+
if (leaf !== 'review')
|
|
167
|
+
continue;
|
|
168
|
+
const rel = entry.review?.rel;
|
|
169
|
+
if (rel && !fs.existsSync(path.join(repoRoot, rel))) {
|
|
170
|
+
parseErrors.push({
|
|
171
|
+
code: 'S17',
|
|
172
|
+
message: `task unit ${entry.number} missing ${path.posix.basename(rel)}`,
|
|
173
|
+
file: rel,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { docs, rels, parseErrors, tasksListing };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* execute / finalize 가 쓸 대상 묶음.
|
|
182
|
+
* verification-target과 같은 effective-task 해석: 매칭되면 그 엔트리만, 아니면
|
|
183
|
+
* 번호 순 첫 묶음. worker에서 lease를 확인하지 못하면 null — gate가 task
|
|
184
|
+
* 문서 부재로 실패하게 한다. 단위 테스트처럼 repoRoot가 없으면
|
|
185
|
+
* docs.tasks·verification·review 평탄 필드로 합성.
|
|
186
|
+
*/
|
|
187
|
+
function resolveTaskUnit(docs, { repoRoot, blueprintDir } = {}) {
|
|
188
|
+
if (repoRoot && blueprintDir) {
|
|
189
|
+
const { effectiveTask: effective, entries } = resolveVerificationTarget({ repoRoot, blueprintDir });
|
|
190
|
+
// worker fail-closed: 빈 entries로 flat docs에 떨어지면 G6 부재를 숨긴다.
|
|
191
|
+
if (effective && effective.source === null)
|
|
192
|
+
return null;
|
|
193
|
+
const entry = entries[0];
|
|
194
|
+
if (entry) {
|
|
195
|
+
const units = Array.isArray(docs.taskUnits) ? docs.taskUnits : [];
|
|
196
|
+
const match = units.find((u) => ((entry.dir && u.dir === entry.dir)
|
|
197
|
+
|| (entry.number != null && u.number === entry.number)
|
|
198
|
+
|| (u.tasks && entry.tasks && u.tasks.rel === entry.tasks.rel)));
|
|
199
|
+
if (match)
|
|
200
|
+
return match;
|
|
201
|
+
// listing에는 있으나 파싱 누락 — 빈 leaf로라도 경로를 유지해 G6 file을 살린다.
|
|
202
|
+
return {
|
|
203
|
+
number: entry.number ?? null,
|
|
204
|
+
dir: entry.dir ?? null,
|
|
205
|
+
tasks: undefined,
|
|
206
|
+
verification: undefined,
|
|
207
|
+
review: undefined,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (docs.tasks || docs.verification || docs.review) {
|
|
212
|
+
const executionKind = docs.tasks ? taskExecutionKind(docs.tasks.data) : null;
|
|
213
|
+
return {
|
|
214
|
+
number: null,
|
|
215
|
+
dir: null,
|
|
216
|
+
tasks: docs.tasks,
|
|
217
|
+
verification: docs.verification,
|
|
218
|
+
review: executionKind === 'verification' ? undefined : docs.review,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/** 파일이 없을 때도 실패 file 경로를 묶음 안으로 고정한다. */
|
|
224
|
+
function unitLeafRel(unit, leaf, fallbackRel) {
|
|
225
|
+
if (unit && leaf in unit) {
|
|
226
|
+
const leafDoc = unit[leaf];
|
|
227
|
+
if (leafDoc && leafDoc.rel)
|
|
228
|
+
return leafDoc.rel;
|
|
229
|
+
}
|
|
230
|
+
if (unit && unit.dir) {
|
|
231
|
+
const idx = ['tasks', 'verification', 'review'].indexOf(leaf);
|
|
232
|
+
if (idx >= 0)
|
|
233
|
+
return `${unit.dir}/${TASK_UNIT_BASENAMES[idx]}`;
|
|
234
|
+
}
|
|
235
|
+
return fallbackRel;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* blueprint index가 blueprint 리뷰 모드인지 판정한다.
|
|
239
|
+
* 모드는 `bouncer.review_scope === 'blueprint'`일 때만 켠다. 필드 부재는
|
|
240
|
+
* 구형 계약(task별 리뷰)을 유지해야 하므로 모드가 아니다. 잘못된 값은
|
|
241
|
+
* 여기서 true로 접지 않는다 — 구조 층이 S31로 거절한다.
|
|
242
|
+
*
|
|
243
|
+
* @param {DocLeaf | undefined} blueprintIndex - blueprint `index.md` 문서. 없으면 모드가 아니다
|
|
244
|
+
* @returns {boolean} 모드이면 true, 아니면 false
|
|
245
|
+
*/
|
|
246
|
+
function isBlueprintReviewMode(blueprintIndex) {
|
|
247
|
+
if (!blueprintIndex)
|
|
248
|
+
return false;
|
|
249
|
+
const data = blueprintIndex.data;
|
|
250
|
+
const bouncer = data && data.bouncer;
|
|
251
|
+
return Boolean(bouncer && bouncer.review_scope === 'blueprint');
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* blueprint 상태에 따라 task 묶음에서 필수인 leaf를 돌려준다.
|
|
255
|
+
* closed는 finalize가 task leaf 전체를 지운 축약 레이아웃(필수 leaf 없음)을
|
|
256
|
+
* 허용한다. 실행 종류가 주어지지 않은 호출은 공통 leaf만 받고, commit
|
|
257
|
+
* 묶음만 review를 추가한다. verification 묶음에는 review leaf가 없다.
|
|
258
|
+
* blueprint 리뷰 모드에서는 commit 묶음도 review leaf를 요구하지 않는다.
|
|
259
|
+
*
|
|
260
|
+
* @param {unknown} status - blueprint index의 bouncer.status
|
|
261
|
+
* @param {'commit'|'verification'|null} [executionKind] task 실행 종류
|
|
262
|
+
* @param {boolean} [reviewMode] blueprint 리뷰 모드. true면 review leaf를 빼 준다
|
|
263
|
+
* @returns {Array<'tasks'|'verification'|'review'>} 필수 leaf 이름
|
|
264
|
+
*/
|
|
265
|
+
function requiredTaskLeaves(status, executionKind, reviewMode) {
|
|
266
|
+
// closed는 finalize가 일회성·검증 문서를 지운 뒤의 단말 상태다.
|
|
267
|
+
// 재개·task 추가가 없으므로 task leaf 부재를 구조 실패로 보지 않는다.
|
|
268
|
+
if (status === 'closed')
|
|
269
|
+
return [];
|
|
270
|
+
const common = ['tasks', 'verification'];
|
|
271
|
+
// 모드에서는 루트 review.md가 유일한 판정 근거다. task 묶음에 파일이
|
|
272
|
+
// 남아 있어도 S17로 요구하지 않는다.
|
|
273
|
+
if (reviewMode)
|
|
274
|
+
return common;
|
|
275
|
+
return executionKind === undefined || executionKind === 'verification'
|
|
276
|
+
? common
|
|
277
|
+
: [...common, 'review'];
|
|
278
|
+
}
|
|
279
|
+
// 존재 여부만 확인: 가볍고 파싱하지 않아야 함. execute gate가 verification을
|
|
280
|
+
// 다시 실행(verification.md를 다시 씀)하기 전에 호출되기 때문.
|
|
281
|
+
function blueprintDocsExist({ repoRoot, blueprintDir }) {
|
|
282
|
+
const bp = toPosix(blueprintDir);
|
|
283
|
+
const tasksListing = listTasksDocs({ repoRoot, blueprintDir });
|
|
284
|
+
// tasks/<NNN>/가 있으면(축약으로 leaf가 비어도) 존재로 본다.
|
|
285
|
+
if (tasksListing.entries.length > 0)
|
|
286
|
+
return true;
|
|
287
|
+
// index.md만 있어도 존재로 본다 — imported exhibit처럼 task leaf가 없는
|
|
288
|
+
// blueprint도 S11이 아니라 이후 상태 검사(S18 등)로 보내야 한다.
|
|
289
|
+
// closed 축약은 index+explain만 남기므로 여기에도 걸린다.
|
|
290
|
+
// 루트 review.md는 존재 신호로 쓰지 않는다.
|
|
291
|
+
return ['index.md', 'explain.md']
|
|
292
|
+
.some((name) => fs.existsSync(path.join(repoRoot, bp, name)));
|
|
293
|
+
}
|
|
294
|
+
// 문서·bouncer 부재는 throw가 아니라 undefined. 호출자가 enum 문자열과
|
|
295
|
+
// 비교하므로, 없는 문서는 자연히 status 검사에서 실패한다.
|
|
296
|
+
function statusOf(doc) {
|
|
297
|
+
if (!doc)
|
|
298
|
+
return undefined;
|
|
299
|
+
const data = doc.data;
|
|
300
|
+
const bouncer = data && data.bouncer;
|
|
301
|
+
return bouncer ? bouncer.status : undefined;
|
|
302
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkPartialCloseEvidence = checkPartialCloseEvidence;
|
|
4
|
+
exports.checkPartialCloseGate = checkPartialCloseGate;
|
|
5
|
+
exports.checkFinalizeGate = checkFinalizeGate;
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const paths = require("./paths");
|
|
9
|
+
const { toPosix } = paths;
|
|
10
|
+
const runtimeState = require("./runtime-state");
|
|
11
|
+
const comprehension = require("./comprehension");
|
|
12
|
+
const { computeDiffSha, EXPLAIN_SECTION_DEFS, resolveComprehensionEntry } = comprehension;
|
|
13
|
+
const validateDocs = require("./validate-docs");
|
|
14
|
+
const { statusOf, isBlueprintReviewMode } = validateDocs;
|
|
15
|
+
const validateSections = require("./validate-sections");
|
|
16
|
+
const { parseExplainSections, collectFindingFailures, EXECUTE_REVIEW_STATUS } = validateSections;
|
|
17
|
+
const schema = require("./schema");
|
|
18
|
+
const { executionKindOf, reviewContractOf } = schema;
|
|
19
|
+
const execution_contracts_1 = require("./execution-contracts");
|
|
20
|
+
const validate_gates_run_1 = require("./validate-gates-run");
|
|
21
|
+
const validate_gates_shared_1 = require("./validate-gates-shared");
|
|
22
|
+
/**
|
|
23
|
+
* partial close의 네 증적을 한 경계에서 판정한다. 일반 finalize와 섞지 않아
|
|
24
|
+
* 실패한 drive가 `closed` 성공 조건을 빌려 통과하지 못하게 한다.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} input - 원장, main-worktree handoff reference, 사용자 확인
|
|
27
|
+
* @returns {{ok: true} | {ok: false, reason: string}} gate 결과
|
|
28
|
+
*/
|
|
29
|
+
function checkPartialCloseEvidence(input) {
|
|
30
|
+
if (!input.ledger || typeof input.ledger !== 'object'
|
|
31
|
+
|| input.ledger.status !== 'awaiting_confirmation') {
|
|
32
|
+
return { ok: false, reason: 'partial-close-awaiting-confirmation-required' };
|
|
33
|
+
}
|
|
34
|
+
const checked = runtimeState.validateCoordinatorLedger({
|
|
35
|
+
...(input.ledger && typeof input.ledger === 'object' ? input.ledger : {}),
|
|
36
|
+
status: 'partial_closed', userConfirmed: input.userConfirmed,
|
|
37
|
+
});
|
|
38
|
+
if (!checked.ok)
|
|
39
|
+
return checked;
|
|
40
|
+
const ledgerHandoff = input.ledger.handoff;
|
|
41
|
+
if (!input.handoff || JSON.stringify(input.handoff) !== JSON.stringify(ledgerHandoff)) {
|
|
42
|
+
return { ok: false, reason: 'partial-close-handoff-reference-required' };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* checkout의 coordinator 원장에서 blueprint 경로만 읽는다. 파일이 없거나
|
|
48
|
+
* blueprint 문자열이 없으면 drive가 아니다. JSON 파손도 drive로 보지 않아
|
|
49
|
+
* stale git을 돌리지 않는다.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ repoRoot: string }} opts - gate를 실행한 checkout 루트
|
|
52
|
+
* @returns {{ blueprint: string } | null} 원장 blueprint 또는 null
|
|
53
|
+
*/
|
|
54
|
+
function defaultReadCoordinatorLedger({ repoRoot }) {
|
|
55
|
+
const file = path.join(repoRoot, '.bouncer', 'runtime', 'coordinator.json');
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// 원장 부재(ENOENT)만 흡수한다. 권한 오류를 "drive 아님"으로 접으면
|
|
62
|
+
// stale 검사를 건너뛰어 잘못된 마감을 통과시킬 수 있다.
|
|
63
|
+
if (error.code === 'ENOENT')
|
|
64
|
+
return null;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (typeof parsed.blueprint !== 'string' || parsed.blueprint === '')
|
|
70
|
+
return null;
|
|
71
|
+
return { blueprint: parsed.blueprint };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// JSON SyntaxError만 흡수한다. 원장을 읽을 수 없으면 drive 판정을
|
|
75
|
+
// 할 수 없고, 그 상태를 stale git 실패로 바꾸면 원인 코드가 갈라진다.
|
|
76
|
+
if (error instanceof SyntaxError)
|
|
77
|
+
return null;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* finalize G21 — blueprint 루트 review.md만 판정한다. task 묶음 review는
|
|
83
|
+
* 읽지 않는다. 폐기된 G4/G9/G15를 재사용하지 않는다.
|
|
84
|
+
*
|
|
85
|
+
* @param {BlueprintDocs} docs - 로드된 문서
|
|
86
|
+
* @param {BlueprintRels} rels - 실패 file 경로
|
|
87
|
+
* @param {FailureEntry[]} failures - G21을 누적할 배열
|
|
88
|
+
* @param {GateContext} ctx - repoRoot·deps
|
|
89
|
+
* @returns {void}
|
|
90
|
+
*/
|
|
91
|
+
function checkG21(docs, rels, failures, ctx) {
|
|
92
|
+
const add = (message) => failures.push({
|
|
93
|
+
code: 'G21', message, file: rels.review,
|
|
94
|
+
});
|
|
95
|
+
const reviewDoc = docs.review;
|
|
96
|
+
if (!reviewDoc) {
|
|
97
|
+
add('blueprint review missing');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const reviewBouncer = reviewDoc.data
|
|
101
|
+
&& reviewDoc.data.bouncer;
|
|
102
|
+
const reviewMeta = reviewBouncer
|
|
103
|
+
? reviewBouncer.review
|
|
104
|
+
: undefined;
|
|
105
|
+
// 루트 리뷰는 review.required: false로 면제하지 않는다. task G8과 달리
|
|
106
|
+
// blueprint 마감의 유일한 리뷰 증적이기 때문이다.
|
|
107
|
+
if (statusOf(reviewDoc) !== 'accepted') {
|
|
108
|
+
add('blueprint review not accepted');
|
|
109
|
+
}
|
|
110
|
+
const head = (0, validate_gates_shared_1.lastReviewHead)(reviewMeta && reviewMeta.rounds);
|
|
111
|
+
if (!head) {
|
|
112
|
+
add('blueprint review has no rounds');
|
|
113
|
+
}
|
|
114
|
+
// review_contract가 brief면 record와 같은 blueprint_brief 계약으로 판정한다. 허용 밖 값은 legacy로
|
|
115
|
+
// 접지 않고 collectFindingFailures 필터를 거치지 않는 직접 실패로 남겨, 마감이 조용히 통과하지 않게 한다.
|
|
116
|
+
const blueprintBouncer = docs.blueprintIndex && docs.blueprintIndex.data
|
|
117
|
+
? docs.blueprintIndex.data.bouncer
|
|
118
|
+
: undefined;
|
|
119
|
+
const reviewContract = reviewContractOf(blueprintBouncer);
|
|
120
|
+
if (reviewContract === null)
|
|
121
|
+
add(schema.REVIEW_CONTRACT_INVALID_MESSAGE);
|
|
122
|
+
for (const message of collectFindingFailures({
|
|
123
|
+
body: reviewDoc.body,
|
|
124
|
+
findings: reviewMeta && reviewMeta.findings,
|
|
125
|
+
rounds: reviewMeta && reviewMeta.rounds,
|
|
126
|
+
sectionLabel: 'review.md',
|
|
127
|
+
findingLabel: 'review',
|
|
128
|
+
allowedStatuses: EXECUTE_REVIEW_STATUS,
|
|
129
|
+
reviewStatus: statusOf(reviewDoc),
|
|
130
|
+
// blueprint 최종 리뷰는 repair wave마다 delta를 기록하므로 전용 순서 계약을 쓴다.
|
|
131
|
+
roundScope: reviewContract === 'brief' ? 'blueprint_brief' : 'blueprint',
|
|
132
|
+
}).filter(validate_gates_shared_1.isBlockingReviewFailure)) {
|
|
133
|
+
// 형식 위반은 기록 시점이 거절하므로 실질 판정만 G21로 남긴다.
|
|
134
|
+
add(message);
|
|
135
|
+
}
|
|
136
|
+
if (!head)
|
|
137
|
+
return;
|
|
138
|
+
const repoRoot = ctx.repoRoot;
|
|
139
|
+
const deps = ctx.deps;
|
|
140
|
+
const readLedger = (deps && deps.readCoordinatorLedger) || defaultReadCoordinatorLedger;
|
|
141
|
+
if (typeof repoRoot !== 'string')
|
|
142
|
+
return;
|
|
143
|
+
const ledger = readLedger({ repoRoot });
|
|
144
|
+
if (!ledger)
|
|
145
|
+
return;
|
|
146
|
+
// 원장의 blueprint가 이 gate 대상과 같을 때만 drive다. 다른 drive의
|
|
147
|
+
// 원장이 checkout에 남아 있어도 stale git을 돌리지 않는다.
|
|
148
|
+
if (toPosix(ledger.blueprint) !== toPosix(ctx.blueprintDir || ''))
|
|
149
|
+
return;
|
|
150
|
+
const ancestor = (0, validate_gates_shared_1.gitExec)(repoRoot, ['merge-base', '--is-ancestor', head, 'HEAD'], deps && deps.exec);
|
|
151
|
+
if (ancestor.status !== 0 && ancestor.status !== 1) {
|
|
152
|
+
add(`blueprint review stale check failed (${(0, validate_gates_shared_1.firstStderrLine)(ancestor.stderr)})`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (ancestor.status === 1) {
|
|
156
|
+
add('blueprint review is stale');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const diff = (0, validate_gates_shared_1.gitExec)(repoRoot, ['diff', '--name-only', head, 'HEAD'], deps && deps.exec);
|
|
160
|
+
if (diff.status !== 0) {
|
|
161
|
+
add(`blueprint review stale check failed (${(0, validate_gates_shared_1.firstStderrLine)(diff.stderr)})`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const dirty = String(diff.stdout).split('\n').filter(Boolean)
|
|
165
|
+
.some((rel) => !toPosix(rel).startsWith('.bouncer/'));
|
|
166
|
+
if (dirty)
|
|
167
|
+
add('blueprint review is stale');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* partial-close gate 판정. 실패 코드 G20과 reason 문자열은 분리 전과 같다.
|
|
171
|
+
*
|
|
172
|
+
* @param {GateStageCtx} stage - add 클로저와 partialClose 입력
|
|
173
|
+
* @returns {void}
|
|
174
|
+
*/
|
|
175
|
+
function checkPartialCloseGate(stage) {
|
|
176
|
+
const result = checkPartialCloseEvidence(stage.partialClose || {});
|
|
177
|
+
if (!result.ok)
|
|
178
|
+
stage.add('G20', result.reason, 'blueprintIndex');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* finalize gate 판정. G 코드·메시지·순서는 분리 전과 같다.
|
|
182
|
+
*
|
|
183
|
+
* @param {GateStageCtx} stage - add 클로저와 문서 묶음
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
function checkFinalizeGate(stage) {
|
|
187
|
+
const { add, docs, rels, failures } = stage;
|
|
188
|
+
const repoRoot = stage.repoRoot;
|
|
189
|
+
const blueprintDir = stage.blueprintDir;
|
|
190
|
+
const deps = stage.deps;
|
|
191
|
+
const ctx = (0, validate_gates_shared_1.toGateContext)(stage);
|
|
192
|
+
const tasksList = Array.isArray(docs.tasksDocs) && docs.tasksDocs.length > 0
|
|
193
|
+
? docs.tasksDocs
|
|
194
|
+
: (docs.tasks ? [docs.tasks] : []);
|
|
195
|
+
const openIds = [];
|
|
196
|
+
// 입증된 범위 밖 terminal 실패는 integrated가 아니어도 닫힌 것으로 본다. 판정은 next·digest·
|
|
197
|
+
// finalize와 같은 validOutsideScope 하나를 쓰고, 문서가 verifying일 때만 적용해 pending/ready 같은
|
|
198
|
+
// 재작업 대기 문서가 분류 하나로 닫히지 않게 한다. 원장이 없거나(비-drive) 분류가 stale이면 null이라
|
|
199
|
+
// 기존 open-task 실패가 그대로 나온다. 통과 시 실패 사실이 사라지지 않도록 경고를 남긴다.
|
|
200
|
+
const ledgerFn = (deps && deps.coordinatorLedgerFor) || validate_gates_run_1.defaultCoordinatorLedgerFor;
|
|
201
|
+
let ledgerCache;
|
|
202
|
+
const taskIsOpen = (tasksDoc) => {
|
|
203
|
+
const data = (0, validate_gates_shared_1.asData)(tasksDoc);
|
|
204
|
+
const taskBouncer = data && data.bouncer;
|
|
205
|
+
const isTerminal = executionKindOf(taskBouncer) === 'verification';
|
|
206
|
+
const status = statusOf(tasksDoc);
|
|
207
|
+
if (status === (isTerminal ? 'integrated' : 'verified'))
|
|
208
|
+
return false;
|
|
209
|
+
if (!isTerminal || status !== 'verifying' || typeof repoRoot !== 'string'
|
|
210
|
+
|| typeof blueprintDir !== 'string' || typeof taskBouncer?.id !== 'string')
|
|
211
|
+
return true;
|
|
212
|
+
if (ledgerCache === undefined)
|
|
213
|
+
ledgerCache = ledgerFn({ repoRoot, blueprint: blueprintDir });
|
|
214
|
+
const accepted = (0, execution_contracts_1.validOutsideScope)(ledgerCache, taskBouncer.id);
|
|
215
|
+
if (!accepted)
|
|
216
|
+
return true;
|
|
217
|
+
if (ctx && Array.isArray(ctx.warnings)) {
|
|
218
|
+
ctx.warnings.push({
|
|
219
|
+
code: 'G16',
|
|
220
|
+
message: `terminal verification ${taskBouncer.id} failed and is classified outside_scope: ${accepted.reason}`,
|
|
221
|
+
file: tasksDoc.rel || rels.tasks,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
};
|
|
226
|
+
const openDocs = [];
|
|
227
|
+
for (const tasksDoc of tasksList) {
|
|
228
|
+
if (!taskIsOpen(tasksDoc))
|
|
229
|
+
continue;
|
|
230
|
+
openDocs.push(tasksDoc);
|
|
231
|
+
const data = (0, validate_gates_shared_1.asData)(tasksDoc);
|
|
232
|
+
const id = data && data.bouncer
|
|
233
|
+
? data.bouncer.id
|
|
234
|
+
: undefined;
|
|
235
|
+
openIds.push(typeof id === 'string' && id ? id : '(unknown)');
|
|
236
|
+
}
|
|
237
|
+
if (openIds.length) {
|
|
238
|
+
// 열린 task id를 메시지에 담아 어느 묶음이 남았는지 바로 보이게 한다.
|
|
239
|
+
// 경고가 아니라 hard fail — 사용자가 넘길 수 없다.
|
|
240
|
+
const openDoc = openDocs[0];
|
|
241
|
+
failures.push({
|
|
242
|
+
code: 'G16',
|
|
243
|
+
message: `open tasks remain (not verified): ${openIds.join(', ')}`,
|
|
244
|
+
file: (openDoc && openDoc.rel) || rels.tasks,
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// G16 open-tasks 다음, Explain 앞. 모드가 아니면 루트 review.md가 있어도
|
|
249
|
+
// 구형 fixture의 G16 판정을 바꾸지 않는다.
|
|
250
|
+
if (isBlueprintReviewMode(docs.blueprintIndex)) {
|
|
251
|
+
checkG21(docs, rels, failures, ctx);
|
|
252
|
+
}
|
|
253
|
+
if (!docs.explain) {
|
|
254
|
+
add('G16', 'explain.md missing', 'explain');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (statusOf(docs.explain) !== 'published') {
|
|
258
|
+
add('G16', 'explain.status != published', 'explain');
|
|
259
|
+
}
|
|
260
|
+
const explainBody = typeof docs.explain.body === 'string' ? docs.explain.body : '';
|
|
261
|
+
const sections = parseExplainSections(explainBody);
|
|
262
|
+
const missing = EXPLAIN_SECTION_DEFS.filter((k) => !sections[k]);
|
|
263
|
+
if (missing.length) {
|
|
264
|
+
add('G16', `explain missing written sections: ${missing.join(', ')}`, 'explain');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const explainData = (0, validate_gates_shared_1.asData)(docs.explain);
|
|
268
|
+
const bouncer = explainData && explainData.bouncer
|
|
269
|
+
? explainData.bouncer
|
|
270
|
+
: {};
|
|
271
|
+
const comp = bouncer.comprehension;
|
|
272
|
+
// BP당 엔트리 하나(배열 마지막). task 번호 루프는 쓰지 않는다 —
|
|
273
|
+
// 0.7 다중 엔트리는 마지막만 보면 읽기 호환이 된다.
|
|
274
|
+
const found = resolveComprehensionEntry(comp);
|
|
275
|
+
// 기본 tsc(strict 꺼짐)는 `!found.ok`로 실패 분기를 좁히지 못한다.
|
|
276
|
+
if (found.ok === false) {
|
|
277
|
+
add('G16', found.reason === 'not-a-list'
|
|
278
|
+
? 'explain comprehension must be a list of task entries'
|
|
279
|
+
: 'explain comprehension record missing', 'explain');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// 계산 실패와 해시 불일치는 서로 다른 문자열 — 원인 분류가 메시지에 드러나야 한다.
|
|
283
|
+
const shaFn = (deps && deps.computeDiffSha) || computeDiffSha;
|
|
284
|
+
if (typeof repoRoot !== 'string') {
|
|
285
|
+
add('G16', 'explain diff_sha could not be computed (missing-repo)', 'explain');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const computed = shaFn({
|
|
289
|
+
repoRoot,
|
|
290
|
+
base: found.entry.range_from,
|
|
291
|
+
exec: deps && deps.exec,
|
|
292
|
+
});
|
|
293
|
+
if (!computed || computed.ok !== true) {
|
|
294
|
+
const reason = computed && computed.reason ? computed.reason : 'exec-failed';
|
|
295
|
+
add('G16', `explain diff_sha could not be computed (${reason})`, 'explain');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (computed.sha !== String(found.entry.diff_sha).trim()) {
|
|
299
|
+
// 메시지에 range_from을 쓰지 않는다 — 실패 사유는 불일치뿐; 범위는 엔트리에 있다.
|
|
300
|
+
add('G16', 'explain diff_sha does not match range_from..HEAD', 'explain');
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|