@quolu/lattice 0.52.2 → 0.52.4
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/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge hub registration protocol — the wire contract between a terminal's
|
|
3
|
+
* `lattice bridge` and the hub that aggregates them for the public dashboard.
|
|
4
|
+
*
|
|
5
|
+
* This module owns validation and pure registry mutation only. It does not open
|
|
6
|
+
* a socket, read a clock, or touch disk — that belongs to the hub server (bh2)
|
|
7
|
+
* and the terminal heartbeat client (bh3). Keeping the contract pure lets it be
|
|
8
|
+
* characterization-tested before either side exists, and keeps bh2/bh3 from
|
|
9
|
+
* inventing their own conflict or staleness rules.
|
|
10
|
+
*
|
|
11
|
+
* Design decisions this module encodes (see docs/adr/0162 for the full record):
|
|
12
|
+
*
|
|
13
|
+
* - The registry is keyed by `project_id`, not `terminal_id`, because hub routes
|
|
14
|
+
* `/projects/<id>/*` by project. A terminal that owns several projects appears
|
|
15
|
+
* as several entries sharing the same terminal_id/address/port.
|
|
16
|
+
* - The request never carries an address. The caller passes the address the
|
|
17
|
+
* registration connection actually arrived from (`remoteAddress`); a terminal
|
|
18
|
+
* can only ever register itself, the same safety property bridge-registrar.mjs
|
|
19
|
+
* already relies on for the ssh-based upstream registrar.
|
|
20
|
+
* - A registration call declares a terminal's *complete* current project
|
|
21
|
+
* portfolio and replaces that terminal's prior contribution wholesale. A
|
|
22
|
+
* project the terminal owned before but omits now is released — there is no
|
|
23
|
+
* separate "deregister" verb, and no partial application: a heartbeat either
|
|
24
|
+
* lands in full or is rejected in full.
|
|
25
|
+
* - A project_id already owned by a *different* terminal is a conflict unless
|
|
26
|
+
* named in `adopt`. Conflicts are collected and reported together — a batch
|
|
27
|
+
* never partially lands, so a caller is never left unsure which half won.
|
|
28
|
+
* - Staleness is read-time only. `projectBridgeHubRegistry` marks an entry
|
|
29
|
+
* 'offline' once `last_seen_at` exceeds the TTL; it never deletes the entry.
|
|
30
|
+
* The plan's fail-closed requirement is "show offline", not "make it vanish".
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
34
|
+
const MAX_PROJECT_IDS = 256;
|
|
35
|
+
|
|
36
|
+
/** Suggested terminal heartbeat cadence. Not part of the wire contract — the hub
|
|
37
|
+
* owns both constants and can retune them without a protocol version bump. */
|
|
38
|
+
export const BRIDGE_HUB_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
39
|
+
/** Grace window after the last heartbeat before a project is shown offline. */
|
|
40
|
+
export const BRIDGE_HUB_HEARTBEAT_TTL_MS = 90_000;
|
|
41
|
+
|
|
42
|
+
export class BridgeHubProtocolError extends Error {
|
|
43
|
+
constructor(code, message, detail = null) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'BridgeHubProtocolError';
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.detail = detail;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function identifier(value) {
|
|
52
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function uniqueIdentifierArray(value, { min = 0, max = MAX_PROJECT_IDS } = {}) {
|
|
56
|
+
return Array.isArray(value) && value.length >= min && value.length <= max
|
|
57
|
+
&& value.every(identifier) && new Set(value).size === value.length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sorted(ids) {
|
|
61
|
+
return [...ids].sort((left, right) => left.localeCompare(right, 'en'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A terminal's registration/heartbeat request, as it crosses the wire. */
|
|
65
|
+
export function validateBridgeHubRegistrationRequest(value) {
|
|
66
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
67
|
+
&& Object.keys(value).sort().join(',') === 'adopt,display_name,port,project_ids,schema,terminal_id'
|
|
68
|
+
&& value.schema === 'lattice.bridge_hub_registration_request.v1'
|
|
69
|
+
&& identifier(value.terminal_id)
|
|
70
|
+
&& typeof value.display_name === 'string' && value.display_name.length > 0
|
|
71
|
+
&& value.display_name === value.display_name.trim()
|
|
72
|
+
&& Number.isSafeInteger(value.port) && value.port > 0 && value.port <= 65_535
|
|
73
|
+
&& uniqueIdentifierArray(value.project_ids, { min: 1 })
|
|
74
|
+
&& uniqueIdentifierArray(value.adopt, { max: value.project_ids.length })
|
|
75
|
+
&& value.adopt.every((projectId) => value.project_ids.includes(projectId));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A single project's routing entry as stored in the hub registry. */
|
|
79
|
+
function validRegistryEntry(entry) {
|
|
80
|
+
return entry !== null && typeof entry === 'object' && !Array.isArray(entry)
|
|
81
|
+
&& Object.keys(entry).sort().join(',')
|
|
82
|
+
=== 'address,display_name,last_seen_at,port,project_id,registered_at,terminal_id'
|
|
83
|
+
&& identifier(entry.project_id) && identifier(entry.terminal_id)
|
|
84
|
+
&& typeof entry.display_name === 'string' && entry.display_name.length > 0
|
|
85
|
+
&& typeof entry.address === 'string' && entry.address.length > 0
|
|
86
|
+
&& Number.isSafeInteger(entry.port) && entry.port > 0 && entry.port <= 65_535
|
|
87
|
+
&& Number.isFinite(Date.parse(entry.registered_at)) && Number.isFinite(Date.parse(entry.last_seen_at));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateBridgeHubRegistryEntry(value) {
|
|
91
|
+
return validRegistryEntry(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validRegistry(registry) {
|
|
95
|
+
return Array.isArray(registry) && registry.every(validRegistryEntry);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Apply one registration/heartbeat call to a registry snapshot. Pure: takes the
|
|
100
|
+
* current entries and returns the next ones plus a result summary, mutating
|
|
101
|
+
* nothing. Throws `BridgeHubProtocolError` for an invalid request or an
|
|
102
|
+
* unresolved project_id conflict; on throw, `registry` is guaranteed untouched
|
|
103
|
+
* because nothing is written until every conflict check has passed.
|
|
104
|
+
*/
|
|
105
|
+
export function applyBridgeHubRegistration({ registry, request, remoteAddress, now = new Date() }) {
|
|
106
|
+
if (!validRegistry(registry)) {
|
|
107
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRY_INVALID', 'bridge hub registry is invalid');
|
|
108
|
+
}
|
|
109
|
+
if (!validateBridgeHubRegistrationRequest(request)) {
|
|
110
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRATION_INVALID', 'bridge hub registration request is invalid');
|
|
111
|
+
}
|
|
112
|
+
if (typeof remoteAddress !== 'string' || remoteAddress.length === 0) {
|
|
113
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRATION_INVALID', 'remote address is required');
|
|
114
|
+
}
|
|
115
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
|
|
116
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRATION_INVALID', 'now must be a valid Date');
|
|
117
|
+
}
|
|
118
|
+
const { terminal_id: terminalId, display_name: displayName, port, project_ids: projectIds, adopt } = request;
|
|
119
|
+
const requestedSet = new Set(projectIds);
|
|
120
|
+
const adoptSet = new Set(adopt);
|
|
121
|
+
|
|
122
|
+
const conflicts = registry
|
|
123
|
+
.filter((entry) => requestedSet.has(entry.project_id)
|
|
124
|
+
&& entry.terminal_id !== terminalId && !adoptSet.has(entry.project_id))
|
|
125
|
+
.map((entry) => ({ project_id: entry.project_id, owning_terminal_id: entry.terminal_id }));
|
|
126
|
+
if (conflicts.length > 0) {
|
|
127
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_PROJECT_CONFLICT',
|
|
128
|
+
'one or more project_ids are already owned by a different terminal',
|
|
129
|
+
{
|
|
130
|
+
conflicts: conflicts.sort((left, right) => left.project_id.localeCompare(right.project_id, 'en')),
|
|
131
|
+
next_action: 're-register naming the conflicting project_ids in adopt',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const registeredAtByProject = new Map(registry
|
|
136
|
+
.filter((entry) => entry.terminal_id === terminalId)
|
|
137
|
+
.map((entry) => [entry.project_id, entry.registered_at]));
|
|
138
|
+
const adopted = sorted(projectIds.filter((projectId) => adoptSet.has(projectId)
|
|
139
|
+
&& !registeredAtByProject.has(projectId)));
|
|
140
|
+
// Full-state reconciliation: every entry this terminal owned is dropped, then
|
|
141
|
+
// rebuilt from `projectIds`. A project omitted from this call — dropped
|
|
142
|
+
// locally, or never re-sent after a crash — is released, not left as a
|
|
143
|
+
// phantom no future heartbeat can retract.
|
|
144
|
+
const others = registry.filter((entry) => entry.terminal_id !== terminalId && !requestedSet.has(entry.project_id));
|
|
145
|
+
const mine = projectIds.map((projectId) => ({
|
|
146
|
+
project_id: projectId,
|
|
147
|
+
terminal_id: terminalId,
|
|
148
|
+
display_name: displayName,
|
|
149
|
+
address: remoteAddress,
|
|
150
|
+
port,
|
|
151
|
+
registered_at: registeredAtByProject.get(projectId) ?? now.toISOString(),
|
|
152
|
+
last_seen_at: now.toISOString(),
|
|
153
|
+
}));
|
|
154
|
+
const nextRegistry = [...others, ...mine]
|
|
155
|
+
.sort((left, right) => left.project_id.localeCompare(right.project_id, 'en'));
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
registry: nextRegistry,
|
|
159
|
+
result: {
|
|
160
|
+
schema: 'lattice.bridge_hub_registration_result.v1',
|
|
161
|
+
terminal_id: terminalId,
|
|
162
|
+
address: remoteAddress,
|
|
163
|
+
port,
|
|
164
|
+
registered: sorted(projectIds),
|
|
165
|
+
adopted,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read-time projection for the aggregate `/projects/` view and per-project
|
|
172
|
+
* routing. Never mutates or drops entries — a terminal that stopped
|
|
173
|
+
* heartbeating shows as 'offline' forever, not gone, until it either
|
|
174
|
+
* re-registers or a different terminal explicitly adopts the project.
|
|
175
|
+
*/
|
|
176
|
+
export function projectBridgeHubRegistry({ registry, now = new Date(), ttlMs = BRIDGE_HUB_HEARTBEAT_TTL_MS }) {
|
|
177
|
+
if (!validRegistry(registry)) {
|
|
178
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRY_INVALID', 'bridge hub registry is invalid');
|
|
179
|
+
}
|
|
180
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
|
|
181
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRATION_INVALID', 'now must be a valid Date');
|
|
182
|
+
}
|
|
183
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
184
|
+
throw new BridgeHubProtocolError('BRIDGE_HUB_REGISTRATION_INVALID', 'ttlMs must be a positive integer');
|
|
185
|
+
}
|
|
186
|
+
return registry
|
|
187
|
+
.map((entry) => ({
|
|
188
|
+
schema: 'lattice.bridge_hub_registry_projection_entry.v1',
|
|
189
|
+
project_id: entry.project_id,
|
|
190
|
+
terminal_id: entry.terminal_id,
|
|
191
|
+
display_name: entry.display_name,
|
|
192
|
+
address: entry.address,
|
|
193
|
+
port: entry.port,
|
|
194
|
+
status: now.getTime() - Date.parse(entry.last_seen_at) <= ttlMs ? 'online' : 'offline',
|
|
195
|
+
last_seen_at: entry.last_seen_at,
|
|
196
|
+
}))
|
|
197
|
+
.sort((left, right) => left.project_id.localeCompare(right.project_id, 'en'));
|
|
198
|
+
}
|