@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.
Files changed (87) hide show
  1. package/dist/activities/resolver.d.ts +3 -0
  2. package/dist/activities/resolver.js +39 -18
  3. package/dist/activities/validate.js +59 -0
  4. package/dist/canon-resolver-utils.d.ts +3 -0
  5. package/dist/canon-resolver-utils.js +16 -0
  6. package/dist/codex/index.d.ts +1 -1
  7. package/dist/codex/index.js +1 -1
  8. package/dist/codex/types.d.ts +9 -2
  9. package/dist/codex/types.js +10 -3
  10. package/dist/codex/validate.js +29 -3
  11. package/dist/compliance/classify.d.ts +11 -1
  12. package/dist/compliance/classify.js +31 -17
  13. package/dist/compliance/impact.d.ts +3 -1
  14. package/dist/compliance/impact.js +41 -13
  15. package/dist/compliance/types.d.ts +6 -0
  16. package/dist/compliance-matrix/build.js +0 -0
  17. package/dist/compliance-verification-matrix/build.d.ts +17 -0
  18. package/dist/compliance-verification-matrix/build.js +65 -0
  19. package/dist/compliance-verification-matrix/csv.d.ts +6 -0
  20. package/dist/compliance-verification-matrix/csv.js +41 -0
  21. package/dist/compliance-verification-matrix/index.d.ts +3 -0
  22. package/dist/compliance-verification-matrix/index.js +2 -0
  23. package/dist/compliance-verification-matrix/types.d.ts +75 -0
  24. package/dist/compliance-verification-matrix/types.js +4 -0
  25. package/dist/fgca/action-change-ids.d.ts +7 -0
  26. package/dist/fgca/action-change-ids.js +11 -0
  27. package/dist/fgca/parse-canonical.d.ts +3 -1
  28. package/dist/fgca/parse-canonical.js +13 -8
  29. package/dist/fgca/resolver.d.ts +6 -2
  30. package/dist/fgca/resolver.js +12 -6
  31. package/dist/fgca/validate.d.ts +2 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/process-blueprint/index.d.ts +3 -1
  35. package/dist/process-blueprint/index.js +1 -0
  36. package/dist/process-blueprint/layout.js +33 -29
  37. package/dist/process-blueprint/resolve-columns.d.ts +52 -0
  38. package/dist/process-blueprint/resolve-columns.js +138 -0
  39. package/dist/process-blueprint/types.d.ts +23 -5
  40. package/dist/process-blueprint/validate.d.ts +19 -1
  41. package/dist/process-blueprint/validate.js +73 -21
  42. package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
  43. package/dist/repo-validate/check-candidate-fields.js +87 -0
  44. package/dist/repo-validate/check-process-parent.d.ts +8 -0
  45. package/dist/repo-validate/check-process-parent.js +172 -0
  46. package/dist/repo-validate/index.d.ts +1 -0
  47. package/dist/repo-validate/index.js +1 -0
  48. package/dist/repo-validate/validate-repo.js +7 -0
  49. package/dist/requirement/types.d.ts +1 -1
  50. package/dist/requirement/types.js +1 -1
  51. package/dist/webview/entry.js +11 -4
  52. package/dist/webview/render-fgca.js +1 -1
  53. package/dist/webview/render-process.d.ts +19 -1
  54. package/dist/webview/render-process.js +75 -42
  55. package/package.json +1 -1
  56. package/src/activities/resolver.ts +40 -20
  57. package/src/activities/validate.ts +58 -0
  58. package/src/canon-resolver-utils.ts +19 -0
  59. package/src/codex/index.ts +1 -1
  60. package/src/codex/types.ts +11 -3
  61. package/src/codex/validate.ts +30 -2
  62. package/src/compliance/classify.ts +40 -17
  63. package/src/compliance/impact.ts +41 -12
  64. package/src/compliance/types.ts +6 -0
  65. package/src/compliance-matrix/build.ts +0 -0
  66. package/src/compliance-verification-matrix/build.ts +84 -0
  67. package/src/compliance-verification-matrix/csv.ts +47 -0
  68. package/src/compliance-verification-matrix/index.ts +7 -0
  69. package/src/compliance-verification-matrix/types.ts +85 -0
  70. package/src/fgca/action-change-ids.ts +13 -0
  71. package/src/fgca/parse-canonical.ts +12 -8
  72. package/src/fgca/resolver.ts +15 -6
  73. package/src/fgca/validate.ts +2 -0
  74. package/src/index.ts +1 -0
  75. package/src/process-blueprint/index.ts +11 -0
  76. package/src/process-blueprint/layout.ts +48 -29
  77. package/src/process-blueprint/resolve-columns.ts +147 -0
  78. package/src/process-blueprint/types.ts +19 -5
  79. package/src/process-blueprint/validate.ts +107 -21
  80. package/src/repo-validate/check-candidate-fields.ts +93 -0
  81. package/src/repo-validate/check-process-parent.ts +184 -0
  82. package/src/repo-validate/index.ts +5 -0
  83. package/src/repo-validate/validate-repo.ts +7 -0
  84. package/src/requirement/types.ts +1 -1
  85. package/src/webview/entry.ts +13 -6
  86. package/src/webview/render-fgca.ts +1 -1
  87. package/src/webview/render-process.ts +94 -49
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Render-time resolution of Process Blueprint column headers and the
3
+ * compliance-lane join key.
4
+ *
5
+ * A `PROCESS-…` column is the process: `name` / `goal` / `result` come from
6
+ * the child PROCESS element (ELEMENT_PRIMITIVES §7.5), never from restated
7
+ * view fields. A `STAGE-…` column keeps the authored sketch copy.
8
+ *
9
+ * Pure: no I/O. The host (VS Code preview, JCEF, CLI) already owns loading
10
+ * canon/elements/** — this module only knows how to pick PROCESS / STEP
11
+ * records out of that pool.
12
+ *
13
+ * This is a read model, never storage. Callers render an ephemeral display
14
+ * copy and must never serialise it back onto the view.
15
+ */
16
+ const PROCESS_ID_RE = /^PROCESS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
17
+ const STAGE_ID_RE = /^STAGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
18
+ export function isProcessColumnId(id) {
19
+ return PROCESS_ID_RE.test(id);
20
+ }
21
+ export function isStageColumnId(id) {
22
+ return STAGE_ID_RE.test(id);
23
+ }
24
+ function isRecord(v) {
25
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
26
+ }
27
+ function optionalString(v) {
28
+ return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
29
+ }
30
+ /**
31
+ * Collect admitted PROCESS elements from a raw canon/elements/** doc pool,
32
+ * keyed by id. Malformed documents are skipped.
33
+ */
34
+ export function collectProcessColumnRecords(docs) {
35
+ const out = new Map();
36
+ for (const doc of docs) {
37
+ if (!isRecord(doc))
38
+ continue;
39
+ if (doc['notation'] !== 'process')
40
+ continue;
41
+ const id = optionalString(doc['id']);
42
+ if (!id || !isProcessColumnId(id))
43
+ continue;
44
+ const name = optionalString(doc['name']);
45
+ if (!name)
46
+ continue;
47
+ const rec = { name };
48
+ const goal = optionalString(doc['goal']);
49
+ const result = optionalString(doc['result']);
50
+ if (goal)
51
+ rec.goal = goal;
52
+ if (result)
53
+ rec.result = result;
54
+ out.set(id, rec);
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Map each STEP id onto the PROCESS that contains it — either inline in
60
+ * `PROCESS.flow.steps[]` or a promoted `{ notation: step, process }` element.
61
+ * First writer wins; a later duplicate is ignored.
62
+ */
63
+ export function collectStepHomeProcess(docs) {
64
+ const out = new Map();
65
+ for (const doc of docs) {
66
+ if (!isRecord(doc))
67
+ continue;
68
+ if (doc['notation'] === 'process') {
69
+ const processId = optionalString(doc['id']);
70
+ if (!processId || !isProcessColumnId(processId))
71
+ continue;
72
+ const flow = isRecord(doc['flow']) ? doc['flow'] : undefined;
73
+ const steps = flow && Array.isArray(flow['steps']) ? flow['steps'] : [];
74
+ for (const step of steps) {
75
+ if (!isRecord(step))
76
+ continue;
77
+ const stepId = optionalString(step['id']);
78
+ if (!stepId || out.has(stepId))
79
+ continue;
80
+ out.set(stepId, processId);
81
+ }
82
+ continue;
83
+ }
84
+ if (doc['notation'] === 'step') {
85
+ const stepId = optionalString(doc['id']);
86
+ const processId = optionalString(doc['process']);
87
+ if (!stepId || !processId || !isProcessColumnId(processId))
88
+ continue;
89
+ if (!out.has(stepId))
90
+ out.set(stepId, processId);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * Display copy for one column. PROCESS- columns ignore any restated view
97
+ * fields; missing catalogue falls back to the id (header) and empty
98
+ * goal/result. STAGE- columns keep authored name/goal/result.
99
+ */
100
+ export function resolveColumnDisplay(stage, catalog) {
101
+ const id = typeof stage.id === 'string' ? stage.id.trim() : '';
102
+ if (isProcessColumnId(id)) {
103
+ const rec = catalog?.get(id);
104
+ return {
105
+ name: rec?.name ?? id,
106
+ goal: rec?.goal ?? '',
107
+ result: rec?.result ?? '',
108
+ };
109
+ }
110
+ return {
111
+ name: typeof stage.name === 'string' ? stage.name : '',
112
+ goal: typeof stage.goal === 'string' ? stage.goal : '',
113
+ result: typeof stage.result === 'string' ? stage.result : '',
114
+ };
115
+ }
116
+ /**
117
+ * Column indexes a `realised_via` token pins on this blueprint.
118
+ *
119
+ * `STAGE-…` is never a join key (ASSERT-004). A `PROCESS-…` token pins that
120
+ * column. A STEP whose home process is a column pins that column.
121
+ */
122
+ export function columnIndexesForRealisedVia(ref, stageIndexById, stepHomeProcess) {
123
+ const token = ref.trim();
124
+ if (token.length === 0)
125
+ return [];
126
+ if (isStageColumnId(token))
127
+ return [];
128
+ const direct = stageIndexById.get(token);
129
+ if (direct !== undefined && isProcessColumnId(token))
130
+ return [direct];
131
+ const home = stepHomeProcess?.get(token);
132
+ if (home && isProcessColumnId(home)) {
133
+ const idx = stageIndexById.get(home);
134
+ if (idx !== undefined)
135
+ return [idx];
136
+ }
137
+ return [];
138
+ }
@@ -27,8 +27,9 @@ export interface ComplianceLaneAssertion {
27
27
  /** Compliance realisation status. */
28
28
  status: 'compliant' | 'partial' | 'non_compliant' | 'under_review' | 'pending_owner' | 'n_a';
29
29
  /**
30
- * Typed IDs of stages / process steps where the requirement is realised.
31
- * When empty/absent, the assertion covers the entire subject, not a specific stage.
30
+ * Typed IDs of processes or steps where the requirement is realised.
31
+ * Sketch `STAGE-…` ids are not a join key. When empty/absent, the assertion
32
+ * covers the entire subject, not a specific column.
32
33
  */
33
34
  realised_via?: string[];
34
35
  }
@@ -89,9 +90,12 @@ export interface ComplianceRow {
89
90
  }
90
91
  export interface Stage {
91
92
  id: string;
92
- name: string;
93
- goal: string;
94
- result: string;
93
+ /** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
94
+ name?: string;
95
+ /** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
96
+ goal?: string;
97
+ /** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
98
+ result?: string;
95
99
  description?: string;
96
100
  }
97
101
  export interface AspectEntry {
@@ -188,6 +192,20 @@ export interface ProcessBlueprintLayoutOptions {
188
192
  * When absent, all stages are shown.
189
193
  */
190
194
  visibleStages?: string[];
195
+ /**
196
+ * Catalogue of `PROCESS-…` column records (id → name / optional goal / result).
197
+ * Required to header catalogued columns; ignored for `STAGE-…` sketches.
198
+ */
199
+ processCatalog?: ReadonlyMap<string, {
200
+ name: string;
201
+ goal?: string;
202
+ result?: string;
203
+ }>;
204
+ /**
205
+ * STEP id → home PROCESS id (contained in `PROCESS.flow` or promoted).
206
+ * Used by the compliance-lane join; omit to skip STEP-level pins.
207
+ */
208
+ stepHomeProcess?: ReadonlyMap<string, string>;
191
209
  }
192
210
  import type { LayoutBounds } from '../geometry.js';
193
211
  export type { LayoutBounds };
@@ -1,3 +1,21 @@
1
1
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
2
+ import type { CanonCatalog } from '../typed-id.js';
2
3
  export type { ValidationError, ValidationWarning, ValidationResult };
3
- export declare function validateProcessBlueprint(input: unknown): ValidationResult;
4
+ export interface ProcessParentEdge {
5
+ /** Child process id (`from`). */
6
+ from: string;
7
+ /** Parent process id (`to`). */
8
+ to: string;
9
+ }
10
+ export interface ProcessBlueprintValidateOptions {
11
+ /** When provided, BP-012 resolves `PROCESS-…` column ids against admitted canon. */
12
+ catalog?: CanonCatalog;
13
+ /**
14
+ * In-effect `process_parent` edges (child → parent). When provided, BP-014
15
+ * warns if `process_blueprint.process` is set and a `PROCESS-…` column has
16
+ * no such edge to that parent. Omit to skip BP-014 (file-scope without the
17
+ * relation catalogue).
18
+ */
19
+ processParentEdges?: readonly ProcessParentEdge[];
20
+ }
21
+ export declare function validateProcessBlueprint(input: unknown, options?: ProcessBlueprintValidateOptions): ValidationResult;
@@ -1,13 +1,27 @@
1
1
  const ID_GRAMMAR_RE = /^[A-Z][A-Z_]*(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
2
2
  const PROCESS_BLUEPRINT_ID_RE = /^PROCESS_BLUEPRINT(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
3
3
  const STAGE_ID_RE = /^STAGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
4
+ const PROCESS_ID_RE = /^PROCESS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
4
5
  const APPLICATION_ID_RE = /^APPLICATION(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
5
6
  const ROLE_ID_RE = /^ROLE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
6
7
  const ASPECT_CATEGORIES = ['systems', 'actors', 'equipment', 'information_entities'];
8
+ const RESTATED_COLUMN_FIELDS = ['name', 'goal', 'result'];
7
9
  function isNonEmptyString(v) {
8
10
  return typeof v === 'string' && v.trim().length > 0;
9
11
  }
10
- export function validateProcessBlueprint(input) {
12
+ function isProcessColumnId(id) {
13
+ return PROCESS_ID_RE.test(id);
14
+ }
15
+ function isStageColumnId(id) {
16
+ return STAGE_ID_RE.test(id);
17
+ }
18
+ function isColumnId(id) {
19
+ return isStageColumnId(id) || isProcessColumnId(id);
20
+ }
21
+ function fieldIsPresent(record, key) {
22
+ return Object.prototype.hasOwnProperty.call(record, key);
23
+ }
24
+ export function validateProcessBlueprint(input, options = {}) {
11
25
  const errors = [];
12
26
  const warnings = [];
13
27
  if (!input || typeof input !== 'object') {
@@ -47,6 +61,7 @@ export function validateProcessBlueprint(input) {
47
61
  return { valid: false, errors, warnings };
48
62
  }
49
63
  const stageIds = new Set();
64
+ const processColumnIds = [];
50
65
  for (let i = 0; i < stagesRaw.length; i++) {
51
66
  const s = stagesRaw[i];
52
67
  const path = `stages[${i}]`;
@@ -55,31 +70,68 @@ export function validateProcessBlueprint(input) {
55
70
  continue;
56
71
  }
57
72
  if (!isNonEmptyString(s['id'])) {
58
- errors.push({ code: 'BP-005', message: `${path}.id is required` });
73
+ errors.push({ code: 'BP-006', message: `${path}.id is required` });
74
+ continue;
75
+ }
76
+ const sid = s['id'].trim();
77
+ if (stageIds.has(sid)) {
78
+ errors.push({ code: 'BP-006', message: `Duplicate stage id: "${sid}"` });
59
79
  }
60
80
  else {
61
- const sid = s['id'].trim();
62
- if (stageIds.has(sid)) {
63
- errors.push({ code: 'BP-006', message: `Duplicate stage id: "${sid}"` });
64
- }
65
- else {
66
- stageIds.add(sid);
81
+ stageIds.add(sid);
82
+ }
83
+ if (!isColumnId(sid)) {
84
+ errors.push({
85
+ code: 'BP-006',
86
+ message: `${path}.id "${sid}" must match STAGE-[<middle>-]<INTEGER> or PROCESS-[<middle>-]<INTEGER>`,
87
+ });
88
+ continue;
89
+ }
90
+ if (isProcessColumnId(sid)) {
91
+ processColumnIds.push(sid);
92
+ for (const field of RESTATED_COLUMN_FIELDS) {
93
+ if (fieldIsPresent(s, field)) {
94
+ errors.push({
95
+ code: 'BP-013',
96
+ message: `${path}.${field} must not be restated on a PROCESS-… column — derive it from the process element`,
97
+ });
98
+ }
67
99
  }
68
- if (!STAGE_ID_RE.test(sid)) {
69
- errors.push({
70
- code: 'BP-006',
71
- message: `${path}.id "${sid}" must match STAGE-[<middle>-]<INTEGER>`,
72
- });
100
+ if (options.catalog) {
101
+ const resolved = options.catalog.typeOf(sid);
102
+ if (resolved !== 'PROCESS') {
103
+ errors.push({
104
+ code: 'BP-012',
105
+ message: `${path}.id "${sid}" must resolve to an admitted PROCESS element`,
106
+ });
107
+ }
73
108
  }
74
109
  }
75
- if (!isNonEmptyString(s['name'])) {
76
- errors.push({ code: 'BP-005', message: `${path}.name is required` });
77
- }
78
- if (!isNonEmptyString(s['goal'])) {
79
- errors.push({ code: 'BP-005', message: `${path}.goal is required` });
110
+ else {
111
+ if (!isNonEmptyString(s['name'])) {
112
+ errors.push({ code: 'BP-005', message: `${path}.name is required` });
113
+ }
114
+ if (!isNonEmptyString(s['goal'])) {
115
+ errors.push({ code: 'BP-005', message: `${path}.goal is required` });
116
+ }
117
+ if (!isNonEmptyString(s['result'])) {
118
+ errors.push({ code: 'BP-005', message: `${path}.result is required` });
119
+ }
80
120
  }
81
- if (!isNonEmptyString(s['result'])) {
82
- errors.push({ code: 'BP-005', message: `${path}.result is required` });
121
+ }
122
+ const parentProcess = isNonEmptyString(pb['process']) ? pb['process'].trim() : undefined;
123
+ if (parentProcess && options.processParentEdges && processColumnIds.length > 0) {
124
+ const linked = new Set(options.processParentEdges
125
+ .filter((e) => e.to === parentProcess)
126
+ .map((e) => e.from));
127
+ for (const child of processColumnIds) {
128
+ if (!linked.has(child)) {
129
+ warnings.push({
130
+ code: 'BP-014',
131
+ message: `Column "${child}" is a PROCESS-… id and process_blueprint.process is "${parentProcess}", ` +
132
+ `but no in-effect process_parent REL links that column to that parent`,
133
+ });
134
+ }
83
135
  }
84
136
  }
85
137
  const usedStageIds = new Set();
@@ -105,7 +157,7 @@ export function validateProcessBlueprint(input) {
105
157
  if (!Array.isArray(entryStages) || entryStages.length === 0) {
106
158
  errors.push({
107
159
  code: 'BP-007',
108
- message: `${path}.stages must be a non-empty array of STAGE-… ids`,
160
+ message: `${path}.stages must be a non-empty array of declared column ids`,
109
161
  });
110
162
  }
111
163
  else {
@@ -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,8 @@
1
+ import type { ProcessParentEdge } from '../process-blueprint/validate.js';
2
+ import type { RepoFinding, RepoModelInput } from './types.js';
3
+ /** Inclusive window: in effect at `asOf` iff valid_from ≤ asOf and (valid_to is null or asOf ≤ valid_to). */
4
+ export declare function relationInEffectAt(data: Record<string, unknown>, asOf: string): boolean;
5
+ export declare function todayIsoDate(now?: Date): string;
6
+ /** In-effect `process_parent` edges (child → parent) among loaded REL files. */
7
+ export declare function collectInEffectProcessParentEdges(input: RepoModelInput, asOf?: string): ProcessParentEdge[];
8
+ export declare function checkProcessParentSemantics(input: RepoModelInput, findings: RepoFinding[]): void;
@@ -0,0 +1,172 @@
1
+ // process_parent relation checks — 17-relations.md §3 / §5 (REL-007, REL-008)
2
+ // and endpoint TYPE (PROCESS → PROCESS), plus the in-effect edge collector
3
+ // that process-blueprint BP-014 consumes.
4
+ //
5
+ // REL-007 is single-file (from === to). REL-008 is cross-cutting over the
6
+ // admitted process_parent graph and is a warning, parallel to REL-006.
7
+ import { docId, endpointId } from './validate-repo.js';
8
+ const PScope = 'repo';
9
+ function isProcessNotation(n) {
10
+ return n === 'process';
11
+ }
12
+ function isoDate(value) {
13
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
14
+ }
15
+ /** Inclusive window: in effect at `asOf` iff valid_from ≤ asOf and (valid_to is null or asOf ≤ valid_to). */
16
+ export function relationInEffectAt(data, asOf) {
17
+ const from = isoDate(data['valid_from']);
18
+ if (from && from > asOf)
19
+ return false;
20
+ const to = data['valid_to'];
21
+ if (to === null || to === undefined)
22
+ return true;
23
+ const toDate = isoDate(to);
24
+ if (!toDate)
25
+ return true;
26
+ return asOf <= toDate;
27
+ }
28
+ export function todayIsoDate(now = new Date()) {
29
+ return now.toISOString().slice(0, 10);
30
+ }
31
+ /** In-effect `process_parent` edges (child → parent) among loaded REL files. */
32
+ export function collectInEffectProcessParentEdges(input, asOf = todayIsoDate()) {
33
+ const edges = [];
34
+ for (const doc of input.relations) {
35
+ if (!doc.data)
36
+ continue;
37
+ if (doc.data['type'] !== 'process_parent')
38
+ continue;
39
+ if (!relationInEffectAt(doc.data, asOf))
40
+ continue;
41
+ const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
42
+ const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
43
+ if (!fromId || !toId)
44
+ continue;
45
+ edges.push({ from: fromId, to: toId });
46
+ }
47
+ return edges;
48
+ }
49
+ function processParentRels(input) {
50
+ const out = [];
51
+ for (const doc of input.relations) {
52
+ if (!doc.data)
53
+ continue;
54
+ if (doc.data['type'] !== 'process_parent')
55
+ continue;
56
+ const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
57
+ const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
58
+ if (!fromId || !toId)
59
+ continue;
60
+ out.push({ relId: docId(doc) ?? '', fromId, toId });
61
+ }
62
+ return out;
63
+ }
64
+ /** REL-007 — process_parent self-reference. */
65
+ function checkSelfReference(rels, findings) {
66
+ for (const rel of rels) {
67
+ if (rel.fromId === rel.toId) {
68
+ findings.push({
69
+ scope: PScope,
70
+ id: rel.relId,
71
+ ruleId: 'REL-007',
72
+ message: `REL-007: process_parent relation '${rel.relId}' has from equal to to ('${rel.fromId}').`,
73
+ });
74
+ }
75
+ }
76
+ }
77
+ /** REL-008 — cycle in the process_parent graph (warning). */
78
+ function checkCycle(rels, findings) {
79
+ const childrenOf = new Map();
80
+ const relByChildParent = new Map();
81
+ const nodes = new Set();
82
+ for (const rel of rels) {
83
+ if (rel.fromId === rel.toId)
84
+ continue;
85
+ nodes.add(rel.fromId);
86
+ nodes.add(rel.toId);
87
+ const list = childrenOf.get(rel.fromId);
88
+ if (list)
89
+ list.push(rel.toId);
90
+ else
91
+ childrenOf.set(rel.fromId, [rel.toId]);
92
+ relByChildParent.set(`${rel.fromId}\0${rel.toId}`, rel.relId);
93
+ }
94
+ const UNVISITED = 0;
95
+ const VISITING = 1;
96
+ const DONE = 2;
97
+ const state = new Map();
98
+ let hit;
99
+ function walk(id) {
100
+ const s = state.get(id) ?? UNVISITED;
101
+ if (s === DONE)
102
+ return false;
103
+ if (s === VISITING) {
104
+ hit = id;
105
+ return true;
106
+ }
107
+ state.set(id, VISITING);
108
+ for (const parent of childrenOf.get(id) ?? []) {
109
+ if (nodes.has(parent) && walk(parent))
110
+ return true;
111
+ }
112
+ state.set(id, DONE);
113
+ return false;
114
+ }
115
+ for (const id of nodes) {
116
+ if (walk(id))
117
+ break;
118
+ }
119
+ if (!hit)
120
+ return;
121
+ const relId = [...relByChildParent.entries()].find(([key]) => key.startsWith(`${hit}\0`))?.[1] ?? hit;
122
+ findings.push({
123
+ scope: PScope,
124
+ id: relId,
125
+ ruleId: 'REL-008',
126
+ severity: 'warning',
127
+ message: `REL-008: process_parent graph contains a cycle involving '${hit}'.`,
128
+ });
129
+ }
130
+ /**
131
+ * Endpoint TYPE for process_parent: both ends must be PROCESS when they
132
+ * resolve. Unknown endpoints stay with referential integrity; this pass only
133
+ * fires when the element exists and is the wrong notation.
134
+ */
135
+ function checkEndpointTypes(input, rels, findings) {
136
+ const elementById = new Map();
137
+ for (const doc of input.elements) {
138
+ const id = docId(doc);
139
+ if (id && doc.data)
140
+ elementById.set(id, doc.data);
141
+ }
142
+ for (const rel of rels) {
143
+ const from = elementById.get(rel.fromId);
144
+ if (from && !isProcessNotation(from['notation'])) {
145
+ findings.push({
146
+ scope: PScope,
147
+ id: rel.relId,
148
+ ruleId: 'REL-002',
149
+ message: `Layer-semantics: '${rel.relId}' type 'process_parent' requires from to be ` +
150
+ `PROCESS; got notation='${from['notation']}'.`,
151
+ });
152
+ }
153
+ const to = elementById.get(rel.toId);
154
+ if (to && !isProcessNotation(to['notation'])) {
155
+ findings.push({
156
+ scope: PScope,
157
+ id: rel.relId,
158
+ ruleId: 'REL-002',
159
+ message: `Layer-semantics: '${rel.relId}' type 'process_parent' requires to to be ` +
160
+ `PROCESS; got notation='${to['notation']}'.`,
161
+ });
162
+ }
163
+ }
164
+ }
165
+ export function checkProcessParentSemantics(input, findings) {
166
+ const rels = processParentRels(input);
167
+ if (rels.length === 0)
168
+ return;
169
+ checkSelfReference(rels, findings);
170
+ checkCycle(rels, findings);
171
+ checkEndpointTypes(input, rels, findings);
172
+ }
@@ -1,3 +1,4 @@
1
1
  export { validateRepoModel } from './validate-repo.js';
2
2
  export { resolveRepoModel } from './resolve-model.js';
3
+ export { checkProcessParentSemantics, collectInEffectProcessParentEdges, todayIsoDate, } from './check-process-parent.js';
3
4
  export type { RepoDoc, RepoFinding, RepoModelInput, ResolvedElementRecord, ResolvedRelationRecord, ResolvedRepoModel } from './types.js';
@@ -1,2 +1,3 @@
1
1
  export { validateRepoModel } from './validate-repo.js';
2
2
  export { resolveRepoModel } from './resolve-model.js';
3
+ export { checkProcessParentSemantics, collectInEffectProcessParentEdges, todayIsoDate, } from './check-process-parent.js';
@@ -29,6 +29,8 @@
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';
33
+ import { checkProcessParentSemantics } from './check-process-parent.js';
32
34
  const PScope = 'repo';
33
35
  /** Statuses that, per lint.py's policy check, require an owner. */
34
36
  const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
@@ -153,6 +155,7 @@ function checkReferentialIntegrity(input, findings) {
153
155
  * realizes — BUSINESS_SERVICE → CAPABILITY (25-business-services.md)
154
156
  * hosts — NODE → TECHNOLOGY_SERVICE (25-nodes.md / 26-technology-services.md)
155
157
  * uses — APPLICATION → TECHNOLOGY_SERVICE (26-technology-services.md)
158
+ * process_parent — PROCESS → PROCESS (17-relations.md §3; REL-007/008)
156
159
  *
157
160
  * Implemented element checks:
158
161
  * TSVC-003 — TECHNOLOGY_SERVICE.node must resolve to NODE notation
@@ -406,6 +409,7 @@ export function validateRepoModel(input) {
406
409
  checkAtomicity(input, findings);
407
410
  checkReferentialIntegrity(input, findings);
408
411
  checkLayerSemantics(input, findings);
412
+ checkProcessParentSemantics(input, findings);
409
413
  checkPolicy(input, findings);
410
414
  // Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
411
415
  // functions (GOALS-010, ACT-006..009, FGCA-008..011).
@@ -415,5 +419,8 @@ export function validateRepoModel(input) {
415
419
  checkElementHygiene(input, findings);
416
420
  // Phase 9 — versioned-attribute sidecar rules (CONTRACT.md §9, VERSIONED-001..005).
417
421
  checkVersionedAttributes(input, findings);
422
+ // Phase 10 — candidate-only fields on admitted canon (ELEMENT_PRIMITIVES.md
423
+ // §7.29, ADMIT-009).
424
+ checkCandidateFields(input, findings);
418
425
  return findings;
419
426
  }