create-agdf 0.9.1 → 0.9.3

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.
@@ -0,0 +1,344 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, posix } from "node:path";
3
+
4
+ export function cleanStatusCell(value) {
5
+ return (value ?? "").replace(/\x60/g, "").trim();
6
+ }
7
+
8
+ export function readTargetFile(targetDir, relativePath) {
9
+ return readFileSync(join(targetDir, relativePath), "utf8");
10
+ }
11
+
12
+ export function addFinding(findings, severity, code, message, path, nextStep) {
13
+ findings.push({
14
+ severity,
15
+ code,
16
+ message,
17
+ path,
18
+ next_step: nextStep,
19
+ });
20
+ }
21
+
22
+ export function nonEmptyTableRows(content) {
23
+ return content
24
+ .split("\n")
25
+ .filter((line) => line.trim().startsWith("|"))
26
+ .filter((line) => !line.includes("---"))
27
+ .map((line) => line.split("|").map((cell) => cell.trim()).filter(Boolean))
28
+ .filter((cells) => cells.some((cell) => cell && !cell.startsWith("`")));
29
+ }
30
+
31
+ export function tableRows(content) {
32
+ return content
33
+ .split("\n")
34
+ .filter((line) => line.trim().startsWith("|"))
35
+ .filter((line) => !line.includes("---"))
36
+ .map((line) => {
37
+ const cells = line.split("|").map((cell) => cell.trim());
38
+ if (cells[0] === "") cells.shift();
39
+ if (cells.at(-1) === "") cells.pop();
40
+ return cells;
41
+ });
42
+ }
43
+
44
+ const compactBacklogHeaders = ["priority", "key", "work item", "status", "artefacts", "current spec", "next step"];
45
+ const legacyBacklogHeaders = ["prio", "key", "title", "status", "ur", "brownfield review", "prd", "sd", "tp", "qa", "or", "current spec", "notes"];
46
+ const backlogStatusLabels = new Map([
47
+ ["needs ur", "needs_ur"],
48
+ ["awaiting brownfield review", "awaiting_brownfield_review"],
49
+ ["awaiting prd", "awaiting_prd"],
50
+ ["awaiting prd approval", "awaiting_prd_approval"],
51
+ ["awaiting sd", "awaiting_sd"],
52
+ ["awaiting sd approval", "awaiting_sd_approval"],
53
+ ["awaiting tp", "awaiting_tp"],
54
+ ["awaiting tp approval", "awaiting_tp_approval"],
55
+ ["in progress", "in_progress"],
56
+ ["blocked", "blocked"],
57
+ ["awaiting qa", "awaiting_qa"],
58
+ ["awaiting uat", "awaiting_uat"],
59
+ ["completed", "completed"],
60
+ ["superseded", "superseded"],
61
+ ["abandoned", "abandoned"],
62
+ ]);
63
+ const backlogArtefactLabels = new Map([
64
+ ["ur", "ur"],
65
+ ["brownfield", "brownfield_review"],
66
+ ["prd", "prd"],
67
+ ["sd", "sd"],
68
+ ["tp", "tp"],
69
+ ["qa", "qa"],
70
+ ["or", "or"],
71
+ ]);
72
+ const backlogScopeLabels = new Map([
73
+ ["framework-maintenance", "framework_maintenance"],
74
+ ["external-delivery", "external_delivery"],
75
+ ]);
76
+
77
+ export function normalizeBacklogHeader(value) {
78
+ return value.replace(/`/g, "").trim().toLowerCase();
79
+ }
80
+
81
+ export function markdownLink(value) {
82
+ const match = value.trim().match(/^\[([^\]]+)\]\(([^)]+)\)$/);
83
+ return match ? { label: match[1].trim(), target: match[2].trim() } : null;
84
+ }
85
+
86
+ export function resolvedBacklogLinkTarget(target) {
87
+ if (!target
88
+ || target.startsWith("/")
89
+ || target.includes("\\")
90
+ || /^[a-z][a-z0-9+.-]*:/i.test(target)) {
91
+ return null;
92
+ }
93
+ const resolved = posix.normalize(posix.join(".agdf/control", target));
94
+ if (resolved === ".." || resolved.startsWith("../")) return null;
95
+ return resolved;
96
+ }
97
+
98
+ export function normalizedBacklogLinkTarget(value, findings, backlogPath, label) {
99
+ const link = markdownLink(value);
100
+ if (!link) return value.trim();
101
+ const resolved = resolvedBacklogLinkTarget(link.target);
102
+ if (!resolved) {
103
+ if (findings) {
104
+ addFinding(
105
+ findings,
106
+ "revise",
107
+ "AGDF_BACKLOG_LINK_TARGET_INVALID",
108
+ `Backlog ${label} link must be relative to MASTER_BACKLOG.md: ${link.target || "<empty>"}.`,
109
+ backlogPath,
110
+ "Use a document-relative artefact target such as artefacts/<key>/UR.md.",
111
+ );
112
+ }
113
+ return link.target;
114
+ }
115
+ return resolved;
116
+ }
117
+
118
+ export function normalizeBacklogStatus(value, findings, backlogPath) {
119
+ const cleaned = cleanStatusCell(value ?? "");
120
+ const lookup = cleaned.replaceAll("_", " ").toLowerCase();
121
+ const normalized = backlogStatusLabels.get(lookup);
122
+ if (normalized) return normalized;
123
+ if (/^[a-z][a-z0-9_]*$/.test(cleaned)) return cleaned;
124
+ if (findings && filled(cleaned)) {
125
+ addFinding(
126
+ findings,
127
+ "revise",
128
+ "AGDF_BACKLOG_STATUS_UNKNOWN",
129
+ `MASTER_BACKLOG.md uses an unknown human status label: ${cleaned}.`,
130
+ backlogPath,
131
+ "Use a documented human status label or a supported legacy snake_case value.",
132
+ );
133
+ }
134
+ return cleaned;
135
+ }
136
+
137
+ export function normalizeBacklogScope(workItem, findings, backlogPath) {
138
+ const match = (workItem ?? "").match(/^\[([^\]]+)\]/);
139
+ if (!match) return undefined;
140
+ const cleaned = match[1].trim();
141
+ const lookup = cleaned.replaceAll("_", " ").toLowerCase().replaceAll(" ", "-");
142
+ const normalized = backlogScopeLabels.get(lookup);
143
+ if (normalized) return normalized;
144
+ if (findings) {
145
+ addFinding(
146
+ findings,
147
+ "revise",
148
+ "AGDF_BACKLOG_SCOPE_LABEL_UNKNOWN",
149
+ `MASTER_BACKLOG.md uses an unknown Work item scope tag: [${cleaned}].`,
150
+ backlogPath,
151
+ "Use [framework-maintenance] or [external-delivery], or remove the bracketed tag.",
152
+ );
153
+ }
154
+ return cleaned;
155
+ }
156
+
157
+ export function emptyBacklogPointer() {
158
+ return {
159
+ prio: "",
160
+ key: "",
161
+ title: "",
162
+ scope: "",
163
+ status: "",
164
+ ur: "",
165
+ brownfield_review: "",
166
+ prd: "",
167
+ sd: "",
168
+ tp: "",
169
+ qa: "",
170
+ or: "",
171
+ current_spec: "",
172
+ notes: "",
173
+ };
174
+ }
175
+
176
+ export function parseCompactArtefacts(value, pointer, findings, backlogPath) {
177
+ if (!filled(value)) return;
178
+ const seen = new Set();
179
+ for (const entry of value.split(/\s+·\s+/)) {
180
+ const link = markdownLink(entry);
181
+ if (!link) {
182
+ if (findings) {
183
+ addFinding(
184
+ findings,
185
+ "revise",
186
+ "AGDF_BACKLOG_ARTEFACT_LINK_INVALID",
187
+ `Compact Artefacts entry is not a Markdown link: ${entry}.`,
188
+ backlogPath,
189
+ "Use [UR](artefacts/<key>/UR.md) style links separated by a middle dot.",
190
+ );
191
+ }
192
+ continue;
193
+ }
194
+ const field = backlogArtefactLabels.get(link.label.toLowerCase());
195
+ if (!field) {
196
+ if (findings) {
197
+ addFinding(
198
+ findings,
199
+ "revise",
200
+ "AGDF_BACKLOG_ARTEFACT_LABEL_UNKNOWN",
201
+ `Compact Artefacts uses an unknown label: ${link.label}.`,
202
+ backlogPath,
203
+ "Use UR, Brownfield, PRD, SD, TP, QA or OR.",
204
+ );
205
+ }
206
+ continue;
207
+ }
208
+ if (seen.has(field)) {
209
+ if (findings) {
210
+ addFinding(
211
+ findings,
212
+ "revise",
213
+ "AGDF_BACKLOG_ARTEFACT_LABEL_DUPLICATE",
214
+ `Compact Artefacts repeats the ${link.label} link.`,
215
+ backlogPath,
216
+ "Keep exactly one link per artefact type.",
217
+ );
218
+ }
219
+ continue;
220
+ }
221
+ seen.add(field);
222
+ pointer[field] = normalizedBacklogLinkTarget(entry, findings, backlogPath, link.label);
223
+ }
224
+ }
225
+
226
+ export function parseBacklogSection(section, findings = null, backlogPath = ".agdf/control/MASTER_BACKLOG.md") {
227
+ const rows = tableRows(section);
228
+ if (rows.length === 0) return [];
229
+ const headers = rows[0].map(normalizeBacklogHeader);
230
+ const layout = headers.join("|") === compactBacklogHeaders.join("|")
231
+ ? "compact"
232
+ : headers.join("|") === legacyBacklogHeaders.join("|")
233
+ ? "legacy"
234
+ : "unknown";
235
+
236
+ if (layout === "unknown") {
237
+ if (findings) {
238
+ addFinding(
239
+ findings,
240
+ "revise",
241
+ "AGDF_BACKLOG_LAYOUT_UNKNOWN",
242
+ `MASTER_BACKLOG.md uses an unsupported table layout: ${headers.join(", ")}.`,
243
+ backlogPath,
244
+ "Use the canonical compact layout or the supported legacy 13-column layout.",
245
+ );
246
+ }
247
+ return [];
248
+ }
249
+
250
+ return rows.slice(1)
251
+ .filter((cells) => cells.some((cell) => filled(cell)))
252
+ .map((cells) => {
253
+ const pointer = emptyBacklogPointer();
254
+ if (layout === "legacy") {
255
+ [
256
+ pointer.prio,
257
+ pointer.key,
258
+ pointer.title,
259
+ pointer.status,
260
+ pointer.ur,
261
+ pointer.brownfield_review,
262
+ pointer.prd,
263
+ pointer.sd,
264
+ pointer.tp,
265
+ pointer.qa,
266
+ pointer.or,
267
+ pointer.current_spec,
268
+ pointer.notes,
269
+ ] = cells.map((cell) => cell ?? "");
270
+ pointer.status = normalizeBacklogStatus(pointer.status, findings, backlogPath);
271
+ return pointer;
272
+ }
273
+
274
+ pointer.prio = cells[0] ?? "";
275
+ pointer.key = cleanStatusCell(cells[1] ?? "");
276
+ pointer.title = cells[2] ?? "";
277
+ pointer.scope = normalizeBacklogScope(cells[2], findings, backlogPath) ?? "";
278
+ pointer.status = normalizeBacklogStatus(cells[3], findings, backlogPath);
279
+ parseCompactArtefacts(cells[4] ?? "", pointer, findings, backlogPath);
280
+ pointer.current_spec = normalizedBacklogLinkTarget(cells[5] ?? "", findings, backlogPath, "Current spec");
281
+ pointer.notes = cells[6] ?? "";
282
+ return pointer;
283
+ });
284
+ }
285
+
286
+ export function hasFilledTableRow(content, firstCellPattern) {
287
+ return tableRows(content)
288
+ .filter((cells) => firstCellPattern.test(cells[0] ?? ""))
289
+ .some((cells) => cells.slice(1).some((cell) => cell && !cell.startsWith("`")));
290
+ }
291
+
292
+ export function isPlaceholderValue(value) {
293
+ return !value || (value.startsWith("`") && value.includes("|"));
294
+ }
295
+
296
+ export function hasFilledEvidenceRow(content) {
297
+ const evidenceSection = content.match(/## Evidence([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "";
298
+ return tableRows(evidenceSection)
299
+ .filter((cells) => cells[0] !== "Evidence")
300
+ .some((cells) => {
301
+ const [evidence, source, covers] = cells;
302
+ return Boolean(evidence || source || covers);
303
+ });
304
+ }
305
+
306
+ export function allowNoActiveRuns(targetDir) {
307
+ const configPath = join(targetDir, ".agdf", "control", "config.json");
308
+ if (!existsSync(configPath)) return false;
309
+ try {
310
+ return JSON.parse(readFileSync(configPath, "utf8")).allow_no_active_runs === true;
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ export function markdownSection(content, heading) {
317
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
318
+ return content.match(new RegExp(`(?:^|\\n)## ${escaped}\\s*\\n([\\s\\S]*?)(?:\\n## |\\n# |$)`))?.[1] ?? "";
319
+ }
320
+
321
+ export function filled(value) {
322
+ return Boolean(value && !isPlaceholderValue(value) && value.trim() !== "");
323
+ }
324
+
325
+ export function runSelectionRecovery(target) {
326
+ const directSelection = "Pass --run <run_id> or set AGDF_RUN_ID to select the intended run";
327
+ return ["doctor", "delivery-map"].includes(target)
328
+ ? `${directSelection}, or use --all-active to evaluate every active run independently.`
329
+ : `${directSelection}.`;
330
+ }
331
+
332
+ export function parseQualityContracts(content) {
333
+ const parsed = JSON.parse(content);
334
+ if (!Array.isArray(parsed.contracts) || parsed.contracts.length === 0) {
335
+ throw new Error("contracts must be a non-empty array");
336
+ }
337
+ for (const contract of parsed.contracts) {
338
+ if (!contract.code || !contract.impact || !contract.required_evidence) {
339
+ throw new Error(`contract ${contract.code ?? "<unknown>"} is missing code, impact or required_evidence`);
340
+ }
341
+ }
342
+ return parsed;
343
+ }
344
+
@@ -0,0 +1,156 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { isAbsolute, posix, relative, resolve } from "node:path";
4
+ import { cleanStatusCell, isPlaceholderValue } from "./shared.js";
5
+
6
+ export function extractField(content, field) {
7
+ const pattern = new RegExp(`^- ${field}:[^\\S\\r\\n]*(.*)$`, "m");
8
+ return content.match(pattern)?.[1]?.trim() ?? "";
9
+ }
10
+
11
+ export const verifiedChangeStatuses = new Set(["draft", "eligible", "executed", "escalated"]);
12
+ export const verifiedChangeEscalationTargets = new Set(["structured_slice", "structured_delivery"]);
13
+
14
+ export function parseVerifiedChangePathList(value) {
15
+ const cleaned = cleanStatusCell(value ?? "");
16
+ if (!cleaned || cleaned === "none") return [];
17
+ return cleaned.split(",").map((item) => item.trim()).filter(Boolean);
18
+ }
19
+
20
+ export function isSafeRepoRelativePath(value) {
21
+ if (!value || value === "none" || value.startsWith("/") || value.includes("\\")) return false;
22
+ const normalized = posix.normalize(value);
23
+ return normalized !== "." && normalized !== ".." && !normalized.startsWith("../") && normalized === value;
24
+ }
25
+
26
+ export function gitPathList(targetDir, args) {
27
+ try {
28
+ return execFileSync("git", args, { cwd: targetDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
29
+ .split("\n")
30
+ .map((item) => item.trim())
31
+ .filter(Boolean);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export function gitValue(targetDir, args) {
38
+ try {
39
+ return execFileSync("git", args, { cwd: targetDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export function readVerifiedChangeRecord(targetDir, runState) {
46
+ const artefact = runState.artefacts.get("Verified Change");
47
+ if (!artefact?.path || isPlaceholderValue(artefact.path)) return { status: "missing", path: "", content: "" };
48
+ if (!isSafeRepoRelativePath(artefact.path)) return { status: "invalid", path: artefact.path, content: "", error: "record_path_invalid" };
49
+ const absolutePath = resolve(targetDir, artefact.path);
50
+ const relativePath = relative(resolve(targetDir), absolutePath);
51
+ if (isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || !existsSync(absolutePath)) {
52
+ return { status: "missing", path: artefact.path, content: "", error: "record_missing" };
53
+ }
54
+ return { status: "present", path: artefact.path, content: readFileSync(absolutePath, "utf8") };
55
+ }
56
+
57
+ export function evaluateVerifiedChange(targetDir, runState) {
58
+ const record = readVerifiedChangeRecord(targetDir, runState);
59
+ const findings = [];
60
+ const add = (code, message, nextStep, severity = "revise") => findings.push({
61
+ severity,
62
+ code,
63
+ message,
64
+ path: record.path || runState.path,
65
+ next_step: nextStep,
66
+ });
67
+
68
+ if (record.status === "missing") {
69
+ return { status: "missing", record, findings };
70
+ }
71
+ if (record.status === "invalid") {
72
+ add("AGDF_VERIFIED_CHANGE_RECORD_PATH_INVALID", "Verified Change record path must be a repository-relative path inside the target repository.", "Link a stable repository-relative VERIFIED_CHANGE.md artefact from the selected run state.", "block");
73
+ return { status: "invalid", record, findings };
74
+ }
75
+
76
+ const status = cleanStatusCell(extractField(record.content, "status"));
77
+ const relatedUr = cleanStatusCell(extractField(record.content, "related_ur"));
78
+ const escalationTarget = cleanStatusCell(extractField(record.content, "escalation_target"));
79
+ const canonicalOwner = cleanStatusCell(extractField(record.content, "canonical_owner"));
80
+ const sourcePaths = parseVerifiedChangePathList(extractField(record.content, "allowed_source_paths"));
81
+ const derivedPaths = parseVerifiedChangePathList(extractField(record.content, "allowed_derived_paths"));
82
+ const prohibitedImpacts = cleanStatusCell(extractField(record.content, "prohibited_impacts"));
83
+ const propagationCommand = extractField(record.content, "propagation_command");
84
+ const validationCommands = extractField(record.content, "validation_commands");
85
+ const baselineTracked = parseVerifiedChangePathList(extractField(record.content, "baseline_tracked_paths"));
86
+ const baselineUntracked = parseVerifiedChangePathList(extractField(record.content, "baseline_untracked_paths"));
87
+ const baselineCommit = cleanStatusCell(extractField(record.content, "baseline_commit"));
88
+ const executionChangedPaths = parseVerifiedChangePathList(extractField(record.content, "execution_changed_paths"));
89
+ const executionScopeStatus = cleanStatusCell(extractField(record.content, "execution_scope_status"));
90
+ const validationStatus = cleanStatusCell(extractField(record.content, "validation_status"));
91
+ const propagationStatus = cleanStatusCell(extractField(record.content, "propagation_status"));
92
+ const urArtefact = runState.artefacts.get("UR");
93
+ const lifecycle = extractField(runState.content, "lifecycle") || "active";
94
+ const runId = extractField(runState.content, "run_id") || "";
95
+
96
+ if (!verifiedChangeStatuses.has(status)) add("AGDF_VERIFIED_CHANGE_STATUS_INVALID", "Verified Change record must use status draft, eligible, executed or escalated.", "Set a supported record status or escalate to the declared structured target.");
97
+ if (!isSafeRepoRelativePath(relatedUr) || relatedUr !== urArtefact?.path) add("AGDF_VERIFIED_CHANGE_RELATED_UR_INVALID", "Verified Change record must link exactly to the selected run's repository-relative UR artefact.", "Set related_ur to the selected run's durable UR artefact path.");
98
+ if (!verifiedChangeEscalationTargets.has(escalationTarget)) add("AGDF_VERIFIED_CHANGE_ESCALATION_INVALID", "Verified Change record must declare structured_slice or structured_delivery as its escalation target.", "Record the Brownfield-selected structured escalation target.");
99
+ if (!canonicalOwner || canonicalOwner.includes(",") || !isSafeRepoRelativePath(canonicalOwner)) add("AGDF_VERIFIED_CHANGE_OWNER_INVALID", "Verified Change requires exactly one repository-relative canonical_owner.", "Name one canonical owner path; otherwise escalate.");
100
+ if (sourcePaths.length === 0 || sourcePaths.some((path) => !isSafeRepoRelativePath(path))) add("AGDF_VERIFIED_CHANGE_SOURCE_PATHS_INVALID", "Verified Change requires non-empty normalized allowed_source_paths.", "Declare a bounded comma-separated source path list; otherwise escalate.");
101
+ if (derivedPaths.some((path) => !isSafeRepoRelativePath(path))) add("AGDF_VERIFIED_CHANGE_DERIVED_PATHS_INVALID", "Verified Change derived paths must be normalized repository-relative paths or none.", "Correct the derived path list or escalate.");
102
+ if (prohibitedImpacts !== "none") add("AGDF_VERIFIED_CHANGE_IMPACTS_INVALID", "Verified Change must declare prohibited_impacts: none after checking gates, permissions, security, persistence, architecture, external API, CLI and release behavior.", "Use the structured path when any prohibited impact applies.");
103
+ if (!validationCommands || validationCommands === "none") add("AGDF_VERIFIED_CHANGE_VALIDATION_MISSING", "Verified Change requires at least one deterministic validation command.", "Record a deterministic acceptance or consistency check, or escalate.");
104
+ if (derivedPaths.length > 0 && (!propagationCommand || propagationCommand === "none")) add("AGDF_VERIFIED_CHANGE_PROPAGATION_MISSING", "Derived paths require a deterministic propagation command.", "Record the propagation command or escalate.");
105
+ if (!extractField(record.content, "baseline_tracked_paths") || !extractField(record.content, "baseline_untracked_paths")) add("AGDF_VERIFIED_CHANGE_BASELINE_MISSING", "Verified Change requires both tracked and untracked baseline path fields, using none when empty.", "Capture the worktree baseline before marking the record eligible.");
106
+ if (["eligible", "executed"].includes(status) && !/^[0-9a-f]{40,64}$/i.test(baselineCommit)) add("AGDF_VERIFIED_CHANGE_BASELINE_COMMIT_INVALID", "Eligible or executed Verified Change requires a full Git baseline_commit.", "Capture the current full Git commit id before eligibility or escalate.");
107
+
108
+ if (["eligible", "executed"].includes(status) && lifecycle !== "completed" && /^[0-9a-f]{40,64}$/i.test(baselineCommit)) {
109
+ const currentHead = gitValue(targetDir, ["rev-parse", "HEAD"]);
110
+ if (!currentHead) {
111
+ add("AGDF_VERIFIED_CHANGE_BASELINE_COMMIT_UNAVAILABLE", "Active Verified Change requires a readable current Git HEAD for baseline identity validation.", "Restore Git repository access or use the structured path.", "block");
112
+ } else if (baselineCommit !== currentHead) {
113
+ add("AGDF_VERIFIED_CHANGE_BASELINE_COMMIT_MISMATCH", "Active Verified Change baseline_commit does not match the current Git HEAD.", "Recapture the baseline before eligibility or escalate; do not reuse a stale or fabricated baseline identity.", "block");
114
+ }
115
+ }
116
+
117
+ const allowedPaths = new Set([...sourcePaths, ...derivedPaths]);
118
+ const baselinePaths = new Set([...baselineTracked, ...baselineUntracked]);
119
+ const dirtyCandidate = [...allowedPaths].find((path) => baselinePaths.has(path));
120
+ if (dirtyCandidate) add("AGDF_VERIFIED_CHANGE_BASELINE_CANDIDATE_DIRTY", `Declared candidate path is already dirty at baseline: ${dirtyCandidate}.`, "Escalate or start from a clean candidate path; do not adopt pre-existing edits.", "block");
121
+
122
+ const permittedControlPaths = new Set([record.path, runState.path, ".agdf/control/MASTER_BACKLOG.md"]);
123
+ const runArtefactPrefix = `.agdf/control/artefacts/${runId}/`;
124
+ for (const artefact of runState.artefacts.values()) {
125
+ if (artefact.path_format === "invalid") continue;
126
+ if (isSafeRepoRelativePath(artefact.path) && artefact.path.startsWith(runArtefactPrefix)) permittedControlPaths.add(artefact.path);
127
+ }
128
+
129
+ if (!(lifecycle === "completed" && status === "executed")) {
130
+ const currentTracked = gitPathList(targetDir, ["diff", "HEAD", "--name-only"]);
131
+ const currentUntracked = gitPathList(targetDir, ["ls-files", "--others", "--exclude-standard"]);
132
+ if (currentTracked === null || currentUntracked === null) {
133
+ add("AGDF_VERIFIED_CHANGE_GIT_BASELINE_UNAVAILABLE", "Verified Change requires a readable Git worktree for scoped baseline validation.", "Use the structured path or restore Git worktree access.", "block");
134
+ } else {
135
+ const introduced = [...new Set([...currentTracked, ...currentUntracked])].filter((path) => !baselinePaths.has(path)).sort();
136
+ const unexpected = introduced.filter((path) => !allowedPaths.has(path) && !permittedControlPaths.has(path));
137
+ if (unexpected.length > 0) add("AGDF_VERIFIED_CHANGE_SCOPE_ESCAPE", `Verified Change introduced unlisted path(s): ${unexpected.join(", ")}.`, "Mark the record escalated and continue at the declared structured target.", "block");
138
+ if (status === "executed" && JSON.stringify(introduced) !== JSON.stringify([...executionChangedPaths].sort())) {
139
+ add("AGDF_VERIFIED_CHANGE_EXECUTION_SCOPE_MISMATCH", "Executed Verified Change execution_changed_paths must equal the post-baseline changed-path set.", "Record the exact changed paths or escalate.", "block");
140
+ }
141
+ }
142
+ } else {
143
+ const unsafeExecutionPath = executionChangedPaths.find((path) => !isSafeRepoRelativePath(path) || (!allowedPaths.has(path) && !permittedControlPaths.has(path)));
144
+ if (unsafeExecutionPath) add("AGDF_VERIFIED_CHANGE_EXECUTION_SCOPE_INVALID", `Recorded execution path is outside the eligible scope: ${unsafeExecutionPath}.`, "Correct the historical execution snapshot or escalate.", "block");
145
+ }
146
+
147
+ if (status === "executed") {
148
+ if (executionChangedPaths.length === 0 || !executionChangedPaths.some((path) => allowedPaths.has(path)) || executionScopeStatus !== "pass") add("AGDF_VERIFIED_CHANGE_EXECUTION_EVIDENCE_MISSING", "Executed Verified Change requires a non-empty execution_changed_paths set containing a declared source/derived path and execution_scope_status: pass.", "Record complete execution-scope evidence or escalate.");
149
+ if (validationStatus !== "pass") add("AGDF_VERIFIED_CHANGE_VALIDATION_EVIDENCE_MISSING", "Executed Verified Change requires validation_status: pass.", "Record passing deterministic validation evidence or escalate.");
150
+ if (derivedPaths.length > 0 && propagationStatus !== "pass") add("AGDF_VERIFIED_CHANGE_PROPAGATION_EVIDENCE_MISSING", "Executed Verified Change with derived paths requires propagation_status: pass.", "Record successful propagation evidence or escalate.");
151
+ }
152
+
153
+ if (status === "escalated") return { status: "escalated", record, escalation_target: escalationTarget, findings };
154
+ if (findings.length > 0) return { status: "invalid", record, escalation_target: escalationTarget, findings };
155
+ return { status, record, escalation_target: escalationTarget, findings };
156
+ }