@neat.is/core 0.3.4 → 0.3.6
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-T3A2GK3X.js → chunk-YHQYHFI3.js} +2 -2
- package/dist/{chunk-33ZZ2ZID.js → chunk-ZU2RQRCN.js} +94 -83
- package/dist/chunk-ZU2RQRCN.js.map +1 -0
- package/dist/cli.cjs +433 -139
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +339 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +84 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +165 -70
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +82 -2
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +57 -45
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-33ZZ2ZID.js.map +0 -1
- /package/dist/{chunk-T3A2GK3X.js.map → chunk-YHQYHFI3.js.map} +0 -0
package/dist/neatd.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-YHQYHFI3.js";
|
|
5
5
|
import {
|
|
6
6
|
listProjects,
|
|
7
7
|
registryPath
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ZU2RQRCN.js";
|
|
9
9
|
import {
|
|
10
10
|
__require
|
|
11
11
|
} from "./chunk-4ASCXBZF.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
// src/neatd.ts
|
|
14
14
|
import { promises as fs } from "fs";
|
|
15
15
|
import path2 from "path";
|
|
16
|
+
import { createRequire } from "module";
|
|
16
17
|
|
|
17
18
|
// src/web-spawn.ts
|
|
18
19
|
import { spawn } from "child_process";
|
|
@@ -107,7 +108,82 @@ async function spawnWebUI(restPort) {
|
|
|
107
108
|
return { child, port, stop };
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// src/version-skew.ts
|
|
112
|
+
import { setTimeout as delay } from "timers/promises";
|
|
113
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org/neat.is/latest";
|
|
114
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
115
|
+
async function fetchRegistryVersion(url, timeoutMs, fetchImpl) {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
let timedOut = false;
|
|
118
|
+
const timer = delay(timeoutMs).then(() => {
|
|
119
|
+
timedOut = true;
|
|
120
|
+
controller.abort();
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const res = await Promise.race([
|
|
124
|
+
fetchImpl(url, { signal: controller.signal, headers: { accept: "application/json" } }),
|
|
125
|
+
timer.then(() => null)
|
|
126
|
+
]);
|
|
127
|
+
if (timedOut || !res) return null;
|
|
128
|
+
if (!res.ok) return null;
|
|
129
|
+
const body = await res.json();
|
|
130
|
+
if (typeof body.version !== "string" || body.version.length === 0) return null;
|
|
131
|
+
return body.version;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
} finally {
|
|
135
|
+
if (!timedOut) controller.abort();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function isLocalBehind(local, remote) {
|
|
139
|
+
if (local === remote) return false;
|
|
140
|
+
const parse = (v) => {
|
|
141
|
+
const core = v.split(/[-+]/)[0] ?? v;
|
|
142
|
+
return core.split(".").map((s) => {
|
|
143
|
+
const n = Number.parseInt(s, 10);
|
|
144
|
+
return Number.isFinite(n) ? n : 0;
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const a = parse(local);
|
|
148
|
+
const b = parse(remote);
|
|
149
|
+
const len = Math.max(a.length, b.length);
|
|
150
|
+
for (let i = 0; i < len; i++) {
|
|
151
|
+
const av = a[i] ?? 0;
|
|
152
|
+
const bv = b[i] ?? 0;
|
|
153
|
+
if (av < bv) return true;
|
|
154
|
+
if (av > bv) return false;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
async function checkVersionSkew(opts) {
|
|
159
|
+
const url = opts.registryUrl ?? NPM_REGISTRY_URL;
|
|
160
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
161
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
162
|
+
const warn = opts.warn ?? ((m) => console.warn(m));
|
|
163
|
+
const remote = await fetchRegistryVersion(url, timeoutMs, fetchImpl);
|
|
164
|
+
if (!remote) return { remoteVersion: null, skewed: false, warned: false };
|
|
165
|
+
if (!isLocalBehind(opts.localVersion, remote)) {
|
|
166
|
+
return { remoteVersion: remote, skewed: false, warned: false };
|
|
167
|
+
}
|
|
168
|
+
warn(
|
|
169
|
+
`[neatd] running neat.is@${opts.localVersion} but neat.is@${remote} is on npm \u2014 run \`npm install -g neat.is@latest\` to update`
|
|
170
|
+
);
|
|
171
|
+
return { remoteVersion: remote, skewed: true, warned: true };
|
|
172
|
+
}
|
|
173
|
+
|
|
110
174
|
// src/neatd.ts
|
|
175
|
+
function localVersion() {
|
|
176
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
177
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const req2 = createRequire(import.meta.url);
|
|
181
|
+
const pkg = req2("../package.json");
|
|
182
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
183
|
+
} catch {
|
|
184
|
+
return "0.0.0";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
111
187
|
function neatHome() {
|
|
112
188
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
113
189
|
return path2.resolve(process.env.NEAT_HOME);
|
|
@@ -137,6 +213,10 @@ async function cmdStart() {
|
|
|
137
213
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
138
214
|
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|
|
139
215
|
if (handle.otlpAddress) console.log(`neatd: OTLP \u2192 ${handle.otlpAddress}`);
|
|
216
|
+
if (process.env.NEAT_DISABLE_VERSION_CHECK !== "1") {
|
|
217
|
+
void checkVersionSkew({ localVersion: localVersion() }).catch(() => {
|
|
218
|
+
});
|
|
219
|
+
}
|
|
140
220
|
const skipWeb = process.env.NEAT_WEB_DISABLED === "1";
|
|
141
221
|
let web = null;
|
|
142
222
|
if (!skipWeb) {
|
package/dist/neatd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/neatd.ts","../src/web-spawn.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `neatd` — distribution-layer daemon CLI (ADR-049).\n *\n * Subcommands:\n * neatd start [--foreground] boot the daemon and watch the registry\n * neatd stop signal the running daemon to shut down\n * neatd reload signal the running daemon to re-read the registry\n * neatd status print PID + per-project last-seen timestamps\n *\n * MVP runs in foreground only. Backgrounding is the supervisor's job\n * (launchd / systemd / nohup) — `neatd start` blocks until SIGINT/SIGTERM.\n *\n * v0.2.10: also brings up the web UI on port 6328 by default (ADR-059).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { startDaemon } from './daemon.js'\nimport { listProjects, registryPath } from './registry.js'\nimport { spawnWebUI, DEFAULT_WEB_PORT, type WebHandle } from './web-spawn.js'\n\nfunction neatHome(): string {\n if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {\n return path.resolve(process.env.NEAT_HOME)\n }\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\nasync function readPid(): Promise<number | null> {\n try {\n const raw = await fs.readFile(path.join(neatHome(), 'neatd.pid'), 'utf8')\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) ? n : null\n } catch {\n return null\n }\n}\n\nfunction usage(): void {\n console.log('usage: neatd <start|stop|reload|status> [--foreground]')\n}\n\nfunction restPortFromEnv(): number {\n const raw = process.env.PORT\n return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080\n}\n\nasync function cmdStart(): Promise<void> {\n const handle = await startDaemon()\n console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`)\n console.log(`neatd: registry at ${registryPath()}`)\n // ADR-063 — surface the bound addresses so the operator can sanity-check\n // the documented happy path without grepping startup logs.\n if (handle.restAddress) console.log(`neatd: REST → ${handle.restAddress}`)\n if (handle.otlpAddress) console.log(`neatd: OTLP → ${handle.otlpAddress}`)\n\n // ADR-059 — bring up the web UI alongside the daemon. Failure here aborts\n // start with the clear-error pattern from ADR-049 instead of silently\n // running headless.\n const skipWeb = process.env.NEAT_WEB_DISABLED === '1'\n let web: WebHandle | null = null\n if (!skipWeb) {\n try {\n web = await spawnWebUI(restPortFromEnv())\n } catch (err) {\n console.error((err as Error).message)\n await handle.stop().catch(() => {})\n process.exit(3)\n }\n } else {\n console.log('neatd: web UI disabled (NEAT_WEB_DISABLED=1)')\n }\n\n console.log('neatd: SIGHUP reloads, SIGTERM/SIGINT stops')\n\n let stopping = false\n const shutdown = (signal: NodeJS.Signals): void => {\n if (stopping) return\n stopping = true\n console.log(`neatd: ${signal} received, stopping…`)\n void Promise.allSettled([handle.stop(), web ? web.stop() : Promise.resolve()])\n .catch((err) => console.error(`neatd: shutdown error — ${(err as Error).message}`))\n .finally(() => process.exit(0))\n }\n process.on('SIGTERM', shutdown)\n process.on('SIGINT', shutdown)\n\n // Block forever — supervisors keep us in the foreground.\n await new Promise<void>(() => {})\n}\n\nasync function cmdStop(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGTERM')\n console.log(`neatd: SIGTERM sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdReload(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGHUP')\n console.log(`neatd: SIGHUP sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdStatus(): Promise<void> {\n const pid = await readPid()\n console.log(`pid: ${pid ?? '(not running)'}`)\n console.log(`registry: ${registryPath()}`)\n const webPort = process.env.NEAT_WEB_PORT\n ? Number.parseInt(process.env.NEAT_WEB_PORT, 10)\n : DEFAULT_WEB_PORT\n console.log(`web ui: http://localhost:${webPort}`)\n const projects = await listProjects().catch(() => [])\n if (projects.length === 0) {\n console.log('projects: (none)')\n return\n }\n console.log('projects:')\n for (const p of projects) {\n const seen = p.lastSeenAt ?? 'never'\n console.log(` ${p.name}\\t${p.status}\\t${p.path}\\tlast-seen=${seen}`)\n }\n}\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2]\n if (!cmd || cmd === '-h' || cmd === '--help') {\n usage()\n process.exit(cmd ? 0 : 2)\n }\n\n if (cmd === 'start') return cmdStart()\n if (cmd === 'stop') return cmdStop()\n if (cmd === 'reload') return cmdReload()\n if (cmd === 'status') return cmdStatus()\n\n console.error(`neatd: unknown command \"${cmd}\"`)\n usage()\n process.exit(1)\n}\n\nconst entry = process.argv[1] ?? ''\nif (/[\\\\/]neatd\\.(?:cjs|js)$/.test(entry) || entry.endsWith('/neatd')) {\n main().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n","/**\n * Web UI spawn helper (ADR-059).\n *\n * `neatd start` brings up the REST API + OTel receivers, then calls\n * `spawnWebUI(restPort)` to launch the Next.js web shell as a child process.\n * Lifecycle is parent-tied: SIGTERM/SIGINT on neatd cascades to the child\n * via `stop()`.\n *\n * Default port `6328` (NEAT in T9). Override with `NEAT_WEB_PORT`.\n * Hard-fails on collision so the operator never loses track of which URL to\n * open.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport net from 'node:net'\nimport path from 'node:path'\n\nexport const DEFAULT_WEB_PORT = 6328\n\nexport interface WebHandle {\n child: ChildProcess\n port: number\n stop: () => Promise<void>\n}\n\n/**\n * Best-effort port collision check before spawning. Binds, closes, returns.\n * Race condition between the check and the actual `next start` is acceptable\n * — Next.js will then fail loudly and the parent exits.\n */\nasync function assertPortFree(port: number): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const tester = net.createServer()\n tester.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(\n new Error(\n `neatd: web UI port ${port} in use; set NEAT_WEB_PORT to override or stop the conflicting process`,\n ),\n )\n } else {\n reject(err)\n }\n })\n tester.once('listening', () => {\n tester.close(() => resolve())\n })\n tester.listen(port, '127.0.0.1')\n })\n}\n\n/**\n * Resolve the web package directory. In a global install\n * (`npm install -g neat.is`) the package lives at\n * `node_modules/@neat.is/web/`; in the monorepo it's\n * `packages/web/`. `require.resolve` finds whichever one Node has on its\n * search path.\n */\nfunction resolveWebPackageDir(): string {\n const req = (typeof require !== 'undefined'\n ? require\n : // ESM fallback — daemon CJS bundle has `require`, but typecheck wants this\n (eval('require') as NodeRequire))\n const pkgJsonPath = req.resolve('@neat.is/web/package.json')\n return path.dirname(pkgJsonPath)\n}\n\n/**\n * Locate the standalone server.js inside the web package. ADR-064 #2 — the\n * tarball ships `.next/standalone/packages/web/server.js` (path preserved\n * relative to the monorepo tracing root). Missing means the package was\n * published without `next build` having run, which the smoke-test gate\n * catches at publish time.\n */\nfunction resolveStandaloneServerEntry(webDir: string): string {\n return path.join(webDir, '.next/standalone/packages/web/server.js')\n}\n\nexport async function spawnWebUI(restPort: number): Promise<WebHandle> {\n const portRaw = process.env.NEAT_WEB_PORT\n const port = portRaw && portRaw.length > 0 ? Number.parseInt(portRaw, 10) : DEFAULT_WEB_PORT\n if (!Number.isFinite(port) || port <= 0 || port > 65535) {\n throw new Error(`neatd: invalid NEAT_WEB_PORT=\"${portRaw}\"`)\n }\n\n await assertPortFree(port)\n\n const cwd = resolveWebPackageDir()\n const serverEntry = resolveStandaloneServerEntry(cwd)\n // ADR-064 — fail loudly if the standalone build is missing. v0.3.0 shipped\n // without it; the symptom on the user side was `next start` aborting with\n // `Could not find a production build in the '.next' directory`. This check\n // catches it at the parent's bootstrap instead of letting the child crash\n // a few moments later with a worse error.\n try {\n require.resolve(serverEntry)\n } catch {\n throw new Error(\n `neatd: web UI standalone build missing at ${serverEntry}. ` +\n `The published @neat.is/web tarball should include it; if you're running from a ` +\n `monorepo checkout, run \\`npm run build --workspace @neat.is/web\\` first, or set ` +\n `NEAT_WEB_DISABLED=1 to skip the web UI.`,\n )\n }\n\n // ADR-059 #6 — child inherits NEAT_API_URL pointing at our REST server,\n // unless the operator pre-configured it.\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n PORT: String(port),\n HOSTNAME: process.env.HOSTNAME ?? '0.0.0.0',\n NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`,\n }\n\n // The standalone bundle is self-contained — its own `node_modules` and\n // `package.json` sit alongside `server.js`. Spawn `node` against it\n // directly; no `next start` (which needs the source tree + build cache)\n // and no `npm exec` (which is monorepo-only in practice).\n // `detached: false` keeps the child in our process group so signals reach it.\n const child = spawn(process.execPath, [serverEntry], {\n cwd: path.dirname(serverEntry),\n env,\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n })\n\n child.on('error', (err) => {\n console.error(`neatd: web UI spawn error — ${err.message}`)\n })\n\n console.log(`neatd: web UI listening on http://localhost:${port}`)\n\n let stopped = false\n async function stop(): Promise<void> {\n if (stopped || !child.pid) return\n stopped = true\n try {\n child.kill('SIGTERM')\n } catch {\n /* already gone */\n }\n // Give the child up to 3s to exit gracefully, then SIGKILL.\n await new Promise<void>((resolve) => {\n const t = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n } catch {\n /* gone */\n }\n resolve()\n }, 3000)\n child.once('exit', () => {\n clearTimeout(t)\n resolve()\n })\n })\n }\n\n return { child, port, stop }\n}\n"],"mappings":"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","names":["path","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/neatd.ts","../src/web-spawn.ts","../src/version-skew.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `neatd` — distribution-layer daemon CLI (ADR-049).\n *\n * Subcommands:\n * neatd start [--foreground] boot the daemon and watch the registry\n * neatd stop signal the running daemon to shut down\n * neatd reload signal the running daemon to re-read the registry\n * neatd status print PID + per-project last-seen timestamps\n *\n * MVP runs in foreground only. Backgrounding is the supervisor's job\n * (launchd / systemd / nohup) — `neatd start` blocks until SIGINT/SIGTERM.\n *\n * v0.2.10: also brings up the web UI on port 6328 by default (ADR-059).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport { startDaemon } from './daemon.js'\nimport { listProjects, registryPath } from './registry.js'\nimport { spawnWebUI, DEFAULT_WEB_PORT, type WebHandle } from './web-spawn.js'\nimport { checkVersionSkew } from './version-skew.js'\n\n// Resolve the running @neat.is/core version from the bundled package.json.\n// Lockstep publishing (ADR-052) keeps this in step with the `neat.is` meta\n// package, which is what the operator installs globally. Tests stub by\n// setting NEAT_LOCAL_VERSION.\nfunction localVersion(): string {\n if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {\n return process.env.NEAT_LOCAL_VERSION\n }\n try {\n const req = createRequire(import.meta.url)\n const pkg = req('../package.json') as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : '0.0.0'\n } catch {\n return '0.0.0'\n }\n}\n\nfunction neatHome(): string {\n if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {\n return path.resolve(process.env.NEAT_HOME)\n }\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\nasync function readPid(): Promise<number | null> {\n try {\n const raw = await fs.readFile(path.join(neatHome(), 'neatd.pid'), 'utf8')\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) ? n : null\n } catch {\n return null\n }\n}\n\nfunction usage(): void {\n console.log('usage: neatd <start|stop|reload|status> [--foreground]')\n}\n\nfunction restPortFromEnv(): number {\n const raw = process.env.PORT\n return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080\n}\n\nasync function cmdStart(): Promise<void> {\n const handle = await startDaemon()\n console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`)\n console.log(`neatd: registry at ${registryPath()}`)\n // ADR-063 — surface the bound addresses so the operator can sanity-check\n // the documented happy path without grepping startup logs.\n if (handle.restAddress) console.log(`neatd: REST → ${handle.restAddress}`)\n if (handle.otlpAddress) console.log(`neatd: OTLP → ${handle.otlpAddress}`)\n\n // Version-skew advisory — non-fatal, fail-open. Surfaces the gap when the\n // operator's globally installed `neat.is` binary lags the published\n // version. Set NEAT_DISABLE_VERSION_CHECK=1 to silence the check (e.g. in\n // air-gapped environments).\n if (process.env.NEAT_DISABLE_VERSION_CHECK !== '1') {\n void checkVersionSkew({ localVersion: localVersion() }).catch(() => {\n // Fail-open: any thrown error from the helper resolves silently. The\n // helper already catches its own internals; this catches anything\n // exotic the Promise chain bubbles up.\n })\n }\n\n // ADR-059 — bring up the web UI alongside the daemon. Failure here aborts\n // start with the clear-error pattern from ADR-049 instead of silently\n // running headless.\n const skipWeb = process.env.NEAT_WEB_DISABLED === '1'\n let web: WebHandle | null = null\n if (!skipWeb) {\n try {\n web = await spawnWebUI(restPortFromEnv())\n } catch (err) {\n console.error((err as Error).message)\n await handle.stop().catch(() => {})\n process.exit(3)\n }\n } else {\n console.log('neatd: web UI disabled (NEAT_WEB_DISABLED=1)')\n }\n\n console.log('neatd: SIGHUP reloads, SIGTERM/SIGINT stops')\n\n let stopping = false\n const shutdown = (signal: NodeJS.Signals): void => {\n if (stopping) return\n stopping = true\n console.log(`neatd: ${signal} received, stopping…`)\n void Promise.allSettled([handle.stop(), web ? web.stop() : Promise.resolve()])\n .catch((err) => console.error(`neatd: shutdown error — ${(err as Error).message}`))\n .finally(() => process.exit(0))\n }\n process.on('SIGTERM', shutdown)\n process.on('SIGINT', shutdown)\n\n // Block forever — supervisors keep us in the foreground.\n await new Promise<void>(() => {})\n}\n\nasync function cmdStop(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGTERM')\n console.log(`neatd: SIGTERM sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdReload(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGHUP')\n console.log(`neatd: SIGHUP sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdStatus(): Promise<void> {\n const pid = await readPid()\n console.log(`pid: ${pid ?? '(not running)'}`)\n console.log(`registry: ${registryPath()}`)\n const webPort = process.env.NEAT_WEB_PORT\n ? Number.parseInt(process.env.NEAT_WEB_PORT, 10)\n : DEFAULT_WEB_PORT\n console.log(`web ui: http://localhost:${webPort}`)\n const projects = await listProjects().catch(() => [])\n if (projects.length === 0) {\n console.log('projects: (none)')\n return\n }\n console.log('projects:')\n for (const p of projects) {\n const seen = p.lastSeenAt ?? 'never'\n console.log(` ${p.name}\\t${p.status}\\t${p.path}\\tlast-seen=${seen}`)\n }\n}\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2]\n if (!cmd || cmd === '-h' || cmd === '--help') {\n usage()\n process.exit(cmd ? 0 : 2)\n }\n\n if (cmd === 'start') return cmdStart()\n if (cmd === 'stop') return cmdStop()\n if (cmd === 'reload') return cmdReload()\n if (cmd === 'status') return cmdStatus()\n\n console.error(`neatd: unknown command \"${cmd}\"`)\n usage()\n process.exit(1)\n}\n\nconst entry = process.argv[1] ?? ''\nif (/[\\\\/]neatd\\.(?:cjs|js)$/.test(entry) || entry.endsWith('/neatd')) {\n main().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n","/**\n * Web UI spawn helper (ADR-059).\n *\n * `neatd start` brings up the REST API + OTel receivers, then calls\n * `spawnWebUI(restPort)` to launch the Next.js web shell as a child process.\n * Lifecycle is parent-tied: SIGTERM/SIGINT on neatd cascades to the child\n * via `stop()`.\n *\n * Default port `6328` (NEAT in T9). Override with `NEAT_WEB_PORT`.\n * Hard-fails on collision so the operator never loses track of which URL to\n * open.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport net from 'node:net'\nimport path from 'node:path'\n\nexport const DEFAULT_WEB_PORT = 6328\n\nexport interface WebHandle {\n child: ChildProcess\n port: number\n stop: () => Promise<void>\n}\n\n/**\n * Best-effort port collision check before spawning. Binds, closes, returns.\n * Race condition between the check and the actual `next start` is acceptable\n * — Next.js will then fail loudly and the parent exits.\n */\nasync function assertPortFree(port: number): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const tester = net.createServer()\n tester.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(\n new Error(\n `neatd: web UI port ${port} in use; set NEAT_WEB_PORT to override or stop the conflicting process`,\n ),\n )\n } else {\n reject(err)\n }\n })\n tester.once('listening', () => {\n tester.close(() => resolve())\n })\n tester.listen(port, '127.0.0.1')\n })\n}\n\n/**\n * Resolve the web package directory. In a global install\n * (`npm install -g neat.is`) the package lives at\n * `node_modules/@neat.is/web/`; in the monorepo it's\n * `packages/web/`. `require.resolve` finds whichever one Node has on its\n * search path.\n */\nfunction resolveWebPackageDir(): string {\n const req = (typeof require !== 'undefined'\n ? require\n : // ESM fallback — daemon CJS bundle has `require`, but typecheck wants this\n (eval('require') as NodeRequire))\n const pkgJsonPath = req.resolve('@neat.is/web/package.json')\n return path.dirname(pkgJsonPath)\n}\n\n/**\n * Locate the standalone server.js inside the web package. ADR-064 #2 — the\n * tarball ships `.next/standalone/packages/web/server.js` (path preserved\n * relative to the monorepo tracing root). Missing means the package was\n * published without `next build` having run, which the smoke-test gate\n * catches at publish time.\n */\nfunction resolveStandaloneServerEntry(webDir: string): string {\n return path.join(webDir, '.next/standalone/packages/web/server.js')\n}\n\nexport async function spawnWebUI(restPort: number): Promise<WebHandle> {\n const portRaw = process.env.NEAT_WEB_PORT\n const port = portRaw && portRaw.length > 0 ? Number.parseInt(portRaw, 10) : DEFAULT_WEB_PORT\n if (!Number.isFinite(port) || port <= 0 || port > 65535) {\n throw new Error(`neatd: invalid NEAT_WEB_PORT=\"${portRaw}\"`)\n }\n\n await assertPortFree(port)\n\n const cwd = resolveWebPackageDir()\n const serverEntry = resolveStandaloneServerEntry(cwd)\n // ADR-064 — fail loudly if the standalone build is missing. v0.3.0 shipped\n // without it; the symptom on the user side was `next start` aborting with\n // `Could not find a production build in the '.next' directory`. This check\n // catches it at the parent's bootstrap instead of letting the child crash\n // a few moments later with a worse error.\n try {\n require.resolve(serverEntry)\n } catch {\n throw new Error(\n `neatd: web UI standalone build missing at ${serverEntry}. ` +\n `The published @neat.is/web tarball should include it; if you're running from a ` +\n `monorepo checkout, run \\`npm run build --workspace @neat.is/web\\` first, or set ` +\n `NEAT_WEB_DISABLED=1 to skip the web UI.`,\n )\n }\n\n // ADR-059 #6 — child inherits NEAT_API_URL pointing at our REST server,\n // unless the operator pre-configured it.\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n PORT: String(port),\n HOSTNAME: process.env.HOSTNAME ?? '0.0.0.0',\n NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`,\n }\n\n // The standalone bundle is self-contained — its own `node_modules` and\n // `package.json` sit alongside `server.js`. Spawn `node` against it\n // directly; no `next start` (which needs the source tree + build cache)\n // and no `npm exec` (which is monorepo-only in practice).\n // `detached: false` keeps the child in our process group so signals reach it.\n const child = spawn(process.execPath, [serverEntry], {\n cwd: path.dirname(serverEntry),\n env,\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n })\n\n child.on('error', (err) => {\n console.error(`neatd: web UI spawn error — ${err.message}`)\n })\n\n console.log(`neatd: web UI listening on http://localhost:${port}`)\n\n let stopped = false\n async function stop(): Promise<void> {\n if (stopped || !child.pid) return\n stopped = true\n try {\n child.kill('SIGTERM')\n } catch {\n /* already gone */\n }\n // Give the child up to 3s to exit gracefully, then SIGKILL.\n await new Promise<void>((resolve) => {\n const t = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n } catch {\n /* gone */\n }\n resolve()\n }, 3000)\n child.once('exit', () => {\n clearTimeout(t)\n resolve()\n })\n })\n }\n\n return { child, port, stop }\n}\n","/**\n * Version-skew check for `neatd start` (ADR-049 §Lifecycle — non-fatal\n * advisory).\n *\n * A globally installed `neat.is` binary may run an older version than the\n * `neat.is` available on npm — happens when the operator has not run\n * `npm install -g neat.is@latest` since the last publish. The six packages\n * move in lockstep (publish-system contract / ADR-052), so the registry\n * version of `neat.is` mirrors the local `@neat.is/core` version.\n *\n * On daemon start we compare the two and log a single advisory line when\n * the registry is ahead. The check is fail-open by design — registry\n * unreachable, slow, or returning an unexpected payload all resolve to\n * \"no warning, daemon proceeds normally.\"\n */\n\nimport { setTimeout as delay } from 'node:timers/promises'\n\nconst NPM_REGISTRY_URL = 'https://registry.npmjs.org/neat.is/latest'\nconst DEFAULT_TIMEOUT_MS = 2000\n\nexport interface VersionSkewCheckOptions {\n // The local version the running daemon reports. Production: read from the\n // bundled @neat.is/core package.json. Tests pass a literal.\n localVersion: string\n // Override the registry URL (tests point at a mock server).\n registryUrl?: string\n // Timeout for the registry fetch. Defaults to 2s.\n timeoutMs?: number\n // Injected so tests can simulate slow responses without real timers.\n fetchImpl?: typeof fetch\n // Sink for the advisory line. Defaults to console.warn.\n warn?: (message: string) => void\n}\n\nexport interface VersionSkewCheckResult {\n // The version the registry reported, or null on any fetch / parse failure.\n remoteVersion: string | null\n // Whether the local version is behind the registry's.\n skewed: boolean\n // Whether the advisory was emitted.\n warned: boolean\n}\n\n/**\n * Fail-open registry lookup. Returns the version string or null on any\n * non-success path (network, timeout, parse, missing field).\n */\nasync function fetchRegistryVersion(\n url: string,\n timeoutMs: number,\n fetchImpl: typeof fetch,\n): Promise<string | null> {\n const controller = new AbortController()\n let timedOut = false\n const timer = delay(timeoutMs).then(() => {\n timedOut = true\n controller.abort()\n })\n try {\n const res = await Promise.race([\n fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }),\n timer.then(() => null),\n ])\n if (timedOut || !res) return null\n if (!res.ok) return null\n const body = (await res.json()) as { version?: unknown }\n if (typeof body.version !== 'string' || body.version.length === 0) return null\n return body.version\n } catch {\n return null\n } finally {\n // Make sure the abort fires so the delay promise resolves even on\n // success — keeps the process from hanging on test teardown.\n if (!timedOut) controller.abort()\n }\n}\n\n/**\n * Compare two semver-shaped strings. Returns true when `local` is strictly\n * older than `remote`. We don't use a real semver library because the\n * advisory is intentionally coarse — any non-equal pair where the registry\n * looks newer is enough to suggest a re-install. The implementation does a\n * plain segment-by-segment integer compare and tolerates pre-release\n * suffixes by stripping them off.\n */\nexport function isLocalBehind(local: string, remote: string): boolean {\n if (local === remote) return false\n const parse = (v: string): number[] => {\n const core = v.split(/[-+]/)[0] ?? v\n return core.split('.').map((s) => {\n const n = Number.parseInt(s, 10)\n return Number.isFinite(n) ? n : 0\n })\n }\n const a = parse(local)\n const b = parse(remote)\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const av = a[i] ?? 0\n const bv = b[i] ?? 0\n if (av < bv) return true\n if (av > bv) return false\n }\n return false\n}\n\nexport async function checkVersionSkew(\n opts: VersionSkewCheckOptions,\n): Promise<VersionSkewCheckResult> {\n const url = opts.registryUrl ?? NPM_REGISTRY_URL\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const fetchImpl = opts.fetchImpl ?? fetch\n const warn = opts.warn ?? ((m) => console.warn(m))\n\n const remote = await fetchRegistryVersion(url, timeoutMs, fetchImpl)\n if (!remote) return { remoteVersion: null, skewed: false, warned: false }\n\n if (!isLocalBehind(opts.localVersion, remote)) {\n return { remoteVersion: remote, skewed: false, warned: false }\n }\n\n warn(\n `[neatd] running neat.is@${opts.localVersion} but neat.is@${remote} is on npm — run \\`npm install -g neat.is@latest\\` to update`,\n )\n return { remoteVersion: remote, skewed: true, warned: true }\n}\n"],"mappings":"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","names":["path","req","path"]}
|
package/dist/server.cjs
CHANGED
|
@@ -716,11 +716,16 @@ init_cjs_shims();
|
|
|
716
716
|
var import_types = require("@neat.is/types");
|
|
717
717
|
var ROOT_CAUSE_MAX_DEPTH = 5;
|
|
718
718
|
var BLAST_RADIUS_DEFAULT_DEPTH = 10;
|
|
719
|
+
function isFrontierNode(graph, nodeId) {
|
|
720
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
721
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
722
|
+
return attrs.type === import_types.NodeType.FrontierNode;
|
|
723
|
+
}
|
|
719
724
|
function bestEdgeBySource(graph, edgeIds) {
|
|
720
725
|
const best = /* @__PURE__ */ new Map();
|
|
721
726
|
for (const id of edgeIds) {
|
|
722
727
|
const e = graph.getEdgeAttributes(id);
|
|
723
|
-
if (e.
|
|
728
|
+
if (isFrontierNode(graph, e.source)) continue;
|
|
724
729
|
const cur = best.get(e.source);
|
|
725
730
|
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
726
731
|
best.set(e.source, e);
|
|
@@ -732,7 +737,7 @@ function bestEdgeByTarget(graph, edgeIds) {
|
|
|
732
737
|
const best = /* @__PURE__ */ new Map();
|
|
733
738
|
for (const id of edgeIds) {
|
|
734
739
|
const e = graph.getEdgeAttributes(id);
|
|
735
|
-
if (e.
|
|
740
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
736
741
|
const cur = best.get(e.target);
|
|
737
742
|
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
738
743
|
best.set(e.target, e);
|
|
@@ -744,8 +749,7 @@ var PROVENANCE_CEILING = {
|
|
|
744
749
|
OBSERVED: 1,
|
|
745
750
|
INFERRED: 0.7,
|
|
746
751
|
EXTRACTED: 0.5,
|
|
747
|
-
STALE: 0.3
|
|
748
|
-
FRONTIER: 0.3
|
|
752
|
+
STALE: 0.3
|
|
749
753
|
};
|
|
750
754
|
function volumeWeight(spanCount) {
|
|
751
755
|
if (!spanCount || spanCount <= 0) return 0.5;
|
|
@@ -1014,9 +1018,6 @@ function bucketEdges(graph) {
|
|
|
1014
1018
|
case import_types2.Provenance.INFERRED:
|
|
1015
1019
|
cur.inferred = e;
|
|
1016
1020
|
break;
|
|
1017
|
-
case import_types2.Provenance.FRONTIER:
|
|
1018
|
-
cur.frontier = e;
|
|
1019
|
-
break;
|
|
1020
1021
|
default:
|
|
1021
1022
|
if (e.provenance === import_types2.Provenance.STALE) cur.stale = e;
|
|
1022
1023
|
}
|
|
@@ -1285,8 +1286,8 @@ var evaluateStructural = ({
|
|
|
1285
1286
|
for (const edgeId of graph.outboundEdges(id)) {
|
|
1286
1287
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1287
1288
|
if (e.type !== rule.edgeType) continue;
|
|
1288
|
-
if (e.provenance === import_types3.Provenance.FRONTIER) continue;
|
|
1289
1289
|
const target = graph.getNodeAttributes(e.target);
|
|
1290
|
+
if (target.type === import_types3.NodeType.FrontierNode) continue;
|
|
1290
1291
|
if (target.type === rule.toNodeType) {
|
|
1291
1292
|
satisfied = true;
|
|
1292
1293
|
break;
|
|
@@ -1405,8 +1406,8 @@ var evaluateCompatibility = ({
|
|
|
1405
1406
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
1406
1407
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1407
1408
|
if (e.type !== import_types3.EdgeType.CONNECTS_TO) continue;
|
|
1408
|
-
if (e.provenance === import_types3.Provenance.FRONTIER) continue;
|
|
1409
1409
|
const dbAttrs = graph.getNodeAttributes(e.target);
|
|
1410
|
+
if (dbAttrs.type === import_types3.NodeType.FrontierNode) continue;
|
|
1410
1411
|
if (dbAttrs.type !== import_types3.NodeType.DatabaseNode) continue;
|
|
1411
1412
|
const db = dbAttrs;
|
|
1412
1413
|
for (const pair of compatPairs()) {
|
|
@@ -1732,31 +1733,6 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1732
1733
|
graph.addNode(id, node);
|
|
1733
1734
|
return id;
|
|
1734
1735
|
}
|
|
1735
|
-
function upsertFrontierEdge(graph, type, source, target, ts) {
|
|
1736
|
-
const id = (0, import_types4.frontierEdgeId)(source, target, type);
|
|
1737
|
-
if (graph.hasEdge(id)) {
|
|
1738
|
-
const existing = graph.getEdgeAttributes(id);
|
|
1739
|
-
const updated = {
|
|
1740
|
-
...existing,
|
|
1741
|
-
provenance: import_types4.Provenance.FRONTIER,
|
|
1742
|
-
lastObserved: ts,
|
|
1743
|
-
callCount: (existing.callCount ?? 0) + 1
|
|
1744
|
-
};
|
|
1745
|
-
graph.replaceEdgeAttributes(id, updated);
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
const edge = {
|
|
1749
|
-
id,
|
|
1750
|
-
source,
|
|
1751
|
-
target,
|
|
1752
|
-
type,
|
|
1753
|
-
provenance: import_types4.Provenance.FRONTIER,
|
|
1754
|
-
confidence: 1,
|
|
1755
|
-
lastObserved: ts,
|
|
1756
|
-
callCount: 1
|
|
1757
|
-
};
|
|
1758
|
-
graph.addEdgeWithKey(id, source, target, edge);
|
|
1759
|
-
}
|
|
1760
1736
|
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1761
1737
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1762
1738
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -1842,6 +1818,14 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
1842
1818
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
1843
1819
|
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1844
1820
|
}
|
|
1821
|
+
function sanitizeAttributes(attrs) {
|
|
1822
|
+
const out = {};
|
|
1823
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1824
|
+
if (typeof v === "bigint") out[k] = v.toString();
|
|
1825
|
+
else out[k] = v;
|
|
1826
|
+
}
|
|
1827
|
+
return out;
|
|
1828
|
+
}
|
|
1845
1829
|
async function handleSpan(ctx, span) {
|
|
1846
1830
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1847
1831
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
@@ -1881,11 +1865,16 @@ async function handleSpan(ctx, span) {
|
|
|
1881
1865
|
affectedNode = targetId;
|
|
1882
1866
|
resolvedViaAddress = true;
|
|
1883
1867
|
} else if (!targetId) {
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1868
|
+
const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
|
|
1869
|
+
upsertObservedEdge(
|
|
1870
|
+
ctx.graph,
|
|
1871
|
+
import_types4.EdgeType.CALLS,
|
|
1872
|
+
sourceId,
|
|
1873
|
+
frontierNodeId,
|
|
1874
|
+
ts,
|
|
1875
|
+
isError
|
|
1876
|
+
);
|
|
1877
|
+
affectedNode = frontierNodeId;
|
|
1889
1878
|
resolvedViaAddress = true;
|
|
1890
1879
|
}
|
|
1891
1880
|
}
|
|
@@ -1907,15 +1896,17 @@ async function handleSpan(ctx, span) {
|
|
|
1907
1896
|
if (span.statusCode === 2) {
|
|
1908
1897
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
1909
1898
|
if (ctx.writeErrorEventInline !== false) {
|
|
1899
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1910
1900
|
const ev = {
|
|
1911
1901
|
id: `${span.traceId}:${span.spanId}`,
|
|
1912
1902
|
timestamp: ts,
|
|
1913
1903
|
service: span.service,
|
|
1914
1904
|
traceId: span.traceId,
|
|
1915
1905
|
spanId: span.spanId,
|
|
1916
|
-
errorMessage: span.exception?.message ??
|
|
1906
|
+
errorMessage: span.exception?.message ?? "unknown error",
|
|
1917
1907
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1918
1908
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1909
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1919
1910
|
affectedNode
|
|
1920
1911
|
};
|
|
1921
1912
|
await appendErrorEvent(ctx, ev);
|
|
@@ -1971,8 +1962,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
|
|
|
1971
1962
|
}
|
|
1972
1963
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1973
1964
|
graph.dropEdge(oldEdgeId);
|
|
1974
|
-
const
|
|
1975
|
-
const newId = promotedProvenance === import_types4.Provenance.OBSERVED ? (0, import_types4.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types4.Provenance.INFERRED ? (0, import_types4.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types4.Provenance.EXTRACTED ? (0, import_types4.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types4.frontierEdgeId)(newSource, newTarget, edge.type);
|
|
1965
|
+
const newId = edge.provenance === import_types4.Provenance.OBSERVED ? (0, import_types4.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types4.Provenance.INFERRED ? (0, import_types4.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types4.extractedEdgeId)(newSource, newTarget, edge.type);
|
|
1976
1966
|
if (graph.hasEdge(newId)) {
|
|
1977
1967
|
const existing = graph.getEdgeAttributes(newId);
|
|
1978
1968
|
const merged = {
|
|
@@ -1987,8 +1977,7 @@ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
|
1987
1977
|
...edge,
|
|
1988
1978
|
id: newId,
|
|
1989
1979
|
source: newSource,
|
|
1990
|
-
target: newTarget
|
|
1991
|
-
provenance: promotedProvenance
|
|
1980
|
+
target: newTarget
|
|
1992
1981
|
};
|
|
1993
1982
|
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
1994
1983
|
}
|
|
@@ -4937,7 +4926,8 @@ async function buildApi(opts) {
|
|
|
4937
4926
|
init_cjs_shims();
|
|
4938
4927
|
var import_node_fs20 = require("fs");
|
|
4939
4928
|
var import_node_path33 = __toESM(require("path"), 1);
|
|
4940
|
-
var
|
|
4929
|
+
var import_types22 = require("@neat.is/types");
|
|
4930
|
+
var SCHEMA_VERSION = 3;
|
|
4941
4931
|
function migrateV1ToV2(payload) {
|
|
4942
4932
|
const nodes = payload.graph.nodes;
|
|
4943
4933
|
if (Array.isArray(nodes)) {
|
|
@@ -4949,6 +4939,25 @@ function migrateV1ToV2(payload) {
|
|
|
4949
4939
|
}
|
|
4950
4940
|
return { ...payload, schemaVersion: 2 };
|
|
4951
4941
|
}
|
|
4942
|
+
function migrateV2ToV3(payload) {
|
|
4943
|
+
const edges = payload.graph.edges;
|
|
4944
|
+
if (Array.isArray(edges)) {
|
|
4945
|
+
for (const edge of edges) {
|
|
4946
|
+
const attrs = edge.attributes;
|
|
4947
|
+
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4948
|
+
attrs.provenance = import_types22.Provenance.OBSERVED;
|
|
4949
|
+
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4950
|
+
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4951
|
+
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4952
|
+
if (type && source && target) {
|
|
4953
|
+
const newId = (0, import_types22.observedEdgeId)(source, target, type);
|
|
4954
|
+
attrs.id = newId;
|
|
4955
|
+
if (edge.key) edge.key = newId;
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
return { ...payload, schemaVersion: 3 };
|
|
4960
|
+
}
|
|
4952
4961
|
async function ensureDir(filePath) {
|
|
4953
4962
|
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
|
|
4954
4963
|
}
|
|
@@ -4975,6 +4984,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4975
4984
|
if (payload.schemaVersion === 1) {
|
|
4976
4985
|
payload = migrateV1ToV2(payload);
|
|
4977
4986
|
}
|
|
4987
|
+
if (payload.schemaVersion === 2) {
|
|
4988
|
+
payload = migrateV2ToV3(payload);
|
|
4989
|
+
}
|
|
4978
4990
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4979
4991
|
throw new Error(
|
|
4980
4992
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|