@neat.is/core 0.2.7 → 0.2.8
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/dist/{chunk-T2U4U256.js → chunk-FTKDVKBC.js} +76 -13
- package/dist/chunk-FTKDVKBC.js.map +1 -0
- package/dist/{chunk-6SFEITLJ.js → chunk-GAYTAGEH.js} +298 -13
- package/dist/chunk-GAYTAGEH.js.map +1 -0
- package/dist/{chunk-6JT6L2OV.js → chunk-HWO746IM.js} +8 -10
- package/dist/{chunk-6JT6L2OV.js.map → chunk-HWO746IM.js.map} +1 -1
- package/dist/cli.cjs +1129 -249
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +22 -1
- package/dist/cli.d.ts +22 -1
- package/dist/cli.js +757 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +303 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -18
- package/dist/neatd.cjs +32 -2
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -3
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +190 -49
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-6SFEITLJ.js.map +0 -1
- package/dist/chunk-T2U4U256.js.map +0 -1
- package/dist/chunk-WX55TLUT.js +0 -184
- package/dist/chunk-WX55TLUT.js.map +0 -1
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_PROJECT,
|
|
3
|
+
EVENT_BUS_CHANNEL,
|
|
3
4
|
PolicyViolationsLog,
|
|
4
5
|
Projects,
|
|
5
6
|
TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH,
|
|
6
7
|
TRANSITIVE_DEPENDENCIES_MAX_DEPTH,
|
|
7
8
|
evaluateAllPolicies,
|
|
9
|
+
eventBus,
|
|
8
10
|
extractFromDirectory,
|
|
9
11
|
getBlastRadius,
|
|
10
12
|
getRootCause,
|
|
11
13
|
getTransitiveDependencies,
|
|
14
|
+
listProjects,
|
|
12
15
|
loadPolicyFile,
|
|
13
16
|
pathsForProject,
|
|
14
17
|
readErrorEvents,
|
|
15
18
|
readStaleEvents
|
|
16
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-GAYTAGEH.js";
|
|
17
20
|
|
|
18
21
|
// src/diff.ts
|
|
19
22
|
import { promises as fs } from "fs";
|
|
@@ -95,6 +98,62 @@ function canonicalJson(value) {
|
|
|
95
98
|
import Fastify from "fastify";
|
|
96
99
|
import cors from "@fastify/cors";
|
|
97
100
|
import { PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
101
|
+
|
|
102
|
+
// src/streaming.ts
|
|
103
|
+
var SSE_HEARTBEAT_MS = 3e4;
|
|
104
|
+
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
105
|
+
function handleSse(req, reply, opts) {
|
|
106
|
+
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
107
|
+
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
108
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
109
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
110
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
111
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
112
|
+
reply.raw.flushHeaders?.();
|
|
113
|
+
let pending = 0;
|
|
114
|
+
let dropped = false;
|
|
115
|
+
const closeConnection = () => {
|
|
116
|
+
if (dropped) return;
|
|
117
|
+
dropped = true;
|
|
118
|
+
eventBus.off(EVENT_BUS_CHANNEL, listener);
|
|
119
|
+
clearInterval(heartbeat);
|
|
120
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
121
|
+
};
|
|
122
|
+
const writeFrame = (frame) => {
|
|
123
|
+
if (dropped) return;
|
|
124
|
+
if (pending >= backpressureCap) {
|
|
125
|
+
const errFrame = `event: error
|
|
126
|
+
data: ${JSON.stringify({ reason: "backpressure" })}
|
|
127
|
+
|
|
128
|
+
`;
|
|
129
|
+
reply.raw.write(errFrame);
|
|
130
|
+
closeConnection();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
pending++;
|
|
134
|
+
reply.raw.write(frame, () => {
|
|
135
|
+
pending = Math.max(0, pending - 1);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const listener = (envelope) => {
|
|
139
|
+
if (envelope.project !== opts.project) return;
|
|
140
|
+
writeFrame(`event: ${envelope.type}
|
|
141
|
+
data: ${JSON.stringify(envelope.payload)}
|
|
142
|
+
|
|
143
|
+
`);
|
|
144
|
+
};
|
|
145
|
+
eventBus.on(EVENT_BUS_CHANNEL, listener);
|
|
146
|
+
const heartbeat = setInterval(() => {
|
|
147
|
+
if (dropped) return;
|
|
148
|
+
reply.raw.write(":heartbeat\n\n");
|
|
149
|
+
}, heartbeatMs);
|
|
150
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
151
|
+
req.raw.on("close", closeConnection);
|
|
152
|
+
reply.raw.on("close", closeConnection);
|
|
153
|
+
reply.raw.on("error", closeConnection);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/api.ts
|
|
98
157
|
function serializeGraph(graph) {
|
|
99
158
|
const nodes = [];
|
|
100
159
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -142,6 +201,11 @@ function buildLegacyRegistry(opts) {
|
|
|
142
201
|
}
|
|
143
202
|
function registerRoutes(scope, ctx) {
|
|
144
203
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
204
|
+
scope.get("/events", (req, reply) => {
|
|
205
|
+
const proj = resolveProject(registry, req, reply);
|
|
206
|
+
if (!proj) return;
|
|
207
|
+
handleSse(req, reply, { project: proj.name });
|
|
208
|
+
});
|
|
145
209
|
scope.get("/health", async (req, reply) => {
|
|
146
210
|
const proj = resolveProject(registry, req, reply);
|
|
147
211
|
if (!proj) return;
|
|
@@ -433,17 +497,16 @@ async function buildApi(opts) {
|
|
|
433
497
|
staleEventsPathFor,
|
|
434
498
|
policyFilePathFor
|
|
435
499
|
};
|
|
436
|
-
app.get("/projects", async () =>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}));
|
|
500
|
+
app.get("/projects", async (_req, reply) => {
|
|
501
|
+
try {
|
|
502
|
+
return await listProjects();
|
|
503
|
+
} catch (err) {
|
|
504
|
+
return reply.code(500).send({
|
|
505
|
+
error: "failed to read project registry",
|
|
506
|
+
details: err.message
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
});
|
|
447
510
|
registerRoutes(app, routeCtx);
|
|
448
511
|
await app.register(
|
|
449
512
|
async (scope) => {
|
|
@@ -459,4 +522,4 @@ export {
|
|
|
459
522
|
computeGraphDiff,
|
|
460
523
|
buildApi
|
|
461
524
|
};
|
|
462
|
-
//# sourceMappingURL=chunk-
|
|
525
|
+
//# sourceMappingURL=chunk-FTKDVKBC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/diff.ts","../src/api.ts","../src/streaming.ts"],"sourcesContent":["import { promises as fs } from 'node:fs'\nimport type { GraphEdge, GraphNode } from '@neat.is/types'\nimport type { NeatGraph } from './graph.js'\n\n// Diff a snapshot on disk (or fetched over HTTP) against the live in-memory\n// graph. The \"base\" is the snapshot you supplied; the \"current\" is whatever\n// the server has loaded right now. Symmetric in shape, but consumers usually\n// want to read it as \"what changed since base.\"\n//\n// The shape mirrors the issue's spec (#77):\n// { added: { nodes, edges }, removed: { nodes, edges },\n// changed: { nodes: [{id, before, after}], edges: [{id, before, after}] } }\n//\n// Both timestamps are echoed back so the caller doesn't have to track them\n// separately.\n\ninterface PersistedNodeEntry {\n key?: string\n attributes?: Record<string, unknown>\n}\n\ninterface PersistedEdgeEntry {\n key?: string\n source?: string\n target?: string\n attributes?: Record<string, unknown>\n}\n\nexport interface PersistedSnapshot {\n schemaVersion?: number\n exportedAt?: string\n graph?: {\n nodes?: PersistedNodeEntry[]\n edges?: PersistedEdgeEntry[]\n }\n}\n\nexport interface GraphDiff {\n base: { exportedAt?: string }\n current: { exportedAt: string }\n added: { nodes: GraphNode[]; edges: GraphEdge[] }\n removed: { nodes: GraphNode[]; edges: GraphEdge[] }\n changed: {\n nodes: { id: string; before: GraphNode; after: GraphNode }[]\n edges: { id: string; before: GraphEdge; after: GraphEdge }[]\n }\n}\n\nexport async function loadSnapshotForDiff(target: string): Promise<PersistedSnapshot> {\n if (/^https?:\\/\\//i.test(target)) {\n const res = await fetch(target)\n if (!res.ok) {\n throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`)\n }\n return (await res.json()) as PersistedSnapshot\n }\n const raw = await fs.readFile(target, 'utf8')\n return JSON.parse(raw) as PersistedSnapshot\n}\n\nfunction indexEntries<T>(\n entries: { key?: string; attributes?: Record<string, unknown> }[] | undefined,\n): Map<string, T> {\n const m = new Map<string, T>()\n if (!entries) return m\n for (const entry of entries) {\n const id = (entry.attributes?.id as string | undefined) ?? entry.key\n if (!id) continue\n m.set(id, entry.attributes as T)\n }\n return m\n}\n\nexport function computeGraphDiff(\n liveGraph: NeatGraph,\n baseSnapshot: PersistedSnapshot,\n currentExportedAt: string = new Date().toISOString(),\n): GraphDiff {\n const baseNodes = indexEntries<GraphNode>(baseSnapshot.graph?.nodes)\n const baseEdges = indexEntries<GraphEdge>(baseSnapshot.graph?.edges)\n\n const liveNodes = new Map<string, GraphNode>()\n liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs as GraphNode))\n const liveEdges = new Map<string, GraphEdge>()\n liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs as GraphEdge))\n\n const result: GraphDiff = {\n base: { exportedAt: baseSnapshot.exportedAt },\n current: { exportedAt: currentExportedAt },\n added: { nodes: [], edges: [] },\n removed: { nodes: [], edges: [] },\n changed: { nodes: [], edges: [] },\n }\n\n for (const [id, after] of liveNodes) {\n const before = baseNodes.get(id)\n if (!before) {\n result.added.nodes.push(after)\n } else if (!shallowEqual(before, after)) {\n result.changed.nodes.push({ id, before, after })\n }\n }\n for (const [id, before] of baseNodes) {\n if (!liveNodes.has(id)) result.removed.nodes.push(before)\n }\n for (const [id, after] of liveEdges) {\n const before = baseEdges.get(id)\n if (!before) {\n result.added.edges.push(after)\n } else if (!shallowEqual(before, after)) {\n result.changed.edges.push({ id, before, after })\n }\n }\n for (const [id, before] of baseEdges) {\n if (!liveEdges.has(id)) result.removed.edges.push(before)\n }\n\n return result\n}\n\n// Stable JSON comparison. Snapshot order isn't guaranteed, so canonicalising\n// keys before stringify keeps the comparison robust against re-ordered fields.\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n return canonicalJson(a) === canonicalJson(b)\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(value, (_key, v) => {\n if (v && typeof v === 'object' && !Array.isArray(v)) {\n return Object.keys(v as Record<string, unknown>)\n .sort()\n .reduce<Record<string, unknown>>((acc, k) => {\n acc[k] = (v as Record<string, unknown>)[k]\n return acc\n }, {})\n }\n return v\n })\n}\n","import Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n} from 'fastify'\nimport cors from '@fastify/cors'\nimport type {\n ErrorEvent,\n GraphEdge,\n GraphNode,\n Policy,\n PolicyViolation,\n} from '@neat.is/types'\nimport { PoliciesCheckBodySchema, PolicySeveritySchema } from '@neat.is/types'\nimport {\n evaluateAllPolicies,\n loadPolicyFile,\n PolicyViolationsLog,\n} from './policy.js'\nimport type { NeatGraph } from './graph.js'\nimport { DEFAULT_PROJECT } from './graph.js'\nimport { extractFromDirectory } from './extract.js'\nimport { readErrorEvents, readStaleEvents } from './ingest.js'\nimport {\n getBlastRadius,\n getRootCause,\n getTransitiveDependencies,\n TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH,\n TRANSITIVE_DEPENDENCIES_MAX_DEPTH,\n} from './traverse.js'\nimport { computeGraphDiff, loadSnapshotForDiff } from './diff.js'\nimport type { SearchIndex } from './search.js'\nimport type { Projects, ProjectContext } from './projects.js'\nimport { Projects as ProjectsClass, pathsForProject } from './projects.js'\nimport { listProjects as listRegistryProjects } from './registry.js'\nimport { handleSse } from './streaming.js'\n\nexport interface BuildApiOptions {\n // Multi-project shape. Optional — when absent we synthesise a single-\n // project registry from the legacy fields below so existing callers\n // (mainly tests) keep working unchanged.\n projects?: Projects\n startedAt?: number\n\n // Legacy single-project shape. Mapped to project=`default` if `projects`\n // isn't provided.\n graph?: NeatGraph\n scanPath?: string\n errorsPath?: string\n staleEventsPath?: string\n searchIndex?: SearchIndex\n}\n\ninterface SerializedGraph {\n nodes: GraphNode[]\n edges: GraphEdge[]\n}\n\nfunction serializeGraph(graph: NeatGraph): SerializedGraph {\n const nodes: GraphNode[] = []\n graph.forEachNode((_id, attrs) => {\n nodes.push(attrs)\n })\n const edges: GraphEdge[] = []\n graph.forEachEdge((_id, attrs) => {\n edges.push(attrs)\n })\n return { nodes, edges }\n}\n\nfunction projectFromReq(req: FastifyRequest): string {\n // `:project` is optional in the URL — the request hits either\n // /projects/:project/X or /X (which means default). Coerce the missing\n // param to DEFAULT_PROJECT here so handlers don't repeat the fallback.\n const params = req.params as { project?: string }\n return params.project ?? DEFAULT_PROJECT\n}\n\nfunction resolveProject(\n registry: Projects,\n req: FastifyRequest,\n reply: FastifyReply,\n): ProjectContext | null {\n const name = projectFromReq(req)\n const ctx = registry.get(name)\n if (!ctx) {\n void reply.code(404).send({ error: 'project not found', project: name })\n return null\n }\n return ctx\n}\n\nfunction buildLegacyRegistry(opts: BuildApiOptions): Projects {\n if (opts.projects) return opts.projects\n if (!opts.graph) {\n throw new Error('buildApi: either `projects` or `graph` must be provided')\n }\n const registry = new ProjectsClass()\n // pathsForProject only matters here for the snapshot/embeddings paths\n // routes never read; the ingest paths come from explicit options below.\n const paths = pathsForProject(DEFAULT_PROJECT, '')\n registry.set(DEFAULT_PROJECT, {\n graph: opts.graph,\n scanPath: opts.scanPath,\n paths: {\n snapshotPath: paths.snapshotPath,\n errorsPath: opts.errorsPath ?? paths.errorsPath,\n staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,\n embeddingsCachePath: paths.embeddingsCachePath,\n policyViolationsPath: paths.policyViolationsPath,\n },\n searchIndex: opts.searchIndex,\n })\n return registry\n}\n\ninterface RouteContext {\n registry: Projects\n startedAt: number\n // Legacy callers passed `errorsPath`/`staleEventsPath` explicitly and\n // expected absent values to disable the read. Track that intent so the\n // /incidents handlers don't accidentally read a phantom file.\n errorsPathFor: (ctx: ProjectContext) => string | undefined\n staleEventsPathFor: (ctx: ProjectContext) => string | undefined\n // policy.json lives at the project root (per ADR-042 §File location), not\n // under neat-out/. Routes that read it map a project context to the path.\n policyFilePathFor: (ctx: ProjectContext) => string | undefined\n}\n\n// Registers every project-scoped route on `scope`. Called twice from\n// buildApi: once on the root app (so /graph etc. land at default), once\n// inside a `register(_, { prefix: '/projects/:project' })` plugin so the\n// same handlers run when the URL names a project explicitly.\nfunction registerRoutes(scope: FastifyInstance, ctx: RouteContext): void {\n const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx\n\n // SSE event stream (ADR-051 #1). Dual-mounted: hits /events for default\n // project and /projects/:project/events when scoped. The handler keeps\n // the connection open and writes one frame per bus envelope whose\n // `project` matches.\n scope.get<{ Params: { project?: string } }>('/events', (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n handleSse(req, reply, { project: proj.name })\n })\n\n scope.get<{ Params: { project?: string } }>('/health', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n return {\n uptime: Math.floor((Date.now() - startedAt) / 1000),\n project: proj.name,\n nodeCount: proj.graph.order,\n edgeCount: proj.graph.size,\n lastUpdated: new Date().toISOString(),\n }\n })\n\n scope.get<{ Params: { project?: string } }>('/graph', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n return serializeGraph(proj.graph)\n })\n\n scope.get<{ Params: { project?: string; id: string } }>(\n '/graph/node/:id',\n async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const { id } = req.params\n if (!proj.graph.hasNode(id)) {\n return reply.code(404).send({ error: 'node not found', id })\n }\n return proj.graph.getNodeAttributes(id) as GraphNode\n },\n )\n\n scope.get<{ Params: { project?: string; id: string } }>(\n '/graph/edges/:id',\n async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const { id } = req.params\n if (!proj.graph.hasNode(id)) {\n return reply.code(404).send({ error: 'node not found', id })\n }\n const inbound = proj.graph\n .inboundEdges(id)\n .map((e) => proj.graph.getEdgeAttributes(e) as GraphEdge)\n const outbound = proj.graph\n .outboundEdges(id)\n .map((e) => proj.graph.getEdgeAttributes(e) as GraphEdge)\n return { inbound, outbound }\n },\n )\n\n // Transitive dependencies (issue #144). BFS outbound to depth N, returning\n // a flat list with distance + edgeType + provenance per dependency.\n // Default depth 3, max 10. The MCP get_dependencies tool calls this.\n scope.get<{\n Params: { project?: string; id: string }\n Querystring: { depth?: string }\n }>('/graph/node/:id/dependencies', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const { id } = req.params\n if (!proj.graph.hasNode(id)) {\n return reply.code(404).send({ error: 'node not found', id })\n }\n const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH\n if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {\n return reply.code(400).send({\n error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`,\n })\n }\n return getTransitiveDependencies(proj.graph, id, depth)\n })\n\n scope.get<{ Params: { project?: string } }>('/incidents', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const epath = errorsPathFor(proj)\n if (!epath) return []\n return readErrorEvents(epath)\n })\n\n scope.get<{\n Params: { project?: string }\n Querystring: { limit?: string; edgeType?: string }\n }>('/incidents/stale', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const spath = staleEventsPathFor(proj)\n if (!spath) return []\n const events = await readStaleEvents(spath)\n const filtered = req.query.edgeType\n ? events.filter((e) => e.edgeType === req.query.edgeType)\n : events\n const ordered = [...filtered].reverse()\n const limit = req.query.limit ? Number(req.query.limit) : 50\n return ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50)\n })\n\n scope.get<{ Params: { project?: string; nodeId: string } }>(\n '/incidents/:nodeId',\n async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const { nodeId } = req.params\n if (!proj.graph.hasNode(nodeId)) {\n return reply.code(404).send({ error: 'node not found', id: nodeId })\n }\n const epath = errorsPathFor(proj)\n if (!epath) return []\n const events = await readErrorEvents(epath)\n return events.filter(\n (e) =>\n e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, ''),\n )\n },\n )\n\n scope.get<{\n Params: { project?: string; nodeId: string }\n Querystring: { errorId?: string }\n }>('/traverse/root-cause/:nodeId', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const { nodeId } = req.params\n if (!proj.graph.hasNode(nodeId)) {\n return reply.code(404).send({ error: 'node not found', id: nodeId })\n }\n let errorEvent: ErrorEvent | undefined\n const epath = errorsPathFor(proj)\n if (req.query.errorId && epath) {\n const events = await readErrorEvents(epath)\n errorEvent = events.find((e) => e.id === req.query.errorId)\n if (!errorEvent) {\n return reply\n .code(404)\n .send({ error: 'error event not found', id: req.query.errorId })\n }\n }\n const result = getRootCause(proj.graph, nodeId, errorEvent)\n if (!result) return reply.code(404).send({ error: 'no root cause found', id: nodeId })\n return result\n })\n\n scope.get<{\n Params: { project?: string; nodeId: string }\n Querystring: { depth?: string }\n }>('/traverse/blast-radius/:nodeId', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const { nodeId } = req.params\n if (!proj.graph.hasNode(nodeId)) {\n return reply.code(404).send({ error: 'node not found', id: nodeId })\n }\n const depth = req.query.depth ? Number(req.query.depth) : undefined\n if (depth !== undefined && (!Number.isFinite(depth) || depth < 0)) {\n return reply.code(400).send({ error: 'depth must be a non-negative number' })\n }\n return getBlastRadius(proj.graph, nodeId, depth)\n })\n\n scope.get<{\n Params: { project?: string }\n Querystring: { q?: string; limit?: string }\n }>('/search', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const raw = (req.query.q ?? '').trim()\n if (!raw) return reply.code(400).send({ error: 'query parameter `q` is required' })\n const limit = req.query.limit ? Number(req.query.limit) : undefined\n const safeLimit =\n limit !== undefined && Number.isFinite(limit) && limit > 0 ? limit : undefined\n if (proj.searchIndex) {\n const result = await proj.searchIndex.search(raw, safeLimit)\n return {\n query: result.query,\n provider: result.provider,\n matches: result.matches.map((m) => ({ ...m.node, score: m.score })),\n }\n }\n const q = raw.toLowerCase()\n const matches: (GraphNode & { score: number })[] = []\n proj.graph.forEachNode((id, attrs) => {\n const name = (attrs as { name?: string }).name ?? ''\n if (id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {\n matches.push({ ...(attrs as GraphNode), score: 1 })\n }\n })\n return {\n query: q,\n provider: 'substring' as const,\n matches: matches.slice(0, safeLimit),\n }\n })\n\n scope.get<{ Params: { project?: string }; Querystring: { against?: string } }>(\n '/graph/diff',\n async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const against = req.query.against\n if (!against) {\n return reply.code(400).send({ error: 'query parameter `against` is required' })\n }\n try {\n const snapshot = await loadSnapshotForDiff(against)\n return computeGraphDiff(proj.graph, snapshot)\n } catch (err) {\n return reply\n .code(400)\n .send({ error: 'failed to load snapshot', against, detail: (err as Error).message })\n }\n },\n )\n\n scope.post<{ Params: { project?: string } }>('/graph/scan', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n if (!proj.scanPath) {\n return reply\n .code(409)\n .send({ error: 'scan path not configured for this project', project: proj.name })\n }\n const result = await extractFromDirectory(proj.graph, proj.scanPath)\n return {\n project: proj.name,\n scanned: proj.scanPath,\n nodesAdded: result.nodesAdded,\n edgesAdded: result.edgesAdded,\n nodeCount: proj.graph.order,\n edgeCount: proj.graph.size,\n }\n })\n\n // Policy surface (ADR-045 / contract #18). /policies returns the parsed\n // policy.json; /policies/violations is the persistent log; /policies/check\n // is dry-run evaluation. All dual-mounted via registerRoutes.\n scope.get<{ Params: { project?: string } }>('/policies', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const policyPath = ctx.policyFilePathFor(proj)\n if (!policyPath) {\n // No policy file configured for this project — return the empty file\n // shape so consumers don't have to special-case \"no policies yet.\"\n return { version: 1, policies: [] }\n }\n try {\n const policies = await loadPolicyFile(policyPath)\n return { version: 1, policies }\n } catch (err) {\n return reply.code(400).send({\n error: 'policy.json failed to parse',\n details: (err as Error).message,\n })\n }\n })\n\n scope.get<{\n Params: { project?: string }\n Querystring: { severity?: string; policyId?: string }\n }>('/policies/violations', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const log = new PolicyViolationsLog(proj.paths.policyViolationsPath)\n let violations = await log.readAll()\n if (req.query.severity) {\n const sev = PolicySeveritySchema.safeParse(req.query.severity)\n if (!sev.success) {\n return reply.code(400).send({\n error: 'invalid severity',\n details: sev.error.format(),\n })\n }\n violations = violations.filter((v) => v.severity === sev.data)\n }\n if (req.query.policyId) {\n violations = violations.filter((v) => v.policyId === req.query.policyId)\n }\n return violations\n })\n\n scope.post<{\n Params: { project?: string }\n Body: { hypotheticalAction?: unknown }\n }>('/policies/check', async (req, reply) => {\n const proj = resolveProject(registry, req, reply)\n if (!proj) return\n const parsed = PoliciesCheckBodySchema.safeParse(req.body ?? {})\n if (!parsed.success) {\n return reply.code(400).send({\n error: 'invalid /policies/check body',\n details: parsed.error.format(),\n })\n }\n\n const policyPath = ctx.policyFilePathFor(proj)\n let policies: Policy[] = []\n if (policyPath) {\n try {\n policies = await loadPolicyFile(policyPath)\n } catch (err) {\n return reply.code(400).send({\n error: 'policy.json failed to parse',\n details: (err as Error).message,\n })\n }\n }\n\n // No hypothetical → return current violations against the live graph.\n // With a hypothetical → simulate the action against a deep-copy graph\n // (avoids mutation authority concerns), evaluate, return the delta.\n const evalCtx = { now: () => Date.now() }\n if (!parsed.data.hypotheticalAction) {\n const violations = evaluateAllPolicies(proj.graph, policies, evalCtx)\n const blocking = violations.filter((v) => v.onViolation === 'block')\n return { allowed: blocking.length === 0, violations }\n }\n\n // For now the dry-run simulation re-uses evaluateAllPolicies on the\n // current graph. Full hypothetical simulation (e.g. \"what if I added\n // this OBSERVED edge?\") is the v0.2.4-δ scope; #117 ships the surface,\n // #118 fills in the action shapes' simulation logic.\n const violations = evaluateAllPolicies(proj.graph, policies, evalCtx)\n const blocking = violations.filter((v) => v.onViolation === 'block')\n return {\n allowed: blocking.length === 0,\n hypotheticalAction: parsed.data.hypotheticalAction,\n violations,\n } as { allowed: boolean; hypotheticalAction: unknown; violations: PolicyViolation[] }\n })\n}\n\nexport async function buildApi(opts: BuildApiOptions): Promise<FastifyInstance> {\n const app = Fastify({ logger: false })\n await app.register(cors, { origin: true })\n\n const startedAt = opts.startedAt ?? Date.now()\n const registry = buildLegacyRegistry(opts)\n\n const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== undefined\n const legacyStaleExplicit = !opts.projects && opts.staleEventsPath !== undefined\n\n const errorsPathFor = (proj: ProjectContext): string | undefined => {\n if (proj.name === DEFAULT_PROJECT && !opts.projects) {\n return legacyErrorsExplicit ? opts.errorsPath : undefined\n }\n return proj.paths.errorsPath\n }\n const staleEventsPathFor = (proj: ProjectContext): string | undefined => {\n if (proj.name === DEFAULT_PROJECT && !opts.projects) {\n return legacyStaleExplicit ? opts.staleEventsPath : undefined\n }\n return proj.paths.staleEventsPath\n }\n\n // policy.json lives at the project's scanPath root per ADR-042. Without a\n // scanPath we have nowhere to read it from — those projects show as\n // \"no policies configured\" via the empty-file response.\n const policyFilePathFor = (proj: ProjectContext): string | undefined => {\n if (!proj.scanPath) return undefined\n return `${proj.scanPath}/policy.json`\n }\n\n const routeCtx: RouteContext = {\n registry,\n startedAt,\n errorsPathFor,\n staleEventsPathFor,\n policyFilePathFor,\n }\n\n // Multi-project switcher (ADR-051 #4). Direct passthrough of the\n // machine-level registry from registry.ts (ADR-048) — distinct from the\n // dual-mount routing in ADR-026, which exposes per-project endpoints.\n // Returns Array<{ name, path, status, registeredAt, lastSeenAt?, languages }>.\n app.get('/projects', async (_req, reply) => {\n try {\n return await listRegistryProjects()\n } catch (err) {\n return reply.code(500).send({\n error: 'failed to read project registry',\n details: (err as Error).message,\n })\n }\n })\n\n // Default mount: /health, /graph, /incidents, etc. all hit project=default.\n registerRoutes(app, routeCtx)\n\n // Project-scoped mount: same handlers, URL params include `:project`.\n await app.register(\n async (scope) => {\n registerRoutes(scope, routeCtx)\n },\n { prefix: '/projects/:project' },\n )\n\n return app\n}\n","// SSE handler for the frontend-facing event stream (ADR-051 #1).\n// Subscribes to the bus in events.ts, filters by project, writes\n// `event: <type>\\ndata: <json>\\n\\n` frames to the client.\n//\n// Backpressure: per-connection queue cap of 1000 outstanding writes; once\n// hit, the connection is dropped with `event: error data: { reason:\n// 'backpressure' }` per ADR-051 #8. Heartbeat: comment line every 30s\n// keeps proxies from idle-timing out (ADR-051 #3).\n\nimport type { FastifyReply, FastifyRequest } from 'fastify'\nimport {\n EVENT_BUS_CHANNEL,\n eventBus,\n type NeatEventEnvelope,\n} from './events.js'\n\nexport const SSE_HEARTBEAT_MS = 30_000\nexport const SSE_BACKPRESSURE_CAP = 1000\n\nexport interface HandleSseOptions {\n project: string\n heartbeatMs?: number\n backpressureCap?: number\n}\n\nexport function handleSse(\n req: FastifyRequest,\n reply: FastifyReply,\n opts: HandleSseOptions,\n): void {\n const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS\n const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP\n\n reply.raw.setHeader('Content-Type', 'text/event-stream')\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform')\n reply.raw.setHeader('Connection', 'keep-alive')\n reply.raw.setHeader('X-Accel-Buffering', 'no')\n reply.raw.flushHeaders?.()\n\n let pending = 0\n let dropped = false\n\n const closeConnection = (): void => {\n if (dropped) return\n dropped = true\n eventBus.off(EVENT_BUS_CHANNEL, listener)\n clearInterval(heartbeat)\n if (!reply.raw.writableEnded) reply.raw.end()\n }\n\n const writeFrame = (frame: string): void => {\n if (dropped) return\n if (pending >= backpressureCap) {\n // Past the cap — emit one final error frame and drop. Don't try to\n // gracefully drain; a slow consumer that's already 1000 frames behind\n // is not going to catch up.\n const errFrame = `event: error\\ndata: ${JSON.stringify({ reason: 'backpressure' })}\\n\\n`\n reply.raw.write(errFrame)\n closeConnection()\n return\n }\n pending++\n reply.raw.write(frame, () => {\n pending = Math.max(0, pending - 1)\n })\n }\n\n const listener = (envelope: NeatEventEnvelope): void => {\n if (envelope.project !== opts.project) return\n writeFrame(`event: ${envelope.type}\\ndata: ${JSON.stringify(envelope.payload)}\\n\\n`)\n }\n\n eventBus.on(EVENT_BUS_CHANNEL, listener)\n\n const heartbeat = setInterval(() => {\n if (dropped) return\n reply.raw.write(':heartbeat\\n\\n')\n }, heartbeatMs)\n if (typeof heartbeat.unref === 'function') heartbeat.unref()\n\n req.raw.on('close', closeConnection)\n reply.raw.on('close', closeConnection)\n reply.raw.on('error', closeConnection)\n}\n"],"mappings":"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","names":["violations","blocking"]}
|
|
@@ -427,19 +427,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
427
427
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
428
428
|
let best = { path: [start], edges: [] };
|
|
429
429
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
430
|
-
function step(node,
|
|
431
|
-
if (
|
|
432
|
-
best = { path: [...
|
|
430
|
+
function step(node, path30, edges) {
|
|
431
|
+
if (path30.length > best.path.length) {
|
|
432
|
+
best = { path: [...path30], edges: [...edges] };
|
|
433
433
|
}
|
|
434
|
-
if (
|
|
434
|
+
if (path30.length - 1 >= maxDepth) return;
|
|
435
435
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
436
436
|
for (const [srcId, edge] of incoming) {
|
|
437
437
|
if (visited.has(srcId)) continue;
|
|
438
438
|
visited.add(srcId);
|
|
439
|
-
|
|
439
|
+
path30.push(srcId);
|
|
440
440
|
edges.push(edge);
|
|
441
|
-
step(srcId,
|
|
442
|
-
|
|
441
|
+
step(srcId, path30, edges);
|
|
442
|
+
path30.pop();
|
|
443
443
|
edges.pop();
|
|
444
444
|
visited.delete(srcId);
|
|
445
445
|
}
|
|
@@ -634,6 +634,69 @@ import {
|
|
|
634
634
|
PolicyFileSchema,
|
|
635
635
|
Provenance as Provenance2
|
|
636
636
|
} from "@neat.is/types";
|
|
637
|
+
|
|
638
|
+
// src/events.ts
|
|
639
|
+
import { EventEmitter } from "events";
|
|
640
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
641
|
+
var NeatEventBus = class extends EventEmitter {
|
|
642
|
+
};
|
|
643
|
+
var eventBus = new NeatEventBus();
|
|
644
|
+
eventBus.setMaxListeners(0);
|
|
645
|
+
function emitNeatEvent(envelope) {
|
|
646
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
647
|
+
}
|
|
648
|
+
function attachGraphToEventBus(graph, opts) {
|
|
649
|
+
const { project } = opts;
|
|
650
|
+
const onNodeAdded = (payload) => {
|
|
651
|
+
emitNeatEvent({
|
|
652
|
+
type: "node-added",
|
|
653
|
+
project,
|
|
654
|
+
payload: { node: payload.attributes }
|
|
655
|
+
});
|
|
656
|
+
};
|
|
657
|
+
const onNodeDropped = (payload) => {
|
|
658
|
+
emitNeatEvent({
|
|
659
|
+
type: "node-removed",
|
|
660
|
+
project,
|
|
661
|
+
payload: { id: payload.key }
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
const onEdgeAdded = (payload) => {
|
|
665
|
+
emitNeatEvent({
|
|
666
|
+
type: "edge-added",
|
|
667
|
+
project,
|
|
668
|
+
payload: { edge: payload.attributes }
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
const onEdgeDropped = (payload) => {
|
|
672
|
+
emitNeatEvent({
|
|
673
|
+
type: "edge-removed",
|
|
674
|
+
project,
|
|
675
|
+
payload: { id: payload.key }
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
const onNodeAttrsUpdated = (payload) => {
|
|
679
|
+
emitNeatEvent({
|
|
680
|
+
type: "node-updated",
|
|
681
|
+
project,
|
|
682
|
+
payload: { id: payload.key, changes: payload.attributes }
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
graph.on("nodeAdded", onNodeAdded);
|
|
686
|
+
graph.on("nodeDropped", onNodeDropped);
|
|
687
|
+
graph.on("edgeAdded", onEdgeAdded);
|
|
688
|
+
graph.on("edgeDropped", onEdgeDropped);
|
|
689
|
+
graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
690
|
+
return () => {
|
|
691
|
+
graph.off("nodeAdded", onNodeAdded);
|
|
692
|
+
graph.off("nodeDropped", onNodeDropped);
|
|
693
|
+
graph.off("edgeAdded", onEdgeAdded);
|
|
694
|
+
graph.off("edgeDropped", onEdgeDropped);
|
|
695
|
+
graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/policy.ts
|
|
637
700
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
638
701
|
info: "log",
|
|
639
702
|
warning: "alert",
|
|
@@ -915,9 +978,11 @@ async function loadPolicyFile(policyPath) {
|
|
|
915
978
|
}
|
|
916
979
|
var PolicyViolationsLog = class {
|
|
917
980
|
path;
|
|
981
|
+
project;
|
|
918
982
|
seen = null;
|
|
919
|
-
constructor(logPath) {
|
|
983
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
920
984
|
this.path = logPath;
|
|
985
|
+
this.project = project;
|
|
921
986
|
}
|
|
922
987
|
async append(v) {
|
|
923
988
|
if (!this.seen) await this.hydrate();
|
|
@@ -925,6 +990,11 @@ var PolicyViolationsLog = class {
|
|
|
925
990
|
this.seen.add(v.id);
|
|
926
991
|
await fs2.mkdir(path2.dirname(this.path), { recursive: true });
|
|
927
992
|
await fs2.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
993
|
+
emitNeatEvent({
|
|
994
|
+
type: "policy-violation",
|
|
995
|
+
project: this.project,
|
|
996
|
+
payload: { violation: v }
|
|
997
|
+
});
|
|
928
998
|
return true;
|
|
929
999
|
}
|
|
930
1000
|
async readAll() {
|
|
@@ -1405,6 +1475,7 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1405
1475
|
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1406
1476
|
const now = options.now ?? Date.now();
|
|
1407
1477
|
const events = [];
|
|
1478
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1408
1479
|
graph.forEachEdge((id, attrs) => {
|
|
1409
1480
|
const e = attrs;
|
|
1410
1481
|
if (e.provenance !== Provenance3.OBSERVED) return;
|
|
@@ -1424,6 +1495,15 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1424
1495
|
lastObserved: e.lastObserved,
|
|
1425
1496
|
transitionedAt: new Date(now).toISOString()
|
|
1426
1497
|
});
|
|
1498
|
+
emitNeatEvent({
|
|
1499
|
+
type: "stale-transition",
|
|
1500
|
+
project,
|
|
1501
|
+
payload: {
|
|
1502
|
+
edgeId: id,
|
|
1503
|
+
from: Provenance3.OBSERVED,
|
|
1504
|
+
to: Provenance3.STALE
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1427
1507
|
}
|
|
1428
1508
|
});
|
|
1429
1509
|
if (options.staleEventsPath && events.length > 0) {
|
|
@@ -1454,7 +1534,8 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1454
1534
|
try {
|
|
1455
1535
|
await markStaleEdges(graph, {
|
|
1456
1536
|
thresholds: options.thresholds,
|
|
1457
|
-
staleEventsPath: options.staleEventsPath
|
|
1537
|
+
staleEventsPath: options.staleEventsPath,
|
|
1538
|
+
project: options.project
|
|
1458
1539
|
});
|
|
1459
1540
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1460
1541
|
} catch (err) {
|
|
@@ -2665,7 +2746,15 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2665
2746
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
2666
2747
|
for (const file of files) {
|
|
2667
2748
|
const parser = path18.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
2668
|
-
|
|
2749
|
+
let targets;
|
|
2750
|
+
try {
|
|
2751
|
+
targets = callsFromSource(file.content, parser, knownHosts);
|
|
2752
|
+
} catch (err) {
|
|
2753
|
+
console.warn(
|
|
2754
|
+
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
2755
|
+
);
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2669
2758
|
for (const t of targets) {
|
|
2670
2759
|
const targetId = hostToNodeId.get(t);
|
|
2671
2760
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -3182,11 +3271,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3182
3271
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3183
3272
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3184
3273
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3185
|
-
|
|
3274
|
+
const result = {
|
|
3186
3275
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3187
3276
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3188
3277
|
frontiersPromoted
|
|
3189
3278
|
};
|
|
3279
|
+
emitNeatEvent({
|
|
3280
|
+
type: "extraction-complete",
|
|
3281
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3282
|
+
payload: {
|
|
3283
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3284
|
+
fileCount: services.length,
|
|
3285
|
+
nodesAdded: result.nodesAdded,
|
|
3286
|
+
edgesAdded: result.edgesAdded
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
return result;
|
|
3190
3290
|
}
|
|
3191
3291
|
|
|
3192
3292
|
// src/persist.ts
|
|
@@ -3327,6 +3427,175 @@ function parseExtraProjects(raw) {
|
|
|
3327
3427
|
return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0 && p !== DEFAULT_PROJECT);
|
|
3328
3428
|
}
|
|
3329
3429
|
|
|
3430
|
+
// src/registry.ts
|
|
3431
|
+
import { promises as fs16 } from "fs";
|
|
3432
|
+
import os2 from "os";
|
|
3433
|
+
import path29 from "path";
|
|
3434
|
+
import {
|
|
3435
|
+
RegistryFileSchema
|
|
3436
|
+
} from "@neat.is/types";
|
|
3437
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
3438
|
+
var LOCK_RETRY_MS = 50;
|
|
3439
|
+
function neatHome() {
|
|
3440
|
+
const override = process.env.NEAT_HOME;
|
|
3441
|
+
if (override && override.length > 0) return path29.resolve(override);
|
|
3442
|
+
return path29.join(os2.homedir(), ".neat");
|
|
3443
|
+
}
|
|
3444
|
+
function registryPath() {
|
|
3445
|
+
return path29.join(neatHome(), "projects.json");
|
|
3446
|
+
}
|
|
3447
|
+
function registryLockPath() {
|
|
3448
|
+
return path29.join(neatHome(), "projects.json.lock");
|
|
3449
|
+
}
|
|
3450
|
+
async function normalizeProjectPath(input) {
|
|
3451
|
+
const resolved = path29.resolve(input);
|
|
3452
|
+
try {
|
|
3453
|
+
return await fs16.realpath(resolved);
|
|
3454
|
+
} catch {
|
|
3455
|
+
return resolved;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
async function writeAtomically(target, contents) {
|
|
3459
|
+
await fs16.mkdir(path29.dirname(target), { recursive: true });
|
|
3460
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
3461
|
+
const fd = await fs16.open(tmp, "w");
|
|
3462
|
+
try {
|
|
3463
|
+
await fd.writeFile(contents, "utf8");
|
|
3464
|
+
await fd.sync();
|
|
3465
|
+
} finally {
|
|
3466
|
+
await fd.close();
|
|
3467
|
+
}
|
|
3468
|
+
await fs16.rename(tmp, target);
|
|
3469
|
+
}
|
|
3470
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
3471
|
+
const deadline = Date.now() + timeoutMs;
|
|
3472
|
+
await fs16.mkdir(path29.dirname(lockPath), { recursive: true });
|
|
3473
|
+
while (true) {
|
|
3474
|
+
try {
|
|
3475
|
+
const fd = await fs16.open(lockPath, "wx");
|
|
3476
|
+
await fd.close();
|
|
3477
|
+
return;
|
|
3478
|
+
} catch (err) {
|
|
3479
|
+
const code = err.code;
|
|
3480
|
+
if (code !== "EEXIST") throw err;
|
|
3481
|
+
if (Date.now() >= deadline) {
|
|
3482
|
+
throw new Error(
|
|
3483
|
+
`neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
async function releaseLock(lockPath) {
|
|
3491
|
+
await fs16.unlink(lockPath).catch(() => {
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
async function withLock(fn) {
|
|
3495
|
+
const lock = registryLockPath();
|
|
3496
|
+
await acquireLock(lock);
|
|
3497
|
+
try {
|
|
3498
|
+
return await fn();
|
|
3499
|
+
} finally {
|
|
3500
|
+
await releaseLock(lock);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
async function readRegistry() {
|
|
3504
|
+
const file = registryPath();
|
|
3505
|
+
let raw;
|
|
3506
|
+
try {
|
|
3507
|
+
raw = await fs16.readFile(file, "utf8");
|
|
3508
|
+
} catch (err) {
|
|
3509
|
+
if (err.code === "ENOENT") {
|
|
3510
|
+
return { version: 1, projects: [] };
|
|
3511
|
+
}
|
|
3512
|
+
throw err;
|
|
3513
|
+
}
|
|
3514
|
+
const parsed = JSON.parse(raw);
|
|
3515
|
+
return RegistryFileSchema.parse(parsed);
|
|
3516
|
+
}
|
|
3517
|
+
async function writeRegistry(reg) {
|
|
3518
|
+
const validated = RegistryFileSchema.parse(reg);
|
|
3519
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
3520
|
+
}
|
|
3521
|
+
var ProjectNameCollisionError = class extends Error {
|
|
3522
|
+
projectName;
|
|
3523
|
+
constructor(name) {
|
|
3524
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
3525
|
+
this.name = "ProjectNameCollisionError";
|
|
3526
|
+
this.projectName = name;
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
async function addProject(opts) {
|
|
3530
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
3531
|
+
return withLock(async () => {
|
|
3532
|
+
const reg = await readRegistry();
|
|
3533
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
3534
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
3535
|
+
if (byName && byName.path !== resolvedPath) {
|
|
3536
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
3537
|
+
}
|
|
3538
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3539
|
+
if (byName && byName.path === resolvedPath) {
|
|
3540
|
+
byName.lastSeenAt = now;
|
|
3541
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
3542
|
+
if (opts.status) byName.status = opts.status;
|
|
3543
|
+
await writeRegistry(reg);
|
|
3544
|
+
return byName;
|
|
3545
|
+
}
|
|
3546
|
+
if (byPath && byPath.name !== opts.name) {
|
|
3547
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
3548
|
+
}
|
|
3549
|
+
const entry = {
|
|
3550
|
+
name: opts.name,
|
|
3551
|
+
path: resolvedPath,
|
|
3552
|
+
registeredAt: now,
|
|
3553
|
+
languages: opts.languages ?? [],
|
|
3554
|
+
status: opts.status ?? "active"
|
|
3555
|
+
};
|
|
3556
|
+
reg.projects.push(entry);
|
|
3557
|
+
await writeRegistry(reg);
|
|
3558
|
+
return entry;
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
async function getProject(name) {
|
|
3562
|
+
const reg = await readRegistry();
|
|
3563
|
+
return reg.projects.find((p) => p.name === name);
|
|
3564
|
+
}
|
|
3565
|
+
async function listProjects() {
|
|
3566
|
+
const reg = await readRegistry();
|
|
3567
|
+
return reg.projects;
|
|
3568
|
+
}
|
|
3569
|
+
async function setStatus(name, status) {
|
|
3570
|
+
return withLock(async () => {
|
|
3571
|
+
const reg = await readRegistry();
|
|
3572
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
3573
|
+
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
3574
|
+
entry.status = status;
|
|
3575
|
+
await writeRegistry(reg);
|
|
3576
|
+
return entry;
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3580
|
+
await withLock(async () => {
|
|
3581
|
+
const reg = await readRegistry();
|
|
3582
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
3583
|
+
if (!entry) return;
|
|
3584
|
+
entry.lastSeenAt = at;
|
|
3585
|
+
await writeRegistry(reg);
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
async function removeProject(name) {
|
|
3589
|
+
return withLock(async () => {
|
|
3590
|
+
const reg = await readRegistry();
|
|
3591
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
3592
|
+
if (idx < 0) return void 0;
|
|
3593
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
3594
|
+
await writeRegistry(reg);
|
|
3595
|
+
return removed;
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3330
3599
|
export {
|
|
3331
3600
|
DEFAULT_PROJECT,
|
|
3332
3601
|
getGraph,
|
|
@@ -3334,6 +3603,10 @@ export {
|
|
|
3334
3603
|
checkCompatibility,
|
|
3335
3604
|
ensureCompatLoaded,
|
|
3336
3605
|
compatPairs,
|
|
3606
|
+
EVENT_BUS_CHANNEL,
|
|
3607
|
+
eventBus,
|
|
3608
|
+
emitNeatEvent,
|
|
3609
|
+
attachGraphToEventBus,
|
|
3337
3610
|
confidenceForEdge,
|
|
3338
3611
|
getRootCause,
|
|
3339
3612
|
getBlastRadius,
|
|
@@ -3366,6 +3639,18 @@ export {
|
|
|
3366
3639
|
startPersistLoop,
|
|
3367
3640
|
pathsForProject,
|
|
3368
3641
|
Projects,
|
|
3369
|
-
parseExtraProjects
|
|
3642
|
+
parseExtraProjects,
|
|
3643
|
+
registryPath,
|
|
3644
|
+
registryLockPath,
|
|
3645
|
+
normalizeProjectPath,
|
|
3646
|
+
writeAtomically,
|
|
3647
|
+
readRegistry,
|
|
3648
|
+
ProjectNameCollisionError,
|
|
3649
|
+
addProject,
|
|
3650
|
+
getProject,
|
|
3651
|
+
listProjects,
|
|
3652
|
+
setStatus,
|
|
3653
|
+
touchLastSeen,
|
|
3654
|
+
removeProject
|
|
3370
3655
|
};
|
|
3371
|
-
//# sourceMappingURL=chunk-
|
|
3656
|
+
//# sourceMappingURL=chunk-GAYTAGEH.js.map
|