@omgbase/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- package/package.json +80 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mintId } from "../core/ids.js";
|
|
2
|
+
import { ingestFile } from "../core/ingest.js";
|
|
3
|
+
import { ensureRepo } from "../core/attach.js";
|
|
4
|
+
import { makeReconcilingResolver } from "./reconciling-ingest.js";
|
|
5
|
+
import { observeOne } from "./observe.js";
|
|
6
|
+
import { sweepResurrectionPool } from "../core/store/gc.js";
|
|
7
|
+
import { tombstoneObservedDeletion } from "./tombstone.js";
|
|
8
|
+
/**
|
|
9
|
+
* Reconcile a batch of changed members against a repo as one checkpoint.
|
|
10
|
+
* `source.fetch(path)` supplies current bytes (or null = left the scope). The
|
|
11
|
+
* engine hashes the bytes itself: a hash equal to the stored file_hash is an
|
|
12
|
+
* echo (no commit); otherwise the member is ingested as an observed commit.
|
|
13
|
+
*/
|
|
14
|
+
export async function reconcileChanges(store, repoId, source, changes, opts = {}) {
|
|
15
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
16
|
+
const checkpointId = mintId("cp");
|
|
17
|
+
const ingested = [];
|
|
18
|
+
const suppressed = [];
|
|
19
|
+
const deleted = [];
|
|
20
|
+
const conflicted = [];
|
|
21
|
+
const fileEntries = [];
|
|
22
|
+
for (const change of changes) {
|
|
23
|
+
const item = await source.fetch(change.path);
|
|
24
|
+
if (item === null) {
|
|
25
|
+
// Member left the source scope (deleted/moved out): tombstone the live doc
|
|
26
|
+
// (drop FTS, tombstone blocks + doc, pool blocks) so it stops being served.
|
|
27
|
+
const existing = store.db
|
|
28
|
+
.prepare("SELECT doc_id, file_hash FROM docs WHERE repo_id = ? AND path = ? AND deleted_commit IS NULL")
|
|
29
|
+
.get(repoId, change.path);
|
|
30
|
+
if (existing) {
|
|
31
|
+
tombstoneObservedDeletion(store, repoId, existing.doc_id, ts);
|
|
32
|
+
deleted.push(change.path);
|
|
33
|
+
}
|
|
34
|
+
fileEntries.push([change.path, existing?.file_hash?.toString("hex") ?? null, null]);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Reconcile the fetched bytes through the shared observe primitive — the one
|
|
38
|
+
// implementation of echo gate + identity threading + conflict flagging (D2).
|
|
39
|
+
// (v1 sources are identity-inferred; borne identity is deferred, ADR-010/D4.)
|
|
40
|
+
const r = observeOne(store, repoId, change.path, item.content, ts);
|
|
41
|
+
if (r.echo)
|
|
42
|
+
suppressed.push(change.path);
|
|
43
|
+
else if (r.conflicted)
|
|
44
|
+
conflicted.push(change.path);
|
|
45
|
+
else
|
|
46
|
+
ingested.push(change.path);
|
|
47
|
+
fileEntries.push([change.path, r.oldHashHex, r.newHashHex]);
|
|
48
|
+
}
|
|
49
|
+
store.db
|
|
50
|
+
.prepare("INSERT INTO checkpoints (id, repo_id, ts, files, git_head) VALUES (?, ?, ?, ?, ?)")
|
|
51
|
+
.run(checkpointId, repoId, ts, JSON.stringify(fileEntries), opts.gitHead ?? null);
|
|
52
|
+
sweepResurrectionPool(store, ts);
|
|
53
|
+
return { checkpointId, ingested, suppressed, deleted, conflicted };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Attach a source scope as a repo: create/reuse the repo row, then ingest every
|
|
57
|
+
* member the source enumerates (identity-inferred sources thread the reconciling
|
|
58
|
+
* resolver so edges + identity are established on the initial walk).
|
|
59
|
+
*/
|
|
60
|
+
export async function attachSource(store, slug, rootPath, source) {
|
|
61
|
+
const repoId = ensureRepo(store, slug, rootPath);
|
|
62
|
+
const inferred = source.capabilities().identity === "inferred";
|
|
63
|
+
const ts = new Date().toISOString();
|
|
64
|
+
let fileCount = 0;
|
|
65
|
+
let allConverged = true;
|
|
66
|
+
for (const entry of await source.enumerate()) {
|
|
67
|
+
const item = await source.fetch(entry.path);
|
|
68
|
+
if (!item)
|
|
69
|
+
continue;
|
|
70
|
+
const res = ingestFile(store, repoId, entry.path, item.content, {
|
|
71
|
+
ts,
|
|
72
|
+
...(inferred ? { resolveIds: makeReconcilingResolver(store, repoId, { ts, path: entry.path }) } : {}),
|
|
73
|
+
});
|
|
74
|
+
fileCount++;
|
|
75
|
+
if (!res.converged)
|
|
76
|
+
allConverged = false;
|
|
77
|
+
}
|
|
78
|
+
return { repoId, fileCount, allConverged };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SyncSource } from "./plugin.js";
|
|
2
|
+
export interface ExternalSourceSpec {
|
|
3
|
+
/** Adapter command (argv[0]). */
|
|
4
|
+
command: string;
|
|
5
|
+
/** Rendered flags from the source config (13 §3.1). */
|
|
6
|
+
args: string[];
|
|
7
|
+
/** Extra environment (secrets; 13 §3.2). Merged over process.env. */
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Spawn an adapter and connect it as a SyncSource. Throws on spawn/handshake
|
|
12
|
+
* failure. Callers MUST call close() to terminate the process.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createExternalSource(spec: ExternalSourceSpec): Promise<SyncSource>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
/**
|
|
4
|
+
* Spawn an adapter and connect it as a SyncSource. Throws on spawn/handshake
|
|
5
|
+
* failure. Callers MUST call close() to terminate the process.
|
|
6
|
+
*/
|
|
7
|
+
export async function createExternalSource(spec) {
|
|
8
|
+
let child;
|
|
9
|
+
try {
|
|
10
|
+
child = spawn(spec.command, spec.args, {
|
|
11
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
12
|
+
env: { ...process.env, ...(spec.env ?? {}) },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new Error(`sync adapter '${spec.command}' failed to spawn: ${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
const rl = createInterface({ input: child.stdout });
|
|
19
|
+
let watchListener = null;
|
|
20
|
+
const router = messageRouter(rl, (paths) => watchListener?.(paths));
|
|
21
|
+
const died = new Promise((_, reject) => {
|
|
22
|
+
child.once("error", (err) => reject(new Error(`sync adapter '${spec.command}' failed to spawn: ${err.message}`)));
|
|
23
|
+
child.once("exit", (code) => reject(new Error(`sync adapter '${spec.command}' exited early (code ${code ?? "?"})`)));
|
|
24
|
+
});
|
|
25
|
+
// Handshake: the first line declares protocol + capabilities.
|
|
26
|
+
const handshakeRaw = await Promise.race([router.nextResponse(), died]);
|
|
27
|
+
let caps;
|
|
28
|
+
try {
|
|
29
|
+
const hs = JSON.parse(handshakeRaw);
|
|
30
|
+
const c = hs.capabilities ?? {};
|
|
31
|
+
caps = {
|
|
32
|
+
identity: c.identity === "borne" ? "borne" : "inferred",
|
|
33
|
+
writeThrough: Boolean(c.writeThrough),
|
|
34
|
+
watch: Boolean(c.watch),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`sync adapter '${spec.command}' sent an invalid handshake: ${handshakeRaw.slice(0, 120)}`);
|
|
39
|
+
}
|
|
40
|
+
let nextId = 1;
|
|
41
|
+
const call = async (method, params = {}) => {
|
|
42
|
+
const id = nextId++;
|
|
43
|
+
child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
|
|
44
|
+
for (;;) {
|
|
45
|
+
const raw = await Promise.race([router.nextResponse(), died]);
|
|
46
|
+
const msg = JSON.parse(raw);
|
|
47
|
+
if (msg.id !== id)
|
|
48
|
+
continue; // response to another in-flight call
|
|
49
|
+
if (msg.error)
|
|
50
|
+
throw new Error(`sync adapter error (${method}): ${msg.error}`);
|
|
51
|
+
return msg.result;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const source = {
|
|
55
|
+
capabilities: () => caps,
|
|
56
|
+
async enumerate() {
|
|
57
|
+
const r = (await call("enumerate"));
|
|
58
|
+
return r.entries ?? [];
|
|
59
|
+
},
|
|
60
|
+
async fetch(path) {
|
|
61
|
+
const r = (await call("fetch", { path }));
|
|
62
|
+
return r.item ?? null;
|
|
63
|
+
},
|
|
64
|
+
...(caps.watch
|
|
65
|
+
? {
|
|
66
|
+
async watch(onBatch) {
|
|
67
|
+
watchListener = onBatch;
|
|
68
|
+
await call("watch");
|
|
69
|
+
return {
|
|
70
|
+
async stop() {
|
|
71
|
+
watchListener = null;
|
|
72
|
+
try {
|
|
73
|
+
await call("unwatch");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// process may already be exiting; ignore.
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
: {}),
|
|
83
|
+
...(caps.writeThrough
|
|
84
|
+
? {
|
|
85
|
+
async write(path, content) {
|
|
86
|
+
await call("write", { path, content });
|
|
87
|
+
},
|
|
88
|
+
async remove(path) {
|
|
89
|
+
await call("remove", { path });
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
async close() {
|
|
94
|
+
rl.close();
|
|
95
|
+
child.stdin.end();
|
|
96
|
+
if (!child.killed)
|
|
97
|
+
child.kill("SIGTERM");
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return source;
|
|
101
|
+
}
|
|
102
|
+
// Demultiplex adapter stdout: {"event":"batch"} lines feed the watch listener;
|
|
103
|
+
// every other line is an id-addressed response buffered for nextResponse().
|
|
104
|
+
function messageRouter(rl, onBatch) {
|
|
105
|
+
const buffered = [];
|
|
106
|
+
const waiters = [];
|
|
107
|
+
rl.on("line", (line) => {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
const peek = JSON.parse(trimmed);
|
|
113
|
+
if (peek.event === "batch") {
|
|
114
|
+
onBatch(peek.paths ?? []);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Non-JSON on stdout is a protocol violation; surface it as a response so
|
|
120
|
+
// the awaiting call() rejects with a parse error rather than hanging.
|
|
121
|
+
}
|
|
122
|
+
const w = waiters.shift();
|
|
123
|
+
if (w)
|
|
124
|
+
w(trimmed);
|
|
125
|
+
else
|
|
126
|
+
buffered.push(trimmed);
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
nextResponse() {
|
|
130
|
+
const b = buffered.shift();
|
|
131
|
+
if (b !== undefined)
|
|
132
|
+
return Promise.resolve(b);
|
|
133
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=external-source.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import { type CheckpointResult } from "./checkpoint.js";
|
|
3
|
+
export interface SweepResult extends CheckpointResult {
|
|
4
|
+
scanned: number;
|
|
5
|
+
candidates: number;
|
|
6
|
+
changed: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Record/refresh a file's stat cache row. Call after any ingest of the file. */
|
|
9
|
+
export declare function recordFileStat(store: Store, repoId: string, path: string, abs: string, hash: Buffer): void;
|
|
10
|
+
/**
|
|
11
|
+
* Run the freshness sweep for a repo. Returns the checkpoint result plus scan
|
|
12
|
+
* counters. Idempotent; safe alongside a live watcher (hash-based echo
|
|
13
|
+
* suppression makes a double ingest a no-op).
|
|
14
|
+
*/
|
|
15
|
+
export declare function freshnessSweep(store: Store, repoId: string, rootPath: string): SweepResult;
|
|
16
|
+
/**
|
|
17
|
+
* Counts of ways the DB disagrees with the current filesystem. All fields are
|
|
18
|
+
* "not yet reconciled" states a freshnessSweep/ingest would resolve.
|
|
19
|
+
*/
|
|
20
|
+
export interface DiskDrift {
|
|
21
|
+
/** non-deleted docs whose on-disk content hash != the doc's stored file_hash (drifted edits). */
|
|
22
|
+
changed: number;
|
|
23
|
+
/** non-deleted docs whose file is no longer on disk (deletes not yet ingested). */
|
|
24
|
+
deleted: number;
|
|
25
|
+
/** *.md files on disk with no corresponding non-deleted doc (new files not yet ingested). */
|
|
26
|
+
untracked: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* READ-ONLY disk-drift detector. Mirrors freshnessSweep's staged cheap approach
|
|
30
|
+
* — stat-compare against file_stats first, hash only the stat-mismatched
|
|
31
|
+
* candidates — but never ingests, commits, or touches file_stats. It answers a
|
|
32
|
+
* single question: does the DB still agree with what's on disk right now?
|
|
33
|
+
*
|
|
34
|
+
* `changed` is decided by comparing a candidate's content hash to the doc's
|
|
35
|
+
* stored `file_hash` (the same sha256(bytes) checkpoint writes), so a stat
|
|
36
|
+
* change without a content change is NOT counted as drift. At the envelope
|
|
37
|
+
* (≤10⁴ docs) the clean case is a directory walk plus stats (tens of ms);
|
|
38
|
+
* hashing is bounded to files whose (mtime_ns, size) moved.
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectDiskDrift(store: Store, repoId: string, rootPath: string): DiskDrift;
|
|
41
|
+
/** Rebuild file_stats from scratch by re-statting + re-hashing every file. */
|
|
42
|
+
export declare function rebuildFileStats(store: Store, repoId: string, rootPath: string): number;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { statSync, readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sha256 } from "../core/hash.js";
|
|
4
|
+
import { processCheckpoint } from "./checkpoint.js";
|
|
5
|
+
import { walkMarkdown } from "./fs-util.js";
|
|
6
|
+
/** Record/refresh a file's stat cache row. Call after any ingest of the file. */
|
|
7
|
+
export function recordFileStat(store, repoId, path, abs, hash) {
|
|
8
|
+
if (!existsSync(abs)) {
|
|
9
|
+
store.db.prepare("DELETE FROM file_stats WHERE repo_id = ? AND path = ?").run(repoId, path);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const st = statSync(abs, { bigint: true });
|
|
13
|
+
store.db
|
|
14
|
+
.prepare(`INSERT INTO file_stats (repo_id, path, mtime_ns, size, hash) VALUES (?, ?, ?, ?, ?)
|
|
15
|
+
ON CONFLICT(repo_id, path) DO UPDATE SET mtime_ns = excluded.mtime_ns, size = excluded.size, hash = excluded.hash`)
|
|
16
|
+
.run(repoId, path, st.mtimeNs.toString(), Number(st.size), hash);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run the freshness sweep for a repo. Returns the checkpoint result plus scan
|
|
20
|
+
* counters. Idempotent; safe alongside a live watcher (hash-based echo
|
|
21
|
+
* suppression makes a double ingest a no-op).
|
|
22
|
+
*/
|
|
23
|
+
export function freshnessSweep(store, repoId, rootPath) {
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
for (const row of store.db
|
|
26
|
+
.prepare("SELECT path, mtime_ns, size, hash FROM file_stats WHERE repo_id = ?")
|
|
27
|
+
.all(repoId)) {
|
|
28
|
+
cache.set(row.path, { mtime_ns: BigInt(row.mtime_ns), size: row.size, hash: row.hash });
|
|
29
|
+
}
|
|
30
|
+
const paths = walkMarkdown(rootPath);
|
|
31
|
+
const seen = new Set(paths);
|
|
32
|
+
// Candidates: files whose (mtime_ns, size) differs from cache, or are new.
|
|
33
|
+
const candidates = [];
|
|
34
|
+
const freshStats = new Map();
|
|
35
|
+
for (const path of paths) {
|
|
36
|
+
const st = statSync(join(rootPath, path), { bigint: true });
|
|
37
|
+
freshStats.set(path, { mtimeNs: st.mtimeNs, size: Number(st.size) });
|
|
38
|
+
const cached = cache.get(path);
|
|
39
|
+
if (!cached || cached.mtime_ns !== st.mtimeNs || cached.size !== Number(st.size)) {
|
|
40
|
+
candidates.push(path);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Deletions: cached paths no longer on disk.
|
|
44
|
+
const deletedPaths = [...cache.keys()].filter((p) => !seen.has(p));
|
|
45
|
+
// Hash candidates; only those whose content hash actually differs from the
|
|
46
|
+
// cached hash need ingesting (mtime can change without content changing).
|
|
47
|
+
const changedPaths = [];
|
|
48
|
+
const candHashes = new Map();
|
|
49
|
+
for (const path of candidates) {
|
|
50
|
+
const hash = sha256(readFileSync(join(rootPath, path), "utf8"));
|
|
51
|
+
candHashes.set(path, hash);
|
|
52
|
+
const cached = cache.get(path);
|
|
53
|
+
if (!cached || !cached.hash.equals(hash))
|
|
54
|
+
changedPaths.push(path);
|
|
55
|
+
else {
|
|
56
|
+
// Content unchanged; refresh the stat cache so we don't re-hash next time.
|
|
57
|
+
const fs = freshStats.get(path);
|
|
58
|
+
store.db
|
|
59
|
+
.prepare("UPDATE file_stats SET mtime_ns = ?, size = ? WHERE repo_id = ? AND path = ?")
|
|
60
|
+
.run(fs.mtimeNs.toString(), fs.size, repoId, path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const toIngest = [...changedPaths, ...deletedPaths];
|
|
64
|
+
const result = processCheckpoint(store, repoId, rootPath, toIngest.map((path) => ({ path })));
|
|
65
|
+
// Refresh stat cache for everything we touched (ingested or echo-suppressed).
|
|
66
|
+
for (const path of changedPaths) {
|
|
67
|
+
recordFileStat(store, repoId, path, join(rootPath, path), candHashes.get(path));
|
|
68
|
+
}
|
|
69
|
+
for (const path of deletedPaths) {
|
|
70
|
+
store.db.prepare("DELETE FROM file_stats WHERE repo_id = ? AND path = ?").run(repoId, path);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...result,
|
|
74
|
+
scanned: paths.length,
|
|
75
|
+
candidates: candidates.length,
|
|
76
|
+
changed: result.ingested.length > 0 || result.deleted.length > 0 || result.conflicted.length > 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* READ-ONLY disk-drift detector. Mirrors freshnessSweep's staged cheap approach
|
|
81
|
+
* — stat-compare against file_stats first, hash only the stat-mismatched
|
|
82
|
+
* candidates — but never ingests, commits, or touches file_stats. It answers a
|
|
83
|
+
* single question: does the DB still agree with what's on disk right now?
|
|
84
|
+
*
|
|
85
|
+
* `changed` is decided by comparing a candidate's content hash to the doc's
|
|
86
|
+
* stored `file_hash` (the same sha256(bytes) checkpoint writes), so a stat
|
|
87
|
+
* change without a content change is NOT counted as drift. At the envelope
|
|
88
|
+
* (≤10⁴ docs) the clean case is a directory walk plus stats (tens of ms);
|
|
89
|
+
* hashing is bounded to files whose (mtime_ns, size) moved.
|
|
90
|
+
*/
|
|
91
|
+
export function detectDiskDrift(store, repoId, rootPath) {
|
|
92
|
+
// Stat cache keyed by path, for cheap change detection.
|
|
93
|
+
const cache = new Map();
|
|
94
|
+
for (const row of store.db
|
|
95
|
+
.prepare("SELECT path, mtime_ns, size, hash FROM file_stats WHERE repo_id = ?")
|
|
96
|
+
.all(repoId)) {
|
|
97
|
+
cache.set(row.path, { mtime_ns: BigInt(row.mtime_ns), size: row.size, hash: row.hash });
|
|
98
|
+
}
|
|
99
|
+
// Live docs keyed by path, with their durable file_hash (the convergence
|
|
100
|
+
// signal). This is the authority for changed/deleted/untracked, independent
|
|
101
|
+
// of the derived file_stats cache.
|
|
102
|
+
const docs = new Map();
|
|
103
|
+
for (const row of store.db
|
|
104
|
+
.prepare("SELECT path, file_hash FROM docs WHERE repo_id = ? AND deleted_commit IS NULL")
|
|
105
|
+
.all(repoId)) {
|
|
106
|
+
docs.set(row.path, row.file_hash);
|
|
107
|
+
}
|
|
108
|
+
const paths = walkMarkdown(rootPath);
|
|
109
|
+
const seen = new Set(paths);
|
|
110
|
+
// Candidates whose (mtime_ns, size) differs from the stat cache (or are new).
|
|
111
|
+
const candidates = [];
|
|
112
|
+
for (const path of paths) {
|
|
113
|
+
const st = statSync(join(rootPath, path), { bigint: true });
|
|
114
|
+
const cached = cache.get(path);
|
|
115
|
+
if (!cached || cached.mtime_ns !== st.mtimeNs || cached.size !== Number(st.size)) {
|
|
116
|
+
candidates.push(path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let changed = 0;
|
|
120
|
+
let untracked = 0;
|
|
121
|
+
for (const path of candidates) {
|
|
122
|
+
const doc = docs.get(path);
|
|
123
|
+
if (doc === undefined) {
|
|
124
|
+
// No non-deleted doc for an on-disk *.md ⇒ untracked (new file).
|
|
125
|
+
untracked += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const hash = sha256(readFileSync(join(rootPath, path), "utf8"));
|
|
129
|
+
// Genuinely drifted only if content hash differs from the doc's file_hash.
|
|
130
|
+
if (!doc || !doc.equals(hash))
|
|
131
|
+
changed += 1;
|
|
132
|
+
}
|
|
133
|
+
// A doc present in the DB whose path isn't on disk anymore is a pending delete.
|
|
134
|
+
let deleted = 0;
|
|
135
|
+
for (const path of docs.keys()) {
|
|
136
|
+
if (!seen.has(path))
|
|
137
|
+
deleted += 1;
|
|
138
|
+
}
|
|
139
|
+
return { changed, deleted, untracked };
|
|
140
|
+
}
|
|
141
|
+
/** Rebuild file_stats from scratch by re-statting + re-hashing every file. */
|
|
142
|
+
export function rebuildFileStats(store, repoId, rootPath) {
|
|
143
|
+
store.db.prepare("DELETE FROM file_stats WHERE repo_id = ?").run(repoId);
|
|
144
|
+
const paths = walkMarkdown(rootPath);
|
|
145
|
+
for (const path of paths) {
|
|
146
|
+
const abs = join(rootPath, path);
|
|
147
|
+
const hash = sha256(readFileSync(abs, "utf8"));
|
|
148
|
+
recordFileStat(store, repoId, path, abs, hash);
|
|
149
|
+
}
|
|
150
|
+
return paths.length;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=freshness.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Walk `root` for *.md files, returning repo-relative canonical paths. */
|
|
2
|
+
export declare function walkMarkdown(root: string): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Async variant of {@link walkMarkdown}: identical result, but yields to the
|
|
5
|
+
* event loop between directories and invokes `onFound(count)` as matches
|
|
6
|
+
* accumulate. This lets a caller drive a live progress counter (e.g. beside an
|
|
7
|
+
* interactive prompt) while the scan runs. `signal` lets the caller stop early
|
|
8
|
+
* once a decision has been made — the partial result is still returned.
|
|
9
|
+
*/
|
|
10
|
+
export declare function walkMarkdownAsync(root: string, onFound?: (count: number) => void, signal?: {
|
|
11
|
+
aborted: boolean;
|
|
12
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
// Shared filesystem walk for the in-process one-shot/reconcile paths (attach,
|
|
5
|
+
// checkpoint, freshness). This is NOT the live watcher — chokidar lives only in
|
|
6
|
+
// the external @omgbase/fs-adapter (sync-plugins). node:fs (a builtin) reads
|
|
7
|
+
// are retained here for the one-shot ingest/freshness fast-path.
|
|
8
|
+
const IGNORED_DIRS = new Set([".omgbase", ".git", "node_modules"]);
|
|
9
|
+
/** Walk `root` for *.md files, returning repo-relative canonical paths. */
|
|
10
|
+
export function walkMarkdown(root) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const recur = (dir) => {
|
|
13
|
+
for (const entry of readdirSync(dir)) {
|
|
14
|
+
if (IGNORED_DIRS.has(entry))
|
|
15
|
+
continue;
|
|
16
|
+
const full = join(dir, entry);
|
|
17
|
+
if (statSync(full).isDirectory())
|
|
18
|
+
recur(full);
|
|
19
|
+
else if (entry.endsWith(".md"))
|
|
20
|
+
out.push(relative(root, full).split(sep).join("/"));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
recur(root);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Async variant of {@link walkMarkdown}: identical result, but yields to the
|
|
28
|
+
* event loop between directories and invokes `onFound(count)` as matches
|
|
29
|
+
* accumulate. This lets a caller drive a live progress counter (e.g. beside an
|
|
30
|
+
* interactive prompt) while the scan runs. `signal` lets the caller stop early
|
|
31
|
+
* once a decision has been made — the partial result is still returned.
|
|
32
|
+
*/
|
|
33
|
+
export async function walkMarkdownAsync(root, onFound, signal) {
|
|
34
|
+
const out = [];
|
|
35
|
+
const recur = async (dir) => {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
return;
|
|
38
|
+
for (const entry of await readdir(dir)) {
|
|
39
|
+
if (signal?.aborted)
|
|
40
|
+
return;
|
|
41
|
+
if (IGNORED_DIRS.has(entry))
|
|
42
|
+
continue;
|
|
43
|
+
const full = join(dir, entry);
|
|
44
|
+
if ((await stat(full)).isDirectory())
|
|
45
|
+
await recur(full);
|
|
46
|
+
else if (entry.endsWith(".md")) {
|
|
47
|
+
out.push(relative(root, full).split(sep).join("/"));
|
|
48
|
+
onFound?.(out.length);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
await recur(root);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=fs-util.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Git-related heuristics (01 §6, 07 task 7.1). Conflict-marker detection flags a
|
|
2
|
+
// document `conflicted` (mutations refused until clean); whole-file hash match
|
|
3
|
+
// against a deleted/moved document detects a rename. These are hints layered on
|
|
4
|
+
// the checkpoint model — a checkout arrives as one large checkpoint that
|
|
5
|
+
// hash-locking makes cheap.
|
|
6
|
+
// Git conflict markers: <<<<<<< … ======= … >>>>>>> at line starts.
|
|
7
|
+
export function hasConflictMarkers(content) {
|
|
8
|
+
// Require both an opening and a closing marker to avoid false positives on
|
|
9
|
+
// prose that happens to start a line with '======='.
|
|
10
|
+
return /^<{7}/m.test(content) && /^>{7}/m.test(content);
|
|
11
|
+
}
|
|
12
|
+
export function detectRename(newFileHashHex, deletedCandidates) {
|
|
13
|
+
const match = deletedCandidates.find((c) => c.fileHashHex === newFileHashHex);
|
|
14
|
+
return match ? match.docId : null;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=git-heuristics.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
export interface ObserveResult {
|
|
3
|
+
docId: string;
|
|
4
|
+
path: string;
|
|
5
|
+
/** null on an echo (bytes already matched the stored revision — no commit). */
|
|
6
|
+
rev: string | null;
|
|
7
|
+
commitId: string | null;
|
|
8
|
+
/** convergence: sha256(bytes) === the committed revision's rendered_hash. */
|
|
9
|
+
converged: boolean;
|
|
10
|
+
/** true when the bytes already matched the stored revision (no new commit). */
|
|
11
|
+
echo: boolean;
|
|
12
|
+
/** true when the bytes carry git conflict markers (doc flagged conflicted). */
|
|
13
|
+
conflicted: boolean;
|
|
14
|
+
/** disposition kind → count for the commit this observation produced. */
|
|
15
|
+
dispositions: {
|
|
16
|
+
kind: string;
|
|
17
|
+
count: number;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
/** `observeOne` plus the change-tracking hashes batch callers need for their
|
|
21
|
+
* checkpoint file-entries: the prior `file_hash` and the new bytes' hash. */
|
|
22
|
+
export interface ObserveOneResult extends ObserveResult {
|
|
23
|
+
/** prior stored file_hash (hex), or null if the doc was new/absent. */
|
|
24
|
+
oldHashHex: string | null;
|
|
25
|
+
/** sha256(content) (hex) — the new file hash. */
|
|
26
|
+
newHashHex: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The reconcile primitive: echo-gate `content` for `path` and, when it differs
|
|
30
|
+
* from the stored revision, ingest it as an observed commit (reconciling block
|
|
31
|
+
* identity against the prior revision). Does NOT sweep the resurrection pool —
|
|
32
|
+
* the caller does that once per batch. Does NOT write a file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function observeOne(store: Store, repoId: string, path: string, content: string, ts: string): ObserveOneResult;
|
|
35
|
+
/** Observe one file's authoritative bytes as an observed commit (echo-suppressed). */
|
|
36
|
+
export declare function observeFile(store: Store, repoId: string, path: string, content: string, opts?: {
|
|
37
|
+
ts?: string;
|
|
38
|
+
}): ObserveResult;
|
|
39
|
+
/** Observe a batch of files under one timestamp + one resurrection sweep. The
|
|
40
|
+
* batch form of `observeFile`; each item is echo-gated independently. */
|
|
41
|
+
export declare function observeMany(store: Store, repoId: string, items: {
|
|
42
|
+
path: string;
|
|
43
|
+
content: string;
|
|
44
|
+
}[], opts?: {
|
|
45
|
+
ts?: string;
|
|
46
|
+
}): ObserveResult[];
|
|
47
|
+
export interface ObserveDeleteResult {
|
|
48
|
+
docId: string | null;
|
|
49
|
+
path: string;
|
|
50
|
+
/** true when a live doc existed at `path` and was tombstoned. */
|
|
51
|
+
deleted: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Observe that `path` left the source scope: tombstone the live doc as an
|
|
55
|
+
* OBSERVED deletion (blocks pooled for resurrection; no file removed — the file
|
|
56
|
+
* is already gone from the source). The deletion counterpart to `observeFile`,
|
|
57
|
+
* for a synchronizer mirroring an external delete. Idempotent: a path with no
|
|
58
|
+
* live doc is a no-op (`deleted:false`). Distinct from `docs_delete`, which is
|
|
59
|
+
* an api-origin, intentional, non-pooled removal that also unlinks the file.
|
|
60
|
+
*/
|
|
61
|
+
export declare function observeDelete(store: Store, repoId: string, path: string, opts?: {
|
|
62
|
+
ts?: string;
|
|
63
|
+
}): ObserveDeleteResult;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { sha256 } from "../core/hash.js";
|
|
2
|
+
import { ingestFile } from "../core/ingest.js";
|
|
3
|
+
import { makeReconcilingResolver } from "./reconciling-ingest.js";
|
|
4
|
+
import { hasConflictMarkers } from "./git-heuristics.js";
|
|
5
|
+
import { tombstoneObservedDeletion } from "./tombstone.js";
|
|
6
|
+
import { sweepResurrectionPool } from "../core/store/gc.js";
|
|
7
|
+
/**
|
|
8
|
+
* The reconcile primitive: echo-gate `content` for `path` and, when it differs
|
|
9
|
+
* from the stored revision, ingest it as an observed commit (reconciling block
|
|
10
|
+
* identity against the prior revision). Does NOT sweep the resurrection pool —
|
|
11
|
+
* the caller does that once per batch. Does NOT write a file.
|
|
12
|
+
*/
|
|
13
|
+
export function observeOne(store, repoId, path, content, ts) {
|
|
14
|
+
const existing = store.db
|
|
15
|
+
.prepare("SELECT doc_id, file_hash FROM docs WHERE repo_id = ? AND path = ? AND deleted_commit IS NULL")
|
|
16
|
+
.get(repoId, path);
|
|
17
|
+
const oldHashHex = existing?.file_hash?.toString("hex") ?? null;
|
|
18
|
+
const diskHash = sha256(content);
|
|
19
|
+
const newHashHex = diskHash.toString("hex");
|
|
20
|
+
// Echo: the stored revision already equals these bytes → no commit. The
|
|
21
|
+
// loop-breaker the coordinator relies on (§6): re-observing what the engine
|
|
22
|
+
// already holds is a no-op.
|
|
23
|
+
if (existing && existing.file_hash && existing.file_hash.equals(diskHash)) {
|
|
24
|
+
return { docId: existing.doc_id, path, rev: null, commitId: null, converged: true, echo: true, conflicted: false, dispositions: [], oldHashHex, newHashHex };
|
|
25
|
+
}
|
|
26
|
+
const conflicted = hasConflictMarkers(content);
|
|
27
|
+
const resolveIds = makeReconcilingResolver(store, repoId, { ts, path });
|
|
28
|
+
const res = ingestFile(store, repoId, path, content, { ts, origin: "observed", resolveIds });
|
|
29
|
+
store.db.prepare("UPDATE docs SET conflicted = ? WHERE repo_id = ? AND path = ?").run(conflicted ? 1 : 0, repoId, path);
|
|
30
|
+
const rows = store.db
|
|
31
|
+
.prepare("SELECT kind, count(*) n FROM dispositions WHERE commit_id = ? GROUP BY kind ORDER BY kind")
|
|
32
|
+
.all(res.commitId);
|
|
33
|
+
return {
|
|
34
|
+
docId: res.docId,
|
|
35
|
+
path,
|
|
36
|
+
rev: res.revId,
|
|
37
|
+
commitId: res.commitId,
|
|
38
|
+
converged: res.converged,
|
|
39
|
+
echo: false,
|
|
40
|
+
conflicted,
|
|
41
|
+
dispositions: rows.map((r) => ({ kind: r.kind, count: r.n })),
|
|
42
|
+
oldHashHex,
|
|
43
|
+
newHashHex,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function toPublic(r) {
|
|
47
|
+
const { oldHashHex: _o, newHashHex: _n, ...pub } = r;
|
|
48
|
+
return pub;
|
|
49
|
+
}
|
|
50
|
+
/** Observe one file's authoritative bytes as an observed commit (echo-suppressed). */
|
|
51
|
+
export function observeFile(store, repoId, path, content, opts = {}) {
|
|
52
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
53
|
+
const r = observeOne(store, repoId, path, content, ts);
|
|
54
|
+
sweepResurrectionPool(store, ts);
|
|
55
|
+
return toPublic(r);
|
|
56
|
+
}
|
|
57
|
+
/** Observe a batch of files under one timestamp + one resurrection sweep. The
|
|
58
|
+
* batch form of `observeFile`; each item is echo-gated independently. */
|
|
59
|
+
export function observeMany(store, repoId, items, opts = {}) {
|
|
60
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
61
|
+
const out = items.map((it) => toPublic(observeOne(store, repoId, it.path, it.content, ts)));
|
|
62
|
+
sweepResurrectionPool(store, ts);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Observe that `path` left the source scope: tombstone the live doc as an
|
|
67
|
+
* OBSERVED deletion (blocks pooled for resurrection; no file removed — the file
|
|
68
|
+
* is already gone from the source). The deletion counterpart to `observeFile`,
|
|
69
|
+
* for a synchronizer mirroring an external delete. Idempotent: a path with no
|
|
70
|
+
* live doc is a no-op (`deleted:false`). Distinct from `docs_delete`, which is
|
|
71
|
+
* an api-origin, intentional, non-pooled removal that also unlinks the file.
|
|
72
|
+
*/
|
|
73
|
+
export function observeDelete(store, repoId, path, opts = {}) {
|
|
74
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
75
|
+
const existing = store.db
|
|
76
|
+
.prepare("SELECT doc_id FROM docs WHERE repo_id = ? AND path = ? AND deleted_commit IS NULL")
|
|
77
|
+
.get(repoId, path);
|
|
78
|
+
if (!existing)
|
|
79
|
+
return { docId: null, path, deleted: false };
|
|
80
|
+
tombstoneObservedDeletion(store, repoId, existing.doc_id, ts);
|
|
81
|
+
sweepResurrectionPool(store, ts);
|
|
82
|
+
return { docId: existing.doc_id, path, deleted: true };
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=observe.js.map
|