@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.
@@ -3,3 +3,5 @@ export { validateActivities } from './validate.js';
3
3
  export { computeCpm } from './cpm.js';
4
4
  export { layoutActivities } from './layout.js';
5
5
  export { computeGanttLayout, isGanttUnavailable } from './gantt.js';
6
+ export { resolveAction, isActionViewDoc } from './resolver.js';
7
+ export type { ActionViewConfig, ActionCanonSources } from './resolver.js';
@@ -2,3 +2,4 @@ export { validateActivities } from './validate.js';
2
2
  export { computeCpm } from './cpm.js';
3
3
  export { layoutActivities } from './layout.js';
4
4
  export { computeGanttLayout, isGanttUnavailable } from './gantt.js';
5
+ export { resolveAction, isActionViewDoc } from './resolver.js';
@@ -0,0 +1,54 @@
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
+ export interface ActionViewConfig {
15
+ scope?: {
16
+ root_action?: string;
17
+ goals?: string[];
18
+ type_filter?: string[];
19
+ valid_at?: string | null;
20
+ };
21
+ schedule?: {
22
+ start_date?: string;
23
+ calendar?: {
24
+ working_days?: string[];
25
+ hours_per_day?: number;
26
+ holidays?: string[];
27
+ };
28
+ };
29
+ display?: {
30
+ view?: 'network' | 'gantt' | 'both';
31
+ depth?: number | null;
32
+ collapsed?: string[];
33
+ };
34
+ }
35
+ export interface ActionCanonSources {
36
+ elements: unknown[];
37
+ /** `canon/relations/**` docs (`notation: relation`). Optional — omit for
38
+ * canon stores with no relations tree; `action_goal` links then fall back
39
+ * to each element's inline `goals[]`. */
40
+ relations?: unknown[];
41
+ }
42
+ /**
43
+ * Returns true when the parsed view document uses the canon-projection form
44
+ * (`view_config` key present, no inline `actions[]`). Used by the preview and
45
+ * CLI to decide whether to run the resolver or fall back to direct
46
+ * `validateActivities` on the inline document.
47
+ */
48
+ export declare function isActionViewDoc(parsed: unknown): boolean;
49
+ /**
50
+ * Assemble a flat canonical Action document from the canon ACTION element
51
+ * store, filtered by the view document's `view_config.scope`. The return
52
+ * value is ready to pass to `validateActivities`.
53
+ */
54
+ export declare function resolveAction(viewDoc: unknown, sources: ActionCanonSources): Record<string, unknown>;
@@ -0,0 +1,172 @@
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
+ import { isObject, str, strArray, descendantsOf, stripEnvelope } from '../canon-resolver-utils.js';
15
+ /** ACTION elements by id. `notation: action` is canonical; `notation: activity`
16
+ * is the deprecated pre-2026-06-25 alias (elements/24-action.md §"Deprecated
17
+ * alias") — accepted the same way `collectByNotation` callers elsewhere do. */
18
+ function collectActionElements(docs) {
19
+ const out = new Map();
20
+ for (const doc of docs) {
21
+ if (!isObject(doc))
22
+ continue;
23
+ const notation = str(doc['notation']);
24
+ if (notation !== 'action' && notation !== 'activity')
25
+ continue;
26
+ const id = str(doc['id']);
27
+ if (!id)
28
+ continue;
29
+ if (!out.has(id))
30
+ out.set(id, doc); // first definition wins
31
+ }
32
+ return out;
33
+ }
34
+ /**
35
+ * GOAL ids reached by an *active* `action_goal` (or legacy `activity_goal`)
36
+ * relation originating at `fromId`. A relation with a non-null `valid_to` has
37
+ * ended (the action was re-aimed) and is excluded. Mirrors
38
+ * `activity-card/resolver.ts`'s `activeActivityGoals` — the canonical, preferred
39
+ * source per elements/24-action.md §3 ("renderers prefer REL files when both
40
+ * are present").
41
+ */
42
+ function activeActionGoalRelations(relations, fromId) {
43
+ const out = [];
44
+ for (const doc of relations) {
45
+ if (!isObject(doc))
46
+ continue;
47
+ if (str(doc['notation']) !== 'relation')
48
+ continue;
49
+ const type = str(doc['type']);
50
+ if (type !== 'action_goal' && type !== 'activity_goal')
51
+ continue;
52
+ if (str(doc['from']) !== fromId)
53
+ continue;
54
+ const to = str(doc['to']);
55
+ if (!to)
56
+ continue;
57
+ const validTo = doc['valid_to'];
58
+ if (validTo !== undefined && validTo !== null)
59
+ continue; // ended relation
60
+ if (!out.includes(to))
61
+ out.push(to);
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Returns true when the parsed view document uses the canon-projection form
67
+ * (`view_config` key present, no inline `actions[]`). Used by the preview and
68
+ * CLI to decide whether to run the resolver or fall back to direct
69
+ * `validateActivities` on the inline document.
70
+ */
71
+ export function isActionViewDoc(parsed) {
72
+ if (!isObject(parsed))
73
+ return false;
74
+ return 'view_config' in parsed && !('actions' in parsed);
75
+ }
76
+ /**
77
+ * Assemble a flat canonical Action document from the canon ACTION element
78
+ * store, filtered by the view document's `view_config.scope`. The return
79
+ * value is ready to pass to `validateActivities`.
80
+ */
81
+ export function resolveAction(viewDoc, sources) {
82
+ if (!isObject(viewDoc)) {
83
+ return { notation: 'action', actions: [] };
84
+ }
85
+ const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
86
+ const scope = isObject(vc['scope']) ? vc['scope'] : {};
87
+ const schedule = isObject(vc['schedule']) ? vc['schedule'] : {};
88
+ const allActions = collectActionElements(sources.elements);
89
+ const relations = sources.relations ?? [];
90
+ /** Effective `goals[]` for an action — the active `action_goal` REL targets
91
+ * when any exist, else the element's transitional inline `goals[]`. */
92
+ function effectiveGoals(id, el) {
93
+ const relGoals = activeActionGoalRelations(relations, id);
94
+ return relGoals.length > 0 ? relGoals : strArray(el['goals']);
95
+ }
96
+ // 1. Scope by root_action (this action + its descendants), or the full
97
+ // catalogue when omitted (§5.1: "Omit to include elements from the full
98
+ // catalogue (or use goals/type_filter for other scoping)").
99
+ const rootAction = str(scope['root_action']);
100
+ let candidateIds;
101
+ if (rootAction) {
102
+ candidateIds = allActions.has(rootAction) ? descendantsOf(rootAction, allActions) : new Set();
103
+ }
104
+ else {
105
+ candidateIds = new Set(allActions.keys());
106
+ }
107
+ // 2. Narrow by goals — only actions linked to at least one listed GOAL
108
+ // (via an active action_goal relation, or inline goals[] when no relation
109
+ // exists for that action).
110
+ const goalsFilter = strArray(scope['goals']);
111
+ if (goalsFilter.length > 0) {
112
+ candidateIds = new Set([...candidateIds].filter((id) => effectiveGoals(id, allActions.get(id)).some((g) => goalsFilter.includes(g))));
113
+ }
114
+ // 3. Narrow by type_filter — only actions of a listed type.
115
+ const typeFilter = strArray(scope['type_filter']);
116
+ if (typeFilter.length > 0) {
117
+ candidateIds = new Set([...candidateIds].filter((id) => {
118
+ const el = allActions.get(id);
119
+ const t = str(el?.['type']) ?? str(el?.['activity_type']);
120
+ return t !== undefined && typeFilter.includes(t);
121
+ }));
122
+ }
123
+ // 4. Narrow by valid_at — only actions in effect on that date. Actions with
124
+ // no valid_from are not lifecycle-tracked and are kept (permissive default,
125
+ // matching the resolver's general "missing = not excluded" stance).
126
+ const validAt = str(scope['valid_at']);
127
+ if (validAt) {
128
+ candidateIds = new Set([...candidateIds].filter((id) => {
129
+ const el = allActions.get(id);
130
+ const from = str(el?.['valid_from']);
131
+ const to = str(el?.['valid_to']);
132
+ if (from && validAt < from)
133
+ return false;
134
+ if (to && validAt > to)
135
+ return false;
136
+ return true;
137
+ }));
138
+ }
139
+ // Element fields map onto Activity fields unchanged, except the canonical
140
+ // `type` (elements/24-action.md §2) which becomes the internal `activity_type`
141
+ // (types.ts `Activity.activity_type`), and `goals` which is resolved via
142
+ // `effectiveGoals` (REL-preferred). The admission/lifecycle envelope fields
143
+ // carry no schedule/render meaning for `validateActivities` and are dropped.
144
+ const selectedActions = [...candidateIds].map((id) => {
145
+ const el = allActions.get(id);
146
+ const { type, activity_type, goals: _inlineGoals, ...rest } = stripEnvelope(el);
147
+ const resolvedType = type ?? activity_type;
148
+ return {
149
+ ...rest,
150
+ id,
151
+ goals: effectiveGoals(id, el),
152
+ ...(resolvedType !== undefined ? { activity_type: resolvedType } : {}),
153
+ };
154
+ });
155
+ const project = {};
156
+ const startDate = str(schedule['start_date']);
157
+ if (startDate)
158
+ project['start_date'] = startDate;
159
+ if (isObject(schedule['calendar']))
160
+ project['calendar'] = schedule['calendar'];
161
+ const out = {
162
+ notation: 'action',
163
+ id: viewDoc['id'],
164
+ title: viewDoc['name'],
165
+ spec_version: viewDoc['spec_version'],
166
+ description: viewDoc['description'],
167
+ actions: selectedActions,
168
+ };
169
+ if (Object.keys(project).length > 0)
170
+ out['project'] = project;
171
+ return out;
172
+ }
@@ -0,0 +1,21 @@
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
+ export declare function isObject(v: unknown): v is Record<string, unknown>;
9
+ /** A non-empty string, trimmed. */
10
+ export declare function str(v: unknown): string | undefined;
11
+ export declare function strArray(v: unknown): string[];
12
+ /** `rootId` and every element reachable by following `parent` links downward,
13
+ * per the element map's own `parent` field. Used to scope a projection to a
14
+ * root element and its transitive descendants (WBS / goal hierarchy / etc). */
15
+ export declare function descendantsOf(rootId: string, all: Map<string, Record<string, unknown>>): Set<string>;
16
+ /** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
17
+ * adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
18
+ * memoized across calls via the shared `memo` map. */
19
+ export declare function parentChainDepth(id: string, all: Map<string, Record<string, unknown>>, memo?: Map<string, number>, visiting?: Set<string>): number;
20
+ /** `el` with the admission/lifecycle envelope fields removed. */
21
+ export declare function stripEnvelope(el: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,81 @@
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
+ export function isObject(v) {
9
+ return !!v && typeof v === 'object' && !Array.isArray(v);
10
+ }
11
+ /** A non-empty string, trimmed. */
12
+ export function str(v) {
13
+ if (typeof v !== 'string')
14
+ return undefined;
15
+ const t = v.trim();
16
+ return t.length > 0 ? t : undefined;
17
+ }
18
+ export function strArray(v) {
19
+ if (!Array.isArray(v))
20
+ return [];
21
+ return v.filter((x) => typeof x === 'string' && x.trim().length > 0);
22
+ }
23
+ /** `rootId` and every element reachable by following `parent` links downward,
24
+ * per the element map's own `parent` field. Used to scope a projection to a
25
+ * root element and its transitive descendants (WBS / goal hierarchy / etc). */
26
+ export function descendantsOf(rootId, all) {
27
+ const childrenByParent = new Map();
28
+ for (const [id, el] of all) {
29
+ const parent = str(el['parent']);
30
+ if (!parent)
31
+ continue;
32
+ const list = childrenByParent.get(parent) ?? [];
33
+ list.push(id);
34
+ childrenByParent.set(parent, list);
35
+ }
36
+ const result = new Set();
37
+ const queue = [rootId];
38
+ while (queue.length > 0) {
39
+ const id = queue.shift();
40
+ if (result.has(id))
41
+ continue;
42
+ result.add(id);
43
+ for (const child of childrenByParent.get(id) ?? [])
44
+ queue.push(child);
45
+ }
46
+ return result;
47
+ }
48
+ /** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
49
+ * adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
50
+ * memoized across calls via the shared `memo` map. */
51
+ export function parentChainDepth(id, all, memo = new Map(), visiting = new Set()) {
52
+ const cached = memo.get(id);
53
+ if (cached !== undefined)
54
+ return cached;
55
+ if (visiting.has(id)) {
56
+ memo.set(id, 0);
57
+ return 0;
58
+ }
59
+ const el = all.get(id);
60
+ const parent = el ? str(el['parent']) : undefined;
61
+ if (!parent || !all.has(parent)) {
62
+ memo.set(id, 0);
63
+ return 0;
64
+ }
65
+ visiting.add(id);
66
+ const level = parentChainDepth(parent, all, memo, visiting) + 1;
67
+ visiting.delete(id);
68
+ memo.set(id, level);
69
+ return level;
70
+ }
71
+ /** The admission/lifecycle envelope fields (CONTRACT.md §6-7) every canon
72
+ * element carries but that carry no rendering/validation meaning once
73
+ * projected into a flat notation-specific document. */
74
+ const ENVELOPE_FIELDS = ['notation', 'zone', 'admitted_at', 'admitted_by', 'gate_checks', 'valid_from', 'valid_to'];
75
+ /** `el` with the admission/lifecycle envelope fields removed. */
76
+ export function stripEnvelope(el) {
77
+ const rest = { ...el };
78
+ for (const field of ENVELOPE_FIELDS)
79
+ delete rest[field];
80
+ return rest;
81
+ }
@@ -66,7 +66,11 @@ export function resolveFGCA(viewDoc, sources) {
66
66
  const factorElems = new Map([...factorElemsLegacy, ...factorElemsNew]);
67
67
  const goalElems = collectByNotation(sources.elements, 'goal');
68
68
  const changeElems = collectByNotation(sources.elements, 'change');
69
- const activityElems = collectByNotation(sources.elements, 'activity');
69
+ // `action` is canonical since methodology 1.0; `activity` is the deprecated
70
+ // pre-rename alias, still accepted (elements/24-action.md §"Deprecated alias").
71
+ const activityElemsLegacy = collectByNotation(sources.elements, 'activity');
72
+ const activityElemsNew = collectByNotation(sources.elements, 'action');
73
+ const activityElems = new Map([...activityElemsLegacy, ...activityElemsNew]);
70
74
  // 1. Select goal set
71
75
  const goalsFilter = str(goalsConf['filter']) ?? 'all';
72
76
  let selectedGoals;
@@ -2,6 +2,8 @@ export type { Goal, GoalType, GoalTree, ImpactType, ValidationResult, Validation
2
2
  export { validateGoalTree } from './validate.js';
3
3
  export { parseCanonicalGoals } from './parse-canonical.js';
4
4
  export type { CanonicalGoalsResult } from './parse-canonical.js';
5
+ export { resolveGoals, isGoalsViewDoc } from './resolver.js';
6
+ export type { GoalsViewConfig, GoalsCanonSources } from './resolver.js';
5
7
  export { layoutGoalTree, selectScopedGoals } from './layout.js';
6
8
  export { reparent, addChild, deleteWithDescendants, moveToBacklog, restoreFromBacklog } from './mutations.js';
7
9
  export { GoalTreeView } from './GoalTreeView.js';
@@ -1,5 +1,6 @@
1
1
  export { validateGoalTree } from './validate.js';
2
2
  export { parseCanonicalGoals } from './parse-canonical.js';
3
+ export { resolveGoals, isGoalsViewDoc } from './resolver.js';
3
4
  export { layoutGoalTree, selectScopedGoals } from './layout.js';
4
5
  export { reparent, addChild, deleteWithDescendants, moveToBacklog, restoreFromBacklog } from './mutations.js';
5
6
  export { GoalTreeView } from './GoalTreeView.js';
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Goals Tree canon resolver — assembles a flat canonical Goals document from
3
+ * a view_config projection and a canon element store (methodology
4
+ * `notations/views/04-goals.md` §4 "Projection form (Full tier —
5
+ * post-promotion)"). Mirrors `fgca/resolver.ts`'s `resolveFGCA`/`isFGCAViewDoc`
6
+ * and `activities/resolver.ts`'s `resolveAction`/`isActionViewDoc` pattern
7
+ * for the `goals` notation.
8
+ *
9
+ * Output shape matches the flat inline form `parseCanonicalGoals` expects:
10
+ * top-level `goal_types[]` + `goals[]`. Filesystem-free — callable from unit
11
+ * tests without vscode.
12
+ *
13
+ * `goal_types[]` synthesis: when `view_config.goal_types` is present it is
14
+ * used as-is (the documented projection-form shape always includes it) and
15
+ * each goal keeps its own authored `level`, validated against that table as
16
+ * usual. When absent, §5.2 says the renderer "infers levels from the parent
17
+ * chain depth (root goals at 0, each step deeper adds 1)" — this resolver
18
+ * implements that literally (`parentChainDepth`), overriding each selected
19
+ * goal's `level` with its structural depth, rather than trusting the
20
+ * element's own stored `level` field. A stored `level` can't be trusted here:
21
+ * nothing enforces cross-element consistency on it (ELEMENT_PRIMITIVES.md
22
+ * §7.2: `type`/`level` are independent, both-optional per-element fields),
23
+ * and worse, using it made the synthesized table's chosen level depend on
24
+ * which element the canon loader happened to enumerate first — the CLI
25
+ * (`readdirSync`, unsorted) and the VS Code preview
26
+ * (`vscode.workspace.fs.readDirectory`, unsorted) can enumerate the same
27
+ * canon store in different orders, so the two surfaces could disagree on
28
+ * `GOALS-008` findings for identical input. Structural depth has no such
29
+ * dependency. A goal with no `type` still surfaces as a genuine `GOALS-006`
30
+ * finding — this resolver never synthesizes a type label to paper over one.
31
+ */
32
+ export interface GoalsViewConfig {
33
+ scope?: {
34
+ root_goal?: string | null;
35
+ period?: string | null;
36
+ type_filter?: string[] | null;
37
+ valid_at?: string | null;
38
+ };
39
+ goal_types?: Array<{
40
+ name: string;
41
+ level: number;
42
+ }>;
43
+ display?: {
44
+ depth?: number | null;
45
+ collapsed?: string[];
46
+ };
47
+ }
48
+ export interface GoalsCanonSources {
49
+ elements: unknown[];
50
+ }
51
+ /**
52
+ * Returns true when the parsed view document uses the canon-projection form
53
+ * (`view_config` key present, no inline `goals[]`). Used by the preview and
54
+ * CLI to decide whether to run the resolver or fall back to direct
55
+ * `parseCanonicalGoals` on the inline document.
56
+ */
57
+ export declare function isGoalsViewDoc(parsed: unknown): boolean;
58
+ /**
59
+ * Assemble a flat canonical Goals document from the canon GOAL element
60
+ * store, filtered by the view document's `view_config.scope`. The return
61
+ * value is ready to pass to `parseCanonicalGoals`.
62
+ */
63
+ export declare function resolveGoals(viewDoc: unknown, sources: GoalsCanonSources): Record<string, unknown>;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Goals Tree canon resolver — assembles a flat canonical Goals document from
3
+ * a view_config projection and a canon element store (methodology
4
+ * `notations/views/04-goals.md` §4 "Projection form (Full tier —
5
+ * post-promotion)"). Mirrors `fgca/resolver.ts`'s `resolveFGCA`/`isFGCAViewDoc`
6
+ * and `activities/resolver.ts`'s `resolveAction`/`isActionViewDoc` pattern
7
+ * for the `goals` notation.
8
+ *
9
+ * Output shape matches the flat inline form `parseCanonicalGoals` expects:
10
+ * top-level `goal_types[]` + `goals[]`. Filesystem-free — callable from unit
11
+ * tests without vscode.
12
+ *
13
+ * `goal_types[]` synthesis: when `view_config.goal_types` is present it is
14
+ * used as-is (the documented projection-form shape always includes it) and
15
+ * each goal keeps its own authored `level`, validated against that table as
16
+ * usual. When absent, §5.2 says the renderer "infers levels from the parent
17
+ * chain depth (root goals at 0, each step deeper adds 1)" — this resolver
18
+ * implements that literally (`parentChainDepth`), overriding each selected
19
+ * goal's `level` with its structural depth, rather than trusting the
20
+ * element's own stored `level` field. A stored `level` can't be trusted here:
21
+ * nothing enforces cross-element consistency on it (ELEMENT_PRIMITIVES.md
22
+ * §7.2: `type`/`level` are independent, both-optional per-element fields),
23
+ * and worse, using it made the synthesized table's chosen level depend on
24
+ * which element the canon loader happened to enumerate first — the CLI
25
+ * (`readdirSync`, unsorted) and the VS Code preview
26
+ * (`vscode.workspace.fs.readDirectory`, unsorted) can enumerate the same
27
+ * canon store in different orders, so the two surfaces could disagree on
28
+ * `GOALS-008` findings for identical input. Structural depth has no such
29
+ * dependency. A goal with no `type` still surfaces as a genuine `GOALS-006`
30
+ * finding — this resolver never synthesizes a type label to paper over one.
31
+ */
32
+ import { isObject, str, strArray, descendantsOf, parentChainDepth, stripEnvelope } from '../canon-resolver-utils.js';
33
+ /** GOAL elements by id (`notation: goal`). */
34
+ function collectGoalElements(docs) {
35
+ const out = new Map();
36
+ for (const doc of docs) {
37
+ if (!isObject(doc))
38
+ continue;
39
+ if (str(doc['notation']) !== 'goal')
40
+ continue;
41
+ const id = str(doc['id']);
42
+ if (!id)
43
+ continue;
44
+ if (!out.has(id))
45
+ out.set(id, doc); // first definition wins
46
+ }
47
+ return out;
48
+ }
49
+ /**
50
+ * Returns true when the parsed view document uses the canon-projection form
51
+ * (`view_config` key present, no inline `goals[]`). Used by the preview and
52
+ * CLI to decide whether to run the resolver or fall back to direct
53
+ * `parseCanonicalGoals` on the inline document.
54
+ */
55
+ export function isGoalsViewDoc(parsed) {
56
+ if (!isObject(parsed))
57
+ return false;
58
+ return 'view_config' in parsed && !('goals' in parsed);
59
+ }
60
+ /**
61
+ * Assemble a flat canonical Goals document from the canon GOAL element
62
+ * store, filtered by the view document's `view_config.scope`. The return
63
+ * value is ready to pass to `parseCanonicalGoals`.
64
+ */
65
+ export function resolveGoals(viewDoc, sources) {
66
+ if (!isObject(viewDoc)) {
67
+ return { notation: 'goals', goal_types: [], goals: [] };
68
+ }
69
+ const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
70
+ const scope = isObject(vc['scope']) ? vc['scope'] : {};
71
+ const allGoals = collectGoalElements(sources.elements);
72
+ // 1. Scope by root_goal (this goal + its descendants), or every goal when
73
+ // omitted (§5.1: "Omit to show all root goals").
74
+ const rootGoal = str(scope['root_goal']);
75
+ let candidateIds;
76
+ if (rootGoal) {
77
+ candidateIds = allGoals.has(rootGoal) ? descendantsOf(rootGoal, allGoals) : new Set();
78
+ }
79
+ else {
80
+ candidateIds = new Set(allGoals.keys());
81
+ }
82
+ // 2. Narrow by period — matched against the element's own `period` field
83
+ // when present (the GOAL element schema, ELEMENT_PRIMITIVES.md §7.2, does
84
+ // not define this field explicitly; treated as a forward-compatible tag).
85
+ const period = str(scope['period']);
86
+ if (period) {
87
+ candidateIds = new Set([...candidateIds].filter((id) => str(allGoals.get(id)?.['period']) === period));
88
+ }
89
+ // 3. Narrow by type_filter — only goals of a listed type.
90
+ const typeFilter = strArray(scope['type_filter']);
91
+ if (typeFilter.length > 0) {
92
+ candidateIds = new Set([...candidateIds].filter((id) => {
93
+ const t = str(allGoals.get(id)?.['type']);
94
+ return t !== undefined && typeFilter.includes(t);
95
+ }));
96
+ }
97
+ // 4. Narrow by valid_at — only goals in effect on that date. Goals with no
98
+ // valid_from are not lifecycle-tracked and are kept (permissive default,
99
+ // matching the action/dgca resolvers' stance).
100
+ const validAt = str(scope['valid_at']);
101
+ if (validAt) {
102
+ candidateIds = new Set([...candidateIds].filter((id) => {
103
+ const el = allGoals.get(id);
104
+ const from = str(el?.['valid_from']);
105
+ const to = str(el?.['valid_to']);
106
+ if (from && validAt < from)
107
+ return false;
108
+ if (to && validAt > to)
109
+ return false;
110
+ return true;
111
+ }));
112
+ }
113
+ const explicitGoalTypes = Array.isArray(vc['goal_types'])
114
+ ? vc['goal_types'].filter(isObject)
115
+ : undefined;
116
+ // Element fields map onto Goal fields unchanged — the admission/lifecycle
117
+ // envelope fields carry no rendering meaning for `parseCanonicalGoals` and
118
+ // are dropped.
119
+ let selectedGoals;
120
+ let goalTypes;
121
+ if (explicitGoalTypes) {
122
+ // Main-line case: goals keep their own authored `level`, validated
123
+ // against the explicit table by parseCanonicalGoals as usual.
124
+ selectedGoals = [...candidateIds].map((id) => ({ ...stripEnvelope(allGoals.get(id)), id }));
125
+ goalTypes = explicitGoalTypes;
126
+ }
127
+ else {
128
+ // §5.2 fallback: level is the goal's structural depth in the parent
129
+ // chain, not its (untrustworthy, order-dependent) stored `level` field.
130
+ const depthMemo = new Map();
131
+ selectedGoals = [...candidateIds].map((id) => {
132
+ const { level: _level, ...rest } = stripEnvelope(allGoals.get(id));
133
+ return { ...rest, id, level: parentChainDepth(id, allGoals, depthMemo) };
134
+ });
135
+ const seen = new Map();
136
+ for (const g of selectedGoals) {
137
+ const name = typeof g['type'] === 'string' ? g['type'] : undefined;
138
+ if (name !== undefined && !seen.has(name)) {
139
+ seen.set(name, g['level']);
140
+ }
141
+ }
142
+ goalTypes = [...seen.entries()].map(([name, level]) => ({ name, level }));
143
+ }
144
+ return {
145
+ notation: 'goals',
146
+ id: viewDoc['id'],
147
+ name: viewDoc['name'],
148
+ spec_version: viewDoc['spec_version'],
149
+ description: viewDoc['description'],
150
+ period: viewDoc['period'],
151
+ goal_types: goalTypes,
152
+ goals: selectedGoals,
153
+ };
154
+ }
@@ -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;