@transitrix/diagrams 1.9.18 → 1.11.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/resolver.d.ts +3 -0
- package/dist/activities/resolver.js +39 -18
- package/dist/activities/validate.js +59 -0
- package/dist/canon-resolver-utils.d.ts +3 -0
- package/dist/canon-resolver-utils.js +16 -0
- package/dist/codex/index.d.ts +1 -1
- package/dist/codex/index.js +1 -1
- package/dist/codex/types.d.ts +9 -2
- package/dist/codex/types.js +10 -3
- package/dist/codex/validate.js +29 -3
- package/dist/compliance/classify.d.ts +11 -1
- package/dist/compliance/classify.js +31 -17
- package/dist/compliance/impact.d.ts +3 -1
- package/dist/compliance/impact.js +41 -13
- package/dist/compliance/types.d.ts +6 -0
- package/dist/compliance-matrix/build.js +0 -0
- package/dist/compliance-verification-matrix/build.d.ts +17 -0
- package/dist/compliance-verification-matrix/build.js +65 -0
- package/dist/compliance-verification-matrix/csv.d.ts +6 -0
- package/dist/compliance-verification-matrix/csv.js +41 -0
- package/dist/compliance-verification-matrix/index.d.ts +3 -0
- package/dist/compliance-verification-matrix/index.js +2 -0
- package/dist/compliance-verification-matrix/types.d.ts +75 -0
- package/dist/compliance-verification-matrix/types.js +4 -0
- package/dist/fgca/action-change-ids.d.ts +7 -0
- package/dist/fgca/action-change-ids.js +11 -0
- package/dist/fgca/parse-canonical.d.ts +3 -1
- package/dist/fgca/parse-canonical.js +13 -8
- package/dist/fgca/resolver.d.ts +6 -2
- package/dist/fgca/resolver.js +12 -6
- package/dist/fgca/validate.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/process-blueprint/index.d.ts +3 -1
- package/dist/process-blueprint/index.js +1 -0
- package/dist/process-blueprint/layout.js +33 -29
- package/dist/process-blueprint/resolve-columns.d.ts +52 -0
- package/dist/process-blueprint/resolve-columns.js +138 -0
- package/dist/process-blueprint/types.d.ts +23 -5
- package/dist/process-blueprint/validate.d.ts +19 -1
- package/dist/process-blueprint/validate.js +73 -21
- package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
- package/dist/repo-validate/check-candidate-fields.js +87 -0
- package/dist/repo-validate/check-process-parent.d.ts +8 -0
- package/dist/repo-validate/check-process-parent.js +172 -0
- package/dist/repo-validate/index.d.ts +1 -0
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/validate-repo.js +7 -0
- package/dist/requirement/types.d.ts +1 -1
- package/dist/requirement/types.js +1 -1
- package/dist/webview/entry.js +11 -4
- package/dist/webview/render-fgca.js +1 -1
- package/dist/webview/render-process.d.ts +19 -1
- package/dist/webview/render-process.js +75 -42
- package/package.json +1 -1
- package/src/activities/resolver.ts +40 -20
- package/src/activities/validate.ts +58 -0
- package/src/canon-resolver-utils.ts +19 -0
- package/src/codex/index.ts +1 -1
- package/src/codex/types.ts +11 -3
- package/src/codex/validate.ts +30 -2
- package/src/compliance/classify.ts +40 -17
- package/src/compliance/impact.ts +41 -12
- package/src/compliance/types.ts +6 -0
- package/src/compliance-matrix/build.ts +0 -0
- package/src/compliance-verification-matrix/build.ts +84 -0
- package/src/compliance-verification-matrix/csv.ts +47 -0
- package/src/compliance-verification-matrix/index.ts +7 -0
- package/src/compliance-verification-matrix/types.ts +85 -0
- package/src/fgca/action-change-ids.ts +13 -0
- package/src/fgca/parse-canonical.ts +12 -8
- package/src/fgca/resolver.ts +15 -6
- package/src/fgca/validate.ts +2 -0
- package/src/index.ts +1 -0
- package/src/process-blueprint/index.ts +11 -0
- package/src/process-blueprint/layout.ts +48 -29
- package/src/process-blueprint/resolve-columns.ts +147 -0
- package/src/process-blueprint/types.ts +19 -5
- package/src/process-blueprint/validate.ts +107 -21
- package/src/repo-validate/check-candidate-fields.ts +93 -0
- package/src/repo-validate/check-process-parent.ts +184 -0
- package/src/repo-validate/index.ts +5 -0
- package/src/repo-validate/validate-repo.ts +7 -0
- package/src/requirement/types.ts +1 -1
- package/src/webview/entry.ts +13 -6
- package/src/webview/render-fgca.ts +1 -1
- package/src/webview/render-process.ts +94 -49
|
@@ -39,6 +39,9 @@ export interface ActionCanonSources {
|
|
|
39
39
|
* to each element's inline `goals[]`. */
|
|
40
40
|
relations?: unknown[];
|
|
41
41
|
}
|
|
42
|
+
/** In-memory bag of ACTION ids dropped by `root_action` scoping. Consumed by
|
|
43
|
+
* `validateActivities` as `ACT-021`. Not part of the notation. */
|
|
44
|
+
export declare const ACT_021_OMITTED_KEY = "__act021_omitted";
|
|
42
45
|
/**
|
|
43
46
|
* Returns true when the parsed view document uses the canon-projection form
|
|
44
47
|
* (`view_config` key present, no inline `actions[]`). Used by the preview and
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* field name; `type` is the canonical element field per elements/24-action.md
|
|
12
12
|
* §2). Filesystem-free — callable from unit tests without vscode.
|
|
13
13
|
*/
|
|
14
|
-
import { isObject, str, strArray,
|
|
14
|
+
import { isObject, str, strArray, stripEnvelope, reachesRootViaParent } from '../canon-resolver-utils.js';
|
|
15
|
+
/** In-memory bag of ACTION ids dropped by `root_action` scoping. Consumed by
|
|
16
|
+
* `validateActivities` as `ACT-021`. Not part of the notation. */
|
|
17
|
+
export const ACT_021_OMITTED_KEY = '__act021_omitted';
|
|
15
18
|
/** ACTION elements by id. `notation: action` is canonical; `notation: activity`
|
|
16
19
|
* is the deprecated pre-2026-06-25 alias (elements/24-action.md §"Deprecated
|
|
17
20
|
* alias") — accepted the same way `collectByNotation` callers elsewhere do. */
|
|
@@ -93,39 +96,32 @@ export function resolveAction(viewDoc, sources) {
|
|
|
93
96
|
const relGoals = activeActionGoalRelations(relations, id);
|
|
94
97
|
return relGoals.length > 0 ? relGoals : strArray(el['goals']);
|
|
95
98
|
}
|
|
96
|
-
// 1.
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (rootAction) {
|
|
102
|
-
candidateIds = allActions.has(rootAction) ? descendantsOf(rootAction, allActions) : new Set();
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
candidateIds = new Set(allActions.keys());
|
|
106
|
-
}
|
|
107
|
-
// 2. Narrow by goals — only actions linked to at least one listed GOAL
|
|
99
|
+
// 1. Other scope fields first — the set the view would otherwise select
|
|
100
|
+
// (07-action.md §5.1). `root_action` then omits (and warns) rather than
|
|
101
|
+
// silently intersecting first.
|
|
102
|
+
let otherwiseIds = new Set(allActions.keys());
|
|
103
|
+
// Narrow by goals — only actions linked to at least one listed GOAL
|
|
108
104
|
// (via an active action_goal relation, or inline goals[] when no relation
|
|
109
105
|
// exists for that action).
|
|
110
106
|
const goalsFilter = strArray(scope['goals']);
|
|
111
107
|
if (goalsFilter.length > 0) {
|
|
112
|
-
|
|
108
|
+
otherwiseIds = new Set([...otherwiseIds].filter((id) => effectiveGoals(id, allActions.get(id)).some((g) => goalsFilter.includes(g))));
|
|
113
109
|
}
|
|
114
|
-
//
|
|
110
|
+
// Narrow by type_filter — only actions of a listed type.
|
|
115
111
|
const typeFilter = strArray(scope['type_filter']);
|
|
116
112
|
if (typeFilter.length > 0) {
|
|
117
|
-
|
|
113
|
+
otherwiseIds = new Set([...otherwiseIds].filter((id) => {
|
|
118
114
|
const el = allActions.get(id);
|
|
119
115
|
const t = str(el?.['type']) ?? str(el?.['activity_type']);
|
|
120
116
|
return t !== undefined && typeFilter.includes(t);
|
|
121
117
|
}));
|
|
122
118
|
}
|
|
123
|
-
//
|
|
119
|
+
// Narrow by valid_at — only actions in effect on that date. Actions with
|
|
124
120
|
// no valid_from are not lifecycle-tracked and are kept (permissive default,
|
|
125
121
|
// matching the resolver's general "missing = not excluded" stance).
|
|
126
122
|
const validAt = str(scope['valid_at']);
|
|
127
123
|
if (validAt) {
|
|
128
|
-
|
|
124
|
+
otherwiseIds = new Set([...otherwiseIds].filter((id) => {
|
|
129
125
|
const el = allActions.get(id);
|
|
130
126
|
const from = str(el?.['valid_from']);
|
|
131
127
|
const to = str(el?.['valid_to']);
|
|
@@ -136,6 +132,29 @@ export function resolveAction(viewDoc, sources) {
|
|
|
136
132
|
return true;
|
|
137
133
|
}));
|
|
138
134
|
}
|
|
135
|
+
const rootAction = str(scope['root_action']);
|
|
136
|
+
let candidateIds;
|
|
137
|
+
const omittedOutsideRoot = [];
|
|
138
|
+
if (rootAction) {
|
|
139
|
+
if (!allActions.has(rootAction)) {
|
|
140
|
+
candidateIds = new Set();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
candidateIds = new Set();
|
|
144
|
+
for (const id of otherwiseIds) {
|
|
145
|
+
if (reachesRootViaParent(id, rootAction, allActions)) {
|
|
146
|
+
candidateIds.add(id);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
omittedOutsideRoot.push(id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
omittedOutsideRoot.sort();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
candidateIds = otherwiseIds;
|
|
157
|
+
}
|
|
139
158
|
// Element fields map onto Activity fields unchanged, except the canonical
|
|
140
159
|
// `type` (elements/24-action.md §2) which becomes the internal `activity_type`
|
|
141
160
|
// (types.ts `Activity.activity_type`), and `goals` which is resolved via
|
|
@@ -168,5 +187,7 @@ export function resolveAction(viewDoc, sources) {
|
|
|
168
187
|
};
|
|
169
188
|
if (Object.keys(project).length > 0)
|
|
170
189
|
out['project'] = project;
|
|
190
|
+
if (omittedOutsideRoot.length > 0)
|
|
191
|
+
out[ACT_021_OMITTED_KEY] = { root: rootAction, ids: omittedOutsideRoot };
|
|
171
192
|
return out;
|
|
172
193
|
}
|
|
@@ -1,5 +1,59 @@
|
|
|
1
|
+
import { isObject, str, reachesRootViaParent } from '../canon-resolver-utils.js';
|
|
2
|
+
import { ACT_021_OMITTED_KEY } from './resolver.js';
|
|
1
3
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2
4
|
const VALID_WEEKDAYS = new Set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
|
|
5
|
+
function act021Message(actionId, rootId) {
|
|
6
|
+
return `ACTION "${actionId}" is not "${rootId}" and is not reachable from it via parent`;
|
|
7
|
+
}
|
|
8
|
+
function collectAct021FromProjection(raw) {
|
|
9
|
+
const bag = raw[ACT_021_OMITTED_KEY];
|
|
10
|
+
delete raw[ACT_021_OMITTED_KEY];
|
|
11
|
+
if (!isObject(bag))
|
|
12
|
+
return [];
|
|
13
|
+
const root = str(bag['root']);
|
|
14
|
+
const ids = Array.isArray(bag['ids']) ? bag['ids'] : [];
|
|
15
|
+
if (!root)
|
|
16
|
+
return [];
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const id of ids) {
|
|
19
|
+
if (typeof id !== 'string' || !id.trim())
|
|
20
|
+
continue;
|
|
21
|
+
out.push({
|
|
22
|
+
code: 'ACT-021',
|
|
23
|
+
message: act021Message(id, root),
|
|
24
|
+
path: 'view_config.scope.root_action',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function collectAct021FromAuthored(raw, actions) {
|
|
30
|
+
const vc = isObject(raw['view_config']) ? raw['view_config'] : undefined;
|
|
31
|
+
const scope = vc && isObject(vc['scope']) ? vc['scope'] : undefined;
|
|
32
|
+
const root = scope ? str(scope['root_action']) : undefined;
|
|
33
|
+
if (!root)
|
|
34
|
+
return [];
|
|
35
|
+
const byId = new Map();
|
|
36
|
+
for (const el of actions) {
|
|
37
|
+
if (!isObject(el))
|
|
38
|
+
continue;
|
|
39
|
+
const id = str(el['id']);
|
|
40
|
+
if (id)
|
|
41
|
+
byId.set(id, el);
|
|
42
|
+
}
|
|
43
|
+
if (!byId.has(root))
|
|
44
|
+
return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const id of byId.keys()) {
|
|
47
|
+
if (reachesRootViaParent(id, root, byId))
|
|
48
|
+
continue;
|
|
49
|
+
out.push({
|
|
50
|
+
code: 'ACT-021',
|
|
51
|
+
message: act021Message(id, root),
|
|
52
|
+
path: 'view_config.scope.root_action',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
3
57
|
export function validateActivities(input) {
|
|
4
58
|
const errors = [];
|
|
5
59
|
const warnings = [];
|
|
@@ -33,6 +87,11 @@ export function validateActivities(input) {
|
|
|
33
87
|
// Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
|
|
34
88
|
if (!Array.isArray(raw.activities))
|
|
35
89
|
raw.activities = raw.actions;
|
|
90
|
+
const fromProjection = collectAct021FromProjection(raw);
|
|
91
|
+
warnings.push(...fromProjection);
|
|
92
|
+
if (fromProjection.length === 0) {
|
|
93
|
+
warnings.push(...collectAct021FromAuthored(raw, rawArray));
|
|
94
|
+
}
|
|
36
95
|
// ACT-014 / ACT-015: project.calendar validation (run before per-activity loop;
|
|
37
96
|
// surfaces clearly even when activities also have issues).
|
|
38
97
|
if (raw.project && typeof raw.project === 'object') {
|
|
@@ -13,6 +13,9 @@ export declare function strArray(v: unknown): string[];
|
|
|
13
13
|
* per the element map's own `parent` field. Used to scope a projection to a
|
|
14
14
|
* root element and its transitive descendants (WBS / goal hierarchy / etc). */
|
|
15
15
|
export declare function descendantsOf(rootId: string, all: Map<string, Record<string, unknown>>): Set<string>;
|
|
16
|
+
/** Walk `parent` from `id` toward its ancestors until `rootId` or the chain
|
|
17
|
+
* ends. Cycle-safe: a cycle is not a path to the root. */
|
|
18
|
+
export declare function reachesRootViaParent(id: string, rootId: string, all: Map<string, Record<string, unknown>>): boolean;
|
|
16
19
|
/** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
|
|
17
20
|
* adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
|
|
18
21
|
* memoized across calls via the shared `memo` map. */
|
|
@@ -45,6 +45,22 @@ export function descendantsOf(rootId, all) {
|
|
|
45
45
|
}
|
|
46
46
|
return result;
|
|
47
47
|
}
|
|
48
|
+
/** Walk `parent` from `id` toward its ancestors until `rootId` or the chain
|
|
49
|
+
* ends. Cycle-safe: a cycle is not a path to the root. */
|
|
50
|
+
export function reachesRootViaParent(id, rootId, all) {
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
let current = id;
|
|
53
|
+
while (current) {
|
|
54
|
+
if (current === rootId)
|
|
55
|
+
return true;
|
|
56
|
+
if (seen.has(current))
|
|
57
|
+
return false;
|
|
58
|
+
seen.add(current);
|
|
59
|
+
const el = all.get(current);
|
|
60
|
+
current = el ? str(el['parent']) : undefined;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
48
64
|
/** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
|
|
49
65
|
* adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
|
|
50
66
|
* memoized across calls via the shared `memo` map. */
|
package/dist/codex/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
|
|
1
|
+
export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, PRINCIPLE_CODEX_TYPES } from './types.js';
|
|
2
2
|
export type { CodexArtefactType } from './types.js';
|
|
3
3
|
export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
|
|
4
4
|
export type { CodexValidateOptions } from './validate.js';
|
package/dist/codex/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
|
|
1
|
+
export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, PRINCIPLE_CODEX_TYPES } from './types.js';
|
|
2
2
|
export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
|
package/dist/codex/types.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
|
|
2
|
-
export declare const CODEX_ARTEFACT_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD"];
|
|
2
|
+
export declare const CODEX_ARTEFACT_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD", "PRINCIPLE"];
|
|
3
3
|
export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
|
|
4
4
|
/** External codex artefacts (codex/external/<jurisdiction>/). */
|
|
5
5
|
export declare const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
|
|
6
|
-
/** Internal codex artefacts (codex/internal/). */
|
|
6
|
+
/** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
|
|
7
7
|
export declare const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
|
|
8
|
+
/**
|
|
9
|
+
* Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
|
|
10
|
+
* `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
|
|
11
|
+
* optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
|
|
12
|
+
* differs; both share the same file location.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PRINCIPLE_CODEX_TYPES: readonly CodexArtefactType[];
|
package/dist/codex/types.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
// Codex artefact — external laws/regulations and internal policies/standards.
|
|
1
|
+
// Codex artefact — external laws/regulations and internal policies/standards/principles.
|
|
2
2
|
// Schema: methodology notations/elements/14-codex.md §3–§4.
|
|
3
3
|
/** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
|
|
4
|
-
export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
|
|
4
|
+
export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'];
|
|
5
5
|
/** External codex artefacts (codex/external/<jurisdiction>/). */
|
|
6
6
|
export const EXTERNAL_CODEX_TYPES = ['LAW', 'REGULATION'];
|
|
7
|
-
/** Internal codex artefacts (codex/internal/). */
|
|
7
|
+
/** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
|
|
8
8
|
export const INTERNAL_CODEX_TYPES = ['POLICY', 'INTERNAL_STANDARD'];
|
|
9
|
+
/**
|
|
10
|
+
* Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
|
|
11
|
+
* `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
|
|
12
|
+
* optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
|
|
13
|
+
* differs; both share the same file location.
|
|
14
|
+
*/
|
|
15
|
+
export const PRINCIPLE_CODEX_TYPES = ['PRINCIPLE'];
|
package/dist/codex/validate.js
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
// CODEX-001 — shape, zone, id grammar, admission envelope.
|
|
4
4
|
// CODEX-002 — `type` field matches the id TYPE prefix when present.
|
|
5
5
|
// CODEX-003 — external artefact frontmatter (jurisdiction, effective_date).
|
|
6
|
-
// CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date
|
|
6
|
+
// CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date for
|
|
7
|
+
// POLICY/INTERNAL_STANDARD; statement, rationale for PRINCIPLE — §4.1).
|
|
7
8
|
// CODEX-005 — jurisdiction must match the parent folder under codex/external/
|
|
8
9
|
// (when `folderJurisdiction` is supplied by the repo sweep).
|
|
10
|
+
// CODEX-006 — a PRINCIPLE artefact does not declare `established_by` (§4.1). The spec
|
|
11
|
+
// marks this "info" severity; this module has no info tier, so it is
|
|
12
|
+
// surfaced as a warning — the nearest existing severity that does not fail
|
|
13
|
+
// `valid`.
|
|
14
|
+
//
|
|
15
|
+
// Note: this file's CODEX-00x numbering predates the spec's own v0.2 renumbering
|
|
16
|
+
// (14-codex.md §6, §8.2) and does not track it code-for-code — e.g. the spec's current
|
|
17
|
+
// CODEX-001 is a folder/jurisdiction check this file assigns to CODEX-005. Realigning the
|
|
18
|
+
// full scheme is out of scope here; this change only adds the purely-additive PRINCIPLE
|
|
19
|
+
// TYPE (§4.1) under the existing local numbering.
|
|
9
20
|
import { isCanonicalId, typeOfId } from '../typed-id.js';
|
|
10
|
-
import { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, } from './types.js';
|
|
21
|
+
import { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, PRINCIPLE_CODEX_TYPES, } from './types.js';
|
|
11
22
|
function isCodexType(type) {
|
|
12
23
|
return type !== null && CODEX_ARTEFACT_TYPES.includes(type);
|
|
13
24
|
}
|
|
@@ -31,7 +42,7 @@ export function validateCodex(input, options = {}) {
|
|
|
31
42
|
if (!isCanonicalId(c.id) || !isCodexType(idType)) {
|
|
32
43
|
errors.push({
|
|
33
44
|
code: 'CODEX-001',
|
|
34
|
-
message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or
|
|
45
|
+
message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, INTERNAL_STANDARD, or PRINCIPLE).`,
|
|
35
46
|
path: 'id',
|
|
36
47
|
});
|
|
37
48
|
}
|
|
@@ -84,6 +95,21 @@ export function validateCodex(input, options = {}) {
|
|
|
84
95
|
errors.push({ code: 'CODEX-004', message: 'effective_date is required for internal codex artefacts.', path: 'effective_date' });
|
|
85
96
|
}
|
|
86
97
|
}
|
|
98
|
+
if (artefactType && PRINCIPLE_CODEX_TYPES.includes(artefactType)) {
|
|
99
|
+
if (typeof c.statement !== 'string' || c.statement.trim() === '') {
|
|
100
|
+
errors.push({ code: 'CODEX-004', message: 'statement is required for a PRINCIPLE artefact.', path: 'statement' });
|
|
101
|
+
}
|
|
102
|
+
if (typeof c.rationale !== 'string' || c.rationale.trim() === '') {
|
|
103
|
+
errors.push({ code: 'CODEX-004', message: 'rationale is required for a PRINCIPLE artefact.', path: 'rationale' });
|
|
104
|
+
}
|
|
105
|
+
if (typeof c.established_by !== 'string' || c.established_by.trim() === '') {
|
|
106
|
+
warnings.push({
|
|
107
|
+
code: 'CODEX-006',
|
|
108
|
+
message: 'established_by is not declared on this PRINCIPLE artefact — admissible, but a review finding for whoever admits it.',
|
|
109
|
+
path: 'established_by',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
87
113
|
return { valid: errors.length === 0, errors, warnings };
|
|
88
114
|
}
|
|
89
115
|
/** True when a parsed document is a codex-zone artefact candidate. */
|
|
@@ -30,6 +30,14 @@ export interface ComplianceCanon {
|
|
|
30
30
|
/** VALIDATION artefacts (28-validation.md) — the validation-domain peer of
|
|
31
31
|
* `verifications`, anchored on NEED instead of REQUIREMENT. */
|
|
32
32
|
validations: IndexValidation[];
|
|
33
|
+
/**
|
|
34
|
+
* Ids rejected by `ingestComplianceDoc` because that id was already present
|
|
35
|
+
* in the same bucket (two documents claiming one id — transitrix-hq#218).
|
|
36
|
+
* The second and any later document is dropped rather than silently
|
|
37
|
+
* duplicating a matrix column; callers that scan a filesystem can surface
|
|
38
|
+
* this list as a diagnostic.
|
|
39
|
+
*/
|
|
40
|
+
duplicateIds: string[];
|
|
33
41
|
}
|
|
34
42
|
export declare function emptyCanon(): ComplianceCanon;
|
|
35
43
|
/**
|
|
@@ -37,6 +45,8 @@ export declare function emptyCanon(): ComplianceCanon;
|
|
|
37
45
|
* pushes its projection into `canon`. Products / requirements / assertions are
|
|
38
46
|
* identified by their `notation` tag; codex source documents by `zone: codex`.
|
|
39
47
|
* Returns the artefact id when ingested (so the caller can record its path), or
|
|
40
|
-
* null when the document is not a (well-formed) compliance artefact
|
|
48
|
+
* null when the document is not a (well-formed) compliance artefact, or when
|
|
49
|
+
* its id duplicates one already ingested into the same bucket (see
|
|
50
|
+
* `canon.duplicateIds`).
|
|
41
51
|
*/
|
|
42
52
|
export declare function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null;
|
|
@@ -12,16 +12,33 @@ export function emptyCanon() {
|
|
|
12
12
|
subjects: [],
|
|
13
13
|
needs: [],
|
|
14
14
|
validations: [],
|
|
15
|
+
duplicateIds: [],
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
18
19
|
const strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Pushes `item` onto `list` unless `list` already holds an entry with the same
|
|
22
|
+
* id, in which case the id is recorded in `canon.duplicateIds` and the item is
|
|
23
|
+
* dropped — two documents claiming one id is a defect (transitrix-hq#218), not
|
|
24
|
+
* a second matrix column.
|
|
25
|
+
*/
|
|
26
|
+
function pushUnique(canon, list, item) {
|
|
27
|
+
if (list.some(existing => existing.id === item.id)) {
|
|
28
|
+
canon.duplicateIds.push(item.id);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
list.push(item);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
19
34
|
/**
|
|
20
35
|
* Classifies one parsed YAML document and, if it is a compliance artefact,
|
|
21
36
|
* pushes its projection into `canon`. Products / requirements / assertions are
|
|
22
37
|
* identified by their `notation` tag; codex source documents by `zone: codex`.
|
|
23
38
|
* Returns the artefact id when ingested (so the caller can record its path), or
|
|
24
|
-
* null when the document is not a (well-formed) compliance artefact
|
|
39
|
+
* null when the document is not a (well-formed) compliance artefact, or when
|
|
40
|
+
* its id duplicates one already ingested into the same bucket (see
|
|
41
|
+
* `canon.duplicateIds`).
|
|
25
42
|
*/
|
|
26
43
|
export function ingestComplianceDoc(canon, doc) {
|
|
27
44
|
if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
|
|
@@ -31,19 +48,17 @@ export function ingestComplianceDoc(canon, doc) {
|
|
|
31
48
|
if (!id)
|
|
32
49
|
return null;
|
|
33
50
|
if (d.notation === 'product') {
|
|
34
|
-
canon.products
|
|
35
|
-
return id;
|
|
51
|
+
return pushUnique(canon, canon.products, { id, name: str(d.name) ?? id }) ? id : null;
|
|
36
52
|
}
|
|
37
53
|
if (d.notation === 'capability' ||
|
|
38
54
|
d.notation === 'process' ||
|
|
39
55
|
d.notation === 'application' ||
|
|
40
56
|
d.notation === 'system') {
|
|
41
|
-
canon.subjects
|
|
42
|
-
return id;
|
|
57
|
+
return pushUnique(canon, canon.subjects, { id, name: str(d.name) ?? id }) ? id : null;
|
|
43
58
|
}
|
|
44
59
|
if (d.notation === 'requirement' || d.notation === 'constraint') {
|
|
45
60
|
const origin = str(d.origin);
|
|
46
|
-
canon.requirements
|
|
61
|
+
const ok = pushUnique(canon, canon.requirements, {
|
|
47
62
|
id,
|
|
48
63
|
name: str(d.name) ?? id,
|
|
49
64
|
severity: str(d.severity),
|
|
@@ -57,11 +72,10 @@ export function ingestComplianceDoc(canon, doc) {
|
|
|
57
72
|
next_review_at: str(d.next_review_at),
|
|
58
73
|
serves: str(d.serves),
|
|
59
74
|
});
|
|
60
|
-
return id;
|
|
75
|
+
return ok ? id : null;
|
|
61
76
|
}
|
|
62
77
|
if (d.notation === 'need') {
|
|
63
|
-
canon.needs
|
|
64
|
-
return id;
|
|
78
|
+
return pushUnique(canon, canon.needs, { id, name: str(d.name) ?? id }) ? id : null;
|
|
65
79
|
}
|
|
66
80
|
if (d.notation === 'validation') {
|
|
67
81
|
const validates = str(d.validates);
|
|
@@ -69,13 +83,13 @@ export function ingestComplianceDoc(canon, doc) {
|
|
|
69
83
|
const outcome = str(d.outcome);
|
|
70
84
|
if (!validates || !method || !outcome)
|
|
71
85
|
return null;
|
|
72
|
-
canon.validations
|
|
86
|
+
const ok = pushUnique(canon, canon.validations, {
|
|
73
87
|
id, validates, method, outcome,
|
|
74
88
|
performed_at: str(d.performed_at),
|
|
75
89
|
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
76
90
|
admitted_at: str(d.admitted_at),
|
|
77
91
|
});
|
|
78
|
-
return id;
|
|
92
|
+
return ok ? id : null;
|
|
79
93
|
}
|
|
80
94
|
if (d.notation === 'assertion') {
|
|
81
95
|
const about = str(d.about);
|
|
@@ -83,7 +97,7 @@ export function ingestComplianceDoc(canon, doc) {
|
|
|
83
97
|
const status = str(d.status);
|
|
84
98
|
if (!about || !subject || !status)
|
|
85
99
|
return null;
|
|
86
|
-
canon.assertions
|
|
100
|
+
const ok = pushUnique(canon, canon.assertions, {
|
|
87
101
|
id, about, subject, status,
|
|
88
102
|
assessed_at: str(d.assessed_at),
|
|
89
103
|
next_review_at: str(d.next_review_at),
|
|
@@ -92,7 +106,7 @@ export function ingestComplianceDoc(canon, doc) {
|
|
|
92
106
|
realised_via: strArray(d.realised_via),
|
|
93
107
|
owner_to_confirm: str(d.owner_to_confirm),
|
|
94
108
|
});
|
|
95
|
-
return id;
|
|
109
|
+
return ok ? id : null;
|
|
96
110
|
}
|
|
97
111
|
if (d.notation === 'verification') {
|
|
98
112
|
const verifies = str(d.verifies);
|
|
@@ -100,17 +114,17 @@ export function ingestComplianceDoc(canon, doc) {
|
|
|
100
114
|
const outcome = str(d.outcome);
|
|
101
115
|
if (!verifies || !method || !outcome)
|
|
102
116
|
return null;
|
|
103
|
-
canon.verifications
|
|
117
|
+
const ok = pushUnique(canon, canon.verifications, {
|
|
104
118
|
id, verifies, method, outcome,
|
|
119
|
+
protocol: str(d.protocol),
|
|
105
120
|
performed_at: str(d.performed_at),
|
|
106
121
|
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
107
122
|
admitted_at: str(d.admitted_at),
|
|
108
123
|
});
|
|
109
|
-
return id;
|
|
124
|
+
return ok ? id : null;
|
|
110
125
|
}
|
|
111
126
|
if (d.zone === 'codex') {
|
|
112
|
-
canon.codex
|
|
113
|
-
return id;
|
|
127
|
+
return pushUnique(canon, canon.codex, { id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) }) ? id : null;
|
|
114
128
|
}
|
|
115
129
|
return null;
|
|
116
130
|
}
|
|
@@ -277,7 +277,9 @@ export declare function computeDeadlineStatus(deadline: string | undefined, toda
|
|
|
277
277
|
* and `config.subjects.capabilities` (in that order). Subjects with no binding
|
|
278
278
|
* obligation still appear as columns so the gap is visible.
|
|
279
279
|
*/
|
|
280
|
-
export declare function buildImpactMatrix(canon: ComplianceCanon, config: ImpactViewConfig, objectDetails?: ObjectDetailInput[]
|
|
280
|
+
export declare function buildImpactMatrix(canon: ComplianceCanon, config: ImpactViewConfig, objectDetails?: ObjectDetailInput[], join?: {
|
|
281
|
+
stepHomeProcess?: ReadonlyMap<string, string>;
|
|
282
|
+
}): ImpactMatrix;
|
|
281
283
|
/**
|
|
282
284
|
* Extract stage-level `ObjectDetailInput` from a raw (YAML-parsed)
|
|
283
285
|
* process-blueprint document.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// (`product` columns × `obligation` rows). Stage / task grouping is planned
|
|
8
8
|
// for CV-3a and intentionally out of scope here.
|
|
9
9
|
import { buildComplianceIndex } from './reverse-index.js';
|
|
10
|
+
import { isStageColumnId } from '../process-blueprint/resolve-columns.js';
|
|
10
11
|
/**
|
|
11
12
|
* Repo-scope cross-reference checks for a parsed compliance-impact view config
|
|
12
13
|
* (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
|
|
@@ -390,25 +391,51 @@ function buildTaskColumns(config, objectDetails, isCombined) {
|
|
|
390
391
|
*
|
|
391
392
|
* At `'product'` grain: `a.subject` must match `col.subjectId`.
|
|
392
393
|
* At `'product-stage'` grain: `a.subject` must match AND either
|
|
393
|
-
* `a.realised_via` is absent (claim covers the whole subject) OR
|
|
394
|
-
* `col.stageId
|
|
395
|
-
*
|
|
396
|
-
* `
|
|
397
|
-
*
|
|
398
|
-
* in that stage.
|
|
394
|
+
* `a.realised_via` is absent (claim covers the whole subject) OR the token
|
|
395
|
+
* pins this column: a `PROCESS-…` id matching `col.stageId`, or a STEP whose
|
|
396
|
+
* home process is that column. Sketch `STAGE-…` ids are never a join key.
|
|
397
|
+
* At `'product-stage-task'` grain: a task-id hit covers that task column; a
|
|
398
|
+
* process-level claim covers every task column of that process.
|
|
399
399
|
*/
|
|
400
|
-
function assertionMatchesColumn(a, col) {
|
|
400
|
+
function assertionMatchesColumn(a, col, stepHomeProcess) {
|
|
401
401
|
if (a.subject !== col.subjectId)
|
|
402
402
|
return false;
|
|
403
403
|
if (!col.stageId)
|
|
404
404
|
return true;
|
|
405
405
|
if (!a.realised_via || a.realised_via.length === 0)
|
|
406
406
|
return true;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
const stageId = col.stageId;
|
|
408
|
+
if (isStageColumnId(stageId)) {
|
|
409
|
+
// Sketch column: only a task-id hit counts — never the STAGE- key.
|
|
410
|
+
return col.taskId != null && a.realised_via.includes(col.taskId);
|
|
410
411
|
}
|
|
411
|
-
|
|
412
|
+
for (const ref of a.realised_via) {
|
|
413
|
+
if (typeof ref !== 'string')
|
|
414
|
+
continue;
|
|
415
|
+
if (isStageColumnId(ref))
|
|
416
|
+
continue;
|
|
417
|
+
if (col.taskId && ref === col.taskId)
|
|
418
|
+
return true;
|
|
419
|
+
if (ref === stageId)
|
|
420
|
+
return true;
|
|
421
|
+
if (!col.taskId && stepHomeProcess?.get(ref) === stageId)
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
function stepHomeFromObjectDetails(objectDetails) {
|
|
427
|
+
const out = new Map();
|
|
428
|
+
if (!objectDetails)
|
|
429
|
+
return out;
|
|
430
|
+
for (const od of objectDetails) {
|
|
431
|
+
for (const d of od.details ?? []) {
|
|
432
|
+
for (const t of d.tasks ?? []) {
|
|
433
|
+
if (typeof t?.id === 'string' && t.id.length > 0)
|
|
434
|
+
out.set(t.id, d.id);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return out;
|
|
412
439
|
}
|
|
413
440
|
/**
|
|
414
441
|
* Build the matrix per §5.2.
|
|
@@ -430,7 +457,7 @@ function assertionMatchesColumn(a, col) {
|
|
|
430
457
|
* and `config.subjects.capabilities` (in that order). Subjects with no binding
|
|
431
458
|
* obligation still appear as columns so the gap is visible.
|
|
432
459
|
*/
|
|
433
|
-
export function buildImpactMatrix(canon, config, objectDetails) {
|
|
460
|
+
export function buildImpactMatrix(canon, config, objectDetails, join) {
|
|
434
461
|
const index = buildComplianceIndex({
|
|
435
462
|
requirements: canon.requirements,
|
|
436
463
|
assertions: canon.assertions,
|
|
@@ -485,6 +512,7 @@ export function buildImpactMatrix(canon, config, objectDetails) {
|
|
|
485
512
|
const allowedStatuses = new Set(config.status_display?.show ?? ['compliant', 'partial', 'non_compliant', 'under_review', 'pending_owner', 'n_a']);
|
|
486
513
|
const rowIndex = new Set(obligations.map(r => r.id));
|
|
487
514
|
const cells = obligations.map(() => columns.map(() => emptyCell()));
|
|
515
|
+
const stepHome = join?.stepHomeProcess ?? stepHomeFromObjectDetails(objectDetails);
|
|
488
516
|
for (let c = 0; c < columns.length; c++) {
|
|
489
517
|
const col = columns[c];
|
|
490
518
|
const subjectAssertions = index.assertionsBySubject.get(col.subjectId) ?? [];
|
|
@@ -493,7 +521,7 @@ export function buildImpactMatrix(canon, config, objectDetails) {
|
|
|
493
521
|
continue;
|
|
494
522
|
if (!allowedStatuses.has(a.status))
|
|
495
523
|
continue;
|
|
496
|
-
if (!assertionMatchesColumn(a, col))
|
|
524
|
+
if (!assertionMatchesColumn(a, col, stepHome))
|
|
497
525
|
continue;
|
|
498
526
|
const rowIdx = obligations.findIndex(r => r.id === a.about);
|
|
499
527
|
if (rowIdx < 0)
|
|
@@ -116,6 +116,12 @@ export interface IndexVerification {
|
|
|
116
116
|
verifies: string;
|
|
117
117
|
method: VerificationMethod;
|
|
118
118
|
outcome: VerificationOutcome;
|
|
119
|
+
/**
|
|
120
|
+
* The verification procedure (27-verification.md §2). VERIFICATION carries
|
|
121
|
+
* no `name` field of its own; `protocol` is the closest human-readable
|
|
122
|
+
* description and is what a matrix/list view shows as the row's label.
|
|
123
|
+
*/
|
|
124
|
+
protocol?: string;
|
|
119
125
|
performed_at?: string;
|
|
120
126
|
/** Number of evidence entries — feeds the VERIF-006 gap. */
|
|
121
127
|
evidenceCount?: number;
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComplianceIndex } from '../compliance/types.js';
|
|
2
|
+
import type { RequirementVerificationMatrix, RequirementVerificationMatrixOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the repository-wide requirement -> verification matrix.
|
|
5
|
+
*
|
|
6
|
+
* Pure projection over an already-built `ComplianceIndex`
|
|
7
|
+
* (`compliance/reverse-index.ts`) — no I/O, no revalidation. Reuses
|
|
8
|
+
* `buildGapReport`'s `REQ-VERIF-COVERAGE-001`/`-002` verdicts verbatim rather
|
|
9
|
+
* than recomputing a competing coverage status.
|
|
10
|
+
*
|
|
11
|
+
* Ordering: requirements sorted by id; within a requirement, its verification
|
|
12
|
+
* rows sorted by verification id. Both are explicit sorts over the index's
|
|
13
|
+
* values, so the result is independent of the Map's insertion order (which —
|
|
14
|
+
* for a caller that built the index from a filesystem walk — would otherwise
|
|
15
|
+
* track directory enumeration order).
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildRequirementVerificationMatrix(index: ComplianceIndex, options?: RequirementVerificationMatrixOptions): RequirementVerificationMatrix;
|