@omgbase/core 0.15.0 → 0.21.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/dist/src/core/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +9 -8
- package/dist/src/core/read/document.js +0 -2
- package/dist/src/core/store/gc.d.ts +13 -1
- package/dist/src/core/store/gc.js +24 -5
- package/dist/src/core/store/size.d.ts +79 -0
- package/dist/src/core/store/size.js +232 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- package/dist/src/mcp/arguments.d.ts +10 -2
- package/dist/src/mcp/arguments.js +37 -3
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +63 -10
- package/dist/src/mcp/server.d.ts +34 -1
- package/dist/src/mcp/server.js +158 -46
- package/dist/src/mutate/canonical.d.ts +27 -0
- package/dist/src/mutate/canonical.js +138 -0
- package/dist/src/mutate/effects.d.ts +58 -8
- package/dist/src/mutate/effects.js +312 -43
- package/dist/src/mutate/known-ids.d.ts +19 -0
- package/dist/src/mutate/known-ids.js +47 -7
- package/dist/src/mutate/ops.d.ts +1 -0
- package/dist/src/mutate/ops.js +20 -1
- package/dist/src/mutate/plan.d.ts +26 -16
- package/dist/src/mutate/plan.js +152 -112
- package/dist/src/mutate/select.d.ts +78 -0
- package/dist/src/mutate/select.js +903 -0
- package/dist/src/mutate/tree.d.ts +2 -2
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +74 -28
- package/dist/src/oqx-js/names.d.ts +40 -0
- package/dist/src/oqx-js/names.js +347 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +17 -9
- package/dist/src/oqx-js/root-row.d.ts +15 -4
- package/dist/src/oqx-js/root-row.js +18 -10
- package/dist/src/oqx-js/run.d.ts +45 -1
- package/dist/src/oqx-js/run.js +114 -32
- package/dist/src/oqx-js/sql/translate.js +1 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/sync/observe.js +9 -3
- package/dist/src/version.d.ts +7 -2
- package/dist/src/version.js +5 -2
- package/package.json +2 -2
|
@@ -16,8 +16,8 @@ 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
|
+
/** 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";
|
|
21
21
|
export declare class MutationError extends Error {
|
|
22
22
|
code: MutationErrorCode | string;
|
|
23
23
|
data: Record<string, unknown>;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { DataContext } from "@omgbase/oqx";
|
|
2
|
+
import { type Root } from "./root-row.js";
|
|
2
3
|
import type { Store } from "../core/store/store.js";
|
|
3
4
|
import type { SemanticVec } from "../search/cel/compile.js";
|
|
4
5
|
export type Target = "docs" | "blocks" | "nodes" | "links";
|
|
6
|
+
/** Every row kind the context tags (spec/surface §1.1): the four targets and
|
|
7
|
+
* `revisions` (4.7) — the history target, whose rows are the `revisions` ×
|
|
8
|
+
* `commits` join. `Target` stays the four kinds the unknown-names pass judges
|
|
9
|
+
* (names.ts keys its tables by it); everything that tags, scans or renders a
|
|
10
|
+
* row is keyed by `Kind`. */
|
|
11
|
+
export type Kind = Root;
|
|
5
12
|
declare const TARGET: unique symbol;
|
|
6
13
|
/** The engine attributes per kind (spec/surface §1.2 table). Exported so the
|
|
7
14
|
* translator's tests pin the two against each other. */
|
|
8
|
-
export declare const ATTRIBUTES: Readonly<Record<
|
|
15
|
+
export declare const ATTRIBUTES: Readonly<Record<Kind, ReadonlySet<string>>>;
|
|
9
16
|
/** The source handles of a document (§1.2): `data` is the merged authored view
|
|
10
17
|
* (frontmatter + inline), the other two one source each. */
|
|
11
18
|
export declare const SOURCE_HANDLES: ReadonlySet<string>;
|
|
@@ -31,7 +38,7 @@ export interface StoreContextOptions {
|
|
|
31
38
|
export declare function makeStoreContext(store: Store, repoId: string, opts?: StoreContextOptions): DataContext;
|
|
32
39
|
/** Tag SQL rows with their target so the store context resolves them (used by the
|
|
33
40
|
* tier-3 planner to hand produced rows back for residual evaluation). */
|
|
34
|
-
export declare function tagRows(rows: Record<string, unknown>[], target:
|
|
41
|
+
export declare function tagRows(rows: Record<string, unknown>[], target: Kind): unknown[];
|
|
35
42
|
/**
|
|
36
43
|
* A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
|
|
37
44
|
* values, 1.2) is rendered as `{ id, path }` (3.0) — its id column and its
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// pushes the hot predicates down; both must agree (conformance suite).
|
|
14
14
|
import { semantics } from "@omgbase/oqx";
|
|
15
15
|
import { storeIndexFor } from "./store-index.js";
|
|
16
|
-
import {
|
|
16
|
+
import { ROOTS, ROOT_NAMES, bareTargetMessage } from "./root-row.js";
|
|
17
17
|
import { COLS, FROM, ORDER, guards } from "./sql/scan.js";
|
|
18
18
|
import { detectRange } from "../core/store/properties.js";
|
|
19
19
|
import { docsRead } from "../core/read/document.js";
|
|
@@ -57,15 +57,29 @@ function tagAll(rows, t) {
|
|
|
57
57
|
// `hash`/`raw` is simply the engine's attribute (before this batch of 3.0 the
|
|
58
58
|
// attributes were `$`-prefixed and the bare spelling was an error, §9). A bare
|
|
59
59
|
// target name — `docs`, `links`, … — is a loud error of its own (root-row.ts).
|
|
60
|
+
// `revisions` on a document is a RELATION (its revision list, §1.2, 4.7) in
|
|
61
|
+
// `rel.docs` below, and names.ts's `RELATIONS.docs` knows it as one.
|
|
60
62
|
const DOC_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "title", "tags", "format"]);
|
|
61
63
|
const BLOCK_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "doc_id", "type", "text", "attrs", "depth", "ordinal"]);
|
|
62
64
|
const NODE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs"]);
|
|
63
65
|
const EDGE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "via_id", "dst_id", "dst_path", "uri", "field", "dst_kind", "predicate", "provenance", "anchor"]);
|
|
66
|
+
// The history target (spec/surface §1.2, 4.7): a revision row's own columns
|
|
67
|
+
// under the names `docs_history` already uses (`rev`, `seq`, `commit`, `origin`,
|
|
68
|
+
// `actor`, `hash`), the commit's `ts` and `reason`, and `is_current`.
|
|
69
|
+
const REVISION_ATTRIBUTES = new Set(["id", "path", "rev", "seq", "doc_id", "commit", "ts", "origin", "actor", "reason", "hash", "is_current"]);
|
|
64
70
|
/** The engine attributes per kind (spec/surface §1.2 table). Exported so the
|
|
65
71
|
* translator's tests pin the two against each other. */
|
|
66
72
|
export const ATTRIBUTES = {
|
|
67
|
-
docs: DOC_ATTRIBUTES, blocks: BLOCK_ATTRIBUTES, nodes: NODE_ATTRIBUTES, links: EDGE_ATTRIBUTES,
|
|
73
|
+
docs: DOC_ATTRIBUTES, blocks: BLOCK_ATTRIBUTES, nodes: NODE_ATTRIBUTES, links: EDGE_ATTRIBUTES, revisions: REVISION_ATTRIBUTES,
|
|
68
74
|
};
|
|
75
|
+
// The revisions scan (spec/surface §1.1, 4.7): every revision of every document
|
|
76
|
+
// of the repository — tombstoned documents included, history is history — with
|
|
77
|
+
// its commit, in the keyset order `(path as of the revision, rev_id)`. The
|
|
78
|
+
// `revisions` table carries no `repo_id`, so the repository guard joins `docs`.
|
|
79
|
+
// `is_current` is `docs_history`'s `isCurrent`: the revision is the document's
|
|
80
|
+
// `current_rev`.
|
|
81
|
+
const REVISION_COLS = "r.rev_id, r.doc_id, r.seq, r.rendered_hash, r.path, r.commit_id, c.ts, c.origin, c.actor, c.reason, (d.current_rev = r.rev_id) AS is_current";
|
|
82
|
+
const REVISION_FROM = "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
|
|
69
83
|
/** The source handles of a document (§1.2): `data` is the merged authored view
|
|
70
84
|
* (frontmatter + inline), the other two one source each. */
|
|
71
85
|
export const SOURCE_HANDLES = new Set(["data", "frontmatter", "inline"]);
|
|
@@ -87,11 +101,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
87
101
|
return Object.values(r)[0];
|
|
88
102
|
};
|
|
89
103
|
// ---- root collections (the shared scan shape of sql/scan.ts: live rows of
|
|
90
|
-
// this repo, ordered for a stable (path, id) default
|
|
104
|
+
// this repo, ordered for a stable (path, id) default; the revisions scan is
|
|
105
|
+
// its own statement above, in the same (path, id) keyset order) -------------
|
|
91
106
|
const scan = (t) => {
|
|
107
|
+
if (t === "revisions")
|
|
108
|
+
return tagAll(all(`SELECT ${REVISION_COLS} FROM ${REVISION_FROM} WHERE d.repo_id = ? ORDER BY r.path, r.rev_id`, repoId), t);
|
|
92
109
|
const g = guards(t, repoId);
|
|
93
110
|
return tagAll(all(`SELECT ${COLS[t]} FROM ${FROM[t]} WHERE ${g.sql} ORDER BY ${ORDER[t]}`, ...g.params), t);
|
|
94
111
|
};
|
|
112
|
+
// A document's revisions (the `revisions` relation of a docs row, §1.2, 4.7):
|
|
113
|
+
// oldest first, as `docs_history` lists them.
|
|
114
|
+
const docRevisions = (docId) => tagAll(all(`SELECT ${REVISION_COLS} FROM ${REVISION_FROM} WHERE r.doc_id = ? ORDER BY r.seq`, docId), "revisions");
|
|
95
115
|
// A root scan is handed out LAZILY: an array (a `Proxy` over one — `Array.isArray`,
|
|
96
116
|
// iteration, indexing, `length`, rendering all see a plain array of tagged
|
|
97
117
|
// rows) that runs its SELECT on first use. The engine's optimizer asks
|
|
@@ -138,21 +158,14 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
138
158
|
rootScans.set(proxy, { target: t, rows: fill });
|
|
139
159
|
return proxy;
|
|
140
160
|
};
|
|
141
|
-
const rootFns =
|
|
142
|
-
docs: () => lazyRoot("docs"), blocks: () => lazyRoot("blocks"), nodes: () => lazyRoot("nodes"), links: () => lazyRoot("links"),
|
|
143
|
-
};
|
|
161
|
+
const rootFns = Object.fromEntries(ROOT_NAMES.map((t) => [t, () => lazyRoot(t)]));
|
|
144
162
|
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18 `rootObject`): the
|
|
145
163
|
// repository, reached as `^$it` from a top-level row (`0^$it` from any depth).
|
|
146
|
-
// Its own enumerable keys are the
|
|
147
|
-
// `entries(^$it)` names them without running a scan and
|
|
148
|
-
// same handle as `^docs` — and `get` answers `id` with the
|
|
149
|
-
// attribute, not an entry). Tagged so `get` recognizes it.
|
|
150
|
-
const rootRow = Object.defineProperties({ [ROOT_ROW]: true }, {
|
|
151
|
-
docs: { enumerable: true, get: () => lazyRoot("docs") },
|
|
152
|
-
blocks: { enumerable: true, get: () => lazyRoot("blocks") },
|
|
153
|
-
nodes: { enumerable: true, get: () => lazyRoot("nodes") },
|
|
154
|
-
links: { enumerable: true, get: () => lazyRoot("links") },
|
|
155
|
-
});
|
|
164
|
+
// Its own enumerable keys are the five collections in `ROOT_NAMES` order —
|
|
165
|
+
// lazy getters, so `entries(^$it)` names them without running a scan and
|
|
166
|
+
// `^$it.docs` is the same handle as `^docs` — and `get` answers `id` with the
|
|
167
|
+
// repository id (an attribute, not an entry). Tagged so `get` recognizes it.
|
|
168
|
+
const rootRow = Object.defineProperties({ [ROOT_ROW]: true }, Object.fromEntries(ROOT_NAMES.map((t) => [t, { enumerable: true, get: () => lazyRoot(t) }])));
|
|
156
169
|
// ---- attrs / property decoding --------------------------------------------
|
|
157
170
|
const parseJson = (v) => {
|
|
158
171
|
if (typeof v !== "string")
|
|
@@ -298,6 +311,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
298
311
|
docs: {
|
|
299
312
|
nodes: (r) => docNodesInOrder(r.doc_id, r.path),
|
|
300
313
|
blocks: (r) => tagAll(docBlocksPreorder(r.doc_id, r.path).map((x) => x.row), "blocks"),
|
|
314
|
+
// A document's revision list (spec/surface §1.2, 4.7), oldest first.
|
|
315
|
+
revisions: (r) => docRevisions(r.doc_id),
|
|
301
316
|
// The linked / linking DOCUMENTS are reached through the link rows (3.0
|
|
302
317
|
// follow-up, §9: the `out` / `in` relations are gone): `out_links collect
|
|
303
318
|
// distinct { from dst }` and `in_links collect distinct { from doc }`.
|
|
@@ -351,6 +366,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
351
366
|
},
|
|
352
367
|
},
|
|
353
368
|
links: {},
|
|
369
|
+
revisions: {},
|
|
354
370
|
};
|
|
355
371
|
const jattr = (row, k) => {
|
|
356
372
|
const a = row.attrs;
|
|
@@ -417,6 +433,11 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
417
433
|
return undefined;
|
|
418
434
|
return tag(one(`SELECT * FROM docs WHERE doc_id = ?`, row.dst_node), "docs");
|
|
419
435
|
};
|
|
436
|
+
// `doc` on a revision row (spec/surface §1.2, 4.7): the document's CURRENT
|
|
437
|
+
// live row — the revision's `path` may be an earlier one — and absent (the
|
|
438
|
+
// single-with-no-row marker, as `dst` on a dangling link) for a tombstoned
|
|
439
|
+
// document: its revisions are rows, `doc == null` tells them apart.
|
|
440
|
+
const revisionDoc = (row) => tag(one(`SELECT * FROM docs WHERE doc_id = ? AND deleted_commit IS NULL`, row.doc_id), "docs");
|
|
420
441
|
const owningBlock = (row) => {
|
|
421
442
|
if (!row.block_id)
|
|
422
443
|
return undefined;
|
|
@@ -501,6 +522,22 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
501
522
|
case "dst_kind":
|
|
502
523
|
case "anchor": return row[name];
|
|
503
524
|
}
|
|
525
|
+
if (t === "revisions")
|
|
526
|
+
switch (name) {
|
|
527
|
+
// the `revisions` × `commits` columns under docs_history's names (spec/surface §1.2, 4.7).
|
|
528
|
+
case "id":
|
|
529
|
+
case "rev": return row.rev_id;
|
|
530
|
+
case "path": return referencePath(String(row.path));
|
|
531
|
+
case "seq": return row.seq;
|
|
532
|
+
case "doc_id": return row.doc_id;
|
|
533
|
+
case "commit": return row.commit_id;
|
|
534
|
+
case "ts": return row.ts;
|
|
535
|
+
case "origin": return row.origin;
|
|
536
|
+
case "actor": return row.actor ?? null;
|
|
537
|
+
case "reason": return row.reason ?? null;
|
|
538
|
+
case "hash": return hex(row.rendered_hash);
|
|
539
|
+
case "is_current": return row.is_current === 1 || row.is_current === true;
|
|
540
|
+
}
|
|
504
541
|
return undefined;
|
|
505
542
|
};
|
|
506
543
|
// `tags` on a document: authored ∪ hashtags, each list as the property rule
|
|
@@ -561,8 +598,11 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
561
598
|
return r;
|
|
562
599
|
if (t === "nodes" && key === "section")
|
|
563
600
|
return r;
|
|
564
|
-
// reach-through to owning entities,
|
|
565
|
-
//
|
|
601
|
+
// reach-through to owning entities, from a link row to its destination and
|
|
602
|
+
// from a revision to its document's current row — statically single
|
|
603
|
+
// relations: absent is the single-with-no-row marker.
|
|
604
|
+
if (t === "revisions" && key === "doc")
|
|
605
|
+
return revisionDoc(r) ?? ABSENT_SINGLE;
|
|
566
606
|
if (t !== "docs" && key === "doc")
|
|
567
607
|
return owningDoc(r) ?? ABSENT_SINGLE;
|
|
568
608
|
if (t === "nodes" && key === "block")
|
|
@@ -576,9 +616,9 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
576
616
|
// A bare target name that is not one of this row's relations reads a
|
|
577
617
|
// property the row has none of (spec/surface §1.1, 2.0): loud, naming the
|
|
578
618
|
// root-row spelling. `docs`/`links` never get here (run.ts refuses them
|
|
579
|
-
// statically); `blocks` on a block or edge row
|
|
580
|
-
//
|
|
581
|
-
if (
|
|
619
|
+
// statically); `blocks` on a block or edge row, `nodes` on a node or edge
|
|
620
|
+
// row and `revisions` on anything but a document do.
|
|
621
|
+
if (ROOTS.has(key))
|
|
582
622
|
throw new FilterInvalid(bareTargetMessage(key, null), "OQX");
|
|
583
623
|
// the author's data.
|
|
584
624
|
if (t === "docs") {
|
|
@@ -652,8 +692,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
652
692
|
}
|
|
653
693
|
};
|
|
654
694
|
const textMatch = (t, r, terms) => {
|
|
655
|
-
if (t === "links")
|
|
656
|
-
throw new FilterInvalid(
|
|
695
|
+
if (t === "links" || t === "revisions")
|
|
696
|
+
throw new FilterInvalid(`text(...) is not available on the ${t} target`, "10 §5");
|
|
657
697
|
const match = sanitizeFtsQuery(terms);
|
|
658
698
|
if (match === "")
|
|
659
699
|
return false;
|
|
@@ -676,7 +716,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
676
716
|
return one(`SELECT vec FROM embeddings WHERE content_hash = ? AND ctx_hash = ? AND model = ?`, r.raw_hash, Buffer.from(ctxHashHex(ctx), "hex"), model);
|
|
677
717
|
};
|
|
678
718
|
const semanticScore = (t, r, phrase) => {
|
|
679
|
-
if (t === "nodes" || t === "links")
|
|
719
|
+
if (t === "nodes" || t === "links" || t === "revisions")
|
|
680
720
|
throw new FilterInvalid("semantic(...) is available on the docs and blocks targets", "OQX semantic");
|
|
681
721
|
const resolved = sv?.get(phrase);
|
|
682
722
|
if (!resolved)
|
|
@@ -771,10 +811,12 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
771
811
|
return row.node_id;
|
|
772
812
|
if (t === "links")
|
|
773
813
|
return row.edge_id;
|
|
814
|
+
if (t === "revisions")
|
|
815
|
+
return row.rev_id;
|
|
774
816
|
return row;
|
|
775
817
|
};
|
|
776
818
|
return {
|
|
777
|
-
// The root scope's names (spec/oqx §2 step 5 at the root): the
|
|
819
|
+
// The root scope's names (spec/oqx §2 step 5 at the root): the five lazy
|
|
778
820
|
// scans and `id`, the repository id — so `^docs` / `0^docs` from any row is
|
|
779
821
|
// the scan and `^id` / `0^id` the id.
|
|
780
822
|
root(name) {
|
|
@@ -817,7 +859,9 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
817
859
|
if (collection === null || typeof collection !== "object")
|
|
818
860
|
return undefined;
|
|
819
861
|
const root = rootScans.get(collection);
|
|
820
|
-
|
|
862
|
+
// The revisions scan has no store index (4.7): the engine builds its own
|
|
863
|
+
// over the materialized rows (one SELECT per context).
|
|
864
|
+
if (!root || root.target === "revisions")
|
|
821
865
|
return undefined;
|
|
822
866
|
const key = `${root.target}\u0000${JSON.stringify(path)}`;
|
|
823
867
|
let index = indexes.get(key);
|
|
@@ -891,8 +935,10 @@ export function rowRef(value) {
|
|
|
891
935
|
if (!t)
|
|
892
936
|
return undefined;
|
|
893
937
|
const r = value;
|
|
894
|
-
const id = t === "docs" ? r.doc_id : t === "blocks" ? r.block_id : t === "nodes" ? r.node_id : r.edge_id;
|
|
895
|
-
|
|
938
|
+
const id = t === "docs" ? r.doc_id : t === "blocks" ? r.block_id : t === "nodes" ? r.node_id : t === "revisions" ? r.rev_id : r.edge_id;
|
|
939
|
+
// a revision's path is `revisions.path` — the path it was committed under
|
|
940
|
+
// (a move re-paths the current revision; §1.2, 4.7).
|
|
941
|
+
const path = t === "docs" || t === "revisions" ? r.path : r.__path;
|
|
896
942
|
return { id: String(id), path: path == null ? "" : referencePath(String(path)) };
|
|
897
943
|
}
|
|
898
944
|
export { TARGET };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Query, ReferenceStep } from "@omgbase/oqx";
|
|
2
|
+
import { type Kind } from "./context.js";
|
|
3
|
+
import type { Store } from "../core/store/store.js";
|
|
4
|
+
/** What to do with a tier-3 name on an open kind: record it in `hints` (every read) or refuse the query (a write selector). */
|
|
5
|
+
export type UnknownNames = "hint" | "refuse";
|
|
6
|
+
/** The relations of each kind and the kind of the rows they yield (spec/surface §1.2). */
|
|
7
|
+
export declare const RELATIONS: Readonly<Record<Kind, Readonly<Record<string, Kind>>>>;
|
|
8
|
+
/** The self and reach-through handles of each kind and the kind of the row they reach (§1.2). */
|
|
9
|
+
export declare const HANDLES: Readonly<Record<Kind, Readonly<Record<string, Kind>>>>;
|
|
10
|
+
/** The vocabulary of a kind in a repository (cached on the head commit; the SQL runs only when a name is not intrinsic). */
|
|
11
|
+
export declare function vocabulary(store: Store, repoId: string, target: Kind): {
|
|
12
|
+
present: boolean;
|
|
13
|
+
keys: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
/** The kind of the rows a scope holds, from its row chain (AST.md §5b); null when the chain reaches a value no kind describes. */
|
|
16
|
+
export declare function kindOfScope(steps: readonly ReferenceStep[]): Kind | null;
|
|
17
|
+
/** Optimal string alignment distance (Damerau–Levenshtein with adjacent transpositions, each edit 1). */
|
|
18
|
+
export declare function editDistance(a: string, b: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* The nearest match of `name` among `candidates` (§1.5): case-insensitive; a
|
|
21
|
+
* candidate qualifies when its distance is ≤ 2, or when one of the two is a
|
|
22
|
+
* substring of the other and the shorter has at least three characters; the
|
|
23
|
+
* lowest distance wins, ties broken by the shorter candidate, then
|
|
24
|
+
* alphabetically. Null when nothing qualifies.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nearest(name: string, candidates: readonly string[]): string | null;
|
|
27
|
+
/** The error of a closed kind: `` `target` is not a field of links (fields: id, path, …); did you mean `…`? ``. */
|
|
28
|
+
export declare function fieldErrorMessage(store: Store, repoId: string, target: Kind, name: string): string;
|
|
29
|
+
/** The hint of an open kind: `` `preview` is not a field of blocks and no block in this repository carries a property by that name; did you mean `…`? ``.
|
|
30
|
+
* Under a source handle the name is an authored lookup by construction, so the
|
|
31
|
+
* hint says only that: `` no document in this repository carries a property `path` (`data.path`); did you mean `…`? ``,
|
|
32
|
+
* the nearest match drawn from the authored keys alone. */
|
|
33
|
+
export declare function propertyHintMessage(store: Store, repoId: string, target: Kind, name: string, handle?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Judge every name the query reads. Returns the hints (one per offending name
|
|
36
|
+
* of an open kind, first-reference order, deduplicated) under `"hint"`; throws
|
|
37
|
+
* `FilterInvalid` for a tier-3 name on a closed kind in either mode, and for
|
|
38
|
+
* one on an open kind under `"refuse"`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkNames(store: Store, repoId: string, raw: Query, mode: UnknownNames): string[];
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Unknown names (spec/surface §1.5, 4.6). A query that projects, filters or
|
|
2
|
+
// orders by a name no row of the scope's kind can carry — `select path, target
|
|
3
|
+
// from links` (links have `dst_path`), `where preview != ""` on blocks — used
|
|
4
|
+
// to run silently: an absent column in every row, a predicate true or false of
|
|
5
|
+
// every row. This pass judges every name the query reads (`@omgbase/oqx`'s
|
|
6
|
+
// `references`, AST.md §5b) against the VOCABULARY of the row kind it is read
|
|
7
|
+
// from, statically — before any row is read, so the verdict never depends on
|
|
8
|
+
// the query's own filter — and either records a hint or refuses the query.
|
|
9
|
+
//
|
|
10
|
+
// The three tiers of a name at a scope whose kind is known:
|
|
11
|
+
// 1. an engine attribute, handle or relation of the kind — known;
|
|
12
|
+
// 2. an authored property at least one live object of the kind in this
|
|
13
|
+
// repository carries (docs: a frontmatter or inline key — `stage` on three
|
|
14
|
+
// processes is enough; blocks and nodes: a key of a live row's `attrs`,
|
|
15
|
+
// the bare-name read of those kinds) — known, however partial;
|
|
16
|
+
// 3. a name no object of the kind carries — the complaint.
|
|
17
|
+
// Severity: `links` has a closed row shape (no authored data reaches a link
|
|
18
|
+
// row), so a tier-3 name there is `filter_invalid`; docs, blocks and nodes are
|
|
19
|
+
// open — on a read the result carries one hint per offending name and the rows
|
|
20
|
+
// still come back; a write selector (the AST entry, `unknownNames: "refuse"`)
|
|
21
|
+
// is refused, since a name no row carries would select every row. A repository
|
|
22
|
+
// with no live object of the kind has no vocabulary: the check is skipped for
|
|
23
|
+
// that kind. The vocabulary is read once per (repository, kind) and cached on
|
|
24
|
+
// the repository's head commit (`MAX(commits.seq)`), the one thing every
|
|
25
|
+
// property or attrs change moves.
|
|
26
|
+
import { references } from "@omgbase/oqx";
|
|
27
|
+
import { ATTRIBUTES, SOURCE_HANDLES } from "./context.js";
|
|
28
|
+
import { ROOTS } from "./root-row.js";
|
|
29
|
+
import { FROM, guards } from "./sql/scan.js";
|
|
30
|
+
import { FilterInvalid } from "../search/cel/parser.js";
|
|
31
|
+
/** The relations of each kind and the kind of the rows they yield (spec/surface §1.2). */
|
|
32
|
+
export const RELATIONS = {
|
|
33
|
+
docs: { blocks: "blocks", nodes: "nodes", out_links: "links", in_links: "links", revisions: "revisions" },
|
|
34
|
+
blocks: { children: "blocks", nodes: "nodes", out_links: "links", section: "nodes" },
|
|
35
|
+
nodes: { blocks: "blocks", subsections: "nodes", children: "nodes" },
|
|
36
|
+
links: {},
|
|
37
|
+
revisions: {},
|
|
38
|
+
};
|
|
39
|
+
/** The self and reach-through handles of each kind and the kind of the row they reach (§1.2). */
|
|
40
|
+
export const HANDLES = {
|
|
41
|
+
docs: { doc: "docs" },
|
|
42
|
+
blocks: { block: "blocks", doc: "docs" },
|
|
43
|
+
nodes: { section: "nodes", doc: "docs", block: "blocks" },
|
|
44
|
+
links: { doc: "docs", dst: "docs" },
|
|
45
|
+
revisions: { doc: "docs" },
|
|
46
|
+
};
|
|
47
|
+
/** The kinds whose row shape is closed: no authored data reaches a row by bare name. */
|
|
48
|
+
const CLOSED = new Set(["links", "revisions"]);
|
|
49
|
+
const NOUN = { docs: "document", blocks: "block", nodes: "node", links: "link", revisions: "revision" };
|
|
50
|
+
const CACHE = new WeakMap();
|
|
51
|
+
function headSeq(store, repoId) {
|
|
52
|
+
const row = store.db.prepare("SELECT COALESCE(MAX(seq), 0) AS seq FROM commits WHERE repo_id = ?").get(repoId);
|
|
53
|
+
return row?.seq ?? 0;
|
|
54
|
+
}
|
|
55
|
+
function repoCache(store, repoId) {
|
|
56
|
+
let repos = CACHE.get(store);
|
|
57
|
+
if (!repos)
|
|
58
|
+
CACHE.set(store, (repos = new Map()));
|
|
59
|
+
const head = headSeq(store, repoId);
|
|
60
|
+
let c = repos.get(repoId);
|
|
61
|
+
if (!c || c.head !== head)
|
|
62
|
+
repos.set(repoId, (c = { head, vocab: {} }));
|
|
63
|
+
return c;
|
|
64
|
+
}
|
|
65
|
+
/** The authored vocabulary of a kind: docs — every live frontmatter or inline
|
|
66
|
+
* key (the merged read a bare name performs); blocks and nodes — every key a
|
|
67
|
+
* live row's `attrs` holds (the bare-name read of those kinds). Computed keys
|
|
68
|
+
* (`$title`, `$tags`) are the intrinsics `title`/`tags`, not authored keys. */
|
|
69
|
+
function authoredKeys(store, repoId, target) {
|
|
70
|
+
let sql;
|
|
71
|
+
switch (target) {
|
|
72
|
+
case "docs":
|
|
73
|
+
sql = "SELECT DISTINCT key FROM properties WHERE repo_id = ? AND deleted_commit IS NULL AND source IN ('frontmatter', 'inline') ORDER BY key";
|
|
74
|
+
return store.db.prepare(sql).all(repoId).map((r) => r.key);
|
|
75
|
+
case "blocks":
|
|
76
|
+
case "nodes": {
|
|
77
|
+
const g = guards(target, repoId);
|
|
78
|
+
const alias = target === "blocks" ? "b" : "n";
|
|
79
|
+
sql = `SELECT DISTINCT j.key AS key FROM ${FROM[target]}, json_each(${alias}.attrs) j WHERE ${g.sql} ORDER BY key`;
|
|
80
|
+
return store.db.prepare(sql).all(...g.params).map((r) => r.key);
|
|
81
|
+
}
|
|
82
|
+
case "links":
|
|
83
|
+
case "revisions": return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function present(store, repoId, target) {
|
|
87
|
+
// A closed kind has no authored vocabulary to be absent; the question never reaches it.
|
|
88
|
+
if (target === "revisions")
|
|
89
|
+
return true;
|
|
90
|
+
const g = guards(target, repoId);
|
|
91
|
+
return store.db.prepare(`SELECT 1 FROM ${FROM[target]} WHERE ${g.sql} LIMIT 1`).get(...g.params) !== undefined;
|
|
92
|
+
}
|
|
93
|
+
/** The vocabulary of a kind in a repository (cached on the head commit; the SQL runs only when a name is not intrinsic). */
|
|
94
|
+
export function vocabulary(store, repoId, target) {
|
|
95
|
+
const v = vocabularyOf(store, repoId, target);
|
|
96
|
+
return { present: v.present, keys: v.list };
|
|
97
|
+
}
|
|
98
|
+
function vocabularyOf(store, repoId, target) {
|
|
99
|
+
const c = repoCache(store, repoId);
|
|
100
|
+
let v = c.vocab[target];
|
|
101
|
+
if (v)
|
|
102
|
+
return v;
|
|
103
|
+
const list = authoredKeys(store, repoId, target);
|
|
104
|
+
const keys = new Set();
|
|
105
|
+
for (const k of list) {
|
|
106
|
+
const segs = k.split(".");
|
|
107
|
+
for (let i = 1; i <= segs.length; i++)
|
|
108
|
+
keys.add(segs.slice(0, i).join("."));
|
|
109
|
+
}
|
|
110
|
+
v = { present: list.length > 0 || present(store, repoId, target), keys, list };
|
|
111
|
+
c.vocab[target] = v;
|
|
112
|
+
return v;
|
|
113
|
+
}
|
|
114
|
+
/** The kind of the rows an expression yields when read on rows of `on` (`"root"`: the root scope), or null when unknown. */
|
|
115
|
+
function kindOfExpr(e, on, readDepth, byDepth) {
|
|
116
|
+
switch (e.kind) {
|
|
117
|
+
case "ident": return kindOfPath(on, [e.name]);
|
|
118
|
+
case "outer": {
|
|
119
|
+
const k = readDepth - e.levels;
|
|
120
|
+
return kindOfPath(k <= 0 ? "root" : byDepth.get(k) ?? null, [e.name]);
|
|
121
|
+
}
|
|
122
|
+
case "member": {
|
|
123
|
+
const path = dotted(e);
|
|
124
|
+
if (path === null)
|
|
125
|
+
return null;
|
|
126
|
+
const head = path.head;
|
|
127
|
+
const base = head.kind === "ident" ? on : (readDepth - head.levels <= 0 ? "root" : byDepth.get(readDepth - head.levels) ?? null);
|
|
128
|
+
return kindOfPath(base, [head.name, ...path.rest]);
|
|
129
|
+
}
|
|
130
|
+
// `refs(x)` resolves document references to live document rows (§1.3).
|
|
131
|
+
case "call": return e.recv === null && e.name === "refs" ? "docs" : null;
|
|
132
|
+
// A value-position block with no projection passes its receiver's rows through (then its own `from`).
|
|
133
|
+
case "op": {
|
|
134
|
+
if (e.sub.select.some((it) => it.name !== "$key") || e.sub.values)
|
|
135
|
+
return null;
|
|
136
|
+
let k = kindOfExpr(e.receiver, on, readDepth, byDepth);
|
|
137
|
+
for (const f of e.sub.from)
|
|
138
|
+
k = k === null ? null : kindOfExpr(f, k, readDepth + 1, byDepth);
|
|
139
|
+
return k;
|
|
140
|
+
}
|
|
141
|
+
default: return null; // a literal, a binding, a tuple (mixed), a template, an operator
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** A dotted navigation headed by a name. */
|
|
145
|
+
function dotted(e) {
|
|
146
|
+
if (e.kind === "ident" || e.kind === "outer")
|
|
147
|
+
return { head: e, rest: [] };
|
|
148
|
+
if (e.kind !== "member")
|
|
149
|
+
return null;
|
|
150
|
+
const inner = dotted(e.recv);
|
|
151
|
+
return inner === null ? null : { head: inner.head, rest: [...inner.rest, e.name] };
|
|
152
|
+
}
|
|
153
|
+
/** Follow a dotted path from a row kind through handles and relations (`doc.blocks` on a link → blocks). */
|
|
154
|
+
function kindOfPath(on, path) {
|
|
155
|
+
let cur = on;
|
|
156
|
+
for (let i = 0; i < path.length; i++) {
|
|
157
|
+
const seg = path[i];
|
|
158
|
+
if (cur === null)
|
|
159
|
+
return null;
|
|
160
|
+
if (cur === "root") {
|
|
161
|
+
if (seg === "$it")
|
|
162
|
+
continue;
|
|
163
|
+
if (!ROOTS.has(seg))
|
|
164
|
+
return null;
|
|
165
|
+
cur = seg;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (seg === "$it" && i === 0)
|
|
169
|
+
continue;
|
|
170
|
+
const next = HANDLES[cur][seg] ?? RELATIONS[cur][seg];
|
|
171
|
+
if (next === undefined)
|
|
172
|
+
return null;
|
|
173
|
+
cur = next;
|
|
174
|
+
}
|
|
175
|
+
return cur === "root" ? null : cur;
|
|
176
|
+
}
|
|
177
|
+
/** The kind of the frontier rows of a walk: the seeds' kind when every destination yields rows of that same kind, else unknown. */
|
|
178
|
+
function kindOfFrontier(seed, f, readDepth, byDepth) {
|
|
179
|
+
if (seed === null)
|
|
180
|
+
return null;
|
|
181
|
+
// A destination block is a value-position `op`: its receiver on the frontier row, then its own `from`.
|
|
182
|
+
for (const d of f.destinations)
|
|
183
|
+
if (kindOfExpr(d, seed, readDepth, byDepth) !== seed)
|
|
184
|
+
return null;
|
|
185
|
+
return seed;
|
|
186
|
+
}
|
|
187
|
+
/** The kind of the rows a scope holds, from its row chain (AST.md §5b); null when the chain reaches a value no kind describes. */
|
|
188
|
+
export function kindOfScope(steps) {
|
|
189
|
+
if (steps.length === 0)
|
|
190
|
+
return null; // the root scope: root names are judged elsewhere
|
|
191
|
+
const byDepth = new Map([[0, "root"]]);
|
|
192
|
+
let cur = "root";
|
|
193
|
+
let readDepth = 0;
|
|
194
|
+
for (const step of steps) {
|
|
195
|
+
const e = step.expr;
|
|
196
|
+
if (e.kind === "follow")
|
|
197
|
+
cur = kindOfFrontier(cur === "root" ? null : cur, e, readDepth, byDepth);
|
|
198
|
+
else
|
|
199
|
+
cur = kindOfExpr(e, cur, readDepth, byDepth);
|
|
200
|
+
byDepth.set(step.depth, cur);
|
|
201
|
+
readDepth = step.depth;
|
|
202
|
+
}
|
|
203
|
+
return cur === "root" ? null : cur;
|
|
204
|
+
}
|
|
205
|
+
/** Judge a dotted name read on rows of `target`; null = known. */
|
|
206
|
+
function judge(store, repoId, target, path) {
|
|
207
|
+
const [head, ...rest] = path;
|
|
208
|
+
// An engine attribute wins on collision; what follows it (`attrs.checked`, `tags.size`) is the value's own navigation.
|
|
209
|
+
if (ATTRIBUTES[target].has(head))
|
|
210
|
+
return null;
|
|
211
|
+
// The source handles reach the authored data under them.
|
|
212
|
+
if (target === "docs" && SOURCE_HANDLES.has(head))
|
|
213
|
+
return rest.length === 0 ? null : judgeAuthored(store, repoId, "docs", rest, head);
|
|
214
|
+
const via = HANDLES[target][head] ?? RELATIONS[target][head];
|
|
215
|
+
if (via !== undefined)
|
|
216
|
+
return rest.length === 0 ? null : judge(store, repoId, via, rest);
|
|
217
|
+
// A bare target name on a row is the context's own loud error (root-row.ts); not doubled here.
|
|
218
|
+
if (ROOTS.has(head))
|
|
219
|
+
return null;
|
|
220
|
+
return judgeAuthored(store, repoId, target, path);
|
|
221
|
+
}
|
|
222
|
+
function judgeAuthored(store, repoId, target, path, handle) {
|
|
223
|
+
const name = path.join(".");
|
|
224
|
+
if (CLOSED.has(target))
|
|
225
|
+
return { target, name, closed: true };
|
|
226
|
+
const v = vocabularyOf(store, repoId, target);
|
|
227
|
+
if (!v.present)
|
|
228
|
+
return null; // no object of the kind: no vocabulary to judge by
|
|
229
|
+
if (v.keys.has(name))
|
|
230
|
+
return null;
|
|
231
|
+
return { target, name, closed: false, ...(handle !== undefined ? { handle } : {}) };
|
|
232
|
+
}
|
|
233
|
+
// ---- messages --------------------------------------------------------------
|
|
234
|
+
/** The names a kind knows, for the nearest match: attributes, handles, relations, the authored keys. */
|
|
235
|
+
function candidates(store, repoId, target) {
|
|
236
|
+
const out = [...ATTRIBUTES[target], ...Object.keys(HANDLES[target]), ...(target === "docs" ? [...SOURCE_HANDLES] : []), ...Object.keys(RELATIONS[target])];
|
|
237
|
+
if (!CLOSED.has(target))
|
|
238
|
+
out.push(...vocabularyOf(store, repoId, target).list);
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
/** Optimal string alignment distance (Damerau–Levenshtein with adjacent transpositions, each edit 1). */
|
|
242
|
+
export function editDistance(a, b) {
|
|
243
|
+
const s = Array.from(a), t = Array.from(b);
|
|
244
|
+
const m = s.length, n = t.length;
|
|
245
|
+
const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
246
|
+
for (let i = 0; i <= m; i++)
|
|
247
|
+
d[i][0] = i;
|
|
248
|
+
for (let j = 0; j <= n; j++)
|
|
249
|
+
d[0][j] = j;
|
|
250
|
+
for (let i = 1; i <= m; i++) {
|
|
251
|
+
for (let j = 1; j <= n; j++) {
|
|
252
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
253
|
+
let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
254
|
+
if (i > 1 && j > 1 && s[i - 1] === t[j - 2] && s[i - 2] === t[j - 1])
|
|
255
|
+
v = Math.min(v, d[i - 2][j - 2] + 1);
|
|
256
|
+
d[i][j] = v;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return d[m][n];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The nearest match of `name` among `candidates` (§1.5): case-insensitive; a
|
|
263
|
+
* candidate qualifies when its distance is ≤ 2, or when one of the two is a
|
|
264
|
+
* substring of the other and the shorter has at least three characters; the
|
|
265
|
+
* lowest distance wins, ties broken by the shorter candidate, then
|
|
266
|
+
* alphabetically. Null when nothing qualifies.
|
|
267
|
+
*/
|
|
268
|
+
export function nearest(name, candidates) {
|
|
269
|
+
const needle = name.toLowerCase();
|
|
270
|
+
// A name under three characters is within distance 2 of nearly anything, so
|
|
271
|
+
// only a substring match (which needs three characters anyway) could qualify.
|
|
272
|
+
if (needle.length < 3)
|
|
273
|
+
return null;
|
|
274
|
+
let best = null;
|
|
275
|
+
for (const c of new Set(candidates)) {
|
|
276
|
+
const hay = c.toLowerCase();
|
|
277
|
+
if (hay === needle)
|
|
278
|
+
continue;
|
|
279
|
+
const d = editDistance(needle, hay);
|
|
280
|
+
const shorter = needle.length <= hay.length ? needle : hay;
|
|
281
|
+
const longer = shorter === needle ? hay : needle;
|
|
282
|
+
const sub = shorter.length >= 3 && longer.includes(shorter);
|
|
283
|
+
if (!(d <= 2 || sub))
|
|
284
|
+
continue;
|
|
285
|
+
// One typo (distance 1) wins first; then a substring match outranks a
|
|
286
|
+
// distance-2 match (`via` → `via_id`, not `id`; `dst_pth` → `dst_path`, not
|
|
287
|
+
// `dst`); within a class the lowest distance, then the shorter candidate,
|
|
288
|
+
// then alphabetical.
|
|
289
|
+
const rank = (x) => (x.d === 1 ? 0 : x.sub ? 1 : 2);
|
|
290
|
+
const better = best === null ||
|
|
291
|
+
rank({ d, sub }) < rank(best) ||
|
|
292
|
+
(rank({ d, sub }) === rank(best) && (d < best.d || (d === best.d && (c.length < best.c.length || (c.length === best.c.length && c < best.c)))));
|
|
293
|
+
if (better)
|
|
294
|
+
best = { c, d, sub };
|
|
295
|
+
}
|
|
296
|
+
return best === null ? null : best.c;
|
|
297
|
+
}
|
|
298
|
+
function suggestion(name, candidates) {
|
|
299
|
+
const c = nearest(name, candidates);
|
|
300
|
+
return c === null ? "" : `; did you mean \`${c}\`?`;
|
|
301
|
+
}
|
|
302
|
+
/** The error of a closed kind: `` `target` is not a field of links (fields: id, path, …); did you mean `…`? ``. */
|
|
303
|
+
export function fieldErrorMessage(store, repoId, target, name) {
|
|
304
|
+
return `\`${name}\` is not a field of ${target} (fields: ${[...ATTRIBUTES[target]].join(", ")})${suggestion(name, candidates(store, repoId, target))}`;
|
|
305
|
+
}
|
|
306
|
+
/** The hint of an open kind: `` `preview` is not a field of blocks and no block in this repository carries a property by that name; did you mean `…`? ``.
|
|
307
|
+
* Under a source handle the name is an authored lookup by construction, so the
|
|
308
|
+
* hint says only that: `` no document in this repository carries a property `path` (`data.path`); did you mean `…`? ``,
|
|
309
|
+
* the nearest match drawn from the authored keys alone. */
|
|
310
|
+
export function propertyHintMessage(store, repoId, target, name, handle) {
|
|
311
|
+
if (handle !== undefined) {
|
|
312
|
+
return `no ${NOUN[target]} in this repository carries a property \`${name}\` (\`${handle}.${name}\`)${suggestion(name, vocabularyOf(store, repoId, target).list)}`;
|
|
313
|
+
}
|
|
314
|
+
return `\`${name}\` is not a field of ${target} and no ${NOUN[target]} in this repository carries a property by that name${suggestion(name, candidates(store, repoId, target))}`;
|
|
315
|
+
}
|
|
316
|
+
// ---- the pass --------------------------------------------------------------
|
|
317
|
+
/**
|
|
318
|
+
* Judge every name the query reads. Returns the hints (one per offending name
|
|
319
|
+
* of an open kind, first-reference order, deduplicated) under `"hint"`; throws
|
|
320
|
+
* `FilterInvalid` for a tier-3 name on a closed kind in either mode, and for
|
|
321
|
+
* one on an open kind under `"refuse"`.
|
|
322
|
+
*/
|
|
323
|
+
export function checkNames(store, repoId, raw, mode) {
|
|
324
|
+
const hints = [];
|
|
325
|
+
const seen = new Set();
|
|
326
|
+
for (const ref of references(raw)) {
|
|
327
|
+
if (ref.scope.steps.length === 0)
|
|
328
|
+
continue; // a root name
|
|
329
|
+
const kind = kindOfScope(ref.scope.steps);
|
|
330
|
+
if (kind === null)
|
|
331
|
+
continue;
|
|
332
|
+
const verdict = judge(store, repoId, kind, ref.name.split("."));
|
|
333
|
+
if (verdict === null)
|
|
334
|
+
continue;
|
|
335
|
+
if (verdict.closed)
|
|
336
|
+
throw new FilterInvalid(fieldErrorMessage(store, repoId, verdict.target, verdict.name), "OQX");
|
|
337
|
+
const message = propertyHintMessage(store, repoId, verdict.target, verdict.name, verdict.handle);
|
|
338
|
+
if (mode === "refuse")
|
|
339
|
+
throw new FilterInvalid(message, "OQX");
|
|
340
|
+
if (!seen.has(message)) {
|
|
341
|
+
seen.add(message);
|
|
342
|
+
hints.push(message);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return hints;
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=names.js.map
|