cclaw-cli 6.8.0 → 6.10.0

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 (40) hide show
  1. package/dist/artifact-linter/design.js +1 -1
  2. package/dist/artifact-linter/plan.js +37 -0
  3. package/dist/artifact-linter/shared.d.ts +48 -2
  4. package/dist/artifact-linter/shared.js +54 -5
  5. package/dist/artifact-linter/tdd.d.ts +31 -0
  6. package/dist/artifact-linter/tdd.js +357 -17
  7. package/dist/artifact-linter.js +87 -2
  8. package/dist/content/examples.js +9 -9
  9. package/dist/content/harness-doc.js +1 -1
  10. package/dist/content/hooks.js +140 -3
  11. package/dist/content/iron-laws.js +6 -2
  12. package/dist/content/node-hooks.js +15 -1308
  13. package/dist/content/reference-patterns.js +2 -2
  14. package/dist/content/skills-elicitation.js +2 -2
  15. package/dist/content/skills.js +1 -1
  16. package/dist/content/stages/brainstorm.js +2 -2
  17. package/dist/content/stages/design.js +2 -2
  18. package/dist/content/stages/scope.js +2 -2
  19. package/dist/content/stages/tdd.js +7 -8
  20. package/dist/content/subagents.js +20 -2
  21. package/dist/content/templates.js +5 -15
  22. package/dist/delegation.d.ts +102 -3
  23. package/dist/delegation.js +172 -14
  24. package/dist/early-loop.js +15 -1
  25. package/dist/gate-evidence.js +15 -23
  26. package/dist/harness-adapters.js +4 -2
  27. package/dist/install.js +37 -221
  28. package/dist/internal/advance-stage.js +19 -3
  29. package/dist/internal/detect-supply-chain-changes.d.ts +6 -0
  30. package/dist/internal/detect-supply-chain-changes.js +138 -0
  31. package/dist/internal/flow-state-repair.d.ts +7 -0
  32. package/dist/internal/flow-state-repair.js +57 -18
  33. package/dist/internal/plan-split-waves.d.ts +66 -0
  34. package/dist/internal/plan-split-waves.js +249 -0
  35. package/dist/run-persistence.d.ts +2 -0
  36. package/dist/run-persistence.js +62 -3
  37. package/dist/runtime/run-hook.mjs +44 -8729
  38. package/dist/tdd-slices.d.ts +90 -0
  39. package/dist/tdd-slices.js +375 -0
  40. package/package.json +1 -1
@@ -424,7 +424,7 @@ export async function lintDesignStage(ctx) {
424
424
  if (layeredDocumentReview !== null) {
425
425
  findings.push({
426
426
  section: "Document Reviewer Structured Findings",
427
- required: false,
427
+ required: true,
428
428
  rule: "When Layered review references coherence-reviewer/scope-guardian-reviewer/feasibility-reviewer, include explicit reviewer status plus calibrated finding lines.",
429
429
  found: layeredDocumentReview.missingStructured.length === 0,
430
430
  details: layeredDocumentReview.missingStructured.length === 0
@@ -3,6 +3,8 @@ import { resolveArtifactPath as resolveStageArtifactPath } from "../artifact-pat
3
3
  import { exists } from "../fs-utils.js";
4
4
  import { FORBIDDEN_PLACEHOLDER_TOKENS, CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
5
5
  import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits } from "../internal/plan-split-waves.js";
6
8
  export async function lintPlanStage(ctx) {
7
9
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
8
10
  evaluateInvestigationTrace(ctx, "Implementation Units");
@@ -114,6 +116,41 @@ export async function lintPlanStage(ctx) {
114
116
  ? "No forbidden placeholder tokens detected outside the rule section."
115
117
  : `Detected forbidden token(s) elsewhere in plan: ${filteredPlanHits.join(", ")}.`
116
118
  });
119
+ // v6.10.0 (P4) — advisory `plan_too_large_no_waves`. Fires when a
120
+ // standard-track plan has more than the wave-split threshold of
121
+ // implementation units AND the wave-plans/ directory is empty.
122
+ // Linter advisories never block stage-complete (`required: false`),
123
+ // so the agent gets a nudge to run `cclaw-cli internal plan-split-waves`
124
+ // without the plan stage failing.
125
+ try {
126
+ const planUnits = parseImplementationUnits(raw);
127
+ if (planUnits.length > PLAN_SPLIT_SMALL_PLAN_THRESHOLD) {
128
+ const artifactsDir = path.dirname(absFile);
129
+ const wavePlansDir = path.join(artifactsDir, "wave-plans");
130
+ let wavePlansHasContent = false;
131
+ try {
132
+ const dirEntries = await fs.readdir(wavePlansDir);
133
+ wavePlansHasContent = dirEntries.some((name) => /^wave-\d+\.md$/u.test(name));
134
+ }
135
+ catch {
136
+ wavePlansHasContent = false;
137
+ }
138
+ if (!wavePlansHasContent) {
139
+ findings.push({
140
+ section: "plan_too_large_no_waves",
141
+ required: false,
142
+ rule: "Plans with > 50 implementation units benefit from being split into manageable waves via `cclaw-cli internal plan-split-waves`.",
143
+ found: false,
144
+ details: `Plan has ${planUnits.length} implementation unit(s) (threshold ${PLAN_SPLIT_SMALL_PLAN_THRESHOLD}) and no wave-plans/ directory yet. ` +
145
+ "Run `cclaw-cli internal plan-split-waves` to break this plan into manageable waves; the linter is advisory only and will not block stage-complete."
146
+ });
147
+ }
148
+ }
149
+ }
150
+ catch {
151
+ // Parser errors should never block the linter — the advisory is
152
+ // purely a nudge.
153
+ }
117
154
  const handoffBody = sectionBodyByName(sections, "Execution Handoff");
118
155
  if (handoffBody !== null) {
119
156
  const ok = /(subagent-driven|inline executor)/iu.test(handoffBody);
@@ -395,11 +395,46 @@ export interface ArchitectureDiagramValidationResult {
395
395
  * mentioning external-dependency keywords).
396
396
  */
397
397
  export declare function validateArchitectureDiagram(sectionBody: string, context?: ArchitectureDiagramValidationContext): ArchitectureDiagramValidationResult;
398
- export declare function validateTddRedEvidence(sectionBody: string): {
398
+ /**
399
+ * v6.10.0 (T3) — pointer-mode evidence acceptance. RED/GREEN sections may
400
+ * substitute pasted stdout with a single line of the form
401
+ * `Evidence: <relative-or-abs-path>` or `Evidence: spanId:<id>`. The
402
+ * validator alone cannot reach the filesystem or delegation ledger
403
+ * synchronously, so the lint pipeline pre-resolves pointers and then
404
+ * passes booleans through these option flags.
405
+ */
406
+ export interface TddEvidencePointerOptions {
407
+ /**
408
+ * True when the section body has at least one `Evidence:` pointer line
409
+ * AND the pointer resolved to either an existing file or a known
410
+ * delegation spanId. The validator then short-circuits without
411
+ * requiring pasted stdout markers.
412
+ */
413
+ pointerSatisfied?: boolean;
414
+ /**
415
+ * True when `06-tdd-slices.jsonl` contains a slice with the matching
416
+ * output ref (`redOutputRef`/`greenOutputRef`); the markdown evidence
417
+ * block is auto-satisfied because the sidecar is the source of truth.
418
+ */
419
+ sidecarAutoSatisfy?: boolean;
420
+ }
421
+ /**
422
+ * Sync helper that scans for `Evidence:` lines in a section body and
423
+ * returns the trimmed value of each. Used by the lint pipeline to
424
+ * pre-resolve pointers (filesystem path-existence or delegation ledger
425
+ * spanId match) before invoking the validators.
426
+ *
427
+ * Recognised forms:
428
+ * Evidence: <path>
429
+ * Evidence: spanId:<id>
430
+ * - Evidence: <path>
431
+ */
432
+ export declare function extractEvidencePointers(sectionBody: string): string[];
433
+ export declare function validateTddRedEvidence(sectionBody: string, opts?: TddEvidencePointerOptions): {
399
434
  ok: boolean;
400
435
  details: string;
401
436
  };
402
- export declare function validateTddGreenEvidence(sectionBody: string): {
437
+ export declare function validateTddGreenEvidence(sectionBody: string, opts?: TddEvidencePointerOptions): {
403
438
  ok: boolean;
404
439
  details: string;
405
440
  };
@@ -543,6 +578,17 @@ export interface ValidateSectionBodyContext {
543
578
  * in the Architecture Diagram body.
544
579
  */
545
580
  liteTier?: boolean;
581
+ /**
582
+ * v6.10.0 (T3) — pre-resolved RED/GREEN Evidence pointer state. The
583
+ * artifact linter resolves `Evidence: <path|spanId:...>` lines and
584
+ * inspects the TDD slice sidecar before invoking
585
+ * `validateSectionBody`; the resulting booleans here let the
586
+ * validator short-circuit without re-doing async work.
587
+ */
588
+ tddEvidence?: {
589
+ red?: TddEvidencePointerOptions;
590
+ green?: TddEvidencePointerOptions;
591
+ };
546
592
  }
547
593
  export declare function validateSectionBody(sectionBody: string, rule: string, sectionName: string, context?: ValidateSectionBodyContext): {
548
594
  ok: boolean;
@@ -1417,7 +1417,43 @@ function shouldEnforceFailureEdge(diagramBody, context) {
1417
1417
  return true;
1418
1418
  return false;
1419
1419
  }
1420
- export function validateTddRedEvidence(sectionBody) {
1420
+ /**
1421
+ * Sync helper that scans for `Evidence:` lines in a section body and
1422
+ * returns the trimmed value of each. Used by the lint pipeline to
1423
+ * pre-resolve pointers (filesystem path-existence or delegation ledger
1424
+ * spanId match) before invoking the validators.
1425
+ *
1426
+ * Recognised forms:
1427
+ * Evidence: <path>
1428
+ * Evidence: spanId:<id>
1429
+ * - Evidence: <path>
1430
+ */
1431
+ export function extractEvidencePointers(sectionBody) {
1432
+ const pointers = [];
1433
+ const pattern = /^\s*-?\s*evidence\s*:\s*(.+?)\s*$/imu;
1434
+ for (const line of sectionBody.split(/\r?\n/u)) {
1435
+ const match = pattern.exec(line);
1436
+ if (match && match[1] !== undefined) {
1437
+ const value = match[1].trim();
1438
+ if (value.length > 0)
1439
+ pointers.push(value);
1440
+ }
1441
+ }
1442
+ return pointers;
1443
+ }
1444
+ export function validateTddRedEvidence(sectionBody, opts = {}) {
1445
+ if (opts.sidecarAutoSatisfy) {
1446
+ return {
1447
+ ok: true,
1448
+ details: "RED Evidence auto-satisfied: 06-tdd-slices.jsonl carries a redOutputRef for the matching slice."
1449
+ };
1450
+ }
1451
+ if (opts.pointerSatisfied) {
1452
+ return {
1453
+ ok: true,
1454
+ details: "RED Evidence satisfied via `Evidence: <path|spanId:...>` pointer (resolved to an existing artifact or delegation span)."
1455
+ };
1456
+ }
1421
1457
  const meaningful = meaningfulLineCount(sectionBody);
1422
1458
  if (meaningful < 2) {
1423
1459
  return {
@@ -1442,7 +1478,19 @@ export function validateTddRedEvidence(sectionBody) {
1442
1478
  details: "RED Evidence includes command + failing output markers."
1443
1479
  };
1444
1480
  }
1445
- export function validateTddGreenEvidence(sectionBody) {
1481
+ export function validateTddGreenEvidence(sectionBody, opts = {}) {
1482
+ if (opts.sidecarAutoSatisfy) {
1483
+ return {
1484
+ ok: true,
1485
+ details: "GREEN Evidence auto-satisfied: 06-tdd-slices.jsonl carries a greenOutputRef for the matching slice."
1486
+ };
1487
+ }
1488
+ if (opts.pointerSatisfied) {
1489
+ return {
1490
+ ok: true,
1491
+ details: "GREEN Evidence satisfied via `Evidence: <path|spanId:...>` pointer (resolved to an existing artifact or delegation span)."
1492
+ };
1493
+ }
1446
1494
  const meaningful = meaningfulLineCount(sectionBody);
1447
1495
  if (meaningful < 2) {
1448
1496
  return {
@@ -1881,7 +1929,8 @@ export function checkInvestigationTrace(sectionBody) {
1881
1929
  */
1882
1930
  export function evaluateInvestigationTrace(ctx, sectionName) {
1883
1931
  const body = sectionBodyByName(ctx.sections, sectionName);
1884
- const result = checkInvestigationTrace(body);
1932
+ const authoredBody = body === null ? null : extractAuthoredBody(body);
1933
+ const result = checkInvestigationTrace(authoredBody);
1885
1934
  if (result === null)
1886
1935
  return;
1887
1936
  ctx.findings.push({
@@ -2057,10 +2106,10 @@ export function validateSectionBody(sectionBody, rule, sectionName, context = {}
2057
2106
  }
2058
2107
  const sectionNameNormalized = normalizeHeadingTitle(sectionName).toLowerCase();
2059
2108
  if (sectionNameNormalized === "red evidence") {
2060
- return validateTddRedEvidence(sectionBody);
2109
+ return validateTddRedEvidence(sectionBody, context.tddEvidence?.red ?? {});
2061
2110
  }
2062
2111
  if (sectionNameNormalized === "green evidence") {
2063
- return validateTddGreenEvidence(sectionBody);
2112
+ return validateTddGreenEvidence(sectionBody, context.tddEvidence?.green ?? {});
2064
2113
  }
2065
2114
  if (sectionNameNormalized === "verification ladder") {
2066
2115
  return validateVerificationLadder(sectionBody);
@@ -1,2 +1,33 @@
1
+ import { type TddSliceLedgerEntry } from "../tdd-slices.js";
1
2
  import { type StageLintContext } from "./shared.js";
2
3
  export declare function lintTddStage(ctx: StageLintContext): Promise<void>;
4
+ interface ParsedSliceCycleResult {
5
+ ok: boolean;
6
+ details: string;
7
+ }
8
+ /**
9
+ * v6.10.0 (T2) — sidecar-aware Watched-RED check. Validates that every
10
+ * slice currently recorded in `06-tdd-slices.jsonl` (folded latest-row
11
+ * per `sliceId`) has the structural evidence the markdown table would
12
+ * have required: RED observation timestamp, test file, test command,
13
+ * and at least one claimed path.
14
+ */
15
+ export declare function evaluateSidecarWatchedRed(rawEntries: TddSliceLedgerEntry[]): ParsedSliceCycleResult;
16
+ /**
17
+ * v6.10.0 (T2) — sidecar-aware Vertical Slice Cycle check. Each slice
18
+ * must have a row whose effective status (latest by sliceId) implies a
19
+ * monotonic progression. Once a slice carries `greenAt`, the linter
20
+ * requires `greenAt >= redObservedAt`. `refactor-deferred` requires a
21
+ * non-empty `refactorRationale`. `refactor-done` requires a `refactorAt`
22
+ * that is `>= greenAt`. Slices stuck at `red` are tolerated only when
23
+ * the run is still in TDD; the linter surfaces them as advisory through
24
+ * the watched-RED check above.
25
+ */
26
+ export declare function evaluateSidecarSliceCycle(rawEntries: TddSliceLedgerEntry[]): ParsedSliceCycleResult;
27
+ export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
28
+ interface VerificationLadderResult {
29
+ ok: boolean;
30
+ details: string;
31
+ }
32
+ export declare function evaluateVerificationLadder(body: string | null): VerificationLadderResult;
33
+ export {};
@@ -1,9 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { readDelegationLedger } from "../delegation.js";
4
+ import { foldTddSliceLedger, readTddSliceLedger } from "../tdd-slices.js";
4
5
  import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
5
6
  export async function lintTddStage(ctx) {
6
7
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
8
+ const sliceLedger = await readTddSliceLedger(projectRoot);
9
+ const sidecarActive = sliceLedger.entries.length > 0;
7
10
  evaluateInvestigationTrace(ctx, "Watched-RED Proof");
8
11
  // Universal Layer 2.6 structural checks (superpowers TDD + evanflow vertical slices).
9
12
  const ironLawBody = sectionBodyByName(sections, "Iron Law Acknowledgement");
@@ -29,7 +32,21 @@ export async function lintTddStage(ctx) {
29
32
  });
30
33
  }
31
34
  const watchedRedBody = sectionBodyByName(sections, "Watched-RED Proof");
32
- if (watchedRedBody === null) {
35
+ if (sidecarActive) {
36
+ // v6.10.0 (T2): when 06-tdd-slices.jsonl carries rows, the sidecar is
37
+ // the source of truth for RED observation evidence; the markdown
38
+ // table is auto-derived (or left as a template stub) and must not
39
+ // block stage advance.
40
+ const sidecarResult = evaluateSidecarWatchedRed(sliceLedger.entries);
41
+ findings.push({
42
+ section: "Watched-RED Proof Shape",
43
+ required: true,
44
+ rule: "Watched-RED Proof: when 06-tdd-slices.jsonl is present, every slice row with status >= red must include redObservedAt, testFile, testCommand, and claimedPaths.",
45
+ found: sidecarResult.ok,
46
+ details: sidecarResult.details
47
+ });
48
+ }
49
+ else if (watchedRedBody === null) {
33
50
  findings.push({
34
51
  section: "Watched-RED Proof Shape",
35
52
  required: true,
@@ -63,28 +80,55 @@ export async function lintTddStage(ctx) {
63
80
  : `${populatedRows.length - validProofRows.length} watched-RED proof row(s) lack an ISO timestamp.`
64
81
  });
65
82
  }
66
- const sliceCycleBody = sectionBodyByName(sections, "Vertical Slice Cycle");
67
- if (sliceCycleBody === null) {
83
+ if (sidecarActive) {
84
+ const cycleResult = evaluateSidecarSliceCycle(sliceLedger.entries);
68
85
  findings.push({
69
86
  section: "Vertical Slice Cycle Coverage",
70
87
  required: true,
71
- rule: "Vertical Slice Cycle must include RED, GREEN, and REFACTOR per slice (refactor may be deferred with rationale).",
72
- found: false,
73
- details: "No ## heading matching required section \"Vertical Slice Cycle\"."
88
+ rule: "Vertical Slice Cycle: 06-tdd-slices.jsonl rows must show RED -> GREEN monotonic progression per slice; REFACTOR is satisfied by `refactor-done` (with refactorAt > greenAt) or `refactor-deferred` (with non-empty refactorRationale).",
89
+ found: cycleResult.ok,
90
+ details: cycleResult.details
74
91
  });
75
92
  }
76
93
  else {
77
- const required = ["RED", "GREEN", "REFACTOR"];
78
- const missing = required.filter((token) => !new RegExp(token, "u").test(sliceCycleBody));
79
- findings.push({
80
- section: "Vertical Slice Cycle Coverage",
81
- required: true,
82
- rule: "Vertical Slice Cycle must include RED, GREEN, and REFACTOR per slice (refactor may be deferred with rationale).",
83
- found: missing.length === 0,
84
- details: missing.length === 0
85
- ? "Vertical Slice Cycle references RED/GREEN/REFACTOR."
86
- : `Vertical Slice Cycle is missing phase token(s): ${missing.join(", ")}.`
87
- });
94
+ const sliceCycleBody = sectionBodyByName(sections, "Vertical Slice Cycle");
95
+ if (sliceCycleBody === null) {
96
+ findings.push({
97
+ section: "Vertical Slice Cycle Coverage",
98
+ required: true,
99
+ rule: "Vertical Slice Cycle must include RED, GREEN, and REFACTOR per slice (refactor may be deferred with rationale).",
100
+ found: false,
101
+ details: "No ## heading matching required section \"Vertical Slice Cycle\"."
102
+ });
103
+ }
104
+ else {
105
+ const cycleResult = parseVerticalSliceCycle(sliceCycleBody);
106
+ findings.push({
107
+ section: "Vertical Slice Cycle Coverage",
108
+ required: true,
109
+ rule: "Vertical Slice Cycle must show RED -> GREEN -> REFACTOR monotonic progression per slice (refactor may be deferred with one-line rationale, e.g. `deferred because <reason>`).",
110
+ found: cycleResult.ok,
111
+ details: cycleResult.details
112
+ });
113
+ }
114
+ }
115
+ if (!sidecarActive) {
116
+ // Advisory nudge: stage finished without ever migrating to the sidecar.
117
+ // Detect "filled markdown" by checking whether the Watched-RED Proof
118
+ // table or Vertical Slice Cycle has any populated rows.
119
+ const sliceCycleBodyAdvisory = sectionBodyByName(sections, "Vertical Slice Cycle");
120
+ const watchedRedBodyAdvisory = sectionBodyByName(sections, "Watched-RED Proof");
121
+ const markdownIsAuthored = hasPopulatedTableRows(watchedRedBodyAdvisory) ||
122
+ hasPopulatedTableRows(sliceCycleBodyAdvisory);
123
+ if (markdownIsAuthored) {
124
+ findings.push({
125
+ section: "tdd_slice_ledger_missing",
126
+ required: false,
127
+ rule: "When markdown TDD tables are filled, prefer recording slice events via `cclaw-cli internal tdd-slice-record` so 06-tdd-slices.jsonl becomes the source of truth.",
128
+ found: false,
129
+ details: "06-tdd-slices.jsonl is empty even though the markdown tables are populated. Migrate this stage's slices to the sidecar with `cclaw-cli internal tdd-slice-record --slice <id> --status <red|green|refactor-done|refactor-deferred> ...`."
130
+ });
131
+ }
88
132
  }
89
133
  const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
90
134
  if (assertionBody !== null) {
@@ -196,4 +240,300 @@ export async function lintTddStage(ctx) {
196
240
  : "integration-overseer completion exists, but PASS/PASS_WITH_GAPS evidence is missing in delegation evidenceRefs and artifact text."
197
241
  });
198
242
  }
243
+ {
244
+ const verificationBody = sectionBodyByName(sections, "Verification Ladder") ??
245
+ sectionBodyByName(sections, "Verification Status") ??
246
+ sectionBodyByName(sections, "Verification");
247
+ const ladderResult = evaluateVerificationLadder(verificationBody);
248
+ findings.push({
249
+ section: "tdd_verification_pending",
250
+ required: true,
251
+ rule: "Verification Ladder rows must not remain `pending`; promote each row to `passed`, `n/a`, `failed`, `skipped`, or `deferred` (with rationale) before stage-complete.",
252
+ found: ladderResult.ok,
253
+ details: ladderResult.details
254
+ });
255
+ }
256
+ }
257
+ /**
258
+ * v6.10.0 (T2) — sidecar-aware Watched-RED check. Validates that every
259
+ * slice currently recorded in `06-tdd-slices.jsonl` (folded latest-row
260
+ * per `sliceId`) has the structural evidence the markdown table would
261
+ * have required: RED observation timestamp, test file, test command,
262
+ * and at least one claimed path.
263
+ */
264
+ export function evaluateSidecarWatchedRed(rawEntries) {
265
+ if (rawEntries.length === 0) {
266
+ return {
267
+ ok: false,
268
+ details: "Sidecar 06-tdd-slices.jsonl is empty; record at least one slice via `cclaw-cli internal tdd-slice-record`."
269
+ };
270
+ }
271
+ const folded = foldTddSliceLedger(rawEntries);
272
+ const errors = [];
273
+ for (const entry of folded) {
274
+ const issues = [];
275
+ if (!entry.redObservedAt || entry.redObservedAt.trim().length === 0) {
276
+ issues.push("missing redObservedAt");
277
+ }
278
+ else if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/u.test(entry.redObservedAt)) {
279
+ issues.push("redObservedAt is not an ISO timestamp");
280
+ }
281
+ if (!entry.testFile || entry.testFile.length === 0) {
282
+ issues.push("missing testFile");
283
+ }
284
+ if (!entry.testCommand || entry.testCommand.length === 0) {
285
+ issues.push("missing testCommand");
286
+ }
287
+ if (!Array.isArray(entry.claimedPaths) || entry.claimedPaths.length === 0) {
288
+ issues.push("missing claimedPaths");
289
+ }
290
+ if (issues.length > 0) {
291
+ errors.push(`${entry.sliceId}: ${issues.join(", ")}`);
292
+ }
293
+ }
294
+ if (errors.length > 0) {
295
+ return {
296
+ ok: false,
297
+ details: `Sidecar slice rows missing RED evidence fields: ${errors.join(" | ")}.`
298
+ };
299
+ }
300
+ return {
301
+ ok: true,
302
+ details: `Sidecar 06-tdd-slices.jsonl has ${folded.length} slice row(s) with required RED evidence fields.`
303
+ };
304
+ }
305
+ /**
306
+ * v6.10.0 (T2) — sidecar-aware Vertical Slice Cycle check. Each slice
307
+ * must have a row whose effective status (latest by sliceId) implies a
308
+ * monotonic progression. Once a slice carries `greenAt`, the linter
309
+ * requires `greenAt >= redObservedAt`. `refactor-deferred` requires a
310
+ * non-empty `refactorRationale`. `refactor-done` requires a `refactorAt`
311
+ * that is `>= greenAt`. Slices stuck at `red` are tolerated only when
312
+ * the run is still in TDD; the linter surfaces them as advisory through
313
+ * the watched-RED check above.
314
+ */
315
+ export function evaluateSidecarSliceCycle(rawEntries) {
316
+ if (rawEntries.length === 0) {
317
+ return {
318
+ ok: false,
319
+ details: "Sidecar 06-tdd-slices.jsonl is empty; the vertical slice cycle has no rows."
320
+ };
321
+ }
322
+ const folded = foldTddSliceLedger(rawEntries);
323
+ const errors = [];
324
+ for (const entry of folded) {
325
+ if (entry.greenAt) {
326
+ const redIso = parseTimestampCell(entry.redObservedAt ?? "");
327
+ const greenIso = parseTimestampCell(entry.greenAt);
328
+ if (greenIso === null) {
329
+ errors.push(`${entry.sliceId}: greenAt is not an ISO timestamp.`);
330
+ continue;
331
+ }
332
+ if (redIso !== null && greenIso < redIso) {
333
+ errors.push(`${entry.sliceId}: greenAt (${entry.greenAt}) precedes redObservedAt (${entry.redObservedAt}) — order must be monotonic.`);
334
+ continue;
335
+ }
336
+ }
337
+ if (entry.status === "refactor-deferred") {
338
+ if (!entry.refactorRationale || entry.refactorRationale.trim().length === 0) {
339
+ errors.push(`${entry.sliceId}: status=refactor-deferred requires a non-empty refactorRationale.`);
340
+ continue;
341
+ }
342
+ }
343
+ if (entry.status === "refactor-done") {
344
+ const greenIso = parseTimestampCell(entry.greenAt ?? "");
345
+ const refactorIso = parseTimestampCell(entry.refactorAt ?? "");
346
+ if (refactorIso === null) {
347
+ errors.push(`${entry.sliceId}: status=refactor-done requires a refactorAt ISO timestamp.`);
348
+ continue;
349
+ }
350
+ if (greenIso !== null && refactorIso < greenIso) {
351
+ errors.push(`${entry.sliceId}: refactorAt (${entry.refactorAt}) precedes greenAt (${entry.greenAt}) — order must be monotonic.`);
352
+ continue;
353
+ }
354
+ }
355
+ }
356
+ if (errors.length > 0) {
357
+ return { ok: false, details: errors.join(" ") };
358
+ }
359
+ return {
360
+ ok: true,
361
+ details: `${folded.length} sidecar slice row(s) show monotonic RED -> GREEN -> REFACTOR (deferred-with-rationale accepted).`
362
+ };
363
+ }
364
+ function hasPopulatedTableRows(body) {
365
+ if (body === null)
366
+ return false;
367
+ const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
368
+ if (tableLines.length < 3)
369
+ return false;
370
+ const dataRows = tableLines.slice(2);
371
+ for (const row of dataRows) {
372
+ const cells = row
373
+ .split("|")
374
+ .slice(1, -1)
375
+ .map((cell) => cell.trim());
376
+ // Skip cells that are entirely placeholder slice IDs (S-1 default).
377
+ const meaningful = cells.filter((cell, idx) => idx !== 0 && cell.length > 0);
378
+ if (meaningful.length > 0)
379
+ return true;
380
+ }
381
+ return false;
382
+ }
383
+ export function parseVerticalSliceCycle(body) {
384
+ const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
385
+ if (tableLines.length < 3) {
386
+ return {
387
+ ok: false,
388
+ details: "Vertical Slice Cycle table must have a header, separator, and at least one slice row."
389
+ };
390
+ }
391
+ const headerCells = splitMarkdownRow(tableLines[0]).map((cell) => cell.toLowerCase());
392
+ const findIdx = (token) => headerCells.findIndex((cell) => cell.includes(token));
393
+ const sliceIdx = findIdx("slice");
394
+ const redIdx = findIdx("red");
395
+ const greenIdx = findIdx("green");
396
+ const refactorIdx = findIdx("refactor");
397
+ if (sliceIdx < 0 || redIdx < 0 || greenIdx < 0 || refactorIdx < 0) {
398
+ return {
399
+ ok: false,
400
+ details: "Vertical Slice Cycle header must include Slice, RED, GREEN, and REFACTOR columns."
401
+ };
402
+ }
403
+ const dataRows = tableLines.slice(2);
404
+ const populated = dataRows.filter((row) => splitMarkdownRow(row).some((cell) => cell.length > 0));
405
+ if (populated.length === 0) {
406
+ return {
407
+ ok: false,
408
+ details: "Vertical Slice Cycle has no populated slice rows."
409
+ };
410
+ }
411
+ const errors = [];
412
+ for (const row of populated) {
413
+ const cells = splitMarkdownRow(row);
414
+ const slice = cells[sliceIdx] ?? "";
415
+ const red = cells[redIdx] ?? "";
416
+ const green = cells[greenIdx] ?? "";
417
+ const refactor = cells[refactorIdx] ?? "";
418
+ const label = slice.length > 0 ? slice : `row ${populated.indexOf(row) + 1}`;
419
+ const redTs = parseTimestampCell(red);
420
+ const greenTs = parseTimestampCell(green);
421
+ if (red.length === 0) {
422
+ errors.push(`${label}: RED ts is empty.`);
423
+ continue;
424
+ }
425
+ if (green.length === 0) {
426
+ errors.push(`${label}: GREEN ts is empty.`);
427
+ continue;
428
+ }
429
+ if (redTs === null) {
430
+ errors.push(`${label}: RED ts \`${red}\` is not an ISO timestamp.`);
431
+ continue;
432
+ }
433
+ if (greenTs === null) {
434
+ errors.push(`${label}: GREEN ts \`${green}\` is not an ISO timestamp.`);
435
+ continue;
436
+ }
437
+ if (greenTs < redTs) {
438
+ errors.push(`${label}: GREEN (${green}) precedes RED (${red}) — order must be monotonic.`);
439
+ continue;
440
+ }
441
+ if (refactor.length === 0) {
442
+ errors.push(`${label}: REFACTOR cell is empty; provide a timestamp or \`deferred because <reason>\`.`);
443
+ continue;
444
+ }
445
+ if (isDeferredOrNotNeeded(refactor)) {
446
+ const rationale = extractDeferRationale(refactor);
447
+ if (rationale.length === 0) {
448
+ errors.push(`${label}: REFACTOR marked deferred/not-needed but rationale is missing — use \`deferred because <reason>\` or \`not needed because <reason>\`.`);
449
+ }
450
+ continue;
451
+ }
452
+ const refactorTs = parseTimestampCell(refactor);
453
+ if (refactorTs === null) {
454
+ errors.push(`${label}: REFACTOR cell \`${refactor}\` is not an ISO timestamp and not marked deferred/not-needed with rationale.`);
455
+ continue;
456
+ }
457
+ if (refactorTs < greenTs) {
458
+ errors.push(`${label}: REFACTOR (${refactor}) precedes GREEN (${green}) — order must be monotonic.`);
459
+ continue;
460
+ }
461
+ }
462
+ if (errors.length > 0) {
463
+ return { ok: false, details: errors.join(" ") };
464
+ }
465
+ return {
466
+ ok: true,
467
+ details: `${populated.length} slice row(s) show monotonic RED -> GREEN -> REFACTOR (deferred-with-rationale accepted).`
468
+ };
469
+ }
470
+ function splitMarkdownRow(line) {
471
+ const trimmed = line.trim();
472
+ if (!trimmed.startsWith("|"))
473
+ return [];
474
+ const inner = trimmed.replace(/^\|/u, "").replace(/\|$/u, "");
475
+ return inner.split("|").map((cell) => cell.trim());
476
+ }
477
+ function parseTimestampCell(cell) {
478
+ const trimmed = cell.replace(/^[`*_\s]+|[`*_\s]+$/gu, "");
479
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/u.test(trimmed))
480
+ return null;
481
+ const t = Date.parse(trimmed);
482
+ return Number.isFinite(t) ? t : null;
483
+ }
484
+ function isDeferredOrNotNeeded(cell) {
485
+ return /\b(deferred|not[\s-]?needed|n\/?a|skipped)\b/iu.test(cell);
486
+ }
487
+ function extractDeferRationale(cell) {
488
+ const cleaned = cell.replace(/`/gu, "").trim();
489
+ const match = /(?:deferred|not[\s-]?needed|skipped)\s+(?:because|since|due to|—|-)\s*(.+)/iu.exec(cleaned);
490
+ if (match !== null && match[1] !== undefined && match[1].trim().length > 0) {
491
+ return match[1].trim();
492
+ }
493
+ // Accept any free-form rationale text following the deferral marker.
494
+ const fallback = cleaned.replace(/^\s*(deferred|not[\s-]?needed|skipped|n\/?a)\b[:\s-]*/iu, "").trim();
495
+ return fallback;
496
+ }
497
+ export function evaluateVerificationLadder(body) {
498
+ if (body === null) {
499
+ return {
500
+ ok: true,
501
+ details: "No Verification Ladder section present; rule advisory."
502
+ };
503
+ }
504
+ const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
505
+ if (tableLines.length < 3) {
506
+ return {
507
+ ok: true,
508
+ details: "Verification Ladder section has no table rows; rule advisory."
509
+ };
510
+ }
511
+ const dataRows = tableLines.slice(2);
512
+ const pendingRows = [];
513
+ for (const row of dataRows) {
514
+ const cells = splitMarkdownRow(row);
515
+ if (cells.length === 0)
516
+ continue;
517
+ if (cells.every((cell) => cell.length === 0))
518
+ continue;
519
+ const cellsLower = cells.map((cell) => cell.toLowerCase().replace(/`/gu, "").trim());
520
+ const hasPending = cellsLower.some((cell) => /\bpending\b/u.test(cell));
521
+ if (hasPending) {
522
+ const label = cells[0] !== undefined && cells[0].length > 0
523
+ ? cells[0]
524
+ : `row ${dataRows.indexOf(row) + 1}`;
525
+ pendingRows.push(label);
526
+ }
527
+ }
528
+ if (pendingRows.length === 0) {
529
+ return {
530
+ ok: true,
531
+ details: "Verification Ladder has no rows still marked `pending`."
532
+ };
533
+ }
534
+ return {
535
+ ok: false,
536
+ details: `Verification Ladder has ${pendingRows.length} row(s) still marked \`pending\`: ${pendingRows.join(", ")}. ` +
537
+ "Promote each to `passed`, `n/a`, `failed`, `skipped`, or `deferred` (with rationale) before stage-complete."
538
+ };
199
539
  }