@transitrix/diagrams 1.15.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compliance/catalogue.d.ts +17 -0
- package/dist/compliance/catalogue.js +68 -0
- package/dist/compliance/index.d.ts +4 -2
- package/dist/compliance/index.js +2 -1
- package/dist/compliance/requirement-chain.d.ts +13 -2
- package/dist/compliance/requirement-chain.js +60 -33
- package/dist/goals/resolver.d.ts +1 -0
- package/dist/goals/resolver.js +44 -2
- package/dist/products/types.d.ts +1 -1
- package/dist/products/validate.js +1 -1
- package/dist/repo-validate/check-strategy-chain.js +58 -23
- package/dist/standalone/validate.js +64 -3
- package/dist/webview/render-activities.d.ts +6 -0
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-activity-card.d.ts +6 -0
- package/dist/webview/render-activity-card.js +2 -2
- package/dist/webview/render-blocks.d.ts +10 -0
- package/dist/webview/render-blocks.js +4 -4
- package/dist/webview/render-fgca.d.ts +6 -0
- package/dist/webview/render-fgca.js +2 -2
- package/dist/webview/render-goals.d.ts +5 -0
- package/dist/webview/render-goals.js +9 -2
- package/dist/webview/render-process-blueprint.d.ts +6 -0
- package/dist/webview/render-process-blueprint.js +2 -2
- package/dist/webview/render-products.js +1 -0
- package/dist/yaml-normalize.d.ts +3 -1
- package/dist/yaml-normalize.js +11 -3
- package/package.json +1 -1
- package/src/compliance/catalogue.ts +49 -0
- package/src/compliance/index.ts +5 -2
- package/src/compliance/requirement-chain.ts +52 -27
- package/src/goals/resolver.ts +46 -2
- package/src/products/types.ts +1 -1
- package/src/products/validate.ts +1 -1
- package/src/repo-validate/check-strategy-chain.ts +49 -23
- package/src/standalone/validate.ts +38 -2
- package/src/webview/render-activities.ts +8 -2
- package/src/webview/render-activity-card.ts +8 -3
- package/src/webview/render-blocks.ts +14 -4
- package/src/webview/render-fgca.ts +8 -2
- package/src/webview/render-goals.ts +21 -2
- package/src/webview/render-process-blueprint.ts +8 -3
- package/src/webview/render-products.ts +1 -0
- package/src/yaml-normalize.ts +12 -3
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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}">
|
|
@@ -8,6 +8,11 @@ export interface RenderGoalsOptions {
|
|
|
8
8
|
entryCurvature?: number;
|
|
9
9
|
edgeStyle?: EdgeStyle;
|
|
10
10
|
nodeSizePreset?: NodeSizePreset;
|
|
11
|
+
/**
|
|
12
|
+
* Theme CSS embedded via {@link renderGoalsLayoutSvg}.
|
|
13
|
+
* Defaults to `'transitrix'` for self-contained host-neutral SVG.
|
|
14
|
+
*/
|
|
15
|
+
embedCssTheme?: ThemeId;
|
|
11
16
|
}
|
|
12
17
|
/**
|
|
13
18
|
* Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
|
|
@@ -27,7 +27,7 @@ const PAD = 24;
|
|
|
27
27
|
* with the shared theme CSS embedded so the output is self-contained.
|
|
28
28
|
*/
|
|
29
29
|
export function renderGoalsSvg(tree, options = {}) {
|
|
30
|
-
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, nodeSizePreset = 'normal' } = options;
|
|
30
|
+
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, nodeSizePreset = 'normal', embedCssTheme = 'transitrix', } = options;
|
|
31
31
|
const nodeSize = resolveGoalsNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
32
32
|
const layout = layoutGoalTree(tree, {
|
|
33
33
|
nodeWidth: nodeSize.width,
|
|
@@ -42,7 +42,14 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
42
42
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
43
43
|
: '';
|
|
44
44
|
// Reserve a separate header row; the theme centers text on its y coordinate.
|
|
45
|
-
return renderGoalsLayoutSvg(layout, {
|
|
45
|
+
return renderGoalsLayoutSvg(layout, {
|
|
46
|
+
curvature,
|
|
47
|
+
entryCurvature,
|
|
48
|
+
edgeStyle,
|
|
49
|
+
title,
|
|
50
|
+
topInset: title ? PAD : 0,
|
|
51
|
+
embedCssTheme,
|
|
52
|
+
});
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
55
|
* The single goals SVG emitter shared by every host. Takes an already-computed
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { ProcessBlueprintFile, ProcessBlueprintLayout, ProcessBlueprintLayoutOptions } from '../process-blueprint/types.js';
|
|
2
2
|
import { type NodeSizePreset } from '../node-size-presets.js';
|
|
3
|
+
import { type ThemeId } from '../theme/index.js';
|
|
3
4
|
export declare function renderProcessBlueprintBody(layout: ProcessBlueprintLayout, ox: number, oy: number, clipId?: string): string;
|
|
4
5
|
export interface RenderProcessBlueprintOptions {
|
|
5
6
|
title?: string;
|
|
6
7
|
nodeSizePreset?: NodeSizePreset;
|
|
7
8
|
layoutOptions?: ProcessBlueprintLayoutOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Theme CSS embedded as `<style>` so the SVG is self-contained.
|
|
11
|
+
* Defaults to `'transitrix'`. VS Code live preview uses body emitters + webview CSS instead.
|
|
12
|
+
*/
|
|
13
|
+
embedCssTheme?: ThemeId;
|
|
8
14
|
}
|
|
9
15
|
export declare function renderProcessBlueprintSvg(doc: ProcessBlueprintFile, options?: RenderProcessBlueprintOptions): string;
|
|
@@ -127,7 +127,7 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
|
|
|
127
127
|
return parts.join('\n');
|
|
128
128
|
}
|
|
129
129
|
export function renderProcessBlueprintSvg(doc, options = {}) {
|
|
130
|
-
const { title = '', nodeSizePreset = 'normal', layoutOptions } = options;
|
|
130
|
+
const { title = '', nodeSizePreset = 'normal', layoutOptions, embedCssTheme = 'transitrix' } = options;
|
|
131
131
|
const sizing = resolveProcessBlueprintSize(parseNodeSizePreset(nodeSizePreset));
|
|
132
132
|
const layout = layoutProcessBlueprint(doc, {
|
|
133
133
|
...sizing,
|
|
@@ -145,7 +145,7 @@ export function renderProcessBlueprintSvg(doc, options = {}) {
|
|
|
145
145
|
const titleSvg = title
|
|
146
146
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
|
|
147
147
|
: '';
|
|
148
|
-
const embedCss = generateSvgEmbedCss(
|
|
148
|
+
const embedCss = generateSvgEmbedCss(embedCssTheme) + BP_EMBED_CSS;
|
|
149
149
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
150
150
|
<style>${embedCss}</style>
|
|
151
151
|
${titleSvg}
|
package/dist/yaml-normalize.d.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* A bare `2026-06-01` parses to midnight UTC and round-trips to
|
|
16
16
|
* `"2026-06-01"` regardless of `process.env.TZ`.
|
|
17
17
|
*
|
|
18
|
-
* Walks in place and returns the same reference for convenience.
|
|
18
|
+
* Walks in place and returns the same reference for convenience. Shared
|
|
19
|
+
* object graphs from YAML aliases (and cycles) are visited once via WeakSet
|
|
20
|
+
* so diamond / recursive anchors cannot amplify the walk into a hang.
|
|
19
21
|
*/
|
|
20
22
|
export declare function coerceDatesToIsoStrings<T>(value: T): T;
|
package/dist/yaml-normalize.js
CHANGED
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* A bare `2026-06-01` parses to midnight UTC and round-trips to
|
|
16
16
|
* `"2026-06-01"` regardless of `process.env.TZ`.
|
|
17
17
|
*
|
|
18
|
-
* Walks in place and returns the same reference for convenience.
|
|
18
|
+
* Walks in place and returns the same reference for convenience. Shared
|
|
19
|
+
* object graphs from YAML aliases (and cycles) are visited once via WeakSet
|
|
20
|
+
* so diamond / recursive anchors cannot amplify the walk into a hang.
|
|
19
21
|
*/
|
|
20
22
|
export function coerceDatesToIsoStrings(value) {
|
|
23
|
+
return walk(value, new WeakSet());
|
|
24
|
+
}
|
|
25
|
+
function walk(value, seen) {
|
|
21
26
|
if (value === null || value === undefined)
|
|
22
27
|
return value;
|
|
23
28
|
if (value instanceof Date) {
|
|
@@ -25,6 +30,9 @@ export function coerceDatesToIsoStrings(value) {
|
|
|
25
30
|
}
|
|
26
31
|
if (typeof value !== 'object')
|
|
27
32
|
return value;
|
|
33
|
+
if (seen.has(value))
|
|
34
|
+
return value;
|
|
35
|
+
seen.add(value);
|
|
28
36
|
if (Array.isArray(value)) {
|
|
29
37
|
for (let i = 0; i < value.length; i++) {
|
|
30
38
|
const v = value[i];
|
|
@@ -32,7 +40,7 @@ export function coerceDatesToIsoStrings(value) {
|
|
|
32
40
|
value[i] = dateToIsoYmd(v);
|
|
33
41
|
}
|
|
34
42
|
else if (v !== null && typeof v === 'object') {
|
|
35
|
-
|
|
43
|
+
walk(v, seen);
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
return value;
|
|
@@ -44,7 +52,7 @@ export function coerceDatesToIsoStrings(value) {
|
|
|
44
52
|
obj[key] = dateToIsoYmd(v);
|
|
45
53
|
}
|
|
46
54
|
else if (v !== null && typeof v === 'object') {
|
|
47
|
-
|
|
55
|
+
walk(v, seen);
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
return value;
|
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { emptyCanon, ingestComplianceDoc } from './classify.js';
|
|
2
|
+
|
|
3
|
+
/** Relative POSIX paths keep scanning policy and content identity independent of host APIs. */
|
|
4
|
+
export interface RequirementCatalogueIO {
|
|
5
|
+
read(path: string): Promise<string>;
|
|
6
|
+
list(path: string): Promise<Array<{ name: string; kind: 'file' | 'directory' | 'symlink' | 'other' }>>;
|
|
7
|
+
missing(error: unknown): boolean;
|
|
8
|
+
parse(content: string): unknown;
|
|
9
|
+
digest(content: string): string;
|
|
10
|
+
sourcePath?(path: string): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Shared disk/editor policy: one manifest boundary, all three zones, lossless failures. */
|
|
14
|
+
export async function scanRequirementCatalogue(io: RequirementCatalogueIO) {
|
|
15
|
+
const canon = emptyCanon();
|
|
16
|
+
const transcript: unknown[] = [];
|
|
17
|
+
const manifest = await io.read('transitrix.yaml');
|
|
18
|
+
const parsed = io.parse(manifest);
|
|
19
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Invalid transitrix.yaml manifest: expected a mapping');
|
|
20
|
+
transcript.push(['transitrix.yaml', manifest]);
|
|
21
|
+
const fail = (path: string, message: string) => {
|
|
22
|
+
transcript.push(['failure', path, message]);
|
|
23
|
+
canon.findings.push({ id: `${path}:load`, owner: path, field: '', code: 'LOAD', message, reference: false, severity: 'error' });
|
|
24
|
+
};
|
|
25
|
+
const walk = async (dir: string, top = false): Promise<void> => {
|
|
26
|
+
let entries: Awaited<ReturnType<RequirementCatalogueIO['list']>>;
|
|
27
|
+
try { entries = await io.list(dir); }
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (top && io.missing(error)) { transcript.push(['absent-zone', dir]); return; }
|
|
30
|
+
fail(dir, 'Directory could not be enumerated'); return;
|
|
31
|
+
}
|
|
32
|
+
if (!top && entries.some(e => e.name === 'transitrix.yaml')) { transcript.push(['catalogue-boundary', dir]); return; }
|
|
33
|
+
for (const { name, kind } of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
34
|
+
const path = `${dir}/${name}`;
|
|
35
|
+
if (kind === 'symlink') { fail(path, 'Symbolic link was not enumerated'); continue; }
|
|
36
|
+
if (kind === 'directory') { await walk(path); continue; }
|
|
37
|
+
if (!/\.ya?ml$/i.test(name)) continue;
|
|
38
|
+
if (kind !== 'file') { fail(path, 'Unsupported file type'); continue; }
|
|
39
|
+
let content: string;
|
|
40
|
+
try { content = await io.read(path); }
|
|
41
|
+
catch { fail(path, 'Unreadable YAML record'); continue; }
|
|
42
|
+
transcript.push([path, content]);
|
|
43
|
+
try { ingestComplianceDoc(canon, io.parse(content), io.sourcePath?.(path) ?? path); }
|
|
44
|
+
catch { fail(path, 'Malformed YAML record'); }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
for (const zone of ['canon', 'codex', 'field']) await walk(zone, true);
|
|
48
|
+
return { canon, snapshotId: `sha256:${io.digest(JSON.stringify(transcript))}` };
|
|
49
|
+
}
|
package/src/compliance/index.ts
CHANGED
|
@@ -76,6 +76,9 @@ export type {
|
|
|
76
76
|
TraceElementRef,
|
|
77
77
|
TraceSourceRef,
|
|
78
78
|
} from './types.js';
|
|
79
|
-
export { buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
|
|
80
|
-
export type { ChainInput, ChainScope, ChainSet, RequirementChainNode, RequirementChainEdge, RequirementChainProjection } from './requirement-chain.js';
|
|
79
|
+
export { buildSubjectRequirementChain, buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
|
|
80
|
+
export type { ChainSubject, ChainInput, ChainScope, ChainSet, RequirementChainNode, RequirementChainEdge, RequirementChainProjection } from './requirement-chain.js';
|
|
81
81
|
export type { ChainRecord, ChainFinding } from './types.js';
|
|
82
|
+
|
|
83
|
+
export { scanRequirementCatalogue } from './catalogue.js';
|
|
84
|
+
export type { RequirementCatalogueIO } from './catalogue.js';
|