yarramate 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -624,6 +624,8 @@ export const startVisualServer = async (options) => {
624
624
  // Threaded whole (ADR 0131): a narrow copy here is how a slot
625
625
  // question would silently never fire in the embedded pane.
626
626
  patternMemberships: compiled.patternMemberships,
627
+ // The vacant half, on the same rule (#447).
628
+ patternVacancies: compiled.patternVacancies,
627
629
  };
628
630
  const workspaceModel = renderedWorkspaceOf(compiledWorkspace, views, {
629
631
  authority: rendered.authority,
@@ -4,7 +4,7 @@ import type { ResolvedWorkspace } from "../../workspace.js";
4
4
  import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.js";
5
5
  import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
6
6
  import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
7
- import { type CataloguePatternMembership } from "../../interrogate-command.js";
7
+ import { type CataloguePatternMembership, type CataloguePatternVacancy } from "../../interrogate-command.js";
8
8
  import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
9
9
  import type { VisualInterrogationOverlay, VisualRenderedModel } from "./wire.js";
10
10
  /**
@@ -60,6 +60,8 @@ export declare const interrogationOverlayOf: (compiled: {
60
60
  readonly profileContext: ResolvedProfileContext;
61
61
  /** From the compilation (ADR 0131); absent, slot questions stay quiet. */
62
62
  readonly patternMemberships?: readonly CataloguePatternMembership[];
63
+ /** From the compilation (#447); absent, `missing-part` stays quiet. */
64
+ readonly patternVacancies?: readonly CataloguePatternVacancy[];
63
65
  },
64
66
  /**
65
67
  * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
@@ -94,6 +96,8 @@ export declare const renderedWorkspaceOf: (compiled: {
94
96
  readonly graph: SemanticGraph;
95
97
  readonly profileContext: ResolvedProfileContext;
96
98
  readonly patternMemberships?: readonly CataloguePatternMembership[];
99
+ /** Threaded on to the overlay (#447); absent, `missing-part` stays quiet. */
100
+ readonly patternVacancies?: readonly CataloguePatternVacancy[];
97
101
  }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary" | "interrogation">, catalogue?: {
98
102
  readonly path: string;
99
103
  readonly source: string;
@@ -64,7 +64,7 @@ dismissed = []) => {
64
64
  const composed = composeCatalogues(Array.isArray(catalogue) ? catalogue : [catalogue]);
65
65
  if (!composed.ok)
66
66
  return undefined;
67
- const report = evaluateCatalogue(composed.composed.catalogue, compiled.graph, compiled.profileContext, undefined, composed.composed.catalogues, compiled.patternMemberships);
67
+ const report = evaluateCatalogue(composed.composed.catalogue, compiled.graph, compiled.profileContext, undefined, composed.composed.catalogues, compiled.patternMemberships, compiled.patternVacancies);
68
68
  const dismissedEverywhere = new Set(dismissed
69
69
  .filter(({ subject }) => subject === undefined)
70
70
  .map(({ questionId }) => questionId));
@@ -27,6 +27,10 @@ import { validateOperations } from './schema-validation.js';
27
27
  // asserted — it never silently shrinks anything.
28
28
  const SCALAR_CONCEPT_FIELDS = ['kind', 'name', 'description', 'status', 'owner'];
29
29
  const LIST_CONCEPT_FIELDS = ['aka', 'constraints', 'references', 'presentIn', 'attestations', 'distinctFrom', 'supersedes'];
30
+ // The third category (#448). `parts` is the first MAP-valued concept field:
31
+ // it neither replaces like a scalar nor appends like a list, it merges by
32
+ // slot. See `mergeMapField`.
33
+ const MAP_CONCEPT_FIELDS = ['parts'];
30
34
  const SCALAR_RELATIONSHIP_FIELDS = ['kind', 'from', 'to', 'name', 'description', 'status', 'mode', 'content'];
31
35
  const LIST_RELATIONSHIP_FIELDS = ['references', 'presentIn'];
32
36
  // An overlay entry's address is the pair (target, key); everything else it
@@ -233,6 +237,66 @@ const appendListField = (source, map, key, additions) => {
233
237
  }
234
238
  return spliceValue(source, start, valueEnd, `\n${sequenceEntries(merged, indent + 2)}`);
235
239
  };
240
+ // Merge a MAP-valued field by key (#448). A named slot rebinds, an unnamed one
241
+ // is untouched: ADR 0062's convention, where a write enriches what is there and
242
+ // never silently shrinks it. Replacing the whole map would unbind slots the
243
+ // operation never mentioned, which is exactly the silent shrinking that rule
244
+ // forbids.
245
+ //
246
+ // The per-slot work is `setScalarField` against the NESTED map, because
247
+ // inserting or replacing one key of a mapping is what that already does. The
248
+ // source is re-parsed between slots for the same reason the caller re-parses
249
+ // between fields: every splice moves the offsets after it.
250
+ const mergeMapField = (source, locateMap, key, additions) => {
251
+ const entries = Object.entries(additions);
252
+ if (entries.length === 0)
253
+ return source;
254
+ const map = locateMap(source);
255
+ if (map === undefined)
256
+ return source;
257
+ if (map.flow) {
258
+ return rewriteFlowItem(source, map, (fields) => ({
259
+ ...fields,
260
+ [key]: {
261
+ ...(fields[key] ?? {}),
262
+ ...additions,
263
+ },
264
+ }));
265
+ }
266
+ const existing = nestedMap(map, key);
267
+ if (existing === undefined) {
268
+ const pair = pairFor(map, key);
269
+ const indent = fieldIndentOf(source, map);
270
+ const rendered = entries
271
+ .map(([slot, value]) => `${' '.repeat(indent + 2)}${slot}: ${valueText(value)}`)
272
+ .join('\n');
273
+ // A `parts` that exists but is not a block mapping (flow, or empty) is
274
+ // replaced wholesale rather than merged into: there is nothing to preserve
275
+ // that the entries do not already carry.
276
+ if (pair !== undefined) {
277
+ const held = isMap(pair.value)
278
+ ? pair.value.toJSON()
279
+ : {};
280
+ const merged = { ...held, ...additions };
281
+ const [start, valueEnd] = nodeRange(pair.value);
282
+ return spliceValue(source, start, valueEnd, `\n${Object.entries(merged)
283
+ .map(([slot, value]) => `${' '.repeat(indent + 2)}${slot}: ${valueText(value)}`)
284
+ .join('\n')}`);
285
+ }
286
+ return insertBlock(source, itemFieldInsertAt(source, map), `${' '.repeat(indent)}${key}:\n${rendered}\n`);
287
+ }
288
+ let updated = source;
289
+ for (const [slot, value] of entries) {
290
+ const host = locateMap(updated);
291
+ if (host === undefined)
292
+ break;
293
+ const target = nestedMap(host, key);
294
+ if (target === undefined)
295
+ break;
296
+ updated = setScalarField(updated, target, slot, value);
297
+ }
298
+ return updated;
299
+ };
236
300
  // Retraction (#115): delete the field's whole entry, from the start of its
237
301
  // key line through the end of its value's last line. A flow item is
238
302
  // rewritten instead — line-based deletion there would take the whole item
@@ -602,6 +666,14 @@ export const applyOperations = (input) => {
602
666
  continue;
603
667
  source = appendListField(source, itemMap(source, collection, id).map, key, additions);
604
668
  }
669
+ if (operation.op === 'update-concept') {
670
+ for (const key of MAP_CONCEPT_FIELDS) {
671
+ const additions = payload[key];
672
+ if (additions === undefined)
673
+ continue;
674
+ source = mergeMapField(source, (current) => itemMap(current, collection, id)?.map, key, additions);
675
+ }
676
+ }
605
677
  for (const key of removals) {
606
678
  const removed = removeField(source, itemMap(source, collection, id).map, key);
607
679
  if (removed === undefined) {
@@ -475,7 +475,7 @@ export function runAskCommand(options, cwd) {
475
475
  }, workspace, cwd), compilation.profileContext);
476
476
  if (!composed.ok)
477
477
  return failed(composed.diagnostics);
478
- const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues, compilation.patternMemberships);
478
+ const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues, compilation.patternMemberships, compilation.patternVacancies);
479
479
  const result = {
480
480
  format: 'yarramate/ask-result/v1',
481
481
  workspace: workspace.id,
@@ -719,7 +719,7 @@ export function runAskCommand(options, cwd) {
719
719
  evidenceObservations.push(...loaded.evidence.observations);
720
720
  }
721
721
  const report = {
722
- ...evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues, compilation.patternMemberships),
722
+ ...evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues, compilation.patternMemberships, compilation.patternVacancies),
723
723
  workspace: workspace.id,
724
724
  };
725
725
  // Field by field, to fix key ORDER in the emitted JSON. Every optional
@@ -735,6 +735,7 @@ export function runAskCommand(options, cwd) {
735
735
  ? {}
736
736
  : { catalogues: report.catalogues }),
737
737
  semantics: report.semantics,
738
+ inputs: report.inputs,
738
739
  summary: report.summary,
739
740
  waves: report.waves,
740
741
  };
@@ -1021,7 +1022,7 @@ export function runAskCommand(options, cwd) {
1021
1022
  return failed(loaded.diagnostics);
1022
1023
  evidenceDocuments.push(loaded.evidence);
1023
1024
  }
1024
- const report = evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues, compilation.patternMemberships);
1025
+ const report = evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues, compilation.patternMemberships, compilation.patternVacancies);
1025
1026
  const openQuestions = [];
1026
1027
  for (const wave of report.waves) {
1027
1028
  for (const question of wave.questions) {
@@ -109,6 +109,31 @@ export interface PatternMembership {
109
109
  readonly instance: string;
110
110
  readonly pattern: string;
111
111
  }
112
+ /**
113
+ * One slot of one pattern instance that nothing is bound into (#447): the
114
+ * shape of a question a pattern already knows to ask. The mirror of
115
+ * {@link PatternMembership}, and it has to be a second array rather than a
116
+ * widening of that one, because a vacancy has no `member` and making that
117
+ * field optional would break every reader.
118
+ *
119
+ * `required` is here because a vacancy on a SUCCESSFUL compile can be a
120
+ * required slot after all. The first draft of this shape left the field out,
121
+ * reasoning that a required slot left unbound is `YM416` and so never reaches
122
+ * a result. That is true only of an instance that declares `parts`: a concept
123
+ * whose kind has a pattern but which declares no parts at all is not a
124
+ * `PatternInstance`, never reaches YM416, and compiles clean with every slot
125
+ * vacant. So the flag is load-bearing rather than permanently `false`, and it
126
+ * is the difference between "you have not decided this yet" and "this model
127
+ * does not stand up without it".
128
+ */
129
+ export interface PatternVacancy {
130
+ readonly instance: string;
131
+ readonly pattern: string;
132
+ readonly slot: string;
133
+ readonly slotKind: string;
134
+ /** The pattern declares this part `required` (ADR 0123). */
135
+ readonly required: boolean;
136
+ }
112
137
  export type CompilationResult = {
113
138
  readonly ok: true;
114
139
  readonly graph: SemanticGraph;
@@ -119,6 +144,8 @@ export type CompilationResult = {
119
144
  * to `evaluateCatalogue`, or `fills-pattern-slot` never fires.
120
145
  */
121
146
  readonly patternMemberships?: readonly PatternMembership[];
147
+ /** Same rule as `patternMemberships`, for the same reason (#447). */
148
+ readonly patternVacancies?: readonly PatternVacancy[];
122
149
  } | {
123
150
  readonly ok: false;
124
151
  readonly diagnostics: readonly Diagnostic[];
@@ -128,6 +155,7 @@ export type ContextualCompilationResult = {
128
155
  readonly graph: SemanticGraph;
129
156
  readonly profileContext: ResolvedProfileContext;
130
157
  readonly patternMemberships?: readonly PatternMembership[];
158
+ readonly patternVacancies?: readonly PatternVacancy[];
131
159
  } | {
132
160
  readonly ok: false;
133
161
  readonly diagnostics: readonly Diagnostic[];
package/dist/compiler.js CHANGED
@@ -689,6 +689,7 @@ function compileWorkspaceResolved(parsed) {
689
689
  name: slot,
690
690
  kindIdentity: kind.identity,
691
691
  required: part.required === true,
692
+ kindMatching: part.kindMatching === 'descendants' ? 'descendants' : 'exact',
692
693
  });
693
694
  }
694
695
  if (!slotsOk)
@@ -846,6 +847,18 @@ function compileWorkspaceResolved(parsed) {
846
847
  return diagnosticFailure(patternDiagnostics);
847
848
  }
848
849
  const patternInstances = [];
850
+ /**
851
+ * Concepts whose kind HAS a pattern but which declare no `parts` at all —
852
+ * the greenfield instance ADR 0123 named and left to a later phase, and the
853
+ * one an interview has the most to ask (#447).
854
+ *
855
+ * Deliberately NOT `patternInstances`. That list drives YM416, wiring
856
+ * expansion and membership, and adding these to it would start refusing
857
+ * workspaces that compile today. Instance-hood for the purpose of BEING
858
+ * ASKED is a wider question than instance-hood for the purpose of being
859
+ * expanded, so it gets its own list rather than a widened one.
860
+ */
861
+ const unbegunInstances = [];
849
862
  const documents = documentInputs.map(({ input, entry, fresh }) => {
850
863
  // Schema-checked by `parseWorkspaceSource`; the faults it found are the
851
864
  // `schemaDiagnostics` returned below, and they gate every later phase.
@@ -1562,9 +1575,17 @@ function compileWorkspaceResolved(parsed) {
1562
1575
  // A pattern instance is only COLLECTED here. What its slots bind may be
1563
1576
  // declared in any document, so the bindings cannot be checked until
1564
1577
  // every document has been read (#268, ADR 0123).
1578
+ const conceptKindIdentity = selectedProfile.conceptKinds.get(concept.kind)?.identity ?? concept.kind;
1579
+ if (concept.parts === undefined) {
1580
+ // Nothing to bind and nothing to expand, but the kind still promises a
1581
+ // shape, so the interview can still ask about it (#447).
1582
+ const pattern = patternsByKind.get(conceptKindIdentity);
1583
+ if (pattern !== undefined) {
1584
+ unbegunInstances.push({ instance: subject, pattern });
1585
+ }
1586
+ }
1565
1587
  if (concept.parts !== undefined) {
1566
- const kindIdentity = selectedProfile.conceptKinds.get(concept.kind)?.identity ??
1567
- concept.kind;
1588
+ const kindIdentity = conceptKindIdentity;
1568
1589
  const pattern = patternsByKind.get(kindIdentity);
1569
1590
  if (pattern === undefined) {
1570
1591
  const where = location(['concepts', index, 'parts'], `/concepts/${index}/parts`);
@@ -2067,6 +2088,13 @@ function compileWorkspaceResolved(parsed) {
2067
2088
  else
2068
2089
  group.push(entry);
2069
2090
  }
2091
+ /**
2092
+ * Every wire that wants to mint, keyed by the TRIPLE it names (#460).
2093
+ * Collected across all instances before any is minted, because two wires
2094
+ * belonging to different instances can name one triple and only one claim
2095
+ * may survive.
2096
+ */
2097
+ const wireCandidates = new Map();
2070
2098
  for (const { instance, pattern, bindings, sourceOf } of patternInstances) {
2071
2099
  const boundTo = new Map();
2072
2100
  for (const [slot, target] of bindings) {
@@ -2103,13 +2131,27 @@ function compileWorkspaceResolved(parsed) {
2103
2131
  }
2104
2132
  boundTo.set(target, slot);
2105
2133
  const actual = kindOfSubject.get(target);
2106
- if (actual !== slotShape.kindIdentity) {
2134
+ // `descendants` admits any kind whose lineage includes the slot kind
2135
+ // (#449), which is what the word already means on catalogue selectors
2136
+ // and on `missing-relationship`. It fails safe: minted wiring is
2137
+ // checked against the relationship table using the ACTUAL bound
2138
+ // subjects' kinds, so a descendant that is not a legal endpoint is
2139
+ // still refused by the ordinary relationship check rather than
2140
+ // slipping through on the pattern's authority.
2141
+ const admitted = actual === slotShape.kindIdentity ||
2142
+ (slotShape.kindMatching === 'descendants' &&
2143
+ actual !== undefined &&
2144
+ (conceptKindByIdentity.get(actual)?.lineage ?? []).includes(slotShape.kindIdentity));
2145
+ if (!admitted) {
2107
2146
  diagnostics.push({
2108
2147
  severity: 'error',
2109
2148
  code: 'YM417',
2110
2149
  message: `Part "${slot}" of "${instance}" binds "${target}", which is ` +
2111
2150
  `"${actual ?? 'not a concept'}"; the pattern declares this part ` +
2112
- `"${slotShape.kindIdentity}"`,
2151
+ `"${slotShape.kindIdentity}"` +
2152
+ (slotShape.kindMatching === 'descendants'
2153
+ ? ' or a kind descending from it'
2154
+ : ''),
2113
2155
  path: where.path,
2114
2156
  pointer: where.pointer,
2115
2157
  line: where.line,
@@ -2173,36 +2215,89 @@ function compileWorkspaceResolved(parsed) {
2173
2215
  }
2174
2216
  if (satisfied)
2175
2217
  continue;
2176
- const wireId = [
2218
+ // Collected rather than minted, because whether this wire mints
2219
+ // depends on wires belonging to OTHER instances (#460). Two wires can
2220
+ // name one triple, and before this they both minted: two relationship
2221
+ // claims for one relationship.
2222
+ const key = `${from}\u0000${wire.kindIdentity}\u0000${to}`;
2223
+ const candidate = {
2177
2224
  instance,
2178
- ...(wire.from === 'self' ? [] : [wire.from]),
2179
- wire.coreKind,
2180
- wire.to,
2181
- ].join('-');
2182
- if (declaredIds.has(wireId)) {
2183
- diagnostics.push({
2184
- severity: 'error',
2185
- code: 'YM420',
2186
- message: `The pattern for "${pattern.kindIdentity}" derives the wiring id ` +
2187
- `"${wireId}" for "${instance}", which is already a declared subject`,
2188
- path: where.path,
2189
- pointer: where.pointer,
2190
- line: where.line,
2191
- column: where.column,
2192
- });
2193
- continue;
2194
- }
2195
- declaredIds.add(wireId);
2196
- subjects.push({ id: wireId, type: 'relationship' });
2197
- claims.push({
2198
- id: wireId,
2199
- subject: from,
2200
- predicate: wire.kindIdentity,
2201
- object: { ref: to },
2202
- origin: 'declared',
2203
- source: where,
2225
+ pattern,
2226
+ wire,
2227
+ from,
2228
+ to,
2229
+ where,
2230
+ owner: wire.from === 'self',
2231
+ wireId: [
2232
+ instance,
2233
+ ...(wire.from === 'self' ? [] : [wire.from]),
2234
+ wire.coreKind,
2235
+ wire.to,
2236
+ ].join('-'),
2237
+ };
2238
+ const group = wireCandidates.get(key);
2239
+ if (group === undefined)
2240
+ wireCandidates.set(key, [candidate]);
2241
+ else
2242
+ group.push(candidate);
2243
+ }
2244
+ }
2245
+ // One claim per triple, whichever wires named it (#460, ADR 0141).
2246
+ //
2247
+ // OWNERSHIP decides the id, not walk order. A wire whose `from` is `self`
2248
+ // owns the edge, because the edge leaves that instance; a wire whose
2249
+ // `from` is a slot is a guest naming somebody else's edge, and defers. So
2250
+ // an edge with an owner keeps the ADR 0123 id it has always had, and
2251
+ // nothing moves for a workspace that compiles today.
2252
+ //
2253
+ // Where every wire is a guest the owner's wire is absent — typically its
2254
+ // instance is greenfield, and an unbound slot wires nothing — so there is
2255
+ // no ADR 0123 id to prefer and the id comes from the TRIPLE instead. That
2256
+ // is stable under adding or removing further guests, which sorting a
2257
+ // winner out of the guests would not be.
2258
+ for (const [, group] of [...wireCandidates].sort(([left], [right]) => left.localeCompare(right))) {
2259
+ const owners = group
2260
+ .filter(({ owner }) => owner)
2261
+ // Two owners on one triple cannot happen: `self` differs per instance,
2262
+ // and within one instance YM315 already refuses two slots naming one
2263
+ // subject. Sorted anyway rather than trusting that from a distance.
2264
+ .sort((left, right) => left.wireId.localeCompare(right.wireId));
2265
+ const winner = owners[0] ?? group[0];
2266
+ if (winner === undefined)
2267
+ continue;
2268
+ // A group of ONE is not a collision and keeps its ADR 0123 id, always.
2269
+ // This is the case that matters most, because a wire between two SLOTS
2270
+ // of one pattern (`component --composition--> interface`) has no `self`
2271
+ // endpoint and so no owner, yet nothing is competing with it. Deriving
2272
+ // its id from the triple would rename every such edge in every
2273
+ // workspace, which is exactly the migration ownership exists to avoid;
2274
+ // ADR 0123's own worked example is one of them.
2275
+ const wireId = group.length === 1 || owners.length > 0
2276
+ ? winner.wireId
2277
+ : [winner.from, winner.wire.coreKind, winner.to].join('-');
2278
+ if (declaredIds.has(wireId)) {
2279
+ diagnostics.push({
2280
+ severity: 'error',
2281
+ code: 'YM420',
2282
+ message: `The pattern for "${winner.pattern.kindIdentity}" derives the wiring id ` +
2283
+ `"${wireId}" for "${winner.instance}", which is already a declared subject`,
2284
+ path: winner.where.path,
2285
+ pointer: winner.where.pointer,
2286
+ line: winner.where.line,
2287
+ column: winner.where.column,
2204
2288
  });
2289
+ continue;
2205
2290
  }
2291
+ declaredIds.add(wireId);
2292
+ subjects.push({ id: wireId, type: 'relationship' });
2293
+ claims.push({
2294
+ id: wireId,
2295
+ subject: winner.from,
2296
+ predicate: winner.wire.kindIdentity,
2297
+ object: { ref: winner.to },
2298
+ origin: 'declared',
2299
+ source: winner.where,
2300
+ });
2206
2301
  }
2207
2302
  }
2208
2303
  // ---- macro edges through ports (#268 phase 2, ADR 0124) ------------------
@@ -2349,9 +2444,44 @@ function compileWorkspaceResolved(parsed) {
2349
2444
  left.pattern.localeCompare(right.pattern) ||
2350
2445
  left.instance.localeCompare(right.instance) ||
2351
2446
  left.slot.localeCompare(right.slot));
2447
+ // The mirror (#447): one entry per slot nothing was bound into. Always
2448
+ // emitted, possibly empty — an empty array is a workspace whose instances
2449
+ // are fully bound, while a missing array is a caller that never looked.
2450
+ //
2451
+ // Derived from BOTH lists, and that is the substance of the thing. An
2452
+ // instance that declares some parts is asked about the rest; an instance
2453
+ // that declares none is asked about all of them, because a template nobody
2454
+ // has begun filling in is the one with the most blanks, not the one with
2455
+ // none. Reading only `patternInstances` here would have reported `[]` for
2456
+ // it, and `[]` means "fully bound".
2457
+ const patternVacancies = [
2458
+ ...patternInstances.map(({ instance, pattern, bindings }) => ({
2459
+ instance,
2460
+ pattern,
2461
+ bound: bindings,
2462
+ })),
2463
+ ...unbegunInstances.map(({ instance, pattern }) => ({
2464
+ instance,
2465
+ pattern,
2466
+ bound: new Map(),
2467
+ })),
2468
+ ]
2469
+ .flatMap(({ instance, pattern, bound }) => [...pattern.slots.values()]
2470
+ .filter((slot) => !bound.has(slot.name))
2471
+ .map((slot) => ({
2472
+ instance,
2473
+ pattern: pattern.kindIdentity,
2474
+ slot: slot.name,
2475
+ slotKind: slot.kindIdentity,
2476
+ required: slot.required === true,
2477
+ })))
2478
+ .sort((left, right) => left.instance.localeCompare(right.instance) ||
2479
+ left.pattern.localeCompare(right.pattern) ||
2480
+ left.slot.localeCompare(right.slot));
2352
2481
  return {
2353
2482
  ok: true,
2354
2483
  patternMemberships,
2484
+ patternVacancies,
2355
2485
  profileContext: {
2356
2486
  conceptKindLineages: immutableMap([...conceptKindByIdentity]
2357
2487
  .sort(([left], [right]) => left.localeCompare(right))
@@ -2441,6 +2571,7 @@ export function compileWorkspace(sources) {
2441
2571
  ok: true,
2442
2572
  graph: result.graph,
2443
2573
  patternMemberships: result.patternMemberships,
2574
+ patternVacancies: result.patternVacancies,
2444
2575
  }
2445
2576
  : result;
2446
2577
  }
@@ -294,7 +294,7 @@ export function runDesignCommand(options, cwd) {
294
294
  };
295
295
  }
296
296
  }
297
- const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues, compilation.patternMemberships);
297
+ const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues, compilation.patternMemberships, compilation.patternVacancies);
298
298
  // Keyed by the QUALIFIED id, matching what the report now carries.
299
299
  const askPlainById = new Map(composed.composed.catalogue.questions.flatMap((question) => question.askPlain === undefined
300
300
  ? []
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { constraintExpectsPredicate, reconcileEvidenceReports, type AssertedRela
10
10
  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
- export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, PatternMembership, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
13
+ export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, PatternMembership, PatternVacancy, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
14
14
  export { canonicalProjection, evaluateProjection, explainProjection, 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';
@@ -19,4 +19,4 @@ export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, typ
19
19
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
20
20
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
21
21
  export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
22
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
22
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CataloguePatternVacancy, type CatalogueInput, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
package/dist/index.js CHANGED
@@ -17,4 +17,4 @@ export { applyOperations, landOperations, posixDirectoryOf, } from './apply-comm
17
17
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
18
18
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
19
19
  export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
20
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
20
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';