@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,74 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const cliFlags = require("./cli-flags");
4
+ const { parseFlags } = cliFlags;
5
+ const commitHook = require("./commit-hook");
6
+ const { evaluateStaged, evaluateCommit } = commitHook;
7
+ const fs = require("node:fs");
8
+ const USAGE = `usage: bouncer commit-guard (--staged | --command-stdin) [--repo <dir>]
9
+
10
+ `;
11
+ /**
12
+ * 커밋 범위 검사 CLI. 두 모드 중 정확히 하나만 받는다.
13
+ * - `--staged`: git pre-commit 훅용. staged 인덱스를 판정한다.
14
+ * - `--command-stdin`: PreToolUse 훅용. stdin 전체를 명령 원문으로 읽어
15
+ * `evaluateCommit`으로 판정한다(명령 탐지·alias·FAIL_CLOSED 규칙 포함).
16
+ * 둘 다 있거나 둘 다 없으면 사용법 오류(exit 2)로 끝내 판정 예외(exit 1,
17
+ * 커밋 차단)와 구분한다.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string[]} opts.argv - `commit-guard` 뒤 인자
21
+ * @param {string} opts.cwd - `--repo`가 없을 때 쓸 저장소 루트
22
+ * @param {Function} [opts.evaluate] - 기본 `evaluateStaged`. 테스트 seam
23
+ * @param {Function} [opts.evaluateCommand] - 기본 `evaluateCommit`. 테스트 seam
24
+ * @param {Function} [opts.readInput] - 기본 stdin 전체 읽기. 테스트 seam
25
+ * @param {Function} [opts.stderr] - 차단 사유·사용법 싱크. 생략 시 stderr
26
+ * @returns {number} 0 허용, 1 차단 또는 내부 오류, 2 사용법
27
+ */
28
+ function runCommitGuard({ argv, cwd, evaluate, evaluateCommand, readInput, stderr, }) {
29
+ const err = stderr || ((s) => {
30
+ process.stderr.write(s);
31
+ });
32
+ const staged = argv.includes('--staged');
33
+ const commandStdin = argv.includes('--command-stdin');
34
+ // 상호 배타: 둘 다 주면 어느 판정을 원하는지 모호하므로 사용법 오류로 거절한다.
35
+ if (staged === commandStdin) {
36
+ err(USAGE);
37
+ return 2;
38
+ }
39
+ const flags = parseFlags(argv);
40
+ const repoRoot = typeof flags.repo === 'string' && flags.repo ? flags.repo : cwd;
41
+ const judge = evaluate || evaluateStaged;
42
+ let result;
43
+ try {
44
+ if (commandStdin) {
45
+ const judgeCommand = evaluateCommand || evaluateCommit;
46
+ const read = readInput || (() => fs.readFileSync(0, 'utf8'));
47
+ // stdin 읽기 실패도 이 try 안에서 internal error(exit 1)로 막는다.
48
+ result = judgeCommand({ command: read(), repoRoot });
49
+ }
50
+ else {
51
+ result = judge({ repoRoot });
52
+ }
53
+ }
54
+ catch (error) {
55
+ // evaluate가 던지는 모든 예외를 흡수한다. hook 안에서 uncaught로 죽으면
56
+ // git은 막히지만 정해진 진단 문자열이 아니라 스택이 나와 복구 힌트가 없다.
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ err(`commit-guard: internal error, blocking commit: ${message}\n`);
59
+ return 1;
60
+ }
61
+ if (result.block) {
62
+ err(`${result.reason || 'commit blocked'}\n`);
63
+ return 1;
64
+ }
65
+ return 0;
66
+ }
67
+ function cmdCommitGuard(rest, io) {
68
+ return runCommitGuard({ argv: rest, cwd: process.cwd(), stderr: io.err });
69
+ }
70
+ module.exports = {
71
+ runCommitGuard,
72
+ run: cmdCommitGuard,
73
+ usage: cliUsage.COMMAND_USAGE['commit-guard'],
74
+ };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const codexAgents = require("./codex-agents");
4
+ const { NAMED_AGENTS } = codexAgents;
5
+ const subagents = require("./subagents");
6
+ const { SUBAGENT_PROVIDERS, SUBAGENT_DISPATCH_VALUES } = subagents;
7
+ const USAGE = 'usage: bouncer config --help\n';
8
+ // 출력 한 줄이 max-len(120)을 넘어 템플릿 밖 상수로 뺀다. 출력 바이트는 그대로다.
9
+ const PROVIDER_DEFAULT = 'unset — CLAUDE_PLUGIN_ROOT selects claude, PLUGIN_ROOT selects codex; '
10
+ + 'cursor and antigravity must be set';
11
+ // provider·agent 이름은 상수에서만 채운다. docs/configuration.md 표와의 일치는
12
+ // test/config-help.test.js가 이 출력을 파싱해 확인하므로 줄 형식을 바꾸면 그 테스트도 맞춘다.
13
+ const HELP = `${USAGE}
14
+ Keys under "subagents" in .bouncer/config.json (read-only help; this command writes nothing):
15
+ subagents.provider ${SUBAGENT_PROVIDERS.join(' | ')}
16
+ default: ${PROVIDER_DEFAULT}
17
+ subagents.dispatch ${SUBAGENT_DISPATCH_VALUES.join(' | ')} | absent
18
+ default: absent (host Task subagents); read only when provider is cursor
19
+ subagents.<provider>.<agent> inherit | <host model slug>
20
+ default: inherit (parent session model; missing, empty, or non-string also inherit)
21
+ <agent>: ${NAMED_AGENTS.join(', ')}
22
+
23
+ Key under "metrics" (read by "bouncer metrics end"):
24
+ metrics.pricing { "<model slug>": { "input_per_mtok": <n>, "output_per_mtok": <n> } } | absent
25
+ default: absent (cost is recorded as null with reason no-pricing)
26
+ `;
27
+ /**
28
+ * `bouncer config` 핸들러. 인자가 정확히 `--help` 또는 `-h` 하나일 때만
29
+ * 도움말을 stdout에 쓰고, 그 외(인자 없음·다른 토큰)는 usage를 stderr에 쓴다.
30
+ * 파일을 읽거나 쓰지 않는다.
31
+ *
32
+ * @param {string[]} rest - `config` 뒤 원시 argv
33
+ * @param {CliIo} io - stdout/stderr 싱크
34
+ * @returns {number} 도움말 0, 그 외 사용법 거절 2
35
+ */
36
+ function cmdConfig(rest, io) {
37
+ if (rest.length === 1 && (rest[0] === '--help' || rest[0] === '-h')) {
38
+ io.out(HELP);
39
+ return 0;
40
+ }
41
+ io.err(USAGE);
42
+ return 2;
43
+ }
44
+ module.exports = {
45
+ run: cmdConfig,
46
+ usage: cliUsage.COMMAND_USAGE['config'],
47
+ };
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const cliFlags = require("./cli-flags");
4
+ const { parseFlags, argvRequestsHelp } = cliFlags;
5
+ // validate-sections는 의도적으로 가벼운 모듈이라 argv 검증 전에 context-review-state를
6
+ // 적재하지 않는 현재 순서를 깨지 않고 도움말 열거값을 검증기 상수에서 만들 수 있다.
7
+ const validateSections = require("./validate-sections");
8
+ const { REVIEW_SEVERITY, CONTEXT_REVIEW_STATUS, REPORT_FINDING_STATUS, FINDING_ACTIONABILITY, FINDING_ORIGIN, CONTEXT_ROUND_MODES, CONTEXT_REPORT_STATUS, CONTEXT_REVIEW_PERSPECTIVE, findingFingerprint, } = validateSections;
9
+ const ALLOWED_STATUS = CONTEXT_REPORT_STATUS;
10
+ const USAGE_BLOCK = ' context-review <start|record|close|restart|route> --blueprint <dir>\n'
11
+ + ' Track plan context-review cycles; limit full restarts to one.\n';
12
+ const USAGE = 'usage: bouncer context-review start --blueprint <dir> --dispatch <json>\n'
13
+ + ' bouncer context-review record --blueprint <dir> --dispatch <json> --round <json> '
14
+ + '[--status <requested|addressed|accepted>] [--accept-risk <id[,id...]>] [--repo <dir>]\n'
15
+ + ' bouncer context-review close --blueprint <dir> --dispositions <json> '
16
+ + '[--accept-risk <id[,id...]>] [--repo <dir>]\n'
17
+ + ' bouncer context-review restart --blueprint <dir> --dispatch <json> --reason <text> [--repo <dir>]\n'
18
+ + ' bouncer context-review route --blueprint <dir> --dispatch <json> [--repo <dir>]\n'
19
+ + ' (each accepts --repo <dir>)\n'
20
+ + '\n'
21
+ + USAGE_BLOCK;
22
+ // 예시 finding의 구성 요소. fingerprint는 검증기와 같은 함수로 계산해 손으로 쓴 값이
23
+ // 공식과 어긋나는 일을 없앤다(cli-review-dispatch-command의 예시와 같은 방식).
24
+ const EXAMPLE_FILE = '.bouncer/context/epics/014-auth/blueprints/001-signup/tasks/001/tasks.md';
25
+ const EXAMPLE_FINDINGS = [
26
+ {
27
+ id: 'CR-1', category: 'scope', brief_clause: 'tasks/001 touch', symbol: 'touch',
28
+ severity: 'major', actionability: 'must_fix',
29
+ },
30
+ {
31
+ id: 'CR-2', category: 'scope', brief_clause: 'tasks/001 wording', symbol: 'wording',
32
+ severity: 'minor', actionability: 'advisory',
33
+ },
34
+ ].map((f) => ({
35
+ id: f.id,
36
+ severity: f.severity,
37
+ status: 'open',
38
+ category: f.category,
39
+ brief_clause: f.brief_clause,
40
+ file: EXAMPLE_FILE,
41
+ symbol: f.symbol,
42
+ fingerprint: findingFingerprint({ ...f, file: EXAMPLE_FILE }, 'context'),
43
+ actionability: f.actionability,
44
+ origin: 'discovery',
45
+ first_seen_round: 1,
46
+ last_seen_round: 1,
47
+ }));
48
+ /**
49
+ * 도움말 예시 round 보고 하나를 JSON 문자열로 만든다. 바꿀 자리는 `<target.digest>`
50
+ * 하나뿐이며 `review-dispatch plan` 출력의 target.digest를 그대로 넣는다.
51
+ *
52
+ * @param {number} round - round 번호(discovery는 1, delta는 2)
53
+ * @param {string} mode - round mode
54
+ * @param {object[]} findings - 이 round가 보고하는 finding 목록
55
+ * @returns {string} 파일 하나로 복사할 수 있는 들여쓴 JSON
56
+ */
57
+ function exampleRound(round, mode, findings) {
58
+ return JSON.stringify({
59
+ round: {
60
+ round,
61
+ mode,
62
+ target: { digest: '<target.digest>' },
63
+ perspectives: [{ name: 'combined', target_digest: '<target.digest>' }],
64
+ severity_changes: [],
65
+ },
66
+ findings,
67
+ }, null, 2);
68
+ }
69
+ const EXAMPLE_DISPOSITIONS = JSON.stringify([
70
+ { id: 'CR-1', status: 'resolved' },
71
+ { id: 'CR-2', status: 'accepted', note: 'Wording only; the clause is still unambiguous.' },
72
+ ], null, 2);
73
+ const HELP = `${USAGE}
74
+ --dispatch is the JSON printed by \`review-dispatch plan --blueprint <dir>\`.
75
+ --round is { "round": object, "findings": object[] } (discovery, then delta).
76
+ --dispositions is a JSON array of [{ id, status, note }] to dispose folded findings.
77
+
78
+ Each example below is a complete file. Replace only <target.digest> with the
79
+ target.digest from the \`review-dispatch plan\` JSON.
80
+
81
+ --round, discovery (round 1):
82
+ \`\`\`json
83
+ ${exampleRound(1, 'discovery', EXAMPLE_FINDINGS)}
84
+ \`\`\`
85
+
86
+ --round, delta (round 2; findings may be [] when nothing new is reported):
87
+ \`\`\`json
88
+ ${exampleRound(2, 'delta', [])}
89
+ \`\`\`
90
+
91
+ --dispositions (close; one entry per finding still open or to be disposed):
92
+ \`\`\`json
93
+ ${EXAMPLE_DISPOSITIONS}
94
+ \`\`\`
95
+
96
+ Allowed values:
97
+ round.round: 1 for discovery, 2 for delta (record in that order)
98
+ round.mode: ${CONTEXT_ROUND_MODES.join('|')}
99
+ finding severity: ${REVIEW_SEVERITY.join('|')}
100
+ finding status (--round): ${REPORT_FINDING_STATUS.join('|')}
101
+ finding actionability: ${FINDING_ACTIONABILITY.join('|')}
102
+ finding origin: ${FINDING_ORIGIN.join('|')}
103
+ perspectives[].name (also finding category): ${CONTEXT_REVIEW_PERSPECTIVE.join('|')}
104
+ perspectives dispatch (review_contract brief): discovery round lists every dispatch perspective, e.g.
105
+ perspectives: [{ name: consistency, target_digest: <target.digest> }, ...];
106
+ delta lists combined (full) or verifiability (light)
107
+ disposition status (--dispositions): ${CONTEXT_REVIEW_STATUS.join('|')} (accepted requires note)
108
+ --status: ${ALLOWED_STATUS.join('|')}
109
+
110
+ --status accepted or close with dispositions closes the cycle: every finding must be resolved or accepted
111
+ with a note; a must_fix accepted needs --accept-risk <id> and explicit user consent.
112
+ When it closes the cycle, close and record --status accepted also write <blueprint>/context-review.md
113
+ (status accepted, rounds, findings, and the ## Findings list) and return its path as written; a rerun
114
+ of the same input rewrites it. A missing context-review.md is rejected: bouncer scaffold context-review.
115
+
116
+ Exit: 0 success or help, 1 rejected transition, 2 usage error.
117
+ Failure JSON: { ok: false, reason, cause, next }. reason is one of
118
+ context-review-restart-limit | context-review-state-conflict |
119
+ context-review-unresolved-findings | context-review-target-invalid |
120
+ context-review-legacy-evidence | context-review-round-order |
121
+ context-review-input-invalid
122
+ A recorded success is not a plan gate pass.
123
+ record success may carry warnings: [{ code: 'finding-merge-candidate', file, symbol, ids }] when findings
124
+ of that round share a file and symbol under different categories; the key is omitted when there is none
125
+ (and on a noop rerun). A warning never blocks the record; the controller decides whether to merge them.
126
+ record rejects (context-review-input-invalid) a round whose merged findings share a fingerprint under different
127
+ ids, at any --status and before writing anything; merge them into one finding and record the same round again.
128
+ record overwrites each finding's fingerprint with the context formula and, when the id key is omitted,
129
+ assigns CR-<first 8 hex of sha256(fingerprint)> (stable across rounds; dispose it by that id in close).
130
+ An explicit id must be a non-empty string; null, "", blank or non-string ids and a generated id taken by a
131
+ different fingerprint are context-review-input-invalid. record success (and its noop rerun) carries
132
+ finding_ids: this round's normalized ids in input order without duplicates. Stored report bytes stay as
133
+ given; fold, close and noop projections apply the formula, so an earlier wrong fingerprint is repaired.
134
+
135
+ route is read-only: it reads follow_up from the --dispatch payload (a
136
+ review-dispatch plan run with --previous) and prints { ok, next_action, reason,
137
+ cause, next, cycle } with next_action reuse | close | delta | restart | user. It never
138
+ writes state; a payload without follow_up is context-review-input-invalid.
139
+ close means the structure and the Interface/Touch contract equal the reviewed baseline and only
140
+ the body changed: run close to certify the current snapshot (no new round). delta is a contract-only
141
+ change after discovery; restart is a structure change. close and an accepted record write
142
+ closure_target_digest (64 lowercase hex) to the runtime closure and the context-review.md frontmatter;
143
+ a closed cycle may be closed again after a body-only edit, and close refuses a changed structure or contract.
144
+ `;
145
+ /**
146
+ * argv를 검증한다. 필수 플래그 누락·중복·허용 밖 값은 상태 파일을 열기 전에
147
+ * 거절해 exit 2(사용법)와 exit 1(전이 거절)을 가른다. 중복 플래그는 parseFlags가
148
+ * 마지막 값만 남겨 다른 대상에 기록하게 되므로 덮이기 전에 센다.
149
+ *
150
+ * @param {string[]} rest - `context-review` 뒤 argv
151
+ * @returns {Parsed} 성공 필드 또는 `{error}`
152
+ */
153
+ function parseArgs(rest) {
154
+ const fail = (message) => ({ error: `context-review: ${message}\n${USAGE}` });
155
+ const verb = rest[0];
156
+ if (verb !== 'start' && verb !== 'record' && verb !== 'close' && verb !== 'restart' && verb !== 'route') {
157
+ return fail('command must be start, record, close, restart or route');
158
+ }
159
+ const flags = rest.slice(1);
160
+ for (const name of ['blueprint', 'dispatch', 'round', 'dispositions', 'status', 'accept-risk', 'reason', 'repo']) {
161
+ if (flags.filter((t) => t === `--${name}`).length > 1)
162
+ return fail(`--${name} specified more than once`);
163
+ }
164
+ const f = parseFlags(flags);
165
+ const text = (name) => {
166
+ const v = f[name];
167
+ return typeof v === 'string' && v !== '' ? v : null;
168
+ };
169
+ const blueprint = text('blueprint');
170
+ if (!blueprint)
171
+ return fail('--blueprint is required');
172
+ const dispatch = text('dispatch');
173
+ const dispositions = text('dispositions');
174
+ const round = text('round');
175
+ const reason = text('reason');
176
+ if (verb === 'close') {
177
+ if (f.dispatch !== undefined)
178
+ return fail('--dispatch is not valid for close');
179
+ if (!dispositions)
180
+ return fail('--dispositions <json-file> is required');
181
+ }
182
+ else {
183
+ if (f.dispositions !== undefined)
184
+ return fail('--dispositions is only valid for close');
185
+ if (!dispatch)
186
+ return fail('--dispatch <json-file> is required');
187
+ }
188
+ if (verb === 'record' && !round)
189
+ return fail('--round <json-file> is required');
190
+ if (verb === 'restart' && !reason)
191
+ return fail('--reason is required');
192
+ if (verb !== 'record' && (round || f.status !== undefined)) {
193
+ return fail('--round and --status are only valid for record');
194
+ }
195
+ if (verb !== 'record' && verb !== 'close' && f['accept-risk'] !== undefined) {
196
+ return fail('--accept-risk is only valid for record and close');
197
+ }
198
+ if (verb !== 'restart' && f.reason !== undefined)
199
+ return fail('--reason is only valid for restart');
200
+ const status = text('status');
201
+ if (f.status !== undefined && (status === null || !ALLOWED_STATUS.includes(status))) {
202
+ return fail('--status must be requested, addressed, or accepted');
203
+ }
204
+ if (f['accept-risk'] !== undefined && text('accept-risk') === null) {
205
+ return fail('--accept-risk requires finding ids');
206
+ }
207
+ if (f.repo !== undefined && text('repo') === null)
208
+ return fail('--repo requires a directory');
209
+ return {
210
+ verb,
211
+ blueprint,
212
+ dispatch,
213
+ round,
214
+ dispositions,
215
+ status,
216
+ acceptRisk: (text('accept-risk') || '').split(',').map((s) => s.trim()).filter(Boolean),
217
+ reason,
218
+ repo: text('repo') || undefined,
219
+ };
220
+ }
221
+ /**
222
+ * 공개 `context-review` 핸들러. 도움말은 상태 모듈을 열기 전에 끝내고, 그 외는
223
+ * argv 검증과 상태 전이 호출·JSON 출력·exit만 연결한다.
224
+ *
225
+ * @param {string[]} rest - 동사와 플래그
226
+ * @param {CliIo} io - stdout/stderr 싱크
227
+ * @returns {number} 성공 0, 전이 거절 1, 사용법 2
228
+ */
229
+ function cmdContextReview(rest, io) {
230
+ if (argvRequestsHelp(rest)) {
231
+ io.out(HELP);
232
+ return 0;
233
+ }
234
+ const parsed = parseArgs(rest);
235
+ if ('error' in parsed) {
236
+ io.err(parsed.error);
237
+ return 2;
238
+ }
239
+ // argv를 통과한 뒤에만 상태 모듈을 적재한다. 사용법 거절이 파일 I/O 경로를 열지 않게 한다.
240
+ const state = require('./context-review-state');
241
+ const repoRoot = parsed.repo ? parsed.repo : process.cwd();
242
+ let result;
243
+ if (parsed.verb === 'start') {
244
+ result = state.startCycle({ repoRoot, blueprintDir: parsed.blueprint, dispatchFile: parsed.dispatch });
245
+ }
246
+ else if (parsed.verb === 'record') {
247
+ result = state.recordReport({
248
+ repoRoot, blueprintDir: parsed.blueprint, dispatchFile: parsed.dispatch,
249
+ roundFile: parsed.round, status: parsed.status, acceptRisk: parsed.acceptRisk,
250
+ });
251
+ }
252
+ else if (parsed.verb === 'close') {
253
+ result = state.closeCycle({
254
+ repoRoot, blueprintDir: parsed.blueprint, dispositionsFile: parsed.dispositions,
255
+ acceptRisk: parsed.acceptRisk,
256
+ });
257
+ }
258
+ else if (parsed.verb === 'route') {
259
+ result = state.routeFollowUp({ repoRoot, blueprintDir: parsed.blueprint, dispatchFile: parsed.dispatch });
260
+ }
261
+ else {
262
+ result = state.restartCycle({
263
+ repoRoot, blueprintDir: parsed.blueprint, dispatchFile: parsed.dispatch,
264
+ reason: parsed.reason,
265
+ });
266
+ }
267
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
268
+ return result.ok ? 0 : 1;
269
+ }
270
+ module.exports = {
271
+ run: cmdContextReview,
272
+ usage: cliUsage.COMMAND_USAGE['context-review'],
273
+ };