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,283 @@
1
+ import { verifiedChangeEscalationTargets } from './verified-change.js';
2
+ import { gateApprovalStatus, gateArtefactStatus, isDurableGateArtefactSatisfied, isInternalStepSatisfied, modeSliceDecision } from './run-state.js';
3
+
4
+ function durableArtefactBlock(gate, nextGate) {
5
+ const label = gate === "QA" ? "QA report" : `${gate} artefact`;
6
+ const stablePath = gate === "QA" ? ".agdf/control/artefacts/<key>/QA_REPORT.md" : `.agdf/control/artefacts/<key>/${gate}.md`;
7
+ return {
8
+ status: "blocked",
9
+ current_gate: gate,
10
+ blocking_reason: `missing_durable_${gate.toLowerCase()}_artefact`,
11
+ missing_approval: "none",
12
+ allowed: [
13
+ `persist the approved ${label} in a stable artefact path such as ${stablePath}`,
14
+ "link the artefact from the selected RUN_STATE.md and MASTER_BACKLOG.md",
15
+ "run gate-check again",
16
+ ],
17
+ forbidden: nextGate
18
+ ? [`create ${nextGate}`, "create later-gate artefacts", "implement gated work", "claim QA or release readiness"]
19
+ : ["create later-gate artefacts", "implement gated work", "claim release readiness"],
20
+ next_allowed_action: `Persist the approved ${label} and link it from the AGDF control state before continuing.`,
21
+ };
22
+ }
23
+
24
+ export function isGateSatisfied(runState, gate) {
25
+ const status = gateApprovalStatus(runState, gate);
26
+ if (status === "not_applicable") return true;
27
+ if (status !== "approved") return false;
28
+ if (gate === "UAT") return true;
29
+ return isDurableGateArtefactSatisfied(runState, gate);
30
+ }
31
+
32
+ function qaRevisionRequired(runState) {
33
+ return gateArtefactStatus(runState, "QA").status === "revise";
34
+ }
35
+
36
+ export function transitionDecisionForRunState(runState, verifiedChange = null) {
37
+ if (gateApprovalStatus(runState, "UR") === "approved" && !isGateSatisfied(runState, "UR")) return durableArtefactBlock("UR", "PRD");
38
+
39
+ if (!isGateSatisfied(runState, "UR")) {
40
+ return {
41
+ status: "open",
42
+ current_gate: "UR",
43
+ blocking_reason: "none",
44
+ missing_approval: "Approval: UR",
45
+ allowed: ["clarify user need", "formulate and persist UR", "record evidence", "request exact UR approval"],
46
+ forbidden: ["create PRD", "create SD", "create TP", "run Brownfield Analysis", "implement code", "claim QA or release readiness"],
47
+ next_allowed_action: "Clarify the user requirement, persist the UR, and request exact approval: Approval: UR",
48
+ };
49
+ }
50
+
51
+ if (gateApprovalStatus(runState, "PRD") === "approved" && !isGateSatisfied(runState, "PRD")) return durableArtefactBlock("PRD", "SD");
52
+
53
+ if (!isGateSatisfied(runState, "PRD")) {
54
+ if (!isInternalStepSatisfied(runState, "Brownfield Review")) {
55
+ return {
56
+ status: "open",
57
+ current_gate: "Brownfield Review",
58
+ blocking_reason: "none",
59
+ missing_approval: "none",
60
+ allowed: [
61
+ "run Brownfield Review after G-00",
62
+ "identify existing workstream, owners, SoT, reuse risks and open PRD/SD questions",
63
+ "mark Brownfield Review as done or not_applicable in the selected RUN_STATE.md",
64
+ ],
65
+ forbidden: ["create PRD before Brownfield Review is resolved", "create SD", "create TP", "implement code", "claim QA or release readiness"],
66
+ next_allowed_action: "Run Brownfield Review after G-00 before drafting PRD, or mark Brownfield Review not_applicable with evidence.",
67
+ };
68
+ }
69
+
70
+ const modeDecision = modeSliceDecision(runState);
71
+ if (modeDecision === "undecided") {
72
+ return {
73
+ status: "open",
74
+ current_gate: "Mode/Slice Decision",
75
+ blocking_reason: "none",
76
+ missing_approval: "none",
77
+ allowed: [
78
+ "decide whether the approved UR is quick_task, verified_change, structured_slice, structured_delivery or block",
79
+ "record scope reason, evidence and required next gate depth in the selected RUN_STATE.md",
80
+ "choose the next required gate depth before drafting PRD or implementing",
81
+ ],
82
+ forbidden: ["create PRD before process size is decided", "create SD", "create TP", "implement code", "claim QA or release readiness"],
83
+ next_allowed_action: "Record the Mode/Slice Decision from Brownfield Review with scope reason and evidence before choosing PRD depth or Quick Task execution.",
84
+ };
85
+ }
86
+
87
+ if (modeDecision === "block") {
88
+ return {
89
+ status: "blocked",
90
+ current_gate: "Mode/Slice Decision",
91
+ blocking_reason: "mode_slice_decision_blocked",
92
+ missing_approval: "none",
93
+ allowed: ["resolve ownership, SoT, evidence, impact or product-direction uncertainty", "run gate-check again"],
94
+ forbidden: ["create PRD", "create SD", "create TP", "implement code", "claim QA or release readiness"],
95
+ next_allowed_action: "Resolve the Brownfield Review blocker before choosing a delivery path.",
96
+ };
97
+ }
98
+
99
+ if (modeDecision === "quick_task") {
100
+ return {
101
+ status: "open",
102
+ current_gate: "Quick Task Execution",
103
+ blocking_reason: "none",
104
+ missing_approval: "none",
105
+ allowed: ["implement the narrow approved UR scope", "run relevant checks", "record evidence and close with OR-lite"],
106
+ forbidden: ["expand scope beyond the Brownfield Review decision", "create broad PRD/SD/TP artefacts by ritual", "claim QA or release readiness without evidence"],
107
+ next_allowed_action: "Proceed as a Quick Task within the Brownfield Review scope and record verification evidence.",
108
+ };
109
+ }
110
+
111
+ if (modeDecision === "verified_change") {
112
+ const state = verifiedChange?.status ?? "missing";
113
+ const escalationTarget = verifiedChange?.escalation_target ?? "structured_slice";
114
+ if (state === "executed") {
115
+ return {
116
+ status: "open",
117
+ current_gate: "OR",
118
+ blocking_reason: "none",
119
+ missing_approval: "none",
120
+ allowed: ["use the Verified Change mini-closeout", "offer delivery closeout when requested"],
121
+ forbidden: ["create PRD, SD, TP, QA or UAT by ritual", "commit, push, open PR or release automatically"],
122
+ next_allowed_action: "Close the executed Verified Change with its compact record and offer delivery closeout; do not perform VCS actions automatically.",
123
+ };
124
+ }
125
+ if (state === "escalated" || (state === "invalid" && verifiedChangeEscalationTargets.has(escalationTarget))) {
126
+ return {
127
+ status: "open",
128
+ current_gate: "PRD",
129
+ blocking_reason: state === "escalated" ? "verified_change_escalated" : "verified_change_invalid_escalated",
130
+ missing_approval: "Approval: PRD",
131
+ allowed: ["draft the structured PRD required by the declared escalation target", "retain the Verified Change escalation evidence"],
132
+ forbidden: ["implement through Verified Change", "create SD or TP before PRD approval", "claim QA or release readiness"],
133
+ next_allowed_action: `Proceed as ${escalationTarget}: draft the required PRD and request exact approval: Approval: PRD.`,
134
+ };
135
+ }
136
+ if (state === "eligible") {
137
+ return {
138
+ status: "open",
139
+ current_gate: "Verified Change Execution",
140
+ blocking_reason: "none",
141
+ missing_approval: "none",
142
+ allowed: ["implement only declared source and derived paths", "run declared propagation and validation commands", "record execution evidence and mini-closeout"],
143
+ forbidden: ["touch unlisted paths", "add prohibited impacts", "claim QA, UAT or release readiness"],
144
+ next_allowed_action: "Implement only the eligible Verified Change record scope, then record passing propagation and validation evidence.",
145
+ };
146
+ }
147
+ return {
148
+ status: state === "missing" || state === "draft" ? "open" : "blocked",
149
+ current_gate: "Verified Change Execution",
150
+ blocking_reason: state === "invalid" ? "verified_change_invalid" : "verified_change_record_required",
151
+ missing_approval: "none",
152
+ allowed: ["create or refine the compact Verified Change record", "capture baseline paths", "prove fail-closed eligibility with doctor and gate-check"],
153
+ forbidden: ["implement candidate changes", "claim validation, QA, UAT or release readiness"],
154
+ next_allowed_action: "Create or repair VERIFIED_CHANGE.md, capture the baseline and satisfy every eligibility check before implementation; escalate when any condition cannot be proven.",
155
+ };
156
+ }
157
+
158
+ return {
159
+ status: "open",
160
+ current_gate: "PRD",
161
+ blocking_reason: "none",
162
+ missing_approval: "Approval: PRD",
163
+ allowed: ["draft or refine PRD", "define scope", "define acceptance criteria", "define non-goals", "request exact PRD approval"],
164
+ forbidden: ["create SD", "create TP", "run Brownfield Analysis as implementation preparation", "implement code", "claim QA or release readiness"],
165
+ next_allowed_action: modeDecision === "structured_slice"
166
+ ? "Draft or refine the smallest PRD slice justified by Brownfield Review; do not implement before required artefacts are approved."
167
+ : "Draft or refine the PRD; do not implement before PRD, SD and TP are approved.",
168
+ };
169
+ }
170
+
171
+ if (gateApprovalStatus(runState, "SD") === "approved" && !isGateSatisfied(runState, "SD")) return durableArtefactBlock("SD", "TP");
172
+
173
+ if (!isGateSatisfied(runState, "SD")) {
174
+ return {
175
+ status: "open",
176
+ current_gate: "SD",
177
+ blocking_reason: "none",
178
+ missing_approval: "Approval: SD",
179
+ allowed: ["draft or refine Solution Design", "define architecture", "define ownership", "request exact SD approval"],
180
+ forbidden: ["create TP", "implement code", "claim QA or release readiness"],
181
+ next_allowed_action: "Draft or refine the Solution Design; do not implement before SD and TP are approved.",
182
+ };
183
+ }
184
+
185
+ if (gateApprovalStatus(runState, "TP") === "approved" && !isGateSatisfied(runState, "TP")) return durableArtefactBlock("TP", "Brownfield Analysis");
186
+
187
+ if (!isGateSatisfied(runState, "TP")) {
188
+ return {
189
+ status: "open",
190
+ current_gate: "TP",
191
+ blocking_reason: "none",
192
+ missing_approval: "Approval: TP",
193
+ allowed: ["draft or refine Task/Test Plan", "define task IDs", "define test evidence", "request exact TP approval"],
194
+ forbidden: ["implement code", "claim QA or release readiness"],
195
+ next_allowed_action: "Draft or refine the Task/Test Plan; do not implement before TP is approved.",
196
+ };
197
+ }
198
+
199
+ const implementationStepsRequired = gateApprovalStatus(runState, "TP") === "approved";
200
+ if (implementationStepsRequired && !isInternalStepSatisfied(runState, "Brownfield Analysis")) {
201
+ return {
202
+ status: "open",
203
+ current_gate: "Brownfield Analysis",
204
+ blocking_reason: "none",
205
+ missing_approval: "none",
206
+ allowed: ["run Brownfield Analysis for the approved TP scope", "verify existing owners, reuse paths and regression risks"],
207
+ forbidden: ["implement before Brownfield evidence supports the approved TP path", "claim QA or release readiness"],
208
+ next_allowed_action: "Run Brownfield Analysis for the approved TP scope before CD+Tests.",
209
+ };
210
+ }
211
+
212
+ if (implementationStepsRequired && !isInternalStepSatisfied(runState, "CD+Tests")) {
213
+ return {
214
+ status: "open",
215
+ current_gate: "CD+Tests",
216
+ blocking_reason: "none",
217
+ missing_approval: "none",
218
+ allowed: ["implement the approved TP tasks", "run the approved test plan", "record implementation and test evidence"],
219
+ forbidden: ["claim QA pass", "request UAT approval", "release"],
220
+ next_allowed_action: "Implement the approved TP scope, run its tests, and record CD+Tests evidence before CR.",
221
+ };
222
+ }
223
+
224
+ if (implementationStepsRequired && !isInternalStepSatisfied(runState, "CR")) {
225
+ return {
226
+ status: "open",
227
+ current_gate: "CR",
228
+ blocking_reason: "none",
229
+ missing_approval: "none",
230
+ allowed: ["run mandatory code review", "record correctness, regression, security and maintainability findings", "fix blocking review findings"],
231
+ forbidden: ["claim QA pass", "request UAT approval", "release"],
232
+ next_allowed_action: "Run Code Review for the implemented TP scope and resolve blocking findings before QA.",
233
+ };
234
+ }
235
+
236
+ if (qaRevisionRequired(runState)) {
237
+ return {
238
+ status: "open",
239
+ current_gate: "QA",
240
+ blocking_reason: "qa_revise_required",
241
+ missing_approval: "none",
242
+ allowed: ["revise the implementation against the QA findings", "refresh CD+Tests and mandatory reviews", "rerun QA with refreshed evidence"],
243
+ forbidden: ["request QA approval", "request UAT approval", "release", "claim delivery readiness"],
244
+ next_allowed_action: "Resolve the QA revise findings, refresh CD+Tests and reviews, then rerun QA. Do not request Approval: QA from a revise report.",
245
+ };
246
+ }
247
+
248
+ if (gateApprovalStatus(runState, "QA") === "approved" && !isGateSatisfied(runState, "QA")) return durableArtefactBlock("QA", "UAT");
249
+
250
+ if (isGateSatisfied(runState, "QA") && !isGateSatisfied(runState, "UAT")) {
251
+ return {
252
+ status: "open",
253
+ current_gate: "UAT",
254
+ blocking_reason: "none",
255
+ missing_approval: "Approval: UAT",
256
+ allowed: ["request exact UAT approval", "prepare non-operative delivery summary"],
257
+ forbidden: ["release", "push", "open PR", "commit without explicit user instruction and required approval"],
258
+ next_allowed_action: "Request exact approval: Approval: UAT before delivery handoff.",
259
+ };
260
+ }
261
+
262
+ if (isGateSatisfied(runState, "UAT")) {
263
+ return {
264
+ status: "open",
265
+ current_gate: "OR",
266
+ blocking_reason: "none",
267
+ missing_approval: "none",
268
+ allowed: ["produce OR or delivery closeout", "prepare commit, push or PR handoff when requested"],
269
+ forbidden: ["commit, push, open PR or release automatically"],
270
+ next_allowed_action: "Produce delivery closeout or requested handoff; do not perform VCS actions automatically.",
271
+ };
272
+ }
273
+
274
+ return {
275
+ status: "open",
276
+ current_gate: "QA",
277
+ blocking_reason: "none",
278
+ missing_approval: "Approval: QA",
279
+ allowed: ["run QA gate", "persist or refine the QA report", "request exact QA approval"],
280
+ forbidden: ["request UAT approval", "release", "claim delivery readiness before QA approval and report evidence"],
281
+ next_allowed_action: "Run the QA gate, persist the QA report, and request exact approval: Approval: QA",
282
+ };
283
+ }
@@ -0,0 +1,217 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import process from "node:process";
4
+ import { parseControlState, resolveRuns } from "../control-state/index.js";
5
+ import { buildRunCandidates } from "../interaction-presentation.js";
6
+ import { cleanStatusCell, filled, isPlaceholderValue, readTargetFile } from "./shared.js";
7
+ import { extractField, isSafeRepoRelativePath, readVerifiedChangeRecord } from "./verified-change.js";
8
+
9
+ export const userGateOrder = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
10
+ export const durableGateArtefacts = new Set(["UR", "PRD", "SD", "TP", "QA"]);
11
+ export const internalStepArtefacts = new Set(["Brownfield Review", "Verified Change", "Brownfield Analysis", "CD+Tests", "TP Review", "Clean Implementation Review", "Clean Review", "CR", "Code Review"]);
12
+ export const closeoutArtefacts = new Set(["OR"]);
13
+
14
+ export function resolvedArtefactFile(targetDir, rawPath) {
15
+ const normalizedPath = String(rawPath ?? "").replace(/^`|`$/g, "").trim();
16
+ if (!normalizedPath || isAbsolute(normalizedPath) || normalizedPath.includes("<") || normalizedPath.includes(">")) return "";
17
+ const absolutePath = resolve(targetDir, normalizedPath);
18
+ if (relative(targetDir, absolutePath).startsWith("..") || !existsSync(absolutePath)) return "";
19
+ try {
20
+ const realPath = realpathSync(absolutePath);
21
+ return relative(realpathSync(targetDir), realPath).startsWith("..") || !statSync(realPath).isFile() ? "" : realPath;
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+
27
+ export function readArtefactHeading(targetDir, artefact) {
28
+ const artefactPath = resolvedArtefactFile(targetDir, artefact?.path);
29
+ if (!artefactPath) return "";
30
+ try {
31
+ return readFileSync(artefactPath, "utf8").match(/^#\s+(.+)$/m)?.[1]?.trim() ?? "";
32
+ } catch {
33
+ return "";
34
+ }
35
+ }
36
+
37
+ export function readRunState(targetDir, selection = {}) {
38
+ let runPath = join(".agdf", "control", "AGDF_RUN.md");
39
+ const canonicalRoot = join(targetDir, ".agdf", "control", "runs");
40
+ let resolutionError;
41
+ let candidateRuns = [];
42
+ if (existsSync(canonicalRoot)) {
43
+ try {
44
+ const selected = resolveRuns(targetDir, {
45
+ runIdArg: selection.runId,
46
+ runIdEnv: process.env.AGDF_RUN_ID,
47
+ });
48
+ runPath = selected.run.path.startsWith(targetDir)
49
+ ? selected.run.path.slice(targetDir.length + 1)
50
+ : selected.run.path;
51
+ } catch (error) {
52
+ resolutionError = error.message;
53
+ if (resolutionError.startsWith("AGDF_ACTIVE_RUN_AMBIGUOUS")) {
54
+ const active = resolveRuns(targetDir, { allActive: true }).runs.map((run) => {
55
+ const controlState = parseControlState(run.content, {
56
+ userGates: userGateOrder,
57
+ internalSteps: [...internalStepArtefacts],
58
+ closeoutArtefacts: [...closeoutArtefacts],
59
+ });
60
+ return {
61
+ ...run,
62
+ control_state: controlState,
63
+ current_artefact_heading: readArtefactHeading(targetDir, controlState.artefacts.get(controlState.current_gate)),
64
+ ur_heading: readArtefactHeading(targetDir, controlState.artefacts.get("UR")),
65
+ };
66
+ });
67
+ candidateRuns = buildRunCandidates(active);
68
+ }
69
+ }
70
+ }
71
+ if (resolutionError || !existsSync(join(targetDir, runPath))) {
72
+ return {
73
+ path: runPath,
74
+ content: "",
75
+ current_gate: "",
76
+ next_allowed_action: "",
77
+ approvals: new Map(),
78
+ artefacts: new Map(),
79
+ evidence_refs: [],
80
+ artefact_chain: [],
81
+ mode_slice_decision: {},
82
+ missing_evidence: [],
83
+ risks: [],
84
+ context_graph: {},
85
+ quality_outlook: "",
86
+ source_scope: {},
87
+ memory: {},
88
+ candidate_runs: candidateRuns,
89
+ resolution_error: resolutionError,
90
+ };
91
+ }
92
+
93
+ const content = readTargetFile(targetDir, runPath);
94
+ const parsed = parseControlState(content, {
95
+ userGates: userGateOrder,
96
+ internalSteps: [...internalStepArtefacts],
97
+ closeoutArtefacts: [...closeoutArtefacts],
98
+ });
99
+
100
+ return {
101
+ path: runPath,
102
+ content,
103
+ ...parsed,
104
+ };
105
+ }
106
+
107
+ export function gateApprovalStatus(runState, gate) {
108
+ if (!userGateOrder.includes(gate)) return "not_applicable";
109
+ return runState.approvals.get(gate)?.status ?? "";
110
+ }
111
+
112
+ export function gateArtefactStatus(runState, gate) {
113
+ if (!userGateOrder.includes(gate)) return { status: "not_applicable", path: "" };
114
+ return runState.artefacts.get(gate) ?? { status: "", path: "" };
115
+ }
116
+
117
+ export function isDurableGateArtefactSatisfied(runState, gate) {
118
+ if (!durableGateArtefacts.has(gate)) return true;
119
+ const artefact = gateArtefactStatus(runState, gate);
120
+ if (!artefact.path || isPlaceholderValue(artefact.path)) return false;
121
+ if (gate === "QA") return artefact.status === "pass" || artefact.status === "passed";
122
+ return artefact.status === "approved";
123
+ }
124
+
125
+ export function expectedDurableArtefactStatuses(gate) {
126
+ return gate === "QA" ? ["pass", "passed"] : ["approved"];
127
+ }
128
+
129
+ export function describeDurableArtefactStatuses(gate) {
130
+ return expectedDurableArtefactStatuses(gate).map((status) => `\`${status}\``).join(" or ");
131
+ }
132
+
133
+ export function analyzeDurableGateArtefactConsistency(runState) {
134
+ const findings = [];
135
+
136
+ for (const gate of durableGateArtefacts) {
137
+ if (gateApprovalStatus(runState, gate) !== "approved") continue;
138
+
139
+ const artefact = gateArtefactStatus(runState, gate);
140
+ if (!artefact.path || isPlaceholderValue(artefact.path) || !artefact.status) continue;
141
+
142
+ const expectedStatuses = expectedDurableArtefactStatuses(gate);
143
+ if (expectedStatuses.includes(artefact.status)) continue;
144
+
145
+ findings.push({
146
+ severity: "revise",
147
+ code: "AGDF_GATE_ARTEFACT_STATUS_INCONSISTENT",
148
+ message: `${gate} approval is recorded, but the durable artefact row uses status \`${artefact.status}\`; expected ${describeDurableArtefactStatuses(gate)}.`,
149
+ path: runState.path,
150
+ next_step: `Update the ${gate} artefact row in the selected RUN_STATE.md to use the gate-specific durable status vocabulary.`,
151
+ });
152
+ }
153
+
154
+ return findings;
155
+ }
156
+
157
+ export function analyzeArtefactRoleConsistency(targetDir, runState) {
158
+ const findings = [];
159
+ for (const [type, artefact] of runState.artefacts) {
160
+ if (artefact.path_format === "invalid") {
161
+ findings.push({
162
+ severity: "block",
163
+ code: "AGDF_ARTEFACT_PATH_FORMAT_INVALID",
164
+ message: `${type} uses an invalid artefact path cell (${artefact.path_reason}).`,
165
+ path: runState.path,
166
+ next_step: "Use a plain repository-relative path or one complete Markdown code span.",
167
+ });
168
+ } else if (artefact.path && !isSafeRepoRelativePath(artefact.path)) {
169
+ findings.push({
170
+ severity: "block",
171
+ code: "AGDF_ARTEFACT_PATH_INVALID",
172
+ message: `${type} must use a normalized repository-relative artefact path.`,
173
+ path: runState.path,
174
+ next_step: "Remove absolute, traversal, unsupported-backslash or otherwise non-normalized path text.",
175
+ });
176
+ }
177
+ }
178
+ const rolesByPath = new Map();
179
+ for (const [type, artefact] of runState.artefacts) {
180
+ if (!artefact.path) continue;
181
+ rolesByPath.set(artefact.path, [...(rolesByPath.get(artefact.path) ?? []), type]);
182
+ }
183
+ for (const [path, roles] of rolesByPath) {
184
+ if (roles.length < 2) continue;
185
+ const allowedRoles = new Set(["Brownfield Review", "Verified Change", "OR"]);
186
+ if (modeSliceDecision(runState) !== "verified_change" || roles.some((role) => !allowedRoles.has(role))) {
187
+ findings.push({ severity: "block", code: "AGDF_ARTEFACT_ROLE_ALIAS_INVALID", message: `Artefact path is reused across incompatible roles: ${roles.join(", ")}.`, path, next_step: "Use distinct artefacts or a lifecycle-consistent Verified Change compact record." });
188
+ continue;
189
+ }
190
+ const record = readVerifiedChangeRecord(targetDir, runState);
191
+ if (record.status !== "present") continue;
192
+ const recordStatus = cleanStatusCell(extractField(record.content, "status"));
193
+ const brownfieldComplete = extractField(record.content, "decision") === "verified_change" && filled(extractField(record.content, "scope_reason")) && filled(extractField(record.content, "evidence"));
194
+ const miniCloseoutComplete = ["delivered", "intentionally_not_delivered", "residual_risk", "next_step"].every((field) => filled(extractField(record.content, field)));
195
+ if (roles.includes("Brownfield Review") && (runState.artefacts.get("Brownfield Review")?.status !== "done" || !brownfieldComplete)) findings.push({ severity: "block", code: "AGDF_VERIFIED_CHANGE_BROWNFIELD_ROLE_INVALID", message: "Consolidated Brownfield Review role is incomplete.", path, next_step: "Complete Brownfield Selection before eligibility." });
196
+ if (roles.includes("Verified Change") && runState.artefacts.get("Verified Change")?.status !== recordStatus) findings.push({ severity: "block", code: "AGDF_VERIFIED_CHANGE_ROLE_STATUS_MISMATCH", message: "Verified Change artefact row status does not match the compact record.", path, next_step: "Align the run-state row and record status." });
197
+ if (roles.includes("OR") && runState.artefacts.get("OR")?.status === "done" && (recordStatus !== "executed" || !miniCloseoutComplete)) findings.push({ severity: "block", code: "AGDF_VERIFIED_CHANGE_OR_ROLE_INVALID", message: "Consolidated OR is complete before execution and Mini-Closeout evidence are complete.", path, next_step: "Complete execution and Mini-Closeout evidence before marking OR done." });
198
+ }
199
+ return findings;
200
+ }
201
+
202
+ export function isInternalStepSatisfied(runState, step) {
203
+ const artefact = runState.artefacts.get(step);
204
+ if (!artefact) return false;
205
+ if (artefact.status === "done") return true;
206
+ return artefact.status === "not_applicable"
207
+ && (step === "Brownfield Review" || step === "Brownfield Analysis");
208
+ }
209
+
210
+ export function modeSliceDecision(runState) {
211
+ const decision = runState.mode_slice_decision?.decision ?? "";
212
+ const scopeReason = runState.mode_slice_decision?.scope_reason ?? "";
213
+ const evidence = runState.mode_slice_decision?.evidence ?? "";
214
+ if (!decision || isPlaceholderValue(decision) || !filled(scopeReason) || !filled(evidence)) return "undecided";
215
+ return decision;
216
+ }
217
+