amalgm 0.1.245 → 0.1.246
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/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The referee for the INCREMENTAL detector: re-read enrolled ground
|
|
5
|
+
* from scratch and report every divergence from what the store
|
|
6
|
+
* believes. It shares no part of the path it judges — no stats
|
|
7
|
+
* shortcut, no doorbells, no dirty sets, no compare functions, no
|
|
8
|
+
* applier — so a belief the scan reached by shortcut is never
|
|
9
|
+
* confirmed by that same shortcut.
|
|
10
|
+
*
|
|
11
|
+
* It does share detection's PRIMITIVES: the same enrollment policy,
|
|
12
|
+
* the same walk, the same truth adapter. That is deliberate — one
|
|
13
|
+
* definition of each, none left to drift — and it bounds the verdict
|
|
14
|
+
* exactly: this referee grades the machinery, never the primitives it
|
|
15
|
+
* asks. Grading those belongs to the oracle
|
|
16
|
+
* (tests/entity-oracle/referee.js), which enumerates and hashes with
|
|
17
|
+
* its own code and none of this layer's, driven by the storms.
|
|
18
|
+
*
|
|
19
|
+
* Read-only: no events, no store writes. An empty result means no
|
|
20
|
+
* belief diverged from the ground the primitives reported. Ground
|
|
21
|
+
* that could not be read reports as an `unsettled:` entry —
|
|
22
|
+
* inconclusive there, never clean and never a divergence.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const { censusBoundaries, gitDirTruth, walkRoot } = require('./scan');
|
|
29
|
+
const { adapterFor } = require('../adapters');
|
|
30
|
+
const { lookFailed } = require('../adapters/truth');
|
|
31
|
+
|
|
32
|
+
// The same socket the observer reads: one three-state answer.
|
|
33
|
+
const fileTruth = adapterFor('file.binary').rails.detect.truth;
|
|
34
|
+
|
|
35
|
+
const UNSETTLED = 'unsettled: the ground could not be read — verification is inconclusive here';
|
|
36
|
+
|
|
37
|
+
function createVerify({ store, shouldEnroll }) {
|
|
38
|
+
return function verify(rootId) {
|
|
39
|
+
const mismatches = [];
|
|
40
|
+
for (const root of store.listRoots()) {
|
|
41
|
+
if (rootId && root.rootId !== rootId) continue;
|
|
42
|
+
if (root.kind === 'repo') {
|
|
43
|
+
// Equality both ways: a registered territory whose .git left disk
|
|
44
|
+
// is as much a divergence as an unregistered one on disk — and a
|
|
45
|
+
// territory that could not be inspected is neither.
|
|
46
|
+
const territory = gitDirTruth(root.path);
|
|
47
|
+
if (territory === 'unsettled') {
|
|
48
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: UNSETTLED });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (territory === 'absent') {
|
|
52
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'ghost territory: the observer believes in a repository disk no longer has' });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// The census referee: every `.git` boundary on disk must have a
|
|
56
|
+
// registered claimant. Grandchildren are checked when their own
|
|
57
|
+
// parent root comes through this loop. Ground the census could
|
|
58
|
+
// not list is inconclusive — it may hide a boundary.
|
|
59
|
+
const census = censusBoundaries(root.path, shouldEnroll);
|
|
60
|
+
for (const relDir of census.unsettled) {
|
|
61
|
+
mismatches.push({ rootId: root.rootId, path: relDir || '.', problem: UNSETTLED });
|
|
62
|
+
}
|
|
63
|
+
for (const relDir of census.boundaries) {
|
|
64
|
+
const claimant = store.rootByPath(path.join(root.path, relDir));
|
|
65
|
+
if (!claimant || claimant.kind !== 'repo') {
|
|
66
|
+
mismatches.push({ rootId: root.rootId, path: relDir, problem: 'unclaimed territory: disk has a repository the observer never registered' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (root.kind === 'file') {
|
|
72
|
+
// The referee for the smallest ground: one path, one truth —
|
|
73
|
+
// and disk decides the truth's kind, exactly like the scan.
|
|
74
|
+
const row = store.fileByPath(root.rootId, '');
|
|
75
|
+
const linkRow = store.linksForRoot(root.rootId)[0] ?? null;
|
|
76
|
+
let stat = null;
|
|
77
|
+
try {
|
|
78
|
+
stat = fs.lstatSync(root.path);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (lookFailed(error).truth === 'unsettled') {
|
|
81
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: UNSETTLED });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
stat = null;
|
|
85
|
+
}
|
|
86
|
+
if (stat && stat.isFile()) {
|
|
87
|
+
const read = fileTruth(root.path);
|
|
88
|
+
if (read.truth === 'unsettled') {
|
|
89
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: UNSETTLED });
|
|
90
|
+
} else if (read.truth === 'present' && read.state.kind === 'file') {
|
|
91
|
+
if (!row) {
|
|
92
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'unknown: disk has a file the observer never saw' });
|
|
93
|
+
} else if (row.contentHash !== read.state.sha256) {
|
|
94
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'stale: disk moved, the observer did not notice' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (linkRow) {
|
|
98
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'ghost: the observer believes in a link disk no longer has' });
|
|
99
|
+
}
|
|
100
|
+
} else if (stat && stat.isSymbolicLink()) {
|
|
101
|
+
let target = null;
|
|
102
|
+
try {
|
|
103
|
+
target = fs.readlinkSync(root.path);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (lookFailed(error).truth === 'unsettled') {
|
|
106
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: UNSETTLED });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
target = null; // vanished mid-verify
|
|
110
|
+
}
|
|
111
|
+
if (target !== null) {
|
|
112
|
+
if (!linkRow) {
|
|
113
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'unknown: disk has a link the observer never saw' });
|
|
114
|
+
} else if (linkRow.target !== target) {
|
|
115
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'stale: the link was retargeted, the observer did not notice' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (row) {
|
|
119
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'ghost: the observer believes in a file disk no longer has' });
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
if (row) {
|
|
123
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'ghost: the observer believes in a file disk no longer has' });
|
|
124
|
+
}
|
|
125
|
+
if (linkRow) {
|
|
126
|
+
mismatches.push({ rootId: root.rootId, path: '.', problem: 'ghost: the observer believes in a link disk no longer has' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (root.kind !== 'files') continue;
|
|
132
|
+
const rows = new Map(store.filesForRoot(root.rootId).map((row) => [row.relPath, row]));
|
|
133
|
+
const { files, dirs, links, repoDirs, unsettled } = walkRoot(root.path, shouldEnroll);
|
|
134
|
+
// Ground the walk could not read is inconclusive — one entry per
|
|
135
|
+
// unsettled path, and NO conclusion beneath one: not clean, not
|
|
136
|
+
// ghost.
|
|
137
|
+
const underUnsettled = (relPath) =>
|
|
138
|
+
unsettled.some((dir) => dir === '' || relPath === dir || relPath.startsWith(`${dir}/`));
|
|
139
|
+
for (const relPath of unsettled) {
|
|
140
|
+
mismatches.push({ rootId: root.rootId, path: relPath || '.', problem: UNSETTLED });
|
|
141
|
+
}
|
|
142
|
+
const linkRows = new Map(store.linksForRoot(root.rootId).map((row) => [row.relPath, row]));
|
|
143
|
+
for (const entry of links) {
|
|
144
|
+
const row = linkRows.get(entry.relPath);
|
|
145
|
+
if (!row) {
|
|
146
|
+
mismatches.push({ rootId: root.rootId, path: entry.relPath, problem: 'unknown: disk has a link the observer never saw' });
|
|
147
|
+
} else if (row.target !== entry.target) {
|
|
148
|
+
mismatches.push({ rootId: root.rootId, path: entry.relPath, problem: 'stale: the link was retargeted, the observer did not notice' });
|
|
149
|
+
}
|
|
150
|
+
linkRows.delete(entry.relPath);
|
|
151
|
+
}
|
|
152
|
+
for (const relPath of linkRows.keys()) {
|
|
153
|
+
if (underUnsettled(relPath)) continue;
|
|
154
|
+
mismatches.push({ rootId: root.rootId, path: relPath, problem: 'ghost: the observer believes in a link disk no longer has' });
|
|
155
|
+
}
|
|
156
|
+
const dirRows = new Set(store.dirsForRoot(root.rootId).map((row) => row.relPath));
|
|
157
|
+
for (const entry of dirs) {
|
|
158
|
+
if (!dirRows.delete(entry.relPath)) {
|
|
159
|
+
mismatches.push({ rootId: root.rootId, path: entry.relPath, problem: 'unknown: disk has a directory the observer never saw' });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const relPath of dirRows) {
|
|
163
|
+
if (underUnsettled(relPath)) continue;
|
|
164
|
+
mismatches.push({ rootId: root.rootId, path: relPath, problem: 'ghost: the observer believes in a directory disk no longer has' });
|
|
165
|
+
}
|
|
166
|
+
for (const relDir of repoDirs) {
|
|
167
|
+
const claimant = store.rootByPath(relDir ? path.join(root.path, relDir) : root.path);
|
|
168
|
+
if (!claimant || claimant.kind !== 'repo') {
|
|
169
|
+
mismatches.push({ rootId: root.rootId, path: relDir || '.', problem: 'unclaimed territory: disk has a repository the observer never registered' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const entry of files) {
|
|
173
|
+
const read = fileTruth(path.join(root.path, entry.relPath));
|
|
174
|
+
if (read.truth === 'unsettled') {
|
|
175
|
+
mismatches.push({ rootId: root.rootId, path: entry.relPath, problem: UNSETTLED });
|
|
176
|
+
rows.delete(entry.relPath); // inconclusive, not ghost
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (read.truth !== 'present' || read.state.kind !== 'file') {
|
|
180
|
+
rows.delete(entry.relPath); // vanished mid-verify: a race, not a conclusion
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const row = rows.get(entry.relPath);
|
|
184
|
+
if (!row) {
|
|
185
|
+
mismatches.push({ rootId: root.rootId, path: entry.relPath, problem: 'unknown: disk has a file the observer never saw' });
|
|
186
|
+
} else if (row.contentHash !== read.state.sha256) {
|
|
187
|
+
mismatches.push({ rootId: root.rootId, path: entry.relPath, problem: 'stale: disk moved, the observer did not notice' });
|
|
188
|
+
}
|
|
189
|
+
rows.delete(entry.relPath);
|
|
190
|
+
}
|
|
191
|
+
for (const row of rows.values()) {
|
|
192
|
+
if (underUnsettled(row.relPath)) continue;
|
|
193
|
+
mismatches.push({ rootId: root.rootId, path: row.relPath, problem: 'ghost: the observer believes in a file disk no longer has' });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return mismatches;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { createVerify };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The doorbell half of detection: one recursive OS watcher per root.
|
|
5
|
+
* macOS = FSEvents, Windows = ReadDirectoryChangesW, Linux = inotify
|
|
6
|
+
* (recursive support requires Node >= 20, which the engine already requires).
|
|
7
|
+
*
|
|
8
|
+
* A watcher is allowed to be lossy and allowed to die — but death is an
|
|
9
|
+
* event like any other: `onDead` fires exactly once so the owner can
|
|
10
|
+
* rebuild the doorbell and reconcile the gap. A watcher that cannot even
|
|
11
|
+
* be born returns already `degraded` and never calls `onDead`. Nothing
|
|
12
|
+
* here throws past this module.
|
|
13
|
+
*
|
|
14
|
+
* A handle is { degraded, close }. The watcher never mutates the ground
|
|
15
|
+
* it observes: its only job is to carry filesystem doorbells to its owner.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
function install(dirPath, recursive, onDirty, onDead) {
|
|
20
|
+
const handle = { degraded: false, close: () => {} };
|
|
21
|
+
let watcher = null;
|
|
22
|
+
try {
|
|
23
|
+
watcher = fs.watch(dirPath, { recursive, persistent: false }, (_event, filename) => {
|
|
24
|
+
onDirty(filename ? String(filename).split('\\').join('/') : null);
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
handle.degraded = true;
|
|
28
|
+
return handle;
|
|
29
|
+
}
|
|
30
|
+
watcher.on('error', () => {
|
|
31
|
+
if (handle.degraded) return; // dead is dead: one death, one report
|
|
32
|
+
handle.degraded = true;
|
|
33
|
+
try { watcher.close(); } catch { /* already dead */ }
|
|
34
|
+
if (onDead) onDead();
|
|
35
|
+
});
|
|
36
|
+
handle.close = () => {
|
|
37
|
+
try { watcher.close(); } catch { /* already dead */ }
|
|
38
|
+
};
|
|
39
|
+
return handle;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function watchRecursive(rootPath, onDirty, onDead) {
|
|
43
|
+
return install(rootPath, true, onDirty, onDead);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The non-recursive doorbell: one directory, every event, the raw name
|
|
48
|
+
* delivered (or null, which could be anything). It hears EVERYTHING —
|
|
49
|
+
* filtering is the consumer's business, because a delivered name proves
|
|
50
|
+
* nothing about what it means: a rename of A to B may lawfully deliver
|
|
51
|
+
* only "B", and a doorbell that filters for "A" has decided the answer
|
|
52
|
+
* before looking. Two consumers ride it: a single-file root's main
|
|
53
|
+
* doorbell (the file's own inode goes deaf on atomic saves; the parent
|
|
54
|
+
* directory does not) and every top-level root's address doorbell (the
|
|
55
|
+
* parent observes address changes; the entity observes content
|
|
56
|
+
* changes). Same death contract as watchRecursive.
|
|
57
|
+
*/
|
|
58
|
+
function watchDir(dirPath, onDirty, onDead) {
|
|
59
|
+
return install(dirPath, false, onDirty, onDead);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { watchDir, watchRecursive };
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* system-instructions.md global user-authored instructions under AMALGM_DIR
|
|
17
17
|
*
|
|
18
18
|
* Direct disk edits (editors, agents with their own file tools) are picked up
|
|
19
|
-
* by the per-project fs watcher
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* by the per-project fs watcher and the fs REST hooks, which funnel into the
|
|
20
|
+
* same reconcile path. Reconcile is idempotent: unchanged revisions emit no
|
|
21
|
+
* events.
|
|
22
22
|
*
|
|
23
23
|
* Everything that emits events runs in the amalgm-mcp process — Local Live
|
|
24
24
|
* only streams events appended in-process. chat-core (chat-server process)
|
|
@@ -56,13 +56,11 @@ const {
|
|
|
56
56
|
|
|
57
57
|
const RESOURCE = 'project_context';
|
|
58
58
|
const WATCH_DEBOUNCE_MS = 200;
|
|
59
|
-
const WATCH_VERIFY_MS = 5_000;
|
|
60
59
|
// The system scope carries user-authored instructions that apply to every
|
|
61
60
|
// session on this computer. It has no change log or project state.
|
|
62
61
|
const SYSTEM_CONTEXT_ID = 'system';
|
|
63
62
|
|
|
64
|
-
const watchers = new Map(); // projectId -> { watcher, timer,
|
|
65
|
-
let watchVerificationTimer = null;
|
|
63
|
+
const watchers = new Map(); // projectId -> { watcher, timer, projectPath }
|
|
66
64
|
|
|
67
65
|
function systemProject() {
|
|
68
66
|
return { id: SYSTEM_CONTEXT_ID, path: path.resolve(AMALGM_DIR), name: 'Amalgm System' };
|
|
@@ -685,50 +683,12 @@ function closeWatcher(projectId) {
|
|
|
685
683
|
const entry = watchers.get(projectId);
|
|
686
684
|
if (!entry) return;
|
|
687
685
|
if (entry.timer) clearTimeout(entry.timer);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// Already closed.
|
|
693
|
-
}
|
|
686
|
+
try {
|
|
687
|
+
entry.watcher.close();
|
|
688
|
+
} catch {
|
|
689
|
+
// Already closed.
|
|
694
690
|
}
|
|
695
691
|
watchers.delete(projectId);
|
|
696
|
-
if (watchers.size === 0 && watchVerificationTimer) {
|
|
697
|
-
clearInterval(watchVerificationTimer);
|
|
698
|
-
watchVerificationTimer = null;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* `fs.watch` is a latency hint, not durable evidence: kernels may coalesce or
|
|
704
|
-
* drop notifications. This bounded pass owns that wait and makes every
|
|
705
|
-
* watched context converge even when no callback arrives.
|
|
706
|
-
*/
|
|
707
|
-
function verifyProjectContextWatchers() {
|
|
708
|
-
for (const entry of watchers.values()) {
|
|
709
|
-
try {
|
|
710
|
-
reconcileProjectContext(entry.project, { source: 'project-context:verify' });
|
|
711
|
-
if (!entry.watcher) {
|
|
712
|
-
entry.watcher = openProjectContextWatcher(entry.project);
|
|
713
|
-
}
|
|
714
|
-
} catch (error) {
|
|
715
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
716
|
-
console.warn(
|
|
717
|
-
`[ProjectContext] Bounded verify failed for ${entry.projectPath}: ${message}`,
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function ensureWatchVerification() {
|
|
724
|
-
if (watchVerificationTimer || watchers.size === 0) return;
|
|
725
|
-
watchVerificationTimer = setInterval(
|
|
726
|
-
verifyProjectContextWatchers,
|
|
727
|
-
WATCH_VERIFY_MS,
|
|
728
|
-
);
|
|
729
|
-
if (typeof watchVerificationTimer.unref === 'function') {
|
|
730
|
-
watchVerificationTimer.unref();
|
|
731
|
-
}
|
|
732
692
|
}
|
|
733
693
|
|
|
734
694
|
/**
|
|
@@ -737,7 +697,13 @@ function ensureWatchVerification() {
|
|
|
737
697
|
* harmless: by the time the debounce fires, disk matches the row and no event
|
|
738
698
|
* is emitted.
|
|
739
699
|
*/
|
|
740
|
-
function
|
|
700
|
+
function watchProjectContext(input) {
|
|
701
|
+
const project = projectFromInput(input);
|
|
702
|
+
if (!project?.id || !project?.path) return;
|
|
703
|
+
const existing = watchers.get(project.id);
|
|
704
|
+
if (existing?.projectPath === project.path) return;
|
|
705
|
+
if (existing) closeWatcher(project.id);
|
|
706
|
+
|
|
741
707
|
const root = contextPathsFor(project).root;
|
|
742
708
|
let watcher;
|
|
743
709
|
try {
|
|
@@ -757,46 +723,12 @@ function openProjectContextWatcher(project) {
|
|
|
757
723
|
if (typeof entry.timer.unref === 'function') entry.timer.unref();
|
|
758
724
|
});
|
|
759
725
|
} catch {
|
|
760
|
-
//
|
|
761
|
-
return null;
|
|
762
|
-
}
|
|
763
|
-
watcher.on('error', () => {
|
|
764
|
-
const entry = watchers.get(project.id);
|
|
765
|
-
if (!entry || entry.watcher !== watcher) return;
|
|
766
|
-
try {
|
|
767
|
-
watcher.close();
|
|
768
|
-
} catch {
|
|
769
|
-
// Already closed.
|
|
770
|
-
}
|
|
771
|
-
entry.watcher = null;
|
|
772
|
-
});
|
|
773
|
-
if (typeof watcher.unref === 'function') watcher.unref();
|
|
774
|
-
return watcher;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function watchProjectContext(input) {
|
|
778
|
-
const project = projectFromInput(input);
|
|
779
|
-
if (!project?.id || !project?.path) return;
|
|
780
|
-
const existing = watchers.get(project.id);
|
|
781
|
-
if (existing?.projectPath === project.path) {
|
|
782
|
-
existing.project = project;
|
|
783
|
-
if (!existing.watcher) {
|
|
784
|
-
existing.watcher = openProjectContextWatcher(project);
|
|
785
|
-
}
|
|
786
|
-
ensureWatchVerification();
|
|
726
|
+
// Context dir missing or unwatchable — best effort; REST hooks still cover writes.
|
|
787
727
|
return;
|
|
788
728
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
watcher: null,
|
|
793
|
-
timer: null,
|
|
794
|
-
project,
|
|
795
|
-
projectPath: project.path,
|
|
796
|
-
};
|
|
797
|
-
watchers.set(project.id, entry);
|
|
798
|
-
entry.watcher = openProjectContextWatcher(project);
|
|
799
|
-
ensureWatchVerification();
|
|
729
|
+
watcher.on('error', () => closeWatcher(project.id));
|
|
730
|
+
if (typeof watcher.unref === 'function') watcher.unref();
|
|
731
|
+
watchers.set(project.id, { watcher, timer: null, projectPath: project.path });
|
|
800
732
|
}
|
|
801
733
|
|
|
802
734
|
/** fs REST hook: reconcile when a context doc is touched outside the store. */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Amalgm's file-type policy: which physical type a piece of file ground
|
|
5
|
+
* takes. Classification is an INJECTED policy everywhere it is used
|
|
6
|
+
* (registration and its lenses refuse to construct without one), so it
|
|
7
|
+
* lives in its own file with no other job — one definition, replaceable
|
|
8
|
+
* whole.
|
|
9
|
+
*
|
|
10
|
+
* The rule is the oldest one there is: bytes holding a NUL are not text.
|
|
11
|
+
* What matters is WHERE that fact comes from. It is not read here.
|
|
12
|
+
* Detect's single read of a file produces its fingerprint and this
|
|
13
|
+
* report together (adapters/filesystem.js), and this policy only names
|
|
14
|
+
* the type that report implies.
|
|
15
|
+
*
|
|
16
|
+
* That is the whole law: A FILE'S HASH AND ITS TYPE DESCRIBE THE SAME
|
|
17
|
+
* OBSERVED BYTES. A policy that opened the ground itself would be a
|
|
18
|
+
* second look — and a second look can meet different bytes, or none at
|
|
19
|
+
* all, leaving a record whose type contradicts the payload beside it.
|
|
20
|
+
* So this function takes evidence, never a path; it is total, and it
|
|
21
|
+
* cannot fail.
|
|
22
|
+
*
|
|
23
|
+
* Both types travel as exact versions today; the distinction reserves
|
|
24
|
+
* `file.text` for the richer merge behavior it earns when Send exists
|
|
25
|
+
* (docs/realtime-rails.md §1).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @param {{ binary: boolean }} evidence — Detect's report on the bytes it hashed. */
|
|
29
|
+
function classifyFile({ binary }) {
|
|
30
|
+
return binary ? 'file.binary' : 'file.text';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { classifyFile };
|