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,291 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { aggregate, resolveRuns } from '../control-state/index.js';
4
+ import { resolveConfiguredChatLanguage } from '../cli/runtime-context.js';
5
+ import { isGateSatisfied, transitionDecisionForRunState } from './gate-policy.js';
6
+ import { gateApprovalStatus, readRunState } from './run-state.js';
7
+ import { allowNoActiveRuns, filled, isPlaceholderValue, markdownSection, parseBacklogSection, readTargetFile } from './shared.js';
8
+
9
+ const deliveryRelationships = [
10
+ { from: "UR", relationship: "approved_by", to: "Approval: UR", requiredBy: "UR" },
11
+ { from: "PRD", relationship: "derived_from", to: "UR", requiredBy: "PRD" },
12
+ { from: "SD", relationship: "derived_from", to: "PRD", requiredBy: "SD" },
13
+ { from: "TP", relationship: "derived_from", to: "SD", requiredBy: "TP" },
14
+ { from: "QA_REPORT", relationship: "tests", to: "TP", requiredBy: "QA" },
15
+ ];
16
+
17
+ function relationshipRequired(runState, requiredBy) {
18
+ if (gateApprovalStatus(runState, requiredBy) === "not_applicable") return false;
19
+ if (requiredBy === "QA") {
20
+ return isGateSatisfied(runState, "QA");
21
+ }
22
+
23
+ return isGateSatisfied(runState, requiredBy);
24
+ }
25
+
26
+ function findRelationship(runState, expected) {
27
+ return runState.artefact_chain.find((row) =>
28
+ row.from === expected.from
29
+ && row.relationship === expected.relationship
30
+ && row.to === expected.to
31
+ );
32
+ }
33
+
34
+ function severityFromImpact(value) {
35
+ if (value === "block") return "block";
36
+ if (value === "revise") return "revise";
37
+ if (value === "warning" || value === "warn") return "warn";
38
+ return null;
39
+ }
40
+
41
+ export function analyzeDeliveryMap(runState) {
42
+ const findings = [];
43
+ const relationships = deliveryRelationships.map((expected) => {
44
+ const row = findRelationship(runState, expected);
45
+ const required = relationshipRequired(runState, expected.requiredBy);
46
+ const evidence = row?.evidence ?? "";
47
+ const status = !row ? "missing" : filled(evidence) ? "pass" : required ? "missing_evidence" : "template";
48
+
49
+ if (required && status !== "pass") {
50
+ findings.push({
51
+ severity: "revise",
52
+ code: status === "missing" ? "AGDF_DELIVERY_RELATIONSHIP_MISSING" : "AGDF_DELIVERY_RELATIONSHIP_EVIDENCE_MISSING",
53
+ message: `${expected.from} must be traceable via ${expected.relationship} ${expected.to}.`,
54
+ path: runState.path,
55
+ next_step: "Fill the Artefact Chain row with concrete evidence before treating the delivery map as complete.",
56
+ });
57
+ }
58
+
59
+ return {
60
+ from: expected.from,
61
+ relationship: expected.relationship,
62
+ to: expected.to,
63
+ required,
64
+ status,
65
+ evidence,
66
+ };
67
+ });
68
+
69
+ for (const item of runState.missing_evidence) {
70
+ const severity = severityFromImpact(item.impact);
71
+ if (!severity) continue;
72
+ findings.push({
73
+ severity,
74
+ code: "AGDF_MISSING_EVIDENCE_DECLARED",
75
+ message: item.missing_evidence,
76
+ path: runState.path,
77
+ next_step: item.required_next_step || "Resolve or explicitly accept the missing evidence before advancing the gate.",
78
+ });
79
+ }
80
+
81
+ for (const item of runState.risks) {
82
+ const severity = severityFromImpact(item.impact);
83
+ if (!severity) continue;
84
+ findings.push({
85
+ severity,
86
+ code: "AGDF_RISK_DECLARED",
87
+ message: item.risk,
88
+ path: runState.path,
89
+ next_step: item.mitigation_or_owner || "Assign mitigation or ownership before advancing the gate.",
90
+ });
91
+ }
92
+
93
+ const contextSeverity = severityFromImpact(runState.context_graph?.gate_effect);
94
+ if (contextSeverity) {
95
+ findings.push({
96
+ severity: contextSeverity,
97
+ code: "AGDF_CONTEXT_GRAPH_GATE_EFFECT",
98
+ message: `Context Graph impact is ${runState.context_graph.impact || "unspecified"} with gate effect ${runState.context_graph.gate_effect}.`,
99
+ path: runState.path,
100
+ next_step: runState.context_graph.required_action && runState.context_graph.required_action !== "none"
101
+ ? `Resolve Context Graph action: ${runState.context_graph.required_action}.`
102
+ : "Clarify the Context Graph impact before advancing the gate.",
103
+ });
104
+ }
105
+
106
+ const multiScopeState = runState.source_scope?.multi_scope_state;
107
+ if (multiScopeState === "ambiguous" || multiScopeState === "blocked") {
108
+ findings.push({
109
+ severity: multiScopeState === "blocked" ? "block" : "revise",
110
+ code: "AGDF_SCOPE_AMBIGUOUS",
111
+ message: "Multiple active scope lines are plausible; the agent must not choose one silently.",
112
+ path: runState.path,
113
+ next_step: "List competing scope lines with gate and artefact evidence, then clarify the active scope.",
114
+ });
115
+ }
116
+
117
+ const branchEffect = runState.source_scope?.branch_workspace_scope_effect;
118
+ if (branchEffect === "conflicts" || branchEffect === "insufficient") {
119
+ findings.push({
120
+ severity: branchEffect === "conflicts" ? "revise" : "warn",
121
+ code: "AGDF_BRANCH_NOT_SCOPE_PROOF",
122
+ message: "Branch or workspace evidence is not sufficient scope proof.",
123
+ path: runState.path,
124
+ next_step: "Confirm the active scope from durable artefacts or record why branch/workspace evidence is only supporting evidence.",
125
+ });
126
+ }
127
+
128
+ if (runState.memory?.target && runState.memory.target !== "none" && !filled(runState.memory.reason)) {
129
+ findings.push({
130
+ severity: "warn",
131
+ code: "AGDF_MEMORY_TARGET_REASON_MISSING",
132
+ message: "Knowledge persistence target is set without a reason.",
133
+ path: runState.path,
134
+ next_step: "Fill memory_reason or set memory_target to none.",
135
+ });
136
+ }
137
+
138
+ return {
139
+ relationships,
140
+ missing_evidence: runState.missing_evidence,
141
+ risks: runState.risks,
142
+ context_graph: runState.context_graph,
143
+ source_scope: runState.source_scope,
144
+ memory: runState.memory,
145
+ findings,
146
+ };
147
+ }
148
+
149
+ export function deriveQualityOutlook(runState, findings = []) {
150
+ if (filled(runState.quality_outlook)) return runState.quality_outlook;
151
+ if (findings.some((finding) => finding.severity === "block" || finding.severity === "revise")) {
152
+ return "Resolve blocking or revise-level delivery-map findings before making stronger quality claims.";
153
+ }
154
+ if (findings.some((finding) => finding.severity === "warn")) {
155
+ return "Review warning-level findings when investing further in delivery confidence.";
156
+ }
157
+ return "No additional quality follow-up identified from the current control state.";
158
+ }
159
+
160
+ function readBacklogPointers(targetDir) {
161
+ const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
162
+ if (!existsSync(join(targetDir, backlogPath))) return [];
163
+
164
+ const backlog = readTargetFile(targetDir, backlogPath);
165
+ const activeSection = markdownSection(backlog, "Active Backlog");
166
+ return parseBacklogSection(activeSection);
167
+ }
168
+
169
+ export function evaluateDeliveryMap(targetDir, selection = {}, dependencies = {}) {
170
+ const { evaluateDoctor, buildStatusCard, postApprovalTransition } = dependencies;
171
+ if (typeof evaluateDoctor !== "function" || typeof buildStatusCard !== "function" || typeof postApprovalTransition !== "function") {
172
+ throw new TypeError("evaluateDeliveryMap requires doctor and status-card composition dependencies.");
173
+ }
174
+ if (selection.allActive) {
175
+ const selected = resolveRuns(targetDir, { allActive: true });
176
+ const runs = selected.runs.map((run) => ({
177
+ run_id: run.run_id,
178
+ report: evaluateDeliveryMap(targetDir, { runId: run.run_id }, dependencies),
179
+ }));
180
+ const status = selected.findings.length
181
+ ? "block"
182
+ : aggregate(
183
+ runs.map((item) => ({
184
+ run_id: item.run_id,
185
+ status: item.report.status,
186
+ })),
187
+ { allowNoActiveRuns: allowNoActiveRuns(targetDir) },
188
+ ).status;
189
+ const findings = [
190
+ ...selected.findings.map((finding) => ({
191
+ ...finding,
192
+ severity: "block",
193
+ message: "Invalid canonical run entry.",
194
+ next_step: "Repair or remove the invalid run entry.",
195
+ })),
196
+ ...runs.flatMap((item) =>
197
+ (item.report.doctor_report?.findings ?? item.report.findings).map((finding) => ({
198
+ ...finding,
199
+ run_id: item.run_id,
200
+ })),
201
+ ),
202
+ ];
203
+ return {
204
+ schema_version: "1",
205
+ status,
206
+ current_gate: "all-active",
207
+ next_allowed_action: runs.length
208
+ ? "Resolve per-run findings."
209
+ : "Create or activate a governed run.",
210
+ quality_outlook: "Keep every active run independently actionable.",
211
+ relationships: runs.flatMap((item) => item.report.relationships),
212
+ findings,
213
+ runs,
214
+ };
215
+ }
216
+ const doctorReport = evaluateDoctor(targetDir, selection);
217
+ const runState = readRunState(targetDir, selection);
218
+ const map = analyzeDeliveryMap(runState);
219
+ const gateDecision = transitionDecisionForRunState(runState);
220
+ const currentGate = gateDecision.current_gate;
221
+
222
+ const severityRank = { block: 3, revise: 2, warn: 1 };
223
+ const deliverySeverity = map.findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
224
+ const doctorSeverity = severityRank[doctorReport.status] ?? 0;
225
+ const maxSeverity = Math.max(deliverySeverity, doctorSeverity);
226
+ const status = maxSeverity >= 3 ? "block" : maxSeverity === 2 ? "revise" : maxSeverity === 1 ? "warn" : "pass";
227
+
228
+ const qualityOutlook = deriveQualityOutlook(runState, map.findings);
229
+ const nextAllowedAction = isPlaceholderValue(runState.next_allowed_action) ? gateDecision.next_allowed_action : runState.next_allowed_action;
230
+ const postApproval = postApprovalTransition(gateDecision.missing_approval);
231
+
232
+ return {
233
+ schema_version: "1",
234
+ status,
235
+ checked_at: new Date().toISOString(),
236
+ target_dir: targetDir,
237
+ current_gate: currentGate,
238
+ next_allowed_action: nextAllowedAction,
239
+ next_gate_after_approval: postApproval.next_gate_after_approval,
240
+ allowed_after_approval: postApproval.allowed_after_approval,
241
+ quality_outlook: qualityOutlook,
242
+ status_card: buildStatusCard({
243
+ status,
244
+ currentGate,
245
+ allowed: gateDecision.allowed,
246
+ forbidden: gateDecision.forbidden,
247
+ blockingReason: gateDecision.blocking_reason,
248
+ missingApproval: gateDecision.missing_approval,
249
+ nextAllowedAction,
250
+ runState,
251
+ chatLanguage: resolveConfiguredChatLanguage(targetDir),
252
+ findings: map.findings,
253
+ }),
254
+ backlog_pointers: readBacklogPointers(targetDir),
255
+ artefacts: Object.fromEntries([...runState.artefacts.entries()]),
256
+ approvals: Object.fromEntries([...runState.approvals.entries()]),
257
+ mode_slice_decision: runState.mode_slice_decision,
258
+ relationships: map.relationships,
259
+ evidence_refs: runState.evidence_refs,
260
+ missing_evidence: map.missing_evidence,
261
+ risks: map.risks,
262
+ context_graph: map.context_graph,
263
+ source_scope: map.source_scope,
264
+ memory: map.memory,
265
+ findings: map.findings,
266
+ doctor_status: doctorReport.status,
267
+ doctor_summary: doctorReport.summary,
268
+ doctor_report: doctorReport,
269
+ };
270
+ }
271
+
272
+ export function printDeliveryMapReport(report, json, io = console) {
273
+ if (json) {
274
+ io.log(JSON.stringify(report, null, 2));
275
+ return;
276
+ }
277
+
278
+ io.log(`AGDF delivery-map: ${report.status}`);
279
+ io.log(`Current gate: ${report.current_gate}`);
280
+ io.log(`Next allowed action: ${report.next_allowed_action}`);
281
+ io.log(`Quality outlook: ${report.quality_outlook}`);
282
+ io.log(`Relationships: ${report.relationships.filter((item) => item.status === "pass").length}/${report.relationships.length} evidenced`);
283
+ io.log(`Findings: ${report.findings.length}`);
284
+
285
+ for (const finding of report.findings) {
286
+ io.log("");
287
+ io.log(`[${finding.severity}] ${finding.code}`);
288
+ io.log(finding.message);
289
+ io.log(`Next step: ${finding.next_step}`);
290
+ }
291
+ }
@@ -0,0 +1,265 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { aggregate, resolveRuns, verifyLegacyProjection } from '../control-state/index.js';
4
+ import { doctorRequiredFiles } from '../scaffold/plan.js';
5
+ import { analyzeDeliveryMap } from './delivery-map.js';
6
+ import { evaluateVerifiedChange } from './verified-change.js';
7
+ import { analyzeArtefactRoleConsistency, analyzeDurableGateArtefactConsistency, modeSliceDecision, readRunState } from './run-state.js';
8
+ import { addFinding, allowNoActiveRuns, filled, hasFilledEvidenceRow, hasFilledTableRow, isPlaceholderValue, markdownSection, nonEmptyTableRows, parseBacklogSection, parseQualityContracts, readTargetFile, runSelectionRecovery, tableRows } from './shared.js';
9
+
10
+ export function evaluateDoctor(targetDir, selection = {}) {
11
+ if (selection.allActive) {
12
+ const selected = resolveRuns(targetDir, { allActive: true });
13
+ const runs = selected.runs.map((run) => ({
14
+ run_id: run.run_id,
15
+ report: evaluateDoctor(targetDir, { runId: run.run_id }),
16
+ }));
17
+ const status = selected.findings.length
18
+ ? "block"
19
+ : aggregate(
20
+ runs.map((item) => ({ run_id: item.run_id, status: item.report.status })),
21
+ { allowNoActiveRuns: allowNoActiveRuns(targetDir) },
22
+ ).status;
23
+ const findings = [
24
+ ...selected.findings.map((finding) => ({
25
+ ...finding,
26
+ severity: "block",
27
+ message: "Invalid canonical run entry.",
28
+ next_step: "Repair or remove the invalid run entry.",
29
+ })),
30
+ ...runs.flatMap((item) => item.report.findings.map((finding) => ({ ...finding, run_id: item.run_id }))),
31
+ ];
32
+ return {
33
+ schema_version: "1",
34
+ status,
35
+ checked_at: new Date().toISOString(),
36
+ target_dir: targetDir,
37
+ summary: {
38
+ findings: findings.length,
39
+ block: findings.filter((finding) => finding.severity === "block").length,
40
+ revise: findings.filter((finding) => finding.severity === "revise").length,
41
+ warn: findings.filter((finding) => finding.severity === "warn").length,
42
+ },
43
+ findings,
44
+ runs,
45
+ };
46
+ }
47
+ const findings = [];
48
+ const hasCanonicalRuns = existsSync(join(targetDir, ".agdf", "control", "runs"));
49
+ if (hasCanonicalRuns) {
50
+ const projection = verifyLegacyProjection(targetDir);
51
+ if (!["absent", "valid"].includes(projection.status)) {
52
+ addFinding(
53
+ findings,
54
+ "block",
55
+ "AGDF_LEGACY_PROJECTION_DRIFT",
56
+ `Legacy compatibility state is not a valid projection: ${projection.status}.`,
57
+ join(".agdf", "control", "AGDF_RUN.md"),
58
+ "Regenerate the explicit legacy projection or remove it after legacy consumers are retired.",
59
+ );
60
+ }
61
+ }
62
+ const missing = doctorRequiredFiles.filter(
63
+ (relativePath) => !(relativePath.endsWith("AGDF_RUN.md") && hasCanonicalRuns) && !existsSync(join(targetDir, relativePath)),
64
+ );
65
+
66
+ for (const relativePath of missing) {
67
+ const templatePath = join(dirname(relativePath), "templates", relativePath.split("/").at(-1));
68
+ const hasTemplate = existsSync(join(targetDir, templatePath));
69
+ addFinding(
70
+ findings,
71
+ "block",
72
+ "AGDF_CONTROL_FILE_MISSING",
73
+ hasTemplate
74
+ ? "Live control file is missing; only the template exists."
75
+ : "Required live control file is missing.",
76
+ relativePath,
77
+ "Run npx --yes @agdf/cli@latest init only when the repository should own durable AGDF control state or deterministic setup is explicitly needed.",
78
+ );
79
+ }
80
+
81
+ if (missing.length === 0) {
82
+ const selectedRunState = readRunState(targetDir, selection);
83
+ const runPath = selectedRunState.path;
84
+ const run = selectedRunState.content;
85
+
86
+ if (selectedRunState.resolution_error) {
87
+ addFinding(
88
+ findings,
89
+ "block",
90
+ selectedRunState.resolution_error.split(":")[0] || "AGDF_ACTIVE_RUN_UNRESOLVED",
91
+ `Run selection failed: ${selectedRunState.resolution_error}`,
92
+ runPath,
93
+ runSelectionRecovery(selection.target),
94
+ );
95
+ } else {
96
+ const currentGateLine = run.match(/^- current_gate:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
97
+ const nextActionLine = run.match(/^- next_allowed_action:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
98
+ const hasEvidence = hasFilledEvidenceRow(run);
99
+
100
+ if (isPlaceholderValue(currentGateLine)) {
101
+ addFinding(
102
+ findings,
103
+ "revise",
104
+ "AGDF_CURRENT_GATE_MISSING",
105
+ "The selected run state does not name the current gate.",
106
+ runPath,
107
+ "Set current_gate to the current delivery gate or none.",
108
+ );
109
+ }
110
+
111
+ if (isPlaceholderValue(nextActionLine)) {
112
+ addFinding(
113
+ findings,
114
+ "revise",
115
+ "AGDF_NEXT_ALLOWED_ACTION_MISSING",
116
+ "The selected run state does not state the next allowed action.",
117
+ runPath,
118
+ "Fill the next allowed action before asking an agent to continue delivery work.",
119
+ );
120
+ }
121
+
122
+ if (!hasEvidence) {
123
+ addFinding(
124
+ findings,
125
+ "warn",
126
+ "AGDF_EVIDENCE_EMPTY",
127
+ "The selected run state has no visible evidence row yet.",
128
+ runPath,
129
+ "Add at least one evidence row or explicitly document that no evidence exists yet.",
130
+ );
131
+ }
132
+ }
133
+
134
+ const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
135
+ const backlog = readTargetFile(targetDir, backlogPath);
136
+ parseBacklogSection(markdownSection(backlog, "Active Backlog"), findings, backlogPath);
137
+ parseBacklogSection(markdownSection(backlog, "Planned / Parking Lot"), findings, backlogPath);
138
+ const completedRows = tableRows(markdownSection(backlog, "Completed / Superseded Pointers"))
139
+ .slice(1)
140
+ .filter((cells) => cells.some((cell) => filled(cell)));
141
+ if (!hasFilledTableRow(backlog, /^P[0-9]/) && completedRows.length === 0) {
142
+ addFinding(
143
+ findings,
144
+ "warn",
145
+ "AGDF_BACKLOG_POINTER_EMPTY",
146
+ "MASTER_BACKLOG.md does not contain an active or planned work pointer.",
147
+ backlogPath,
148
+ "Add the active item or document that no governed delivery item is active.",
149
+ );
150
+ }
151
+
152
+ const sotPath = join(".agdf", "control", "SOT_REGISTRY.md");
153
+ const sot = readTargetFile(targetDir, sotPath);
154
+ if (!hasFilledTableRow(sot, /Product intent|Architecture|Runtime contracts|UX \/ user flows|Operations \/ release/)) {
155
+ addFinding(
156
+ findings,
157
+ "warn",
158
+ "AGDF_SOT_REGISTRY_EMPTY",
159
+ "SOT_REGISTRY.md has no filled primary source-of-truth row.",
160
+ sotPath,
161
+ "Assign one primary source of truth for at least the domains relevant to the next run.",
162
+ );
163
+ }
164
+
165
+ const activeDomains = new Map();
166
+ for (const cells of nonEmptyTableRows(sot)) {
167
+ const [domain, document, status] = cells;
168
+ if (!domain || !document || status !== "active") continue;
169
+ activeDomains.set(domain, (activeDomains.get(domain) ?? 0) + 1);
170
+ }
171
+ for (const [domain, count] of activeDomains.entries()) {
172
+ if (count > 1) {
173
+ addFinding(
174
+ findings,
175
+ "block",
176
+ "AGDF_PARALLEL_SOT",
177
+ `Domain has ${count} active source-of-truth rows: ${domain}.`,
178
+ sotPath,
179
+ "Keep exactly one active primary source of truth for this domain.",
180
+ );
181
+ }
182
+ }
183
+
184
+ const contextPath = join(".agdf", "control", "CONTEXT_GRAPH.md");
185
+ const contextGraph = readTargetFile(targetDir, contextPath);
186
+ if (contextGraph.includes("### CG-001 Example")) {
187
+ addFinding(
188
+ findings,
189
+ "warn",
190
+ "AGDF_CONTEXT_GRAPH_TEMPLATE_NODE",
191
+ "CONTEXT_GRAPH.md still contains the example node.",
192
+ contextPath,
193
+ "Remove the example or replace it with an evidenced node that has an exit criterion.",
194
+ );
195
+ }
196
+
197
+ const contractsPath = join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json");
198
+ try {
199
+ parseQualityContracts(readTargetFile(targetDir, contractsPath));
200
+ } catch (error) {
201
+ addFinding(
202
+ findings,
203
+ "block",
204
+ "AGDF_QUALITY_CONTRACTS_INVALID",
205
+ `AGENT_QUALITY_CONTRACTS.json is invalid: ${error.message}`,
206
+ contractsPath,
207
+ "Restore the generated contracts or fix the JSON contract schema.",
208
+ );
209
+ }
210
+
211
+ const runState = selectedRunState;
212
+ if (modeSliceDecision(runState) === "verified_change") {
213
+ const verifiedChange = evaluateVerifiedChange(targetDir, runState);
214
+ for (const finding of verifiedChange.findings) {
215
+ addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
216
+ }
217
+ }
218
+ for (const finding of analyzeDurableGateArtefactConsistency(runState)) {
219
+ addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
220
+ }
221
+ for (const finding of analyzeArtefactRoleConsistency(targetDir, runState)) {
222
+ addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
223
+ }
224
+ for (const finding of analyzeDeliveryMap(runState).findings) {
225
+ addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
226
+ }
227
+ }
228
+
229
+ const severityRank = { block: 3, revise: 2, warn: 1 };
230
+ const maxSeverity = findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
231
+ const status = maxSeverity >= 3 ? "block" : maxSeverity === 2 ? "revise" : maxSeverity === 1 ? "warn" : "pass";
232
+
233
+ return {
234
+ schema_version: "1",
235
+ status,
236
+ checked_at: new Date().toISOString(),
237
+ target_dir: targetDir,
238
+ summary: {
239
+ findings: findings.length,
240
+ block: findings.filter((finding) => finding.severity === "block").length,
241
+ revise: findings.filter((finding) => finding.severity === "revise").length,
242
+ warn: findings.filter((finding) => finding.severity === "warn").length,
243
+ },
244
+ findings,
245
+ };
246
+ }
247
+
248
+ export function printDoctorReport(report, json, io = console) {
249
+ if (json) {
250
+ io.log(JSON.stringify(report, null, 2));
251
+ return;
252
+ }
253
+
254
+ io.log(`AGDF doctor: ${report.status}`);
255
+ io.log(`Checked: ${report.target_dir}`);
256
+ io.log(`Findings: ${report.summary.findings} (${report.summary.block} block, ${report.summary.revise} revise, ${report.summary.warn} warn)`);
257
+
258
+ for (const finding of report.findings) {
259
+ io.log("");
260
+ io.log(`[${finding.severity}] ${finding.code}`);
261
+ io.log(`Path: ${finding.path}`);
262
+ io.log(finding.message);
263
+ io.log(`Next step: ${finding.next_step}`);
264
+ }
265
+ }