create-agdf 0.10.1 → 0.10.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.
Files changed (94) hide show
  1. package/README.md +21 -13
  2. package/bin/agdf-validator.js +11 -0
  3. package/generated/.agdf/control/README.md +3 -3
  4. package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +1 -1
  5. package/generated/.github/skills/agdf-delivery-path-search/SKILL.md +7 -6
  6. package/generated/.github/skills/agdf-gate-check/SKILL.md +35 -105
  7. package/generated/.github/skills/agdf-interaction-locales.json +10 -0
  8. package/generated/.github/skills/agdf-release-or/SKILL.md +1 -1
  9. package/generated/.github/skills/contracts/gate-transition.md +4 -2
  10. package/generated/.github/skills/contracts/interaction.md +23 -0
  11. package/generated/.github/skills/contracts/modes.md +12 -1
  12. package/generated/.opencode/agdf-interaction-locales.json +10 -0
  13. package/generated/.opencode/contracts/gate-transition.md +4 -2
  14. package/generated/.opencode/contracts/interaction.md +23 -0
  15. package/generated/.opencode/contracts/modes.md +12 -1
  16. package/generated/.opencode/skills/agdf-brownfield-analysis/SKILL.md +1 -1
  17. package/generated/.opencode/skills/agdf-delivery-path-search/SKILL.md +7 -6
  18. package/generated/.opencode/skills/agdf-gate-check/SKILL.md +35 -105
  19. package/generated/.opencode/skills/agdf-release-or/SKILL.md +1 -1
  20. package/generated/plugins/agdf/.claude-plugin/plugin.json +19 -0
  21. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  22. package/generated/plugins/agdf/control/README.md +3 -3
  23. package/generated/plugins/agdf/hooks/session-start.sh +1 -1
  24. package/generated/plugins/agdf/meta/agdf-interaction-locales.json +10 -0
  25. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  26. package/generated/plugins/agdf/meta/contracts/gate-transition.md +4 -2
  27. package/generated/plugins/agdf/meta/contracts/interaction.md +23 -0
  28. package/generated/plugins/agdf/meta/contracts/modes.md +12 -1
  29. package/generated/plugins/agdf/runtime/agdf-local.js +11 -0
  30. package/generated/plugins/agdf/runtime/create-agdf/NOTICE +10 -0
  31. package/generated/plugins/agdf/runtime/create-agdf/bin/agdf-validator.js +11 -0
  32. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +426 -0
  33. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +220 -0
  34. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +136 -0
  35. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/delivery-path-search-command.js +75 -0
  36. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/parse-args.js +156 -0
  37. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/runtime-context.js +83 -0
  38. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/validation-handlers.js +46 -0
  39. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/delivery-map.js +291 -0
  40. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +265 -0
  41. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +450 -0
  42. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-policy.js +283 -0
  43. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/required-files.js +9 -0
  44. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +217 -0
  45. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/shared.js +344 -0
  46. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/verified-change.js +156 -0
  47. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/aggregate.js +18 -0
  48. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/gate-approval-validator.js +49 -0
  49. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/index.js +7 -0
  50. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/legacy-migration.js +95 -0
  51. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +227 -0
  52. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-repository.js +68 -0
  53. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-resolver.js +45 -0
  54. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-writer.js +97 -0
  55. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/candidate-policy.js +99 -0
  56. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/contracts.js +237 -0
  57. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/claude.js +44 -0
  58. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/codex.js +53 -0
  59. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/protocol.js +13 -0
  60. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/generators/claude.js +40 -0
  61. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/generators/codex.js +38 -0
  62. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/generators/protocol.js +12 -0
  63. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/persistence.js +39 -0
  64. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/scoring.js +21 -0
  65. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/search-engine.js +154 -0
  66. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/state-adapter.js +161 -0
  67. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/surfaces/capabilities.js +14 -0
  68. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/transports/read-only-guard.js +43 -0
  69. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +670 -0
  70. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/local-validator.js +142 -0
  71. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/validator-application.js +40 -0
  72. package/generated/plugins/agdf/runtime/create-agdf/package.json +66 -0
  73. package/generated/plugins/agdf/runtime/runtime-manifest.json +8 -0
  74. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +1055 -0
  75. package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +1 -1
  76. package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +7 -6
  77. package/generated/plugins/agdf/skills/gate-check/SKILL.md +35 -105
  78. package/generated/plugins/agdf/skills/release-or/SKILL.md +1 -1
  79. package/lib/cli/application.js +16 -54
  80. package/lib/cli/command-registry.js +9 -11
  81. package/lib/cli/validation-handlers.js +46 -0
  82. package/lib/control-evaluation/doctor.js +1 -1
  83. package/lib/control-evaluation/gate-check.js +23 -32
  84. package/lib/control-evaluation/required-files.js +9 -0
  85. package/lib/installers/local-marketplace.js +322 -0
  86. package/lib/installers/opencode.js +84 -6
  87. package/lib/installers/plugin-installers.js +102 -26
  88. package/lib/interaction-presentation.js +80 -0
  89. package/lib/runtime/local-validator.js +142 -0
  90. package/lib/runtime/validator-application.js +40 -0
  91. package/lib/scaffold/plan.js +1 -7
  92. package/lib/scaffold/presentation.js +2 -2
  93. package/opencode-plugin.js +1 -1
  94. package/package.json +6 -2
@@ -0,0 +1,227 @@
1
+ export const RUN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
2
+ export const LIFECYCLES = new Set([
3
+ "active",
4
+ "completed",
5
+ "superseded",
6
+ "abandoned",
7
+ ]);
8
+ export function scalarFields(content) {
9
+ const values = new Map(),
10
+ duplicates = [];
11
+ const runMeta = content.match(/(?:^|\n)## Run Meta\s*\n([\s\S]*?)(?=\n## |$)/)?.[1] ?? "";
12
+ for (const [, k, v] of runMeta.matchAll(/^- ([a-z_]+):\s*(.*?)\s*$/gm)) {
13
+ if (values.has(k)) duplicates.push(k);
14
+ else values.set(k, v.replace(/^`|`$/g, ""));
15
+ }
16
+ return { values, duplicates };
17
+ }
18
+ export function parseRunState(content, expected) {
19
+ const { values, duplicates } = scalarFields(content),
20
+ findings = duplicates.map((field) => ({
21
+ code: "AGDF_RUN_FIELD_DUPLICATE",
22
+ field,
23
+ })),
24
+ id = values.get("run_id") ?? "";
25
+ if (content.split(/\r?\n/).some((line) => line.trimStart().startsWith("|") && !line.trimEnd().endsWith("|"))) {
26
+ findings.push({ code: "AGDF_RUN_TABLE_INVALID" });
27
+ }
28
+ if (!RUN_ID_PATTERN.test(id)) findings.push({ code: "AGDF_RUN_ID_INVALID" });
29
+ if (expected && id !== expected)
30
+ findings.push({ code: "AGDF_RUN_PATH_MISMATCH" });
31
+ if (values.get("control_state_version") !== "2")
32
+ findings.push({ code: "AGDF_RUN_VERSION_UNSUPPORTED" });
33
+ if (!LIFECYCLES.has(values.get("lifecycle")))
34
+ findings.push({ code: "AGDF_RUN_LIFECYCLE_INVALID" });
35
+ if (!/^[1-9]\d*$/.test(values.get("revision") ?? ""))
36
+ 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") ?? ""))
38
+ findings.push({ code: "AGDF_RUN_REVISION_ID_INVALID" });
39
+ return {
40
+ content,
41
+ meta: Object.fromEntries(values),
42
+ findings,
43
+ valid: !findings.length,
44
+ };
45
+ }
46
+ export function semanticBody(content) {
47
+ return content
48
+ .replace(/^# AGDF Run State\s*/m, "")
49
+ .replace(/## Run Meta[\s\S]*?(?=\n## )/, "")
50
+ .trim();
51
+ }
52
+ export function semanticFingerprint(content) {
53
+ return semanticBody(content).replace(/\s+/g, " ");
54
+ }
55
+
56
+ function section(c, h) {
57
+ const e = h.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58
+ return (
59
+ c.match(
60
+ new RegExp(`(?:^|\\n)## ${e}\\s*\\n([\\s\\S]*?)(?:\\n## |\\n# |$)`),
61
+ )?.[1] ?? ""
62
+ );
63
+ }
64
+ function rows(s) {
65
+ return s
66
+ .split(/\r?\n/)
67
+ .filter((l) => l.trim().startsWith("|"))
68
+ .map((l) =>
69
+ l
70
+ .split("|")
71
+ .slice(1, -1)
72
+ .map((x) => x.trim()),
73
+ )
74
+ .filter((r) => !r.every((x) => /^[-:]+$/.test(x)));
75
+ }
76
+ function clean(v = "") {
77
+ return v.replace(/^`|`$/g, "").trim();
78
+ }
79
+ export function parseArtefactPathCell(value = "") {
80
+ const raw = String(value).trim();
81
+ if (!raw) return { raw, path: "", format: "plain", reason: "empty" };
82
+ const ticks = [...raw].filter((character) => character === "`").length;
83
+ if (ticks === 0) return { raw, path: raw, format: "plain", reason: "none" };
84
+ if (ticks === 2 && raw.startsWith("`") && raw.endsWith("`")) {
85
+ const path = raw.slice(1, -1).trim();
86
+ if (path && !path.includes("`")) return { raw, path, format: "code_span", reason: "none" };
87
+ }
88
+ return {
89
+ raw,
90
+ path: "",
91
+ format: "invalid",
92
+ reason: ticks === 1 || raw.startsWith("`") !== raw.endsWith("`")
93
+ ? "unmatched_delimiter"
94
+ : "embedded_delimiter",
95
+ };
96
+ }
97
+ function meaningful(v = "") {
98
+ return Boolean(v && !(v.startsWith("`") && v.includes("|")));
99
+ }
100
+ function field(c, k) {
101
+ return (
102
+ c.match(new RegExp(`^- ${k}:[^\\S\\r\\n]*(.*)$`, "m"))?.[1]?.trim() ?? ""
103
+ );
104
+ }
105
+ function sectionField(c, h, k) {
106
+ return field(section(c, h), k);
107
+ }
108
+ function dataRows(c, h, header) {
109
+ return rows(section(c, h)).filter(
110
+ (r) =>
111
+ r[0] !== header &&
112
+ (h === "Evidence" ? r.slice(0, 3).some(meaningful) : r.some(meaningful)),
113
+ );
114
+ }
115
+ export function parseControlState(
116
+ content,
117
+ { userGates = [], internalSteps = [], closeoutArtefacts = [] } = {},
118
+ ) {
119
+ const approvals = new Map();
120
+ for (const h of ["Approvals", "Gate Checklist"])
121
+ for (const [g, s, e] of rows(section(content, h))) {
122
+ if (userGates.includes(g)) {
123
+ const status = clean(s);
124
+ approvals.set(g, {
125
+ status: g === "QA" && (status === "pass" || status === "passed") ? "approved" : status,
126
+ evidence: e ?? "",
127
+ });
128
+ }
129
+ }
130
+ const artefacts = new Map();
131
+ for (const [t, p, s, n] of rows(section(content, "Artefacts"))) {
132
+ if (userGates.includes(t) || internalSteps.includes(t) || closeoutArtefacts.includes(t)) {
133
+ const parsedPath = parseArtefactPathCell(p ?? "");
134
+ artefacts.set(t, {
135
+ path: parsedPath.path,
136
+ status: clean(s),
137
+ notes: n ?? "",
138
+ raw_path: parsedPath.raw,
139
+ path_format: parsedPath.format,
140
+ path_reason: parsedPath.reason,
141
+ });
142
+ }
143
+ }
144
+ const mapRows = (h, header, fn) => dataRows(content, h, header).map(fn);
145
+ const mode = section(content, "Mode/Slice Decision").trim()
146
+ ? "Mode/Slice Decision"
147
+ : "Mode / Slice Decision",
148
+ source = "Source And Scope State",
149
+ memory = "Knowledge Persistence Decision";
150
+ return {
151
+ current_gate: field(content, "current_gate"),
152
+ next_allowed_action: field(content, "next_allowed_action"),
153
+ approvals,
154
+ artefacts,
155
+ evidence_refs: mapRows("Evidence", "Evidence", (r) => ({
156
+ evidence: r[0] ?? "",
157
+ source: r[1] ?? "",
158
+ covers: r[2] ?? "",
159
+ strength: clean(r[3]),
160
+ })),
161
+ artefact_chain: mapRows("Artefact Chain", "From", (r) => ({
162
+ from: r[0] ?? "",
163
+ relationship: clean(r[1]),
164
+ to: clean(r[2]),
165
+ evidence: r[3] ?? "",
166
+ })),
167
+ missing_evidence: mapRows("Missing Evidence", "Missing evidence", (r) => ({
168
+ missing_evidence: r[0] ?? "",
169
+ impact: clean(r[1]),
170
+ required_next_step: r[2] ?? "",
171
+ })),
172
+ risks: mapRows("Risks", "Risk", (r) => ({
173
+ risk: r[0] ?? "",
174
+ impact: clean(r[1]),
175
+ mitigation_or_owner: r[2] ?? "",
176
+ })),
177
+ context_graph: {
178
+ impact: clean(field(content, "context_graph_impact")),
179
+ refs: field(content, "context_graph_refs"),
180
+ required_action: clean(field(content, "context_graph_required_action")),
181
+ gate_effect: clean(field(content, "context_graph_gate_effect")),
182
+ evidence: field(content, "context_graph_evidence"),
183
+ },
184
+ quality_outlook: field(content, "quality_outlook"),
185
+ mode_slice_decision: {
186
+ decision: clean(sectionField(content, mode, "decision")),
187
+ required_next_gate: clean(
188
+ sectionField(content, mode, "required_next_gate"),
189
+ ),
190
+ scope_reason: sectionField(content, mode, "scope_reason"),
191
+ evidence: sectionField(content, mode, "evidence"),
192
+ },
193
+ source_scope: {
194
+ normative_instruction_source: sectionField(
195
+ content,
196
+ source,
197
+ "normative_instruction_source",
198
+ ),
199
+ multi_scope_state: clean(
200
+ sectionField(content, source, "multi_scope_state"),
201
+ ),
202
+ active_scope_evidence: sectionField(
203
+ content,
204
+ source,
205
+ "active_scope_evidence",
206
+ ),
207
+ competing_scope_lines: sectionField(
208
+ content,
209
+ source,
210
+ "competing_scope_lines",
211
+ ),
212
+ branch_workspace_evidence: sectionField(
213
+ content,
214
+ source,
215
+ "branch_workspace_evidence",
216
+ ),
217
+ branch_workspace_scope_effect: clean(
218
+ sectionField(content, source, "branch_workspace_scope_effect"),
219
+ ),
220
+ },
221
+ memory: {
222
+ target: clean(sectionField(content, memory, "memory_target")),
223
+ reason: sectionField(content, memory, "memory_reason"),
224
+ refs: sectionField(content, memory, "memory_refs"),
225
+ },
226
+ };
227
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+ import { parseRunState, RUN_ID_PATTERN } from "./run-state-parser.js";
12
+ export function runPath(root, id) {
13
+ if (!RUN_ID_PATTERN.test(id)) throw Error(`invalid run_id: ${id}`);
14
+ return join(root, ".agdf", "control", "runs", id, "RUN_STATE.md");
15
+ }
16
+ export function discoverRuns(root) {
17
+ const dir = join(root, ".agdf", "control", "runs");
18
+ if (!existsSync(dir)) return [];
19
+ const rootStats = lstatSync(dir);
20
+ if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
21
+ return [invalidRun("<repository>", dir)];
22
+ }
23
+ return readdirSync(dir)
24
+ .filter((x) => !x.startsWith("."))
25
+ .sort()
26
+ .map((id) => {
27
+ const d = join(dir, id),
28
+ path = join(d, "RUN_STATE.md");
29
+ const directoryStats = lstatSync(d);
30
+ if (
31
+ !RUN_ID_PATTERN.test(id) ||
32
+ directoryStats.isSymbolicLink() ||
33
+ !directoryStats.isDirectory() ||
34
+ !existsSync(path)
35
+ )
36
+ return invalidRun(id, path);
37
+
38
+ const stateStats = lstatSync(path);
39
+ if (stateStats.isSymbolicLink() || !stateStats.isFile()) {
40
+ return invalidRun(id, path);
41
+ }
42
+ return {
43
+ run_id: id,
44
+ path,
45
+ ...parseRunState(readFileSync(path, "utf8"), id),
46
+ };
47
+ });
48
+ }
49
+
50
+ function invalidRun(runId, path) {
51
+ return {
52
+ run_id: runId,
53
+ path,
54
+ valid: false,
55
+ findings: [{ code: "AGDF_RUN_PATH_INVALID", path }],
56
+ };
57
+ }
58
+ const DEFAULT_BODY = `## Objective\n\nDescribe the trustworthy outcome.\n\n## Current Control State\n\n| Question | Answer |\n|---|---|\n| What is known? | New run created. |\n| What is approved? | Nothing yet. |\n| What is missing? | Durable UR and exact approval. |\n| What is the next allowed action? | Draft the UR. |\n| What is explicitly forbidden right now? | Later artefacts and implementation. |\n\n## Evidence\n\n| Evidence | Source | Covers | Strength |\n|---|---|---|---|\n| Run creation | run-create | Control initialization | direct |\n\n## Closeout\n\n- next_allowed_action: Draft the UR.\n- quality_outlook: Establish evidence before later gates.\n`;
59
+ export function renderRunState(id, body = DEFAULT_BODY, meta = {}) {
60
+ return `# AGDF Run State\n\n## Run Meta\n\n- control_state_version: 2\n- run_id: ${id}\n- lifecycle: ${meta.lifecycle ?? "active"}\n- revision: 1\n- revision_id: ${randomUUID()}\n- mode: ${meta.mode ?? "structured_delivery"}\n- current_gate: ${meta.current_gate ?? "UR"}\n- decision: ${meta.decision ?? "in_progress"}\n- owner: ${meta.owner ?? "agent"}\n\n${body}`;
61
+ }
62
+ export function createRun(root, id, body = "") {
63
+ const path = runPath(root, id);
64
+ if (existsSync(path)) throw Error("AGDF_RUN_COLLISION");
65
+ mkdirSync(dirname(path), { recursive: true });
66
+ writeFileSync(path, renderRunState(id, body), { flag: "wx" });
67
+ return path;
68
+ }
@@ -0,0 +1,45 @@
1
+ import { discoverRuns } from "./run-state-repository.js";
2
+ export function resolveRuns(
3
+ root,
4
+ { runIdArg, runIdEnv, allActive = false } = {},
5
+ ) {
6
+ if (
7
+ (allActive && (runIdArg || runIdEnv)) ||
8
+ (runIdArg && runIdEnv && runIdArg !== runIdEnv)
9
+ )
10
+ throw Error("AGDF_SELECTOR_CONFLICT");
11
+ const runs = discoverRuns(root),
12
+ active = runs.filter((r) => r.valid && r.meta.lifecycle === "active");
13
+ const findings = runs
14
+ .filter((run) => !run.valid)
15
+ .flatMap((run) =>
16
+ run.findings.map((finding) => ({ ...finding, run_id: run.run_id })),
17
+ );
18
+ if (allActive)
19
+ return { selection_source: "all_active", runs: active, findings };
20
+ const id = runIdArg || runIdEnv;
21
+ if (id) {
22
+ const run = runs.find((r) => r.run_id === id);
23
+ if (!run?.valid) {
24
+ throw Error(
25
+ `AGDF_RUN_NOT_SELECTABLE:${id}; discovered=${runs.map((item) => item.run_id).join(",")}; next=select a valid run`,
26
+ );
27
+ }
28
+ return {
29
+ selection_source: runIdArg ? "cli" : "environment",
30
+ run,
31
+ active_run_ids: active.map((r) => r.run_id),
32
+ };
33
+ }
34
+ if (active.length !== 1)
35
+ throw Error(
36
+ active.length
37
+ ? `AGDF_ACTIVE_RUN_AMBIGUOUS:${active.map((r) => r.run_id)}`
38
+ : "AGDF_ACTIVE_RUN_MISSING",
39
+ );
40
+ return {
41
+ selection_source: "single_active",
42
+ run: active[0],
43
+ active_run_ids: [active[0].run_id],
44
+ };
45
+ }
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ fsyncSync,
5
+ existsSync,
6
+ lstatSync,
7
+ openSync,
8
+ readFileSync,
9
+ renameSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { dirname } from "node:path";
14
+
15
+ import { parseRunState } from "./run-state-parser.js";
16
+
17
+ function fsyncDirectory(path) {
18
+ if (process.platform === "win32") return;
19
+ const descriptor = openSync(path, "r");
20
+ try {
21
+ fsyncSync(descriptor);
22
+ } finally {
23
+ closeSync(descriptor);
24
+ }
25
+ }
26
+
27
+ export function atomicWrite(path, content) {
28
+ if (existsSync(path)) {
29
+ const destination = lstatSync(path);
30
+ if (destination.isSymbolicLink() || !destination.isFile()) {
31
+ throw new Error("AGDF_RUN_PATH_INVALID");
32
+ }
33
+ }
34
+
35
+ const parent = lstatSync(dirname(path));
36
+ if (parent.isSymbolicLink() || !parent.isDirectory()) {
37
+ throw new Error("AGDF_RUN_PATH_INVALID");
38
+ }
39
+
40
+ const temp = `${path}.tmp-${process.pid}-${randomUUID()}`;
41
+ let descriptor;
42
+
43
+ try {
44
+ descriptor = openSync(temp, "wx");
45
+ writeFileSync(descriptor, content, "utf8");
46
+ fsyncSync(descriptor);
47
+ closeSync(descriptor);
48
+ descriptor = undefined;
49
+
50
+ renameSync(temp, path);
51
+ fsyncDirectory(dirname(path));
52
+ } catch (error) {
53
+ if (descriptor !== undefined) closeSync(descriptor);
54
+ try {
55
+ unlinkSync(temp);
56
+ } catch (cleanupError) {
57
+ if (cleanupError.code !== "ENOENT") throw cleanupError;
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ export function writeRun(path, content, expectedRevisionId) {
64
+ const lockPath = `${path}.lock`;
65
+ let lockDescriptor;
66
+ try {
67
+ lockDescriptor = openSync(lockPath, "wx");
68
+ } catch (error) {
69
+ if (error.code === "EEXIST") throw new Error("AGDF_RUN_WRITE_LOCKED");
70
+ throw error;
71
+ }
72
+
73
+ try {
74
+ const current = parseRunState(readFileSync(path, "utf8"));
75
+ if (!current.valid) throw new Error("AGDF_RUN_STATE_INVALID");
76
+ if (current.meta.revision_id !== expectedRevisionId) {
77
+ throw new Error("AGDF_STALE_RUN_REVISION");
78
+ }
79
+
80
+ const next = content
81
+ .replace(
82
+ /^- revision:\s*.*$/m,
83
+ `- revision: ${Number(current.meta.revision) + 1}`,
84
+ )
85
+ .replace(/^- revision_id:\s*.*$/m, `- revision_id: ${randomUUID()}`);
86
+ const candidate = parseRunState(next);
87
+ if (!candidate.valid || candidate.meta.run_id !== current.meta.run_id) {
88
+ throw new Error("AGDF_RUN_STATE_INVALID");
89
+ }
90
+
91
+ atomicWrite(path, next);
92
+ return candidate;
93
+ } finally {
94
+ closeSync(lockDescriptor);
95
+ unlinkSync(lockPath);
96
+ }
97
+ }
@@ -0,0 +1,99 @@
1
+ import { validateCandidate } from "./contracts.js";
2
+
3
+ function normalized(value) {
4
+ return String(value).normalize("NFKC").toLowerCase().replace(/[`*_]/g, "").replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ const STOP_WORDS = new Set(["a", "an", "and", "der", "die", "das", "ein", "eine", "the", "to", "und", "zu"]);
8
+
9
+ function tokens(value) {
10
+ return new Set(normalized(value).split(/\s+/).filter((item) => item && !STOP_WORDS.has(item)));
11
+ }
12
+
13
+ function jaccard(left, right) {
14
+ const union = new Set([...left, ...right]);
15
+ if (!union.size) return 1;
16
+ let intersection = 0;
17
+ for (const item of left) if (right.has(item)) intersection += 1;
18
+ return intersection / union.size;
19
+ }
20
+
21
+ function signature(candidate) {
22
+ return JSON.stringify({
23
+ gate_action: normalized(candidate.gate_action ?? candidate.action),
24
+ affected_boundaries: [...(candidate.affected_boundaries ?? [])].map(normalized).sort(),
25
+ evidence: [...(candidate.expected_evidence ?? [])].map(normalized).sort(),
26
+ tests: [...(candidate.tests ?? [])].map(normalized).sort(),
27
+ risk_strategy: normalized(candidate.risk_strategy ?? ""),
28
+ reversibility: normalized(candidate.reversibility ?? ""),
29
+ });
30
+ }
31
+
32
+ export function candidateLegality(candidateValue, input) {
33
+ const candidate = validateCandidate(candidateValue);
34
+ const action = normalized(candidate.gate_action ?? candidate.action);
35
+ const forbiddenMatch = input.forbidden_actions.find((item) => {
36
+ const forbidden = normalized(item);
37
+ return forbidden && action === forbidden;
38
+ });
39
+ if (forbiddenMatch) return { legal: false, reason: `forbidden_by_gate: ${forbiddenMatch}`, candidate };
40
+
41
+ const allowedMatch = input.allowed_actions.some((item) => {
42
+ const allowed = normalized(item);
43
+ return allowed && action === allowed;
44
+ });
45
+ if (!allowedMatch) return { legal: false, reason: "not_in_allowed_actions", candidate };
46
+ return { legal: true, reason: "allowed_by_current_gate", candidate };
47
+ }
48
+
49
+ export function candidatesFromInput(input) {
50
+ return input.allowed_actions.slice(0, input.budgets.max_candidates).map((action, index) => ({
51
+ id: `candidate-${index + 1}`,
52
+ action,
53
+ expected_evidence: input.evidence_refs,
54
+ tests: [],
55
+ assumptions: [],
56
+ depth: 0,
57
+ parent_id: null,
58
+ }));
59
+ }
60
+
61
+ export function generatedCandidatesFromResponse(response, input, existingCandidates = []) {
62
+ const accepted = [];
63
+ const rejected = [];
64
+ for (const proposal of response.proposals) {
65
+ const candidate = {
66
+ id: `generated-${proposal.proposal_id}`,
67
+ action: proposal.intent,
68
+ source: "generated",
69
+ gate_action: proposal.gate_action,
70
+ intent: proposal.intent,
71
+ expected_evidence: proposal.expected_evidence,
72
+ tests: proposal.tests,
73
+ assumptions: proposal.assumptions,
74
+ affected_boundaries: proposal.affected_boundaries,
75
+ risk_strategy: proposal.risk_strategy,
76
+ reversibility: proposal.reversibility,
77
+ generator_proposal_id: proposal.proposal_id,
78
+ depth: 0,
79
+ parent_id: null,
80
+ };
81
+ const legality = candidateLegality(candidate, input);
82
+ if (!legality.legal) {
83
+ rejected.push({ proposal_id: proposal.proposal_id, reason: legality.reason });
84
+ continue;
85
+ }
86
+ const peers = [...existingCandidates, ...accepted].filter((item) => normalized(item.gate_action ?? item.action) === normalized(candidate.gate_action));
87
+ const duplicate = peers.find((item) => {
88
+ const same = normalized(item.intent ?? item.action) === normalized(candidate.intent);
89
+ const sameDecision = signature(item) === signature(candidate);
90
+ return same || sameDecision || jaccard(tokens(item.intent ?? item.action), tokens(candidate.intent)) >= 0.8;
91
+ });
92
+ if (duplicate) {
93
+ rejected.push({ proposal_id: proposal.proposal_id, reason: "cosmetic_or_material_duplicate" });
94
+ continue;
95
+ }
96
+ accepted.push(candidate);
97
+ }
98
+ return { accepted, rejected };
99
+ }