cclaw-cli 0.48.18 → 0.48.20
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/content/node-hooks.js +32 -34
- package/dist/internal/tdd-red-evidence.js +33 -10
- package/dist/tdd-cycle.d.ts +39 -1
- package/dist/tdd-cycle.js +96 -32
- package/package.json +1 -1
|
@@ -368,13 +368,31 @@ function normalizeText(value) {
|
|
|
368
368
|
return String(value || "").replace(/\\s+/gu, " ").trim();
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// Mirrors \`src/tdd-cycle.ts::normalizeTddPath\`. Any change to
|
|
372
|
+
// canonical normalization must be updated in BOTH places; the
|
|
373
|
+
// tdd-parity test asserts matcher behavior agrees end-to-end.
|
|
371
374
|
function normalizePathForMatch(rawPath) {
|
|
372
|
-
return
|
|
375
|
+
return String(rawPath == null ? "" : rawPath)
|
|
376
|
+
.trim()
|
|
373
377
|
.replace(/\\\\/gu, "/")
|
|
374
378
|
.replace(/^\\.\\//u, "")
|
|
375
379
|
.toLowerCase();
|
|
376
380
|
}
|
|
377
381
|
|
|
382
|
+
// Mirrors \`src/tdd-cycle.ts::pathMatchesTarget\`. Use instead of raw
|
|
383
|
+
// \`===\` when checking recorded files against a target path.
|
|
384
|
+
function pathMatchesTargetInline(candidate, target) {
|
|
385
|
+
const normalizedCandidate = normalizePathForMatch(candidate);
|
|
386
|
+
const normalizedTarget = normalizePathForMatch(target);
|
|
387
|
+
if (normalizedCandidate.length === 0 || normalizedTarget.length === 0) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
return (
|
|
391
|
+
normalizedCandidate === normalizedTarget ||
|
|
392
|
+
normalizedCandidate.endsWith("/" + normalizedTarget)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
378
396
|
function normalizeToolName(value) {
|
|
379
397
|
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
380
398
|
if (value && typeof value === "object") {
|
|
@@ -1279,27 +1297,6 @@ async function handlePromptGuard(runtime) {
|
|
|
1279
1297
|
return 0;
|
|
1280
1298
|
}
|
|
1281
1299
|
|
|
1282
|
-
async function tddCycleCounts(stateDir, runId) {
|
|
1283
|
-
const filePath = path.join(stateDir, "tdd-cycle-log.jsonl");
|
|
1284
|
-
const raw = await readTextFile(filePath, "");
|
|
1285
|
-
const lines = raw.split(/\\r?\\n/gu).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1286
|
-
let red = 0;
|
|
1287
|
-
let green = 0;
|
|
1288
|
-
for (const line of lines) {
|
|
1289
|
-
try {
|
|
1290
|
-
const row = JSON.parse(line);
|
|
1291
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
1292
|
-
const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
|
|
1293
|
-
if (rowRun !== runId) continue;
|
|
1294
|
-
if (row.phase === "red") red += 1;
|
|
1295
|
-
if (row.phase === "green") green += 1;
|
|
1296
|
-
} catch {
|
|
1297
|
-
// ignore malformed rows
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
return { red, green };
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
1300
|
// Mirrors src/knowledge-store.ts::computeCompoundReadiness — kept inline so
|
|
1304
1301
|
// SessionStart can refresh compound-readiness.json without the CLI binary.
|
|
1305
1302
|
// Any schema change must update src/knowledge-store.ts::computeCompoundReadiness
|
|
@@ -1458,14 +1455,7 @@ async function computeRalphLoopStatusInline(stateDir, runId) {
|
|
|
1458
1455
|
};
|
|
1459
1456
|
}
|
|
1460
1457
|
|
|
1461
|
-
function tddCycleStateFromCounts(counts) {
|
|
1462
|
-
if (counts.red <= 0) return "need_red";
|
|
1463
|
-
if (counts.red > counts.green) return "red_open";
|
|
1464
|
-
return "green_done";
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
1458
|
async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
|
|
1468
|
-
const normalizedTarget = normalizePathForMatch(rawPath);
|
|
1469
1459
|
const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
|
|
1470
1460
|
for (const line of cycleRaw.split(/\\r?\\n/gu)) {
|
|
1471
1461
|
const trimmed = line.trim();
|
|
@@ -1484,7 +1474,10 @@ async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
|
|
|
1484
1474
|
const files = Array.isArray(row.files) ? row.files : [];
|
|
1485
1475
|
for (const filePath of files) {
|
|
1486
1476
|
if (typeof filePath !== "string") continue;
|
|
1487
|
-
|
|
1477
|
+
// endsWith-aware match (mirrors tdd-cycle.ts::pathMatchesTarget)
|
|
1478
|
+
// — previously the inline impl used strict === which disagreed
|
|
1479
|
+
// with the CLI/internal path and produced guard blind spots.
|
|
1480
|
+
if (pathMatchesTargetInline(filePath, rawPath)) return true;
|
|
1488
1481
|
}
|
|
1489
1482
|
} catch {
|
|
1490
1483
|
// ignore malformed line
|
|
@@ -1508,7 +1501,7 @@ async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
|
|
|
1508
1501
|
const paths = Array.isArray(row.paths) ? row.paths : [];
|
|
1509
1502
|
for (const filePath of paths) {
|
|
1510
1503
|
if (typeof filePath !== "string") continue;
|
|
1511
|
-
if (
|
|
1504
|
+
if (pathMatchesTargetInline(filePath, rawPath)) return true;
|
|
1512
1505
|
}
|
|
1513
1506
|
} catch {
|
|
1514
1507
|
// ignore malformed line
|
|
@@ -1667,9 +1660,14 @@ async function handleWorkflowGuard(runtime) {
|
|
|
1667
1660
|
reasons.push("tdd_write_without_red_for_path");
|
|
1668
1661
|
}
|
|
1669
1662
|
} else if (productionPatterns.length === 0 && !isTestPayload(payloadLower, payloadPaths, testPatterns)) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1663
|
+
// Slice-aware fallback: the previous implementation used a flat
|
|
1664
|
+
// red/green count which said "ok" as long as the totals balanced
|
|
1665
|
+
// across ALL slices, so a closed S-1 could unlock production
|
|
1666
|
+
// writes that actually belonged to a new, not-yet-red S-2. Now
|
|
1667
|
+
// we reuse the canonical Ralph Loop status: if NO slice has an
|
|
1668
|
+
// open RED, we block.
|
|
1669
|
+
const ralphStatus = await computeRalphLoopStatusInline(stateDir, currentRun);
|
|
1670
|
+
if (!ralphStatus.redOpen) {
|
|
1673
1671
|
reasons.push("tdd_write_without_open_red");
|
|
1674
1672
|
}
|
|
1675
1673
|
}
|
|
@@ -2,10 +2,10 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
4
4
|
import { readFlowState } from "../runs.js";
|
|
5
|
-
import { hasFailingTestForPath, parseTddCycleLog } from "../tdd-cycle.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { hasFailingTestForPath, parseTddCycleLog, pathMatchesTarget } from "../tdd-cycle.js";
|
|
6
|
+
// normalizePath and the path matcher live in src/tdd-cycle.ts so all
|
|
7
|
+
// TDD-related guards (internal CLI, runtime hook, unit tests) agree on
|
|
8
|
+
// what "path X matches recorded file Y" means.
|
|
9
9
|
function parseArgs(tokens) {
|
|
10
10
|
const args = { quiet: false };
|
|
11
11
|
for (const token of tokens) {
|
|
@@ -73,15 +73,12 @@ function parseAutoEvidence(text) {
|
|
|
73
73
|
return out;
|
|
74
74
|
}
|
|
75
75
|
function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
|
|
76
|
-
const normalizedTarget = normalizePath(targetPath);
|
|
77
76
|
for (const entry of entries) {
|
|
78
77
|
if (options.runId && entry.runId !== options.runId)
|
|
79
78
|
continue;
|
|
80
79
|
for (const filePath of entry.paths) {
|
|
81
|
-
|
|
82
|
-
if (normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`)) {
|
|
80
|
+
if (pathMatchesTarget(filePath, targetPath))
|
|
83
81
|
return true;
|
|
84
|
-
}
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
return false;
|
|
@@ -89,14 +86,40 @@ function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
|
|
|
89
86
|
export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
|
|
90
87
|
const args = parseArgs(tokens);
|
|
91
88
|
const flowState = await readFlowState(projectRoot).catch(() => null);
|
|
89
|
+
// Strict runId scoping: a previous implementation fell back to no
|
|
90
|
+
// filter when both `--runId` and `flowState.activeRunId` were missing,
|
|
91
|
+
// which let evidence rows from past runs satisfy the current check
|
|
92
|
+
// (false positive). Now: require an explicit or inferred runId or
|
|
93
|
+
// fail loud so the caller cannot silently inherit cross-run state.
|
|
92
94
|
const effectiveRunId = args.runId ?? flowState?.activeRunId;
|
|
95
|
+
if (!effectiveRunId || effectiveRunId.trim().length === 0) {
|
|
96
|
+
const reason = "tdd-red-evidence: cannot scope check — no --runId provided and " +
|
|
97
|
+
"flow-state.json has no activeRunId. Pass --runId=<id> explicitly " +
|
|
98
|
+
"or run `cclaw doctor` to reconcile state.";
|
|
99
|
+
if (!args.quiet) {
|
|
100
|
+
io.stdout.write(`${JSON.stringify({
|
|
101
|
+
ok: false,
|
|
102
|
+
path: args.targetPath,
|
|
103
|
+
runId: null,
|
|
104
|
+
error: reason,
|
|
105
|
+
sources: { tddCycleLog: false, autoEvidence: false }
|
|
106
|
+
}, null, 2)}\n`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
io.stderr.write(`${reason}\n`);
|
|
110
|
+
}
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
93
113
|
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
94
114
|
const autoEvidencePath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-red-evidence.jsonl");
|
|
95
115
|
let cycleLogHasRed = false;
|
|
96
116
|
let autoEvidenceHasRed = false;
|
|
97
117
|
try {
|
|
98
118
|
const raw = await fs.readFile(tddLogPath, "utf8");
|
|
99
|
-
|
|
119
|
+
// Strict parse: drop malformed/underspecified rows rather than
|
|
120
|
+
// backfilling runId=active / stage=tdd defaults, which used to
|
|
121
|
+
// silently glue foreign entries to the current run.
|
|
122
|
+
const entries = parseTddCycleLog(raw, { strict: true });
|
|
100
123
|
cycleLogHasRed = hasFailingTestForPath(entries, args.targetPath, {
|
|
101
124
|
runId: effectiveRunId
|
|
102
125
|
});
|
|
@@ -119,7 +142,7 @@ export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
|
|
|
119
142
|
io.stdout.write(`${JSON.stringify({
|
|
120
143
|
ok: hasRed,
|
|
121
144
|
path: args.targetPath,
|
|
122
|
-
runId: effectiveRunId
|
|
145
|
+
runId: effectiveRunId,
|
|
123
146
|
sources: {
|
|
124
147
|
tddCycleLog: cycleLogHasRed,
|
|
125
148
|
autoEvidence: autoEvidenceHasRed
|
package/dist/tdd-cycle.d.ts
CHANGED
|
@@ -22,10 +22,48 @@ 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;
|
|
50
|
+
/**
|
|
51
|
+
* Canonical path normalization used by ALL TDD path-matching layers
|
|
52
|
+
* (cycle-log analysis, internal CLI `tdd-red-evidence`, and the
|
|
53
|
+
* inline runtime hook). Previously each layer had its own near-copy,
|
|
54
|
+
* which produced subtle drift (e.g. `./src/app.ts` vs `src/app.ts`
|
|
55
|
+
* differing between hook and CLI). Keep this function in one place
|
|
56
|
+
* so all callers agree.
|
|
57
|
+
*/
|
|
58
|
+
export declare function normalizeTddPath(value: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Shared "does the recorded file path refer to the target" matcher.
|
|
61
|
+
* Uses canonical normalization plus the traditional `endsWith('/'+x)`
|
|
62
|
+
* rule so a recorded `src/app.ts` matches either `src/app.ts` or
|
|
63
|
+
* `subdir/src/app.ts` but NOT `src/app.test.ts`. All TDD path checks
|
|
64
|
+
* MUST go through this helper.
|
|
65
|
+
*/
|
|
66
|
+
export declare function pathMatchesTarget(candidate: string, target: string): boolean;
|
|
29
67
|
export interface RalphLoopSliceState {
|
|
30
68
|
slice: string;
|
|
31
69
|
redCount: number;
|
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
|
}
|
|
@@ -123,8 +159,40 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
123
159
|
sliceCount: bySlice.size
|
|
124
160
|
};
|
|
125
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Canonical path normalization used by ALL TDD path-matching layers
|
|
164
|
+
* (cycle-log analysis, internal CLI `tdd-red-evidence`, and the
|
|
165
|
+
* inline runtime hook). Previously each layer had its own near-copy,
|
|
166
|
+
* which produced subtle drift (e.g. `./src/app.ts` vs `src/app.ts`
|
|
167
|
+
* differing between hook and CLI). Keep this function in one place
|
|
168
|
+
* so all callers agree.
|
|
169
|
+
*/
|
|
170
|
+
export function normalizeTddPath(value) {
|
|
171
|
+
const trimmed = String(value ?? "").trim();
|
|
172
|
+
return trimmed
|
|
173
|
+
.replace(/\\/gu, "/")
|
|
174
|
+
.replace(/^\.\//u, "")
|
|
175
|
+
.toLowerCase();
|
|
176
|
+
}
|
|
177
|
+
// Legacy alias kept for local callers below.
|
|
126
178
|
function normalizePath(value) {
|
|
127
|
-
return value
|
|
179
|
+
return normalizeTddPath(value);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Shared "does the recorded file path refer to the target" matcher.
|
|
183
|
+
* Uses canonical normalization plus the traditional `endsWith('/'+x)`
|
|
184
|
+
* rule so a recorded `src/app.ts` matches either `src/app.ts` or
|
|
185
|
+
* `subdir/src/app.ts` but NOT `src/app.test.ts`. All TDD path checks
|
|
186
|
+
* MUST go through this helper.
|
|
187
|
+
*/
|
|
188
|
+
export function pathMatchesTarget(candidate, target) {
|
|
189
|
+
const normalizedCandidate = normalizeTddPath(candidate);
|
|
190
|
+
const normalizedTarget = normalizeTddPath(target);
|
|
191
|
+
if (normalizedCandidate.length === 0 || normalizedTarget.length === 0) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return (normalizedCandidate === normalizedTarget ||
|
|
195
|
+
normalizedCandidate.endsWith(`/${normalizedTarget}`));
|
|
128
196
|
}
|
|
129
197
|
/**
|
|
130
198
|
* Derive a lightweight Ralph Loop summary from parsed tdd-cycle-log entries.
|
|
@@ -197,7 +265,6 @@ export function computeRalphLoopStatus(entries, options = {}) {
|
|
|
197
265
|
* `productionPath` for the active run.
|
|
198
266
|
*/
|
|
199
267
|
export function hasFailingTestForPath(entries, productionPath, options = {}) {
|
|
200
|
-
const normalizedTarget = normalizePath(productionPath);
|
|
201
268
|
const filtered = options.runId
|
|
202
269
|
? entries.filter((entry) => entry.runId === options.runId)
|
|
203
270
|
: entries;
|
|
@@ -208,10 +275,7 @@ export function hasFailingTestForPath(entries, productionPath, options = {}) {
|
|
|
208
275
|
continue;
|
|
209
276
|
if (!Array.isArray(entry.files) || entry.files.length === 0)
|
|
210
277
|
continue;
|
|
211
|
-
const hasMatch = entry.files.some((filePath) =>
|
|
212
|
-
const normalized = normalizePath(filePath);
|
|
213
|
-
return normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`);
|
|
214
|
-
});
|
|
278
|
+
const hasMatch = entry.files.some((filePath) => pathMatchesTarget(filePath, productionPath));
|
|
215
279
|
if (hasMatch) {
|
|
216
280
|
return true;
|
|
217
281
|
}
|