@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,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// finalize comprehension gate(G16)용 diff fingerprint 및 explain 섹션 키.
|
|
3
|
+
// gate 판정은 validate.ts에 두고, 이 모듈은 base..HEAD의 비-governance 슬라이스를
|
|
4
|
+
// 무엇을 측정하고 어떻게 해시할지만 정의한다.
|
|
5
|
+
const { createHash } = require('node:crypto');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
// 이 prefix 아래 governance 문서는 hash를 바꾸면 안 된다 — 그렇지 않으면 explain.md
|
|
8
|
+
// 작성 자체가 그 문서가 담는 comprehension 기록을 무효화한다.
|
|
9
|
+
const DIFF_EXCLUDED_PREFIXES = ['.bouncer/context/'];
|
|
10
|
+
// G16 필수 절 키만 정의. heading 정규식은 parseSections 옆 validate.ts에 두어
|
|
11
|
+
// 빈 섹션 규칙이 section 파서와 공유되게 한다. intuition은 새 Explain이 더 이상 쓰지 않으므로
|
|
12
|
+
// 필수에서 뺀다(기존 문서의 Intuition은 파서가 선택적으로 계속 읽는다).
|
|
13
|
+
const EXPLAIN_SECTION_DEFS = [
|
|
14
|
+
'background',
|
|
15
|
+
'code',
|
|
16
|
+
'quiz',
|
|
17
|
+
];
|
|
18
|
+
function defaultExec(repoRoot, args) {
|
|
19
|
+
const r = spawnSync('git', args, {
|
|
20
|
+
cwd: repoRoot,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
// 결정적 테스트/샌드박스에서 호스트 GIT_* 유출 방지.
|
|
23
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
status: typeof r.status === 'number' ? r.status : 1,
|
|
27
|
+
stdout: r.stdout || '',
|
|
28
|
+
stderr: r.stderr || '',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function isExcluded(relPath) {
|
|
32
|
+
const p = String(relPath).split('\\').join('/');
|
|
33
|
+
return DIFF_EXCLUDED_PREFIXES.some((prefix) => p === prefix.slice(0, -1) || p.startsWith(prefix));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* governance 문서를 제외한 `base`와 HEAD 사이 변경 경로의 fingerprint.
|
|
37
|
+
* 절대 throw하지 않으며, 모든 실패는 `{ ok: false, reason }`이다.
|
|
38
|
+
*
|
|
39
|
+
* @param {{ repoRoot: string, base: string,
|
|
40
|
+
* exec?: (args: string[]) => { status: number, stdout: string, stderr: string } }} opts
|
|
41
|
+
* @returns {{ ok: true, sha: string } | { ok: false, reason: 'no-base' | 'not-a-repo' | 'exec-failed' }}
|
|
42
|
+
*/
|
|
43
|
+
function computeDiffSha({ repoRoot, base, exec }) {
|
|
44
|
+
// 주입된 exec는 `git` 뒤 argv만 받는다; 기본값은 repoRoot에 바인딩.
|
|
45
|
+
const run = typeof exec === 'function'
|
|
46
|
+
? exec
|
|
47
|
+
: (args) => defaultExec(repoRoot, args);
|
|
48
|
+
try {
|
|
49
|
+
const inside = run(['rev-parse', '--is-inside-work-tree']);
|
|
50
|
+
if (inside.status !== 0) {
|
|
51
|
+
return { ok: false, reason: 'not-a-repo' };
|
|
52
|
+
}
|
|
53
|
+
// `--verify`로 "알 수 없는 base"를 이후 diff 실패와 구분한다.
|
|
54
|
+
const verified = run(['rev-parse', '--verify', `${base}^{commit}`]);
|
|
55
|
+
if (verified.status !== 0) {
|
|
56
|
+
return { ok: false, reason: 'no-base' };
|
|
57
|
+
}
|
|
58
|
+
// two-dot 범위: base에는 없고 HEAD에 있는 것. name-only면 충분 —
|
|
59
|
+
// 제외 경로 아래 content churn도 digest에 영향을 주면 안 된다.
|
|
60
|
+
const diff = run(['diff', '--name-only', `${base}..HEAD`]);
|
|
61
|
+
if (diff.status !== 0) {
|
|
62
|
+
return { ok: false, reason: 'exec-failed' };
|
|
63
|
+
}
|
|
64
|
+
const files = diff.stdout
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.filter((f) => !isExcluded(f))
|
|
69
|
+
// git argv/순서가 실행마다 digest를 흔들지 않도록 정렬.
|
|
70
|
+
.sort();
|
|
71
|
+
const sha = createHash('sha256').update(files.join('\n'), 'utf8').digest('hex');
|
|
72
|
+
return { ok: true, sha };
|
|
73
|
+
}
|
|
74
|
+
catch (_e) {
|
|
75
|
+
// spawnSync는 바이너리/cwd 누락 시 throw할 수 있음; 동일한 닫힌 집합으로 매핑.
|
|
76
|
+
return { ok: false, reason: 'exec-failed' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* explain.md `bouncer.comprehension`에서 BP 단일 엔트리(배열의 마지막).
|
|
81
|
+
* 0.7의 task별 다중 엔트리는 마이그레이션하지 않고 마지막만 본다 — 읽기 호환
|
|
82
|
+
* 규칙이 변환 함수를 대신한다. 구 단일 객체는 자동 변환하지 않는다.
|
|
83
|
+
* `quiz_score`·`disposition`은 옛 필드이므로 없어도 incomplete가 아니다.
|
|
84
|
+
* 절대 throw하지 않는다.
|
|
85
|
+
*
|
|
86
|
+
* @param {unknown} comprehension - explain frontmatter의 `bouncer.comprehension` 값
|
|
87
|
+
* @returns {{ ok: true, entry: { range_from: string, diff_sha: string } & Record<string, unknown> }
|
|
88
|
+
* | { ok: false, reason: 'not-a-list' | 'missing' | 'incomplete' }}
|
|
89
|
+
*/
|
|
90
|
+
function resolveComprehensionEntry(comprehension) {
|
|
91
|
+
try {
|
|
92
|
+
// 배열이 아니면(구 단일 객체 포함) 조회 자체가 성립하지 않는다.
|
|
93
|
+
if (!Array.isArray(comprehension)) {
|
|
94
|
+
return { ok: false, reason: 'not-a-list' };
|
|
95
|
+
}
|
|
96
|
+
if (comprehension.length === 0) {
|
|
97
|
+
return { ok: false, reason: 'missing' };
|
|
98
|
+
}
|
|
99
|
+
const entry = comprehension[comprehension.length - 1];
|
|
100
|
+
if (entry == null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
101
|
+
return { ok: false, reason: 'incomplete' };
|
|
102
|
+
}
|
|
103
|
+
const rec = entry;
|
|
104
|
+
const rangeFrom = typeof rec.range_from === 'string' ? rec.range_from : '';
|
|
105
|
+
const diffSha = typeof rec.diff_sha === 'string' ? rec.diff_sha : '';
|
|
106
|
+
// 빈 range_from·diff_sha는 scaffold 잔여와 같다 — hash mismatch가 아니라 기록 없음.
|
|
107
|
+
// 옛 quiz_score·disposition은 읽기만 하고 필수로 보지 않는다.
|
|
108
|
+
if (!rangeFrom.trim() || !diffSha.trim()) {
|
|
109
|
+
return { ok: false, reason: 'incomplete' };
|
|
110
|
+
}
|
|
111
|
+
// 원본 entry 객체를 그대로 돌려 추가 필드(task 등)를 보존한다.
|
|
112
|
+
// range_from/diff_sha는 위에서 문자열임을 확인했으므로 반환 타입만 좁힌다.
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
entry: entry,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (_e) {
|
|
119
|
+
return { ok: false, reason: 'not-a-list' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
module.exports = {
|
|
123
|
+
DIFF_EXCLUDED_PREFIXES,
|
|
124
|
+
EXPLAIN_SECTION_DEFS,
|
|
125
|
+
computeDiffSha,
|
|
126
|
+
resolveComprehensionEntry,
|
|
127
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const guards = require("./guards");
|
|
3
|
+
const { isRecord, isEnoentError } = guards;
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
// prepare·readyWave가 공유하는 동시 실행 기본값. config 파일·coordinator 키
|
|
7
|
+
// 부재와 같은 답을 내야 모든 호출처가 같은 폭으로 열린다.
|
|
8
|
+
const DEFAULT_MAX_PARALLEL = 2;
|
|
9
|
+
// `waves_per_session`의 "auto"·부재 상한과 설정 가능한 최댓값. 세션이 길수록 compaction
|
|
10
|
+
// 위험이 커지므로 상한을 8로 닫고, auto는 그 절반인 4에서 끊는다.
|
|
11
|
+
const AUTO_WAVES_PER_SESSION = 4;
|
|
12
|
+
// root 직접 실행의 기본 상한. 짧은 직렬 체인(세 task 이하)만 coordinator 기동 비용을 아낄
|
|
13
|
+
// 만큼 작다고 본다. 0으로 설정하면 직접 실행이 꺼진다.
|
|
14
|
+
const DEFAULT_DIRECT_MAX_COMMIT_TASKS = 3;
|
|
15
|
+
const MAX_WAVES_PER_SESSION = 8;
|
|
16
|
+
// 검증 실행은 shell:false argv만 허용한다. argv0 실행 파일명이 이 목록(또는
|
|
17
|
+
// 저장소 `verify_allowlist`)에 있어야 프로세스를 시작한다. 커스텀 바이너리는
|
|
18
|
+
// npm script로 감싸거나 저장소 allowlist에 명시한다.
|
|
19
|
+
//
|
|
20
|
+
// 기본 목록은 config 파일이 없을 때만 쓴다. 파손된 파일에 이 목록을 주면
|
|
21
|
+
// 운영자가 막은 명령을 plan/S12와 runtime이 서로 다른 답으로 통과시킨다.
|
|
22
|
+
const DEFAULT_VERIFY_ALLOWLIST = Object.freeze([
|
|
23
|
+
'npm',
|
|
24
|
+
'npx',
|
|
25
|
+
'node',
|
|
26
|
+
'pnpm',
|
|
27
|
+
'yarn',
|
|
28
|
+
'bun',
|
|
29
|
+
'deno',
|
|
30
|
+
'make',
|
|
31
|
+
'python',
|
|
32
|
+
'python3',
|
|
33
|
+
'pytest',
|
|
34
|
+
'go',
|
|
35
|
+
'cargo',
|
|
36
|
+
// /bin/true·false — 테스트 fixture와 no-op verify에 쓰는 표준 유틸.
|
|
37
|
+
// 셸 내장이 아니라 PATH 실행 파일이라 shell:false로도 동작한다.
|
|
38
|
+
'true',
|
|
39
|
+
'false',
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* 이미 파싱된 config 객체에서 허용 목록만 고른다. 키가 없거나 배열이 아니면
|
|
43
|
+
* 기본값을 쓴다 — 잘못된 형태를 빈 목록으로 접으면 모든 verify가 거절되어
|
|
44
|
+
* 기존 저장소의 execute가 한꺼번에 멈춘다. 명시적 `[]`는 그대로 두어
|
|
45
|
+
* 운영자가 전면 차단을 의도한 경우를 구분한다. 기본 목록과 합집합을
|
|
46
|
+
* 만들지 않는다. 파일 부재·파손 판정은 `readVerifyPolicy`가 맡는다.
|
|
47
|
+
*
|
|
48
|
+
* @param {unknown} [config] - `.bouncer/config.json` 파싱 결과
|
|
49
|
+
* @returns {readonly string[]} argv0 실행 파일명 허용 목록
|
|
50
|
+
*/
|
|
51
|
+
function getVerifyAllowlist(config = {}) {
|
|
52
|
+
if (!isRecord(config) || !Object.prototype.hasOwnProperty.call(config, 'verify_allowlist')) {
|
|
53
|
+
return DEFAULT_VERIFY_ALLOWLIST;
|
|
54
|
+
}
|
|
55
|
+
const raw = config.verify_allowlist;
|
|
56
|
+
if (!Array.isArray(raw)) {
|
|
57
|
+
return DEFAULT_VERIFY_ALLOWLIST;
|
|
58
|
+
}
|
|
59
|
+
// 문자열만 남긴다. 숫자·객체 항목은 무시해 basename 비교가 항상 문자열끼리만
|
|
60
|
+
// 이뤄지게 한다.
|
|
61
|
+
return raw.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `.bouncer/config.json` 단일 파서. 값의 모양은 검사하지 않는다 — cli·
|
|
65
|
+
* subagents·session-graph가 배열·원시값을 그대로 받아 왔고, 여기서 객체를
|
|
66
|
+
* 강제하면 그 세 곳의 동작이 조용히 바뀐다. 객체 여부는 init 호출 지점이 맡는다.
|
|
67
|
+
*
|
|
68
|
+
* missing은 ENOENT만: verification이 파일 없음과 깨진 JSON을 서로 다른 오류로
|
|
69
|
+
* 던지므로, 권한 오류·SyntaxError를 missing에 넣으면 VERIFY_CONFIG_MISSING으로
|
|
70
|
+
* 위장된다. graphify의 옛 readConfigSafe가 「파일 없음·깨진 JSON·권한 오류 모두
|
|
71
|
+
* config 없음과 같게」 삼키던 선택은 호출자가 readConfig() === null 로 흡수한다.
|
|
72
|
+
*
|
|
73
|
+
* {} 기본값·스키마·캐시는 넣지 않는다. 부재와 빈 설정을 같게 보려는 호출자만
|
|
74
|
+
* `readConfig(root) ?? {}` 로 받는다.
|
|
75
|
+
*/
|
|
76
|
+
function readConfigResult(repoRoot) {
|
|
77
|
+
const configPath = path.join(repoRoot, '.bouncer', 'config.json');
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// ENOENT만 "아직 없다". EACCES 등을 missing으로 합치면 verification이
|
|
84
|
+
// 권한 문제를 파일 부재로 안내한다.
|
|
85
|
+
if (isEnoentError(error)) {
|
|
86
|
+
return { ok: false, reason: 'missing' };
|
|
87
|
+
}
|
|
88
|
+
return { ok: false, reason: 'invalid' };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// JSON.parse의 선언 반환은 any라, 바로 객체로 쓰면 이후 모듈이 any를
|
|
92
|
+
// 전파한다. 형태 검사는 호출자 몫이므로 unknown으로만 고정한다.
|
|
93
|
+
const value = JSON.parse(raw);
|
|
94
|
+
return { ok: true, value };
|
|
95
|
+
}
|
|
96
|
+
catch (_e) {
|
|
97
|
+
// 파일이 있는 상태에서 깨진 것 — missing이 아니다.
|
|
98
|
+
return { ok: false, reason: 'invalid' };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function readConfig(repoRoot) {
|
|
102
|
+
const result = readConfigResult(repoRoot);
|
|
103
|
+
// 실패를 null로 접는다. {} 는 넣지 않는다 — 호출자가 부재와 빈 설정을
|
|
104
|
+
// 같게 볼지 정한다 (cli·subagents는 ?? {}, session-graph·graphify는 null).
|
|
105
|
+
return result.ok ? result.value : null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `repoRoot`의 검증 정책을 한 번 읽는다. S12·`readVerifyCommand`·
|
|
109
|
+
* `executeVerify`가 이 결과만 써야 같은 명령에 다른 판정을 내지 않는다.
|
|
110
|
+
* 파일이 없을 때만 기본 목록이다. 깨진 JSON·EACCES 같은 읽기 오류는
|
|
111
|
+
* invalid이며, 기본 목록으로 넓히면 파손된 설정이 통과된다.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} repoRoot - 저장소 루트 절대 경로
|
|
114
|
+
* @returns {VerifyPolicy} missing/present는 allowlist, invalid는 목록 없음
|
|
115
|
+
*/
|
|
116
|
+
function readVerifyPolicy(repoRoot) {
|
|
117
|
+
const parsed = readConfigResult(repoRoot);
|
|
118
|
+
if (parsed.ok === false) {
|
|
119
|
+
if (parsed.reason === 'missing') {
|
|
120
|
+
return { ok: true, reason: 'missing', allowlist: DEFAULT_VERIFY_ALLOWLIST };
|
|
121
|
+
}
|
|
122
|
+
return { ok: false, reason: 'invalid' };
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, reason: 'present', allowlist: getVerifyAllowlist(parsed.value) };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* integration checkout의 coordinator 동시 실행 정책을 읽는다. 파일·키 부재는
|
|
128
|
+
* 기본 한도(2)로 통과시켜 기존 저장소가 prepare 전에 설정을 추가하지 않아도
|
|
129
|
+
* 된다. `max_parallel`이 정수 1 미만·비정수면 invalid — prepare는 worktree를
|
|
130
|
+
* 만들기 전에 거절하고, 읽기 전용 소비자는 1로 폴백해 잘못된 설정이 병렬
|
|
131
|
+
* 폭을 넓히지 않게 한다.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} repoRoot - 정책을 읽을 checkout 루트(보통 integration)
|
|
134
|
+
* @returns {CoordinatorPolicy} 유효 한도 또는 invalid
|
|
135
|
+
*/
|
|
136
|
+
function readCoordinatorPolicy(repoRoot) {
|
|
137
|
+
const parsed = readConfigResult(repoRoot);
|
|
138
|
+
// 깨진 JSON·권한 오류도 한도를 넓히지 않는다. missing만 기본값이다.
|
|
139
|
+
if (parsed.ok === false) {
|
|
140
|
+
if (parsed.reason === 'missing') {
|
|
141
|
+
return { ok: true, maxParallel: DEFAULT_MAX_PARALLEL };
|
|
142
|
+
}
|
|
143
|
+
return { ok: false, reason: 'invalid' };
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(parsed.value)
|
|
146
|
+
|| !Object.prototype.hasOwnProperty.call(parsed.value, 'coordinator')) {
|
|
147
|
+
return { ok: true, maxParallel: DEFAULT_MAX_PARALLEL };
|
|
148
|
+
}
|
|
149
|
+
const coordinator = parsed.value.coordinator;
|
|
150
|
+
if (!isRecord(coordinator)
|
|
151
|
+
|| !Object.prototype.hasOwnProperty.call(coordinator, 'max_parallel')) {
|
|
152
|
+
return { ok: true, maxParallel: DEFAULT_MAX_PARALLEL };
|
|
153
|
+
}
|
|
154
|
+
const raw = coordinator.max_parallel;
|
|
155
|
+
// Number.isInteger는 문자열·null을 거절한다. 0·음수는 병렬 폭이 아니라
|
|
156
|
+
// "실행 없음"이라 prepare가 명시적으로 막아야 한다.
|
|
157
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
|
|
158
|
+
return { ok: false, reason: 'invalid' };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, maxParallel: raw };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* `worktree.setup`을 읽어 worktree 의존성 준비 끝에 실행할 명령을 돌려준다.
|
|
164
|
+
* 형태만 본다 — argv 파싱과 허용 목록 검사는 호출자(prepareDependencies)가
|
|
165
|
+
* command-argv로 한다. config.ts가 command-argv를 가져오면 순환하기 때문이다.
|
|
166
|
+
* `worktree` 키나 `setup` 키가 없으면 command:null(설정 안 함)이고, `worktree`가
|
|
167
|
+
* 객체가 아니거나 `setup`이 문자열이 아니면 invalid다. 값을 조용히 무시하면
|
|
168
|
+
* 운영자가 빌드 단계가 돌 거라 믿고 산출물 없는 worktree에서 검증하게 된다.
|
|
169
|
+
*
|
|
170
|
+
* @param {unknown} config - `.bouncer/config.json` 파싱 결과
|
|
171
|
+
* @returns {{ ok: true, command: string | null } | { ok: false, reason: 'invalid' }}
|
|
172
|
+
* command는 원문 문자열(빈 문자열 포함)이고 미설정이면 null
|
|
173
|
+
*/
|
|
174
|
+
function readWorktreeSetup(config) {
|
|
175
|
+
if (!isRecord(config) || !Object.prototype.hasOwnProperty.call(config, 'worktree')) {
|
|
176
|
+
return { ok: true, command: null };
|
|
177
|
+
}
|
|
178
|
+
const worktree = config.worktree;
|
|
179
|
+
if (!isRecord(worktree))
|
|
180
|
+
return { ok: false, reason: 'invalid' };
|
|
181
|
+
if (!Object.prototype.hasOwnProperty.call(worktree, 'setup')) {
|
|
182
|
+
return { ok: true, command: null };
|
|
183
|
+
}
|
|
184
|
+
if (typeof worktree.setup !== 'string')
|
|
185
|
+
return { ok: false, reason: 'invalid' };
|
|
186
|
+
return { ok: true, command: worktree.setup };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* `coordinator.waves_per_session`을 읽어 한 coordinator 세션이 처리할 wave 수 상한을
|
|
190
|
+
* 돌려준다. `"auto"`와 부재는 기본 상한(4)이고 정수 1–8은 그대로 쓴다. 그 밖의 값과
|
|
191
|
+
* 깨진 config는 invalid — prepare는 worktree를 만들기 전에 거절하고, 읽기 전용 소비자
|
|
192
|
+
* (`coordinate next`)는 상한 1로 접어 잘못된 설정이 세션을 늘리지 못하게 한다.
|
|
193
|
+
* `max_parallel`과 달리 파일 부재만 기본값이고, 깨진 파일은 상한을 넓히지 않는다.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} repoRoot - 정책을 읽을 checkout 루트(보통 integration)
|
|
196
|
+
* @returns {WavesPerSessionResult} 유효 상한 또는 invalid
|
|
197
|
+
*/
|
|
198
|
+
function readWavesPerSession(repoRoot) {
|
|
199
|
+
const parsed = readConfigResult(repoRoot);
|
|
200
|
+
if (parsed.ok === false) {
|
|
201
|
+
return parsed.reason === 'missing'
|
|
202
|
+
? { ok: true, cap: AUTO_WAVES_PER_SESSION }
|
|
203
|
+
: { ok: false, reason: 'invalid' };
|
|
204
|
+
}
|
|
205
|
+
if (!isRecord(parsed.value)
|
|
206
|
+
|| !Object.prototype.hasOwnProperty.call(parsed.value, 'coordinator')) {
|
|
207
|
+
return { ok: true, cap: AUTO_WAVES_PER_SESSION };
|
|
208
|
+
}
|
|
209
|
+
const coordinator = parsed.value.coordinator;
|
|
210
|
+
if (!isRecord(coordinator)
|
|
211
|
+
|| !Object.prototype.hasOwnProperty.call(coordinator, 'waves_per_session')) {
|
|
212
|
+
return { ok: true, cap: AUTO_WAVES_PER_SESSION };
|
|
213
|
+
}
|
|
214
|
+
const raw = coordinator.waves_per_session;
|
|
215
|
+
if (raw === 'auto')
|
|
216
|
+
return { ok: true, cap: AUTO_WAVES_PER_SESSION };
|
|
217
|
+
// 정수만 허용한다. 문자열 "3"이나 null은 의도가 모호해 invalid로 돌린다.
|
|
218
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw)
|
|
219
|
+
|| raw < 1 || raw > MAX_WAVES_PER_SESSION) {
|
|
220
|
+
return { ok: false, reason: 'invalid' };
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, cap: raw };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* `coordinator.direct_max_commit_tasks`를 읽어 root 직접 실행을 허용할 열린 commit task
|
|
226
|
+
* 수 상한을 돌려준다. 부재(파일·`coordinator`·키)는 기본 3이고 0은 직접 실행을 끈다.
|
|
227
|
+
* 정수가 아니거나 음수, 깨진 config는 0으로 접는다 — 잘못된 설정이 coordinator 없는
|
|
228
|
+
* 실행을 열어 주면 안 되고, 직접 실행은 선택 최적화라 꺼져도 드라이브는 그대로 돈다.
|
|
229
|
+
* (파일 부재만 기본값인 점은 `waves_per_session`과 같다.)
|
|
230
|
+
*
|
|
231
|
+
* @param {string} repoRoot - 설정을 읽을 checkout 루트(보통 main worktree)
|
|
232
|
+
* @returns {number} 0 이상의 정수 상한(0이면 꺼짐)
|
|
233
|
+
*/
|
|
234
|
+
function readDirectMaxCommitTasks(repoRoot) {
|
|
235
|
+
const parsed = readConfigResult(repoRoot);
|
|
236
|
+
if (parsed.ok === false) {
|
|
237
|
+
return parsed.reason === 'missing' ? DEFAULT_DIRECT_MAX_COMMIT_TASKS : 0;
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(parsed.value)
|
|
240
|
+
|| !Object.prototype.hasOwnProperty.call(parsed.value, 'coordinator')) {
|
|
241
|
+
return DEFAULT_DIRECT_MAX_COMMIT_TASKS;
|
|
242
|
+
}
|
|
243
|
+
const coordinator = parsed.value.coordinator;
|
|
244
|
+
if (!isRecord(coordinator)
|
|
245
|
+
|| !Object.prototype.hasOwnProperty.call(coordinator, 'direct_max_commit_tasks')) {
|
|
246
|
+
return DEFAULT_DIRECT_MAX_COMMIT_TASKS;
|
|
247
|
+
}
|
|
248
|
+
const raw = coordinator.direct_max_commit_tasks;
|
|
249
|
+
return typeof raw === 'number' && Number.isInteger(raw) && raw >= 0 ? raw : 0;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* `metrics.pricing`을 읽는다. 파일·`metrics`·`pricing` 키 부재는 빈 표(단가 없음)이고
|
|
253
|
+
* 비용은 `no-pricing`으로 남는다. 항목이 객체가 아니거나 단가가 음이 아닌 유한 수가
|
|
254
|
+
* 아니면, 일부만 믿어 비용을 틀리게 내는 것보다 표 전체를 invalid로 돌린다.
|
|
255
|
+
* 깨진 config 파일도 invalid다 — 단가를 알 수 없는 것을 "없음"으로 말하지 않는다.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} repoRoot - 설정을 읽을 checkout 루트(보통 main worktree)
|
|
258
|
+
* @returns {MetricsPricingResult} 단가 표 또는 invalid
|
|
259
|
+
*/
|
|
260
|
+
function readMetricsPricing(repoRoot) {
|
|
261
|
+
const parsed = readConfigResult(repoRoot);
|
|
262
|
+
if (parsed.ok === false) {
|
|
263
|
+
return parsed.reason === 'missing' ? { ok: true, pricing: {} } : { ok: false, reason: 'invalid' };
|
|
264
|
+
}
|
|
265
|
+
if (!isRecord(parsed.value) || !Object.prototype.hasOwnProperty.call(parsed.value, 'metrics')) {
|
|
266
|
+
return { ok: true, pricing: {} };
|
|
267
|
+
}
|
|
268
|
+
const metrics = parsed.value.metrics;
|
|
269
|
+
if (!isRecord(metrics))
|
|
270
|
+
return { ok: false, reason: 'invalid' };
|
|
271
|
+
if (!Object.prototype.hasOwnProperty.call(metrics, 'pricing'))
|
|
272
|
+
return { ok: true, pricing: {} };
|
|
273
|
+
const raw = metrics.pricing;
|
|
274
|
+
if (!isRecord(raw))
|
|
275
|
+
return { ok: false, reason: 'invalid' };
|
|
276
|
+
const pricing = {};
|
|
277
|
+
for (const [model, entry] of Object.entries(raw)) {
|
|
278
|
+
const valid = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
279
|
+
if (!isRecord(entry) || !valid(entry.input_per_mtok) || !valid(entry.output_per_mtok)) {
|
|
280
|
+
return { ok: false, reason: 'invalid' };
|
|
281
|
+
}
|
|
282
|
+
pricing[model] = { input_per_mtok: entry.input_per_mtok, output_per_mtok: entry.output_per_mtok };
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, pricing };
|
|
285
|
+
}
|
|
286
|
+
module.exports = {
|
|
287
|
+
readMetricsPricing,
|
|
288
|
+
readConfigResult,
|
|
289
|
+
readConfig,
|
|
290
|
+
readVerifyPolicy,
|
|
291
|
+
readCoordinatorPolicy,
|
|
292
|
+
readWorktreeSetup,
|
|
293
|
+
readWavesPerSession,
|
|
294
|
+
readDirectMaxCommitTasks,
|
|
295
|
+
DEFAULT_VERIFY_ALLOWLIST,
|
|
296
|
+
DEFAULT_MAX_PARALLEL,
|
|
297
|
+
getVerifyAllowlist,
|
|
298
|
+
};
|