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.
- package/dist/artifact-linter/design.js +1 -1
- package/dist/artifact-linter/plan.js +37 -0
- package/dist/artifact-linter/shared.d.ts +48 -2
- package/dist/artifact-linter/shared.js +54 -5
- package/dist/artifact-linter/tdd.d.ts +31 -0
- package/dist/artifact-linter/tdd.js +357 -17
- package/dist/artifact-linter.js +87 -2
- package/dist/content/examples.js +9 -9
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/hooks.js +140 -3
- package/dist/content/iron-laws.js +6 -2
- package/dist/content/node-hooks.js +15 -1308
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/skills-elicitation.js +2 -2
- package/dist/content/skills.js +1 -1
- package/dist/content/stages/brainstorm.js +2 -2
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/scope.js +2 -2
- package/dist/content/stages/tdd.js +7 -8
- package/dist/content/subagents.js +20 -2
- package/dist/content/templates.js +5 -15
- package/dist/delegation.d.ts +102 -3
- package/dist/delegation.js +172 -14
- package/dist/early-loop.js +15 -1
- package/dist/gate-evidence.js +15 -23
- package/dist/harness-adapters.js +4 -2
- package/dist/install.js +37 -221
- package/dist/internal/advance-stage.js +19 -3
- package/dist/internal/detect-supply-chain-changes.d.ts +6 -0
- package/dist/internal/detect-supply-chain-changes.js +138 -0
- package/dist/internal/flow-state-repair.d.ts +7 -0
- package/dist/internal/flow-state-repair.js +57 -18
- package/dist/internal/plan-split-waves.d.ts +66 -0
- package/dist/internal/plan-split-waves.js +249 -0
- package/dist/run-persistence.d.ts +2 -0
- package/dist/run-persistence.js +62 -3
- package/dist/runtime/run-hook.mjs +44 -8729
- package/dist/tdd-slices.d.ts +90 -0
- package/dist/tdd-slices.js +375 -0
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
72
|
-
found:
|
|
73
|
-
details:
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|