@transitrix/diagrams 1.8.8 → 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.
- package/dist/activities/index.d.ts +2 -0
- package/dist/activities/index.js +1 -0
- package/dist/activities/resolver.d.ts +54 -0
- package/dist/activities/resolver.js +172 -0
- package/dist/canon-resolver-utils.d.ts +21 -0
- package/dist/canon-resolver-utils.js +81 -0
- package/dist/fgca/resolver.js +5 -1
- package/dist/goals/index.d.ts +2 -0
- package/dist/goals/index.js +1 -0
- package/dist/goals/resolver.d.ts +63 -0
- package/dist/goals/resolver.js +154 -0
- package/dist/repo-validate/index.d.ts +2 -1
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/resolve-model.d.ts +5 -0
- package/dist/repo-validate/resolve-model.js +71 -0
- package/dist/repo-validate/types.d.ts +56 -0
- package/dist/repo-validate/validate-repo.d.ts +11 -1
- package/dist/repo-validate/validate-repo.js +5 -4
- package/package.json +1 -1
- package/src/activities/index.ts +2 -0
- package/src/activities/resolver.ts +208 -0
- package/src/canon-resolver-utils.ts +89 -0
- package/src/fgca/resolver.ts +5 -1
- package/src/goals/index.ts +2 -0
- package/src/goals/resolver.ts +186 -0
- package/src/repo-validate/index.ts +2 -1
- package/src/repo-validate/resolve-model.ts +79 -0
- package/src/repo-validate/types.ts +59 -0
- package/src/repo-validate/validate-repo.ts +5 -4
|
@@ -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
|
-
|
|
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
package/src/activities/index.ts
CHANGED
|
@@ -26,3 +26,5 @@ export { validateActivities } from './validate.js';
|
|
|
26
26
|
export { computeCpm } from './cpm.js';
|
|
27
27
|
export { layoutActivities } from './layout.js';
|
|
28
28
|
export { computeGanttLayout, isGanttUnavailable } from './gantt.js';
|
|
29
|
+
export { resolveAction, isActionViewDoc } from './resolver.js';
|
|
30
|
+
export type { ActionViewConfig, ActionCanonSources } from './resolver.js';
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Schedule canon resolver — assembles a flat canonical Action document
|
|
3
|
+
* from a view_config projection and a canon element store (methodology
|
|
4
|
+
* `notations/views/07-action.md` §4 "Projection form (Full tier —
|
|
5
|
+
* post-promotion)"). Mirrors `fgca/resolver.ts`'s `resolveFGCA`/`isFGCAViewDoc`
|
|
6
|
+
* pattern for the `action` notation.
|
|
7
|
+
*
|
|
8
|
+
* Output shape matches the flat inline form `validateActivities` expects:
|
|
9
|
+
* top-level `project` block + `actions[]`, with ACTION element fields carried
|
|
10
|
+
* through unchanged except `type` → `activity_type` (the internal Activity
|
|
11
|
+
* field name; `type` is the canonical element field per elements/24-action.md
|
|
12
|
+
* §2). Filesystem-free — callable from unit tests without vscode.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { isObject, str, strArray, descendantsOf, stripEnvelope } from '../canon-resolver-utils.js';
|
|
16
|
+
|
|
17
|
+
export interface ActionViewConfig {
|
|
18
|
+
scope?: {
|
|
19
|
+
root_action?: string;
|
|
20
|
+
goals?: string[];
|
|
21
|
+
type_filter?: string[];
|
|
22
|
+
valid_at?: string | null;
|
|
23
|
+
};
|
|
24
|
+
schedule?: {
|
|
25
|
+
start_date?: string;
|
|
26
|
+
calendar?: {
|
|
27
|
+
working_days?: string[];
|
|
28
|
+
hours_per_day?: number;
|
|
29
|
+
holidays?: string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
display?: {
|
|
33
|
+
view?: 'network' | 'gantt' | 'both';
|
|
34
|
+
depth?: number | null;
|
|
35
|
+
collapsed?: string[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ActionCanonSources {
|
|
40
|
+
elements: unknown[];
|
|
41
|
+
/** `canon/relations/**` docs (`notation: relation`). Optional — omit for
|
|
42
|
+
* canon stores with no relations tree; `action_goal` links then fall back
|
|
43
|
+
* to each element's inline `goals[]`. */
|
|
44
|
+
relations?: unknown[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** ACTION elements by id. `notation: action` is canonical; `notation: activity`
|
|
48
|
+
* is the deprecated pre-2026-06-25 alias (elements/24-action.md §"Deprecated
|
|
49
|
+
* alias") — accepted the same way `collectByNotation` callers elsewhere do. */
|
|
50
|
+
function collectActionElements(docs: unknown[]): Map<string, Record<string, unknown>> {
|
|
51
|
+
const out = new Map<string, Record<string, unknown>>();
|
|
52
|
+
for (const doc of docs) {
|
|
53
|
+
if (!isObject(doc)) continue;
|
|
54
|
+
const notation = str(doc['notation']);
|
|
55
|
+
if (notation !== 'action' && notation !== 'activity') continue;
|
|
56
|
+
const id = str(doc['id']);
|
|
57
|
+
if (!id) continue;
|
|
58
|
+
if (!out.has(id)) out.set(id, doc); // first definition wins
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* GOAL ids reached by an *active* `action_goal` (or legacy `activity_goal`)
|
|
65
|
+
* relation originating at `fromId`. A relation with a non-null `valid_to` has
|
|
66
|
+
* ended (the action was re-aimed) and is excluded. Mirrors
|
|
67
|
+
* `activity-card/resolver.ts`'s `activeActivityGoals` — the canonical, preferred
|
|
68
|
+
* source per elements/24-action.md §3 ("renderers prefer REL files when both
|
|
69
|
+
* are present").
|
|
70
|
+
*/
|
|
71
|
+
function activeActionGoalRelations(relations: unknown[], fromId: string): string[] {
|
|
72
|
+
const out: string[] = [];
|
|
73
|
+
for (const doc of relations) {
|
|
74
|
+
if (!isObject(doc)) continue;
|
|
75
|
+
if (str(doc['notation']) !== 'relation') continue;
|
|
76
|
+
const type = str(doc['type']);
|
|
77
|
+
if (type !== 'action_goal' && type !== 'activity_goal') continue;
|
|
78
|
+
if (str(doc['from']) !== fromId) continue;
|
|
79
|
+
const to = str(doc['to']);
|
|
80
|
+
if (!to) continue;
|
|
81
|
+
const validTo = doc['valid_to'];
|
|
82
|
+
if (validTo !== undefined && validTo !== null) continue; // ended relation
|
|
83
|
+
if (!out.includes(to)) out.push(to);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns true when the parsed view document uses the canon-projection form
|
|
90
|
+
* (`view_config` key present, no inline `actions[]`). Used by the preview and
|
|
91
|
+
* CLI to decide whether to run the resolver or fall back to direct
|
|
92
|
+
* `validateActivities` on the inline document.
|
|
93
|
+
*/
|
|
94
|
+
export function isActionViewDoc(parsed: unknown): boolean {
|
|
95
|
+
if (!isObject(parsed)) return false;
|
|
96
|
+
return 'view_config' in parsed && !('actions' in parsed);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Assemble a flat canonical Action document from the canon ACTION element
|
|
101
|
+
* store, filtered by the view document's `view_config.scope`. The return
|
|
102
|
+
* value is ready to pass to `validateActivities`.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveAction(
|
|
105
|
+
viewDoc: unknown,
|
|
106
|
+
sources: ActionCanonSources,
|
|
107
|
+
): Record<string, unknown> {
|
|
108
|
+
if (!isObject(viewDoc)) {
|
|
109
|
+
return { notation: 'action', actions: [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
|
|
113
|
+
const scope = isObject(vc['scope']) ? vc['scope'] : {};
|
|
114
|
+
const schedule = isObject(vc['schedule']) ? vc['schedule'] : {};
|
|
115
|
+
|
|
116
|
+
const allActions = collectActionElements(sources.elements);
|
|
117
|
+
const relations = sources.relations ?? [];
|
|
118
|
+
|
|
119
|
+
/** Effective `goals[]` for an action — the active `action_goal` REL targets
|
|
120
|
+
* when any exist, else the element's transitional inline `goals[]`. */
|
|
121
|
+
function effectiveGoals(id: string, el: Record<string, unknown>): string[] {
|
|
122
|
+
const relGoals = activeActionGoalRelations(relations, id);
|
|
123
|
+
return relGoals.length > 0 ? relGoals : strArray(el['goals']);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 1. Scope by root_action (this action + its descendants), or the full
|
|
127
|
+
// catalogue when omitted (§5.1: "Omit to include elements from the full
|
|
128
|
+
// catalogue (or use goals/type_filter for other scoping)").
|
|
129
|
+
const rootAction = str(scope['root_action']);
|
|
130
|
+
let candidateIds: Set<string>;
|
|
131
|
+
if (rootAction) {
|
|
132
|
+
candidateIds = allActions.has(rootAction) ? descendantsOf(rootAction, allActions) : new Set();
|
|
133
|
+
} else {
|
|
134
|
+
candidateIds = new Set(allActions.keys());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Narrow by goals — only actions linked to at least one listed GOAL
|
|
138
|
+
// (via an active action_goal relation, or inline goals[] when no relation
|
|
139
|
+
// exists for that action).
|
|
140
|
+
const goalsFilter = strArray(scope['goals']);
|
|
141
|
+
if (goalsFilter.length > 0) {
|
|
142
|
+
candidateIds = new Set(
|
|
143
|
+
[...candidateIds].filter((id) => effectiveGoals(id, allActions.get(id)!).some((g) => goalsFilter.includes(g))),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3. Narrow by type_filter — only actions of a listed type.
|
|
148
|
+
const typeFilter = strArray(scope['type_filter']);
|
|
149
|
+
if (typeFilter.length > 0) {
|
|
150
|
+
candidateIds = new Set(
|
|
151
|
+
[...candidateIds].filter((id) => {
|
|
152
|
+
const el = allActions.get(id);
|
|
153
|
+
const t = str(el?.['type']) ?? str(el?.['activity_type']);
|
|
154
|
+
return t !== undefined && typeFilter.includes(t);
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 4. Narrow by valid_at — only actions in effect on that date. Actions with
|
|
160
|
+
// no valid_from are not lifecycle-tracked and are kept (permissive default,
|
|
161
|
+
// matching the resolver's general "missing = not excluded" stance).
|
|
162
|
+
const validAt = str(scope['valid_at']);
|
|
163
|
+
if (validAt) {
|
|
164
|
+
candidateIds = new Set(
|
|
165
|
+
[...candidateIds].filter((id) => {
|
|
166
|
+
const el = allActions.get(id);
|
|
167
|
+
const from = str(el?.['valid_from']);
|
|
168
|
+
const to = str(el?.['valid_to']);
|
|
169
|
+
if (from && validAt < from) return false;
|
|
170
|
+
if (to && validAt > to) return false;
|
|
171
|
+
return true;
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Element fields map onto Activity fields unchanged, except the canonical
|
|
177
|
+
// `type` (elements/24-action.md §2) which becomes the internal `activity_type`
|
|
178
|
+
// (types.ts `Activity.activity_type`), and `goals` which is resolved via
|
|
179
|
+
// `effectiveGoals` (REL-preferred). The admission/lifecycle envelope fields
|
|
180
|
+
// carry no schedule/render meaning for `validateActivities` and are dropped.
|
|
181
|
+
const selectedActions = [...candidateIds].map((id) => {
|
|
182
|
+
const el = allActions.get(id)!;
|
|
183
|
+
const { type, activity_type, goals: _inlineGoals, ...rest } = stripEnvelope(el);
|
|
184
|
+
const resolvedType = type ?? activity_type;
|
|
185
|
+
return {
|
|
186
|
+
...rest,
|
|
187
|
+
id,
|
|
188
|
+
goals: effectiveGoals(id, el),
|
|
189
|
+
...(resolvedType !== undefined ? { activity_type: resolvedType } : {}),
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const project: Record<string, unknown> = {};
|
|
194
|
+
const startDate = str(schedule['start_date']);
|
|
195
|
+
if (startDate) project['start_date'] = startDate;
|
|
196
|
+
if (isObject(schedule['calendar'])) project['calendar'] = schedule['calendar'];
|
|
197
|
+
|
|
198
|
+
const out: Record<string, unknown> = {
|
|
199
|
+
notation: 'action',
|
|
200
|
+
id: viewDoc['id'],
|
|
201
|
+
title: viewDoc['name'],
|
|
202
|
+
spec_version: viewDoc['spec_version'],
|
|
203
|
+
description: viewDoc['description'],
|
|
204
|
+
actions: selectedActions,
|
|
205
|
+
};
|
|
206
|
+
if (Object.keys(project).length > 0) out['project'] = project;
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared primitives for the per-notation canon-projection resolvers
|
|
3
|
+
* (activities/resolver.ts, goals/resolver.ts — fgca/resolver.ts predates this
|
|
4
|
+
* module and keeps its own copies). Each resolver assembles a flat document
|
|
5
|
+
* from a canon element store filtered by a view_config.scope block; these
|
|
6
|
+
* helpers are the parts that don't vary by notation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function isObject(v: unknown): v is Record<string, unknown> {
|
|
10
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A non-empty string, trimmed. */
|
|
14
|
+
export function str(v: unknown): string | undefined {
|
|
15
|
+
if (typeof v !== 'string') return undefined;
|
|
16
|
+
const t = v.trim();
|
|
17
|
+
return t.length > 0 ? t : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function strArray(v: unknown): string[] {
|
|
21
|
+
if (!Array.isArray(v)) return [];
|
|
22
|
+
return v.filter((x): x is string => typeof x === 'string' && x.trim().length > 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `rootId` and every element reachable by following `parent` links downward,
|
|
26
|
+
* per the element map's own `parent` field. Used to scope a projection to a
|
|
27
|
+
* root element and its transitive descendants (WBS / goal hierarchy / etc). */
|
|
28
|
+
export function descendantsOf(
|
|
29
|
+
rootId: string,
|
|
30
|
+
all: Map<string, Record<string, unknown>>,
|
|
31
|
+
): Set<string> {
|
|
32
|
+
const childrenByParent = new Map<string, string[]>();
|
|
33
|
+
for (const [id, el] of all) {
|
|
34
|
+
const parent = str(el['parent']);
|
|
35
|
+
if (!parent) continue;
|
|
36
|
+
const list = childrenByParent.get(parent) ?? [];
|
|
37
|
+
list.push(id);
|
|
38
|
+
childrenByParent.set(parent, list);
|
|
39
|
+
}
|
|
40
|
+
const result = new Set<string>();
|
|
41
|
+
const queue: string[] = [rootId];
|
|
42
|
+
while (queue.length > 0) {
|
|
43
|
+
const id = queue.shift()!;
|
|
44
|
+
if (result.has(id)) continue;
|
|
45
|
+
result.add(id);
|
|
46
|
+
for (const child of childrenByParent.get(id) ?? []) queue.push(child);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
|
|
52
|
+
* adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
|
|
53
|
+
* memoized across calls via the shared `memo` map. */
|
|
54
|
+
export function parentChainDepth(
|
|
55
|
+
id: string,
|
|
56
|
+
all: Map<string, Record<string, unknown>>,
|
|
57
|
+
memo: Map<string, number> = new Map(),
|
|
58
|
+
visiting: Set<string> = new Set(),
|
|
59
|
+
): number {
|
|
60
|
+
const cached = memo.get(id);
|
|
61
|
+
if (cached !== undefined) return cached;
|
|
62
|
+
if (visiting.has(id)) {
|
|
63
|
+
memo.set(id, 0);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
const el = all.get(id);
|
|
67
|
+
const parent = el ? str(el['parent']) : undefined;
|
|
68
|
+
if (!parent || !all.has(parent)) {
|
|
69
|
+
memo.set(id, 0);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
visiting.add(id);
|
|
73
|
+
const level = parentChainDepth(parent, all, memo, visiting) + 1;
|
|
74
|
+
visiting.delete(id);
|
|
75
|
+
memo.set(id, level);
|
|
76
|
+
return level;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The admission/lifecycle envelope fields (CONTRACT.md §6-7) every canon
|
|
80
|
+
* element carries but that carry no rendering/validation meaning once
|
|
81
|
+
* projected into a flat notation-specific document. */
|
|
82
|
+
const ENVELOPE_FIELDS = ['notation', 'zone', 'admitted_at', 'admitted_by', 'gate_checks', 'valid_from', 'valid_to'] as const;
|
|
83
|
+
|
|
84
|
+
/** `el` with the admission/lifecycle envelope fields removed. */
|
|
85
|
+
export function stripEnvelope(el: Record<string, unknown>): Record<string, unknown> {
|
|
86
|
+
const rest = { ...el };
|
|
87
|
+
for (const field of ENVELOPE_FIELDS) delete rest[field];
|
|
88
|
+
return rest;
|
|
89
|
+
}
|
package/src/fgca/resolver.ts
CHANGED
|
@@ -91,7 +91,11 @@ export function resolveFGCA(
|
|
|
91
91
|
const factorElems = new Map([...factorElemsLegacy, ...factorElemsNew]);
|
|
92
92
|
const goalElems = collectByNotation(sources.elements, 'goal');
|
|
93
93
|
const changeElems = collectByNotation(sources.elements, 'change');
|
|
94
|
-
|
|
94
|
+
// `action` is canonical since methodology 1.0; `activity` is the deprecated
|
|
95
|
+
// pre-rename alias, still accepted (elements/24-action.md §"Deprecated alias").
|
|
96
|
+
const activityElemsLegacy = collectByNotation(sources.elements, 'activity');
|
|
97
|
+
const activityElemsNew = collectByNotation(sources.elements, 'action');
|
|
98
|
+
const activityElems = new Map([...activityElemsLegacy, ...activityElemsNew]);
|
|
95
99
|
|
|
96
100
|
// 1. Select goal set
|
|
97
101
|
const goalsFilter = str(goalsConf['filter']) ?? 'all';
|
package/src/goals/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type {
|
|
|
16
16
|
export { validateGoalTree } from './validate.js';
|
|
17
17
|
export { parseCanonicalGoals } from './parse-canonical.js';
|
|
18
18
|
export type { CanonicalGoalsResult } from './parse-canonical.js';
|
|
19
|
+
export { resolveGoals, isGoalsViewDoc } from './resolver.js';
|
|
20
|
+
export type { GoalsViewConfig, GoalsCanonSources } from './resolver.js';
|
|
19
21
|
export { layoutGoalTree, selectScopedGoals } from './layout.js';
|
|
20
22
|
export { reparent, addChild, deleteWithDescendants, moveToBacklog, restoreFromBacklog } from './mutations.js';
|
|
21
23
|
|