@quolu/lattice 0.52.0 → 0.52.2
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-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/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-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/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -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 -1502
- 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 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- 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 -162
- 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 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- 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 -1167
- 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 +3141 -3126
- 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 -625
- 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 +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
import { readFile, lstat } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* dotagents工場の native factory diagnostics 面(親plan L6要件)。
|
|
7
|
-
*
|
|
8
|
-
* - schema `lattice.native_factory_diagnostics.v1`: exact key・bounded・秘密なしJSON。
|
|
9
|
-
* - read-only: provider・network・run store書込・index生成を一切行わない。
|
|
10
|
-
* - 非0意味論: overall `failed` → exit 1。CLI usage違反は既存contractどおり exit 2。
|
|
11
|
-
* - 本面が健全性の唯一の診断正本(旧`doctor --json`はADR 0052で退役済み)。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const SCHEMA = 'lattice.native_factory_diagnostics.v1';
|
|
15
|
-
const PRODUCT = 'lattice';
|
|
16
|
-
const MAX_DETAIL_LENGTH = 256;
|
|
17
|
-
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
18
|
-
const UNKNOWN_VERSION_SENTINEL = '0.0.0-unknown';
|
|
19
|
-
const NODE_VERSION_PATTERN = /^v(\d+)\.(\d+)\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
20
|
-
const ENGINES_FLOOR_RANGE_PATTERN = /^(>=(\d+)(?:\.(\d+))?(?:\.\d+)?)(?: +<25(?:\.0\.0)? +\|\| +>=26(?:\.0\.0)?)$/;
|
|
21
|
-
const CHECK_IDS = Object.freeze([
|
|
22
|
-
'package_version',
|
|
23
|
-
'node_runtime',
|
|
24
|
-
'cli_surface',
|
|
25
|
-
'mcp_entry',
|
|
26
|
-
'sensor_attribution',
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const rootDirDefault = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
-
|
|
31
|
-
function isPlainObject(value) {
|
|
32
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function hasExactKeys(value, keys) {
|
|
37
|
-
const actualKeys = Object.keys(value).sort();
|
|
38
|
-
const expectedKeys = [...keys].sort();
|
|
39
|
-
return actualKeys.length === expectedKeys.length
|
|
40
|
-
&& actualKeys.every((key, index) => key === expectedKeys[index]);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function check(id, ok, detail) {
|
|
44
|
-
return { id, status: ok ? 'ok' : 'failed', detail: String(detail).slice(0, MAX_DETAIL_LENGTH) };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function isNonEmptyFile(absolutePath) {
|
|
48
|
-
try {
|
|
49
|
-
const stats = await lstat(absolutePath);
|
|
50
|
-
return stats.isFile() && stats.size > 0;
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function checkPackageVersion(packageJson) {
|
|
57
|
-
const version = packageJson?.version;
|
|
58
|
-
if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {
|
|
59
|
-
return check('package_version', false, 'package.json version is missing or not semver');
|
|
60
|
-
}
|
|
61
|
-
if (version === UNKNOWN_VERSION_SENTINEL) {
|
|
62
|
-
// ADR 0049 Decision 3(c): sentinel versionはビルド破損=沈黙させない。
|
|
63
|
-
return check('package_version', false, `sentinel version ${UNKNOWN_VERSION_SENTINEL} means a broken build`);
|
|
64
|
-
}
|
|
65
|
-
return check('package_version', true, version);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function checkNodeRuntime(nodeVersion, enginesNode) {
|
|
69
|
-
const runtime = NODE_VERSION_PATTERN.exec(nodeVersion ?? '');
|
|
70
|
-
if (!runtime) {
|
|
71
|
-
return check('node_runtime', false, 'node version is unreadable');
|
|
72
|
-
}
|
|
73
|
-
const floor = ENGINES_FLOOR_RANGE_PATTERN.exec(enginesNode ?? '');
|
|
74
|
-
if (!floor) {
|
|
75
|
-
return check('node_runtime', false, 'package.json engines.node floor is unreadable');
|
|
76
|
-
}
|
|
77
|
-
const [major, minor] = [Number(runtime[1]), Number(runtime[2])];
|
|
78
|
-
const [floorMajor, floorMinor] = [Number(floor[2]), Number(floor[3] ?? '0')];
|
|
79
|
-
const satisfied = major > floorMajor || (major === floorMajor && minor >= floorMinor);
|
|
80
|
-
return check(
|
|
81
|
-
'node_runtime',
|
|
82
|
-
satisfied,
|
|
83
|
-
satisfied ? `${nodeVersion} satisfies engines.node floor ${floor[1]}`
|
|
84
|
-
: `${nodeVersion} is below engines.node floor ${floor[1]}`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function checkCliSurface(rootDir) {
|
|
89
|
-
try {
|
|
90
|
-
const runtimeCli = await import(pathToFileURL(path.join(rootDir, 'src', 'runtime-cli.mjs')).href);
|
|
91
|
-
if (typeof runtimeCli.runRuntimeCli !== 'function') {
|
|
92
|
-
return check('cli_surface', false, 'src/runtime-cli.mjs does not export runRuntimeCli');
|
|
93
|
-
}
|
|
94
|
-
return check('cli_surface', true, 'runtime CLI surface loads and exports runRuntimeCli');
|
|
95
|
-
} catch (error) {
|
|
96
|
-
return check('cli_surface', false, `runtime CLI failed to load: ${error?.constructor?.name ?? 'Error'}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function checkMcpEntry(rootDir) {
|
|
101
|
-
const ok = await isNonEmptyFile(path.join(rootDir, 'bin', 'lattice-mcp.mjs'));
|
|
102
|
-
return check('mcp_entry', ok, ok ? 'bin/lattice-mcp.mjs is present' : 'bin/lattice-mcp.mjs is missing or empty');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function checkSensorAttribution(rootDir) {
|
|
106
|
-
// ADR 0047: fork吸収の受入条件=MIT license noticeとattributionの維持。
|
|
107
|
-
const license = await isNonEmptyFile(path.join(rootDir, 'sensor', 'LICENSE'));
|
|
108
|
-
const notice = await isNonEmptyFile(path.join(rootDir, 'sensor', 'NOTICE'));
|
|
109
|
-
const ok = license && notice;
|
|
110
|
-
return check(
|
|
111
|
-
'sensor_attribution',
|
|
112
|
-
ok,
|
|
113
|
-
ok ? 'sensor/LICENSE and sensor/NOTICE are present' : 'sensor/LICENSE or sensor/NOTICE is missing or empty',
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Builds the bounded, read-only native factory diagnostics object.
|
|
119
|
-
* @param {{ rootDir?: string, nodeVersion?: string }} options
|
|
120
|
-
* @returns {Promise<object>}
|
|
121
|
-
*/
|
|
122
|
-
export async function buildFactoryDiagnostics({ rootDir = rootDirDefault, nodeVersion = process.version } = {}) {
|
|
123
|
-
let packageJson = null;
|
|
124
|
-
try {
|
|
125
|
-
packageJson = JSON.parse(await readFile(path.join(rootDir, 'package.json'), 'utf8'));
|
|
126
|
-
} catch {
|
|
127
|
-
packageJson = null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const checks = [
|
|
131
|
-
checkPackageVersion(packageJson),
|
|
132
|
-
checkNodeRuntime(nodeVersion, packageJson?.engines?.node),
|
|
133
|
-
await checkCliSurface(rootDir),
|
|
134
|
-
await checkMcpEntry(rootDir),
|
|
135
|
-
await checkSensorAttribution(rootDir),
|
|
136
|
-
];
|
|
137
|
-
|
|
138
|
-
const diagnostics = {
|
|
139
|
-
schema: SCHEMA,
|
|
140
|
-
product: PRODUCT,
|
|
141
|
-
version: typeof packageJson?.version === 'string' ? packageJson.version.slice(0, MAX_DETAIL_LENGTH) : UNKNOWN_VERSION_SENTINEL,
|
|
142
|
-
overall: checks.every((entry) => entry.status === 'ok') ? 'ok' : 'failed',
|
|
143
|
-
checks,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
if (!validateFactoryDiagnostics(diagnostics)) {
|
|
147
|
-
throw new TypeError('invalid factory diagnostics output');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return diagnostics;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Strictly validates the public native factory diagnostics schema.
|
|
155
|
-
* @param {unknown} value
|
|
156
|
-
* @returns {boolean}
|
|
157
|
-
*/
|
|
158
|
-
export function validateFactoryDiagnostics(value) {
|
|
159
|
-
if (!isPlainObject(value) || !hasExactKeys(value, ['schema', 'product', 'version', 'overall', 'checks'])) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
if (value.schema !== SCHEMA || value.product !== PRODUCT) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
if (typeof value.version !== 'string' || value.version.length === 0 || value.version.length > MAX_DETAIL_LENGTH) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
if (value.overall !== 'ok' && value.overall !== 'failed') {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
if (!Array.isArray(value.checks) || value.checks.length !== CHECK_IDS.length) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
const allOk = value.checks.every((entry) => isPlainObject(entry)
|
|
175
|
-
&& hasExactKeys(entry, ['id', 'status', 'detail'])
|
|
176
|
-
&& (entry.status === 'ok' || entry.status === 'failed')
|
|
177
|
-
&& typeof entry.detail === 'string'
|
|
178
|
-
&& entry.detail.length <= MAX_DETAIL_LENGTH);
|
|
179
|
-
if (!allOk) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
const ids = value.checks.map((entry) => entry.id);
|
|
183
|
-
if (ids.some((id, index) => id !== CHECK_IDS[index])) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
const expectedOverall = value.checks.every((entry) => entry.status === 'ok') ? 'ok' : 'failed';
|
|
187
|
-
return value.overall === expectedOverall;
|
|
188
|
-
}
|
|
1
|
+
import { readFile, lstat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* dotagents工場の native factory diagnostics 面(親plan L6要件)。
|
|
7
|
+
*
|
|
8
|
+
* - schema `lattice.native_factory_diagnostics.v1`: exact key・bounded・秘密なしJSON。
|
|
9
|
+
* - read-only: provider・network・run store書込・index生成を一切行わない。
|
|
10
|
+
* - 非0意味論: overall `failed` → exit 1。CLI usage違反は既存contractどおり exit 2。
|
|
11
|
+
* - 本面が健全性の唯一の診断正本(旧`doctor --json`はADR 0052で退役済み)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SCHEMA = 'lattice.native_factory_diagnostics.v1';
|
|
15
|
+
const PRODUCT = 'lattice';
|
|
16
|
+
const MAX_DETAIL_LENGTH = 256;
|
|
17
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
18
|
+
const UNKNOWN_VERSION_SENTINEL = '0.0.0-unknown';
|
|
19
|
+
const NODE_VERSION_PATTERN = /^v(\d+)\.(\d+)\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
20
|
+
const ENGINES_FLOOR_RANGE_PATTERN = /^(>=(\d+)(?:\.(\d+))?(?:\.\d+)?)(?: +<25(?:\.0\.0)? +\|\| +>=26(?:\.0\.0)?)$/;
|
|
21
|
+
const CHECK_IDS = Object.freeze([
|
|
22
|
+
'package_version',
|
|
23
|
+
'node_runtime',
|
|
24
|
+
'cli_surface',
|
|
25
|
+
'mcp_entry',
|
|
26
|
+
'sensor_attribution',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const rootDirDefault = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasExactKeys(value, keys) {
|
|
37
|
+
const actualKeys = Object.keys(value).sort();
|
|
38
|
+
const expectedKeys = [...keys].sort();
|
|
39
|
+
return actualKeys.length === expectedKeys.length
|
|
40
|
+
&& actualKeys.every((key, index) => key === expectedKeys[index]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function check(id, ok, detail) {
|
|
44
|
+
return { id, status: ok ? 'ok' : 'failed', detail: String(detail).slice(0, MAX_DETAIL_LENGTH) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function isNonEmptyFile(absolutePath) {
|
|
48
|
+
try {
|
|
49
|
+
const stats = await lstat(absolutePath);
|
|
50
|
+
return stats.isFile() && stats.size > 0;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkPackageVersion(packageJson) {
|
|
57
|
+
const version = packageJson?.version;
|
|
58
|
+
if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {
|
|
59
|
+
return check('package_version', false, 'package.json version is missing or not semver');
|
|
60
|
+
}
|
|
61
|
+
if (version === UNKNOWN_VERSION_SENTINEL) {
|
|
62
|
+
// ADR 0049 Decision 3(c): sentinel versionはビルド破損=沈黙させない。
|
|
63
|
+
return check('package_version', false, `sentinel version ${UNKNOWN_VERSION_SENTINEL} means a broken build`);
|
|
64
|
+
}
|
|
65
|
+
return check('package_version', true, version);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkNodeRuntime(nodeVersion, enginesNode) {
|
|
69
|
+
const runtime = NODE_VERSION_PATTERN.exec(nodeVersion ?? '');
|
|
70
|
+
if (!runtime) {
|
|
71
|
+
return check('node_runtime', false, 'node version is unreadable');
|
|
72
|
+
}
|
|
73
|
+
const floor = ENGINES_FLOOR_RANGE_PATTERN.exec(enginesNode ?? '');
|
|
74
|
+
if (!floor) {
|
|
75
|
+
return check('node_runtime', false, 'package.json engines.node floor is unreadable');
|
|
76
|
+
}
|
|
77
|
+
const [major, minor] = [Number(runtime[1]), Number(runtime[2])];
|
|
78
|
+
const [floorMajor, floorMinor] = [Number(floor[2]), Number(floor[3] ?? '0')];
|
|
79
|
+
const satisfied = major > floorMajor || (major === floorMajor && minor >= floorMinor);
|
|
80
|
+
return check(
|
|
81
|
+
'node_runtime',
|
|
82
|
+
satisfied,
|
|
83
|
+
satisfied ? `${nodeVersion} satisfies engines.node floor ${floor[1]}`
|
|
84
|
+
: `${nodeVersion} is below engines.node floor ${floor[1]}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function checkCliSurface(rootDir) {
|
|
89
|
+
try {
|
|
90
|
+
const runtimeCli = await import(pathToFileURL(path.join(rootDir, 'src', 'runtime-cli.mjs')).href);
|
|
91
|
+
if (typeof runtimeCli.runRuntimeCli !== 'function') {
|
|
92
|
+
return check('cli_surface', false, 'src/runtime-cli.mjs does not export runRuntimeCli');
|
|
93
|
+
}
|
|
94
|
+
return check('cli_surface', true, 'runtime CLI surface loads and exports runRuntimeCli');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return check('cli_surface', false, `runtime CLI failed to load: ${error?.constructor?.name ?? 'Error'}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function checkMcpEntry(rootDir) {
|
|
101
|
+
const ok = await isNonEmptyFile(path.join(rootDir, 'bin', 'lattice-mcp.mjs'));
|
|
102
|
+
return check('mcp_entry', ok, ok ? 'bin/lattice-mcp.mjs is present' : 'bin/lattice-mcp.mjs is missing or empty');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function checkSensorAttribution(rootDir) {
|
|
106
|
+
// ADR 0047: fork吸収の受入条件=MIT license noticeとattributionの維持。
|
|
107
|
+
const license = await isNonEmptyFile(path.join(rootDir, 'sensor', 'LICENSE'));
|
|
108
|
+
const notice = await isNonEmptyFile(path.join(rootDir, 'sensor', 'NOTICE'));
|
|
109
|
+
const ok = license && notice;
|
|
110
|
+
return check(
|
|
111
|
+
'sensor_attribution',
|
|
112
|
+
ok,
|
|
113
|
+
ok ? 'sensor/LICENSE and sensor/NOTICE are present' : 'sensor/LICENSE or sensor/NOTICE is missing or empty',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds the bounded, read-only native factory diagnostics object.
|
|
119
|
+
* @param {{ rootDir?: string, nodeVersion?: string }} options
|
|
120
|
+
* @returns {Promise<object>}
|
|
121
|
+
*/
|
|
122
|
+
export async function buildFactoryDiagnostics({ rootDir = rootDirDefault, nodeVersion = process.version } = {}) {
|
|
123
|
+
let packageJson = null;
|
|
124
|
+
try {
|
|
125
|
+
packageJson = JSON.parse(await readFile(path.join(rootDir, 'package.json'), 'utf8'));
|
|
126
|
+
} catch {
|
|
127
|
+
packageJson = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const checks = [
|
|
131
|
+
checkPackageVersion(packageJson),
|
|
132
|
+
checkNodeRuntime(nodeVersion, packageJson?.engines?.node),
|
|
133
|
+
await checkCliSurface(rootDir),
|
|
134
|
+
await checkMcpEntry(rootDir),
|
|
135
|
+
await checkSensorAttribution(rootDir),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const diagnostics = {
|
|
139
|
+
schema: SCHEMA,
|
|
140
|
+
product: PRODUCT,
|
|
141
|
+
version: typeof packageJson?.version === 'string' ? packageJson.version.slice(0, MAX_DETAIL_LENGTH) : UNKNOWN_VERSION_SENTINEL,
|
|
142
|
+
overall: checks.every((entry) => entry.status === 'ok') ? 'ok' : 'failed',
|
|
143
|
+
checks,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!validateFactoryDiagnostics(diagnostics)) {
|
|
147
|
+
throw new TypeError('invalid factory diagnostics output');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return diagnostics;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Strictly validates the public native factory diagnostics schema.
|
|
155
|
+
* @param {unknown} value
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function validateFactoryDiagnostics(value) {
|
|
159
|
+
if (!isPlainObject(value) || !hasExactKeys(value, ['schema', 'product', 'version', 'overall', 'checks'])) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (value.schema !== SCHEMA || value.product !== PRODUCT) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (typeof value.version !== 'string' || value.version.length === 0 || value.version.length > MAX_DETAIL_LENGTH) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (value.overall !== 'ok' && value.overall !== 'failed') {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(value.checks) || value.checks.length !== CHECK_IDS.length) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const allOk = value.checks.every((entry) => isPlainObject(entry)
|
|
175
|
+
&& hasExactKeys(entry, ['id', 'status', 'detail'])
|
|
176
|
+
&& (entry.status === 'ok' || entry.status === 'failed')
|
|
177
|
+
&& typeof entry.detail === 'string'
|
|
178
|
+
&& entry.detail.length <= MAX_DETAIL_LENGTH);
|
|
179
|
+
if (!allOk) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const ids = value.checks.map((entry) => entry.id);
|
|
183
|
+
if (ids.some((id, index) => id !== CHECK_IDS[index])) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const expectedOverall = value.checks.every((entry) => entry.status === 'ok') ? 'ok' : 'failed';
|
|
187
|
+
return value.overall === expectedOverall;
|
|
188
|
+
}
|
package/src/hash-chain.mjs
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
/** Hash bytes without imposing an artifact schema or serialization limit. */
|
|
4
|
-
export function sha256Bytes(bytes) {
|
|
5
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Compute a self digest using the caller's canonical serializer. */
|
|
9
|
-
export function digestWithoutField(value, digestField, canonicalize) {
|
|
10
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
11
|
-
throw new TypeError('chain value must be a plain object');
|
|
12
|
-
}
|
|
13
|
-
const projection = {};
|
|
14
|
-
for (const key of Object.keys(value)) {
|
|
15
|
-
if (key !== digestField) projection[key] = value[key];
|
|
16
|
-
}
|
|
17
|
-
return sha256Bytes(Buffer.from(canonicalize(projection), 'utf8'));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Verify the storage order, sequence, genesis, self digest and previous link of
|
|
22
|
-
* a linear hash chain. Domain validators remain with each store family.
|
|
23
|
-
*/
|
|
24
|
-
export function verifyLinearHashChain({
|
|
25
|
-
entries,
|
|
26
|
-
canonicalize,
|
|
27
|
-
digestField,
|
|
28
|
-
sequenceField = 'sequence',
|
|
29
|
-
previousField = 'previous_digest',
|
|
30
|
-
genesisPrevious = null,
|
|
31
|
-
}) {
|
|
32
|
-
const failures = new Set();
|
|
33
|
-
const bySequence = new Map();
|
|
34
|
-
for (const entry of entries) {
|
|
35
|
-
const sequence = entry?.[sequenceField];
|
|
36
|
-
let bytes;
|
|
37
|
-
try {
|
|
38
|
-
bytes = canonicalize(entry);
|
|
39
|
-
if (entry[digestField] !== digestWithoutField(entry, digestField, canonicalize)) {
|
|
40
|
-
failures.add('event_digest_mismatch');
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
failures.add('event_shape');
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
const seen = bySequence.get(sequence);
|
|
47
|
-
if (seen === undefined) bySequence.set(sequence, { entries: [entry], bytes: new Set([bytes]) });
|
|
48
|
-
else {
|
|
49
|
-
seen.entries.push(entry);
|
|
50
|
-
seen.bytes.add(bytes);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
for (const seen of bySequence.values()) {
|
|
54
|
-
if (seen.entries.length > 1 && seen.bytes.size === 1) failures.add('duplicate_event');
|
|
55
|
-
if (seen.bytes.size > 1) failures.add('sequence_fork');
|
|
56
|
-
}
|
|
57
|
-
if (!entries.every((entry, index) => index === 0
|
|
58
|
-
|| entries[index - 1]?.[sequenceField] < entry?.[sequenceField])) {
|
|
59
|
-
failures.add('storage_order');
|
|
60
|
-
}
|
|
61
|
-
const sequences = [...bySequence.keys()].sort((left, right) => left - right);
|
|
62
|
-
if (sequences.length === 0 || sequences.some((sequence, index) => sequence !== index)) {
|
|
63
|
-
failures.add('sequence_gap');
|
|
64
|
-
}
|
|
65
|
-
const genesis = bySequence.get(0)?.entries[0];
|
|
66
|
-
if (genesis === undefined || genesis[previousField] !== genesisPrevious
|
|
67
|
-
|| entries.some((entry) => (entry[sequenceField] === 0) !== (entry[previousField] === genesisPrevious))) {
|
|
68
|
-
failures.add('genesis_binding');
|
|
69
|
-
}
|
|
70
|
-
for (let index = 1; index < sequences.length; index += 1) {
|
|
71
|
-
const previous = bySequence.get(sequences[index - 1]).entries[0];
|
|
72
|
-
const current = bySequence.get(sequences[index]).entries[0];
|
|
73
|
-
if (current[previousField] !== previous[digestField]) failures.add('digest_chain_mismatch');
|
|
74
|
-
}
|
|
75
|
-
return failures;
|
|
76
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/** Hash bytes without imposing an artifact schema or serialization limit. */
|
|
4
|
+
export function sha256Bytes(bytes) {
|
|
5
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Compute a self digest using the caller's canonical serializer. */
|
|
9
|
+
export function digestWithoutField(value, digestField, canonicalize) {
|
|
10
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
11
|
+
throw new TypeError('chain value must be a plain object');
|
|
12
|
+
}
|
|
13
|
+
const projection = {};
|
|
14
|
+
for (const key of Object.keys(value)) {
|
|
15
|
+
if (key !== digestField) projection[key] = value[key];
|
|
16
|
+
}
|
|
17
|
+
return sha256Bytes(Buffer.from(canonicalize(projection), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify the storage order, sequence, genesis, self digest and previous link of
|
|
22
|
+
* a linear hash chain. Domain validators remain with each store family.
|
|
23
|
+
*/
|
|
24
|
+
export function verifyLinearHashChain({
|
|
25
|
+
entries,
|
|
26
|
+
canonicalize,
|
|
27
|
+
digestField,
|
|
28
|
+
sequenceField = 'sequence',
|
|
29
|
+
previousField = 'previous_digest',
|
|
30
|
+
genesisPrevious = null,
|
|
31
|
+
}) {
|
|
32
|
+
const failures = new Set();
|
|
33
|
+
const bySequence = new Map();
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const sequence = entry?.[sequenceField];
|
|
36
|
+
let bytes;
|
|
37
|
+
try {
|
|
38
|
+
bytes = canonicalize(entry);
|
|
39
|
+
if (entry[digestField] !== digestWithoutField(entry, digestField, canonicalize)) {
|
|
40
|
+
failures.add('event_digest_mismatch');
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
failures.add('event_shape');
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const seen = bySequence.get(sequence);
|
|
47
|
+
if (seen === undefined) bySequence.set(sequence, { entries: [entry], bytes: new Set([bytes]) });
|
|
48
|
+
else {
|
|
49
|
+
seen.entries.push(entry);
|
|
50
|
+
seen.bytes.add(bytes);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const seen of bySequence.values()) {
|
|
54
|
+
if (seen.entries.length > 1 && seen.bytes.size === 1) failures.add('duplicate_event');
|
|
55
|
+
if (seen.bytes.size > 1) failures.add('sequence_fork');
|
|
56
|
+
}
|
|
57
|
+
if (!entries.every((entry, index) => index === 0
|
|
58
|
+
|| entries[index - 1]?.[sequenceField] < entry?.[sequenceField])) {
|
|
59
|
+
failures.add('storage_order');
|
|
60
|
+
}
|
|
61
|
+
const sequences = [...bySequence.keys()].sort((left, right) => left - right);
|
|
62
|
+
if (sequences.length === 0 || sequences.some((sequence, index) => sequence !== index)) {
|
|
63
|
+
failures.add('sequence_gap');
|
|
64
|
+
}
|
|
65
|
+
const genesis = bySequence.get(0)?.entries[0];
|
|
66
|
+
if (genesis === undefined || genesis[previousField] !== genesisPrevious
|
|
67
|
+
|| entries.some((entry) => (entry[sequenceField] === 0) !== (entry[previousField] === genesisPrevious))) {
|
|
68
|
+
failures.add('genesis_binding');
|
|
69
|
+
}
|
|
70
|
+
for (let index = 1; index < sequences.length; index += 1) {
|
|
71
|
+
const previous = bySequence.get(sequences[index - 1]).entries[0];
|
|
72
|
+
const current = bySequence.get(sequences[index]).entries[0];
|
|
73
|
+
if (current[previousField] !== previous[digestField]) failures.add('digest_chain_mismatch');
|
|
74
|
+
}
|
|
75
|
+
return failures;
|
|
76
|
+
}
|