@transitrix/diagrams 1.0.0 → 1.0.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/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- package/dist/activity-card/__tests__/example.test.js +20 -1
- package/dist/activity-card/__tests__/layout.test.js +102 -11
- package/dist/activity-card/__tests__/resolver.test.js +139 -6
- package/dist/activity-card/index.d.ts +1 -1
- package/dist/activity-card/layout.d.ts +1 -1
- package/dist/activity-card/layout.js +220 -103
- package/dist/activity-card/resolver.js +107 -26
- package/dist/activity-card/types.d.ts +150 -27
- package/dist/activity-card/validate.js +3 -0
- package/dist/actor/__tests__/validate.test.js +63 -31
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/types.d.ts +7 -3
- package/dist/actor/types.js +17 -3
- package/dist/actor/validate.js +55 -26
- package/dist/assertion/types.d.ts +3 -1
- package/dist/assertion/types.js +1 -1
- package/dist/change/__tests__/validate.test.js +40 -34
- package/dist/change/index.d.ts +1 -0
- package/dist/change/types.d.ts +1 -1
- package/dist/change/types.js +2 -2
- package/dist/change/validate.d.ts +6 -1
- package/dist/change/validate.js +68 -41
- package/dist/compliance/__tests__/classify.test.js +34 -0
- package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
- package/dist/compliance/__tests__/html.test.js +2 -2
- package/dist/compliance/__tests__/impact.test.js +311 -6
- package/dist/compliance/classify.d.ts +6 -0
- package/dist/compliance/classify.js +9 -1
- package/dist/compliance/coverage-metric.d.ts +31 -11
- package/dist/compliance/coverage-metric.js +149 -39
- package/dist/compliance/html.js +77 -74
- package/dist/compliance/impact.d.ts +77 -16
- package/dist/compliance/impact.js +193 -25
- package/dist/compliance/index.d.ts +2 -2
- package/dist/compliance/index.js +1 -1
- package/dist/compliance/markdown.js +1 -0
- package/dist/compliance/types.d.ts +25 -6
- package/dist/edge-path.d.ts +8 -4
- package/dist/edge-path.js +11 -6
- package/dist/factor/__tests__/validate.test.js +74 -45
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +4 -7
- package/dist/factor/types.js +10 -5
- package/dist/factor/validate.d.ts +6 -1
- package/dist/factor/validate.js +68 -50
- package/dist/fgca/__tests__/resolver.test.js +185 -0
- package/dist/fgca/index.d.ts +2 -0
- package/dist/fgca/index.js +1 -0
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +124 -0
- package/dist/goals/index.d.ts +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/process-blueprint/__tests__/layout.test.js +37 -0
- package/dist/process-blueprint/layout.js +5 -1
- package/dist/process-blueprint/types.d.ts +22 -1
- package/dist/stakeholder/__tests__/validate.test.js +49 -47
- package/dist/stakeholder/index.d.ts +1 -0
- package/dist/stakeholder/types.d.ts +3 -4
- package/dist/stakeholder/types.js +2 -3
- package/dist/stakeholder/validate.d.ts +6 -1
- package/dist/stakeholder/validate.js +71 -50
- package/dist/target-state/__tests__/validate.test.js +43 -27
- package/dist/target-state/index.d.ts +2 -0
- package/dist/target-state/index.js +1 -0
- package/dist/target-state/types.d.ts +6 -1
- package/dist/target-state/types.js +8 -7
- package/dist/target-state/validate.d.ts +6 -1
- package/dist/target-state/validate.js +69 -45
- package/dist/theme/themes.js +69 -69
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-activity-card.js +104 -77
- package/dist/webview/render-fgca.d.ts +4 -0
- package/dist/webview/render-fgca.js +2 -5
- package/dist/webview/render-goals.d.ts +1 -0
- package/dist/webview/render-goals.js +15 -15
- package/dist/webview/render-process-blueprint.js +10 -10
- package/package.json +40 -40
- package/src/__tests__/edge-path.test.ts +58 -46
- package/src/__tests__/scope.test.ts +27 -27
- package/src/__tests__/yaml-normalize.test.ts +158 -158
- package/src/activities/__tests__/cpm.test.ts +164 -164
- package/src/activities/__tests__/gantt.test.ts +245 -245
- package/src/activities/__tests__/layout.test.ts +112 -112
- package/src/activities/__tests__/validate.test.ts +409 -409
- package/src/activities/cpm.ts +95 -95
- package/src/activities/gantt.ts +276 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +148 -148
- package/src/activities/validate.ts +357 -357
- package/src/activity-card/__tests__/example.test.ts +85 -66
- package/src/activity-card/__tests__/layout.test.ts +169 -63
- package/src/activity-card/__tests__/resolver.test.ts +307 -161
- package/src/activity-card/__tests__/validate.test.ts +131 -131
- package/src/activity-card/index.ts +38 -32
- package/src/activity-card/layout.ts +324 -194
- package/src/activity-card/resolver.ts +415 -337
- package/src/activity-card/types.ts +376 -237
- package/src/activity-card/validate.ts +162 -158
- package/src/actor/__tests__/validate.test.ts +70 -31
- package/src/actor/index.ts +1 -1
- package/src/actor/types.ts +21 -7
- package/src/actor/validate.ts +59 -27
- package/src/applications/__tests__/validate.test.ts +256 -256
- package/src/applications/index.ts +11 -11
- package/src/applications/types.ts +43 -43
- package/src/applications/validate.ts +131 -131
- package/src/assertion/__tests__/example.test.ts +37 -37
- package/src/assertion/__tests__/validate.test.ts +142 -142
- package/src/assertion/index.ts +12 -12
- package/src/assertion/types.ts +60 -58
- package/src/assertion/validate.ts +145 -145
- package/src/blocks/__tests__/layout.test.ts +169 -169
- package/src/blocks/__tests__/validate.test.ts +260 -260
- package/src/blocks/index.ts +17 -17
- package/src/blocks/layout.ts +186 -186
- package/src/blocks/types.ts +62 -62
- package/src/blocks/validate.ts +206 -206
- package/src/capability-map/__tests__/validate.test.ts +238 -238
- package/src/capability-map/index.ts +8 -8
- package/src/capability-map/types.ts +29 -29
- package/src/capability-map/validate.ts +127 -127
- package/src/change/__tests__/validate.test.ts +43 -34
- package/src/change/index.ts +1 -0
- package/src/change/types.ts +5 -3
- package/src/change/validate.ts +75 -43
- package/src/compliance/__tests__/classify.test.ts +64 -27
- package/src/compliance/__tests__/confidence.test.ts +98 -98
- package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
- package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
- package/src/compliance/__tests__/example.test.ts +57 -57
- package/src/compliance/__tests__/gap-report.test.ts +128 -128
- package/src/compliance/__tests__/html.test.ts +141 -141
- package/src/compliance/__tests__/impact.test.ts +840 -481
- package/src/compliance/__tests__/markdown.test.ts +94 -94
- package/src/compliance/__tests__/views.test.ts +64 -64
- package/src/compliance/classify.ts +100 -84
- package/src/compliance/confidence.ts +69 -69
- package/src/compliance/coverage-metric.ts +404 -263
- package/src/compliance/gap-report.ts +94 -94
- package/src/compliance/html.ts +298 -295
- package/src/compliance/impact.ts +861 -677
- package/src/compliance/index.ts +57 -56
- package/src/compliance/markdown.ts +136 -135
- package/src/compliance/reverse-index.ts +33 -33
- package/src/compliance/types.ts +136 -120
- package/src/compliance/views.ts +46 -46
- package/src/compliance-matrix/__tests__/build.test.ts +200 -200
- package/src/compliance-matrix/__tests__/example.test.ts +57 -57
- package/src/compliance-matrix/index.ts +11 -11
- package/src/compliance-matrix/types.ts +102 -102
- package/src/edge-path.ts +50 -44
- package/src/factor/__tests__/validate.test.ts +80 -45
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +14 -18
- package/src/factor/validate.ts +77 -51
- package/src/fgca/__tests__/chain-table.test.ts +172 -172
- package/src/fgca/__tests__/layout.test.ts +179 -179
- package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
- package/src/fgca/__tests__/preview-layout.test.ts +124 -124
- package/src/fgca/__tests__/resolver.test.ts +209 -0
- package/src/fgca/__tests__/validate.test.ts +142 -142
- package/src/fgca/chain-table.ts +225 -225
- package/src/fgca/index.ts +47 -45
- package/src/fgca/layout.ts +204 -204
- package/src/fgca/nodeTypes.ts +11 -11
- package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
- package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
- package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
- package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
- package/src/fgca/parse-canonical.ts +400 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +157 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +126 -126
- package/src/geometry.ts +9 -9
- package/src/goals/__tests__/layout.test.ts +175 -175
- package/src/goals/__tests__/mutations.test.ts +111 -111
- package/src/goals/__tests__/parse-canonical.test.ts +145 -145
- package/src/goals/__tests__/validate.test.ts +202 -202
- package/src/goals/index.ts +20 -21
- package/src/goals/layout.ts +172 -172
- package/src/goals/mutations.ts +123 -123
- package/src/goals/parse-canonical.ts +286 -286
- package/src/goals/types.ts +85 -85
- package/src/goals/validate.ts +136 -136
- package/src/index.ts +28 -23
- package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
- package/src/process-blueprint/__tests__/layout.test.ts +240 -195
- package/src/process-blueprint/__tests__/validate.test.ts +310 -310
- package/src/process-blueprint/index.ts +31 -31
- package/src/process-blueprint/layout.ts +535 -531
- package/src/process-blueprint/types.ts +263 -242
- package/src/process-blueprint/validate.ts +181 -181
- package/src/process-map/__tests__/validate.test.ts +223 -223
- package/src/process-map/index.ts +10 -10
- package/src/process-map/types.ts +36 -36
- package/src/process-map/validate.ts +135 -135
- package/src/products/__tests__/validate.test.ts +239 -239
- package/src/products/index.ts +9 -9
- package/src/products/types.ts +31 -31
- package/src/products/validate.ts +112 -112
- package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
- package/src/repo-validate/index.ts +2 -2
- package/src/repo-validate/types.ts +50 -50
- package/src/repo-validate/validate-repo.ts +196 -196
- package/src/requirement/__tests__/example.test.ts +24 -24
- package/src/requirement/__tests__/validate.test.ts +96 -96
- package/src/requirement/index.ts +4 -4
- package/src/requirement/types.ts +35 -35
- package/src/requirement/validate.ts +92 -92
- package/src/scenarios/__tests__/validate.test.ts +209 -209
- package/src/scenarios/index.ts +15 -15
- package/src/scenarios/types.ts +37 -37
- package/src/scenarios/validate.ts +122 -122
- package/src/schema-version.ts +16 -16
- package/src/scope.ts +46 -46
- package/src/stakeholder/__tests__/validate.test.ts +51 -47
- package/src/stakeholder/index.ts +1 -0
- package/src/stakeholder/types.ts +7 -7
- package/src/stakeholder/validate.ts +79 -52
- package/src/target-state/__tests__/validate.test.ts +48 -27
- package/src/target-state/index.ts +2 -0
- package/src/target-state/types.ts +11 -7
- package/src/target-state/validate.ts +76 -46
- package/src/theme/index.ts +4 -4
- package/src/theme/themes.ts +237 -237
- package/src/theme/tokens.ts +133 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +77 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/entry.ts +292 -292
- package/src/webview/render-activities.ts +5 -3
- package/src/webview/render-activity-card.ts +266 -234
- package/src/webview/render-fgca.ts +6 -5
- package/src/webview/render-goals.ts +111 -110
- package/src/webview/render-process-blueprint.ts +207 -207
- package/src/webview/styles.css +322 -323
- package/src/yaml-normalize.ts +56 -56
- package/dist/element-envelope.d.ts +0 -15
- package/dist/element-envelope.js +0 -55
- package/dist/issues/__tests__/layout.test.js +0 -62
- package/dist/issues/__tests__/validate.test.d.ts +0 -1
- package/dist/issues/__tests__/validate.test.js +0 -82
- package/dist/issues/index.d.ts +0 -4
- package/dist/issues/index.js +0 -2
- package/dist/issues/layout.d.ts +0 -10
- package/dist/issues/layout.js +0 -82
- package/dist/issues/types.d.ts +0 -61
- package/dist/issues/types.js +0 -6
- package/dist/issues/validate.d.ts +0 -3
- package/dist/issues/validate.js +0 -143
- package/dist/webview/transitrix-render.css +0 -323
- package/dist/webview/transitrix-render.js +0 -6452
- package/dist/webview/transitrix-render.js.map +0 -7
- package/src/element-envelope.ts +0 -76
- /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
|
@@ -16,9 +16,9 @@ import { buildComplianceIndex } from './reverse-index.js';
|
|
|
16
16
|
* are auditable ("what I assumed").
|
|
17
17
|
*/
|
|
18
18
|
export const COMPLIANCE_IMPACT_DEFAULTS = {
|
|
19
|
-
/** Accept all
|
|
19
|
+
/** Accept all active statuses + n_a in cell aggregation. */
|
|
20
20
|
status_display: {
|
|
21
|
-
show: ['compliant', 'partial', 'non_compliant', 'under_review', 'n_a'],
|
|
21
|
+
show: ['compliant', 'partial', 'non_compliant', 'under_review', 'pending_owner', 'n_a'],
|
|
22
22
|
},
|
|
23
23
|
/** Order rows by canonical REQUIREMENT ID (lexicographic). */
|
|
24
24
|
order_rows_by: 'id',
|
|
@@ -30,9 +30,9 @@ export const COMPLIANCE_IMPACT_DEFAULTS = {
|
|
|
30
30
|
/** Obligation scope: all known REQUIREMENTs in the canon (no filter). */
|
|
31
31
|
obligations: { include: undefined, filter: undefined },
|
|
32
32
|
/** Subjects: empty — must be supplied explicitly in the view config. */
|
|
33
|
-
subjects: { products: [], processes: [] },
|
|
34
|
-
/** Column grain: one column per
|
|
35
|
-
grouping: { columns: '
|
|
33
|
+
subjects: { products: [], processes: [], capabilities: [] },
|
|
34
|
+
/** Column grain: one column per subject (no stage/task decomposition). */
|
|
35
|
+
grouping: { columns: 'product' },
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Validate and normalise a raw (YAML-parsed) value into an ImpactViewConfig.
|
|
@@ -81,6 +81,16 @@ export function parseImpactViewConfig(raw) {
|
|
|
81
81
|
const emptyCells = v.empty_cells && typeof v.empty_cells === 'object' && !Array.isArray(v.empty_cells)
|
|
82
82
|
? v.empty_cells
|
|
83
83
|
: {};
|
|
84
|
+
// Parse grouping.columns — accept spec-canonical values plus backward-compat aliases.
|
|
85
|
+
const rawGrouping = v.grouping && typeof v.grouping === 'object' && !Array.isArray(v.grouping)
|
|
86
|
+
? v.grouping
|
|
87
|
+
: null;
|
|
88
|
+
const rawColumns = typeof rawGrouping?.columns === 'string' ? rawGrouping.columns : undefined;
|
|
89
|
+
const VALID_COLUMNS = new Set(['product', 'product-stage', 'product-stage-task']);
|
|
90
|
+
const normalizedColumns = rawColumns === 'object-details' ? 'product-stage' // backward-compat alias
|
|
91
|
+
: rawColumns === 'object' ? 'product' // backward-compat alias
|
|
92
|
+
: VALID_COLUMNS.has(rawColumns ?? '') ? rawColumns
|
|
93
|
+
: undefined;
|
|
84
94
|
const config = {
|
|
85
95
|
id: v.id,
|
|
86
96
|
name: v.name,
|
|
@@ -93,6 +103,9 @@ export function parseImpactViewConfig(raw) {
|
|
|
93
103
|
processes: Array.isArray(subjects.processes)
|
|
94
104
|
? subjects.processes.filter((x) => typeof x === 'string')
|
|
95
105
|
: [...COMPLIANCE_IMPACT_DEFAULTS.subjects.processes],
|
|
106
|
+
capabilities: Array.isArray(subjects.capabilities)
|
|
107
|
+
? subjects.capabilities.filter((x) => typeof x === 'string')
|
|
108
|
+
: [...COMPLIANCE_IMPACT_DEFAULTS.subjects.capabilities],
|
|
96
109
|
},
|
|
97
110
|
obligations: {
|
|
98
111
|
include: Array.isArray(obligations.include)
|
|
@@ -120,6 +133,7 @@ export function parseImpactViewConfig(raw) {
|
|
|
120
133
|
: COMPLIANCE_IMPACT_DEFAULTS.empty_cells.no_obligation_applies_label,
|
|
121
134
|
},
|
|
122
135
|
order_rows_by: v.order_rows_by === 'name' ? 'name' : COMPLIANCE_IMPACT_DEFAULTS.order_rows_by,
|
|
136
|
+
grouping: normalizedColumns !== undefined ? { columns: normalizedColumns } : undefined,
|
|
123
137
|
};
|
|
124
138
|
return { ok: true, config };
|
|
125
139
|
}
|
|
@@ -182,9 +196,11 @@ function orderRows(rows, key) {
|
|
|
182
196
|
return sorted;
|
|
183
197
|
}
|
|
184
198
|
/** Aggregate multiple matching assertions into one cell value, per §5.2 step 4.
|
|
199
|
+
* Precedence: non_compliant > partial > pending_owner > under_review > compliant > n_a.
|
|
185
200
|
* Assumes at least one assertion matches an allowed status; caller filters. */
|
|
186
201
|
function aggregateStatus(assertions) {
|
|
187
202
|
let sawPartial = false;
|
|
203
|
+
let sawPendingOwner = false;
|
|
188
204
|
let sawUnderReview = false;
|
|
189
205
|
let sawCompliant = false;
|
|
190
206
|
for (const a of assertions) {
|
|
@@ -192,6 +208,8 @@ function aggregateStatus(assertions) {
|
|
|
192
208
|
return 'non_compliant';
|
|
193
209
|
if (a.status === 'partial')
|
|
194
210
|
sawPartial = true;
|
|
211
|
+
else if (a.status === 'pending_owner')
|
|
212
|
+
sawPendingOwner = true;
|
|
195
213
|
else if (a.status === 'under_review')
|
|
196
214
|
sawUnderReview = true;
|
|
197
215
|
else if (a.status === 'compliant')
|
|
@@ -199,6 +217,8 @@ function aggregateStatus(assertions) {
|
|
|
199
217
|
}
|
|
200
218
|
if (sawPartial)
|
|
201
219
|
return 'partial';
|
|
220
|
+
if (sawPendingOwner)
|
|
221
|
+
return 'pending_owner';
|
|
202
222
|
if (sawUnderReview)
|
|
203
223
|
return 'under_review';
|
|
204
224
|
if (sawCompliant)
|
|
@@ -206,13 +226,22 @@ function aggregateStatus(assertions) {
|
|
|
206
226
|
return 'n_a';
|
|
207
227
|
}
|
|
208
228
|
// ── Column builder helpers (CV-3a) ──────────────────────────────────────────
|
|
209
|
-
function buildProductColumns(config) {
|
|
210
|
-
const
|
|
211
|
-
|
|
229
|
+
function buildProductColumns(config, namedItems) {
|
|
230
|
+
const nameMap = new Map(namedItems.map(p => [p.id, p.name]));
|
|
231
|
+
const subjects = [
|
|
232
|
+
...(config.subjects.products ?? []),
|
|
233
|
+
...(config.subjects.processes ?? []),
|
|
234
|
+
...(config.subjects.capabilities ?? []),
|
|
235
|
+
];
|
|
236
|
+
return subjects.map(id => ({ subjectId: id, label: id, subjectName: nameMap.get(id) }));
|
|
212
237
|
}
|
|
213
238
|
function buildObjectDetailColumns(config, objectDetails) {
|
|
214
239
|
const detailMap = new Map(objectDetails.map(d => [d.objectId, d.details]));
|
|
215
|
-
const subjects = [
|
|
240
|
+
const subjects = [
|
|
241
|
+
...(config.subjects.products ?? []),
|
|
242
|
+
...(config.subjects.processes ?? []),
|
|
243
|
+
...(config.subjects.capabilities ?? []),
|
|
244
|
+
];
|
|
216
245
|
const cols = [];
|
|
217
246
|
for (const subjectId of subjects) {
|
|
218
247
|
const details = detailMap.get(subjectId);
|
|
@@ -227,13 +256,57 @@ function buildObjectDetailColumns(config, objectDetails) {
|
|
|
227
256
|
}
|
|
228
257
|
return cols;
|
|
229
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Build `product-stage-task` columns: one column per (subject, stage, task) triple.
|
|
261
|
+
* Each stage in `objectDetails` that carries a `tasks[]` array is expanded to
|
|
262
|
+
* per-task columns `{ subjectId, stageId, taskId, label: subjectId:stageId:taskId }`.
|
|
263
|
+
* Stages with no tasks fall back to a single stage-grain column.
|
|
264
|
+
*/
|
|
265
|
+
function buildTaskColumns(config, objectDetails) {
|
|
266
|
+
const detailMap = new Map(objectDetails.map(d => [d.objectId, d.details]));
|
|
267
|
+
const subjects = [
|
|
268
|
+
...(config.subjects.products ?? []),
|
|
269
|
+
...(config.subjects.processes ?? []),
|
|
270
|
+
...(config.subjects.capabilities ?? []),
|
|
271
|
+
];
|
|
272
|
+
const cols = [];
|
|
273
|
+
for (const subjectId of subjects) {
|
|
274
|
+
const stages = detailMap.get(subjectId);
|
|
275
|
+
if (!stages || stages.length === 0) {
|
|
276
|
+
cols.push({ subjectId, label: subjectId });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
for (const stage of stages) {
|
|
280
|
+
if (!stage.tasks || stage.tasks.length === 0) {
|
|
281
|
+
// Stage with no tasks: fall back to stage-grain column.
|
|
282
|
+
cols.push({ subjectId, stageId: stage.id, label: `${subjectId}:${stage.id}` });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
for (const task of stage.tasks) {
|
|
286
|
+
cols.push({
|
|
287
|
+
subjectId,
|
|
288
|
+
stageId: stage.id,
|
|
289
|
+
taskId: task.id,
|
|
290
|
+
label: `${subjectId}:${stage.id}:${task.id}`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return cols;
|
|
298
|
+
}
|
|
230
299
|
/**
|
|
231
300
|
* Returns true when assertion `a` contributes to column `col`.
|
|
232
301
|
*
|
|
233
302
|
* At `'product'` grain: `a.subject` must match `col.subjectId`.
|
|
234
303
|
* At `'product-stage'` grain: `a.subject` must match AND either
|
|
235
|
-
* `a.realised_via` is absent (claim covers the whole subject
|
|
236
|
-
*
|
|
304
|
+
* `a.realised_via` is absent (claim covers the whole subject) OR
|
|
305
|
+
* `col.stageId` ∈ `a.realised_via`.
|
|
306
|
+
* At `'product-stage-task'` grain: as above for stage, plus `col.taskId` ∈
|
|
307
|
+
* `a.realised_via` counts as a match (task-level claim covers this task column).
|
|
308
|
+
* A stage-level claim (`col.stageId` ∈ `a.realised_via`) covers all task columns
|
|
309
|
+
* in that stage.
|
|
237
310
|
*/
|
|
238
311
|
function assertionMatchesColumn(a, col) {
|
|
239
312
|
if (a.subject !== col.subjectId)
|
|
@@ -242,19 +315,30 @@ function assertionMatchesColumn(a, col) {
|
|
|
242
315
|
return true;
|
|
243
316
|
if (!a.realised_via || a.realised_via.length === 0)
|
|
244
317
|
return true;
|
|
318
|
+
if (col.taskId) {
|
|
319
|
+
// Task grain: match on taskId OR on stageId (stage claim covers all tasks).
|
|
320
|
+
return a.realised_via.includes(col.taskId) || a.realised_via.includes(col.stageId);
|
|
321
|
+
}
|
|
245
322
|
return a.realised_via.includes(col.stageId);
|
|
246
323
|
}
|
|
247
324
|
/**
|
|
248
325
|
* Build the matrix per §5.2.
|
|
249
326
|
*
|
|
250
327
|
* At the default `grouping.columns: 'product'` grain, one column per subject.
|
|
251
|
-
* At `grouping.columns: 'product-stage'`, pass `objectDetails` to expand each
|
|
252
|
-
* subject into per-stage sub-columns (typically extracted from a
|
|
253
|
-
* process-blueprint document via `extractObjectDetails`). Subjects with no
|
|
254
|
-
* stage mapping fall back to a single product-grain column automatically.
|
|
255
328
|
*
|
|
256
|
-
*
|
|
257
|
-
* `
|
|
329
|
+
* At `grouping.columns: 'product-stage'`, pass `objectDetails` (from
|
|
330
|
+
* `extractObjectDetails` on a process-blueprint) to expand each subject into
|
|
331
|
+
* per-stage sub-columns. Subjects with no stage mapping fall back to a single
|
|
332
|
+
* product-grain column.
|
|
333
|
+
*
|
|
334
|
+
* At `grouping.columns: 'product-stage-task'`, pass `objectDetails` where each
|
|
335
|
+
* stage carries a `tasks[]` array (combine blueprint stages and process-flow
|
|
336
|
+
* tasks via `mergeStageTaskDetails`). Each task becomes a
|
|
337
|
+
* `(subject, stage, task)` column. Stages with no tasks fall back to
|
|
338
|
+
* stage-grain columns.
|
|
339
|
+
*
|
|
340
|
+
* Subjects are taken from `config.subjects.products`, `config.subjects.processes`,
|
|
341
|
+
* and `config.subjects.capabilities` (in that order). Subjects with no binding
|
|
258
342
|
* obligation still appear as columns so the gap is visible.
|
|
259
343
|
*/
|
|
260
344
|
export function buildImpactMatrix(canon, config, objectDetails) {
|
|
@@ -263,11 +347,14 @@ export function buildImpactMatrix(canon, config, objectDetails) {
|
|
|
263
347
|
assertions: canon.assertions,
|
|
264
348
|
});
|
|
265
349
|
const obligations = orderRows(resolveObligations(config, index, canon.requirements), config.order_rows_by);
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
350
|
+
const grain = config.grouping?.columns ?? 'product';
|
|
351
|
+
const hasDetails = objectDetails !== undefined && objectDetails.length > 0;
|
|
352
|
+
const columns = grain === 'product-stage-task' && hasDetails
|
|
353
|
+
? buildTaskColumns(config, objectDetails)
|
|
354
|
+
: (grain === 'product-stage' || grain === 'object-details') && hasDetails
|
|
355
|
+
? buildObjectDetailColumns(config, objectDetails)
|
|
356
|
+
: buildProductColumns(config, [...(canon.products ?? []), ...(canon.subjects ?? [])]);
|
|
357
|
+
const allowedStatuses = new Set(config.status_display?.show ?? ['compliant', 'partial', 'non_compliant', 'under_review', 'pending_owner', 'n_a']);
|
|
271
358
|
const rowIndex = new Set(obligations.map(r => r.id));
|
|
272
359
|
const cells = obligations.map(() => columns.map(() => emptyCell()));
|
|
273
360
|
for (let c = 0; c < columns.length; c++) {
|
|
@@ -357,9 +444,10 @@ function emptyCell() {
|
|
|
357
444
|
decoration: { isNew: false, isUrgent: false, deadlineStatus: 'none' },
|
|
358
445
|
};
|
|
359
446
|
}
|
|
360
|
-
// ── Process-blueprint
|
|
447
|
+
// ── Process-blueprint / process-flow extractors (CV-3a) ─────────────────────
|
|
361
448
|
/**
|
|
362
|
-
* Extract `ObjectDetailInput` from a raw (YAML-parsed)
|
|
449
|
+
* Extract stage-level `ObjectDetailInput` from a raw (YAML-parsed)
|
|
450
|
+
* process-blueprint document.
|
|
363
451
|
*
|
|
364
452
|
* Accepts both the bare document object and the wrapped form
|
|
365
453
|
* (`{ process_blueprint: { id, stages: […] } }`).
|
|
@@ -368,6 +456,8 @@ function emptyCell() {
|
|
|
368
456
|
*
|
|
369
457
|
* Pass the returned value into `buildImpactMatrix` as part of the
|
|
370
458
|
* `objectDetails` array when `grouping.columns: 'product-stage'` is configured.
|
|
459
|
+
* For `grouping.columns: 'product-stage-task'`, combine with
|
|
460
|
+
* `extractProcessFlowTasks` via `mergeStageTaskDetails`.
|
|
371
461
|
*/
|
|
372
462
|
export function extractObjectDetails(doc) {
|
|
373
463
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc))
|
|
@@ -396,12 +486,90 @@ export function extractObjectDetails(doc) {
|
|
|
396
486
|
}
|
|
397
487
|
return { objectId: id, details };
|
|
398
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Extract task-type steps from a raw (YAML-parsed) PROCESS element document.
|
|
491
|
+
*
|
|
492
|
+
* Reads `notation: process` documents with an inline `flow.steps[]` array.
|
|
493
|
+
* Filters for task-type nodes (`task`, `userTask`, `serviceTask`) — start/end
|
|
494
|
+
* events and gateways are excluded. Returns a flat `ObjectDetailInput` where
|
|
495
|
+
* `details` are the extracted task steps in declaration order.
|
|
496
|
+
*
|
|
497
|
+
* Returns `null` when the document is not a recognisable process element or
|
|
498
|
+
* has no `id` / `flow.steps` array.
|
|
499
|
+
*
|
|
500
|
+
* Use the returned value with `mergeStageTaskDetails` to embed tasks into
|
|
501
|
+
* blueprint stages for `grouping.columns: product-stage-task`.
|
|
502
|
+
*/
|
|
503
|
+
export function extractProcessFlowTasks(doc) {
|
|
504
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc))
|
|
505
|
+
return null;
|
|
506
|
+
const top = doc;
|
|
507
|
+
if (top.notation !== 'process')
|
|
508
|
+
return null;
|
|
509
|
+
const id = typeof top.id === 'string' ? top.id : null;
|
|
510
|
+
if (!id)
|
|
511
|
+
return null;
|
|
512
|
+
const flow = top.flow && typeof top.flow === 'object' && !Array.isArray(top.flow)
|
|
513
|
+
? top.flow
|
|
514
|
+
: null;
|
|
515
|
+
if (!flow || !Array.isArray(flow.steps))
|
|
516
|
+
return null;
|
|
517
|
+
const TASK_TYPES = new Set(['task', 'userTask', 'serviceTask', 'scriptTask',
|
|
518
|
+
'receiveTask', 'sendTask', 'manualTask', 'businessRuleTask', 'callActivity', 'subProcess']);
|
|
519
|
+
const details = [];
|
|
520
|
+
for (const step of flow.steps) {
|
|
521
|
+
if (!step || typeof step !== 'object' || Array.isArray(step))
|
|
522
|
+
continue;
|
|
523
|
+
const s = step;
|
|
524
|
+
const stepId = typeof s.id === 'string' ? s.id : null;
|
|
525
|
+
if (!stepId)
|
|
526
|
+
continue;
|
|
527
|
+
if (!TASK_TYPES.has(s.type))
|
|
528
|
+
continue;
|
|
529
|
+
details.push({ id: stepId, name: typeof s.name === 'string' ? s.name : stepId });
|
|
530
|
+
}
|
|
531
|
+
return details.length > 0 ? { objectId: id, details } : null;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Combine blueprint stage details (from `extractObjectDetails`) with
|
|
535
|
+
* process-flow task details (from `extractProcessFlowTasks`) into a
|
|
536
|
+
* stage-with-tasks hierarchy for `grouping.columns: product-stage-task`.
|
|
537
|
+
*
|
|
538
|
+
* All tasks from `taskInput.details` are distributed into their parent stage
|
|
539
|
+
* by matching the `stageOwner` map: `stageId → taskId[]`. If `stageOwner` is
|
|
540
|
+
* omitted (no stage-to-task mapping is available), all tasks are placed into
|
|
541
|
+
* every stage (`undefined` key `→` all tasks).
|
|
542
|
+
*
|
|
543
|
+
* When `taskInput` is `null` or its `objectId` does not match `stageInput`,
|
|
544
|
+
* returns `stageInput` unchanged (callers can safely chain the result into
|
|
545
|
+
* `buildImpactMatrix` — it will degrade to stage-grain columns).
|
|
546
|
+
*
|
|
547
|
+
* @param stageInput Stage-level detail (from `extractObjectDetails`).
|
|
548
|
+
* @param taskInput Task-level detail (from `extractProcessFlowTasks`).
|
|
549
|
+
* @param stageOwner Optional mapping `stageId → taskId[]`. When provided,
|
|
550
|
+
* only tasks listed for a stage are embedded in it.
|
|
551
|
+
*/
|
|
552
|
+
export function mergeStageTaskDetails(stageInput, taskInput, stageOwner) {
|
|
553
|
+
if (!taskInput || taskInput.objectId !== stageInput.objectId)
|
|
554
|
+
return stageInput;
|
|
555
|
+
const allTasks = taskInput.details;
|
|
556
|
+
return {
|
|
557
|
+
objectId: stageInput.objectId,
|
|
558
|
+
details: stageInput.details.map(stage => {
|
|
559
|
+
const stageTasks = stageOwner
|
|
560
|
+
? allTasks.filter(t => (stageOwner[stage.id] ?? []).includes(t.id))
|
|
561
|
+
: allTasks;
|
|
562
|
+
return stageTasks.length > 0 ? { ...stage, tasks: stageTasks } : stage;
|
|
563
|
+
}),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
399
566
|
// ── Markdown rendering ──────────────────────────────────────────────────────
|
|
400
567
|
const STATUS_GLYPH = {
|
|
401
568
|
compliant: 'OK',
|
|
402
569
|
partial: 'PARTIAL',
|
|
403
570
|
non_compliant: 'FAIL',
|
|
404
571
|
under_review: 'REVIEW',
|
|
572
|
+
pending_owner: 'PENDING',
|
|
405
573
|
n_a: 'N/A',
|
|
406
574
|
};
|
|
407
575
|
function escMd(s) {
|
|
@@ -450,7 +618,7 @@ export function renderImpactMarkdown(matrix) {
|
|
|
450
618
|
lines.push('');
|
|
451
619
|
lines.push('## Legend');
|
|
452
620
|
lines.push('');
|
|
453
|
-
lines.push('- **OK / PARTIAL / FAIL / REVIEW / N/A** — aggregated `ASSERTION.status` per §5.2.');
|
|
621
|
+
lines.push('- **OK / PARTIAL / FAIL / REVIEW / PENDING / N/A** — aggregated `ASSERTION.status` per §5.2.');
|
|
454
622
|
lines.push(`- **${escMd(matrix.emptyLabels.no_obligation_label)}** — modelling gap; no admitted ASSERTION binds this (obligation, subject) pair.`);
|
|
455
623
|
lines.push(`- **${escMd(matrix.emptyLabels.no_obligation_applies_label)}** — modelled fact; an admitted ASSERTION with status \`n_a\` excludes this pair.`);
|
|
456
624
|
return lines.join('\n') + '\n';
|
|
@@ -8,9 +8,9 @@ export { renderComplianceMarkdown } from './markdown.js';
|
|
|
8
8
|
export type { ReportScope, MarkdownOptions } from './markdown.js';
|
|
9
9
|
export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
|
|
10
10
|
export type { HtmlOptions, ImpactMatrixHtmlOptions } from './html.js';
|
|
11
|
-
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, computeDeadlineStatus } from './impact.js';
|
|
11
|
+
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
|
|
12
12
|
export type { ImpactCell, ImpactColumn, ImpactEmptyCellLabels, ImpactGrouping, ImpactMatrix, ImpactObligationFilter, ImpactStatusDisplay, ImpactSubjects, ImpactViewConfig, ParseImpactViewConfigResult, } from './impact.js';
|
|
13
13
|
export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
|
|
14
14
|
export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
|
|
15
|
-
export type { CoverageMetricConfig,
|
|
15
|
+
export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
|
|
16
16
|
export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, } from './types.js';
|
package/dist/compliance/index.js
CHANGED
|
@@ -4,6 +4,6 @@ export { buildGapReport } from './gap-report.js';
|
|
|
4
4
|
export { emptyCanon, ingestComplianceDoc } from './classify.js';
|
|
5
5
|
export { renderComplianceMarkdown } from './markdown.js';
|
|
6
6
|
export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
|
|
7
|
-
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, computeDeadlineStatus } from './impact.js';
|
|
7
|
+
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
|
|
8
8
|
export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
|
|
9
9
|
export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
|
|
@@ -44,18 +44,37 @@ export interface IndexAssertion {
|
|
|
44
44
|
* stage-grouped matrix cells.
|
|
45
45
|
*/
|
|
46
46
|
realised_via?: string[];
|
|
47
|
+
/** ID or name of the party who must confirm this assertion (pending-owner state). */
|
|
48
|
+
owner_to_confirm?: string;
|
|
47
49
|
}
|
|
48
|
-
/** One
|
|
50
|
+
/** One stage (or task) element of a business object, used as a matrix sub-column. */
|
|
49
51
|
export interface ObjectDetailDef {
|
|
50
52
|
id: string;
|
|
51
53
|
name: string;
|
|
54
|
+
/**
|
|
55
|
+
* Ordered task-type flow steps within this stage.
|
|
56
|
+
* Present only when `grouping.columns: product-stage-task` grain is active.
|
|
57
|
+
* Each task produces a separate matrix column: (subject, stageId, taskId).
|
|
58
|
+
* When absent or empty, the stage itself is the column grain.
|
|
59
|
+
*/
|
|
60
|
+
tasks?: Array<{
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
}>;
|
|
52
64
|
}
|
|
53
65
|
/**
|
|
54
|
-
* Maps a business object to its ordered
|
|
55
|
-
* Passed into `buildImpactMatrix`
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
66
|
+
* Maps a business object to its ordered stages (and optionally their tasks).
|
|
67
|
+
* Passed into `buildImpactMatrix` to enable stage/task column grouping.
|
|
68
|
+
*
|
|
69
|
+
* - For `grouping.columns: product-stage`: `details` contains stages; each
|
|
70
|
+
* stage becomes one matrix column.
|
|
71
|
+
* - For `grouping.columns: product-stage-task`: each stage in `details` carries
|
|
72
|
+
* a `tasks` array; each task produces a `(subject, stage, task)` column.
|
|
73
|
+
* Stages with no tasks fall back to a stage-grain column.
|
|
74
|
+
*
|
|
75
|
+
* Typically built by combining `extractObjectDetails` (blueprint stages) and
|
|
76
|
+
* `extractProcessFlowTasks` (process flow tasks), then merging via
|
|
77
|
+
* `mergeStageTaskDetails`.
|
|
59
78
|
*/
|
|
60
79
|
export interface ObjectDetailInput {
|
|
61
80
|
objectId: string;
|
package/dist/edge-path.d.ts
CHANGED
|
@@ -10,9 +10,13 @@ export declare const DEFAULT_EDGE_CURVATURE = 1;
|
|
|
10
10
|
* Builds the SVG `d` for a horizontal-tangent cubic Bézier from (sx,sy) to
|
|
11
11
|
* (tx,ty).
|
|
12
12
|
*
|
|
13
|
-
* `curvature` scales the control-handle
|
|
14
|
-
*
|
|
13
|
+
* `curvature` scales the exit control-handle (departure from source).
|
|
14
|
+
* `entryCurvature` scales the entry control-handle (arrival at target);
|
|
15
|
+
* defaults to `curvature` when omitted so callers that pass one value get
|
|
16
|
+
* symmetric handles (the historical behaviour).
|
|
17
|
+
*
|
|
18
|
+
* - 0 → handle collapses onto its endpoint ⇒ straight at that end.
|
|
15
19
|
* - 1 → the historical curve (no visual change from before #76).
|
|
16
|
-
* - >1 → progressively stronger arc.
|
|
20
|
+
* - >1 → progressively stronger arc at that end.
|
|
17
21
|
*/
|
|
18
|
-
export declare function horizontalCubicEdgePath(sx: number, sy: number, tx: number, ty: number, curvature?: number): string;
|
|
22
|
+
export declare function horizontalCubicEdgePath(sx: number, sy: number, tx: number, ty: number, curvature?: number, entryCurvature?: number): string;
|
package/dist/edge-path.js
CHANGED
|
@@ -21,15 +21,20 @@ export const DEFAULT_EDGE_CURVATURE = 1;
|
|
|
21
21
|
* Builds the SVG `d` for a horizontal-tangent cubic Bézier from (sx,sy) to
|
|
22
22
|
* (tx,ty).
|
|
23
23
|
*
|
|
24
|
-
* `curvature` scales the control-handle
|
|
25
|
-
*
|
|
24
|
+
* `curvature` scales the exit control-handle (departure from source).
|
|
25
|
+
* `entryCurvature` scales the entry control-handle (arrival at target);
|
|
26
|
+
* defaults to `curvature` when omitted so callers that pass one value get
|
|
27
|
+
* symmetric handles (the historical behaviour).
|
|
28
|
+
*
|
|
29
|
+
* - 0 → handle collapses onto its endpoint ⇒ straight at that end.
|
|
26
30
|
* - 1 → the historical curve (no visual change from before #76).
|
|
27
|
-
* - >1 → progressively stronger arc.
|
|
31
|
+
* - >1 → progressively stronger arc at that end.
|
|
28
32
|
*/
|
|
29
|
-
export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE_CURVATURE) {
|
|
33
|
+
export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE_CURVATURE, entryCurvature) {
|
|
30
34
|
const dx = tx - sx;
|
|
31
35
|
const dy = ty - sy;
|
|
32
36
|
const baseHandle = Math.max(EDGE_MIN_HANDLE, Math.abs(dx) * DX_FACTOR, Math.abs(dy) * DY_FACTOR);
|
|
33
|
-
const
|
|
34
|
-
|
|
37
|
+
const exitHandle = baseHandle * curvature;
|
|
38
|
+
const entryHandle = baseHandle * (entryCurvature ?? curvature);
|
|
39
|
+
return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
|
|
35
40
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { validateFactor } from '../validate.js';
|
|
3
|
-
function
|
|
3
|
+
function validExternal() {
|
|
4
4
|
return {
|
|
5
5
|
notation: 'factor',
|
|
6
6
|
id: 'FACTOR-EU-REG-1',
|
|
7
|
-
name: 'EU regulatory window
|
|
7
|
+
name: 'EU regulatory window',
|
|
8
8
|
type: 'external',
|
|
9
9
|
category: 'legal',
|
|
10
10
|
description: 'Standing external driver — the EU regulatory regime.',
|
|
@@ -17,67 +17,96 @@ function valid() {
|
|
|
17
17
|
valid_to: null,
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function validInternal() {
|
|
21
|
+
return {
|
|
22
|
+
notation: 'factor',
|
|
23
|
+
id: 'FACTOR-COMP-1',
|
|
24
|
+
name: 'Support response time',
|
|
25
|
+
type: 'internal',
|
|
26
|
+
description: 'Standing internal driver.',
|
|
27
|
+
zone: 'canon',
|
|
28
|
+
admitted_at: '2026-05-29',
|
|
29
|
+
admitted_by: 'v.korobeinikov',
|
|
30
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
31
|
+
valid_from: '2026-05-26',
|
|
32
|
+
valid_to: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const codes = (input, opts) => validateFactor(input, opts).errors.map(e => e.code);
|
|
22
36
|
describe('validateFactor — positive', () => {
|
|
23
|
-
it('accepts
|
|
24
|
-
const r = validateFactor(
|
|
37
|
+
it('accepts a well-formed external PESTLE factor', () => {
|
|
38
|
+
const r = validateFactor(validExternal());
|
|
25
39
|
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
26
|
-
expect(r.errors).toHaveLength(0);
|
|
27
40
|
});
|
|
28
|
-
it('accepts a
|
|
29
|
-
|
|
30
|
-
delete r.type;
|
|
31
|
-
delete r.category;
|
|
32
|
-
delete r.references_constraint;
|
|
33
|
-
expect(validateFactor(r).valid).toBe(true);
|
|
41
|
+
it('accepts a well-formed internal factor (no PESTLE category)', () => {
|
|
42
|
+
expect(validateFactor(validInternal()).valid).toBe(true);
|
|
34
43
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
expect(
|
|
44
|
+
it('accepts a minimal factor with no optional fields', () => {
|
|
45
|
+
const f = validInternal();
|
|
46
|
+
delete f.type;
|
|
47
|
+
delete f.description;
|
|
48
|
+
expect(validateFactor(f).valid).toBe(true);
|
|
40
49
|
});
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
});
|
|
51
|
+
describe('validateFactor — FACTOR-001 (shape / id grammar)', () => {
|
|
52
|
+
it('flags a missing required envelope field', () => {
|
|
53
|
+
const f = validExternal();
|
|
54
|
+
delete f.admitted_by;
|
|
55
|
+
expect(codes(f)).toContain('FACTOR-001');
|
|
43
56
|
});
|
|
44
|
-
it('flags an id
|
|
45
|
-
expect(codes({ ...
|
|
57
|
+
it('flags an id that violates the grammar', () => {
|
|
58
|
+
expect(codes({ ...validExternal(), id: 'FACTOR-EU-REG-001' })).toContain('FACTOR-001');
|
|
59
|
+
expect(codes({ ...validExternal(), id: 'FAC-1' })).toContain('FACTOR-001');
|
|
60
|
+
expect(codes({ ...validExternal(), id: 'factor-1' })).toContain('FACTOR-001');
|
|
46
61
|
});
|
|
47
|
-
it('flags
|
|
48
|
-
expect(codes({ ...
|
|
62
|
+
it('flags a wrong notation tag and a non-canon zone', () => {
|
|
63
|
+
expect(codes({ ...validExternal(), notation: 'driver' })).toContain('FACTOR-001');
|
|
64
|
+
expect(codes({ ...validExternal(), zone: 'sandbox' })).toContain('FACTOR-001');
|
|
49
65
|
});
|
|
50
66
|
it('flags a missing valid_to key (distinct from a null value)', () => {
|
|
51
|
-
const
|
|
52
|
-
delete
|
|
53
|
-
expect(codes(
|
|
54
|
-
expect(validateFactor({ ...valid(), valid_to: null }).valid).toBe(true);
|
|
67
|
+
const f = validExternal();
|
|
68
|
+
delete f.valid_to;
|
|
69
|
+
expect(codes(f)).toContain('FACTOR-001');
|
|
55
70
|
});
|
|
56
|
-
it('
|
|
57
|
-
expect(codes(
|
|
58
|
-
expect(codes(
|
|
71
|
+
it('rejects a non-object', () => {
|
|
72
|
+
expect(codes(null)).toEqual(['FACTOR-001']);
|
|
73
|
+
expect(codes(['a'])).toEqual(['FACTOR-001']);
|
|
59
74
|
});
|
|
60
75
|
});
|
|
61
76
|
describe('validateFactor — FACTOR-002 (type enum)', () => {
|
|
62
|
-
it('flags a type outside external
|
|
63
|
-
expect(codes({ ...
|
|
64
|
-
});
|
|
65
|
-
it('accepts each permitted type', () => {
|
|
66
|
-
for (const t of ['external', 'internal']) {
|
|
67
|
-
expect(codes({ ...valid(), type: t, category: undefined })).not.toContain('FACTOR-002');
|
|
68
|
-
}
|
|
77
|
+
it('flags a type value outside {external, internal}', () => {
|
|
78
|
+
expect(codes({ ...validExternal(), type: 'driver' })).toContain('FACTOR-002');
|
|
69
79
|
});
|
|
70
80
|
});
|
|
71
81
|
describe('validateFactor — FACTOR-003 (PESTLE category)', () => {
|
|
72
|
-
it('flags a category outside
|
|
73
|
-
expect(codes({ ...
|
|
82
|
+
it('flags a category outside PESTLE', () => {
|
|
83
|
+
expect(codes({ ...validExternal(), category: 'cosmic' })).toContain('FACTOR-003');
|
|
74
84
|
});
|
|
75
|
-
it('
|
|
76
|
-
|
|
85
|
+
it('flags PESTLE category on an internal factor', () => {
|
|
86
|
+
const f = validInternal();
|
|
87
|
+
f.category = 'legal';
|
|
88
|
+
expect(codes(f)).toContain('FACTOR-003');
|
|
77
89
|
});
|
|
78
|
-
it('accepts each permitted PESTLE
|
|
79
|
-
for (const
|
|
80
|
-
|
|
90
|
+
it('accepts each permitted PESTLE category on an external factor', () => {
|
|
91
|
+
for (const cat of ['political', 'economic', 'social', 'technological', 'legal', 'environmental']) {
|
|
92
|
+
const f = validExternal();
|
|
93
|
+
f.category = cat;
|
|
94
|
+
expect(codes(f)).not.toContain('FACTOR-003');
|
|
81
95
|
}
|
|
82
96
|
});
|
|
83
97
|
});
|
|
98
|
+
describe('validateFactor — FACTOR-004 (references_constraint)', () => {
|
|
99
|
+
it('flags a malformed references_constraint entry', () => {
|
|
100
|
+
expect(codes({ ...validExternal(), references_constraint: ['not an id'] })).toContain('FACTOR-004');
|
|
101
|
+
});
|
|
102
|
+
it('flags a references_constraint entry of the wrong TYPE', () => {
|
|
103
|
+
expect(codes({ ...validExternal(), references_constraint: ['GOAL-X-1'] })).toContain('FACTOR-004');
|
|
104
|
+
});
|
|
105
|
+
it('flags an unresolved id when a catalog is supplied', () => {
|
|
106
|
+
const catalog = { typeOf: () => undefined };
|
|
107
|
+
expect(codes(validExternal(), { catalog })).toContain('FACTOR-004');
|
|
108
|
+
});
|
|
109
|
+
it('does not flag without a catalog for a well-formed CONSTRAINT id', () => {
|
|
110
|
+
expect(codes(validExternal())).not.toContain('FACTOR-004');
|
|
111
|
+
});
|
|
112
|
+
});
|
package/dist/factor/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export type { Factor
|
|
2
|
-
export {
|
|
1
|
+
export type { Factor, FactorType, FactorPestleCategory } from './types.js';
|
|
2
|
+
export { FACTOR_PESTLE_CATEGORIES } from './types.js';
|
|
3
3
|
export { validateFactor } from './validate.js';
|
|
4
|
+
export type { FactorValidateOptions } from './validate.js';
|
package/dist/factor/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { FACTOR_PESTLE_CATEGORIES } from './types.js';
|
|
2
2
|
export { validateFactor } from './validate.js';
|