yarramate 0.8.1 → 0.10.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.
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { parseDocument } from 'yaml';
5
5
  import { compareArchitectureStates, } from './architecture-state.js';
6
6
  import { renderBrief } from './brief.js';
7
+ import { deriveChangedSubjects } from './changed.js';
7
8
  import { runCheckCommand } from './check-command.js';
8
9
  import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
9
10
  import { compileWorkspaceWithProfileContext, } from './compiler.js';
@@ -124,6 +125,7 @@ export function runAskCommand(options, cwd) {
124
125
  let kinds = false;
125
126
  let advise = false;
126
127
  let compare;
128
+ let changed;
127
129
  let budget;
128
130
  let kindFilter;
129
131
  let statusFilter;
@@ -172,7 +174,8 @@ export function runAskCommand(options, cwd) {
172
174
  if (option === '--budget' ||
173
175
  option === '--kind' ||
174
176
  option === '--status' ||
175
- option === '--catalogue') {
177
+ option === '--catalogue' ||
178
+ option === '--changed') {
176
179
  const value = options[index + 1];
177
180
  if (value === undefined || value.startsWith('-')) {
178
181
  return { exitCode: 2, stdout: '', stderr: usage };
@@ -183,6 +186,12 @@ export function runAskCommand(options, cwd) {
183
186
  }
184
187
  budget = Number(value);
185
188
  }
189
+ else if (option === '--changed') {
190
+ if (changed !== undefined) {
191
+ return { exitCode: 2, stdout: '', stderr: usage };
192
+ }
193
+ changed = value;
194
+ }
186
195
  else if (option === '--kind') {
187
196
  if (kindFilter !== undefined) {
188
197
  return { exitCode: 2, stdout: '', stderr: usage };
@@ -223,9 +232,12 @@ export function runAskCommand(options, cwd) {
223
232
  (advise && exclusiveModes > 0) ||
224
233
  (advise && query.length === 0) ||
225
234
  (query.length > 0 && exclusiveModes > 0) ||
235
+ (changed !== undefined &&
236
+ (query.length > 0 || exclusiveModes > 0 || advise)) ||
226
237
  ((kindFilter !== undefined || statusFilter !== undefined) && !subjects) ||
227
238
  (cataloguePath !== undefined && !open && !advise) ||
228
- (budget !== undefined && (json || (query.length === 0 && !advise)))) {
239
+ (budget !== undefined &&
240
+ (json || (query.length === 0 && !advise && changed === undefined)))) {
229
241
  return { exitCode: 2, stdout: '', stderr: usage };
230
242
  }
231
243
  try {
@@ -263,7 +275,8 @@ export function runAskCommand(options, cwd) {
263
275
  !open &&
264
276
  !kinds &&
265
277
  !advise &&
266
- compare === undefined) {
278
+ compare === undefined &&
279
+ changed === undefined) {
267
280
  const checked = runCheckCommand([workspacePath, '--json'], cwd);
268
281
  const checkPayload = JSON.parse(checked.stdout);
269
282
  if (!checkPayload.ok) {
@@ -544,6 +557,83 @@ export function runAskCommand(options, cwd) {
544
557
  };
545
558
  return emit(result, renderInterrogationReport(ordered));
546
559
  }
560
+ // --changed: the review slice (ADR 0065). Git says what changed; the
561
+ // engine maps changed lines to subjects and renders their connected
562
+ // neighbourhood, plus a coverage note when a changed subject appears
563
+ // in no authored projection.
564
+ if (changed !== undefined) {
565
+ const documentIdByPath = new Map(graph.documents.map(({ id, source }) => [source, id]));
566
+ const derived = deriveChangedSubjects(cwd, changed, workspace.documents.map((path) => ({
567
+ path,
568
+ source: readFileSync(resolve(cwd, path), 'utf8'),
569
+ documentId: documentIdByPath.get(path) ?? path,
570
+ })));
571
+ if (!derived.ok) {
572
+ return { exitCode: 2, stdout: '', stderr: `${derived.message}\n` };
573
+ }
574
+ const endpoints = new Set();
575
+ for (const relationshipId of derived.changed.relationships) {
576
+ const claim = graph.claims.find((candidate) => candidate.id === relationshipId && 'ref' in candidate.object);
577
+ if (claim !== undefined && 'ref' in claim.object) {
578
+ endpoints.add(claim.subject);
579
+ endpoints.add(claim.object.ref);
580
+ }
581
+ }
582
+ const seeds = [
583
+ ...new Set([...derived.changed.concepts, ...endpoints]),
584
+ ].sort();
585
+ const changedIds = [
586
+ ...derived.changed.concepts,
587
+ ...derived.changed.relationships,
588
+ ];
589
+ const covered = new Set();
590
+ for (const projectionPath of workspace.projections) {
591
+ const loaded = loadProjection({
592
+ path: projectionPath,
593
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
594
+ });
595
+ if (!loaded.ok)
596
+ continue;
597
+ const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext);
598
+ for (const subject of membership.subjects) {
599
+ covered.add(subject.id);
600
+ }
601
+ }
602
+ const uncovered = changedIds.filter((id) => !covered.has(id));
603
+ const coverage = {
604
+ projections: workspace.projections.length,
605
+ uncovered,
606
+ };
607
+ const evaluated = sliceProjection(graph, seeds, `Review slice ${changed}`, compilation.profileContext);
608
+ const result = {
609
+ format: 'yarramate/ask-result/v1',
610
+ workspace: workspace.id,
611
+ mode: 'slice',
612
+ addressing: 'changed',
613
+ seeds,
614
+ changed: derived.changed,
615
+ coverage,
616
+ result: evaluated,
617
+ };
618
+ if (changedIds.length === 0) {
619
+ return emit(result, `No model subjects changed in ${changed}.\n`);
620
+ }
621
+ const rendered = budget === undefined
622
+ ? renderBrief(evaluated, compilation.profileContext)
623
+ : renderBudgetedContext(evaluated, budget);
624
+ const lines = [
625
+ `Review slice ${changed} — ${plural(derived.changed.concepts.length, 'concept')}, ` +
626
+ `${plural(derived.changed.relationships.length, 'relationship')} changed (workspace ${workspace.id})`,
627
+ '',
628
+ rendered.trimEnd(),
629
+ '',
630
+ uncovered.length === 0
631
+ ? `Review coverage: every changed subject appears in at least one of the ${coverage.projections} authored projections.`
632
+ : `Review coverage: ${uncovered.length} of ${changedIds.length} changed subjects appear in no authored projection:` +
633
+ `\n${uncovered.map((id) => ` ${id}`).join('\n')}`,
634
+ ];
635
+ return emit(result, `${lines.join('\n')}\n`);
636
+ }
547
637
  // Slice and advice both start from seeds. A single query term that
548
638
  // names a projection file is precise addressing; anything else runs
549
639
  // through free-text seeding, where exact subject ids win.
@@ -0,0 +1,17 @@
1
+ export interface ChangedSubjects {
2
+ readonly range: string;
3
+ readonly concepts: readonly string[];
4
+ readonly relationships: readonly string[];
5
+ }
6
+ export type ChangedResult = {
7
+ readonly ok: true;
8
+ readonly changed: ChangedSubjects;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly message: string;
12
+ };
13
+ export declare function deriveChangedSubjects(cwd: string, range: string, documents: ReadonlyArray<{
14
+ readonly path: string;
15
+ readonly source: string;
16
+ readonly documentId: string;
17
+ }>): ChangedResult;
@@ -0,0 +1,111 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
3
+ const lineOfOffset = (lineStarts, offset) => {
4
+ let low = 0;
5
+ let high = lineStarts.length - 1;
6
+ while (low < high) {
7
+ const mid = (low + high + 1) >> 1;
8
+ if (lineStarts[mid] <= offset)
9
+ low = mid;
10
+ else
11
+ high = mid - 1;
12
+ }
13
+ return low + 1;
14
+ };
15
+ const itemSpans = (source) => {
16
+ const lineStarts = [0];
17
+ for (let index = 0; index < source.length; index += 1) {
18
+ if (source[index] === '\n')
19
+ lineStarts.push(index + 1);
20
+ }
21
+ const document = parseDocument(source);
22
+ const root = document.contents;
23
+ if (!isMap(root))
24
+ return [];
25
+ const spans = [];
26
+ for (const collection of ['concepts', 'relationships', 'states']) {
27
+ const pair = root.items.find((candidate) => isScalar(candidate.key) && candidate.key.value === collection);
28
+ if (pair === undefined || !isSeq(pair.value))
29
+ continue;
30
+ for (const item of pair.value.items) {
31
+ if (!isMap(item))
32
+ continue;
33
+ const idPair = item.items.find((field) => isScalar(field.key) && field.key.value === 'id');
34
+ if (idPair === undefined ||
35
+ !isScalar(idPair.value) ||
36
+ typeof idPair.value.value !== 'string') {
37
+ continue;
38
+ }
39
+ const range = item
40
+ .range;
41
+ if (range === undefined)
42
+ continue;
43
+ spans.push({
44
+ id: idPair.value.value,
45
+ collection,
46
+ startLine: lineOfOffset(lineStarts, range[0]),
47
+ // range[1] can extend past the trailing newline; anchor the end
48
+ // on the last content character instead.
49
+ endLine: lineOfOffset(lineStarts, Math.max(range[0], range[1] - 1)),
50
+ });
51
+ }
52
+ }
53
+ return spans;
54
+ };
55
+ // New-side line ranges from `git diff --unified=0`. A pure deletion has a
56
+ // zero count; it still touches the position it collapsed onto.
57
+ const changedLineRanges = (diff) => {
58
+ const ranges = [];
59
+ for (const match of diff.matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)) {
60
+ const start = Number(match[1]);
61
+ const count = match[2] === undefined ? 1 : Number(match[2]);
62
+ ranges.push(count === 0 ? [Math.max(start, 1), Math.max(start, 1)] : [start, start + count - 1]);
63
+ }
64
+ return ranges;
65
+ };
66
+ export function deriveChangedSubjects(cwd, range, documents) {
67
+ const probe = spawnSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
68
+ encoding: 'utf8',
69
+ });
70
+ if (probe.status !== 0) {
71
+ return {
72
+ ok: false,
73
+ message: '--changed requires the workspace to live in a git repository',
74
+ };
75
+ }
76
+ const concepts = new Set();
77
+ const relationships = new Set();
78
+ for (const document of documents) {
79
+ const diffed = spawnSync('git', ['-C', cwd, 'diff', '--unified=0', range, '--', document.path], { encoding: 'utf8' });
80
+ if (diffed.status !== 0) {
81
+ return {
82
+ ok: false,
83
+ message: `git diff failed for range "${range}": ${(diffed.stderr ?? '').trim()}`,
84
+ };
85
+ }
86
+ const ranges = changedLineRanges(diffed.stdout ?? '');
87
+ if (ranges.length === 0)
88
+ continue;
89
+ const spans = itemSpans(document.source);
90
+ for (const span of spans) {
91
+ if (span.collection === 'states')
92
+ continue;
93
+ const touched = ranges.some(([from, to]) => from <= span.endLine && to >= span.startLine);
94
+ if (!touched)
95
+ continue;
96
+ const qualified = `${document.documentId}#${span.id}`;
97
+ if (span.collection === 'concepts')
98
+ concepts.add(qualified);
99
+ else
100
+ relationships.add(qualified);
101
+ }
102
+ }
103
+ return {
104
+ ok: true,
105
+ changed: {
106
+ range,
107
+ concepts: [...concepts].sort(),
108
+ relationships: [...relationships].sort(),
109
+ },
110
+ };
111
+ }
@@ -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 <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--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>] [--catalogue <catalogue.yaml>] [--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 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 briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n";
10
+ export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--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>] [--catalogue <catalogue.yaml>] [--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>] [--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 likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\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;
@@ -23,7 +23,7 @@ export const versionResult = (binary) => ({
23
23
  stdout: `${binary} ${packageVersion}\n`,
24
24
  stderr: '',
25
25
  });
26
- export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--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>] [--catalogue <catalogue.yaml>] [--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 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 briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n';
26
+ export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--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>] [--catalogue <catalogue.yaml>] [--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>] [--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 likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n';
27
27
  export const diagnosticJson = (diagnostics) => `${JSON.stringify({
28
28
  format: 'yarramate/diagnostic-result/v1',
29
29
  diagnostics,
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { parseDocument } from 'yaml';
6
6
  import { renderBrief } from './brief.js';
7
+ import { deriveChangedSubjects } from './changed.js';
7
8
  import { humanDiagnostics, usage } from './cli-support.js';
8
9
  import { compileWorkspaceWithProfileContext, } from './compiler.js';
9
10
  import { serializeSemanticGraph } from './graph.js';
@@ -23,6 +24,7 @@ const parseExportOptions = (options) => {
23
24
  const positionals = [];
24
25
  let out;
25
26
  let budget;
27
+ let changed;
26
28
  let json = false;
27
29
  for (let index = 0; index < options.length; index += 1) {
28
30
  const option = options[index];
@@ -30,7 +32,9 @@ const parseExportOptions = (options) => {
30
32
  json = true;
31
33
  continue;
32
34
  }
33
- if (option === '--out' || option === '--budget') {
35
+ if (option === '--out' ||
36
+ option === '--budget' ||
37
+ option === '--changed') {
34
38
  const value = options[index + 1];
35
39
  if (value === undefined || value.startsWith('-'))
36
40
  return undefined;
@@ -39,6 +43,11 @@ const parseExportOptions = (options) => {
39
43
  return undefined;
40
44
  out = value;
41
45
  }
46
+ else if (option === '--changed') {
47
+ if (changed !== undefined)
48
+ return undefined;
49
+ changed = value;
50
+ }
42
51
  else {
43
52
  if (budget !== undefined || !/^[1-9][0-9]*$/.test(value)) {
44
53
  return undefined;
@@ -56,6 +65,7 @@ const parseExportOptions = (options) => {
56
65
  positionals,
57
66
  ...(out === undefined ? {} : { out }),
58
67
  ...(budget === undefined ? {} : { budget }),
68
+ ...(changed === undefined ? {} : { changed }),
59
69
  json,
60
70
  };
61
71
  };
@@ -103,12 +113,14 @@ export function runExportCommand(options, cwd) {
103
113
  stderr: delegated.stderr ?? '',
104
114
  };
105
115
  }
106
- const expectedPositionals = kind === 'graph' ? 1 : 2;
116
+ const usesChanged = parsed.changed !== undefined;
117
+ const expectedPositionals = kind === 'graph' || usesChanged ? 1 : 2;
107
118
  const workspacePath = parsed.positionals[expectedPositionals - 1];
108
- const projectionPath = kind === 'graph' ? undefined : parsed.positionals[0];
119
+ const projectionPath = kind === 'graph' || usesChanged ? undefined : parsed.positionals[0];
109
120
  if (parsed.positionals.length !== expectedPositionals ||
110
121
  workspacePath === undefined ||
111
122
  parsed.json ||
123
+ (usesChanged && kind === 'graph') ||
112
124
  (parsed.budget !== undefined && kind !== 'briefs') ||
113
125
  (kind === 'briefs' && parsed.out === undefined)) {
114
126
  return { exitCode: 2, stdout: '', stderr: usage };
@@ -151,13 +163,51 @@ export function runExportCommand(options, cwd) {
151
163
  stderr: '',
152
164
  };
153
165
  }
154
- const loadedProjection = loadProjection({
155
- path: projectionPath,
156
- source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
157
- });
158
- if (!loadedProjection.ok)
159
- return failed(loadedProjection.diagnostics);
160
- const result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
166
+ let result;
167
+ if (parsed.changed !== undefined) {
168
+ // Review slices derive from git (ADR 0065): changed subjects seed
169
+ // the connected neighbourhood the reviewer inspects.
170
+ const documentIdByPath = new Map(compilation.graph.documents.map(({ id, source }) => [source, id]));
171
+ const derived = deriveChangedSubjects(cwd, parsed.changed, workspace.documents.map((path) => ({
172
+ path,
173
+ source: readFileSync(resolve(cwd, path), 'utf8'),
174
+ documentId: documentIdByPath.get(path) ?? path,
175
+ })));
176
+ if (!derived.ok) {
177
+ return { exitCode: 2, stdout: '', stderr: `${derived.message}\n` };
178
+ }
179
+ const endpoints = new Set();
180
+ for (const relationshipId of derived.changed.relationships) {
181
+ const claim = compilation.graph.claims.find((candidate) => candidate.id === relationshipId && 'ref' in candidate.object);
182
+ if (claim !== undefined && 'ref' in claim.object) {
183
+ endpoints.add(claim.subject);
184
+ endpoints.add(claim.object.ref);
185
+ }
186
+ }
187
+ const seeds = [
188
+ ...new Set([...derived.changed.concepts, ...endpoints]),
189
+ ].sort();
190
+ result = evaluateProjection(compilation.graph, {
191
+ format: 'yarramate/projection/v1',
192
+ id: 'review-slice',
193
+ version: '0.0',
194
+ query: { subjects: seeds, relationships: 'connected' },
195
+ presentation: {
196
+ title: `Review slice ${parsed.changed}`,
197
+ description: `Connected neighbourhood of the subjects changed in ` +
198
+ `${parsed.changed}.`,
199
+ },
200
+ }, compilation.profileContext);
201
+ }
202
+ else {
203
+ const loadedProjection = loadProjection({
204
+ path: projectionPath,
205
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
206
+ });
207
+ if (!loadedProjection.ok)
208
+ return failed(loadedProjection.diagnostics);
209
+ result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
210
+ }
161
211
  if (kind === 'markdown') {
162
212
  const rendered = renderProjectionMarkdown(result);
163
213
  if (parsed.out === undefined) {
@@ -9,6 +9,7 @@ export interface ProjectionDefinition {
9
9
  readonly documents?: readonly string[];
10
10
  readonly kinds?: readonly string[];
11
11
  readonly statuses?: readonly LifecycleStatus[];
12
+ readonly excludeStatuses?: readonly LifecycleStatus[];
12
13
  readonly states?: readonly string[];
13
14
  readonly owners?: readonly string[];
14
15
  readonly constraints?: readonly string[];
@@ -65,6 +65,9 @@ export function evaluateProjection(graph, projection, profileContext) {
65
65
  (projection.query.statuses === undefined ||
66
66
  (status !== undefined &&
67
67
  projection.query.statuses.includes(status))) &&
68
+ (projection.query.excludeStatuses === undefined ||
69
+ status === undefined ||
70
+ !projection.query.excludeStatuses.includes(status)) &&
68
71
  (projection.query.owners === undefined ||
69
72
  (owner !== undefined &&
70
73
  projection.query.owners.includes(owner))) &&
@@ -80,8 +83,12 @@ export function evaluateProjection(graph, projection, profileContext) {
80
83
  if (subject.type !== 'relationship')
81
84
  continue;
82
85
  const relationship = graph.claims.find((claim) => claim.id === subject.id);
86
+ const relationshipStatus = claimValue(graph.claims, subject.id, 'yarramate/lifecycle/status');
83
87
  if (relationship === undefined ||
84
88
  !('ref' in relationship.object) ||
89
+ (projection.query.excludeStatuses !== undefined &&
90
+ relationshipStatus !== undefined &&
91
+ projection.query.excludeStatuses.includes(relationshipStatus)) ||
85
92
  (projection.query.relationshipKinds !== undefined &&
86
93
  !projection.query.relationshipKinds.some((selectedKind) => selectedKind === relationship.predicate ||
87
94
  (projection.query.kindMatching === 'descendants' &&
@@ -93,6 +100,20 @@ export function evaluateProjection(graph, projection, profileContext) {
93
100
  }
94
101
  const sourceSelected = initiallySelectedConceptIds.has(relationship.subject);
95
102
  const targetSelected = initiallySelectedConceptIds.has(relationship.object.ref);
103
+ // excludeStatuses also vetoes connected expansion: an excluded
104
+ // concept is never pulled in as a neighbour, and edges touching
105
+ // one are dropped rather than left dangling.
106
+ const endpointExcluded = (id) => {
107
+ if (projection.query.excludeStatuses === undefined)
108
+ return false;
109
+ const endpointStatus = claimValue(graph.claims, id, 'yarramate/lifecycle/status');
110
+ return (endpointStatus !== undefined &&
111
+ projection.query.excludeStatuses.includes(endpointStatus));
112
+ };
113
+ if (endpointExcluded(relationship.subject) ||
114
+ endpointExcluded(relationship.object.ref)) {
115
+ continue;
116
+ }
96
117
  if ((relationshipMode === 'between' &&
97
118
  sourceSelected &&
98
119
  targetSelected) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yarramate",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Tool-neutral semantic architecture engine and guided methodology",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -4,7 +4,11 @@
4
4
  "title": "YarraMate ask result",
5
5
  "description": "One envelope for every consumed-now read: orientation, roster, slice, advice, next, open questions, and state comparison, discriminated by mode.",
6
6
  "type": "object",
7
- "required": ["format", "workspace", "mode"],
7
+ "required": [
8
+ "format",
9
+ "workspace",
10
+ "mode"
11
+ ],
8
12
  "properties": {
9
13
  "format": {
10
14
  "const": "yarramate/ask-result/v1"
@@ -29,18 +33,34 @@
29
33
  "oneOf": [
30
34
  {
31
35
  "type": "object",
32
- "required": ["mode", "ok", "check", "backlog"],
36
+ "required": [
37
+ "mode",
38
+ "ok",
39
+ "check",
40
+ "backlog"
41
+ ],
33
42
  "properties": {
34
43
  "format": true,
35
44
  "workspace": true,
36
- "mode": { "const": "orientation" },
37
- "ok": { "type": "boolean" },
45
+ "mode": {
46
+ "const": "orientation"
47
+ },
48
+ "ok": {
49
+ "type": "boolean"
50
+ },
38
51
  "check": {
39
52
  "type": "object",
40
- "required": ["ok", "diagnostics"],
53
+ "required": [
54
+ "ok",
55
+ "diagnostics"
56
+ ],
41
57
  "properties": {
42
- "ok": { "type": "boolean" },
43
- "diagnostics": { "type": "array" },
58
+ "ok": {
59
+ "type": "boolean"
60
+ },
61
+ "diagnostics": {
62
+ "type": "array"
63
+ },
44
64
  "counted": {
45
65
  "type": "object",
46
66
  "required": [
@@ -50,10 +70,18 @@
50
70
  "states"
51
71
  ],
52
72
  "properties": {
53
- "documents": { "type": "integer" },
54
- "concepts": { "type": "integer" },
55
- "relationships": { "type": "integer" },
56
- "states": { "type": "integer" }
73
+ "documents": {
74
+ "type": "integer"
75
+ },
76
+ "concepts": {
77
+ "type": "integer"
78
+ },
79
+ "relationships": {
80
+ "type": "integer"
81
+ },
82
+ "states": {
83
+ "type": "integer"
84
+ }
57
85
  },
58
86
  "additionalProperties": false
59
87
  }
@@ -65,28 +93,47 @@
65
93
  },
66
94
  "design": {
67
95
  "type": "object",
68
- "required": ["catalogue", "open"],
96
+ "required": [
97
+ "catalogue",
98
+ "open"
99
+ ],
69
100
  "properties": {
70
- "catalogue": { "type": "string", "minLength": 1 },
71
- "open": { "type": "integer", "minimum": 0 }
101
+ "catalogue": {
102
+ "type": "string",
103
+ "minLength": 1
104
+ },
105
+ "open": {
106
+ "type": "integer",
107
+ "minimum": 0
108
+ }
72
109
  },
73
110
  "additionalProperties": false
74
111
  },
75
112
  "backlog": {
76
113
  "type": "object",
77
- "required": ["planned", "current", "retired"],
114
+ "required": [
115
+ "planned",
116
+ "current",
117
+ "retired"
118
+ ],
78
119
  "properties": {
79
120
  "planned": {
80
121
  "type": "array",
81
- "items": { "$ref": "#/$defs/nextSubject" }
122
+ "items": {
123
+ "$ref": "#/$defs/nextSubject"
124
+ }
82
125
  },
83
126
  "current": {
84
127
  "type": "array",
85
- "items": { "$ref": "#/$defs/conceptEntry" }
128
+ "items": {
129
+ "$ref": "#/$defs/conceptEntry"
130
+ }
86
131
  },
87
132
  "retired": {
88
133
  "type": "array",
89
- "items": { "$ref": "#/$defs/conceptEntry" }
134
+ "items": {
135
+ "$ref": "#/$defs/conceptEntry"
136
+ }
90
137
  }
91
138
  },
92
139
  "additionalProperties": false
@@ -96,36 +143,119 @@
96
143
  },
97
144
  {
98
145
  "type": "object",
99
- "required": ["mode", "total", "subjects"],
146
+ "required": [
147
+ "mode",
148
+ "total",
149
+ "subjects"
150
+ ],
100
151
  "properties": {
101
152
  "format": true,
102
153
  "workspace": true,
103
- "mode": { "const": "roster" },
104
- "total": { "type": "integer", "minimum": 0 },
154
+ "mode": {
155
+ "const": "roster"
156
+ },
157
+ "total": {
158
+ "type": "integer",
159
+ "minimum": 0
160
+ },
105
161
  "subjects": {
106
162
  "type": "array",
107
- "items": { "$ref": "#/$defs/conceptEntry" }
163
+ "items": {
164
+ "$ref": "#/$defs/conceptEntry"
165
+ }
108
166
  }
109
167
  },
110
168
  "additionalProperties": false
111
169
  },
112
170
  {
113
171
  "type": "object",
114
- "required": ["mode", "addressing", "result"],
172
+ "required": [
173
+ "mode",
174
+ "addressing",
175
+ "result"
176
+ ],
115
177
  "properties": {
116
178
  "format": true,
117
179
  "workspace": true,
118
- "mode": { "const": "slice" },
180
+ "mode": {
181
+ "const": "slice"
182
+ },
119
183
  "addressing": {
120
- "enum": ["free-text", "subjects", "projection"]
184
+ "enum": [
185
+ "free-text",
186
+ "subjects",
187
+ "projection",
188
+ "changed"
189
+ ]
190
+ },
191
+ "topic": {
192
+ "type": "string",
193
+ "minLength": 1
121
194
  },
122
- "topic": { "type": "string", "minLength": 1 },
123
195
  "seeds": {
124
196
  "type": "array",
125
- "items": { "type": "string", "minLength": 1 }
197
+ "items": {
198
+ "type": "string",
199
+ "minLength": 1
200
+ }
201
+ },
202
+ "matched": {
203
+ "type": "integer",
204
+ "minimum": 0
205
+ },
206
+ "result": {
207
+ "$ref": "#/$defs/projectionResult"
126
208
  },
127
- "matched": { "type": "integer", "minimum": 0 },
128
- "result": { "$ref": "#/$defs/projectionResult" }
209
+ "changed": {
210
+ "type": "object",
211
+ "required": [
212
+ "range",
213
+ "concepts",
214
+ "relationships"
215
+ ],
216
+ "properties": {
217
+ "range": {
218
+ "type": "string",
219
+ "minLength": 1
220
+ },
221
+ "concepts": {
222
+ "type": "array",
223
+ "items": {
224
+ "type": "string",
225
+ "minLength": 1
226
+ }
227
+ },
228
+ "relationships": {
229
+ "type": "array",
230
+ "items": {
231
+ "type": "string",
232
+ "minLength": 1
233
+ }
234
+ }
235
+ },
236
+ "additionalProperties": false
237
+ },
238
+ "coverage": {
239
+ "type": "object",
240
+ "required": [
241
+ "projections",
242
+ "uncovered"
243
+ ],
244
+ "properties": {
245
+ "projections": {
246
+ "type": "integer",
247
+ "minimum": 0
248
+ },
249
+ "uncovered": {
250
+ "type": "array",
251
+ "items": {
252
+ "type": "string",
253
+ "minLength": 1
254
+ }
255
+ }
256
+ },
257
+ "additionalProperties": false
258
+ }
129
259
  },
130
260
  "additionalProperties": false
131
261
  },
@@ -142,36 +272,84 @@
142
272
  "properties": {
143
273
  "format": true,
144
274
  "workspace": true,
145
- "mode": { "const": "advice" },
146
- "topic": { "type": "string", "minLength": 1 },
275
+ "mode": {
276
+ "const": "advice"
277
+ },
278
+ "topic": {
279
+ "type": "string",
280
+ "minLength": 1
281
+ },
147
282
  "seeds": {
148
283
  "type": "array",
149
- "items": { "type": "string", "minLength": 1 }
284
+ "items": {
285
+ "type": "string",
286
+ "minLength": 1
287
+ }
288
+ },
289
+ "matched": {
290
+ "type": "integer",
291
+ "minimum": 0
292
+ },
293
+ "slice": {
294
+ "type": "string",
295
+ "minLength": 1
150
296
  },
151
- "matched": { "type": "integer", "minimum": 0 },
152
- "slice": { "type": "string", "minLength": 1 },
153
297
  "openQuestions": {
154
298
  "type": "array",
155
299
  "items": {
156
300
  "type": "object",
157
- "required": ["wave", "id", "authority", "question", "materiality"],
301
+ "required": [
302
+ "wave",
303
+ "id",
304
+ "authority",
305
+ "question",
306
+ "materiality"
307
+ ],
158
308
  "properties": {
159
- "wave": { "type": "string", "minLength": 1 },
160
- "id": { "type": "string", "minLength": 1 },
161
- "authority": { "enum": ["human", "agent", "either"] },
162
- "question": { "type": "string", "minLength": 1 },
163
- "materiality": { "type": "string", "minLength": 1 },
164
- "subject": { "type": "string", "minLength": 1 }
309
+ "wave": {
310
+ "type": "string",
311
+ "minLength": 1
312
+ },
313
+ "id": {
314
+ "type": "string",
315
+ "minLength": 1
316
+ },
317
+ "authority": {
318
+ "enum": [
319
+ "human",
320
+ "agent",
321
+ "either"
322
+ ]
323
+ },
324
+ "question": {
325
+ "type": "string",
326
+ "minLength": 1
327
+ },
328
+ "materiality": {
329
+ "type": "string",
330
+ "minLength": 1
331
+ },
332
+ "subject": {
333
+ "type": "string",
334
+ "minLength": 1
335
+ }
165
336
  },
166
337
  "additionalProperties": false
167
338
  }
168
339
  },
169
340
  "reconciliation": {
170
341
  "type": "object",
171
- "required": ["summary", "findings"],
342
+ "required": [
343
+ "summary",
344
+ "findings"
345
+ ],
172
346
  "properties": {
173
- "summary": { "$ref": "#/$defs/reconciliationSummary" },
174
- "findings": { "type": "array" }
347
+ "summary": {
348
+ "$ref": "#/$defs/reconciliationSummary"
349
+ },
350
+ "findings": {
351
+ "type": "array"
352
+ }
175
353
  },
176
354
  "additionalProperties": false
177
355
  }
@@ -180,34 +358,62 @@
180
358
  },
181
359
  {
182
360
  "type": "object",
183
- "required": ["mode", "subjects"],
361
+ "required": [
362
+ "mode",
363
+ "subjects"
364
+ ],
184
365
  "properties": {
185
366
  "format": true,
186
367
  "workspace": true,
187
- "mode": { "const": "next" },
368
+ "mode": {
369
+ "const": "next"
370
+ },
188
371
  "subjects": {
189
372
  "type": "array",
190
- "items": { "$ref": "#/$defs/nextSubject" }
373
+ "items": {
374
+ "$ref": "#/$defs/nextSubject"
375
+ }
191
376
  }
192
377
  },
193
378
  "additionalProperties": false
194
379
  },
195
380
  {
196
381
  "type": "object",
197
- "required": ["mode", "report"],
382
+ "required": [
383
+ "mode",
384
+ "report"
385
+ ],
198
386
  "properties": {
199
387
  "format": true,
200
388
  "workspace": true,
201
- "mode": { "const": "open" },
389
+ "mode": {
390
+ "const": "open"
391
+ },
202
392
  "report": {
203
393
  "type": "object",
204
- "required": ["format", "workspace", "catalogue", "summary", "waves"],
394
+ "required": [
395
+ "format",
396
+ "workspace",
397
+ "catalogue",
398
+ "summary",
399
+ "waves"
400
+ ],
205
401
  "properties": {
206
- "format": { "const": "yarramate/interrogation-report/v1" },
207
- "workspace": { "type": "string" },
208
- "catalogue": { "type": "string" },
209
- "summary": { "type": "object" },
210
- "waves": { "type": "array" }
402
+ "format": {
403
+ "const": "yarramate/interrogation-report/v1"
404
+ },
405
+ "workspace": {
406
+ "type": "string"
407
+ },
408
+ "catalogue": {
409
+ "type": "string"
410
+ },
411
+ "summary": {
412
+ "type": "object"
413
+ },
414
+ "waves": {
415
+ "type": "array"
416
+ }
211
417
  },
212
418
  "additionalProperties": false
213
419
  }
@@ -216,22 +422,48 @@
216
422
  },
217
423
  {
218
424
  "type": "object",
219
- "required": ["mode", "conceptKinds", "relationshipKinds", "extensions"],
425
+ "required": [
426
+ "mode",
427
+ "conceptKinds",
428
+ "relationshipKinds",
429
+ "extensions"
430
+ ],
220
431
  "properties": {
221
432
  "format": true,
222
433
  "workspace": true,
223
- "mode": { "const": "kinds" },
434
+ "mode": {
435
+ "const": "kinds"
436
+ },
224
437
  "conceptKinds": {
225
438
  "type": "array",
226
439
  "items": {
227
440
  "type": "object",
228
- "required": ["id", "name", "layer", "aspect"],
441
+ "required": [
442
+ "id",
443
+ "name",
444
+ "layer",
445
+ "aspect"
446
+ ],
229
447
  "properties": {
230
- "id": { "type": "string", "minLength": 1 },
231
- "name": { "type": "string", "minLength": 1 },
232
- "layer": { "type": "string", "minLength": 1 },
233
- "aspect": { "type": "string", "minLength": 1 },
234
- "inspiredBy": { "type": "string" }
448
+ "id": {
449
+ "type": "string",
450
+ "minLength": 1
451
+ },
452
+ "name": {
453
+ "type": "string",
454
+ "minLength": 1
455
+ },
456
+ "layer": {
457
+ "type": "string",
458
+ "minLength": 1
459
+ },
460
+ "aspect": {
461
+ "type": "string",
462
+ "minLength": 1
463
+ },
464
+ "inspiredBy": {
465
+ "type": "string"
466
+ }
235
467
  },
236
468
  "additionalProperties": false
237
469
  }
@@ -240,19 +472,34 @@
240
472
  "type": "array",
241
473
  "items": {
242
474
  "type": "object",
243
- "required": ["id", "intent"],
475
+ "required": [
476
+ "id",
477
+ "intent"
478
+ ],
244
479
  "properties": {
245
- "id": { "type": "string", "minLength": 1 },
246
- "intent": { "type": "string", "minLength": 1 },
480
+ "id": {
481
+ "type": "string",
482
+ "minLength": 1
483
+ },
484
+ "intent": {
485
+ "type": "string",
486
+ "minLength": 1
487
+ },
247
488
  "sourceAspects": {
248
489
  "type": "array",
249
- "items": { "type": "string" }
490
+ "items": {
491
+ "type": "string"
492
+ }
250
493
  },
251
494
  "targetAspects": {
252
495
  "type": "array",
253
- "items": { "type": "string" }
496
+ "items": {
497
+ "type": "string"
498
+ }
254
499
  },
255
- "repair": { "type": "string" }
500
+ "repair": {
501
+ "type": "string"
502
+ }
256
503
  },
257
504
  "additionalProperties": false
258
505
  }
@@ -261,13 +508,27 @@
261
508
  "type": "array",
262
509
  "items": {
263
510
  "type": "object",
264
- "required": ["id", "type", "lineage"],
511
+ "required": [
512
+ "id",
513
+ "type",
514
+ "lineage"
515
+ ],
265
516
  "properties": {
266
- "id": { "type": "string", "minLength": 1 },
267
- "type": { "enum": ["concept", "relationship"] },
517
+ "id": {
518
+ "type": "string",
519
+ "minLength": 1
520
+ },
521
+ "type": {
522
+ "enum": [
523
+ "concept",
524
+ "relationship"
525
+ ]
526
+ },
268
527
  "lineage": {
269
528
  "type": "array",
270
- "items": { "type": "string" }
529
+ "items": {
530
+ "type": "string"
531
+ }
271
532
  }
272
533
  },
273
534
  "additionalProperties": false
@@ -278,21 +539,45 @@
278
539
  },
279
540
  {
280
541
  "type": "object",
281
- "required": ["mode", "comparison"],
542
+ "required": [
543
+ "mode",
544
+ "comparison"
545
+ ],
282
546
  "properties": {
283
547
  "format": true,
284
548
  "workspace": true,
285
- "mode": { "const": "compare" },
549
+ "mode": {
550
+ "const": "compare"
551
+ },
286
552
  "comparison": {
287
553
  "type": "object",
288
- "required": ["format", "from", "to", "added", "removed", "retained"],
554
+ "required": [
555
+ "format",
556
+ "from",
557
+ "to",
558
+ "added",
559
+ "removed",
560
+ "retained"
561
+ ],
289
562
  "properties": {
290
- "format": { "const": "yarramate/state-comparison/v1" },
291
- "from": { "type": "string" },
292
- "to": { "type": "string" },
293
- "added": { "type": "array" },
294
- "removed": { "type": "array" },
295
- "retained": { "type": "array" }
563
+ "format": {
564
+ "const": "yarramate/state-comparison/v1"
565
+ },
566
+ "from": {
567
+ "type": "string"
568
+ },
569
+ "to": {
570
+ "type": "string"
571
+ },
572
+ "added": {
573
+ "type": "array"
574
+ },
575
+ "removed": {
576
+ "type": "array"
577
+ },
578
+ "retained": {
579
+ "type": "array"
580
+ }
296
581
  },
297
582
  "additionalProperties": false
298
583
  }
@@ -303,30 +588,67 @@
303
588
  "$defs": {
304
589
  "conceptEntry": {
305
590
  "type": "object",
306
- "required": ["id", "kind"],
591
+ "required": [
592
+ "id",
593
+ "kind"
594
+ ],
307
595
  "properties": {
308
- "id": { "type": "string", "minLength": 1 },
309
- "kind": { "type": "string", "minLength": 1 },
310
- "name": { "type": "string" },
311
- "status": { "enum": ["planned", "current", "retired"] },
312
- "description": { "type": "string" }
596
+ "id": {
597
+ "type": "string",
598
+ "minLength": 1
599
+ },
600
+ "kind": {
601
+ "type": "string",
602
+ "minLength": 1
603
+ },
604
+ "name": {
605
+ "type": "string"
606
+ },
607
+ "status": {
608
+ "enum": [
609
+ "planned",
610
+ "current",
611
+ "retired"
612
+ ]
613
+ },
614
+ "description": {
615
+ "type": "string"
616
+ }
313
617
  },
314
618
  "additionalProperties": false
315
619
  },
316
620
  "nextSubject": {
317
621
  "type": "object",
318
- "required": ["id", "kind", "dependsOn", "requiredBy", "evidence"],
622
+ "required": [
623
+ "id",
624
+ "kind",
625
+ "dependsOn",
626
+ "requiredBy",
627
+ "evidence"
628
+ ],
319
629
  "properties": {
320
- "id": { "type": "string", "minLength": 1 },
321
- "kind": { "type": "string", "minLength": 1 },
322
- "name": { "type": "string" },
630
+ "id": {
631
+ "type": "string",
632
+ "minLength": 1
633
+ },
634
+ "kind": {
635
+ "type": "string",
636
+ "minLength": 1
637
+ },
638
+ "name": {
639
+ "type": "string"
640
+ },
323
641
  "dependsOn": {
324
642
  "type": "array",
325
- "items": { "type": "string" }
643
+ "items": {
644
+ "type": "string"
645
+ }
326
646
  },
327
647
  "requiredBy": {
328
648
  "type": "array",
329
- "items": { "type": "string" }
649
+ "items": {
650
+ "type": "string"
651
+ }
330
652
  },
331
653
  "evidence": {
332
654
  "type": "object",
@@ -338,28 +660,58 @@
338
660
  "notObserved"
339
661
  ],
340
662
  "properties": {
341
- "observations": { "type": "integer" },
342
- "confirmed": { "type": "integer" },
343
- "contradicted": { "type": "integer" },
344
- "unknown": { "type": "integer" },
345
- "notObserved": { "type": "integer" }
663
+ "observations": {
664
+ "type": "integer"
665
+ },
666
+ "confirmed": {
667
+ "type": "integer"
668
+ },
669
+ "contradicted": {
670
+ "type": "integer"
671
+ },
672
+ "unknown": {
673
+ "type": "integer"
674
+ },
675
+ "notObserved": {
676
+ "type": "integer"
677
+ }
346
678
  },
347
679
  "additionalProperties": false
348
680
  },
349
- "cycle": { "const": true }
681
+ "cycle": {
682
+ "const": true
683
+ }
350
684
  },
351
685
  "additionalProperties": false
352
686
  },
353
687
  "projectionResult": {
354
688
  "type": "object",
355
- "required": ["format", "projection", "documents", "subjects", "claims"],
689
+ "required": [
690
+ "format",
691
+ "projection",
692
+ "documents",
693
+ "subjects",
694
+ "claims"
695
+ ],
356
696
  "properties": {
357
- "format": { "const": "yarramate/projection-result/v1" },
358
- "projection": { "type": "string" },
359
- "presentation": { "type": "object" },
360
- "documents": { "type": "array" },
361
- "subjects": { "type": "array" },
362
- "claims": { "type": "array" }
697
+ "format": {
698
+ "const": "yarramate/projection-result/v1"
699
+ },
700
+ "projection": {
701
+ "type": "string"
702
+ },
703
+ "presentation": {
704
+ "type": "object"
705
+ },
706
+ "documents": {
707
+ "type": "array"
708
+ },
709
+ "subjects": {
710
+ "type": "array"
711
+ },
712
+ "claims": {
713
+ "type": "array"
714
+ }
363
715
  },
364
716
  "additionalProperties": false
365
717
  },
@@ -376,14 +728,30 @@
376
728
  "subjectsWithoutEvidence"
377
729
  ],
378
730
  "properties": {
379
- "evidenceDocuments": { "type": "integer" },
380
- "observations": { "type": "integer" },
381
- "confirmed": { "type": "integer" },
382
- "findings": { "type": "integer" },
383
- "contradicted": { "type": "integer" },
384
- "unknown": { "type": "integer" },
385
- "notObserved": { "type": "integer" },
386
- "subjectsWithoutEvidence": { "type": "integer" }
731
+ "evidenceDocuments": {
732
+ "type": "integer"
733
+ },
734
+ "observations": {
735
+ "type": "integer"
736
+ },
737
+ "confirmed": {
738
+ "type": "integer"
739
+ },
740
+ "findings": {
741
+ "type": "integer"
742
+ },
743
+ "contradicted": {
744
+ "type": "integer"
745
+ },
746
+ "unknown": {
747
+ "type": "integer"
748
+ },
749
+ "notObserved": {
750
+ "type": "integer"
751
+ },
752
+ "subjectsWithoutEvidence": {
753
+ "type": "integer"
754
+ }
387
755
  },
388
756
  "additionalProperties": false
389
757
  }
@@ -49,6 +49,14 @@
49
49
  "enum": ["planned", "current", "retired"]
50
50
  }
51
51
  },
52
+ "excludeStatuses": {
53
+ "description": "Drop concepts carrying one of these lifecycle statuses while keeping concepts that declare no status at all - 'everything except retired' for viewpoint projections, where a bare statuses filter would wrongly drop unstatused actors and motivation elements.",
54
+ "type": "array",
55
+ "uniqueItems": true,
56
+ "items": {
57
+ "enum": ["planned", "current", "retired"]
58
+ }
59
+ },
52
60
  "states": {
53
61
  "type": "array",
54
62
  "minItems": 1,