@transitrix/diagrams 1.8.21 → 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/activity-card/types.js +1 -1
- package/dist/agreement.d.ts +15 -0
- package/dist/agreement.js +61 -0
- package/dist/applications/index.d.ts +2 -0
- package/dist/applications/index.js +1 -0
- package/dist/applications/resolve-maturity.d.ts +16 -0
- package/dist/applications/resolve-maturity.js +57 -0
- package/dist/applications/validate.js +20 -0
- package/dist/capability-map/CapabilityCardNode.d.ts +1 -1
- package/dist/capability-map/CapabilityRootNode.d.ts +1 -1
- package/dist/capability-map/index.d.ts +2 -0
- package/dist/capability-map/index.js +1 -0
- package/dist/capability-map/resolve-maturity.d.ts +20 -0
- package/dist/capability-map/resolve-maturity.js +72 -0
- package/dist/capability-map/validate.js +1 -2
- 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 +21 -2
- package/dist/compliance/classify.js +68 -13
- package/dist/compliance/confidence.js +1 -1
- package/dist/compliance/gap-report.d.ts +31 -1
- package/dist/compliance/gap-report.js +55 -3
- package/dist/compliance/html.js +14 -6
- package/dist/compliance/impact.js +1 -1
- package/dist/compliance/index.d.ts +3 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/markdown.js +26 -4
- package/dist/compliance/reverse-index.d.ts +6 -1
- package/dist/compliance/reverse-index.js +27 -3
- package/dist/compliance/trace.js +5 -1
- package/dist/compliance/types.d.ts +70 -0
- package/dist/compliance/types.js +1 -2
- package/dist/compliance/views.js +1 -1
- package/dist/compliance-matrix/build.js +0 -0
- package/dist/compliance-matrix/types.d.ts +1 -1
- package/dist/compliance-matrix/types.js +1 -1
- package/dist/confidence/score.js +1 -1
- package/dist/constraint/validate.js +3 -0
- package/dist/edge-path.js +1 -1
- package/dist/factor/validate.js +2 -2
- package/dist/fgca/chain-table.js +1 -1
- package/dist/fgca/nodeTypes.d.ts +4 -4
- package/dist/fgca/nodes/FGCAActivityNode.d.ts +1 -1
- package/dist/fgca/nodes/FGCAChangeNode.d.ts +1 -1
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +1 -1
- package/dist/fgca/nodes/FGCAGoalNode.d.ts +1 -1
- package/dist/fgca/parse-canonical.d.ts +2 -2
- package/dist/fgca/parse-canonical.js +2 -2
- package/dist/fgca/preview-layout.d.ts +1 -1
- package/dist/goals/GoalNode.d.ts +1 -1
- package/dist/goals/layout.d.ts +1 -1
- package/dist/goals/layout.js +1 -1
- package/dist/goals/parse-canonical.d.ts +1 -1
- package/dist/goals/parse-canonical.js +1 -1
- package/dist/goals/types.d.ts +1 -1
- package/dist/goals/validate.js +2 -2
- package/dist/link-suspicion.d.ts +43 -0
- package/dist/link-suspicion.js +156 -0
- package/dist/metric/index.d.ts +4 -0
- package/dist/metric/index.js +2 -0
- package/dist/metric/types.d.ts +24 -0
- package/dist/metric/types.js +7 -0
- package/dist/metric/validate.d.ts +7 -0
- package/dist/metric/validate.js +90 -0
- package/dist/need/index.d.ts +3 -0
- package/dist/need/index.js +1 -0
- package/dist/need/types.d.ts +21 -0
- package/dist/need/types.js +3 -0
- package/dist/need/validate.d.ts +7 -0
- package/dist/need/validate.js +60 -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/check-versioned-attributes.d.ts +4 -0
- package/dist/repo-validate/check-versioned-attributes.js +105 -0
- package/dist/repo-validate/validate-repo.js +7 -0
- package/dist/requirement/index.d.ts +2 -2
- package/dist/requirement/index.js +1 -1
- package/dist/requirement/types.d.ts +19 -1
- package/dist/requirement/types.js +3 -1
- package/dist/requirement/validate.js +22 -1
- package/dist/risk/index.d.ts +4 -0
- package/dist/risk/index.js +2 -0
- package/dist/risk/types.d.ts +25 -0
- package/dist/risk/types.js +3 -0
- package/dist/risk/validate.d.ts +7 -0
- package/dist/risk/validate.js +108 -0
- package/dist/schema-version.d.ts +9 -6
- package/dist/schema-version.js +9 -6
- package/dist/theme/themes.js +3 -2
- package/dist/validation/index.d.ts +4 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/types.d.ts +31 -0
- package/dist/validation/types.js +21 -0
- package/dist/validation/validate.d.ts +7 -0
- package/dist/validation/validate.js +106 -0
- package/dist/verification/index.d.ts +4 -0
- package/dist/verification/index.js +2 -0
- package/dist/verification/types.d.ts +31 -0
- package/dist/verification/types.js +12 -0
- package/dist/verification/validate.d.ts +7 -0
- package/dist/verification/validate.js +104 -0
- package/dist/versioned-attribute/index.d.ts +4 -0
- package/dist/versioned-attribute/index.js +2 -0
- package/dist/versioned-attribute/resolve.d.ts +15 -0
- package/dist/versioned-attribute/resolve.js +37 -0
- package/dist/versioned-attribute/types.d.ts +12 -0
- package/dist/versioned-attribute/types.js +6 -0
- package/dist/versioned-attribute/validate.d.ts +17 -0
- package/dist/versioned-attribute/validate.js +87 -0
- package/dist/webview/entry.js +11 -4
- package/dist/webview/render-applications.d.ts +15 -1
- package/dist/webview/render-applications.js +7 -2
- package/dist/webview/render-blocks.js +6 -4
- package/dist/webview/render-capability-map.d.ts +16 -1
- package/dist/webview/render-capability-map.js +4 -1
- package/dist/webview/render-process.d.ts +1 -1
- package/dist/webview/render-process.js +5 -5
- package/package.json +9 -6
- package/src/activity-card/types.ts +1 -1
- package/src/agreement.ts +71 -0
- package/src/applications/index.ts +3 -0
- package/src/applications/resolve-maturity.ts +66 -0
- package/src/applications/validate.ts +23 -0
- package/src/capability-map/index.ts +7 -0
- package/src/capability-map/resolve-maturity.ts +84 -0
- package/src/capability-map/validate.ts +1 -2
- 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 +87 -14
- package/src/compliance/confidence.ts +1 -1
- package/src/compliance/gap-report.ts +91 -4
- package/src/compliance/html.ts +18 -6
- package/src/compliance/impact.ts +1 -1
- package/src/compliance/index.ts +6 -0
- package/src/compliance/markdown.ts +18 -4
- package/src/compliance/reverse-index.ts +35 -4
- package/src/compliance/trace.ts +7 -1
- package/src/compliance/types.ts +75 -2
- package/src/compliance/views.ts +1 -1
- package/src/compliance-matrix/build.ts +0 -0
- package/src/compliance-matrix/types.ts +2 -2
- package/src/confidence/score.ts +1 -1
- package/src/constraint/validate.ts +4 -0
- package/src/edge-path.ts +1 -1
- package/src/factor/validate.ts +2 -2
- package/src/fgca/chain-table.ts +1 -1
- package/src/fgca/parse-canonical.ts +2 -2
- package/src/fgca/preview-layout.ts +1 -1
- package/src/goals/layout.ts +1 -1
- package/src/goals/parse-canonical.ts +1 -1
- package/src/goals/types.ts +1 -1
- package/src/goals/validate.ts +2 -2
- package/src/link-suspicion.ts +181 -0
- package/src/metric/index.ts +4 -0
- package/src/metric/types.ts +38 -0
- package/src/metric/validate.ts +102 -0
- package/src/need/index.ts +3 -0
- package/src/need/types.ts +31 -0
- package/src/need/validate.ts +71 -0
- package/src/repo-validate/check-candidate-fields.ts +93 -0
- package/src/repo-validate/check-versioned-attributes.ts +112 -0
- package/src/repo-validate/validate-repo.ts +7 -0
- package/src/requirement/index.ts +2 -2
- package/src/requirement/types.ts +26 -1
- package/src/requirement/validate.ts +25 -1
- package/src/risk/index.ts +4 -0
- package/src/risk/types.ts +35 -0
- package/src/risk/validate.ts +121 -0
- package/src/schema-version.ts +9 -6
- package/src/theme/themes.ts +3 -2
- package/src/validation/index.ts +4 -0
- package/src/validation/types.ts +57 -0
- package/src/validation/validate.ts +113 -0
- package/src/verification/index.ts +4 -0
- package/src/verification/types.ts +48 -0
- package/src/verification/validate.ts +111 -0
- package/src/versioned-attribute/index.ts +4 -0
- package/src/versioned-attribute/resolve.ts +45 -0
- package/src/versioned-attribute/types.ts +19 -0
- package/src/versioned-attribute/validate.ts +109 -0
- package/src/webview/entry.ts +13 -6
- package/src/webview/render-applications.ts +25 -2
- package/src/webview/render-blocks.ts +8 -4
- package/src/webview/render-capability-map.ts +23 -1
- package/src/webview/render-process.ts +5 -5
- package/src/webview/styles.css +6 -0
|
@@ -37,7 +37,7 @@ export interface FGCAPreviewLayoutOptions {
|
|
|
37
37
|
colGap?: number;
|
|
38
38
|
/** Vertical gap (px) between stacked nodes within a column. Default matches the historical hardcoded value. */
|
|
39
39
|
rowGap?: number;
|
|
40
|
-
/** Trim to a level cap or a single root goal
|
|
40
|
+
/** Trim to a level cap or a single root goal. Defaults to 'all'. */
|
|
41
41
|
scope?: Scope;
|
|
42
42
|
/** Entity node width (px). Default {@link FGCA_NODE_W}. */
|
|
43
43
|
nodeWidth?: number;
|
package/dist/goals/GoalNode.d.ts
CHANGED
package/dist/goals/layout.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Goal, GoalTree, LayoutOptions, GoalTreeLayout } from './types.js';
|
|
2
2
|
import type { Scope } from '../scope.js';
|
|
3
3
|
/**
|
|
4
|
-
* Trims a flat goal list to a scope
|
|
4
|
+
* Trims a flat goal list to a scope.
|
|
5
5
|
* - 'level' → goals with `level <= maxLevel`.
|
|
6
6
|
* - 'root' → the root goal (matched by string id) plus all its descendants
|
|
7
7
|
* via `parent_id`; an empty list when the root is absent.
|
package/dist/goals/layout.js
CHANGED
|
@@ -4,7 +4,7 @@ const DEFAULT_NODE_HEIGHT = 80;
|
|
|
4
4
|
const DEFAULT_RANK_SEP = 80;
|
|
5
5
|
const DEFAULT_NODE_SEP = 40;
|
|
6
6
|
/**
|
|
7
|
-
* Trims a flat goal list to a scope
|
|
7
|
+
* Trims a flat goal list to a scope.
|
|
8
8
|
* - 'level' → goals with `level <= maxLevel`.
|
|
9
9
|
* - 'root' → the root goal (matched by string id) plus all its descendants
|
|
10
10
|
* via `parent_id`; an empty list when the root is absent.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* mapped to internal sequential numbers, `parent: GOAL-X` becomes
|
|
16
16
|
* `parent_id: <numeric>`, `parent_id: 0` denotes a root.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* This lives at the input boundary so the library's
|
|
19
19
|
* internal types (numeric IDs) stay where they are. Convergence on
|
|
20
20
|
* canonical typed-string IDs across all sister modules is a follow-up
|
|
21
21
|
* task.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* mapped to internal sequential numbers, `parent: GOAL-X` becomes
|
|
16
16
|
* `parent_id: <numeric>`, `parent_id: 0` denotes a root.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* This lives at the input boundary so the library's
|
|
19
19
|
* internal types (numeric IDs) stay where they are. Convergence on
|
|
20
20
|
* canonical typed-string IDs across all sister modules is a follow-up
|
|
21
21
|
* task.
|
package/dist/goals/types.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface LayoutOptions {
|
|
|
44
44
|
nodeSep?: number;
|
|
45
45
|
hideCollapsed?: number[];
|
|
46
46
|
viewDepth?: number | null;
|
|
47
|
-
/** Trim the tree to a level cap or a subtree root
|
|
47
|
+
/** Trim the tree to a level cap or a subtree root. Defaults to 'all'. */
|
|
48
48
|
scope?: Scope;
|
|
49
49
|
}
|
|
50
50
|
export interface LaidOutNode {
|
package/dist/goals/validate.js
CHANGED
|
@@ -15,7 +15,7 @@ export function validateGoalTree(input) {
|
|
|
15
15
|
return { valid: false, errors, warnings };
|
|
16
16
|
const tree = raw;
|
|
17
17
|
// GOALS-013 — goal_types[].level values must be contiguous starting from 0.
|
|
18
|
-
// Per the N+1 rule
|
|
18
|
+
// Per the N+1 rule, the level set must
|
|
19
19
|
// be exactly {0, 1, …, N}; a gap breaks the parent→child level invariant
|
|
20
20
|
// and means GOALS-012 cannot be enforced consistently.
|
|
21
21
|
const levels = tree.goal_types
|
|
@@ -84,7 +84,7 @@ export function validateGoalTree(input) {
|
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
// GOALS-012 — parent must be exactly one level above the child (N+1
|
|
87
|
-
// hierarchy
|
|
87
|
+
// hierarchy). Only enforced when the
|
|
88
88
|
// parent is resolvable; a missing parent is covered by BROKEN_PARENT_REF.
|
|
89
89
|
if (g.parent_id !== 0) {
|
|
90
90
|
const parentLevel = levelById.get(g.parent_id);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const ENVELOPE_FIELDS: string[];
|
|
2
|
+
/** Every non-blank, non-comment line of the file, except those belonging to
|
|
3
|
+
* an `ENVELOPE_FIELDS` block (the key line and its more-indented
|
|
4
|
+
* continuation lines), whitespace-normalised and sorted —
|
|
5
|
+
* order-independent, formatting-independent, comment-independent. Not a
|
|
6
|
+
* general YAML parser — a flat, line-oriented pass over the already-known
|
|
7
|
+
* envelope field names, the same posture the methodology reference script
|
|
8
|
+
* takes (its own header comment cites `baseline-manifest.mjs` /
|
|
9
|
+
* `check-agreement.mjs` for precedent). */
|
|
10
|
+
export declare function statementLines(text: string): string[];
|
|
11
|
+
export interface LineEdit {
|
|
12
|
+
from: string;
|
|
13
|
+
to: string;
|
|
14
|
+
}
|
|
15
|
+
/** A parsed `migrations/<slug>/TRANSFORM.yaml` manifest (§16.3). */
|
|
16
|
+
export interface MigrationManifest {
|
|
17
|
+
slug: string;
|
|
18
|
+
mechanical: boolean;
|
|
19
|
+
appliesTo: string[];
|
|
20
|
+
lineEdits: LineEdit[];
|
|
21
|
+
}
|
|
22
|
+
/** Minimal parse of the fixed `TRANSFORM.yaml` shape — `mechanical: true`,
|
|
23
|
+
* `applies_to: [exact relative paths]`, `line_edits: [{from, to}]`. Not a
|
|
24
|
+
* general YAML parser, same posture as {@link statementLines}. */
|
|
25
|
+
export declare function parseMigrationManifest(text: string): Omit<MigrationManifest, 'slug'>;
|
|
26
|
+
export interface SuspicionResult {
|
|
27
|
+
suspicious: boolean;
|
|
28
|
+
/** True when a manifest declared `mechanical: true` for this target but
|
|
29
|
+
* its replay did not fully explain the change — the hatch does not
|
|
30
|
+
* self-grant; suspicion stands regardless of the flag (§16.3). */
|
|
31
|
+
hatchRefused?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* §16.2: *Suspicious* ⟺ the target's content identity at the anchor commit
|
|
35
|
+
* differs from its content identity now, **and** §16.3's hatch does not
|
|
36
|
+
* explain the difference.
|
|
37
|
+
*
|
|
38
|
+
* `beforeText` / `afterText` are already-fetched (git IO lives in the
|
|
39
|
+
* caller); `undefined` for either (no anchor, or the target no longer
|
|
40
|
+
* exists) means nothing to compare — not suspicious, per the reference
|
|
41
|
+
* script's `target-absent-at-anchor` / `target-absent-now` cases.
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkSuspicion(beforeText: string | undefined, afterText: string | undefined, applicableManifests?: Array<Omit<MigrationManifest, 'slug'>>): SuspicionResult;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Link suspicion — methodology CONTRACT.md §16, reference implementation
|
|
2
|
+
// `scripts/check-link-suspicion.mjs`. Pure: content-identity normalisation
|
|
3
|
+
// (§16.1) and the suspicion verdict (§16.2 / §16.3's mechanical hatch) over
|
|
4
|
+
// already-fetched file text. Git plumbing (finding the anchor commit,
|
|
5
|
+
// reading a file at a ref) and the repo walk are IO and live in the CLI
|
|
6
|
+
// (`src/repo-validate.ts`), same split as the rest of this package.
|
|
7
|
+
//
|
|
8
|
+
// Nothing here is stored: a caller re-derives the verdict on every check;
|
|
9
|
+
// no file ever carries a `suspicious: true` flag (§16.2).
|
|
10
|
+
// The administrative envelope (§16.1) — field names CONTRACT.md already
|
|
11
|
+
// defines by name (§6 / §6.1 / §6.2 / §6.3 when present / §7). One list,
|
|
12
|
+
// defined once; a spec revision that adds an envelope field extends this
|
|
13
|
+
// list in the same revision rather than inventing a second mechanism.
|
|
14
|
+
export const ENVELOPE_FIELDS = [
|
|
15
|
+
'zone', 'admitted_at', 'admitted_by', 'gate_checks', 'derived_from',
|
|
16
|
+
'admission_state', 'proposed_at', 'proposed_by', 'owner_to_confirm',
|
|
17
|
+
'rejected_at', 'rejected_by', 'rejection_reason',
|
|
18
|
+
'reviewer_authority',
|
|
19
|
+
'agreement', 'agreed_by', 'agreed_at',
|
|
20
|
+
'valid_from', 'valid_to',
|
|
21
|
+
];
|
|
22
|
+
function stripComment(line) {
|
|
23
|
+
let inQuotes = false;
|
|
24
|
+
for (let i = 0; i < line.length; i++) {
|
|
25
|
+
const ch = line[i];
|
|
26
|
+
if (ch === '"')
|
|
27
|
+
inQuotes = !inQuotes;
|
|
28
|
+
else if (ch === '#' && !inQuotes)
|
|
29
|
+
return line.slice(0, i);
|
|
30
|
+
}
|
|
31
|
+
return line;
|
|
32
|
+
}
|
|
33
|
+
/** Every non-blank, non-comment line of the file, except those belonging to
|
|
34
|
+
* an `ENVELOPE_FIELDS` block (the key line and its more-indented
|
|
35
|
+
* continuation lines), whitespace-normalised and sorted —
|
|
36
|
+
* order-independent, formatting-independent, comment-independent. Not a
|
|
37
|
+
* general YAML parser — a flat, line-oriented pass over the already-known
|
|
38
|
+
* envelope field names, the same posture the methodology reference script
|
|
39
|
+
* takes (its own header comment cites `baseline-manifest.mjs` /
|
|
40
|
+
* `check-agreement.mjs` for precedent). */
|
|
41
|
+
export function statementLines(text) {
|
|
42
|
+
const kept = [];
|
|
43
|
+
let skipIndent = null;
|
|
44
|
+
for (const raw of text.split('\n')) {
|
|
45
|
+
const stripped = stripComment(raw).replace(/\s+$/, '');
|
|
46
|
+
if (!stripped.trim())
|
|
47
|
+
continue;
|
|
48
|
+
const indent = stripped.match(/^ */)?.[0].length ?? 0;
|
|
49
|
+
if (skipIndent !== null) {
|
|
50
|
+
if (indent > skipIndent)
|
|
51
|
+
continue;
|
|
52
|
+
skipIndent = null;
|
|
53
|
+
}
|
|
54
|
+
const keyMatch = stripped.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*):/);
|
|
55
|
+
if (keyMatch && keyMatch[1].length === 0 && ENVELOPE_FIELDS.includes(keyMatch[2])) {
|
|
56
|
+
skipIndent = indent;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
kept.push(stripped.trim().replace(/\s+/g, ' '));
|
|
60
|
+
}
|
|
61
|
+
return kept.sort();
|
|
62
|
+
}
|
|
63
|
+
function sameLines(a, b) {
|
|
64
|
+
return a.length === b.length && a.every((line, i) => line === b[i]);
|
|
65
|
+
}
|
|
66
|
+
function unquote(s) {
|
|
67
|
+
const m = s.match(/^"(.*)"$/);
|
|
68
|
+
return m ? m[1] : s;
|
|
69
|
+
}
|
|
70
|
+
function normalizeEditLine(s) {
|
|
71
|
+
return unquote(s.trim()).trim().replace(/\s+/g, ' ');
|
|
72
|
+
}
|
|
73
|
+
/** Minimal parse of the fixed `TRANSFORM.yaml` shape — `mechanical: true`,
|
|
74
|
+
* `applies_to: [exact relative paths]`, `line_edits: [{from, to}]`. Not a
|
|
75
|
+
* general YAML parser, same posture as {@link statementLines}. */
|
|
76
|
+
export function parseMigrationManifest(text) {
|
|
77
|
+
const manifest = { mechanical: false, appliesTo: [], lineEdits: [] };
|
|
78
|
+
let section = null;
|
|
79
|
+
let current = null;
|
|
80
|
+
for (const raw of text.split('\n')) {
|
|
81
|
+
const line = raw.replace(/\s+$/, '');
|
|
82
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
83
|
+
continue;
|
|
84
|
+
const topMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
85
|
+
if (topMatch) {
|
|
86
|
+
const [, key, val] = topMatch;
|
|
87
|
+
if (key === 'mechanical') {
|
|
88
|
+
manifest.mechanical = val.trim() === 'true';
|
|
89
|
+
section = null;
|
|
90
|
+
}
|
|
91
|
+
else if (key === 'applies_to') {
|
|
92
|
+
section = 'applies_to';
|
|
93
|
+
}
|
|
94
|
+
else if (key === 'line_edits') {
|
|
95
|
+
section = 'line_edits';
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (section === 'applies_to') {
|
|
100
|
+
const m = line.match(/^\s*-\s*(.+)$/);
|
|
101
|
+
if (m)
|
|
102
|
+
manifest.appliesTo.push(unquote(m[1].trim()));
|
|
103
|
+
}
|
|
104
|
+
else if (section === 'line_edits') {
|
|
105
|
+
const fromMatch = line.match(/^\s*-\s*from:\s*(.+)$/);
|
|
106
|
+
if (fromMatch) {
|
|
107
|
+
current = { from: normalizeEditLine(fromMatch[1]), to: '' };
|
|
108
|
+
manifest.lineEdits.push(current);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const toMatch = line.match(/^\s*to:\s*(.+)$/);
|
|
112
|
+
if (toMatch && current)
|
|
113
|
+
current.to = normalizeEditLine(toMatch[1]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return manifest;
|
|
117
|
+
}
|
|
118
|
+
/** Replay a manifest's declared `lineEdits` against the before-state and
|
|
119
|
+
* check the result matches the after-state exactly — the independent
|
|
120
|
+
* verification §16.3 requires: the tool's `mechanical: true` declaration is
|
|
121
|
+
* never trusted on its own; only a replay that reproduces the after-state
|
|
122
|
+
* suppresses suspicion. */
|
|
123
|
+
function replayExplains(beforeLines, afterLines, lineEdits) {
|
|
124
|
+
const working = [...beforeLines];
|
|
125
|
+
for (const edit of lineEdits) {
|
|
126
|
+
const idx = working.indexOf(edit.from);
|
|
127
|
+
if (idx === -1)
|
|
128
|
+
return false;
|
|
129
|
+
working.splice(idx, 1, edit.to);
|
|
130
|
+
}
|
|
131
|
+
return sameLines([...working].sort(), [...afterLines].sort());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* §16.2: *Suspicious* ⟺ the target's content identity at the anchor commit
|
|
135
|
+
* differs from its content identity now, **and** §16.3's hatch does not
|
|
136
|
+
* explain the difference.
|
|
137
|
+
*
|
|
138
|
+
* `beforeText` / `afterText` are already-fetched (git IO lives in the
|
|
139
|
+
* caller); `undefined` for either (no anchor, or the target no longer
|
|
140
|
+
* exists) means nothing to compare — not suspicious, per the reference
|
|
141
|
+
* script's `target-absent-at-anchor` / `target-absent-now` cases.
|
|
142
|
+
*/
|
|
143
|
+
export function checkSuspicion(beforeText, afterText, applicableManifests = []) {
|
|
144
|
+
if (beforeText === undefined || afterText === undefined)
|
|
145
|
+
return { suspicious: false };
|
|
146
|
+
const beforeLines = statementLines(beforeText);
|
|
147
|
+
const afterLines = statementLines(afterText);
|
|
148
|
+
if (sameLines(beforeLines, afterLines))
|
|
149
|
+
return { suspicious: false };
|
|
150
|
+
for (const manifest of applicableManifests) {
|
|
151
|
+
if (replayExplains(beforeLines, afterLines, manifest.lineEdits)) {
|
|
152
|
+
return { suspicious: false };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { suspicious: true, hatchRefused: applicableManifests.some((m) => m.mechanical) };
|
|
156
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
/** How to read movement relative to `target` (§7.27). */
|
|
3
|
+
export type MetricDirectionOfGood = 'higher_is_better' | 'lower_is_better' | 'on_target';
|
|
4
|
+
export declare const METRIC_DIRECTIONS_OF_GOOD: readonly MetricDirectionOfGood[];
|
|
5
|
+
export interface Metric {
|
|
6
|
+
notation: 'metric';
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Typed IDs of what this metric tracks — GOAL, CAPABILITY, or PROCESS. Non-empty. */
|
|
10
|
+
measures: string[];
|
|
11
|
+
/** Free-form unit the value and `target` are expressed in (percent, days, …). */
|
|
12
|
+
unit: string;
|
|
13
|
+
target: number;
|
|
14
|
+
direction_of_good: MetricDirectionOfGood;
|
|
15
|
+
/** ROLE-… accountable for tracking this metric and acting on it. */
|
|
16
|
+
owner_role: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
zone: 'canon';
|
|
19
|
+
admitted_at: string;
|
|
20
|
+
admitted_by: string;
|
|
21
|
+
gate_checks: GateChecks;
|
|
22
|
+
valid_from: string;
|
|
23
|
+
valid_to: string | null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
2
|
+
import { type CanonCatalog } from '../typed-id.js';
|
|
3
|
+
export interface MetricValidateOptions {
|
|
4
|
+
/** When provided, METRIC-002/004 enforce artefact existence for cross-refs. */
|
|
5
|
+
catalog?: CanonCatalog;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateMetric(input: unknown, options?: MetricValidateOptions): ValidationResult;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// METRIC validator — methodology ELEMENT_PRIMITIVES.md §7.27 / §9.
|
|
2
|
+
//
|
|
3
|
+
// Codes:
|
|
4
|
+
// METRIC-001 — shape / id grammar / required envelope + per-type fields
|
|
5
|
+
// (measures, unit, target, direction_of_good, owner_role).
|
|
6
|
+
// METRIC-002 — measures is empty, or an entry does not resolve to an
|
|
7
|
+
// admitted GOAL / CAPABILITY / PROCESS in canon.
|
|
8
|
+
// METRIC-003 — direction_of_good outside {higher_is_better, lower_is_better,
|
|
9
|
+
// on_target}.
|
|
10
|
+
// METRIC-004 — owner_role does not resolve to an admitted ROLE in canon.
|
|
11
|
+
//
|
|
12
|
+
// Resolution rules (METRIC-002/004) that need artefact *existence* run only
|
|
13
|
+
// when a `CanonCatalog` is supplied; the TYPE checks are derivable from the
|
|
14
|
+
// id prefix and run either way.
|
|
15
|
+
import { typeOfId, isCanonicalIdOfType } from '../typed-id.js';
|
|
16
|
+
import { METRIC_DIRECTIONS_OF_GOOD } from './types.js';
|
|
17
|
+
const MEASURES_TYPES = ['GOAL', 'CAPABILITY', 'PROCESS'];
|
|
18
|
+
export function validateMetric(input, options = {}) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
22
|
+
errors.push({ code: 'METRIC-001', message: 'Metric must be a YAML mapping.' });
|
|
23
|
+
return { valid: false, errors, warnings };
|
|
24
|
+
}
|
|
25
|
+
const m = input;
|
|
26
|
+
// METRIC-001 — id grammar + notation tag + envelope + plain per-type fields.
|
|
27
|
+
if (!isCanonicalIdOfType(m.id, 'METRIC')) {
|
|
28
|
+
errors.push({ code: 'METRIC-001', message: `id "${String(m.id)}" must match METRIC-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
29
|
+
}
|
|
30
|
+
if (m.notation !== 'metric') {
|
|
31
|
+
errors.push({ code: 'METRIC-001', message: 'notation must be the fixed value "metric".', path: 'notation' });
|
|
32
|
+
}
|
|
33
|
+
if (m.zone !== 'canon') {
|
|
34
|
+
errors.push({ code: 'METRIC-001', message: 'zone is required and must be "canon".', path: 'zone' });
|
|
35
|
+
}
|
|
36
|
+
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from', 'unit', 'owner_role']) {
|
|
37
|
+
if (typeof m[f] !== 'string' || m[f].trim() === '') {
|
|
38
|
+
errors.push({ code: 'METRIC-001', message: `${f} is required.`, path: f });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (m.gate_checks === null || typeof m.gate_checks !== 'object') {
|
|
42
|
+
errors.push({ code: 'METRIC-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
43
|
+
}
|
|
44
|
+
if (!('valid_to' in m) || !(typeof m.valid_to === 'string' || m.valid_to === null)) {
|
|
45
|
+
errors.push({ code: 'METRIC-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
46
|
+
}
|
|
47
|
+
if (typeof m.target !== 'number' || Number.isNaN(m.target)) {
|
|
48
|
+
errors.push({ code: 'METRIC-001', message: 'target is required and must be a number.', path: 'target' });
|
|
49
|
+
}
|
|
50
|
+
// direction_of_good — required (METRIC-001) + enum (METRIC-003).
|
|
51
|
+
const dir = m.direction_of_good;
|
|
52
|
+
if (dir === undefined || dir === null || dir === '') {
|
|
53
|
+
errors.push({ code: 'METRIC-001', message: 'direction_of_good is required.', path: 'direction_of_good' });
|
|
54
|
+
}
|
|
55
|
+
else if (!METRIC_DIRECTIONS_OF_GOOD.includes(dir)) {
|
|
56
|
+
errors.push({
|
|
57
|
+
code: 'METRIC-003',
|
|
58
|
+
message: `direction_of_good "${String(dir)}" must be one of ${METRIC_DIRECTIONS_OF_GOOD.join(', ')}.`,
|
|
59
|
+
path: 'direction_of_good',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// measures — required non-empty list (METRIC-001), each resolves to GOAL/CAPABILITY/PROCESS (METRIC-002).
|
|
63
|
+
const measures = m.measures;
|
|
64
|
+
if (!Array.isArray(measures) || measures.length === 0) {
|
|
65
|
+
errors.push({ code: 'METRIC-001', message: 'measures is required and must be a non-empty list of typed IDs.', path: 'measures' });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
measures.forEach((ref, i) => {
|
|
69
|
+
const type = typeOfId(ref);
|
|
70
|
+
if (!type || !MEASURES_TYPES.includes(type)) {
|
|
71
|
+
errors.push({ code: 'METRIC-002', message: `measures[${i}] "${String(ref)}" must resolve to a GOAL, CAPABILITY, or PROCESS.`, path: `measures[${i}]` });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.catalog && options.catalog.typeOf(ref) === undefined) {
|
|
75
|
+
errors.push({ code: 'METRIC-002', message: `measures[${i}] "${String(ref)}" does not resolve to an admitted artefact.`, path: `measures[${i}]` });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// owner_role — must be a typed ROLE id (METRIC-004), and resolve when a catalog is supplied.
|
|
80
|
+
const ownerRole = m.owner_role;
|
|
81
|
+
if (typeof ownerRole === 'string' && ownerRole.trim() !== '') {
|
|
82
|
+
if (!isCanonicalIdOfType(ownerRole, 'ROLE')) {
|
|
83
|
+
errors.push({ code: 'METRIC-004', message: `owner_role "${ownerRole}" must be a typed ROLE id.`, path: 'owner_role' });
|
|
84
|
+
}
|
|
85
|
+
else if (options.catalog && options.catalog.typeOf(ownerRole) === undefined) {
|
|
86
|
+
errors.push({ code: 'METRIC-004', message: `owner_role "${ownerRole}" does not resolve to an admitted artefact.`, path: 'owner_role' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { validateNeed } from './validate.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
import type { AgreementValue } from '../agreement.js';
|
|
3
|
+
export interface Need {
|
|
4
|
+
notation: 'need';
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** Typed ID of the STAKEHOLDER whose need this is. */
|
|
8
|
+
stakeholder: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** `draft` \| `agreed` \| `disputed`. Absent ⇒ `agreed` (back-compat). */
|
|
11
|
+
agreement?: AgreementValue;
|
|
12
|
+
/** Required whenever `agreement` is present (AGREE-003). Written only by a human when `agreement: agreed` (AGREE-002). */
|
|
13
|
+
agreed_by?: string;
|
|
14
|
+
agreed_at?: string;
|
|
15
|
+
zone: 'canon';
|
|
16
|
+
admitted_at: string;
|
|
17
|
+
admitted_by: string;
|
|
18
|
+
gate_checks: GateChecks;
|
|
19
|
+
valid_from: string;
|
|
20
|
+
valid_to: string | null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
2
|
+
import { type CanonCatalog } from '../typed-id.js';
|
|
3
|
+
export interface NeedValidateOptions {
|
|
4
|
+
/** When provided, NEED-002 enforces artefact existence for `stakeholder`. */
|
|
5
|
+
catalog?: CanonCatalog;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateNeed(input: unknown, options?: NeedValidateOptions): ValidationResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// NEED validator — methodology ELEMENT_PRIMITIVES.md §7.28 / §9.
|
|
2
|
+
//
|
|
3
|
+
// Codes:
|
|
4
|
+
// NEED-001 — shape / id grammar / required envelope fields, including the
|
|
5
|
+
// required per-type `stakeholder` field.
|
|
6
|
+
// NEED-002 — stakeholder does not resolve to an admitted STAKEHOLDER in canon.
|
|
7
|
+
//
|
|
8
|
+
// The cross-cutting NEED-COVERAGE-001 / NEED-VALIDATION-COVERAGE-001..002
|
|
9
|
+
// reverse-trace rules are owned elsewhere (CONTRACT.md §8, the compliance
|
|
10
|
+
// reverse-index) and are out of scope for this single-artefact validator —
|
|
11
|
+
// same posture as `validateRequirement` / `validateVerification`.
|
|
12
|
+
//
|
|
13
|
+
// Resolution (NEED-002 existence) runs only when a `CanonCatalog` is
|
|
14
|
+
// supplied; the TYPE check is derivable from the id prefix and runs either way.
|
|
15
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
16
|
+
import { checkAgreement } from '../agreement.js';
|
|
17
|
+
export function validateNeed(input, options = {}) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const warnings = [];
|
|
20
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
21
|
+
errors.push({ code: 'NEED-001', message: 'Need must be a YAML mapping.' });
|
|
22
|
+
return { valid: false, errors, warnings };
|
|
23
|
+
}
|
|
24
|
+
const n = input;
|
|
25
|
+
// NEED-001 — id grammar + notation tag + envelope.
|
|
26
|
+
if (!isCanonicalIdOfType(n.id, 'NEED')) {
|
|
27
|
+
errors.push({ code: 'NEED-001', message: `id "${String(n.id)}" must match NEED-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
28
|
+
}
|
|
29
|
+
if (n.notation !== 'need') {
|
|
30
|
+
errors.push({ code: 'NEED-001', message: 'notation must be the fixed value "need".', path: 'notation' });
|
|
31
|
+
}
|
|
32
|
+
if (n.zone !== 'canon') {
|
|
33
|
+
errors.push({ code: 'NEED-001', message: 'zone is required and must be "canon".', path: 'zone' });
|
|
34
|
+
}
|
|
35
|
+
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from']) {
|
|
36
|
+
if (typeof n[f] !== 'string' || n[f].trim() === '') {
|
|
37
|
+
errors.push({ code: 'NEED-001', message: `${f} is required.`, path: f });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (n.gate_checks === null || typeof n.gate_checks !== 'object') {
|
|
41
|
+
errors.push({ code: 'NEED-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
42
|
+
}
|
|
43
|
+
if (!('valid_to' in n) || !(typeof n.valid_to === 'string' || n.valid_to === null)) {
|
|
44
|
+
errors.push({ code: 'NEED-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
45
|
+
}
|
|
46
|
+
// stakeholder — required (NEED-001) + resolves to STAKEHOLDER (NEED-002).
|
|
47
|
+
const stakeholder = n.stakeholder;
|
|
48
|
+
if (typeof stakeholder !== 'string' || stakeholder.trim() === '') {
|
|
49
|
+
errors.push({ code: 'NEED-001', message: 'stakeholder is required.', path: 'stakeholder' });
|
|
50
|
+
}
|
|
51
|
+
else if (!isCanonicalIdOfType(stakeholder, 'STAKEHOLDER')) {
|
|
52
|
+
errors.push({ code: 'NEED-002', message: `stakeholder "${stakeholder}" must be a typed STAKEHOLDER id.`, path: 'stakeholder' });
|
|
53
|
+
}
|
|
54
|
+
else if (options.catalog && options.catalog.typeOf(stakeholder) === undefined) {
|
|
55
|
+
errors.push({ code: 'NEED-002', message: `stakeholder "${stakeholder}" does not resolve to an admitted artefact.`, path: 'stakeholder' });
|
|
56
|
+
}
|
|
57
|
+
// AGREE-001..003 — agreement axis (CONTRACT.md §6.3.1), optional; absent ⇒ agreed.
|
|
58
|
+
errors.push(...checkAgreement(n));
|
|
59
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
2
|
+
/** Run the versioned-attribute sidecar checks (VERSIONED-001..005) over the
|
|
3
|
+
* loaded element set and append findings. Pure, deterministic order. */
|
|
4
|
+
export declare function checkVersionedAttributes(input: RepoModelInput, findings: RepoFinding[]): void;
|