yarramate 1.20.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/adapters/likec4-prepare.js +1 -1
  2. package/dist/adapters/visual/protocol-contract.d.ts +15 -0
  3. package/dist/adapters/visual/session-server.js +12 -6
  4. package/dist/adapters/visual/workspace-model.d.ts +4 -4
  5. package/dist/adapters/visual/workspace-model.js +17 -5
  6. package/dist/ask-command.js +2 -2
  7. package/dist/check-command.js +12 -1
  8. package/dist/export-command.js +4 -1
  9. package/dist/fold-tree.d.ts +21 -8
  10. package/dist/fold-tree.js +118 -23
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/projection.d.ts +63 -4
  14. package/dist/projection.js +179 -9
  15. package/dist/schema-validators.generated.js +346 -264
  16. package/dist/subject-references.js +8 -0
  17. package/dist/visual-app/assets/index-C0nYrzna.js +395 -0
  18. package/dist/visual-app/index.html +1 -1
  19. package/dist/visual-app-lib/editor.js +27981 -27699
  20. package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +15 -0
  21. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +4 -4
  22. package/dist/visual-app-lib/types/fold-tree.d.ts +21 -8
  23. package/dist/visual-app-lib/types/projection.d.ts +63 -4
  24. package/dist/visual-app-lib/types/visual-app/constraint-rows.d.ts +70 -0
  25. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +23 -0
  26. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +13 -2
  27. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
  28. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +10 -3
  29. package/dist/visual-app-lib/types/visual-app/session-client.d.ts +7 -1
  30. package/dist/visual-app-lib/types/visual-app/state.d.ts +2 -0
  31. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +8 -1
  32. package/package.json +1 -1
  33. package/schema/yarramate-projection.schema.json +13 -0
  34. package/dist/visual-app/assets/index-EegLnUfX.js +0 -394
@@ -141,7 +141,7 @@ export function prepareLikeC4Export(input) {
141
141
  };
142
142
  }
143
143
  }
144
- const projectionResult = evaluateProjection(compilation.graph, projection.projection, compilation.profileContext);
144
+ const projectionResult = evaluateProjection(compilation.graph, projection.projection, compilation.profileContext, compilation.patternMemberships);
145
145
  const comparison = input.comparison === undefined
146
146
  ? undefined
147
147
  : compareArchitectureStates(compilation.graph, input.comparison.from, input.comparison.to);
@@ -9,6 +9,7 @@
9
9
  * `node:path`, or the schema documents into its bundle.
10
10
  */
11
11
  import type { CanvasGraph } from "../../graph-projection.js";
12
+ import type { NestingKind } from "../../nesting.js";
12
13
  import type { YarramateApplyResult, YarramateOperation } from "../../operations.js";
13
14
  import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
14
15
  /**
@@ -163,6 +164,20 @@ export interface VisualKindOption {
163
164
  }
164
165
  export interface VisualFilterQueryPayload {
165
166
  readonly query: ProjectionQuery;
167
+ /**
168
+ * The nesting the canvas is drawing with, when the browser knows it (#473
169
+ * phase 2).
170
+ *
171
+ * Only `query.instances` reads it, and it must: the closure of an instance IS
172
+ * the containment tree, so evaluating it under a different nesting than the
173
+ * canvas answers a different question. On the ApertureX reference that is 15
174
+ * subjects against 2.
175
+ *
176
+ * Optional, so an older browser and every filter that names no instance keep
177
+ * working unchanged; absent, the evaluator falls back to the default nesting
178
+ * exactly as it did before.
179
+ */
180
+ readonly nesting?: readonly NestingKind[];
166
181
  }
167
182
  export interface VisualFilterResultPayload {
168
183
  readonly query: ProjectionQuery;
@@ -690,9 +690,15 @@ export const startVisualServer = async (options) => {
690
690
  if (!started.ok)
691
691
  standingDiagnostics = started.diagnostics;
692
692
  }
693
- const filterMatchedIds = (query) => compiledWorkspace === undefined
693
+ const filterMatchedIds = (query, nesting) => compiledWorkspace === undefined
694
694
  ? []
695
- : matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
695
+ : matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext,
696
+ // A view's query can name `instances`, and the facet resolves to the
697
+ // instance alone without these (ADR 0144).
698
+ compiledWorkspace.patternMemberships,
699
+ // And it resolves the WRONG closure without the nesting the canvas is
700
+ // drawing with, which is a wrong number rather than a missing one.
701
+ nesting);
696
702
  /**
697
703
  * Why a query dropped what it dropped, as the editor's "excluded, and why"
698
704
  * list reads it (#248).
@@ -704,9 +710,9 @@ export const startVisualServer = async (options) => {
704
710
  * the reason the editor shows and the set the canvas draws cannot come from
705
711
  * two readings of the same query.
706
712
  */
707
- const filterExclusions = (query) => compiledWorkspace === undefined
713
+ const filterExclusions = (query, nesting) => compiledWorkspace === undefined
708
714
  ? []
709
- : exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
715
+ : exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext, compiledWorkspace.patternMemberships, nesting);
710
716
  let listening = false;
711
717
  let bootstrapSpent = false;
712
718
  let agentAttached = false;
@@ -1243,8 +1249,8 @@ export const startVisualServer = async (options) => {
1243
1249
  kind: "filter-result",
1244
1250
  result: {
1245
1251
  query: event.payload.query,
1246
- matchedIds: filterMatchedIds(event.payload.query),
1247
- excluded: filterExclusions(event.payload.query),
1252
+ matchedIds: filterMatchedIds(event.payload.query, event.payload.nesting),
1253
+ excluded: filterExclusions(event.payload.query, event.payload.nesting),
1248
1254
  },
1249
1255
  });
1250
1256
  return;
@@ -2,7 +2,7 @@ import type { Diagnostic } from "../../compiler.js";
2
2
  import type { PendingWrite, SourceStore } from "../../source-store.js";
3
3
  import type { ResolvedWorkspace } from "../../workspace.js";
4
4
  import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.js";
5
- import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
5
+ import type { NestingKind, ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
6
6
  import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
7
7
  import { type CataloguePatternMembership, type CataloguePatternVacancy } from "../../interrogate-command.js";
8
8
  import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
@@ -41,7 +41,7 @@ export declare const kindOptionsOf: (lineages: ReadonlyMap<string, readonly stri
41
41
  * subject count. A view over three components with two relationships between
42
42
  * them would read as five, and the reviewer counting boxes would find three.
43
43
  */
44
- export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => number;
44
+ export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => number;
45
45
  /**
46
46
  * Folds one interrogation report into what the canvas draws (#292).
47
47
  *
@@ -109,9 +109,9 @@ export declare const renderedWorkspaceOf: (compiled: {
109
109
  readonly views: readonly VisualViewSummary[];
110
110
  };
111
111
  /** Every subject a query draws, concepts and relationships alike. */
112
- export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => readonly string[];
112
+ export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly string[];
113
113
  /** Every concept a query dropped, and the facet that dropped it (#248). */
114
- export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => readonly ProjectionExclusion[];
114
+ export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly ProjectionExclusion[];
115
115
  /**
116
116
  * One saved view, as the rail reads it. `subjectCount` is the caller's,
117
117
  * because counting needs a compiled graph and a session builds its first list
@@ -45,7 +45,7 @@ export const kindOptionsOf = (lineages) => [...lineages.keys()].map((id) => ({
45
45
  * subject count. A view over three components with two relationships between
46
46
  * them would read as five, and the reviewer counting boxes would find three.
47
47
  */
48
- export const conceptCountOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.filter(({ type }) => type === "concept").length;
48
+ export const conceptCountOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.filter(({ type }) => type === "concept").length;
49
49
  export const interrogationOverlayOf = (compiled,
50
50
  /**
51
51
  * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
@@ -118,7 +118,14 @@ dismissed = []) => {
118
118
  export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismissed) => {
119
119
  const refreshedViews = views.map((view) => ({
120
120
  ...view,
121
- subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext),
121
+ subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext,
122
+ // Without these an `instances` view counts 1 and the rail says so, which
123
+ // is a wrong number rather than a missing one (ADR 0144).
124
+ compiled.patternMemberships,
125
+ // Each view's OWN nesting, because each view's closure is its own. A rail
126
+ // sitting beside the canvas must not count a different tree than the
127
+ // canvas draws.
128
+ view.presentation?.nesting),
122
129
  }));
123
130
  const interrogation = catalogue === undefined
124
131
  ? undefined
@@ -148,18 +155,23 @@ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismis
148
155
  };
149
156
  };
150
157
  /** Every subject a query draws, concepts and relationships alike. */
151
- export const matchedIdsOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.map(({ id }) => id);
158
+ export const matchedIdsOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.map(({ id }) => id);
152
159
  /** Every concept a query dropped, and the facet that dropped it (#248). */
153
- export const exclusionsOf = (graph, query, profileContext) => explainProjection(graph, adHoc(query), profileContext);
160
+ export const exclusionsOf = (graph, query, profileContext, memberships, nesting) => explainProjection(graph, adHoc(query, nesting), profileContext, memberships);
154
161
  /**
155
162
  * A query on its own is not a projection, and every evaluator here wants one.
156
163
  * The id is a placeholder that never reaches a document.
157
164
  */
158
- const adHoc = (query) => ({
165
+ const adHoc = (query, nesting) => ({
159
166
  format: "yarramate/projection/v1",
160
167
  id: "ad-hoc",
161
168
  version: "0",
162
169
  query,
170
+ // `query.instances` resolves its closure through the view's nesting, so an
171
+ // ad-hoc projection that dropped the nesting would answer a DIFFERENT
172
+ // question than the canvas is drawing: 2 subjects against 15 on the
173
+ // ApertureX reference, with nothing to say it had (#473 phase 2).
174
+ ...(nesting === undefined ? {} : { presentation: { nesting } }),
163
175
  });
164
176
  /**
165
177
  * One saved view, as the rail reads it. `subjectCount` is the caller's,
@@ -784,7 +784,7 @@ export function runAskCommand(options, cwd) {
784
784
  });
785
785
  if (!loaded.ok)
786
786
  continue;
787
- const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext);
787
+ const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext, compilation.patternMemberships);
788
788
  for (const subject of membership.subjects) {
789
789
  covered.add(subject.id);
790
790
  }
@@ -857,7 +857,7 @@ export function runAskCommand(options, cwd) {
857
857
  });
858
858
  if (!loaded.ok)
859
859
  return failed(loaded.diagnostics);
860
- const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext);
860
+ const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext, compilation.patternMemberships);
861
861
  const result = {
862
862
  format: 'yarramate/ask-result/v1',
863
863
  workspace: workspace.id,
@@ -214,7 +214,18 @@ export function runCheckCommand(options, cwd) {
214
214
  // does not build would bury the real failure under its consequences.
215
215
  const referenceDiagnostics = result.ok
216
216
  ? loadedProjections.flatMap(({ source, loaded }) => loaded.ok
217
- ? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext)
217
+ ? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext,
218
+ // Instance-hood from BOTH lists. A membership row exists only
219
+ // for a BOUND slot, so an instance whose slots are all empty
220
+ // has none - and judging it by bindings alone would call a real
221
+ // instance "not an instance" on the day it was authored, before
222
+ // anything was wired into it. The honest question is not "did
223
+ // it bind anything" but "does the model know it as an
224
+ // instance" (rule 2).
225
+ new Set([
226
+ ...(result.patternMemberships ?? []).map(({ instance }) => instance),
227
+ ...(result.patternVacancies ?? []).map(({ instance }) => instance),
228
+ ]))
218
229
  : [])
219
230
  : [];
220
231
  const optionalDiagnostics = sortDiagnostics([
@@ -250,7 +250,10 @@ export function runExportCommand(options, cwd) {
250
250
  });
251
251
  if (!loadedProjection.ok)
252
252
  return failed(loadedProjection.diagnostics);
253
- result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
253
+ result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext,
254
+ // An AUTHORED projection can name `instances`, and the facet resolves
255
+ // to the instance alone without these (ADR 0144).
256
+ compilation.patternMemberships);
254
257
  }
255
258
  if (kind === 'xlsx') {
256
259
  // A workbook an architect can work in (#355). It takes a PROJECTION,
@@ -110,15 +110,28 @@ export declare function nestingTree(edges: readonly FoldEdge[], nesting: readonl
110
110
  /**
111
111
  * The containment tree: what a view nests, plus what a pattern owns.
112
112
  *
113
- * A slot member joins the tree only when all three hold, and each condition is
114
- * a different way of getting the answer wrong:
113
+ * A slot member joins the tree only when all of these hold, and each condition
114
+ * is a different way of getting the answer wrong:
115
115
  *
116
- * - **Exclusive.** A subject bound into two instances has two owners, and a
117
- * single-parent tree would silently pick one. Shared subjects stay outside.
118
- * - **`owned` or `unwired`, never `context`.** A context slot names something
119
- * the instance USES and does not contain the upstream API it calls, the
120
- * plane it runs on. Folding those would swallow half the landscape into
121
- * whichever box happened to reference it.
116
+ * - **Held inside one box.** A member's HOLDERS are every instance whose slots
117
+ * name it. One holder puts the member in that holder. Several put it in their
118
+ * lowest common ancestor, which is the level at which the holders diverge and
119
+ * therefore the innermost box that contains all of them. Holders with no
120
+ * common ancestor leave the member outside, because there is no one box it
121
+ * sits within and a single-parent tree would have to pick.
122
+ *
123
+ * This AMENDS ADR 0143's "Exclusive" rule, which kept every shared subject
124
+ * outside (#473 phase 3, ADR 0145, Nabeel's decision of 2026-09-05). The
125
+ * original reasoning was that two owners force a silent choice; it is only
126
+ * true when the owners sit in different boxes. Where both already sit under
127
+ * one box there is nothing to choose, and the old rule left 14 of the
128
+ * reference Landscape's 30 data objects outside the single application whose
129
+ * own parts were the things binding them.
130
+ * - **`owned` or `unwired`, never `context` alone.** A context slot names
131
+ * something the instance USES and does not contain — the upstream API it
132
+ * calls, the plane it runs on. Folding those would swallow half the landscape
133
+ * into whichever box happened to reference it. At least one binding must be
134
+ * `owned` or `unwired` for the member to fold at all.
122
135
  * - **Not a ruling.** See {@link RULING_CORE_KINDS}.
123
136
  *
124
137
  * A view's own nesting wins where both apply: the view is the more specific
package/dist/fold-tree.js CHANGED
@@ -127,6 +127,18 @@ export function nestingTree(edges, nesting, coreKindOf) {
127
127
  * a straight-line ancestor of a cycle is still validly nested under its own
128
128
  * non-cyclic parent. Mutates `parentOf` and returns what it removed.
129
129
  */
130
+ /** Whether `id` sits anywhere inside `ancestor` in the tree built so far. */
131
+ const isDescendantOf = (id, ancestor, parentOf) => {
132
+ const seen = new Set([id]);
133
+ let current = parentOf.get(id);
134
+ while (current !== undefined && !seen.has(current)) {
135
+ if (current === ancestor)
136
+ return true;
137
+ seen.add(current);
138
+ current = parentOf.get(current);
139
+ }
140
+ return false;
141
+ };
130
142
  function unnestCycles(parentOf) {
131
143
  const cycleMembers = new Set();
132
144
  for (const start of parentOf.keys()) {
@@ -149,18 +161,59 @@ function unnestCycles(parentOf) {
149
161
  parentOf.delete(id);
150
162
  return [...cycleMembers];
151
163
  }
164
+ /**
165
+ * The lowest node that contains every one of `ids`, counting each id as an
166
+ * ancestor of itself, or `undefined` when they do not share one.
167
+ *
168
+ * "Counting each id as an ancestor of itself" is the part that matters: where
169
+ * one holder already sits inside another, the answer is the outer holder rather
170
+ * than something above them both.
171
+ */
172
+ const lowestCommonAncestor = (ids, parentOf) => {
173
+ const chainOf = (id) => {
174
+ const chain = [];
175
+ const seen = new Set();
176
+ let current = id;
177
+ while (current !== undefined && !seen.has(current)) {
178
+ seen.add(current);
179
+ chain.push(current);
180
+ current = parentOf.get(current);
181
+ }
182
+ return chain;
183
+ };
184
+ const [first, ...rest] = ids;
185
+ if (first === undefined)
186
+ return undefined;
187
+ const others = rest.map((id) => new Set(chainOf(id)));
188
+ // Walking the first chain from the node OUTWARDS makes the first hit the
189
+ // lowest by construction.
190
+ return chainOf(first).find((candidate) => others.every((chain) => chain.has(candidate)));
191
+ };
152
192
  /**
153
193
  * The containment tree: what a view nests, plus what a pattern owns.
154
194
  *
155
- * A slot member joins the tree only when all three hold, and each condition is
156
- * a different way of getting the answer wrong:
195
+ * A slot member joins the tree only when all of these hold, and each condition
196
+ * is a different way of getting the answer wrong:
157
197
  *
158
- * - **Exclusive.** A subject bound into two instances has two owners, and a
159
- * single-parent tree would silently pick one. Shared subjects stay outside.
160
- * - **`owned` or `unwired`, never `context`.** A context slot names something
161
- * the instance USES and does not contain the upstream API it calls, the
162
- * plane it runs on. Folding those would swallow half the landscape into
163
- * whichever box happened to reference it.
198
+ * - **Held inside one box.** A member's HOLDERS are every instance whose slots
199
+ * name it. One holder puts the member in that holder. Several put it in their
200
+ * lowest common ancestor, which is the level at which the holders diverge and
201
+ * therefore the innermost box that contains all of them. Holders with no
202
+ * common ancestor leave the member outside, because there is no one box it
203
+ * sits within and a single-parent tree would have to pick.
204
+ *
205
+ * This AMENDS ADR 0143's "Exclusive" rule, which kept every shared subject
206
+ * outside (#473 phase 3, ADR 0145, Nabeel's decision of 2026-09-05). The
207
+ * original reasoning was that two owners force a silent choice; it is only
208
+ * true when the owners sit in different boxes. Where both already sit under
209
+ * one box there is nothing to choose, and the old rule left 14 of the
210
+ * reference Landscape's 30 data objects outside the single application whose
211
+ * own parts were the things binding them.
212
+ * - **`owned` or `unwired`, never `context` alone.** A context slot names
213
+ * something the instance USES and does not contain — the upstream API it
214
+ * calls, the plane it runs on. Folding those would swallow half the landscape
215
+ * into whichever box happened to reference it. At least one binding must be
216
+ * `owned` or `unwired` for the member to fold at all.
164
217
  * - **Not a ruling.** See {@link RULING_CORE_KINDS}.
165
218
  *
166
219
  * A view's own nesting wins where both apply: the view is the more specific
@@ -178,22 +231,64 @@ export function foldTree(input) {
178
231
  else
179
232
  instances.add(membership.instance);
180
233
  }
181
- const parentOf = new Map(fromNesting.parentOf);
234
+ // Whether ANY of a member's bindings is one the instance holds it out by. A
235
+ // member bound only through context slots never folds, however many hold it.
236
+ const heldOutSomewhere = new Set();
182
237
  for (const membership of input.memberships) {
183
- if (parentOf.has(membership.member))
184
- continue;
185
- if ((instancesOf.get(membership.member)?.size ?? 0) !== 1)
186
- continue;
187
- if (membership.wiring === 'context')
188
- continue;
189
- if (RULING_CORE_KINDS.has(coreKindOf(membership.member)))
190
- continue;
191
- // A node the input does not carry cannot be drawn inside anything.
192
- if (!coreKindById.has(membership.member))
193
- continue;
194
- if (membership.member === membership.instance)
195
- continue;
196
- parentOf.set(membership.member, membership.instance);
238
+ if (membership.wiring !== 'context')
239
+ heldOutSomewhere.add(membership.member);
240
+ }
241
+ const parentOf = new Map(fromNesting.parentOf);
242
+ const candidates = [
243
+ ...new Set(input.memberships
244
+ .map(({ member }) => member)
245
+ .filter((member) =>
246
+ // A view's own nesting already placed it, and the view wins.
247
+ !parentOf.has(member) &&
248
+ heldOutSomewhere.has(member) &&
249
+ !RULING_CORE_KINDS.has(coreKindOf(member)) &&
250
+ // A node the input does not carry cannot be drawn inside anything.
251
+ coreKindById.has(member) &&
252
+ // Something that holds itself is not held by anything.
253
+ instancesOf.get(member)?.has(member) !== true)),
254
+ ];
255
+ // Resolved in ROUNDS rather than one pass, because a member's holders may
256
+ // themselves be members whose own parents are decided here. Placing a member
257
+ // before its holders are settled would measure the lowest common ancestor
258
+ // against a tree that is still missing the levels that separate them, and the
259
+ // reference has five-deep chains (spec, mapping, call, client, application),
260
+ // so this is exercised rather than theoretical.
261
+ //
262
+ // A member is settled once it is placed or once it is known to stay outside.
263
+ // Whatever a round cannot decide it hands to the next; when a round decides
264
+ // nothing, what is left is a mutual dependency and stays outside, which is the
265
+ // same answer the cycle guard below would reach for it anyway.
266
+ let pending = candidates;
267
+ while (pending.length > 0) {
268
+ const deferred = [];
269
+ let decided = false;
270
+ for (const member of pending) {
271
+ const holders = [...(instancesOf.get(member) ?? [])];
272
+ if (holders.some((holder) => pending.includes(holder) && holder !== member)) {
273
+ deferred.push(member);
274
+ continue;
275
+ }
276
+ decided = true;
277
+ const parent = holders.length === 1
278
+ ? holders[0]
279
+ : lowestCommonAncestor(holders, parentOf);
280
+ if (parent === undefined || parent === member)
281
+ continue;
282
+ // A member that already contains one of its holders cannot also sit
283
+ // inside it. The cycle guard below is the backstop, not the rule.
284
+ if (holders.some((holder) => isDescendantOf(holder, member, parentOf))) {
285
+ continue;
286
+ }
287
+ parentOf.set(member, parent);
288
+ }
289
+ if (!decided)
290
+ break;
291
+ pending = deferred;
197
292
  }
198
293
  // Slot membership can close a loop the view's nesting alone did not, so the
199
294
  // guard runs again over the combined tree rather than trusting the first.
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { deriveAttestationStaleness } from './attestation-staleness.js';
11
11
  export { deriveArtifactCoverage, type ArtifactCoverage, type CoverageScopePattern, } from './artifact-coverage.js';
12
12
  export { buildRtm, renderRtmMarkdown, type RequirementsTraceabilityMatrix, type RtmAttestation, type RtmContextEntry, type RtmDescopedEntry, type RtmEvidenceVerdict, type RtmLineageEntry, type RtmRealizer, type RtmRow, type RtmSource, } from './rtm.js';
13
13
  export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, PatternMembership, PatternVacancy, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
14
- export { canonicalProjection, evaluateProjection, explainProjection, loadProjection, renderProjectionMarkdown, type ConceptFacet, type ProjectionExclusion, } from './projection.js';
14
+ export { canonicalProjection, evaluateProjection, explainProjection, instanceClosureOf, loadProjection, renderProjectionMarkdown, type ConceptFacet, type ProjectionExclusion, } from './projection.js';
15
15
  export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, type AdapterMapping, type AdapterMappingLoadResult, type AdapterMappingValidationResult, type AdapterMappingsValidationResult, type AdapterSubjectMapping, } from './adapter-mapping.js';
16
16
  export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, ProjectionResult, } from './projection.js';
17
17
  export { createFileSystemStore, type PendingWrite, type SourceStore, type StoredSource, type WriteConflict, type WriteOutcome, } from './source-store.js';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export { constraintExpectsPredicate, reconcileEvidenceReports, } from './reconci
10
10
  export { deriveAttestationStaleness } from './attestation-staleness.js';
11
11
  export { deriveArtifactCoverage, } from './artifact-coverage.js';
12
12
  export { buildRtm, renderRtmMarkdown, } from './rtm.js';
13
- export { canonicalProjection, evaluateProjection, explainProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
13
+ export { canonicalProjection, evaluateProjection, explainProjection, instanceClosureOf, loadProjection, renderProjectionMarkdown, } from './projection.js';
14
14
  export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, } from './adapter-mapping.js';
15
15
  export { createFileSystemStore, } from './source-store.js';
16
16
  export { applyOperations, landOperations, posixDirectoryOf, } from './apply-command.js';
@@ -6,6 +6,25 @@ export interface ProjectionDefinition {
6
6
  readonly version: string;
7
7
  readonly query: {
8
8
  readonly subjects?: readonly string[];
9
+ /**
10
+ * Pattern instances whose CONTENTS this query wants (#473, ADR 0144).
11
+ *
12
+ * Each id names an instance, and the facet selects that instance together
13
+ * with everything the fold tree would draw inside it - the same closure
14
+ * `presentation.fold: instances` collapses into one box, read through the
15
+ * view's own `nesting`. A view that wanted the box wanted what is in it.
16
+ *
17
+ * This is the ONE facet that adds rather than narrows, and it is why
18
+ * `subjects` and this are read as a single identity facet whose values
19
+ * combine with OR (`docs/PROJECTIONS.md`). Every other field still ANDs
20
+ * over the union: `instances` says which subjects are in play, and `kinds`
21
+ * or `statuses` narrow that set the way they always did.
22
+ *
23
+ * Hand-listing the members instead is what this replaces, and the list goes
24
+ * stale the moment the pattern binds another slot. The names here are the
25
+ * instance, not its parts, so the view follows the model.
26
+ */
27
+ readonly instances?: readonly string[];
9
28
  /**
10
29
  * Subjects this query would otherwise select and the author has taken out
11
30
  * (#267, ADR 0122). A facet view states a rule, and every interesting rule
@@ -59,6 +78,17 @@ export interface ProjectionDefinition {
59
78
  readonly showLifecycle?: boolean;
60
79
  readonly showEvidence?: boolean;
61
80
  readonly showOwnership?: boolean;
81
+ /**
82
+ * Whether a bound RULING draws as a row inside its holder rather than as a
83
+ * node of its own (#473 phase 3, ADR 0145).
84
+ *
85
+ * On the reference this takes the whole model from 173 boxes to 91, because
86
+ * 82 of its rulings are bound into slots and every one of them was drawing
87
+ * as a box with a single association edge. Presentation only: the model,
88
+ * the query and the selected set do not move, and turning it off restores
89
+ * the boxes.
90
+ */
91
+ readonly showConstraints?: boolean;
62
92
  /**
63
93
  * The folder this view files itself under in an editor's rail: a label the
64
94
  * author declares, nested with `/`, never the directory the projection
@@ -90,6 +120,7 @@ export { DEFAULT_NESTING, type NestingKind } from './nesting.js';
90
120
  */
91
121
  export { DEFAULT_FOLD, type FoldMode } from './fold-tree.js';
92
122
  import type { NestingKind } from './nesting.js';
123
+ import { type FoldMembership } from './fold-tree.js';
93
124
  /**
94
125
  * Which way a view runs, and the default. Split out for the same reason as the
95
126
  * nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
@@ -118,12 +149,40 @@ export declare function canonicalProjection(projection: ProjectionDefinition): P
118
149
  * A facet of a query, named the way the query names it. What
119
150
  * {@link explainProjection} reports as the reason a subject is not in a view.
120
151
  */
121
- export type ConceptFacet = 'exclude' | 'states' | 'subjects' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
152
+ export type ConceptFacet = 'exclude' | 'states' | 'subjects' | 'instances' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
122
153
  /** One subject a query dropped, and the facet that dropped it. */
123
154
  export interface ProjectionExclusion {
124
155
  readonly id: string;
125
156
  readonly facet: ConceptFacet;
126
157
  }
158
+ /**
159
+ * How a query decides about concepts, built once and shared by the two things
160
+ * that ask.
161
+ *
162
+ * `evaluateProjection` asks whether a subject is in; `explainProjection` asks
163
+ * why one is out. They must never be able to disagree, which is why there is
164
+ * one selector rather than a filter here and a reason-finder somewhere else.
165
+ */
166
+ /**
167
+ * Which subjects each named pattern instance holds, transitively.
168
+ *
169
+ * The SAME tree the canvas folds, computed from the same module over the same
170
+ * inputs, because a view that names an instance must select exactly what the
171
+ * box would have contained. Two implementations of "what is inside this
172
+ * instance" would be two answers to one question, and the one the reader sees
173
+ * is whichever they happened to open (rule: a test must go through the seam it
174
+ * is testing - so must a second caller).
175
+ *
176
+ * Nesting comes from THIS view's `presentation.nesting`, not the default, for
177
+ * the reason folding reads it too: containment is a property of the view, and a
178
+ * view that nests on composition alone holds less than one that also nests on
179
+ * assignment.
180
+ *
181
+ * Without `memberships` the closure is the named instances ALONE. That is a
182
+ * degradation, not an answer, and it is why `check` refuses rather than
183
+ * quietly returning a smaller view (#447, #450).
184
+ */
185
+ export declare function instanceClosureOf(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext, memberships?: readonly FoldMembership[]): ReadonlyMap<string, string>;
127
186
  /** A query facet naming something the model does not have. */
128
187
  export interface UnmatchedSelector {
129
188
  readonly facet: string;
@@ -170,7 +229,7 @@ export declare function unmatchedSelectors(graph: SemanticGraph, projection: Pro
170
229
  * caller so `check`, and anything that adopts this later, refuse in identical
171
230
  * words with an identical code.
172
231
  */
173
- export declare function projectionReferenceDiagnostics(source: WorkspaceSource, projection: ProjectionDefinition, graph: SemanticGraph, profileContext?: ResolvedProfileContext): readonly Diagnostic[];
232
+ export declare function projectionReferenceDiagnostics(source: WorkspaceSource, projection: ProjectionDefinition, graph: SemanticGraph, profileContext?: ResolvedProfileContext, instances?: ReadonlySet<string>): readonly Diagnostic[];
174
233
  /**
175
234
  * Every concept a query leaves out, and the facet that left it out.
176
235
  *
@@ -186,7 +245,7 @@ export declare function projectionReferenceDiagnostics(source: WorkspaceSource,
186
245
  * `additionalProperties: false` and this is a question about a query rather
187
246
  * than part of what a projection IS.
188
247
  */
189
- export declare function explainProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext): readonly ProjectionExclusion[];
190
- export declare function evaluateProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext): ProjectionResult;
248
+ export declare function explainProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext, memberships?: readonly FoldMembership[]): readonly ProjectionExclusion[];
249
+ export declare function evaluateProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext, memberships?: readonly FoldMembership[]): ProjectionResult;
191
250
  export declare function renderProjectionMarkdown(result: ProjectionResult, profileContext?: ResolvedProfileContext): string;
192
251
  export declare function renderBudgetedContext(result: ProjectionResult, budgetTokens: number): string;