@omgbase/core 0.11.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/src/core/ids.d.ts +4 -0
  3. package/dist/src/core/ids.js +17 -0
  4. package/dist/src/core/ingest.d.ts +39 -3
  5. package/dist/src/core/ingest.js +233 -202
  6. package/dist/src/core/store/schema.d.ts +3 -2
  7. package/dist/src/core/store/schema.js +28 -3
  8. package/dist/src/core/store/store.d.ts +9 -0
  9. package/dist/src/core/store/store.js +11 -0
  10. package/dist/src/graph/history.js +15 -12
  11. package/dist/src/index.d.ts +9 -5
  12. package/dist/src/index.js +14 -7
  13. package/dist/src/mcp/arguments.d.ts +9 -2
  14. package/dist/src/mcp/arguments.js +15 -2
  15. package/dist/src/mcp/errors.d.ts +1 -1
  16. package/dist/src/mcp/errors.js +3 -0
  17. package/dist/src/mcp/server.d.ts +22 -0
  18. package/dist/src/mcp/server.js +262 -185
  19. package/dist/src/mutate/apply.d.ts +93 -12
  20. package/dist/src/mutate/apply.js +203 -113
  21. package/dist/src/mutate/commit.d.ts +150 -0
  22. package/dist/src/mutate/commit.js +349 -0
  23. package/dist/src/mutate/doc-store.d.ts +3 -3
  24. package/dist/src/mutate/doc-store.js +21 -6
  25. package/dist/src/mutate/docs.d.ts +42 -87
  26. package/dist/src/mutate/docs.js +50 -204
  27. package/dist/src/mutate/effects.d.ts +331 -0
  28. package/dist/src/mutate/effects.js +981 -0
  29. package/dist/src/mutate/materialize.d.ts +49 -0
  30. package/dist/src/mutate/materialize.js +174 -0
  31. package/dist/src/mutate/opset.d.ts +27 -2
  32. package/dist/src/mutate/opset.js +12 -5
  33. package/dist/src/mutate/plan-update.d.ts +1 -46
  34. package/dist/src/mutate/plan-update.js +19 -67
  35. package/dist/src/mutate/plan.d.ts +200 -0
  36. package/dist/src/mutate/plan.js +472 -0
  37. package/dist/src/mutate/sugar.d.ts +135 -0
  38. package/dist/src/mutate/sugar.js +167 -0
  39. package/dist/src/mutate/tree.d.ts +4 -2
  40. package/dist/src/spec-versions.d.ts +5 -5
  41. package/dist/src/spec-versions.js +5 -5
  42. package/dist/src/surface-paths.d.ts +17 -26
  43. package/dist/src/surface-paths.js +27 -22
  44. package/dist/src/sync/checkpoint.d.ts +5 -2
  45. package/dist/src/sync/checkpoint.js +10 -6
  46. package/dist/src/sync/freshness.d.ts +10 -7
  47. package/dist/src/sync/freshness.js +15 -17
  48. package/dist/src/sync/observe.d.ts +4 -1
  49. package/dist/src/sync/observe.js +56 -5
  50. package/dist/src/sync/recovery.d.ts +4 -0
  51. package/dist/src/sync/recovery.js +4 -1
  52. package/dist/src/sync/stat-cache.d.ts +7 -0
  53. package/dist/src/sync/stat-cache.js +22 -0
  54. package/dist/src/sync/tombstone.js +23 -19
  55. package/dist/src/sync/watcher.d.ts +2 -0
  56. package/dist/src/sync/watcher.js +4 -1
  57. package/dist/src/sync/writer-lock.d.ts +15 -0
  58. package/dist/src/sync/writer-lock.js +104 -17
  59. package/package.json +1 -1
@@ -0,0 +1,135 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import { type InboundLink } from "../graph/inbound-links.js";
3
+ import { type DocStore } from "./doc-store.js";
4
+ import type { Effect, EffectAt, EffectExpect, EffectTarget } from "./effects.js";
5
+ import { type ExecuteRequest, type Plan, type Receipt } from "./plan.js";
6
+ /** What a builder returns: the ordered effects and the commit reason they carry (absent → the caller's `reason` argument). */
7
+ export interface Sugar {
8
+ effects: Effect[];
9
+ reason?: string;
10
+ }
11
+ /** A live document as the builders address it: its id and its storage-form path (the reasons spell the path). */
12
+ export interface SugarDoc {
13
+ id: string;
14
+ path: string;
15
+ }
16
+ /** A caller's guard as an effect guard: absent stays absent, an empty object stays an empty guard (the planner then pins what it needs). */
17
+ export declare function guardOf(e: {
18
+ content_hash?: string | undefined;
19
+ parent_children_hash?: string | undefined;
20
+ } | undefined): {
21
+ expect?: EffectExpect;
22
+ };
23
+ /**
24
+ * The pre-check a `create` / `move document` caller runs before planning
25
+ * (spec/surface §4 "The document operations", §9): `path_taken` exactly as the
26
+ * 2.x library operations raised it — the caller's message (`document already
27
+ * exists at <rel>` for a create, `a document already exists at <rel>` for a
28
+ * move), then `file already exists on disk at <rel>` — with NO `data`, so the
29
+ * planner's own `path_taken { path }` never fires. Shared by the MCP tools and
30
+ * the `omg` verbs (the Rust port mirrors it).
31
+ */
32
+ export declare function requireAbsentPath(store: Store, repoId: string, where: {
33
+ rootPath?: string;
34
+ docStore?: DocStore;
35
+ }, rel: string, takenMessage: string): void;
36
+ /** `docs_create` / `omg new`: one `create` at the canonical path; reason `create <rel>`. */
37
+ export declare function createEffects(path: string, markdown: string, frontmatter?: Record<string, unknown>): Sugar;
38
+ /** `docs_move` / `omg mv`: one `move { target: document, to, retarget_inbound }`; reason `move <from> -> <to>`. */
39
+ export declare function moveDocEffects(doc: SugarDoc, toPath: string, retargetInbound?: boolean): Sugar;
40
+ /** `docs_delete` / `omg rm --doc`: one `remove { target: document }`; reason `delete <path>`. */
41
+ export declare function deleteDocEffects(doc: SugarDoc): Sugar;
42
+ /**
43
+ * `docs_set_meta` / `omg meta`: one `set`/`unset frontmatter.<k>` per key, `unset`
44
+ * winning a key named in both (the legacy merge spread `set` then deleted `unset`);
45
+ * reason `set_meta <path>`. An empty patch yields no effect — the caller refuses
46
+ * it (`filter_invalid` "pass `set` and/or `unset`", spec/surface §9).
47
+ */
48
+ export declare function setMetaEffects(doc: SugarDoc, set: Record<string, unknown> | undefined, unset: readonly string[] | undefined): Sugar;
49
+ /** `docs_update` / `omg update <doc>`: one `set { document, content }`; the reason is the caller's. */
50
+ export declare function updateContentEffects(doc: Pick<SugarDoc, "id">, content: string, reason?: string): Sugar;
51
+ /** `blocks_insert` / `omg insert`: `insert into <document|block> at …`; the caller's `expect` is the destination-parent CAS. */
52
+ export declare function insertEffects(into: EffectTarget, at: EffectAt, markdown: string, expect?: {
53
+ parent_children_hash?: string | undefined;
54
+ }): Sugar;
55
+ /**
56
+ * `blocks_update` / `omg update <block>` / `omg edit`: `set markdown` and/or `set
57
+ * attrs.checked`. The caller's `expect` guards the FIRST effect (the content CAS
58
+ * is checked once, against the bytes the caller saw); a second effect is pinned
59
+ * by the planner to the block as the first left it. Nothing to set → no effect
60
+ * (the caller refuses it).
61
+ */
62
+ export declare function updateBlockEffects(id: string, patch: {
63
+ markdown?: string | undefined;
64
+ checked?: boolean | undefined;
65
+ expect?: {
66
+ content_hash?: string | undefined;
67
+ parent_children_hash?: string | undefined;
68
+ } | undefined;
69
+ }): Sugar;
70
+ /** `blocks_move` / `omg move`: one `move { targets, into, at, expect }`. */
71
+ export declare function moveBlocksEffects(ids: readonly string[], into: EffectTarget, at: EffectAt, expect?: {
72
+ parent_children_hash?: string | undefined;
73
+ }): Sugar;
74
+ /** `blocks_remove` / `omg rm`: one `remove { targets }` (the planner pins each block). */
75
+ export declare function removeBlocksEffects(ids: readonly string[]): Sugar;
76
+ /** `blocks_split` / `omg split`: one `split { target, at }` (the planner pins the content CAS). */
77
+ export declare function splitEffects(id: string, at: readonly number[]): Sugar;
78
+ /** `blocks_merge` / `omg merge`: one `merge { targets, separator? }`. */
79
+ export declare function mergeEffects(ids: readonly string[], separator?: string): Sugar;
80
+ /** `blocks_check` / `omg done`: N × `set attrs.checked` (a block named twice is one write — the planner drops the duplicate). */
81
+ export declare function checkEffects(ids: readonly string[], checked: boolean): Sugar;
82
+ /** `nodes_set` / `omg node set`: one `set { node, field: prop, value }`. */
83
+ export declare function nodeSetEffects(nodeId: string, prop: string, value: string): Sugar;
84
+ /** `blocks_append` / `omg append`: `insert at end` into a heading's section or a container block (the caller decided which by the block's kind). */
85
+ export declare function appendEffects(into: EffectTarget, text: string): Sugar;
86
+ /** `docs_append`: `insert at end` into the document's top level or a resolved section. */
87
+ export declare function docsAppendEffects(into: EffectTarget, text: string): Sugar;
88
+ /** `links_retarget` / `links_repair` / `omg retarget --apply`: one `retarget { repairs, path_glob? }` under the calling tool's name. */
89
+ export declare function retargetEffects(repairs: readonly {
90
+ from: string;
91
+ to: string;
92
+ }[], pathGlob: string | undefined, reason: "links_retarget" | "links_repair"): Sugar;
93
+ /** The request half a caller supplies: where to write and under whom (`ExecuteRequest` minus the origin's reason, which the sugar carries). */
94
+ export type SugarRequest = Omit<ExecuteRequest, "origin"> & {
95
+ actor: string | null;
96
+ reason?: string;
97
+ };
98
+ /**
99
+ * Plan + execute a builder's effects as one request (`applyEffectsDetailed`: one
100
+ * lock, one transaction) under `actor` and the sugar's reason — `request.reason`
101
+ * wins when given (a `--reason`, a `docs_update` argument) — and report any
102
+ * planner failure without its `effect_index`. `dryRun` plans only (the CLI's
103
+ * `--dry-run`, spec/cli §3.6: every dry run is a plan and burns no id).
104
+ */
105
+ export declare function applySugar(store: Store, request: SugarRequest & {
106
+ dryRun?: boolean;
107
+ }, sugar: Sugar): {
108
+ plan: Plan;
109
+ receipt?: Receipt;
110
+ };
111
+ /** The document-operation result the `omg` document verbs and the fixture `docs` step record (`spec/mutate` §9): `{ id, path, committed }`, the path in the storage form. */
112
+ export interface DocOpOutcome {
113
+ id: string;
114
+ path: string;
115
+ committed: true;
116
+ }
117
+ /** `docs_create`'s document is the receipt's first revision (`new_ids["$new.0"]` is the same id); every other operation's is the one addressed. */
118
+ export declare function createdDoc(receipt: Receipt, path: string): DocOpOutcome;
119
+ export declare function docOutcome(doc: SugarDoc, path?: string): DocOpOutcome;
120
+ /** What a move reports about the inbound links (spec/mutate §6 `docs_move`; the `retargeted`/`dangling` extras of the tool and the verb). */
121
+ export interface MoveLinks {
122
+ /** inbound links still naming the OLD path after the move — every link when the retarget was opted out, else the frontmatter relations a move never rewrites */
123
+ dangling: InboundLink[];
124
+ /** the source blocks rewritten and their documents; `null` when the retarget was opted out or nothing linked in */
125
+ retargeted: {
126
+ blocks: string[];
127
+ docs: string[];
128
+ } | null;
129
+ }
130
+ /**
131
+ * Who links to the document BY PATH as written, and what the move's retarget
132
+ * rewrites — computed BEFORE the move from the current blocks (`planInboundRetarget`
133
+ * is pure, so it equals what the planner lowers for the same `move` effect).
134
+ */
135
+ export declare function moveLinks(store: Store, repoId: string, doc: SugarDoc, toPath: string, retargetInbound?: boolean): MoveLinks;
@@ -0,0 +1,167 @@
1
+ import { findDoc } from "../core/read/reader.js";
2
+ import { inboundLinksTo } from "../graph/inbound-links.js";
3
+ import { canonicalDocPath, planInboundRetarget } from "./docs.js";
4
+ import { resolveDocStore } from "./doc-store.js";
5
+ import { MutationError } from "./tree.js";
6
+ import { applyEffectsDetailed, stripEffectIndex } from "./plan.js";
7
+ const documentTarget = (id) => ({ kind: "document", id });
8
+ const blockTarget = (id) => ({ kind: "block", id });
9
+ /** A caller's guard as an effect guard: absent stays absent, an empty object stays an empty guard (the planner then pins what it needs). */
10
+ export function guardOf(e) {
11
+ if (!e)
12
+ return {};
13
+ const expect = {};
14
+ if (e.content_hash !== undefined)
15
+ expect.content_hash = e.content_hash;
16
+ if (e.parent_children_hash !== undefined)
17
+ expect.parent_children_hash = e.parent_children_hash;
18
+ return { expect };
19
+ }
20
+ // ---- the document operations (spec/mutate §6) ---------------------------------------------
21
+ /**
22
+ * The pre-check a `create` / `move document` caller runs before planning
23
+ * (spec/surface §4 "The document operations", §9): `path_taken` exactly as the
24
+ * 2.x library operations raised it — the caller's message (`document already
25
+ * exists at <rel>` for a create, `a document already exists at <rel>` for a
26
+ * move), then `file already exists on disk at <rel>` — with NO `data`, so the
27
+ * planner's own `path_taken { path }` never fires. Shared by the MCP tools and
28
+ * the `omg` verbs (the Rust port mirrors it).
29
+ */
30
+ export function requireAbsentPath(store, repoId, where, rel, takenMessage) {
31
+ if (findDoc(store, { repoId, path: rel }))
32
+ throw new MutationError("path_taken", takenMessage);
33
+ if (resolveDocStore(where).exists(rel))
34
+ throw new MutationError("path_taken", `file already exists on disk at ${rel}`);
35
+ }
36
+ /** `docs_create` / `omg new`: one `create` at the canonical path; reason `create <rel>`. */
37
+ export function createEffects(path, markdown, frontmatter) {
38
+ const rel = canonicalDocPath(path);
39
+ return { effects: [{ op: "create", path: rel, markdown, ...(frontmatter !== undefined ? { frontmatter } : {}) }], reason: `create ${rel}` };
40
+ }
41
+ /** `docs_move` / `omg mv`: one `move { target: document, to, retarget_inbound }`; reason `move <from> -> <to>`. */
42
+ export function moveDocEffects(doc, toPath, retargetInbound = true) {
43
+ const to = canonicalDocPath(toPath);
44
+ return { effects: [{ op: "move", target: documentTarget(doc.id), to, ...(retargetInbound ? {} : { retarget_inbound: false }) }], reason: `move ${doc.path} -> ${to}` };
45
+ }
46
+ /** `docs_delete` / `omg rm --doc`: one `remove { target: document }`; reason `delete <path>`. */
47
+ export function deleteDocEffects(doc) {
48
+ return { effects: [{ op: "remove", target: documentTarget(doc.id) }], reason: `delete ${doc.path}` };
49
+ }
50
+ /**
51
+ * `docs_set_meta` / `omg meta`: one `set`/`unset frontmatter.<k>` per key, `unset`
52
+ * winning a key named in both (the legacy merge spread `set` then deleted `unset`);
53
+ * reason `set_meta <path>`. An empty patch yields no effect — the caller refuses
54
+ * it (`filter_invalid` "pass `set` and/or `unset`", spec/surface §9).
55
+ */
56
+ export function setMetaEffects(doc, set, unset) {
57
+ const gone = unset ?? [];
58
+ const target = documentTarget(doc.id);
59
+ const effects = [
60
+ ...Object.entries(set ?? {}).filter(([k]) => !gone.includes(k)).map(([k, value]) => ({ op: "set", target, field: `frontmatter.${k}`, value })),
61
+ ...gone.map((k) => ({ op: "unset", target, field: `frontmatter.${k}` })),
62
+ ];
63
+ return { effects, reason: `set_meta ${doc.path}` };
64
+ }
65
+ /** `docs_update` / `omg update <doc>`: one `set { document, content }`; the reason is the caller's. */
66
+ export function updateContentEffects(doc, content, reason) {
67
+ return { effects: [{ op: "set", target: documentTarget(doc.id), field: "content", value: content }], ...(reason !== undefined ? { reason } : {}) };
68
+ }
69
+ // ---- the block-level tools (spec/surface §4) ----------------------------------------------
70
+ /** `blocks_insert` / `omg insert`: `insert into <document|block> at …`; the caller's `expect` is the destination-parent CAS. */
71
+ export function insertEffects(into, at, markdown, expect) {
72
+ return { effects: [{ op: "insert", into, at, markdown, ...guardOf(expect) }], reason: "blocks_insert" };
73
+ }
74
+ /**
75
+ * `blocks_update` / `omg update <block>` / `omg edit`: `set markdown` and/or `set
76
+ * attrs.checked`. The caller's `expect` guards the FIRST effect (the content CAS
77
+ * is checked once, against the bytes the caller saw); a second effect is pinned
78
+ * by the planner to the block as the first left it. Nothing to set → no effect
79
+ * (the caller refuses it).
80
+ */
81
+ export function updateBlockEffects(id, patch) {
82
+ const target = blockTarget(id);
83
+ const expect = guardOf(patch.expect);
84
+ const effects = [];
85
+ if (patch.markdown !== undefined)
86
+ effects.push({ op: "set", target, field: "markdown", value: patch.markdown, ...expect });
87
+ if (patch.checked !== undefined)
88
+ effects.push({ op: "set", target, field: "attrs.checked", value: patch.checked, ...(effects.length === 0 ? expect : {}) });
89
+ return { effects, reason: "blocks_update" };
90
+ }
91
+ /** `blocks_move` / `omg move`: one `move { targets, into, at, expect }`. */
92
+ export function moveBlocksEffects(ids, into, at, expect) {
93
+ return { effects: [{ op: "move", targets: ids.map(blockTarget), into, at, ...guardOf(expect) }], reason: "blocks_move" };
94
+ }
95
+ /** `blocks_remove` / `omg rm`: one `remove { targets }` (the planner pins each block). */
96
+ export function removeBlocksEffects(ids) {
97
+ return { effects: [{ op: "remove", targets: ids.map(blockTarget) }], reason: "blocks_remove" };
98
+ }
99
+ /** `blocks_split` / `omg split`: one `split { target, at }` (the planner pins the content CAS). */
100
+ export function splitEffects(id, at) {
101
+ return { effects: [{ op: "split", target: blockTarget(id), at: [...at] }], reason: "blocks_split" };
102
+ }
103
+ /** `blocks_merge` / `omg merge`: one `merge { targets, separator? }`. */
104
+ export function mergeEffects(ids, separator) {
105
+ return { effects: [{ op: "merge", targets: ids.map(blockTarget), ...(separator !== undefined ? { separator } : {}) }], reason: "blocks_merge" };
106
+ }
107
+ /** `blocks_check` / `omg done`: N × `set attrs.checked` (a block named twice is one write — the planner drops the duplicate). */
108
+ export function checkEffects(ids, checked) {
109
+ return { effects: ids.map((id) => ({ op: "set", target: blockTarget(id), field: "attrs.checked", value: checked })), reason: "blocks_check" };
110
+ }
111
+ /** `nodes_set` / `omg node set`: one `set { node, field: prop, value }`. */
112
+ export function nodeSetEffects(nodeId, prop, value) {
113
+ return { effects: [{ op: "set", target: { kind: "node", id: nodeId }, field: prop, value }], reason: "nodes_set" };
114
+ }
115
+ /** `blocks_append` / `omg append`: `insert at end` into a heading's section or a container block (the caller decided which by the block's kind). */
116
+ export function appendEffects(into, text) {
117
+ return { effects: [{ op: "insert", into, at: "end", markdown: text }], reason: "blocks_append" };
118
+ }
119
+ /** `docs_append`: `insert at end` into the document's top level or a resolved section. */
120
+ export function docsAppendEffects(into, text) {
121
+ return { effects: [{ op: "insert", into, at: "end", markdown: text }], reason: "docs_append" };
122
+ }
123
+ /** `links_retarget` / `links_repair` / `omg retarget --apply`: one `retarget { repairs, path_glob? }` under the calling tool's name. */
124
+ export function retargetEffects(repairs, pathGlob, reason) {
125
+ return { effects: [{ op: "retarget", repairs: repairs.map((r) => ({ from: r.from, to: r.to })), ...(pathGlob !== undefined ? { path_glob: pathGlob } : {}) }], reason };
126
+ }
127
+ /**
128
+ * Plan + execute a builder's effects as one request (`applyEffectsDetailed`: one
129
+ * lock, one transaction) under `actor` and the sugar's reason — `request.reason`
130
+ * wins when given (a `--reason`, a `docs_update` argument) — and report any
131
+ * planner failure without its `effect_index`. `dryRun` plans only (the CLI's
132
+ * `--dry-run`, spec/cli §3.6: every dry run is a plan and burns no id).
133
+ */
134
+ export function applySugar(store, request, sugar) {
135
+ const { actor, reason, dryRun, ...rest } = request;
136
+ const chosen = reason ?? sugar.reason;
137
+ try {
138
+ return applyEffectsDetailed(store, { ...rest, effects: sugar.effects, origin: { actor, ...(chosen !== undefined ? { reason: chosen } : {}) }, ...(dryRun ? { dryRun: true } : {}) });
139
+ }
140
+ catch (e) {
141
+ throw stripEffectIndex(e);
142
+ }
143
+ }
144
+ /** `docs_create`'s document is the receipt's first revision (`new_ids["$new.0"]` is the same id); every other operation's is the one addressed. */
145
+ export function createdDoc(receipt, path) {
146
+ const id = receipt.new_ids["$new.0"] ?? receipt.revisions[0]?.id;
147
+ if (id === undefined)
148
+ throw new Error("createdDoc: the receipt names no created document");
149
+ return { id, path, committed: true };
150
+ }
151
+ export function docOutcome(doc, path = doc.path) {
152
+ return { id: doc.id, path, committed: true };
153
+ }
154
+ /**
155
+ * Who links to the document BY PATH as written, and what the move's retarget
156
+ * rewrites — computed BEFORE the move from the current blocks (`planInboundRetarget`
157
+ * is pure, so it equals what the planner lowers for the same `move` effect).
158
+ */
159
+ export function moveLinks(store, repoId, doc, toPath, retargetInbound = true) {
160
+ const to = canonicalDocPath(toPath);
161
+ const inbound = inboundLinksTo(store, repoId, doc.id, doc.path);
162
+ if (!retargetInbound || inbound.length === 0)
163
+ return { dangling: inbound, retargeted: null };
164
+ const plan = planInboundRetarget(store, doc.id, doc.path, to, inbound);
165
+ return { dangling: plan.dangling, retargeted: { blocks: plan.blocks, docs: plan.docs } };
166
+ }
167
+ //# sourceMappingURL=sugar.js.map
@@ -16,10 +16,12 @@ export interface MutDoc {
16
16
  frontmatterRaw: string | null;
17
17
  children: MutBlock[];
18
18
  }
19
+ /** The codes spec/mutate §8 lists (2.3 adds `conflicting_effects`, `unsupported_plan_version`, `plan_mismatch`); `filter_invalid` is the planner's shape error, spec/surface §4's envelope. */
20
+ export type MutationErrorCode = "stale_expectation" | "block_missing" | "target_missing" | "parent_missing" | "doc_missing" | "cycle_move" | "not_contiguous" | "type_mismatch" | "path_taken" | "node_not_editable" | "stale_plan" | "plan_not_convergent" | "sync_conflict" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "filter_invalid";
19
21
  export declare class MutationError extends Error {
20
- code: string;
22
+ code: MutationErrorCode | string;
21
23
  data: Record<string, unknown>;
22
- constructor(code: string, message: string, data?: Record<string, unknown>);
24
+ constructor(code: MutationErrorCode | string, message: string, data?: Record<string, unknown>);
23
25
  }
24
26
  /** Find a block by id anywhere in the doc; returns the block + its parent list. */
25
27
  export declare function locate(doc: MutDoc, blockId: string): {
@@ -2,13 +2,13 @@ export declare const SPEC_VERSIONS: {
2
2
  readonly oqx: "0.20";
3
3
  readonly format: "0.2";
4
4
  readonly reconcile: "2.3";
5
- readonly store: "13.5";
5
+ readonly store: "14.0";
6
6
  readonly properties: "1.1";
7
7
  readonly graph: "1.1";
8
8
  readonly search: "2.0";
9
- readonly mutate: "2.0";
10
- readonly sync: "2.0";
11
- readonly surface: "3.0";
12
- readonly cli: "3.0";
9
+ readonly mutate: "3.0";
10
+ readonly sync: "2.2";
11
+ readonly surface: "4.0";
12
+ readonly cli: "4.0";
13
13
  };
14
14
  export type SpecName = keyof typeof SPEC_VERSIONS;
@@ -13,13 +13,13 @@ export const SPEC_VERSIONS = {
13
13
  oqx: LANGUAGE_VERSION,
14
14
  format: "0.2",
15
15
  reconcile: "2.3",
16
- store: "13.5",
16
+ store: "14.0",
17
17
  properties: "1.1",
18
18
  graph: "1.1",
19
19
  search: "2.0",
20
- mutate: "2.0",
21
- sync: "2.0",
22
- surface: "3.0",
23
- cli: "3.0",
20
+ mutate: "3.0",
21
+ sync: "2.2",
22
+ surface: "4.0",
23
+ cli: "4.0",
24
24
  };
25
25
  //# sourceMappingURL=spec-versions.js.map
@@ -1,28 +1,16 @@
1
1
  import type { ApplyResult } from "./mutate/apply.js";
2
- import type { DocOpResult, DocMoveResult } from "./mutate/docs.js";
3
2
  import type { RetargetHit } from "./mutate/macros.js";
4
- import type { Opset } from "./mutate/opset.js";
5
- import type { DocsUpdateResult } from "./mutate/plan-update.js";
3
+ import type { Plan, Receipt } from "./mutate/plan.js";
6
4
  import type { InboundLink } from "./graph/inbound-links.js";
7
5
  import type { ObserveResult, ObserveDeleteResult } from "./sync/observe.js";
8
6
  import type { SweepResult } from "./sync/freshness.js";
9
7
  import type { TextSearchResult } from "./search/text.js";
10
8
  import type { ResolveHit } from "./search/resolve.js";
11
- /** `spec/mutate` §4's result: `revisions[].path` and the dry-run `diffs` keys. */
9
+ /** `spec/mutate` §4's kernel result (the library's `apply`; no tool returns it since 4.0): `revisions[].path` and the dry-run `diffs` keys. */
12
10
  export declare function surfaceApplyResult<T extends ApplyResult>(res: T): T;
13
- /** A document operation's result on the wire (spec/surface §4, 3.0): the
14
- * library's `{ docId, path, … }` (`spec/mutate` §6, unchanged there) renamed
15
- * to the document's row attributes `id`/`path`, the path rooted, the
16
- * dry-run `diffs` re-keyed; every other key (`committed`, `deleted`, …) as is. */
17
- export type SurfaceDocOpResult<T extends DocOpResult> = Omit<T, "docId" | "path"> & {
18
- id: string;
19
- path: string;
20
- };
21
- /** `docs_create` / `docs_delete` / `docs_set_meta`. */
22
- export declare function surfaceDocOpResult<T extends DocOpResult>(res: T): SurfaceDocOpResult<T>;
23
- /** An inbound link on the wire (`docs_move.dangling[]`): an edge row under the
24
- * `edges` target's names (spec/surface §1.2) — `doc_id`, `path` (rooted),
25
- * `block_id`, `target` (as authored), `anchor`, `field?`. */
11
+ /** An inbound link on the wire (`docs_move.dangling[]`, `omg mv`): a link row
12
+ * under the `links` target's names (spec/surface §1.2) — `doc_id`, `path`
13
+ * (rooted), `block_id`, `target` (as authored), `anchor`, `field?`. */
26
14
  export interface SurfaceInboundLink {
27
15
  doc_id: string;
28
16
  path: string;
@@ -32,11 +20,6 @@ export interface SurfaceInboundLink {
32
20
  field?: string;
33
21
  }
34
22
  export declare function surfaceInboundLink(l: InboundLink): SurfaceInboundLink;
35
- /** `docs_move`: the document-op fields plus each dangling link (its `target`
36
- * stays as authored — that is what the link says). */
37
- export declare function surfaceDocMoveResult(res: DocMoveResult): Omit<SurfaceDocOpResult<DocMoveResult>, "dangling"> & {
38
- dangling: SurfaceInboundLink[];
39
- };
40
23
  /** An observe outcome on the wire (spec/surface §4, 3.0): `spec/store` §5's
41
24
  * `{ docId, path, … }` (unchanged there) renamed to the document's row
42
25
  * attributes `id`/`path`, the path rooted. */
@@ -58,9 +41,17 @@ export declare function surfaceTextSearch(res: TextSearchResult): TextSearchResu
58
41
  export declare function surfaceResolveHits(hits: ResolveHit[]): ResolveHit[];
59
42
  /** `links_retarget` / `links_repair` hits (`path` is the block's document). */
60
43
  export declare function surfaceRetargetHits(hits: RetargetHit[]): RetargetHit[];
61
- /** A whole-document update plan: `target.path` and `precondition.path`. */
62
- export declare function surfaceOpset(opset: Opset): Opset;
63
- /** `docs_update`: the opset and, when committed, the apply result. */
64
- export declare function surfaceDocsUpdate(res: DocsUpdateResult): DocsUpdateResult;
44
+ /**
45
+ * A Plan on the wire (spec/surface §4, 3.1): the executable core — `effects`,
46
+ * `opset`, `preconditions`, `new_ids`, `plan_id` — is returned VERBATIM in the
47
+ * storage form, because `plan_id` digests exactly those bytes and the client
48
+ * hands the object back to `execute` (spec/mutate §11.5; the one result the
49
+ * path rule exempts, like `dangling[].target`). Only the un-digested `preview`
50
+ * is rooted: its `revisions[].path`, `diffs` keys and `retarget[].hits[].path`
51
+ * (3.0; `identity` is keyed by document id and carries no path).
52
+ */
53
+ export declare function surfacePlan(plan: Plan): Plan;
54
+ /** A Receipt on the wire: `revisions[].path` rooted (the same shape as an apply revision). */
55
+ export declare function surfaceReceipt(receipt: Receipt): Receipt;
65
56
  /** `omg sync`'s one-shot sweep: the ingested / suppressed / deleted / conflicted paths. */
66
57
  export declare function surfaceSweepResult(res: SweepResult): SweepResult;
@@ -11,14 +11,14 @@
11
11
  // Row names (spec/surface §1.2, 3.0): a result that renders a document, block
12
12
  // or edge row spells it the way the query binding does — `id`, `path`,
13
13
  // `hash`, `raw`, `updated_at`, plain `doc_id`/`block_id` foreign keys. The
14
- // shapes other specs own under their own names — `spec/mutate`'s document
15
- // operation result `{ docId, path, … }`, `spec/store`'s observe outcome — are
16
- // re-shaped HERE too (`SurfaceDocOpResult`, `SurfaceObserveResult`, …).
14
+ // shapes other specs own under their own names — `spec/store`'s observe
15
+ // outcome, a move's dangling inbound links — are re-shaped HERE too
16
+ // (`SurfaceObserveResult`, `surfaceInboundLink`, …).
17
17
  //
18
18
  // Both the MCP server and the `omg` verbs call these, so the two clients
19
19
  // cannot drift. Each function returns a new value; the input is not mutated.
20
20
  import { referencePath, referenceKeyed } from "./core/paths.js";
21
- /** `spec/mutate` §4's result: `revisions[].path` and the dry-run `diffs` keys. */
21
+ /** `spec/mutate` §4's kernel result (the library's `apply`; no tool returns it since 4.0): `revisions[].path` and the dry-run `diffs` keys. */
22
22
  export function surfaceApplyResult(res) {
23
23
  return {
24
24
  ...res,
@@ -26,20 +26,10 @@ export function surfaceApplyResult(res) {
26
26
  ...(res.diffs ? { diffs: referenceKeyed(res.diffs) } : {}),
27
27
  };
28
28
  }
29
- /** `docs_create` / `docs_delete` / `docs_set_meta`. */
30
- export function surfaceDocOpResult(res) {
31
- const { docId, path, ...rest } = res;
32
- return { id: docId, path: referencePath(path), ...rest, ...(res.diffs ? { diffs: referenceKeyed(res.diffs) } : {}) };
33
- }
34
29
  export function surfaceInboundLink(l) {
35
30
  const { path, ...rest } = l;
36
31
  return { doc_id: rest.doc_id, path: referencePath(path), block_id: rest.block_id, target: rest.target, anchor: rest.anchor, ...(rest.field !== undefined ? { field: rest.field } : {}) };
37
32
  }
38
- /** `docs_move`: the document-op fields plus each dangling link (its `target`
39
- * stays as authored — that is what the link says). */
40
- export function surfaceDocMoveResult(res) {
41
- return { ...surfaceDocOpResult(res), dangling: res.dangling.map(surfaceInboundLink) };
42
- }
43
33
  /** `observe` / `observe_many` outcomes. */
44
34
  export function surfaceObserveResult(res) {
45
35
  const { docId, path, ...rest } = res;
@@ -66,17 +56,31 @@ export function surfaceResolveHits(hits) {
66
56
  export function surfaceRetargetHits(hits) {
67
57
  return hits.map((h) => ({ ...h, path: referencePath(h.path) }));
68
58
  }
69
- /** A whole-document update plan: `target.path` and `precondition.path`. */
70
- export function surfaceOpset(opset) {
59
+ /**
60
+ * A Plan on the wire (spec/surface §4, 3.1): the executable core — `effects`,
61
+ * `opset`, `preconditions`, `new_ids`, `plan_id` — is returned VERBATIM in the
62
+ * storage form, because `plan_id` digests exactly those bytes and the client
63
+ * hands the object back to `execute` (spec/mutate §11.5; the one result the
64
+ * path rule exempts, like `dangling[].target`). Only the un-digested `preview`
65
+ * is rooted: its `revisions[].path`, `diffs` keys and `retarget[].hits[].path`
66
+ * (3.0; `identity` is keyed by document id and carries no path).
67
+ */
68
+ export function surfacePlan(plan) {
69
+ if (!plan.preview)
70
+ return plan;
71
71
  return {
72
- ...opset,
73
- target: { ...opset.target, path: referencePath(opset.target.path) },
74
- precondition: { ...opset.precondition, path: referencePath(opset.precondition.path) },
72
+ ...plan,
73
+ preview: {
74
+ ...plan.preview,
75
+ revisions: plan.preview.revisions.map((r) => ({ ...r, path: referencePath(r.path) })),
76
+ diffs: referenceKeyed(plan.preview.diffs),
77
+ ...(plan.preview.retarget !== undefined ? { retarget: plan.preview.retarget.map((r) => ({ ...r, hits: surfaceRetargetHits(r.hits) })) } : {}),
78
+ },
75
79
  };
76
80
  }
77
- /** `docs_update`: the opset and, when committed, the apply result. */
78
- export function surfaceDocsUpdate(res) {
79
- return { opset: surfaceOpset(res.opset), result: res.result ? surfaceApplyResult(res.result) : null };
81
+ /** A Receipt on the wire: `revisions[].path` rooted (the same shape as an apply revision). */
82
+ export function surfaceReceipt(receipt) {
83
+ return { ...receipt, revisions: receipt.revisions.map((r) => ({ ...r, path: referencePath(r.path) })) };
80
84
  }
81
85
  /** `omg sync`'s one-shot sweep: the ingested / suppressed / deleted / conflicted paths. */
82
86
  export function surfaceSweepResult(res) {
@@ -86,6 +90,7 @@ export function surfaceSweepResult(res) {
86
90
  suppressed: res.suppressed.map(referencePath),
87
91
  deleted: res.deleted.map(referencePath),
88
92
  conflicted: res.conflicted.map(referencePath),
93
+ materialized: res.materialized.map(referencePath),
89
94
  };
90
95
  }
91
96
  //# sourceMappingURL=surface-paths.js.map
@@ -29,8 +29,11 @@ export declare function processCheckpoint(store: Store, repoId: string, rootPath
29
29
  }): CheckpointResult;
30
30
  /**
31
31
  * Turn a batch's outcomes into the checkpoint row + result, then sweep the
32
- * resurrection pool. Shared with the external-source driver (driver.ts), which
33
- * obtains its bytes asynchronously but records its checkpoint the same way.
32
+ * resurrection pool — the row and the sweep in ONE transaction (spec/sync §4.1),
33
+ * so a checkpoint is never recorded without its sweep or swept without its row
34
+ * (the member commits before it are one transaction each, as before). Shared
35
+ * with the external-source driver (driver.ts), which obtains its bytes
36
+ * asynchronously but records its checkpoint the same way.
34
37
  */
35
38
  export declare function finishCheckpoint(store: Store, repoId: string, outcomes: BatchOutcome[], opts: {
36
39
  ts: string;
@@ -20,8 +20,11 @@ export function processCheckpoint(store, repoId, rootPath, changes, opts = {}) {
20
20
  }
21
21
  /**
22
22
  * Turn a batch's outcomes into the checkpoint row + result, then sweep the
23
- * resurrection pool. Shared with the external-source driver (driver.ts), which
24
- * obtains its bytes asynchronously but records its checkpoint the same way.
23
+ * resurrection pool — the row and the sweep in ONE transaction (spec/sync §4.1),
24
+ * so a checkpoint is never recorded without its sweep or swept without its row
25
+ * (the member commits before it are one transaction each, as before). Shared
26
+ * with the external-source driver (driver.ts), which obtains its bytes
27
+ * asynchronously but records its checkpoint the same way.
25
28
  */
26
29
  export function finishCheckpoint(store, repoId, outcomes, opts) {
27
30
  const checkpointId = mintId("cp");
@@ -45,10 +48,11 @@ export function finishCheckpoint(store, repoId, outcomes, opts) {
45
48
  ingested.push(o.path);
46
49
  fileEntries.push([o.path, o.oldHashHex, o.newHashHex]);
47
50
  }
48
- store.db
49
- .prepare("INSERT INTO checkpoints (id, repo_id, ts, files, git_head) VALUES (?, ?, ?, ?, ?)")
50
- .run(checkpointId, repoId, opts.ts, JSON.stringify(fileEntries), opts.gitHead);
51
- sweepResurrectionPool(store, opts.ts);
51
+ store.write((db) => {
52
+ db.prepare("INSERT INTO checkpoints (id, repo_id, ts, files, git_head) VALUES (?, ?, ?, ?, ?)")
53
+ .run(checkpointId, repoId, opts.ts, JSON.stringify(fileEntries), opts.gitHead);
54
+ sweepResurrectionPool(store, opts.ts);
55
+ });
52
56
  return { checkpointId, ingested, suppressed, deleted, conflicted };
53
57
  }
54
58
  //# sourceMappingURL=checkpoint.js.map
@@ -1,6 +1,8 @@
1
1
  import type { Store } from "../core/store/store.js";
2
2
  import { type CheckpointResult } from "./checkpoint.js";
3
- import { type FileStat, type SyncFs } from "./fs-util.js";
3
+ import { type SyncFs } from "./fs-util.js";
4
+ import { type DocStore } from "../mutate/doc-store.js";
5
+ export { recordStat, recordFileStat } from "./stat-cache.js";
4
6
  /** One `file_stats` row (spec/sync §4.3 "the repo's cache rows"). */
5
7
  export interface StatCacheRow {
6
8
  path: string;
@@ -44,22 +46,23 @@ export interface SweepResult extends CheckpointResult {
44
46
  scanned: number;
45
47
  candidates: number;
46
48
  changed: boolean;
49
+ /** step 0 (spec/sync §4.3, 2.2): the paths whose pending materialization rows were replayed before the walk. */
50
+ materialized: string[];
47
51
  }
48
- /** Write a file's stat cache row from a stat (`INSERT … ON CONFLICT DO UPDATE`); a null stat (the
49
- * file vanished) deletes the row. The DB half of `record_file_stat` (spec/sync §4.3 step 4). */
50
- export declare function recordStat(store: Store, repoId: string, path: string, st: FileStat | null, hash: Buffer): void;
51
- /** Record/refresh a file's stat cache row from its absolute path. Call after any ingest of the file. */
52
- export declare function recordFileStat(store: Store, repoId: string, path: string, abs: string, hash: Buffer): void;
53
52
  /**
54
53
  * Run the freshness sweep for a repo (spec/sync §4.3). Returns the checkpoint
55
54
  * result plus scan counters. Idempotent; safe alongside a live watcher
56
55
  * (hash-based echo suppression makes a double ingest a no-op). `opts.ts` pins
57
- * the batch timestamp (default now); `opts.fs` the filesystem seam.
56
+ * the batch timestamp (default now); `opts.fs` the filesystem seam;
57
+ * `opts.docStore` the write target step 0 materializes through (default: the
58
+ * filesystem store at `rootPath` — the fixtures' in-memory filesystem never
59
+ * holds a pending row, so the default is never exercised there).
58
60
  */
59
61
  export declare function freshnessSweep(store: Store, repoId: string, rootPath: string, opts?: {
60
62
  ts?: string;
61
63
  gitHead?: string | null;
62
64
  fs?: SyncFs;
65
+ docStore?: DocStore;
63
66
  }): SweepResult;
64
67
  /**
65
68
  * Counts of ways the DB disagrees with the current filesystem. All fields are