@quolu/lattice 0.52.4 → 0.53.0
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/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-bridge.mjs +25 -0
- package/bin/lattice-hub.mjs +67 -0
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- package/src/bridge-cli.mjs +11 -5
- package/src/bridge-config.mjs +41 -5
- package/src/bridge-hub-heartbeat.mjs +170 -0
- package/src/bridge-hub-server.mjs +544 -0
- package/src/cli-help.mjs +4 -4
- package/src/todo-store.mjs +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-side bridge-hub heartbeat client (bh3).
|
|
3
|
+
*
|
|
4
|
+
* Wires the pure wire contract in `bridge-hub-protocol.mjs` (bh1) around this
|
|
5
|
+
* terminal's own state: a persisted terminal identity, the locally active
|
|
6
|
+
* project set (`todo-dashboard-registry.mjs`), and the configured hub origin
|
|
7
|
+
* (`bridge-config.mjs`'s `hub` field). It sends `POST /__lattice/hub/register`
|
|
8
|
+
* on `bridge-hub-server.mjs`'s (bh2) contract — the request body is the raw
|
|
9
|
+
* `lattice.bridge_hub_registration_request.v1` object, unmodified in transit.
|
|
10
|
+
*
|
|
11
|
+
* DHCP addresses are never read or sent here: ADR 0162 has the hub derive
|
|
12
|
+
* `address` from the registration connection's own source, the same safety
|
|
13
|
+
* property `bridge-registrar.mjs` relies on. A moved lease is invisible to
|
|
14
|
+
* this module by design — the next heartbeat just arrives from a new source.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomBytes } from 'node:crypto';
|
|
18
|
+
import { hostname } from 'node:os';
|
|
19
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { bridgeConfigPaths } from './bridge-config.mjs';
|
|
23
|
+
import { BRIDGE_HUB_HEARTBEAT_INTERVAL_MS, validateBridgeHubRegistrationRequest } from './bridge-hub-protocol.mjs';
|
|
24
|
+
import { readActiveTodoDashboardProjects } from './todo-dashboard-registry.mjs';
|
|
25
|
+
|
|
26
|
+
export { BRIDGE_HUB_HEARTBEAT_INTERVAL_MS };
|
|
27
|
+
|
|
28
|
+
const TERMINAL_IDENTITY_SCHEMA = 'lattice.bridge_hub_terminal_identity.v1';
|
|
29
|
+
const HEARTBEAT_RESULT_SCHEMA = 'lattice.bridge_hub_heartbeat_result.v1';
|
|
30
|
+
const REGISTRATION_REQUEST_SCHEMA = 'lattice.bridge_hub_registration_request.v1';
|
|
31
|
+
const REGISTER_PATH = '__lattice/hub/register';
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
33
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
34
|
+
|
|
35
|
+
export class BridgeHubHeartbeatError extends Error {
|
|
36
|
+
constructor(code, message, detail = undefined, cause = undefined) {
|
|
37
|
+
super(message, { cause });
|
|
38
|
+
this.name = 'BridgeHubHeartbeatError';
|
|
39
|
+
this.code = code;
|
|
40
|
+
if (detail !== undefined) this.detail = detail;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function terminalIdentityPath(env) {
|
|
45
|
+
return path.join(bridgeConfigPaths(env).root, 'bridge-hub-terminal.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This terminal's stable bridge-hub identity, created once and reused across
|
|
50
|
+
* restarts. It must survive restarts: registration is a full-state
|
|
51
|
+
* reconciliation keyed by terminal_id (ADR 0162 Decision 4), so a fresh id on
|
|
52
|
+
* every daemon start would make the hub see "a new terminal" contesting the
|
|
53
|
+
* same project_ids the old id still owns until its TTL lapses — a
|
|
54
|
+
* self-inflicted `BRIDGE_HUB_PROJECT_CONFLICT`.
|
|
55
|
+
*/
|
|
56
|
+
export async function readOrCreateBridgeHubTerminalId({ env = process.env } = {}) {
|
|
57
|
+
const refs = bridgeConfigPaths(env);
|
|
58
|
+
await mkdir(refs.root, { recursive: true, mode: 0o700 });
|
|
59
|
+
const ref = terminalIdentityPath(env);
|
|
60
|
+
for (;;) {
|
|
61
|
+
try {
|
|
62
|
+
const value = JSON.parse(await readFile(ref, 'utf8'));
|
|
63
|
+
if (value?.schema === TERMINAL_IDENTITY_SCHEMA && IDENTIFIER.test(value.terminal_id)) return value.terminal_id;
|
|
64
|
+
throw new BridgeHubHeartbeatError('BRIDGE_HUB_TERMINAL_IDENTITY_INVALID',
|
|
65
|
+
'bridge hub terminal identity file is invalid');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
68
|
+
}
|
|
69
|
+
const terminalId = randomBytes(16).toString('hex');
|
|
70
|
+
try {
|
|
71
|
+
await writeFile(ref, `${JSON.stringify({ schema: TERMINAL_IDENTITY_SCHEMA, terminal_id: terminalId })}\n`,
|
|
72
|
+
{ encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
73
|
+
return terminalId;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
76
|
+
// Lost the create race to another process; loop back and read what it wrote.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The terminal's own display name, shown for every project it registers
|
|
83
|
+
* (registry entries are per-project but `display_name` is terminal-wide —
|
|
84
|
+
* ADR 0162 Decision 2). Hostname, not a project name: the active project set
|
|
85
|
+
* changes heartbeat to heartbeat but the terminal's identity does not.
|
|
86
|
+
*/
|
|
87
|
+
function terminalDisplayName() {
|
|
88
|
+
return hostname().slice(0, 128) || 'terminal';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Build and validate one registration/heartbeat request. Throws rather than
|
|
92
|
+
* sending a request the hub would reject as malformed. */
|
|
93
|
+
export function buildBridgeHubRegistrationRequest({ terminalId, port, projectIds, adopt = [] }) {
|
|
94
|
+
const request = {
|
|
95
|
+
schema: REGISTRATION_REQUEST_SCHEMA,
|
|
96
|
+
terminal_id: terminalId,
|
|
97
|
+
display_name: terminalDisplayName(),
|
|
98
|
+
port,
|
|
99
|
+
project_ids: [...new Set(projectIds)].sort((left, right) => left.localeCompare(right, 'en')),
|
|
100
|
+
adopt: [...adopt],
|
|
101
|
+
};
|
|
102
|
+
if (!validateBridgeHubRegistrationRequest(request)) {
|
|
103
|
+
throw new BridgeHubHeartbeatError('BRIDGE_HUB_HEARTBEAT_REQUEST_INVALID',
|
|
104
|
+
'constructed bridge hub registration request is invalid', { request });
|
|
105
|
+
}
|
|
106
|
+
return request;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send one heartbeat. Never throws for a remote or network failure — the
|
|
111
|
+
* bridge daemon loop calling this must keep serving locally even when the
|
|
112
|
+
* hub is unreachable, matching `bridge-registrar.mjs`'s posture. Failures are
|
|
113
|
+
* returned typed so callers can surface or log them instead of losing them.
|
|
114
|
+
*/
|
|
115
|
+
export async function sendBridgeHubHeartbeat({ hubUrl, request, fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
response = await fetchImpl(new URL(REGISTER_PATH, hubUrl), {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'content-type': 'application/json' },
|
|
121
|
+
body: JSON.stringify(request),
|
|
122
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { schema: HEARTBEAT_RESULT_SCHEMA, state: 'unreachable',
|
|
126
|
+
detail: (error?.message ?? 'network error').slice(0, 500) };
|
|
127
|
+
}
|
|
128
|
+
let body = null;
|
|
129
|
+
try { body = await response.json(); } catch { body = null; }
|
|
130
|
+
if (response.status !== 200) {
|
|
131
|
+
return { schema: HEARTBEAT_RESULT_SCHEMA, state: 'rejected', status: response.status, detail: body };
|
|
132
|
+
}
|
|
133
|
+
return { schema: HEARTBEAT_RESULT_SCHEMA, state: 'accepted', result: body };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Periodic controller for the bridge daemon's own poll loop
|
|
138
|
+
* (`bin/lattice-bridge.mjs`). Call `tick({ config })` on every iteration; it
|
|
139
|
+
* self-throttles to `intervalMs` and is a no-op (no disk or network access)
|
|
140
|
+
* whenever the terminal has no hub configured, so callers pay only a null
|
|
141
|
+
* check on the common path.
|
|
142
|
+
*/
|
|
143
|
+
export function createBridgeHubHeartbeatController({
|
|
144
|
+
env = process.env, fetchImpl = fetch, now = () => Date.now(),
|
|
145
|
+
intervalMs = BRIDGE_HUB_HEARTBEAT_INTERVAL_MS,
|
|
146
|
+
readActiveProjects = readActiveTodoDashboardProjects,
|
|
147
|
+
} = {}) {
|
|
148
|
+
let lastSentAt = null;
|
|
149
|
+
let lastResult = null;
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
async tick({ config }) {
|
|
152
|
+
if (config?.hub == null) { lastSentAt = null; lastResult = null; return null; }
|
|
153
|
+
const nowMs = now();
|
|
154
|
+
if (lastSentAt !== null && nowMs - lastSentAt < intervalMs) return lastResult;
|
|
155
|
+
lastSentAt = nowMs;
|
|
156
|
+
const projects = await readActiveProjects({ env });
|
|
157
|
+
if (projects.length === 0) {
|
|
158
|
+
lastResult = { schema: HEARTBEAT_RESULT_SCHEMA, state: 'skipped_no_projects' };
|
|
159
|
+
return lastResult;
|
|
160
|
+
}
|
|
161
|
+
const terminalId = await readOrCreateBridgeHubTerminalId({ env });
|
|
162
|
+
const request = buildBridgeHubRegistrationRequest({
|
|
163
|
+
terminalId, port: config.listen.port, projectIds: projects.map((project) => project.project_id),
|
|
164
|
+
});
|
|
165
|
+
lastResult = await sendBridgeHubHeartbeat({ hubUrl: config.hub.url, request, fetchImpl });
|
|
166
|
+
return lastResult;
|
|
167
|
+
},
|
|
168
|
+
lastHeartbeatResult: () => lastResult,
|
|
169
|
+
});
|
|
170
|
+
}
|