yarramate 1.2.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 +16 -1
- 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 +20 -1
- package/dist/interrogate-command.js +23 -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 +40 -0
- package/package.json +1 -1
- package/schema/yarramate-design-step.schema.json +13 -0
- package/schema/yarramate-interrogation-report.schema.json +13 -0
- package/schema/yarramate-projection.schema.json +9 -0
- package/schema/yarramate-question-catalogue.schema.json +13 -0
- package/skills/yarramate-architecture/SKILL.md +5 -0
- 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';
|
|
@@ -202,6 +203,20 @@ export function runDesignCommand(options, cwd) {
|
|
|
202
203
|
})));
|
|
203
204
|
if (!compilation.ok)
|
|
204
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
|
+
}
|
|
205
220
|
if (subjectFilter !== undefined) {
|
|
206
221
|
const known = new Set(compilation.graph.subjects.map(({ id }) => id));
|
|
207
222
|
if (!known.has(subjectFilter)) {
|
|
@@ -212,7 +227,7 @@ export function runDesignCommand(options, cwd) {
|
|
|
212
227
|
};
|
|
213
228
|
}
|
|
214
229
|
}
|
|
215
|
-
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
|
|
230
|
+
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceObservations);
|
|
216
231
|
const askPlainById = new Map(loadedCatalogue.catalogue.questions.flatMap((question) => question.askPlain === undefined
|
|
217
232
|
? []
|
|
218
233
|
: [[question.id, question.askPlain]]));
|
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;
|
|
@@ -159,7 +178,7 @@ export interface InterrogationReport {
|
|
|
159
178
|
readonly waves: readonly ReportWave[];
|
|
160
179
|
}
|
|
161
180
|
export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string;
|
|
162
|
-
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'>;
|
|
163
182
|
export type CatalogueLoadResult = {
|
|
164
183
|
readonly ok: true;
|
|
165
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;
|
|
@@ -447,14 +466,14 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
|
447
466
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
448
467
|
};
|
|
449
468
|
if (question.scope === 'workspace') {
|
|
450
|
-
const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext));
|
|
469
|
+
const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence));
|
|
451
470
|
if (isOpen) {
|
|
452
471
|
open += 1;
|
|
453
472
|
openQuestions += 1;
|
|
454
473
|
}
|
|
455
474
|
return { ...base, open: isOpen };
|
|
456
475
|
}
|
|
457
|
-
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)));
|
|
458
477
|
if (matches.length === 0) {
|
|
459
478
|
return { ...base, open: false };
|
|
460
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 &&
|
|
@@ -33,8 +33,16 @@ export declare const connectableKinds: (graph: CanvasGraph, fromId: string, toId
|
|
|
33
33
|
* relationship kind is a single lowercase word. A collision takes a numeric
|
|
34
34
|
* suffix rather than a hash, because the id is authored text a human will read
|
|
35
35
|
* in a diff.
|
|
36
|
+
*
|
|
37
|
+
* `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
|
|
38
|
+
* draft never enters the rendered graph, so without it a second relationship
|
|
39
|
+
* between the same pair re-proposed the identical id and the editor's
|
|
40
|
+
* replace-by-target staging silently swallowed the first (#306). The schema
|
|
41
|
+
* places no uniqueness on the (from, kind, to) triple - parallel relationships
|
|
42
|
+
* with distinct ids compile cleanly - so the id proposal is the only place the
|
|
43
|
+
* collision can be stepped past.
|
|
36
44
|
*/
|
|
37
|
-
export declare const proposeRelationshipId: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string) => string;
|
|
45
|
+
export declare const proposeRelationshipId: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string, reserved?: Iterable<string>) => string;
|
|
38
46
|
/**
|
|
39
47
|
* The operation that lands a drafted relationship, or `null` when the draft is
|
|
40
48
|
* one the table does not permit.
|
|
@@ -47,5 +55,16 @@ export declare const proposeRelationshipId: (graph: CanvasGraph, fromId: string,
|
|
|
47
55
|
* relationship has to live somewhere, both endpoints are equally defensible,
|
|
48
56
|
* and the source is where a reader looking for what this thing does would go
|
|
49
57
|
* first.
|
|
58
|
+
*
|
|
59
|
+
* A caller holding drafts the graph has not landed yet - an editor with a
|
|
60
|
+
* pending changeset - passes their ids as `reserved`, so a second parallel
|
|
61
|
+
* relationship steps to `-2` instead of colliding with the first (#306).
|
|
62
|
+
*/
|
|
63
|
+
export declare const draftRelationship: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string, reserved?: Iterable<string>) => YarramateOperation | null;
|
|
64
|
+
/**
|
|
65
|
+
* The ids a pending changeset already claims, for `proposeRelationshipId`'s
|
|
66
|
+
* `reserved` parameter. Every operation that names a subject id reserves it -
|
|
67
|
+
* an update's id is already in the graph and reserving it twice is harmless,
|
|
68
|
+
* while an add's id is exactly the one the graph cannot know yet.
|
|
50
69
|
*/
|
|
51
|
-
export declare const
|
|
70
|
+
export declare const stagedSubjectIds: (operations: readonly YarramateOperation[]) => readonly string[];
|
|
@@ -45,11 +45,20 @@ export const connectableKinds = (graph, fromId, toId) => {
|
|
|
45
45
|
* relationship kind is a single lowercase word. A collision takes a numeric
|
|
46
46
|
* suffix rather than a hash, because the id is authored text a human will read
|
|
47
47
|
* in a diff.
|
|
48
|
+
*
|
|
49
|
+
* `reserved` carries ids the graph does not know yet: a staged-but-uncommitted
|
|
50
|
+
* draft never enters the rendered graph, so without it a second relationship
|
|
51
|
+
* between the same pair re-proposed the identical id and the editor's
|
|
52
|
+
* replace-by-target staging silently swallowed the first (#306). The schema
|
|
53
|
+
* places no uniqueness on the (from, kind, to) triple - parallel relationships
|
|
54
|
+
* with distinct ids compile cleanly - so the id proposal is the only place the
|
|
55
|
+
* collision can be stepped past.
|
|
48
56
|
*/
|
|
49
|
-
export const proposeRelationshipId = (graph, fromId, kind, toId) => {
|
|
57
|
+
export const proposeRelationshipId = (graph, fromId, kind, toId, reserved = []) => {
|
|
50
58
|
const taken = new Set([
|
|
51
59
|
...graph.nodes.map((node) => node.id),
|
|
52
60
|
...graph.edges.map((edge) => edge.id),
|
|
61
|
+
...reserved,
|
|
53
62
|
]);
|
|
54
63
|
const base = `${fromId}-${kind}-${toId}`;
|
|
55
64
|
if (!taken.has(base))
|
|
@@ -72,8 +81,12 @@ export const proposeRelationshipId = (graph, fromId, kind, toId) => {
|
|
|
72
81
|
* relationship has to live somewhere, both endpoints are equally defensible,
|
|
73
82
|
* and the source is where a reader looking for what this thing does would go
|
|
74
83
|
* first.
|
|
84
|
+
*
|
|
85
|
+
* A caller holding drafts the graph has not landed yet - an editor with a
|
|
86
|
+
* pending changeset - passes their ids as `reserved`, so a second parallel
|
|
87
|
+
* relationship steps to `-2` instead of colliding with the first (#306).
|
|
75
88
|
*/
|
|
76
|
-
export const draftRelationship = (graph, fromId, kind, toId) => {
|
|
89
|
+
export const draftRelationship = (graph, fromId, kind, toId, reserved = []) => {
|
|
77
90
|
if (!connectableKinds(graph, fromId, toId).includes(kind))
|
|
78
91
|
return null;
|
|
79
92
|
const from = graph.nodes.find((node) => node.id === fromId);
|
|
@@ -83,10 +96,27 @@ export const draftRelationship = (graph, fromId, kind, toId) => {
|
|
|
83
96
|
op: 'add-relationship',
|
|
84
97
|
document: from.document,
|
|
85
98
|
relationship: {
|
|
86
|
-
id: proposeRelationshipId(graph, fromId, kind, toId),
|
|
99
|
+
id: proposeRelationshipId(graph, fromId, kind, toId, reserved),
|
|
87
100
|
kind,
|
|
88
101
|
from: fromId,
|
|
89
102
|
to: toId,
|
|
90
103
|
},
|
|
91
104
|
};
|
|
92
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* The ids a pending changeset already claims, for `proposeRelationshipId`'s
|
|
108
|
+
* `reserved` parameter. Every operation that names a subject id reserves it -
|
|
109
|
+
* an update's id is already in the graph and reserving it twice is harmless,
|
|
110
|
+
* while an add's id is exactly the one the graph cannot know yet.
|
|
111
|
+
*/
|
|
112
|
+
export const stagedSubjectIds = (operations) => operations.flatMap((op) => {
|
|
113
|
+
// A staged rename claims the id it moves to as well as the one it leaves.
|
|
114
|
+
const renamedTo = op.op === 'rename-concept' || op.op === 'rename-relationship'
|
|
115
|
+
? [op.to]
|
|
116
|
+
: [];
|
|
117
|
+
if ('relationship' in op)
|
|
118
|
+
return [op.relationship.id, ...renamedTo];
|
|
119
|
+
if ('concept' in op)
|
|
120
|
+
return [op.concept.id, ...renamedTo];
|
|
121
|
+
return renamedTo;
|
|
122
|
+
});
|
|
@@ -56,6 +56,11 @@ export const SUBJECT_REFERENCE_POSITIONS = [
|
|
|
56
56
|
path: ['query', 'subjects', '*'],
|
|
57
57
|
form: 'qualified',
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
group: 'projection',
|
|
61
|
+
path: ['query', 'exclude', '*'],
|
|
62
|
+
form: 'qualified',
|
|
63
|
+
},
|
|
59
64
|
{ group: 'projection', path: ['query', 'owners', '*'], form: 'qualified' },
|
|
60
65
|
{
|
|
61
66
|
group: 'projection',
|