@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.
Files changed (59) hide show
  1. package/LICENSE +147 -147
  2. package/README.ja.md +355 -355
  3. package/README.md +258 -258
  4. package/bin/lattice-bridge.mjs +25 -0
  5. package/bin/lattice-hub.mjs +67 -0
  6. package/bin/lattice-mcp.mjs +0 -0
  7. package/bin/lattice-scripted-adapter.mjs +0 -0
  8. package/bin/lattice-scripted-worker.mjs +0 -0
  9. package/bin/lattice-work-order-adapter.mjs +0 -0
  10. package/bin/lattice.mjs +0 -0
  11. package/docs/bridge-setup.md +132 -132
  12. package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
  13. package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
  14. package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
  15. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
  16. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
  17. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
  18. package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
  19. package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
  20. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
  21. package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
  22. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
  23. package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
  24. package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
  25. package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
  26. package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
  27. package/package.json +103 -103
  28. package/sensor/LICENSE +21 -21
  29. package/sensor/NOTICE +19 -19
  30. package/sensor/dist/bin/lattice-sensor.js +9 -9
  31. package/sensor/dist/db/index.js +24 -24
  32. package/sensor/dist/db/migrations.js +41 -41
  33. package/sensor/dist/db/queries.js +164 -164
  34. package/sensor/dist/db/schema.sql +205 -205
  35. package/sensor/dist/directory.js +5 -5
  36. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  37. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  38. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  39. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  40. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  41. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  42. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  43. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  44. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  45. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  46. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  47. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  48. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  49. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  50. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  51. package/sensor/dist/mcp/liveness-watchdog.js +53 -53
  52. package/sensor/dist/mcp/server-instructions.js +95 -95
  53. package/sensor/package.json +56 -56
  54. package/src/bridge-cli.mjs +11 -5
  55. package/src/bridge-config.mjs +41 -5
  56. package/src/bridge-hub-heartbeat.mjs +170 -0
  57. package/src/bridge-hub-server.mjs +544 -0
  58. package/src/cli-help.mjs +4 -4
  59. 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
+ }