fix(gsd): normalize described expected output paths
This commit is contained in:
parent
0dd7c31213
commit
a8e1a026a4
4 changed files with 94 additions and 3 deletions
|
|
@ -113,6 +113,25 @@ function escapeRegex(s: string): string {
|
|||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a task-plan file reference that may include inline description text
|
||||
* after the path, for example:
|
||||
* "docs/file.md — explanation"
|
||||
* "docs/file.md - explanation"
|
||||
*/
|
||||
export function normalizePlannedFileReference(value: string): string {
|
||||
const trimmed = value.trim().replace(/`/g, "");
|
||||
const match = /^(.*?)(?:\s+(?:—|-)\s+)(.+)$/.exec(trimmed);
|
||||
if (!match) return trimmed;
|
||||
|
||||
const pathCandidate = match[1].trim();
|
||||
if (pathCandidate.includes("/") || pathCandidate.includes("\\") || pathCandidate.includes(".")) {
|
||||
return pathCandidate;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/** Parse bullet list items from a text block. */
|
||||
export function parseBullets(text: string): string[] {
|
||||
return text.split('\n')
|
||||
|
|
@ -603,11 +622,11 @@ export function parseTaskPlanIO(content: string): { inputFiles: string[]; output
|
|||
let match: RegExpExecArray | null;
|
||||
backtickPathRegex.lastIndex = 0;
|
||||
while ((match = backtickPathRegex.exec(trimmed)) !== null) {
|
||||
const candidate = match[1];
|
||||
const candidate = normalizePlannedFileReference(match[1]);
|
||||
// Filter out things that look like code tokens rather than file paths
|
||||
// (e.g. `true`, `false`, `npm run test`). A file path has at least one
|
||||
// dot or slash.
|
||||
if (candidate.includes("/") || candidate.includes(".")) {
|
||||
if (candidate.includes("/") || candidate.includes("\\") || candidate.includes(".")) {
|
||||
paths.push(candidate);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { normalizePlannedFileReference } from "../files.js";
|
||||
import { logWarning } from "../workflow-logger.js";
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -57,7 +58,9 @@ export function validateFileChanges(
|
|||
|
||||
// Normalize expected paths (strip leading ./ or /)
|
||||
const normalizedExpected = new Set(
|
||||
[...allExpected].map(f => f.replace(/^\.\//, "").replace(/^\//, "")),
|
||||
[...allExpected].map((f) =>
|
||||
normalizePlannedFileReference(f).replace(/^\.\//, "").replace(/^\//, ""),
|
||||
),
|
||||
);
|
||||
|
||||
// Compute symmetric difference
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { validateFileChanges } from "../safety/file-change-validator.ts";
|
||||
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
return execFileSync("git", args, {
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
}
|
||||
|
||||
test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
|
||||
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
||||
t.after(() => rmSync(base, { recursive: true, force: true }));
|
||||
|
||||
mkdirSync(join(base, "definitions"), { recursive: true });
|
||||
git(base, "init");
|
||||
git(base, "config", "user.email", "test@example.com");
|
||||
git(base, "config", "user.name", "Test User");
|
||||
|
||||
const target = join(base, "definitions", "ac-audit.md");
|
||||
writeFileSync(target, "initial\n");
|
||||
git(base, "add", ".");
|
||||
git(base, "commit", "-m", "initial");
|
||||
|
||||
writeFileSync(target, "updated\n");
|
||||
git(base, "add", ".");
|
||||
git(base, "commit", "-m", "update");
|
||||
|
||||
const audit = validateFileChanges(
|
||||
base,
|
||||
["definitions/ac-audit.md — current state of AC CRM, tags, pipelines, automations"],
|
||||
[],
|
||||
);
|
||||
|
||||
assert.ok(audit, "audit should be produced when expected output exists");
|
||||
assert.deepEqual(audit.unexpectedFiles, []);
|
||||
assert.deepEqual(audit.missingFiles, []);
|
||||
assert.equal(
|
||||
audit.violations.some((v) => v.severity === "warning"),
|
||||
false,
|
||||
"described expected output should not trigger unexpected-file warnings",
|
||||
);
|
||||
});
|
||||
|
|
@ -101,6 +101,25 @@ test("parseTaskPlanIO handles multiple backtick tokens on one line", () => {
|
|||
assert.deepEqual(io.outputFiles, ["src/c.ts"]);
|
||||
});
|
||||
|
||||
test("parseTaskPlanIO strips inline descriptions from backtick-wrapped file references", () => {
|
||||
const content = `# T01: Described Paths
|
||||
|
||||
## Inputs
|
||||
|
||||
- \`src/config.ts — existing configuration\`
|
||||
- \`src/flags.ts - feature flags\`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- \`definitions/ac-audit.md — current state of AC CRM\`
|
||||
- \`docs/runbook.md - update deployment notes\`
|
||||
`;
|
||||
|
||||
const io = parseTaskPlanIO(content);
|
||||
assert.deepEqual(io.inputFiles, ["src/config.ts", "src/flags.ts"]);
|
||||
assert.deepEqual(io.outputFiles, ["definitions/ac-audit.md", "docs/runbook.md"]);
|
||||
});
|
||||
|
||||
// ─── deriveTaskGraph ──────────────────────────────────────────────────────
|
||||
|
||||
test("deriveTaskGraph: linear chain T01→T02→T03", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue