cclaw-cli 0.48.18 → 0.48.19
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/internal/tdd-red-evidence.js +28 -2
- package/dist/tdd-cycle.d.ts +22 -1
- package/dist/tdd-cycle.js +62 -26
- package/package.json +1 -1
|
@@ -89,14 +89,40 @@ function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
|
|
|
89
89
|
export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
|
|
90
90
|
const args = parseArgs(tokens);
|
|
91
91
|
const flowState = await readFlowState(projectRoot).catch(() => null);
|
|
92
|
+
// Strict runId scoping: a previous implementation fell back to no
|
|
93
|
+
// filter when both `--runId` and `flowState.activeRunId` were missing,
|
|
94
|
+
// which let evidence rows from past runs satisfy the current check
|
|
95
|
+
// (false positive). Now: require an explicit or inferred runId or
|
|
96
|
+
// fail loud so the caller cannot silently inherit cross-run state.
|
|
92
97
|
const effectiveRunId = args.runId ?? flowState?.activeRunId;
|
|
98
|
+
if (!effectiveRunId || effectiveRunId.trim().length === 0) {
|
|
99
|
+
const reason = "tdd-red-evidence: cannot scope check — no --runId provided and " +
|
|
100
|
+
"flow-state.json has no activeRunId. Pass --runId=<id> explicitly " +
|
|
101
|
+
"or run `cclaw doctor` to reconcile state.";
|
|
102
|
+
if (!args.quiet) {
|
|
103
|
+
io.stdout.write(`${JSON.stringify({
|
|
104
|
+
ok: false,
|
|
105
|
+
path: args.targetPath,
|
|
106
|
+
runId: null,
|
|
107
|
+
error: reason,
|
|
108
|
+
sources: { tddCycleLog: false, autoEvidence: false }
|
|
109
|
+
}, null, 2)}\n`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
io.stderr.write(`${reason}\n`);
|
|
113
|
+
}
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
93
116
|
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
94
117
|
const autoEvidencePath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-red-evidence.jsonl");
|
|
95
118
|
let cycleLogHasRed = false;
|
|
96
119
|
let autoEvidenceHasRed = false;
|
|
97
120
|
try {
|
|
98
121
|
const raw = await fs.readFile(tddLogPath, "utf8");
|
|
99
|
-
|
|
122
|
+
// Strict parse: drop malformed/underspecified rows rather than
|
|
123
|
+
// backfilling runId=active / stage=tdd defaults, which used to
|
|
124
|
+
// silently glue foreign entries to the current run.
|
|
125
|
+
const entries = parseTddCycleLog(raw, { strict: true });
|
|
100
126
|
cycleLogHasRed = hasFailingTestForPath(entries, args.targetPath, {
|
|
101
127
|
runId: effectiveRunId
|
|
102
128
|
});
|
|
@@ -119,7 +145,7 @@ export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
|
|
|
119
145
|
io.stdout.write(`${JSON.stringify({
|
|
120
146
|
ok: hasRed,
|
|
121
147
|
path: args.targetPath,
|
|
122
|
-
runId: effectiveRunId
|
|
148
|
+
runId: effectiveRunId,
|
|
123
149
|
sources: {
|
|
124
150
|
tddCycleLog: cycleLogHasRed,
|
|
125
151
|
autoEvidence: autoEvidenceHasRed
|
package/dist/tdd-cycle.d.ts
CHANGED
|
@@ -22,7 +22,28 @@ export interface TddCycleValidation {
|
|
|
22
22
|
openRedSlices: string[];
|
|
23
23
|
sliceCount: number;
|
|
24
24
|
}
|
|
25
|
-
export
|
|
25
|
+
export interface TddCycleParseIssue {
|
|
26
|
+
lineNumber: number;
|
|
27
|
+
reason: string;
|
|
28
|
+
rawLine: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ParseTddCycleLogOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Collect one issue per dropped/malformed line. Callers that care
|
|
33
|
+
* (doctor, red-evidence) can surface them; hooks keep soft-fail.
|
|
34
|
+
*/
|
|
35
|
+
issues?: TddCycleParseIssue[];
|
|
36
|
+
/**
|
|
37
|
+
* When true, reject lines that omit required fields instead of
|
|
38
|
+
* back-filling them with defaults. Used by validation paths
|
|
39
|
+
* (`validateTddCycleOrder`, `cclaw doctor`) to avoid silently
|
|
40
|
+
* bucketing unscoped rows into "runId=active, stage=tdd". Soft paths
|
|
41
|
+
* (generated hooks) keep the legacy defaults so a half-written file
|
|
42
|
+
* never takes the session down.
|
|
43
|
+
*/
|
|
44
|
+
strict?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function parseTddCycleLog(text: string, options?: ParseTddCycleLogOptions): TddCycleEntry[];
|
|
26
47
|
export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
|
|
27
48
|
runId?: string;
|
|
28
49
|
}): TddCycleValidation;
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -1,41 +1,77 @@
|
|
|
1
|
-
export function parseTddCycleLog(text) {
|
|
1
|
+
export function parseTddCycleLog(text, options = {}) {
|
|
2
|
+
const issues = options.issues;
|
|
3
|
+
const strict = options.strict === true;
|
|
2
4
|
const out = [];
|
|
3
5
|
// Strip a leading UTF-8 BOM on the whole blob so the first line parses
|
|
4
6
|
// cleanly; `trim()` handles BOM on subsequent lines through the same
|
|
5
7
|
// codepath (empty/whitespace-only lines are skipped).
|
|
6
8
|
const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
7
|
-
|
|
9
|
+
const lines = normalized.split(/\r?\n/);
|
|
10
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
11
|
+
const raw = lines[index] ?? "";
|
|
8
12
|
const line = raw.trim();
|
|
9
13
|
if (!line)
|
|
10
14
|
continue;
|
|
15
|
+
const lineNumber = index + 1;
|
|
16
|
+
let parsed;
|
|
11
17
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
parsed = JSON.parse(line);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
issues?.push({
|
|
22
|
+
lineNumber,
|
|
23
|
+
reason: `json-parse-failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
24
|
+
rawLine: raw
|
|
25
|
+
});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const phase = parsed.phase;
|
|
29
|
+
if (phase !== "red" && phase !== "green" && phase !== "refactor") {
|
|
30
|
+
issues?.push({
|
|
31
|
+
lineNumber,
|
|
32
|
+
reason: `invalid-phase: ${JSON.stringify(parsed.phase)}`,
|
|
33
|
+
rawLine: raw
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const runIdField = typeof parsed.runId === "string" ? parsed.runId : null;
|
|
38
|
+
const stageField = typeof parsed.stage === "string" ? parsed.stage : null;
|
|
39
|
+
const sliceField = typeof parsed.slice === "string" ? parsed.slice : null;
|
|
40
|
+
if (strict) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
if (!runIdField)
|
|
43
|
+
missing.push("runId");
|
|
44
|
+
if (!stageField)
|
|
45
|
+
missing.push("stage");
|
|
46
|
+
if (!sliceField)
|
|
47
|
+
missing.push("slice");
|
|
48
|
+
if (missing.length > 0) {
|
|
49
|
+
issues?.push({
|
|
50
|
+
lineNumber,
|
|
51
|
+
reason: `missing-required-fields: ${missing.join(",")}`,
|
|
52
|
+
rawLine: raw
|
|
53
|
+
});
|
|
15
54
|
continue;
|
|
16
55
|
}
|
|
17
|
-
const entry = {
|
|
18
|
-
ts: typeof parsed.ts === "string" ? parsed.ts : "",
|
|
19
|
-
runId: typeof parsed.runId === "string" ? parsed.runId : "active",
|
|
20
|
-
stage: typeof parsed.stage === "string" ? parsed.stage : "tdd",
|
|
21
|
-
slice: typeof parsed.slice === "string" ? parsed.slice : "S-unknown",
|
|
22
|
-
phase,
|
|
23
|
-
command: typeof parsed.command === "string" ? parsed.command : "",
|
|
24
|
-
files: Array.isArray(parsed.files)
|
|
25
|
-
? parsed.files.filter((item) => typeof item === "string")
|
|
26
|
-
: undefined,
|
|
27
|
-
exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
|
|
28
|
-
note: typeof parsed.note === "string" ? parsed.note : undefined,
|
|
29
|
-
acIds: Array.isArray(parsed.acIds)
|
|
30
|
-
? parsed.acIds
|
|
31
|
-
.filter((item) => typeof item === "string" && item.length > 0)
|
|
32
|
-
: undefined
|
|
33
|
-
};
|
|
34
|
-
out.push(entry);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// skip malformed line
|
|
38
56
|
}
|
|
57
|
+
const entry = {
|
|
58
|
+
ts: typeof parsed.ts === "string" ? parsed.ts : "",
|
|
59
|
+
runId: runIdField ?? "active",
|
|
60
|
+
stage: stageField ?? "tdd",
|
|
61
|
+
slice: sliceField ?? "S-unknown",
|
|
62
|
+
phase,
|
|
63
|
+
command: typeof parsed.command === "string" ? parsed.command : "",
|
|
64
|
+
files: Array.isArray(parsed.files)
|
|
65
|
+
? parsed.files.filter((item) => typeof item === "string")
|
|
66
|
+
: undefined,
|
|
67
|
+
exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
|
|
68
|
+
note: typeof parsed.note === "string" ? parsed.note : undefined,
|
|
69
|
+
acIds: Array.isArray(parsed.acIds)
|
|
70
|
+
? parsed.acIds
|
|
71
|
+
.filter((item) => typeof item === "string" && item.length > 0)
|
|
72
|
+
: undefined
|
|
73
|
+
};
|
|
74
|
+
out.push(entry);
|
|
39
75
|
}
|
|
40
76
|
return out;
|
|
41
77
|
}
|