@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/src/products/validate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { schemaFinding } from '../schema-finding.js';
|
|
1
2
|
import type { ProductsCatalogueFile, ProductType, ProductStatus } from './types.js';
|
|
2
3
|
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
3
4
|
|
|
@@ -11,8 +12,8 @@ export function validateProductsCatalogue(input: unknown): ValidationResult {
|
|
|
11
12
|
const errors: ValidationError[] = [];
|
|
12
13
|
const warnings: ValidationWarning[] = [];
|
|
13
14
|
|
|
14
|
-
if (!input || typeof input !== 'object') {
|
|
15
|
-
return { valid: false, errors: [
|
|
15
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
16
|
+
return { valid: false, errors: [schemaFinding('products inline', '$', 'mapping', input)], warnings };
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const raw = input as Record<string, unknown>;
|
|
@@ -26,84 +27,89 @@ export function validateProductsCatalogue(input: unknown): ValidationResult {
|
|
|
26
27
|
|
|
27
28
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
28
29
|
|
|
30
|
+
if (('view_config' in raw || 'view' in raw) && !('products_catalogue' in raw)) {
|
|
31
|
+
return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
|
|
32
|
+
message: 'products projection form is unsupported and remains unvalidated.' }] };
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
// PROD-002: catalogue header fields
|
|
30
36
|
const cat = (raw['products_catalogue'] ?? {}) as Record<string, unknown>;
|
|
31
|
-
if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object') {
|
|
32
|
-
errors.push(
|
|
37
|
+
if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object' || Array.isArray(raw['products_catalogue'])) {
|
|
38
|
+
errors.push(schemaFinding('products inline', 'products_catalogue', 'mapping', raw['products_catalogue']));
|
|
33
39
|
return { valid: false, errors, warnings };
|
|
34
40
|
}
|
|
35
41
|
if (!cat['id'] || typeof cat['id'] !== 'string' || !(cat['id'] as string).trim()) {
|
|
36
|
-
errors.push(
|
|
42
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.id', 'nonempty string', cat['id']));
|
|
37
43
|
}
|
|
38
44
|
if (!cat['name'] || typeof cat['name'] !== 'string' || !(cat['name'] as string).trim()) {
|
|
39
|
-
errors.push(
|
|
45
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.name', 'nonempty string', cat['name']));
|
|
40
46
|
}
|
|
41
47
|
if (!cat['updated_at'] || typeof cat['updated_at'] !== 'string') {
|
|
42
|
-
errors.push(
|
|
48
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
46
52
|
|
|
47
|
-
//
|
|
53
|
+
// Schema: updated_at format
|
|
48
54
|
if (!DATE_RE.test(cat['updated_at'] as string)) {
|
|
49
|
-
errors.push(
|
|
55
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
const products = cat['products'];
|
|
53
59
|
if (!Array.isArray(products)) {
|
|
54
|
-
errors.push(
|
|
60
|
+
errors.push(schemaFinding('products inline', 'products_catalogue.products', 'array', cat['products']));
|
|
55
61
|
return { valid: false, errors, warnings };
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
//
|
|
64
|
+
// Schema: unique product_id
|
|
59
65
|
const seenIds = new Set<string>();
|
|
60
66
|
|
|
61
67
|
for (let i = 0; i < products.length; i++) {
|
|
62
68
|
const rawProduct = products[i];
|
|
63
69
|
const idx = `products[${i}]`;
|
|
64
70
|
|
|
65
|
-
if (!rawProduct || typeof rawProduct !== 'object') {
|
|
66
|
-
errors.push(
|
|
71
|
+
if (!rawProduct || typeof rawProduct !== 'object' || Array.isArray(rawProduct)) {
|
|
72
|
+
errors.push(schemaFinding('products inline', `${idx}`, 'mapping', rawProduct));
|
|
67
73
|
continue;
|
|
68
74
|
}
|
|
69
75
|
const p = rawProduct as Record<string, unknown>;
|
|
70
76
|
|
|
71
|
-
//
|
|
77
|
+
// Schema: required per-product fields
|
|
72
78
|
if (!p['product_id'] || typeof p['product_id'] !== 'string' || !(p['product_id'] as string).trim()) {
|
|
73
|
-
errors.push(
|
|
79
|
+
errors.push(schemaFinding('products inline', `${idx}.product_id`, 'nonempty string', p['product_id']));
|
|
74
80
|
} else {
|
|
75
81
|
const pid = p['product_id'] as string;
|
|
76
82
|
if (seenIds.has(pid)) {
|
|
77
|
-
errors.push({
|
|
83
|
+
errors.push(schemaFinding('products inline', `${idx}.product_id`, 'unique identifier', p['product_id']));
|
|
78
84
|
}
|
|
79
85
|
seenIds.add(pid);
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
if (!p['name'] || typeof p['name'] !== 'string' || !(p['name'] as string).trim()) {
|
|
83
|
-
errors.push(
|
|
89
|
+
errors.push(schemaFinding('products inline', `${idx}.name`, 'nonempty string', p['name']));
|
|
84
90
|
}
|
|
85
91
|
if (!p['type']) {
|
|
86
|
-
errors.push(
|
|
92
|
+
errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
|
|
87
93
|
}
|
|
88
94
|
if (!p['status']) {
|
|
89
|
-
errors.push(
|
|
95
|
+
errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
//
|
|
98
|
+
// Schema: type enum
|
|
93
99
|
if (p['type'] && !VALID_TYPES.has(p['type'] as ProductType)) {
|
|
94
|
-
errors.push(
|
|
100
|
+
errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
//
|
|
103
|
+
// Schema: status enum
|
|
98
104
|
if (p['status'] && !VALID_STATUSES.has(p['status'] as ProductStatus)) {
|
|
99
|
-
errors.push(
|
|
105
|
+
errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
//
|
|
108
|
+
// Schema: maturity range
|
|
103
109
|
if (p['maturity'] !== undefined) {
|
|
104
110
|
const m = p['maturity'];
|
|
105
111
|
if (typeof m !== 'number' || !Number.isInteger(m) || m < 1 || m > 5) {
|
|
106
|
-
errors.push(
|
|
112
|
+
errors.push(schemaFinding('products inline', `${idx}.maturity`, 'integer in 1–5', p['maturity']));
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
}
|
|
@@ -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
|
|
@@ -244,34 +244,34 @@ function checkGoalParentResolution(goals: ChainElement[], findings: RepoFinding[
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
/**
|
|
247
|
+
/** ACTION-009 — an ACTION cannot list itself as its own predecessor. */
|
|
248
248
|
function checkActionSelfPredecessor(actions: ChainElement[], findings: RepoFinding[]): void {
|
|
249
249
|
for (const a of actions) {
|
|
250
250
|
if (readStringArray(a.data, 'predecessors').includes(a.id)) {
|
|
251
251
|
findings.push({
|
|
252
252
|
scope: PScope,
|
|
253
253
|
id: a.id,
|
|
254
|
-
ruleId: '
|
|
255
|
-
message: `
|
|
254
|
+
ruleId: 'ACTION-009',
|
|
255
|
+
message: `ACTION-009: action '${a.id}' lists itself as a predecessor.`,
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
/**
|
|
261
|
+
/** ACTION-008 — the ACTION `predecessors` graph must not contain a cycle. */
|
|
262
262
|
function checkActionPredecessorCycle(actions: ChainElement[], findings: RepoFinding[]): void {
|
|
263
263
|
const cyc = findPredecessorCycle(actions);
|
|
264
264
|
if (cyc) {
|
|
265
265
|
findings.push({
|
|
266
266
|
scope: PScope,
|
|
267
267
|
id: cyc,
|
|
268
|
-
ruleId: '
|
|
269
|
-
message: `
|
|
268
|
+
ruleId: 'ACTION-008',
|
|
269
|
+
message: `ACTION-008: predecessor graph contains a cycle involving action '${cyc}'.`,
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
/**
|
|
274
|
+
/** ACTION-007 — an ACTION `predecessors` entry or `parent` that does not resolve
|
|
275
275
|
* to a known ACTION is an orphan reference (warning — advisory in DSM, same
|
|
276
276
|
* as the goal-parent orphan checks above). */
|
|
277
277
|
function checkActionOrphanReferences(actions: ChainElement[], findings: RepoFinding[]): void {
|
|
@@ -282,9 +282,9 @@ function checkActionOrphanReferences(actions: ChainElement[], findings: RepoFind
|
|
|
282
282
|
findings.push({
|
|
283
283
|
scope: PScope,
|
|
284
284
|
id: a.id,
|
|
285
|
-
ruleId: '
|
|
285
|
+
ruleId: 'ACTION-007',
|
|
286
286
|
severity: 'warning',
|
|
287
|
-
message: `
|
|
287
|
+
message: `ACTION-007: action '${a.id}' predecessor '${p}' does not resolve to a known action.`,
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
}
|
|
@@ -293,9 +293,9 @@ function checkActionOrphanReferences(actions: ChainElement[], findings: RepoFind
|
|
|
293
293
|
findings.push({
|
|
294
294
|
scope: PScope,
|
|
295
295
|
id: a.id,
|
|
296
|
-
ruleId: '
|
|
296
|
+
ruleId: 'ACTION-007',
|
|
297
297
|
severity: 'warning',
|
|
298
|
-
message: `
|
|
298
|
+
message: `ACTION-007: action '${a.id}' parent '${parent}' does not resolve to a known action.`,
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
301
|
}
|
|
@@ -316,34 +316,34 @@ function parseIsoDate(value: string): Date | undefined {
|
|
|
316
316
|
return dt;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
/**
|
|
319
|
+
/** ACTION-010 — `start_date`/`end_date` must be valid ISO dates, and `end_date`
|
|
320
320
|
* must not be before `start_date` (equal is allowed — e.g. a milestone). */
|
|
321
321
|
function checkActionDates(actions: ChainElement[], findings: RepoFinding[]): void {
|
|
322
322
|
for (const a of actions) {
|
|
323
|
-
const startRaw =
|
|
324
|
-
const endRaw =
|
|
323
|
+
const startRaw = a.data.start_date == null ? undefined : a.data.start_date;
|
|
324
|
+
const endRaw = a.data.end_date == null ? undefined : a.data.end_date;
|
|
325
325
|
let start: Date | undefined;
|
|
326
326
|
let end: Date | undefined;
|
|
327
327
|
|
|
328
328
|
if (startRaw !== undefined) {
|
|
329
|
-
start = parseIsoDate(startRaw);
|
|
329
|
+
start = typeof startRaw === 'string' ? parseIsoDate(startRaw) : undefined;
|
|
330
330
|
if (!start) {
|
|
331
331
|
findings.push({
|
|
332
332
|
scope: PScope,
|
|
333
333
|
id: a.id,
|
|
334
|
-
ruleId: '
|
|
335
|
-
message: `
|
|
334
|
+
ruleId: 'ACTION-010',
|
|
335
|
+
message: `ACTION-010: action '${a.id}' start_date '${startRaw}' is not a valid YYYY-MM-DD date.`,
|
|
336
336
|
});
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
if (endRaw !== undefined) {
|
|
340
|
-
end = parseIsoDate(endRaw);
|
|
340
|
+
end = typeof endRaw === 'string' ? parseIsoDate(endRaw) : undefined;
|
|
341
341
|
if (!end) {
|
|
342
342
|
findings.push({
|
|
343
343
|
scope: PScope,
|
|
344
344
|
id: a.id,
|
|
345
|
-
ruleId: '
|
|
346
|
-
message: `
|
|
345
|
+
ruleId: 'ACTION-010',
|
|
346
|
+
message: `ACTION-010: action '${a.id}' end_date '${endRaw}' is not a valid YYYY-MM-DD date.`,
|
|
347
347
|
});
|
|
348
348
|
}
|
|
349
349
|
}
|
|
@@ -351,36 +351,24 @@ function checkActionDates(actions: ChainElement[], findings: RepoFinding[]): voi
|
|
|
351
351
|
findings.push({
|
|
352
352
|
scope: PScope,
|
|
353
353
|
id: a.id,
|
|
354
|
-
ruleId: '
|
|
355
|
-
message: `
|
|
354
|
+
ruleId: 'ACTION-010',
|
|
355
|
+
message: `ACTION-010: action '${a.id}' end_date '${endRaw}' is before start_date '${startRaw}'.`,
|
|
356
356
|
});
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
* (elements/24-action.md §2), but `duration_days` is an accepted alias in
|
|
364
|
-
* this codebase's document-form validator (activities/validate.ts) and is
|
|
365
|
-
* the field acme_corp's own ACTION elements actually use. */
|
|
366
|
-
function checkActionNegativeNumbers(actions: ChainElement[], findings: RepoFinding[]): void {
|
|
367
|
-
const fields = ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score'] as const;
|
|
361
|
+
/** Shared ACTION type and versioned numeric checks. */
|
|
362
|
+
function checkActionFields(actions: ChainElement[], findings: RepoFinding[], version?: string): void {
|
|
368
363
|
for (const a of actions) {
|
|
369
|
-
for (const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
findings.push({
|
|
373
|
-
scope: PScope,
|
|
374
|
-
id: a.id,
|
|
375
|
-
ruleId: 'ACT-009',
|
|
376
|
-
message: `ACT-009: action '${a.id}' field '${field}' is negative (${v}).`,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
364
|
+
for (const error of validateActionFields(a.data, version)) {
|
|
365
|
+
findings.push({ scope: PScope, id: a.id, ruleId: error.code,
|
|
366
|
+
message: `${a.doc.path}: canonical action '${a.id}' ${error.message}` });
|
|
379
367
|
}
|
|
380
368
|
}
|
|
381
369
|
}
|
|
382
370
|
|
|
383
|
-
/**
|
|
371
|
+
/** DGCA-REPO-008..011 — inline strategy-chain cross-references must resolve within
|
|
384
372
|
* the repo: GOAL.factors -> DRIVER, CHANGE.goals -> GOAL, ACTION.goals ->
|
|
385
373
|
* GOAL, ACTION.delivers_changes -> CHANGE (ELEMENT_PRIMITIVES.md §7.1-§7.4). */
|
|
386
374
|
function checkStrategyChainReferences(
|
|
@@ -400,8 +388,8 @@ function checkStrategyChainReferences(
|
|
|
400
388
|
findings.push({
|
|
401
389
|
scope: PScope,
|
|
402
390
|
id: g.id,
|
|
403
|
-
ruleId: '
|
|
404
|
-
message: `
|
|
391
|
+
ruleId: 'DGCA-REPO-008',
|
|
392
|
+
message: `DGCA-REPO-008: goal '${g.id}' references undefined driver '${f}'.`,
|
|
405
393
|
});
|
|
406
394
|
}
|
|
407
395
|
}
|
|
@@ -412,8 +400,8 @@ function checkStrategyChainReferences(
|
|
|
412
400
|
findings.push({
|
|
413
401
|
scope: PScope,
|
|
414
402
|
id: c.id,
|
|
415
|
-
ruleId: '
|
|
416
|
-
message: `
|
|
403
|
+
ruleId: 'DGCA-REPO-009',
|
|
404
|
+
message: `DGCA-REPO-009: change '${c.id}' references undefined goal '${g}'.`,
|
|
417
405
|
});
|
|
418
406
|
}
|
|
419
407
|
}
|
|
@@ -424,8 +412,8 @@ function checkStrategyChainReferences(
|
|
|
424
412
|
findings.push({
|
|
425
413
|
scope: PScope,
|
|
426
414
|
id: a.id,
|
|
427
|
-
ruleId: '
|
|
428
|
-
message: `
|
|
415
|
+
ruleId: 'DGCA-REPO-010',
|
|
416
|
+
message: `DGCA-REPO-010: action '${a.id}' references undefined change '${c}'.`,
|
|
429
417
|
});
|
|
430
418
|
}
|
|
431
419
|
}
|
|
@@ -434,8 +422,8 @@ function checkStrategyChainReferences(
|
|
|
434
422
|
findings.push({
|
|
435
423
|
scope: PScope,
|
|
436
424
|
id: a.id,
|
|
437
|
-
ruleId: '
|
|
438
|
-
message: `
|
|
425
|
+
ruleId: 'DGCA-REPO-011',
|
|
426
|
+
message: `DGCA-REPO-011: action '${a.id}' references undefined goal '${g}'.`,
|
|
439
427
|
});
|
|
440
428
|
}
|
|
441
429
|
}
|
|
@@ -505,8 +493,8 @@ function checkStrategyChainOrphans(
|
|
|
505
493
|
}
|
|
506
494
|
|
|
507
495
|
/**
|
|
508
|
-
* Run the strategy-chain semantic checks (GOALS-009..011,
|
|
509
|
-
*
|
|
496
|
+
* Run the strategy-chain semantic checks (GOALS-009..011, ACTION-007..011,
|
|
497
|
+
* DGCA-REPO-008..014 except GOALS-008 — see the module header) over the loaded
|
|
510
498
|
* element set and append findings. Called from `validateRepoModel` after the
|
|
511
499
|
* structural phases. Pure, deterministic order.
|
|
512
500
|
*/
|
|
@@ -522,7 +510,24 @@ export function checkStrategyChainSemantics(input: RepoModelInput, findings: Rep
|
|
|
522
510
|
checkActionPredecessorCycle(actions, findings);
|
|
523
511
|
checkActionOrphanReferences(actions, findings);
|
|
524
512
|
checkActionDates(actions, findings);
|
|
525
|
-
|
|
513
|
+
checkActionFields(actions, findings, input.methodologyVersion);
|
|
526
514
|
checkStrategyChainReferences(goals, actions, drivers, changes, findings);
|
|
527
|
-
checkStrategyChainOrphans(goals, actions, drivers, changes, findings);
|
|
515
|
+
if (!usesCoverageObservations(input)) checkStrategyChainOrphans(goals, actions, drivers, changes, findings);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
function usesCoverageObservations(input: RepoModelInput): boolean {
|
|
520
|
+
return /^\d+\./.test(input.methodologyVersion ?? '') && Number(input.methodologyVersion!.split('.')[0]) >= 5;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Removed FGCA orphan rules become visible coverage observations, not new aliases. */
|
|
524
|
+
export function collectStrategyCoverageObservations(input: RepoModelInput): Array<{id: string; kind: 'unreferenced'; message: string}> {
|
|
525
|
+
if (!usesCoverageObservations(input)) return [];
|
|
526
|
+
const findings: RepoFinding[] = [];
|
|
527
|
+
checkStrategyChainOrphans(
|
|
528
|
+
collectByNotation(input.elements, isGoalNotation),
|
|
529
|
+
collectByNotation(input.elements, isActionNotation),
|
|
530
|
+
collectByNotation(input.elements, isDriverNotation),
|
|
531
|
+
collectByNotation(input.elements, isChangeNotation), findings);
|
|
532
|
+
return findings.map(f => ({ id: f.id, kind: 'unreferenced', message: f.message.replace(/^FGCA-01[234]: /, '') }));
|
|
528
533
|
}
|
|
@@ -74,6 +74,8 @@ export interface RepoDoc {
|
|
|
74
74
|
* canon zone exactly as `lint.py` partitions them (`canon/elements/**` vs
|
|
75
75
|
* `canon/relations/**`). */
|
|
76
76
|
export interface RepoModelInput {
|
|
77
|
+
/** Catalogue manifest version; document spec_version never selects compatibility. */
|
|
78
|
+
methodologyVersion?: string;
|
|
77
79
|
/** Documents found under `canon/elements/**`. */
|
|
78
80
|
elements: RepoDoc[];
|
|
79
81
|
/** Documents found under `canon/relations/**`. */
|
|
@@ -135,6 +135,7 @@ function checkReferentialIntegrity(input: RepoModelInput, findings: RepoFinding[
|
|
|
135
135
|
findings.push({
|
|
136
136
|
scope: PScope,
|
|
137
137
|
id: relId,
|
|
138
|
+
ruleId: 'REL-002',
|
|
138
139
|
message: `Referential integrity: relation '${relId || doc.path}' endpoint '${fromId}' (from) does not resolve to a known element.`,
|
|
139
140
|
});
|
|
140
141
|
}
|
|
@@ -142,6 +143,7 @@ function checkReferentialIntegrity(input: RepoModelInput, findings: RepoFinding[
|
|
|
142
143
|
findings.push({
|
|
143
144
|
scope: PScope,
|
|
144
145
|
id: relId,
|
|
146
|
+
ruleId: 'REL-002',
|
|
145
147
|
message: `Referential integrity: relation '${relId || doc.path}' endpoint '${toId}' (to) does not resolve to a known element.`,
|
|
146
148
|
});
|
|
147
149
|
}
|
package/src/risk/index.ts
CHANGED
package/src/risk/types.ts
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
import type { GateChecks } from '../requirement/types.js';
|
|
5
5
|
|
|
6
6
|
/** Likelihood / impact / residual severity vocabulary (§7.26). */
|
|
7
|
-
export type RiskLevel = 'low' | 'medium' | 'high';
|
|
7
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | number;
|
|
8
|
+
|
|
9
|
+
export interface RiskScale {
|
|
10
|
+
id: string;
|
|
11
|
+
min: number;
|
|
12
|
+
max: number;
|
|
13
|
+
direction: 'higher' | 'lower';
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
export const RISK_LEVELS: readonly RiskLevel[] = ['low', 'medium', 'high'];
|
|
10
17
|
|
|
@@ -12,6 +19,7 @@ export interface Risk {
|
|
|
12
19
|
notation: 'risk';
|
|
13
20
|
id: string;
|
|
14
21
|
name: string;
|
|
22
|
+
risk_scale?: RiskScale;
|
|
15
23
|
likelihood: RiskLevel;
|
|
16
24
|
impact: RiskLevel;
|
|
17
25
|
residual: RiskLevel;
|
package/src/risk/validate.ts
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).
|
|
@@ -62,13 +62,27 @@ export function validateRisk(input: unknown, options: RiskValidateOptions = {}):
|
|
|
62
62
|
errors.push({ code: 'RISK-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
|
|
65
|
+
// Authored numeric degrees use one explicitly declared adopter scale.
|
|
66
|
+
const fields = ['likelihood', 'impact', 'residual'] as const;
|
|
67
|
+
const scale = r.risk_scale as Record<string, unknown> | undefined;
|
|
68
|
+
const finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
|
|
69
|
+
const validScale = scale !== null && typeof scale === 'object' && !Array.isArray(scale)
|
|
70
|
+
&& typeof scale.id === 'string' && scale.id.trim().length > 0
|
|
71
|
+
&& finite(scale.min) && finite(scale.max) && scale.min < scale.max
|
|
72
|
+
&& (scale.direction === 'higher' || scale.direction === 'lower');
|
|
73
|
+
if ((scale !== undefined || fields.some(f => typeof r[f] === 'number')) && !validScale) {
|
|
74
|
+
errors.push({ code: 'RISK-002', message: 'risk_scale requires id, finite min < max, and direction higher or lower.', path: 'risk_scale' });
|
|
75
|
+
}
|
|
76
|
+
for (const f of fields) {
|
|
67
77
|
const v = r[f];
|
|
68
78
|
if (v === undefined || v === null || v === '') {
|
|
69
79
|
errors.push({ code: 'RISK-001', message: `${f} is required.`, path: f });
|
|
70
|
-
} else if (
|
|
71
|
-
|
|
80
|
+
} else if (typeof v === 'number') {
|
|
81
|
+
if (!finite(v) || (validScale && (v < (scale!.min as number) || v > (scale!.max as number)))) {
|
|
82
|
+
errors.push({ code: 'RISK-002', message: `${f} must be finite and within risk_scale bounds.`, path: f });
|
|
83
|
+
}
|
|
84
|
+
} else if (!(RISK_LEVELS as readonly unknown[]).includes(v)) {
|
|
85
|
+
errors.push({ code: 'RISK-002', message: `${f} must be low, medium, high, or a number with risk_scale.`, path: f });
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
|
|
@@ -38,6 +38,11 @@ export function validateScenario(input: unknown): ValidationResult {
|
|
|
38
38
|
}
|
|
39
39
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
40
40
|
|
|
41
|
+
if (('view_config' in raw || 'view' in raw || 'scenarios' in raw) && !('scenario' in raw)) {
|
|
42
|
+
return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
|
|
43
|
+
message: 'scenarios projection or scenario-set form is unsupported and remains unvalidated.' }] };
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
const scn = raw['scenario'];
|
|
42
47
|
if (!scn || typeof scn !== 'object') {
|
|
43
48
|
errors.push({ code: 'SCN-002', message: 'Missing required field: scenario' });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ValidationError } from './validation-types.js';
|
|
2
|
+
|
|
3
|
+
/** Shared schema identity; callers supply the owning form and exact field. */
|
|
4
|
+
export function schemaFinding(form: string, path: string, expected: string, actual: unknown): ValidationError {
|
|
5
|
+
const type = actual === null ? 'null' : Array.isArray(actual) ? 'array' : typeof actual;
|
|
6
|
+
return { code: 'SCHEMA_INVALID', path,
|
|
7
|
+
message: `${form} field '${path}': expected ${expected}; actual ${type} (${String(actual)}).` };
|
|
8
|
+
}
|
package/src/webview/entry.ts
CHANGED
|
@@ -275,10 +275,14 @@ function dispatchValidate(kind: NotationKind, doc: unknown): RenderResult {
|
|
|
275
275
|
return renderFromValidation('scenarios', validateScenario(doc), () =>
|
|
276
276
|
renderScenarioHtml((doc as ScenarioFile).scenario),
|
|
277
277
|
);
|
|
278
|
-
case 'capability-map':
|
|
279
|
-
|
|
278
|
+
case 'capability-map': {
|
|
279
|
+
const validation = validateCapabilityMap(doc);
|
|
280
|
+
const unresolved = validation.warnings.find(w => w.code === 'NOTATION-SKIP-001');
|
|
281
|
+
if (validation.valid && unresolved) return errorResult('capability-map', unresolved.code, unresolved.message);
|
|
282
|
+
return renderFromValidation('capability-map', validation, () =>
|
|
280
283
|
renderCapabilityMapHtml((doc as CapabilityMapFile).capability_map),
|
|
281
284
|
);
|
|
285
|
+
}
|
|
282
286
|
default:
|
|
283
287
|
return errorResult(
|
|
284
288
|
kind,
|
|
@@ -56,7 +56,8 @@ export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {})
|
|
|
56
56
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
57
57
|
: '';
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// Reserve a separate header row; the theme centers text on its y coordinate.
|
|
60
|
+
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, edgeStyle, title, topInset: title ? PAD : 0, embedCssTheme: 'transitrix' });
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export interface RenderGoalsLayoutOptions {
|
package/src/webview/styles.css
CHANGED
|
@@ -70,6 +70,14 @@ body {
|
|
|
70
70
|
margin-left: 6px;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* The bundled SVG renderer uses its light palette independently of IDE chrome.
|
|
74
|
+
* Give transparent SVG titles and edges a matching canvas. */
|
|
75
|
+
.tx-svg-host {
|
|
76
|
+
background: #ffffff;
|
|
77
|
+
color: #0d2b35;
|
|
78
|
+
min-height: calc(100vh - 24px);
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
.tx-svg-host svg {
|
|
74
82
|
max-width: 100%;
|
|
75
83
|
height: auto;
|