@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
|
@@ -1,158 +1,162 @@
|
|
|
1
|
-
// Activity Card — document-local structural validation.
|
|
2
|
-
//
|
|
3
|
-
// This validator checks only what can be known from the card document ALONE:
|
|
4
|
-
// the header, the `activity_card` block shape, the project-reference grammar
|
|
5
|
-
// (PC-001's "missing or malformed" half), and each milestone's shape.
|
|
6
|
-
//
|
|
7
|
-
// The cross-document rules — PC-001 "does not resolve", PC-002
|
|
8
|
-
// (activity_type), PC-003 (milestone changes ⊆ project changes), PC-004
|
|
9
|
-
// (milestone date within the project lifecycle window) and the LIFECYCLE-*
|
|
10
|
-
// checks — require the sibling `*.activities.*` / `*.fgca.*` documents and so
|
|
11
|
-
// live in `resolver.ts`. The preview merges both result sets.
|
|
12
|
-
//
|
|
13
|
-
// Codes:
|
|
14
|
-
// AC-001 document structure (root object, `activity_card` block)
|
|
15
|
-
// AC-002 `activity_card.id` grammar
|
|
16
|
-
// AC-003 milestone structure (missing id / name / date)
|
|
17
|
-
// AC-004 milestone id grammar / duplicate
|
|
18
|
-
// AC-005 milestone date format
|
|
19
|
-
// AC-006 milestone `delivers_changes` shape / grammar
|
|
20
|
-
// HDR-001 missing `notation` (CONTRACT.md §2)
|
|
21
|
-
// HDR-002 wrong `notation` (CONTRACT.md §2)
|
|
22
|
-
// PC-001 `project` missing or malformed (the document-local half; the
|
|
23
|
-
// "does not resolve" half is emitted by the resolver)
|
|
24
|
-
|
|
25
|
-
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
26
|
-
|
|
27
|
-
export type { ValidationError, ValidationWarning, ValidationResult };
|
|
28
|
-
|
|
29
|
-
const ACTIVITY_CARD_ID_RE = /^ACTIVITY_CARD(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
30
|
-
const ACTIVITY_ID_RE = /^ACTIVITY(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
31
|
-
const MILESTONE_ID_RE = /^MILESTONE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
32
|
-
const CHANGE_ID_RE = /^CHANGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
33
|
-
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
34
|
-
|
|
35
|
-
function isNonEmptyString(v: unknown): v is string {
|
|
36
|
-
return typeof v === 'string' && v.trim().length > 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function validateActivityCard(input: unknown): ValidationResult {
|
|
40
|
-
const errors: ValidationError[] = [];
|
|
41
|
-
const warnings: ValidationWarning[] = [];
|
|
42
|
-
|
|
43
|
-
if (!input || typeof input !== 'object') {
|
|
44
|
-
return { valid: false, errors: [{ code: 'AC-001', message: 'document root is not an object' }], warnings };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const raw = input as Record<string, unknown>;
|
|
48
|
-
|
|
49
|
-
// ── Header (CONTRACT.md §2) ────────────────────────────────────────────────
|
|
50
|
-
if (!('notation' in raw)) {
|
|
51
|
-
errors.push({ code: 'HDR-001', message: 'notation field is required' });
|
|
52
|
-
} else if (raw['notation'] !== 'activity-card') {
|
|
53
|
-
errors.push({
|
|
54
|
-
code: 'HDR-002',
|
|
55
|
-
message: `notation must be "activity-card", got "${String(raw['notation'])}"`,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── activity_card block ─────────────────────────────────────────────────────
|
|
60
|
-
const block = raw['activity_card'];
|
|
61
|
-
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
62
|
-
errors.push({ code: 'AC-001', message: 'Missing required root key: activity_card (object)' });
|
|
63
|
-
return { valid: false, errors, warnings };
|
|
64
|
-
}
|
|
65
|
-
const card = block as Record<string, unknown>;
|
|
66
|
-
|
|
67
|
-
if (!isNonEmptyString(card['id'])) {
|
|
68
|
-
errors.push({ code: 'AC-002', message: 'activity_card.id is required' });
|
|
69
|
-
} else if (!ACTIVITY_CARD_ID_RE.test(card['id'])) {
|
|
70
|
-
errors.push({
|
|
71
|
-
code: 'AC-002',
|
|
72
|
-
message: `activity_card.id "${card['id']}" must match ACTIVITY_CARD-[<middle>-]<INTEGER>`,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// PC-001 (document-local half): project present + well-formed.
|
|
77
|
-
if (!isNonEmptyString(card['project'])) {
|
|
78
|
-
errors.push({ code: 'PC-001', message: 'activity_card.project is required (an ACTIVITY-… id)' });
|
|
79
|
-
} else if (!ACTIVITY_ID_RE.test(card['project'])) {
|
|
80
|
-
errors.push({
|
|
81
|
-
code: 'PC-001',
|
|
82
|
-
message: `activity_card.project "${card['project']}" is malformed; must match ACTIVITY-[<middle>-]<INTEGER>`,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (card['description'] !== undefined && typeof card['description'] !== 'string') {
|
|
87
|
-
errors.push({ code: 'AC-001', message: 'activity_card.description must be a string' });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!isNonEmptyString(m['
|
|
124
|
-
errors.push({ code: 'AC-003', message: `${p}.
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
1
|
+
// Activity Card — document-local structural validation.
|
|
2
|
+
//
|
|
3
|
+
// This validator checks only what can be known from the card document ALONE:
|
|
4
|
+
// the header, the `activity_card` block shape, the project-reference grammar
|
|
5
|
+
// (PC-001's "missing or malformed" half), and each milestone's shape.
|
|
6
|
+
//
|
|
7
|
+
// The cross-document rules — PC-001 "does not resolve", PC-002
|
|
8
|
+
// (activity_type), PC-003 (milestone changes ⊆ project changes), PC-004
|
|
9
|
+
// (milestone date within the project lifecycle window) and the LIFECYCLE-*
|
|
10
|
+
// checks — require the sibling `*.activities.*` / `*.fgca.*` documents and so
|
|
11
|
+
// live in `resolver.ts`. The preview merges both result sets.
|
|
12
|
+
//
|
|
13
|
+
// Codes:
|
|
14
|
+
// AC-001 document structure (root object, `activity_card` block)
|
|
15
|
+
// AC-002 `activity_card.id` grammar
|
|
16
|
+
// AC-003 milestone structure (missing id / name / date)
|
|
17
|
+
// AC-004 milestone id grammar / duplicate
|
|
18
|
+
// AC-005 milestone date format
|
|
19
|
+
// AC-006 milestone `delivers_changes` shape / grammar
|
|
20
|
+
// HDR-001 missing `notation` (CONTRACT.md §2)
|
|
21
|
+
// HDR-002 wrong `notation` (CONTRACT.md §2)
|
|
22
|
+
// PC-001 `project` missing or malformed (the document-local half; the
|
|
23
|
+
// "does not resolve" half is emitted by the resolver)
|
|
24
|
+
|
|
25
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
26
|
+
|
|
27
|
+
export type { ValidationError, ValidationWarning, ValidationResult };
|
|
28
|
+
|
|
29
|
+
const ACTIVITY_CARD_ID_RE = /^ACTIVITY_CARD(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
30
|
+
const ACTIVITY_ID_RE = /^ACTIVITY(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
31
|
+
const MILESTONE_ID_RE = /^MILESTONE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
32
|
+
const CHANGE_ID_RE = /^CHANGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
33
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
34
|
+
|
|
35
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
36
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateActivityCard(input: unknown): ValidationResult {
|
|
40
|
+
const errors: ValidationError[] = [];
|
|
41
|
+
const warnings: ValidationWarning[] = [];
|
|
42
|
+
|
|
43
|
+
if (!input || typeof input !== 'object') {
|
|
44
|
+
return { valid: false, errors: [{ code: 'AC-001', message: 'document root is not an object' }], warnings };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const raw = input as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
// ── Header (CONTRACT.md §2) ────────────────────────────────────────────────
|
|
50
|
+
if (!('notation' in raw)) {
|
|
51
|
+
errors.push({ code: 'HDR-001', message: 'notation field is required' });
|
|
52
|
+
} else if (raw['notation'] !== 'activity-card') {
|
|
53
|
+
errors.push({
|
|
54
|
+
code: 'HDR-002',
|
|
55
|
+
message: `notation must be "activity-card", got "${String(raw['notation'])}"`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── activity_card block ─────────────────────────────────────────────────────
|
|
60
|
+
const block = raw['activity_card'];
|
|
61
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
62
|
+
errors.push({ code: 'AC-001', message: 'Missing required root key: activity_card (object)' });
|
|
63
|
+
return { valid: false, errors, warnings };
|
|
64
|
+
}
|
|
65
|
+
const card = block as Record<string, unknown>;
|
|
66
|
+
|
|
67
|
+
if (!isNonEmptyString(card['id'])) {
|
|
68
|
+
errors.push({ code: 'AC-002', message: 'activity_card.id is required' });
|
|
69
|
+
} else if (!ACTIVITY_CARD_ID_RE.test(card['id'])) {
|
|
70
|
+
errors.push({
|
|
71
|
+
code: 'AC-002',
|
|
72
|
+
message: `activity_card.id "${card['id']}" must match ACTIVITY_CARD-[<middle>-]<INTEGER>`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// PC-001 (document-local half): project present + well-formed.
|
|
77
|
+
if (!isNonEmptyString(card['project'])) {
|
|
78
|
+
errors.push({ code: 'PC-001', message: 'activity_card.project is required (an ACTIVITY-… id)' });
|
|
79
|
+
} else if (!ACTIVITY_ID_RE.test(card['project'])) {
|
|
80
|
+
errors.push({
|
|
81
|
+
code: 'PC-001',
|
|
82
|
+
message: `activity_card.project "${card['project']}" is malformed; must match ACTIVITY-[<middle>-]<INTEGER>`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (card['description'] !== undefined && typeof card['description'] !== 'string') {
|
|
87
|
+
errors.push({ code: 'AC-001', message: 'activity_card.description must be a string' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (card['notes'] !== undefined && typeof card['notes'] !== 'string') {
|
|
91
|
+
errors.push({ code: 'AC-001', message: 'activity_card.notes must be a string' });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── milestones[] (optional) ─────────────────────────────────────────────────
|
|
95
|
+
const milestonesRaw = card['milestones'];
|
|
96
|
+
if (milestonesRaw !== undefined) {
|
|
97
|
+
if (!Array.isArray(milestonesRaw)) {
|
|
98
|
+
errors.push({ code: 'AC-003', message: 'activity_card.milestones must be an array', path: 'milestones' });
|
|
99
|
+
} else {
|
|
100
|
+
const seenIds = new Set<string>();
|
|
101
|
+
milestonesRaw.forEach((el, i) => {
|
|
102
|
+
const p = `milestones[${i}]`;
|
|
103
|
+
if (!el || typeof el !== 'object' || Array.isArray(el)) {
|
|
104
|
+
errors.push({ code: 'AC-003', message: `${p} must be an object`, path: p });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const m = el as Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
if (!isNonEmptyString(m['id'])) {
|
|
110
|
+
errors.push({ code: 'AC-003', message: `${p}.id is required`, path: p });
|
|
111
|
+
} else {
|
|
112
|
+
const mid = m['id'].trim();
|
|
113
|
+
if (!MILESTONE_ID_RE.test(mid)) {
|
|
114
|
+
errors.push({ code: 'AC-004', message: `${p}.id "${mid}" must match MILESTONE-[<middle>-]<INTEGER>`, path: p });
|
|
115
|
+
}
|
|
116
|
+
if (seenIds.has(mid)) {
|
|
117
|
+
errors.push({ code: 'AC-004', message: `Duplicate milestone id "${mid}"`, path: p });
|
|
118
|
+
} else {
|
|
119
|
+
seenIds.add(mid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isNonEmptyString(m['name'])) {
|
|
124
|
+
errors.push({ code: 'AC-003', message: `${p}.name is required`, path: p });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!isNonEmptyString(m['date'])) {
|
|
128
|
+
errors.push({ code: 'AC-003', message: `${p}.date is required`, path: p });
|
|
129
|
+
} else if (!DATE_RE.test(m['date'])) {
|
|
130
|
+
errors.push({
|
|
131
|
+
code: 'AC-005',
|
|
132
|
+
message: `${p}.date "${m['date']}" must be a quoted ISO 8601 date (YYYY-MM-DD)`,
|
|
133
|
+
path: p,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (m['description'] !== undefined && typeof m['description'] !== 'string') {
|
|
138
|
+
errors.push({ code: 'AC-003', message: `${p}.description must be a string`, path: p });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const dc = m['delivers_changes'];
|
|
142
|
+
if (dc !== undefined) {
|
|
143
|
+
if (!Array.isArray(dc)) {
|
|
144
|
+
errors.push({ code: 'AC-006', message: `${p}.delivers_changes must be an array of CHANGE-… ids`, path: p });
|
|
145
|
+
} else {
|
|
146
|
+
dc.forEach((c) => {
|
|
147
|
+
if (typeof c !== 'string' || !CHANGE_ID_RE.test(c)) {
|
|
148
|
+
errors.push({
|
|
149
|
+
code: 'AC-006',
|
|
150
|
+
message: `${p}.delivers_changes[] entry "${String(c)}" must match CHANGE-[<middle>-]<INTEGER>`,
|
|
151
|
+
path: p,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
162
|
+
}
|
|
@@ -4,64 +4,103 @@ import { validateActor } from '../validate.js';
|
|
|
4
4
|
function valid(): Record<string, unknown> {
|
|
5
5
|
return {
|
|
6
6
|
notation: 'actor',
|
|
7
|
-
id: 'ACTOR-
|
|
8
|
-
name: '
|
|
9
|
-
type: '
|
|
10
|
-
description: '
|
|
11
|
-
contact: '
|
|
7
|
+
id: 'ACTOR-OPS-1',
|
|
8
|
+
name: 'Operations',
|
|
9
|
+
type: 'business_unit',
|
|
10
|
+
description: 'The operations organisation.',
|
|
11
|
+
contact: 'ops@acme.example',
|
|
12
12
|
zone: 'canon',
|
|
13
13
|
admitted_at: '2026-05-29',
|
|
14
14
|
admitted_by: 'v.korobeinikov',
|
|
15
15
|
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
16
|
-
valid_from: '
|
|
16
|
+
valid_from: '2024-01-01',
|
|
17
17
|
valid_to: null,
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const codes = (input: unknown): string[] =>
|
|
21
|
+
const codes = (input: unknown): string[] =>
|
|
22
|
+
validateActor(input).errors.map(e => e.code);
|
|
22
23
|
|
|
23
24
|
describe('validateActor — positive', () => {
|
|
24
|
-
it('accepts
|
|
25
|
+
it('accepts a well-formed business_unit actor', () => {
|
|
25
26
|
const r = validateActor(valid());
|
|
26
27
|
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
27
|
-
expect(r.errors).toHaveLength(0);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it('accepts
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
it('accepts a person actor with no optional fields', () => {
|
|
31
|
+
const a = valid();
|
|
32
|
+
a.id = 'ACTOR-JANE-DOE-1';
|
|
33
|
+
a.name = 'Jane Doe';
|
|
34
|
+
a.type = 'person';
|
|
35
|
+
delete a.description;
|
|
36
|
+
delete a.contact;
|
|
37
|
+
expect(validateActor(a).valid).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('accepts a system actor with external_ref', () => {
|
|
41
|
+
const a = valid();
|
|
42
|
+
a.id = 'ACTOR-ORDER-MGMT-SYS-1';
|
|
43
|
+
a.name = 'Order Management System';
|
|
44
|
+
a.type = 'system';
|
|
45
|
+
a.external_ref = 'https://oms.acme.example/api';
|
|
46
|
+
expect(validateActor(a).valid).toBe(true);
|
|
34
47
|
});
|
|
35
48
|
});
|
|
36
49
|
|
|
37
|
-
describe('validateActor — ACTOR-001 (
|
|
38
|
-
it('flags a
|
|
39
|
-
|
|
50
|
+
describe('validateActor — ACTOR-001 (shape / id grammar)', () => {
|
|
51
|
+
it('flags a missing required envelope field', () => {
|
|
52
|
+
const a = valid();
|
|
53
|
+
delete a.name;
|
|
54
|
+
expect(codes(a)).toContain('ACTOR-001');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('flags an id that violates the grammar', () => {
|
|
58
|
+
expect(codes({ ...valid(), id: 'ACTOR-OPS-001' })).toContain('ACTOR-001');
|
|
59
|
+
expect(codes({ ...valid(), id: 'A-1' })).toContain('ACTOR-001');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('flags a wrong notation tag and non-canon zone', () => {
|
|
63
|
+
expect(codes({ ...valid(), notation: 'business_actor' })).toContain('ACTOR-001');
|
|
64
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('ACTOR-001');
|
|
40
65
|
});
|
|
41
66
|
|
|
42
|
-
it('flags a
|
|
43
|
-
|
|
67
|
+
it('flags a missing type', () => {
|
|
68
|
+
const a = valid();
|
|
69
|
+
delete a.type;
|
|
70
|
+
expect(codes(a)).toContain('ACTOR-001');
|
|
44
71
|
});
|
|
45
72
|
|
|
46
|
-
it('flags
|
|
47
|
-
|
|
73
|
+
it('flags a missing valid_to key', () => {
|
|
74
|
+
const a = valid();
|
|
75
|
+
delete a.valid_to;
|
|
76
|
+
expect(codes(a)).toContain('ACTOR-001');
|
|
48
77
|
});
|
|
49
78
|
|
|
50
|
-
it('
|
|
51
|
-
|
|
52
|
-
delete r.name;
|
|
53
|
-
expect(codes(r)).toContain('ACTOR-001');
|
|
79
|
+
it('rejects a non-object', () => {
|
|
80
|
+
expect(codes(null)).toEqual(['ACTOR-001']);
|
|
54
81
|
});
|
|
55
82
|
});
|
|
56
83
|
|
|
57
|
-
describe('validateActor — ACTOR-002 (
|
|
58
|
-
it('flags a
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
describe('validateActor — ACTOR-002 (type enum)', () => {
|
|
85
|
+
it('flags a type outside {person, business_unit, system}', () => {
|
|
86
|
+
expect(codes({ ...valid(), type: 'organisation' })).toContain('ACTOR-002');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('validateActor — ACTOR-003 (engagement fields forbidden inline)', () => {
|
|
91
|
+
it('flags inline employment', () => {
|
|
92
|
+
expect(codes({ ...valid(), employment: 'REL-EMP-1' })).toContain('ACTOR-003');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('flags inline unit_parent', () => {
|
|
96
|
+
expect(codes({ ...valid(), unit_parent: 'ACTOR-PARENT-1' })).toContain('ACTOR-003');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('flags inline roles', () => {
|
|
100
|
+
expect(codes({ ...valid(), roles: ['ROLE-OPS-1'] })).toContain('ACTOR-003');
|
|
62
101
|
});
|
|
63
102
|
|
|
64
|
-
it('flags
|
|
65
|
-
expect(codes({ ...valid(),
|
|
103
|
+
it('flags inline owner', () => {
|
|
104
|
+
expect(codes({ ...valid(), owner: 'ACTOR-OPS-1' })).toContain('ACTOR-003');
|
|
66
105
|
});
|
|
67
106
|
});
|
package/src/actor/index.ts
CHANGED
package/src/actor/types.ts
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
|
-
// ACTOR —
|
|
2
|
-
// Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.10
|
|
3
|
-
// notations/elements/19-actors.md + envelope §3.
|
|
1
|
+
// ACTOR — active-structure identity primitive (ArchiMate Business Actor).
|
|
2
|
+
// Schema: methodology notations/elements/19-actors.md + ELEMENT_PRIMITIVES.md §7.10.
|
|
4
3
|
|
|
5
4
|
import type { GateChecks } from '../requirement/types.js';
|
|
6
5
|
|
|
7
|
-
/** Identity discriminator — what kind of actor (person, business_unit, system). */
|
|
8
|
-
export type ActorType = 'person' | 'business_unit' | 'system';
|
|
9
|
-
|
|
10
6
|
export const ACTOR_TYPES = ['person', 'business_unit', 'system'] as const;
|
|
7
|
+
export type ActorType = typeof ACTOR_TYPES[number];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fields that record engagement / org-hierarchy and therefore MUST NOT appear
|
|
11
|
+
* inline on an ACTOR file (§7.10, ACTOR-003). They belong in time-aware REL
|
|
12
|
+
* records, never on the identity primitive.
|
|
13
|
+
*/
|
|
14
|
+
export const ACTOR_FORBIDDEN_INLINE_FIELDS = [
|
|
15
|
+
'employment',
|
|
16
|
+
'candidacy',
|
|
17
|
+
'alumni_membership',
|
|
18
|
+
'community_membership',
|
|
19
|
+
'contracting',
|
|
20
|
+
'unit_parent',
|
|
21
|
+
'roles',
|
|
22
|
+
'owner',
|
|
23
|
+
] as const;
|
|
11
24
|
|
|
12
25
|
export interface Actor {
|
|
13
26
|
notation: 'actor';
|
|
14
27
|
id: string;
|
|
15
28
|
name: string;
|
|
16
|
-
/** Required identity discriminator. */
|
|
17
29
|
type: ActorType;
|
|
18
30
|
description?: string;
|
|
19
31
|
contact?: string;
|
|
20
32
|
external_ref?: string;
|
|
21
33
|
|
|
34
|
+
// Admission record (CONTRACT.md §6).
|
|
22
35
|
zone: 'canon';
|
|
23
36
|
admitted_at: string;
|
|
24
37
|
admitted_by: string;
|
|
25
38
|
gate_checks: GateChecks;
|
|
26
39
|
|
|
40
|
+
// Primitive lifecycle (CONTRACT.md §7).
|
|
27
41
|
valid_from: string;
|
|
28
42
|
valid_to: string | null;
|
|
29
43
|
}
|
package/src/actor/validate.ts
CHANGED
|
@@ -1,37 +1,69 @@
|
|
|
1
|
-
// ACTOR validator — methodology notations/
|
|
2
|
-
// notations/elements/19-actors.md.
|
|
1
|
+
// ACTOR validator — methodology notations/elements/19-actors.md §5.
|
|
3
2
|
//
|
|
4
|
-
// Implements ACTOR-001..
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Implements ACTOR-001..003. The shared HDR-/LIFECYCLE- rules are
|
|
4
|
+
// cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
5
|
+
// single-artefact validator.
|
|
7
6
|
|
|
8
|
-
import type { ValidationError,
|
|
9
|
-
import {
|
|
10
|
-
import { ACTOR_TYPES } from './types.js';
|
|
7
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
8
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
9
|
+
import { ACTOR_TYPES, ACTOR_FORBIDDEN_INLINE_FIELDS } from './types.js';
|
|
10
|
+
|
|
11
|
+
const REQUIRED_STRING_FIELDS = [
|
|
12
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const TYPES: readonly string[] = ACTOR_TYPES;
|
|
11
16
|
|
|
12
17
|
export function validateActor(input: unknown): ValidationResult {
|
|
13
18
|
const errors: ValidationError[] = [];
|
|
14
19
|
const warnings: ValidationWarning[] = [];
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
errors.push({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
22
|
+
errors.push({ code: 'ACTOR-001', message: 'Actor must be a YAML mapping.' });
|
|
23
|
+
return { valid: false, errors, warnings };
|
|
24
|
+
}
|
|
25
|
+
const a = input as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
// ACTOR-001 — id grammar + notation tag + envelope.
|
|
28
|
+
if (!isCanonicalIdOfType(a.id, 'ACTOR')) {
|
|
29
|
+
errors.push({ code: 'ACTOR-001', message: `id "${String(a.id)}" must match ACTOR-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
30
|
+
}
|
|
31
|
+
if (a.notation !== 'actor') {
|
|
32
|
+
errors.push({ code: 'ACTOR-001', message: 'notation must be the fixed value "actor".', path: 'notation' });
|
|
33
|
+
}
|
|
34
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
35
|
+
errors.push({ code: 'ACTOR-001', message: 'zone must be "canon" for an ACTOR.', path: 'zone' });
|
|
36
|
+
} else if (a.zone === undefined) {
|
|
37
|
+
errors.push({ code: 'ACTOR-001', message: 'zone is required.', path: 'zone' });
|
|
38
|
+
}
|
|
39
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
40
|
+
const v = a[field];
|
|
41
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
42
|
+
errors.push({ code: 'ACTOR-001', message: `${field} is required.`, path: field });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
46
|
+
errors.push({ code: 'ACTOR-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
47
|
+
}
|
|
48
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
49
|
+
errors.push({ code: 'ACTOR-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
50
|
+
}
|
|
51
|
+
if (typeof a.type !== 'string' || a.type.trim() === '') {
|
|
52
|
+
errors.push({ code: 'ACTOR-001', message: 'type is required.', path: 'type' });
|
|
53
|
+
} else if (!TYPES.includes(a.type)) {
|
|
54
|
+
// ACTOR-002 — type enum.
|
|
55
|
+
errors.push({ code: 'ACTOR-002', message: `type "${a.type}" must be one of ${TYPES.join(', ')}.`, path: 'type' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ACTOR-003 — engagement / hierarchy fields belong on REL records, not here.
|
|
59
|
+
for (const field of ACTOR_FORBIDDEN_INLINE_FIELDS) {
|
|
60
|
+
if (field in a) {
|
|
61
|
+
errors.push({
|
|
62
|
+
code: 'ACTOR-003',
|
|
63
|
+
message: `${field} is engagement/hierarchy data — it must live in a REL-… record, not inline on the actor.`,
|
|
64
|
+
path: field,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
35
67
|
}
|
|
36
68
|
|
|
37
69
|
return { valid: errors.length === 0, errors, warnings };
|