yarramate 1.1.0 → 1.3.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 (70) hide show
  1. package/catalogues/core-enrichment.yaml +225 -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 +23 -3
  7. package/dist/adapters/visual/workspace-model.js +52 -2
  8. package/dist/ask-command.js +29 -13
  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/concept-drafting.d.ts +13 -2
  14. package/dist/concept-drafting.js +15 -3
  15. package/dist/design-command.js +83 -2
  16. package/dist/evidence.js +25 -0
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +1 -1
  19. package/dist/interrogate-command.d.ts +28 -1
  20. package/dist/interrogate-command.js +24 -4
  21. package/dist/interrogation-entry.d.ts +1 -1
  22. package/dist/layout-direction.d.ts +14 -0
  23. package/dist/layout-direction.js +5 -0
  24. package/dist/projection.d.ts +22 -6
  25. package/dist/projection.js +23 -2
  26. package/dist/relationship-drafting.d.ts +21 -2
  27. package/dist/relationship-drafting.js +33 -3
  28. package/dist/subject-references.js +5 -0
  29. package/dist/visual-app/assets/index-C3i9SxTe.js +394 -0
  30. package/dist/visual-app/assets/index-Rkq6smL2.css +1 -0
  31. package/dist/visual-app/index.html +2 -2
  32. package/dist/visual-app-lib/editor.js +28464 -26953
  33. package/dist/visual-app-lib/styles.css +1 -1
  34. package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +1 -1
  35. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +30 -0
  36. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +23 -3
  37. package/dist/visual-app-lib/types/concept-drafting.d.ts +13 -2
  38. package/dist/visual-app-lib/types/interrogate-command.d.ts +190 -0
  39. package/dist/visual-app-lib/types/layout-direction.d.ts +14 -0
  40. package/dist/visual-app-lib/types/projection.d.ts +22 -6
  41. package/dist/visual-app-lib/types/relationship-drafting.d.ts +21 -2
  42. package/dist/visual-app-lib/types/subject-identity.d.ts +34 -0
  43. package/dist/visual-app-lib/types/visual-app/App.d.ts +27 -2
  44. package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
  45. package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +9 -1
  46. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +15 -6
  47. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +77 -6
  48. package/dist/visual-app-lib/types/visual-app/kind-palette.d.ts +37 -0
  49. package/dist/visual-app-lib/types/visual-app/mount.d.ts +73 -2
  50. package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +15 -0
  51. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
  52. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +8 -1
  53. package/dist/visual-app-lib/types/visual-app/save-view.d.ts +11 -5
  54. package/dist/visual-app-lib/types/visual-app/shipped-catalogue.d.ts +10 -0
  55. package/dist/visual-app-lib/types/visual-app/state.d.ts +15 -4
  56. package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +13 -1
  57. package/dist/visual-app-lib/types/visual-app/subject-filter.d.ts +35 -0
  58. package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +8 -0
  59. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +42 -9
  60. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +11 -2
  61. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +81 -11
  62. package/docs/CONSUMING-YARRAMATE.md +51 -0
  63. package/package.json +1 -1
  64. package/schema/yarramate-design-step.schema.json +395 -1
  65. package/schema/yarramate-interrogation-report.schema.json +488 -23
  66. package/schema/yarramate-projection.schema.json +9 -0
  67. package/schema/yarramate-question-catalogue.schema.json +13 -0
  68. package/skills/yarramate-architecture/SKILL.md +10 -1
  69. package/dist/visual-app/assets/index-CcmfL3oY.js +0 -394
  70. package/dist/visual-app/assets/index-DLutIWES.css +0 -1
@@ -1,6 +1,6 @@
1
1
  format: yarramate/question-catalogue/v1
2
2
  id: core-enrichment
3
- version: "1.1"
3
+ version: "1.2"
4
4
  profile: yarramate/core@0.1
5
5
  presentation:
6
6
  title: Core enrichment interview
@@ -880,6 +880,67 @@ questions:
880
880
  Add the externally meaningful services and serve them to their
881
881
  consumers.
882
882
 
883
+ - id: no-capability-declared
884
+ wave: business
885
+ since: "1.2"
886
+ scope: workspace
887
+ trigger:
888
+ - condition: no-subject-of-kind
889
+ kinds:
890
+ - yarramate/core@0.1#capability
891
+ question: >-
892
+ No capability is declared. What can this system do, in its own terms?
893
+ askPlain: >-
894
+ Forget the parts list for a moment: what are the handful of things
895
+ this system is actually able to do for the people it serves?
896
+ materiality: >-
897
+ Capabilities name what the system can do apart from how it currently
898
+ does it; without them, investment and sourcing trade-offs can only be
899
+ argued in component names, and nothing in the model can say which
900
+ parts serve the same ability. A subject-driven interview never asks
901
+ about a layer with zero subjects, so an absent capability map reads
902
+ as a covered one until this question opens it.
903
+ authority: human
904
+ resolution: >-
905
+ Add the small set of capabilities this system provides — four to
906
+ seven usually carries the whole conversation — and relate each to
907
+ the services or components that realize it with realization.
908
+
909
+ - id: no-contract-declared
910
+ wave: business
911
+ since: "1.2"
912
+ scope: workspace
913
+ trigger:
914
+ - condition: exists-linkage
915
+ kinds:
916
+ - yarramate/core@0.1#serving
917
+ - yarramate/core@0.1#flow
918
+ - yarramate/core@0.1#triggering
919
+ direction: either
920
+ counterpartKinds:
921
+ - yarramate/core@0.1#applicationComponent
922
+ - yarramate/core@0.1#applicationInterface
923
+ - yarramate/core@0.1#applicationService
924
+ - yarramate/core@0.1#businessActor
925
+ - condition: no-subject-of-kind
926
+ kinds:
927
+ - yarramate/core@0.1#contract
928
+ question: >-
929
+ Interactions exist and no contract governs any of them. Which
930
+ agreements bind what this architecture exchanges?
931
+ askPlain: >-
932
+ Things here talk to each other and to the outside. Where is it
933
+ written down what each side may expect — the API description, the
934
+ published schema, the service agreement?
935
+ materiality: >-
936
+ A hop with no contract is renegotiated by every implementer who
937
+ touches it; compatibility and versioning obligations exist only
938
+ where the agreement is a subject someone can change deliberately.
939
+ authority: human
940
+ resolution: >-
941
+ Add a contract for each formal agreement and access it from the
942
+ behavior bound by it; realization from the data object or artifact
943
+ that embodies the agreement says where it lives.
883
944
 
884
945
  - id: service-realizes-no-motivation
885
946
  wave: business
@@ -1063,6 +1124,38 @@ questions:
1063
1124
  not perform application behavior: assign the actor to the business
1064
1125
  process that uses it and let the application service serve that
1065
1126
  process.
1127
+ - id: no-event-declared
1128
+ wave: application
1129
+ since: "1.2"
1130
+ scope: workspace
1131
+ trigger:
1132
+ - condition: no-subject-of-kind
1133
+ kinds:
1134
+ - yarramate/core@0.1#applicationEvent
1135
+ - yarramate/core@0.1#businessEvent
1136
+ question: >-
1137
+ Nothing in this model reacts to an event. Does state change only
1138
+ when something is called, or are there happenings the architecture
1139
+ responds to?
1140
+ askPlain: >-
1141
+ Is everything here a direct request, or do things also happen on
1142
+ their own — a message arrives, a job finishes, a threshold is
1143
+ crossed — that the system has to react to?
1144
+ materiality: >-
1145
+ Whether change propagates by call or by event decides coupling,
1146
+ ordering, and failure isolation. An event-driven seam modelled as
1147
+ silence leaves every consumer to discover the event stream on its
1148
+ own, and a repository full of event definitions the interview never
1149
+ asked about reads as covered when it is absent.
1150
+ authority: human
1151
+ resolution: >-
1152
+ Add the application or business events the system emits or responds
1153
+ to and wire each with triggering from what raises it to what
1154
+ responds. If there are genuinely none, that is a coupling decision
1155
+ worth stating in a principle or a service description; this question
1156
+ then stays on the agenda as the standing record that nobody has
1157
+ declared one.
1158
+
1066
1159
  - id: event-triggers-nothing
1067
1160
  wave: application
1068
1161
  since: "0.3"
@@ -1232,6 +1325,31 @@ questions:
1232
1325
  Add realization from the node, system software, or technology
1233
1326
  behavior that provides the service.
1234
1327
 
1328
+ - id: no-artifact-declared
1329
+ wave: technology
1330
+ since: "1.2"
1331
+ scope: workspace
1332
+ trigger:
1333
+ - condition: no-subject-of-kind
1334
+ kinds:
1335
+ - yarramate/core@0.1#artifact
1336
+ question: >-
1337
+ No artifact is declared. What do the declared components actually
1338
+ ship as?
1339
+ askPlain: >-
1340
+ When this system is deployed, what is the thing that moves — an
1341
+ image, a package, a bundle? What are those called?
1342
+ materiality: >-
1343
+ Release engineering starts where a build output becomes a subject.
1344
+ An architecture with components but no artifacts can say what should
1345
+ exist and nothing about what is deployed, so drift between built and
1346
+ declared has nowhere to register.
1347
+ authority: human
1348
+ resolution: >-
1349
+ Add the artifacts the build produces, assignment from the node that
1350
+ deploys each one, and realization from the artifact to the component
1351
+ or data object it materializes.
1352
+
1235
1353
  - id: artifact-unassigned
1236
1354
  wave: technology
1237
1355
  since: "0.4"
@@ -1258,6 +1376,37 @@ questions:
1258
1376
  realization from the artifact to the component or data object it
1259
1377
  materializes.
1260
1378
  # ---- implementation ------------------------------------------------------
1379
+ - id: implementation-path-missing
1380
+ wave: implementation
1381
+ since: "1.2"
1382
+ scope: workspace
1383
+ trigger:
1384
+ - condition: no-subject-of-kind
1385
+ kinds:
1386
+ - yarramate/core@0.1#workPackage
1387
+ - yarramate/core@0.1#deliverable
1388
+ - yarramate/core@0.1#plateau
1389
+ question: >-
1390
+ No work package, deliverable, or plateau is declared. How does the
1391
+ planned architecture become real?
1392
+ askPlain: >-
1393
+ Who is doing what to get from here to there? What are the packages
1394
+ of work, and what does each one hand over?
1395
+ materiality: >-
1396
+ A model that names a target but no work reaching it is a wish with
1397
+ an architecture diagram; committed work packages and their
1398
+ deliverables are what make progress reviewable rather than
1399
+ reported. A repository mid-migration that models none of the
1400
+ migration is the sharpest form of the gap: the change is in the
1401
+ tree and invisible in the model.
1402
+ authority: human
1403
+ resolution: >-
1404
+ Add the work packages in flight, realization to the deliverables
1405
+ each produces, and plateaus where an intermediate state needs a
1406
+ name. An architecture genuinely at rest keeps this question open,
1407
+ which is itself information: the model is saying nothing is
1408
+ changing.
1409
+
1261
1410
  - id: workpackage-delivers-nothing
1262
1411
  wave: implementation
1263
1412
  since: "0.4"
@@ -1426,6 +1575,36 @@ questions:
1426
1575
  resolution: >-
1427
1576
  Add a description claim stating meaning and explicit exclusions.
1428
1577
 
1578
+ - id: capability-uncited
1579
+ wave: hygiene
1580
+ since: "1.2"
1581
+ scope: subject
1582
+ subjects:
1583
+ kinds:
1584
+ - yarramate/core@0.1#capability
1585
+ trigger:
1586
+ - condition: missing-reference
1587
+ predicate: yarramate/reference/refers-to
1588
+ direction: outgoing
1589
+ question: >-
1590
+ {subject.name} cites nothing. Where is this ability specified,
1591
+ decided, or documented?
1592
+ askPlain: >-
1593
+ If someone asked where it says the system can do
1594
+ "{subject.name}", what would you point at?
1595
+ materiality: >-
1596
+ A capability with no citation floats free of every record that
1597
+ could confirm or correct it: an audit has nothing to grade the
1598
+ claim against, and when its scope is disputed the dispute starts
1599
+ from memory. The reference is where the model meets the
1600
+ repository's own account of itself.
1601
+ authority: either
1602
+ resolution: >-
1603
+ Add a references entry from the capability to the subject that
1604
+ specifies it — the decision record, the published schema, the
1605
+ guide that documents the journey. Model that record as a subject
1606
+ first where it is not one yet.
1607
+
1429
1608
  - id: status-missing
1430
1609
  wave: hygiene
1431
1610
  since: "0.1"
@@ -1479,6 +1658,12 @@ questions:
1479
1658
  genuinely different, say so in the model: add the counterpart's id to
1480
1659
  this subject's distinctFrom list. That answer is itself a claim, so it
1481
1660
  closes the question permanently and survives re-running the interview.
1661
+ A third answer is ordinary in a repository mid-migration: they are
1662
+ different subjects and one is taking over from the other. Record the
1663
+ succession with supersedes, scoped where the takeover is partial
1664
+ (ADR 0109), and record distinctFrom alongside it. Succession says what
1665
+ the pair is to each other and distinctness is what this question asks,
1666
+ so only the second closes it.
1482
1667
 
1483
1668
  - id: states-undefined
1484
1669
  wave: hygiene
@@ -1489,13 +1674,24 @@ questions:
1489
1674
  question: >-
1490
1675
  Does change shape matter here — should baseline, transition, or target
1491
1676
  states be declared?
1677
+ askPlain: >-
1678
+ Are we describing how things are, how they should become, or both?
1679
+ If the repository already carries a migration plan or a target
1680
+ design, that document is the answer.
1492
1681
  materiality: >-
1493
1682
  Without architecture states, current and target intent share one
1494
- undifferentiated model and comparisons are impossible.
1683
+ undifferentiated model and comparisons are impossible. The answer is
1684
+ often already on the record: a repository that carries its own
1685
+ migration plan or target design in-tree has declared that change
1686
+ shape matters, so evidence can bring this question its answer rather
1687
+ than waiting for someone to remember it.
1495
1688
  authority: human
1496
1689
  resolution: >-
1497
1690
  Declare architecture states and mark presence with present-in where the
1498
1691
  distinction carries decisions; explicitly decline states otherwise.
1692
+ Where in-tree documents already describe the target, model that
1693
+ record and cite it with references from the subjects it changes, so
1694
+ the declared states stand on evidence a reviewer can open.
1499
1695
 
1500
1696
  - id: kind-untested
1501
1697
  wave: hygiene
@@ -1580,3 +1776,30 @@ questions:
1580
1776
  which says the same thing without a qualifier. If the two subjects
1581
1777
  simply coexist and neither takes over from the other, the succession
1582
1778
  is the wrong claim; remove it.
1779
+
1780
+ - id: evidence-unchallenged
1781
+ wave: hygiene
1782
+ since: "1.2"
1783
+ scope: workspace
1784
+ trigger:
1785
+ - condition: unchallenged-evidence
1786
+ question: >-
1787
+ Every observation this workspace records is a frictionless
1788
+ confirmation. Did the inspection ever test a claim it might fail?
1789
+ askPlain: >-
1790
+ The evidence agrees with the model everywhere. Did we ever look
1791
+ for something that might not be there — a documented piece missing
1792
+ from the tree, a declared dependency nothing carries?
1793
+ materiality: >-
1794
+ An overlay that only ever says confirmed is indistinguishable from
1795
+ one that only looked where success was guaranteed. One recorded
1796
+ search or one honest non-confirmation is what makes the agreement
1797
+ between model and reality worth believing; a reconcile summary of
1798
+ pure confirmations has never put a claim at risk.
1799
+ authority: either
1800
+ resolution: >-
1801
+ Probe at least one claim the sources assert that the tree might not
1802
+ honour, and record the observation with its honest result. A
1803
+ not-observed carries the searches that came back empty (ADR 0107);
1804
+ a confirmed negative claim carries them too, because the empty
1805
+ search is the confirmation. Either closes this.
@@ -109,6 +109,21 @@ const LIMIT_FREEZE = {
109
109
  };
110
110
  /** Built browser application, beside the compiled adapter in `dist`. */
111
111
  const DEFAULT_ASSET_ROOT = fileURLToPath(new URL("../../visual-app/", import.meta.url));
112
+ /**
113
+ * The shipped question catalogue, read once per process for the model's
114
+ * interrogation overlay (#292). The same package-relative hop `design`
115
+ * makes; `undefined` when unreadable, which ships models with no overlay
116
+ * rather than failing the session over a garnish.
117
+ */
118
+ const shippedCatalogue = (() => {
119
+ const path = fileURLToPath(new URL("../../../catalogues/core-enrichment.yaml", import.meta.url));
120
+ try {
121
+ return { path, source: readFileSync(path, "utf8") };
122
+ }
123
+ catch {
124
+ return undefined;
125
+ }
126
+ })();
112
127
  /**
113
128
  * Takes one session terminal, whatever caused it: a reviewer's End, a child
114
129
  * that failed, a browser that never came back, a cancelling main agent, or a
@@ -550,7 +565,7 @@ export const startVisualServer = async (options) => {
550
565
  // that. A staged view operation pins against these (ADR 0103), and a
551
566
  // projection missing from the map is one the commit will create.
552
567
  projectionDigests: projectionDigestsNow(),
553
- });
568
+ }, shippedCatalogue);
554
569
  // Closures below retain this array, so refresh its contents without
555
570
  // replacing the identity the session started with.
556
571
  views.splice(0, views.length, ...workspaceModel.views);
@@ -59,7 +59,7 @@ export declare const declaredFolder: (view: {
59
59
  *
60
60
  * Every other presentation field the view declared is carried through by
61
61
  * `composeProjection`, so a rename cannot quietly drop a nesting vocabulary or
62
- * a direction the canvas never showed.
62
+ * the direction the view runs.
63
63
  */
64
64
  export declare const renameView: (view: SavedView, title: string) => {
65
65
  readonly path: string;
@@ -71,7 +71,7 @@ export const declaredFolder = (view) => view.presentation?.folder ?? "";
71
71
  *
72
72
  * Every other presentation field the view declared is carried through by
73
73
  * `composeProjection`, so a rename cannot quietly drop a nesting vocabulary or
74
- * a direction the canvas never showed.
74
+ * the direction the view runs.
75
75
  */
76
76
  export const renameView = (view, title) => ({
77
77
  path: view.path,
@@ -137,8 +137,9 @@ export const enumeratesSubjects = (query) => query.subjects !== undefined;
137
137
  * it was is a row the reviewer has to read and discard for nothing.
138
138
  */
139
139
  export const withMembership = (projection, subjectId, membership) => {
140
- if (!enumeratesSubjects(projection.query))
141
- return null;
140
+ if (!enumeratesSubjects(projection.query)) {
141
+ return withExclusion(projection, subjectId, membership);
142
+ }
142
143
  const subjects = projection.query.subjects;
143
144
  const holds = subjects.includes(subjectId);
144
145
  if (membership === "add" ? holds : !holds)
@@ -155,6 +156,45 @@ export const withMembership = (projection, subjectId, membership) => {
155
156
  },
156
157
  };
157
158
  };
159
+ /**
160
+ * The same faceted view, holding one more exception or one fewer (#267,
161
+ * ADR 0122).
162
+ *
163
+ * A view that describes its subjects with facets states a RULE, and there is
164
+ * no list to edit — which is why membership used to stop here. But every
165
+ * interesting rule has an exception, and `exclude` is where the author writes
166
+ * one down instead of abandoning the facet for a hand-enumerated list. So
167
+ * "remove from view" names the subject in `exclude`, and "add to this view"
168
+ * takes the name back out.
169
+ *
170
+ * The asymmetry is deliberate and is the whole reason there is no `include`
171
+ * tier here: taking an exception BACK is expressible, while adding a subject
172
+ * the facets do not select is not — that would silently convert the rule into
173
+ * a list. A subject that is neither excluded nor selected therefore gets
174
+ * `null`, the same answer this returns for every other no-op.
175
+ *
176
+ * `exclude` is a statement about the rule rather than about today's match set,
177
+ * so it is written whether or not the subject happens to be drawn: a model
178
+ * that later grows into the facet still honours the decision someone made.
179
+ */
180
+ const withExclusion = (projection, subjectId, membership) => {
181
+ const excluded = projection.query.exclude ?? [];
182
+ const isExcluded = excluded.includes(subjectId);
183
+ if (membership === "add" ? !isExcluded : isExcluded)
184
+ return null;
185
+ const next = membership === "add"
186
+ ? excluded.filter((id) => id !== subjectId)
187
+ : // Appended rather than sorted in, the rule the subjects list follows.
188
+ [...excluded, subjectId];
189
+ // The last exception leaving takes the key with it: `exclude: []` says the
190
+ // same thing as no key at all, and the schema's `minItems` refuses it
191
+ // anyway. Rebuilt rather than spread-over so the key is genuinely absent.
192
+ const { exclude: _dropped, ...rest } = projection.query;
193
+ return {
194
+ ...projection,
195
+ query: next.length === 0 ? rest : { ...rest, exclude: next },
196
+ };
197
+ };
158
198
  /**
159
199
  * What a membership edit did to a view, as the tray reads it: `+id` for a
160
200
  * subject this row adds, `-id` for one it drops, against the document the
@@ -162,6 +202,24 @@ export const withMembership = (projection, subjectId, membership) => {
162
202
  * query, a presentation flag — which the row says by other means.
163
203
  */
164
204
  export const membershipDelta = (saved, staged) => {
205
+ if (!enumeratesSubjects(saved) && !enumeratesSubjects(staged)) {
206
+ // A faceted view's membership moves through `exclude`, and reads inverted
207
+ // there: a name ARRIVING in the exclusion list is a subject leaving the
208
+ // view (#267). The tray says what happened to the view, not what happened
209
+ // to the list.
210
+ const savedExclusions = saved.exclude ?? [];
211
+ const stagedExclusions = staged.exclude ?? [];
212
+ return [
213
+ ...savedExclusions
214
+ .filter((id) => !stagedExclusions.includes(id))
215
+ .map((id) => `+${id}`),
216
+ ...stagedExclusions
217
+ .filter((id) => !savedExclusions.includes(id))
218
+ .map((id) => `-${id}`),
219
+ ];
220
+ }
221
+ // One side enumerates and the other does not: the query changed shape rather
222
+ // than its membership, which the row says by other means.
165
223
  if (!enumeratesSubjects(saved) || !enumeratesSubjects(staged))
166
224
  return [];
167
225
  return [
@@ -10,11 +10,41 @@ import type { VISUAL_PROTOCOL_VERSION, VisualApplyResultPayload, VisualAuthority
10
10
  * that serves it — this module holds types only, and every dependency it has on
11
11
  * the protocol is type-only too.
12
12
  */
13
+ /** One open question, as the question panel and a node's chip read it. */
14
+ export interface VisualQuestionEntry {
15
+ readonly questionId: string;
16
+ /** Rendered phrasing — per-subject questions arrive already interpolated. */
17
+ readonly question: string;
18
+ readonly authority: 'human' | 'agent' | 'either';
19
+ readonly since?: string;
20
+ }
21
+ /**
22
+ * The interrogation report, folded for drawing (#292).
23
+ *
24
+ * Derived per successful recompile from the same compile the graph came
25
+ * from, and never stored — the stateless-interview rule as the canvas sees
26
+ * it. Optional on the model: a host that computes no overlay ships none,
27
+ * and the app draws no chips rather than zeros.
28
+ */
29
+ export interface VisualInterrogationOverlay {
30
+ /** `id@version` of the catalogue that asked. */
31
+ readonly catalogue: string;
32
+ /** Engine condition-semantics version (ADR 0106), carried so a consumer
33
+ * can tell "the model moved" from "the engine moved". */
34
+ readonly semantics: string;
35
+ /** Workspace-scoped open questions — they name no subject, so they are
36
+ * shown when nothing is selected rather than pinned to a node. */
37
+ readonly workspace: readonly VisualQuestionEntry[];
38
+ /** Open questions per qualified subject id — `CanvasNode.id`'s space. */
39
+ readonly subjects: Readonly<Record<string, readonly VisualQuestionEntry[]>>;
40
+ }
13
41
  /** The resolved graph a session renders, as the browser receives it. */
14
42
  export interface VisualRenderedModel {
15
43
  readonly authority: VisualAuthority;
16
44
  readonly initialView: string;
17
45
  readonly graph: CanvasGraph;
46
+ /** Present only when the host evaluated the catalogue for this compile. */
47
+ readonly interrogation?: VisualInterrogationOverlay;
18
48
  /** Manifest-relative document paths — the add-concept target dropdown. */
19
49
  readonly documents: readonly string[];
20
50
  readonly vocabulary: {
@@ -5,7 +5,7 @@ import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.
5
5
  import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
6
6
  import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
7
7
  import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
8
- import type { VisualRenderedModel } from "./wire.js";
8
+ import type { VisualInterrogationOverlay, VisualRenderedModel } from "./wire.js";
9
9
  /**
10
10
  * What a workspace looks like to the editor, however the editor is being run
11
11
  * (#252).
@@ -41,17 +41,37 @@ export declare const kindOptionsOf: (lineages: ReadonlyMap<string, readonly stri
41
41
  * them would read as five, and the reviewer counting boxes would find three.
42
42
  */
43
43
  export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => number;
44
+ /**
45
+ * Folds one interrogation report into what the canvas draws (#292).
46
+ *
47
+ * Undefined — never a throw — when the catalogue does not load: the overlay
48
+ * is a garnish on the model, and a model frame must not be blocked by it.
49
+ * Subject ids come from the same compiled graph as `CanvasNode.id`, so the
50
+ * join in the browser is a plain lookup.
51
+ */
52
+ export declare const interrogationOverlayOf: (compiled: {
53
+ readonly graph: SemanticGraph;
54
+ readonly profileContext: ResolvedProfileContext;
55
+ }, catalogue: {
56
+ readonly path: string;
57
+ readonly source: string;
58
+ }) => VisualInterrogationOverlay | undefined;
44
59
  /**
45
60
  * Rebuilds the shared editor workspace from one successful compile.
46
61
  *
47
62
  * The caller owns metadata which cannot be inferred from a graph (authority,
48
63
  * source revisions, layouts, and initial view); this helper owns all derived
49
- * canvas, vocabulary, and view-count arithmetic.
64
+ * canvas, vocabulary, view-count, and interrogation arithmetic. `catalogue`
65
+ * is the question catalogue's bytes — this module cannot read files, so
66
+ * whoever can hands them over; omitting it ships a model with no overlay.
50
67
  */
51
68
  export declare const renderedWorkspaceOf: (compiled: {
52
69
  readonly graph: SemanticGraph;
53
70
  readonly profileContext: ResolvedProfileContext;
54
- }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary">) => {
71
+ }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary" | "interrogation">, catalogue?: {
72
+ readonly path: string;
73
+ readonly source: string;
74
+ }) => {
55
75
  readonly model: VisualRenderedModel;
56
76
  readonly views: readonly VisualViewSummary[];
57
77
  };
@@ -6,6 +6,7 @@ import { projectGraphForCanvas } from "../../graph-projection.js";
6
6
  import { DEFAULT_PROJECTION_DIRECTORY } from "./view-identity.js";
7
7
  import { evaluateProjection, explainProjection } from "../../projection.js";
8
8
  import { kindLabelOf } from "../../kind-label.js";
9
+ import { evaluateCatalogue, loadQuestionCatalogue, } from "../../interrogate-command.js";
9
10
  /**
10
11
  * What a workspace looks like to the editor, however the editor is being run
11
12
  * (#252).
@@ -45,18 +46,66 @@ export const kindOptionsOf = (lineages) => [...lineages.keys()].map((id) => ({
45
46
  * them would read as five, and the reviewer counting boxes would find three.
46
47
  */
47
48
  export const conceptCountOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.filter(({ type }) => type === "concept").length;
49
+ /**
50
+ * Folds one interrogation report into what the canvas draws (#292).
51
+ *
52
+ * Undefined — never a throw — when the catalogue does not load: the overlay
53
+ * is a garnish on the model, and a model frame must not be blocked by it.
54
+ * Subject ids come from the same compiled graph as `CanvasNode.id`, so the
55
+ * join in the browser is a plain lookup.
56
+ */
57
+ export const interrogationOverlayOf = (compiled, catalogue) => {
58
+ const loaded = loadQuestionCatalogue(catalogue);
59
+ if (!loaded.ok)
60
+ return undefined;
61
+ const report = evaluateCatalogue(loaded.catalogue, compiled.graph, compiled.profileContext);
62
+ const workspace = [];
63
+ const subjects = {};
64
+ for (const wave of report.waves) {
65
+ for (const question of wave.questions) {
66
+ if (!question.open)
67
+ continue;
68
+ const base = {
69
+ questionId: question.id,
70
+ authority: question.authority,
71
+ ...(question.since === undefined ? {} : { since: question.since }),
72
+ };
73
+ if (question.subjects === undefined) {
74
+ workspace.push({ ...base, question: question.question });
75
+ continue;
76
+ }
77
+ for (const subject of question.subjects) {
78
+ (subjects[subject.id] ??= []).push({
79
+ ...base,
80
+ question: subject.question,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ catalogue: report.catalogue,
87
+ semantics: report.semantics,
88
+ workspace,
89
+ subjects,
90
+ };
91
+ };
48
92
  /**
49
93
  * Rebuilds the shared editor workspace from one successful compile.
50
94
  *
51
95
  * The caller owns metadata which cannot be inferred from a graph (authority,
52
96
  * source revisions, layouts, and initial view); this helper owns all derived
53
- * canvas, vocabulary, and view-count arithmetic.
97
+ * canvas, vocabulary, view-count, and interrogation arithmetic. `catalogue`
98
+ * is the question catalogue's bytes — this module cannot read files, so
99
+ * whoever can hands them over; omitting it ships a model with no overlay.
54
100
  */
55
- export const renderedWorkspaceOf = (compiled, views, metadata) => {
101
+ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue) => {
56
102
  const refreshedViews = views.map((view) => ({
57
103
  ...view,
58
104
  subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext),
59
105
  }));
106
+ const interrogation = catalogue === undefined
107
+ ? undefined
108
+ : interrogationOverlayOf(compiled, catalogue);
60
109
  return {
61
110
  model: {
62
111
  ...metadata,
@@ -65,6 +114,7 @@ export const renderedWorkspaceOf = (compiled, views, metadata) => {
65
114
  conceptKinds: kindOptionsOf(compiled.profileContext.conceptKindLineages),
66
115
  relationshipKinds: kindOptionsOf(compiled.profileContext.relationshipKindLineages),
67
116
  },
117
+ ...(interrogation === undefined ? {} : { interrogation }),
68
118
  },
69
119
  views: refreshedViews,
70
120
  };
@@ -470,7 +470,7 @@ export function runAskCommand(options, cwd) {
470
470
  });
471
471
  if (!loadedCatalogue.ok)
472
472
  return failed(loadedCatalogue.diagnostics);
473
- const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
473
+ const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations));
474
474
  const result = {
475
475
  format: 'yarramate/ask-result/v1',
476
476
  workspace: workspace.id,
@@ -689,8 +689,21 @@ export function runAskCommand(options, cwd) {
689
689
  });
690
690
  if (!loadedCatalogue.ok)
691
691
  return failed(loadedCatalogue.diagnostics);
692
+ // The evidence overlay rides along for the one condition that
693
+ // reads it (unchallenged-evidence); a workspace declaring no
694
+ // evidence passes an overlay known to be empty.
695
+ const evidenceObservations = [];
696
+ for (const path of workspace.evidence) {
697
+ const loaded = loadEvidence({
698
+ path,
699
+ source: readFileSync(resolve(cwd, path), 'utf8'),
700
+ });
701
+ if (!loaded.ok)
702
+ return failed(loaded.diagnostics);
703
+ evidenceObservations.push(...loaded.evidence.observations);
704
+ }
692
705
  const report = {
693
- ...evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext),
706
+ ...evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext, evidenceObservations),
694
707
  workspace: workspace.id,
695
708
  };
696
709
  const ordered = {
@@ -971,7 +984,20 @@ export function runAskCommand(options, cwd) {
971
984
  });
972
985
  if (!loadedCatalogue.ok)
973
986
  return failed(loadedCatalogue.diagnostics);
974
- const report = evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext);
987
+ // Loaded ahead of evaluation so the overlay feeds the one condition
988
+ // that reads it (unchallenged-evidence), then reused for the
989
+ // reconciliation summary below.
990
+ const evidenceDocuments = [];
991
+ for (const path of workspace.evidence) {
992
+ const loaded = loadEvidence({
993
+ path,
994
+ source: readFileSync(resolve(cwd, path), 'utf8'),
995
+ });
996
+ if (!loaded.ok)
997
+ return failed(loaded.diagnostics);
998
+ evidenceDocuments.push(loaded.evidence);
999
+ }
1000
+ const report = evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations));
975
1001
  const openQuestions = [];
976
1002
  for (const wave of report.waves) {
977
1003
  for (const question of wave.questions) {
@@ -1003,16 +1029,6 @@ export function runAskCommand(options, cwd) {
1003
1029
  }
1004
1030
  let reconciliation;
1005
1031
  if (workspace.evidence.length > 0) {
1006
- const evidenceDocuments = [];
1007
- for (const path of workspace.evidence) {
1008
- const loaded = loadEvidence({
1009
- path,
1010
- source: readFileSync(resolve(cwd, path), 'utf8'),
1011
- });
1012
- if (!loaded.ok)
1013
- return failed(loaded.diagnostics);
1014
- evidenceDocuments.push(loaded.evidence);
1015
- }
1016
1032
  const evaluation = evaluateEvidenceWorkspace(graph, evidenceDocuments);
1017
1033
  if (!evaluation.ok)
1018
1034
  return failed(evaluation.diagnostics);