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
@@ -1,6 +1,6 @@
1
1
  format: yarramate/question-catalogue/v1
2
2
  id: core-enrichment
3
- version: "1.1"
3
+ version: "1.3"
4
4
  profile: yarramate/core@0.1
5
5
  presentation:
6
6
  title: Core enrichment interview
@@ -25,27 +25,39 @@ waves:
25
25
  Load-bearing hops: the behavior a component is assigned to or the
26
26
  interface it composes, the serving/triggering/flow mechanism,
27
27
  payload, trust, reliability, and capacity. Hygiene waits.
28
+ opensWhen:
29
+ - condition: has-any-subject
28
30
  - id: business
29
31
  name: Business
30
32
  description: Who acts, what is served, and what information matters.
33
+ opensWhen:
34
+ - condition: has-any-subject
31
35
  - id: application
32
36
  name: Application
33
37
  description: >-
34
38
  How declared services are realized, performed, and fed with
35
39
  information.
40
+ opensWhen:
41
+ - condition: has-any-subject
36
42
  - id: technology
37
43
  name: Technology
38
44
  description: >-
39
45
  Where the declared applications actually run and what materializes
40
46
  them.
47
+ opensWhen:
48
+ - condition: has-any-subject
41
49
  - id: implementation
42
50
  name: Implementation
43
51
  description: >-
44
52
  How the planned architecture becomes real: work, deliverables, and
45
53
  the plateaus between here and there.
54
+ opensWhen:
55
+ - condition: has-any-subject
46
56
  - id: hygiene
47
57
  name: Model hygiene
48
58
  description: Cross-cutting completeness that keeps every wave honest.
59
+ opensWhen:
60
+ - condition: has-any-subject
49
61
 
50
62
  questions:
51
63
  # ---- motivation ----------------------------------------------------------
@@ -880,6 +892,67 @@ questions:
880
892
  Add the externally meaningful services and serve them to their
881
893
  consumers.
882
894
 
895
+ - id: no-capability-declared
896
+ wave: business
897
+ since: "1.2"
898
+ scope: workspace
899
+ trigger:
900
+ - condition: no-subject-of-kind
901
+ kinds:
902
+ - yarramate/core@0.1#capability
903
+ question: >-
904
+ No capability is declared. What can this system do, in its own terms?
905
+ askPlain: >-
906
+ Forget the parts list for a moment: what are the handful of things
907
+ this system is actually able to do for the people it serves?
908
+ materiality: >-
909
+ Capabilities name what the system can do apart from how it currently
910
+ does it; without them, investment and sourcing trade-offs can only be
911
+ argued in component names, and nothing in the model can say which
912
+ parts serve the same ability. A subject-driven interview never asks
913
+ about a layer with zero subjects, so an absent capability map reads
914
+ as a covered one until this question opens it.
915
+ authority: human
916
+ resolution: >-
917
+ Add the small set of capabilities this system provides — four to
918
+ seven usually carries the whole conversation — and relate each to
919
+ the services or components that realize it with realization.
920
+
921
+ - id: no-contract-declared
922
+ wave: business
923
+ since: "1.2"
924
+ scope: workspace
925
+ trigger:
926
+ - condition: exists-linkage
927
+ kinds:
928
+ - yarramate/core@0.1#serving
929
+ - yarramate/core@0.1#flow
930
+ - yarramate/core@0.1#triggering
931
+ direction: either
932
+ counterpartKinds:
933
+ - yarramate/core@0.1#applicationComponent
934
+ - yarramate/core@0.1#applicationInterface
935
+ - yarramate/core@0.1#applicationService
936
+ - yarramate/core@0.1#businessActor
937
+ - condition: no-subject-of-kind
938
+ kinds:
939
+ - yarramate/core@0.1#contract
940
+ question: >-
941
+ Interactions exist and no contract governs any of them. Which
942
+ agreements bind what this architecture exchanges?
943
+ askPlain: >-
944
+ Things here talk to each other and to the outside. Where is it
945
+ written down what each side may expect — the API description, the
946
+ published schema, the service agreement?
947
+ materiality: >-
948
+ A hop with no contract is renegotiated by every implementer who
949
+ touches it; compatibility and versioning obligations exist only
950
+ where the agreement is a subject someone can change deliberately.
951
+ authority: human
952
+ resolution: >-
953
+ Add a contract for each formal agreement and access it from the
954
+ behavior bound by it; realization from the data object or artifact
955
+ that embodies the agreement says where it lives.
883
956
 
884
957
  - id: service-realizes-no-motivation
885
958
  wave: business
@@ -1063,6 +1136,38 @@ questions:
1063
1136
  not perform application behavior: assign the actor to the business
1064
1137
  process that uses it and let the application service serve that
1065
1138
  process.
1139
+ - id: no-event-declared
1140
+ wave: application
1141
+ since: "1.2"
1142
+ scope: workspace
1143
+ trigger:
1144
+ - condition: no-subject-of-kind
1145
+ kinds:
1146
+ - yarramate/core@0.1#applicationEvent
1147
+ - yarramate/core@0.1#businessEvent
1148
+ question: >-
1149
+ Nothing in this model reacts to an event. Does state change only
1150
+ when something is called, or are there happenings the architecture
1151
+ responds to?
1152
+ askPlain: >-
1153
+ Is everything here a direct request, or do things also happen on
1154
+ their own — a message arrives, a job finishes, a threshold is
1155
+ crossed — that the system has to react to?
1156
+ materiality: >-
1157
+ Whether change propagates by call or by event decides coupling,
1158
+ ordering, and failure isolation. An event-driven seam modelled as
1159
+ silence leaves every consumer to discover the event stream on its
1160
+ own, and a repository full of event definitions the interview never
1161
+ asked about reads as covered when it is absent.
1162
+ authority: human
1163
+ resolution: >-
1164
+ Add the application or business events the system emits or responds
1165
+ to and wire each with triggering from what raises it to what
1166
+ responds. If there are genuinely none, that is a coupling decision
1167
+ worth stating in a principle or a service description; this question
1168
+ then stays on the agenda as the standing record that nobody has
1169
+ declared one.
1170
+
1066
1171
  - id: event-triggers-nothing
1067
1172
  wave: application
1068
1173
  since: "0.3"
@@ -1232,6 +1337,31 @@ questions:
1232
1337
  Add realization from the node, system software, or technology
1233
1338
  behavior that provides the service.
1234
1339
 
1340
+ - id: no-artifact-declared
1341
+ wave: technology
1342
+ since: "1.2"
1343
+ scope: workspace
1344
+ trigger:
1345
+ - condition: no-subject-of-kind
1346
+ kinds:
1347
+ - yarramate/core@0.1#artifact
1348
+ question: >-
1349
+ No artifact is declared. What do the declared components actually
1350
+ ship as?
1351
+ askPlain: >-
1352
+ When this system is deployed, what is the thing that moves — an
1353
+ image, a package, a bundle? What are those called?
1354
+ materiality: >-
1355
+ Release engineering starts where a build output becomes a subject.
1356
+ An architecture with components but no artifacts can say what should
1357
+ exist and nothing about what is deployed, so drift between built and
1358
+ declared has nowhere to register.
1359
+ authority: human
1360
+ resolution: >-
1361
+ Add the artifacts the build produces, assignment from the node that
1362
+ deploys each one, and realization from the artifact to the component
1363
+ or data object it materializes.
1364
+
1235
1365
  - id: artifact-unassigned
1236
1366
  wave: technology
1237
1367
  since: "0.4"
@@ -1258,6 +1388,37 @@ questions:
1258
1388
  realization from the artifact to the component or data object it
1259
1389
  materializes.
1260
1390
  # ---- implementation ------------------------------------------------------
1391
+ - id: implementation-path-missing
1392
+ wave: implementation
1393
+ since: "1.2"
1394
+ scope: workspace
1395
+ trigger:
1396
+ - condition: no-subject-of-kind
1397
+ kinds:
1398
+ - yarramate/core@0.1#workPackage
1399
+ - yarramate/core@0.1#deliverable
1400
+ - yarramate/core@0.1#plateau
1401
+ question: >-
1402
+ No work package, deliverable, or plateau is declared. How does the
1403
+ planned architecture become real?
1404
+ askPlain: >-
1405
+ Who is doing what to get from here to there? What are the packages
1406
+ of work, and what does each one hand over?
1407
+ materiality: >-
1408
+ A model that names a target but no work reaching it is a wish with
1409
+ an architecture diagram; committed work packages and their
1410
+ deliverables are what make progress reviewable rather than
1411
+ reported. A repository mid-migration that models none of the
1412
+ migration is the sharpest form of the gap: the change is in the
1413
+ tree and invisible in the model.
1414
+ authority: human
1415
+ resolution: >-
1416
+ Add the work packages in flight, realization to the deliverables
1417
+ each produces, and plateaus where an intermediate state needs a
1418
+ name. An architecture genuinely at rest keeps this question open,
1419
+ which is itself information: the model is saying nothing is
1420
+ changing.
1421
+
1261
1422
  - id: workpackage-delivers-nothing
1262
1423
  wave: implementation
1263
1424
  since: "0.4"
@@ -1426,6 +1587,36 @@ questions:
1426
1587
  resolution: >-
1427
1588
  Add a description claim stating meaning and explicit exclusions.
1428
1589
 
1590
+ - id: capability-uncited
1591
+ wave: hygiene
1592
+ since: "1.2"
1593
+ scope: subject
1594
+ subjects:
1595
+ kinds:
1596
+ - yarramate/core@0.1#capability
1597
+ trigger:
1598
+ - condition: missing-reference
1599
+ predicate: yarramate/reference/refers-to
1600
+ direction: outgoing
1601
+ question: >-
1602
+ {subject.name} cites nothing. Where is this ability specified,
1603
+ decided, or documented?
1604
+ askPlain: >-
1605
+ If someone asked where it says the system can do
1606
+ "{subject.name}", what would you point at?
1607
+ materiality: >-
1608
+ A capability with no citation floats free of every record that
1609
+ could confirm or correct it: an audit has nothing to grade the
1610
+ claim against, and when its scope is disputed the dispute starts
1611
+ from memory. The reference is where the model meets the
1612
+ repository's own account of itself.
1613
+ authority: either
1614
+ resolution: >-
1615
+ Add a references entry from the capability to the subject that
1616
+ specifies it — the decision record, the published schema, the
1617
+ guide that documents the journey. Model that record as a subject
1618
+ first where it is not one yet.
1619
+
1429
1620
  - id: status-missing
1430
1621
  wave: hygiene
1431
1622
  since: "0.1"
@@ -1479,6 +1670,12 @@ questions:
1479
1670
  genuinely different, say so in the model: add the counterpart's id to
1480
1671
  this subject's distinctFrom list. That answer is itself a claim, so it
1481
1672
  closes the question permanently and survives re-running the interview.
1673
+ A third answer is ordinary in a repository mid-migration: they are
1674
+ different subjects and one is taking over from the other. Record the
1675
+ succession with supersedes, scoped where the takeover is partial
1676
+ (ADR 0109), and record distinctFrom alongside it. Succession says what
1677
+ the pair is to each other and distinctness is what this question asks,
1678
+ so only the second closes it.
1482
1679
 
1483
1680
  - id: states-undefined
1484
1681
  wave: hygiene
@@ -1489,13 +1686,24 @@ questions:
1489
1686
  question: >-
1490
1687
  Does change shape matter here — should baseline, transition, or target
1491
1688
  states be declared?
1689
+ askPlain: >-
1690
+ Are we describing how things are, how they should become, or both?
1691
+ If the repository already carries a migration plan or a target
1692
+ design, that document is the answer.
1492
1693
  materiality: >-
1493
1694
  Without architecture states, current and target intent share one
1494
- undifferentiated model and comparisons are impossible.
1695
+ undifferentiated model and comparisons are impossible. The answer is
1696
+ often already on the record: a repository that carries its own
1697
+ migration plan or target design in-tree has declared that change
1698
+ shape matters, so evidence can bring this question its answer rather
1699
+ than waiting for someone to remember it.
1495
1700
  authority: human
1496
1701
  resolution: >-
1497
1702
  Declare architecture states and mark presence with present-in where the
1498
1703
  distinction carries decisions; explicitly decline states otherwise.
1704
+ Where in-tree documents already describe the target, model that
1705
+ record and cite it with references from the subjects it changes, so
1706
+ the declared states stand on evidence a reviewer can open.
1499
1707
 
1500
1708
  - id: kind-untested
1501
1709
  wave: hygiene
@@ -1580,3 +1788,30 @@ questions:
1580
1788
  which says the same thing without a qualifier. If the two subjects
1581
1789
  simply coexist and neither takes over from the other, the succession
1582
1790
  is the wrong claim; remove it.
1791
+
1792
+ - id: evidence-unchallenged
1793
+ wave: hygiene
1794
+ since: "1.2"
1795
+ scope: workspace
1796
+ trigger:
1797
+ - condition: unchallenged-evidence
1798
+ question: >-
1799
+ Every observation this workspace records is a frictionless
1800
+ confirmation. Did the inspection ever test a claim it might fail?
1801
+ askPlain: >-
1802
+ The evidence agrees with the model everywhere. Did we ever look
1803
+ for something that might not be there — a documented piece missing
1804
+ from the tree, a declared dependency nothing carries?
1805
+ materiality: >-
1806
+ An overlay that only ever says confirmed is indistinguishable from
1807
+ one that only looked where success was guaranteed. One recorded
1808
+ search or one honest non-confirmation is what makes the agreement
1809
+ between model and reality worth believing; a reconcile summary of
1810
+ pure confirmations has never put a claim at risk.
1811
+ authority: either
1812
+ resolution: >-
1813
+ Probe at least one claim the sources assert that the tree might not
1814
+ honour, and record the observation with its honest result. A
1815
+ not-observed carries the searches that came back empty (ADR 0107);
1816
+ a confirmed negative claim carries them too, because the empty
1817
+ 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,49 @@ 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 interface DismissedQuestion {
53
+ readonly questionId: string;
54
+ /** Absent dismisses the question wherever it appears. */
55
+ readonly subject?: string;
56
+ }
57
+ export declare const interrogationOverlayOf: (compiled: {
58
+ readonly graph: SemanticGraph;
59
+ readonly profileContext: ResolvedProfileContext;
60
+ }, catalogue: {
61
+ readonly path: string;
62
+ readonly source: string;
63
+ },
64
+ /**
65
+ * What the host has already dealt with (#328). Evaluation is unchanged and
66
+ * the model is untouched: this decides only what the pane draws, because a
67
+ * question set aside in the host's own product should not be asked again by
68
+ * a pane embedded in it.
69
+ */
70
+ dismissed?: readonly DismissedQuestion[]) => VisualInterrogationOverlay | undefined;
44
71
  /**
45
72
  * Rebuilds the shared editor workspace from one successful compile.
46
73
  *
47
74
  * The caller owns metadata which cannot be inferred from a graph (authority,
48
75
  * source revisions, layouts, and initial view); this helper owns all derived
49
- * canvas, vocabulary, and view-count arithmetic.
76
+ * canvas, vocabulary, view-count, and interrogation arithmetic. `catalogue`
77
+ * is the question catalogue's bytes — this module cannot read files, so
78
+ * whoever can hands them over; omitting it ships a model with no overlay.
50
79
  */
51
80
  export declare const renderedWorkspaceOf: (compiled: {
52
81
  readonly graph: SemanticGraph;
53
82
  readonly profileContext: ResolvedProfileContext;
54
- }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary">) => {
83
+ }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary" | "interrogation">, catalogue?: {
84
+ readonly path: string;
85
+ readonly source: string;
86
+ }, dismissed?: readonly DismissedQuestion[]) => {
55
87
  readonly model: VisualRenderedModel;
56
88
  readonly views: readonly VisualViewSummary[];
57
89
  };
@@ -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,76 @@ 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
+ export const interrogationOverlayOf = (compiled, catalogue,
50
+ /**
51
+ * What the host has already dealt with (#328). Evaluation is unchanged and
52
+ * the model is untouched: this decides only what the pane draws, because a
53
+ * question set aside in the host's own product should not be asked again by
54
+ * a pane embedded in it.
55
+ */
56
+ dismissed = []) => {
57
+ const loaded = loadQuestionCatalogue(catalogue);
58
+ if (!loaded.ok)
59
+ return undefined;
60
+ const report = evaluateCatalogue(loaded.catalogue, compiled.graph, compiled.profileContext);
61
+ const dismissedEverywhere = new Set(dismissed
62
+ .filter(({ subject }) => subject === undefined)
63
+ .map(({ questionId }) => questionId));
64
+ const dismissedForSubject = new Set(dismissed
65
+ .filter(({ subject }) => subject !== undefined)
66
+ .map(({ questionId, subject }) => `${questionId}\u0000${subject}`));
67
+ const workspace = [];
68
+ const subjects = {};
69
+ for (const wave of report.waves) {
70
+ for (const question of wave.questions) {
71
+ if (!question.open)
72
+ continue;
73
+ if (dismissedEverywhere.has(question.id))
74
+ continue;
75
+ const base = {
76
+ questionId: question.id,
77
+ authority: question.authority,
78
+ ...(question.since === undefined ? {} : { since: question.since }),
79
+ };
80
+ if (question.subjects === undefined) {
81
+ workspace.push({ ...base, question: question.question });
82
+ continue;
83
+ }
84
+ for (const subject of question.subjects) {
85
+ if (dismissedForSubject.has(`${question.id}\u0000${subject.id}`)) {
86
+ continue;
87
+ }
88
+ (subjects[subject.id] ??= []).push({
89
+ ...base,
90
+ question: subject.question,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ return {
96
+ catalogue: report.catalogue,
97
+ semantics: report.semantics,
98
+ workspace,
99
+ subjects,
100
+ };
101
+ };
48
102
  /**
49
103
  * Rebuilds the shared editor workspace from one successful compile.
50
104
  *
51
105
  * The caller owns metadata which cannot be inferred from a graph (authority,
52
106
  * source revisions, layouts, and initial view); this helper owns all derived
53
- * canvas, vocabulary, and view-count arithmetic.
107
+ * canvas, vocabulary, view-count, and interrogation arithmetic. `catalogue`
108
+ * is the question catalogue's bytes — this module cannot read files, so
109
+ * whoever can hands them over; omitting it ships a model with no overlay.
54
110
  */
55
- export const renderedWorkspaceOf = (compiled, views, metadata) => {
111
+ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismissed) => {
56
112
  const refreshedViews = views.map((view) => ({
57
113
  ...view,
58
114
  subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext),
59
115
  }));
116
+ const interrogation = catalogue === undefined
117
+ ? undefined
118
+ : interrogationOverlayOf(compiled, catalogue, dismissed);
60
119
  return {
61
120
  model: {
62
121
  ...metadata,
@@ -65,6 +124,7 @@ export const renderedWorkspaceOf = (compiled, views, metadata) => {
65
124
  conceptKinds: kindOptionsOf(compiled.profileContext.conceptKindLineages),
66
125
  relationshipKinds: kindOptionsOf(compiled.profileContext.relationshipKindLineages),
67
126
  },
127
+ ...(interrogation === undefined ? {} : { interrogation }),
68
128
  },
69
129
  views: refreshedViews,
70
130
  };