create-agdf 0.11.4 → 0.13.0

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 (111) hide show
  1. package/README.md +1 -1
  2. package/generated/.agdf/control/templates/artefacts/BROWNFIELD_REVIEW.md +25 -0
  3. package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +26 -0
  4. package/generated/.github/skills/agdf-gate-check/SKILL.md +40 -18
  5. package/generated/.github/skills/agdf-interaction-locales.json +50 -0
  6. package/generated/.github/skills/agdf-runtime-contract.md +1 -0
  7. package/generated/.github/skills/contracts/gate-transition.md +7 -3
  8. package/generated/.github/skills/contracts/interaction.md +22 -0
  9. package/generated/.github/skills/contracts/modes.md +81 -0
  10. package/generated/.github/skills/contracts/task-target-resolution.md +102 -0
  11. package/generated/.opencode/AGDF.md +2 -0
  12. package/generated/.opencode/agdf-interaction-locales.json +50 -0
  13. package/generated/.opencode/agdf-runtime-contract.md +1 -0
  14. package/generated/.opencode/contracts/gate-transition.md +7 -3
  15. package/generated/.opencode/contracts/interaction.md +22 -0
  16. package/generated/.opencode/contracts/modes.md +81 -0
  17. package/generated/.opencode/contracts/task-target-resolution.md +102 -0
  18. package/generated/.opencode/skills/agdf-brownfield-analysis/SKILL.md +26 -0
  19. package/generated/.opencode/skills/agdf-gate-check/SKILL.md +40 -18
  20. package/generated/AGENTS.md +1 -0
  21. package/generated/plugins/agdf/.claude-plugin/plugin.json +2 -2
  22. package/generated/plugins/agdf/.codex-plugin/plugin.json +9 -6
  23. package/generated/plugins/agdf/control/templates/artefacts/BROWNFIELD_REVIEW.md +25 -0
  24. package/generated/plugins/agdf/meta/agdf-agent-router.md +16 -0
  25. package/generated/plugins/agdf/meta/agdf-interaction-locales.json +50 -0
  26. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +41 -5
  27. package/generated/plugins/agdf/meta/agdf-runtime-contract.md +1 -0
  28. package/generated/plugins/agdf/meta/contracts/gate-transition.md +7 -3
  29. package/generated/plugins/agdf/meta/contracts/interaction.md +22 -0
  30. package/generated/plugins/agdf/meta/contracts/modes.md +81 -0
  31. package/generated/plugins/agdf/meta/contracts/task-target-resolution.md +102 -0
  32. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +50 -0
  33. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +41 -5
  34. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-policy.js +14 -7
  35. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +85 -0
  36. package/generated/plugins/agdf/runtime/create-agdf/package.json +4 -65
  37. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  38. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +146 -40
  39. package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +26 -0
  40. package/generated/plugins/agdf/skills/gate-check/SKILL.md +40 -18
  41. package/generated/plugins/agdf/submission/openai/availability.md +16 -0
  42. package/generated/plugins/agdf/submission/openai/capability-matrix.json +49 -0
  43. package/generated/plugins/agdf/submission/openai/release-notes.md +16 -0
  44. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +96 -0
  45. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +43 -0
  46. package/generated/submissions/openai/agdf/assets/agdf-icon.svg +16 -0
  47. package/generated/submissions/openai/agdf/assets/agdf-logo.svg +16 -0
  48. package/generated/submissions/openai/agdf/meta/agdf-agent-router.md +129 -0
  49. package/generated/submissions/openai/agdf/meta/agdf-constitution.md +329 -0
  50. package/generated/submissions/openai/agdf/meta/agdf-interaction-locales.json +514 -0
  51. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +263 -0
  52. package/generated/submissions/openai/agdf/meta/agdf-runtime-contract.md +16 -0
  53. package/generated/submissions/openai/agdf/meta/agdf-tenets.md +201 -0
  54. package/generated/submissions/openai/agdf/meta/contracts/closeout.md +29 -0
  55. package/generated/submissions/openai/agdf/meta/contracts/context-graph.md +48 -0
  56. package/generated/submissions/openai/agdf/meta/contracts/control-scaffold.md +115 -0
  57. package/generated/submissions/openai/agdf/meta/contracts/gate-transition.md +165 -0
  58. package/generated/submissions/openai/agdf/meta/contracts/interaction.md +476 -0
  59. package/generated/submissions/openai/agdf/meta/contracts/modes.md +183 -0
  60. package/generated/submissions/openai/agdf/meta/contracts/quality.md +157 -0
  61. package/generated/submissions/openai/agdf/meta/contracts/task-target-resolution.md +102 -0
  62. package/generated/submissions/openai/agdf/skills/brownfield-analysis/SKILL.md +175 -0
  63. package/generated/submissions/openai/agdf/skills/brownfield-analysis/help.md +23 -0
  64. package/generated/submissions/openai/agdf/skills/clean-implementation-review/SKILL.md +126 -0
  65. package/generated/submissions/openai/agdf/skills/clean-implementation-review/help.md +23 -0
  66. package/generated/submissions/openai/agdf/skills/code-review/SKILL.md +119 -0
  67. package/generated/submissions/openai/agdf/skills/code-review/help.md +23 -0
  68. package/generated/submissions/openai/agdf/skills/delivery-closeout/SKILL.md +118 -0
  69. package/generated/submissions/openai/agdf/skills/delivery-closeout/help.md +23 -0
  70. package/generated/submissions/openai/agdf/skills/delivery-path-search/SKILL.md +94 -0
  71. package/generated/submissions/openai/agdf/skills/delivery-path-search/help.md +23 -0
  72. package/generated/submissions/openai/agdf/skills/gate-check/SKILL.md +238 -0
  73. package/generated/submissions/openai/agdf/skills/gate-check/help.md +23 -0
  74. package/generated/submissions/openai/agdf/skills/qa-gate/SKILL.md +137 -0
  75. package/generated/submissions/openai/agdf/skills/qa-gate/help.md +23 -0
  76. package/generated/submissions/openai/agdf/skills/release-or/SKILL.md +137 -0
  77. package/generated/submissions/openai/agdf/skills/release-or/help.md +23 -0
  78. package/generated/submissions/openai/agdf/skills/task-plan-review/SKILL.md +157 -0
  79. package/generated/submissions/openai/agdf/skills/task-plan-review/help.md +23 -0
  80. package/generated/submissions/openai/agdf/skills/ux-intent-definition/SKILL.md +94 -0
  81. package/generated/submissions/openai/agdf/skills/ux-intent-definition/help.md +17 -0
  82. package/generated/submissions/openai/agdf/submission/openai/availability.md +16 -0
  83. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +49 -0
  84. package/generated/submissions/openai/agdf/submission/openai/inventory.json +175 -0
  85. package/generated/submissions/openai/agdf/submission/openai/listing.json +38 -0
  86. package/generated/submissions/openai/agdf/submission/openai/readiness.json +40 -0
  87. package/generated/submissions/openai/agdf/submission/openai/readiness.md +24 -0
  88. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +16 -0
  89. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +96 -0
  90. package/lib/cli/application.js +13 -2
  91. package/lib/control-evaluation/gate-policy.js +14 -7
  92. package/lib/installers/local-marketplace.js +1 -1
  93. package/lib/installers/opencode.js +128 -1
  94. package/lib/interaction-presentation.js +85 -0
  95. package/lib/live-agent/read-only-structured.js +43 -0
  96. package/lib/proportionality-benchmark/blind-prompt.js +37 -0
  97. package/lib/proportionality-benchmark/contracts.js +120 -0
  98. package/lib/proportionality-benchmark/corpus-loader.js +110 -0
  99. package/lib/proportionality-benchmark/evaluator.js +250 -0
  100. package/lib/proportionality-benchmark/index.js +7 -0
  101. package/lib/proportionality-benchmark/live-recorder.js +100 -0
  102. package/lib/proportionality-benchmark/report.js +51 -0
  103. package/lib/proportionality-benchmark/source-fingerprint.js +30 -0
  104. package/lib/public-plugin/builder.js +82 -0
  105. package/lib/public-plugin/contract.js +102 -0
  106. package/lib/public-plugin/manifest.js +72 -0
  107. package/lib/public-plugin/report.js +33 -0
  108. package/lib/public-plugin/validator.js +101 -0
  109. package/lib/skill-evals/live-recorder.js +3 -26
  110. package/opencode-plugin.js +40 -1
  111. package/package.json +7 -2
@@ -349,6 +349,91 @@ export function renderOperationalStatusCard(statusCard, {
349
349
  });
350
350
  }
351
351
 
352
+ export function renderTaskTargetOrientation(resolution, {
353
+ registry,
354
+ requestedLocale,
355
+ } = {}) {
356
+ if (!plainObject(resolution)) return null;
357
+
358
+ const resolutionState = String(resolution.resolution_state ?? "").trim();
359
+ const reasonCode = String(resolution.reason_code ?? "").trim();
360
+ const primaryTarget = String(resolution.primary_target ?? "").trim();
361
+ const governanceTarget = String(resolution.governance_target ?? "").trim();
362
+ const workingDirectory = String(resolution.working_directory ?? "").trim();
363
+ const nextAction = String(resolution.next_action ?? "").trim();
364
+ if (typeof resolution.target_changed !== "boolean") return null;
365
+ const targetChanged = resolution.target_changed === true;
366
+ const evidenceSources = Array.isArray(resolution.evidence_sources)
367
+ ? resolution.evidence_sources.map((item) => String(item ?? "").trim()).filter(Boolean)
368
+ : null;
369
+
370
+ const resolvedReasons = new Set(["explicit_target", "continued_target"]);
371
+ const unresolvedReasons = new Set([
372
+ "multiple_plausible_targets",
373
+ "target_content_mismatch",
374
+ "target_unavailable",
375
+ "no_reliable_target",
376
+ ]);
377
+
378
+ if (!["resolved", "unresolved"].includes(resolutionState)) return null;
379
+ if (!workingDirectory || evidenceSources === null) return null;
380
+ if (resolutionState === "resolved") {
381
+ if (!resolvedReasons.has(reasonCode) || !primaryTarget || nextAction) return null;
382
+ } else if (!unresolvedReasons.has(reasonCode) || primaryTarget || governanceTarget || !nextAction || targetChanged) {
383
+ return null;
384
+ }
385
+
386
+ let locale;
387
+ try {
388
+ locale = resolvePresentationLocale(registry, requestedLocale);
389
+ } catch {
390
+ return null;
391
+ }
392
+ if (!locale) return null;
393
+
394
+ const pack = localePack(registry, locale);
395
+ const labels = pack?.taskTargetResolution;
396
+ if (!plainObject(labels) || !plainObject(labels.reasonCodes)) return null;
397
+ const reason = labels.reasonCodes[reasonCode];
398
+ if (!reason) return null;
399
+
400
+ const rows = resolutionState === "resolved"
401
+ ? [
402
+ [labels.state, labels.resolved],
403
+ [labels.primaryTarget, primaryTarget],
404
+ [labels.governanceTarget, governanceTarget || labels.none],
405
+ [labels.evidenceSources, evidenceSources.join("; ") || labels.none],
406
+ [labels.workingDirectory, workingDirectory],
407
+ [labels.reason, reason],
408
+ ...(targetChanged ? [[labels.targetChanged, labels.yes]] : []),
409
+ ]
410
+ : [
411
+ [labels.state, labels.unresolved],
412
+ [labels.reason, reason],
413
+ [labels.evidenceSources, evidenceSources.join("; ") || labels.none],
414
+ [labels.workingDirectory, workingDirectory],
415
+ [labels.nextAction, nextAction],
416
+ ];
417
+
418
+ const markdown = [
419
+ `## ${labels.title}`,
420
+ "",
421
+ `| ${labels.field} | ${labels.value} |`,
422
+ "|---|---|",
423
+ ...rows.map(([label, value]) => `| ${markdownCell(label)} | ${markdownCell(value)} |`),
424
+ ].join("\n");
425
+
426
+ return Object.freeze({
427
+ schema_version: "1",
428
+ semantic_block: "task_target_orientation",
429
+ presentation_language: locale,
430
+ resolution_state: resolutionState,
431
+ reason_code: reasonCode,
432
+ markdown,
433
+ authorizes: false,
434
+ });
435
+ }
436
+
352
437
  export function renderScopeClassificationCard(classification, {
353
438
  registry,
354
439
  requestedLocale,
@@ -0,0 +1,43 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { guardedExecFileSync } from "../delivery-path-search/transports/read-only-guard.js";
5
+
6
+ export function executeStructuredAgent({ surface, cwd, prompt, model, outputSchema, timeoutMs = 120000 }) {
7
+ if (surface === "codex") {
8
+ const temp = mkdtempSync(join(tmpdir(), "agdf-live-codex-"));
9
+ try {
10
+ const schemaPath = join(temp, "schema.json");
11
+ const outputPath = join(temp, "output.json");
12
+ writeFileSync(schemaPath, `${JSON.stringify(outputSchema, null, 2)}\n`, "utf8");
13
+ const args = [
14
+ "exec", "--sandbox", "read-only", "--ephemeral", "--ignore-user-config",
15
+ "--skip-git-repo-check", "--output-schema", schemaPath,
16
+ "--output-last-message", outputPath, "--color", "never",
17
+ ];
18
+ if (model) args.push("--model", model);
19
+ args.push(prompt);
20
+ guardedExecFileSync("codex", args, {
21
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs,
22
+ });
23
+ return readFileSync(outputPath, "utf8");
24
+ } finally {
25
+ rmSync(temp, { recursive: true, force: true });
26
+ }
27
+ }
28
+ if (surface === "claude") {
29
+ const args = [
30
+ "-p", "--disallowedTools", "Edit,Write,Bash", "--output-format", "json",
31
+ "--json-schema", JSON.stringify(outputSchema),
32
+ ];
33
+ if (model) args.push("--model", model);
34
+ args.push(prompt);
35
+ const raw = guardedExecFileSync("claude", args, {
36
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs,
37
+ });
38
+ const envelope = JSON.parse(raw);
39
+ if (envelope.is_error) throw new Error(`claude live adapter failed: ${envelope.result ?? "unknown error"}`);
40
+ return envelope.result;
41
+ }
42
+ throw new Error(`unsupported live surface: ${surface}`);
43
+ }
@@ -0,0 +1,37 @@
1
+ import { DELIVERY_PATHS } from "./contracts.js";
2
+
3
+ export function buildBlindPrompt(testCase, behaviorSources) {
4
+ if (testCase.profile_id === "staged-v2") {
5
+ return [
6
+ "Perform a read-only, non-authorizing AGDF benchmark classification. Do not modify files, invoke tools, persist control state, or claim a real approval.",
7
+ "Synthetic fixture state is measurement input only. Return only JSON matching the supplied schema.",
8
+ `Allowed stages: ${testCase.allowed_stages.join(", ")}.`,
9
+ `Allowed paths: ${DELIVERY_PATHS.join(", ")}.`,
10
+ `Requested axes: ${testCase.requested_axes.join(", ")}. Every requested axis must use evaluated with one non-null allowed value. Non-requested axes must be null with not_evaluable_yet.`,
11
+ "eventual_delivery_path is a benchmark candidate, never an operative Mode/Slice Decision.",
12
+ "compact_delivery requires visible approved UR, completed Brownfield analysis, and stored quick_task context in decision_grounds.",
13
+ `Scenario ID: ${testCase.scenario_id}`,
14
+ `Case ID: ${testCase.case_id}`,
15
+ `Lifecycle stage: ${testCase.lifecycle_stage}`,
16
+ `Task: ${testCase.task_summary}`,
17
+ `Repository context: ${testCase.repository_context}`,
18
+ `Control-state context: ${testCase.control_state_context}`,
19
+ `Evidence pack: ${JSON.stringify(testCase.evidence_pack ?? {})}`,
20
+ "Current canonical routing sources follow:",
21
+ behaviorSources,
22
+ ].join("\n\n");
23
+ }
24
+ return [
25
+ "Classify the delivery path AGDF would select for this task. Do not modify files or invoke tools.",
26
+ "Return only JSON matching the supplied schema. Do not reveal hidden reasoning.",
27
+ `Allowed paths: ${DELIVERY_PATHS.join(", ")}.`,
28
+ "Use null plus ambiguous=true when the evidence does not support one path.",
29
+ "compact_delivery requires decision_grounds that visibly reference approved UR, completed Brownfield analysis, and stored quick_task context.",
30
+ `Case ID: ${testCase.case_id}`,
31
+ `Task: ${testCase.task_summary}`,
32
+ `Repository context: ${testCase.repository_context}`,
33
+ `Control-state context: ${testCase.control_state_context}`,
34
+ "Current canonical routing sources follow:",
35
+ behaviorSources,
36
+ ].join("\n\n");
37
+ }
@@ -0,0 +1,120 @@
1
+ export const DELIVERY_PATHS = Object.freeze([
2
+ "trivial_change", "quick_task", "compact_delivery",
3
+ "verified_change", "structured_slice", "structured_delivery",
4
+ ]);
5
+ export const PATH_RANK = Object.freeze(Object.fromEntries(DELIVERY_PATHS.map((path, index) => [path, index])));
6
+ export const STAGES = Object.freeze([
7
+ "ungated_execution", "ur", "brownfield_review", "prd", "sd", "tp",
8
+ "brownfield_analysis", "cd_tests", "cr", "qa", "uat", "or", "blocked",
9
+ ]);
10
+ export const OBSERVATION_SCHEMA = Object.freeze({
11
+ type: "object",
12
+ additionalProperties: false,
13
+ required: ["schema_version", "observed_delivery_path", "ambiguous", "rationale", "decision_grounds"],
14
+ properties: {
15
+ schema_version: { type: "string", const: "1" },
16
+ observed_delivery_path: { type: ["string", "null"], enum: [...DELIVERY_PATHS, null] },
17
+ ambiguous: { type: "boolean" },
18
+ rationale: { type: "string", maxLength: 600 },
19
+ decision_grounds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 120 } },
20
+ },
21
+ });
22
+ export const STAGED_OBSERVATION_SCHEMA = Object.freeze({
23
+ type: "object",
24
+ additionalProperties: false,
25
+ required: [
26
+ "schema_version", "observed_next_permissible_stage", "stage_evaluability",
27
+ "observed_delivery_path", "path_evaluability", "rationale", "decision_grounds",
28
+ ],
29
+ properties: {
30
+ schema_version: { type: "string", const: "2" },
31
+ observed_next_permissible_stage: { type: ["string", "null"], enum: [...STAGES, null] },
32
+ stage_evaluability: { type: "string", enum: ["evaluated", "not_evaluable_yet"] },
33
+ observed_delivery_path: { type: ["string", "null"], enum: [...DELIVERY_PATHS, null] },
34
+ path_evaluability: { type: "string", enum: ["evaluated", "not_evaluable_yet"] },
35
+ rationale: { type: "string", maxLength: 600 },
36
+ decision_grounds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 120 } },
37
+ },
38
+ });
39
+
40
+ const forbiddenBaselineKeys = new Set(["observed_delivery_path", "classification", "status", "passed"]);
41
+ const forbiddenText = /(api[_-]?key|access[_-]?token|cookie|authorization:\s*bearer|hidden reasoning|\/Users\/[^/\s]+|[A-Z]:\\Users\\[^\\\s]+)/i;
42
+ export function fail(message, code = "PROPORTIONALITY_SCHEMA_INVALID") {
43
+ throw Object.assign(new Error(message), { code });
44
+ }
45
+ export function stable(value) {
46
+ if (Array.isArray(value)) return value.map(stable);
47
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
48
+ return value;
49
+ }
50
+ export function validateBaseline(value) {
51
+ if (value?.schema_version !== "1" || value?.baseline_version !== "1.0.0") fail("unsupported baseline");
52
+ if (!Array.isArray(value.cases) || value.cases.length !== 40) fail("baseline must contain exactly 40 cases");
53
+ const ids = new Set();
54
+ for (const item of value.cases) {
55
+ if (!/^PB-\d{3}$/.test(item.case_id) || ids.has(item.case_id)) fail(`invalid or duplicate case ${item.case_id}`);
56
+ if (!DELIVERY_PATHS.includes(item.expected_delivery_path)) fail(`unknown path for ${item.case_id}`);
57
+ if (!item.task_summary || !item.rationale || !item.evidence_ref || item.evidence_ref.startsWith("/")) fail(`incomplete case ${item.case_id}`);
58
+ for (const key of forbiddenBaselineKeys) if (key in item) fail(`baseline contains forbidden field ${key}`);
59
+ ids.add(item.case_id);
60
+ }
61
+ if (new Set(value.cases.map((item) => item.expected_delivery_path)).size !== DELIVERY_PATHS.length) fail("baseline path coverage incomplete");
62
+ if (value.cases.filter((item) => item.adversarial).length < 10) fail("adversarial coverage too small");
63
+ return value;
64
+ }
65
+ export function normalizeAgentOutput(raw) {
66
+ let value;
67
+ try { value = typeof raw === "string" ? JSON.parse(raw) : raw; }
68
+ catch { fail("agent output is not JSON", "PROPORTIONALITY_OUTPUT_INVALID"); }
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("agent output is not an object", "PROPORTIONALITY_OUTPUT_INVALID");
70
+ const allowedKeys = new Set(["schema_version", "observed_delivery_path", "ambiguous", "rationale", "decision_grounds"]);
71
+ if (Object.keys(value).some((key) => !allowedKeys.has(key))) fail("agent output contains unknown fields", "PROPORTIONALITY_OUTPUT_INVALID");
72
+ if (value.schema_version !== "1" || typeof value.ambiguous !== "boolean") fail("agent output contract mismatch", "PROPORTIONALITY_OUTPUT_INVALID");
73
+ if (value.observed_delivery_path !== null && !DELIVERY_PATHS.includes(value.observed_delivery_path)) fail("unknown observed path", "PROPORTIONALITY_OUTPUT_INVALID");
74
+ if (typeof value.rationale !== "string" || value.rationale.length > 600 || forbiddenText.test(value.rationale)) fail("unsafe rationale", "PROPORTIONALITY_REDACTION_FAILED");
75
+ if (!Array.isArray(value.decision_grounds) || value.decision_grounds.length > 12 || value.decision_grounds.some((item) => typeof item !== "string" || item.length > 120 || forbiddenText.test(item))) fail("unsafe decision grounds", "PROPORTIONALITY_REDACTION_FAILED");
76
+ let path = value.observed_delivery_path;
77
+ let ambiguous = value.ambiguous || path === null;
78
+ if (path === "compact_delivery") {
79
+ const grounds = value.decision_grounds.map((item) => item.toLowerCase()).join(" ");
80
+ if (!grounds.includes("ur") || !grounds.includes("brownfield") || !grounds.includes("quick_task")) {
81
+ path = null;
82
+ ambiguous = true;
83
+ }
84
+ }
85
+ if (ambiguous) path = null;
86
+ return { schema_version: "1", observed_delivery_path: path, ambiguous, rationale: value.rationale, decision_grounds: [...value.decision_grounds] };
87
+ }
88
+ export function normalizeStagedAgentOutput(raw, requestedAxes) {
89
+ let value;
90
+ try { value = typeof raw === "string" ? JSON.parse(raw) : raw; }
91
+ catch { fail("agent output is not JSON", "PROPORTIONALITY_OUTPUT_INVALID"); }
92
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("agent output is not an object", "PROPORTIONALITY_OUTPUT_INVALID");
93
+ const allowed = new Set(Object.keys(STAGED_OBSERVATION_SCHEMA.properties));
94
+ if (Object.keys(value).some((key) => !allowed.has(key))) fail("agent output contains unknown fields", "PROPORTIONALITY_OUTPUT_INVALID");
95
+ if (value.schema_version !== "2" || (!STAGES.includes(value.observed_next_permissible_stage) && value.observed_next_permissible_stage !== null)) fail("invalid staged contract", "PROPORTIONALITY_OUTPUT_INVALID");
96
+ if (!DELIVERY_PATHS.includes(value.observed_delivery_path) && value.observed_delivery_path !== null) fail("invalid staged path", "PROPORTIONALITY_OUTPUT_INVALID");
97
+ if (!["evaluated", "not_evaluable_yet"].includes(value.stage_evaluability) || !["evaluated", "not_evaluable_yet"].includes(value.path_evaluability)) fail("invalid staged evaluability", "PROPORTIONALITY_OUTPUT_INVALID");
98
+ for (const [axis, observed, evaluability] of [
99
+ ["next_permissible_stage", value.observed_next_permissible_stage, value.stage_evaluability],
100
+ ["eventual_delivery_path", value.observed_delivery_path, value.path_evaluability],
101
+ ]) {
102
+ const requested = requestedAxes.includes(axis);
103
+ if (requested !== (evaluability === "evaluated") || (requested ? observed === null : observed !== null)) fail(`axis invariant failed: ${axis}`, "PROPORTIONALITY_OUTPUT_INVALID");
104
+ }
105
+ if (typeof value.rationale !== "string" || value.rationale.length > 600 || forbiddenText.test(value.rationale)) fail("unsafe rationale", "PROPORTIONALITY_REDACTION_FAILED");
106
+ if (!Array.isArray(value.decision_grounds) || value.decision_grounds.length > 12 || value.decision_grounds.some((item) => typeof item !== "string" || item.length > 120 || forbiddenText.test(item))) fail("unsafe decision grounds", "PROPORTIONALITY_REDACTION_FAILED");
107
+ if (value.observed_delivery_path === "compact_delivery") {
108
+ const grounds = value.decision_grounds.join(" ").toLowerCase();
109
+ if (!grounds.includes("ur") || !grounds.includes("brownfield") || !grounds.includes("quick_task")) fail("compact delivery evidence missing", "PROPORTIONALITY_OUTPUT_INVALID");
110
+ }
111
+ return {
112
+ schema_version: "2",
113
+ observed_next_permissible_stage: value.observed_next_permissible_stage,
114
+ stage_evaluability: value.stage_evaluability,
115
+ observed_delivery_path: value.observed_delivery_path,
116
+ path_evaluability: value.path_evaluability,
117
+ rationale: value.rationale,
118
+ decision_grounds: [...value.decision_grounds],
119
+ };
120
+ }
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, relative, resolve, sep } from "node:path";
4
+ import { fail, validateBaseline } from "./contracts.js";
5
+
6
+ function inside(root, candidate) {
7
+ const target = resolve(root, candidate);
8
+ const rel = relative(root, target);
9
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`)) fail(`unsafe corpus path ${candidate}`);
10
+ return target;
11
+ }
12
+ function existingInside(root, candidate) {
13
+ const target = inside(root, candidate);
14
+ if (!existsSync(target)) fail(`missing corpus path ${candidate}`);
15
+ const canonicalRoot = realpathSync(root);
16
+ const canonicalTarget = realpathSync(target);
17
+ const rel = relative(canonicalRoot, canonicalTarget);
18
+ if (rel === ".." || rel.startsWith(`..${sep}`)) fail(`corpus path escapes through symlink ${candidate}`);
19
+ return target;
20
+ }
21
+ function sha256(path) {
22
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
23
+ }
24
+ const stagedForbidden = /expected_|evidence_ref|classification|threshold|trivial_change|compact_delivery|verified_change|structured_slice|structured_delivery/i;
25
+ export function validateStagedBlindScenario(scenario, evidencePack = null, baselineScenario = null) {
26
+ if (stagedForbidden.test(JSON.stringify(scenario))) fail(`staged scenario leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
27
+ const quickTaskReferences = JSON.stringify(scenario).match(/quick_task/gi)?.length ?? 0;
28
+ if (quickTaskReferences && (
29
+ scenario.lifecycle_stage !== "post_brownfield_decision"
30
+ || scenario.control_state_context !== "A durable UR is approved. Brownfield analysis is complete and the stored mode/slice decision is quick_task."
31
+ || quickTaskReferences !== 1
32
+ )) fail(`staged scenario path leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
33
+ if (evidencePack && stagedForbidden.test(JSON.stringify(evidencePack))) fail(`staged evidence-pack leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
34
+ if (baselineScenario && (
35
+ JSON.stringify(scenario).includes(baselineScenario.rationale)
36
+ || JSON.stringify(evidencePack ?? {}).includes(baselineScenario.rationale)
37
+ )) fail(`staged rationale leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
38
+ return scenario;
39
+ }
40
+ export function loadCorpus(repoRoot, profileId = "legacy-v1") {
41
+ const root = existingInside(repoRoot, "evals/proportionality");
42
+ if (profileId === "staged-v2") {
43
+ const manifest = JSON.parse(readFileSync(existingInside(root, "staged-manifest.json"), "utf8"));
44
+ const index = JSON.parse(readFileSync(existingInside(root, manifest.scenarios_path), "utf8"));
45
+ const fixtures = JSON.parse(readFileSync(existingInside(root, manifest.fixture_path), "utf8"));
46
+ const baselinePath = existingInside(repoRoot, manifest.baseline_path);
47
+ const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
48
+ if (
49
+ manifest.profile_id !== "staged-v2"
50
+ || baseline.schema_version !== "2"
51
+ || baseline.baseline_version !== manifest.baseline_version
52
+ || !manifest.corpus_version
53
+ || !manifest.fixture_version
54
+ || fixtures.fixture_version !== manifest.fixture_version
55
+ ) fail("staged profile version mismatch");
56
+ if (baseline.cases.length !== 40 || index.scenarios.length !== 72 || new Set(index.scenario_ids).size !== 72) fail("staged corpus coverage mismatch");
57
+ const expectedIds = new Set(baseline.scenarios.map((item) => item.scenario_id));
58
+ if (index.scenarios.some((item) => !expectedIds.has(item.scenario_id)) || baseline.scenarios.some((item) => !index.scenario_ids.includes(item.scenario_id))) fail("staged scenario baseline mismatch");
59
+ for (const scenario of index.scenarios) {
60
+ if (scenario.evidence_pack_id && !fixtures.evidence_packs[scenario.evidence_pack_id]) fail(`missing evidence pack ${scenario.scenario_id}`);
61
+ validateStagedBlindScenario(
62
+ scenario,
63
+ scenario.evidence_pack_id ? fixtures.evidence_packs[scenario.evidence_pack_id] : null,
64
+ baseline.scenarios.find((item) => item.scenario_id === scenario.scenario_id),
65
+ );
66
+ }
67
+ const cases = index.scenarios.map((scenario) => ({
68
+ ...scenario,
69
+ profile_id: "staged-v2",
70
+ corpus_version: manifest.corpus_version,
71
+ fixture_version: manifest.fixture_version,
72
+ allowed_stages: ["ungated_execution", "ur", "brownfield_review", "prd", "sd", "tp", "brownfield_analysis", "cd_tests", "cr", "qa", "uat", "or", "blocked"],
73
+ evidence_pack: scenario.evidence_pack_id ? fixtures.evidence_packs[scenario.evidence_pack_id] : null,
74
+ }));
75
+ return { profile_id: "staged-v2", manifest, baseline, fixtures, cases };
76
+ }
77
+ if (profileId !== "legacy-v1") fail(`unknown proportionality profile ${profileId}`);
78
+ const manifest = JSON.parse(readFileSync(existingInside(root, "manifest.json"), "utf8"));
79
+ const provenance = JSON.parse(readFileSync(existingInside(root, "legacy-v1-provenance.json"), "utf8"));
80
+ const index = JSON.parse(readFileSync(existingInside(root, "cases.json"), "utf8"));
81
+ const fixtures = JSON.parse(readFileSync(existingInside(root, "fixtures/catalog.json"), "utf8"));
82
+ const baselinePath = existingInside(repoRoot, manifest.baseline_path);
83
+ const baseline = validateBaseline(JSON.parse(readFileSync(baselinePath, "utf8")));
84
+ if (manifest.schema_version !== "1" || index.schema_version !== "1" || manifest.baseline_version !== baseline.baseline_version) fail("manifest/corpus version mismatch");
85
+ if (provenance.profile_id !== "legacy-v1" || Object.keys(provenance.source_fingerprints ?? {}).length !== 40) fail("legacy provenance incomplete");
86
+ for (const [path, expectedHash] of Object.entries(provenance.files ?? {})) {
87
+ const target = existingInside(repoRoot, path);
88
+ if (sha256(target) !== expectedHash) fail(`legacy provenance drift ${path}`, "PROPORTIONALITY_HISTORY_DRIFT");
89
+ }
90
+ if (!Array.isArray(index.case_ids) || index.case_ids.length !== 40 || new Set(index.case_ids).size !== 40) fail("blind corpus must list 40 unique ids");
91
+ if (Object.keys(index.case_contexts ?? {}).some((id) => !index.case_ids.includes(id))) fail("case context references unknown id");
92
+ if (Object.values(index.case_contexts ?? {}).some((value) => typeof value !== "string" || /expected_delivery_path|compact_delivery|structured_slice|structured_delivery|verified_change|trivial_change/i.test(value))) fail("case context contains result label");
93
+ const baselineIds = new Set(baseline.cases.map((item) => item.case_id));
94
+ if (index.case_ids.some((id) => !baselineIds.has(id)) || baseline.cases.some((item) => !index.case_ids.includes(item.case_id))) fail("baseline/corpus ids differ");
95
+ const artefactRoot = realpathSync(resolve(repoRoot, ".agdf/control/artefacts"));
96
+ for (const item of baseline.cases) {
97
+ const evidence = resolve(dirname(baselinePath), item.evidence_ref);
98
+ if (!existsSync(evidence)) fail(`unresolvable evidence_ref ${item.case_id}`);
99
+ const rel = relative(artefactRoot, realpathSync(evidence));
100
+ if (rel === ".." || rel.startsWith(`..${sep}`)) fail(`unsafe evidence_ref ${item.case_id}`);
101
+ }
102
+ const fixture = fixtures.fixtures?.[index.fixture_id];
103
+ if (!fixture || "expected_delivery_path" in fixture || "rationale" in fixture) fail("invalid blind fixture");
104
+ const cases = baseline.cases.map(({ case_id, task_summary }) => ({
105
+ case_id, task_summary, fixture_id: index.fixture_id,
106
+ repository_context: fixture.repository_context,
107
+ control_state_context: index.case_contexts?.[case_id] ?? fixture.control_state_context,
108
+ }));
109
+ return { profile_id: "legacy-v1", manifest, baseline, fixtures, provenance, cases };
110
+ }