yarramate 1.34.0 → 1.36.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 (39) hide show
  1. package/catalogues/core-enrichment.yaml +4 -18
  2. package/dist/adapters/visual/protocol-contract.d.ts +10 -0
  3. package/dist/adapters/visual/session-server.js +8 -6
  4. package/dist/adapters/visual/workspace-model.d.ts +3 -3
  5. package/dist/adapters/visual/workspace-model.js +27 -15
  6. package/dist/ask-command.js +6 -0
  7. package/dist/governance.js +11 -2
  8. package/dist/host/local-host.js +2 -2
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/projection.js +16 -3
  12. package/dist/relationship-convention.d.ts +25 -0
  13. package/dist/relationship-convention.js +176 -0
  14. package/dist/shipped-catalogue.generated.d.ts +1 -1
  15. package/dist/shipped-catalogue.generated.js +1 -1
  16. package/dist/tools/ask.d.ts +41 -1
  17. package/dist/tools/ask.js +28 -2
  18. package/dist/tools/design.js +2 -0
  19. package/dist/tools/export.js +2 -0
  20. package/dist/tools-entry.d.ts +1 -1
  21. package/dist/tools-entry.js +1 -1
  22. package/dist/visual-app/assets/{elk.bundled-CFiHtVQt.js → elk.bundled-B_x2vU_0.js} +1 -1
  23. package/dist/visual-app/assets/{index-BaQ0vy3y.js → index-BRduex3u.js} +2 -2
  24. package/dist/visual-app/assets/index-Oc1hphkX.css +1 -0
  25. package/dist/visual-app/index.html +2 -2
  26. package/dist/visual-app-lib/editor.js +21490 -21386
  27. package/dist/visual-app-lib/styles.css +1 -1
  28. package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +10 -0
  29. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +3 -3
  30. package/dist/visual-app-lib/types/relationship-convention.d.ts +25 -0
  31. package/dist/visual-app-lib/types/shipped-catalogue.generated.d.ts +1 -1
  32. package/dist/visual-app-lib/types/visual-app/relationship-kind-options.d.ts +8 -0
  33. package/dist/visual-app-lib/types/visual-app/session-client.d.ts +7 -1
  34. package/dist/visual-app-lib/types/visual-app/state.d.ts +1 -0
  35. package/package.json +1 -1
  36. package/schema/yarramate-ask-result.schema.json +13 -0
  37. package/schema/yarramate-visual-event.schema.json +4 -0
  38. package/skills/yarramate-architecture/references/native-authoring.md +8 -0
  39. package/dist/visual-app/assets/index-DYTKTQsG.css +0 -1
@@ -907,9 +907,6 @@ questions:
907
907
  subjects:
908
908
  kinds:
909
909
  - yarramate/policy@0.3#risk
910
- statuses:
911
- - planned
912
- - current
913
910
  trigger:
914
911
  - condition: missing-linkage
915
912
  kinds:
@@ -939,8 +936,6 @@ questions:
939
936
  subjects:
940
937
  kinds:
941
938
  - yarramate/policy@0.3#risk
942
- statuses:
943
- - current
944
939
  trigger:
945
940
  - condition: missing-linkage
946
941
  kinds:
@@ -955,11 +950,11 @@ questions:
955
950
  question: >-
956
951
  What mitigates {subject.name}?
957
952
  askPlain: >-
958
- "{subject.name}" is live. What work, deliverable, rule or decision is in
959
- place to reduce it?
953
+ "{subject.name}" is on the log. What work, deliverable, rule or decision
954
+ is in place, or planned, to reduce it?
960
955
  materiality: >-
961
- A live risk with no mitigation is a bet the engagement is making without
962
- saying so; the log exists to make that visible.
956
+ A risk on the log with no mitigation is a bet the engagement is making
957
+ without saying so; the log exists to make that visible.
963
958
  authority: either
964
959
  resolution: >-
965
960
  Add an `influence` relationship from the work package, deliverable,
@@ -971,9 +966,6 @@ questions:
971
966
  subjects:
972
967
  kinds:
973
968
  - yarramate/policy@0.3#risk
974
- statuses:
975
- - planned
976
- - current
977
969
  trigger:
978
970
  - condition: missing-claim
979
971
  predicate: yarramate/ownership/owner
@@ -995,9 +987,6 @@ questions:
995
987
  subjects:
996
988
  kinds:
997
989
  - yarramate/policy@0.3#assumption
998
- statuses:
999
- - planned
1000
- - current
1001
990
  trigger:
1002
991
  - condition: missing-attestation
1003
992
  topic: assumption-confirmed
@@ -1021,9 +1010,6 @@ questions:
1021
1010
  subjects:
1022
1011
  kinds:
1023
1012
  - yarramate/policy@0.3#assumption
1024
- statuses:
1025
- - planned
1026
- - current
1027
1013
  trigger:
1028
1014
  - condition: missing-relationship
1029
1015
  kinds:
@@ -255,6 +255,16 @@ export interface VisualFilterQueryPayload {
255
255
  * exactly as it did before.
256
256
  */
257
257
  readonly nesting?: readonly NestingKind[];
258
+ /**
259
+ * Whether the view shows responsibility edges (#563, ADR 0161).
260
+ *
261
+ * Only the `connected` walk reads it: a responsibility edge the canvas hides
262
+ * must not bring a person into the picture, and one the canvas shows must.
263
+ * Optional, so an older browser and every filter that does not send it
264
+ * evaluate as a view with the flag off, which is what the canvas draws by
265
+ * default.
266
+ */
267
+ readonly showResponsibility?: boolean;
258
268
  }
259
269
  export interface VisualFilterResultPayload {
260
270
  readonly query: ProjectionQuery;
@@ -680,7 +680,7 @@ export const startVisualServer = async (options) => {
680
680
  if (!started.ok)
681
681
  standingDiagnostics = started.diagnostics;
682
682
  }
683
- const filterMatchedIds = (query, nesting) => compiledWorkspace === undefined
683
+ const filterMatchedIds = (query, nesting, showResponsibility) => compiledWorkspace === undefined
684
684
  ? []
685
685
  : matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext,
686
686
  // A view's query can name `instances`, and the facet resolves to the
@@ -688,7 +688,9 @@ export const startVisualServer = async (options) => {
688
688
  compiledWorkspace.patternMemberships,
689
689
  // And it resolves the WRONG closure without the nesting the canvas is
690
690
  // drawing with, which is a wrong number rather than a missing one.
691
- nesting);
691
+ nesting,
692
+ // And the walk reads the responsibility flag (#563).
693
+ showResponsibility);
692
694
  /**
693
695
  * Why a query dropped what it dropped, as the editor's "excluded, and why"
694
696
  * list reads it (#248).
@@ -700,9 +702,9 @@ export const startVisualServer = async (options) => {
700
702
  * the reason the editor shows and the set the canvas draws cannot come from
701
703
  * two readings of the same query.
702
704
  */
703
- const filterExclusions = (query, nesting) => compiledWorkspace === undefined
705
+ const filterExclusions = (query, nesting, showResponsibility) => compiledWorkspace === undefined
704
706
  ? []
705
- : exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext, compiledWorkspace.patternMemberships, nesting);
707
+ : exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext, compiledWorkspace.patternMemberships, nesting, showResponsibility);
706
708
  let listening = false;
707
709
  let bootstrapSpent = false;
708
710
  let agentAttached = false;
@@ -1255,8 +1257,8 @@ export const startVisualServer = async (options) => {
1255
1257
  kind: "filter-result",
1256
1258
  result: {
1257
1259
  query: event.payload.query,
1258
- matchedIds: filterMatchedIds(event.payload.query, event.payload.nesting),
1259
- excluded: filterExclusions(event.payload.query, event.payload.nesting),
1260
+ matchedIds: filterMatchedIds(event.payload.query, event.payload.nesting, event.payload.showResponsibility),
1261
+ excluded: filterExclusions(event.payload.query, event.payload.nesting, event.payload.showResponsibility),
1260
1262
  },
1261
1263
  });
1262
1264
  return;
@@ -57,7 +57,7 @@ export declare const patternOptionsOf: (patterns: readonly PatternShape[], linea
57
57
  * subject count. A view over three components with two relationships between
58
58
  * them would read as five, and the reviewer counting boxes would find three.
59
59
  */
60
- export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => number;
60
+ export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[], showResponsibility?: boolean) => number;
61
61
  /**
62
62
  * Folds one interrogation report into what the canvas draws (#292).
63
63
  *
@@ -125,9 +125,9 @@ export declare const renderedWorkspaceOf: (compiled: {
125
125
  readonly views: readonly VisualViewSummary[];
126
126
  };
127
127
  /** Every subject a query draws, concepts and relationships alike. */
128
- export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly string[];
128
+ export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[], showResponsibility?: boolean) => readonly string[];
129
129
  /** Every concept a query dropped, and the facet that dropped it (#248). */
130
- export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly ProjectionExclusion[];
130
+ export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[], showResponsibility?: boolean) => readonly ProjectionExclusion[];
131
131
  /**
132
132
  * One saved view, as the rail reads it. `subjectCount` is the caller's,
133
133
  * because counting needs a compiled graph and a session builds its first list
@@ -104,7 +104,7 @@ const wiringOf = (pattern, slot) => {
104
104
  * subject count. A view over three components with two relationships between
105
105
  * them would read as five, and the reviewer counting boxes would find three.
106
106
  */
107
- export const conceptCountOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.filter(({ type }) => type === "concept").length;
107
+ export const conceptCountOf = (graph, query, profileContext, memberships, nesting, showResponsibility) => evaluateProjection(graph, adHoc(query, nesting, showResponsibility), profileContext, memberships).subjects.filter(({ type }) => type === "concept").length;
108
108
  export const interrogationOverlayOf = (compiled,
109
109
  /**
110
110
  * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
@@ -209,7 +209,10 @@ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismis
209
209
  // Each view's OWN nesting, because each view's closure is its own. A rail
210
210
  // sitting beside the canvas must not count a different tree than the
211
211
  // canvas draws.
212
- view.presentation?.nesting),
212
+ view.presentation?.nesting,
213
+ // And each view's own responsibility flag, for the same reason: the
214
+ // walk reads it (#563), and the rail must count what the canvas draws.
215
+ view.presentation?.showResponsibility),
213
216
  }));
214
217
  const interrogation = catalogue === undefined
215
218
  ? undefined
@@ -250,24 +253,33 @@ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismis
250
253
  };
251
254
  };
252
255
  /** Every subject a query draws, concepts and relationships alike. */
253
- export const matchedIdsOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.map(({ id }) => id);
256
+ export const matchedIdsOf = (graph, query, profileContext, memberships, nesting, showResponsibility) => evaluateProjection(graph, adHoc(query, nesting, showResponsibility), profileContext, memberships).subjects.map(({ id }) => id);
254
257
  /** Every concept a query dropped, and the facet that dropped it (#248). */
255
- export const exclusionsOf = (graph, query, profileContext, memberships, nesting) => explainProjection(graph, adHoc(query, nesting), profileContext, memberships);
258
+ export const exclusionsOf = (graph, query, profileContext, memberships, nesting, showResponsibility) => explainProjection(graph, adHoc(query, nesting, showResponsibility), profileContext, memberships);
256
259
  /**
257
260
  * A query on its own is not a projection, and every evaluator here wants one.
258
261
  * The id is a placeholder that never reaches a document.
259
262
  */
260
- const adHoc = (query, nesting) => ({
261
- format: "yarramate/projection/v1",
262
- id: "ad-hoc",
263
- version: "0",
264
- query,
265
- // `query.instances` resolves its closure through the view's nesting, so an
266
- // ad-hoc projection that dropped the nesting would answer a DIFFERENT
267
- // question than the canvas is drawing: 2 subjects against 15 on the
268
- // ApertureX reference, with nothing to say it had (#473 phase 2).
269
- ...(nesting === undefined ? {} : { presentation: { nesting } }),
270
- });
263
+ const adHoc = (query, nesting, showResponsibility) => {
264
+ const presentation = {
265
+ // `query.instances` resolves its closure through the view's nesting, so an
266
+ // ad-hoc projection that dropped the nesting would answer a DIFFERENT
267
+ // question than the canvas is drawing: 2 subjects against 15 on the
268
+ // ApertureX reference, with nothing to say it had (#473 phase 2).
269
+ ...(nesting === undefined ? {} : { nesting }),
270
+ // And the `connected` walk reads this flag (#563, ADR 0161): dropped, the
271
+ // evaluation would walk a responsibility edge the canvas hides and stand a
272
+ // person in the picture with no line to anything.
273
+ ...(showResponsibility === undefined ? {} : { showResponsibility }),
274
+ };
275
+ return {
276
+ format: "yarramate/projection/v1",
277
+ id: "ad-hoc",
278
+ version: "0",
279
+ query,
280
+ ...(Object.keys(presentation).length === 0 ? {} : { presentation }),
281
+ };
282
+ };
271
283
  /**
272
284
  * One saved view, as the rail reads it. `subjectCount` is the caller's,
273
285
  * because counting needs a compiled graph and a session builds its first list
@@ -605,6 +605,12 @@ export function runAskCommand(options, cwd) {
605
605
  topic,
606
606
  seeds: resolution.seeds,
607
607
  matched: resolution.matched,
608
+ // The same document the tools entry emits, field for field (ADR 0156),
609
+ // and `test/tools-entry.test.ts` compares them. The ranked candidates
610
+ // ride on both (#569).
611
+ ...(resolution.addressing === 'free-text'
612
+ ? { candidates: resolution.candidates }
613
+ : {}),
608
614
  ...(neighbourhood === undefined ? {} : { neighbourhood }),
609
615
  result: evaluated,
610
616
  };
@@ -122,7 +122,16 @@ export function buildGovernanceLog(workspace, graph, profileContext, options = {
122
122
  });
123
123
  const ids = (predicate) => rows.filter(predicate).map(({ subject }) => subject);
124
124
  const unowned = ids((row) => row.owner === null);
125
- const unmitigated = ids((row) => row.type === 'risk' && row.status === 'current' && row.mitigatedBy.length === 0);
125
+ const unmitigated = ids((row) =>
126
+ // Every risk still open, whatever its status says or does not say. The
127
+ // first cut counted only `status: current`, so a risk nobody had given a
128
+ // status, which is every freshly authored one, was never here and the
129
+ // tile read 0 over a log where every risk was unmitigated (#564). The
130
+ // engine's word for "open" is not-retired: that is what keeps unstatused
131
+ // subjects in a projection with `excludeStatuses: [retired]` and what
132
+ // the interrogation applies by default. A planned risk is asked for its
133
+ // mitigation too; a mitigation can be planned.
134
+ row.type === 'risk' && row.status !== 'retired' && row.mitigatedBy.length === 0);
126
135
  const unconfirmed = ids((row) => row.type === 'assumption' && row.review === null);
127
136
  const unreviewed = ids((row) => row.type === 'risk' && row.review === null);
128
137
  return {
@@ -159,6 +168,6 @@ export function renderGovernanceMarkdown(log) {
159
168
  }
160
169
  lines.push('');
161
170
  }
162
- lines.push('## Gaps', '', `- Unowned: ${list(log.gaps.unowned)}`, `- Unmitigated (current risks): ${list(log.gaps.unmitigated)}`, `- Unconfirmed assumptions: ${list(log.gaps.unconfirmed)}`, `- Unreviewed risks: ${list(log.gaps.unreviewed)}`, '');
171
+ lines.push('## Gaps', '', `- Unowned: ${list(log.gaps.unowned)}`, `- Unmitigated risks: ${list(log.gaps.unmitigated)}`, `- Unconfirmed assumptions: ${list(log.gaps.unconfirmed)}`, `- Unreviewed risks: ${list(log.gaps.unreviewed)}`, '');
163
172
  return `${lines.join('\n')}\n`;
164
173
  }
@@ -384,8 +384,8 @@ export const createLocalHost = (options) => {
384
384
  kind: 'filter-result',
385
385
  result: {
386
386
  query: input.payload.query,
387
- matchedIds: matchedIdsOf(compiled.graph, input.payload.query, compiled.profileContext, compiled.patternMemberships, input.payload.nesting),
388
- excluded: exclusionsOf(compiled.graph, input.payload.query, compiled.profileContext, compiled.patternMemberships, input.payload.nesting),
387
+ matchedIds: matchedIdsOf(compiled.graph, input.payload.query, compiled.profileContext, compiled.patternMemberships, input.payload.nesting, input.payload.showResponsibility),
388
+ excluded: exclusionsOf(compiled.graph, input.payload.query, compiled.profileContext, compiled.patternMemberships, input.payload.nesting, input.payload.showResponsibility),
389
389
  },
390
390
  });
391
391
  return;
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, Proje
21
21
  export { createFileSystemStore, type PendingWrite, type SourceStore, type StoredSource, type WriteConflict, type WriteOutcome, } from './source-store.js';
22
22
  export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, type ApplyOutcome, } from './apply-command.js';
23
23
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
24
+ export { conventionalRelationshipKind, CONVENTION_RULES, type RelationshipConvention, } from './relationship-convention.js';
24
25
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
25
26
  export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
26
27
  export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CataloguePatternVacancy, type CatalogueInput, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, }
19
19
  export { createFileSystemStore, } from './source-store.js';
20
20
  export { applyOperations, landOperations, posixDirectoryOf, } from './apply-command.js';
21
21
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
22
+ export { conventionalRelationshipKind, CONVENTION_RULES, } from './relationship-convention.js';
22
23
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
23
24
  export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
24
25
  export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
@@ -18,6 +18,7 @@ export { DEFAULT_FOLD } from './fold-tree.js';
18
18
  import { DEFAULT_NESTING as NESTING_DEFAULT } from './nesting.js';
19
19
  import { foldTree } from './fold-tree.js';
20
20
  import { kindLabelOf } from './kind-label.js';
21
+ import { responsibilityLetterOf } from './responsibility-kinds.js';
21
22
  /**
22
23
  * Which way a view runs, and the default. Split out for the same reason as the
23
24
  * nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
@@ -563,10 +564,22 @@ export function evaluateProjection(graph, projection, profileContext, membership
563
564
  endpointExcluded(relationship.object.ref)) {
564
565
  continue;
565
566
  }
566
- if ((relationshipMode === 'between' &&
567
- sourceSelected &&
568
- targetSelected) ||
567
+ const bothSelected = sourceSelected && targetSelected;
568
+ // A responsibility edge does not carry the `connected` walk unless the
569
+ // view shows responsibility (#563, ADR 0161). The canvas hides these
570
+ // edges while the flag is off (ADR 0159), so walking one stood a person
571
+ // in an application landscape with no line to anything: four of them on
572
+ // the ApertureX reference. Between two subjects the query selected on
573
+ // their own merits the edge is selected like any other, so a
574
+ // whole-workspace evaluation, the workbook and the matrix builders keep
575
+ // seeing every relationship; only the walk reads the flag. Read through
576
+ // the lineage, so an adopter's subkind of `responsible` is held back
577
+ // exactly as the shipped kind is.
578
+ const carriesTheWalk = projection.presentation?.showResponsibility === true ||
579
+ responsibilityLetterOf(profileContext?.relationshipKindLineages.get(relationship.predicate), relationship.predicate) === null;
580
+ if (bothSelected ||
569
581
  (relationshipMode === 'connected' &&
582
+ carriesTheWalk &&
570
583
  (sourceSelected || targetSelected))) {
571
584
  selectedRelationshipIds.add(subject.id);
572
585
  if (relationshipMode === 'connected') {
@@ -0,0 +1,25 @@
1
+ import { type RelationshipKind } from './profile.js';
2
+ /**
3
+ * One convention, as a named rule. The name is carried into the answer so a
4
+ * caller can say WHY, which is the difference between guidance a reviewer can
5
+ * argue with and a number they have to take on faith.
6
+ */
7
+ export interface RelationshipConvention {
8
+ readonly kind: RelationshipKind;
9
+ readonly rule: string;
10
+ readonly because: string;
11
+ }
12
+ /**
13
+ * The kind practice expects from `from` to `to`, or `null` where practice has
14
+ * no expectation.
15
+ *
16
+ * Never contradicts the table: an answer is returned only when the table also
17
+ * permits it, so guidance can never point at an edge `check` would refuse.
18
+ */
19
+ export declare const conventionalRelationshipKind: (from: string, to: string) => RelationshipConvention | null;
20
+ /** Every rule's name and sentence, for documentation and for the test that checks them. */
21
+ export declare const CONVENTION_RULES: readonly {
22
+ readonly name: string;
23
+ readonly kind: RelationshipKind;
24
+ readonly because: string;
25
+ }[];
@@ -0,0 +1,176 @@
1
+ import { conceptKinds, } from './profile.js';
2
+ import { isCoreConceptKindId, permittedRelationshipKinds, } from './relationship-matrix.js';
3
+ /**
4
+ * The relationship kind ArchiMate practice expects for a pair of core kinds,
5
+ * where practice has an expectation at all (#571).
6
+ *
7
+ * The relationship table is a PERMISSION table. It answers "may I draw this",
8
+ * and for an application component pointing at an application function it
9
+ * answers yes six times. Practice is much narrower: across this repository's
10
+ * own model and the ApertureX reference, 970 relationships over 72 core-kind
11
+ * pairs, the author used exactly one kind for 34 of the pairs the table lets
12
+ * them choose among, and used `assignment` for that particular pair 87 times
13
+ * out of 88. The engine knew the permission and never said the convention, so
14
+ * every palette offered six, every agent guessed, and a reviewer could never
15
+ * be told "this pair is usually X and yours is Y".
16
+ *
17
+ * ## Where these answers come from
18
+ *
19
+ * They are read off ArchiMate's own definitions of the relationships, NOT
20
+ * fitted to the two records. That direction matters. Both records agree, for
21
+ * instance, that a business actor points at a constraint with `association`,
22
+ * 87 times out of 87 - but that is one adopter's house convention for
23
+ * recording sign-off, not a rule of the language, and blessing it here would
24
+ * have told every other adopter their own style was wrong. The records are
25
+ * used as a CHECK: `test/relationship-convention.test.ts` asserts that every
26
+ * convention declared here matches real practice on a record that has enough
27
+ * of that pair to have a habit, and prints the disagreements when it does not.
28
+ *
29
+ * ## Silence is an answer
30
+ *
31
+ * `conventionalRelationshipKind` returns `null` far more often than not, and
32
+ * that is the point. A pair with no entry here is a pair where the language
33
+ * genuinely permits several readings and the author must choose: an artifact
34
+ * pointing at an application function is `realization` 41 times and
35
+ * `association` 16, a capability pointing at a goal splits evenly, and two
36
+ * application functions relate by composition, triggering, flow or serving
37
+ * depending on what the author means. Declaring a convention there would
38
+ * manufacture confidence the language does not have.
39
+ *
40
+ * Nothing here is a rule the compiler enforces. A permitted kind stays
41
+ * permitted; `check` never reads this file. It is guidance, offered to a
42
+ * palette, an agent, or a reviewer, and it says nothing when it has nothing
43
+ * to say.
44
+ */
45
+ const KIND_META = new Map(conceptKinds.map((kind) => [kind.id, { layer: kind.layer, aspect: kind.aspect }]));
46
+ /** The three service elements: external behaviour a layer offers outward. */
47
+ const SERVICES = new Set([
48
+ 'businessService',
49
+ 'applicationService',
50
+ 'technologyService',
51
+ ]);
52
+ /** The three interface elements: the point at which a service is offered. */
53
+ const INTERFACES = new Set([
54
+ 'businessInterface',
55
+ 'applicationInterface',
56
+ 'technologyInterface',
57
+ ]);
58
+ const isService = (kind) => SERVICES.has(kind);
59
+ const isInterface = (kind) => INTERFACES.has(kind);
60
+ const isInternalBehaviour = (kind) => KIND_META.get(kind)?.aspect === 'behavior' && !isService(kind);
61
+ /**
62
+ * Ordered; the first rule that matches answers, and order is load-bearing
63
+ * twice. A service touching passive structure is `access` before it is
64
+ * anything else, because a service is behaviour first and an offering second.
65
+ * And an interface pointing at a service of its own layer is `assignment`,
66
+ * the interface exposing that service, while an interface pointing at
67
+ * anything else is `serving`. Both times the specific rule comes first.
68
+ */
69
+ const RULES = [
70
+ {
71
+ name: 'behaviour-uses-data',
72
+ because: 'behaviour reads, writes or creates the passive structure it touches',
73
+ kind: 'access',
74
+ when: (from, to, f, t) => t.aspect === 'passive-structure' &&
75
+ (isInternalBehaviour(from) || isService(from)) &&
76
+ // An artifact is passive structure in the technology layer, and
77
+ // behaviour pointing at one is not "reading a file": both records write
78
+ // `association` there far more often than `access`. Left to the author.
79
+ to !== 'artifact',
80
+ },
81
+ {
82
+ name: 'interface-exposes-service',
83
+ because: 'an interface is assigned to the service it exposes, not to the behaviour that uses it',
84
+ kind: 'assignment',
85
+ when: (from, to, f, t) => isInterface(from) && isService(to) && f.layer === t.layer,
86
+ },
87
+ {
88
+ name: 'service-is-offered-outward',
89
+ because: 'a service is made available to whoever uses it',
90
+ kind: 'serving',
91
+ when: (from, _to, _f, t) => (isService(from) || isInterface(from)) &&
92
+ (t.aspect === 'active-structure' || t.aspect === 'behavior'),
93
+ },
94
+ {
95
+ name: 'grouping-collects-members',
96
+ because: 'a grouping aggregates what it collects; the members exist without it',
97
+ kind: 'aggregation',
98
+ when: (from) => from === 'grouping',
99
+ },
100
+ {
101
+ name: 'artifact-realizes-what-it-implements',
102
+ because: 'an artifact is the concrete file or deployable that fulfils the thing it stands for',
103
+ kind: 'realization',
104
+ when: (from, to, _f, t) => from === 'artifact' &&
105
+ (t.aspect === 'active-structure' || t.aspect === 'passive-structure') &&
106
+ to !== 'artifact',
107
+ },
108
+ {
109
+ name: 'active-structure-performs-behaviour',
110
+ because: 'an active structure element is assigned to the internal behaviour it performs',
111
+ kind: 'assignment',
112
+ when: (from, to, f, t) => f.aspect === 'active-structure' &&
113
+ isInternalBehaviour(to) &&
114
+ f.layer === t.layer,
115
+ },
116
+ {
117
+ name: 'internal-realizes-service',
118
+ because: 'the internal element is the concrete thing that fulfils the service its layer offers',
119
+ kind: 'realization',
120
+ when: (from, to, f, t) => isService(to) &&
121
+ f.layer === t.layer &&
122
+ (f.aspect === 'active-structure' || isInternalBehaviour(from)) &&
123
+ !isInterface(from),
124
+ },
125
+ {
126
+ name: 'technology-serves-application',
127
+ // The least certain rule here. The authoring reference lists this pair as
128
+ // "realizes or serves", two readings: realization when the node IS the
129
+ // implementation, serving when it hosts one. Both records are unanimous
130
+ // for `serving`, ten times in ten, so that is what is offered - and the
131
+ // other reading stays one click away, as every non-conventional kind does.
132
+ because: 'a technology element is not part of the application it runs; it serves it',
133
+ kind: 'serving',
134
+ when: (_from, _to, f, t) => f.layer === 'technology' &&
135
+ t.layer === 'application' &&
136
+ (t.aspect === 'active-structure' || t.aspect === 'behavior'),
137
+ },
138
+ ];
139
+ const cache = new Map();
140
+ /**
141
+ * The kind practice expects from `from` to `to`, or `null` where practice has
142
+ * no expectation.
143
+ *
144
+ * Never contradicts the table: an answer is returned only when the table also
145
+ * permits it, so guidance can never point at an edge `check` would refuse.
146
+ */
147
+ export const conventionalRelationshipKind = (from, to) => {
148
+ const key = `${from}>${to}`;
149
+ const cached = cache.get(key);
150
+ if (cached !== undefined)
151
+ return cached;
152
+ const answer = compute(from, to);
153
+ cache.set(key, answer);
154
+ return answer;
155
+ };
156
+ const compute = (from, to) => {
157
+ if (!isCoreConceptKindId(from) || !isCoreConceptKindId(to))
158
+ return null;
159
+ const fromMeta = KIND_META.get(from);
160
+ const toMeta = KIND_META.get(to);
161
+ if (fromMeta === undefined || toMeta === undefined)
162
+ return null;
163
+ for (const rule of RULES) {
164
+ if (!rule.when(from, to, fromMeta, toMeta))
165
+ continue;
166
+ // Guidance that the table forbids would be worse than none: it would send
167
+ // an author at an edge `check` refuses. A rule that lands outside the
168
+ // permission is treated as having no answer for this pair.
169
+ if (!permittedRelationshipKinds(from, to).has(rule.kind))
170
+ return null;
171
+ return { kind: rule.kind, rule: rule.name, because: rule.because };
172
+ }
173
+ return null;
174
+ };
175
+ /** Every rule's name and sentence, for documentation and for the test that checks them. */
176
+ export const CONVENTION_RULES = RULES.map(({ name, kind, because }) => ({ name, kind, because }));