cclaw-cli 6.9.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/plan.js +37 -0
- package/dist/artifact-linter/shared.d.ts +48 -2
- package/dist/artifact-linter/shared.js +52 -4
- package/dist/artifact-linter/tdd.d.ts +20 -0
- package/dist/artifact-linter/tdd.js +187 -14
- package/dist/artifact-linter.js +87 -2
- package/dist/content/examples.js +9 -9
- package/dist/content/hooks.js +135 -2
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/skills.js +1 -1
- package/dist/content/stages/tdd.js +6 -8
- package/dist/content/subagents.js +9 -1
- package/dist/content/templates.js +5 -15
- package/dist/delegation.d.ts +92 -0
- package/dist/delegation.js +159 -10
- package/dist/internal/advance-stage.js +19 -3
- package/dist/internal/plan-split-waves.d.ts +66 -0
- package/dist/internal/plan-split-waves.js +249 -0
- package/dist/tdd-slices.d.ts +90 -0
- package/dist/tdd-slices.js +375 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
@@ -2058,10 +2106,10 @@ export function validateSectionBody(sectionBody, rule, sectionName, context = {}
|
|
|
2058
2106
|
}
|
|
2059
2107
|
const sectionNameNormalized = normalizeHeadingTitle(sectionName).toLowerCase();
|
|
2060
2108
|
if (sectionNameNormalized === "red evidence") {
|
|
2061
|
-
return validateTddRedEvidence(sectionBody);
|
|
2109
|
+
return validateTddRedEvidence(sectionBody, context.tddEvidence?.red ?? {});
|
|
2062
2110
|
}
|
|
2063
2111
|
if (sectionNameNormalized === "green evidence") {
|
|
2064
|
-
return validateTddGreenEvidence(sectionBody);
|
|
2112
|
+
return validateTddGreenEvidence(sectionBody, context.tddEvidence?.green ?? {});
|
|
2065
2113
|
}
|
|
2066
2114
|
if (sectionNameNormalized === "verification ladder") {
|
|
2067
2115
|
return validateVerificationLadder(sectionBody);
|
|
@@ -1,9 +1,29 @@
|
|
|
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>;
|
|
3
4
|
interface ParsedSliceCycleResult {
|
|
4
5
|
ok: boolean;
|
|
5
6
|
details: string;
|
|
6
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;
|
|
7
27
|
export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
|
|
8
28
|
interface VerificationLadderResult {
|
|
9
29
|
ok: boolean;
|
|
@@ -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,26 +80,56 @@ 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: false,
|
|
73
|
-
details: "No ## heading matching required section \"Vertical Slice Cycle\"."
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
const cycleResult = parseVerticalSliceCycle(sliceCycleBody);
|
|
78
|
-
findings.push({
|
|
79
|
-
section: "Vertical Slice Cycle Coverage",
|
|
80
|
-
required: true,
|
|
81
|
-
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>`).",
|
|
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).",
|
|
82
89
|
found: cycleResult.ok,
|
|
83
90
|
details: cycleResult.details
|
|
84
91
|
});
|
|
85
92
|
}
|
|
93
|
+
else {
|
|
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
|
+
}
|
|
132
|
+
}
|
|
86
133
|
const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
|
|
87
134
|
if (assertionBody !== null) {
|
|
88
135
|
const tableRows = assertionBody.split("\n").filter((line) => /^\|/u.test(line));
|
|
@@ -207,6 +254,132 @@ export async function lintTddStage(ctx) {
|
|
|
207
254
|
});
|
|
208
255
|
}
|
|
209
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
|
+
}
|
|
210
383
|
export function parseVerticalSliceCycle(body) {
|
|
211
384
|
const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
|
|
212
385
|
if (tableLines.length < 3) {
|
package/dist/artifact-linter.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
|
|
3
4
|
import { exists } from "./fs-utils.js";
|
|
4
5
|
import { stageSchema } from "./content/stage-schema.js";
|
|
5
6
|
import { readFlowState } from "./run-persistence.js";
|
|
6
|
-
import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
|
|
7
|
+
import { duplicateH2Headings, extractEvidencePointers, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
|
|
8
|
+
import { foldTddSliceLedger, readTddSliceLedger } from "./tdd-slices.js";
|
|
7
9
|
import { shouldDemoteArtifactValidationByTrack } from "./content/stage-schema.js";
|
|
8
10
|
import { readDelegationLedger, recordArtifactValidationDemotedByTrack } from "./delegation.js";
|
|
9
11
|
import { classifyAndPersistFindings } from "./artifact-linter/findings-dedup.js";
|
|
@@ -145,6 +147,17 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
const liteTierForValidators = shouldDemoteArtifactValidationByTrack(track, taskClass);
|
|
150
|
+
// v6.10.0 (T3) — pre-resolve RED/GREEN Evidence pointers and sidecar
|
|
151
|
+
// auto-satisfy state once for the whole TDD loop, then thread the
|
|
152
|
+
// booleans through `validateSectionBody`. We do the async resolution
|
|
153
|
+
// here (path existence + delegation spanId match) so the validators
|
|
154
|
+
// themselves stay sync.
|
|
155
|
+
const tddEvidenceContext = stage === "tdd"
|
|
156
|
+
? await resolveTddEvidencePointerContext({
|
|
157
|
+
projectRoot,
|
|
158
|
+
sections
|
|
159
|
+
})
|
|
160
|
+
: { red: {}, green: {} };
|
|
148
161
|
for (const v of schema.artifactValidation) {
|
|
149
162
|
const sectionKey = normalizeHeadingTitle(v.section).toLowerCase();
|
|
150
163
|
const scopeBoundaryAlias = stage === "scope" && sectionKey === "in scope / out of scope";
|
|
@@ -164,7 +177,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
164
177
|
? { ok: false, details: `No ## heading matching required section "${v.section}".` }
|
|
165
178
|
: validateSectionBody(body, v.validationRule, v.section, {
|
|
166
179
|
sections,
|
|
167
|
-
liteTier: liteTierForValidators
|
|
180
|
+
liteTier: liteTierForValidators,
|
|
181
|
+
tddEvidence: stage === "tdd" ? tddEvidenceContext : undefined
|
|
168
182
|
});
|
|
169
183
|
const found = hasHeading && validation.ok;
|
|
170
184
|
findings.push({
|
|
@@ -420,3 +434,74 @@ const ARTIFACT_VALIDATION_LITE_DEMOTE_SECTIONS = new Set([
|
|
|
420
434
|
"Stale Diagram Drift Check",
|
|
421
435
|
"Product Discovery Delegation (Strategist Mode)"
|
|
422
436
|
]);
|
|
437
|
+
/**
|
|
438
|
+
* v6.10.0 (T3) — pre-resolve `Evidence:` pointers and sidecar
|
|
439
|
+
* auto-satisfy state for the TDD stage's RED/GREEN Evidence rows so
|
|
440
|
+
* `validateSectionBody` (sync) can short-circuit. A pointer of the form
|
|
441
|
+
* `<path>` is satisfied when the path exists on disk relative to the
|
|
442
|
+
* project root; `spanId:<id>` is satisfied when any delegation ledger
|
|
443
|
+
* row carries that span id. Sidecar auto-satisfy fires when
|
|
444
|
+
* `06-tdd-slices.jsonl` carries at least one slice with a non-empty
|
|
445
|
+
* `redOutputRef` / `greenOutputRef`.
|
|
446
|
+
*/
|
|
447
|
+
async function resolveTddEvidencePointerContext(input) {
|
|
448
|
+
const { projectRoot, sections } = input;
|
|
449
|
+
const redSection = sectionBodyByName(sections, "RED Evidence") ?? "";
|
|
450
|
+
const greenSection = sectionBodyByName(sections, "GREEN Evidence") ?? "";
|
|
451
|
+
const redPointers = extractEvidencePointers(redSection);
|
|
452
|
+
const greenPointers = extractEvidencePointers(greenSection);
|
|
453
|
+
let knownSpanIds = new Set();
|
|
454
|
+
if (redPointers.length > 0 || greenPointers.length > 0) {
|
|
455
|
+
try {
|
|
456
|
+
const ledger = await readDelegationLedger(projectRoot);
|
|
457
|
+
knownSpanIds = new Set(ledger.entries
|
|
458
|
+
.map((entry) => entry.spanId)
|
|
459
|
+
.filter((id) => typeof id === "string" && id.length > 0));
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
knownSpanIds = new Set();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function pointerResolves(value) {
|
|
466
|
+
const trimmed = value.replace(/[`*_]/gu, "").trim();
|
|
467
|
+
if (trimmed.length === 0)
|
|
468
|
+
return false;
|
|
469
|
+
if (/^spanid\s*:/iu.test(trimmed)) {
|
|
470
|
+
const id = trimmed.replace(/^spanid\s*:\s*/iu, "").trim();
|
|
471
|
+
return id.length > 0 && knownSpanIds.has(id);
|
|
472
|
+
}
|
|
473
|
+
const candidate = path.isAbsolute(trimmed) ? trimmed : path.join(projectRoot, trimmed);
|
|
474
|
+
return exists(candidate);
|
|
475
|
+
}
|
|
476
|
+
async function anyResolved(values) {
|
|
477
|
+
for (const value of values) {
|
|
478
|
+
if (await pointerResolves(value))
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
let redSidecarAutoSatisfy = false;
|
|
484
|
+
let greenSidecarAutoSatisfy = false;
|
|
485
|
+
try {
|
|
486
|
+
const sidecar = await readTddSliceLedger(projectRoot);
|
|
487
|
+
if (sidecar.entries.length > 0) {
|
|
488
|
+
const folded = foldTddSliceLedger(sidecar.entries);
|
|
489
|
+
redSidecarAutoSatisfy = folded.some((entry) => typeof entry.redOutputRef === "string" && entry.redOutputRef.length > 0);
|
|
490
|
+
greenSidecarAutoSatisfy = folded.some((entry) => typeof entry.greenOutputRef === "string" && entry.greenOutputRef.length > 0);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
redSidecarAutoSatisfy = false;
|
|
495
|
+
greenSidecarAutoSatisfy = false;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
red: {
|
|
499
|
+
pointerSatisfied: await anyResolved(redPointers),
|
|
500
|
+
sidecarAutoSatisfy: redSidecarAutoSatisfy
|
|
501
|
+
},
|
|
502
|
+
green: {
|
|
503
|
+
pointerSatisfied: await anyResolved(greenPointers),
|
|
504
|
+
sidecarAutoSatisfy: greenSidecarAutoSatisfy
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
package/dist/content/examples.js
CHANGED
|
@@ -36,10 +36,10 @@ export const BEHAVIOR_ANCHORS = [
|
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
stage: "tdd",
|
|
39
|
-
section: "RED
|
|
40
|
-
bad: "
|
|
41
|
-
good: "
|
|
42
|
-
ruleHint: "
|
|
39
|
+
section: "Watched-RED Proof",
|
|
40
|
+
bad: "Hand-edit `S-1 | 2026-04-15T10:00 | observed RED` into the markdown table; nothing lands in the JSONL sidecar, so retries silently overwrite the row.",
|
|
41
|
+
good: "Run `cclaw-cli internal tdd-slice-record --slice S-1 --status red --test-file tests/feed.spec.ts --command \"npm test\" --paths src/api/feed.ts --ac AC-3`; the linter reads the sidecar.",
|
|
42
|
+
ruleHint: "RED/GREEN/REFACTOR transitions are recorded by `cclaw-cli internal tdd-slice-record`; the markdown tables are an auto-derived view from v6.10.0 onward."
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
stage: "review",
|
|
@@ -247,12 +247,12 @@ Plan is ready to execute after user confirmation.
|
|
|
247
247
|
| S-1 feed window | expected 30d window, got 7d |
|
|
248
248
|
| S-2 degraded banner | banner absent after forced disconnect |
|
|
249
249
|
|
|
250
|
-
## Acceptance
|
|
250
|
+
## Acceptance & Failure Map
|
|
251
251
|
|
|
252
|
-
| Slice | AC
|
|
253
|
-
| --- | --- |
|
|
254
|
-
| S-1 | AC-1 |
|
|
255
|
-
| S-2 | AC-3 |
|
|
252
|
+
| Slice | Source ID | AC ID | Expected behavior | RED-link |
|
|
253
|
+
| --- | --- | --- | --- | --- |
|
|
254
|
+
| S-1 | SRC-1 | AC-1 | feed window honors 30d cap | spanId:tdd-feed-window-red |
|
|
255
|
+
| S-2 | SRC-2 | AC-3 | degraded banner appears on disconnect | .cclaw/artifacts/06-tdd-slices.jsonl |
|
|
256
256
|
|
|
257
257
|
## GREEN
|
|
258
258
|
|