yarramate 1.2.0 → 1.4.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 (84) hide show
  1. package/catalogues/core-enrichment.yaml +237 -2
  2. package/dist/adapters/visual/session-server.js +16 -1
  3. package/dist/adapters/visual/view-identity.d.ts +1 -1
  4. package/dist/adapters/visual/view-identity.js +61 -3
  5. package/dist/adapters/visual/wire.d.ts +30 -0
  6. package/dist/adapters/visual/workspace-model.d.ts +35 -3
  7. package/dist/adapters/visual/workspace-model.js +62 -2
  8. package/dist/ask-command.js +37 -14
  9. package/dist/cli-support.d.ts +1 -1
  10. package/dist/cli-support.js +1 -1
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +26 -5
  13. package/dist/compiler.d.ts +13 -1
  14. package/dist/compiler.js +646 -0
  15. package/dist/concept-drafting.d.ts +13 -2
  16. package/dist/concept-drafting.js +15 -3
  17. package/dist/design-command.js +30 -4
  18. package/dist/evidence.js +25 -0
  19. package/dist/graph-projection.d.ts +10 -0
  20. package/dist/graph-projection.js +1 -0
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/interrogate-command.d.ts +37 -1
  24. package/dist/interrogate-command.js +93 -42
  25. package/dist/interrogation-entry.d.ts +1 -1
  26. package/dist/layout-direction.d.ts +14 -0
  27. package/dist/layout-direction.js +5 -0
  28. package/dist/projection.d.ts +22 -6
  29. package/dist/projection.js +23 -2
  30. package/dist/relationship-drafting.d.ts +21 -2
  31. package/dist/relationship-drafting.js +49 -4
  32. package/dist/subject-references.js +5 -0
  33. package/dist/visual-app/assets/index-Rkq6smL2.css +1 -0
  34. package/dist/visual-app/assets/index-yCHtmQUH.js +394 -0
  35. package/dist/visual-app/index.html +2 -2
  36. package/dist/visual-app-lib/editor.js +29639 -27514
  37. package/dist/visual-app-lib/styles.css +1 -1
  38. package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +1 -1
  39. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +30 -0
  40. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +35 -3
  41. package/dist/visual-app-lib/types/compiler.d.ts +13 -1
  42. package/dist/visual-app-lib/types/concept-drafting.d.ts +13 -2
  43. package/dist/visual-app-lib/types/graph-projection.d.ts +10 -0
  44. package/dist/visual-app-lib/types/interrogate-command.d.ts +207 -0
  45. package/dist/visual-app-lib/types/layout-direction.d.ts +14 -0
  46. package/dist/visual-app-lib/types/projection.d.ts +22 -6
  47. package/dist/visual-app-lib/types/relationship-drafting.d.ts +21 -2
  48. package/dist/visual-app-lib/types/subject-identity.d.ts +34 -0
  49. package/dist/visual-app-lib/types/visual-app/App.d.ts +27 -2
  50. package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
  51. package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +9 -1
  52. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +15 -6
  53. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +77 -6
  54. package/dist/visual-app-lib/types/visual-app/kind-palette.d.ts +37 -0
  55. package/dist/visual-app-lib/types/visual-app/local-host.d.ts +42 -0
  56. package/dist/visual-app-lib/types/visual-app/mount.d.ts +73 -2
  57. package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +15 -0
  58. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
  59. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +8 -1
  60. package/dist/visual-app-lib/types/visual-app/save-view.d.ts +11 -5
  61. package/dist/visual-app-lib/types/visual-app/shipped-catalogue.d.ts +10 -0
  62. package/dist/visual-app-lib/types/visual-app/state.d.ts +15 -4
  63. package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +13 -1
  64. package/dist/visual-app-lib/types/visual-app/subject-filter.d.ts +35 -0
  65. package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +8 -0
  66. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +42 -9
  67. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +11 -2
  68. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +81 -11
  69. package/dist/visual-app-lib/types/workspace.d.ts +2 -0
  70. package/dist/workspace.d.ts +2 -0
  71. package/dist/workspace.js +1 -0
  72. package/docs/CONSUMING-YARRAMATE.md +81 -0
  73. package/package.json +2 -1
  74. package/schema/yarramate-design-step.schema.json +26 -0
  75. package/schema/yarramate-document.schema.json +14 -0
  76. package/schema/yarramate-interrogation-report.schema.json +31 -0
  77. package/schema/yarramate-pattern.schema.json +133 -0
  78. package/schema/yarramate-projection.schema.json +9 -0
  79. package/schema/yarramate-question-catalogue.schema.json +34 -0
  80. package/schema/yarramate-visual-graph.schema.json +9 -0
  81. package/schema/yarramate-workspace.schema.json +4 -0
  82. package/skills/yarramate-architecture/SKILL.md +5 -0
  83. package/dist/visual-app/assets/index-CcmfL3oY.js +0 -394
  84. package/dist/visual-app/assets/index-DLutIWES.css +0 -1
@@ -9,8 +9,14 @@ import type { YarramateOperation } from './operations.js';
9
9
  * about a name produces worse ids than a transliteration of the name does.
10
10
  * Returning null rather than a placeholder keeps a subject called `"???"` from
11
11
  * landing as `subject-1`, which nothing could later be traced back from.
12
+ *
13
+ * `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
14
+ * draft never enters the rendered graph, so without it a second subject whose
15
+ * name slugs to the same id re-proposed it and the editor's replace-by-target
16
+ * staging silently swallowed the first (#315) - the identical blind spot
17
+ * `proposeRelationshipId` had before #306's fix, and the identical way out.
12
18
  */
13
- export declare const proposeConceptId: (graph: CanvasGraph, name: string) => string | null;
19
+ export declare const proposeConceptId: (graph: CanvasGraph, name: string, reserved?: Iterable<string>) => string | null;
14
20
  /**
15
21
  * The operation that lands a new subject, or `null` when the draft is one no
16
22
  * document could accept.
@@ -19,9 +25,14 @@ export declare const proposeConceptId: (graph: CanvasGraph, name: string) => str
19
25
  * a kind outside it here, rather than trusting the caller's palette, is the
20
26
  * same posture `draftRelationship` takes: the guarantee has to hold for any
21
27
  * caller, not only one that filtered first.
28
+ *
29
+ * A caller holding drafts the graph has not landed yet - an editor with a
30
+ * pending changeset - passes their ids as `reserved`, so a second subject
31
+ * slugging to a taken id steps to `-2` instead of colliding with the first
32
+ * (#315).
22
33
  */
23
34
  export declare const draftConcept: (graph: CanvasGraph, input: {
24
35
  readonly name: string;
25
36
  readonly kind: string;
26
37
  readonly document: string;
27
- }, kinds: readonly string[]) => YarramateOperation | null;
38
+ }, kinds: readonly string[], reserved?: Iterable<string>) => YarramateOperation | null;
@@ -17,8 +17,14 @@ const ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
17
17
  * about a name produces worse ids than a transliteration of the name does.
18
18
  * Returning null rather than a placeholder keeps a subject called `"???"` from
19
19
  * landing as `subject-1`, which nothing could later be traced back from.
20
+ *
21
+ * `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
22
+ * draft never enters the rendered graph, so without it a second subject whose
23
+ * name slugs to the same id re-proposed it and the editor's replace-by-target
24
+ * staging silently swallowed the first (#315) - the identical blind spot
25
+ * `proposeRelationshipId` had before #306's fix, and the identical way out.
20
26
  */
21
- export const proposeConceptId = (graph, name) => {
27
+ export const proposeConceptId = (graph, name, reserved = []) => {
22
28
  const base = name
23
29
  .normalize('NFKD')
24
30
  // The marks NFKD split off, dropped rather than treated as boundaries:
@@ -38,6 +44,7 @@ export const proposeConceptId = (graph, name) => {
38
44
  const taken = new Set([
39
45
  ...graph.nodes.map((node) => node.id),
40
46
  ...graph.edges.map((edge) => edge.id),
47
+ ...reserved,
41
48
  ]);
42
49
  if (!taken.has(base))
43
50
  return base;
@@ -55,8 +62,13 @@ export const proposeConceptId = (graph, name) => {
55
62
  * a kind outside it here, rather than trusting the caller's palette, is the
56
63
  * same posture `draftRelationship` takes: the guarantee has to hold for any
57
64
  * caller, not only one that filtered first.
65
+ *
66
+ * A caller holding drafts the graph has not landed yet - an editor with a
67
+ * pending changeset - passes their ids as `reserved`, so a second subject
68
+ * slugging to a taken id steps to `-2` instead of colliding with the first
69
+ * (#315).
58
70
  */
59
- export const draftConcept = (graph, input, kinds) => {
71
+ export const draftConcept = (graph, input, kinds, reserved = []) => {
60
72
  if (input.document === '')
61
73
  return null;
62
74
  if (!kinds.includes(input.kind))
@@ -64,7 +76,7 @@ export const draftConcept = (graph, input, kinds) => {
64
76
  const name = input.name.trim();
65
77
  if (name === '')
66
78
  return null;
67
- const id = proposeConceptId(graph, name);
79
+ const id = proposeConceptId(graph, name, reserved);
68
80
  if (id === null)
69
81
  return null;
70
82
  return {
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { parseDocument } from 'yaml';
5
5
  import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
6
6
  import { compileWorkspaceWithProfileContext, } from './compiler.js';
7
+ import { loadEvidence } from './evidence.js';
7
8
  import { evaluateCatalogue, loadQuestionCatalogue, renderQuestion, } from './interrogate-command.js';
8
9
  import { evaluateProjection } from './projection.js';
9
10
  import { renderBrief } from './brief.js';
@@ -202,6 +203,20 @@ export function runDesignCommand(options, cwd) {
202
203
  })));
203
204
  if (!compilation.ok)
204
205
  return failed(compilation.diagnostics);
206
+ // The evidence overlay rides along for the one condition that reads
207
+ // it (unchallenged-evidence). A workspace declaring no evidence
208
+ // passes an empty overlay — known to be empty, which keeps that
209
+ // condition quiet — rather than an absent one.
210
+ const evidenceObservations = [];
211
+ for (const path of workspace.evidence) {
212
+ const loadedEvidence = loadEvidence({
213
+ path,
214
+ source: readFileSync(resolve(cwd, path), 'utf8'),
215
+ });
216
+ if (!loadedEvidence.ok)
217
+ return failed(loadedEvidence.diagnostics);
218
+ evidenceObservations.push(...loadedEvidence.evidence.observations);
219
+ }
205
220
  if (subjectFilter !== undefined) {
206
221
  const known = new Set(compilation.graph.subjects.map(({ id }) => id));
207
222
  if (!known.has(subjectFilter)) {
@@ -212,7 +227,7 @@ export function runDesignCommand(options, cwd) {
212
227
  };
213
228
  }
214
229
  }
215
- const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
230
+ const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceObservations);
216
231
  const askPlainById = new Map(loadedCatalogue.catalogue.questions.flatMap((question) => question.askPlain === undefined
217
232
  ? []
218
233
  : [[question.id, question.askPlain]]));
@@ -267,9 +282,20 @@ export function runDesignCommand(options, cwd) {
267
282
  '',
268
283
  ];
269
284
  if (step === null) {
270
- lines.push(subjectFilter === undefined
271
- ? 'Interview complete: no open questions. The model answers everything the catalogue asks.'
272
- : `Interview complete for ${subjectFilter}: no open questions touch it.`);
285
+ // "No open questions" has two causes and only one of them is complete
286
+ // (#334). Every wave gated shut asks NOTHING, so a blank model reaches
287
+ // zero without a single question having been put - and claiming the
288
+ // model answers everything the catalogue asks is then flatly false
289
+ // about a catalogue that asked nothing. Completion inferred from an
290
+ // empty set is the same fault the wave rail and the report renderer
291
+ // each carried; this is the sentence an agent reads to decide it is
292
+ // done, so it is the worst place for it.
293
+ const asked = report.waves.some((wave) => wave.questions.length > 0);
294
+ lines.push(!asked
295
+ ? 'No wave has opened yet: this catalogue asks nothing until the model has substance. Declare a subject and run this again.'
296
+ : subjectFilter === undefined
297
+ ? 'Interview complete: no open questions. The model answers everything the catalogue asks.'
298
+ : `Interview complete for ${subjectFilter}: no open questions touch it.`);
273
299
  }
274
300
  else {
275
301
  // --facilitate prefers the workshop phrasing and falls back to the
package/dist/evidence.js CHANGED
@@ -27,6 +27,31 @@ export function loadEvidence(source) {
27
27
  ...(observation.key === undefined || observation.value === undefined
28
28
  ? {}
29
29
  : { key: observation.key, value: observation.value }),
30
+ // Provenance survives normalization: reconcile counts a
31
+ // not-observed naming no search (ADR 0107) and interrogation's
32
+ // unchallenged-evidence reads recorded searches (ADR 0120), so a
33
+ // load path that dropped them made both read every overlay as
34
+ // probe-free however carefully its author recorded one.
35
+ ...(observation.searched === undefined
36
+ ? {}
37
+ : {
38
+ searched: observation.searched.map((probe) => 'glob' in probe
39
+ ? { glob: probe.glob }
40
+ : {
41
+ grep: probe.grep,
42
+ ...(probe.paths === undefined
43
+ ? {}
44
+ : { paths: [...probe.paths] }),
45
+ }),
46
+ }),
47
+ ...(observation.measured === undefined
48
+ ? {}
49
+ : {
50
+ measured: observation.measured.map(({ value, method }) => ({
51
+ value,
52
+ method,
53
+ })),
54
+ }),
30
55
  evidence: {
31
56
  uri: observation.evidence.uri,
32
57
  ...(observation.evidence.message === undefined
@@ -14,6 +14,16 @@ export interface CanvasNode {
14
14
  * profile kind it was authored as.
15
15
  */
16
16
  readonly coreKindLabel: string;
17
+ /**
18
+ * The core relationship kinds this node's PATTERN ports, or `[]` where its
19
+ * kind has no pattern with ports (#268 phase 3, ADR 0124).
20
+ *
21
+ * A macro edge needs both ends to port its kind, so an editor offering a
22
+ * palette between two instances intersects the two lists. Two raw groupings
23
+ * permit ten of the eleven kinds, which is no guidance at all; this is what
24
+ * restores the narrowing the relationship table gives everywhere else.
25
+ */
26
+ readonly portKinds: readonly string[];
17
27
  readonly layer: Layer | null;
18
28
  readonly aspect: Aspect | null;
19
29
  readonly name: string;
@@ -119,6 +119,7 @@ const projectConcept = (subjectId, claims, profileContext) => {
119
119
  kind,
120
120
  kindLabel: kindLabelOf(kind),
121
121
  coreKindLabel: kindLabelOf(profileContext.conceptKindLineages.get(kind)?.[0] ?? kind),
122
+ portKinds: profileContext.patternPortKinds.get(kind) ?? [],
122
123
  layer: profileContext.conceptKindLayers.get(kind) ?? null,
123
124
  aspect: profileContext.conceptKindAspects.get(kind) ?? null,
124
125
  name: claimValue(nameClaim),
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, ty
15
15
  export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, ProjectionResult, } from './projection.js';
16
16
  export { createFileSystemStore, type PendingWrite, type SourceStore, type StoredSource, type WriteConflict, type WriteOutcome, } from './source-store.js';
17
17
  export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, type ApplyOutcome, } from './apply-command.js';
18
- export { connectableKinds, draftRelationship, proposeRelationshipId, } from './relationship-drafting.js';
18
+ export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
19
19
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
20
20
  export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
21
- export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
21
+ export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, 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
@@ -13,7 +13,7 @@ export { canonicalProjection, evaluateProjection, explainProjection, loadProject
13
13
  export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, } from './adapter-mapping.js';
14
14
  export { createFileSystemStore, } from './source-store.js';
15
15
  export { applyOperations, landOperations, posixDirectoryOf, } from './apply-command.js';
16
- export { connectableKinds, draftRelationship, proposeRelationshipId, } from './relationship-drafting.js';
16
+ export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
17
17
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
18
18
  export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
19
19
  export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
@@ -85,7 +85,28 @@ export type CatalogueCondition = {
85
85
  readonly condition: 'unconstrained-kind';
86
86
  } | {
87
87
  readonly condition: 'unscoped-succession';
88
+ } | {
89
+ readonly condition: 'unchallenged-evidence';
90
+ } | {
91
+ readonly condition: 'has-any-subject';
88
92
  };
93
+ /**
94
+ * One observation from the workspace's evidence overlay, reduced to what
95
+ * interrogation reads: the result, and whether a search was recorded with
96
+ * it. The only condition that reads the overlay is `unchallenged-evidence`;
97
+ * every other condition reads the compiled graph alone, and the overlay
98
+ * never influences which subjects a selector matches.
99
+ *
100
+ * Shaped structurally rather than importing {@link EvidenceObservation} so
101
+ * the pure engine entry (`./interrogation-entry`) keeps owning its whole
102
+ * input surface: a caller passes
103
+ * `evidenceDocuments.flatMap(({ observations }) => observations)` and the
104
+ * wider evidence shape is never dragged in.
105
+ */
106
+ export interface CatalogueEvidenceObservation {
107
+ readonly result: 'confirmed' | 'contradicted' | 'unknown' | 'not-observed';
108
+ readonly searched?: readonly unknown[];
109
+ }
89
110
  export interface CatalogueQuestion {
90
111
  readonly id: string;
91
112
  readonly wave: string;
@@ -112,6 +133,11 @@ export interface QuestionCatalogue {
112
133
  readonly id: string;
113
134
  readonly name: string;
114
135
  readonly description?: string;
136
+ /**
137
+ * Conditions that must all hold before this wave opens (#334, ADR 0125).
138
+ * Absent means always open, which is what every wave did before this.
139
+ */
140
+ readonly opensWhen?: readonly CatalogueCondition[];
115
141
  }[];
116
142
  readonly questions: readonly CatalogueQuestion[];
117
143
  }
@@ -142,6 +168,16 @@ export interface ReportQuestion {
142
168
  export interface ReportWave {
143
169
  readonly id: string;
144
170
  readonly name: string;
171
+ /**
172
+ * Whether the wave's gate is met (#334, ADR 0125). A wave with no
173
+ * `opensWhen` is always open.
174
+ *
175
+ * A wave reported `false` carries NO questions and contributes nothing to
176
+ * the summary. Its questions are premature rather than answered, and a
177
+ * progress rail that counted them as answered would flatter itself exactly
178
+ * where someone is most likely to trust it.
179
+ */
180
+ readonly opened: boolean;
145
181
  readonly questions: readonly ReportQuestion[];
146
182
  }
147
183
  export interface InterrogationSummary {
@@ -159,7 +195,7 @@ export interface InterrogationReport {
159
195
  readonly waves: readonly ReportWave[];
160
196
  }
161
197
  export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string;
162
- export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext): Omit<InterrogationReport, 'workspace'>;
198
+ export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext, evidence?: readonly CatalogueEvidenceObservation[]): Omit<InterrogationReport, 'workspace'>;
163
199
  export type CatalogueLoadResult = {
164
200
  readonly ok: true;
165
201
  readonly catalogue: QuestionCatalogue;
@@ -236,8 +236,34 @@ const linkageHits = (index, condition, subjectId, profileContext) => {
236
236
  return counterparts.some((counterpart) => kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
237
237
  });
238
238
  };
239
- const conditionHolds = (index, condition, subjectId, profileContext) => {
239
+ const conditionHolds = (index, condition, subjectId, profileContext, evidence) => {
240
240
  switch (condition.condition) {
241
+ case 'has-any-subject':
242
+ // The guard a late wave needs to say "only once the model has
243
+ // substance" (#334). An empty model is not an architecture at rest -
244
+ // ADR 0120's reading, which stays true for a model that HAS started -
245
+ // it has not begun, and asking it how the planned architecture becomes
246
+ // real greets someone ahead of question one.
247
+ return index.concepts.size > 0;
248
+ case 'unchallenged-evidence':
249
+ // Fires where the overlay records observations and every one is a
250
+ // frictionless confirmation: no contradicted, unknown, or
251
+ // not-observed result, and no recorded search. A discovery that
252
+ // never records anything but success never tested a claim it might
253
+ // fail — 39 of 39 GitLab observations said confirmed while Praefect
254
+ // sat declared upstream and absent from the tree (#272). A recorded
255
+ // search closes it even on a confirmed result, because a
256
+ // confirmation of a negative claim rests on exactly the empty
257
+ // search ADR 0107 made auditable; so does any honest non-confirmed
258
+ // result. An empty overlay stays quiet: with no observations there
259
+ // is no inspection to interrogate. An absent overlay also stays
260
+ // quiet — the caller did not supply one, so its diversity is
261
+ // unknown, not absent, the same rule `unconstrained-kind` applies
262
+ // to a missing profile context.
263
+ return (evidence !== undefined &&
264
+ evidence.length > 0 &&
265
+ evidence.every(({ result, searched }) => result === 'confirmed' &&
266
+ (searched === undefined || searched.length === 0)));
241
267
  case 'missing-claim':
242
268
  return !(index.claimsBySubject.get(subjectId) ?? []).some(({ predicate }) => predicate === condition.predicate);
243
269
  case 'unscoped-succession': {
@@ -425,61 +451,76 @@ const describeCounterparts = (index, question, subjectId) => {
425
451
  return name === undefined ? id : `${name} (${id})`;
426
452
  });
427
453
  };
428
- export function evaluateCatalogue(catalogue, graph, profileContext) {
454
+ export function evaluateCatalogue(catalogue, graph, profileContext, evidence) {
429
455
  const index = indexGraph(graph);
430
456
  let open = 0;
431
457
  let openQuestions = 0;
432
458
  const applicableQuestions = catalogue.questions.filter((question) => questionIsApplicable(question, graph.profiles));
459
+ const waveOpens = (wave) => wave.opensWhen === undefined ||
460
+ wave.opensWhen.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence));
433
461
  const waves = catalogue.waves.map((wave) => ({
434
462
  id: wave.id,
435
463
  name: wave.name,
436
- questions: applicableQuestions
437
- .filter((question) => question.wave === wave.id)
438
- .map((question) => {
439
- const base = {
440
- id: question.id,
441
- scope: question.scope,
442
- authority: question.authority,
443
- question: question.question.trim(),
444
- materiality: question.materiality.trim(),
445
- resolution: question.resolution.trim(),
446
- trigger: question.trigger,
447
- ...(question.since === undefined ? {} : { since: question.since }),
448
- };
449
- if (question.scope === 'workspace') {
450
- const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext));
451
- if (isOpen) {
452
- open += 1;
453
- openQuestions += 1;
464
+ opened: waveOpens(wave),
465
+ // A closed wave asks nothing. Its questions are not evaluated at all,
466
+ // rather than evaluated and reported closed - the latter would say they
467
+ // had been asked and answered - so they reach neither the report nor the
468
+ // summary.
469
+ questions: !waveOpens(wave)
470
+ ? []
471
+ : applicableQuestions
472
+ .filter((question) => question.wave === wave.id)
473
+ .map((question) => {
474
+ const base = {
475
+ id: question.id,
476
+ scope: question.scope,
477
+ authority: question.authority,
478
+ question: question.question.trim(),
479
+ materiality: question.materiality.trim(),
480
+ resolution: question.resolution.trim(),
481
+ trigger: question.trigger,
482
+ ...(question.since === undefined ? {} : { since: question.since }),
483
+ };
484
+ if (question.scope === 'workspace') {
485
+ const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence));
486
+ if (isOpen) {
487
+ open += 1;
488
+ openQuestions += 1;
489
+ }
490
+ return { ...base, open: isOpen };
454
491
  }
455
- return { ...base, open: isOpen };
456
- }
457
- const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext)));
458
- if (matches.length === 0) {
459
- return { ...base, open: false };
460
- }
461
- open += matches.length;
462
- openQuestions += 1;
463
- return {
464
- ...base,
465
- open: true,
466
- subjects: matches.map((id) => {
467
- const name = index.nameOf.get(id);
468
- return {
469
- id,
470
- ...(name === undefined ? {} : { name }),
471
- question: renderQuestion(question.question, id, name, describeCounterparts(index, question, id)),
472
- };
473
- }),
474
- };
475
- }),
492
+ const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext, evidence)));
493
+ if (matches.length === 0) {
494
+ return { ...base, open: false };
495
+ }
496
+ open += matches.length;
497
+ openQuestions += 1;
498
+ return {
499
+ ...base,
500
+ open: true,
501
+ subjects: matches.map((id) => {
502
+ const name = index.nameOf.get(id);
503
+ return {
504
+ id,
505
+ ...(name === undefined ? {} : { name }),
506
+ question: renderQuestion(question.question, id, name, describeCounterparts(index, question, id)),
507
+ };
508
+ }),
509
+ };
510
+ }),
476
511
  }));
477
512
  return {
478
513
  format: 'yarramate/interrogation-report/v1',
479
514
  catalogue: `${catalogue.id}@${catalogue.version}`,
480
515
  semantics: INTERROGATION_SEMANTICS_VERSION,
481
516
  summary: {
482
- questions: applicableQuestions.length,
517
+ // Questions in OPENED waves only (#334, ADR 0125). A closed wave's
518
+ // questions have not been asked, so counting them in the denominator
519
+ // would report them as answered - "3 of 51" reading as forty-eight
520
+ // done when forty-eight were never put. The denominator grows as the
521
+ // model gains substance and waves open, which is the interview
522
+ // revealing itself rather than a rail filling up.
523
+ questions: waves.reduce((total, wave) => total + wave.questions.length, 0),
483
524
  openQuestions,
484
525
  open,
485
526
  },
@@ -519,6 +560,16 @@ export function renderInterrogationReport(report) {
519
560
  ];
520
561
  for (const wave of report.waves) {
521
562
  lines.push('', `== ${wave.name} ==`);
563
+ // A wave that has not opened must not read like one whose questions are
564
+ // all closed (#334). Both carry no OPEN questions, and a bare heading with
565
+ // nothing under it is the more flattering of the two readings: "nothing
566
+ // outstanding here" rather than "nobody has been asked anything here".
567
+ // The same shape - completion inferred from an empty set - was found in a
568
+ // consuming product's wave rail on the same day.
569
+ if (!wave.opened) {
570
+ lines.push(' not yet — this wave has not opened');
571
+ continue;
572
+ }
522
573
  for (const question of wave.questions) {
523
574
  if (!question.open) {
524
575
  lines.push(` closed ${question.id}`);
@@ -1 +1 @@
1
- export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
1
+ export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Which way a view runs its layers, kept in a module that imports nothing.
3
+ *
4
+ * Here rather than in `projection.ts` for the reason `./nesting.ts` gives: the
5
+ * browser needs the value and not only the type, and `projection.ts` drags Ajv
6
+ * and the projection schema in for one constant. `projection.ts` re-exports
7
+ * both.
8
+ */
9
+ export type LayoutDirection = 'top-down' | 'left-right';
10
+ /**
11
+ * How a view runs when it does not say. Top-down is the behaviour that
12
+ * shipped, and it is what ArchiMate's layer bands read as (ADR 0121).
13
+ */
14
+ export declare const DEFAULT_DIRECTION: LayoutDirection;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * How a view runs when it does not say. Top-down is the behaviour that
3
+ * shipped, and it is what ArchiMate's layer bands read as (ADR 0121).
4
+ */
5
+ export const DEFAULT_DIRECTION = 'top-down';
@@ -6,6 +6,16 @@ export interface ProjectionDefinition {
6
6
  readonly version: string;
7
7
  readonly query: {
8
8
  readonly subjects?: readonly string[];
9
+ /**
10
+ * Subjects this query would otherwise select and the author has taken out
11
+ * (#267, ADR 0122). A facet view states a rule, and every interesting rule
12
+ * has an exception someone would rather state than abandon the rule for;
13
+ * this is where that exception is written down instead of being the silent
14
+ * absence a hand-enumerated list produces. Applied after every other facet
15
+ * AND after `relationships: connected` expansion, so an excluded subject is
16
+ * out whichever way it would have come back in.
17
+ */
18
+ readonly exclude?: readonly string[];
9
19
  readonly documents?: readonly string[];
10
20
  readonly kinds?: readonly string[];
11
21
  readonly layers?: readonly string[];
@@ -24,12 +34,12 @@ export interface ProjectionDefinition {
24
34
  readonly description?: string;
25
35
  readonly layout?: 'layered';
26
36
  /**
27
- * Read by the LikeC4 export for its `autoLayout`, and by nothing else. The
28
- * canvas draws ArchiMate, whose layer bands only read top-down, so it
29
- * ignores this rather than offering a control that would tilt the bands
30
- * away from what they mean.
37
+ * Which way this view runs its layers. Read by the LikeC4 export for its
38
+ * `autoLayout` and by the canvas for ELK's `elk.direction` (ADR 0121); a
39
+ * view that says nothing runs `top-down`, which is what ArchiMate's layer
40
+ * bands read as.
31
41
  */
32
- readonly direction?: 'top-down' | 'left-right';
42
+ readonly direction?: LayoutDirection;
33
43
  /**
34
44
  * The relationship kinds that draw as nesting in this view, in precedence
35
45
  * order (ADR 0101). Absent means `['composition']`, which is the behaviour
@@ -64,6 +74,12 @@ export interface ProjectionDefinition {
64
74
  */
65
75
  export { DEFAULT_NESTING, type NestingKind } from './nesting.js';
66
76
  import type { NestingKind } from './nesting.js';
77
+ /**
78
+ * Which way a view runs, and the default. Split out for the same reason as the
79
+ * nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
80
+ */
81
+ export { DEFAULT_DIRECTION, type LayoutDirection } from './layout-direction.js';
82
+ import type { LayoutDirection } from './layout-direction.js';
67
83
  export type ProjectionQuery = ProjectionDefinition['query'];
68
84
  export interface ProjectionResult {
69
85
  readonly format: 'yarramate/projection-result/v1';
@@ -86,7 +102,7 @@ export declare function canonicalProjection(projection: ProjectionDefinition): P
86
102
  * A facet of a query, named the way the query names it. What
87
103
  * {@link explainProjection} reports as the reason a subject is not in a view.
88
104
  */
89
- export type ConceptFacet = 'states' | 'subjects' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
105
+ export type ConceptFacet = 'exclude' | 'states' | 'subjects' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
90
106
  /** One subject a query dropped, and the facet that dropped it. */
91
107
  export interface ProjectionExclusion {
92
108
  readonly id: string;
@@ -14,6 +14,11 @@ const validateProjection = new Ajv2020({ allErrors: true }).compile(projectionSc
14
14
  * and a schema, which is a great deal of bundle for one constant (ADR 0101).
15
15
  */
16
16
  export { DEFAULT_NESTING } from './nesting.js';
17
+ /**
18
+ * Which way a view runs, and the default. Split out for the same reason as the
19
+ * nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
20
+ */
21
+ export { DEFAULT_DIRECTION } from './layout-direction.js';
17
22
  export function loadProjection(source) {
18
23
  const loaded = loadSourceDocument(source, validateProjection, 'Projection');
19
24
  return loaded.ok
@@ -124,7 +129,13 @@ const conceptSelector = (graph, projection, profileContext) => {
124
129
  ? undefined
125
130
  : profileContext?.conceptKindLayers.get(kind);
126
131
  // Ordered the way a query declares its facets, so "the first reason" is
127
- // the one a reader would reach first themselves.
132
+ // the one a reader would reach first themselves - except the explicit
133
+ // exception, which outranks every rule: when someone has written the
134
+ // subject down as taken out, that IS the first reason, whatever else
135
+ // would also have dropped it (#267).
136
+ if (query.exclude?.includes(id) === true) {
137
+ return 'exclude';
138
+ }
128
139
  if (query.states !== undefined &&
129
140
  (architectureStateIds.has(id) || !participatesInSelectedState(id))) {
130
141
  return 'states';
@@ -201,6 +212,12 @@ export function explainProjection(graph, projection, profileContext) {
201
212
  export function evaluateProjection(graph, projection, profileContext) {
202
213
  const { droppedBy, architectureStateIds, participatesInSelectedState } = conceptSelector(graph, projection, profileContext);
203
214
  const endpointExcluded = (id) => {
215
+ // An exclusion is final (#267, ADR 0122). Dropping the subject from the
216
+ // initial selection alone would not be: `relationships: connected` adds
217
+ // the far end of every relationship it draws, so an excluded subject would
218
+ // walk back in by the other end of a relationship to one that stayed.
219
+ if (projection.query.exclude?.includes(id) === true)
220
+ return true;
204
221
  if (!participatesInSelectedState(id))
205
222
  return true;
206
223
  const status = claimValue(graph.claims, id, 'yarramate/lifecycle/status');
@@ -221,7 +238,11 @@ export function evaluateProjection(graph, projection, profileContext) {
221
238
  continue;
222
239
  const relationship = graph.claims.find((claim) => claim.id === subject.id);
223
240
  const relationshipStatus = claimValue(graph.claims, subject.id, 'yarramate/lifecycle/status');
224
- if (relationship === undefined ||
241
+ if (
242
+ // A relationship can be taken out by name too: `exclude` names
243
+ // subjects, and a relationship is a subject.
244
+ projection.query.exclude?.includes(subject.id) === true ||
245
+ relationship === undefined ||
225
246
  !('ref' in relationship.object) ||
226
247
  (projection.query.excludeStatuses !== undefined &&
227
248
  relationshipStatus !== undefined &&
@@ -33,8 +33,16 @@ export declare const connectableKinds: (graph: CanvasGraph, fromId: string, toId
33
33
  * relationship kind is a single lowercase word. A collision takes a numeric
34
34
  * suffix rather than a hash, because the id is authored text a human will read
35
35
  * in a diff.
36
+ *
37
+ * `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
38
+ * draft never enters the rendered graph, so without it a second relationship
39
+ * between the same pair re-proposed the identical id and the editor's
40
+ * replace-by-target staging silently swallowed the first (#306). The schema
41
+ * places no uniqueness on the (from, kind, to) triple - parallel relationships
42
+ * with distinct ids compile cleanly - so the id proposal is the only place the
43
+ * collision can be stepped past.
36
44
  */
37
- export declare const proposeRelationshipId: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string) => string;
45
+ export declare const proposeRelationshipId: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string, reserved?: Iterable<string>) => string;
38
46
  /**
39
47
  * The operation that lands a drafted relationship, or `null` when the draft is
40
48
  * one the table does not permit.
@@ -47,5 +55,16 @@ export declare const proposeRelationshipId: (graph: CanvasGraph, fromId: string,
47
55
  * relationship has to live somewhere, both endpoints are equally defensible,
48
56
  * and the source is where a reader looking for what this thing does would go
49
57
  * first.
58
+ *
59
+ * A caller holding drafts the graph has not landed yet - an editor with a
60
+ * pending changeset - passes their ids as `reserved`, so a second parallel
61
+ * relationship steps to `-2` instead of colliding with the first (#306).
62
+ */
63
+ export declare const draftRelationship: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string, reserved?: Iterable<string>) => YarramateOperation | null;
64
+ /**
65
+ * The ids a pending changeset already claims, for `proposeRelationshipId`'s
66
+ * `reserved` parameter. Every operation that names a subject id reserves it -
67
+ * an update's id is already in the graph and reserving it twice is harmless,
68
+ * while an add's id is exactly the one the graph cannot know yet.
50
69
  */
51
- export declare const draftRelationship: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string) => YarramateOperation | null;
70
+ export declare const stagedSubjectIds: (operations: readonly YarramateOperation[]) => readonly string[];