@omgbase/core 0.11.1 → 0.19.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 (72) 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/size.d.ts +76 -0
  9. package/dist/src/core/store/size.js +223 -0
  10. package/dist/src/core/store/store.d.ts +9 -0
  11. package/dist/src/core/store/store.js +11 -0
  12. package/dist/src/graph/history.js +15 -12
  13. package/dist/src/index.d.ts +12 -5
  14. package/dist/src/index.js +18 -7
  15. package/dist/src/mcp/arguments.d.ts +9 -2
  16. package/dist/src/mcp/arguments.js +15 -2
  17. package/dist/src/mcp/errors.d.ts +1 -1
  18. package/dist/src/mcp/errors.js +3 -0
  19. package/dist/src/mcp/reference.d.ts +1 -1
  20. package/dist/src/mcp/reference.js +27 -6
  21. package/dist/src/mcp/server.d.ts +56 -1
  22. package/dist/src/mcp/server.js +365 -203
  23. package/dist/src/mutate/apply.d.ts +93 -12
  24. package/dist/src/mutate/apply.js +203 -113
  25. package/dist/src/mutate/canonical.d.ts +27 -0
  26. package/dist/src/mutate/canonical.js +138 -0
  27. package/dist/src/mutate/commit.d.ts +150 -0
  28. package/dist/src/mutate/commit.js +349 -0
  29. package/dist/src/mutate/doc-store.d.ts +3 -3
  30. package/dist/src/mutate/doc-store.js +21 -6
  31. package/dist/src/mutate/docs.d.ts +42 -87
  32. package/dist/src/mutate/docs.js +50 -204
  33. package/dist/src/mutate/effects.d.ts +363 -0
  34. package/dist/src/mutate/effects.js +1161 -0
  35. package/dist/src/mutate/materialize.d.ts +49 -0
  36. package/dist/src/mutate/materialize.js +174 -0
  37. package/dist/src/mutate/opset.d.ts +27 -2
  38. package/dist/src/mutate/opset.js +12 -5
  39. package/dist/src/mutate/plan-update.d.ts +1 -46
  40. package/dist/src/mutate/plan-update.js +19 -67
  41. package/dist/src/mutate/plan.d.ts +210 -0
  42. package/dist/src/mutate/plan.js +512 -0
  43. package/dist/src/mutate/select.d.ts +89 -0
  44. package/dist/src/mutate/select.js +879 -0
  45. package/dist/src/mutate/sugar.d.ts +135 -0
  46. package/dist/src/mutate/sugar.js +167 -0
  47. package/dist/src/mutate/tree.d.ts +4 -2
  48. package/dist/src/oqx-js/planner.js +4 -0
  49. package/dist/src/oqx-js/run.d.ts +28 -1
  50. package/dist/src/oqx-js/run.js +85 -22
  51. package/dist/src/spec-versions.d.ts +6 -6
  52. package/dist/src/spec-versions.js +5 -5
  53. package/dist/src/surface-paths.d.ts +17 -26
  54. package/dist/src/surface-paths.js +27 -22
  55. package/dist/src/sync/checkpoint.d.ts +5 -2
  56. package/dist/src/sync/checkpoint.js +10 -6
  57. package/dist/src/sync/freshness.d.ts +10 -7
  58. package/dist/src/sync/freshness.js +15 -17
  59. package/dist/src/sync/observe.d.ts +4 -1
  60. package/dist/src/sync/observe.js +56 -5
  61. package/dist/src/sync/recovery.d.ts +4 -0
  62. package/dist/src/sync/recovery.js +4 -1
  63. package/dist/src/sync/stat-cache.d.ts +7 -0
  64. package/dist/src/sync/stat-cache.js +22 -0
  65. package/dist/src/sync/tombstone.js +23 -19
  66. package/dist/src/sync/watcher.d.ts +2 -0
  67. package/dist/src/sync/watcher.js +4 -1
  68. package/dist/src/sync/writer-lock.d.ts +15 -0
  69. package/dist/src/sync/writer-lock.js +104 -17
  70. package/dist/src/version.d.ts +7 -2
  71. package/dist/src/version.js +5 -2
  72. package/package.json +2 -2
@@ -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`; 3.1 `ambiguous_target`, `selection_too_large` — §11.8); `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" | "ambiguous_target" | "selection_too_large" | "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): {
@@ -125,6 +125,10 @@ function exprMayRaise(e, target, root) {
125
125
  return (e.lo !== null && exprMayRaise(e.lo, target, root)) || (e.hi !== null && exprMayRaise(e.hi, target, root));
126
126
  // A tuple (oqx 0.20) is never pushed down; it raises iff an element does.
127
127
  case "tuple": return e.items.some((x) => exprMayRaise(x, target, root));
128
+ // A template literal (oqx 0.21) is never pushed down; interpolating a list or
129
+ // a record is an eval error only the data decides, so with any interpolation
130
+ // it may raise.
131
+ case "template": return e.parts.some((p) => typeof p !== "string");
128
132
  case "lit":
129
133
  case "binding": return false;
130
134
  }
@@ -1,5 +1,6 @@
1
- import type { Query, Expr } from "@omgbase/oqx";
1
+ import type { Query, Expr, SelectItem } from "@omgbase/oqx";
2
2
  import type { Store } from "../core/store/store.js";
3
+ import { FilterInvalid } from "../search/cel/parser.js";
3
4
  import type { SemanticVec } from "../search/cel/compile.js";
4
5
  export type OqxConsumer = "collect" | "count" | "exists" | "none" | "first" | "single";
5
6
  export type EmbedQuery = (text: string) => Promise<{
@@ -18,6 +19,10 @@ export interface OqxHit {
18
19
  [k: string]: unknown;
19
20
  }
20
21
  export interface OqxResult {
22
+ /** `{ id, path, ...projection }` per hit — or, under `OqxOptions.bare`, the
23
+ * projection alone (`id`/`path` present only when the query projected them;
24
+ * typed as `OqxHit` for the one hit shape every consumer handles, a bare
25
+ * caller knows it asked). */
21
26
  hits: OqxHit[];
22
27
  truncated: boolean;
23
28
  cursor: string | null;
@@ -35,6 +40,17 @@ export interface OqxResult {
35
40
  export interface OqxOptions {
36
41
  limit?: number;
37
42
  cursor?: string | null;
43
+ /** Return each hit as its PROJECTION only — no automatic `id`/`path`
44
+ * members (spec/surface §1.4, 4.2): `select title, era from docs` yields
45
+ * `{ title, era }` rows. Needs a `select` (without a projection a hit is
46
+ * only its envelope) and is refused with `values` (already the bare-values
47
+ * form); both are `filter_invalid` before the engine runs. A projected
48
+ * `id`/`path` is the intrinsic as everywhere (`select id` → `{ id }`), and
49
+ * with no envelope there is nothing to collide with (`select path: layer`
50
+ * is fine). Paging, `distinct`, `truncated` and `cursor` are unchanged —
51
+ * the runner keeps identity internally and strips the envelope last. The
52
+ * scalar consumers (`count`/`exists`/`none`) have no hits and ignore it. */
53
+ bare?: boolean;
38
54
  semanticVectors?: Map<string, SemanticVec>;
39
55
  /** Force the pure in-memory engine (skip tier-3 pushdown). Internal — used by
40
56
  * the differential conformance suite to prove planned == in-memory. */
@@ -43,7 +59,18 @@ export interface OqxOptions {
43
59
  export declare function rootPathLiterals(e: Expr): Expr;
44
60
  export declare function checkRootSpellings(q: Query): void;
45
61
  export declare function collectSemanticPhrases(source: string): string[];
62
+ export declare function reservedAlias(items: readonly SelectItem[]): FilterInvalid | null;
63
+ export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";
64
+ export declare const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` \u2014 `values` is already the bare-values form; drop one";
46
65
  export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
66
+ /**
67
+ * The AST entry (4.1): run an already parsed query — the source path above
68
+ * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
69
+ * its slot items to the RAW tree so a user alias a template reads is inlined
70
+ * by the alias resolution here. Errors are the library's own (`OqxError`);
71
+ * `oqxRun` normalizes them, a caller of this entry does the same.
72
+ */
73
+ export declare function oqxRunQuery(store: Store, repoId: string, raw: Query, opts?: OqxOptions): OqxResult;
47
74
  export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;
48
75
  /** Parse an OQX source into the `@omgbase/oqx` AST (re-exported for callers). */
49
76
  export declare function parseOqx(source: string): Query;
@@ -128,6 +128,21 @@ export function collectSemanticPhrases(source) {
128
128
  }
129
129
  const ID_ITEM = build.field("__oqx_id", build.ident("id"));
130
130
  const PATH_ITEM = build.field("__oqx_path", build.ident("path"));
131
+ // The runner's own projection names (`__oqx_id`, `__oqx_path`, `__oqx_self`,
132
+ // `__oqx_value`) and the selector compiler's slots (`__omg_sel_<t>_<slot>`,
133
+ // mutate/select.ts) share two reserved prefixes. A user alias under either would
134
+ // shadow an injected item (`select __oqx_id: 1` overwrote the hit's `id` through
135
+ // 4.0) or collide with a slot, so it is refused before anything runs — over the
136
+ // query's own top-level items, the only ones the runner injects beside.
137
+ const RESERVED_ALIAS_PREFIXES = ["__oqx_", "__omg_"];
138
+ export function reservedAlias(items) {
139
+ for (const item of items) {
140
+ const prefix = RESERVED_ALIAS_PREFIXES.find((p) => item.name.startsWith(p));
141
+ if (prefix !== undefined)
142
+ return new FilterInvalid(`projection \`${item.name}\` uses the reserved prefix \`${prefix}\` — choose another alias`, "OQX");
143
+ }
144
+ return null;
145
+ }
131
146
  // Result projections may not overwrite the envelope (spec/surface §1.4, 3.0):
132
147
  // a projection whose output key is `id` or `path` is allowed only when it IS
133
148
  // the envelope's own selection — the bare attribute of the same name, which
@@ -213,33 +228,56 @@ function renderValue(v) {
213
228
  out[k] = renderValue(x);
214
229
  return out;
215
230
  }
216
- function toHit(row) {
231
+ function shapeRow(row) {
217
232
  const o = row;
218
233
  const { __oqx_id, __oqx_path, __oqx_self, ...rest } = o;
219
234
  if (SELF_KEY in o && !rowRef(__oqx_self))
220
235
  throw notAStoreRow(__oqx_self);
221
- // The envelope keys lead; a user projection of `id`/`path` collapses onto
222
- // them in place (the same value, read by the same attribute).
223
- return { id: String(__oqx_id), path: String(__oqx_path ?? ""), ...renderValue(rest) };
236
+ return { id: String(__oqx_id), path: String(__oqx_path ?? ""), proj: renderValue(rest) };
237
+ }
238
+ // The envelope keys lead; a user projection of `id`/`path` collapses onto
239
+ // them in place (the same value, read by the same attribute).
240
+ function toHit(s) {
241
+ return { id: s.id, path: s.path, ...s.proj };
242
+ }
243
+ // `bare` (4.2): the projection alone. A projected `id`/`path` is an ordinary
244
+ // column here — the intrinsic the attribute reads, under the key the query
245
+ // gave it — and nothing is injected beside it.
246
+ function toBareHit(s) {
247
+ return s.proj;
224
248
  }
225
- // Top-level `select distinct`: dedup hits by their USER projection (every field
226
- // except the envelope's `id`/`path`), keeping the first (so the retained hit
227
- // carries a real identity). Order-preserving.
228
- function dedupHitsByProjection(hits) {
249
+ // Top-level `select distinct`: dedup hits by their USER projection — every
250
+ // field except the envelope's `id`/`path`, which a projection of the attribute
251
+ // collapses onto (so `select distinct id` dedups on nothing but the empty
252
+ // projection, as it always has); under `bare` there is no envelope, so a
253
+ // projected `id`/`path` IS part of the projection and counts. Keeps the first
254
+ // (so the retained hit carries a real identity). Order-preserving.
255
+ function dedupByProjection(rows, bare) {
229
256
  const seen = new Set();
230
257
  const out = [];
231
- for (const h of hits) {
232
- const { id: _id, path: _path, ...proj } = h;
233
- void _id;
234
- void _path;
235
- const key = JSON.stringify(Object.keys(proj).sort().map((k) => [k, proj[k]]));
258
+ for (const s of rows) {
259
+ const keys = Object.keys(s.proj).filter((k) => bare || !ENVELOPE_KEYS.has(k)).sort();
260
+ const key = JSON.stringify(keys.map((k) => [k, s.proj[k]]));
236
261
  if (seen.has(key))
237
262
  continue;
238
263
  seen.add(key);
239
- out.push(h);
264
+ out.push(s);
240
265
  }
241
266
  return out;
242
267
  }
268
+ // `bare` is examined where hits are built (spec/surface §1.4, 4.2): without a
269
+ // projection a hit is only its envelope, and `values` is already the
270
+ // bare-values form — both refused before the engine runs, over the resolved
271
+ // query, so the error never depends on the rows.
272
+ export const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` — without a projection a hit is only its envelope";
273
+ export const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
274
+ function bareMisuse(parsed) {
275
+ if (parsed.values)
276
+ return new FilterInvalid(BARE_WITH_VALUES_MESSAGE, "OQX");
277
+ if (parsed.select.length === 0)
278
+ return new FilterInvalid(BARE_NEEDS_SELECT_MESSAGE, "OQX");
279
+ return null;
280
+ }
243
281
  export function oqxRun(store, repoId, source, opts = {}) {
244
282
  try {
245
283
  return oqxRunInner(store, repoId, source, opts);
@@ -252,9 +290,22 @@ export function oqxRun(store, repoId, source, opts = {}) {
252
290
  }
253
291
  }
254
292
  function oqxRunInner(store, repoId, source, opts) {
293
+ const raw = parse(source);
294
+ const reserved = reservedAlias(raw.select);
295
+ if (reserved)
296
+ throw reserved;
297
+ return oqxRunQuery(store, repoId, raw, opts);
298
+ }
299
+ /**
300
+ * The AST entry (4.1): run an already parsed query — the source path above
301
+ * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
302
+ * its slot items to the RAW tree so a user alias a template reads is inlined
303
+ * by the alias resolution here. Errors are the library's own (`OqxError`);
304
+ * `oqxRun` normalizes them, a caller of this entry does the same.
305
+ */
306
+ export function oqxRunQuery(store, repoId, raw, opts = {}) {
255
307
  // The query's `select` aliases are resolved HERE, once, before the runner
256
308
  // renames/injects items (an engine evaluates the query it is given).
257
- const raw = parse(source);
258
309
  checkRootSpellings(raw);
259
310
  const parsed = rewriteQuery(resolveAliases(raw));
260
311
  const consumer = parsed.consumer;
@@ -289,11 +340,21 @@ function oqxRunInner(store, repoId, source, opts) {
289
340
  // the final page below and returned as `values` with `hits` empty.
290
341
  const topValues = parsed.values;
291
342
  const userSelect = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
292
- if (!topValues) {
343
+ // `bare` (4.2): the hits are the projection alone — refused without one and
344
+ // with `values`; with no envelope there is no collision to refuse either, a
345
+ // projected `id`/`path` is simply a column.
346
+ const bare = opts.bare === true;
347
+ if (bare) {
348
+ const misuse = bareMisuse(parsed);
349
+ if (misuse)
350
+ throw misuse;
351
+ }
352
+ else if (!topValues) {
293
353
  const collision = envelopeCollision(userSelect);
294
354
  if (collision)
295
355
  throw collision;
296
356
  }
357
+ const render = bare ? toBareHit : toHit;
297
358
  // On the collect path a top-level `limit`/`offset` is ALSO taken out of the
298
359
  // engine query and applied here, after the runner's own distinct — the engine
299
360
  // would otherwise bound the raw rows before dedup (`select distinct type
@@ -307,13 +368,13 @@ function oqxRunInner(store, repoId, source, opts) {
307
368
  if (consumer === "first" || consumer === "single") {
308
369
  const row = res.consumer === "first" || res.consumer === "single" ? res.row : null;
309
370
  if (topValues)
310
- return { hits: [], truncated: false, cursor: null, consumer, values: row == null ? [] : [toHit(row)[VALUE_KEY]] };
311
- return { hits: row == null ? [] : [toHit(row)], truncated: false, cursor: null, consumer };
371
+ return { hits: [], truncated: false, cursor: null, consumer, values: row == null ? [] : [shapeRow(row).proj[VALUE_KEY]] };
372
+ return { hits: row == null ? [] : [render(shapeRow(row))], truncated: false, cursor: null, consumer };
312
373
  }
313
374
  // collect: keyset pagination on (path, id) when the order is the default.
314
- let rows = (res.consumer === "collect" ? res.rows : []).map(toHit);
375
+ let rows = (res.consumer === "collect" ? res.rows : []).map(shapeRow);
315
376
  if (topDistinct)
316
- rows = dedupHitsByProjection(rows);
377
+ rows = dedupByProjection(rows, bare);
317
378
  // The query's own bound defines the result SET; the `limit`/`cursor` options
318
379
  // then page within it.
319
380
  const offset = constBound(topOffset, "offset") ?? 0;
@@ -331,9 +392,11 @@ function oqxRunInner(store, repoId, source, opts) {
331
392
  page = page.slice(0, cap);
332
393
  const last = page[page.length - 1];
333
394
  const cursor = truncated && last && !custom ? encodeCursor(last.path, last.id) : null;
395
+ // The envelope is stripped LAST (`bare`): the page and the cursor above ran
396
+ // on identity, so a bare result pages exactly like the enveloped one.
334
397
  if (topValues)
335
- return { hits: [], truncated, cursor, consumer: "collect", values: page.map((h) => h[VALUE_KEY]) };
336
- return { hits: page, truncated, cursor, consumer: "collect" };
398
+ return { hits: [], truncated, cursor, consumer: "collect", values: page.map((s) => s.proj[VALUE_KEY]) };
399
+ return { hits: page.map(render), truncated, cursor, consumer: "collect" };
337
400
  }
338
401
  export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery) {
339
402
  const phrases = collectSemanticPhrases(source);
@@ -1,14 +1,14 @@
1
1
  export declare const SPEC_VERSIONS: {
2
- readonly oqx: "0.20";
2
+ readonly oqx: "0.21";
3
3
  readonly format: "0.2";
4
4
  readonly reconcile: "2.3";
5
- readonly store: "13.5";
5
+ readonly store: "14.1";
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.3";
10
+ readonly sync: "2.2";
11
+ readonly surface: "4.4";
12
+ readonly cli: "4.3";
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.1",
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.3",
21
+ sync: "2.2",
22
+ surface: "4.4",
23
+ cli: "4.3",
24
24
  };
25
25
  //# sourceMappingURL=spec-versions.js.map