@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/yaml-normalize.ts
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coerce native `Date` instances inside a parsed YAML document into ISO
|
|
3
|
-
* `YYYY-MM-DD` strings.
|
|
4
|
-
*
|
|
5
|
-
* Background: per the YAML 1.1 timestamp tag, a bare `2026-06-01` is parsed
|
|
6
|
-
* by `js-yaml` as a native `Date`, not a string. Every notation validator in
|
|
7
|
-
* this package expects ISO date strings (`typeof === 'string'` +
|
|
8
|
-
* `YYYY-MM-DD` regex), so a Date reaching validation would falsely fail
|
|
9
|
-
* shape checks even though the user followed the spec, minus quotes.
|
|
10
|
-
*
|
|
11
|
-
* The canonical, methodology-correct form is the quoted ISO string. This
|
|
12
|
-
* coercion is a backstop so the unquoted form does not silently bite.
|
|
13
|
-
*
|
|
14
|
-
* UTC-safe: uses `getUTC*` so the JS process timezone cannot drift the day.
|
|
15
|
-
* A bare `2026-06-01` parses to midnight UTC and round-trips to
|
|
16
|
-
* `"2026-06-01"` regardless of `process.env.TZ`.
|
|
17
|
-
*
|
|
18
|
-
* Walks in place and returns the same reference for convenience.
|
|
19
|
-
*/
|
|
20
|
-
export function coerceDatesToIsoStrings<T>(value: T): T {
|
|
21
|
-
if (value === null || value === undefined) return value;
|
|
22
|
-
if (value instanceof Date) {
|
|
23
|
-
return dateToIsoYmd(value) as unknown as T;
|
|
24
|
-
}
|
|
25
|
-
if (typeof value !== 'object') return value;
|
|
26
|
-
|
|
27
|
-
if (Array.isArray(value)) {
|
|
28
|
-
for (let i = 0; i < value.length; i++) {
|
|
29
|
-
const v = value[i];
|
|
30
|
-
if (v instanceof Date) {
|
|
31
|
-
value[i] = dateToIsoYmd(v) as unknown as typeof v;
|
|
32
|
-
} else if (v !== null && typeof v === 'object') {
|
|
33
|
-
coerceDatesToIsoStrings(v);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const obj = value as Record<string, unknown>;
|
|
40
|
-
for (const key of Object.keys(obj)) {
|
|
41
|
-
const v = obj[key];
|
|
42
|
-
if (v instanceof Date) {
|
|
43
|
-
obj[key] = dateToIsoYmd(v);
|
|
44
|
-
} else if (v !== null && typeof v === 'object') {
|
|
45
|
-
coerceDatesToIsoStrings(v);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function dateToIsoYmd(d: Date): string {
|
|
52
|
-
const y = d.getUTCFullYear();
|
|
53
|
-
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
54
|
-
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
55
|
-
return `${y}-${m}-${day}`;
|
|
56
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Coerce native `Date` instances inside a parsed YAML document into ISO
|
|
3
|
+
* `YYYY-MM-DD` strings.
|
|
4
|
+
*
|
|
5
|
+
* Background: per the YAML 1.1 timestamp tag, a bare `2026-06-01` is parsed
|
|
6
|
+
* by `js-yaml` as a native `Date`, not a string. Every notation validator in
|
|
7
|
+
* this package expects ISO date strings (`typeof === 'string'` +
|
|
8
|
+
* `YYYY-MM-DD` regex), so a Date reaching validation would falsely fail
|
|
9
|
+
* shape checks even though the user followed the spec, minus quotes.
|
|
10
|
+
*
|
|
11
|
+
* The canonical, methodology-correct form is the quoted ISO string. This
|
|
12
|
+
* coercion is a backstop so the unquoted form does not silently bite.
|
|
13
|
+
*
|
|
14
|
+
* UTC-safe: uses `getUTC*` so the JS process timezone cannot drift the day.
|
|
15
|
+
* A bare `2026-06-01` parses to midnight UTC and round-trips to
|
|
16
|
+
* `"2026-06-01"` regardless of `process.env.TZ`.
|
|
17
|
+
*
|
|
18
|
+
* Walks in place and returns the same reference for convenience.
|
|
19
|
+
*/
|
|
20
|
+
export function coerceDatesToIsoStrings<T>(value: T): T {
|
|
21
|
+
if (value === null || value === undefined) return value;
|
|
22
|
+
if (value instanceof Date) {
|
|
23
|
+
return dateToIsoYmd(value) as unknown as T;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== 'object') return value;
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (let i = 0; i < value.length; i++) {
|
|
29
|
+
const v = value[i];
|
|
30
|
+
if (v instanceof Date) {
|
|
31
|
+
value[i] = dateToIsoYmd(v) as unknown as typeof v;
|
|
32
|
+
} else if (v !== null && typeof v === 'object') {
|
|
33
|
+
coerceDatesToIsoStrings(v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const obj = value as Record<string, unknown>;
|
|
40
|
+
for (const key of Object.keys(obj)) {
|
|
41
|
+
const v = obj[key];
|
|
42
|
+
if (v instanceof Date) {
|
|
43
|
+
obj[key] = dateToIsoYmd(v);
|
|
44
|
+
} else if (v !== null && typeof v === 'object') {
|
|
45
|
+
coerceDatesToIsoStrings(v);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function dateToIsoYmd(d: Date): string {
|
|
52
|
+
const y = d.getUTCFullYear();
|
|
53
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
54
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
55
|
+
return `${y}-${m}-${day}`;
|
|
56
|
+
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ValidationError } from './validation-types.js';
|
|
2
|
-
export interface EnvelopeOptions {
|
|
3
|
-
/** Fixed `notation:` tag — lowercased TYPE short name (e.g. `factor`). */
|
|
4
|
-
notation: string;
|
|
5
|
-
/** Required `id:` TYPE prefix (e.g. `FACTOR`). */
|
|
6
|
-
idType: string;
|
|
7
|
-
/** Error code used for every envelope failure (e.g. `FACTOR-001`). */
|
|
8
|
-
code: string;
|
|
9
|
-
}
|
|
10
|
-
export interface EnvelopeResult {
|
|
11
|
-
/** The input narrowed to a record on success; `null` when the input is not a mapping. */
|
|
12
|
-
record: Record<string, unknown> | null;
|
|
13
|
-
errors: ValidationError[];
|
|
14
|
-
}
|
|
15
|
-
export declare function validateElementEnvelope(input: unknown, opts: EnvelopeOptions): EnvelopeResult;
|
package/dist/element-envelope.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// Shared §3 envelope validator for standalone element primitives.
|
|
2
|
-
//
|
|
3
|
-
// Every standalone element file in the methodology canon carries the same
|
|
4
|
-
// envelope: a fixed `notation` tag, a canonical `id` whose TYPE prefix matches
|
|
5
|
-
// the element TYPE, `name`, an admission record (`zone`/`admitted_at`/
|
|
6
|
-
// `admitted_by`/`gate_checks`), and a primitive lifecycle
|
|
7
|
-
// (`valid_from`/`valid_to`). Per-TYPE fields (§7) sit between.
|
|
8
|
-
//
|
|
9
|
-
// Each per-TYPE validator delegates envelope checks here so the §3 rule lives
|
|
10
|
-
// in one place; per-TYPE fields (subtype enums, cross-references) are checked
|
|
11
|
-
// in the TYPE's own validator. The error `code` is parameterised so an
|
|
12
|
-
// envelope failure surfaces under the TYPE's own code (e.g. `FACTOR-001`,
|
|
13
|
-
// `CHANGE-001`), preserving per-TYPE attribution.
|
|
14
|
-
import { isCanonicalIdOfType } from './typed-id.js';
|
|
15
|
-
export function validateElementEnvelope(input, opts) {
|
|
16
|
-
const errors = [];
|
|
17
|
-
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
18
|
-
errors.push({ code: opts.code, message: `${opts.idType} must be a YAML mapping.` });
|
|
19
|
-
return { record: null, errors };
|
|
20
|
-
}
|
|
21
|
-
const r = input;
|
|
22
|
-
if (r.notation !== opts.notation) {
|
|
23
|
-
errors.push({
|
|
24
|
-
code: opts.code,
|
|
25
|
-
message: `notation must be the fixed value "${opts.notation}".`,
|
|
26
|
-
path: 'notation',
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
if (!isCanonicalIdOfType(r.id, opts.idType)) {
|
|
30
|
-
errors.push({
|
|
31
|
-
code: opts.code,
|
|
32
|
-
message: `id "${String(r.id)}" must match ${opts.idType}-[<middle>-]<INTEGER>.`,
|
|
33
|
-
path: 'id',
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from']) {
|
|
37
|
-
if (typeof r[f] !== 'string' || r[f].trim() === '') {
|
|
38
|
-
errors.push({ code: opts.code, message: `${f} is required.`, path: f });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (r.zone !== 'canon') {
|
|
42
|
-
errors.push({ code: opts.code, message: 'zone is required and must be "canon".', path: 'zone' });
|
|
43
|
-
}
|
|
44
|
-
if (r.gate_checks === null || typeof r.gate_checks !== 'object' || Array.isArray(r.gate_checks)) {
|
|
45
|
-
errors.push({ code: opts.code, message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
46
|
-
}
|
|
47
|
-
if (!('valid_to' in r) || !(typeof r.valid_to === 'string' || r.valid_to === null)) {
|
|
48
|
-
errors.push({
|
|
49
|
-
code: opts.code,
|
|
50
|
-
message: 'valid_to is required (an ISO date string or null).',
|
|
51
|
-
path: 'valid_to',
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return { record: r, errors };
|
|
55
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { layoutIssues } from '../layout.js';
|
|
3
|
-
function build(issues) {
|
|
4
|
-
return {
|
|
5
|
-
notation: 'issues',
|
|
6
|
-
issues_catalogue: {
|
|
7
|
-
id: 'ISSUES-CAT-1',
|
|
8
|
-
name: 'Test',
|
|
9
|
-
updated_at: '2026-05-25',
|
|
10
|
-
issues,
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
describe('layoutIssues', () => {
|
|
15
|
-
it('lays out one row per issue', () => {
|
|
16
|
-
const layout = layoutIssues(build([
|
|
17
|
-
{ issue_id: 'ISSUE-1', name: 'A', status: 'open' },
|
|
18
|
-
{ issue_id: 'ISSUE-2', name: 'B', status: 'open' },
|
|
19
|
-
]));
|
|
20
|
-
expect(layout.rows).toHaveLength(2);
|
|
21
|
-
});
|
|
22
|
-
it('indents sub-issues by nesting depth', () => {
|
|
23
|
-
const layout = layoutIssues(build([
|
|
24
|
-
{ issue_id: 'ISSUE-1', name: 'Root', status: 'open' },
|
|
25
|
-
{ issue_id: 'ISSUE-2', name: 'Child', status: 'open', parent: 'ISSUE-1' },
|
|
26
|
-
{ issue_id: 'ISSUE-3', name: 'Grandchild', status: 'open', parent: 'ISSUE-2' },
|
|
27
|
-
]));
|
|
28
|
-
const byId = new Map(layout.rows.map((r) => [r.issue_id, r]));
|
|
29
|
-
expect(byId.get('ISSUE-1').depth).toBe(0);
|
|
30
|
-
expect(byId.get('ISSUE-2').depth).toBe(1);
|
|
31
|
-
expect(byId.get('ISSUE-3').depth).toBe(2);
|
|
32
|
-
expect(byId.get('ISSUE-2').x).toBeGreaterThan(byId.get('ISSUE-1').x);
|
|
33
|
-
});
|
|
34
|
-
it('renders pre-order: a child row follows its parent', () => {
|
|
35
|
-
const layout = layoutIssues(build([
|
|
36
|
-
{ issue_id: 'ISSUE-1', name: 'Root', status: 'open' },
|
|
37
|
-
{ issue_id: 'ISSUE-2', name: 'Child', status: 'open', parent: 'ISSUE-1' },
|
|
38
|
-
]));
|
|
39
|
-
expect(layout.rows[0].issue_id).toBe('ISSUE-1');
|
|
40
|
-
expect(layout.rows[1].issue_id).toBe('ISSUE-2');
|
|
41
|
-
expect(layout.rows[0].hasChildren).toBe(true);
|
|
42
|
-
expect(layout.rows[1].hasChildren).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
it('places a broken-parent issue at the root', () => {
|
|
45
|
-
const layout = layoutIssues(build([
|
|
46
|
-
{ issue_id: 'ISSUE-1', name: 'Orphan', status: 'open', parent: 'ISSUE-99' },
|
|
47
|
-
]));
|
|
48
|
-
expect(layout.rows[0].depth).toBe(0);
|
|
49
|
-
});
|
|
50
|
-
it('does not stack-overflow or drop nodes on a parent cycle', () => {
|
|
51
|
-
const layout = layoutIssues(build([
|
|
52
|
-
{ issue_id: 'ISSUE-1', name: 'A', status: 'open', parent: 'ISSUE-2' },
|
|
53
|
-
{ issue_id: 'ISSUE-2', name: 'B', status: 'open', parent: 'ISSUE-1' },
|
|
54
|
-
]));
|
|
55
|
-
expect(layout.rows).toHaveLength(2);
|
|
56
|
-
});
|
|
57
|
-
it('returns empty bounds for an empty register', () => {
|
|
58
|
-
const layout = layoutIssues(build([]));
|
|
59
|
-
expect(layout.rows).toHaveLength(0);
|
|
60
|
-
expect(layout.bounds).toEqual({ x: 0, y: 0, width: 0, height: 0 });
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validateIssues } from '../validate.js';
|
|
3
|
-
const VALID = {
|
|
4
|
-
notation: 'issues',
|
|
5
|
-
issues_catalogue: {
|
|
6
|
-
id: 'ISSUES-CAT-1',
|
|
7
|
-
name: 'Test Register',
|
|
8
|
-
updated_at: '2026-05-25',
|
|
9
|
-
issues: [
|
|
10
|
-
{ issue_id: 'ISSUE-1', name: 'Root issue', status: 'open' },
|
|
11
|
-
{ issue_id: 'ISSUE-2', name: 'Sub issue', status: 'in_progress', parent: 'ISSUE-1' },
|
|
12
|
-
],
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
function withIssues(issues) {
|
|
16
|
-
return { ...VALID, issues_catalogue: { ...VALID.issues_catalogue, issues } };
|
|
17
|
-
}
|
|
18
|
-
describe('validateIssues', () => {
|
|
19
|
-
it('passes on valid input', () => {
|
|
20
|
-
const r = validateIssues(VALID);
|
|
21
|
-
expect(r.valid).toBe(true);
|
|
22
|
-
expect(r.errors).toHaveLength(0);
|
|
23
|
-
});
|
|
24
|
-
it('ISS-000: rejects a non-object', () => {
|
|
25
|
-
expect(validateIssues(null).valid).toBe(false);
|
|
26
|
-
expect(validateIssues('x').errors[0].code).toBe('ISS-000');
|
|
27
|
-
});
|
|
28
|
-
it('ISS-000: rejects a wrong notation header', () => {
|
|
29
|
-
const r = validateIssues({ ...VALID, notation: 'goals' });
|
|
30
|
-
expect(r.errors.some(e => e.code === 'ISS-000')).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
it('ISS-000: rejects a missing issues_catalogue', () => {
|
|
33
|
-
const r = validateIssues({ notation: 'issues' });
|
|
34
|
-
expect(r.errors.some(e => e.code === 'ISS-000')).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
it('ISS-001: detects a duplicate issue_id', () => {
|
|
37
|
-
const r = validateIssues(withIssues([
|
|
38
|
-
{ issue_id: 'ISSUE-1', name: 'A', status: 'open' },
|
|
39
|
-
{ issue_id: 'ISSUE-1', name: 'B', status: 'open' },
|
|
40
|
-
]));
|
|
41
|
-
expect(r.errors.some(e => e.code === 'ISS-001')).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
it('ISS-002: detects a status outside the vocabulary', () => {
|
|
44
|
-
const r = validateIssues(withIssues([{ issue_id: 'ISSUE-1', name: 'A', status: 'wip' }]));
|
|
45
|
-
expect(r.errors.some(e => e.code === 'ISS-002')).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
it('ISS-003: detects a missing or empty name', () => {
|
|
48
|
-
const r = validateIssues(withIssues([{ issue_id: 'ISSUE-1', name: '', status: 'open' }]));
|
|
49
|
-
expect(r.errors.some(e => e.code === 'ISS-003')).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
it('ISS-004: warns on a broken parent reference and stays valid', () => {
|
|
52
|
-
const r = validateIssues(withIssues([
|
|
53
|
-
{ issue_id: 'ISSUE-1', name: 'Orphan', status: 'open', parent: 'ISSUE-99' },
|
|
54
|
-
]));
|
|
55
|
-
expect(r.valid).toBe(true);
|
|
56
|
-
expect(r.warnings.some(w => w.code === 'ISS-004')).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
it('ISS-005: detects a parent cycle', () => {
|
|
59
|
-
const r = validateIssues(withIssues([
|
|
60
|
-
{ issue_id: 'ISSUE-1', name: 'A', status: 'open', parent: 'ISSUE-2' },
|
|
61
|
-
{ issue_id: 'ISSUE-2', name: 'B', status: 'open', parent: 'ISSUE-1' },
|
|
62
|
-
]));
|
|
63
|
-
expect(r.errors.some(e => e.code === 'ISS-005')).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
it('ISS-006: rejects a relates_to entry that is not an ACTIVITY-/GOAL- id', () => {
|
|
66
|
-
const r = validateIssues(withIssues([
|
|
67
|
-
{ issue_id: 'ISSUE-1', name: 'A', status: 'open', relates_to: ['PRODUCT-1'] },
|
|
68
|
-
]));
|
|
69
|
-
expect(r.errors.some(e => e.code === 'ISS-006')).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
it('accepts valid ACTIVITY-/GOAL- relates_to ids', () => {
|
|
72
|
-
const r = validateIssues(withIssues([
|
|
73
|
-
{ issue_id: 'ISSUE-1', name: 'A', status: 'open', relates_to: ['ACTIVITY-5', 'GOAL-CUST-1'] },
|
|
74
|
-
]));
|
|
75
|
-
expect(r.valid).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
it('tolerates a null element in issues[] without throwing', () => {
|
|
78
|
-
const r = validateIssues(withIssues([null]));
|
|
79
|
-
expect(r.valid).toBe(false);
|
|
80
|
-
expect(r.errors.some(e => e.code === 'ISS-003')).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
});
|
package/dist/issues/index.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export type { IssueStatus, Issue, IssuesCatalogue, IssuesFile, IssuesLayoutOptions, LaidOutIssue, IssuesLayoutBounds, IssuesLayout, } from './types.js';
|
|
2
|
-
export { validateIssues } from './validate.js';
|
|
3
|
-
export type { ValidationError as IssuesValidationError, ValidationWarning as IssuesValidationWarning, ValidationResult as IssuesValidationResult, } from './validate.js';
|
|
4
|
-
export { layoutIssues } from './layout.js';
|
package/dist/issues/index.js
DELETED
package/dist/issues/layout.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { IssuesFile, IssuesLayout, IssuesLayoutOptions } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Lays an Issues document out as a nested outline: one row per issue, in
|
|
4
|
-
* pre-order, with x indented by nesting depth and y stacked top-to-bottom.
|
|
5
|
-
*
|
|
6
|
-
* Robust to malformed input — a broken `parent` reference is treated as a
|
|
7
|
-
* root, and a `parent` cycle cannot stack-overflow or drop nodes (a
|
|
8
|
-
* `placed` guard ensures every issue is laid out exactly once).
|
|
9
|
-
*/
|
|
10
|
-
export declare function layoutIssues(file: IssuesFile, options?: IssuesLayoutOptions): IssuesLayout;
|
package/dist/issues/layout.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
const DEFAULTS = {
|
|
2
|
-
rowHeight: 44,
|
|
3
|
-
rowGap: 10,
|
|
4
|
-
indentWidth: 32,
|
|
5
|
-
nodeWidth: 460,
|
|
6
|
-
paddingX: 0,
|
|
7
|
-
paddingY: 0,
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Lays an Issues document out as a nested outline: one row per issue, in
|
|
11
|
-
* pre-order, with x indented by nesting depth and y stacked top-to-bottom.
|
|
12
|
-
*
|
|
13
|
-
* Robust to malformed input — a broken `parent` reference is treated as a
|
|
14
|
-
* root, and a `parent` cycle cannot stack-overflow or drop nodes (a
|
|
15
|
-
* `placed` guard ensures every issue is laid out exactly once).
|
|
16
|
-
*/
|
|
17
|
-
export function layoutIssues(file, options = {}) {
|
|
18
|
-
const o = { ...DEFAULTS, ...options };
|
|
19
|
-
const issues = file?.issues_catalogue?.issues ?? [];
|
|
20
|
-
const byId = new Map();
|
|
21
|
-
for (const it of issues) {
|
|
22
|
-
if (it && typeof it.issue_id === 'string')
|
|
23
|
-
byId.set(it.issue_id, it);
|
|
24
|
-
}
|
|
25
|
-
const childrenOf = new Map();
|
|
26
|
-
const roots = [];
|
|
27
|
-
for (const it of issues) {
|
|
28
|
-
if (!it || typeof it.issue_id !== 'string')
|
|
29
|
-
continue;
|
|
30
|
-
const parent = it.parent;
|
|
31
|
-
if (typeof parent === 'string' && byId.has(parent) && parent !== it.issue_id) {
|
|
32
|
-
const arr = childrenOf.get(parent) ?? [];
|
|
33
|
-
arr.push(it.issue_id);
|
|
34
|
-
childrenOf.set(parent, arr);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
roots.push(it.issue_id);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const rows = [];
|
|
41
|
-
const placed = new Set();
|
|
42
|
-
let y = o.paddingY;
|
|
43
|
-
function place(id, depth) {
|
|
44
|
-
if (placed.has(id))
|
|
45
|
-
return; // cycle / duplicate guard
|
|
46
|
-
const issue = byId.get(id);
|
|
47
|
-
if (!issue)
|
|
48
|
-
return;
|
|
49
|
-
placed.add(id);
|
|
50
|
-
const kids = childrenOf.get(id) ?? [];
|
|
51
|
-
rows.push({
|
|
52
|
-
issue_id: id,
|
|
53
|
-
depth,
|
|
54
|
-
x: o.paddingX + depth * o.indentWidth,
|
|
55
|
-
y,
|
|
56
|
-
width: o.nodeWidth,
|
|
57
|
-
height: o.rowHeight,
|
|
58
|
-
data: issue,
|
|
59
|
-
hasChildren: kids.length > 0,
|
|
60
|
-
});
|
|
61
|
-
y += o.rowHeight + o.rowGap;
|
|
62
|
-
for (const childId of kids)
|
|
63
|
-
place(childId, depth + 1);
|
|
64
|
-
}
|
|
65
|
-
for (const rootId of roots)
|
|
66
|
-
place(rootId, 0);
|
|
67
|
-
// Any issue not reached above (e.g. trapped inside a parent cycle) is
|
|
68
|
-
// still laid out, at the root level, so nothing silently disappears.
|
|
69
|
-
for (const it of issues) {
|
|
70
|
-
if (it && typeof it.issue_id === 'string' && !placed.has(it.issue_id)) {
|
|
71
|
-
place(it.issue_id, 0);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (rows.length === 0) {
|
|
75
|
-
return { rows: [], bounds: { x: 0, y: 0, width: 0, height: 0 } };
|
|
76
|
-
}
|
|
77
|
-
const minX = Math.min(...rows.map((r) => r.x));
|
|
78
|
-
const minY = Math.min(...rows.map((r) => r.y));
|
|
79
|
-
const maxX = Math.max(...rows.map((r) => r.x + r.width));
|
|
80
|
-
const maxY = Math.max(...rows.map((r) => r.y + r.height));
|
|
81
|
-
return { rows, bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY } };
|
|
82
|
-
}
|
package/dist/issues/types.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issues notation — a text-native register of issues (problems, defects,
|
|
3
|
-
* open questions) with parent/child nesting. See methodology spec
|
|
4
|
-
* `notations/12-issues.md`.
|
|
5
|
-
*/
|
|
6
|
-
export type IssueStatus = 'open' | 'in_progress' | 'blocked' | 'resolved' | 'closed';
|
|
7
|
-
export interface Issue {
|
|
8
|
-
issue_id: string;
|
|
9
|
-
name: string;
|
|
10
|
-
status: IssueStatus;
|
|
11
|
-
/** issue_id of the parent issue — the nesting mechanism. */
|
|
12
|
-
parent?: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
/** Typed IDs of ACTIVITY / GOAL elements this issue concerns. */
|
|
15
|
-
relates_to?: string[];
|
|
16
|
-
/** ROLE element ID of the role accountable for the issue. */
|
|
17
|
-
owner_role?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface IssuesCatalogue {
|
|
20
|
-
id: string;
|
|
21
|
-
name: string;
|
|
22
|
-
description?: string;
|
|
23
|
-
version?: string;
|
|
24
|
-
updated_at: string;
|
|
25
|
-
issues: Issue[];
|
|
26
|
-
}
|
|
27
|
-
export interface IssuesFile {
|
|
28
|
-
notation: string;
|
|
29
|
-
spec_version?: string;
|
|
30
|
-
issues_catalogue: IssuesCatalogue;
|
|
31
|
-
}
|
|
32
|
-
export interface IssuesLayoutOptions {
|
|
33
|
-
rowHeight?: number;
|
|
34
|
-
rowGap?: number;
|
|
35
|
-
indentWidth?: number;
|
|
36
|
-
nodeWidth?: number;
|
|
37
|
-
paddingX?: number;
|
|
38
|
-
paddingY?: number;
|
|
39
|
-
}
|
|
40
|
-
export interface LaidOutIssue {
|
|
41
|
-
issue_id: string;
|
|
42
|
-
/** 0 for a root issue, +1 per nesting level. */
|
|
43
|
-
depth: number;
|
|
44
|
-
x: number;
|
|
45
|
-
y: number;
|
|
46
|
-
width: number;
|
|
47
|
-
height: number;
|
|
48
|
-
data: Issue;
|
|
49
|
-
hasChildren: boolean;
|
|
50
|
-
}
|
|
51
|
-
export interface IssuesLayoutBounds {
|
|
52
|
-
x: number;
|
|
53
|
-
y: number;
|
|
54
|
-
width: number;
|
|
55
|
-
height: number;
|
|
56
|
-
}
|
|
57
|
-
export interface IssuesLayout {
|
|
58
|
-
rows: LaidOutIssue[];
|
|
59
|
-
bounds: IssuesLayoutBounds;
|
|
60
|
-
}
|
|
61
|
-
export type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
package/dist/issues/types.js
DELETED
package/dist/issues/validate.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates an Issues document against `notations/12-issues.md`.
|
|
3
|
-
*
|
|
4
|
-
* Error codes:
|
|
5
|
-
* - ISS-000 structural / header problem (not an object, wrong notation,
|
|
6
|
-
* missing issues_catalogue, missing catalogue fields)
|
|
7
|
-
* - ISS-001 duplicate issue_id
|
|
8
|
-
* - ISS-002 status outside the vocabulary
|
|
9
|
-
* - ISS-003 issue_id or name missing or empty
|
|
10
|
-
* - ISS-004 parent references a missing issue (warning)
|
|
11
|
-
* - ISS-005 cycle in the parent chain
|
|
12
|
-
* - ISS-006 relates_to entry is not an ACTIVITY- / GOAL- id
|
|
13
|
-
*/
|
|
14
|
-
const STATUS_VALUES = ['open', 'in_progress', 'blocked', 'resolved', 'closed'];
|
|
15
|
-
const RELATES_TO_RE = /^(ACTIVITY|GOAL)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
16
|
-
function isNonEmptyString(v) {
|
|
17
|
-
return typeof v === 'string' && v.trim().length > 0;
|
|
18
|
-
}
|
|
19
|
-
export function validateIssues(input) {
|
|
20
|
-
const errors = [];
|
|
21
|
-
const warnings = [];
|
|
22
|
-
if (!input || typeof input !== 'object') {
|
|
23
|
-
return { valid: false, errors: [{ code: 'ISS-000', message: 'Input must be an object' }], warnings };
|
|
24
|
-
}
|
|
25
|
-
const raw = input;
|
|
26
|
-
if ('notation' in raw && raw['notation'] !== 'issues') {
|
|
27
|
-
errors.push({ code: 'ISS-000', message: `notation must be "issues", got "${String(raw['notation'])}"` });
|
|
28
|
-
}
|
|
29
|
-
const catRaw = raw['issues_catalogue'];
|
|
30
|
-
if (!catRaw || typeof catRaw !== 'object') {
|
|
31
|
-
errors.push({ code: 'ISS-000', message: 'Missing required root key: issues_catalogue' });
|
|
32
|
-
return { valid: false, errors, warnings };
|
|
33
|
-
}
|
|
34
|
-
const cat = catRaw;
|
|
35
|
-
if (!isNonEmptyString(cat['id'])) {
|
|
36
|
-
errors.push({ code: 'ISS-000', message: 'issues_catalogue.id is required', path: 'issues_catalogue.id' });
|
|
37
|
-
}
|
|
38
|
-
if (!isNonEmptyString(cat['name'])) {
|
|
39
|
-
errors.push({ code: 'ISS-000', message: 'issues_catalogue.name is required', path: 'issues_catalogue.name' });
|
|
40
|
-
}
|
|
41
|
-
if (!isNonEmptyString(cat['updated_at'])) {
|
|
42
|
-
errors.push({ code: 'ISS-000', message: 'issues_catalogue.updated_at is required', path: 'issues_catalogue.updated_at' });
|
|
43
|
-
}
|
|
44
|
-
const issuesRaw = cat['issues'];
|
|
45
|
-
if (!Array.isArray(issuesRaw)) {
|
|
46
|
-
errors.push({ code: 'ISS-000', message: 'issues_catalogue.issues must be an array', path: 'issues_catalogue.issues' });
|
|
47
|
-
return { valid: false, errors, warnings };
|
|
48
|
-
}
|
|
49
|
-
const idSet = new Set();
|
|
50
|
-
for (let i = 0; i < issuesRaw.length; i++) {
|
|
51
|
-
const it = issuesRaw[i];
|
|
52
|
-
const path = `issues_catalogue.issues[${i}]`;
|
|
53
|
-
if (!it || typeof it !== 'object') {
|
|
54
|
-
errors.push({ code: 'ISS-003', message: `${path} must be an object`, path });
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const issue = it;
|
|
58
|
-
const idOk = isNonEmptyString(issue['issue_id']);
|
|
59
|
-
if (!idOk) {
|
|
60
|
-
errors.push({ code: 'ISS-003', message: `${path}.issue_id is missing or empty`, path });
|
|
61
|
-
}
|
|
62
|
-
if (!isNonEmptyString(issue['name'])) {
|
|
63
|
-
errors.push({ code: 'ISS-003', message: `${path}.name is missing or empty`, path });
|
|
64
|
-
}
|
|
65
|
-
if (idOk) {
|
|
66
|
-
const id = issue['issue_id'].trim();
|
|
67
|
-
if (idSet.has(id)) {
|
|
68
|
-
errors.push({ code: 'ISS-001', message: `Duplicate issue_id: "${id}"`, path });
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
idSet.add(id);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const status = issue['status'];
|
|
75
|
-
if (!STATUS_VALUES.includes(status)) {
|
|
76
|
-
errors.push({
|
|
77
|
-
code: 'ISS-002',
|
|
78
|
-
message: `${path}.status "${String(status)}" must be one of: ${STATUS_VALUES.join(', ')}`,
|
|
79
|
-
path,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
const relates = issue['relates_to'];
|
|
83
|
-
if (relates !== undefined) {
|
|
84
|
-
if (!Array.isArray(relates)) {
|
|
85
|
-
errors.push({ code: 'ISS-006', message: `${path}.relates_to must be an array`, path });
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
for (let j = 0; j < relates.length; j++) {
|
|
89
|
-
const ref = relates[j];
|
|
90
|
-
if (typeof ref !== 'string' || !RELATES_TO_RE.test(ref)) {
|
|
91
|
-
errors.push({
|
|
92
|
-
code: 'ISS-006',
|
|
93
|
-
message: `${path}.relates_to[${j}] "${String(ref)}" must be an ACTIVITY- or GOAL- id`,
|
|
94
|
-
path,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// parent reference + cycle checks — run once all ids are collected.
|
|
102
|
-
const parentOf = new Map();
|
|
103
|
-
for (let i = 0; i < issuesRaw.length; i++) {
|
|
104
|
-
const it = issuesRaw[i];
|
|
105
|
-
if (!it || typeof it !== 'object')
|
|
106
|
-
continue;
|
|
107
|
-
const issue = it;
|
|
108
|
-
const parent = issue['parent'];
|
|
109
|
-
const path = `issues_catalogue.issues[${i}]`;
|
|
110
|
-
if (parent === undefined)
|
|
111
|
-
continue;
|
|
112
|
-
if (typeof parent !== 'string' || !idSet.has(parent.trim())) {
|
|
113
|
-
warnings.push({
|
|
114
|
-
code: 'ISS-004',
|
|
115
|
-
message: `${path}.parent "${String(parent)}" does not resolve to a known issue — rendered at the root`,
|
|
116
|
-
path,
|
|
117
|
-
});
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (isNonEmptyString(issue['issue_id'])) {
|
|
121
|
-
parentOf.set(issue['issue_id'].trim(), parent.trim());
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
let cycleNode;
|
|
125
|
-
for (const start of parentOf.keys()) {
|
|
126
|
-
if (cycleNode)
|
|
127
|
-
break;
|
|
128
|
-
const seen = new Set();
|
|
129
|
-
let cur = start;
|
|
130
|
-
while (cur !== undefined) {
|
|
131
|
-
if (seen.has(cur)) {
|
|
132
|
-
cycleNode = cur;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
seen.add(cur);
|
|
136
|
-
cur = parentOf.get(cur);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (cycleNode) {
|
|
140
|
-
errors.push({ code: 'ISS-005', message: `Cycle detected in the parent chain involving issue "${cycleNode}"` });
|
|
141
|
-
}
|
|
142
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
143
|
-
}
|