yarramate 1.32.0 → 1.34.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 (60) hide show
  1. package/catalogues/core-enrichment.yaml +210 -0
  2. package/dist/adapters/mcp-cli.js +28 -2
  3. package/dist/brief.js +15 -2
  4. package/dist/cli-support.d.ts +1 -1
  5. package/dist/cli-support.js +1 -1
  6. package/dist/compiler.js +70 -50
  7. package/dist/export-command.js +48 -8
  8. package/dist/governance-kinds.d.ts +24 -0
  9. package/dist/governance-kinds.js +40 -0
  10. package/dist/governance.d.ts +79 -0
  11. package/dist/governance.js +164 -0
  12. package/dist/graph-projection.d.ts +18 -0
  13. package/dist/graph-projection.js +24 -3
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +4 -0
  16. package/dist/projection.d.ts +5 -0
  17. package/dist/projection.js +4 -0
  18. package/dist/relationship-reading.d.ts +34 -1
  19. package/dist/relationship-reading.js +66 -3
  20. package/dist/responsibility-kinds.d.ts +23 -0
  21. package/dist/responsibility-kinds.js +43 -0
  22. package/dist/responsibility.d.ts +99 -0
  23. package/dist/responsibility.js +239 -0
  24. package/dist/schema-validators.generated.js +37 -25
  25. package/dist/shipped-catalogue.generated.d.ts +1 -1
  26. package/dist/shipped-catalogue.generated.js +1 -1
  27. package/dist/shipped-profile.d.ts +24 -0
  28. package/dist/shipped-profile.js +52 -0
  29. package/dist/tools/export.d.ts +21 -0
  30. package/dist/tools/export.js +40 -0
  31. package/dist/tools/table.js +15 -6
  32. package/dist/tools-entry.d.ts +5 -1
  33. package/dist/tools-entry.js +5 -1
  34. package/dist/visual-app/assets/elk-worker.min-B0rP1gRk.js +22 -0
  35. package/dist/visual-app/assets/elk.bundled-CFiHtVQt.js +24 -0
  36. package/dist/visual-app/assets/index-BaQ0vy3y.js +373 -0
  37. package/dist/visual-app/index.html +1 -1
  38. package/dist/visual-app-lib/editor.js +30875 -30029
  39. package/dist/visual-app-lib/types/graph-projection.d.ts +18 -0
  40. package/dist/visual-app-lib/types/projection.d.ts +5 -0
  41. package/dist/visual-app-lib/types/relationship-reading.d.ts +34 -1
  42. package/dist/visual-app-lib/types/responsibility-kinds.d.ts +23 -0
  43. package/dist/visual-app-lib/types/shipped-catalogue.generated.d.ts +1 -1
  44. package/dist/visual-app-lib/types/shipped-profile.d.ts +24 -0
  45. package/dist/visual-app-lib/types/visual-app/elk-layout.d.ts +4 -0
  46. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +10 -1
  47. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
  48. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +3 -1
  49. package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +5 -0
  50. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +4 -1
  51. package/docs/CONSUMING-YARRAMATE.md +1 -1
  52. package/package.json +11 -9
  53. package/schema/yarramate-governance.schema.json +91 -0
  54. package/schema/yarramate-projection.schema.json +4 -0
  55. package/schema/yarramate-responsibility.schema.json +123 -0
  56. package/schema/yarramate-visual-graph.schema.json +12 -0
  57. package/skills/yarramate-architecture/SKILL.md +2 -0
  58. package/dist/visual-app/assets/elk-worker.min-D8OVqK8T.js +0 -22
  59. package/dist/visual-app/assets/elk.bundled-DwTh4wCo.js +0 -24
  60. package/dist/visual-app/assets/index-DJYV9FNm.js +0 -373
@@ -830,6 +830,216 @@ questions:
830
830
  Add an owner reference to an accountable actor (evidence such as
831
831
  CODEOWNERS may propose it; a human confirms accountability).
832
832
 
833
+ - id: responsible-missing
834
+ wave: business
835
+ since: "1.33"
836
+ scope: subject
837
+ subjects:
838
+ kinds:
839
+ - yarramate/core@0.1#businessService
840
+ - yarramate/core@0.1#applicationService
841
+ - yarramate/core@0.1#applicationComponent
842
+ - yarramate/core@0.1#capability
843
+ trigger:
844
+ - condition: missing-linkage
845
+ kinds:
846
+ - yarramate/policy@0.2#responsible
847
+ direction: incoming
848
+ counterpartKinds:
849
+ - yarramate/core@0.1#businessActor
850
+ - yarramate/core@0.1#businessRole
851
+ - yarramate/core@0.1#businessCollaboration
852
+ - yarramate/core@0.1#stakeholder
853
+ question: >-
854
+ Who is responsible for {subject.name}?
855
+ askPlain: >-
856
+ Who actually builds, runs or delivers "{subject.name}" day to day?
857
+ materiality: >-
858
+ Accountability says whose desk a failure lands on; responsibility says
859
+ whose hands are on it. A subject with nobody responsible has nobody to
860
+ hand the work to, and a RACI row with no R is a gap the matrix reports.
861
+ authority: either
862
+ resolution: >-
863
+ Add a `responsible` relationship from the actor, role or collaboration
864
+ that does the work to the subject (a support contract or CODEOWNERS may
865
+ propose it; a person confirms it).
866
+ - id: role-idle
867
+ wave: business
868
+ since: "1.33"
869
+ scope: subject
870
+ subjects:
871
+ kinds:
872
+ - yarramate/core@0.1#businessActor
873
+ - yarramate/core@0.1#businessRole
874
+ - yarramate/core@0.1#businessCollaboration
875
+ trigger:
876
+ - condition: missing-reference
877
+ predicate: yarramate/ownership/owner
878
+ direction: incoming
879
+ - condition: missing-relationship
880
+ kinds:
881
+ - yarramate/policy@0.2#responsible
882
+ - yarramate/policy@0.2#consulted
883
+ - yarramate/policy@0.2#informed
884
+ direction: outgoing
885
+ - condition: missing-relationship
886
+ kinds:
887
+ - yarramate/core@0.1#serving
888
+ direction: incoming
889
+ question: >-
890
+ What does {subject.name} answer for?
891
+ askPlain: >-
892
+ "{subject.name}" is in the model but owns nothing, is responsible for
893
+ nothing, and is neither consulted nor informed about anything. What is
894
+ their part, or should they go?
895
+ materiality: >-
896
+ A person with no letter is either missing the edges a delivery depends
897
+ on or a name that outlived its role. A served actor is a consumer, not
898
+ a responsibility holder, and is not asked.
899
+ authority: human
900
+ resolution: >-
901
+ Give the person a letter (an owner claim on what they answer for, or a
902
+ responsible, consulted or informed relationship) or retire the subject.
903
+ - id: risk-threatens-nothing
904
+ wave: motivation
905
+ since: "1.34"
906
+ scope: subject
907
+ subjects:
908
+ kinds:
909
+ - yarramate/policy@0.3#risk
910
+ statuses:
911
+ - planned
912
+ - current
913
+ trigger:
914
+ - condition: missing-linkage
915
+ kinds:
916
+ - yarramate/core@0.1#influence
917
+ direction: outgoing
918
+ counterpartKinds:
919
+ - yarramate/core@0.1#goal
920
+ - yarramate/core@0.1#requirement
921
+ - yarramate/core@0.1#constraint
922
+ question: >-
923
+ What does {subject.name} threaten?
924
+ askPlain: >-
925
+ If "{subject.name}" comes true, which goal, requirement or constraint
926
+ takes the hit?
927
+ materiality: >-
928
+ A risk that threatens nothing named cannot be prioritised or retired:
929
+ its severity has nothing to be measured against, and nobody can say
930
+ when it has passed.
931
+ authority: either
932
+ resolution: >-
933
+ Add an `influence` relationship from the risk to the goal, requirement
934
+ or constraint it threatens.
935
+ - id: risk-unmitigated
936
+ wave: motivation
937
+ since: "1.34"
938
+ scope: subject
939
+ subjects:
940
+ kinds:
941
+ - yarramate/policy@0.3#risk
942
+ statuses:
943
+ - current
944
+ trigger:
945
+ - condition: missing-linkage
946
+ kinds:
947
+ - yarramate/core@0.1#influence
948
+ - yarramate/core@0.1#association
949
+ direction: incoming
950
+ counterpartKinds:
951
+ - yarramate/core@0.1#workPackage
952
+ - yarramate/core@0.1#deliverable
953
+ - yarramate/core@0.1#constraint
954
+ - yarramate/core@0.1#courseOfAction
955
+ question: >-
956
+ What mitigates {subject.name}?
957
+ askPlain: >-
958
+ "{subject.name}" is live. What work, deliverable, rule or decision is in
959
+ place to reduce it?
960
+ 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.
963
+ authority: either
964
+ resolution: >-
965
+ Add an `influence` relationship from the work package, deliverable,
966
+ constraint or decision that mitigates it to the risk, or retire the risk.
967
+ - id: risk-unowned
968
+ wave: motivation
969
+ since: "1.34"
970
+ scope: subject
971
+ subjects:
972
+ kinds:
973
+ - yarramate/policy@0.3#risk
974
+ statuses:
975
+ - planned
976
+ - current
977
+ trigger:
978
+ - condition: missing-claim
979
+ predicate: yarramate/ownership/owner
980
+ question: >-
981
+ Who owns {subject.name}?
982
+ askPlain: >-
983
+ Whose name is on "{subject.name}": who reviews it and decides when it
984
+ has passed?
985
+ materiality: >-
986
+ The risk owner is who a review date is asked of; without one,
987
+ risk-reviewed has no authority to sign it.
988
+ authority: either
989
+ resolution: >-
990
+ Add an owner reference to the actor or role that carries the risk.
991
+ - id: assumption-unconfirmed
992
+ wave: motivation
993
+ since: "1.34"
994
+ scope: subject
995
+ subjects:
996
+ kinds:
997
+ - yarramate/policy@0.3#assumption
998
+ statuses:
999
+ - planned
1000
+ - current
1001
+ trigger:
1002
+ - condition: missing-attestation
1003
+ topic: assumption-confirmed
1004
+ question: >-
1005
+ Has anyone confirmed {subject.name}?
1006
+ askPlain: >-
1007
+ "{subject.name}" is being built on. Has the person who can confirm it
1008
+ said so, and when?
1009
+ materiality: >-
1010
+ An unconfirmed assumption is a risk wearing a calmer name; the
1011
+ confirmation, with its date, is what turns it into a fact the delivery
1012
+ can rest on.
1013
+ authority: human
1014
+ resolution: >-
1015
+ Record an `assumption-confirmed` attestation by the role or actor that
1016
+ can confirm it, dated; or retire the assumption.
1017
+ - id: assumption-bears-on-nothing
1018
+ wave: motivation
1019
+ since: "1.34"
1020
+ scope: subject
1021
+ subjects:
1022
+ kinds:
1023
+ - yarramate/policy@0.3#assumption
1024
+ statuses:
1025
+ - planned
1026
+ - current
1027
+ trigger:
1028
+ - condition: missing-relationship
1029
+ kinds:
1030
+ - yarramate/core@0.1#association
1031
+ direction: outgoing
1032
+ question: >-
1033
+ What does {subject.name} bear on?
1034
+ askPlain: >-
1035
+ If "{subject.name}" turned out false, what in this design would change?
1036
+ materiality: >-
1037
+ An assumption that bears on nothing named is either decorative or
1038
+ missing its edges, and the log cannot say which.
1039
+ authority: either
1040
+ resolution: >-
1041
+ Add an `association` relationship from the assumption to the goal,
1042
+ requirement, constraint or subject it bears on, or retire it.
833
1043
  - id: actor-unassigned
834
1044
  wave: business
835
1045
  since: "0.1"
@@ -148,8 +148,8 @@ const runExport = (input, tool, root) => {
148
148
  stderr: '',
149
149
  };
150
150
  }
151
- // A text kind under `out`: markdown and graph are one file, rtm and
152
- // briefs a directory, as the CLI lays them out.
151
+ // A text kind under `out`: markdown and graph are one file, rtm,
152
+ // responsibility, governance and briefs a directory, as the CLI lays them out.
153
153
  if (kind === 'markdown' || kind === 'graph') {
154
154
  mkdirSync(dirname(target), { recursive: true });
155
155
  writeFileSync(target, outcome.text, 'utf8');
@@ -172,6 +172,32 @@ const runExport = (input, tool, root) => {
172
172
  stderr: '',
173
173
  };
174
174
  }
175
+ if (kind === 'governance') {
176
+ const result = outcome.result;
177
+ mkdirSync(target, { recursive: true });
178
+ writeFileSync(join(target, 'GOVERNANCE.md'), outcome.text, 'utf8');
179
+ if (result !== undefined) {
180
+ writeFileSync(join(target, 'governance.json'), `${JSON.stringify(result.log, null, 2)}\n`, 'utf8');
181
+ }
182
+ return {
183
+ exitCode: 0,
184
+ stdout: `Wrote GOVERNANCE.md and governance.json to ${out}\n`,
185
+ stderr: '',
186
+ };
187
+ }
188
+ if (kind === 'responsibility') {
189
+ const result = outcome.result;
190
+ mkdirSync(target, { recursive: true });
191
+ writeFileSync(join(target, 'RESPONSIBILITY.md'), outcome.text, 'utf8');
192
+ if (result !== undefined) {
193
+ writeFileSync(join(target, 'responsibility.json'), `${JSON.stringify(result.matrix, null, 2)}\n`, 'utf8');
194
+ }
195
+ return {
196
+ exitCode: 0,
197
+ stdout: `Wrote RESPONSIBILITY.md and responsibility.json to ${out}\n`,
198
+ stderr: '',
199
+ };
200
+ }
175
201
  if (kind === 'briefs') {
176
202
  const result = outcome.result;
177
203
  mkdirSync(target, { recursive: true });
package/dist/brief.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { conceptKinds } from './profile.js';
2
- import { RELATIONSHIP_READING, humanizeKind } from './relationship-reading.js';
2
+ import { EXTENSION_READING, RELATIONSHIP_READING, contextualReading, humanizeKind } from './relationship-reading.js';
3
3
  const coreKindNames = new Map(conceptKinds.map(({ id, name }) => [id, name]));
4
4
  const motivationKindIds = new Set(conceptKinds
5
5
  .filter(({ layer }) => layer === 'motivation')
@@ -66,6 +66,9 @@ const relationshipPhrase = (coreKind, fallbackKind, mode, content) => {
66
66
  if (coreKind === 'flow') {
67
67
  return content === undefined ? 'flows to' : `sends ${content} to`;
68
68
  }
69
+ const extension = EXTENSION_READING[fallbackKind];
70
+ if (extension !== undefined)
71
+ return extension;
69
72
  const reading = coreKind === undefined ? undefined : RELATIONSHIP_READING[coreKind];
70
73
  return reading ?? humanizeKind(fallbackKind);
71
74
  };
@@ -107,8 +110,18 @@ workspaceClaims) {
107
110
  if (claim === undefined || !('ref' in claim.object))
108
111
  continue;
109
112
  const description = claimValue(result.claims, subject.id, 'yarramate/relationship/description');
113
+ const coreKind = coreLocalKind(claim.predicate, profileContext?.relationshipKindLineages);
114
+ // A reading the endpoints decide (ADR 0160) speaks before the kind's own.
115
+ const endpointLineage = (id) => {
116
+ const kind = claimValue(result.claims, id, 'yarramate/concept/kind');
117
+ return kind === undefined ? undefined : profileContext?.conceptKindLineages.get(kind);
118
+ };
119
+ const contextual = coreKind === undefined
120
+ ? undefined
121
+ : contextualReading(endpointLineage(claim.subject), endpointLineage(claim.object.ref), coreKind);
110
122
  const entry = {
111
- phrase: relationshipPhrase(coreLocalKind(claim.predicate, profileContext?.relationshipKindLineages), claim.predicate, claimValue(result.claims, subject.id, 'yarramate/access/mode'), claimValue(result.claims, subject.id, 'yarramate/flow/content')),
123
+ phrase: contextual ??
124
+ relationshipPhrase(coreKind, claim.predicate, claimValue(result.claims, subject.id, 'yarramate/access/mode'), claimValue(result.claims, subject.id, 'yarramate/flow/content')),
112
125
  target: claim.object.ref,
113
126
  ...(description === undefined ? {} : { description }),
114
127
  };
@@ -7,7 +7,7 @@ export interface CliResult {
7
7
  export declare const isMainModule: (moduleUrl: string, entrypoint: string | undefined) => boolean;
8
8
  export declare const packageVersion: string;
9
9
  export declare const versionResult: (binary: string) => CliResult;
10
- export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json | --text]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
10
+ export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json | --text]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export responsibility <projection.yaml> <workspace.yaml> --out <directory>\n yarramate export governance <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
11
11
  export declare const diagnosticJson: (diagnostics: unknown) => string;
12
12
  export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counted?: {
13
13
  readonly documents: number;
@@ -22,7 +22,7 @@ export const versionResult = (binary) => ({
22
22
  stdout: `${binary} ${packageVersion}\n`,
23
23
  stderr: '',
24
24
  });
25
- export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json | --text]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
25
+ export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json | --text]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export responsibility <projection.yaml> <workspace.yaml> --out <directory>\n yarramate export governance <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
26
26
  export const diagnosticJson = (diagnostics) => `${JSON.stringify({
27
27
  format: 'yarramate/diagnostic-result/v1',
28
28
  diagnostics,
package/dist/compiler.js CHANGED
@@ -3,7 +3,7 @@ import { conceptKinds, relationshipPolicies, } from './profile.js';
3
3
  import { isCoreConceptKindId, matrixEndpointAspects, permittedRelationshipKinds as tablePermittedKinds, } from './relationship-matrix.js';
4
4
  import { closestCandidate, describeSchemaViolation, } from './source-document.js';
5
5
  import { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue } from './graph-claims.js';
6
- import { shippedPolicyIdentity, shippedPolicySource, } from './shipped-profile.js';
6
+ import { shippedProfileOf } from './shipped-profile.js';
7
7
  import { validateDocument, validateProfile, validatePattern } from './schema-validation.js';
8
8
  const coreProfile = 'yarramate/core@0.1';
9
9
  /**
@@ -371,58 +371,78 @@ function compileWorkspaceResolved(parsed) {
371
371
  const identity = `${value.id}@${value.version}`;
372
372
  pendingProfiles.push({ input, value, identity, positionFor });
373
373
  }
374
- const alreadyDeclaresPolicy = pendingProfiles.some(({ identity }) => identity === shippedPolicyIdentity);
375
- if (!alreadyDeclaresPolicy) {
376
- // This probe runs BEFORE the document gate that rejects a source whose
377
- // schema check failed, so it has to hold its own precondition: a source
378
- // that composes to anything but a mapping - an empty file, a comment-only
379
- // one, a bare scalar - selects no profile at all. It used to read
380
- // `.profile` through an `as` cast, which is what hid the null from the
381
- // typechecker, and an empty document crashed the whole compile with a
382
- // `TypeError` instead of the `YM201 must be object` its schema already
383
- // produces. Every other consumer of a parsed entry checks its diagnostics
384
- // first (the profile walk above, the pattern walk below); this one could
385
- // not, because it runs before that gate exists, so it narrows instead.
386
- const selected = documentInputs.some(({ entry }) => {
387
- const value = entry.value;
388
- return (typeof value === 'object' &&
389
- value !== null &&
390
- value.profile ===
391
- shippedPolicyIdentity);
392
- });
393
- const extended = pendingProfiles.some(({ value }) => value.extends === shippedPolicyIdentity);
394
- if (selected || extended) {
395
- const input = {
396
- path: 'yarramate:profile:yarramate/policy@0.1',
397
- source: shippedPolicySource,
398
- };
399
- const { entry, fresh } = parseWorkspaceSource(input);
400
- const value = entry.value;
401
- if (entry.schemaDiagnostics.length > 0) {
402
- profileDiagnostics.push(...entry.schemaDiagnostics);
403
- }
404
- else if (!validateProfile(value)) {
405
- for (const error of validateProfile.errors ?? []) {
406
- profileDiagnostics.push({
407
- severity: 'error',
408
- code: 'YM201',
409
- message: `Profile schema violation: ${describeSchemaViolation(error)}`,
410
- path: input.path,
411
- pointer: error.instancePath || '/',
412
- line: 1,
413
- column: 1,
414
- });
415
- }
416
- }
417
- else {
418
- pendingProfiles.push({
419
- input,
420
- value,
421
- identity: shippedPolicyIdentity,
422
- positionFor: positionReader(input.source, entry.positions, fresh),
374
+ // The shipped optional profiles (ADR 0095; policy@0.2 per ADR 0159). Each
375
+ // is injected when a document selects it or a profile extends it, and a
376
+ // shipped profile's own parent follows it in, so `extends:
377
+ // yarramate/policy@0.2` brings 0.1 along. A workspace file that declares
378
+ // the same identity wins; the shipped copy is not added beside it.
379
+ //
380
+ // This probe runs BEFORE the document gate that rejects a source whose
381
+ // schema check failed, so it has to hold its own precondition: a source
382
+ // that composes to anything but a mapping - an empty file, a comment-only
383
+ // one, a bare scalar - selects no profile at all. It used to read
384
+ // `.profile` through an `as` cast, which is what hid the null from the
385
+ // typechecker, and an empty document crashed the whole compile with a
386
+ // `TypeError` instead of the `YM201 must be object` its schema already
387
+ // produces. Every other consumer of a parsed entry checks its diagnostics
388
+ // first (the profile walk above, the pattern walk below); this one could
389
+ // not, because it runs before that gate exists, so it narrows instead.
390
+ const declaredIdentities = new Set(pendingProfiles.map(({ identity }) => identity));
391
+ const requestedIdentities = new Set();
392
+ for (const { entry } of documentInputs) {
393
+ const value = entry.value;
394
+ if (typeof value !== 'object' || value === null)
395
+ continue;
396
+ const profile = value.profile;
397
+ if (typeof profile === 'string')
398
+ requestedIdentities.add(profile);
399
+ }
400
+ for (const { value } of pendingProfiles)
401
+ requestedIdentities.add(value.extends);
402
+ const wantedShipped = [];
403
+ const want = (identity) => {
404
+ if (declaredIdentities.has(identity) || wantedShipped.includes(identity))
405
+ return;
406
+ const shipped = shippedProfileOf(identity);
407
+ if (shipped === undefined)
408
+ return;
409
+ wantedShipped.push(identity);
410
+ want(shipped.extends);
411
+ };
412
+ for (const identity of requestedIdentities)
413
+ want(identity);
414
+ for (const identity of wantedShipped) {
415
+ const shipped = shippedProfileOf(identity);
416
+ const input = {
417
+ path: `yarramate:profile:${identity}`,
418
+ source: shipped.source,
419
+ };
420
+ const { entry, fresh } = parseWorkspaceSource(input);
421
+ const value = entry.value;
422
+ if (entry.schemaDiagnostics.length > 0) {
423
+ profileDiagnostics.push(...entry.schemaDiagnostics);
424
+ }
425
+ else if (!validateProfile(value)) {
426
+ for (const error of validateProfile.errors ?? []) {
427
+ profileDiagnostics.push({
428
+ severity: 'error',
429
+ code: 'YM201',
430
+ message: `Profile schema violation: ${describeSchemaViolation(error)}`,
431
+ path: input.path,
432
+ pointer: error.instancePath || '/',
433
+ line: 1,
434
+ column: 1,
423
435
  });
424
436
  }
425
437
  }
438
+ else {
439
+ pendingProfiles.push({
440
+ input,
441
+ value,
442
+ identity,
443
+ positionFor: positionReader(input.source, entry.positions, fresh),
444
+ });
445
+ }
426
446
  }
427
447
  let unresolvedProfiles = pendingProfiles.sort((left, right) => left.identity.localeCompare(right.identity) ||
428
448
  left.input.path.localeCompare(right.input.path));
@@ -8,7 +8,7 @@ import { humanDiagnostics, usage, } from './cli-support.js';
8
8
  import { evaluateProjection, renderProjectionMarkdown } from './projection.js';
9
9
  import { createFileSystemStore } from './source-store.js';
10
10
  import { posixDirectoryOf } from './apply-command.js';
11
- import { briefsFromResult, exportBriefs, exportGraph, exportMarkdown, exportRtm, exportWorkbook, } from './tools/export.js';
11
+ import { briefsFromResult, exportBriefs, exportGraph, exportMarkdown, exportGovernance, exportResponsibility, exportRtm, exportWorkbook, } from './tools/export.js';
12
12
  import { compileOf, readSource, } from './tools/workspace.js';
13
13
  import { loadWorkspaceManifest } from './workspace.js';
14
14
  // Every kind is derived in `tools/export.ts` (ADR 0156): this command
@@ -70,7 +70,16 @@ const parseExportOptions = (options) => {
70
70
  export function runExportCommand(options, cwd) {
71
71
  const [kind, ...rest] = options;
72
72
  if (kind === undefined ||
73
- !['graph', 'markdown', 'briefs', 'rtm', 'likec4', 'xlsx'].includes(kind)) {
73
+ ![
74
+ 'graph',
75
+ 'markdown',
76
+ 'briefs',
77
+ 'rtm',
78
+ 'responsibility',
79
+ 'governance',
80
+ 'likec4',
81
+ 'xlsx',
82
+ ].includes(kind)) {
74
83
  return { exitCode: 2, stdout: '', stderr: usage };
75
84
  }
76
85
  const parsed = parseExportOptions(rest);
@@ -113,17 +122,20 @@ export function runExportCommand(options, cwd) {
113
122
  };
114
123
  }
115
124
  const usesChanged = parsed.changed !== undefined;
116
- const expectedPositionals = kind === 'graph' || kind === 'rtm' || usesChanged ? 1 : 2;
125
+ const wholeWorkspace = kind === 'graph' || kind === 'rtm' || kind === 'governance';
126
+ const expectedPositionals = wholeWorkspace || usesChanged ? 1 : 2;
117
127
  const workspacePath = parsed.positionals[expectedPositionals - 1];
118
- const projectionPath = kind === 'graph' || kind === 'rtm' || usesChanged
119
- ? undefined
120
- : parsed.positionals[0];
128
+ const projectionPath = wholeWorkspace || usesChanged ? undefined : parsed.positionals[0];
121
129
  if (parsed.positionals.length !== expectedPositionals ||
122
130
  workspacePath === undefined ||
123
131
  parsed.json ||
124
- (usesChanged && (kind === 'graph' || kind === 'rtm')) ||
132
+ (usesChanged && (wholeWorkspace || kind === 'responsibility')) ||
125
133
  (parsed.budget !== undefined && kind !== 'briefs') ||
126
- ((kind === 'briefs' || kind === 'rtm' || kind === 'xlsx') &&
134
+ ((kind === 'briefs' ||
135
+ kind === 'rtm' ||
136
+ kind === 'responsibility' ||
137
+ kind === 'governance' ||
138
+ kind === 'xlsx') &&
127
139
  parsed.out === undefined)) {
128
140
  return { exitCode: 2, stdout: '', stderr: usage };
129
141
  }
@@ -171,6 +183,34 @@ export function runExportCommand(options, cwd) {
171
183
  stderr: '',
172
184
  };
173
185
  }
186
+ if (kind === 'governance') {
187
+ const exported = exportGovernance(tool);
188
+ if (!exported.ok)
189
+ return failedTool(exported);
190
+ const { markdown, log } = exported.result;
191
+ writeText(join(parsed.out, 'GOVERNANCE.md'), markdown);
192
+ writeText(join(parsed.out, 'governance.json'), `${JSON.stringify(log, null, 2)}\n`);
193
+ const gaps = log.summary.unowned + log.summary.unmitigated + log.summary.unconfirmed + log.summary.unreviewed;
194
+ return {
195
+ exitCode: 0,
196
+ stdout: `Wrote GOVERNANCE.md and governance.json (${log.summary.rows} row${log.summary.rows === 1 ? '' : 's'}, ${gaps} gap${gaps === 1 ? '' : 's'}) to ${parsed.out}\n`,
197
+ stderr: '',
198
+ };
199
+ }
200
+ if (kind === 'responsibility') {
201
+ const exported = exportResponsibility(tool, projectionPath);
202
+ if (!exported.ok)
203
+ return failedTool(exported);
204
+ const { markdown, matrix } = exported.result;
205
+ writeText(join(parsed.out, 'RESPONSIBILITY.md'), markdown);
206
+ writeText(join(parsed.out, 'responsibility.json'), `${JSON.stringify(matrix, null, 2)}\n`);
207
+ const gaps = matrix.summary.noAccountable + matrix.summary.noResponsible;
208
+ return {
209
+ exitCode: 0,
210
+ stdout: `Wrote RESPONSIBILITY.md and responsibility.json (${matrix.summary.rows} row${matrix.summary.rows === 1 ? '' : 's'}, ${gaps} gap${gaps === 1 ? '' : 's'}) to ${parsed.out}\n`,
211
+ stderr: '',
212
+ };
213
+ }
174
214
  if (kind === 'graph') {
175
215
  const exported = exportGraph(tool);
176
216
  if (!exported.ok)
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The governance concept kinds (#560, ADR 0160): `yarramate/policy@0.3#risk`
3
+ * and `#assumption`, each a subkind of core `assessment`. A decision is the
4
+ * adopter's trial kind under `courseOfAction` until the trial reports, so
5
+ * it is not here. Review dates are attestations on the shipped topics
6
+ * below. Pure, so the canvas projection, the log builder and the brief read
7
+ * one table.
8
+ */
9
+ export declare const GOVERNANCE_PROFILE = "yarramate/policy@0.3";
10
+ export type GovernanceType = 'risk' | 'assumption';
11
+ export declare const GOVERNANCE_KINDS: Readonly<Record<GovernanceType, string>>;
12
+ /**
13
+ * The attestation topic that records a review of each type: a risk owner
14
+ * or the client role attests `risk-reviewed` on a risk, and the client
15
+ * confirms an assumption with `assumption-confirmed`. How old is too old is
16
+ * the adopter's threshold, not the engine's.
17
+ */
18
+ export declare const REVIEW_TOPICS: Readonly<Record<GovernanceType, string>>;
19
+ /**
20
+ * Whether a concept kind is a risk or an assumption, read through its
21
+ * lineage (ancestor-first) so a profile's own subkind counts; null for
22
+ * every other kind.
23
+ */
24
+ export declare const governanceTypeOf: (lineage: readonly string[] | undefined, kind: string) => GovernanceType | null;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The governance concept kinds (#560, ADR 0160): `yarramate/policy@0.3#risk`
3
+ * and `#assumption`, each a subkind of core `assessment`. A decision is the
4
+ * adopter's trial kind under `courseOfAction` until the trial reports, so
5
+ * it is not here. Review dates are attestations on the shipped topics
6
+ * below. Pure, so the canvas projection, the log builder and the brief read
7
+ * one table.
8
+ */
9
+ export const GOVERNANCE_PROFILE = 'yarramate/policy@0.3';
10
+ export const GOVERNANCE_KINDS = {
11
+ risk: `${GOVERNANCE_PROFILE}#risk`,
12
+ assumption: `${GOVERNANCE_PROFILE}#assumption`,
13
+ };
14
+ /**
15
+ * The attestation topic that records a review of each type: a risk owner
16
+ * or the client role attests `risk-reviewed` on a risk, and the client
17
+ * confirms an assumption with `assumption-confirmed`. How old is too old is
18
+ * the adopter's threshold, not the engine's.
19
+ */
20
+ export const REVIEW_TOPICS = {
21
+ risk: 'risk-reviewed',
22
+ assumption: 'assumption-confirmed',
23
+ };
24
+ const TYPE_BY_KIND = {
25
+ [GOVERNANCE_KINDS.risk]: 'risk',
26
+ [GOVERNANCE_KINDS.assumption]: 'assumption',
27
+ };
28
+ /**
29
+ * Whether a concept kind is a risk or an assumption, read through its
30
+ * lineage (ancestor-first) so a profile's own subkind counts; null for
31
+ * every other kind.
32
+ */
33
+ export const governanceTypeOf = (lineage, kind) => {
34
+ for (const member of lineage ?? [kind]) {
35
+ const type = TYPE_BY_KIND[member];
36
+ if (type !== undefined)
37
+ return type;
38
+ }
39
+ return TYPE_BY_KIND[kind] ?? null;
40
+ };