@milehimikey/em 1.9.1 → 1.11.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/.claude/skills/event-modeling/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/SKILL.md +5 -1
- package/.claude/skills/event-modeling-design/SKILL.md +31 -12
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +14 -3
- package/.claude/skills/event-modeling-implement/reference/implement.md +73 -6
- package/.claude/skills/event-modeling-review/SKILL.md +14 -3
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +110 -11
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +4 -1
- package/.claude/skills/event-modeling-shared/templates/constitution.md +111 -0
- package/.claude/skills/event-modeling-shared/templates/model-readme.md +2 -2
- package/.claude/skills/event-modeling-shared/templates/slice.md +4 -0
- package/README.md +8 -1
- package/dist/catalog/docJoin.d.ts +8 -0
- package/dist/catalog/docJoin.js +4 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +8 -0
- package/dist/catalog/sliceDoc.js +2 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/cli/ciInit.d.ts +10 -3
- package/dist/cli/ciInit.js +22 -14
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/ledgerCheck.d.ts +72 -0
- package/dist/cli/ledgerCheck.js +122 -1
- package/dist/cli/ledgerCheck.js.map +1 -1
- package/dist/cli/ratify.d.ts +37 -5
- package/dist/cli/ratify.js +106 -9
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.js +14 -5
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/review.d.ts +38 -0
- package/dist/cli/review.js +160 -0
- package/dist/cli/review.js.map +1 -0
- package/dist/cli/sliceIndex.d.ts +3 -0
- package/dist/cli/sliceIndex.js +5 -4
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/stateFile.d.ts +10 -0
- package/dist/cli/stateFile.js +14 -0
- package/dist/cli/stateFile.js.map +1 -1
- package/dist/cli/status.d.ts +44 -3
- package/dist/cli/status.js +75 -18
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/systemInputs.d.ts +34 -0
- package/dist/cli/systemInputs.js +129 -0
- package/dist/cli/systemInputs.js.map +1 -0
- package/dist/cli.js +362 -23
- package/dist/cli.js.map +1 -1
- package/dist/emit/freshnessJson.d.ts +1 -1
- package/dist/emit/freshnessJson.js +5 -1
- package/dist/emit/freshnessJson.js.map +1 -1
- package/dist/emit/json.d.ts +37 -1
- package/dist/emit/json.js +67 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/ledgerJson.d.ts +1 -1
- package/dist/emit/ledgerJson.js +6 -1
- package/dist/emit/ledgerJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +6 -0
- package/dist/emit/queryJson.js +33 -0
- package/dist/emit/queryJson.js.map +1 -0
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +5 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/systemJson.d.ts +8 -0
- package/dist/emit/systemJson.js +38 -0
- package/dist/emit/systemJson.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +195 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/edges.d.ts +28 -1
- package/dist/model/edges.js +77 -15
- package/dist/model/edges.js.map +1 -1
- package/dist/model/model.d.ts +6 -0
- package/dist/model/model.js +2 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/qualifiedRef.d.ts +47 -0
- package/dist/model/qualifiedRef.js +100 -0
- package/dist/model/qualifiedRef.js.map +1 -0
- package/dist/model/queryIndex.d.ts +53 -0
- package/dist/model/queryIndex.js +114 -0
- package/dist/model/queryIndex.js.map +1 -0
- package/dist/model/rules.d.ts +88 -0
- package/dist/model/rules.js +101 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/sliceDocIndex.d.ts +30 -0
- package/dist/model/sliceDocIndex.js +110 -0
- package/dist/model/sliceDocIndex.js.map +1 -0
- package/dist/model/validate.js +85 -11
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +25 -0
- package/dist/parser/parser.d.ts +2 -0
- package/dist/parser/parser.js +104 -34
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.d.ts +8 -0
- package/dist/query/format.js +80 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/pipeline.d.ts +17 -0
- package/dist/query/pipeline.js +38 -0
- package/dist/query/pipeline.js.map +1 -0
- package/dist/query/system.d.ts +60 -0
- package/dist/query/system.js +87 -0
- package/dist/query/system.js.map +1 -0
- package/dist/query/verbs.d.ts +100 -0
- package/dist/query/verbs.js +306 -0
- package/dist/query/verbs.js.map +1 -0
- package/dist/render/drawEdges.js +15 -4
- package/dist/render/drawEdges.js.map +1 -1
- package/dist/render/drawNotes.d.ts +13 -3
- package/dist/render/drawNotes.js +96 -20
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.js +6 -5
- package/dist/render/render.js.map +1 -1
- package/dist/render/sliceDiagram.js +1 -0
- package/dist/render/sliceDiagram.js.map +1 -1
- package/dist/system/manifest.d.ts +45 -0
- package/dist/system/manifest.js +178 -0
- package/dist/system/manifest.js.map +1 -0
- package/dist/system/verify.d.ts +88 -0
- package/dist/system/verify.js +263 -0
- package/dist/system/verify.js.map +1 -0
- package/dist/templates.d.ts +14 -1
- package/dist/templates.js +134 -5
- package/dist/templates.js.map +1 -1
- package/dist/util/slug.js +16 -10
- package/dist/util/slug.js.map +1 -1
- package/package.json +10 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/query/format.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { RefsResult } from "../model/refs.js";
|
|
3
|
+
import { Diagnostic } from "../model/validate.js";
|
|
4
|
+
import { ModelIndex } from "../model/queryIndex.js";
|
|
5
|
+
export interface QueryCompileResult {
|
|
6
|
+
model: NormalizedModel;
|
|
7
|
+
refs: RefsResult;
|
|
8
|
+
diagnostics: Diagnostic[];
|
|
9
|
+
index: ModelIndex;
|
|
10
|
+
}
|
|
11
|
+
/** Compile one `.em` source for query use: parse + normalize + refs + validate (grid-free, see
|
|
12
|
+
* header) + the ModelIndex every verb queries. `baseDir` is the `.em` file's own directory —
|
|
13
|
+
* same doc-resolution convention every other command uses (passed straight through to
|
|
14
|
+
* `buildModelIndex`'s slice-doc join). Throws `ParseError` on a syntax error, same as
|
|
15
|
+
* `pipeline.ts`'s `compile()` — callers (cli.ts's compileFile-style wrapper, the MCP tool)
|
|
16
|
+
* catch it the same way. */
|
|
17
|
+
export declare function compileForQuery(source: string, baseDir: string): QueryCompileResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em query`'s sub-pipeline (MIL-168): `parse -> normalize -> computeRefs -> buildIndex`,
|
|
3
|
+
// skipping `pipeline.ts`'s own `layout()` (the dense per-cell Grid allocation — rows x slice
|
|
4
|
+
// columns) and `emitDot()` entirely. Neither is query work: nothing here renders, and
|
|
5
|
+
// `layout()`'s only diagnostic contribution `validate()` reads is `grid.collisions` (row/col
|
|
6
|
+
// placement clashes — a rendering-only concern, `src/model/validate.ts` greps clean for any
|
|
7
|
+
// other `grid.` use). `EMPTY_GRID` below hands `validate()` a real, empty `Grid` shape instead
|
|
8
|
+
// of computing one, so query pays zero layout cost while every OTHER validate.ts diagnostic
|
|
9
|
+
// (unresolved refs, dangling views, tag/field completeness, ...) still runs at full strength.
|
|
10
|
+
//
|
|
11
|
+
// validate() itself is deliberately still called, one explicit deviation from this ticket's
|
|
12
|
+
// design note (which named it alongside layout/DOT as work to skip): the CLI/MCP contract
|
|
13
|
+
// this ticket also specifies is "refuse (exit 1) if a model has compile errors," the same
|
|
14
|
+
// `hasErrors(diagnostics)` gate `em export`/`em status`/every other JSON surface uses — dropping
|
|
15
|
+
// validate() would mean `em query` silently traverses a model with unresolved references
|
|
16
|
+
// instead of refusing, a behavior change from every sibling command with no upside once
|
|
17
|
+
// layout's own cost is already zero. See this ticket's report for the full tradeoff.
|
|
18
|
+
import { parse } from "../parser/parser.js";
|
|
19
|
+
import { normalize } from "../model/model.js";
|
|
20
|
+
import { computeRefs } from "../model/refs.js";
|
|
21
|
+
import { validate } from "../model/validate.js";
|
|
22
|
+
import { buildModelIndex } from "../model/queryIndex.js";
|
|
23
|
+
const EMPTY_GRID = { rows: [], cols: 0, sliceNames: [], cells: [], rowIndexByKey: new Map(), collisions: [] };
|
|
24
|
+
/** Compile one `.em` source for query use: parse + normalize + refs + validate (grid-free, see
|
|
25
|
+
* header) + the ModelIndex every verb queries. `baseDir` is the `.em` file's own directory —
|
|
26
|
+
* same doc-resolution convention every other command uses (passed straight through to
|
|
27
|
+
* `buildModelIndex`'s slice-doc join). Throws `ParseError` on a syntax error, same as
|
|
28
|
+
* `pipeline.ts`'s `compile()` — callers (cli.ts's compileFile-style wrapper, the MCP tool)
|
|
29
|
+
* catch it the same way. */
|
|
30
|
+
export function compileForQuery(source, baseDir) {
|
|
31
|
+
const ast = parse(source);
|
|
32
|
+
const model = normalize(ast);
|
|
33
|
+
const refs = computeRefs(model);
|
|
34
|
+
const diagnostics = [...validate(model, EMPTY_GRID, refs), ...refs.diagnostics];
|
|
35
|
+
const index = buildModelIndex(model, refs, baseDir);
|
|
36
|
+
return { model, refs, diagnostics, index };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/query/pipeline.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0FAA0F;AAC1F,6FAA6F;AAC7F,sFAAsF;AACtF,6FAA6F;AAC7F,4FAA4F;AAC5F,+FAA+F;AAC/F,4FAA4F;AAC5F,8FAA8F;AAC9F,EAAE;AACF,4FAA4F;AAC5F,0FAA0F;AAC1F,0FAA0F;AAC1F,iGAAiG;AACjG,yFAAyF;AACzF,wFAAwF;AACxF,qFAAqF;AAErF,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAmB,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAc,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAc,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAc,MAAM,wBAAwB,CAAC;AAGrE,MAAM,UAAU,GAAS,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,GAAG,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;AASpH;;;;;6BAK6B;AAC7B,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,OAAe;IAC7D,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAChF,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACpD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { RefsResult } from "../model/refs.js";
|
|
3
|
+
import { ModelIndex } from "../model/queryIndex.js";
|
|
4
|
+
import type { Diagnostic } from "../model/validate.js";
|
|
5
|
+
export interface QueryModelEntry {
|
|
6
|
+
file: string;
|
|
7
|
+
modelKey: string;
|
|
8
|
+
model: NormalizedModel;
|
|
9
|
+
refs: RefsResult;
|
|
10
|
+
index: ModelIndex;
|
|
11
|
+
}
|
|
12
|
+
export interface QuerySystem {
|
|
13
|
+
entries: QueryModelEntry[];
|
|
14
|
+
/** True once more than one file was given — the CLI/MCP layer's signal to qualify every ref
|
|
15
|
+
* in its output (`qualifyRef` below), and the signal `resolveElement`'s ambiguity rule uses
|
|
16
|
+
* ("unqualified + multi-model + ambiguous" -> list qualified candidates, per this ticket's
|
|
17
|
+
* design note). */
|
|
18
|
+
multiModel: boolean;
|
|
19
|
+
/** `duplicate-model-key` warnings from keying the entries (MIL-193, `computeModelKeys()`):
|
|
20
|
+
* two input models deriving the same key, the later one suffixed `~2`/`~3`, … in file-list
|
|
21
|
+
* order. Empty for a single-model system. The CLI prints these to stderr like any other
|
|
22
|
+
* compile warning; the JSON document is unaffected (MCP parity holds). */
|
|
23
|
+
diagnostics: Diagnostic[];
|
|
24
|
+
}
|
|
25
|
+
export declare function buildQuerySystem(entries: Array<{
|
|
26
|
+
file: string;
|
|
27
|
+
model: NormalizedModel;
|
|
28
|
+
refs: RefsResult;
|
|
29
|
+
index: ModelIndex;
|
|
30
|
+
}>): QuerySystem;
|
|
31
|
+
/** `<modelKey>:<ref>` when the system spans more than one model, else the bare ref — the one
|
|
32
|
+
* place every query verb decides whether to qualify a ref for output, so the WHEN can't drift
|
|
33
|
+
* between verbs (the HOW is `formatQualifiedRef()`'s). */
|
|
34
|
+
export declare function qualifyRef(system: QuerySystem, modelKey: string, ref: string): string;
|
|
35
|
+
export interface ResolvedElement {
|
|
36
|
+
modelKey: string;
|
|
37
|
+
file: string;
|
|
38
|
+
entry: QueryModelEntry;
|
|
39
|
+
ref: string;
|
|
40
|
+
qualifiedRef: string;
|
|
41
|
+
elementKind: string;
|
|
42
|
+
elementName: string;
|
|
43
|
+
}
|
|
44
|
+
export type ResolveResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
match: ResolvedElement;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
error: string;
|
|
50
|
+
};
|
|
51
|
+
/** Resolve one user-supplied element reference — a stable ref, a `<modelKey>:`-qualified ref, or
|
|
52
|
+
* a bare display name — against every model in `system` (or just the named model, when
|
|
53
|
+
* qualified). A bare name/ref matching more than one element across the searched model(s) is
|
|
54
|
+
* always an error listing every candidate as a fully-qualified ref (`<modelKey>:<ref>`, even
|
|
55
|
+
* when `system` is single-model — a candidate list must be unambiguous on its own) — resolution
|
|
56
|
+
* never guesses. "Resolution is per-model" (MIL-168's spec): a bare name is looked up in each
|
|
57
|
+
* candidate model's OWN `byName` index, never merged across models before matching. A repeated
|
|
58
|
+
* read model's later instances (`view X again`) don't count as separate name matches — see
|
|
59
|
+
* the bare-name branch below. */
|
|
60
|
+
export declare function resolveElement(system: QuerySystem, raw: string): ResolveResult;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em query`'s multi-model addressing (MIL-168): a `QuerySystem` is one or more compiled
|
|
3
|
+
// models, each keyed by a short, stable `modelKey`. Refs (`<sliceKey>/<kind>.<slug>`) are, and
|
|
4
|
+
// remain, model-UNQUALIFIED — `computeRefs()`'s contract is untouched. For a query answer that
|
|
5
|
+
// spans more than one input file, that's ambiguous on its own (two models can easily mint the
|
|
6
|
+
// same slice/element ref independently), so `em query` addresses cross-model results with the
|
|
7
|
+
// qualified form `<modelKey>:<sliceKey>/<kind>.<slug>` — accepted on input alongside a bare
|
|
8
|
+
// ref/display name, and used to disambiguate/print results whenever more than one file was given.
|
|
9
|
+
//
|
|
10
|
+
// Query was the first surface to need this (MIL-168) and originally derived `modelKey` from the
|
|
11
|
+
// input filename. Since MIL-193 the key, the grammar, and the parse/format live in
|
|
12
|
+
// model/qualifiedRef.ts — the ONE cross-model addressing scheme every em surface shares
|
|
13
|
+
// (`em export`'s `model.key`, MIL-194's seam manifest, em-portal deep links): the key is the
|
|
14
|
+
// kebab-slug of the declared `model "Name"` (basename fallback when none is declared), so a
|
|
15
|
+
// query result's qualified ref is the same string an export consumer would mint for the same
|
|
16
|
+
// element. This module only decides WHEN to qualify (more than one file given) and how a
|
|
17
|
+
// user-supplied ref resolves; it never spells the grammar itself.
|
|
18
|
+
import { normalizeName } from "../model/model.js";
|
|
19
|
+
import { computeModelKeys, formatQualifiedRef, parseQualifiedRef } from "../model/qualifiedRef.js";
|
|
20
|
+
export function buildQuerySystem(entries) {
|
|
21
|
+
const { keys, diagnostics } = computeModelKeys(entries);
|
|
22
|
+
return {
|
|
23
|
+
entries: entries.map((e, i) => ({ file: e.file, modelKey: keys[i], model: e.model, refs: e.refs, index: e.index })),
|
|
24
|
+
multiModel: entries.length > 1,
|
|
25
|
+
diagnostics,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** `<modelKey>:<ref>` when the system spans more than one model, else the bare ref — the one
|
|
29
|
+
* place every query verb decides whether to qualify a ref for output, so the WHEN can't drift
|
|
30
|
+
* between verbs (the HOW is `formatQualifiedRef()`'s). */
|
|
31
|
+
export function qualifyRef(system, modelKey, ref) {
|
|
32
|
+
return system.multiModel ? formatQualifiedRef(modelKey, ref) : ref;
|
|
33
|
+
}
|
|
34
|
+
const REF_SHAPE_RE = /\//;
|
|
35
|
+
/** Resolve one user-supplied element reference — a stable ref, a `<modelKey>:`-qualified ref, or
|
|
36
|
+
* a bare display name — against every model in `system` (or just the named model, when
|
|
37
|
+
* qualified). A bare name/ref matching more than one element across the searched model(s) is
|
|
38
|
+
* always an error listing every candidate as a fully-qualified ref (`<modelKey>:<ref>`, even
|
|
39
|
+
* when `system` is single-model — a candidate list must be unambiguous on its own) — resolution
|
|
40
|
+
* never guesses. "Resolution is per-model" (MIL-168's spec): a bare name is looked up in each
|
|
41
|
+
* candidate model's OWN `byName` index, never merged across models before matching. A repeated
|
|
42
|
+
* read model's later instances (`view X again`) don't count as separate name matches — see
|
|
43
|
+
* the bare-name branch below. */
|
|
44
|
+
export function resolveElement(system, raw) {
|
|
45
|
+
// `parseQualifiedRef` only splits when the prefix has the model-key SHAPE; a prefix that
|
|
46
|
+
// isn't one of THIS system's keys is then left in place — a display name may legitimately
|
|
47
|
+
// contain a colon — so the search runs over every model; if nothing matches, the miss names
|
|
48
|
+
// the unrecognised prefix (a typo'd qualifier is the likely cause).
|
|
49
|
+
const parsed = parseQualifiedRef(raw);
|
|
50
|
+
const known = parsed.modelKey !== null && system.entries.some((e) => e.modelKey === parsed.modelKey);
|
|
51
|
+
const modelKey = known ? parsed.modelKey : undefined;
|
|
52
|
+
const rest = known ? parsed.ref : raw;
|
|
53
|
+
const candidates = modelKey ? system.entries.filter((e) => e.modelKey === modelKey) : system.entries;
|
|
54
|
+
const matches = [];
|
|
55
|
+
for (const entry of candidates) {
|
|
56
|
+
// A slash makes the input look like a ref; when no ref matches, fall through to the
|
|
57
|
+
// display-name lookup — a name may itself contain a slash ("Approve/Reject Screen").
|
|
58
|
+
const byRef = REF_SHAPE_RE.test(rest) ? entry.index.byRef.get(rest) : undefined;
|
|
59
|
+
if (byRef) {
|
|
60
|
+
const ref = entry.refs.refById.get(byRef.id);
|
|
61
|
+
matches.push({ modelKey: entry.modelKey, file: entry.file, entry, ref, qualifiedRef: formatQualifiedRef(entry.modelKey, ref), elementKind: byRef.kind, elementName: byRef.name });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// A repeated read model (`view X again`) has one name and several instances; the bare
|
|
65
|
+
// name means the read model, which resolves to its FIRST instance (`logicalId`) — every
|
|
66
|
+
// closure/path verb then follows the instance chain from there (verbs.ts's header). A
|
|
67
|
+
// later instance stays addressable by its own ref.
|
|
68
|
+
const bucket = (entry.model.byName.get(normalizeName(rest)) ?? []).filter((el) => el.logicalId === el.id);
|
|
69
|
+
for (const el of bucket) {
|
|
70
|
+
const ref = entry.refs.refById.get(el.id);
|
|
71
|
+
matches.push({ modelKey: entry.modelKey, file: entry.file, entry, ref, qualifiedRef: formatQualifiedRef(entry.modelKey, ref), elementKind: el.kind, elementName: el.name });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (matches.length === 0) {
|
|
76
|
+
const hint = parsed.modelKey !== null && !known
|
|
77
|
+
? ` ("${parsed.modelKey}" is not a model key — input models are: ${system.entries.map((e) => e.modelKey).join(", ")})`
|
|
78
|
+
: "";
|
|
79
|
+
return { ok: false, error: `em query: no element matches "${raw}"${hint}` };
|
|
80
|
+
}
|
|
81
|
+
if (matches.length > 1) {
|
|
82
|
+
const candidateRefs = matches.map((m) => m.qualifiedRef).join(", ");
|
|
83
|
+
return { ok: false, error: `em query: "${raw}" is ambiguous — matches ${matches.length} elements: ${candidateRefs}` };
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, match: matches[0] };
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.js","sourceRoot":"","sources":["../../src/query/system.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { QuerySystem } from "./system.js";
|
|
2
|
+
import { Citation } from "../cli/coverage.js";
|
|
3
|
+
export interface ElementSummary {
|
|
4
|
+
ref: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
name: string;
|
|
7
|
+
sliceKey: string;
|
|
8
|
+
sliceName: string;
|
|
9
|
+
}
|
|
10
|
+
export type VerbResult<T> = {
|
|
11
|
+
ok: true;
|
|
12
|
+
results: T[];
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
export interface ConsumerEntry extends ElementSummary {
|
|
18
|
+
via: string;
|
|
19
|
+
}
|
|
20
|
+
/** Views/reactions (and anything wired by an explicit `arrow`) consuming the named event — its
|
|
21
|
+
* direct out-neighbors in the legal-connection graph, each with the slice it lives in. */
|
|
22
|
+
export declare function queryConsumers(system: QuerySystem, eventRef: string): VerbResult<ConsumerEntry>;
|
|
23
|
+
export interface ProducerEntry extends ElementSummary {
|
|
24
|
+
via: string;
|
|
25
|
+
uiTriggers: ElementSummary[];
|
|
26
|
+
}
|
|
27
|
+
/** Commands producing the named event, each with its own slice and (MIL-168 spec: "+ slices, ui
|
|
28
|
+
* triggers") the `ui` elements that trigger that command in turn. */
|
|
29
|
+
export declare function queryProducers(system: QuerySystem, eventRef: string): VerbResult<ProducerEntry>;
|
|
30
|
+
export interface ClosureEntry extends ElementSummary {
|
|
31
|
+
depth: number;
|
|
32
|
+
via: string;
|
|
33
|
+
}
|
|
34
|
+
/** Transitive closure along legal edges (direction: element -> whatever it feeds) — impact
|
|
35
|
+
* analysis: "what breaks if this changes." */
|
|
36
|
+
export declare function queryDownstream(system: QuerySystem, ofRef: string, depth?: number): VerbResult<ClosureEntry>;
|
|
37
|
+
/** Transitive closure against legal-edge direction — "what feeds this." */
|
|
38
|
+
export declare function queryUpstream(system: QuerySystem, ofRef: string, depth?: number): VerbResult<ClosureEntry>;
|
|
39
|
+
export interface SliceQueryEntry {
|
|
40
|
+
ref: string;
|
|
41
|
+
name: string;
|
|
42
|
+
index: number;
|
|
43
|
+
pattern: string;
|
|
44
|
+
status: string | null;
|
|
45
|
+
personas: string[];
|
|
46
|
+
contexts: string[];
|
|
47
|
+
tags: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface SliceFilters {
|
|
50
|
+
pattern?: string;
|
|
51
|
+
status?: string;
|
|
52
|
+
context?: string;
|
|
53
|
+
persona?: string;
|
|
54
|
+
tag?: string;
|
|
55
|
+
}
|
|
56
|
+
/** The filtered slice list — every filter AND-combines. `pattern`/`status` match the slice's
|
|
57
|
+
* own single value exactly (case-insensitive); `context`/`persona`/`tag` match when ANY
|
|
58
|
+
* element in the slice carries that context/persona/tag key. */
|
|
59
|
+
export declare function querySlices(system: QuerySystem, filters: SliceFilters): VerbResult<SliceQueryEntry>;
|
|
60
|
+
export interface InvariantQueryEntry {
|
|
61
|
+
id: string;
|
|
62
|
+
sliceRef: string;
|
|
63
|
+
sliceName: string;
|
|
64
|
+
docPath: string | null;
|
|
65
|
+
status: string | null;
|
|
66
|
+
citations: Citation[] | null;
|
|
67
|
+
}
|
|
68
|
+
/** The declaring slice + doc facts for one `INV-*` id, and (with `testsDir`) every test file
|
|
69
|
+
* citing it — a single-ID `scanTestCitations()` call, the same word-boundary citation scan
|
|
70
|
+
* `em coverage` uses for every ID in the model, reused rather than reimplemented. An id
|
|
71
|
+
* matching more than one input model is the same "unqualified + multi-model + ambiguous" case
|
|
72
|
+
* every other verb refuses on, listing qualified candidates. */
|
|
73
|
+
export declare function queryInvariant(system: QuerySystem, id: string, testsDir?: string): VerbResult<InvariantQueryEntry>;
|
|
74
|
+
export interface FieldQueryEntry {
|
|
75
|
+
elementRef: string;
|
|
76
|
+
name: string;
|
|
77
|
+
type: string | null;
|
|
78
|
+
tag: boolean;
|
|
79
|
+
assigned: boolean;
|
|
80
|
+
renamedFrom: string[] | null;
|
|
81
|
+
/** The owning element's own `renamed from` chain (event/command only, MIL-68) — provenance
|
|
82
|
+
* has two axes: the field's prior names, and the prior names of the element it lives on. */
|
|
83
|
+
elementRenamedFrom: string[] | null;
|
|
84
|
+
}
|
|
85
|
+
/** One field's facts on the named element — type, identity-tag/system-assigned markers, its
|
|
86
|
+
* `renamed from` chain, and the owning element's. An element that resolves but carries no
|
|
87
|
+
* field of that name is a legitimate empty result (`results: []`), not an error — same "none,
|
|
88
|
+
* exit 0" contract as every other verb's empty answer. */
|
|
89
|
+
export declare function queryField(system: QuerySystem, ofRef: string, name: string): VerbResult<FieldQueryEntry>;
|
|
90
|
+
export interface PathQueryEntry {
|
|
91
|
+
refs: string[];
|
|
92
|
+
edgeKinds: string[];
|
|
93
|
+
length: number;
|
|
94
|
+
}
|
|
95
|
+
/** Shortest directed path from `fromRef` to `toRef` through the legal-connection graph (BFS,
|
|
96
|
+
* unweighted — first-found is shortest by construction; a `view-instance` hop costs nothing
|
|
97
|
+
* and isn't counted in `length`). Endpoints resolving to different models is a legitimate
|
|
98
|
+
* empty result (edges never cross model files — see system.ts's header) rather than an
|
|
99
|
+
* error. */
|
|
100
|
+
export declare function queryPath(system: QuerySystem, fromRef: string, toRef: string): VerbResult<PathQueryEntry>;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The eight `em query` verbs (MIL-168), as pure functions over an already-built `QuerySystem`
|
|
3
|
+
// (system.ts) — every verb reduces to a map hit or a bounded BFS over `ModelIndex.out`/`.in`
|
|
4
|
+
// (model/queryIndex.ts). No fs, no process.exit: a verb either returns `{ ok: true, results }`
|
|
5
|
+
// or `{ ok: false, error }` (a bad/ambiguous ref, an unknown invariant ID, cross-model `path`
|
|
6
|
+
// endpoints) — the CLI action and the MCP tool each turn that into their own exit-code/tool-error
|
|
7
|
+
// convention (never a third implementation of "what counts as a query error").
|
|
8
|
+
//
|
|
9
|
+
// Determinism (MIL-168's own requirement): every traversal below visits models in `system`
|
|
10
|
+
// order, slices in `model.slices` order, and elements in `model.elements` order — the same
|
|
11
|
+
// order `ModelIndex.byRef`/`.out`/`.in` were built in (queryIndex.ts's own header), so no verb
|
|
12
|
+
// here does its own sorting; it just doesn't disturb the order its inputs already have.
|
|
13
|
+
//
|
|
14
|
+
// Repeated read models (`view X again`): the closures and `path` treat every timeline instance
|
|
15
|
+
// of one read model as the same node — reaching one instance reaches them all, at the same
|
|
16
|
+
// depth, reported `via: "view-instance"` — because a change upstream of the read model
|
|
17
|
+
// reaches everything that reads ANY instance of it. Instances are never edges (edges.ts's own
|
|
18
|
+
// note); this is the query-engine rule MIL-191's design note assigns to MIL-168.
|
|
19
|
+
import { resolveElement, qualifyRef } from "./system.js";
|
|
20
|
+
import { classifySlicePattern } from "../catalog/classify.js";
|
|
21
|
+
import { collectTags } from "../model/model.js";
|
|
22
|
+
import { scanTestCitations } from "../cli/coverage.js";
|
|
23
|
+
function summarize(system, entry, el) {
|
|
24
|
+
const ref = entry.refs.refById.get(el.id);
|
|
25
|
+
const slice = entry.model.slices[el.sliceIndex];
|
|
26
|
+
return {
|
|
27
|
+
ref: qualifyRef(system, entry.modelKey, ref),
|
|
28
|
+
kind: el.kind,
|
|
29
|
+
name: el.name,
|
|
30
|
+
sliceKey: qualifyRef(system, entry.modelKey, entry.refs.sliceKeys[el.sliceIndex]),
|
|
31
|
+
sliceName: slice.name,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function elementByEdgeRef(entry, edge) {
|
|
35
|
+
return entry.index.byRef.get(edge.ref);
|
|
36
|
+
}
|
|
37
|
+
function err(message) {
|
|
38
|
+
return { ok: false, error: message };
|
|
39
|
+
}
|
|
40
|
+
/** Views/reactions (and anything wired by an explicit `arrow`) consuming the named event — its
|
|
41
|
+
* direct out-neighbors in the legal-connection graph, each with the slice it lives in. */
|
|
42
|
+
export function queryConsumers(system, eventRef) {
|
|
43
|
+
const resolved = resolveElement(system, eventRef);
|
|
44
|
+
if (!resolved.ok)
|
|
45
|
+
return err(resolved.error);
|
|
46
|
+
const { entry, ref, elementKind } = resolved.match;
|
|
47
|
+
if (elementKind !== "event")
|
|
48
|
+
return err(`em query consumers: "${eventRef}" resolved to a ${elementKind}, not an event`);
|
|
49
|
+
const edges = entry.index.out.get(ref) ?? [];
|
|
50
|
+
return { ok: true, results: edges.map((e) => ({ ...summarize(system, entry, elementByEdgeRef(entry, e)), via: e.kind })) };
|
|
51
|
+
}
|
|
52
|
+
/** Commands producing the named event, each with its own slice and (MIL-168 spec: "+ slices, ui
|
|
53
|
+
* triggers") the `ui` elements that trigger that command in turn. */
|
|
54
|
+
export function queryProducers(system, eventRef) {
|
|
55
|
+
const resolved = resolveElement(system, eventRef);
|
|
56
|
+
if (!resolved.ok)
|
|
57
|
+
return err(resolved.error);
|
|
58
|
+
const { entry, ref, elementKind } = resolved.match;
|
|
59
|
+
if (elementKind !== "event")
|
|
60
|
+
return err(`em query producers: "${eventRef}" resolved to a ${elementKind}, not an event`);
|
|
61
|
+
const edges = entry.index.in.get(ref) ?? [];
|
|
62
|
+
const results = edges.map((e) => {
|
|
63
|
+
const producer = elementByEdgeRef(entry, e);
|
|
64
|
+
const producerRef = entry.refs.refById.get(producer.id);
|
|
65
|
+
const triggerEdges = (entry.index.in.get(producerRef) ?? []).filter((x) => x.kind === "ui->command");
|
|
66
|
+
return {
|
|
67
|
+
...summarize(system, entry, producer),
|
|
68
|
+
via: e.kind,
|
|
69
|
+
uiTriggers: triggerEdges.map((x) => summarize(system, entry, elementByEdgeRef(entry, x))),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return { ok: true, results };
|
|
73
|
+
}
|
|
74
|
+
function closure(system, ofRef, depth, direction, verbName) {
|
|
75
|
+
const resolved = resolveElement(system, ofRef);
|
|
76
|
+
if (!resolved.ok)
|
|
77
|
+
return err(resolved.error);
|
|
78
|
+
if (depth !== undefined && (!Number.isInteger(depth) || depth < 1)) {
|
|
79
|
+
return err(`em query ${verbName}: --depth must be a positive integer`);
|
|
80
|
+
}
|
|
81
|
+
const { entry, ref } = resolved.match;
|
|
82
|
+
const adjacency = direction === "out" ? entry.index.out : entry.index.in;
|
|
83
|
+
const results = [];
|
|
84
|
+
const visited = new Set([ref]);
|
|
85
|
+
let frontier = [];
|
|
86
|
+
// Discovering a node also discovers every other instance of the same read model, at the
|
|
87
|
+
// same depth (a zero-cost hop, see this module's header) — including the start's own.
|
|
88
|
+
const add = (next, target, d, via) => {
|
|
89
|
+
if (visited.has(target))
|
|
90
|
+
return false;
|
|
91
|
+
visited.add(target);
|
|
92
|
+
next.push(target);
|
|
93
|
+
results.push({ ...summarize(system, entry, entry.index.byRef.get(target)), depth: d, via });
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
// One level of sibling discovery, never recursive: `instances` already lists EVERY other
|
|
97
|
+
// member of a read model's group, so recursing would only re-visit the same set.
|
|
98
|
+
const discover = (next, target, d, via) => {
|
|
99
|
+
if (!add(next, target, d, via))
|
|
100
|
+
return;
|
|
101
|
+
for (const sibling of entry.index.instances.get(target) ?? [])
|
|
102
|
+
add(next, sibling, d, "view-instance");
|
|
103
|
+
};
|
|
104
|
+
frontier.push(ref);
|
|
105
|
+
for (const sibling of entry.index.instances.get(ref) ?? [])
|
|
106
|
+
add(frontier, sibling, 0, "view-instance");
|
|
107
|
+
let d = 0;
|
|
108
|
+
while (frontier.length > 0 && (depth === undefined || d < depth)) {
|
|
109
|
+
d++;
|
|
110
|
+
const next = [];
|
|
111
|
+
for (const cur of frontier) {
|
|
112
|
+
for (const edge of adjacency.get(cur) ?? [])
|
|
113
|
+
discover(next, edge.ref, d, edge.kind);
|
|
114
|
+
}
|
|
115
|
+
frontier = next;
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, results };
|
|
118
|
+
}
|
|
119
|
+
/** Transitive closure along legal edges (direction: element -> whatever it feeds) — impact
|
|
120
|
+
* analysis: "what breaks if this changes." */
|
|
121
|
+
export function queryDownstream(system, ofRef, depth) {
|
|
122
|
+
return closure(system, ofRef, depth, "out", "downstream");
|
|
123
|
+
}
|
|
124
|
+
/** Transitive closure against legal-edge direction — "what feeds this." */
|
|
125
|
+
export function queryUpstream(system, ofRef, depth) {
|
|
126
|
+
return closure(system, ofRef, depth, "in", "upstream");
|
|
127
|
+
}
|
|
128
|
+
/** The filtered slice list — every filter AND-combines. `pattern`/`status` match the slice's
|
|
129
|
+
* own single value exactly (case-insensitive); `context`/`persona`/`tag` match when ANY
|
|
130
|
+
* element in the slice carries that context/persona/tag key. */
|
|
131
|
+
export function querySlices(system, filters) {
|
|
132
|
+
const results = [];
|
|
133
|
+
for (const entry of system.entries) {
|
|
134
|
+
entry.model.slices.forEach((slice, i) => {
|
|
135
|
+
const key = entry.refs.sliceKeys[i];
|
|
136
|
+
const pattern = classifySlicePattern(slice);
|
|
137
|
+
const status = entry.index.sliceFacts.get(key)?.doc.status ?? null;
|
|
138
|
+
const personas = [...new Set(slice.elements.filter((e) => e.kind === "ui").map((e) => e.persona))];
|
|
139
|
+
const contexts = [...new Set(slice.elements.filter((e) => e.kind === "event").map((e) => e.context))];
|
|
140
|
+
const tags = [...new Set(slice.elements.filter((e) => e.kind === "event").flatMap((e) => collectTags(e).map((t) => t.key)))];
|
|
141
|
+
if (filters.pattern && pattern !== filters.pattern)
|
|
142
|
+
return;
|
|
143
|
+
if (filters.status && (status ?? "").toLowerCase() !== filters.status.toLowerCase())
|
|
144
|
+
return;
|
|
145
|
+
if (filters.context && !contexts.some((c) => c.toLowerCase() === filters.context.toLowerCase()))
|
|
146
|
+
return;
|
|
147
|
+
if (filters.persona && !personas.some((p) => p.toLowerCase() === filters.persona.toLowerCase()))
|
|
148
|
+
return;
|
|
149
|
+
if (filters.tag && !tags.some((t) => t.toLowerCase() === filters.tag.toLowerCase()))
|
|
150
|
+
return;
|
|
151
|
+
results.push({ ref: qualifyRef(system, entry.modelKey, key), name: slice.name, index: i, pattern, status, personas, contexts, tags });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, results };
|
|
155
|
+
}
|
|
156
|
+
/** The declaring slice + doc facts for one `INV-*` id, and (with `testsDir`) every test file
|
|
157
|
+
* citing it — a single-ID `scanTestCitations()` call, the same word-boundary citation scan
|
|
158
|
+
* `em coverage` uses for every ID in the model, reused rather than reimplemented. An id
|
|
159
|
+
* matching more than one input model is the same "unqualified + multi-model + ambiguous" case
|
|
160
|
+
* every other verb refuses on, listing qualified candidates. */
|
|
161
|
+
export function queryInvariant(system, id, testsDir) {
|
|
162
|
+
const hits = [];
|
|
163
|
+
for (const entry of system.entries) {
|
|
164
|
+
const found = entry.index.invariants.get(id);
|
|
165
|
+
if (found)
|
|
166
|
+
hits.push({ entry, sliceKey: found.sliceKey });
|
|
167
|
+
}
|
|
168
|
+
if (hits.length === 0)
|
|
169
|
+
return err(`em query invariant: no invariant "${id}" found in ${system.entries.map((e) => e.file).join(", ")}`);
|
|
170
|
+
if (hits.length > 1) {
|
|
171
|
+
const candidates = hits.map((h) => qualifyRef(system, h.entry.modelKey, id)).join(", ");
|
|
172
|
+
return err(`em query invariant: "${id}" is ambiguous across models — ${candidates}`);
|
|
173
|
+
}
|
|
174
|
+
const { entry, sliceKey } = hits[0];
|
|
175
|
+
const fact = entry.index.sliceFacts.get(sliceKey);
|
|
176
|
+
const citations = testsDir !== undefined ? scanTestCitations(testsDir, [id]).get(id) ?? [] : null;
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
results: [
|
|
180
|
+
{
|
|
181
|
+
id,
|
|
182
|
+
sliceRef: qualifyRef(system, entry.modelKey, sliceKey),
|
|
183
|
+
sliceName: fact.name,
|
|
184
|
+
docPath: fact.doc.found ? fact.doc.path : null,
|
|
185
|
+
status: fact.doc.status,
|
|
186
|
+
citations,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** One field's facts on the named element — type, identity-tag/system-assigned markers, its
|
|
192
|
+
* `renamed from` chain, and the owning element's. An element that resolves but carries no
|
|
193
|
+
* field of that name is a legitimate empty result (`results: []`), not an error — same "none,
|
|
194
|
+
* exit 0" contract as every other verb's empty answer. */
|
|
195
|
+
export function queryField(system, ofRef, name) {
|
|
196
|
+
const resolved = resolveElement(system, ofRef);
|
|
197
|
+
if (!resolved.ok)
|
|
198
|
+
return err(resolved.error);
|
|
199
|
+
const { entry, ref } = resolved.match;
|
|
200
|
+
const el = entry.index.byRef.get(ref);
|
|
201
|
+
const field = (el.fields ?? []).find((f) => f.name === name);
|
|
202
|
+
if (!field)
|
|
203
|
+
return { ok: true, results: [] };
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
results: [
|
|
207
|
+
{
|
|
208
|
+
elementRef: qualifyRef(system, entry.modelKey, ref),
|
|
209
|
+
name: field.name,
|
|
210
|
+
type: field.type ?? null,
|
|
211
|
+
tag: field.tag === true,
|
|
212
|
+
assigned: field.assigned === true,
|
|
213
|
+
renamedFrom: field.renamedFrom ?? null,
|
|
214
|
+
elementRenamedFrom: el.renamedFrom ?? null,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Shortest directed path from `fromRef` to `toRef` through the legal-connection graph (BFS,
|
|
220
|
+
* unweighted — first-found is shortest by construction; a `view-instance` hop costs nothing
|
|
221
|
+
* and isn't counted in `length`). Endpoints resolving to different models is a legitimate
|
|
222
|
+
* empty result (edges never cross model files — see system.ts's header) rather than an
|
|
223
|
+
* error. */
|
|
224
|
+
export function queryPath(system, fromRef, toRef) {
|
|
225
|
+
const from = resolveElement(system, fromRef);
|
|
226
|
+
if (!from.ok)
|
|
227
|
+
return err(from.error);
|
|
228
|
+
const to = resolveElement(system, toRef);
|
|
229
|
+
if (!to.ok)
|
|
230
|
+
return err(to.error);
|
|
231
|
+
if (from.match.modelKey !== to.match.modelKey)
|
|
232
|
+
return { ok: true, results: [] };
|
|
233
|
+
const { entry } = from.match;
|
|
234
|
+
const startRef = from.match.ref;
|
|
235
|
+
const endRef = to.match.ref;
|
|
236
|
+
if (startRef === endRef) {
|
|
237
|
+
return { ok: true, results: [{ refs: [qualifyRef(system, entry.modelKey, startRef)], edgeKinds: [], length: 0 }] };
|
|
238
|
+
}
|
|
239
|
+
const prev = new Map();
|
|
240
|
+
const visited = new Set([startRef]);
|
|
241
|
+
// Discovering a node discovers every other instance of the same read model with it, at the
|
|
242
|
+
// same distance (this module's header) — recorded as a "view-instance" step in `edgeKinds`
|
|
243
|
+
// but not counted in `length`. Doing it at discovery time (not as a frontier edge) is what
|
|
244
|
+
// keeps BFS's first-found-is-shortest guarantee intact with zero-cost hops in the graph.
|
|
245
|
+
const add = (next, target, from, kind) => {
|
|
246
|
+
if (visited.has(target))
|
|
247
|
+
return "seen";
|
|
248
|
+
visited.add(target);
|
|
249
|
+
prev.set(target, { from, kind });
|
|
250
|
+
if (target === endRef)
|
|
251
|
+
return "found";
|
|
252
|
+
next.push(target);
|
|
253
|
+
return "new";
|
|
254
|
+
};
|
|
255
|
+
// One level of sibling discovery, never recursive: `instances` already lists EVERY other
|
|
256
|
+
// member of the group, and each sibling links straight back to `target` — so a path through
|
|
257
|
+
// a three-instance read model reports exactly one `view-instance` step, not a chain of two.
|
|
258
|
+
const discover = (next, target, from, kind) => {
|
|
259
|
+
const outcome = add(next, target, from, kind);
|
|
260
|
+
if (outcome !== "new")
|
|
261
|
+
return outcome === "found";
|
|
262
|
+
for (const sibling of entry.index.instances.get(target) ?? []) {
|
|
263
|
+
if (add(next, sibling, target, "view-instance") === "found")
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
};
|
|
268
|
+
let frontier = [];
|
|
269
|
+
let found = false;
|
|
270
|
+
for (const sibling of entry.index.instances.get(startRef) ?? []) {
|
|
271
|
+
if (add(frontier, sibling, startRef, "view-instance") === "found")
|
|
272
|
+
found = true;
|
|
273
|
+
}
|
|
274
|
+
frontier.unshift(startRef);
|
|
275
|
+
while (frontier.length > 0 && !found) {
|
|
276
|
+
const next = [];
|
|
277
|
+
for (const cur of frontier) {
|
|
278
|
+
for (const edge of entry.index.out.get(cur) ?? []) {
|
|
279
|
+
if (discover(next, edge.ref, cur, edge.kind)) {
|
|
280
|
+
found = true;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (found)
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
frontier = next;
|
|
288
|
+
}
|
|
289
|
+
if (!found)
|
|
290
|
+
return { ok: true, results: [] };
|
|
291
|
+
const chain = [endRef];
|
|
292
|
+
const kinds = [];
|
|
293
|
+
let cur = endRef;
|
|
294
|
+
while (cur !== startRef) {
|
|
295
|
+
const step = prev.get(cur);
|
|
296
|
+
kinds.unshift(step.kind);
|
|
297
|
+
chain.unshift(step.from);
|
|
298
|
+
cur = step.from;
|
|
299
|
+
}
|
|
300
|
+
const length = kinds.filter((k) => k !== "view-instance").length;
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
results: [{ refs: chain.map((r) => qualifyRef(system, entry.modelKey, r)), edgeKinds: kinds, length }],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=verbs.js.map
|