@transitrix/diagrams 1.13.3 → 1.14.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/dist/activities/action-fields.d.ts +6 -0
- package/dist/activities/action-fields.js +34 -0
- package/dist/activities/validate.d.ts +3 -1
- package/dist/activities/validate.js +36 -34
- package/dist/applications/validate.js +27 -26
- package/dist/capability-map/validate.d.ts +4 -1
- package/dist/capability-map/validate.js +32 -30
- package/dist/codex/validate.js +3 -3
- package/dist/compliance/canon-catalog.js +1 -1
- package/dist/compliance/classify.d.ts +4 -1
- package/dist/compliance/classify.js +21 -1
- package/dist/compliance/coverage-metric.js +3 -3
- package/dist/compliance/impact.js +4 -4
- package/dist/compliance/index.d.ts +3 -0
- package/dist/compliance/index.js +1 -0
- package/dist/compliance/requirement-chain.d.ts +111 -0
- package/dist/compliance/requirement-chain.js +501 -0
- package/dist/compliance/reverse-index.js +4 -0
- package/dist/compliance/types.d.ts +20 -0
- package/dist/fgca/parse-canonical.d.ts +0 -25
- package/dist/fgca/parse-canonical.js +16 -26
- package/dist/fgca/projection-origin.d.ts +2 -0
- package/dist/fgca/projection-origin.js +2 -0
- package/dist/fgca/resolver.d.ts +0 -8
- package/dist/fgca/resolver.js +2 -0
- package/dist/products/validate.js +30 -25
- package/dist/repo-validate/check-strategy-chain.d.ts +8 -2
- package/dist/repo-validate/check-strategy-chain.js +64 -64
- package/dist/repo-validate/types.d.ts +2 -0
- package/dist/repo-validate/validate-repo.js +2 -0
- package/dist/risk/index.d.ts +1 -1
- package/dist/risk/types.d.ts +8 -1
- package/dist/risk/validate.js +19 -4
- package/dist/scenarios/validate.js +4 -0
- package/dist/schema-finding.d.ts +3 -0
- package/dist/schema-finding.js +6 -0
- package/dist/webview/entry.js +7 -2
- package/dist/webview/render-goals.js +2 -1
- package/package.json +1 -1
- package/src/activities/action-fields.ts +31 -0
- package/src/activities/validate.ts +37 -34
- package/src/applications/validate.ts +27 -26
- package/src/capability-map/validate.ts +33 -29
- package/src/codex/validate.ts +3 -3
- package/src/compliance/canon-catalog.ts +1 -1
- package/src/compliance/classify.ts +24 -1
- package/src/compliance/coverage-metric.ts +3 -3
- package/src/compliance/impact.ts +4 -4
- package/src/compliance/index.ts +3 -0
- package/src/compliance/requirement-chain.ts +430 -0
- package/src/compliance/reverse-index.ts +3 -0
- package/src/compliance/types.ts +21 -0
- package/src/fgca/parse-canonical.ts +14 -1
- package/src/fgca/projection-origin.ts +2 -0
- package/src/fgca/resolver.ts +2 -0
- package/src/products/validate.ts +31 -25
- package/src/repo-validate/check-strategy-chain.ts +69 -64
- package/src/repo-validate/types.ts +2 -0
- package/src/repo-validate/validate-repo.ts +2 -0
- package/src/risk/index.ts +1 -1
- package/src/risk/types.ts +9 -1
- package/src/risk/validate.ts +19 -5
- package/src/scenarios/validate.ts +5 -0
- package/src/schema-finding.ts +8 -0
- package/src/webview/entry.ts +6 -2
- package/src/webview/render-goals.ts +2 -1
- package/src/webview/styles.css +8 -0
package/dist/fgca/resolver.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FGCA canon resolver — assembles a flat canonical FGCA document from a
|
|
3
|
-
* view_config projection and a canon element/relation store (VP-3).
|
|
4
|
-
*
|
|
5
|
-
* Output shape matches the flat form that `parseCanonicalFGCA` expects:
|
|
6
|
-
* top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
|
|
7
|
-
* string IDs. Filesystem-free — callable from unit tests without vscode.
|
|
8
|
-
*/
|
|
9
1
|
export interface FGCAViewConfig {
|
|
10
2
|
goals?: {
|
|
11
3
|
filter?: 'all' | 'ids' | 'tags';
|
package/dist/fgca/resolver.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RESOLVED_DGCA } from './projection-origin.js';
|
|
1
2
|
/**
|
|
2
3
|
* FGCA canon resolver — assembles a flat canonical FGCA document from a
|
|
3
4
|
* view_config projection and a canon element/relation store (VP-3).
|
|
@@ -137,6 +138,7 @@ export function resolveFGCA(viewDoc, sources) {
|
|
|
137
138
|
outOfScopeFactorIds = new Set([...factorElems.keys()].filter((id) => !selectedFactorIds.has(id)));
|
|
138
139
|
}
|
|
139
140
|
return {
|
|
141
|
+
[RESOLVED_DGCA]: true,
|
|
140
142
|
notation: 'dgca',
|
|
141
143
|
id: viewDoc['id'],
|
|
142
144
|
name: viewDoc['name'],
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { schemaFinding } from '../schema-finding.js';
|
|
1
2
|
const VALID_TYPES = new Set(['digital_product', 'service', 'platform', 'bundle']);
|
|
2
3
|
const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated']);
|
|
3
4
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
5
|
export function validateProductsCatalogue(input) {
|
|
5
6
|
const errors = [];
|
|
6
7
|
const warnings = [];
|
|
7
|
-
if (!input || typeof input !== 'object') {
|
|
8
|
-
return { valid: false, errors: [
|
|
8
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
9
|
+
return { valid: false, errors: [schemaFinding('products inline', '$', 'mapping', input)], warnings };
|
|
9
10
|
}
|
|
10
11
|
const raw = input;
|
|
11
12
|
// PROD-001: notation header
|
|
@@ -17,75 +18,79 @@ export function validateProductsCatalogue(input) {
|
|
|
17
18
|
}
|
|
18
19
|
if (errors.length > 0)
|
|
19
20
|
return { valid: false, errors, warnings };
|
|
21
|
+
if (('view_config' in raw || 'view' in raw) && !('products_catalogue' in raw)) {
|
|
22
|
+
return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
|
|
23
|
+
message: 'products projection form is unsupported and remains unvalidated.' }] };
|
|
24
|
+
}
|
|
20
25
|
// PROD-002: catalogue header fields
|
|
21
26
|
const cat = (raw['products_catalogue'] ?? {});
|
|
22
|
-
if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object') {
|
|
23
|
-
errors.push(
|
|
27
|
+
if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object' || Array.isArray(raw['products_catalogue'])) {
|
|
28
|
+
errors.push(schemaFinding('products inline', 'products_catalogue', 'mapping', raw['products_catalogue']));
|
|
24
29
|
return { valid: false, errors, warnings };
|
|
25
30
|
}
|
|
26
31
|
if (!cat['id'] || typeof cat['id'] !== 'string' || !cat['id'].trim()) {
|
|
27
|
-
errors.push(
|
|
32
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.id', 'nonempty string', cat['id']));
|
|
28
33
|
}
|
|
29
34
|
if (!cat['name'] || typeof cat['name'] !== 'string' || !cat['name'].trim()) {
|
|
30
|
-
errors.push(
|
|
35
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.name', 'nonempty string', cat['name']));
|
|
31
36
|
}
|
|
32
37
|
if (!cat['updated_at'] || typeof cat['updated_at'] !== 'string') {
|
|
33
|
-
errors.push(
|
|
38
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
|
|
34
39
|
}
|
|
35
40
|
if (errors.length > 0)
|
|
36
41
|
return { valid: false, errors, warnings };
|
|
37
|
-
//
|
|
42
|
+
// Schema: updated_at format
|
|
38
43
|
if (!DATE_RE.test(cat['updated_at'])) {
|
|
39
|
-
errors.push(
|
|
44
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
|
|
40
45
|
}
|
|
41
46
|
const products = cat['products'];
|
|
42
47
|
if (!Array.isArray(products)) {
|
|
43
|
-
errors.push(
|
|
48
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.products', 'array', cat['products']));
|
|
44
49
|
return { valid: false, errors, warnings };
|
|
45
50
|
}
|
|
46
|
-
//
|
|
51
|
+
// Schema: unique product_id
|
|
47
52
|
const seenIds = new Set();
|
|
48
53
|
for (let i = 0; i < products.length; i++) {
|
|
49
54
|
const rawProduct = products[i];
|
|
50
55
|
const idx = `products[${i}]`;
|
|
51
|
-
if (!rawProduct || typeof rawProduct !== 'object') {
|
|
52
|
-
errors.push(
|
|
56
|
+
if (!rawProduct || typeof rawProduct !== 'object' || Array.isArray(rawProduct)) {
|
|
57
|
+
errors.push(schemaFinding('products inline', `${idx}`, 'mapping', rawProduct));
|
|
53
58
|
continue;
|
|
54
59
|
}
|
|
55
60
|
const p = rawProduct;
|
|
56
|
-
//
|
|
61
|
+
// Schema: required per-product fields
|
|
57
62
|
if (!p['product_id'] || typeof p['product_id'] !== 'string' || !p['product_id'].trim()) {
|
|
58
|
-
errors.push(
|
|
63
|
+
errors.push(schemaFinding('products inline', `${idx}.product_id`, 'nonempty string', p['product_id']));
|
|
59
64
|
}
|
|
60
65
|
else {
|
|
61
66
|
const pid = p['product_id'];
|
|
62
67
|
if (seenIds.has(pid)) {
|
|
63
|
-
errors.push({
|
|
68
|
+
errors.push(schemaFinding('products inline', `${idx}.product_id`, 'unique identifier', p['product_id']));
|
|
64
69
|
}
|
|
65
70
|
seenIds.add(pid);
|
|
66
71
|
}
|
|
67
72
|
if (!p['name'] || typeof p['name'] !== 'string' || !p['name'].trim()) {
|
|
68
|
-
errors.push(
|
|
73
|
+
errors.push(schemaFinding('products inline', `${idx}.name`, 'nonempty string', p['name']));
|
|
69
74
|
}
|
|
70
75
|
if (!p['type']) {
|
|
71
|
-
errors.push(
|
|
76
|
+
errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
|
|
72
77
|
}
|
|
73
78
|
if (!p['status']) {
|
|
74
|
-
errors.push(
|
|
79
|
+
errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
|
|
75
80
|
}
|
|
76
|
-
//
|
|
81
|
+
// Schema: type enum
|
|
77
82
|
if (p['type'] && !VALID_TYPES.has(p['type'])) {
|
|
78
|
-
errors.push(
|
|
83
|
+
errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
|
|
79
84
|
}
|
|
80
|
-
//
|
|
85
|
+
// Schema: status enum
|
|
81
86
|
if (p['status'] && !VALID_STATUSES.has(p['status'])) {
|
|
82
|
-
errors.push(
|
|
87
|
+
errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
|
|
83
88
|
}
|
|
84
|
-
//
|
|
89
|
+
// Schema: maturity range
|
|
85
90
|
if (p['maturity'] !== undefined) {
|
|
86
91
|
const m = p['maturity'];
|
|
87
92
|
if (typeof m !== 'number' || !Number.isInteger(m) || m < 1 || m > 5) {
|
|
88
|
-
errors.push(
|
|
93
|
+
errors.push(schemaFinding('products inline', `${idx}.maturity`, 'integer in 1–5', p['maturity']));
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Run the strategy-chain semantic checks (GOALS-009..011,
|
|
4
|
-
*
|
|
3
|
+
* Run the strategy-chain semantic checks (GOALS-009..011, ACTION-007..011,
|
|
4
|
+
* DGCA-REPO-008..014 except GOALS-008 — see the module header) over the loaded
|
|
5
5
|
* element set and append findings. Called from `validateRepoModel` after the
|
|
6
6
|
* structural phases. Pure, deterministic order.
|
|
7
7
|
*/
|
|
8
8
|
export declare function checkStrategyChainSemantics(input: RepoModelInput, findings: RepoFinding[]): void;
|
|
9
|
+
/** Removed FGCA orphan rules become visible coverage observations, not new aliases. */
|
|
10
|
+
export declare function collectStrategyCoverageObservations(input: RepoModelInput): Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
kind: 'unreferenced';
|
|
13
|
+
message: string;
|
|
14
|
+
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateActionFields } from '../activities/action-fields.js';
|
|
1
2
|
// Strategy-chain semantic checks — the error-severity subset of DSM's Go
|
|
2
3
|
// `Validate*` functions (`api02/internal/importer/{goals,activities,fgca}.go`
|
|
3
4
|
// in transitrix-dsm), ported onto the standalone-element repo shape
|
|
@@ -5,9 +6,8 @@
|
|
|
5
6
|
// can drop those Go functions without regressing the checks they enforce
|
|
6
7
|
// today.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// import-log taxonomy (`RepoFinding.ruleId`).
|
|
9
|
+
// ACTION identities follow the versioned element contract; schedule ACT codes
|
|
10
|
+
// remain separate. Stored historical findings must retain their original codes.
|
|
11
11
|
//
|
|
12
12
|
// Scope — DSM's full rule set is now ported except GOALS-008 (see below).
|
|
13
13
|
// `RepoFinding` grew a `severity` field precisely so DSM's warn-severity
|
|
@@ -43,20 +43,20 @@
|
|
|
43
43
|
//
|
|
44
44
|
// Ported (error-severity, blocking):
|
|
45
45
|
// GOALS-010 — GOAL `parent` chain contains a cycle.
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
46
|
+
// ACTION-008 — ACTION `predecessors` graph contains a cycle.
|
|
47
|
+
// ACTION-009 — ACTION lists itself as its own predecessor.
|
|
48
|
+
// ACTION-010 — ACTION `start_date`/`end_date` unparseable, or end before start.
|
|
49
|
+
// ACTION-011 — ACTION numeric field (`duration`/`duration_days`, `labor_cost`,
|
|
50
50
|
// `resources_cost`, `effort`, `score`) is negative.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
51
|
+
// DGCA-REPO-008 — GOAL.factors references an undefined DRIVER.
|
|
52
|
+
// DGCA-REPO-009 — CHANGE.goals references an undefined GOAL.
|
|
53
|
+
// DGCA-REPO-010 — ACTION.delivers_changes references an undefined CHANGE.
|
|
54
|
+
// DGCA-REPO-011 — ACTION.goals references an undefined GOAL.
|
|
55
55
|
//
|
|
56
56
|
// Ported (warning-severity, advisory):
|
|
57
57
|
// GOALS-009 — GOAL.parent is set but does not resolve to a known GOAL (orphan).
|
|
58
58
|
// GOALS-011 — GOAL has no `parent` and `level` >= 1 (backlog).
|
|
59
|
-
//
|
|
59
|
+
// ACTION-007 — ACTION.predecessors entry or ACTION.parent does not resolve
|
|
60
60
|
// to a known ACTION (orphan).
|
|
61
61
|
// FGCA-012 — a DRIVER is not referenced by any GOAL.factors (unreferenced).
|
|
62
62
|
// FGCA-013 — a GOAL is not referenced by any CHANGE.goals or ACTION.goals
|
|
@@ -228,32 +228,32 @@ function checkGoalParentResolution(goals, findings) {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
-
/**
|
|
231
|
+
/** ACTION-009 — an ACTION cannot list itself as its own predecessor. */
|
|
232
232
|
function checkActionSelfPredecessor(actions, findings) {
|
|
233
233
|
for (const a of actions) {
|
|
234
234
|
if (readStringArray(a.data, 'predecessors').includes(a.id)) {
|
|
235
235
|
findings.push({
|
|
236
236
|
scope: PScope,
|
|
237
237
|
id: a.id,
|
|
238
|
-
ruleId: '
|
|
239
|
-
message: `
|
|
238
|
+
ruleId: 'ACTION-009',
|
|
239
|
+
message: `ACTION-009: action '${a.id}' lists itself as a predecessor.`,
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
/**
|
|
244
|
+
/** ACTION-008 — the ACTION `predecessors` graph must not contain a cycle. */
|
|
245
245
|
function checkActionPredecessorCycle(actions, findings) {
|
|
246
246
|
const cyc = findPredecessorCycle(actions);
|
|
247
247
|
if (cyc) {
|
|
248
248
|
findings.push({
|
|
249
249
|
scope: PScope,
|
|
250
250
|
id: cyc,
|
|
251
|
-
ruleId: '
|
|
252
|
-
message: `
|
|
251
|
+
ruleId: 'ACTION-008',
|
|
252
|
+
message: `ACTION-008: predecessor graph contains a cycle involving action '${cyc}'.`,
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
-
/**
|
|
256
|
+
/** ACTION-007 — an ACTION `predecessors` entry or `parent` that does not resolve
|
|
257
257
|
* to a known ACTION is an orphan reference (warning — advisory in DSM, same
|
|
258
258
|
* as the goal-parent orphan checks above). */
|
|
259
259
|
function checkActionOrphanReferences(actions, findings) {
|
|
@@ -264,9 +264,9 @@ function checkActionOrphanReferences(actions, findings) {
|
|
|
264
264
|
findings.push({
|
|
265
265
|
scope: PScope,
|
|
266
266
|
id: a.id,
|
|
267
|
-
ruleId: '
|
|
267
|
+
ruleId: 'ACTION-007',
|
|
268
268
|
severity: 'warning',
|
|
269
|
-
message: `
|
|
269
|
+
message: `ACTION-007: action '${a.id}' predecessor '${p}' does not resolve to a known action.`,
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
272
|
}
|
|
@@ -275,9 +275,9 @@ function checkActionOrphanReferences(actions, findings) {
|
|
|
275
275
|
findings.push({
|
|
276
276
|
scope: PScope,
|
|
277
277
|
id: a.id,
|
|
278
|
-
ruleId: '
|
|
278
|
+
ruleId: 'ACTION-007',
|
|
279
279
|
severity: 'warning',
|
|
280
|
-
message: `
|
|
280
|
+
message: `ACTION-007: action '${a.id}' parent '${parent}' does not resolve to a known action.`,
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
283
|
}
|
|
@@ -296,33 +296,33 @@ function parseIsoDate(value) {
|
|
|
296
296
|
}
|
|
297
297
|
return dt;
|
|
298
298
|
}
|
|
299
|
-
/**
|
|
299
|
+
/** ACTION-010 — `start_date`/`end_date` must be valid ISO dates, and `end_date`
|
|
300
300
|
* must not be before `start_date` (equal is allowed — e.g. a milestone). */
|
|
301
301
|
function checkActionDates(actions, findings) {
|
|
302
302
|
for (const a of actions) {
|
|
303
|
-
const startRaw =
|
|
304
|
-
const endRaw =
|
|
303
|
+
const startRaw = a.data.start_date == null ? undefined : a.data.start_date;
|
|
304
|
+
const endRaw = a.data.end_date == null ? undefined : a.data.end_date;
|
|
305
305
|
let start;
|
|
306
306
|
let end;
|
|
307
307
|
if (startRaw !== undefined) {
|
|
308
|
-
start = parseIsoDate(startRaw);
|
|
308
|
+
start = typeof startRaw === 'string' ? parseIsoDate(startRaw) : undefined;
|
|
309
309
|
if (!start) {
|
|
310
310
|
findings.push({
|
|
311
311
|
scope: PScope,
|
|
312
312
|
id: a.id,
|
|
313
|
-
ruleId: '
|
|
314
|
-
message: `
|
|
313
|
+
ruleId: 'ACTION-010',
|
|
314
|
+
message: `ACTION-010: action '${a.id}' start_date '${startRaw}' is not a valid YYYY-MM-DD date.`,
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
if (endRaw !== undefined) {
|
|
319
|
-
end = parseIsoDate(endRaw);
|
|
319
|
+
end = typeof endRaw === 'string' ? parseIsoDate(endRaw) : undefined;
|
|
320
320
|
if (!end) {
|
|
321
321
|
findings.push({
|
|
322
322
|
scope: PScope,
|
|
323
323
|
id: a.id,
|
|
324
|
-
ruleId: '
|
|
325
|
-
message: `
|
|
324
|
+
ruleId: 'ACTION-010',
|
|
325
|
+
message: `ACTION-010: action '${a.id}' end_date '${endRaw}' is not a valid YYYY-MM-DD date.`,
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
328
|
}
|
|
@@ -330,34 +330,22 @@ function checkActionDates(actions, findings) {
|
|
|
330
330
|
findings.push({
|
|
331
331
|
scope: PScope,
|
|
332
332
|
id: a.id,
|
|
333
|
-
ruleId: '
|
|
334
|
-
message: `
|
|
333
|
+
ruleId: 'ACTION-010',
|
|
334
|
+
message: `ACTION-010: action '${a.id}' end_date '${endRaw}' is before start_date '${startRaw}'.`,
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
-
/**
|
|
340
|
-
|
|
341
|
-
* (elements/24-action.md §2), but `duration_days` is an accepted alias in
|
|
342
|
-
* this codebase's document-form validator (activities/validate.ts) and is
|
|
343
|
-
* the field acme_corp's own ACTION elements actually use. */
|
|
344
|
-
function checkActionNegativeNumbers(actions, findings) {
|
|
345
|
-
const fields = ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score'];
|
|
339
|
+
/** Shared ACTION type and versioned numeric checks. */
|
|
340
|
+
function checkActionFields(actions, findings, version) {
|
|
346
341
|
for (const a of actions) {
|
|
347
|
-
for (const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
findings.push({
|
|
351
|
-
scope: PScope,
|
|
352
|
-
id: a.id,
|
|
353
|
-
ruleId: 'ACT-009',
|
|
354
|
-
message: `ACT-009: action '${a.id}' field '${field}' is negative (${v}).`,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
342
|
+
for (const error of validateActionFields(a.data, version)) {
|
|
343
|
+
findings.push({ scope: PScope, id: a.id, ruleId: error.code,
|
|
344
|
+
message: `${a.doc.path}: canonical action '${a.id}' ${error.message}` });
|
|
357
345
|
}
|
|
358
346
|
}
|
|
359
347
|
}
|
|
360
|
-
/**
|
|
348
|
+
/** DGCA-REPO-008..011 — inline strategy-chain cross-references must resolve within
|
|
361
349
|
* the repo: GOAL.factors -> DRIVER, CHANGE.goals -> GOAL, ACTION.goals ->
|
|
362
350
|
* GOAL, ACTION.delivers_changes -> CHANGE (ELEMENT_PRIMITIVES.md §7.1-§7.4). */
|
|
363
351
|
function checkStrategyChainReferences(goals, actions, drivers, changes, findings) {
|
|
@@ -370,8 +358,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
|
|
|
370
358
|
findings.push({
|
|
371
359
|
scope: PScope,
|
|
372
360
|
id: g.id,
|
|
373
|
-
ruleId: '
|
|
374
|
-
message: `
|
|
361
|
+
ruleId: 'DGCA-REPO-008',
|
|
362
|
+
message: `DGCA-REPO-008: goal '${g.id}' references undefined driver '${f}'.`,
|
|
375
363
|
});
|
|
376
364
|
}
|
|
377
365
|
}
|
|
@@ -382,8 +370,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
|
|
|
382
370
|
findings.push({
|
|
383
371
|
scope: PScope,
|
|
384
372
|
id: c.id,
|
|
385
|
-
ruleId: '
|
|
386
|
-
message: `
|
|
373
|
+
ruleId: 'DGCA-REPO-009',
|
|
374
|
+
message: `DGCA-REPO-009: change '${c.id}' references undefined goal '${g}'.`,
|
|
387
375
|
});
|
|
388
376
|
}
|
|
389
377
|
}
|
|
@@ -394,8 +382,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
|
|
|
394
382
|
findings.push({
|
|
395
383
|
scope: PScope,
|
|
396
384
|
id: a.id,
|
|
397
|
-
ruleId: '
|
|
398
|
-
message: `
|
|
385
|
+
ruleId: 'DGCA-REPO-010',
|
|
386
|
+
message: `DGCA-REPO-010: action '${a.id}' references undefined change '${c}'.`,
|
|
399
387
|
});
|
|
400
388
|
}
|
|
401
389
|
}
|
|
@@ -404,8 +392,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
|
|
|
404
392
|
findings.push({
|
|
405
393
|
scope: PScope,
|
|
406
394
|
id: a.id,
|
|
407
|
-
ruleId: '
|
|
408
|
-
message: `
|
|
395
|
+
ruleId: 'DGCA-REPO-011',
|
|
396
|
+
message: `DGCA-REPO-011: action '${a.id}' references undefined goal '${g}'.`,
|
|
409
397
|
});
|
|
410
398
|
}
|
|
411
399
|
}
|
|
@@ -469,8 +457,8 @@ function checkStrategyChainOrphans(goals, actions, drivers, changes, findings) {
|
|
|
469
457
|
}
|
|
470
458
|
}
|
|
471
459
|
/**
|
|
472
|
-
* Run the strategy-chain semantic checks (GOALS-009..011,
|
|
473
|
-
*
|
|
460
|
+
* Run the strategy-chain semantic checks (GOALS-009..011, ACTION-007..011,
|
|
461
|
+
* DGCA-REPO-008..014 except GOALS-008 — see the module header) over the loaded
|
|
474
462
|
* element set and append findings. Called from `validateRepoModel` after the
|
|
475
463
|
* structural phases. Pure, deterministic order.
|
|
476
464
|
*/
|
|
@@ -485,7 +473,19 @@ export function checkStrategyChainSemantics(input, findings) {
|
|
|
485
473
|
checkActionPredecessorCycle(actions, findings);
|
|
486
474
|
checkActionOrphanReferences(actions, findings);
|
|
487
475
|
checkActionDates(actions, findings);
|
|
488
|
-
|
|
476
|
+
checkActionFields(actions, findings, input.methodologyVersion);
|
|
489
477
|
checkStrategyChainReferences(goals, actions, drivers, changes, findings);
|
|
490
|
-
|
|
478
|
+
if (!usesCoverageObservations(input))
|
|
479
|
+
checkStrategyChainOrphans(goals, actions, drivers, changes, findings);
|
|
480
|
+
}
|
|
481
|
+
function usesCoverageObservations(input) {
|
|
482
|
+
return /^\d+\./.test(input.methodologyVersion ?? '') && Number(input.methodologyVersion.split('.')[0]) >= 5;
|
|
483
|
+
}
|
|
484
|
+
/** Removed FGCA orphan rules become visible coverage observations, not new aliases. */
|
|
485
|
+
export function collectStrategyCoverageObservations(input) {
|
|
486
|
+
if (!usesCoverageObservations(input))
|
|
487
|
+
return [];
|
|
488
|
+
const findings = [];
|
|
489
|
+
checkStrategyChainOrphans(collectByNotation(input.elements, isGoalNotation), collectByNotation(input.elements, isActionNotation), collectByNotation(input.elements, isDriverNotation), collectByNotation(input.elements, isChangeNotation), findings);
|
|
490
|
+
return findings.map(f => ({ id: f.id, kind: 'unreferenced', message: f.message.replace(/^FGCA-01[234]: /, '') }));
|
|
491
491
|
}
|
|
@@ -40,6 +40,8 @@ export interface RepoDoc {
|
|
|
40
40
|
* canon zone exactly as `lint.py` partitions them (`canon/elements/**` vs
|
|
41
41
|
* `canon/relations/**`). */
|
|
42
42
|
export interface RepoModelInput {
|
|
43
|
+
/** Catalogue manifest version; document spec_version never selects compatibility. */
|
|
44
|
+
methodologyVersion?: string;
|
|
43
45
|
/** Documents found under `canon/elements/**`. */
|
|
44
46
|
elements: RepoDoc[];
|
|
45
47
|
/** Documents found under `canon/relations/**`. */
|
|
@@ -129,6 +129,7 @@ function checkReferentialIntegrity(input, findings) {
|
|
|
129
129
|
findings.push({
|
|
130
130
|
scope: PScope,
|
|
131
131
|
id: relId,
|
|
132
|
+
ruleId: 'REL-002',
|
|
132
133
|
message: `Referential integrity: relation '${relId || doc.path}' endpoint '${fromId}' (from) does not resolve to a known element.`,
|
|
133
134
|
});
|
|
134
135
|
}
|
|
@@ -136,6 +137,7 @@ function checkReferentialIntegrity(input, findings) {
|
|
|
136
137
|
findings.push({
|
|
137
138
|
scope: PScope,
|
|
138
139
|
id: relId,
|
|
140
|
+
ruleId: 'REL-002',
|
|
139
141
|
message: `Referential integrity: relation '${relId || doc.path}' endpoint '${toId}' (to) does not resolve to a known element.`,
|
|
140
142
|
});
|
|
141
143
|
}
|
package/dist/risk/index.d.ts
CHANGED
package/dist/risk/types.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { GateChecks } from '../requirement/types.js';
|
|
2
2
|
/** Likelihood / impact / residual severity vocabulary (§7.26). */
|
|
3
|
-
export type RiskLevel = 'low' | 'medium' | 'high';
|
|
3
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | number;
|
|
4
|
+
export interface RiskScale {
|
|
5
|
+
id: string;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
direction: 'higher' | 'lower';
|
|
9
|
+
}
|
|
4
10
|
export declare const RISK_LEVELS: readonly RiskLevel[];
|
|
5
11
|
export interface Risk {
|
|
6
12
|
notation: 'risk';
|
|
7
13
|
id: string;
|
|
8
14
|
name: string;
|
|
15
|
+
risk_scale?: RiskScale;
|
|
9
16
|
likelihood: RiskLevel;
|
|
10
17
|
impact: RiskLevel;
|
|
11
18
|
residual: RiskLevel;
|
package/dist/risk/validate.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Codes:
|
|
4
4
|
// RISK-001 — shape / id grammar / required envelope + per-type fields
|
|
5
5
|
// (likelihood, impact, residual, owner_role, threatens).
|
|
6
|
-
// RISK-002 —
|
|
6
|
+
// RISK-002 — invalid qualitative/numeric degree or adopter scale.
|
|
7
7
|
// RISK-003 — threatens is empty, or an entry does not resolve to an
|
|
8
8
|
// admitted element in canon (no TYPE restriction — any core
|
|
9
9
|
// element may be threatened).
|
|
@@ -50,14 +50,29 @@ export function validateRisk(input, options = {}) {
|
|
|
50
50
|
if (!('valid_to' in r) || !(typeof r.valid_to === 'string' || r.valid_to === null)) {
|
|
51
51
|
errors.push({ code: 'RISK-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
52
52
|
}
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// Authored numeric degrees use one explicitly declared adopter scale.
|
|
54
|
+
const fields = ['likelihood', 'impact', 'residual'];
|
|
55
|
+
const scale = r.risk_scale;
|
|
56
|
+
const finite = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
57
|
+
const validScale = scale !== null && typeof scale === 'object' && !Array.isArray(scale)
|
|
58
|
+
&& typeof scale.id === 'string' && scale.id.trim().length > 0
|
|
59
|
+
&& finite(scale.min) && finite(scale.max) && scale.min < scale.max
|
|
60
|
+
&& (scale.direction === 'higher' || scale.direction === 'lower');
|
|
61
|
+
if ((scale !== undefined || fields.some(f => typeof r[f] === 'number')) && !validScale) {
|
|
62
|
+
errors.push({ code: 'RISK-002', message: 'risk_scale requires id, finite min < max, and direction higher or lower.', path: 'risk_scale' });
|
|
63
|
+
}
|
|
64
|
+
for (const f of fields) {
|
|
55
65
|
const v = r[f];
|
|
56
66
|
if (v === undefined || v === null || v === '') {
|
|
57
67
|
errors.push({ code: 'RISK-001', message: `${f} is required.`, path: f });
|
|
58
68
|
}
|
|
69
|
+
else if (typeof v === 'number') {
|
|
70
|
+
if (!finite(v) || (validScale && (v < scale.min || v > scale.max))) {
|
|
71
|
+
errors.push({ code: 'RISK-002', message: `${f} must be finite and within risk_scale bounds.`, path: f });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
59
74
|
else if (!RISK_LEVELS.includes(v)) {
|
|
60
|
-
errors.push({ code: 'RISK-002', message: `${f}
|
|
75
|
+
errors.push({ code: 'RISK-002', message: `${f} must be low, medium, high, or a number with risk_scale.`, path: f });
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
// threatens — required non-empty list (RISK-001), each entry resolves (RISK-003).
|
|
@@ -24,6 +24,10 @@ export function validateScenario(input) {
|
|
|
24
24
|
}
|
|
25
25
|
if (errors.length > 0)
|
|
26
26
|
return { valid: false, errors, warnings };
|
|
27
|
+
if (('view_config' in raw || 'view' in raw || 'scenarios' in raw) && !('scenario' in raw)) {
|
|
28
|
+
return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
|
|
29
|
+
message: 'scenarios projection or scenario-set form is unsupported and remains unvalidated.' }] };
|
|
30
|
+
}
|
|
27
31
|
const scn = raw['scenario'];
|
|
28
32
|
if (!scn || typeof scn !== 'object') {
|
|
29
33
|
errors.push({ code: 'SCN-002', message: 'Missing required field: scenario' });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared schema identity; callers supply the owning form and exact field. */
|
|
2
|
+
export function schemaFinding(form, path, expected, actual) {
|
|
3
|
+
const type = actual === null ? 'null' : Array.isArray(actual) ? 'array' : typeof actual;
|
|
4
|
+
return { code: 'SCHEMA_INVALID', path,
|
|
5
|
+
message: `${form} field '${path}': expected ${expected}; actual ${type} (${String(actual)}).` };
|
|
6
|
+
}
|
package/dist/webview/entry.js
CHANGED
|
@@ -185,8 +185,13 @@ function dispatchValidate(kind, doc) {
|
|
|
185
185
|
return renderFromValidation('process-map', validateProcessMap(doc), () => renderProcessMapHtml(doc.process_map));
|
|
186
186
|
case 'scenarios':
|
|
187
187
|
return renderFromValidation('scenarios', validateScenario(doc), () => renderScenarioHtml(doc.scenario));
|
|
188
|
-
case 'capability-map':
|
|
189
|
-
|
|
188
|
+
case 'capability-map': {
|
|
189
|
+
const validation = validateCapabilityMap(doc);
|
|
190
|
+
const unresolved = validation.warnings.find(w => w.code === 'NOTATION-SKIP-001');
|
|
191
|
+
if (validation.valid && unresolved)
|
|
192
|
+
return errorResult('capability-map', unresolved.code, unresolved.message);
|
|
193
|
+
return renderFromValidation('capability-map', validation, () => renderCapabilityMapHtml(doc.capability_map));
|
|
194
|
+
}
|
|
190
195
|
default:
|
|
191
196
|
return errorResult(kind, 'NOTATION-NOT-WIRED', `Notation '${kind}' is not wired into the IntelliJ bundle yet.`);
|
|
192
197
|
}
|
|
@@ -41,7 +41,8 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
41
41
|
const title = treeName
|
|
42
42
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
43
43
|
: '';
|
|
44
|
-
|
|
44
|
+
// Reserve a separate header row; the theme centers text on its y coordinate.
|
|
45
|
+
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, edgeStyle, title, topInset: title ? PAD : 0, embedCssTheme: 'transitrix' });
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
48
|
* The single goals SVG emitter shared by every host. Takes an already-computed
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** ACTION field types and the manifest-selected 7.0.0 numeric boundary. */
|
|
2
|
+
export function validateActionFields(
|
|
3
|
+
action: Record<string, unknown>, methodologyVersion?: string,
|
|
4
|
+
): Array<{ code: string; message: string; path: string }> {
|
|
5
|
+
const errors: Array<{ code: string; message: string; path: string }> = [];
|
|
6
|
+
const version = /^(\d+)\.(\d+)\.(\d+)(-[^+]+)?(?:\+.*)?$/.exec(methodologyVersion ?? '');
|
|
7
|
+
const nonnegative = version !== null && (Number(version[1]) > 7 ||
|
|
8
|
+
(Number(version[1]) === 7 && (Number(version[2]) > 0 || Number(version[3]) > 0 || !version[4])));
|
|
9
|
+
const type = (value: unknown): string => value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
10
|
+
const schema = (path: string, expected: string, value: unknown) => errors.push({
|
|
11
|
+
code: 'SCHEMA_INVALID', path,
|
|
12
|
+
message: `ACTION field '${path}' expected ${expected}, actual ${type(value)} (${String(value)}).`,
|
|
13
|
+
});
|
|
14
|
+
for (const field of ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score', 'sort']) {
|
|
15
|
+
const value = action[field];
|
|
16
|
+
if (value === undefined || (value === null && (field === 'duration' || field === 'duration_days'))) continue;
|
|
17
|
+
const integer = field === 'score' || field === 'sort';
|
|
18
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || (integer && !Number.isInteger(value))) {
|
|
19
|
+
schema(field, integer ? 'finite integer' : 'finite number', value);
|
|
20
|
+
} else if (nonnegative && field !== 'sort' && value < 0) {
|
|
21
|
+
errors.push({ code: 'ACTION-011', path: field, message: `ACTION field '${field}' is negative (${value}).` });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (action.predecessors !== undefined) {
|
|
25
|
+
if (!Array.isArray(action.predecessors)) schema('predecessors', 'array of strings', action.predecessors);
|
|
26
|
+
else action.predecessors.forEach((value, index) => {
|
|
27
|
+
if (typeof value !== 'string') schema(`predecessors[${index}]`, 'string', value);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return errors;
|
|
31
|
+
}
|