@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,138 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const cliFlags = require("./cli-flags");
6
+ const { parseFlags } = cliFlags;
7
+ const retroSources = require("./retro-sources");
8
+ const { listRetroSources, defaultRetroRoots } = retroSources;
9
+ const retroRead = require("./retro-read");
10
+ const { readTranscript } = retroRead;
11
+ const runtimeState = require("./runtime-state");
12
+ const { runtimePaths } = runtimeState;
13
+ const USAGE = 'usage: bouncer retro sources [--repo <dir>] [--limit <1..200>] '
14
+ + '[--claude-root <dir>] [--codex-root <dir>] [--cursor-root <dir>]\n'
15
+ + ' bouncer retro read --path <file> [--max-chars <n>]\n';
16
+ const KNOWN_FLAGS = ['repo', 'limit', 'claude-root', 'codex-root', 'cursor-root'];
17
+ const DEFAULT_LIMIT = 20;
18
+ const READ_FLAGS = ['path', 'max-chars'];
19
+ const DEFAULT_MAX_CHARS = 200000;
20
+ const UNREADABLE_MESSAGE = 'transcript 파일을 읽을 수 없습니다.';
21
+ const UNSUPPORTED_MESSAGE = '지원하지 않는 transcript 형식이거나 읽을 메시지가 없습니다.';
22
+ /**
23
+ * `retro read` 하위 명령. 한 transcript를 정규화해 JSON으로 내보내며 파일을 쓰지 않는다.
24
+ *
25
+ * @param {string[]} rest - `read` 뒤 원시 argv
26
+ * @param {CliIo} io - stdout/stderr 싱크
27
+ * @returns {number} 성공 0, 읽기 실패(unreadable·unsupported-schema) 1, 잘못된 사용 2
28
+ */
29
+ function cmdRetroRead(rest, io) {
30
+ const flags = parseFlags(rest);
31
+ // 모르는 플래그나 값 없는 플래그는 오타일 가능성이 높아 읽기 전에 거절한다.
32
+ if (Object.keys(flags).some((k) => !READ_FLAGS.includes(k))
33
+ || READ_FLAGS.some((k) => flags[k] === true)
34
+ || flags.path === undefined) {
35
+ io.err(USAGE);
36
+ return 2;
37
+ }
38
+ let maxChars = DEFAULT_MAX_CHARS;
39
+ if (flags['max-chars'] !== undefined) {
40
+ const raw = flags['max-chars'];
41
+ maxChars = /^\d+$/.test(raw) ? Number(raw) : NaN;
42
+ if (!Number.isSafeInteger(maxChars) || maxChars < 1) {
43
+ io.err(USAGE);
44
+ return 2;
45
+ }
46
+ }
47
+ const result = readTranscript(path.resolve(flags.path), { maxChars });
48
+ if (!result.ok) {
49
+ io.out(`${JSON.stringify({
50
+ ok: false,
51
+ reason: result.reason,
52
+ error: result.reason === 'unreadable' ? UNREADABLE_MESSAGE : UNSUPPORTED_MESSAGE,
53
+ }, null, 2)}\n`);
54
+ return 1;
55
+ }
56
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
57
+ return 0;
58
+ }
59
+ /**
60
+ * `bouncer project-root`와 같은 resolver로 project root를 구한다. linked
61
+ * worktree에서도 main worktree를 정본으로 삼기 위해 같은 함수를 쓴다.
62
+ *
63
+ * @param {string} cwd - 조회 시작 디렉터리
64
+ * @returns {string} project root 절대 경로. 못 정하면 throw
65
+ */
66
+ function defaultResolveProjectRoot(cwd) {
67
+ const paths = runtimePaths({ repoRoot: cwd });
68
+ if (paths.unavailable || !paths.projectRoot) {
69
+ throw new Error(paths.reason || 'Bouncer requires a Git repository');
70
+ }
71
+ return paths.projectRoot;
72
+ }
73
+ /**
74
+ * `bouncer retro` 핸들러. `sources`는 transcript 본문을 읽지 않고, `read`는
75
+ * transcript 하나를 정규화한다. 잘못된 사용은 throw 없이 stderr에 usage를 쓰고 2를 돌려준다.
76
+ *
77
+ * @param {string[]} rest - `retro` 뒤 원시 argv
78
+ * @param {CliIo} io - stdout/stderr 싱크
79
+ * @param {RetroDeps} [deps] - 테스트용 주입(project root resolver, home, platform, env)
80
+ * @returns {number} 성공 0, project root를 못 정한 운영 실패 1, 잘못된 사용 2
81
+ */
82
+ function cmdRetro(rest, io, deps = {}) {
83
+ if (rest[0] === 'read')
84
+ return cmdRetroRead(rest.slice(1), io);
85
+ if (rest[0] !== 'sources') {
86
+ io.err(USAGE);
87
+ return 2;
88
+ }
89
+ const flags = parseFlags(rest.slice(1));
90
+ // 모르는 플래그를 조용히 무시하면 오타(--clude-root)가 실제 home 탐색으로 번진다.
91
+ if (Object.keys(flags).some((k) => !KNOWN_FLAGS.includes(k))) {
92
+ io.err(USAGE);
93
+ return 2;
94
+ }
95
+ // 값이 필요한 플래그가 값 없이 오면 parseFlags는 true를 돌려준다.
96
+ if (KNOWN_FLAGS.some((k) => flags[k] === true)) {
97
+ io.err(USAGE);
98
+ return 2;
99
+ }
100
+ let limit = DEFAULT_LIMIT;
101
+ if (flags.limit !== undefined) {
102
+ const raw = flags.limit;
103
+ limit = /^\d+$/.test(raw) ? Number(raw) : NaN;
104
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
105
+ io.err(USAGE);
106
+ return 2;
107
+ }
108
+ }
109
+ let projectRoot;
110
+ try {
111
+ projectRoot = flags.repo
112
+ ? path.resolve(flags.repo)
113
+ : (deps.resolveProjectRoot ?? defaultResolveProjectRoot)(process.cwd());
114
+ }
115
+ catch (err) {
116
+ // resolver 실패는 모두 "project root를 정하지 못함" 한 가지 운영 실패다.
117
+ // 사유 문자열은 resolver가 준 메시지를 그대로 실어 원인을 보존한다.
118
+ io.out(`${JSON.stringify({
119
+ ok: false,
120
+ error: `project root를 정하지 못했습니다: ${err.message}`,
121
+ }, null, 2)}\n`);
122
+ return 1;
123
+ }
124
+ const defaults = defaultRetroRoots({
125
+ homeDir: deps.homeDir ?? os.homedir(),
126
+ platform: deps.platform ?? process.platform,
127
+ env: deps.env ?? process.env,
128
+ });
129
+ const roots = {
130
+ claude: flags['claude-root'] ?? defaults.claude,
131
+ codex: flags['codex-root'] ?? defaults.codex,
132
+ cursor: flags['cursor-root'] ?? defaults.cursor,
133
+ };
134
+ const result = listRetroSources({ projectRoot, roots, limit });
135
+ io.out(`${JSON.stringify({ ok: true, ...result }, null, 2)}\n`);
136
+ return 0;
137
+ }
138
+ module.exports = { run: cmdRetro, usage: cliUsage.COMMAND_USAGE['retro'] };
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ const cliFlags = require("./cli-flags");
4
+ const { parseFlags, argvRequestsHelp } = cliFlags;
5
+ const paths = require("./paths");
6
+ const { isNumericContextId } = paths;
7
+ const ALLOWED_STATUS = ['requested', 'addressed', 'accepted'];
8
+ const USAGE_BLOCK = ' review record --blueprint <dir> [--task <ddd>] --round <json-file> '
9
+ + '[--status <requested|addressed|accepted>]\n'
10
+ + ' Record one review round and finding updates (JSON).\n'
11
+ + ' review accept-risk --blueprint <dir> --finding <id> --note <text> --yes\n'
12
+ + ' Close one open blueprint final-review finding as a user-accepted risk.\n';
13
+ const USAGE = 'usage: bouncer review record --blueprint <dir> [--task <ddd>] '
14
+ + '--round <json-file> [--status <requested|addressed|accepted>] [--repo <dir>]\n'
15
+ + '\n'
16
+ + USAGE_BLOCK;
17
+ /**
18
+ * `review record --help`에 실리는 예시 round JSON.
19
+ * blueprint 모드에서 두 맵을 round 최상위에 두고 target 안에 넣지 않게 보여
20
+ * 준다. 검증기를 통과하는 discovery round 1이며 펜스는 하나다. aaa/bbb와
21
+ * `<snapshot_id>`는 placeholder이고 실제 값은 review-dispatch execute 출력에서 옮긴다.
22
+ */
23
+ const HELP_ROUND_EXAMPLE = `{
24
+ "round": {
25
+ "round": 1,
26
+ "mode": "discovery",
27
+ "target": { "base": "aaa", "head": "bbb", "snapshot_id": "<snapshot_id>" },
28
+ "perspectives": [{ "name": "combined", "target_head": "bbb" }],
29
+ "task_brief_hashes": { "TASKS-001": "<hash>" },
30
+ "intent_bundles": { "TASKS-001": { "id": "...", "revision": 1 } },
31
+ "previous_finding_ids": [],
32
+ "new": 1,
33
+ "resolved": 0,
34
+ "regressed": 0
35
+ },
36
+ "findings": [
37
+ {
38
+ "id": "F1",
39
+ "severity": "major",
40
+ "status": "open",
41
+ "category": "correctness",
42
+ "brief_clause": "tasks/001 Interface",
43
+ "file": "scripts/lib/x.js",
44
+ "symbol": "f",
45
+ "fingerprint": "correctness:tasks/001 interface:scripts/lib/x.js#f",
46
+ "actionability": "must_fix",
47
+ "origin": "discovery",
48
+ "first_seen_round": 1,
49
+ "last_seen_round": 1
50
+ }
51
+ ]
52
+ }`;
53
+ /**
54
+ * `review record --help` stdout 본문.
55
+ * 예시 JSON 앞에 blueprint 모드(`--task` 생략)가 두 맵을 싣는다는 한 줄을
56
+ * 두어 coordinator가 review-record 소스를 열지 않게 한다.
57
+ */
58
+ const HELP = `${USAGE}
59
+ --round file format: a JSON object { "round": object, "findings": array }.
60
+ Blueprint review (omit --task) puts task_brief_hashes and intent_bundles on round.
61
+ Every new round needs round.target.snapshot_id (64 lowercase hex) from review-dispatch execute,
62
+ with target.base and target.head copied from the same output. The recorder rejects it with
63
+ review-snapshot-required|invalid|mismatch when missing, unknown, corrupt or for another scope.
64
+ With --status accepted (or an inherited accepted status) it also rejects review-snapshot-stale
65
+ when the worktree no longer matches the snapshot; dispatch a new review on the current state.
66
+
67
+ \`\`\`json
68
+ ${HELP_ROUND_EXAMPLE}
69
+ \`\`\`
70
+
71
+ Enums:
72
+ severity: blocker|major|minor|nit
73
+ finding status: resolved|accepted|deferred|open (accepted and deferred require note;\
74
+ open is rejected when --status accepted)
75
+ actionability: must_fix|advisory
76
+ origin: discovery|introduced_by_revision|missed_critical
77
+ round mode: discovery|delta|critical_recovery
78
+ perspective name: combined|spec_scope|correctness_tests|minimality_maintainability|security
79
+ --status: requested|addressed|accepted
80
+
81
+ Success is { ok: true, path, round, status, findings, finding_ids } (exit 0). finding_ids lists this
82
+ round's normalized finding ids in input order without duplicates. When findings recorded in this round
83
+ share a file and symbol under different categories it also carries warnings: [{ code:
84
+ 'finding-merge-candidate', file, symbol, ids }] (ids in input order); the key is omitted when there is
85
+ none. A warning never blocks the record; the controller decides whether to merge those findings.
86
+
87
+ fingerprint formula: lower(category):lower(brief_clause):posix(file)#symbol
88
+ The recorder always overwrites a supplied fingerprint with this formula. Omit the id key to get
89
+ F-<first 8 hex of sha256(fingerprint)>; the same fingerprint gets the same id in every round. An
90
+ explicit id must be a non-empty string (null, "", blank or non-string is review-input-invalid), and an
91
+ omitted id whose generated id is taken by a different fingerprint is rejected as review-input-invalid.
92
+
93
+ Repair wave example (open in discovery, resolved in delta): see the review and
94
+ final_review coordinator cards, section "Finding status".
95
+ Full ledger example: references/spec-authoring/review-rounds.md
96
+ `;
97
+ /**
98
+ * `--flag` 토큰 횟수. parseFlags는 마지막 값만 남겨 중복 --blueprint를 덮으므로
99
+ * 덮기 전에 거절해야 다른 문서에 기록하지 않는다.
100
+ *
101
+ * @param {string[]} rest - 플래그 argv
102
+ * @param {string} name - `--` 없는 플래그 이름
103
+ * @returns {number} 등장 횟수
104
+ */
105
+ function countFlag(rest, name) {
106
+ let n = 0;
107
+ for (const tok of rest) {
108
+ if (tok === `--${name}`)
109
+ n += 1;
110
+ }
111
+ return n;
112
+ }
113
+ /**
114
+ * `review record` argv. record 밖의 동사와 필수 플래그 누락·중복·허용 밖
115
+ * status는 resolver를 열기 전에 거절해 exit 2와 원장 실패(exit 1)를 가른다.
116
+ *
117
+ * @param {string[]} rest - `review` 뒤 argv
118
+ * @returns {ParsedRecord} 성공 필드 또는 error
119
+ */
120
+ function parseReviewRecordArgs(rest) {
121
+ const fail = (message) => ({
122
+ error: `review record: ${message}\n${USAGE}`,
123
+ blueprint: null,
124
+ task: null,
125
+ round: null,
126
+ status: null,
127
+ });
128
+ if (rest[0] !== 'record') {
129
+ return fail('command must be record or accept-risk');
130
+ }
131
+ const flags = rest.slice(1);
132
+ for (const name of ['blueprint', 'round', 'task', 'status', 'repo']) {
133
+ if (countFlag(flags, name) > 1) {
134
+ return fail(`--${name} specified more than once`);
135
+ }
136
+ }
137
+ const f = parseFlags(flags);
138
+ if (typeof f.blueprint !== 'string' || f.blueprint === '') {
139
+ return fail('--blueprint is required');
140
+ }
141
+ if (typeof f.round !== 'string' || f.round === '') {
142
+ return fail('--round <json-file> is required');
143
+ }
144
+ if (Object.prototype.hasOwnProperty.call(f, 'task')) {
145
+ if (typeof f.task !== 'string' || !isNumericContextId(f.task)) {
146
+ return fail('--task must be a three-digit id');
147
+ }
148
+ }
149
+ if (Object.prototype.hasOwnProperty.call(f, 'status')) {
150
+ if (typeof f.status !== 'string' || !ALLOWED_STATUS.includes(f.status)) {
151
+ return fail('--status must be requested, addressed, or accepted');
152
+ }
153
+ }
154
+ if (f.repo !== undefined && (typeof f.repo !== 'string' || f.repo === '')) {
155
+ return fail('--repo requires a directory');
156
+ }
157
+ return {
158
+ blueprint: f.blueprint,
159
+ task: typeof f.task === 'string' ? f.task : null,
160
+ round: f.round,
161
+ status: typeof f.status === 'string' ? f.status : null,
162
+ repo: typeof f.repo === 'string' ? f.repo : undefined,
163
+ };
164
+ }
165
+ /**
166
+ * `review accept-risk` argv. 필수 플래그 누락·중복과 --task는 기록 모듈을 열기 전에
167
+ * 거절해 사용법 오류(exit 2)와 원장 거절(exit 1)을 가른다. --yes 누락은 사용법이 아니라
168
+ * 승인 경계이므로 여기서 통과시켜 기록 모듈이 confirmation-required로 거절한다.
169
+ *
170
+ * @param {string[]} flags - `accept-risk` 뒤 argv
171
+ * @returns {ParsedAcceptRisk} 성공 필드 또는 error
172
+ */
173
+ function parseAcceptRiskArgs(flags) {
174
+ const fail = (message) => ({
175
+ error: `review accept-risk: ${message}\n${USAGE}`, blueprint: '', finding: '', note: '', yes: false,
176
+ });
177
+ if (countFlag(flags, 'task') > 0)
178
+ return fail('--task is not allowed');
179
+ for (const name of ['blueprint', 'finding', 'note', 'repo']) {
180
+ if (countFlag(flags, name) > 1)
181
+ return fail(`--${name} specified more than once`);
182
+ }
183
+ const f = parseFlags(flags);
184
+ for (const name of ['blueprint', 'finding', 'note']) {
185
+ if (typeof f[name] !== 'string' || f[name] === '')
186
+ return fail(`--${name} is required`);
187
+ }
188
+ if (f.repo !== undefined && (typeof f.repo !== 'string' || f.repo === '')) {
189
+ return fail('--repo requires a directory');
190
+ }
191
+ return {
192
+ blueprint: f.blueprint,
193
+ finding: f.finding,
194
+ note: f.note,
195
+ yes: f.yes === true,
196
+ repo: typeof f.repo === 'string' ? f.repo : undefined,
197
+ };
198
+ }
199
+ /**
200
+ * 공개 `review` 핸들러. `--help`는 기록 모듈을 열기 전에 stdout으로 끝내고,
201
+ * 그 외는 argv 검증과 원장 기록·JSON·exit만 연결한다.
202
+ *
203
+ * @param {string[]} rest - `record`와 플래그
204
+ * @param {CliIo} io - stdout/stderr 싱크
205
+ * @returns {number} 성공 0, 원장 거절 1, 사용법 2
206
+ */
207
+ function cmdReview(rest, io) {
208
+ // 도움말은 argv 검사·lazy require보다 먼저. 형식은 여기 stdout에만 적는다.
209
+ if (argvRequestsHelp(rest)) {
210
+ io.out(HELP);
211
+ return 0;
212
+ }
213
+ if (rest[0] === 'accept-risk') {
214
+ const accept = parseAcceptRiskArgs(rest.slice(1));
215
+ if (accept.error) {
216
+ io.err(accept.error);
217
+ return 2;
218
+ }
219
+ const reviewRecord = require('./review-record');
220
+ const outcome = reviewRecord.acceptReviewRisk({
221
+ repoRoot: accept.repo ? accept.repo : process.cwd(),
222
+ blueprintDir: accept.blueprint,
223
+ finding: accept.finding,
224
+ note: accept.note,
225
+ yes: accept.yes,
226
+ });
227
+ io.out(`${JSON.stringify(outcome, null, 2)}\n`);
228
+ return outcome.ok ? 0 : 1;
229
+ }
230
+ const parsed = parseReviewRecordArgs(rest);
231
+ if (parsed.error) {
232
+ io.err(parsed.error);
233
+ return 2;
234
+ }
235
+ // argv를 통과한 뒤에만 기록 모듈을 적재한다. 사용법 거절이 review.md I/O
236
+ // 경로를 require.cache에 남기지 않게 하기 위함.
237
+ const reviewRecord = require('./review-record');
238
+ const result = reviewRecord.recordReview({
239
+ repoRoot: parsed.repo ? parsed.repo : process.cwd(),
240
+ blueprintDir: parsed.blueprint,
241
+ task: parsed.task,
242
+ roundFile: parsed.round,
243
+ status: parsed.status,
244
+ });
245
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
246
+ return result.ok ? 0 : 1;
247
+ }
248
+ module.exports = {
249
+ run: cmdReview,
250
+ usage: cliUsage.COMMAND_USAGE['review'],
251
+ };