yarramate 1.33.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.
- package/catalogues/core-enrichment.yaml +140 -0
- package/dist/adapters/mcp-cli.js +14 -1
- package/dist/brief.js +12 -2
- package/dist/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/export-command.js +21 -7
- package/dist/governance-kinds.d.ts +24 -0
- package/dist/governance-kinds.js +40 -0
- package/dist/governance.d.ts +79 -0
- package/dist/governance.js +164 -0
- package/dist/graph-projection.d.ts +6 -0
- package/dist/graph-projection.js +18 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/relationship-reading.d.ts +22 -0
- package/dist/relationship-reading.js +38 -0
- package/dist/shipped-catalogue.generated.d.ts +1 -1
- package/dist/shipped-catalogue.generated.js +1 -1
- package/dist/shipped-profile.d.ts +6 -3
- package/dist/shipped-profile.js +19 -0
- package/dist/tools/export.d.ts +10 -0
- package/dist/tools/export.js +14 -0
- package/dist/tools/table.js +8 -5
- package/dist/tools-entry.d.ts +3 -1
- package/dist/tools-entry.js +3 -1
- package/dist/visual-app/assets/{elk.bundled-NyuRbCog.js → elk.bundled-CFiHtVQt.js} +1 -1
- package/dist/visual-app/assets/{index-Bg4LA2rt.js → index-BaQ0vy3y.js} +1 -1
- package/dist/visual-app/index.html +1 -1
- package/dist/visual-app-lib/editor.js +22987 -22964
- package/dist/visual-app-lib/types/graph-projection.d.ts +6 -0
- package/dist/visual-app-lib/types/relationship-reading.d.ts +22 -0
- package/dist/visual-app-lib/types/shipped-catalogue.generated.d.ts +1 -1
- package/dist/visual-app-lib/types/shipped-profile.d.ts +6 -3
- package/dist/visual-app-lib/types/visual-app/elk-layout.d.ts +2 -0
- package/docs/CONSUMING-YARRAMATE.md +1 -1
- package/package.json +2 -1
- package/schema/yarramate-governance.schema.json +91 -0
- package/schema/yarramate-visual-graph.schema.json +4 -0
- package/skills/yarramate-architecture/SKILL.md +1 -0
|
@@ -900,6 +900,146 @@ questions:
|
|
|
900
900
|
resolution: >-
|
|
901
901
|
Give the person a letter (an owner claim on what they answer for, or a
|
|
902
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.
|
|
903
1043
|
- id: actor-unassigned
|
|
904
1044
|
wave: business
|
|
905
1045
|
since: "0.1"
|
package/dist/adapters/mcp-cli.js
CHANGED
|
@@ -149,7 +149,7 @@ const runExport = (input, tool, root) => {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
// A text kind under `out`: markdown and graph are one file, rtm,
|
|
152
|
-
// responsibility and briefs a directory, as the CLI lays them out.
|
|
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,19 @@ 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
|
+
}
|
|
175
188
|
if (kind === 'responsibility') {
|
|
176
189
|
const result = outcome.result;
|
|
177
190
|
mkdirSync(target, { recursive: true });
|
package/dist/brief.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { conceptKinds } from './profile.js';
|
|
2
|
-
import { EXTENSION_READING, 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')
|
|
@@ -110,8 +110,18 @@ workspaceClaims) {
|
|
|
110
110
|
if (claim === undefined || !('ref' in claim.object))
|
|
111
111
|
continue;
|
|
112
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);
|
|
113
122
|
const entry = {
|
|
114
|
-
phrase:
|
|
123
|
+
phrase: contextual ??
|
|
124
|
+
relationshipPhrase(coreKind, claim.predicate, claimValue(result.claims, subject.id, 'yarramate/access/mode'), claimValue(result.claims, subject.id, 'yarramate/flow/content')),
|
|
115
125
|
target: claim.object.ref,
|
|
116
126
|
...(description === undefined ? {} : { description }),
|
|
117
127
|
};
|
package/dist/cli-support.d.ts
CHANGED
|
@@ -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 responsibility <projection.yaml> <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;
|
package/dist/cli-support.js
CHANGED
|
@@ -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 responsibility <projection.yaml> <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/export-command.js
CHANGED
|
@@ -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, exportResponsibility, 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
|
|
@@ -76,6 +76,7 @@ export function runExportCommand(options, cwd) {
|
|
|
76
76
|
'briefs',
|
|
77
77
|
'rtm',
|
|
78
78
|
'responsibility',
|
|
79
|
+
'governance',
|
|
79
80
|
'likec4',
|
|
80
81
|
'xlsx',
|
|
81
82
|
].includes(kind)) {
|
|
@@ -121,20 +122,19 @@ export function runExportCommand(options, cwd) {
|
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
const usesChanged = parsed.changed !== undefined;
|
|
124
|
-
const
|
|
125
|
+
const wholeWorkspace = kind === 'graph' || kind === 'rtm' || kind === 'governance';
|
|
126
|
+
const expectedPositionals = wholeWorkspace || usesChanged ? 1 : 2;
|
|
125
127
|
const workspacePath = parsed.positionals[expectedPositionals - 1];
|
|
126
|
-
const projectionPath =
|
|
127
|
-
? undefined
|
|
128
|
-
: parsed.positionals[0];
|
|
128
|
+
const projectionPath = wholeWorkspace || usesChanged ? undefined : parsed.positionals[0];
|
|
129
129
|
if (parsed.positionals.length !== expectedPositionals ||
|
|
130
130
|
workspacePath === undefined ||
|
|
131
131
|
parsed.json ||
|
|
132
|
-
(usesChanged &&
|
|
133
|
-
(kind === 'graph' || kind === 'rtm' || kind === 'responsibility')) ||
|
|
132
|
+
(usesChanged && (wholeWorkspace || kind === 'responsibility')) ||
|
|
134
133
|
(parsed.budget !== undefined && kind !== 'briefs') ||
|
|
135
134
|
((kind === 'briefs' ||
|
|
136
135
|
kind === 'rtm' ||
|
|
137
136
|
kind === 'responsibility' ||
|
|
137
|
+
kind === 'governance' ||
|
|
138
138
|
kind === 'xlsx') &&
|
|
139
139
|
parsed.out === undefined)) {
|
|
140
140
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
@@ -183,6 +183,20 @@ export function runExportCommand(options, cwd) {
|
|
|
183
183
|
stderr: '',
|
|
184
184
|
};
|
|
185
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
|
+
}
|
|
186
200
|
if (kind === 'responsibility') {
|
|
187
201
|
const exported = exportResponsibility(tool, projectionPath);
|
|
188
202
|
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
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The governance log (#560, ADR 0160): the RAID log's risks and
|
|
3
|
+
* assumptions as the model holds them, derived the way the responsibility
|
|
4
|
+
* matrix is derived and never authored. One row per risk or assumption:
|
|
5
|
+
* its owner and status, what it threatens or bears on, what mitigates it,
|
|
6
|
+
* the latest review attestation, what it supersedes, and the groupings it
|
|
7
|
+
* aggregates into (an adopter's severity or likelihood classes, which the
|
|
8
|
+
* engine carries without knowing the scale). Issues and open questions are
|
|
9
|
+
* the adopter's and the interrogation's; the engine never sees an issue.
|
|
10
|
+
*
|
|
11
|
+
* Deterministic: identical inputs give identical bytes, so CI can diff it.
|
|
12
|
+
*/
|
|
13
|
+
import type { ResolvedProfileContext, SemanticGraph } from './compiler.js';
|
|
14
|
+
import { type GovernanceType } from './governance-kinds.js';
|
|
15
|
+
export interface GovernanceSource {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly line: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GovernanceRef {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
}
|
|
23
|
+
/** The latest review attestation on a row, by date. */
|
|
24
|
+
export interface GovernanceReview {
|
|
25
|
+
readonly topic: string;
|
|
26
|
+
readonly on: string;
|
|
27
|
+
readonly by: string;
|
|
28
|
+
readonly source: GovernanceSource;
|
|
29
|
+
}
|
|
30
|
+
export interface GovernanceRow {
|
|
31
|
+
readonly subject: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly kind: string;
|
|
34
|
+
readonly type: GovernanceType;
|
|
35
|
+
readonly status: string | null;
|
|
36
|
+
readonly description?: string;
|
|
37
|
+
readonly owner: GovernanceRef | null;
|
|
38
|
+
/** A risk's outgoing `influence` targets: what it threatens. */
|
|
39
|
+
readonly threatens: readonly GovernanceRef[];
|
|
40
|
+
/** An assumption's outgoing `association` targets: what it bears on. */
|
|
41
|
+
readonly bearsOn: readonly GovernanceRef[];
|
|
42
|
+
/** Incoming `influence` or `association` sources on a risk: what mitigates it. */
|
|
43
|
+
readonly mitigatedBy: readonly GovernanceRef[];
|
|
44
|
+
readonly review: GovernanceReview | null;
|
|
45
|
+
readonly supersedes: readonly string[];
|
|
46
|
+
/** The groupings that aggregate this row: an adopter's rating classes. */
|
|
47
|
+
readonly groupedBy: readonly GovernanceRef[];
|
|
48
|
+
}
|
|
49
|
+
export interface GovernanceLog {
|
|
50
|
+
readonly format: 'yarramate/governance/v1';
|
|
51
|
+
readonly workspace: string;
|
|
52
|
+
readonly summary: {
|
|
53
|
+
readonly rows: number;
|
|
54
|
+
readonly risks: number;
|
|
55
|
+
readonly assumptions: number;
|
|
56
|
+
readonly unowned: number;
|
|
57
|
+
readonly unmitigated: number;
|
|
58
|
+
readonly unconfirmed: number;
|
|
59
|
+
readonly unreviewed: number;
|
|
60
|
+
};
|
|
61
|
+
readonly rows: readonly GovernanceRow[];
|
|
62
|
+
readonly gaps: {
|
|
63
|
+
/** Rows with no owner. */
|
|
64
|
+
readonly unowned: readonly string[];
|
|
65
|
+
/** Current risks nothing mitigates. */
|
|
66
|
+
readonly unmitigated: readonly string[];
|
|
67
|
+
/** Assumptions with no `assumption-confirmed` attestation. */
|
|
68
|
+
readonly unconfirmed: readonly string[];
|
|
69
|
+
/** Risks with no `risk-reviewed` attestation. How old is too old is the adopter's. */
|
|
70
|
+
readonly unreviewed: readonly string[];
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface GovernanceOptions {
|
|
74
|
+
/** The subjects that may get a row; absent, every risk and assumption. */
|
|
75
|
+
readonly rows?: readonly string[];
|
|
76
|
+
}
|
|
77
|
+
export declare function buildGovernanceLog(workspace: string, graph: SemanticGraph, profileContext: ResolvedProfileContext, options?: GovernanceOptions): GovernanceLog;
|
|
78
|
+
/** The log as one markdown table a person reads, with the gaps under it. */
|
|
79
|
+
export declare function renderGovernanceMarkdown(log: GovernanceLog): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ATTESTATION_PREDICATE_PREFIX, parseAttestationClaimValue, } from './graph-claims.js';
|
|
2
|
+
import { REVIEW_TOPICS, governanceTypeOf, } from './governance-kinds.js';
|
|
3
|
+
const CORE = 'yarramate/core@0.1#';
|
|
4
|
+
const documentOf = (subject) => {
|
|
5
|
+
const hash = subject.indexOf('#');
|
|
6
|
+
return hash === -1 ? '' : subject.slice(0, hash);
|
|
7
|
+
};
|
|
8
|
+
const bySubjectOrder = (left, right) => documentOf(left).localeCompare(documentOf(right)) ||
|
|
9
|
+
left.localeCompare(right);
|
|
10
|
+
const citation = (claim) => ({
|
|
11
|
+
path: claim.source.path,
|
|
12
|
+
line: claim.source.line,
|
|
13
|
+
});
|
|
14
|
+
export function buildGovernanceLog(workspace, graph, profileContext, options = {}) {
|
|
15
|
+
const conceptIds = new Set(graph.subjects.filter(({ type }) => type === 'concept').map(({ id }) => id));
|
|
16
|
+
const relationshipIds = new Set(graph.subjects
|
|
17
|
+
.filter(({ type }) => type === 'relationship')
|
|
18
|
+
.map(({ id }) => id));
|
|
19
|
+
const values = new Map();
|
|
20
|
+
const refs = new Map();
|
|
21
|
+
const attestations = new Map();
|
|
22
|
+
const definingClaims = [];
|
|
23
|
+
for (const claim of graph.claims) {
|
|
24
|
+
if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
|
|
25
|
+
definingClaims.push(claim);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if ('ref' in claim.object) {
|
|
29
|
+
const key = `${claim.subject} ${claim.predicate}`;
|
|
30
|
+
refs.set(key, [...(refs.get(key) ?? []), claim.object.ref]);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (claim.predicate.startsWith(ATTESTATION_PREDICATE_PREFIX)) {
|
|
34
|
+
attestations.set(claim.subject, [
|
|
35
|
+
...(attestations.get(claim.subject) ?? []),
|
|
36
|
+
claim,
|
|
37
|
+
]);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
values.set(`${claim.subject} ${claim.predicate}`, claim.object.value);
|
|
41
|
+
}
|
|
42
|
+
const value = (subject, predicate) => values.get(`${subject} ${predicate}`);
|
|
43
|
+
const refsOf = (subject, predicate) => refs.get(`${subject} ${predicate}`) ?? [];
|
|
44
|
+
const kindOf = (subject) => value(subject, 'yarramate/concept/kind') ?? '';
|
|
45
|
+
const nameOf = (subject) => value(subject, 'yarramate/concept/name') ?? subject;
|
|
46
|
+
const refOf = (id) => ({ id, name: nameOf(id) });
|
|
47
|
+
const typeOf = (subject) => {
|
|
48
|
+
const kind = kindOf(subject);
|
|
49
|
+
return governanceTypeOf(profileContext.conceptKindLineages.get(kind), kind);
|
|
50
|
+
};
|
|
51
|
+
const coreKindOf = (relationshipKind) => (profileContext.relationshipKindLineages.get(relationshipKind) ?? [
|
|
52
|
+
relationshipKind,
|
|
53
|
+
])[0];
|
|
54
|
+
const rowIds = (options.rows ?? [...conceptIds])
|
|
55
|
+
.filter((id) => conceptIds.has(id) && typeOf(id) !== null)
|
|
56
|
+
.sort(bySubjectOrder);
|
|
57
|
+
const rowSet = new Set(rowIds);
|
|
58
|
+
const outgoing = new Map();
|
|
59
|
+
const incoming = new Map();
|
|
60
|
+
for (const claim of definingClaims) {
|
|
61
|
+
if (!('ref' in claim.object))
|
|
62
|
+
continue;
|
|
63
|
+
const core = coreKindOf(claim.predicate);
|
|
64
|
+
if (rowSet.has(claim.subject)) {
|
|
65
|
+
outgoing.set(claim.subject, [
|
|
66
|
+
...(outgoing.get(claim.subject) ?? []),
|
|
67
|
+
{ core, to: claim.object.ref },
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
if (rowSet.has(claim.object.ref)) {
|
|
71
|
+
incoming.set(claim.object.ref, [
|
|
72
|
+
...(incoming.get(claim.object.ref) ?? []),
|
|
73
|
+
{ core, from: claim.subject },
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const targets = (subject, core) => (outgoing.get(subject) ?? [])
|
|
78
|
+
.filter((edge) => edge.core === `${CORE}${core}`)
|
|
79
|
+
.map(({ to }) => to)
|
|
80
|
+
.sort(bySubjectOrder)
|
|
81
|
+
.map(refOf);
|
|
82
|
+
const sources = (subject, cores) => (incoming.get(subject) ?? [])
|
|
83
|
+
.filter((edge) => cores.some((core) => edge.core === `${CORE}${core}`))
|
|
84
|
+
.map(({ from }) => from)
|
|
85
|
+
.sort(bySubjectOrder)
|
|
86
|
+
.map(refOf);
|
|
87
|
+
const latestReview = (subject, topic) => {
|
|
88
|
+
let latest = null;
|
|
89
|
+
for (const claim of attestations.get(subject) ?? []) {
|
|
90
|
+
if (claim.predicate !== `${ATTESTATION_PREDICATE_PREFIX}${topic}`)
|
|
91
|
+
continue;
|
|
92
|
+
if (!('value' in claim.object))
|
|
93
|
+
continue;
|
|
94
|
+
const parts = parseAttestationClaimValue(claim.object.value);
|
|
95
|
+
if (parts === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
if (latest === null || parts.on >= latest.on) {
|
|
98
|
+
latest = { topic, on: parts.on, by: parts.by, source: citation(claim) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return latest;
|
|
102
|
+
};
|
|
103
|
+
const rows = rowIds.map((subject) => {
|
|
104
|
+
const type = typeOf(subject);
|
|
105
|
+
const owner = refsOf(subject, 'yarramate/ownership/owner')[0];
|
|
106
|
+
const description = value(subject, 'yarramate/concept/description');
|
|
107
|
+
return {
|
|
108
|
+
subject,
|
|
109
|
+
name: nameOf(subject),
|
|
110
|
+
kind: kindOf(subject),
|
|
111
|
+
type,
|
|
112
|
+
status: value(subject, 'yarramate/lifecycle/status') ?? null,
|
|
113
|
+
...(description === undefined ? {} : { description }),
|
|
114
|
+
owner: owner === undefined ? null : refOf(owner),
|
|
115
|
+
threatens: type === 'risk' ? targets(subject, 'influence') : [],
|
|
116
|
+
bearsOn: type === 'assumption' ? targets(subject, 'association') : [],
|
|
117
|
+
mitigatedBy: type === 'risk' ? sources(subject, ['influence', 'association']) : [],
|
|
118
|
+
review: latestReview(subject, REVIEW_TOPICS[type]),
|
|
119
|
+
supersedes: [...refsOf(subject, 'yarramate/lineage/supersedes')].sort(),
|
|
120
|
+
groupedBy: sources(subject, ['aggregation']),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
const ids = (predicate) => rows.filter(predicate).map(({ subject }) => subject);
|
|
124
|
+
const unowned = ids((row) => row.owner === null);
|
|
125
|
+
const unmitigated = ids((row) => row.type === 'risk' && row.status === 'current' && row.mitigatedBy.length === 0);
|
|
126
|
+
const unconfirmed = ids((row) => row.type === 'assumption' && row.review === null);
|
|
127
|
+
const unreviewed = ids((row) => row.type === 'risk' && row.review === null);
|
|
128
|
+
return {
|
|
129
|
+
format: 'yarramate/governance/v1',
|
|
130
|
+
workspace,
|
|
131
|
+
summary: {
|
|
132
|
+
rows: rows.length,
|
|
133
|
+
risks: rows.filter(({ type }) => type === 'risk').length,
|
|
134
|
+
assumptions: rows.filter(({ type }) => type === 'assumption').length,
|
|
135
|
+
unowned: unowned.length,
|
|
136
|
+
unmitigated: unmitigated.length,
|
|
137
|
+
unconfirmed: unconfirmed.length,
|
|
138
|
+
unreviewed: unreviewed.length,
|
|
139
|
+
},
|
|
140
|
+
rows,
|
|
141
|
+
gaps: { unowned, unmitigated, unconfirmed, unreviewed },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const escapeCell = (text) => text.replaceAll('|', '\\|');
|
|
145
|
+
const names = (entries) => entries.map(({ name }) => escapeCell(name)).join(', ');
|
|
146
|
+
const list = (ids) => ids.length === 0 ? 'none' : ids.map((id) => `\`${id}\``).join(', ');
|
|
147
|
+
/** The log as one markdown table a person reads, with the gaps under it. */
|
|
148
|
+
export function renderGovernanceMarkdown(log) {
|
|
149
|
+
const lines = ['# Governance log', ''];
|
|
150
|
+
lines.push(`Workspace \`${log.workspace}\`: ${log.summary.risks} risk${log.summary.risks === 1 ? '' : 's'}, ${log.summary.assumptions} assumption${log.summary.assumptions === 1 ? '' : 's'}. Derived from the record; edit the native documents, not this file.`, '');
|
|
151
|
+
if (log.rows.length === 0) {
|
|
152
|
+
lines.push('_No rows._', '');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
lines.push('| Subject | Type | Status | Owner | Threatens / bears on | Mitigated by | Review | Grouped by |', '|---|---|---|---|---|---|---|---|');
|
|
156
|
+
for (const row of log.rows) {
|
|
157
|
+
const review = row.review === null ? '' : `${row.review.topic} ${row.review.on} by ${escapeCell(row.review.by)}`;
|
|
158
|
+
lines.push(`| ${escapeCell(row.name)} (\`${row.subject}\`) | ${row.type} | ${row.status ?? ''} | ${row.owner === null ? '' : escapeCell(row.owner.name)} | ${names(row.type === 'risk' ? row.threatens : row.bearsOn)} | ${names(row.mitigatedBy)} | ${review} | ${names(row.groupedBy)} |`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
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)}`, '');
|
|
163
|
+
return `${lines.join('\n')}\n`;
|
|
164
|
+
}
|
|
@@ -78,6 +78,12 @@ export interface CanvasEdge {
|
|
|
78
78
|
* kind. The canvas draws these only when the view asks (`showResponsibility`).
|
|
79
79
|
*/
|
|
80
80
|
readonly responsibility?: ResponsibilityLetter | null;
|
|
81
|
+
/**
|
|
82
|
+
* A reading the endpoints decide (ADR 0160): "threatens" for an
|
|
83
|
+
* influence from a risk, "mitigates" for one into a risk, "bears on" for
|
|
84
|
+
* an association from an assumption. Absent, the kind's own reading.
|
|
85
|
+
*/
|
|
86
|
+
readonly reading?: string;
|
|
81
87
|
readonly from: string;
|
|
82
88
|
readonly to: string;
|
|
83
89
|
readonly name: string | null;
|