@transitrix/diagrams 1.15.0 → 1.16.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 (39) hide show
  1. package/dist/compliance/catalogue.d.ts +17 -0
  2. package/dist/compliance/catalogue.js +68 -0
  3. package/dist/compliance/index.d.ts +4 -2
  4. package/dist/compliance/index.js +2 -1
  5. package/dist/compliance/requirement-chain.d.ts +13 -2
  6. package/dist/compliance/requirement-chain.js +60 -33
  7. package/dist/products/types.d.ts +1 -1
  8. package/dist/products/validate.js +1 -1
  9. package/dist/standalone/validate.js +64 -3
  10. package/dist/webview/render-activities.d.ts +6 -0
  11. package/dist/webview/render-activities.js +2 -2
  12. package/dist/webview/render-activity-card.d.ts +6 -0
  13. package/dist/webview/render-activity-card.js +2 -2
  14. package/dist/webview/render-blocks.d.ts +10 -0
  15. package/dist/webview/render-blocks.js +4 -4
  16. package/dist/webview/render-fgca.d.ts +6 -0
  17. package/dist/webview/render-fgca.js +2 -2
  18. package/dist/webview/render-goals.d.ts +5 -0
  19. package/dist/webview/render-goals.js +9 -2
  20. package/dist/webview/render-process-blueprint.d.ts +6 -0
  21. package/dist/webview/render-process-blueprint.js +2 -2
  22. package/dist/webview/render-products.js +1 -0
  23. package/dist/yaml-normalize.d.ts +3 -1
  24. package/dist/yaml-normalize.js +11 -3
  25. package/package.json +1 -1
  26. package/src/compliance/catalogue.ts +49 -0
  27. package/src/compliance/index.ts +5 -2
  28. package/src/compliance/requirement-chain.ts +52 -27
  29. package/src/products/types.ts +1 -1
  30. package/src/products/validate.ts +1 -1
  31. package/src/standalone/validate.ts +38 -2
  32. package/src/webview/render-activities.ts +8 -2
  33. package/src/webview/render-activity-card.ts +8 -3
  34. package/src/webview/render-blocks.ts +14 -4
  35. package/src/webview/render-fgca.ts +8 -2
  36. package/src/webview/render-goals.ts +21 -2
  37. package/src/webview/render-process-blueprint.ts +8 -3
  38. package/src/webview/render-products.ts +1 -0
  39. package/src/yaml-normalize.ts +12 -3
@@ -0,0 +1,17 @@
1
+ /** Relative POSIX paths keep scanning policy and content identity independent of host APIs. */
2
+ export interface RequirementCatalogueIO {
3
+ read(path: string): Promise<string>;
4
+ list(path: string): Promise<Array<{
5
+ name: string;
6
+ kind: 'file' | 'directory' | 'symlink' | 'other';
7
+ }>>;
8
+ missing(error: unknown): boolean;
9
+ parse(content: string): unknown;
10
+ digest(content: string): string;
11
+ sourcePath?(path: string): string;
12
+ }
13
+ /** Shared disk/editor policy: one manifest boundary, all three zones, lossless failures. */
14
+ export declare function scanRequirementCatalogue(io: RequirementCatalogueIO): Promise<{
15
+ canon: import("./classify.js").ComplianceCanon;
16
+ snapshotId: string;
17
+ }>;
@@ -0,0 +1,68 @@
1
+ import { emptyCanon, ingestComplianceDoc } from './classify.js';
2
+ /** Shared disk/editor policy: one manifest boundary, all three zones, lossless failures. */
3
+ export async function scanRequirementCatalogue(io) {
4
+ const canon = emptyCanon();
5
+ const transcript = [];
6
+ const manifest = await io.read('transitrix.yaml');
7
+ const parsed = io.parse(manifest);
8
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
9
+ throw new Error('Invalid transitrix.yaml manifest: expected a mapping');
10
+ transcript.push(['transitrix.yaml', manifest]);
11
+ const fail = (path, message) => {
12
+ transcript.push(['failure', path, message]);
13
+ canon.findings.push({ id: `${path}:load`, owner: path, field: '', code: 'LOAD', message, reference: false, severity: 'error' });
14
+ };
15
+ const walk = async (dir, top = false) => {
16
+ let entries;
17
+ try {
18
+ entries = await io.list(dir);
19
+ }
20
+ catch (error) {
21
+ if (top && io.missing(error)) {
22
+ transcript.push(['absent-zone', dir]);
23
+ return;
24
+ }
25
+ fail(dir, 'Directory could not be enumerated');
26
+ return;
27
+ }
28
+ if (!top && entries.some(e => e.name === 'transitrix.yaml')) {
29
+ transcript.push(['catalogue-boundary', dir]);
30
+ return;
31
+ }
32
+ for (const { name, kind } of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
33
+ const path = `${dir}/${name}`;
34
+ if (kind === 'symlink') {
35
+ fail(path, 'Symbolic link was not enumerated');
36
+ continue;
37
+ }
38
+ if (kind === 'directory') {
39
+ await walk(path);
40
+ continue;
41
+ }
42
+ if (!/\.ya?ml$/i.test(name))
43
+ continue;
44
+ if (kind !== 'file') {
45
+ fail(path, 'Unsupported file type');
46
+ continue;
47
+ }
48
+ let content;
49
+ try {
50
+ content = await io.read(path);
51
+ }
52
+ catch {
53
+ fail(path, 'Unreadable YAML record');
54
+ continue;
55
+ }
56
+ transcript.push([path, content]);
57
+ try {
58
+ ingestComplianceDoc(canon, io.parse(content), io.sourcePath?.(path) ?? path);
59
+ }
60
+ catch {
61
+ fail(path, 'Malformed YAML record');
62
+ }
63
+ }
64
+ };
65
+ for (const zone of ['canon', 'codex', 'field'])
66
+ await walk(zone, true);
67
+ return { canon, snapshotId: `sha256:${io.digest(JSON.stringify(transcript))}` };
68
+ }
@@ -19,6 +19,8 @@ export { assertionToScoringElement, complianceConfidenceHeader, requirementToSco
19
19
  export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
20
20
  export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, CoverageViewFinding, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
21
21
  export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, IndexVerification, IndexNeed, IndexValidation, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, RequirementTrace, TraceAssertionRow, TraceVerificationRow, TraceElementCatalog, TraceElementRef, TraceSourceRef, } from './types.js';
22
- export { buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
23
- export type { ChainInput, ChainScope, ChainSet, RequirementChainNode, RequirementChainEdge, RequirementChainProjection } from './requirement-chain.js';
22
+ export { buildSubjectRequirementChain, buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
23
+ export type { ChainSubject, ChainInput, ChainScope, ChainSet, RequirementChainNode, RequirementChainEdge, RequirementChainProjection } from './requirement-chain.js';
24
24
  export type { ChainRecord, ChainFinding } from './types.js';
25
+ export { scanRequirementCatalogue } from './catalogue.js';
26
+ export type { RequirementCatalogueIO } from './catalogue.js';
@@ -11,4 +11,5 @@ export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
11
11
  export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
12
12
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
13
13
  export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
14
- export { buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
14
+ export { buildSubjectRequirementChain, buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
15
+ export { scanRequirementCatalogue } from './catalogue.js';
@@ -1,7 +1,12 @@
1
1
  import type { ChainRecord, ChainFinding, ComplianceIndex } from './types.js';
2
2
  export declare const CHAIN_STAGES: readonly ["Source document", "Driver", "Need", "Stakeholder requirement", "System requirement", "Software requirement", "Unclassified requirement", "Verification definition", "Result"];
3
+ export interface ChainSubject {
4
+ type: 'PRODUCT' | 'APPLICATION';
5
+ id: string;
6
+ }
3
7
  export interface ChainScope {
4
8
  catalogue: string;
9
+ subject?: ChainSubject;
5
10
  product?: string;
6
11
  release?: string;
7
12
  project?: string;
@@ -46,7 +51,7 @@ export interface ChainAssignment {
46
51
  relations: string[];
47
52
  }
48
53
  export interface RequirementChainProjection {
49
- contract: 'requirement-chain/0.2';
54
+ contract: 'requirement-chain/0.2' | 'requirement-chain/0.3';
50
55
  contractRevision: string;
51
56
  asAt: string;
52
57
  snapshotId: string;
@@ -89,8 +94,14 @@ export interface RequirementChainProjection {
89
94
  executions: Record<string, string[]>;
90
95
  }
91
96
  export declare function chainDate(v: unknown): v is string;
92
- /** Pure, lossless release projection; no legacy coverage verdict is reinterpreted. */
97
+ /** Explicit PRODUCT compatibility adapter for requirement-chain/0.2. */
93
98
  export declare function buildRequirementChain(input: ChainInput): RequirementChainProjection;
99
+ /** Subject-aware 0.3 entry point; both versions use the same deterministic projection. */
100
+ export declare function buildSubjectRequirementChain(input: ChainInput & {
101
+ scope: ChainScope & {
102
+ subject: ChainSubject;
103
+ };
104
+ }): RequirementChainProjection;
94
105
  /** Both is two independent walks, never an expansion from the union frontier. */
95
106
  export declare function selectRequirementChain(p: RequirementChainProjection, focus?: string, direction?: 'upstream' | 'downstream' | 'both'): {
96
107
  nodes: RequirementChainNode[];
@@ -26,10 +26,28 @@ function freeze(value) {
26
26
  }
27
27
  const CODEX = ['LAW', 'REGULATION', 'STANDARD', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'];
28
28
  const FIELD = ['INTERVIEW', 'SURVEY', 'OBSERVATION', 'DRAFT'];
29
- /** Pure, lossless release projection; no legacy coverage verdict is reinterpreted. */
29
+ /** Explicit PRODUCT compatibility adapter for requirement-chain/0.2. */
30
30
  export function buildRequirementChain(input) {
31
+ if (input.scope.subject)
32
+ throw new Error('Use buildSubjectRequirementChain for explicit subjects');
33
+ return projectRequirementChain(input);
34
+ }
35
+ /** Subject-aware 0.3 entry point; both versions use the same deterministic projection. */
36
+ export function buildSubjectRequirementChain(input) {
37
+ if (input.scope.product !== undefined)
38
+ throw new Error('Conflicting product and subject selectors');
39
+ if (!['PRODUCT', 'APPLICATION'].includes(input.scope.subject.type))
40
+ throw new Error('Unsupported subject type');
41
+ return projectRequirementChain(input);
42
+ }
43
+ /** Pure, lossless release projection; no legacy coverage verdict is reinterpreted. */
44
+ function projectRequirementChain(input) {
31
45
  var _a;
32
46
  const { index, asAt, scope } = input;
47
+ const subject = scope.subject ?? { type: 'PRODUCT', id: scope.product };
48
+ const membershipKind = subject.type === 'APPLICATION' ? 'application_scope' : 'product_scope';
49
+ const pairKind = subject.type === 'APPLICATION' ? 'project_application' : 'project_product';
50
+ const subjectWord = scope.subject ? 'subject' : 'product';
33
51
  const findings = structuredClone(index.findings);
34
52
  const scopeFindings = [];
35
53
  const records = new Map();
@@ -39,7 +57,7 @@ export function buildRequirementChain(input) {
39
57
  if (!findings.some(f => f.id === id && f.code === code))
40
58
  findings.push({ id, owner, field, code, message, reference, severity: warning ? 'warning' : 'error' });
41
59
  };
42
- for (const [id, versions] of index.recordsById) {
60
+ for (const [id, versions] of [...index.recordsById].sort(([a], [b]) => a.localeCompare(b))) {
43
61
  if (versions.length !== 1) {
44
62
  finding(id, 'id', 'DUPLICATE', 'Ambiguous catalogue identity');
45
63
  complete = false;
@@ -109,7 +127,7 @@ export function buildRequirementChain(input) {
109
127
  node(r, 2);
110
128
  else if (r.type === 'DRIVER')
111
129
  node(r, 1);
112
- else if (['PRODUCT', 'RELEASE', 'ACTION'].includes(r.type))
130
+ else if (['PRODUCT', 'APPLICATION', 'RELEASE', 'ACTION'].includes(r.type))
113
131
  node(r, -1);
114
132
  else if (CODEX.includes(r.type) || FIELD.includes(r.type)) {
115
133
  node(r, 0);
@@ -165,13 +183,15 @@ export function buildRequirementChain(input) {
165
183
  const src = records.get(from), dst = records.get(to);
166
184
  const endpoints = kind === 'requirement_parent' ? is(from, 'REQUIREMENT') && is(to, 'REQUIREMENT') :
167
185
  kind === 'serves' ? is(from, 'REQUIREMENT') && is(to, 'NEED') :
168
- kind === 'product_scope' ? is(from, 'REQUIREMENT') && is(to, 'PRODUCT') :
169
- kind === 'project_scope' ? is(from, 'REQUIREMENT') && isProject(to) :
170
- kind === 'project_product' ? isProject(from) && is(to, 'PRODUCT') :
171
- kind === 'required_for' ? is(from, 'REQUIREMENT') && is(to, 'RELEASE') :
172
- kind === 'source_trace' ? !!src && !!dst && envelope.get(from) === true && envelope.get(to) === true &&
173
- (['REQUIREMENT', 'NEED'].includes(src.type) && (dst.type === 'DRIVER' || FIELD.includes(dst.type)) ||
174
- src.type === 'DRIVER' && FIELD.includes(dst.type)) : !!src && !!dst;
186
+ kind === 'application_scope' ? is(from, 'REQUIREMENT') && is(to, 'APPLICATION') :
187
+ kind === 'project_application' ? isProject(from) && is(to, 'APPLICATION') :
188
+ kind === 'product_scope' ? is(from, 'REQUIREMENT') && is(to, 'PRODUCT') :
189
+ kind === 'project_scope' ? is(from, 'REQUIREMENT') && isProject(to) :
190
+ kind === 'project_product' ? isProject(from) && is(to, 'PRODUCT') :
191
+ kind === 'required_for' ? is(from, 'REQUIREMENT') && is(to, 'RELEASE') :
192
+ kind === 'source_trace' ? !!src && !!dst && envelope.get(from) === true && envelope.get(to) === true &&
193
+ (['REQUIREMENT', 'NEED'].includes(src.type) && (dst.type === 'DRIVER' || FIELD.includes(dst.type)) ||
194
+ src.type === 'DRIVER' && FIELD.includes(dst.type)) : !!src && !!dst;
175
195
  const temporal = contains(r, src) && contains(r, dst);
176
196
  if (!temporal)
177
197
  finding(r.id, 'to', 'REL-003', 'Relation window must be contained in both endpoints', true);
@@ -207,38 +227,38 @@ export function buildRequirementChain(input) {
207
227
  }
208
228
  }
209
229
  const memberships = (id, kind) => rels.filter(r => usable.has(r.id) && r.raw.type === kind && r.raw.from === id).map(r => String(r.raw.to));
210
- const unresolved = requirements.filter(r => active.get(r.id) && memberships(r.id, 'product_scope').length === 0).map(r => r.id);
211
- const P = requirements.filter(r => active.get(r.id) && memberships(r.id, 'product_scope').includes(scope.product ?? '')).map(r => r.id);
230
+ const unresolved = requirements.filter(r => active.get(r.id) && memberships(r.id, 'product_scope').length === 0 && (!scope.subject || memberships(r.id, 'application_scope').length === 0)).map(r => r.id);
231
+ const P = requirements.filter(r => active.get(r.id) && memberships(r.id, membershipKind).includes(subject.id ?? '')).map(r => r.id);
212
232
  let productComplete = complete && unresolved.length === 0;
213
233
  if (unresolved.length)
214
- scopeFindings.push('Unresolved product membership');
215
- if (requirements.some(r => !envelope.get(r.id)) || rels.some(r => r.raw.type === 'product_scope' && active.get(r.id) && !usable.has(r.id)))
234
+ scopeFindings.push(`Unresolved ${subjectWord} membership`);
235
+ if (requirements.some(r => !envelope.get(r.id)) || rels.some(r => r.raw.type === membershipKind && active.get(r.id) && !usable.has(r.id)))
216
236
  productComplete = false;
217
237
  const invalidAssignments = new Set();
218
238
  for (const r of rels.filter(r => r.raw.type === 'required_for')) {
219
239
  const from = String(r.raw.from), to = records.get(String(r.raw.to));
220
240
  const historical = chainDate(r.raw.valid_to) && r.raw.valid_to < asAt;
221
241
  const future = chainDate(r.raw.valid_from) && r.raw.valid_from > asAt;
222
- if (!historical && !future && (!usable.has(r.id) || !memberships(from, 'product_scope').includes(String(to?.raw.of)))) {
242
+ if (!historical && !future && (!usable.has(r.id) || ![...memberships(from, 'product_scope'), ...(scope.subject ? memberships(from, 'application_scope') : [])].includes(String(to?.raw.of)))) {
223
243
  invalidAssignments.add(from);
224
- finding(r.id, 'to', 'ASSIGNMENT', 'Invalid, dangling or wrong-product assignment', true);
244
+ finding(r.id, 'to', 'ASSIGNMENT', `Invalid, dangling or wrong-${subjectWord} assignment`, true);
225
245
  if (!envelope.get(r.id))
226
246
  productComplete = false;
227
247
  }
228
248
  }
229
249
  let selectedComplete = complete;
230
- if (!is(scope.product, 'PRODUCT')) {
250
+ if (!is(subject.id, subject.type)) {
231
251
  productComplete = false;
232
252
  selectedComplete = false;
233
- scopeFindings.push('Product unselected or unresolved');
253
+ scopeFindings.push(scope.subject ? 'Subject unselected or unresolved' : 'Product unselected or unresolved');
234
254
  }
235
- if (!is(scope.release, 'RELEASE') || records.get(scope.release ?? '')?.raw.of !== scope.product) {
255
+ if (!is(scope.release, 'RELEASE') || records.get(scope.release ?? '')?.raw.of !== subject.id) {
236
256
  selectedComplete = false;
237
- scopeFindings.push('Release unselected, unresolved or wrong product');
257
+ scopeFindings.push(`Release unselected, unresolved or wrong ${subjectWord}`);
238
258
  }
239
- if (scope.project && (!isProject(scope.project) || !memberships(scope.project, 'project_product').includes(scope.product ?? ''))) {
259
+ if (scope.project && (!isProject(scope.project) || !memberships(scope.project, pairKind).includes(subject.id ?? ''))) {
240
260
  selectedComplete = false;
241
- scopeFindings.push('Invalid or unmodelled project/product pair');
261
+ scopeFindings.push(`Invalid or unmodelled project/${subjectWord} pair`);
242
262
  }
243
263
  function query(release) {
244
264
  const chain = [];
@@ -246,9 +266,9 @@ export function buildRequirementChain(input) {
246
266
  let valid = true;
247
267
  while (cursor) {
248
268
  const r = records.get(cursor);
249
- if (chain.includes(cursor) || !is(cursor, 'RELEASE') || r?.raw.of !== scope.product) {
269
+ if (chain.includes(cursor) || !is(cursor, 'RELEASE') || r?.raw.of !== subject.id) {
250
270
  valid = false;
251
- finding(chain[chain.length - 1] ?? release, 'predecessor', 'RELEASE-SCOPE', chain.includes(cursor) ? 'Release predecessor cycle' : 'Missing, invalid or cross-product predecessor');
271
+ finding(chain[chain.length - 1] ?? release, 'predecessor', 'RELEASE-SCOPE', chain.includes(cursor) ? 'Release predecessor cycle' : `Missing, invalid or cross-${subjectWord} predecessor`);
252
272
  break;
253
273
  }
254
274
  chain.push(cursor);
@@ -275,16 +295,16 @@ export function buildRequirementChain(input) {
275
295
  selectedComplete = false;
276
296
  scopeFindings.push('Unresolved project membership');
277
297
  }
278
- if (rels.some(r => ['product_scope', 'project_scope', 'required_for'].includes(String(r.raw.type)) && !envelope.get(r.id)))
298
+ if (rels.some(r => [membershipKind, 'project_scope', 'required_for'].includes(String(r.raw.type)) && !envelope.get(r.id)))
279
299
  selectedComplete = false;
280
- if (scope.project && L.some(id => memberships(id, 'project_scope').some(project => !memberships(project, 'project_product').includes(scope.product ?? '')))) {
300
+ if (scope.project && L.some(id => memberships(id, 'project_scope').some(project => !memberships(project, pairKind).includes(subject.id ?? '')))) {
281
301
  selectedComplete = false;
282
- scopeFindings.push('Inconsistent project/product membership');
302
+ scopeFindings.push(`Inconsistent project/${subjectWord} membership`);
283
303
  }
284
304
  const S = L.filter(id => !scope.project || memberships(id, 'project_scope').includes(scope.project));
285
305
  const elsewhere = new Set();
286
306
  for (const release of records.values())
287
- if (release.type === 'RELEASE' && release.raw.of === scope.product) {
307
+ if (release.type === 'RELEASE' && release.raw.of === subject.id) {
288
308
  const q = query(release.id);
289
309
  productComplete && (productComplete = q.complete);
290
310
  q.ids.forEach(id => elsewhere.add(id));
@@ -352,7 +372,7 @@ export function buildRequirementChain(input) {
352
372
  if (v.raw.verifies === r.id)
353
373
  owners.add(v.id);
354
374
  for (const rel of rels) {
355
- const ownScope = ['product_scope', 'project_scope', 'required_for'].includes(String(rel.raw.type));
375
+ const ownScope = [membershipKind, 'project_scope', 'required_for'].includes(String(rel.raw.type));
356
376
  if (ownScope ? rel.raw.from === r.id : owners.has(String(rel.raw.from)))
357
377
  owners.add(rel.id);
358
378
  }
@@ -379,15 +399,17 @@ export function buildRequirementChain(input) {
379
399
  const broken = S.filter(id => selectedRefs.some(f => affectedRequirements[f.id]?.includes(id)));
380
400
  for (const n of nodes)
381
401
  if (records.get(n.recordId)?.type === 'REQUIREMENT' && !S.includes(n.recordId)) {
382
- n.context.push(!active.get(n.recordId) ? 'inactive' : !P.includes(n.recordId) ? 'unresolved or other product' :
402
+ n.context.push(!active.get(n.recordId) ? 'inactive' : !P.includes(n.recordId) ? `unresolved or other ${subjectWord}` :
383
403
  !L.includes(n.recordId) ? 'other release or unassigned' : 'other project');
384
404
  }
405
+ if (scope.subject && (!is(subject.id, subject.type) || !is(scope.release, 'RELEASE') || records.get(scope.release ?? '')?.raw.of !== subject.id))
406
+ productComplete = false;
385
407
  const unassignedSet = set(unassigned, productComplete);
386
408
  const result = {
387
- contract: 'requirement-chain/0.2', contractRevision: '97c9d41819011ead8fe192c266cba32707eae82f', asAt, snapshotId: input.snapshotId, sourceRevision: input.sourceRevision,
409
+ contract: scope.subject ? 'requirement-chain/0.3' : 'requirement-chain/0.2', contractRevision: scope.subject ? 'a131b1862d86f34be8feb729e1cc6ebc994c3891' : '97c9d41819011ead8fe192c266cba32707eae82f', asAt, snapshotId: input.snapshotId, sourceRevision: input.sourceRevision,
388
410
  scope: structuredClone(scope), completeness: productComplete && selectedComplete ? 'complete' : 'incomplete', scopeFindings: sorted(scopeFindings),
389
411
  populations: { product: set(P, productComplete), release: set(L, selectedComplete), selected: set(S, selectedComplete),
390
- inactive: set(requirements.filter(r => envelope.get(r.id) && !active.get(r.id) && rels.some(rel => rel.raw.type === 'product_scope' && rel.raw.from === r.id && rel.raw.to === scope.product && envelope.get(rel.id))).map(r => r.id), complete),
412
+ inactive: set(requirements.filter(r => envelope.get(r.id) && !active.get(r.id) && rels.some(rel => rel.raw.type === membershipKind && rel.raw.from === r.id && rel.raw.to === subject.id && envelope.get(rel.id))).map(r => r.id), complete),
391
413
  unresolved: set(unresolved, complete) },
392
414
  assignments: { here: set(L, selectedComplete), otherReleaseOnly: set(P.filter(id => elsewhere.has(id) && !L.includes(id)), productComplete),
393
415
  unassigned: unassignedSet, invalid: set(P.filter(id => invalidAssignments.has(id)), complete), provenance: selected.provenance.sort((a, b) => a.requirement.localeCompare(b.requirement)) },
@@ -474,6 +496,9 @@ export function requirementReleaseCounts(p) {
474
496
  const contextUnits = { 0: 'source documents', 1: 'drivers', 2: 'needs', 7: 'definition parts', 8: 'result parts' };
475
497
  for (const stage of [0, 1, 2, 7, 8])
476
498
  add('context-' + stage, CHAIN_STAGES[stage], set(view.nodes.filter(n => n.stage === stage).map(n => n.id), p.populations.selected.completeness === 'complete'), contextUnits[stage]);
499
+ if (p.scope.subject)
500
+ for (const count of Object.values(counts))
501
+ count.label = count.label.replace(/whole product/g, 'whole subject').replace('Product requirements', 'Subject requirements');
477
502
  return freeze(counts);
478
503
  }
479
504
  /** Monotonic refresh gate shared by reports; late successes and failures cannot replace newer data. */
@@ -488,7 +513,9 @@ export class RequirementChainSnapshot {
488
513
  const input = await load();
489
514
  if (generation !== this.generation)
490
515
  return false;
491
- this.current = buildRequirementChain(input);
516
+ this.current = input.scope.subject
517
+ ? buildSubjectRequirementChain({ ...input, scope: { ...input.scope, subject: input.scope.subject } })
518
+ : buildRequirementChain(input);
492
519
  this.stale = false;
493
520
  return true;
494
521
  }
@@ -1,4 +1,4 @@
1
- export type ProductType = 'digital_product' | 'service' | 'platform' | 'bundle';
1
+ export type ProductType = 'physical_product' | 'digital_product' | 'service' | 'platform' | 'bundle';
2
2
  export type ProductStatus = 'Draft' | 'Active' | 'Deprecated';
3
3
  export interface Product {
4
4
  product_id: string;
@@ -1,5 +1,5 @@
1
1
  import { schemaFinding } from '../schema-finding.js';
2
- const VALID_TYPES = new Set(['digital_product', 'service', 'platform', 'bundle']);
2
+ const VALID_TYPES = new Set(['physical_product', 'digital_product', 'service', 'platform', 'bundle']);
3
3
  const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated']);
4
4
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
5
5
  export function validateProductsCatalogue(input) {
@@ -1,6 +1,6 @@
1
1
  // Standalone forms from ELEMENT_PRIMITIVES and CONTRACT (Methodology 7).
2
2
  // Field payloads are opaque: only their published identity/admission contract is checked.
3
- import { isCanonicalIdOfType, typeOfId } from '../typed-id.js';
3
+ import { isCanonicalId, isCanonicalIdOfType, typeOfId } from '../typed-id.js';
4
4
  export const mapping = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
5
5
  const text = (v) => typeof v === 'string' && v.trim().length > 0;
6
6
  export const isoDate = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v)) && new Date(v).toISOString().slice(0, 10) === v;
@@ -68,8 +68,8 @@ export function validateStandalone(notation, input, options = {}) {
68
68
  };
69
69
  optionalText('description');
70
70
  if (notation === 'product') {
71
- if (!['digital_product', 'service', 'platform', 'bundle'].includes(d.type))
72
- fail('type', 'digital_product|service|platform|bundle', d.type);
71
+ if (!['physical_product', 'digital_product', 'service', 'platform', 'bundle'].includes(d.type))
72
+ fail('type', 'physical_product|digital_product|service|platform|bundle', d.type);
73
73
  optionalText('domain');
74
74
  reference('owner_role', ['ROLE']);
75
75
  if ('maturity' in d && !(Number.isInteger(d.maturity) && Number(d.maturity) >= 1 && Number(d.maturity) <= 5))
@@ -78,6 +78,67 @@ export function validateStandalone(notation, input, options = {}) {
78
78
  references('processes', ['PROCESS']);
79
79
  references('supporting_apps', ['APPLICATION']);
80
80
  }
81
+ else if (notation === 'process') {
82
+ reference('owner_role', ['ROLE']);
83
+ reference('capability', ['CAPABILITY']);
84
+ references('participants', ['ROLE', 'ACTOR']);
85
+ optionalText('goal');
86
+ optionalText('result');
87
+ for (const key of ['parent', 'bpmn_file', 'maturity'])
88
+ if (key in d)
89
+ fail(key, 'absent inline; use canonical relations, flow or history', d[key]);
90
+ if (Array.isArray(d.participants) && new Set(d.participants).size !== d.participants.length)
91
+ fail('participants', 'unique references', d.participants);
92
+ if ('flow' in d) {
93
+ if (!mapping(d.flow))
94
+ fail('flow', 'mapping', d.flow);
95
+ else {
96
+ const flow = d.flow;
97
+ const steps = Array.isArray(flow.steps) ? flow.steps : [];
98
+ const sequence = Array.isArray(flow.sequence) ? flow.sequence : [];
99
+ if (!Array.isArray(flow.steps) || !steps.length)
100
+ fail('flow.steps', 'nonempty list', flow.steps);
101
+ if (!Array.isArray(flow.sequence))
102
+ fail('flow.sequence', 'list', flow.sequence);
103
+ const ids = new Set();
104
+ const types = ['startEvent', 'endEvent', 'task', 'userTask', 'serviceTask', 'exclusiveGateway', 'parallelGateway'];
105
+ steps.forEach((step, i) => {
106
+ const key = `flow.steps[${i}]`;
107
+ if (!mapping(step)) {
108
+ fail(key, 'mapping', step);
109
+ return;
110
+ }
111
+ if (!isCanonicalId(step.id) || ids.has(step.id))
112
+ fail(`${key}.id`, 'unique canonical step ID', step.id);
113
+ else
114
+ ids.add(step.id);
115
+ if (!types.includes(step.type))
116
+ fail(`${key}.type`, types.join('|'), step.type);
117
+ if ((!['startEvent', 'endEvent'].includes(step.type) || 'name' in step) && !text(step.name))
118
+ fail(`${key}.name`, 'nonempty string', step.name);
119
+ ref(`${key}.performed_by`, step.performed_by, ['ROLE', 'ACTOR']);
120
+ if (!Array.isArray(d.participants) || !d.participants.includes(step.performed_by))
121
+ fail(`${key}.performed_by`, 'member of participants', step.performed_by);
122
+ if ('supported_by_application' in step)
123
+ ref(`${key}.supported_by_application`, step.supported_by_application, ['APPLICATION']);
124
+ });
125
+ sequence.forEach((edge, i) => {
126
+ const key = `flow.sequence[${i}]`;
127
+ if (!mapping(edge)) {
128
+ fail(key, 'mapping', edge);
129
+ return;
130
+ }
131
+ for (const end of ['from', 'to'])
132
+ if (typeof edge[end] !== 'string' || !ids.has(edge[end]))
133
+ fail(`${key}.${end}`, 'ID in flow.steps', edge[end]);
134
+ if ('condition' in edge && !text(edge.condition))
135
+ fail(`${key}.condition`, 'nonempty string', edge.condition);
136
+ if ('default' in edge && typeof edge.default !== 'boolean')
137
+ fail(`${key}.default`, 'boolean', edge.default);
138
+ });
139
+ }
140
+ }
141
+ }
81
142
  else if (notation === 'capability') {
82
143
  if (!['domain', 'supporting'].includes(d.type))
83
144
  fail('type', 'domain|supporting', d.type);
@@ -1,6 +1,7 @@
1
1
  import { computeCpm } from '../activities/cpm.js';
2
2
  import type { ActivityDoc, ActivitiesLayout, ActivitiesLayoutOptions } from '../activities/types.js';
3
3
  import { type NodeSizePreset } from '../node-size-presets.js';
4
+ import { type ThemeId } from '../theme/index.js';
4
5
  /**
5
6
  * Network-view diagram CSS (critical path, milestones, edge colours) — the
6
7
  * canonical rules shared with the VS Code preview's `ACTIVITIES_DIAGRAM_CSS`
@@ -54,6 +55,11 @@ export interface RenderActivitiesOptions {
54
55
  * Action name in the view header.
55
56
  */
56
57
  suppressProjectNodes?: boolean;
58
+ /**
59
+ * Theme CSS embedded as `<style>` so the SVG is self-contained.
60
+ * Defaults to `'transitrix'`. VS Code live preview uses body emitters + webview CSS instead.
61
+ */
62
+ embedCssTheme?: ThemeId;
57
63
  }
58
64
  /**
59
65
  * Render the network (PSND) view of an already-validated ActivityDoc to a
@@ -194,7 +194,7 @@ export function renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entr
194
194
  * than short-circuiting.
195
195
  */
196
196
  export function renderActivitiesSvg(doc, options = {}) {
197
- const { title = '', gaps = {}, nodeSizePreset = 'normal', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, suppressProjectNodes = true, } = options;
197
+ const { title = '', gaps = {}, nodeSizePreset = 'normal', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, suppressProjectNodes = true, embedCssTheme = 'transitrix', } = options;
198
198
  const nodeSize = resolveActionNodeSize(parseNodeSizePreset(nodeSizePreset));
199
199
  const layoutGaps = {
200
200
  ...gaps,
@@ -228,7 +228,7 @@ export function renderActivitiesSvg(doc, options = {}) {
228
228
  // drop the SVG into the DOM and styling resolves without any cooperation from
229
229
  // the host stylesheet. Matches what the VS Code path produces via
230
230
  // `prepareSvgForExport`.
231
- const embedCss = generateSvgEmbedCss('transitrix') + ACTIVITIES_NETWORK_CSS;
231
+ const embedCss = generateSvgEmbedCss(embedCssTheme) + ACTIVITIES_NETWORK_CSS;
232
232
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
233
233
  <style>${embedCss}</style>
234
234
  ${ACTIVITIES_NETWORK_DEFS}
@@ -1,6 +1,12 @@
1
1
  import type { ActivityCardDoc, ActivityCardLayout } from '../activity-card/types.js';
2
+ import { type ThemeId } from '../theme/index.js';
2
3
  export interface RenderActivityCardOptions {
3
4
  title?: string;
5
+ /**
6
+ * Theme CSS embedded as `<style>` so the SVG is self-contained.
7
+ * Defaults to `'transitrix'`. VS Code live preview uses body emitters + webview CSS instead.
8
+ */
9
+ embedCssTheme?: ThemeId;
4
10
  }
5
11
  /**
6
12
  * The single Activity Card SVG body emitter shared by every host. Takes an
@@ -175,7 +175,7 @@ export function renderActivityCardSvg(doc, options = {}) {
175
175
  // `ARCHIMATE_CLASS` is referenced so the host-neutral module pins the same
176
176
  // §5.1 class convention the layout applies; keeps the import meaningful.
177
177
  void ARCHIMATE_CLASS;
178
- const { title = '' } = options;
178
+ const { title = '', embedCssTheme = 'transitrix' } = options;
179
179
  const resolved = resolveSingleDoc(doc);
180
180
  const layout = layoutActivityCard(resolved);
181
181
  if (layout.bounds.width <= 0 || layout.bounds.height <= 0)
@@ -189,7 +189,7 @@ export function renderActivityCardSvg(doc, options = {}) {
189
189
  const titleSvg = title
190
190
  ? `<text class="text-header" x="${PAD}" y="${PAD + 14}">${escXml(`Activity Card — ${title}`)}</text>`
191
191
  : '';
192
- const embedCss = generateSvgEmbedCss('transitrix');
192
+ const embedCss = generateSvgEmbedCss(embedCssTheme);
193
193
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
194
194
  <style>${embedCss}</style>
195
195
  <defs>
@@ -5,6 +5,11 @@ export interface RenderBlocksOptions {
5
5
  title?: string;
6
6
  nodeSizePreset?: NodeSizePreset;
7
7
  layoutOptions?: BlocksLayoutOptions;
8
+ /**
9
+ * Theme CSS embedded via {@link renderBlocksLayoutSvg}.
10
+ * Defaults to `'transitrix'` for self-contained host-neutral SVG.
11
+ */
12
+ embedCssTheme?: ThemeId;
8
13
  }
9
14
  export interface RenderBlocksLayoutOptions {
10
15
  /** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
@@ -48,6 +53,11 @@ export declare function renderGridLayoutSvg(layout: GridLayout, options?: Render
48
53
  export interface RenderGridOptions {
49
54
  title?: string;
50
55
  layoutOptions?: GridLayoutOptions;
56
+ /**
57
+ * Theme CSS embedded via {@link renderGridLayoutSvg}.
58
+ * Defaults to `'transitrix'` for self-contained host-neutral SVG.
59
+ */
60
+ embedCssTheme?: ThemeId;
51
61
  }
52
62
  /** Host-neutral grid (matrix subset) renderer (IntelliJ/UI). */
53
63
  export declare function renderGridSvg(doc: GridFile, options?: RenderGridOptions): string;
@@ -90,7 +90,7 @@ ${parts.join('\n')}
90
90
  * with the shared theme CSS embedded so the output is self-contained.
91
91
  */
92
92
  export function renderBlocksSvg(doc, options = {}) {
93
- const { title = '', nodeSizePreset = 'normal', layoutOptions } = options;
93
+ const { title = '', nodeSizePreset = 'normal', layoutOptions, embedCssTheme = 'transitrix' } = options;
94
94
  const leaf = resolveBlocksLeafSize(parseNodeSizePreset(nodeSizePreset));
95
95
  const layout = layoutNestedBlocks(doc, {
96
96
  leafWidth: leaf.width,
@@ -103,7 +103,7 @@ export function renderBlocksSvg(doc, options = {}) {
103
103
  const titleSvg = title
104
104
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
105
105
  : '';
106
- return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
106
+ return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme });
107
107
  }
108
108
  /**
109
109
  * Matrix subset (08-blocks.md §4a) — first cut: a single-layer rectangular
@@ -195,7 +195,7 @@ ${body}
195
195
  }
196
196
  /** Host-neutral grid (matrix subset) renderer (IntelliJ/UI). */
197
197
  export function renderGridSvg(doc, options = {}) {
198
- const { title = '', layoutOptions } = options;
198
+ const { title = '', layoutOptions, embedCssTheme = 'transitrix' } = options;
199
199
  const layout = layoutGrid(doc, layoutOptions);
200
200
  if (layout.columns.length === 0 || layout.rows.length === 0) {
201
201
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
@@ -203,5 +203,5 @@ export function renderGridSvg(doc, options = {}) {
203
203
  const titleSvg = title
204
204
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
205
205
  : '';
206
- return renderGridLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
206
+ return renderGridLayoutSvg(layout, { title: titleSvg, embedCssTheme });
207
207
  }
@@ -2,6 +2,7 @@ import { layoutFGCAPreview } from '../fgca/preview-layout.js';
2
2
  import type { FGCADoc } from '../fgca/validate.js';
3
3
  import { type EdgeStyle } from '../edge-path.js';
4
4
  import { type NodeSizePreset } from '../node-size-presets.js';
5
+ import { type ThemeId } from '../theme/index.js';
5
6
  type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
6
7
  export interface RenderFgcaBodyOptions {
7
8
  nodeWidth?: number;
@@ -35,6 +36,11 @@ export interface RenderFgcaOptions {
35
36
  scopeCaption?: boolean;
36
37
  /** Show completion percent badges on action nodes. */
37
38
  showCompletionPercent?: boolean;
39
+ /**
40
+ * Theme CSS embedded as `<style>` so the SVG is self-contained.
41
+ * Defaults to `'transitrix'`. VS Code live preview uses body emitters + webview CSS instead.
42
+ */
43
+ embedCssTheme?: ThemeId;
38
44
  }
39
45
  export declare function renderFgcaSvg(doc: FGCADoc, options?: RenderFgcaOptions): string;
40
46
  export {};
@@ -86,7 +86,7 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
86
86
  return `${headerSvg}\n${nodeSvg}\n${edgeSvg}`;
87
87
  }
88
88
  export function renderFgcaSvg(doc, options = {}) {
89
- const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, nodeSizePreset = 'normal', layoutOptions, scopeCaption = false, showCompletionPercent = true, } = options;
89
+ const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, nodeSizePreset = 'normal', layoutOptions, scopeCaption = false, showCompletionPercent = true, embedCssTheme = 'transitrix', } = options;
90
90
  const hideChanges = variant === 'dga' || doc.hideChanges === true;
91
91
  const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
92
92
  const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
@@ -110,7 +110,7 @@ export function renderFgcaSvg(doc, options = {}) {
110
110
  const captionSvg = scopeCaption
111
111
  ? `<text class="text-caption" x="${PAD}" y="${height + 16}">${escXml('This diagram is shown for the selected goal scope. Actions on it may also serve goals outside that scope.')}</text>`
112
112
  : '';
113
- const embedCss = generateSvgEmbedCss('transitrix');
113
+ const embedCss = generateSvgEmbedCss(embedCssTheme);
114
114
  // Calculate final height including caption if present
115
115
  const finalHeight = scopeCaption ? height + 24 : height;
116
116
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${finalHeight}" viewBox="0 0 ${width} ${finalHeight}">