@transitrix/diagrams 1.0.0 → 1.1.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.
- package/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- package/dist/activities/cpm.js +11 -2
- package/dist/activities/gantt.js +6 -3
- package/dist/activities/types.d.ts +1 -0
- package/dist/activities/validate.js +3 -3
- 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 +161 -7
- 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 +115 -28
- 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 +509 -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 +78 -81
- package/dist/compliance/impact.d.ts +100 -16
- package/dist/compliance/impact.js +272 -28
- 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 +75 -46
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +5 -8
- 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__/chain-table.test.js +2 -2
- package/dist/fgca/__tests__/layout.test.js +11 -11
- package/dist/fgca/__tests__/preview-layout.test.js +7 -7
- package/dist/fgca/__tests__/resolver.test.js +185 -0
- package/dist/fgca/chain-table.d.ts +1 -1
- package/dist/fgca/chain-table.js +4 -4
- package/dist/fgca/index.d.ts +4 -2
- package/dist/fgca/index.js +2 -1
- package/dist/fgca/layout.d.ts +2 -2
- package/dist/fgca/layout.js +9 -9
- package/dist/fgca/nodeTypes.d.ts +1 -1
- package/dist/fgca/nodeTypes.js +2 -2
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
- package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
- package/dist/fgca/parse-canonical.js +15 -10
- package/dist/fgca/preview-layout.d.ts +1 -1
- package/dist/fgca/preview-layout.js +4 -4
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +126 -0
- package/dist/fgca/types.d.ts +11 -11
- package/dist/fgca/types.js +2 -2
- package/dist/fgca/validate.d.ts +2 -2
- package/dist/fgca/validate.js +17 -14
- 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/index.d.ts +1 -1
- package/dist/process-blueprint/layout.js +66 -35
- package/dist/process-blueprint/types.d.ts +36 -1
- package/dist/scenarios/index.d.ts +1 -1
- package/dist/scenarios/types.d.ts +4 -4
- 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 +71 -69
- package/dist/theme/tokens.d.ts +7 -1
- package/dist/theme/tokens.js +3 -1
- package/dist/webview/__tests__/entry-notations.test.js +2 -0
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/__tests__/render-process.test.js +463 -0
- package/dist/webview/__tests__/render-util.test.d.ts +1 -0
- package/dist/webview/__tests__/render-util.test.js +54 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +9 -5
- package/dist/webview/render-activities.d.ts +24 -2
- package/dist/webview/render-activities.js +92 -57
- package/dist/webview/render-activity-card.d.ts +11 -1
- package/dist/webview/render-activity-card.js +115 -79
- package/dist/webview/render-blocks.d.ts +24 -1
- package/dist/webview/render-blocks.js +58 -24
- package/dist/webview/render-fgca.d.ts +31 -0
- package/dist/webview/render-fgca.js +30 -19
- package/dist/webview/render-goals.d.ts +27 -1
- package/dist/webview/render-goals.js +42 -32
- package/dist/webview/render-process-blueprint.d.ts +11 -1
- package/dist/webview/render-process-blueprint.js +118 -53
- package/dist/webview/render-process.d.ts +95 -0
- package/dist/webview/render-process.js +392 -0
- package/dist/webview/render-scenarios.js +2 -2
- package/dist/webview/render-util.d.ts +19 -6
- package/dist/webview/render-util.js +23 -15
- package/package.json +54 -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 +107 -95
- package/src/activities/gantt.ts +277 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +149 -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 +330 -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 +422 -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 +1059 -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 +291 -295
- package/src/compliance/impact.ts +979 -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 +81 -46
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +15 -19
- 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 +405 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +159 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +129 -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 +32 -31
- package/src/process-blueprint/layout.ts +564 -531
- package/src/process-blueprint/types.ts +278 -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 +239 -237
- package/src/theme/tokens.ts +135 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +79 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/__tests__/render-process.test.ts +538 -0
- package/src/webview/__tests__/render-util.test.ts +66 -0
- package/src/webview/entry.ts +298 -292
- package/src/webview/render-activities.ts +111 -69
- package/src/webview/render-activity-card.ts +275 -234
- package/src/webview/render-blocks.ts +78 -28
- package/src/webview/render-fgca.ts +53 -28
- package/src/webview/render-goals.ts +133 -110
- package/src/webview/render-process-blueprint.ts +281 -207
- package/src/webview/render-process.ts +507 -0
- package/src/webview/render-scenarios.ts +3 -3
- package/src/webview/render-util.ts +27 -14
- 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.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
- /package/dist/{issues/__tests__/validate.test.d.ts → webview/__tests__/render-process.test.d.ts} +0 -0
package/src/factor/validate.ts
CHANGED
|
@@ -1,70 +1,96 @@
|
|
|
1
1
|
// FACTOR validator — methodology notations/ELEMENT_PRIMITIVES.md §7.1.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Codes:
|
|
4
|
+
// FACTOR-001 — shape / id grammar / required envelope fields.
|
|
5
|
+
// FACTOR-002 — `type` outside {external, internal}.
|
|
6
|
+
// FACTOR-003 — `category` outside PESTLE vocabulary, or PESTLE applied to a
|
|
7
|
+
// non-external factor (PESTLE applies only to external drivers).
|
|
8
|
+
// FACTOR-004 — `references_constraint` entry malformed or wrong TYPE.
|
|
9
|
+
//
|
|
10
|
+
// `references_constraint` existence (resolves to an admitted artefact) runs
|
|
11
|
+
// only when a `CanonCatalog` is supplied; the prefix-TYPE check runs either way.
|
|
12
|
+
|
|
13
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
14
|
+
import { typeOfId, isCanonicalIdOfType, type CanonCatalog } from '../typed-id.js';
|
|
15
|
+
import { FACTOR_PESTLE_CATEGORIES } from './types.js';
|
|
16
|
+
|
|
17
|
+
export interface FactorValidateOptions {
|
|
18
|
+
/** When provided, enforces that each `references_constraint` id is admitted. */
|
|
19
|
+
catalog?: CanonCatalog;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const REQUIRED_STRING_FIELDS = [
|
|
23
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
24
|
+
] as const;
|
|
6
25
|
|
|
7
|
-
|
|
8
|
-
import { typeOfId } from '../typed-id.js';
|
|
9
|
-
import { validateElementEnvelope } from '../element-envelope.js';
|
|
10
|
-
import { FACTOR_CATEGORIES, FACTOR_TYPES } from './types.js';
|
|
26
|
+
const PESTLE: readonly string[] = FACTOR_PESTLE_CATEGORIES;
|
|
11
27
|
|
|
12
|
-
export function validateFactor(input: unknown): ValidationResult {
|
|
28
|
+
export function validateFactor(input: unknown, options: FactorValidateOptions = {}): ValidationResult {
|
|
13
29
|
const errors: ValidationError[] = [];
|
|
14
30
|
const warnings: ValidationWarning[] = [];
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
errors.push(...envErrors);
|
|
22
|
-
if (!record) return { valid: false, errors, warnings };
|
|
32
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
33
|
+
errors.push({ code: 'FACTOR-001', message: 'Factor must be a YAML mapping.' });
|
|
34
|
+
return { valid: false, errors, warnings };
|
|
35
|
+
}
|
|
36
|
+
const f = input as Record<string, unknown>;
|
|
23
37
|
|
|
24
|
-
// FACTOR-
|
|
25
|
-
if (
|
|
26
|
-
errors.push({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
// FACTOR-001 — id grammar + notation tag + envelope.
|
|
39
|
+
if (!isCanonicalIdOfType(f.id, 'FACTOR')) {
|
|
40
|
+
errors.push({ code: 'FACTOR-001', message: `id "${String(f.id)}" must match FACTOR-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
41
|
+
}
|
|
42
|
+
if (f.notation !== 'driver') {
|
|
43
|
+
errors.push({ code: 'FACTOR-001', message: 'notation must be the fixed value "driver".', path: 'notation' });
|
|
44
|
+
}
|
|
45
|
+
if (f.zone !== undefined && f.zone !== 'canon') {
|
|
46
|
+
errors.push({ code: 'FACTOR-001', message: 'zone must be "canon" for a FACTOR.', path: 'zone' });
|
|
47
|
+
} else if (f.zone === undefined) {
|
|
48
|
+
errors.push({ code: 'FACTOR-001', message: 'zone is required.', path: 'zone' });
|
|
49
|
+
}
|
|
50
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
51
|
+
const v = f[field];
|
|
52
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
53
|
+
errors.push({ code: 'FACTOR-001', message: `${field} is required.`, path: field });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (f.gate_checks === null || typeof f.gate_checks !== 'object') {
|
|
57
|
+
errors.push({ code: 'FACTOR-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
58
|
+
}
|
|
59
|
+
if (!('valid_to' in f) || !(typeof f.valid_to === 'string' || f.valid_to === null)) {
|
|
60
|
+
errors.push({ code: 'FACTOR-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
31
61
|
}
|
|
32
62
|
|
|
33
|
-
// FACTOR-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
code: 'FACTOR-003',
|
|
45
|
-
message: 'category is set on an internal factor; PESTLE applies to external drivers only.',
|
|
46
|
-
path: 'category',
|
|
47
|
-
});
|
|
63
|
+
// FACTOR-002 — type enum (optional field; only check when present).
|
|
64
|
+
if (f.type !== undefined && f.type !== 'external' && f.type !== 'internal') {
|
|
65
|
+
errors.push({ code: 'FACTOR-002', message: `type "${String(f.type)}" must be one of external, internal.`, path: 'type' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// FACTOR-003 — category PESTLE vocabulary + external-only constraint.
|
|
69
|
+
if (f.category !== undefined) {
|
|
70
|
+
if (typeof f.category !== 'string' || !PESTLE.includes(f.category)) {
|
|
71
|
+
errors.push({ code: 'FACTOR-003', message: `category "${String(f.category)}" must be one of ${PESTLE.join(', ')}.`, path: 'category' });
|
|
72
|
+
} else if (f.type === 'internal') {
|
|
73
|
+
errors.push({ code: 'FACTOR-003', message: 'category is PESTLE — applies only to external factors; omit on internal factors.', path: 'category' });
|
|
48
74
|
}
|
|
49
75
|
}
|
|
50
76
|
|
|
51
|
-
// FACTOR-
|
|
52
|
-
if (
|
|
53
|
-
if (!Array.isArray(
|
|
54
|
-
errors.push({
|
|
55
|
-
code: 'FACTOR-001',
|
|
56
|
-
message: 'references_constraint must be a list of typed CONSTRAINT IDs.',
|
|
57
|
-
path: 'references_constraint',
|
|
58
|
-
});
|
|
77
|
+
// FACTOR-004 — references_constraint refs.
|
|
78
|
+
if (f.references_constraint !== undefined) {
|
|
79
|
+
if (!Array.isArray(f.references_constraint)) {
|
|
80
|
+
errors.push({ code: 'FACTOR-001', message: 'references_constraint must be a list of typed IDs.', path: 'references_constraint' });
|
|
59
81
|
} else {
|
|
60
|
-
|
|
82
|
+
f.references_constraint.forEach((ref, i) => {
|
|
61
83
|
const type = typeOfId(ref);
|
|
84
|
+
if (!type) {
|
|
85
|
+
errors.push({ code: 'FACTOR-004', message: `references_constraint[${i}] "${String(ref)}" is not a resolvable typed ID.`, path: `references_constraint[${i}]` });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
62
88
|
if (type !== 'CONSTRAINT') {
|
|
63
|
-
errors.push({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
89
|
+
errors.push({ code: 'FACTOR-004', message: `references_constraint[${i}] TYPE "${type}" must be CONSTRAINT.`, path: `references_constraint[${i}]` });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (options.catalog && options.catalog.typeOf(ref as string) === undefined) {
|
|
93
|
+
errors.push({ code: 'FACTOR-004', message: `references_constraint[${i}] "${String(ref)}" does not resolve to an admitted artefact.`, path: `references_constraint[${i}]` });
|
|
68
94
|
}
|
|
69
95
|
});
|
|
70
96
|
}
|
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildChainTable, type ChainTable, type ChainColumn } from '../chain-table.js';
|
|
3
|
-
import type { FGCAPreviewDoc } from '../preview-layout.js';
|
|
4
|
-
|
|
5
|
-
// Renders a ChainTable to a compact text grid for readable assertions. Each
|
|
6
|
-
// cell shows "label×rowSpan" (×1 omitted); a position covered by a span above
|
|
7
|
-
// shows "·".
|
|
8
|
-
function asGrid(t: ChainTable): string[][] {
|
|
9
|
-
return t.rows.map(row =>
|
|
10
|
-
row.map(c => {
|
|
11
|
-
if (c === null) return '·';
|
|
12
|
-
const label = c.cell ? c.cell.label : '∅';
|
|
13
|
-
return c.rowSpan > 1 ? `${label}×${c.rowSpan}` : label;
|
|
14
|
-
}),
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('buildChainTable — worked example (FGCA)', () => {
|
|
19
|
-
// F1 → G1, G2 ; G1 → C1 → A1 ; G2 → C2 → A2, A3
|
|
20
|
-
const doc: FGCAPreviewDoc = {
|
|
21
|
-
factors: [{ id: 1, name: 'F1' }],
|
|
22
|
-
goals: [
|
|
23
|
-
{ id: 10, name: 'G1', factor: [{ id: 1 }] },
|
|
24
|
-
{ id: 11, name: 'G2', factor: [{ id: 1 }] },
|
|
25
|
-
],
|
|
26
|
-
changes: [
|
|
27
|
-
{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] },
|
|
28
|
-
{ id: 21, name: 'C2', goal_id: 11, activity_ids: [31, 32] },
|
|
29
|
-
],
|
|
30
|
-
activities: [
|
|
31
|
-
{ id: 30, name: 'A1', goal_id: 10 },
|
|
32
|
-
{ id: 31, name: 'A2', goal_id: 11 },
|
|
33
|
-
{ id: 32, name: 'A3', goal_id: 11 },
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
it('produces the spec grid with F1 spanning 3 and G2/C2 spanning 2', () => {
|
|
38
|
-
const t = buildChainTable(doc);
|
|
39
|
-
expect(t.columns).toEqual<ChainColumn[]>(['
|
|
40
|
-
expect(asGrid(t)).toEqual([
|
|
41
|
-
['F1×3', 'G1', 'C1', 'A1'],
|
|
42
|
-
['·', 'G2×2', 'C2×2', 'A2'],
|
|
43
|
-
['·', '·', '·', 'A3'],
|
|
44
|
-
]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('orders the merged cells with correct rowSpans on the model', () => {
|
|
48
|
-
const t = buildChainTable(doc);
|
|
49
|
-
// Factor cell spans all three rows.
|
|
50
|
-
expect(t.rows[0][0]).toMatchObject({ rowSpan: 3, cell: { label: 'F1' } });
|
|
51
|
-
// Covered positions are null (no <td> emitted).
|
|
52
|
-
expect(t.rows[1][0]).toBeNull();
|
|
53
|
-
expect(t.rows[2][1]).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('buildChainTable — FGA (no Change column)', () => {
|
|
58
|
-
// FGA links Goal → Activity directly via activity.goal_id; no changes.
|
|
59
|
-
const doc: FGCAPreviewDoc = {
|
|
60
|
-
factors: [{ id: 1, name: 'F1' }],
|
|
61
|
-
goals: [{ id: 10, name: 'G1', factor: [{ id: 1 }] }],
|
|
62
|
-
activities: [
|
|
63
|
-
{ id: 30, name: 'A1', goal_id: 10 },
|
|
64
|
-
{ id: 31, name: 'A2', goal_id: 10 },
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
it('emits three columns and merges Factor/Goal across the two activities', () => {
|
|
69
|
-
const t = buildChainTable(doc, { hideChanges: true });
|
|
70
|
-
expect(t.columns).toEqual<ChainColumn[]>(['
|
|
71
|
-
expect(asGrid(t)).toEqual([
|
|
72
|
-
['F1×2', 'G1×2', 'A1'],
|
|
73
|
-
['·', '·', 'A2'],
|
|
74
|
-
]);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('buildChainTable — missing downstream links (gaps stay visible)', () => {
|
|
79
|
-
it('shows a factor with no goal, a goal with no change, and a change with no activity', () => {
|
|
80
|
-
const doc: FGCAPreviewDoc = {
|
|
81
|
-
factors: [
|
|
82
|
-
{ id: 1, name: 'F1' },
|
|
83
|
-
{ id: 2, name: 'F2' }, // no goals
|
|
84
|
-
],
|
|
85
|
-
goals: [
|
|
86
|
-
{ id: 10, name: 'G1', factor: [{ id: 1 }] }, // no change, no activity
|
|
87
|
-
{ id: 11, name: 'G2', factor: [{ id: 1 }] }, // has a change with no activity
|
|
88
|
-
],
|
|
89
|
-
changes: [{ id: 20, name: 'C1', goal_id: 11, activity_ids: [] }],
|
|
90
|
-
activities: [],
|
|
91
|
-
};
|
|
92
|
-
const t = buildChainTable(doc);
|
|
93
|
-
expect(asGrid(t)).toEqual([
|
|
94
|
-
['F1×2', 'G1', '∅', '∅'],
|
|
95
|
-
['·', 'G2', 'C1', '∅'],
|
|
96
|
-
['F2', '∅', '∅', '∅'],
|
|
97
|
-
]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('renders a change-less direct goal→activity path with an empty Change cell', () => {
|
|
101
|
-
const doc: FGCAPreviewDoc = {
|
|
102
|
-
factors: [{ id: 1, name: 'F1' }],
|
|
103
|
-
goals: [{ id: 10, name: 'G1', factor: [{ id: 1 }] }],
|
|
104
|
-
// activity bound straight to the goal, not covered by any change
|
|
105
|
-
changes: [],
|
|
106
|
-
activities: [{ id: 30, name: 'A1', goal_id: 10 }],
|
|
107
|
-
};
|
|
108
|
-
const t = buildChainTable(doc);
|
|
109
|
-
expect(asGrid(t)).toEqual([['F1', 'G1', '∅', 'A1']]);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('buildChainTable — orphans (broken refs are warnings, not errors)', () => {
|
|
114
|
-
it('places a goal with a missing factor in a trailing empty-Factor row', () => {
|
|
115
|
-
const doc: FGCAPreviewDoc = {
|
|
116
|
-
factors: [{ id: 1, name: 'F1' }],
|
|
117
|
-
goals: [
|
|
118
|
-
{ id: 10, name: 'G1', factor: [{ id: 1 }] },
|
|
119
|
-
{ id: 11, name: 'Gx', factor: [{ id: 99 }] }, // factor 99 does not exist
|
|
120
|
-
{ id: 12, name: 'Gy' }, // no factor at all
|
|
121
|
-
],
|
|
122
|
-
changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
|
|
123
|
-
activities: [{ id: 30, name: 'A1', goal_id: 10 }],
|
|
124
|
-
};
|
|
125
|
-
const t = buildChainTable(doc);
|
|
126
|
-
expect(asGrid(t)).toEqual([
|
|
127
|
-
['F1', 'G1', 'C1', 'A1'],
|
|
128
|
-
['∅×2', 'Gx', '∅', '∅'],
|
|
129
|
-
['·', 'Gy', '∅', '∅'],
|
|
130
|
-
]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('surfaces an unlinked activity as a trailing row with empty cells to its left', () => {
|
|
134
|
-
const doc: FGCAPreviewDoc = {
|
|
135
|
-
factors: [{ id: 1, name: 'F1' }],
|
|
136
|
-
goals: [{ id: 10, name: 'G1', factor: [{ id: 1 }] }],
|
|
137
|
-
changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
|
|
138
|
-
activities: [
|
|
139
|
-
{ id: 30, name: 'A1', goal_id: 10 },
|
|
140
|
-
{ id: 99, name: 'Aorphan' }, // no goal, no change
|
|
141
|
-
],
|
|
142
|
-
};
|
|
143
|
-
const t = buildChainTable(doc);
|
|
144
|
-
expect(asGrid(t)).toEqual([
|
|
145
|
-
['F1', 'G1', 'C1', 'A1'],
|
|
146
|
-
['∅', '∅', '∅', 'Aorphan'],
|
|
147
|
-
]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('surfaces a change whose goal is missing as an empty Factor+Goal row', () => {
|
|
151
|
-
const doc: FGCAPreviewDoc = {
|
|
152
|
-
factors: [],
|
|
153
|
-
goals: [],
|
|
154
|
-
changes: [{ id: 20, name: 'Cx', goal_id: 999, activity_ids: [30] }],
|
|
155
|
-
activities: [{ id: 30, name: 'A1' }],
|
|
156
|
-
};
|
|
157
|
-
const t = buildChainTable(doc);
|
|
158
|
-
expect(asGrid(t)).toEqual([['∅', '∅', 'Cx', 'A1']]);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('buildChainTable — name fallback', () => {
|
|
163
|
-
it('falls back to the id when an element has an empty name', () => {
|
|
164
|
-
const doc: FGCAPreviewDoc = {
|
|
165
|
-
factors: [{ id: 7, name: '' }],
|
|
166
|
-
goals: [],
|
|
167
|
-
activities: [],
|
|
168
|
-
};
|
|
169
|
-
const t = buildChainTable(doc);
|
|
170
|
-
expect(t.rows[0][0]?.cell?.label).toBe('7');
|
|
171
|
-
});
|
|
172
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildChainTable, type ChainTable, type ChainColumn } from '../chain-table.js';
|
|
3
|
+
import type { FGCAPreviewDoc } from '../preview-layout.js';
|
|
4
|
+
|
|
5
|
+
// Renders a ChainTable to a compact text grid for readable assertions. Each
|
|
6
|
+
// cell shows "label×rowSpan" (×1 omitted); a position covered by a span above
|
|
7
|
+
// shows "·".
|
|
8
|
+
function asGrid(t: ChainTable): string[][] {
|
|
9
|
+
return t.rows.map(row =>
|
|
10
|
+
row.map(c => {
|
|
11
|
+
if (c === null) return '·';
|
|
12
|
+
const label = c.cell ? c.cell.label : '∅';
|
|
13
|
+
return c.rowSpan > 1 ? `${label}×${c.rowSpan}` : label;
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('buildChainTable — worked example (FGCA)', () => {
|
|
19
|
+
// F1 → G1, G2 ; G1 → C1 → A1 ; G2 → C2 → A2, A3
|
|
20
|
+
const doc: FGCAPreviewDoc = {
|
|
21
|
+
factors: [{ id: 1, name: 'F1' }],
|
|
22
|
+
goals: [
|
|
23
|
+
{ id: 10, name: 'G1', factor: [{ id: 1 }] },
|
|
24
|
+
{ id: 11, name: 'G2', factor: [{ id: 1 }] },
|
|
25
|
+
],
|
|
26
|
+
changes: [
|
|
27
|
+
{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] },
|
|
28
|
+
{ id: 21, name: 'C2', goal_id: 11, activity_ids: [31, 32] },
|
|
29
|
+
],
|
|
30
|
+
activities: [
|
|
31
|
+
{ id: 30, name: 'A1', goal_id: 10 },
|
|
32
|
+
{ id: 31, name: 'A2', goal_id: 11 },
|
|
33
|
+
{ id: 32, name: 'A3', goal_id: 11 },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
it('produces the spec grid with F1 spanning 3 and G2/C2 spanning 2', () => {
|
|
38
|
+
const t = buildChainTable(doc);
|
|
39
|
+
expect(t.columns).toEqual<ChainColumn[]>(['driver', 'goal', 'change', 'activity']);
|
|
40
|
+
expect(asGrid(t)).toEqual([
|
|
41
|
+
['F1×3', 'G1', 'C1', 'A1'],
|
|
42
|
+
['·', 'G2×2', 'C2×2', 'A2'],
|
|
43
|
+
['·', '·', '·', 'A3'],
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('orders the merged cells with correct rowSpans on the model', () => {
|
|
48
|
+
const t = buildChainTable(doc);
|
|
49
|
+
// Factor cell spans all three rows.
|
|
50
|
+
expect(t.rows[0][0]).toMatchObject({ rowSpan: 3, cell: { label: 'F1' } });
|
|
51
|
+
// Covered positions are null (no <td> emitted).
|
|
52
|
+
expect(t.rows[1][0]).toBeNull();
|
|
53
|
+
expect(t.rows[2][1]).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('buildChainTable — FGA (no Change column)', () => {
|
|
58
|
+
// FGA links Goal → Activity directly via activity.goal_id; no changes.
|
|
59
|
+
const doc: FGCAPreviewDoc = {
|
|
60
|
+
factors: [{ id: 1, name: 'F1' }],
|
|
61
|
+
goals: [{ id: 10, name: 'G1', factor: [{ id: 1 }] }],
|
|
62
|
+
activities: [
|
|
63
|
+
{ id: 30, name: 'A1', goal_id: 10 },
|
|
64
|
+
{ id: 31, name: 'A2', goal_id: 10 },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
it('emits three columns and merges Factor/Goal across the two activities', () => {
|
|
69
|
+
const t = buildChainTable(doc, { hideChanges: true });
|
|
70
|
+
expect(t.columns).toEqual<ChainColumn[]>(['driver', 'goal', 'activity']);
|
|
71
|
+
expect(asGrid(t)).toEqual([
|
|
72
|
+
['F1×2', 'G1×2', 'A1'],
|
|
73
|
+
['·', '·', 'A2'],
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('buildChainTable — missing downstream links (gaps stay visible)', () => {
|
|
79
|
+
it('shows a factor with no goal, a goal with no change, and a change with no activity', () => {
|
|
80
|
+
const doc: FGCAPreviewDoc = {
|
|
81
|
+
factors: [
|
|
82
|
+
{ id: 1, name: 'F1' },
|
|
83
|
+
{ id: 2, name: 'F2' }, // no goals
|
|
84
|
+
],
|
|
85
|
+
goals: [
|
|
86
|
+
{ id: 10, name: 'G1', factor: [{ id: 1 }] }, // no change, no activity
|
|
87
|
+
{ id: 11, name: 'G2', factor: [{ id: 1 }] }, // has a change with no activity
|
|
88
|
+
],
|
|
89
|
+
changes: [{ id: 20, name: 'C1', goal_id: 11, activity_ids: [] }],
|
|
90
|
+
activities: [],
|
|
91
|
+
};
|
|
92
|
+
const t = buildChainTable(doc);
|
|
93
|
+
expect(asGrid(t)).toEqual([
|
|
94
|
+
['F1×2', 'G1', '∅', '∅'],
|
|
95
|
+
['·', 'G2', 'C1', '∅'],
|
|
96
|
+
['F2', '∅', '∅', '∅'],
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders a change-less direct goal→activity path with an empty Change cell', () => {
|
|
101
|
+
const doc: FGCAPreviewDoc = {
|
|
102
|
+
factors: [{ id: 1, name: 'F1' }],
|
|
103
|
+
goals: [{ id: 10, name: 'G1', factor: [{ id: 1 }] }],
|
|
104
|
+
// activity bound straight to the goal, not covered by any change
|
|
105
|
+
changes: [],
|
|
106
|
+
activities: [{ id: 30, name: 'A1', goal_id: 10 }],
|
|
107
|
+
};
|
|
108
|
+
const t = buildChainTable(doc);
|
|
109
|
+
expect(asGrid(t)).toEqual([['F1', 'G1', '∅', 'A1']]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('buildChainTable — orphans (broken refs are warnings, not errors)', () => {
|
|
114
|
+
it('places a goal with a missing factor in a trailing empty-Factor row', () => {
|
|
115
|
+
const doc: FGCAPreviewDoc = {
|
|
116
|
+
factors: [{ id: 1, name: 'F1' }],
|
|
117
|
+
goals: [
|
|
118
|
+
{ id: 10, name: 'G1', factor: [{ id: 1 }] },
|
|
119
|
+
{ id: 11, name: 'Gx', factor: [{ id: 99 }] }, // factor 99 does not exist
|
|
120
|
+
{ id: 12, name: 'Gy' }, // no factor at all
|
|
121
|
+
],
|
|
122
|
+
changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
|
|
123
|
+
activities: [{ id: 30, name: 'A1', goal_id: 10 }],
|
|
124
|
+
};
|
|
125
|
+
const t = buildChainTable(doc);
|
|
126
|
+
expect(asGrid(t)).toEqual([
|
|
127
|
+
['F1', 'G1', 'C1', 'A1'],
|
|
128
|
+
['∅×2', 'Gx', '∅', '∅'],
|
|
129
|
+
['·', 'Gy', '∅', '∅'],
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('surfaces an unlinked activity as a trailing row with empty cells to its left', () => {
|
|
134
|
+
const doc: FGCAPreviewDoc = {
|
|
135
|
+
factors: [{ id: 1, name: 'F1' }],
|
|
136
|
+
goals: [{ id: 10, name: 'G1', factor: [{ id: 1 }] }],
|
|
137
|
+
changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
|
|
138
|
+
activities: [
|
|
139
|
+
{ id: 30, name: 'A1', goal_id: 10 },
|
|
140
|
+
{ id: 99, name: 'Aorphan' }, // no goal, no change
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
const t = buildChainTable(doc);
|
|
144
|
+
expect(asGrid(t)).toEqual([
|
|
145
|
+
['F1', 'G1', 'C1', 'A1'],
|
|
146
|
+
['∅', '∅', '∅', 'Aorphan'],
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('surfaces a change whose goal is missing as an empty Factor+Goal row', () => {
|
|
151
|
+
const doc: FGCAPreviewDoc = {
|
|
152
|
+
factors: [],
|
|
153
|
+
goals: [],
|
|
154
|
+
changes: [{ id: 20, name: 'Cx', goal_id: 999, activity_ids: [30] }],
|
|
155
|
+
activities: [{ id: 30, name: 'A1' }],
|
|
156
|
+
};
|
|
157
|
+
const t = buildChainTable(doc);
|
|
158
|
+
expect(asGrid(t)).toEqual([['∅', '∅', 'Cx', 'A1']]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('buildChainTable — name fallback', () => {
|
|
163
|
+
it('falls back to the id when an element has an empty name', () => {
|
|
164
|
+
const doc: FGCAPreviewDoc = {
|
|
165
|
+
factors: [{ id: 7, name: '' }],
|
|
166
|
+
goals: [],
|
|
167
|
+
activities: [],
|
|
168
|
+
};
|
|
169
|
+
const t = buildChainTable(doc);
|
|
170
|
+
expect(t.rows[0][0]?.cell?.label).toBe('7');
|
|
171
|
+
});
|
|
172
|
+
});
|