yarramate 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/catalogues/core-enrichment.yaml +225 -2
- package/dist/adapters/visual/session-server.js +16 -1
- package/dist/adapters/visual/view-identity.d.ts +1 -1
- package/dist/adapters/visual/view-identity.js +61 -3
- package/dist/adapters/visual/wire.d.ts +30 -0
- package/dist/adapters/visual/workspace-model.d.ts +23 -3
- package/dist/adapters/visual/workspace-model.js +52 -2
- package/dist/ask-command.js +29 -13
- package/dist/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26 -5
- package/dist/concept-drafting.d.ts +13 -2
- package/dist/concept-drafting.js +15 -3
- package/dist/design-command.js +83 -2
- package/dist/evidence.js +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/interrogate-command.d.ts +28 -1
- package/dist/interrogate-command.js +24 -4
- package/dist/interrogation-entry.d.ts +1 -1
- package/dist/layout-direction.d.ts +14 -0
- package/dist/layout-direction.js +5 -0
- package/dist/projection.d.ts +22 -6
- package/dist/projection.js +23 -2
- package/dist/relationship-drafting.d.ts +21 -2
- package/dist/relationship-drafting.js +33 -3
- package/dist/subject-references.js +5 -0
- package/dist/visual-app/assets/index-C3i9SxTe.js +394 -0
- package/dist/visual-app/assets/index-Rkq6smL2.css +1 -0
- package/dist/visual-app/index.html +2 -2
- package/dist/visual-app-lib/editor.js +28464 -26953
- package/dist/visual-app-lib/styles.css +1 -1
- package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +1 -1
- package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +30 -0
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +23 -3
- package/dist/visual-app-lib/types/concept-drafting.d.ts +13 -2
- package/dist/visual-app-lib/types/interrogate-command.d.ts +190 -0
- package/dist/visual-app-lib/types/layout-direction.d.ts +14 -0
- package/dist/visual-app-lib/types/projection.d.ts +22 -6
- package/dist/visual-app-lib/types/relationship-drafting.d.ts +21 -2
- package/dist/visual-app-lib/types/subject-identity.d.ts +34 -0
- package/dist/visual-app-lib/types/visual-app/App.d.ts +27 -2
- package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
- package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +9 -1
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +15 -6
- package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +77 -6
- package/dist/visual-app-lib/types/visual-app/kind-palette.d.ts +37 -0
- package/dist/visual-app-lib/types/visual-app/mount.d.ts +73 -2
- package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +15 -0
- package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
- package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +8 -1
- package/dist/visual-app-lib/types/visual-app/save-view.d.ts +11 -5
- package/dist/visual-app-lib/types/visual-app/shipped-catalogue.d.ts +10 -0
- package/dist/visual-app-lib/types/visual-app/state.d.ts +15 -4
- package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +13 -1
- package/dist/visual-app-lib/types/visual-app/subject-filter.d.ts +35 -0
- package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +8 -0
- package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +42 -9
- package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +11 -2
- package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +81 -11
- package/docs/CONSUMING-YARRAMATE.md +51 -0
- package/package.json +1 -1
- package/schema/yarramate-design-step.schema.json +395 -1
- package/schema/yarramate-interrogation-report.schema.json +488 -23
- package/schema/yarramate-projection.schema.json +9 -0
- package/schema/yarramate-question-catalogue.schema.json +13 -0
- package/skills/yarramate-architecture/SKILL.md +10 -1
- package/dist/visual-app/assets/index-CcmfL3oY.js +0 -394
- package/dist/visual-app/assets/index-DLutIWES.css +0 -1
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
|
|
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]\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 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
|
|
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]\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 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/cli.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type CliResult } from './cli-support.js';
|
|
3
3
|
export type { CliResult } from './cli-support.js';
|
|
4
|
+
export declare const deriveInitId: (directory: string) => string;
|
|
4
5
|
export declare function runCli(args: readonly string[], cwd?: string): CliResult;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
3
|
-
import { dirname, relative, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, relative, resolve } from 'node:path';
|
|
4
4
|
import { compileWorkspace } from './compiler.js';
|
|
5
5
|
import { diagnosticJson, isMainModule, usage, versionResult, } from './cli-support.js';
|
|
6
6
|
import { runAskCommand } from './ask-command.js';
|
|
@@ -13,8 +13,12 @@ import { deriveAttestationStaleness } from './attestation-staleness.js';
|
|
|
13
13
|
import { reconcileEvidenceReports } from './reconciliation.js';
|
|
14
14
|
import { loadWorkspaceManifest } from './workspace.js';
|
|
15
15
|
const runReconciliation = (options, cwd) => {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Bare reconcile already emits JSON, so --json changes nothing — but a
|
|
17
|
+
// harness scripting "add --json to every verb" must not hit exit 2 on
|
|
18
|
+
// the one verb that treats it as unknown (#275). Accepted as a no-op.
|
|
19
|
+
const positional = options.filter((option) => option !== '--json');
|
|
20
|
+
const [workspacePath] = positional;
|
|
21
|
+
if (positional.length !== 1 ||
|
|
18
22
|
workspacePath === undefined ||
|
|
19
23
|
workspacePath.startsWith('-')) {
|
|
20
24
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
@@ -89,6 +93,22 @@ const runReconciliation = (options, cwd) => {
|
|
|
89
93
|
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
90
94
|
}
|
|
91
95
|
};
|
|
96
|
+
// The id grammar the document and workspace schemas share: a lowercase
|
|
97
|
+
// letter first, then lowercase alphanumerics in single-hyphen segments.
|
|
98
|
+
const initIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
99
|
+
// The first check output should say the project's own name, not `main`
|
|
100
|
+
// (#275, ADR 0112): the default id is the target directory's basename,
|
|
101
|
+
// slugified to the shared id grammar. `init .` resolves before deriving,
|
|
102
|
+
// so the cwd's basename is what gets named. `main` remains the fallback
|
|
103
|
+
// when the basename yields nothing the schemas accept ('.', '..',
|
|
104
|
+
// all-symbols, a leading digit).
|
|
105
|
+
export const deriveInitId = (directory) => {
|
|
106
|
+
const slug = basename(directory)
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
109
|
+
.replace(/^-+|-+$/g, '');
|
|
110
|
+
return initIdPattern.test(slug) ? slug : 'main';
|
|
111
|
+
};
|
|
92
112
|
const runInit = (options, cwd) => {
|
|
93
113
|
const positional = options.filter((option) => option !== '--no-pointer');
|
|
94
114
|
const writePointer = positional.length === options.length;
|
|
@@ -114,14 +134,15 @@ const runInit = (options, cwd) => {
|
|
|
114
134
|
stderr: `${existing.join(' and ')} ${existing.length === 1 ? 'already exists' : 'already exist'}; nothing was changed\n`,
|
|
115
135
|
};
|
|
116
136
|
}
|
|
137
|
+
const workspaceId = deriveInitId(workspaceRoot);
|
|
117
138
|
mkdirSync(dirname(documentPath), { recursive: true });
|
|
118
139
|
writeFileSync(documentPath, 'format: yarramate/v1\n' +
|
|
119
|
-
|
|
140
|
+
`id: ${workspaceId}\n` +
|
|
120
141
|
'profile: yarramate/core@0.1\n' +
|
|
121
142
|
'concepts: []\n' +
|
|
122
143
|
'relationships: []\n', 'utf8');
|
|
123
144
|
writeFileSync(manifestPath, 'format: yarramate/workspace/v1\n' +
|
|
124
|
-
|
|
145
|
+
`id: ${workspaceId}\n` +
|
|
125
146
|
'documents:\n' +
|
|
126
147
|
' - architecture/*.yaml\n' +
|
|
127
148
|
'profiles: []\n' +
|
|
@@ -9,8 +9,14 @@ import type { YarramateOperation } from './operations.js';
|
|
|
9
9
|
* about a name produces worse ids than a transliteration of the name does.
|
|
10
10
|
* Returning null rather than a placeholder keeps a subject called `"???"` from
|
|
11
11
|
* landing as `subject-1`, which nothing could later be traced back from.
|
|
12
|
+
*
|
|
13
|
+
* `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
|
|
14
|
+
* draft never enters the rendered graph, so without it a second subject whose
|
|
15
|
+
* name slugs to the same id re-proposed it and the editor's replace-by-target
|
|
16
|
+
* staging silently swallowed the first (#315) - the identical blind spot
|
|
17
|
+
* `proposeRelationshipId` had before #306's fix, and the identical way out.
|
|
12
18
|
*/
|
|
13
|
-
export declare const proposeConceptId: (graph: CanvasGraph, name: string) => string | null;
|
|
19
|
+
export declare const proposeConceptId: (graph: CanvasGraph, name: string, reserved?: Iterable<string>) => string | null;
|
|
14
20
|
/**
|
|
15
21
|
* The operation that lands a new subject, or `null` when the draft is one no
|
|
16
22
|
* document could accept.
|
|
@@ -19,9 +25,14 @@ export declare const proposeConceptId: (graph: CanvasGraph, name: string) => str
|
|
|
19
25
|
* a kind outside it here, rather than trusting the caller's palette, is the
|
|
20
26
|
* same posture `draftRelationship` takes: the guarantee has to hold for any
|
|
21
27
|
* caller, not only one that filtered first.
|
|
28
|
+
*
|
|
29
|
+
* A caller holding drafts the graph has not landed yet - an editor with a
|
|
30
|
+
* pending changeset - passes their ids as `reserved`, so a second subject
|
|
31
|
+
* slugging to a taken id steps to `-2` instead of colliding with the first
|
|
32
|
+
* (#315).
|
|
22
33
|
*/
|
|
23
34
|
export declare const draftConcept: (graph: CanvasGraph, input: {
|
|
24
35
|
readonly name: string;
|
|
25
36
|
readonly kind: string;
|
|
26
37
|
readonly document: string;
|
|
27
|
-
}, kinds: readonly string[]) => YarramateOperation | null;
|
|
38
|
+
}, kinds: readonly string[], reserved?: Iterable<string>) => YarramateOperation | null;
|
package/dist/concept-drafting.js
CHANGED
|
@@ -17,8 +17,14 @@ const ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
17
17
|
* about a name produces worse ids than a transliteration of the name does.
|
|
18
18
|
* Returning null rather than a placeholder keeps a subject called `"???"` from
|
|
19
19
|
* landing as `subject-1`, which nothing could later be traced back from.
|
|
20
|
+
*
|
|
21
|
+
* `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
|
|
22
|
+
* draft never enters the rendered graph, so without it a second subject whose
|
|
23
|
+
* name slugs to the same id re-proposed it and the editor's replace-by-target
|
|
24
|
+
* staging silently swallowed the first (#315) - the identical blind spot
|
|
25
|
+
* `proposeRelationshipId` had before #306's fix, and the identical way out.
|
|
20
26
|
*/
|
|
21
|
-
export const proposeConceptId = (graph, name) => {
|
|
27
|
+
export const proposeConceptId = (graph, name, reserved = []) => {
|
|
22
28
|
const base = name
|
|
23
29
|
.normalize('NFKD')
|
|
24
30
|
// The marks NFKD split off, dropped rather than treated as boundaries:
|
|
@@ -38,6 +44,7 @@ export const proposeConceptId = (graph, name) => {
|
|
|
38
44
|
const taken = new Set([
|
|
39
45
|
...graph.nodes.map((node) => node.id),
|
|
40
46
|
...graph.edges.map((edge) => edge.id),
|
|
47
|
+
...reserved,
|
|
41
48
|
]);
|
|
42
49
|
if (!taken.has(base))
|
|
43
50
|
return base;
|
|
@@ -55,8 +62,13 @@ export const proposeConceptId = (graph, name) => {
|
|
|
55
62
|
* a kind outside it here, rather than trusting the caller's palette, is the
|
|
56
63
|
* same posture `draftRelationship` takes: the guarantee has to hold for any
|
|
57
64
|
* caller, not only one that filtered first.
|
|
65
|
+
*
|
|
66
|
+
* A caller holding drafts the graph has not landed yet - an editor with a
|
|
67
|
+
* pending changeset - passes their ids as `reserved`, so a second subject
|
|
68
|
+
* slugging to a taken id steps to `-2` instead of colliding with the first
|
|
69
|
+
* (#315).
|
|
58
70
|
*/
|
|
59
|
-
export const draftConcept = (graph, input, kinds) => {
|
|
71
|
+
export const draftConcept = (graph, input, kinds, reserved = []) => {
|
|
60
72
|
if (input.document === '')
|
|
61
73
|
return null;
|
|
62
74
|
if (!kinds.includes(input.kind))
|
|
@@ -64,7 +76,7 @@ export const draftConcept = (graph, input, kinds) => {
|
|
|
64
76
|
const name = input.name.trim();
|
|
65
77
|
if (name === '')
|
|
66
78
|
return null;
|
|
67
|
-
const id = proposeConceptId(graph, name);
|
|
79
|
+
const id = proposeConceptId(graph, name, reserved);
|
|
68
80
|
if (id === null)
|
|
69
81
|
return null;
|
|
70
82
|
return {
|
package/dist/design-command.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
5
|
import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
|
|
6
6
|
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
7
|
+
import { loadEvidence } from './evidence.js';
|
|
7
8
|
import { evaluateCatalogue, loadQuestionCatalogue, renderQuestion, } from './interrogate-command.js';
|
|
8
9
|
import { evaluateProjection } from './projection.js';
|
|
9
10
|
import { renderBrief } from './brief.js';
|
|
@@ -37,6 +38,7 @@ const selectStep = (report, subjectFilter, askPlainById) => {
|
|
|
37
38
|
: { askPlain: askPlainTemplate.trim() }),
|
|
38
39
|
materiality: question.materiality,
|
|
39
40
|
resolution: question.resolution,
|
|
41
|
+
trigger: question.trigger,
|
|
40
42
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -57,6 +59,7 @@ const selectStep = (report, subjectFilter, askPlainById) => {
|
|
|
57
59
|
: { askPlain: renderQuestion(askPlainTemplate, first.id, first.name) }),
|
|
58
60
|
materiality: question.materiality,
|
|
59
61
|
resolution: question.resolution,
|
|
62
|
+
trigger: question.trigger,
|
|
60
63
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
61
64
|
subject: {
|
|
62
65
|
id: first.id,
|
|
@@ -74,6 +77,56 @@ const selectStep = (report, subjectFilter, askPlainById) => {
|
|
|
74
77
|
}
|
|
75
78
|
return null;
|
|
76
79
|
};
|
|
80
|
+
const localKind = (qualified) => {
|
|
81
|
+
const hash = qualified.lastIndexOf('#');
|
|
82
|
+
return hash === -1 ? qualified : qualified.slice(hash + 1);
|
|
83
|
+
};
|
|
84
|
+
const skeletonHeader = (documentAddress, op) => [
|
|
85
|
+
'',
|
|
86
|
+
'Prefilled skeleton (edit the <placeholders>, save as operations.yaml):',
|
|
87
|
+
' format: yarramate/operations/v1',
|
|
88
|
+
' operations:',
|
|
89
|
+
` - op: ${op}`,
|
|
90
|
+
` document: ${documentAddress}`,
|
|
91
|
+
];
|
|
92
|
+
// The skeleton is a rendering of the step's trigger (#289), printed only
|
|
93
|
+
// when a single condition maps unambiguously onto one operation, so a
|
|
94
|
+
// wrong skeleton is never offered; every other trigger leaves the output
|
|
95
|
+
// exactly as before. Kinds print as local names: that is the form a
|
|
96
|
+
// native document declares.
|
|
97
|
+
const renderSkeleton = (step, documentAddress) => {
|
|
98
|
+
if (documentAddress === undefined || step.trigger.length !== 1)
|
|
99
|
+
return [];
|
|
100
|
+
const condition = step.trigger[0];
|
|
101
|
+
if (condition.condition === 'no-subject-of-kind') {
|
|
102
|
+
const kinds = condition.kinds.map(localKind);
|
|
103
|
+
const alternatives = kinds.length > 1 ? ` # or: ${kinds.slice(1).join(', ')}` : '';
|
|
104
|
+
return [
|
|
105
|
+
...skeletonHeader(documentAddress, 'add-concept'),
|
|
106
|
+
' concept:',
|
|
107
|
+
' id: <kebab-case-id>',
|
|
108
|
+
` kind: ${kinds[0]}${alternatives}`,
|
|
109
|
+
' name: <one line>',
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
if (condition.condition === 'missing-relationship' &&
|
|
113
|
+
step.subject !== undefined) {
|
|
114
|
+
const kinds = condition.kinds.map(localKind);
|
|
115
|
+
const alternatives = kinds.length > 1 ? ` # or: ${kinds.slice(1).join(', ')}` : '';
|
|
116
|
+
const swap = condition.direction === 'any' ? ' # or swap the endpoints' : '';
|
|
117
|
+
const from = condition.direction === 'incoming' ? '<counterpart-id>' : step.subject.id;
|
|
118
|
+
const to = condition.direction === 'incoming' ? step.subject.id : '<counterpart-id>';
|
|
119
|
+
return [
|
|
120
|
+
...skeletonHeader(documentAddress, 'add-relationship'),
|
|
121
|
+
' relationship:',
|
|
122
|
+
' id: <kebab-case-id>',
|
|
123
|
+
` kind: ${kinds[0]}${alternatives}`,
|
|
124
|
+
` from: ${from}${swap}`,
|
|
125
|
+
` to: ${to}`,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
};
|
|
77
130
|
export function runDesignCommand(options, cwd) {
|
|
78
131
|
const json = options.includes('--json');
|
|
79
132
|
// Facilitation is a rendering preference, not an interview mode: the
|
|
@@ -150,6 +203,20 @@ export function runDesignCommand(options, cwd) {
|
|
|
150
203
|
})));
|
|
151
204
|
if (!compilation.ok)
|
|
152
205
|
return failed(compilation.diagnostics);
|
|
206
|
+
// The evidence overlay rides along for the one condition that reads
|
|
207
|
+
// it (unchallenged-evidence). A workspace declaring no evidence
|
|
208
|
+
// passes an empty overlay — known to be empty, which keeps that
|
|
209
|
+
// condition quiet — rather than an absent one.
|
|
210
|
+
const evidenceObservations = [];
|
|
211
|
+
for (const path of workspace.evidence) {
|
|
212
|
+
const loadedEvidence = loadEvidence({
|
|
213
|
+
path,
|
|
214
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
215
|
+
});
|
|
216
|
+
if (!loadedEvidence.ok)
|
|
217
|
+
return failed(loadedEvidence.diagnostics);
|
|
218
|
+
evidenceObservations.push(...loadedEvidence.evidence.observations);
|
|
219
|
+
}
|
|
153
220
|
if (subjectFilter !== undefined) {
|
|
154
221
|
const known = new Set(compilation.graph.subjects.map(({ id }) => id));
|
|
155
222
|
if (!known.has(subjectFilter)) {
|
|
@@ -160,7 +227,7 @@ export function runDesignCommand(options, cwd) {
|
|
|
160
227
|
};
|
|
161
228
|
}
|
|
162
229
|
}
|
|
163
|
-
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
|
|
230
|
+
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceObservations);
|
|
164
231
|
const askPlainById = new Map(loadedCatalogue.catalogue.questions.flatMap((question) => question.askPlain === undefined
|
|
165
232
|
? []
|
|
166
233
|
: [[question.id, question.askPlain]]));
|
|
@@ -232,7 +299,21 @@ export function runDesignCommand(options, cwd) {
|
|
|
232
299
|
if (slice !== undefined) {
|
|
233
300
|
lines.push('', 'Subject slice:', '', slice.trimEnd());
|
|
234
301
|
}
|
|
235
|
-
|
|
302
|
+
// The skeleton's document address is the manifest-relative form
|
|
303
|
+
// when the first document sits under the manifest directory - the
|
|
304
|
+
// address an author naturally writes and apply accepts (#216) -
|
|
305
|
+
// falling back to the workspace path, which apply also accepts.
|
|
306
|
+
const manifestDirectory = workspacePath.includes('/')
|
|
307
|
+
? workspacePath.slice(0, workspacePath.lastIndexOf('/'))
|
|
308
|
+
: '';
|
|
309
|
+
const firstDocument = workspace.documents[0];
|
|
310
|
+
const documentAddress = firstDocument === undefined
|
|
311
|
+
? undefined
|
|
312
|
+
: manifestDirectory !== '' &&
|
|
313
|
+
firstDocument.startsWith(`${manifestDirectory}/`)
|
|
314
|
+
? firstDocument.slice(manifestDirectory.length + 1)
|
|
315
|
+
: firstDocument;
|
|
316
|
+
lines.push('', 'Answer by updating the model (one atomic batch):', ` yarramate apply <operations.yaml> ${workspacePath}`, ...renderSkeleton(step, documentAddress), `Then re-run: yarramate design ${workspacePath}`);
|
|
236
317
|
}
|
|
237
318
|
return { exitCode: 0, stdout: `${lines.join('\n')}\n`, stderr: '' };
|
|
238
319
|
}
|
package/dist/evidence.js
CHANGED
|
@@ -27,6 +27,31 @@ export function loadEvidence(source) {
|
|
|
27
27
|
...(observation.key === undefined || observation.value === undefined
|
|
28
28
|
? {}
|
|
29
29
|
: { key: observation.key, value: observation.value }),
|
|
30
|
+
// Provenance survives normalization: reconcile counts a
|
|
31
|
+
// not-observed naming no search (ADR 0107) and interrogation's
|
|
32
|
+
// unchallenged-evidence reads recorded searches (ADR 0120), so a
|
|
33
|
+
// load path that dropped them made both read every overlay as
|
|
34
|
+
// probe-free however carefully its author recorded one.
|
|
35
|
+
...(observation.searched === undefined
|
|
36
|
+
? {}
|
|
37
|
+
: {
|
|
38
|
+
searched: observation.searched.map((probe) => 'glob' in probe
|
|
39
|
+
? { glob: probe.glob }
|
|
40
|
+
: {
|
|
41
|
+
grep: probe.grep,
|
|
42
|
+
...(probe.paths === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { paths: [...probe.paths] }),
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
...(observation.measured === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: {
|
|
50
|
+
measured: observation.measured.map(({ value, method }) => ({
|
|
51
|
+
value,
|
|
52
|
+
method,
|
|
53
|
+
})),
|
|
54
|
+
}),
|
|
30
55
|
evidence: {
|
|
31
56
|
uri: observation.evidence.uri,
|
|
32
57
|
...(observation.evidence.message === undefined
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, ty
|
|
|
15
15
|
export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, ProjectionResult, } from './projection.js';
|
|
16
16
|
export { createFileSystemStore, type PendingWrite, type SourceStore, type StoredSource, type WriteConflict, type WriteOutcome, } from './source-store.js';
|
|
17
17
|
export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, type ApplyOutcome, } from './apply-command.js';
|
|
18
|
-
export { connectableKinds, draftRelationship, proposeRelationshipId, } from './relationship-drafting.js';
|
|
18
|
+
export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
|
|
19
19
|
export { draftConcept, proposeConceptId } from './concept-drafting.js';
|
|
20
20
|
export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
|
|
21
|
-
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
|
|
21
|
+
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, 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
|
@@ -13,7 +13,7 @@ export { canonicalProjection, evaluateProjection, explainProjection, loadProject
|
|
|
13
13
|
export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, } from './adapter-mapping.js';
|
|
14
14
|
export { createFileSystemStore, } from './source-store.js';
|
|
15
15
|
export { applyOperations, landOperations, posixDirectoryOf, } from './apply-command.js';
|
|
16
|
-
export { connectableKinds, draftRelationship, proposeRelationshipId, } from './relationship-drafting.js';
|
|
16
|
+
export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
|
|
17
17
|
export { draftConcept, proposeConceptId } from './concept-drafting.js';
|
|
18
18
|
export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
|
|
19
19
|
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
|
|
@@ -85,7 +85,26 @@ export type CatalogueCondition = {
|
|
|
85
85
|
readonly condition: 'unconstrained-kind';
|
|
86
86
|
} | {
|
|
87
87
|
readonly condition: 'unscoped-succession';
|
|
88
|
+
} | {
|
|
89
|
+
readonly condition: 'unchallenged-evidence';
|
|
88
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* One observation from the workspace's evidence overlay, reduced to what
|
|
93
|
+
* interrogation reads: the result, and whether a search was recorded with
|
|
94
|
+
* it. The only condition that reads the overlay is `unchallenged-evidence`;
|
|
95
|
+
* every other condition reads the compiled graph alone, and the overlay
|
|
96
|
+
* never influences which subjects a selector matches.
|
|
97
|
+
*
|
|
98
|
+
* Shaped structurally rather than importing {@link EvidenceObservation} so
|
|
99
|
+
* the pure engine entry (`./interrogation-entry`) keeps owning its whole
|
|
100
|
+
* input surface: a caller passes
|
|
101
|
+
* `evidenceDocuments.flatMap(({ observations }) => observations)` and the
|
|
102
|
+
* wider evidence shape is never dragged in.
|
|
103
|
+
*/
|
|
104
|
+
export interface CatalogueEvidenceObservation {
|
|
105
|
+
readonly result: 'confirmed' | 'contradicted' | 'unknown' | 'not-observed';
|
|
106
|
+
readonly searched?: readonly unknown[];
|
|
107
|
+
}
|
|
89
108
|
export interface CatalogueQuestion {
|
|
90
109
|
readonly id: string;
|
|
91
110
|
readonly wave: string;
|
|
@@ -128,6 +147,14 @@ export interface ReportQuestion {
|
|
|
128
147
|
readonly question: string;
|
|
129
148
|
readonly materiality: string;
|
|
130
149
|
readonly resolution: string;
|
|
150
|
+
/**
|
|
151
|
+
* The catalogue trigger, verbatim (#289). The conditions that opened a
|
|
152
|
+
* question are its machine-readable answer shape: a host builds the
|
|
153
|
+
* matching affordance (a prefilled form, an operations skeleton) from
|
|
154
|
+
* them instead of re-deriving the shape from its own catalogue copy and
|
|
155
|
+
* drifting from engine semantics.
|
|
156
|
+
*/
|
|
157
|
+
readonly trigger: readonly CatalogueCondition[];
|
|
131
158
|
readonly since?: string;
|
|
132
159
|
readonly subjects?: readonly OpenSubject[];
|
|
133
160
|
}
|
|
@@ -151,7 +178,7 @@ export interface InterrogationReport {
|
|
|
151
178
|
readonly waves: readonly ReportWave[];
|
|
152
179
|
}
|
|
153
180
|
export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string;
|
|
154
|
-
export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext): Omit<InterrogationReport, 'workspace'>;
|
|
181
|
+
export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext, evidence?: readonly CatalogueEvidenceObservation[]): Omit<InterrogationReport, 'workspace'>;
|
|
155
182
|
export type CatalogueLoadResult = {
|
|
156
183
|
readonly ok: true;
|
|
157
184
|
readonly catalogue: QuestionCatalogue;
|
|
@@ -236,8 +236,27 @@ const linkageHits = (index, condition, subjectId, profileContext) => {
|
|
|
236
236
|
return counterparts.some((counterpart) => kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
|
|
237
237
|
});
|
|
238
238
|
};
|
|
239
|
-
const conditionHolds = (index, condition, subjectId, profileContext) => {
|
|
239
|
+
const conditionHolds = (index, condition, subjectId, profileContext, evidence) => {
|
|
240
240
|
switch (condition.condition) {
|
|
241
|
+
case 'unchallenged-evidence':
|
|
242
|
+
// Fires where the overlay records observations and every one is a
|
|
243
|
+
// frictionless confirmation: no contradicted, unknown, or
|
|
244
|
+
// not-observed result, and no recorded search. A discovery that
|
|
245
|
+
// never records anything but success never tested a claim it might
|
|
246
|
+
// fail — 39 of 39 GitLab observations said confirmed while Praefect
|
|
247
|
+
// sat declared upstream and absent from the tree (#272). A recorded
|
|
248
|
+
// search closes it even on a confirmed result, because a
|
|
249
|
+
// confirmation of a negative claim rests on exactly the empty
|
|
250
|
+
// search ADR 0107 made auditable; so does any honest non-confirmed
|
|
251
|
+
// result. An empty overlay stays quiet: with no observations there
|
|
252
|
+
// is no inspection to interrogate. An absent overlay also stays
|
|
253
|
+
// quiet — the caller did not supply one, so its diversity is
|
|
254
|
+
// unknown, not absent, the same rule `unconstrained-kind` applies
|
|
255
|
+
// to a missing profile context.
|
|
256
|
+
return (evidence !== undefined &&
|
|
257
|
+
evidence.length > 0 &&
|
|
258
|
+
evidence.every(({ result, searched }) => result === 'confirmed' &&
|
|
259
|
+
(searched === undefined || searched.length === 0)));
|
|
241
260
|
case 'missing-claim':
|
|
242
261
|
return !(index.claimsBySubject.get(subjectId) ?? []).some(({ predicate }) => predicate === condition.predicate);
|
|
243
262
|
case 'unscoped-succession': {
|
|
@@ -425,7 +444,7 @@ const describeCounterparts = (index, question, subjectId) => {
|
|
|
425
444
|
return name === undefined ? id : `${name} (${id})`;
|
|
426
445
|
});
|
|
427
446
|
};
|
|
428
|
-
export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
447
|
+
export function evaluateCatalogue(catalogue, graph, profileContext, evidence) {
|
|
429
448
|
const index = indexGraph(graph);
|
|
430
449
|
let open = 0;
|
|
431
450
|
let openQuestions = 0;
|
|
@@ -443,17 +462,18 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
|
443
462
|
question: question.question.trim(),
|
|
444
463
|
materiality: question.materiality.trim(),
|
|
445
464
|
resolution: question.resolution.trim(),
|
|
465
|
+
trigger: question.trigger,
|
|
446
466
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
447
467
|
};
|
|
448
468
|
if (question.scope === 'workspace') {
|
|
449
|
-
const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext));
|
|
469
|
+
const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence));
|
|
450
470
|
if (isOpen) {
|
|
451
471
|
open += 1;
|
|
452
472
|
openQuestions += 1;
|
|
453
473
|
}
|
|
454
474
|
return { ...base, open: isOpen };
|
|
455
475
|
}
|
|
456
|
-
const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext)));
|
|
476
|
+
const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext, evidence)));
|
|
457
477
|
if (matches.length === 0) {
|
|
458
478
|
return { ...base, open: false };
|
|
459
479
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
|
|
1
|
+
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which way a view runs its layers, kept in a module that imports nothing.
|
|
3
|
+
*
|
|
4
|
+
* Here rather than in `projection.ts` for the reason `./nesting.ts` gives: the
|
|
5
|
+
* browser needs the value and not only the type, and `projection.ts` drags Ajv
|
|
6
|
+
* and the projection schema in for one constant. `projection.ts` re-exports
|
|
7
|
+
* both.
|
|
8
|
+
*/
|
|
9
|
+
export type LayoutDirection = 'top-down' | 'left-right';
|
|
10
|
+
/**
|
|
11
|
+
* How a view runs when it does not say. Top-down is the behaviour that
|
|
12
|
+
* shipped, and it is what ArchiMate's layer bands read as (ADR 0121).
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_DIRECTION: LayoutDirection;
|
package/dist/projection.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ export interface ProjectionDefinition {
|
|
|
6
6
|
readonly version: string;
|
|
7
7
|
readonly query: {
|
|
8
8
|
readonly subjects?: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* Subjects this query would otherwise select and the author has taken out
|
|
11
|
+
* (#267, ADR 0122). A facet view states a rule, and every interesting rule
|
|
12
|
+
* has an exception someone would rather state than abandon the rule for;
|
|
13
|
+
* this is where that exception is written down instead of being the silent
|
|
14
|
+
* absence a hand-enumerated list produces. Applied after every other facet
|
|
15
|
+
* AND after `relationships: connected` expansion, so an excluded subject is
|
|
16
|
+
* out whichever way it would have come back in.
|
|
17
|
+
*/
|
|
18
|
+
readonly exclude?: readonly string[];
|
|
9
19
|
readonly documents?: readonly string[];
|
|
10
20
|
readonly kinds?: readonly string[];
|
|
11
21
|
readonly layers?: readonly string[];
|
|
@@ -24,12 +34,12 @@ export interface ProjectionDefinition {
|
|
|
24
34
|
readonly description?: string;
|
|
25
35
|
readonly layout?: 'layered';
|
|
26
36
|
/**
|
|
27
|
-
* Read by the LikeC4 export for its
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
37
|
+
* Which way this view runs its layers. Read by the LikeC4 export for its
|
|
38
|
+
* `autoLayout` and by the canvas for ELK's `elk.direction` (ADR 0121); a
|
|
39
|
+
* view that says nothing runs `top-down`, which is what ArchiMate's layer
|
|
40
|
+
* bands read as.
|
|
31
41
|
*/
|
|
32
|
-
readonly direction?:
|
|
42
|
+
readonly direction?: LayoutDirection;
|
|
33
43
|
/**
|
|
34
44
|
* The relationship kinds that draw as nesting in this view, in precedence
|
|
35
45
|
* order (ADR 0101). Absent means `['composition']`, which is the behaviour
|
|
@@ -64,6 +74,12 @@ export interface ProjectionDefinition {
|
|
|
64
74
|
*/
|
|
65
75
|
export { DEFAULT_NESTING, type NestingKind } from './nesting.js';
|
|
66
76
|
import type { NestingKind } from './nesting.js';
|
|
77
|
+
/**
|
|
78
|
+
* Which way a view runs, and the default. Split out for the same reason as the
|
|
79
|
+
* nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
|
|
80
|
+
*/
|
|
81
|
+
export { DEFAULT_DIRECTION, type LayoutDirection } from './layout-direction.js';
|
|
82
|
+
import type { LayoutDirection } from './layout-direction.js';
|
|
67
83
|
export type ProjectionQuery = ProjectionDefinition['query'];
|
|
68
84
|
export interface ProjectionResult {
|
|
69
85
|
readonly format: 'yarramate/projection-result/v1';
|
|
@@ -86,7 +102,7 @@ export declare function canonicalProjection(projection: ProjectionDefinition): P
|
|
|
86
102
|
* A facet of a query, named the way the query names it. What
|
|
87
103
|
* {@link explainProjection} reports as the reason a subject is not in a view.
|
|
88
104
|
*/
|
|
89
|
-
export type ConceptFacet = 'states' | 'subjects' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
|
|
105
|
+
export type ConceptFacet = 'exclude' | 'states' | 'subjects' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
|
|
90
106
|
/** One subject a query dropped, and the facet that dropped it. */
|
|
91
107
|
export interface ProjectionExclusion {
|
|
92
108
|
readonly id: string;
|
package/dist/projection.js
CHANGED
|
@@ -14,6 +14,11 @@ const validateProjection = new Ajv2020({ allErrors: true }).compile(projectionSc
|
|
|
14
14
|
* and a schema, which is a great deal of bundle for one constant (ADR 0101).
|
|
15
15
|
*/
|
|
16
16
|
export { DEFAULT_NESTING } from './nesting.js';
|
|
17
|
+
/**
|
|
18
|
+
* Which way a view runs, and the default. Split out for the same reason as the
|
|
19
|
+
* nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
|
|
20
|
+
*/
|
|
21
|
+
export { DEFAULT_DIRECTION } from './layout-direction.js';
|
|
17
22
|
export function loadProjection(source) {
|
|
18
23
|
const loaded = loadSourceDocument(source, validateProjection, 'Projection');
|
|
19
24
|
return loaded.ok
|
|
@@ -124,7 +129,13 @@ const conceptSelector = (graph, projection, profileContext) => {
|
|
|
124
129
|
? undefined
|
|
125
130
|
: profileContext?.conceptKindLayers.get(kind);
|
|
126
131
|
// Ordered the way a query declares its facets, so "the first reason" is
|
|
127
|
-
// the one a reader would reach first themselves
|
|
132
|
+
// the one a reader would reach first themselves - except the explicit
|
|
133
|
+
// exception, which outranks every rule: when someone has written the
|
|
134
|
+
// subject down as taken out, that IS the first reason, whatever else
|
|
135
|
+
// would also have dropped it (#267).
|
|
136
|
+
if (query.exclude?.includes(id) === true) {
|
|
137
|
+
return 'exclude';
|
|
138
|
+
}
|
|
128
139
|
if (query.states !== undefined &&
|
|
129
140
|
(architectureStateIds.has(id) || !participatesInSelectedState(id))) {
|
|
130
141
|
return 'states';
|
|
@@ -201,6 +212,12 @@ export function explainProjection(graph, projection, profileContext) {
|
|
|
201
212
|
export function evaluateProjection(graph, projection, profileContext) {
|
|
202
213
|
const { droppedBy, architectureStateIds, participatesInSelectedState } = conceptSelector(graph, projection, profileContext);
|
|
203
214
|
const endpointExcluded = (id) => {
|
|
215
|
+
// An exclusion is final (#267, ADR 0122). Dropping the subject from the
|
|
216
|
+
// initial selection alone would not be: `relationships: connected` adds
|
|
217
|
+
// the far end of every relationship it draws, so an excluded subject would
|
|
218
|
+
// walk back in by the other end of a relationship to one that stayed.
|
|
219
|
+
if (projection.query.exclude?.includes(id) === true)
|
|
220
|
+
return true;
|
|
204
221
|
if (!participatesInSelectedState(id))
|
|
205
222
|
return true;
|
|
206
223
|
const status = claimValue(graph.claims, id, 'yarramate/lifecycle/status');
|
|
@@ -221,7 +238,11 @@ export function evaluateProjection(graph, projection, profileContext) {
|
|
|
221
238
|
continue;
|
|
222
239
|
const relationship = graph.claims.find((claim) => claim.id === subject.id);
|
|
223
240
|
const relationshipStatus = claimValue(graph.claims, subject.id, 'yarramate/lifecycle/status');
|
|
224
|
-
if (
|
|
241
|
+
if (
|
|
242
|
+
// A relationship can be taken out by name too: `exclude` names
|
|
243
|
+
// subjects, and a relationship is a subject.
|
|
244
|
+
projection.query.exclude?.includes(subject.id) === true ||
|
|
245
|
+
relationship === undefined ||
|
|
225
246
|
!('ref' in relationship.object) ||
|
|
226
247
|
(projection.query.excludeStatuses !== undefined &&
|
|
227
248
|
relationshipStatus !== undefined &&
|