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.
Files changed (70) hide show
  1. package/catalogues/core-enrichment.yaml +225 -2
  2. package/dist/adapters/visual/session-server.js +16 -1
  3. package/dist/adapters/visual/view-identity.d.ts +1 -1
  4. package/dist/adapters/visual/view-identity.js +61 -3
  5. package/dist/adapters/visual/wire.d.ts +30 -0
  6. package/dist/adapters/visual/workspace-model.d.ts +23 -3
  7. package/dist/adapters/visual/workspace-model.js +52 -2
  8. package/dist/ask-command.js +29 -13
  9. package/dist/cli-support.d.ts +1 -1
  10. package/dist/cli-support.js +1 -1
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +26 -5
  13. package/dist/concept-drafting.d.ts +13 -2
  14. package/dist/concept-drafting.js +15 -3
  15. package/dist/design-command.js +16 -1
  16. package/dist/evidence.js +25 -0
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +1 -1
  19. package/dist/interrogate-command.d.ts +20 -1
  20. package/dist/interrogate-command.js +23 -4
  21. package/dist/interrogation-entry.d.ts +1 -1
  22. package/dist/layout-direction.d.ts +14 -0
  23. package/dist/layout-direction.js +5 -0
  24. package/dist/projection.d.ts +22 -6
  25. package/dist/projection.js +23 -2
  26. package/dist/relationship-drafting.d.ts +21 -2
  27. package/dist/relationship-drafting.js +33 -3
  28. package/dist/subject-references.js +5 -0
  29. package/dist/visual-app/assets/index-C3i9SxTe.js +394 -0
  30. package/dist/visual-app/assets/index-Rkq6smL2.css +1 -0
  31. package/dist/visual-app/index.html +2 -2
  32. package/dist/visual-app-lib/editor.js +28464 -26953
  33. package/dist/visual-app-lib/styles.css +1 -1
  34. package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +1 -1
  35. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +30 -0
  36. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +23 -3
  37. package/dist/visual-app-lib/types/concept-drafting.d.ts +13 -2
  38. package/dist/visual-app-lib/types/interrogate-command.d.ts +190 -0
  39. package/dist/visual-app-lib/types/layout-direction.d.ts +14 -0
  40. package/dist/visual-app-lib/types/projection.d.ts +22 -6
  41. package/dist/visual-app-lib/types/relationship-drafting.d.ts +21 -2
  42. package/dist/visual-app-lib/types/subject-identity.d.ts +34 -0
  43. package/dist/visual-app-lib/types/visual-app/App.d.ts +27 -2
  44. package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
  45. package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +9 -1
  46. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +15 -6
  47. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +77 -6
  48. package/dist/visual-app-lib/types/visual-app/kind-palette.d.ts +37 -0
  49. package/dist/visual-app-lib/types/visual-app/mount.d.ts +73 -2
  50. package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +15 -0
  51. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
  52. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +8 -1
  53. package/dist/visual-app-lib/types/visual-app/save-view.d.ts +11 -5
  54. package/dist/visual-app-lib/types/visual-app/shipped-catalogue.d.ts +10 -0
  55. package/dist/visual-app-lib/types/visual-app/state.d.ts +15 -4
  56. package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +13 -1
  57. package/dist/visual-app-lib/types/visual-app/subject-filter.d.ts +35 -0
  58. package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +8 -0
  59. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +42 -9
  60. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +11 -2
  61. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +81 -11
  62. package/docs/CONSUMING-YARRAMATE.md +40 -0
  63. package/package.json +1 -1
  64. package/schema/yarramate-design-step.schema.json +13 -0
  65. package/schema/yarramate-interrogation-report.schema.json +13 -0
  66. package/schema/yarramate-projection.schema.json +9 -0
  67. package/schema/yarramate-question-catalogue.schema.json +13 -0
  68. package/skills/yarramate-architecture/SKILL.md +5 -0
  69. package/dist/visual-app/assets/index-CcmfL3oY.js +0 -394
  70. package/dist/visual-app/assets/index-DLutIWES.css +0 -1
@@ -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>\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";
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;
@@ -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>\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';
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
- const [workspacePath] = options;
17
- if (options.length !== 1 ||
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
- 'id: main\n' +
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
- 'id: main\n' +
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;
@@ -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 {
@@ -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;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * How a view runs when it does not say. Top-down is the behaviour that
3
+ * shipped, and it is what ArchiMate's layer bands read as (ADR 0121).
4
+ */
5
+ export const DEFAULT_DIRECTION = 'top-down';
@@ -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 `autoLayout`, and by nothing else. The
28
- * canvas draws ArchiMate, whose layer bands only read top-down, so it
29
- * ignores this rather than offering a control that would tilt the bands
30
- * away from what they mean.
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?: 'top-down' | 'left-right';
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;
@@ -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 (relationship === undefined ||
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 draftRelationship: (graph: CanvasGraph, fromId: string, kind: RelationshipKind, toId: string) => YarramateOperation | null;
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',