create-agdf 0.14.2 → 0.14.4

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.
Files changed (103) hide show
  1. package/README.md +39 -3
  2. package/generated/.opencode/contracts/control-scaffold.md +17 -2
  3. package/generated/.opencode/contracts/interaction.md +2 -2
  4. package/generated/.opencode/skills/agdf-delivery-path-search/SKILL.md +16 -4
  5. package/generated/.opencode/skills/agdf-gate-check/SKILL.md +6 -4
  6. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  7. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  8. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  9. package/generated/plugins/agdf/meta/contracts/control-scaffold.md +17 -2
  10. package/generated/plugins/agdf/meta/contracts/interaction.md +2 -2
  11. package/generated/plugins/agdf/meta/copilot-payload-baseline.json +3 -3
  12. package/generated/plugins/agdf/meta/distribution-profile-history.json +125 -0
  13. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  14. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +8 -1
  15. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/delivery-path-search-command.js +51 -19
  16. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/parse-args.js +3 -0
  17. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +13 -0
  18. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +64 -13
  19. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +6 -0
  20. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-identity.js +9 -0
  21. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +4 -2
  22. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/contracts.js +37 -0
  23. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/persistence.js +14 -2
  24. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/search-engine.js +126 -36
  25. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/state-adapter.js +40 -92
  26. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +57 -7
  27. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +46 -3
  28. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  29. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  30. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +2 -0
  31. package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +16 -4
  32. package/generated/plugins/agdf/skills/gate-check/SKILL.md +6 -4
  33. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  34. package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
  35. package/generated/plugins/agdf/submission/openai/release-notes.md +7 -1
  36. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  37. package/generated/plugins/copilot/agdf/.agdf-payload-inventory.json +71 -51
  38. package/generated/plugins/copilot/agdf/copilot-skills/agdf-delivery-path-search/SKILL.md +16 -4
  39. package/generated/plugins/copilot/agdf/copilot-skills/agdf-gate-check/SKILL.md +6 -4
  40. package/generated/plugins/copilot/agdf/copilot-skills/contracts/control-scaffold.md +17 -2
  41. package/generated/plugins/copilot/agdf/copilot-skills/contracts/interaction.md +2 -2
  42. package/generated/plugins/copilot/agdf/meta/agdf-plugin.definition.json +1 -1
  43. package/generated/plugins/copilot/agdf/meta/distribution-profile-history.json +125 -0
  44. package/generated/plugins/copilot/agdf/plugin.json +1 -1
  45. package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  46. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/command-registry.js +8 -1
  47. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/delivery-path-search-command.js +51 -19
  48. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/parse-args.js +3 -0
  49. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +13 -0
  50. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +64 -13
  51. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +6 -0
  52. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-state/run-identity.js +9 -0
  53. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +4 -2
  54. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/delivery-path-search/contracts.js +37 -0
  55. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/delivery-path-search/persistence.js +14 -2
  56. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/delivery-path-search/search-engine.js +126 -36
  57. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/delivery-path-search/state-adapter.js +40 -92
  58. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/interaction-presentation.js +57 -7
  59. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +46 -3
  60. package/generated/plugins/copilot/agdf/runtime/create-agdf/package.json +1 -1
  61. package/generated/plugins/copilot/agdf/runtime/runtime-manifest.json +2 -2
  62. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  63. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +1 -1
  64. package/generated/submissions/openai/agdf/meta/contracts/control-scaffold.md +17 -2
  65. package/generated/submissions/openai/agdf/meta/contracts/interaction.md +2 -2
  66. package/generated/submissions/openai/agdf/meta/copilot-payload-baseline.json +3 -3
  67. package/generated/submissions/openai/agdf/meta/distribution-profile-history.json +125 -0
  68. package/generated/submissions/openai/agdf/skills/delivery-path-search/SKILL.md +16 -4
  69. package/generated/submissions/openai/agdf/skills/gate-check/SKILL.md +6 -4
  70. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  71. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
  72. package/generated/submissions/openai/agdf/submission/openai/inventory.json +18 -14
  73. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  74. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  75. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  76. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +7 -1
  77. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  78. package/lib/cli/application.js +24 -6
  79. package/lib/cli/command-registry.js +8 -1
  80. package/lib/cli/delivery-path-search-command.js +51 -19
  81. package/lib/cli/parse-args.js +3 -0
  82. package/lib/control-evaluation/doctor.js +13 -0
  83. package/lib/control-evaluation/gate-check.js +64 -13
  84. package/lib/control-evaluation/run-state.js +6 -0
  85. package/lib/control-state/run-identity.js +9 -0
  86. package/lib/control-state/run-state-parser.js +4 -2
  87. package/lib/delivery-path-search/contracts.js +37 -0
  88. package/lib/delivery-path-search/persistence.js +14 -2
  89. package/lib/delivery-path-search/search-engine.js +126 -36
  90. package/lib/delivery-path-search/state-adapter.js +40 -92
  91. package/lib/installers/claude-cache-recovery.js +76 -0
  92. package/lib/installers/copilot-settings.js +146 -16
  93. package/lib/installers/local-marketplace.js +169 -14
  94. package/lib/installers/plugin-installers.js +96 -9
  95. package/lib/interaction-presentation.js +57 -7
  96. package/lib/lifecycle/operations.js +28 -4
  97. package/lib/lifecycle/result.js +4 -4
  98. package/lib/release/profile-history.js +222 -0
  99. package/lib/release/version-bump.js +527 -0
  100. package/lib/release/version-coherence.js +92 -38
  101. package/lib/runtime/distribution-profile-history.js +173 -0
  102. package/lib/runtime/plugin-provenance.js +46 -3
  103. package/package.json +7 -3
@@ -130,6 +130,19 @@ export function evaluateDoctor(targetDir, selection = {}) {
130
130
  "Add at least one evidence row or explicitly document that no evidence exists yet.",
131
131
  );
132
132
  }
133
+
134
+ for (const identityFinding of selectedRunState.identity_findings ?? []) {
135
+ addFinding(
136
+ findings,
137
+ "revise",
138
+ identityFinding.code,
139
+ identityFinding.code === "AGDF_RUN_ID_INVALID"
140
+ ? "The selected run state has a missing or invalid run_id; interaction cards cannot be rendered for it."
141
+ : "The selected run state has a missing or invalid revision_id; approval interactions cannot be rendered for it.",
142
+ runPath,
143
+ "Run run-migrate to repair the run identity, or migrate the legacy state to a canonical run record.",
144
+ );
145
+ }
133
146
  }
134
147
 
135
148
  const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
@@ -1,4 +1,4 @@
1
- import { attachApprovalOrientationSnapshot, buildArtefactRefs, buildQualityReadiness, gateTitle, localePack, renderApprovalOrientationSnapshot, renderOperationalStatusCard, resolveHumanRunTitle } from '../interaction-presentation.js';
1
+ import { attachApprovalOrientationSnapshot, buildArtefactRefs, buildQualityReadiness, gateTitle, localePack, renderApprovalOrientationSnapshot, renderOperationalStatusCard, resolveHumanRunTitle, validateApprovalOrientationPreconditions, validateApprovalOrientationSnapshot, validateOperationalStatusCardPreconditions } from '../interaction-presentation.js';
2
2
  import { interactionLocales, resolveConfiguredChatLanguage } from '../cli/runtime-context.js';
3
3
  import { evaluateDoctor } from './doctor.js';
4
4
  import { analyzeDeliveryMap, deriveQualityOutlook } from './delivery-map.js';
@@ -300,8 +300,9 @@ export function evaluateGateCheck(targetDir, selection = {}) {
300
300
  humanPresentation,
301
301
  revisionId,
302
302
  });
303
+ const readyForApproval = isReadyUserGateApproval({ status, currentGate, missingApproval });
303
304
  const approvalOrientation = attachApprovalOrientationSnapshot(statusCard, {
304
- ready: isReadyUserGateApproval({ status, currentGate, missingApproval }),
305
+ ready: readyForApproval,
305
306
  humanPresentation,
306
307
  revisionId,
307
308
  registry: interactionLocales,
@@ -312,6 +313,50 @@ export function evaluateGateCheck(targetDir, selection = {}) {
312
313
  enumerable: false,
313
314
  });
314
315
 
316
+ const approvalPresentation = renderApprovalOrientationSnapshot(approvalOrientation, {
317
+ registry: interactionLocales,
318
+ expectedIdentity: {
319
+ run_id: statusCard.run_id,
320
+ revision_id: revisionId,
321
+ current_gate: currentGate,
322
+ presentation_language: statusCard.presentation_language,
323
+ },
324
+ });
325
+ const presentationDiagnostics = {};
326
+ if (!statusPresentation) {
327
+ presentationDiagnostics.status_presentation_errors = [
328
+ ...validateOperationalStatusCardPreconditions(statusCard, {
329
+ registry: interactionLocales,
330
+ humanPresentation,
331
+ }).errors,
332
+ ];
333
+ }
334
+ if (readyForApproval && !approvalPresentation) {
335
+ presentationDiagnostics.approval_presentation_errors = approvalOrientation
336
+ ? [
337
+ ...validateApprovalOrientationSnapshot(approvalOrientation, {
338
+ registry: interactionLocales,
339
+ expectedIdentity: {
340
+ run_id: statusCard.run_id,
341
+ revision_id: revisionId,
342
+ current_gate: currentGate,
343
+ presentation_language: statusCard.presentation_language,
344
+ },
345
+ }).errors,
346
+ ]
347
+ : [
348
+ ...validateApprovalOrientationPreconditions({
349
+ statusCard,
350
+ humanPresentation,
351
+ registry: interactionLocales,
352
+ requestedLocale: presentationLocale,
353
+ }).errors,
354
+ ];
355
+ if (!presentationDiagnostics.approval_presentation_errors.length) {
356
+ presentationDiagnostics.approval_presentation_errors = ["snapshot_unavailable"];
357
+ }
358
+ }
359
+
315
360
  return {
316
361
  schema_version: "1",
317
362
  status,
@@ -332,15 +377,8 @@ export function evaluateGateCheck(targetDir, selection = {}) {
332
377
  candidate_runs: runState.candidate_runs ?? [],
333
378
  status_card: statusCard,
334
379
  status_presentation: statusPresentation,
335
- approval_presentation: renderApprovalOrientationSnapshot(approvalOrientation, {
336
- registry: interactionLocales,
337
- expectedIdentity: {
338
- run_id: statusCard.run_id,
339
- revision_id: revisionId,
340
- current_gate: currentGate,
341
- presentation_language: statusCard.presentation_language,
342
- },
343
- }),
380
+ approval_presentation: approvalPresentation,
381
+ ...(Object.keys(presentationDiagnostics).length ? { presentation_diagnostics: presentationDiagnostics } : {}),
344
382
  delivery_map: {
345
383
  relationships: deliveryMap.relationships,
346
384
  mode_slice_decision: runState.mode_slice_decision,
@@ -368,6 +406,14 @@ export function printApprovalEnvelope(report, { io = console, reEvaluate } = {})
368
406
  if (report?.approval_presentation) {
369
407
  io.log(report.approval_presentation.blocks.run_status_card.markdown);
370
408
  io.log("");
409
+ if (report.status_presentation?.markdown) {
410
+ io.log(report.status_presentation.markdown);
411
+ } else {
412
+ const pack = localePack(interactionLocales, report.approval_presentation.presentation_language || "en");
413
+ const codes = report.presentation_diagnostics?.status_presentation_errors ?? [];
414
+ io.log(codes.length ? `${pack.interaction.statusPresentationFailure} (${codes.join(", ")})` : pack.interaction.statusPresentationFailure);
415
+ }
416
+ io.log("");
371
417
  io.log(report.approval_presentation.blocks.gate_transition_card.markdown);
372
418
  io.log("");
373
419
  io.log(report.approval_presentation.approval_interaction.exact_text_fallback);
@@ -386,7 +432,10 @@ export function printApprovalEnvelope(report, { io = console, reEvaluate } = {})
386
432
  const pack = localePack(interactionLocales, refreshed?.status_card?.presentation_language || report?.status_card?.presentation_language || "en");
387
433
  const refreshedApproval = canonicalApprovalForReport(refreshed);
388
434
  if (initialApproval && refreshedApproval) {
389
- io.log(pack.interaction.presentationFailure);
435
+ const codes = refreshed?.presentation_diagnostics?.approval_presentation_errors
436
+ ?? report?.presentation_diagnostics?.approval_presentation_errors
437
+ ?? [];
438
+ io.log(codes.length ? `${pack.interaction.presentationFailure} (${codes.join(", ")})` : pack.interaction.presentationFailure);
390
439
  io.log(pack.interaction.exactTextRequest.replace("{approval}", refreshedApproval));
391
440
  return Object.freeze({ outcome: "exact_text_recovery", requested_decision: true, status: refreshed.status });
392
441
  }
@@ -404,7 +453,9 @@ export function printApprovalEnvelope(report, { io = console, reEvaluate } = {})
404
453
  function printGateCheckStatusCard(report, io) {
405
454
  const card = report.status_card;
406
455
  if (!report.status_presentation?.markdown) {
407
- io.log(localePack(interactionLocales, card?.presentation_language || "en").interaction.statusPresentationFailure);
456
+ const failure = localePack(interactionLocales, card?.presentation_language || "en").interaction.statusPresentationFailure;
457
+ const codes = report.presentation_diagnostics?.status_presentation_errors ?? [];
458
+ io.log(codes.length ? `${failure} (${codes.join(", ")})` : failure);
408
459
  return false;
409
460
  }
410
461
  io.log(report.status_presentation.markdown);
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, join, relative, resolve } from "node:path";
3
3
  import process from "node:process";
4
4
  import { parseControlState, resolveRuns } from "../control-state/index.js";
5
+ import { validateRunIdentity } from "../control-state/run-identity.js";
5
6
  import { buildRunCandidates } from "../interaction-presentation.js";
6
7
  import { cleanStatusCell, filled, isPlaceholderValue, readTargetFile } from "./shared.js";
7
8
  import { extractField, isSafeRepoRelativePath, readVerifiedChangeRecord } from "./verified-change.js";
@@ -87,6 +88,7 @@ export function readRunState(targetDir, selection = {}) {
87
88
  memory: {},
88
89
  candidate_runs: candidateRuns,
89
90
  resolution_error: resolutionError,
91
+ identity_findings: [],
90
92
  };
91
93
  }
92
94
 
@@ -101,6 +103,10 @@ export function readRunState(targetDir, selection = {}) {
101
103
  path: runPath,
102
104
  content,
103
105
  ...parsed,
106
+ identity_findings: validateRunIdentity({
107
+ runId: extractField(content, "run_id").replace(/^`|`$/g, ""),
108
+ revisionId: extractField(content, "revision_id").replace(/^`|`$/g, ""),
109
+ }),
104
110
  };
105
111
  }
106
112
 
@@ -0,0 +1,9 @@
1
+ export const RUN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
2
+ export const REVISION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+
4
+ export function validateRunIdentity({ runId, revisionId } = {}) {
5
+ const findings = [];
6
+ if (!RUN_ID_PATTERN.test(String(runId ?? ""))) findings.push({ code: "AGDF_RUN_ID_INVALID" });
7
+ if (!REVISION_ID_PATTERN.test(String(revisionId ?? ""))) findings.push({ code: "AGDF_RUN_REVISION_ID_INVALID" });
8
+ return findings;
9
+ }
@@ -1,4 +1,6 @@
1
- export const RUN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
1
+ import { REVISION_ID_PATTERN, RUN_ID_PATTERN } from "./run-identity.js";
2
+
3
+ export { RUN_ID_PATTERN } from "./run-identity.js";
2
4
  export const LIFECYCLES = new Set([
3
5
  "active",
4
6
  "completed",
@@ -34,7 +36,7 @@ export function parseRunState(content, expected) {
34
36
  findings.push({ code: "AGDF_RUN_LIFECYCLE_INVALID" });
35
37
  if (!/^[1-9]\d*$/.test(values.get("revision") ?? ""))
36
38
  findings.push({ code: "AGDF_RUN_REVISION_INVALID" });
37
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(values.get("revision_id") ?? ""))
39
+ if (!REVISION_ID_PATTERN.test(values.get("revision_id") ?? ""))
38
40
  findings.push({ code: "AGDF_RUN_REVISION_ID_INVALID" });
39
41
  return {
40
42
  content,
@@ -68,6 +68,10 @@ export function validateSearchInput(value) {
68
68
  }
69
69
  requireString(input.objective, "objective");
70
70
  requireString(input.current_gate, "current_gate");
71
+ optionalBoundedString(input.scope_revision, "scope_revision", 200);
72
+ optionalBoundedString(input.input_failure_code, "input_failure_code", 200);
73
+ optionalBoundedString(input.input_failure_detail, "input_failure_detail", 1000);
74
+ optionalBoundedString(input.input_recovery_action, "input_recovery_action", 1000);
71
75
  stringArray(input.allowed_actions, "allowed_actions");
72
76
  stringArray(input.forbidden_actions, "forbidden_actions");
73
77
  stringArray(input.evidence_refs ?? [], "evidence_refs");
@@ -81,6 +85,39 @@ export function validateSearchInput(value) {
81
85
  return structuredClone(input);
82
86
  }
83
87
 
88
+ const RESULT_PHASE_BY_STATUS = new Map([
89
+ ["input_unavailable", "input"],
90
+ ["no_legal_candidates", "candidate"],
91
+ ["evaluator_unavailable", "evaluation"],
92
+ ["evaluator_error", "evaluation"],
93
+ ["recommendation", "search"],
94
+ ["no_safe_recommendation", "search"],
95
+ ]);
96
+
97
+ export function validateSearchResult(value) {
98
+ const result = requireObject(value, "search result");
99
+ if (result.contract_version !== CONTRACT_VERSION) throw new Error(`unsupported search result contract_version: ${result.contract_version}`);
100
+ const status = requireString(result.status, "result.status");
101
+ const expectedPhase = RESULT_PHASE_BY_STATUS.get(status);
102
+ if (!expectedPhase) throw new Error(`unsupported search result status: ${status}`);
103
+ if (result.outcome_phase !== expectedPhase) throw new Error(`${status} requires outcome_phase ${expectedPhase}`);
104
+ requireString(result.scope_key, "result.scope_key");
105
+ optionalBoundedString(result.scope_revision, "result.scope_revision", 200);
106
+ const provenance = requireObject(result.provenance, "result.provenance");
107
+ for (const key of ["baseline_candidates", "generated_candidates", "legal_candidates", "rejected_candidates", "evaluation_attempts", "valid_evaluations", "invalid_evaluations"]) {
108
+ if (!Number.isInteger(provenance[key]) || provenance[key] < 0) throw new Error(`result.provenance.${key} must be a non-negative integer`);
109
+ }
110
+ if (provenance.evaluation_attempts !== provenance.valid_evaluations + provenance.invalid_evaluations) throw new Error("result evaluation provenance counts are inconsistent");
111
+ const recommendationFacing = status === "recommendation" || status === "no_safe_recommendation";
112
+ if (recommendationFacing && provenance.valid_evaluations < 1) throw new Error(`${status} requires at least one valid evaluation`);
113
+ if (status === "recommendation" && !result.recommendation) throw new Error("recommendation status requires recommendation content");
114
+ if (status !== "recommendation" && result.recommendation != null) throw new Error(`${status} must not contain recommendation content`);
115
+ if (status === "no_legal_candidates" && (provenance.legal_candidates !== 0 || provenance.evaluation_attempts !== 0)) throw new Error("no_legal_candidates requires zero legal candidates and evaluation attempts");
116
+ if (status === "input_unavailable" && provenance.evaluation_attempts !== 0) throw new Error("input_unavailable requires zero evaluation attempts");
117
+ if ((status === "evaluator_unavailable" || status === "evaluator_error") && provenance.valid_evaluations !== 0) throw new Error(`${status} requires zero valid evaluations`);
118
+ return structuredClone(result);
119
+ }
120
+
84
121
  export function validateCandidate(value) {
85
122
  const candidate = requireObject(value, "candidate");
86
123
  requireString(candidate.id, "candidate.id");
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { validateSearchResult } from "./contracts.js";
3
4
 
4
5
  function safeResult(result) {
5
6
  return JSON.parse(JSON.stringify(result, (key, value) => {
@@ -9,22 +10,33 @@ function safeResult(result) {
9
10
  }
10
11
 
11
12
  export function persistSearchResult(targetDir, result) {
12
- const root = join(targetDir, ".agdf", "control", "artefacts", result.scope_key);
13
+ const validated = validateSearchResult(result);
14
+ if (!["recommendation", "no_safe_recommendation"].includes(validated.status)) {
15
+ throw new Error(`search result status ${validated.status} is not persistable`);
16
+ }
17
+ if (validated.provenance.valid_evaluations < 1) {
18
+ throw new Error("search result requires at least one valid evaluation before persistence");
19
+ }
20
+ const root = join(targetDir, ".agdf", "control", "artefacts", validated.scope_key);
13
21
  const jsonPath = join(root, "DELIVERY_PATH_SEARCH.json");
14
22
  const markdownPath = join(root, "DELIVERY_PATH_SEARCH.md");
15
23
  mkdirSync(dirname(jsonPath), { recursive: true });
16
- const safe = safeResult(result);
24
+ const safe = safeResult(validated);
17
25
  writeFileSync(jsonPath, `${JSON.stringify(safe, null, 2)}\n`, "utf8");
18
26
  const recommendation = safe.recommendation?.action ?? "No safe recommendation";
19
27
  writeFileSync(markdownPath, [
20
28
  "# Delivery Path Search Decision",
21
29
  "",
22
30
  `- status: ${safe.status}`,
31
+ `- outcome_phase: ${safe.outcome_phase}`,
32
+ `- scope_revision: ${safe.scope_revision}`,
23
33
  `- current_gate: ${safe.current_gate}`,
24
34
  `- enforcement: ${safe.enforcement.level}`,
25
35
  `- recommendation: ${recommendation}`,
26
36
  `- stopping_reason: ${safe.stopping_reason}`,
27
37
  `- evaluations: ${safe.budgets.evaluations}`,
38
+ `- evaluation_attempts: ${safe.provenance.evaluation_attempts}`,
39
+ `- invalid_evaluations: ${safe.provenance.invalid_evaluations}`,
28
40
  `- generation_status: ${safe.generation?.status ?? "legacy_deterministic"}`,
29
41
  `- generated_candidates: ${safe.generation ? `${safe.generation.accepted}/${safe.generation.returned}` : "0/0"}`,
30
42
  `- generation_cost_units: ${safe.generation?.cost_units ?? 0}`,
@@ -1,4 +1,4 @@
1
- import { CONTRACT_VERSION, SCORING_POLICY_VERSION, validateEvaluation, validateSearchInput } from "./contracts.js";
1
+ import { CONTRACT_VERSION, SCORING_POLICY_VERSION, validateEvaluation, validateSearchInput, validateSearchResult } from "./contracts.js";
2
2
  import { candidateLegality, candidatesFromInput, generatedCandidatesFromResponse } from "./candidate-policy.js";
3
3
  import { scoreEvaluation } from "./scoring.js";
4
4
  import { generatorRequestFromInput } from "./state-adapter.js";
@@ -14,6 +14,30 @@ function generatorFailureCode(error) {
14
14
  return "generator_schema_invalid";
15
15
  }
16
16
 
17
+ function evaluatorFailureCode(error) {
18
+ if (error?.code === "ENOENT") return "evaluator_unavailable";
19
+ if (/auth/i.test(error?.message ?? "")) return "evaluator_authentication_failed";
20
+ if (/unavailable|not found|not configured/i.test(error?.message ?? "")) return "evaluator_unavailable";
21
+ return "evaluator_output_invalid";
22
+ }
23
+
24
+ export function classifyTerminalOutcome({
25
+ inputUnavailable = false,
26
+ legalCandidates = 0,
27
+ evaluationAttempts = 0,
28
+ validEvaluations = 0,
29
+ invalidEvaluations = 0,
30
+ evaluatorUnavailable = false,
31
+ leader = null,
32
+ }) {
33
+ if (inputUnavailable) return { status: "input_unavailable", outcome_phase: "input" };
34
+ if (legalCandidates === 0) return { status: "no_legal_candidates", outcome_phase: "candidate" };
35
+ if (validEvaluations === 0 && evaluatorUnavailable) return { status: "evaluator_unavailable", outcome_phase: "evaluation" };
36
+ if (validEvaluations === 0 && legalCandidates > 0) return { status: "evaluator_error", outcome_phase: "evaluation" };
37
+ if (leader) return { status: "recommendation", outcome_phase: "search" };
38
+ return { status: "no_safe_recommendation", outcome_phase: "search" };
39
+ }
40
+
17
41
  export async function runDeliveryPathSearch(inputValue, evaluator, options = {}) {
18
42
  const input = validateSearchInput(inputValue);
19
43
  const startedAt = Date.now();
@@ -22,12 +46,18 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
22
46
  const rejected = [];
23
47
  const evaluated = [];
24
48
  let evaluations = 0;
49
+ let evaluationAttempts = 0;
50
+ let invalidEvaluations = 0;
51
+ let legalCandidates = 0;
52
+ let rejectedCandidates = 0;
53
+ let evaluatorUnavailable = false;
54
+ let evaluationFailureCode = null;
25
55
  let costUnits = 0;
26
56
  let lastLeaderId = null;
27
57
  let stableComparisons = 0;
28
58
  let stoppingReason = "candidate_queue_exhausted";
29
59
  const generation = {
30
- status: input.generation?.enabled ? "failed" : "disabled",
60
+ status: input.generation?.enabled ? "not_run" : "disabled",
31
61
  adapter: options.generator?.metadata ?? null,
32
62
  budgets: input.generation ?? null,
33
63
  returned: 0,
@@ -38,7 +68,66 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
38
68
  failure_code: null,
39
69
  };
40
70
 
71
+ const finish = ({ leader = null, outcome, failureCode = null, recoveryAction = null }) => {
72
+ evaluated.sort((a, b) => b.score - a.score || a.candidate.id.localeCompare(b.candidate.id));
73
+ const result = {
74
+ contract_version: CONTRACT_VERSION,
75
+ scoring_policy_version: SCORING_POLICY_VERSION,
76
+ scope_key: input.scope_key,
77
+ scope_revision: input.scope_revision ?? "unversioned",
78
+ objective: input.objective,
79
+ current_gate: input.current_gate,
80
+ outcome_phase: outcome.outcome_phase,
81
+ status: outcome.status,
82
+ recommendation: outcome.status === "recommendation" && leader ? {
83
+ candidate_id: leader.candidate.id,
84
+ action: leader.candidate.action,
85
+ score: leader.score,
86
+ rationale: leader.evaluation.rationale,
87
+ expected_evidence: leader.candidate.expected_evidence,
88
+ tests: leader.candidate.tests,
89
+ risks: leader.evaluation.risks,
90
+ assumptions: leader.evaluation.assumptions,
91
+ } : null,
92
+ alternatives: outcome.outcome_phase === "search" ? evaluated.slice(1).map((item) => ({
93
+ candidate_id: item.candidate.id,
94
+ action: item.candidate.action,
95
+ score: item.score,
96
+ rejection_reason: `ranked_below_${leader?.candidate.id ?? "search_policy"}`,
97
+ })) : [],
98
+ rejected,
99
+ provenance: {
100
+ baseline_candidates: baseline.length,
101
+ generated_candidates: generation.accepted,
102
+ legal_candidates: legalCandidates,
103
+ rejected_candidates: rejectedCandidates + generation.rejected.length,
104
+ evaluation_attempts: evaluationAttempts,
105
+ valid_evaluations: evaluations,
106
+ invalid_evaluations: invalidEvaluations,
107
+ },
108
+ enforcement: input.enforcement,
109
+ evaluator: evaluator?.metadata ?? { name: evaluator?.name ?? "unknown" },
110
+ generation,
111
+ budgets: { configured: input.budgets, evaluations, cost_units: costUnits, stable_comparisons: stableComparisons, duration_ms: Date.now() - startedAt },
112
+ stopping_reason: stoppingReason,
113
+ failure_code: failureCode,
114
+ next_action: recoveryAction ?? "Run canonical AGDF gate-check; search does not grant permission.",
115
+ next_gate_action: recoveryAction ?? "Run canonical AGDF gate-check; search does not grant permission.",
116
+ };
117
+ return validateSearchResult(result);
118
+ };
119
+
120
+ if (input.input_failure_code || input.allowed_actions.length === 0) {
121
+ stoppingReason = input.input_failure_code || "canonical_actions_unavailable";
122
+ return finish({
123
+ outcome: classifyTerminalOutcome({ inputUnavailable: true }),
124
+ failureCode: input.input_failure_code || "canonical_actions_unavailable",
125
+ recoveryAction: input.input_recovery_action || "Run canonical AGDF gate-check and repair the selected control state.",
126
+ });
127
+ }
128
+
41
129
  if (input.generation?.enabled) {
130
+ generation.status = "failed";
42
131
  if (!options.generator) {
43
132
  generation.failure_code = "generator_unavailable";
44
133
  } else {
@@ -75,17 +164,20 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
75
164
  }
76
165
 
77
166
  while (queue.length > 0) {
78
- if (evaluations >= input.budgets.max_evaluations) { stoppingReason = "evaluation_budget_exhausted"; break; }
79
- if (costUnits >= input.budgets.max_cost_units) { stoppingReason = "cost_budget_exhausted"; break; }
80
- if (Date.now() - startedAt >= input.budgets.max_duration_ms) { stoppingReason = "duration_budget_exhausted"; break; }
81
167
  const candidate = queue.shift();
82
168
  const legality = candidateLegality(candidate, input);
83
169
  if (!legality.legal) {
170
+ rejectedCandidates += 1;
84
171
  rejected.push({ candidate_id: candidate.id, action: candidate.action, reason: legality.reason });
85
172
  continue;
86
173
  }
174
+ legalCandidates += 1;
175
+ if (evaluations >= input.budgets.max_evaluations) { stoppingReason = "evaluation_budget_exhausted"; evaluationFailureCode = stoppingReason; break; }
176
+ if (costUnits >= input.budgets.max_cost_units) { stoppingReason = "cost_budget_exhausted"; evaluationFailureCode = stoppingReason; break; }
177
+ if (Date.now() - startedAt >= input.budgets.max_duration_ms) { stoppingReason = "duration_budget_exhausted"; evaluationFailureCode = stoppingReason; break; }
87
178
 
88
179
  try {
180
+ evaluationAttempts += 1;
89
181
  const evaluation = validateEvaluation(await evaluator.evaluate(input, candidate), candidate.id);
90
182
  evaluations += 1;
91
183
  costUnits += evaluation.cost;
@@ -115,41 +207,39 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
115
207
  }
116
208
  } catch (error) {
117
209
  if (error?.fatalEvaluator === true) throw error;
210
+ invalidEvaluations += 1;
211
+ evaluationFailureCode = evaluatorFailureCode(error);
212
+ evaluatorUnavailable ||= evaluationFailureCode === "evaluator_unavailable" || evaluationFailureCode === "evaluator_authentication_failed";
118
213
  rejected.push({ candidate_id: candidate.id, action: candidate.action, reason: `invalid_evaluation: ${error.message}` });
214
+ if (evaluatorUnavailable) break;
119
215
  }
120
216
  }
121
217
 
122
218
  evaluated.sort((a, b) => b.score - a.score || a.candidate.id.localeCompare(b.candidate.id));
123
219
  const leader = evaluated[0];
124
- if (!leader) stoppingReason = rejected.length ? "no_safe_candidate" : stoppingReason;
125
- return {
126
- contract_version: CONTRACT_VERSION,
127
- scoring_policy_version: SCORING_POLICY_VERSION,
128
- scope_key: input.scope_key,
129
- current_gate: input.current_gate,
130
- status: leader ? "recommendation" : "no_safe_recommendation",
131
- recommendation: leader ? {
132
- candidate_id: leader.candidate.id,
133
- action: leader.candidate.action,
134
- score: leader.score,
135
- rationale: leader.evaluation.rationale,
136
- expected_evidence: leader.candidate.expected_evidence,
137
- tests: leader.candidate.tests,
138
- risks: leader.evaluation.risks,
139
- assumptions: leader.evaluation.assumptions,
140
- } : null,
141
- alternatives: evaluated.slice(1).map((item) => ({
142
- candidate_id: item.candidate.id,
143
- action: item.candidate.action,
144
- score: item.score,
145
- rejection_reason: `ranked_below_${leader?.candidate.id ?? "safety_threshold"}`,
146
- })),
147
- rejected,
148
- enforcement: input.enforcement,
149
- evaluator: evaluator.metadata ?? { name: evaluator.name ?? "unknown" },
150
- generation,
151
- budgets: { configured: input.budgets, evaluations, cost_units: costUnits, stable_comparisons: stableComparisons, duration_ms: Date.now() - startedAt },
152
- stopping_reason: stoppingReason,
153
- next_gate_action: "Run canonical AGDF gate-check; search does not grant permission.",
154
- };
220
+ const outcome = classifyTerminalOutcome({
221
+ legalCandidates,
222
+ evaluationAttempts,
223
+ validEvaluations: evaluations,
224
+ invalidEvaluations,
225
+ evaluatorUnavailable,
226
+ leader,
227
+ });
228
+ if (!leader) {
229
+ stoppingReason = outcome.status === "no_legal_candidates"
230
+ ? "no_legal_candidates"
231
+ : outcome.status === "evaluator_unavailable"
232
+ ? "evaluator_unavailable"
233
+ : outcome.status === "evaluator_error"
234
+ ? "evaluator_error"
235
+ : stoppingReason;
236
+ }
237
+ return finish({
238
+ leader,
239
+ outcome,
240
+ failureCode: outcome.outcome_phase === "evaluation" ? evaluationFailureCode : null,
241
+ recoveryAction: outcome.outcome_phase === "evaluation"
242
+ ? "Repair the declared evaluator capability and retry from a fresh canonical control snapshot."
243
+ : null,
244
+ });
155
245
  }