cc-devflow 4.5.9 → 4.5.11

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 (122) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +11 -0
  2. package/.claude/skills/cc-act/SKILL.md +19 -10
  3. package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +1 -1
  4. package/.claude/skills/cc-act/references/closure-contract.md +1 -1
  5. package/.claude/skills/cc-act/references/git-commit-guidelines.md +1 -1
  6. package/.claude/skills/cc-check/CHANGELOG.md +23 -0
  7. package/.claude/skills/cc-check/PLAYBOOK.md +1 -0
  8. package/.claude/skills/cc-check/SKILL.md +15 -9
  9. package/.claude/skills/cc-check/references/review-contract.md +7 -0
  10. package/.claude/skills/cc-check/scripts/render-report-card.js +6 -1
  11. package/.claude/skills/cc-dev/CHANGELOG.md +10 -0
  12. package/.claude/skills/cc-dev/SKILL.md +34 -2
  13. package/.claude/skills/cc-do/CHANGELOG.md +18 -0
  14. package/.claude/skills/cc-do/PLAYBOOK.md +7 -7
  15. package/.claude/skills/cc-do/SKILL.md +47 -40
  16. package/.claude/skills/cc-do/references/execution-recovery.md +18 -13
  17. package/.claude/skills/cc-do/scripts/build-task-context.sh +4 -17
  18. package/.claude/skills/cc-do/scripts/record-review-decision.sh +4 -5
  19. package/.claude/skills/cc-do/scripts/recover-workflow.sh +9 -11
  20. package/.claude/skills/cc-do/scripts/verify-task-gates.sh +12 -10
  21. package/.claude/skills/cc-do/scripts/write-task-checkpoint.sh +7 -29
  22. package/.claude/skills/cc-investigate/CHANGELOG.md +24 -0
  23. package/.claude/skills/cc-investigate/PLAYBOOK.md +10 -9
  24. package/.claude/skills/cc-investigate/SKILL.md +163 -417
  25. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +56 -10
  26. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +6 -6
  27. package/.claude/skills/cc-investigate/assets/{ANALYSIS_TEMPLATE.md → legacy/ANALYSIS_TEMPLATE.md} +1 -0
  28. package/.claude/skills/cc-investigate/references/investigation-contract.md +5 -4
  29. package/.claude/skills/cc-investigate/scripts/bootstrap-analysis.sh +1 -1
  30. package/.claude/skills/cc-plan/CHANGELOG.md +32 -0
  31. package/.claude/skills/cc-plan/PLAYBOOK.md +55 -53
  32. package/.claude/skills/cc-plan/SKILL.md +209 -536
  33. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +50 -14
  34. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +5 -4
  35. package/.claude/skills/cc-plan/assets/{DESIGN_TEMPLATE.md → legacy/DESIGN_TEMPLATE.md} +1 -0
  36. package/.claude/skills/cc-plan/assets/{TINY_DESIGN_TEMPLATE.md → legacy/TINY_DESIGN_TEMPLATE.md} +1 -1
  37. package/.claude/skills/cc-plan/references/planning-contract.md +12 -10
  38. package/.claude/skills/cc-review/CHANGELOG.md +6 -0
  39. package/.claude/skills/cc-review/PLAYBOOK.md +9 -11
  40. package/.claude/skills/cc-review/SKILL.md +37 -61
  41. package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +1 -1
  42. package/.claude/skills/cc-review/references/implementation-review-branch.md +5 -5
  43. package/.claude/skills/cc-review/references/plan-review-branch.md +1 -1
  44. package/.claude/skills/cc-review/references/review-methods.md +4 -4
  45. package/.claude/skills/cc-review/scripts/collect-review-context.sh +14 -7
  46. package/CHANGELOG.md +30 -0
  47. package/CONTRIBUTING.md +40 -4
  48. package/CONTRIBUTING.zh-CN.md +40 -4
  49. package/README.md +22 -8
  50. package/README.zh-CN.md +22 -8
  51. package/bin/cc-devflow-cli.js +293 -36
  52. package/docs/examples/START-HERE.md +6 -4
  53. package/docs/examples/example-bindings.json +8 -8
  54. package/docs/examples/full-design-blocked/README.md +2 -2
  55. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +2 -1
  56. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +3 -2
  57. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +11 -8
  58. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/review/report-card.json +4 -4
  59. package/docs/examples/local-handoff/README.md +2 -2
  60. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +2 -1
  61. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +3 -2
  62. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +9 -6
  63. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/review/report-card.json +1 -1
  64. package/docs/examples/pdca-loop/README.md +2 -2
  65. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/pr-brief.md +2 -2
  66. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +2 -1
  67. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +2 -1
  68. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +9 -6
  69. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/review/report-card.json +1 -1
  70. package/docs/examples/scripts/check-example-bindings.sh +2 -0
  71. package/docs/get-shit-done-strategy-audit.md +22 -22
  72. package/docs/guides/artifact-contract.md +5 -1
  73. package/docs/guides/getting-started.md +11 -8
  74. package/docs/guides/getting-started.zh-CN.md +11 -8
  75. package/docs/guides/minimize-artifacts.md +137 -0
  76. package/lib/compiler/__tests__/skills-registry.test.js +2 -2
  77. package/lib/skill-runtime/CLAUDE.md +1 -1
  78. package/lib/skill-runtime/__tests__/autopilot.test.js +42 -6
  79. package/lib/skill-runtime/__tests__/benchmark-artifacts.test.js +165 -0
  80. package/lib/skill-runtime/__tests__/benchmark-skills.test.js +109 -0
  81. package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +2 -2
  82. package/lib/skill-runtime/__tests__/dispatch.test.js +8 -38
  83. package/lib/skill-runtime/__tests__/intent.test.js +4 -20
  84. package/lib/skill-runtime/__tests__/lifecycle.test.js +1 -1
  85. package/lib/skill-runtime/__tests__/paths.test.js +7 -1
  86. package/lib/skill-runtime/__tests__/planner.tdd.test.js +61 -0
  87. package/lib/skill-runtime/__tests__/prepare-pr.test.js +3 -16
  88. package/lib/skill-runtime/__tests__/query.test.js +388 -7
  89. package/lib/skill-runtime/__tests__/review-check-integration.test.js +148 -0
  90. package/lib/skill-runtime/__tests__/review-records.test.js +619 -0
  91. package/lib/skill-runtime/__tests__/runtime.integration.test.js +64 -23
  92. package/lib/skill-runtime/__tests__/schemas.test.js +43 -0
  93. package/lib/skill-runtime/__tests__/task-contract-migrate.test.js +137 -0
  94. package/lib/skill-runtime/__tests__/task-contract.test.js +874 -0
  95. package/lib/skill-runtime/__tests__/verify-artifacts.test.js +203 -0
  96. package/lib/skill-runtime/__tests__/worker-run.test.js +4 -11
  97. package/lib/skill-runtime/__tests__/workflow-context-legacy-fallback.test.js +31 -0
  98. package/lib/skill-runtime/__tests__/workflow-context.test.js +98 -0
  99. package/lib/skill-runtime/artifacts.js +0 -5
  100. package/lib/skill-runtime/context-index.js +545 -0
  101. package/lib/skill-runtime/intent.js +9 -33
  102. package/lib/skill-runtime/lifecycle.js +1 -1
  103. package/lib/skill-runtime/operations/CLAUDE.md +2 -2
  104. package/lib/skill-runtime/operations/dispatch.js +4 -42
  105. package/lib/skill-runtime/operations/init.js +2 -6
  106. package/lib/skill-runtime/operations/janitor.js +2 -18
  107. package/lib/skill-runtime/operations/resume.js +21 -38
  108. package/lib/skill-runtime/operations/review-records.js +265 -0
  109. package/lib/skill-runtime/operations/snapshot.js +1 -1
  110. package/lib/skill-runtime/operations/task-contract.js +593 -0
  111. package/lib/skill-runtime/operations/worker-run.js +2 -30
  112. package/lib/skill-runtime/paths.js +4 -4
  113. package/lib/skill-runtime/planner.js +24 -11
  114. package/lib/skill-runtime/query-registry.js +2 -2
  115. package/lib/skill-runtime/query.js +15 -2
  116. package/lib/skill-runtime/review-records.js +123 -0
  117. package/lib/skill-runtime/review.js +246 -11
  118. package/lib/skill-runtime/schemas.js +174 -12
  119. package/lib/skill-runtime/store.js +0 -10
  120. package/lib/skill-runtime/task-contract.js +188 -0
  121. package/lib/skill-runtime/workflow-context.js +748 -0
  122. package/package.json +6 -2
@@ -0,0 +1,619 @@
1
+ /**
2
+ * [INPUT]: 依赖 lib/skill-runtime/schemas.js 暴露的 ReviewLedgerEventSchema + ReviewFindingsDocSchema 以及对应 parse helpers。
3
+ * [OUTPUT]: 通过 zod parse 行为证明 review-ledger.jsonl 每种 event 和 review-findings.json 顶层 doc 的结构契约。
4
+ * [POS]: REQ-003-minimize-workflow-artifacts T001 的 Red+Green 合一证据;支撑后续 review CLI 的 schema 边界。
5
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const { spawnSync } = require('child_process');
12
+
13
+ const {
14
+ ReviewLedgerEventSchema,
15
+ ReviewFindingsDocSchema,
16
+ parseReviewLedgerEvent,
17
+ parseReviewFindingsDoc
18
+ } = require('../schemas');
19
+ const { parseReviewLedger } = require('../review-records');
20
+
21
+ const CLI = path.resolve(__dirname, '../../../bin/cc-devflow-cli.js');
22
+
23
+ const VALID_COMMON = {
24
+ schema: 'review-ledger.v2',
25
+ change: 'REQ-003-minimize-workflow-artifacts',
26
+ reviewId: 'RVW-20260512-001',
27
+ createdAt: '2026-05-12T00:00:00.000Z',
28
+ createdBy: 'cc-devflow-cli'
29
+ };
30
+
31
+ function build(eventOverrides) {
32
+ return { ...VALID_COMMON, ...eventOverrides };
33
+ }
34
+
35
+ function readJsonl(filePath) {
36
+ return fs.readFileSync(filePath, 'utf8')
37
+ .trim()
38
+ .split('\n')
39
+ .filter(Boolean)
40
+ .map((line) => JSON.parse(line));
41
+ }
42
+
43
+ describe('ReviewLedgerEventSchema', () => {
44
+ describe('review-started event', () => {
45
+ const validStarted = () => build({
46
+ event: 'review-started',
47
+ mode: 'implementation',
48
+ scope: 'current-diff',
49
+ baseSha: 'abc123',
50
+ headSha: 'def456',
51
+ selectedNodes: ['R001', 'R002'],
52
+ skippedNodes: [{ node: 'browser', reason: 'not UI-facing' }],
53
+ riskLanes: ['intent-regression', 'contracts-coverage']
54
+ });
55
+
56
+ test('accepts a well-formed review-started event with selectedNodes and riskLanes', () => {
57
+ expect(() => parseReviewLedgerEvent(validStarted())).not.toThrow();
58
+ });
59
+
60
+ test('rejects review-started event missing baseSha', () => {
61
+ const bad = validStarted();
62
+ delete bad.baseSha;
63
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/baseSha/);
64
+ });
65
+
66
+ test('rejects review-started event with unknown mode', () => {
67
+ const bad = { ...validStarted(), mode: 'exploration' };
68
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/mode/);
69
+ });
70
+ });
71
+
72
+ describe('review-node-checked event', () => {
73
+ const validNode = () => build({
74
+ event: 'review-node-checked',
75
+ nodeId: 'R001',
76
+ mode: 'implementation',
77
+ target: 'lib/skill-runtime/workflow-context.js',
78
+ status: 'checked',
79
+ coverage: ['contract', 'tests'],
80
+ evidenceRefs: ['cmd:npm test -- workflow-context'],
81
+ findings: [],
82
+ next: 'cc-check'
83
+ });
84
+
85
+ test('accepts a checked node with evidence refs', () => {
86
+ expect(() => parseReviewLedgerEvent(validNode())).not.toThrow();
87
+ });
88
+
89
+ test('rejects a node event with unknown status', () => {
90
+ const bad = { ...validNode(), status: 'maybe' };
91
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/status/);
92
+ });
93
+
94
+ test('rejects a node event with unknown next route', () => {
95
+ const bad = { ...validNode(), next: 'cc-unknown' };
96
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/next/);
97
+ });
98
+ });
99
+
100
+ describe('review-finding-added event', () => {
101
+ const validFinding = () => build({
102
+ event: 'review-finding-added',
103
+ findingId: 'F001',
104
+ severity: 'important',
105
+ confidence: 8,
106
+ displayTier: 'blocking',
107
+ fingerprint: 'sha256:deadbeef',
108
+ scope: 'inside current requirement blast radius',
109
+ path: 'lib/skill-runtime/workflow-context.js',
110
+ evidence: 'legacyFallback branch still returns exists:false',
111
+ recommendation: 'ensure fallback branch probes both design.md and analysis.md',
112
+ route: 'cc-do'
113
+ });
114
+
115
+ test('accepts a well-formed important finding with numeric confidence', () => {
116
+ expect(() => parseReviewLedgerEvent(validFinding())).not.toThrow();
117
+ });
118
+
119
+ test('rejects a finding with unknown severity', () => {
120
+ const bad = { ...validFinding(), severity: 'catastrophic' };
121
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/severity/);
122
+ });
123
+
124
+ test('rejects a finding whose confidence is out of 0..10 range', () => {
125
+ const bad = { ...validFinding(), confidence: 11 };
126
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/confidence/);
127
+ });
128
+
129
+ test('rejects a finding with unknown displayTier', () => {
130
+ const bad = { ...validFinding(), displayTier: 'critical-bright-red' };
131
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/displayTier/);
132
+ });
133
+ });
134
+
135
+ describe('review-closed event', () => {
136
+ const validClosed = () => build({
137
+ event: 'review-closed',
138
+ status: 'findings',
139
+ blockingCount: 1,
140
+ warningCount: 0,
141
+ next: 'cc-do'
142
+ });
143
+
144
+ test('accepts a well-formed review-closed event', () => {
145
+ expect(() => parseReviewLedgerEvent(validClosed())).not.toThrow();
146
+ });
147
+
148
+ test('rejects a close event with unknown status', () => {
149
+ const bad = { ...validClosed(), status: 'done' };
150
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/status/);
151
+ });
152
+
153
+ test('rejects a close event with negative blockingCount', () => {
154
+ const bad = { ...validClosed(), blockingCount: -1 };
155
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/blockingCount/);
156
+ });
157
+ });
158
+
159
+ describe('discriminator', () => {
160
+ test('rejects an event with unknown literal', () => {
161
+ const bad = build({ event: 'review-suspended' });
162
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/event/);
163
+ });
164
+
165
+ test('rejects an event missing createdBy', () => {
166
+ const bad = build({ event: 'review-closed', status: 'clean', blockingCount: 0, warningCount: 0, next: 'cc-check' });
167
+ delete bad.createdBy;
168
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/createdBy/);
169
+ });
170
+
171
+ test('rejects an event with wrong schema string', () => {
172
+ const bad = build({ event: 'review-closed', status: 'clean', blockingCount: 0, warningCount: 0, next: 'cc-check' });
173
+ bad.schema = 'review-ledger.v1';
174
+ expect(() => parseReviewLedgerEvent(bad)).toThrow(/schema/);
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('review start CLI', () => {
180
+ test('writes one parseable review-started event and prints the reviewId', () => {
181
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-review-start-'));
182
+ const changeId = 'REQ-130';
183
+ const changeKey = 'REQ-130-review-start';
184
+ const result = spawnSync(process.execPath, [
185
+ CLI,
186
+ 'review',
187
+ 'start',
188
+ '--cwd',
189
+ repoRoot,
190
+ '--change',
191
+ changeId,
192
+ '--change-key',
193
+ changeKey,
194
+ '--mode',
195
+ 'implementation',
196
+ '--scope',
197
+ 'current-diff',
198
+ '--base-sha',
199
+ 'abc123',
200
+ '--head-sha',
201
+ 'def456',
202
+ '--selected-node',
203
+ 'R001',
204
+ '--selected-node',
205
+ 'R002',
206
+ '--skipped-node',
207
+ 'browser:not UI-facing',
208
+ '--risk-lane',
209
+ 'intent-regression',
210
+ '--risk-lane',
211
+ 'contracts-coverage'
212
+ ], { encoding: 'utf8' });
213
+
214
+ expect(result.status).toBe(0);
215
+ const stdout = JSON.parse(result.stdout);
216
+ expect(stdout.reviewId).toMatch(/^RVW-\d{8}-001$/);
217
+
218
+ const ledgerPath = path.join(
219
+ repoRoot,
220
+ 'devflow',
221
+ 'changes',
222
+ changeKey,
223
+ 'review',
224
+ 'review-ledger.jsonl'
225
+ );
226
+ const events = readJsonl(ledgerPath);
227
+ expect(events).toHaveLength(1);
228
+
229
+ const event = parseReviewLedgerEvent(events[0]);
230
+ expect(event).toMatchObject({
231
+ schema: 'review-ledger.v2',
232
+ change: changeKey,
233
+ reviewId: stdout.reviewId,
234
+ createdBy: 'cc-devflow-cli',
235
+ event: 'review-started',
236
+ mode: 'implementation',
237
+ scope: 'current-diff',
238
+ baseSha: 'abc123',
239
+ headSha: 'def456',
240
+ selectedNodes: ['R001', 'R002'],
241
+ skippedNodes: [{ node: 'browser', reason: 'not UI-facing' }],
242
+ riskLanes: ['intent-regression', 'contracts-coverage']
243
+ });
244
+
245
+ fs.rmSync(repoRoot, { recursive: true, force: true });
246
+ });
247
+ });
248
+
249
+ describe('review event CLI', () => {
250
+ test('appends node, finding, and one close event after review start', () => {
251
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-review-events-'));
252
+ const changeId = 'REQ-131';
253
+ const changeKey = 'REQ-131-review-events';
254
+ const scoped = [
255
+ '--cwd',
256
+ repoRoot,
257
+ '--change',
258
+ changeId,
259
+ '--change-key',
260
+ changeKey
261
+ ];
262
+ const start = spawnSync(process.execPath, [
263
+ CLI,
264
+ 'review',
265
+ 'start',
266
+ ...scoped,
267
+ '--base-sha',
268
+ 'abc123',
269
+ '--head-sha',
270
+ 'def456'
271
+ ], { encoding: 'utf8' });
272
+ const { reviewId } = JSON.parse(start.stdout);
273
+
274
+ const recordNode = spawnSync(process.execPath, [
275
+ CLI,
276
+ 'review',
277
+ 'record-node',
278
+ ...scoped,
279
+ '--review-id',
280
+ reviewId,
281
+ '--node-id',
282
+ 'R001',
283
+ '--mode',
284
+ 'implementation',
285
+ '--target',
286
+ 'lib/skill-runtime/workflow-context.js',
287
+ '--status',
288
+ 'checked',
289
+ '--coverage',
290
+ 'contract',
291
+ '--coverage',
292
+ 'tests',
293
+ '--evidence-ref',
294
+ 'cmd:npm test -- review-records',
295
+ '--finding',
296
+ 'F001',
297
+ '--next',
298
+ 'cc-do'
299
+ ], { encoding: 'utf8' });
300
+ const addFinding = spawnSync(process.execPath, [
301
+ CLI,
302
+ 'review',
303
+ 'add-finding',
304
+ ...scoped,
305
+ '--review-id',
306
+ reviewId,
307
+ '--finding-id',
308
+ 'F001',
309
+ '--severity',
310
+ 'important',
311
+ '--confidence',
312
+ '8',
313
+ '--display-tier',
314
+ 'blocking',
315
+ '--fingerprint',
316
+ 'sha256:abc123',
317
+ '--scope',
318
+ 'inside current requirement blast radius',
319
+ '--path',
320
+ 'lib/skill-runtime/workflow-context.js',
321
+ '--evidence',
322
+ 'review start has no follow-up event writer',
323
+ '--recommendation',
324
+ 'append node and finding events through review CLI',
325
+ '--route',
326
+ 'cc-do'
327
+ ], { encoding: 'utf8' });
328
+ const close = spawnSync(process.execPath, [
329
+ CLI,
330
+ 'review',
331
+ 'close',
332
+ ...scoped,
333
+ '--review-id',
334
+ reviewId,
335
+ '--status',
336
+ 'findings',
337
+ '--blocking-count',
338
+ '1',
339
+ '--warning-count',
340
+ '0',
341
+ '--next',
342
+ 'cc-do'
343
+ ], { encoding: 'utf8' });
344
+ const duplicateClose = spawnSync(process.execPath, [
345
+ CLI,
346
+ 'review',
347
+ 'close',
348
+ ...scoped,
349
+ '--review-id',
350
+ reviewId,
351
+ '--status',
352
+ 'findings',
353
+ '--blocking-count',
354
+ '1',
355
+ '--warning-count',
356
+ '0',
357
+ '--next',
358
+ 'cc-do'
359
+ ], { encoding: 'utf8' });
360
+
361
+ expect(recordNode.status).toBe(0);
362
+ expect(addFinding.status).toBe(0);
363
+ expect(close.status).toBe(0);
364
+ expect(duplicateClose.status).not.toBe(0);
365
+
366
+ const ledgerPath = path.join(
367
+ repoRoot,
368
+ 'devflow',
369
+ 'changes',
370
+ changeKey,
371
+ 'review',
372
+ 'review-ledger.jsonl'
373
+ );
374
+ const events = readJsonl(ledgerPath).map(parseReviewLedgerEvent);
375
+ expect(events.map((event) => event.event)).toEqual([
376
+ 'review-started',
377
+ 'review-node-checked',
378
+ 'review-finding-added',
379
+ 'review-closed'
380
+ ]);
381
+ expect(events[1]).toMatchObject({
382
+ reviewId,
383
+ nodeId: 'R001',
384
+ coverage: ['contract', 'tests'],
385
+ evidenceRefs: ['cmd:npm test -- review-records'],
386
+ findings: ['F001'],
387
+ next: 'cc-do'
388
+ });
389
+ expect(events[2]).toMatchObject({
390
+ reviewId,
391
+ findingId: 'F001',
392
+ severity: 'important',
393
+ confidence: 8,
394
+ displayTier: 'blocking',
395
+ route: 'cc-do'
396
+ });
397
+ expect(events[3]).toMatchObject({
398
+ reviewId,
399
+ status: 'findings',
400
+ blockingCount: 1,
401
+ warningCount: 0,
402
+ next: 'cc-do'
403
+ });
404
+
405
+ fs.rmSync(repoRoot, { recursive: true, force: true });
406
+ });
407
+ });
408
+
409
+ describe('parseReviewLedger', () => {
410
+ test('degrades malformed ledger rows to unknown freshness without throwing', () => {
411
+ const parsed = parseReviewLedger([
412
+ JSON.stringify(build({
413
+ event: 'review-started',
414
+ mode: 'implementation',
415
+ scope: 'current-diff',
416
+ baseSha: 'abc123',
417
+ headSha: 'def456',
418
+ selectedNodes: [],
419
+ skippedNodes: [],
420
+ riskLanes: []
421
+ })),
422
+ '{"schema":"review-ledger.v1","event":"review-closed"}'
423
+ ].join('\n'));
424
+
425
+ expect(parsed.entries).toHaveLength(1);
426
+ expect(parsed.errors).toHaveLength(1);
427
+ expect(parsed.freshness).toMatchObject({ status: 'unknown' });
428
+ });
429
+ });
430
+
431
+ describe('review render CLI', () => {
432
+ test('renders Markdown from ledger events on demand', () => {
433
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-review-render-'));
434
+ const changeId = 'REQ-132';
435
+ const changeKey = 'REQ-132-review-render';
436
+ const outputPath = path.join(repoRoot, 'review-output.md');
437
+ const scoped = [
438
+ '--cwd',
439
+ repoRoot,
440
+ '--change',
441
+ changeId,
442
+ '--change-key',
443
+ changeKey
444
+ ];
445
+ const start = spawnSync(process.execPath, [
446
+ CLI,
447
+ 'review',
448
+ 'start',
449
+ ...scoped,
450
+ '--base-sha',
451
+ 'abc123',
452
+ '--head-sha',
453
+ 'def456'
454
+ ], { encoding: 'utf8' });
455
+ const { reviewId } = JSON.parse(start.stdout);
456
+
457
+ spawnSync(process.execPath, [
458
+ CLI,
459
+ 'review',
460
+ 'record-node',
461
+ ...scoped,
462
+ '--review-id',
463
+ reviewId,
464
+ '--node-id',
465
+ 'R001',
466
+ '--target',
467
+ 'lib/skill-runtime/workflow-context.js',
468
+ '--status',
469
+ 'checked',
470
+ '--coverage',
471
+ 'contract',
472
+ '--evidence-ref',
473
+ 'cmd:npm test -- review-records',
474
+ '--next',
475
+ 'cc-do'
476
+ ], { encoding: 'utf8' });
477
+ spawnSync(process.execPath, [
478
+ CLI,
479
+ 'review',
480
+ 'add-finding',
481
+ ...scoped,
482
+ '--review-id',
483
+ reviewId,
484
+ '--finding-id',
485
+ 'F001',
486
+ '--severity',
487
+ 'important',
488
+ '--confidence',
489
+ '8',
490
+ '--display-tier',
491
+ 'blocking',
492
+ '--fingerprint',
493
+ 'sha256:abc123',
494
+ '--scope',
495
+ 'inside current requirement blast radius',
496
+ '--path',
497
+ 'lib/skill-runtime/workflow-context.js',
498
+ '--evidence',
499
+ 'review ledger needs Markdown output',
500
+ '--recommendation',
501
+ 'render a compact review report from ledger events',
502
+ '--route',
503
+ 'cc-do'
504
+ ], { encoding: 'utf8' });
505
+ spawnSync(process.execPath, [
506
+ CLI,
507
+ 'review',
508
+ 'close',
509
+ ...scoped,
510
+ '--review-id',
511
+ reviewId,
512
+ '--status',
513
+ 'findings',
514
+ '--blocking-count',
515
+ '1',
516
+ '--warning-count',
517
+ '0',
518
+ '--next',
519
+ 'cc-do'
520
+ ], { encoding: 'utf8' });
521
+
522
+ const render = spawnSync(process.execPath, [
523
+ CLI,
524
+ 'review',
525
+ 'render',
526
+ ...scoped,
527
+ '--review-id',
528
+ reviewId,
529
+ '--output',
530
+ outputPath
531
+ ], { encoding: 'utf8' });
532
+
533
+ expect(render.status).toBe(0);
534
+ expect(fs.readFileSync(outputPath, 'utf8')).toEqual(expect.stringContaining(`reviewId: ${reviewId}`));
535
+ expect(fs.readFileSync(outputPath, 'utf8')).toEqual(expect.stringContaining('Output language: en'));
536
+ expect(fs.readFileSync(outputPath, 'utf8')).toEqual(expect.stringContaining('## Summary'));
537
+ expect(fs.readFileSync(outputPath, 'utf8')).toEqual(expect.stringContaining('## Findings'));
538
+ expect(fs.readFileSync(outputPath, 'utf8')).toEqual(expect.stringContaining('F001'));
539
+ expect(fs.readFileSync(outputPath, 'utf8')).toEqual(expect.stringContaining('## Ledger Events'));
540
+
541
+ fs.rmSync(repoRoot, { recursive: true, force: true });
542
+ });
543
+ });
544
+
545
+ describe('ReviewFindingsDocSchema', () => {
546
+ const validDoc = () => ({
547
+ schema: 'review-findings.v2',
548
+ change: 'REQ-003-minimize-workflow-artifacts',
549
+ reviewId: 'RVW-20260512-001',
550
+ headSha: 'def456',
551
+ freshness: {
552
+ status: 'fresh',
553
+ reviewedCommit: 'def456',
554
+ currentCommit: 'def456',
555
+ commitsSinceReview: 0
556
+ },
557
+ summary: {
558
+ status: 'findings',
559
+ blockingCount: 1,
560
+ warningCount: 0,
561
+ next: 'cc-do'
562
+ },
563
+ findings: [
564
+ {
565
+ id: 'F001',
566
+ severity: 'important',
567
+ confidence: 8,
568
+ displayTier: 'blocking',
569
+ fingerprint: 'sha256:deadbeef',
570
+ scope: 'inside current requirement blast radius',
571
+ path: 'lib/skill-runtime/workflow-context.js',
572
+ evidence: 'legacyFallback branch still returns exists:false',
573
+ recommendation: 'ensure fallback branch probes both design.md and analysis.md',
574
+ route: 'cc-do'
575
+ }
576
+ ]
577
+ });
578
+
579
+ test('accepts a well-formed findings doc with one finding', () => {
580
+ expect(() => parseReviewFindingsDoc(validDoc())).not.toThrow();
581
+ });
582
+
583
+ test('accepts a clean findings doc with empty findings array', () => {
584
+ const doc = validDoc();
585
+ doc.summary = { status: 'clean', blockingCount: 0, warningCount: 0, next: 'cc-check' };
586
+ doc.findings = [];
587
+ expect(() => parseReviewFindingsDoc(doc)).not.toThrow();
588
+ });
589
+
590
+ test('rejects a findings doc with unknown freshness.status', () => {
591
+ const bad = validDoc();
592
+ bad.freshness.status = 'warm';
593
+ expect(() => parseReviewFindingsDoc(bad)).toThrow(/freshness/);
594
+ });
595
+
596
+ test('rejects a findings doc with unknown summary.status', () => {
597
+ const bad = validDoc();
598
+ bad.summary.status = 'maybe';
599
+ expect(() => parseReviewFindingsDoc(bad)).toThrow(/summary/);
600
+ });
601
+
602
+ test('rejects a findings doc whose schema string is not review-findings.v2', () => {
603
+ const bad = validDoc();
604
+ bad.schema = 'review-findings.v1';
605
+ expect(() => parseReviewFindingsDoc(bad)).toThrow(/schema/);
606
+ });
607
+
608
+ test('rejects a finding entry with unknown severity', () => {
609
+ const bad = validDoc();
610
+ bad.findings[0].severity = 'catastrophic';
611
+ expect(() => parseReviewFindingsDoc(bad)).toThrow(/severity/);
612
+ });
613
+
614
+ test('rejects a finding entry with confidence above 10', () => {
615
+ const bad = validDoc();
616
+ bad.findings[0].confidence = 12;
617
+ expect(() => parseReviewFindingsDoc(bad)).toThrow(/confidence/);
618
+ });
619
+ });