@transitrix/diagrams 1.9.18 → 1.10.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/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 +30 -17
- package/dist/compliance-matrix/build.js +0 -0
- 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/validate-repo.js +4 -0
- package/dist/requirement/types.d.ts +1 -1
- package/dist/requirement/types.js +1 -1
- package/dist/webview/entry.js +11 -4
- package/package.json +1 -1
- 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 +39 -17
- package/src/compliance-matrix/build.ts +0 -0
- package/src/repo-validate/check-candidate-fields.ts +93 -0
- package/src/repo-validate/validate-repo.ts +4 -0
- package/src/requirement/types.ts +1 -1
- package/src/webview/entry.ts +13 -6
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,16 @@ 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,
|
|
105
119
|
performed_at: str(d.performed_at),
|
|
106
120
|
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
107
121
|
admitted_at: str(d.admitted_at),
|
|
108
122
|
});
|
|
109
|
-
return id;
|
|
123
|
+
return ok ? id : null;
|
|
110
124
|
}
|
|
111
125
|
if (d.zone === 'codex') {
|
|
112
|
-
canon.codex
|
|
113
|
-
return id;
|
|
126
|
+
return pushUnique(canon, canon.codex, { id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) }) ? id : null;
|
|
114
127
|
}
|
|
115
128
|
return null;
|
|
116
129
|
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `ADMIT-009` — a candidate-only field present on an admitted (`zone: canon`)
|
|
4
|
+
* element, of any TYPE. Appends findings; pure, deterministic order.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkCandidateFields(input: RepoModelInput, findings: RepoFinding[]): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Candidate-only fields on admitted canon — `ADMIT-009`.
|
|
2
|
+
//
|
|
3
|
+
// `ELEMENT_PRIMITIVES.md` §7.29 (methodology v3.6.0) states it normatively for
|
|
4
|
+
// `RELEASE` and then generalises it to every TYPE:
|
|
5
|
+
//
|
|
6
|
+
// > `RELEASE` has no `extraction_confidence` field — and neither does any
|
|
7
|
+
// > other canon element, of any TYPE. `extraction_confidence` is a review
|
|
8
|
+
// > flag on an ingest candidate (`candidate.extraction_confidence`,
|
|
9
|
+
// > vocabulary.yaml); it is surfaced in the review queue, drives
|
|
10
|
+
// > reviewer-authority routing (CONTRACT.md §6.2), and is never persisted
|
|
11
|
+
// > into canon. An admitted element carrying it is a defect in whatever
|
|
12
|
+
// > wrote it, not a precedent to follow.
|
|
13
|
+
//
|
|
14
|
+
// Nothing enforced that mechanically. The ingest pipeline that produces the
|
|
15
|
+
// field lives in `methodology`'s `packages/ingest-cli` and only ever writes
|
|
16
|
+
// *candidates*; admission into `canon/elements/**` happens in an adopter's own
|
|
17
|
+
// tooling, which is `@transitrix/cli validate`'s side of the fence — so this is
|
|
18
|
+
// where the rule has to bite.
|
|
19
|
+
//
|
|
20
|
+
// **Why the check keys on `zone`, not on the folder.** `extraction_confidence`
|
|
21
|
+
// is legitimate on a candidate and meaningless — but not a defect — elsewhere;
|
|
22
|
+
// only the *admitted* form is wrong. `zone: canon` is the admission marker
|
|
23
|
+
// every standalone element carries (`ELEMENT_PRIMITIVES.md` §3, required
|
|
24
|
+
// envelope field, `ELEM-001`), and it is the discriminator against the two
|
|
25
|
+
// zones that may legitimately carry harvest metadata: `zone: field`, whose
|
|
26
|
+
// provenance contract owns `source_quality` (CONTRACT.md §5/§11.2), and
|
|
27
|
+
// `zone: codex`. Sidecars (`*.history.yaml`, `*.runstate.yaml`) carry no
|
|
28
|
+
// `zone` and no `id`, so they fall out for free — the same "no id -> not an
|
|
29
|
+
// element" split `docId` already makes for every sibling check in this folder.
|
|
30
|
+
//
|
|
31
|
+
// **Rule code.** Studio-authored as `ELEM-CANDIDATE-FIELD-001` (in the shape
|
|
32
|
+
// methodology already uses for a named element-envelope rule outside the
|
|
33
|
+
// numbered run — `ELEM-ALIAS-001`, `ELEM-FORMER-ID-001` — deliberately not
|
|
34
|
+
// `ELEM-006`, since the numbered `ELEM-*` run is methodology's to extend and
|
|
35
|
+
// squatting the next free number would collide with whatever it registers
|
|
36
|
+
// there next). Renamed to `ADMIT-009` (transitrix/methodology#505, merged
|
|
37
|
+
// 2026-08-19) now that methodology has registered a code of its own for
|
|
38
|
+
// §7.29 — no functional change, this is that alias.
|
|
39
|
+
import { docId } from './validate-repo.js';
|
|
40
|
+
const PScope = 'repo';
|
|
41
|
+
/**
|
|
42
|
+
* Fields that belong to an ingest candidate and are never persisted into
|
|
43
|
+
* canon. Each entry names the field and the provenance pattern that replaces
|
|
44
|
+
* it, so the finding points at the correct shape rather than only failing
|
|
45
|
+
* closed. One entry today; the list is the extension point if methodology
|
|
46
|
+
* closes another candidate-side field out of canon the same way.
|
|
47
|
+
*/
|
|
48
|
+
const CANDIDATE_ONLY_FIELDS = [
|
|
49
|
+
{
|
|
50
|
+
field: 'extraction_confidence',
|
|
51
|
+
instead: "cite the evidence through 'derived_from' (ELEMENT_PRIMITIVES.md §3) — " +
|
|
52
|
+
'an OBSERVATION or another Field/Codex artefact; the review flag stays on the ' +
|
|
53
|
+
'ingest candidate and is not carried across admission',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
/** True when the doc is an admitted canon element — `zone: canon` per
|
|
57
|
+
* `ELEMENT_PRIMITIVES.md` §3. Deliberately narrow: a doc with no `zone`
|
|
58
|
+
* (a sidecar) or a non-canon zone (`field`, `codex`) is not admitted canon
|
|
59
|
+
* and this rule says nothing about it. */
|
|
60
|
+
function isAdmittedCanonElement(doc) {
|
|
61
|
+
return doc.data?.['zone'] === 'canon';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `ADMIT-009` — a candidate-only field present on an admitted (`zone: canon`)
|
|
65
|
+
* element, of any TYPE. Appends findings; pure, deterministic order.
|
|
66
|
+
*/
|
|
67
|
+
export function checkCandidateFields(input, findings) {
|
|
68
|
+
for (const doc of input.elements) {
|
|
69
|
+
if (!doc.data)
|
|
70
|
+
continue;
|
|
71
|
+
if (!isAdmittedCanonElement(doc))
|
|
72
|
+
continue;
|
|
73
|
+
const id = docId(doc) ?? '';
|
|
74
|
+
for (const { field, instead } of CANDIDATE_ONLY_FIELDS) {
|
|
75
|
+
if (doc.data[field] === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
findings.push({
|
|
78
|
+
scope: PScope,
|
|
79
|
+
id,
|
|
80
|
+
ruleId: 'ADMIT-009',
|
|
81
|
+
message: `ADMIT-009: admitted element '${id || doc.path}' carries '${field}', ` +
|
|
82
|
+
`which is an ingest-candidate review flag and is never persisted into canon ` +
|
|
83
|
+
`(ELEMENT_PRIMITIVES.md §7.29). Remove it and ${instead}.`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
30
30
|
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
31
31
|
import { checkVersionedAttributes } from './check-versioned-attributes.js';
|
|
32
|
+
import { checkCandidateFields } from './check-candidate-fields.js';
|
|
32
33
|
const PScope = 'repo';
|
|
33
34
|
/** Statuses that, per lint.py's policy check, require an owner. */
|
|
34
35
|
const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
|
|
@@ -415,5 +416,8 @@ export function validateRepoModel(input) {
|
|
|
415
416
|
checkElementHygiene(input, findings);
|
|
416
417
|
// Phase 9 — versioned-attribute sidecar rules (CONTRACT.md §9, VERSIONED-001..005).
|
|
417
418
|
checkVersionedAttributes(input, findings);
|
|
419
|
+
// Phase 10 — candidate-only fields on admitted canon (ELEMENT_PRIMITIVES.md
|
|
420
|
+
// §7.29, ADMIT-009).
|
|
421
|
+
checkCandidateFields(input, findings);
|
|
418
422
|
return findings;
|
|
419
423
|
}
|
|
@@ -7,7 +7,7 @@ export interface GateChecks {
|
|
|
7
7
|
completeness?: string;
|
|
8
8
|
}
|
|
9
9
|
/** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
|
|
10
|
-
export declare const REQUIREMENT_DERIVED_FROM_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD"];
|
|
10
|
+
export declare const REQUIREMENT_DERIVED_FROM_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD", "PRINCIPLE"];
|
|
11
11
|
/** ISO/IEC/IEEE 29148 specification-tier ladder (15-requirement.md §2.5, REQ-005). */
|
|
12
12
|
export type RequirementLevel = 'stakeholder' | 'system' | 'software';
|
|
13
13
|
export declare const REQUIREMENT_LEVELS: readonly RequirementLevel[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// REQUIREMENT — motivation-layer positive obligation.
|
|
2
2
|
// Schema: methodology notations/elements/15-requirement.md §2.
|
|
3
3
|
/** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
|
|
4
|
-
export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
|
|
4
|
+
export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'];
|
|
5
5
|
export const REQUIREMENT_LEVELS = ['stakeholder', 'system', 'software'];
|
|
6
6
|
export const REQUIREMENT_KINDS = ['functional', 'quality'];
|
package/dist/webview/entry.js
CHANGED
|
@@ -28,7 +28,7 @@ import yaml from 'js-yaml';
|
|
|
28
28
|
import { validateActivities } from '../activities/index.js';
|
|
29
29
|
import { validateActivityCard } from '../activity-card/index.js';
|
|
30
30
|
import { validateApplicationsCatalogue } from '../applications/index.js';
|
|
31
|
-
import {
|
|
31
|
+
import { validateBlocks } from '../blocks/index.js';
|
|
32
32
|
import { validateCapabilityMap } from '../capability-map/validate.js';
|
|
33
33
|
import { parseCanonicalFGCA, parseCanonicalFGA } from '../fgca/parse-canonical.js';
|
|
34
34
|
import { parseCanonicalGoals } from '../goals/parse-canonical.js';
|
|
@@ -40,7 +40,7 @@ import { coerceDatesToIsoStrings } from '../yaml-normalize.js';
|
|
|
40
40
|
import { renderActivitiesSvg } from './render-activities.js';
|
|
41
41
|
import { renderActivityCardSvg } from './render-activity-card.js';
|
|
42
42
|
import { renderApplicationsHtml } from './render-applications.js';
|
|
43
|
-
import { renderBlocksSvg } from './render-blocks.js';
|
|
43
|
+
import { renderBlocksSvg, renderGridSvg } from './render-blocks.js';
|
|
44
44
|
import { renderCapabilityMapHtml } from './render-capability-map.js';
|
|
45
45
|
import { renderFgcaSvg } from './render-fgca.js';
|
|
46
46
|
import { renderGoalsSvg } from './render-goals.js';
|
|
@@ -138,8 +138,15 @@ function dispatchValidate(kind, doc) {
|
|
|
138
138
|
return renderFromValidation('action-card', validateActivityCard(doc), () => renderActivityCardSvg(doc));
|
|
139
139
|
case 'process-blueprint':
|
|
140
140
|
return renderFromValidation('process-blueprint', validateProcessBlueprint(doc), () => renderProcessBlueprintSvg(doc));
|
|
141
|
-
case 'blocks':
|
|
142
|
-
|
|
141
|
+
case 'blocks': {
|
|
142
|
+
// Two alternative root forms (08-blocks.md §4): a `nested_blocks:`
|
|
143
|
+
// containment tree, or a `grid:` matrix subset (§4a). `validateBlocks`
|
|
144
|
+
// is the shared root-form dispatcher (BL-020 on both/neither present);
|
|
145
|
+
// mirrors the VS Code path's `hasGrid` branch in `blocks-preview.ts`.
|
|
146
|
+
const raw = (doc && typeof doc === 'object' ? doc : {});
|
|
147
|
+
const hasGrid = raw['grid'] !== undefined && raw['grid'] !== null;
|
|
148
|
+
return renderFromValidation('blocks', validateBlocks(doc), () => hasGrid ? renderGridSvg(doc) : renderBlocksSvg(doc));
|
|
149
|
+
}
|
|
143
150
|
// Catalogue notations render an HTML fragment from the header nested under
|
|
144
151
|
// their top-level wrapper key (`<thing>_catalogue` / `scenario` / …).
|
|
145
152
|
case 'applications':
|
package/package.json
CHANGED
package/src/codex/index.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/src/codex/types.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
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
|
|
|
4
4
|
/** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
|
|
5
|
-
export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'] as const;
|
|
5
|
+
export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'] as const;
|
|
6
6
|
|
|
7
7
|
export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
|
|
8
8
|
|
|
9
9
|
/** External codex artefacts (codex/external/<jurisdiction>/). */
|
|
10
10
|
export const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['LAW', 'REGULATION'];
|
|
11
11
|
|
|
12
|
-
/** Internal codex artefacts (codex/internal/). */
|
|
12
|
+
/** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
|
|
13
13
|
export const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['POLICY', 'INTERNAL_STANDARD'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
|
|
17
|
+
* `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
|
|
18
|
+
* optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
|
|
19
|
+
* differs; both share the same file location.
|
|
20
|
+
*/
|
|
21
|
+
export const PRINCIPLE_CODEX_TYPES: readonly CodexArtefactType[] = ['PRINCIPLE'];
|
package/src/codex/validate.ts
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
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
|
|
|
10
21
|
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
11
22
|
import { isCanonicalId, typeOfId } from '../typed-id.js';
|
|
@@ -13,6 +24,7 @@ import {
|
|
|
13
24
|
CODEX_ARTEFACT_TYPES,
|
|
14
25
|
EXTERNAL_CODEX_TYPES,
|
|
15
26
|
INTERNAL_CODEX_TYPES,
|
|
27
|
+
PRINCIPLE_CODEX_TYPES,
|
|
16
28
|
type CodexArtefactType,
|
|
17
29
|
} from './types.js';
|
|
18
30
|
|
|
@@ -48,7 +60,7 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
|
|
|
48
60
|
if (!isCanonicalId(c.id) || !isCodexType(idType)) {
|
|
49
61
|
errors.push({
|
|
50
62
|
code: 'CODEX-001',
|
|
51
|
-
message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or
|
|
63
|
+
message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, INTERNAL_STANDARD, or PRINCIPLE).`,
|
|
52
64
|
path: 'id',
|
|
53
65
|
});
|
|
54
66
|
}
|
|
@@ -106,6 +118,22 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
|
|
|
106
118
|
}
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
if (artefactType && (PRINCIPLE_CODEX_TYPES as readonly string[]).includes(artefactType)) {
|
|
122
|
+
if (typeof c.statement !== 'string' || c.statement.trim() === '') {
|
|
123
|
+
errors.push({ code: 'CODEX-004', message: 'statement is required for a PRINCIPLE artefact.', path: 'statement' });
|
|
124
|
+
}
|
|
125
|
+
if (typeof c.rationale !== 'string' || c.rationale.trim() === '') {
|
|
126
|
+
errors.push({ code: 'CODEX-004', message: 'rationale is required for a PRINCIPLE artefact.', path: 'rationale' });
|
|
127
|
+
}
|
|
128
|
+
if (typeof c.established_by !== 'string' || c.established_by.trim() === '') {
|
|
129
|
+
warnings.push({
|
|
130
|
+
code: 'CODEX-006',
|
|
131
|
+
message: 'established_by is not declared on this PRINCIPLE artefact — admissible, but a review finding for whoever admits it.',
|
|
132
|
+
path: 'established_by',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
109
137
|
return { valid: errors.length === 0, errors, warnings };
|
|
110
138
|
}
|
|
111
139
|
|
|
@@ -40,6 +40,14 @@ export interface ComplianceCanon {
|
|
|
40
40
|
/** VALIDATION artefacts (28-validation.md) — the validation-domain peer of
|
|
41
41
|
* `verifications`, anchored on NEED instead of REQUIREMENT. */
|
|
42
42
|
validations: IndexValidation[];
|
|
43
|
+
/**
|
|
44
|
+
* Ids rejected by `ingestComplianceDoc` because that id was already present
|
|
45
|
+
* in the same bucket (two documents claiming one id — transitrix-hq#218).
|
|
46
|
+
* The second and any later document is dropped rather than silently
|
|
47
|
+
* duplicating a matrix column; callers that scan a filesystem can surface
|
|
48
|
+
* this list as a diagnostic.
|
|
49
|
+
*/
|
|
50
|
+
duplicateIds: string[];
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
export function emptyCanon(): ComplianceCanon {
|
|
@@ -52,6 +60,7 @@ export function emptyCanon(): ComplianceCanon {
|
|
|
52
60
|
subjects: [],
|
|
53
61
|
needs: [],
|
|
54
62
|
validations: [],
|
|
63
|
+
duplicateIds: [],
|
|
55
64
|
};
|
|
56
65
|
}
|
|
57
66
|
|
|
@@ -59,12 +68,29 @@ const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : und
|
|
|
59
68
|
const strArray = (v: unknown): string[] | undefined =>
|
|
60
69
|
Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : undefined;
|
|
61
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Pushes `item` onto `list` unless `list` already holds an entry with the same
|
|
73
|
+
* id, in which case the id is recorded in `canon.duplicateIds` and the item is
|
|
74
|
+
* dropped — two documents claiming one id is a defect (transitrix-hq#218), not
|
|
75
|
+
* a second matrix column.
|
|
76
|
+
*/
|
|
77
|
+
function pushUnique<T extends { id: string }>(canon: ComplianceCanon, list: T[], item: T): boolean {
|
|
78
|
+
if (list.some(existing => existing.id === item.id)) {
|
|
79
|
+
canon.duplicateIds.push(item.id);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
list.push(item);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
/**
|
|
63
87
|
* Classifies one parsed YAML document and, if it is a compliance artefact,
|
|
64
88
|
* pushes its projection into `canon`. Products / requirements / assertions are
|
|
65
89
|
* identified by their `notation` tag; codex source documents by `zone: codex`.
|
|
66
90
|
* Returns the artefact id when ingested (so the caller can record its path), or
|
|
67
|
-
* null when the document is not a (well-formed) compliance artefact
|
|
91
|
+
* null when the document is not a (well-formed) compliance artefact, or when
|
|
92
|
+
* its id duplicates one already ingested into the same bucket (see
|
|
93
|
+
* `canon.duplicateIds`).
|
|
68
94
|
*/
|
|
69
95
|
export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null {
|
|
70
96
|
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return null;
|
|
@@ -73,8 +99,7 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
|
|
|
73
99
|
if (!id) return null;
|
|
74
100
|
|
|
75
101
|
if (d.notation === 'product') {
|
|
76
|
-
canon.products
|
|
77
|
-
return id;
|
|
102
|
+
return pushUnique(canon, canon.products, { id, name: str(d.name) ?? id }) ? id : null;
|
|
78
103
|
}
|
|
79
104
|
if (
|
|
80
105
|
d.notation === 'capability' ||
|
|
@@ -82,12 +107,11 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
|
|
|
82
107
|
d.notation === 'application' ||
|
|
83
108
|
d.notation === 'system'
|
|
84
109
|
) {
|
|
85
|
-
canon.subjects
|
|
86
|
-
return id;
|
|
110
|
+
return pushUnique(canon, canon.subjects, { id, name: str(d.name) ?? id }) ? id : null;
|
|
87
111
|
}
|
|
88
112
|
if (d.notation === 'requirement' || d.notation === 'constraint') {
|
|
89
113
|
const origin = str(d.origin);
|
|
90
|
-
canon.requirements
|
|
114
|
+
const ok = pushUnique(canon, canon.requirements, {
|
|
91
115
|
id,
|
|
92
116
|
name: str(d.name) ?? id,
|
|
93
117
|
severity: str(d.severity),
|
|
@@ -101,31 +125,30 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
|
|
|
101
125
|
next_review_at: str(d.next_review_at),
|
|
102
126
|
serves: str(d.serves),
|
|
103
127
|
});
|
|
104
|
-
return id;
|
|
128
|
+
return ok ? id : null;
|
|
105
129
|
}
|
|
106
130
|
if (d.notation === 'need') {
|
|
107
|
-
canon.needs
|
|
108
|
-
return id;
|
|
131
|
+
return pushUnique(canon, canon.needs, { id, name: str(d.name) ?? id }) ? id : null;
|
|
109
132
|
}
|
|
110
133
|
if (d.notation === 'validation') {
|
|
111
134
|
const validates = str(d.validates);
|
|
112
135
|
const method = str(d.method) as ValidationMethod | undefined;
|
|
113
136
|
const outcome = str(d.outcome) as ValidationOutcome | undefined;
|
|
114
137
|
if (!validates || !method || !outcome) return null;
|
|
115
|
-
canon.validations
|
|
138
|
+
const ok = pushUnique(canon, canon.validations, {
|
|
116
139
|
id, validates, method, outcome,
|
|
117
140
|
performed_at: str(d.performed_at),
|
|
118
141
|
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
119
142
|
admitted_at: str(d.admitted_at),
|
|
120
143
|
});
|
|
121
|
-
return id;
|
|
144
|
+
return ok ? id : null;
|
|
122
145
|
}
|
|
123
146
|
if (d.notation === 'assertion') {
|
|
124
147
|
const about = str(d.about);
|
|
125
148
|
const subject = str(d.subject);
|
|
126
149
|
const status = str(d.status) as AssertionStatus | undefined;
|
|
127
150
|
if (!about || !subject || !status) return null;
|
|
128
|
-
canon.assertions
|
|
151
|
+
const ok = pushUnique(canon, canon.assertions, {
|
|
129
152
|
id, about, subject, status,
|
|
130
153
|
assessed_at: str(d.assessed_at),
|
|
131
154
|
next_review_at: str(d.next_review_at),
|
|
@@ -134,24 +157,23 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
|
|
|
134
157
|
realised_via: strArray(d.realised_via),
|
|
135
158
|
owner_to_confirm: str(d.owner_to_confirm),
|
|
136
159
|
});
|
|
137
|
-
return id;
|
|
160
|
+
return ok ? id : null;
|
|
138
161
|
}
|
|
139
162
|
if (d.notation === 'verification') {
|
|
140
163
|
const verifies = str(d.verifies);
|
|
141
164
|
const method = str(d.method) as VerificationMethod | undefined;
|
|
142
165
|
const outcome = str(d.outcome) as VerificationOutcome | undefined;
|
|
143
166
|
if (!verifies || !method || !outcome) return null;
|
|
144
|
-
canon.verifications
|
|
167
|
+
const ok = pushUnique(canon, canon.verifications, {
|
|
145
168
|
id, verifies, method, outcome,
|
|
146
169
|
performed_at: str(d.performed_at),
|
|
147
170
|
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
148
171
|
admitted_at: str(d.admitted_at),
|
|
149
172
|
});
|
|
150
|
-
return id;
|
|
173
|
+
return ok ? id : null;
|
|
151
174
|
}
|
|
152
175
|
if (d.zone === 'codex') {
|
|
153
|
-
canon.codex
|
|
154
|
-
return id;
|
|
176
|
+
return pushUnique(canon, canon.codex, { id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) }) ? id : null;
|
|
155
177
|
}
|
|
156
178
|
return null;
|
|
157
179
|
}
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Candidate-only fields on admitted canon — `ADMIT-009`.
|
|
2
|
+
//
|
|
3
|
+
// `ELEMENT_PRIMITIVES.md` §7.29 (methodology v3.6.0) states it normatively for
|
|
4
|
+
// `RELEASE` and then generalises it to every TYPE:
|
|
5
|
+
//
|
|
6
|
+
// > `RELEASE` has no `extraction_confidence` field — and neither does any
|
|
7
|
+
// > other canon element, of any TYPE. `extraction_confidence` is a review
|
|
8
|
+
// > flag on an ingest candidate (`candidate.extraction_confidence`,
|
|
9
|
+
// > vocabulary.yaml); it is surfaced in the review queue, drives
|
|
10
|
+
// > reviewer-authority routing (CONTRACT.md §6.2), and is never persisted
|
|
11
|
+
// > into canon. An admitted element carrying it is a defect in whatever
|
|
12
|
+
// > wrote it, not a precedent to follow.
|
|
13
|
+
//
|
|
14
|
+
// Nothing enforced that mechanically. The ingest pipeline that produces the
|
|
15
|
+
// field lives in `methodology`'s `packages/ingest-cli` and only ever writes
|
|
16
|
+
// *candidates*; admission into `canon/elements/**` happens in an adopter's own
|
|
17
|
+
// tooling, which is `@transitrix/cli validate`'s side of the fence — so this is
|
|
18
|
+
// where the rule has to bite.
|
|
19
|
+
//
|
|
20
|
+
// **Why the check keys on `zone`, not on the folder.** `extraction_confidence`
|
|
21
|
+
// is legitimate on a candidate and meaningless — but not a defect — elsewhere;
|
|
22
|
+
// only the *admitted* form is wrong. `zone: canon` is the admission marker
|
|
23
|
+
// every standalone element carries (`ELEMENT_PRIMITIVES.md` §3, required
|
|
24
|
+
// envelope field, `ELEM-001`), and it is the discriminator against the two
|
|
25
|
+
// zones that may legitimately carry harvest metadata: `zone: field`, whose
|
|
26
|
+
// provenance contract owns `source_quality` (CONTRACT.md §5/§11.2), and
|
|
27
|
+
// `zone: codex`. Sidecars (`*.history.yaml`, `*.runstate.yaml`) carry no
|
|
28
|
+
// `zone` and no `id`, so they fall out for free — the same "no id -> not an
|
|
29
|
+
// element" split `docId` already makes for every sibling check in this folder.
|
|
30
|
+
//
|
|
31
|
+
// **Rule code.** Studio-authored as `ELEM-CANDIDATE-FIELD-001` (in the shape
|
|
32
|
+
// methodology already uses for a named element-envelope rule outside the
|
|
33
|
+
// numbered run — `ELEM-ALIAS-001`, `ELEM-FORMER-ID-001` — deliberately not
|
|
34
|
+
// `ELEM-006`, since the numbered `ELEM-*` run is methodology's to extend and
|
|
35
|
+
// squatting the next free number would collide with whatever it registers
|
|
36
|
+
// there next). Renamed to `ADMIT-009` (transitrix/methodology#505, merged
|
|
37
|
+
// 2026-08-19) now that methodology has registered a code of its own for
|
|
38
|
+
// §7.29 — no functional change, this is that alias.
|
|
39
|
+
|
|
40
|
+
import { docId } from './validate-repo.js';
|
|
41
|
+
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
42
|
+
|
|
43
|
+
const PScope: RepoFinding['scope'] = 'repo';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fields that belong to an ingest candidate and are never persisted into
|
|
47
|
+
* canon. Each entry names the field and the provenance pattern that replaces
|
|
48
|
+
* it, so the finding points at the correct shape rather than only failing
|
|
49
|
+
* closed. One entry today; the list is the extension point if methodology
|
|
50
|
+
* closes another candidate-side field out of canon the same way.
|
|
51
|
+
*/
|
|
52
|
+
const CANDIDATE_ONLY_FIELDS: readonly { field: string; instead: string }[] = [
|
|
53
|
+
{
|
|
54
|
+
field: 'extraction_confidence',
|
|
55
|
+
instead:
|
|
56
|
+
"cite the evidence through 'derived_from' (ELEMENT_PRIMITIVES.md §3) — " +
|
|
57
|
+
'an OBSERVATION or another Field/Codex artefact; the review flag stays on the ' +
|
|
58
|
+
'ingest candidate and is not carried across admission',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/** True when the doc is an admitted canon element — `zone: canon` per
|
|
63
|
+
* `ELEMENT_PRIMITIVES.md` §3. Deliberately narrow: a doc with no `zone`
|
|
64
|
+
* (a sidecar) or a non-canon zone (`field`, `codex`) is not admitted canon
|
|
65
|
+
* and this rule says nothing about it. */
|
|
66
|
+
function isAdmittedCanonElement(doc: RepoDoc): boolean {
|
|
67
|
+
return doc.data?.['zone'] === 'canon';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `ADMIT-009` — a candidate-only field present on an admitted (`zone: canon`)
|
|
72
|
+
* element, of any TYPE. Appends findings; pure, deterministic order.
|
|
73
|
+
*/
|
|
74
|
+
export function checkCandidateFields(input: RepoModelInput, findings: RepoFinding[]): void {
|
|
75
|
+
for (const doc of input.elements) {
|
|
76
|
+
if (!doc.data) continue;
|
|
77
|
+
if (!isAdmittedCanonElement(doc)) continue;
|
|
78
|
+
|
|
79
|
+
const id = docId(doc) ?? '';
|
|
80
|
+
for (const { field, instead } of CANDIDATE_ONLY_FIELDS) {
|
|
81
|
+
if (doc.data[field] === undefined) continue;
|
|
82
|
+
findings.push({
|
|
83
|
+
scope: PScope,
|
|
84
|
+
id,
|
|
85
|
+
ruleId: 'ADMIT-009',
|
|
86
|
+
message:
|
|
87
|
+
`ADMIT-009: admitted element '${id || doc.path}' carries '${field}', ` +
|
|
88
|
+
`which is an ingest-candidate review flag and is never persisted into canon ` +
|
|
89
|
+
`(ELEMENT_PRIMITIVES.md §7.29). Remove it and ${instead}.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
31
31
|
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
32
32
|
import { checkVersionedAttributes } from './check-versioned-attributes.js';
|
|
33
|
+
import { checkCandidateFields } from './check-candidate-fields.js';
|
|
33
34
|
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
34
35
|
|
|
35
36
|
const PScope: RepoFinding['scope'] = 'repo';
|
|
@@ -431,5 +432,8 @@ export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
|
|
|
431
432
|
checkElementHygiene(input, findings);
|
|
432
433
|
// Phase 9 — versioned-attribute sidecar rules (CONTRACT.md §9, VERSIONED-001..005).
|
|
433
434
|
checkVersionedAttributes(input, findings);
|
|
435
|
+
// Phase 10 — candidate-only fields on admitted canon (ELEMENT_PRIMITIVES.md
|
|
436
|
+
// §7.29, ADMIT-009).
|
|
437
|
+
checkCandidateFields(input, findings);
|
|
434
438
|
return findings;
|
|
435
439
|
}
|
package/src/requirement/types.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface GateChecks {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
|
|
16
|
-
export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'] as const;
|
|
16
|
+
export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'] as const;
|
|
17
17
|
|
|
18
18
|
/** ISO/IEC/IEEE 29148 specification-tier ladder (15-requirement.md §2.5, REQ-005). */
|
|
19
19
|
export type RequirementLevel = 'stakeholder' | 'system' | 'software';
|
package/src/webview/entry.ts
CHANGED
|
@@ -32,8 +32,8 @@ import { validateActivityCard } from '../activity-card/index.js';
|
|
|
32
32
|
import type { ActivityCardDoc } from '../activity-card/index.js';
|
|
33
33
|
import { validateApplicationsCatalogue } from '../applications/index.js';
|
|
34
34
|
import type { ApplicationsCatalogueFile } from '../applications/index.js';
|
|
35
|
-
import {
|
|
36
|
-
import type { BlocksFile } from '../blocks/index.js';
|
|
35
|
+
import { validateBlocks } from '../blocks/index.js';
|
|
36
|
+
import type { BlocksFile, GridFile } from '../blocks/index.js';
|
|
37
37
|
import { validateCapabilityMap } from '../capability-map/validate.js';
|
|
38
38
|
import type { CapabilityMapFile } from '../capability-map/types.js';
|
|
39
39
|
import { parseCanonicalFGCA, parseCanonicalFGA } from '../fgca/parse-canonical.js';
|
|
@@ -51,7 +51,7 @@ import { coerceDatesToIsoStrings } from '../yaml-normalize.js';
|
|
|
51
51
|
import { renderActivitiesSvg } from './render-activities.js';
|
|
52
52
|
import { renderActivityCardSvg } from './render-activity-card.js';
|
|
53
53
|
import { renderApplicationsHtml } from './render-applications.js';
|
|
54
|
-
import { renderBlocksSvg } from './render-blocks.js';
|
|
54
|
+
import { renderBlocksSvg, renderGridSvg } from './render-blocks.js';
|
|
55
55
|
import { renderCapabilityMapHtml } from './render-capability-map.js';
|
|
56
56
|
import { renderFgcaSvg } from './render-fgca.js';
|
|
57
57
|
import { renderGoalsSvg } from './render-goals.js';
|
|
@@ -218,10 +218,17 @@ function dispatchValidate(kind: NotationKind, doc: unknown): RenderResult {
|
|
|
218
218
|
return renderFromValidation('process-blueprint', validateProcessBlueprint(doc), () =>
|
|
219
219
|
renderProcessBlueprintSvg(doc as ProcessBlueprintFile),
|
|
220
220
|
);
|
|
221
|
-
case 'blocks':
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
case 'blocks': {
|
|
222
|
+
// Two alternative root forms (08-blocks.md §4): a `nested_blocks:`
|
|
223
|
+
// containment tree, or a `grid:` matrix subset (§4a). `validateBlocks`
|
|
224
|
+
// is the shared root-form dispatcher (BL-020 on both/neither present);
|
|
225
|
+
// mirrors the VS Code path's `hasGrid` branch in `blocks-preview.ts`.
|
|
226
|
+
const raw = (doc && typeof doc === 'object' ? doc : {}) as Record<string, unknown>;
|
|
227
|
+
const hasGrid = raw['grid'] !== undefined && raw['grid'] !== null;
|
|
228
|
+
return renderFromValidation('blocks', validateBlocks(doc), () =>
|
|
229
|
+
hasGrid ? renderGridSvg(doc as GridFile) : renderBlocksSvg(doc as BlocksFile),
|
|
224
230
|
);
|
|
231
|
+
}
|
|
225
232
|
// Catalogue notations render an HTML fragment from the header nested under
|
|
226
233
|
// their top-level wrapper key (`<thing>_catalogue` / `scenario` / …).
|
|
227
234
|
case 'applications':
|