ai-foreman 1.7.19 → 1.7.21

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 (205) hide show
  1. package/README.md +29 -5
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +92 -5
  20. package/dist/buildAdmission.d.ts +61 -0
  21. package/dist/buildAdmission.js +286 -0
  22. package/dist/buildApproval.d.ts +2 -0
  23. package/dist/buildApproval.js +17 -0
  24. package/dist/buildAssignment.d.ts +33 -0
  25. package/dist/buildAssignment.js +215 -0
  26. package/dist/buildInterventions.d.ts +90 -0
  27. package/dist/buildInterventions.js +334 -0
  28. package/dist/buildLaunchGate.d.ts +1 -0
  29. package/dist/buildLaunchGate.js +36 -0
  30. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  31. package/dist/buildOwnershipReconciliation.js +41 -0
  32. package/dist/buildOwnershipRepair.d.ts +33 -0
  33. package/dist/buildOwnershipRepair.js +111 -0
  34. package/dist/buildRuns.d.ts +24 -1
  35. package/dist/buildRuns.js +216 -70
  36. package/dist/buildSyntheticWork.d.ts +2 -0
  37. package/dist/buildSyntheticWork.js +30 -0
  38. package/dist/buildWorkAdmission.d.ts +17 -0
  39. package/dist/buildWorkAdmission.js +57 -0
  40. package/dist/buildWorkAuthorization.d.ts +2 -0
  41. package/dist/buildWorkAuthorization.js +23 -0
  42. package/dist/buildWorkContext.d.ts +3 -0
  43. package/dist/buildWorkContext.js +6 -0
  44. package/dist/buildWorkMigration.d.ts +9 -0
  45. package/dist/buildWorkMigration.js +206 -0
  46. package/dist/buildWorkReconciliation.d.ts +12 -0
  47. package/dist/buildWorkReconciliation.js +70 -0
  48. package/dist/builderGuidanceFollowup.d.ts +15 -0
  49. package/dist/builderGuidanceFollowup.js +34 -0
  50. package/dist/cli/doctor.js +3 -2
  51. package/dist/cli/graph.d.ts +2 -0
  52. package/dist/cli/graph.js +75 -0
  53. package/dist/cli/manager.d.ts +8 -0
  54. package/dist/cli/manager.js +234 -12
  55. package/dist/cli/recovery.js +3 -2
  56. package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
  57. package/dist/cli/runtimeAuthPrompt.js +41 -7
  58. package/dist/cli/start.d.ts +8 -0
  59. package/dist/cli/start.js +2091 -1849
  60. package/dist/cli/status.js +20 -5
  61. package/dist/cli/tickets.js +9 -7
  62. package/dist/continuity.d.ts +16 -1
  63. package/dist/continuity.js +68 -12
  64. package/dist/foreman.d.ts +12 -4
  65. package/dist/foreman.js +397 -60
  66. package/dist/graph/boundary.d.ts +15 -0
  67. package/dist/graph/boundary.js +50 -0
  68. package/dist/graph/bridge.d.ts +9 -0
  69. package/dist/graph/bridge.js +146 -0
  70. package/dist/graph/bridge.py +205 -0
  71. package/dist/graph/config.d.ts +11 -0
  72. package/dist/graph/config.js +40 -0
  73. package/dist/graph/corpus.d.ts +16 -0
  74. package/dist/graph/corpus.js +188 -0
  75. package/dist/graph/derived.d.ts +17 -0
  76. package/dist/graph/derived.js +73 -0
  77. package/dist/graph/host.d.ts +4 -0
  78. package/dist/graph/host.js +160 -0
  79. package/dist/graph/install.d.ts +3 -0
  80. package/dist/graph/install.js +50 -0
  81. package/dist/graph/lifecycle.d.ts +11 -0
  82. package/dist/graph/lifecycle.js +124 -0
  83. package/dist/graph/maintenance.d.ts +43 -0
  84. package/dist/graph/maintenance.js +301 -0
  85. package/dist/graph/manager.d.ts +9 -0
  86. package/dist/graph/manager.js +92 -0
  87. package/dist/graph/preparation.d.ts +41 -0
  88. package/dist/graph/preparation.js +43 -0
  89. package/dist/graph/read.d.ts +27 -0
  90. package/dist/graph/read.js +152 -0
  91. package/dist/graph/semantic.d.ts +17 -0
  92. package/dist/graph/semantic.js +27 -0
  93. package/dist/graph/session.d.ts +8 -0
  94. package/dist/graph/session.js +92 -0
  95. package/dist/graph/sources.d.ts +4 -0
  96. package/dist/graph/sources.js +11 -0
  97. package/dist/graph/storage.d.ts +38 -0
  98. package/dist/graph/storage.js +159 -0
  99. package/dist/graph/turn.d.ts +17 -0
  100. package/dist/graph/turn.js +360 -0
  101. package/dist/graph/util.d.ts +5 -0
  102. package/dist/graph/util.js +51 -0
  103. package/dist/handoffs.d.ts +1 -0
  104. package/dist/handoffs.js +5 -1
  105. package/dist/humanDecision.d.ts +21 -2
  106. package/dist/humanDecision.js +169 -10
  107. package/dist/index.js +4 -1
  108. package/dist/installOwnership.d.ts +46 -0
  109. package/dist/installOwnership.js +440 -0
  110. package/dist/linuxProbeInventory.d.ts +12 -0
  111. package/dist/linuxProbeInventory.js +48 -0
  112. package/dist/log.d.ts +1 -1
  113. package/dist/log.js +7 -1
  114. package/dist/managerEvidence.d.ts +25 -0
  115. package/dist/managerEvidence.js +326 -0
  116. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  117. package/dist/managerEvidenceArtifacts.js +118 -0
  118. package/dist/managerPacket.d.ts +2 -0
  119. package/dist/managerPacket.js +23 -0
  120. package/dist/markers.d.ts +1 -1
  121. package/dist/markers.js +2 -2
  122. package/dist/observability.js +1 -1
  123. package/dist/processIdentity.d.ts +21 -1
  124. package/dist/processIdentity.js +87 -5
  125. package/dist/providerPhase.d.ts +13 -0
  126. package/dist/providerPhase.js +26 -0
  127. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  128. package/dist/qaAuthorizedDisposition.js +26 -0
  129. package/dist/qaBuildGate.d.ts +4 -0
  130. package/dist/qaBuildGate.js +216 -0
  131. package/dist/qaBuilderCoverage.d.ts +5 -0
  132. package/dist/qaBuilderCoverage.js +78 -0
  133. package/dist/qaContractCoverage.d.ts +10 -0
  134. package/dist/qaContractCoverage.js +55 -0
  135. package/dist/qaContractDelivery.d.ts +23 -0
  136. package/dist/qaContractDelivery.js +145 -0
  137. package/dist/qaContractFreshness.d.ts +15 -0
  138. package/dist/qaContractFreshness.js +36 -0
  139. package/dist/qaDeliveryJournal.d.ts +10 -0
  140. package/dist/qaDeliveryJournal.js +16 -1
  141. package/dist/qaEffectiveConfig.d.ts +20 -0
  142. package/dist/qaEffectiveConfig.js +40 -0
  143. package/dist/qaEvidenceReader.d.ts +14 -0
  144. package/dist/qaEvidenceReader.js +176 -0
  145. package/dist/qaFailureDelivery.d.ts +2 -0
  146. package/dist/qaFailureDelivery.js +107 -10
  147. package/dist/qaHandbackMigration.d.ts +1 -1
  148. package/dist/qaHandbackMigration.js +3 -3
  149. package/dist/qaPreparation.d.ts +26 -0
  150. package/dist/qaPreparation.js +150 -0
  151. package/dist/qaPreparationInputs.d.ts +8 -0
  152. package/dist/qaPreparationInputs.js +47 -0
  153. package/dist/qaPreparationMetrics.d.ts +145 -0
  154. package/dist/qaPreparationMetrics.js +120 -0
  155. package/dist/qaPreparationPolicy.d.ts +5 -0
  156. package/dist/qaPreparationPolicy.js +51 -0
  157. package/dist/qaPreparationStore.d.ts +157 -0
  158. package/dist/qaPreparationStore.js +412 -0
  159. package/dist/qaProtocolV2.d.ts +28 -0
  160. package/dist/qaProtocolV2.js +12 -0
  161. package/dist/qaReview.d.ts +6 -0
  162. package/dist/qaReview.js +293 -41
  163. package/dist/qaTimeline.d.ts +18 -0
  164. package/dist/qaTimeline.js +123 -0
  165. package/dist/qaVerificationContract.d.ts +17 -0
  166. package/dist/qaVerificationContract.js +236 -0
  167. package/dist/readinessCleanup.d.ts +19 -0
  168. package/dist/readinessCleanup.js +122 -0
  169. package/dist/readinessGate.d.ts +1 -0
  170. package/dist/readinessGate.js +82 -0
  171. package/dist/recoveryGuidance.d.ts +17 -0
  172. package/dist/recoveryGuidance.js +38 -0
  173. package/dist/runtimeAuth.d.ts +1 -0
  174. package/dist/runtimeAuth.js +5 -1
  175. package/dist/runtimeReadiness.d.ts +13 -0
  176. package/dist/runtimeReadiness.js +206 -10
  177. package/dist/sessionIdentity.d.ts +2 -0
  178. package/dist/sessionIdentity.js +14 -0
  179. package/dist/sessionLifecycle.js +3 -3
  180. package/dist/sources/sourceRegistry.js +7 -0
  181. package/dist/stateTransfer.js +85 -8
  182. package/dist/statusReporter.d.ts +1 -0
  183. package/dist/statusReporter.js +1 -0
  184. package/dist/supervisedStart.d.ts +6 -0
  185. package/dist/supervisedStart.js +178 -62
  186. package/dist/supervisor.d.ts +2 -0
  187. package/dist/supervisor.js +26 -2
  188. package/dist/terminalStyle.d.ts +21 -0
  189. package/dist/terminalStyle.js +50 -0
  190. package/dist/ticketPopulation.d.ts +1 -0
  191. package/dist/ticketPopulation.js +5 -1
  192. package/dist/tickets/commands.js +2 -2
  193. package/dist/tickets/queue.js +1 -1
  194. package/dist/tickets/ticketSchema.d.ts +198 -0
  195. package/dist/tickets/ticketSchema.js +2 -0
  196. package/dist/windowsProbeJob.d.ts +12 -0
  197. package/dist/windowsProbeJob.js +144 -0
  198. package/dist/workflowDb.d.ts +121 -6
  199. package/dist/workflowDb.js +874 -30
  200. package/dist/workflowReader.d.ts +116 -2
  201. package/dist/workflowReader.js +131 -4
  202. package/package.json +89 -8
  203. package/scripts/copy-graph-assets.mjs +3 -0
  204. package/scripts/graph-benchmark.mjs +20 -0
  205. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,15 @@
1
+ import type { BuilderAdapter } from "../adapters/types.js";
2
+ export declare const GRAPH_CHANGE_SIGNAL = "RAFI_GRAPH_MAINTENANCE: meaningful-indexed-change";
3
+ export interface GraphPendingTask {
4
+ version: 1;
5
+ state: "pending" | "attempted";
6
+ scope: string;
7
+ workspace: string;
8
+ logicalTaskId: string;
9
+ reason: string;
10
+ outcome?: unknown;
11
+ }
12
+ /** A completed editing assignment plus host-observed source change is required.
13
+ * Changed bytes alone are deliberately insufficient to establish this trigger. */
14
+ export declare function queueGraphMaintenance(root: string, workspace: string, logicalTaskId: string, response: string, sourceChanged: boolean): void;
15
+ export declare function maintainGraphTask(root: string, workspace: string, logicalTaskId: string, host: BuilderAdapter): Promise<unknown>;
@@ -0,0 +1,50 @@
1
+ import { WorkflowDb } from "../workflowDb.js";
2
+ import { withActivityPhase } from "../activity.js";
3
+ import { loadGraphConfig } from "./config.js";
4
+ import { graphScope } from "./read.js";
5
+ import { refreshGraph } from "./maintenance.js";
6
+ import { graphSemanticHost } from "./host.js";
7
+ import { digest } from "./util.js";
8
+ export const GRAPH_CHANGE_SIGNAL = "RAFI_GRAPH_MAINTENANCE: meaningful-indexed-change";
9
+ /** A completed editing assignment plus host-observed source change is required.
10
+ * Changed bytes alone are deliberately insufficient to establish this trigger. */
11
+ export function queueGraphMaintenance(root, workspace, logicalTaskId, response, sourceChanged) {
12
+ if (!sourceChanged || !response.split(/\r?\n/).some(line => line.trim() === GRAPH_CHANGE_SIGNAL))
13
+ return;
14
+ const effective = loadGraphConfig(root);
15
+ if (!effective.enabled || effective.config?.maintenance !== "selective")
16
+ return;
17
+ const scope = graphScope(workspace), id = digest("pending-task", { scope, logicalTaskId, policy: effective.policyDigest });
18
+ const db = new WorkflowDb(root);
19
+ try {
20
+ const store = db.graphStore();
21
+ if (!store.get("job", id))
22
+ store.put("job", id, { version: 1, state: "pending", scope, workspace, logicalTaskId, reason: "Completed editing agent declared indexed relationships or behavior changed; host observed source change" });
23
+ }
24
+ finally {
25
+ db.close();
26
+ }
27
+ }
28
+ export async function maintainGraphTask(root, workspace, logicalTaskId, host) {
29
+ const effective = loadGraphConfig(root);
30
+ if (!effective.enabled || effective.config?.maintenance !== "selective")
31
+ return;
32
+ const scope = graphScope(workspace), id = digest("pending-task", { scope, logicalTaskId, policy: effective.policyDigest });
33
+ const db = new WorkflowDb(root);
34
+ try {
35
+ const store = db.graphStore(), pending = store.get("job", id);
36
+ if (!pending || pending.value.state !== "pending")
37
+ return;
38
+ // Reserve the attempt before awaiting any child; duplicate consumers cannot
39
+ // each start semantic/provider work for this same editing boundary.
40
+ store.put("job", id, { ...pending.value, state: "attempted" }, pending.revision);
41
+ const outcome = await withActivityPhase("updating scoped Graphify evidence", () => refreshGraph(root, workspace, logicalTaskId, { semanticHost: graphSemanticHost(root, host) }));
42
+ store.put("job", id, { ...pending.value, state: "attempted", outcome });
43
+ if (outcome.state !== "published")
44
+ console.error(`rafi graph: maintenance ${outcome.state}: ${outcome.reason ?? "inspect graph status for recovery"}`);
45
+ return outcome;
46
+ }
47
+ finally {
48
+ db.close();
49
+ }
50
+ }
@@ -0,0 +1,9 @@
1
+ /** Resolve existing tools only. Never executes discovery commands which can install. */
2
+ export declare function graphPythonCandidates(): string[];
3
+ export declare function resolveGraphPython(): Promise<string>;
4
+ export declare function runBridge<T>(python: string, input: unknown, options?: {
5
+ timeoutMs?: number;
6
+ maxOutputBytes?: number;
7
+ cwd?: string;
8
+ signal?: AbortSignal;
9
+ }): Promise<T>;
@@ -0,0 +1,146 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
+ import { delimiter, dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const asset = fileURLToPath(new URL("./bridge.py", import.meta.url));
6
+ /** Resolve existing tools only. Never executes discovery commands which can install. */
7
+ export function graphPythonCandidates() {
8
+ const candidates = [];
9
+ if (process.env.RAFI_GRAPH_PYTHON)
10
+ candidates.push(process.env.RAFI_GRAPH_PYTHON);
11
+ for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
12
+ const bin = join(dir, process.platform === "win32" ? "graphify.exe" : "graphify");
13
+ if (existsSync(bin)) {
14
+ if (process.platform === "win32")
15
+ candidates.push(join(dirname(dirname(bin)), "python.exe"));
16
+ else {
17
+ const line = readFileSync(realpathSync(bin), "utf8").split(/\r?\n/, 1)[0];
18
+ if (line?.startsWith("#!/") && !line.slice(2).includes("\0") && !line.includes("/env "))
19
+ candidates.push(line.slice(2).trim());
20
+ }
21
+ }
22
+ }
23
+ candidates.push(process.platform === "win32" ? "python" : "python3");
24
+ return [...new Set(candidates)];
25
+ }
26
+ function dispatchBridge(python, input, options = {}) {
27
+ if (options.signal?.aborted)
28
+ return Promise.reject(new Error("Graph operation cancelled"));
29
+ const serialized = JSON.stringify(input);
30
+ if (Buffer.byteLength(serialized) > 128 * 1024 * 1024)
31
+ return Promise.reject(new Error("Graph bridge input budget exceeded"));
32
+ return new Promise((resolve, reject) => {
33
+ const env = { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, WINDIR: process.env.WINDIR, PYTHONDONTWRITEBYTECODE: "1", PYTHONNOUSERSITE: "1", PYTHONHASHSEED: "0" };
34
+ const child = spawn(python, ["-I", "-B", asset], { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true, detached: process.platform !== "win32" });
35
+ let done = false, size = 0, errorBytes = 0;
36
+ const chunks = [];
37
+ const stop = () => {
38
+ try {
39
+ if (process.platform !== "win32" && child.pid)
40
+ process.kill(-child.pid, "SIGKILL");
41
+ else
42
+ child.kill("SIGKILL");
43
+ }
44
+ catch {
45
+ child.kill("SIGKILL");
46
+ }
47
+ };
48
+ const finish = (error, result) => {
49
+ if (done)
50
+ return;
51
+ done = true;
52
+ clearTimeout(timer);
53
+ options.signal?.removeEventListener("abort", aborted);
54
+ if (error) {
55
+ stop();
56
+ reject(error);
57
+ }
58
+ else
59
+ resolve(result);
60
+ };
61
+ const aborted = () => finish(new Error("Graph operation cancelled"));
62
+ const timer = setTimeout(() => finish(new Error("Graph bridge deadline exceeded")), options.timeoutMs ?? 15000);
63
+ options.signal?.addEventListener("abort", aborted, { once: true });
64
+ if (options.signal?.aborted)
65
+ aborted();
66
+ child.on("error", error => finish(error));
67
+ child.stdout.on("data", (chunk) => {
68
+ size += chunk.length;
69
+ if (size > (options.maxOutputBytes ?? 1024 * 1024))
70
+ finish(new Error("Graph bridge output limit exceeded"));
71
+ else
72
+ chunks.push(chunk);
73
+ });
74
+ child.stderr.on("data", (chunk) => {
75
+ errorBytes += chunk.length;
76
+ if (errorBytes > 64 * 1024)
77
+ finish(new Error("Graph bridge diagnostic limit exceeded"));
78
+ });
79
+ child.stdin.on("error", error => finish(error));
80
+ child.on("close", code => {
81
+ if (done)
82
+ return;
83
+ try {
84
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
85
+ if (code !== 0 || result.ok !== true)
86
+ throw new Error(result.error ?? `Graph bridge exited ${code}`);
87
+ finish(undefined, result.result);
88
+ }
89
+ catch (error) {
90
+ finish(error instanceof Error ? error : new Error(String(error)));
91
+ }
92
+ });
93
+ child.stdin.end(serialized);
94
+ });
95
+ }
96
+ export async function resolveGraphPython() {
97
+ const errors = [];
98
+ for (const candidate of graphPythonCandidates()) {
99
+ try {
100
+ await runBridge(candidate, { action: "probe" }, { timeoutMs: 5000 });
101
+ return candidate;
102
+ }
103
+ catch (error) {
104
+ errors.push(String(error));
105
+ }
106
+ }
107
+ throw new Error(`Compatible graphifyy==0.9.82 unavailable. Run graph adopt to repair an authorized installation. ${errors.at(-1) ?? ""}`);
108
+ }
109
+ let activeWorkers = 0;
110
+ const workerWaiters = [];
111
+ export async function runBridge(python, input, options = {}) {
112
+ const deadline = Date.now() + (options.timeoutMs ?? 15000);
113
+ if (options.signal?.aborted)
114
+ throw new Error("Graph operation cancelled");
115
+ if (activeWorkers >= 2)
116
+ await new Promise((resolve, reject) => {
117
+ const cleanup = () => { clearTimeout(timer); options.signal?.removeEventListener("abort", abort); };
118
+ const ready = () => { cleanup(); resolve(); };
119
+ const fail = (message) => {
120
+ const index = workerWaiters.indexOf(ready);
121
+ if (index < 0)
122
+ return;
123
+ workerWaiters.splice(index, 1);
124
+ cleanup();
125
+ reject(new Error(message));
126
+ };
127
+ const abort = () => fail("Graph operation cancelled while waiting for worker capacity");
128
+ const timer = setTimeout(() => fail("Graph worker capacity deadline exceeded"), Math.max(1, deadline - Date.now()));
129
+ workerWaiters.push(ready);
130
+ options.signal?.addEventListener("abort", abort, { once: true });
131
+ if (options.signal?.aborted)
132
+ abort();
133
+ });
134
+ else
135
+ activeWorkers++;
136
+ try {
137
+ return await dispatchBridge(python, input, { ...options, timeoutMs: Math.max(1, deadline - Date.now()) });
138
+ }
139
+ finally {
140
+ const next = workerWaiters.shift();
141
+ if (next)
142
+ next(); // Transfer the occupied slot directly to the queued caller.
143
+ else
144
+ activeWorkers--;
145
+ }
146
+ }
@@ -0,0 +1,205 @@
1
+ """Pinned Graphify adapter. One bounded JSON request on stdin, one JSON result.
2
+
3
+ Read requests never call Graphify's CLI/server loaders, query logging or detectors.
4
+ The audit hook denies filesystem writes, subprocesses and networking in read mode.
5
+ """
6
+ import sys
7
+ sys.dont_write_bytecode = True
8
+ import json
9
+ import os
10
+ import importlib.metadata
11
+ from pathlib import Path
12
+
13
+ # OS address-space cap where supported; conservative collection admission below
14
+ # also applies on platforms without resource controls.
15
+ resource_controls = {"addressSpaceBytes": None, "cpuSeconds": None,
16
+ "memoryEnforcement": "collection-admission-only"}
17
+ try:
18
+ import resource
19
+ try:
20
+ resource.setrlimit(resource.RLIMIT_AS, (512 * 1024 * 1024, 512 * 1024 * 1024))
21
+ resource_controls["addressSpaceBytes"] = 512 * 1024 * 1024
22
+ resource_controls["memoryEnforcement"] = "os-address-space-limit"
23
+ except (ValueError, OSError):
24
+ pass
25
+ try:
26
+ resource.setrlimit(resource.RLIMIT_CPU, (300, 300))
27
+ resource_controls["cpuSeconds"] = 300
28
+ except (ValueError, OSError):
29
+ pass
30
+ except ImportError:
31
+ pass
32
+ MAX_WIRE = 128 * 1024 * 1024
33
+ raw = sys.stdin.buffer.read(MAX_WIRE + 1)
34
+ if len(raw) > MAX_WIRE:
35
+ raise ValueError("bridge input limit exceeded")
36
+ request = json.loads(raw)
37
+ write_root = Path(request["staging"]).resolve() if request.get("action") == "extract" else None
38
+
39
+
40
+ def audit(event, args):
41
+ if event.startswith(("socket.", "subprocess.", "os.system", "os.exec", "os.spawn", "os.fork", "os.posix_spawn")):
42
+ raise PermissionError("graph bridge cannot start processes or use network")
43
+ paths = []
44
+ if event == "open":
45
+ path, mode, flags = args
46
+ writing = isinstance(mode, str) and any(c in mode for c in "wax+")
47
+ writing = writing or isinstance(flags, int) and bool(flags & (os.O_WRONLY | os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_APPEND))
48
+ if writing:
49
+ paths = [path]
50
+ elif event in ("os.mkdir", "os.remove", "os.rmdir", "os.chmod", "os.utime", "os.truncate"):
51
+ paths = [args[0]]
52
+ elif event in ("os.rename", "os.link", "os.symlink"):
53
+ paths = list(args[:2])
54
+ for path in paths:
55
+ # fdopen on an already confined tempfile descriptor is safe: this child
56
+ # inherits only stdio and all path-based writable opens pass this hook.
57
+ if write_root and isinstance(path, int):
58
+ continue
59
+ if not write_root or not isinstance(path, (str, bytes)) or not Path(os.fsdecode(path)).resolve().is_relative_to(write_root):
60
+ raise PermissionError("graph bridge write outside authorized staging")
61
+
62
+
63
+ sys.addaudithook(audit)
64
+
65
+
66
+ def main():
67
+ version = importlib.metadata.version("graphifyy")
68
+ if version != "0.9.82":
69
+ raise ValueError("incompatible Graphify version: " + version)
70
+ if request.get("action") == "probe":
71
+ import graphify.serve
72
+ return {"version": version, "schema": 1, "python": sys.version.split()[0]}
73
+ if request.get("action") == "extract":
74
+ from graphify.extract import extract
75
+ from graphify.build import build_from_json
76
+ from networkx.readwrite import json_graph
77
+ root = write_root / "input"
78
+ paths = []
79
+ for relative in request["paths"]:
80
+ path = (root / relative).resolve()
81
+ if not path.is_relative_to(root) or path.is_symlink():
82
+ raise ValueError("escaped captured input")
83
+ paths.append(path)
84
+ # Copy only content/version-keyed AST entries for the current capture.
85
+ # Upstream rebases portable cached IDs onto this immutable input root.
86
+ import hashlib
87
+ from graphify import cache
88
+ copied = 0
89
+ parent = request.get("parentCache")
90
+ expected = request.get("cacheChecksums", {})
91
+ if parent:
92
+ namespace = "graphify-out/cache/ast/v" + version + "-s" + str(cache._AST_CACHE_SCHEMA)
93
+ for path in paths:
94
+ key = cache.file_hash(path, root, cache_root=write_root)
95
+ relative = namespace + "/" + key + ".json"
96
+ previous = Path(parent) / relative
97
+ if relative not in expected or not previous.is_file() or previous.is_symlink():
98
+ continue
99
+ if previous.stat().st_size > 8 * 1024 * 1024:
100
+ raise ValueError("AST cache item budget exceeded")
101
+ content = previous.read_bytes()
102
+ if hashlib.sha256(content).hexdigest() != expected[relative]:
103
+ raise ValueError("AST cache checksum mismatch")
104
+ target = write_root / relative
105
+ target.parent.mkdir(parents=True, exist_ok=True)
106
+ target.write_bytes(content)
107
+ copied += 1
108
+ # All caches and parser work remain inside owned staging; no worker pool.
109
+ result = extract(paths, root=root, cache_root=write_root, parallel=False)
110
+ if result.get("failed_sources"):
111
+ raise ValueError("AST failed inputs: " + repr(result["failed_sources"][:20]))
112
+ # Never persist staging-directory identities as portable provenance.
113
+ def portable(value):
114
+ if isinstance(value, str):
115
+ return value.replace(str(root) + os.sep, "")
116
+ if isinstance(value, list):
117
+ return [portable(v) for v in value]
118
+ if isinstance(value, dict):
119
+ return {k: portable(v) for k, v in value.items()}
120
+ return value
121
+ result = portable(result)
122
+ semantic = request.get("semantic", {"nodes": [], "edges": []})
123
+ result["nodes"].extend(semantic.get("nodes", []))
124
+ result["edges"].extend(semantic.get("edges", []))
125
+ graph = build_from_json(result, root=root, directed=True)
126
+ from graphify.export import to_html
127
+ if not to_html(graph, {}, str(write_root / "graph.html"), learning_overlay={}):
128
+ raise ValueError("Required graph visualization was not written")
129
+ data = json_graph.node_link_data(graph, edges="links")
130
+ data.setdefault("graph", {}).update(schema_version=1, graphify_version=version)
131
+ cache_files = {}
132
+ cache_base = write_root / "graphify-out" / "cache" / "ast"
133
+ if cache_base.exists():
134
+ for cached in cache_base.rglob("*.json"):
135
+ cache_files[cached.relative_to(write_root).as_posix()] = hashlib.sha256(cached.read_bytes()).hexdigest()
136
+ return {"graph": data, "extraction": result, "cacheFiles": cache_files, "reusedAstInputs": copied}
137
+
138
+ import networkx as nx
139
+ from networkx.readwrite import json_graph
140
+ from graphify import serve
141
+ # Optional jieba lazily writes a tokenizer cache. Use the pure fallback.
142
+ serve._jieba = None
143
+ data = request["graph"]
144
+ if data.get("graph", {}).get("schema_version") != 1:
145
+ raise ValueError("incompatible graph schema")
146
+ nodes = data.get("nodes")
147
+ links = data.get("links", data.get("edges"))
148
+ if not isinstance(nodes, list) or not isinstance(links, list):
149
+ raise ValueError("invalid graph collections")
150
+ if len(nodes) > 100000 or len(links) > 300000:
151
+ raise ValueError("graph memory admission limit exceeded")
152
+ ids = {n["id"] for n in nodes}
153
+ if len(ids) != len(nodes) or any(not isinstance(i, str) for i in ids):
154
+ raise ValueError("invalid or duplicate node identity")
155
+ if any(e.get("source") not in ids or e.get("target") not in ids or e.get("_src", e["source"]) not in ids or e.get("_tgt", e["target"]) not in ids for e in links):
156
+ raise ValueError("dangling graph endpoint")
157
+ graph = json_graph.node_link_graph({**data, "links": links, "directed": True}, edges="links")
158
+ graph.graph["_logical_directed"] = bool(data.get("directed"))
159
+ op = request["operation"]
160
+ seeds = []
161
+ ambiguous = False
162
+ for seed in op.get("seeds", []):
163
+ found = [seed] if seed in graph else serve._find_node(graph, seed)
164
+ ambiguous = ambiguous or len(found) > 1
165
+ seeds.extend(found[:16])
166
+ if op["operation"] == "query":
167
+ terms = serve._query_terms(op["query"])
168
+ scores = serve._score_query(graph, terms, collect_per_term_seeds=True)
169
+ seeds = serve._pick_seeds(scores.ranked, G=graph, best_seed_by_term=scores.best_seed_by_term)[:16]
170
+ direction = op.get("direction", "both")
171
+ view = serve._path_search_graph(graph, direction == "both")
172
+ if direction == "incoming":
173
+ view = view.reverse(copy=False)
174
+ selected = set(seeds)
175
+ if op["operation"] == "path" and len(seeds) == 1 and not ambiguous:
176
+ targets = [op["target"]] if op["target"] in graph else serve._find_node(graph, op["target"])
177
+ ambiguous = len(targets) > 1
178
+ if len(targets) == 1:
179
+ try:
180
+ selected = set(nx.shortest_path(view, seeds[0], targets[0]))
181
+ except nx.NetworkXNoPath:
182
+ selected = set()
183
+ else:
184
+ selected = set(targets)
185
+ elif op["operation"] not in ("node", "status") and not ambiguous and seeds:
186
+ selected, _ = serve._bfs(view, seeds, op.get("depth", 2))
187
+ if op["operation"] == "status":
188
+ selected = set()
189
+ selected_order = list(dict.fromkeys([n for n in seeds if n in selected] + sorted(selected)))[:500]
190
+ kept = set(selected_order)
191
+ edges = [e for e in links if e["source"] in kept and e["target"] in kept]
192
+ return {"nodes": [{"id": n, **dict(graph.nodes[n])} for n in selected_order], "edges": edges[:1000], "ambiguous": ambiguous,
193
+ "truncated": len(selected) > len(kept) or len(edges) > 1000, "nodeCount": len(nodes), "edgeCount": len(links)}
194
+
195
+
196
+ try:
197
+ # Libraries occasionally print extraction diagnostics. Keep stdout framed.
198
+ import contextlib
199
+ with contextlib.redirect_stdout(sys.stderr):
200
+ result = main()
201
+ result["resourceControls"] = resource_controls
202
+ sys.stdout.write(json.dumps({"ok": True, "result": result}, ensure_ascii=False))
203
+ except Exception as error:
204
+ sys.stdout.write(json.dumps({"ok": False, "error": str(error)[:2000]}))
205
+ sys.exit(1)
@@ -0,0 +1,11 @@
1
+ import { DEFAULT_GRAPH_LIMITS, type GraphAdoptionV1, type GraphConfigV1 } from "rafi-spec";
2
+ export interface EffectiveGraphConfig {
3
+ config?: GraphConfigV1;
4
+ adoption?: GraphAdoptionV1;
5
+ enabled: boolean;
6
+ reason: string;
7
+ policyDigest: string;
8
+ projectRef: string;
9
+ limits: typeof DEFAULT_GRAPH_LIMITS;
10
+ }
11
+ export declare function loadGraphConfig(configRoot: string): EffectiveGraphConfig;
@@ -0,0 +1,40 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parse } from "yaml";
4
+ import { assertGraphConfig, DEFAULT_GRAPH_LIMITS } from "rafi-spec";
5
+ import { WorkflowReader } from "../workflowReader.js";
6
+ import { digest, readBounded } from "./util.js";
7
+ export function loadGraphConfig(configRoot) {
8
+ const root = realpathSync(configRoot);
9
+ const projectRef = digest("project", root);
10
+ let config;
11
+ for (const name of ["rafi-config.yaml", "project.yaml"]) {
12
+ const path = join(root, name);
13
+ if (!existsSync(path))
14
+ continue;
15
+ const raw = parse(readBounded(path, 4 * 1024 * 1024).toString("utf8"));
16
+ if (raw?.graph !== undefined) {
17
+ assertGraphConfig(raw.graph);
18
+ config = raw.graph;
19
+ }
20
+ break;
21
+ }
22
+ const reader = new WorkflowReader(root);
23
+ let adoption;
24
+ try {
25
+ adoption = reader.graphRecord("adoption", "project")?.value;
26
+ }
27
+ finally {
28
+ reader.close();
29
+ }
30
+ if (adoption) {
31
+ if (adoption.version !== 1)
32
+ throw new Error("Unsupported graph adoption version");
33
+ assertGraphConfig(adoption.config);
34
+ }
35
+ config ??= adoption?.config;
36
+ const policyDigest = digest("policy", config ?? null);
37
+ const enabled = Boolean(config?.enabled && adoption && adoption.policyDigest === policyDigest && adoption.projectRef === projectRef);
38
+ const reason = !config?.enabled ? "disabled" : !adoption ? "unadopted" : adoption.projectRef !== projectRef ? "destination-adoption-required" : adoption.policyDigest !== policyDigest ? "policy-acceptance-required" : "adopted";
39
+ return { config, adoption, enabled, reason, policyDigest, projectRef, limits: { ...DEFAULT_GRAPH_LIMITS, ...config?.limits } };
40
+ }
@@ -0,0 +1,16 @@
1
+ import type { GraphInputV1, GraphSourceBindingV1 } from "rafi-spec";
2
+ import type { EffectiveGraphConfig } from "./config.js";
3
+ export interface CapturedGraphCorpus {
4
+ inputs: GraphInputV1[];
5
+ bytes: Map<string, Buffer>;
6
+ corpusDigest: string;
7
+ binding: GraphSourceBindingV1;
8
+ exclusionsDigest: string;
9
+ }
10
+ export declare function globMatch(path: string, pattern: string): boolean;
11
+ export declare function graphExclusions(workspace: string, inputPaths?: string[]): {
12
+ text: string;
13
+ patterns: string[];
14
+ };
15
+ export declare function graphPathAllowed(path: string, config: EffectiveGraphConfig, patterns: string[]): boolean;
16
+ export declare function captureGraphCorpus(workspace: string, configRoot: string, config: EffectiveGraphConfig, sourceRef?: string): CapturedGraphCorpus;