@transitrix/diagrams 1.8.11 → 1.8.13

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.
@@ -1,2 +1,3 @@
1
1
  export { validateRepoModel } from './validate-repo.js';
2
- export type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
2
+ export { resolveRepoModel } from './resolve-model.js';
3
+ export type { RepoDoc, RepoFinding, RepoModelInput, ResolvedElementRecord, ResolvedRelationRecord, ResolvedRepoModel } from './types.js';
@@ -1 +1,2 @@
1
1
  export { validateRepoModel } from './validate-repo.js';
2
+ export { resolveRepoModel } from './resolve-model.js';
@@ -0,0 +1,5 @@
1
+ import type { RepoModelInput, ResolvedRepoModel } from './types.js';
2
+ /** Project a loaded canon model into the resolved element/relation records a
3
+ * non-JS consumer can read directly, without depending on this repo's YAML
4
+ * parsing internals. Pure: no IO. */
5
+ export declare function resolveRepoModel(input: RepoModelInput): ResolvedRepoModel;
@@ -0,0 +1,71 @@
1
+ // Resolved element/relation records for `validate --scope=repo --json --include-model`.
2
+ // A non-JS consumer (DSM's Go backend) needs the canon model this CLI already
3
+ // parses without re-implementing the notation schema — this is a pure
4
+ // projection over the same `RepoModelInput` the repo-scope validator
5
+ // consumes, not a second parse pass, so the two never drift out of sync.
6
+ import { docId, endpointId } from './validate-repo.js';
7
+ /** Matches the `<NN>_<layer>` folder segment under `canon/elements/`, e.g.
8
+ * `01_motivation` -> `motivation` (ELEMENT_PRIMITIVES.md §2/§6). */
9
+ const LAYER_FOLDER_RE = /^\d+_(.+)$/;
10
+ function readString(data, key) {
11
+ const v = data[key];
12
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
13
+ }
14
+ /** Derive `layer` from the `canon/elements/<NN>_<layer>/…` folder segment when
15
+ * the doc has no explicit `layer` field — the folder is authoritative
16
+ * (ELEMENT_PRIMITIVES.md §3, `ELEM-003`). */
17
+ function layerFromPath(path) {
18
+ const segs = path.split('/');
19
+ const layerSeg = segs[2]; // canon / elements / <NN>_<layer> / …
20
+ if (!layerSeg)
21
+ return undefined;
22
+ const m = LAYER_FOLDER_RE.exec(layerSeg);
23
+ return m ? m[1] : undefined;
24
+ }
25
+ function resolveElement(doc) {
26
+ const id = docId(doc);
27
+ if (!id || !doc.data)
28
+ return null;
29
+ return {
30
+ id,
31
+ name: readString(doc.data, 'name') ?? '',
32
+ notation: readString(doc.data, 'notation') ?? '',
33
+ type: readString(doc.data, 'type'),
34
+ layer: readString(doc.data, 'layer') ?? layerFromPath(doc.path),
35
+ sourceFile: doc.path,
36
+ data: doc.data,
37
+ };
38
+ }
39
+ function resolveRelation(doc) {
40
+ if (!doc.data)
41
+ return null;
42
+ const source = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
43
+ const target = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
44
+ if (!source || !target)
45
+ return null;
46
+ return {
47
+ id: docId(doc) ?? '',
48
+ kind: readString(doc.data, 'type'),
49
+ source,
50
+ target,
51
+ sourceFile: doc.path,
52
+ };
53
+ }
54
+ /** Project a loaded canon model into the resolved element/relation records a
55
+ * non-JS consumer can read directly, without depending on this repo's YAML
56
+ * parsing internals. Pure: no IO. */
57
+ export function resolveRepoModel(input) {
58
+ const elements = [];
59
+ for (const doc of input.elements) {
60
+ const rec = resolveElement(doc);
61
+ if (rec)
62
+ elements.push(rec);
63
+ }
64
+ const relations = [];
65
+ for (const doc of input.relations) {
66
+ const rec = resolveRelation(doc);
67
+ if (rec)
68
+ relations.push(rec);
69
+ }
70
+ return { elements, relations };
71
+ }
@@ -32,3 +32,59 @@ export interface RepoModelInput {
32
32
  /** Documents found under `canon/relations/**`. */
33
33
  relations: RepoDoc[];
34
34
  }
35
+ /** One resolved canon element, projected from a `RepoDoc` for a non-JS
36
+ * consumer (DSM's Go importer). The top-level fields below are a stable,
37
+ * minimal identity projection — kept for backward compatibility with
38
+ * consumers that only need `id`/`name`/`notation`/`type`/`layer`/`sourceFile`
39
+ * (mirrors DSM's `methodology_element` table). `data` carries the complete
40
+ * parsed element alongside it, so a consumer never needs an engine schema
41
+ * change to read a canon-authored field the engine already parsed (a goal's
42
+ * `level`/`parent`/`description`/`link`/`tags`, an action's scheduling and
43
+ * ownership fields, etc.) — the engine owns each notation's field set
44
+ * (ELEMENT_PRIMITIVES.md), so its output stays notation-shaped rather than
45
+ * growing a bespoke field list per consumer. This is the faithful-projection
46
+ * fallback: a typed per-notation shape may replace `data` later, but the
47
+ * contract must not be curated down to what one consumer currently needs. */
48
+ export interface ResolvedElementRecord {
49
+ /** Canonical id, e.g. `DRIVER-COMP-1`. */
50
+ id: string;
51
+ /** `name` field; `''` when absent (should not happen for an admitted element). */
52
+ name: string;
53
+ /** The element TYPE's short name — the doc's `notation` field, e.g. `driver`, `goal`, `capability`. */
54
+ notation: string;
55
+ /** Subtype from the TYPE's controlled vocabulary (ELEMENT_PRIMITIVES.md §3), e.g. `internal`/`external` for DRIVER. Omitted when the doc has none. */
56
+ type?: string;
57
+ /** `motivation` / `business` / `application` / `technology` / `implementation`. Read from the doc's `layer` field when present, else derived from the `canon/elements/<NN>_<layer>/…` folder. */
58
+ layer?: string;
59
+ /** Source file path, relative to the scanned root. */
60
+ sourceFile: string;
61
+ /** The full parsed element document — every field its author supplied,
62
+ * unfiltered (including the admission/lifecycle envelope). The same
63
+ * `RepoDoc.data` the repo-scope validator already consumes, so this
64
+ * projection and the validation pass never see a different parse. */
65
+ data: Record<string, unknown>;
66
+ }
67
+ /** One resolved canon relation, projected from a `RepoDoc`. Only
68
+ * emitted when both endpoints resolve to a non-empty id — an endpoint that
69
+ * fails to resolve is already surfaced as a referential-integrity finding by
70
+ * `validateRepoModel`; this projection does not duplicate that as a
71
+ * half-resolved record. */
72
+ export interface ResolvedRelationRecord {
73
+ /** Canonical id, e.g. `REL-EMP-PERSON-OPS-1`. `''` when the relation doc has no `id`. */
74
+ id: string;
75
+ /** The relation's `type` field, e.g. `employment`, `realizes`. Omitted when the doc has none. */
76
+ kind?: string;
77
+ /** Resolved `from` (or legacy `source`) endpoint id. */
78
+ source: string;
79
+ /** Resolved `to` (or legacy `target`) endpoint id. */
80
+ target: string;
81
+ /** Source file path, relative to the scanned root. */
82
+ sourceFile: string;
83
+ }
84
+ /** The resolved element/relation records for a repo-scope run —
85
+ * the shape `transitrix validate --scope=repo --json --include-model` adds
86
+ * alongside the validation findings. */
87
+ export interface ResolvedRepoModel {
88
+ elements: ResolvedElementRecord[];
89
+ relations: ResolvedRelationRecord[];
90
+ }
@@ -1,4 +1,14 @@
1
- import type { RepoFinding, RepoModelInput } from './types.js';
1
+ import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
2
+ export declare function isRecord(v: unknown): v is Record<string, unknown>;
3
+ /** Read a string `id` from a parsed doc, or `null` if absent/non-string.
4
+ * Mirrors lint.py, which only treats a doc as an element/relation when it has
5
+ * a top-level `id` (sidecars such as versioned-attribute files have no `id`
6
+ * and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
7
+ * same id/endpoint reading rules to stay in lockstep with this validator. */
8
+ export declare function docId(doc: RepoDoc): string | null;
9
+ /** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
10
+ * mapping (lint.py accepts both). Returns the id string or `null`. */
11
+ export declare function endpointId(value: unknown): string | null;
2
12
  /**
3
13
  * Run all repo-scope checks over a loaded canon model and return the findings.
4
14
  * Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
@@ -28,14 +28,15 @@
28
28
  const PScope = 'repo';
29
29
  /** Statuses that, per lint.py's policy check, require an owner. */
30
30
  const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
31
- function isRecord(v) {
31
+ export function isRecord(v) {
32
32
  return typeof v === 'object' && v !== null && !Array.isArray(v);
33
33
  }
34
34
  /** Read a string `id` from a parsed doc, or `null` if absent/non-string.
35
35
  * Mirrors lint.py, which only treats a doc as an element/relation when it has
36
36
  * a top-level `id` (sidecars such as versioned-attribute files have no `id`
37
- * and are ignored). */
38
- function docId(doc) {
37
+ * and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
38
+ * same id/endpoint reading rules to stay in lockstep with this validator. */
39
+ export function docId(doc) {
39
40
  if (!doc.data)
40
41
  return null;
41
42
  const id = doc.data['id'];
@@ -43,7 +44,7 @@ function docId(doc) {
43
44
  }
44
45
  /** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
45
46
  * mapping (lint.py accepts both). Returns the id string or `null`. */
46
- function endpointId(value) {
47
+ export function endpointId(value) {
47
48
  if (typeof value === 'string')
48
49
  return value.length > 0 ? value : null;
49
50
  if (isRecord(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.8.11",
3
+ "version": "1.8.13",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -1,2 +1,3 @@
1
1
  export { validateRepoModel } from './validate-repo.js';
2
- export type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
2
+ export { resolveRepoModel } from './resolve-model.js';
3
+ export type { RepoDoc, RepoFinding, RepoModelInput, ResolvedElementRecord, ResolvedRelationRecord, ResolvedRepoModel } from './types.js';
@@ -0,0 +1,79 @@
1
+ // Resolved element/relation records for `validate --scope=repo --json --include-model`.
2
+ // A non-JS consumer (DSM's Go backend) needs the canon model this CLI already
3
+ // parses without re-implementing the notation schema — this is a pure
4
+ // projection over the same `RepoModelInput` the repo-scope validator
5
+ // consumes, not a second parse pass, so the two never drift out of sync.
6
+
7
+ import { docId, endpointId } from './validate-repo.js';
8
+ import type {
9
+ RepoDoc,
10
+ RepoModelInput,
11
+ ResolvedElementRecord,
12
+ ResolvedRelationRecord,
13
+ ResolvedRepoModel,
14
+ } from './types.js';
15
+
16
+ /** Matches the `<NN>_<layer>` folder segment under `canon/elements/`, e.g.
17
+ * `01_motivation` -> `motivation` (ELEMENT_PRIMITIVES.md §2/§6). */
18
+ const LAYER_FOLDER_RE = /^\d+_(.+)$/;
19
+
20
+ function readString(data: Record<string, unknown>, key: string): string | undefined {
21
+ const v = data[key];
22
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
23
+ }
24
+
25
+ /** Derive `layer` from the `canon/elements/<NN>_<layer>/…` folder segment when
26
+ * the doc has no explicit `layer` field — the folder is authoritative
27
+ * (ELEMENT_PRIMITIVES.md §3, `ELEM-003`). */
28
+ function layerFromPath(path: string): string | undefined {
29
+ const segs = path.split('/');
30
+ const layerSeg = segs[2]; // canon / elements / <NN>_<layer> / …
31
+ if (!layerSeg) return undefined;
32
+ const m = LAYER_FOLDER_RE.exec(layerSeg);
33
+ return m ? m[1] : undefined;
34
+ }
35
+
36
+ function resolveElement(doc: RepoDoc): ResolvedElementRecord | null {
37
+ const id = docId(doc);
38
+ if (!id || !doc.data) return null;
39
+ return {
40
+ id,
41
+ name: readString(doc.data, 'name') ?? '',
42
+ notation: readString(doc.data, 'notation') ?? '',
43
+ type: readString(doc.data, 'type'),
44
+ layer: readString(doc.data, 'layer') ?? layerFromPath(doc.path),
45
+ sourceFile: doc.path,
46
+ data: doc.data,
47
+ };
48
+ }
49
+
50
+ function resolveRelation(doc: RepoDoc): ResolvedRelationRecord | null {
51
+ if (!doc.data) return null;
52
+ const source = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
53
+ const target = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
54
+ if (!source || !target) return null;
55
+ return {
56
+ id: docId(doc) ?? '',
57
+ kind: readString(doc.data, 'type'),
58
+ source,
59
+ target,
60
+ sourceFile: doc.path,
61
+ };
62
+ }
63
+
64
+ /** Project a loaded canon model into the resolved element/relation records a
65
+ * non-JS consumer can read directly, without depending on this repo's YAML
66
+ * parsing internals. Pure: no IO. */
67
+ export function resolveRepoModel(input: RepoModelInput): ResolvedRepoModel {
68
+ const elements: ResolvedElementRecord[] = [];
69
+ for (const doc of input.elements) {
70
+ const rec = resolveElement(doc);
71
+ if (rec) elements.push(rec);
72
+ }
73
+ const relations: ResolvedRelationRecord[] = [];
74
+ for (const doc of input.relations) {
75
+ const rec = resolveRelation(doc);
76
+ if (rec) relations.push(rec);
77
+ }
78
+ return { elements, relations };
79
+ }
@@ -47,3 +47,62 @@ export interface RepoModelInput {
47
47
  /** Documents found under `canon/relations/**`. */
48
48
  relations: RepoDoc[];
49
49
  }
50
+
51
+ /** One resolved canon element, projected from a `RepoDoc` for a non-JS
52
+ * consumer (DSM's Go importer). The top-level fields below are a stable,
53
+ * minimal identity projection — kept for backward compatibility with
54
+ * consumers that only need `id`/`name`/`notation`/`type`/`layer`/`sourceFile`
55
+ * (mirrors DSM's `methodology_element` table). `data` carries the complete
56
+ * parsed element alongside it, so a consumer never needs an engine schema
57
+ * change to read a canon-authored field the engine already parsed (a goal's
58
+ * `level`/`parent`/`description`/`link`/`tags`, an action's scheduling and
59
+ * ownership fields, etc.) — the engine owns each notation's field set
60
+ * (ELEMENT_PRIMITIVES.md), so its output stays notation-shaped rather than
61
+ * growing a bespoke field list per consumer. This is the faithful-projection
62
+ * fallback: a typed per-notation shape may replace `data` later, but the
63
+ * contract must not be curated down to what one consumer currently needs. */
64
+ export interface ResolvedElementRecord {
65
+ /** Canonical id, e.g. `DRIVER-COMP-1`. */
66
+ id: string;
67
+ /** `name` field; `''` when absent (should not happen for an admitted element). */
68
+ name: string;
69
+ /** The element TYPE's short name — the doc's `notation` field, e.g. `driver`, `goal`, `capability`. */
70
+ notation: string;
71
+ /** Subtype from the TYPE's controlled vocabulary (ELEMENT_PRIMITIVES.md §3), e.g. `internal`/`external` for DRIVER. Omitted when the doc has none. */
72
+ type?: string;
73
+ /** `motivation` / `business` / `application` / `technology` / `implementation`. Read from the doc's `layer` field when present, else derived from the `canon/elements/<NN>_<layer>/…` folder. */
74
+ layer?: string;
75
+ /** Source file path, relative to the scanned root. */
76
+ sourceFile: string;
77
+ /** The full parsed element document — every field its author supplied,
78
+ * unfiltered (including the admission/lifecycle envelope). The same
79
+ * `RepoDoc.data` the repo-scope validator already consumes, so this
80
+ * projection and the validation pass never see a different parse. */
81
+ data: Record<string, unknown>;
82
+ }
83
+
84
+ /** One resolved canon relation, projected from a `RepoDoc`. Only
85
+ * emitted when both endpoints resolve to a non-empty id — an endpoint that
86
+ * fails to resolve is already surfaced as a referential-integrity finding by
87
+ * `validateRepoModel`; this projection does not duplicate that as a
88
+ * half-resolved record. */
89
+ export interface ResolvedRelationRecord {
90
+ /** Canonical id, e.g. `REL-EMP-PERSON-OPS-1`. `''` when the relation doc has no `id`. */
91
+ id: string;
92
+ /** The relation's `type` field, e.g. `employment`, `realizes`. Omitted when the doc has none. */
93
+ kind?: string;
94
+ /** Resolved `from` (or legacy `source`) endpoint id. */
95
+ source: string;
96
+ /** Resolved `to` (or legacy `target`) endpoint id. */
97
+ target: string;
98
+ /** Source file path, relative to the scanned root. */
99
+ sourceFile: string;
100
+ }
101
+
102
+ /** The resolved element/relation records for a repo-scope run —
103
+ * the shape `transitrix validate --scope=repo --json --include-model` adds
104
+ * alongside the validation findings. */
105
+ export interface ResolvedRepoModel {
106
+ elements: ResolvedElementRecord[];
107
+ relations: ResolvedRelationRecord[];
108
+ }
@@ -33,15 +33,16 @@ const PScope: RepoFinding['scope'] = 'repo';
33
33
  /** Statuses that, per lint.py's policy check, require an owner. */
34
34
  const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
35
35
 
36
- function isRecord(v: unknown): v is Record<string, unknown> {
36
+ export function isRecord(v: unknown): v is Record<string, unknown> {
37
37
  return typeof v === 'object' && v !== null && !Array.isArray(v);
38
38
  }
39
39
 
40
40
  /** Read a string `id` from a parsed doc, or `null` if absent/non-string.
41
41
  * Mirrors lint.py, which only treats a doc as an element/relation when it has
42
42
  * a top-level `id` (sidecars such as versioned-attribute files have no `id`
43
- * and are ignored). */
44
- function docId(doc: RepoDoc): string | null {
43
+ * and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
44
+ * same id/endpoint reading rules to stay in lockstep with this validator. */
45
+ export function docId(doc: RepoDoc): string | null {
45
46
  if (!doc.data) return null;
46
47
  const id = doc.data['id'];
47
48
  return typeof id === 'string' && id.length > 0 ? id : null;
@@ -49,7 +50,7 @@ function docId(doc: RepoDoc): string | null {
49
50
 
50
51
  /** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
51
52
  * mapping (lint.py accepts both). Returns the id string or `null`. */
52
- function endpointId(value: unknown): string | null {
53
+ export function endpointId(value: unknown): string | null {
53
54
  if (typeof value === 'string') return value.length > 0 ? value : null;
54
55
  if (isRecord(value)) {
55
56
  const id = value['id'];