@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.
- package/README.md +1 -1
- package/dist/src/core/ids.d.ts +4 -0
- package/dist/src/core/ids.js +17 -0
- package/dist/src/core/ingest.d.ts +39 -3
- package/dist/src/core/ingest.js +233 -202
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +28 -3
- package/dist/src/core/store/size.d.ts +76 -0
- package/dist/src/core/store/size.js +223 -0
- package/dist/src/core/store/store.d.ts +9 -0
- package/dist/src/core/store/store.js +11 -0
- package/dist/src/graph/history.js +15 -12
- package/dist/src/index.d.ts +12 -5
- package/dist/src/index.js +18 -7
- package/dist/src/mcp/arguments.d.ts +9 -2
- package/dist/src/mcp/arguments.js +15 -2
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +3 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +27 -6
- package/dist/src/mcp/server.d.ts +56 -1
- package/dist/src/mcp/server.js +365 -203
- package/dist/src/mutate/apply.d.ts +93 -12
- package/dist/src/mutate/apply.js +203 -113
- package/dist/src/mutate/canonical.d.ts +27 -0
- package/dist/src/mutate/canonical.js +138 -0
- package/dist/src/mutate/commit.d.ts +150 -0
- package/dist/src/mutate/commit.js +349 -0
- package/dist/src/mutate/doc-store.d.ts +3 -3
- package/dist/src/mutate/doc-store.js +21 -6
- package/dist/src/mutate/docs.d.ts +42 -87
- package/dist/src/mutate/docs.js +50 -204
- package/dist/src/mutate/effects.d.ts +363 -0
- package/dist/src/mutate/effects.js +1161 -0
- package/dist/src/mutate/materialize.d.ts +49 -0
- package/dist/src/mutate/materialize.js +174 -0
- package/dist/src/mutate/opset.d.ts +27 -2
- package/dist/src/mutate/opset.js +12 -5
- package/dist/src/mutate/plan-update.d.ts +1 -46
- package/dist/src/mutate/plan-update.js +19 -67
- package/dist/src/mutate/plan.d.ts +210 -0
- package/dist/src/mutate/plan.js +512 -0
- package/dist/src/mutate/select.d.ts +89 -0
- package/dist/src/mutate/select.js +879 -0
- package/dist/src/mutate/sugar.d.ts +135 -0
- package/dist/src/mutate/sugar.js +167 -0
- package/dist/src/mutate/tree.d.ts +4 -2
- package/dist/src/oqx-js/planner.js +4 -0
- package/dist/src/oqx-js/run.d.ts +28 -1
- package/dist/src/oqx-js/run.js +85 -22
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +17 -26
- package/dist/src/surface-paths.js +27 -22
- package/dist/src/sync/checkpoint.d.ts +5 -2
- package/dist/src/sync/checkpoint.js +10 -6
- package/dist/src/sync/freshness.d.ts +10 -7
- package/dist/src/sync/freshness.js +15 -17
- package/dist/src/sync/observe.d.ts +4 -1
- package/dist/src/sync/observe.js +56 -5
- package/dist/src/sync/recovery.d.ts +4 -0
- package/dist/src/sync/recovery.js +4 -1
- package/dist/src/sync/stat-cache.d.ts +7 -0
- package/dist/src/sync/stat-cache.js +22 -0
- package/dist/src/sync/tombstone.js +23 -19
- package/dist/src/sync/watcher.d.ts +2 -0
- package/dist/src/sync/watcher.js +4 -1
- package/dist/src/sync/writer-lock.d.ts +15 -0
- package/dist/src/sync/writer-lock.js +104 -17
- package/dist/src/version.d.ts +7 -2
- package/dist/src/version.js +5 -2
- 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
|
}
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/oqx-js/run.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
226
|
-
// except the envelope's `id`/`path
|
|
227
|
-
//
|
|
228
|
-
|
|
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
|
|
232
|
-
const
|
|
233
|
-
|
|
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(
|
|
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
|
-
|
|
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 ? [] : [
|
|
311
|
-
return { hits: row == null ? [] : [
|
|
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(
|
|
375
|
+
let rows = (res.consumer === "collect" ? res.rows : []).map(shapeRow);
|
|
315
376
|
if (topDistinct)
|
|
316
|
-
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((
|
|
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.
|
|
2
|
+
readonly oqx: "0.21";
|
|
3
3
|
readonly format: "0.2";
|
|
4
4
|
readonly reconcile: "2.3";
|
|
5
|
-
readonly store: "
|
|
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: "
|
|
10
|
-
readonly sync: "2.
|
|
11
|
-
readonly surface: "
|
|
12
|
-
readonly cli: "3
|
|
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: "
|
|
16
|
+
store: "14.1",
|
|
17
17
|
properties: "1.1",
|
|
18
18
|
graph: "1.1",
|
|
19
19
|
search: "2.0",
|
|
20
|
-
mutate: "
|
|
21
|
-
sync: "2.
|
|
22
|
-
surface: "
|
|
23
|
-
cli: "3
|
|
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
|