yarramate 1.11.0 → 1.12.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.
@@ -46,6 +46,22 @@ export type CatalogueCondition = {
46
46
  readonly condition: 'no-subject-of-kind';
47
47
  readonly kinds: readonly string[];
48
48
  readonly kindMatching?: 'exact' | 'descendants';
49
+ } | {
50
+ /**
51
+ * The positive twin of `no-subject-of-kind`, and workspace-scope like it
52
+ * (#398). A gate wants the opposite polarity from a question: a question
53
+ * exists to be closed by an absence ending, while a gate opens once the
54
+ * thing is there. `opensWhen` requires every condition to hold and has no
55
+ * `not`, so inverting was not available and the phase-ordered interview
56
+ * an adopter was authoring could say "the model still lacks X" but never
57
+ * "the model now has X".
58
+ *
59
+ * ADR 0125 anticipated arrivals in this position: a further condition
60
+ * "can join later without changing the mechanism".
61
+ */
62
+ readonly condition: 'has-subject-of-kind';
63
+ readonly kinds: readonly string[];
64
+ readonly kindMatching?: 'exact' | 'descendants';
49
65
  } | {
50
66
  readonly condition: 'no-state-defined';
51
67
  } | {
@@ -238,6 +254,7 @@ export interface InterrogationReport {
238
254
  readonly summary: InterrogationSummary;
239
255
  readonly waves: readonly ReportWave[];
240
256
  }
257
+ export declare const conditionScope: (condition: CatalogueCondition) => 'workspace' | 'subject';
241
258
  export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string;
242
259
  export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext, evidence?: readonly CatalogueEvidenceObservation[],
243
260
  /**
@@ -199,6 +199,7 @@ const namedKinds = (question) => {
199
199
  switch (condition.condition) {
200
200
  case 'missing-relationship':
201
201
  case 'no-subject-of-kind':
202
+ case 'has-subject-of-kind':
202
203
  case 'missing-constraint':
203
204
  kinds.push(...condition.kinds);
204
205
  break;
@@ -237,6 +238,49 @@ const linkageHits = (index, condition, subjectId, profileContext) => {
237
238
  return counterparts.some((counterpart) => kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
238
239
  });
239
240
  };
241
+ /**
242
+ * What a condition needs in order to mean anything: a subject, or only the
243
+ * workspace (#400).
244
+ *
245
+ * A wave gate evaluates with NO subject, and the catalogue schema offered the
246
+ * whole vocabulary in that position, so a subject-scope condition in
247
+ * `opensWhen` produced a wave that silently never opened (`has-linkage`,
248
+ * `near-duplicate`, `fills-pattern-slot`) or a gate that was silently inert
249
+ * (`missing-linkage`, `isolated`, `missing-claim`, `missing-constraint`) —
250
+ * measured, both halves. Neither was refused. That is the same failure
251
+ * `YM914` already refuses from a different cause: a gate nothing can satisfy
252
+ * is indistinguishable from a gate that is merely unmet.
253
+ *
254
+ * This is a `Record` over the union's discriminant rather than a list of the
255
+ * workspace-scope names, and that is the point. An allowlist cannot fail for
256
+ * the author who wrote it (CONTRIBUTING.md's ninth rule), so a new condition
257
+ * must not be able to arrive and be quietly absent from a gate check. Here it
258
+ * cannot: adding a member to `CatalogueCondition` is a TYPECHECK ERROR until
259
+ * its scope is declared, so the compiler asks the question rather than this
260
+ * table remembering the answer.
261
+ */
262
+ const CONDITION_SCOPE = {
263
+ 'has-any-subject': 'workspace',
264
+ 'no-subject-of-kind': 'workspace',
265
+ 'has-subject-of-kind': 'workspace',
266
+ 'no-state-defined': 'workspace',
267
+ 'exists-linkage': 'workspace',
268
+ 'missing-claim': 'subject',
269
+ 'missing-relationship': 'subject',
270
+ isolated: 'subject',
271
+ 'missing-linkage': 'subject',
272
+ 'has-linkage': 'subject',
273
+ 'missing-constraint': 'subject',
274
+ 'missing-flow-content': 'subject',
275
+ 'missing-reference': 'subject',
276
+ 'missing-attestation': 'subject',
277
+ 'near-duplicate': 'subject',
278
+ 'unconstrained-kind': 'subject',
279
+ 'unscoped-succession': 'subject',
280
+ 'unchallenged-evidence': 'workspace',
281
+ 'fills-pattern-slot': 'subject',
282
+ };
283
+ export const conditionScope = (condition) => CONDITION_SCOPE[condition.condition];
240
284
  const conditionHolds = (index, condition, subjectId, profileContext, evidence, memberships) => {
241
285
  switch (condition.condition) {
242
286
  case 'fills-pattern-slot':
@@ -325,6 +369,14 @@ const conditionHolds = (index, condition, subjectId, profileContext, evidence, m
325
369
  const matching = condition.kindMatching ?? 'descendants';
326
370
  return ![...index.concepts].some((id) => kindMatches(index.kindOf.get(id), condition.kinds, matching, profileContext));
327
371
  }
372
+ case 'has-subject-of-kind': {
373
+ // Deliberately not `!no-subject-of-kind`: written as its own existence
374
+ // check so the empty workspace falls out right rather than by double
375
+ // negative. No subjects of any kind means every gate using it stays
376
+ // shut, which is the #334 posture.
377
+ const matching = condition.kindMatching ?? 'descendants';
378
+ return [...index.concepts].some((id) => kindMatches(index.kindOf.get(id), condition.kinds, matching, profileContext));
379
+ }
328
380
  case 'no-state-defined':
329
381
  return !index.hasStates;
330
382
  case 'missing-linkage':
@@ -813,6 +865,39 @@ const undeclaredWaveDiagnostics = ({ catalogue, locate }, declaredWaves) => cata
813
865
  ...locate(['questions', questionIndex, 'wave']),
814
866
  },
815
867
  ]);
868
+ /**
869
+ * `YM917`: a wave gate may only ask about the workspace (#400).
870
+ *
871
+ * `opensWhen` is evaluated with no subject, so a subject-scope condition here
872
+ * cannot mean what it reads as. Half of them then leave the wave permanently
873
+ * shut and half leave the gate inert, and an author reviewing the YAML sees a
874
+ * gate either way — the same invisible failure `YM914` refuses when a gate
875
+ * names a kind that resolves nowhere.
876
+ *
877
+ * Refused at load rather than narrowed in the schema on purpose. The schema
878
+ * could express it as a second `oneOf`, but a `oneOf` miss reports "must match
879
+ * exactly one schema", which names neither the offending condition nor the
880
+ * ones that would work — and this diagnostic exists precisely because the
881
+ * author cannot see the problem.
882
+ */
883
+ const gateScopeDiagnostics = ({ catalogue, locate, }) => catalogue.waves.flatMap((wave, waveIndex) => (wave.opensWhen ?? []).flatMap((condition, conditionIndex) => conditionScope(condition) === 'subject'
884
+ ? [
885
+ {
886
+ severity: 'error',
887
+ code: 'YM917',
888
+ message: `Condition "${condition.condition}" gates wave "${wave.id}" but asks about a subject, ` +
889
+ 'and a gate is evaluated with none, so the wave would never open or the gate would do ' +
890
+ `nothing. Gate on the workspace instead: ${workspaceScopeConditions().join(', ')}.`,
891
+ ...locate(['waves', waveIndex, 'opensWhen', conditionIndex]),
892
+ },
893
+ ]
894
+ : []));
895
+ // Read off the scope table rather than restated, so the remedy a diagnostic
896
+ // offers cannot drift from the set the engine actually accepts.
897
+ const workspaceScopeConditions = () => Object.entries(CONDITION_SCOPE)
898
+ .filter(([, scope]) => scope === 'workspace')
899
+ .map(([condition]) => condition)
900
+ .sort();
816
901
  const unresolvableKindDiagnostics = ({ catalogue, locate }, profileContext) =>
817
902
  // Only when a caller has a compiled workspace to check against. Without one
818
903
  // there is no way to tell a typo from a kind whose profile simply is not
@@ -951,6 +1036,7 @@ export function composeCatalogues(sources, profileContext) {
951
1036
  const declaredWaves = new Set(declaredBy.keys());
952
1037
  const crossDiagnostics = documents.flatMap((document) => [
953
1038
  ...undeclaredWaveDiagnostics(document, declaredWaves),
1039
+ ...gateScopeDiagnostics(document),
954
1040
  ...unresolvableKindDiagnostics(document, profileContext),
955
1041
  ...unauthorableOfferDiagnostics(document, profileContext),
956
1042
  ]);
@@ -985,6 +1071,13 @@ export function loadQuestionCatalogue(catalogueSource, profileContext) {
985
1071
  if (waveDiagnostics.length > 0) {
986
1072
  return { ok: false, diagnostics: waveDiagnostics };
987
1073
  }
1074
+ // Before the kind checks, and unlike them it needs no profile context: a
1075
+ // gate that asks about a subject is wrong on its own terms, whether or not
1076
+ // a caller brought a compiled workspace to resolve kinds against.
1077
+ const scopeDiagnostics = gateScopeDiagnostics(loaded.document);
1078
+ if (scopeDiagnostics.length > 0) {
1079
+ return { ok: false, diagnostics: scopeDiagnostics };
1080
+ }
988
1081
  const kindDiagnostics = unresolvableKindDiagnostics(loaded.document, profileContext);
989
1082
  if (kindDiagnostics.length > 0) {
990
1083
  return { ok: false, diagnostics: kindDiagnostics };