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.
- package/README.md +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- 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;
|