structured-context 0.13.0 → 0.14.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 (70) hide show
  1. package/README.md +2 -2
  2. package/dist/api.d.ts +17 -17
  3. package/dist/api.js +12 -12
  4. package/dist/commands/dump.js +1 -1
  5. package/dist/commands/validate.js +8 -8
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.js +4 -4
  8. package/dist/filter/augment-nodes.d.ts +5 -4
  9. package/dist/filter/augment-nodes.js +20 -19
  10. package/dist/filter/expand-include.d.ts +2 -2
  11. package/dist/filter/expand-include.js +15 -17
  12. package/dist/filter/filter-nodes.js +2 -2
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +17 -17
  15. package/dist/judge/context.d.ts +1 -1
  16. package/dist/judge/context.js +12 -12
  17. package/dist/judge/factory.d.ts +1 -1
  18. package/dist/judge/factory.js +1 -1
  19. package/dist/judge/index.d.ts +1 -1
  20. package/dist/judge/judge-node.d.ts +4 -4
  21. package/dist/judge/judge-node.js +7 -7
  22. package/dist/judge/prompt.d.ts +1 -1
  23. package/dist/judge/prompt.js +3 -3
  24. package/dist/judge/provider/fake.d.ts +1 -1
  25. package/dist/judge/provider/fake.js +3 -6
  26. package/dist/judge/provider/jev.js +1 -1
  27. package/dist/judge/provider/openai-decisions.js +1 -1
  28. package/dist/judge/trim.d.ts +1 -1
  29. package/dist/judge/types.d.ts +4 -1
  30. package/dist/judge/verdicts.d.ts +1 -1
  31. package/dist/load-space-graph.d.ts +3 -3
  32. package/dist/load-space-graph.js +4 -4
  33. package/dist/plugins/loader.js +1 -1
  34. package/dist/plugins/markdown/index.d.ts +6 -0
  35. package/dist/plugins/markdown/index.js +1 -0
  36. package/dist/plugins/markdown/parse-embedded.d.ts +7 -2
  37. package/dist/plugins/markdown/parse-embedded.js +23 -21
  38. package/dist/plugins/markdown/read-space.js +9 -8
  39. package/dist/plugins/markdown/render-bullets.js +3 -3
  40. package/dist/plugins/markdown/render-directory.js +1 -1
  41. package/dist/plugins/markdown/util.d.ts +12 -1
  42. package/dist/plugins/markdown/util.js +21 -0
  43. package/dist/plugins/mermaid/render-mermaid.js +5 -6
  44. package/dist/plugins/miro/cache.js +1 -1
  45. package/dist/plugins/miro/sync.js +3 -2
  46. package/dist/read/resolve-graph-edges.js +12 -18
  47. package/dist/render/render.d.ts +1 -1
  48. package/dist/render/render.js +1 -1
  49. package/dist/schema/evaluate-rule.d.ts +4 -4
  50. package/dist/schema/evaluate-rule.js +14 -10
  51. package/dist/schema/gating.d.ts +10 -10
  52. package/dist/schema/gating.js +45 -44
  53. package/dist/schema/validate-graph.js +8 -11
  54. package/dist/schema/validate-rules.d.ts +4 -4
  55. package/dist/schema/validate-rules.js +15 -17
  56. package/dist/schemas/_ost_strict.json +1 -1
  57. package/dist/space-context.d.ts +2 -2
  58. package/dist/space-context.js +3 -3
  59. package/dist/space-graph.d.ts +9 -9
  60. package/dist/space-graph.js +16 -16
  61. package/dist/testing.d.ts +2 -2
  62. package/dist/testing.js +3 -3
  63. package/dist/types.d.ts +18 -14
  64. package/dist/validate.d.ts +6 -6
  65. package/dist/validate.js +26 -27
  66. package/docs/concepts.md +25 -7
  67. package/docs/config.md +8 -0
  68. package/docs/rules.md +17 -13
  69. package/package.json +5 -5
  70. package/schemas/_ost_strict.json +1 -1
@@ -31,11 +31,11 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
31
31
  const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
32
32
  const childType = resolveNodeType(rawChildType, typeAliases);
33
33
  const parentType = resolveNodeType(rawParentType, typeAliases);
34
- function pushParentRef(childNode, parentTitle) {
35
- // Deduplicate by (field, title) — same parent via different fields is intentional
36
- if (!childNode.resolvedParents.some((r) => r.field === field && r.title === parentTitle)) {
34
+ function pushParentRef(childNode, parentId) {
35
+ // Deduplicate by (field, id) — same parent via different fields is intentional
36
+ if (!childNode.resolvedParents.some((r) => r.field === field && r.id === parentId)) {
37
37
  childNode.resolvedParents.push({
38
- title: parentTitle,
38
+ id: parentId,
39
39
  field,
40
40
  fieldOn,
41
41
  source,
@@ -47,7 +47,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
47
47
  function checkShape(ownerNode, rawField) {
48
48
  if (multiple && !Array.isArray(rawField)) {
49
49
  unresolvedRefs.push({
50
- label: ownerNode.label,
50
+ id: ownerNode.id,
51
51
  ref: String(rawField),
52
52
  field,
53
53
  reason: 'invalid_shape',
@@ -57,7 +57,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
57
57
  }
58
58
  if (!multiple && typeof rawField !== 'string') {
59
59
  unresolvedRefs.push({
60
- label: ownerNode.label,
60
+ id: ownerNode.id,
61
61
  ref: String(rawField),
62
62
  field,
63
63
  reason: 'invalid_shape',
@@ -73,7 +73,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
73
73
  const resolved = targetIndex.get(target);
74
74
  if (resolved === undefined) {
75
75
  unresolvedRefs.push({
76
- label: ownerNode.label,
76
+ id: ownerNode.id,
77
77
  ref,
78
78
  field,
79
79
  reason: 'not_found',
@@ -84,7 +84,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
84
84
  }
85
85
  if (resolved === null) {
86
86
  unresolvedRefs.push({
87
- label: ownerNode.label,
87
+ id: ownerNode.id,
88
88
  ref,
89
89
  field,
90
90
  reason: 'ambiguous',
@@ -109,10 +109,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
109
109
  const childNode = checkAndRecordRef(parentNode, ref);
110
110
  if (!childNode)
111
111
  continue;
112
- const parentTitle = parentNode.schemaData.title;
113
- if (typeof parentTitle !== 'string')
114
- continue;
115
- pushParentRef(childNode, parentTitle);
112
+ pushParentRef(childNode, parentNode.id);
116
113
  }
117
114
  }
118
115
  }
@@ -129,10 +126,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
129
126
  const parentNode = checkAndRecordRef(childNode, ref);
130
127
  if (!parentNode)
131
128
  continue;
132
- const parentTitle = parentNode.schemaData.title;
133
- if (typeof parentTitle !== 'string')
134
- continue;
135
- pushParentRef(childNode, parentTitle);
129
+ pushParentRef(childNode, parentNode.id);
136
130
  }
137
131
  }
138
132
  }
@@ -237,12 +231,12 @@ export function resolveGraphEdges(nodes, metadata) {
237
231
  for (const node of spaceNodes) {
238
232
  node.resolvedLinks = (node.contentLinks ?? []).map((link) => classifyContentLink(link, targetIndex));
239
233
  }
240
- // Deduplicate by (label, field, ref) — the same broken link may be encountered across
234
+ // Deduplicate by (id, field, ref) — the same broken link may be encountered across
241
235
  // multiple resolveEdge calls (e.g. selfRef + regular hierarchy share the same field).
242
236
  const seen = new Set();
243
237
  const deduped = [];
244
238
  for (const u of unresolvedRefs) {
245
- const key = `${u.label}\0${u.field}\0${u.ref}`;
239
+ const key = `${u.id}\0${u.field}\0${u.ref}`;
246
240
  if (!seen.has(key)) {
247
241
  seen.add(key);
248
242
  deduped.push(u);
@@ -1,4 +1,4 @@
1
- import { type RenderResult } from '../plugins/util.js';
1
+ import { type RenderResult } from '../plugins/util';
2
2
  import type { SpaceContext } from '../types';
3
3
  export declare function executeRender(formatName: string, context: SpaceContext, options: {
4
4
  filter?: string;
@@ -1,7 +1,7 @@
1
- import { shortenPluginName } from '../plugins/util.js';
2
1
  import { updateSpaceField } from '../config';
3
2
  import { assembleSpaceGraph } from '../load-space-graph';
4
3
  import { loadPlugins } from '../plugins/loader';
4
+ import { shortenPluginName } from '../plugins/util';
5
5
  import { buildFormatRegistry } from './registry';
6
6
  export async function executeRender(formatName, context, options) {
7
7
  const pluginMap = context.space?.plugins ?? {};
@@ -1,4 +1,4 @@
1
- import type { Rule, RuleViolation, SpaceNode } from '../types';
1
+ import type { NodeId, Rule, RuleViolation, SpaceNode } from '../types';
2
2
  /** Evaluation context for JSONata expressions */
3
3
  export interface EvalContext {
4
4
  /** All nodes in the space (flattened) */
@@ -37,10 +37,10 @@ export declare function tryEvaluateExpression(expr: string, context: EvalContext
37
37
  *
38
38
  * @param node - The node to build context for
39
39
  * @param allNodes - All nodes in the space
40
- * @param nodeIndex - Map of node titles to nodes for efficient lookup
40
+ * @param nodeIndex - Map of node ids to nodes for efficient lookup
41
41
  * @returns Evaluation context for the node
42
42
  */
43
- export declare function buildEvalContext(node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<string, SpaceNode>): EvalContext;
43
+ export declare function buildEvalContext(node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<NodeId, SpaceNode>): EvalContext;
44
44
  /** The violation reported when a rule's `expression` or `when` cannot be evaluated: an authoring error, so always `error`. */
45
45
  export declare function expressionErrorViolation(rule: Rule, file: string, error: string): RuleViolation;
46
46
  /** The outcome of a rule's `expression` on one node. */
@@ -58,4 +58,4 @@ export type ExpressionRuleResult = {
58
58
  * reports the rule's severity; a rule with a semantic part names the expression in the evidence, so the
59
59
  * violation is not mistaken for a verdict. An evaluation error reports `error` and is not a `failed` result.
60
60
  */
61
- export declare function evaluateRuleExpression(rule: Rule, node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<string, SpaceNode>, file?: string): Promise<ExpressionRuleResult>;
61
+ export declare function evaluateRuleExpression(rule: Rule, node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<NodeId, SpaceNode>, file?: string): Promise<ExpressionRuleResult>;
@@ -64,12 +64,16 @@ export async function tryEvaluateExpression(expr, context) {
64
64
  * @param node - The node to flatten
65
65
  * @returns Flattened node with properties directly accessible
66
66
  */
67
- function flattenNode(node) {
67
+ function flattenNode(node, nodeIndex) {
68
+ const parentTitle = (r) => nodeIndex.get(r.id)?.title ?? r.id;
68
69
  return {
69
70
  ...node.schemaData,
71
+ id: node.id,
70
72
  resolvedType: node.resolvedType,
71
- resolvedParentTitle: node.resolvedParents[0]?.title, // first parent or undefined, provided for convenience
72
- resolvedParentTitles: node.resolvedParents.map((r) => r.title), // full array of parent titles
73
+ resolvedParentId: node.resolvedParents[0]?.id, // first parent or undefined, provided for convenience
74
+ resolvedParentIds: node.resolvedParents.map((r) => r.id), // full array of parent ids
75
+ resolvedParentTitle: node.resolvedParents[0] && parentTitle(node.resolvedParents[0]), // display title of the first parent
76
+ resolvedParentTitles: node.resolvedParents.map(parentTitle), // display titles of all parents
73
77
  resolvedLinks: node.resolvedLinks,
74
78
  };
75
79
  }
@@ -78,23 +82,23 @@ function flattenNode(node) {
78
82
  *
79
83
  * @param node - The node to build context for
80
84
  * @param allNodes - All nodes in the space
81
- * @param nodeIndex - Map of node titles to nodes for efficient lookup
85
+ * @param nodeIndex - Map of node ids to nodes for efficient lookup
82
86
  * @returns Evaluation context for the node
83
87
  */
84
88
  export function buildEvalContext(node, allNodes, nodeIndex) {
85
89
  // Flatten all nodes for JSONata access
86
- const flattenedNodes = allNodes.map(flattenNode);
90
+ const flattenedNodes = allNodes.map((n) => flattenNode(n, nodeIndex));
87
91
  // Build all parent objects from resolvedParents array
88
92
  const flattenedParents = [];
89
- for (const { title: parentTitle } of node.resolvedParents) {
90
- const parentNode = nodeIndex.get(parentTitle);
93
+ for (const { id: parentId } of node.resolvedParents) {
94
+ const parentNode = nodeIndex.get(parentId);
91
95
  if (parentNode) {
92
- flattenedParents.push(flattenNode(parentNode));
96
+ flattenedParents.push(flattenNode(parentNode, nodeIndex));
93
97
  }
94
98
  }
95
99
  return {
96
100
  nodes: flattenedNodes,
97
- $$: flattenNode(node),
101
+ $$: flattenNode(node, nodeIndex),
98
102
  parent: flattenedParents[0],
99
103
  parents: flattenedParents.length > 0 ? flattenedParents : undefined,
100
104
  };
@@ -114,7 +118,7 @@ export function expressionErrorViolation(rule, file, error) {
114
118
  * reports the rule's severity; a rule with a semantic part names the expression in the evidence, so the
115
119
  * violation is not mistaken for a verdict. An evaluation error reports `error` and is not a `failed` result.
116
120
  */
117
- export async function evaluateRuleExpression(rule, node, allNodes, nodeIndex, file = node.label) {
121
+ export async function evaluateRuleExpression(rule, node, allNodes, nodeIndex, file = node.id) {
118
122
  const expression = rule.expression;
119
123
  const outcome = await tryEvaluateExpression(expression, buildEvalContext(node, allNodes, nodeIndex));
120
124
  if ('error' in outcome) {
@@ -1,10 +1,10 @@
1
1
  import type { TrimReport } from '../judge/trim';
2
2
  import type { SemanticJudgeProvider } from '../judge/types';
3
3
  import type { SpaceGraph } from '../space-graph';
4
- import type { Rule, RuleSeverity, RuleViolation, SpaceNode } from '../types';
4
+ import type { NodeId, Rule, RuleSeverity, RuleViolation, SpaceNode } from '../types';
5
5
  /**
6
- * Whether a failure of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
7
- * `severity` is that of the failure: the rule's own for an expression, the verdict's for a semantic part.
6
+ * Whether a violation of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
7
+ * `severity` is that of the violation: the rule's own for an expression, the verdict's for a semantic part.
8
8
  */
9
9
  export declare function ruleGatesDescendants(rule: Rule, severity?: RuleSeverity): boolean;
10
10
  /**
@@ -12,10 +12,10 @@ export declare function ruleGatesDescendants(rule: Rule, severity?: RuleSeverity
12
12
  * (schema validation or hierarchy structure) that failed on a specific node.
13
13
  */
14
14
  export type GatingSeed = {
15
- /** Label (file) of the failing node. */
15
+ /** Id (file) of the failing node. */
16
16
  file: string;
17
- /** Title of the failing node. */
18
- title: string;
17
+ /** Id of the failing node. */
18
+ id: NodeId;
19
19
  /** Which always-run layer produced the failure. */
20
20
  reason: 'schema' | 'hierarchy';
21
21
  };
@@ -24,10 +24,10 @@ export type GatingSeed = {
24
24
  * rule evaluation for itself (if schema/hierarchy caused it) and/or its hierarchy subtree.
25
25
  */
26
26
  export type CoverageGate = {
27
- /** Label (file) of the node whose failure triggered this gate. */
27
+ /** Id (file) of the node whose failure triggered this gate. */
28
28
  file: string;
29
- /** Title of the blocking node. */
30
- nodeTitle: string;
29
+ /** Id of the blocking node. */
30
+ nodeId: NodeId;
31
31
  /** What kind of check triggered the gate. */
32
32
  reason: 'schema' | 'hierarchy' | 'rule';
33
33
  /** Rule id responsible, when `reason` is `'rule'`. */
@@ -56,7 +56,7 @@ export interface GatingOptions {
56
56
  * Compute breadth-first depth (1-based, from hierarchy roots and orphans) for every
57
57
  * hierarchy-typed node in the graph. Non-hierarchy nodes are not included.
58
58
  */
59
- export declare function computeHierarchyDepths(graph: SpaceGraph): Map<string, number>;
59
+ export declare function computeHierarchyDepths(graph: SpaceGraph): Map<NodeId, number>;
60
60
  /**
61
61
  * Evaluate executable rules with cascade gating: a schema, hierarchy, or blocking-rule
62
62
  * failure on a node suppresses rule evaluation for that node (when the failure is
@@ -2,8 +2,8 @@ import { judgeNodes } from '../judge/judge-node';
2
2
  import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
3
3
  import { ruleSeverities, ruleSeverity } from './rule-severity';
4
4
  /**
5
- * Whether a failure of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
6
- * `severity` is that of the failure: the rule's own for an expression, the verdict's for a semantic part.
5
+ * Whether a violation of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
6
+ * `severity` is that of the violation: the rule's own for an expression, the verdict's for a semantic part.
7
7
  */
8
8
  export function ruleGatesDescendants(rule, severity = ruleSeverity(rule)) {
9
9
  return rule.gatesDescendants ?? severity === 'error';
@@ -20,17 +20,17 @@ export function computeHierarchyDepths(graph) {
20
20
  const depths = new Map();
21
21
  const queue = [];
22
22
  for (const node of [...graph.hierarchyRoots, ...graph.orphans]) {
23
- queue.push({ title: node.title, depth: 1 });
23
+ queue.push({ id: node.id, depth: 1 });
24
24
  }
25
25
  let i = 0;
26
26
  while (i < queue.length) {
27
- const { title, depth } = queue[i++];
28
- if (depths.has(title))
27
+ const { id, depth } = queue[i++];
28
+ if (depths.has(id))
29
29
  continue;
30
- depths.set(title, depth);
31
- for (const child of graph.hierarchyChildren.get(title) ?? []) {
32
- if (!depths.has(child.title))
33
- queue.push({ title: child.title, depth: depth + 1 });
30
+ depths.set(id, depth);
31
+ for (const child of graph.hierarchyChildren.get(id) ?? []) {
32
+ if (!depths.has(child.id))
33
+ queue.push({ id: child.id, depth: depth + 1 });
34
34
  }
35
35
  }
36
36
  return depths;
@@ -48,24 +48,24 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
48
48
  const includeGated = options.includeGated ?? false;
49
49
  const nodeIndex = new Map();
50
50
  for (const node of nodes) {
51
- if (node.title)
52
- nodeIndex.set(node.title, node);
51
+ if (node.id)
52
+ nodeIndex.set(node.id, node);
53
53
  }
54
54
  // When a node has both a schema and hierarchy failure, report schema as the reason.
55
- const seedByTitle = new Map();
55
+ const seedById = new Map();
56
56
  for (const seed of seeds) {
57
- if (!seedByTitle.has(seed.title) || seed.reason === 'schema') {
58
- seedByTitle.set(seed.title, seed);
57
+ if (!seedById.has(seed.id) || seed.reason === 'schema') {
58
+ seedById.set(seed.id, seed);
59
59
  }
60
60
  }
61
61
  const judge = options.provider;
62
62
  const violations = [];
63
63
  const gateOrigins = new Map();
64
- function originFor(title, seed) {
65
- let origin = gateOrigins.get(title);
64
+ function originFor(id, seed) {
65
+ let origin = gateOrigins.get(id);
66
66
  if (!origin) {
67
- origin = { file: seed.file, nodeTitle: title, reason: seed.reason, ruleId: seed.ruleId, gatedFiles: [] };
68
- gateOrigins.set(title, origin);
67
+ origin = { file: seed.file, nodeId: id, reason: seed.reason, ruleId: seed.ruleId, gatedFiles: [] };
68
+ gateOrigins.set(id, origin);
69
69
  }
70
70
  return origin;
71
71
  }
@@ -85,10 +85,10 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
85
85
  const parentsOf = new Map();
86
86
  for (const [parent, children] of graph.hierarchyChildren) {
87
87
  for (const child of children)
88
- parentsOf.set(child.title, [...(parentsOf.get(child.title) ?? []), parent]);
88
+ parentsOf.set(child.id, [...(parentsOf.get(child.id) ?? []), parent]);
89
89
  }
90
- const target = nodes.find((n) => n.label === options.judgeTarget);
91
- const pending = target ? [target.title] : [];
90
+ const target = nodes.find((n) => n.id === options.judgeTarget);
91
+ const pending = target ? [target.id] : [];
92
92
  while (pending.length > 0) {
93
93
  for (const parent of parentsOf.get(pending.pop()) ?? []) {
94
94
  if (!targetAncestors.has(parent)) {
@@ -104,12 +104,9 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
104
104
  for (const r of perNodeRules) {
105
105
  if (r.type && r.type !== node.resolvedType)
106
106
  continue;
107
- if (judge &&
108
- r.semantic !== undefined &&
109
- options.judgeTarget !== undefined &&
110
- node.label !== options.judgeTarget) {
107
+ if (judge && r.semantic !== undefined && options.judgeTarget !== undefined && node.id !== options.judgeTarget) {
111
108
  // Ancestors only matter through the gates they could raise.
112
- if (!(targetAncestors.has(node.title) && ruleCanGate(r)))
109
+ if (!(targetAncestors.has(node.id) && ruleCanGate(r)))
113
110
  continue;
114
111
  }
115
112
  if (r.when) {
@@ -128,11 +125,11 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
128
125
  async function prepareNode(node) {
129
126
  const local = [];
130
127
  const prepared = { violations: local, semantic: [], judged: () => { } };
131
- const applicable = await applicableRules(node, (rule, error) => local.push(expressionErrorViolation(rule, node.label, error)));
128
+ const applicable = await applicableRules(node, (rule, error) => local.push(expressionErrorViolation(rule, node.id, error)));
132
129
  function report(rule, detail = {}) {
133
130
  const severity = detail.severity ?? ruleSeverities(rule)[0] ?? ruleSeverity(rule);
134
131
  local.push({
135
- file: node.label,
132
+ file: node.id,
136
133
  ruleId: rule.id,
137
134
  category: rule.category,
138
135
  severity,
@@ -148,7 +145,9 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
148
145
  prepared.blockingRuleId = rule.id;
149
146
  }
150
147
  }
151
- // Cheap expressions first; a semantic part is only evaluated when its own expression passes.
148
+ // Cheap expressions first. A semantic part is evaluated only when its own expression passes, and not at all
149
+ // on a node with a gating violation.
150
+ const semantic = [];
152
151
  for (const rule of applicable) {
153
152
  if (rule.expression !== undefined) {
154
153
  const result = await evaluateRuleExpression(rule, node, nodes, nodeIndex);
@@ -161,11 +160,13 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
161
160
  }
162
161
  }
163
162
  if (rule.semantic !== undefined && judge)
164
- prepared.semantic.push(rule);
163
+ semantic.push(rule);
165
164
  }
165
+ if (!prepared.blockingRuleId)
166
+ prepared.semantic = semantic;
166
167
  prepared.judged = (judgements) => {
167
168
  for (const j of judgements) {
168
- if (j.failed) {
169
+ if (j.violated) {
169
170
  report(j.rule, {
170
171
  severity: j.severity,
171
172
  verdict: j.verdict,
@@ -209,38 +210,38 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
209
210
  while (depthNodes.length > 0) {
210
211
  const current = [];
211
212
  for (const entry of depthNodes) {
212
- if (visited.has(entry.node.title))
213
+ if (visited.has(entry.node.id))
213
214
  continue;
214
- visited.add(entry.node.title);
215
+ visited.add(entry.node.id);
215
216
  current.push(entry);
216
217
  }
217
- const isGated = (e) => e.ancestorOrigin !== undefined || seedByTitle.has(e.node.title);
218
+ const isGated = (e) => e.ancestorOrigin !== undefined || seedById.has(e.node.id);
218
219
  const toEvaluate = current.filter((e) => !isGated(e) || includeGated);
219
220
  const evaluated = await evaluateNodeSet(toEvaluate.map((e) => e.node));
220
- const blockingByTitle = new Map(toEvaluate.map((e, i) => [e.node.title, evaluated[i].blockingRuleId]));
221
+ const blockingById = new Map(toEvaluate.map((e, i) => [e.node.id, evaluated[i].blockingRuleId]));
221
222
  const next = [];
222
223
  for (const { node, ancestorOrigin } of current) {
223
- const selfSeed = seedByTitle.get(node.title);
224
+ const selfSeed = seedById.get(node.id);
224
225
  const isGatedByAncestor = ancestorOrigin !== undefined;
225
226
  const isSelfBlocked = selfSeed !== undefined;
226
- const blockingRuleId = blockingByTitle.get(node.title);
227
+ const blockingRuleId = blockingById.get(node.id);
227
228
  let childOrigin = ancestorOrigin;
228
229
  if (isGatedByAncestor) {
229
- gateOrigins.get(ancestorOrigin).gatedFiles.push(node.label);
230
+ gateOrigins.get(ancestorOrigin).gatedFiles.push(node.id);
230
231
  }
231
232
  else if (isSelfBlocked) {
232
- const origin = originFor(node.title, { file: selfSeed.file, reason: selfSeed.reason });
233
- origin.gatedFiles.push(node.label);
234
- childOrigin = node.title;
233
+ const origin = originFor(node.id, { file: selfSeed.file, reason: selfSeed.reason });
234
+ origin.gatedFiles.push(node.id);
235
+ childOrigin = node.id;
235
236
  }
236
237
  else if (blockingRuleId) {
237
- originFor(node.title, { file: node.label, reason: 'rule', ruleId: blockingRuleId });
238
- childOrigin = node.title;
238
+ originFor(node.id, { file: node.id, reason: 'rule', ruleId: blockingRuleId });
239
+ childOrigin = node.id;
239
240
  }
240
241
  else {
241
242
  childOrigin = undefined;
242
243
  }
243
- for (const child of graph.hierarchyChildren.get(node.title) ?? []) {
244
+ for (const child of graph.hierarchyChildren.get(node.id) ?? []) {
244
245
  next.push({ node: child, ancestorOrigin: childOrigin });
245
246
  }
246
247
  }
@@ -5,7 +5,7 @@ import { resolveNodeType } from '../schema/schema';
5
5
  */
6
6
  export function validateGraph(nodes, metadata, unresolvedRefs = []) {
7
7
  const refErrors = unresolvedRefs.map((u) => ({
8
- file: u.source?.file ?? u.label,
8
+ file: u.source?.file ?? u.id,
9
9
  ...(u.source?.line !== undefined ? { line: u.source.line } : {}),
10
10
  parent: u.ref,
11
11
  error: u.message,
@@ -69,20 +69,17 @@ export function validateHierarchyStructure(nodes, metadata) {
69
69
  }
70
70
  }
71
71
  const nodeIndex = new Map();
72
- for (const node of nodes) {
73
- const title = node.title;
74
- if (title)
75
- nodeIndex.set(title, node);
76
- }
72
+ for (const node of nodes)
73
+ nodeIndex.set(node.id, node);
77
74
  for (const node of nodes) {
78
75
  const nodeType = node.resolvedType;
79
76
  const nodeTitle = node.title;
80
77
  for (const parentRef of node.resolvedParents) {
81
- const parentNode = nodeIndex.get(parentRef.title);
78
+ const parentNode = nodeIndex.get(parentRef.id);
82
79
  if (!parentNode)
83
80
  continue;
84
81
  const parentType = parentNode.resolvedType;
85
- const parentTitle = parentRef.title;
82
+ const parentTitle = parentNode.title;
86
83
  if (parentRef.fieldOn === 'parent') {
87
84
  // Field is on the parent node; validate that this node's type matches the expected child type.
88
85
  // Violation is attributed to the field-owner (parent) node.
@@ -90,7 +87,7 @@ export function validateHierarchyStructure(nodes, metadata) {
90
87
  if (allowedChildTypes && !allowedChildTypes.has(nodeType)) {
91
88
  const expected = [...allowedChildTypes].join(' or ');
92
89
  violations.push({
93
- file: parentNode.label,
90
+ file: parentNode.id,
94
91
  nodeType: parentType,
95
92
  nodeTitle: parentTitle,
96
93
  parentType: nodeType,
@@ -105,7 +102,7 @@ export function validateHierarchyStructure(nodes, metadata) {
105
102
  if (allowedTypes && !allowedTypes.has(parentType)) {
106
103
  const expected = [...allowedTypes].join(' or ');
107
104
  violations.push({
108
- file: node.label,
105
+ file: node.id,
109
106
  nodeType,
110
107
  nodeTitle,
111
108
  parentType,
@@ -143,7 +140,7 @@ export function validateHierarchyStructure(nodes, metadata) {
143
140
  }
144
141
  if (!isValidHierarchy) {
145
142
  violations.push({
146
- file: node.label,
143
+ file: node.id,
147
144
  nodeType,
148
145
  nodeTitle,
149
146
  parentType,
@@ -1,14 +1,14 @@
1
1
  import type { TrimReport } from '../judge/trim';
2
2
  import type { SemanticJudgeProvider } from '../judge/types';
3
- import type { Rule, RuleViolation, SpaceNode } from '../types';
3
+ import type { NodeId, Rule, RuleViolation, SpaceNode } from '../types';
4
4
  export { DEFAULT_SEVERITY_BY_CATEGORY, ruleSeverity } from './rule-severity';
5
5
  export interface ValidateRulesOptions {
6
6
  /** Evaluates semantic rules. Without one, a rule runs only its `expression`, and a semantic-only rule is skipped. */
7
7
  judge?: SemanticJudgeProvider;
8
- /** Label of the only node to judge (`validate-file`); other nodes' semantic rules are skipped. */
8
+ /** Id of the only node to judge (`validate-file`); other nodes' semantic rules are skipped. */
9
9
  judgeTarget?: string;
10
- /** Set of node labels that have already failed upstream structural checks. */
11
- gatedNodeLabels?: Set<string>;
10
+ /** Set of node ids that have already failed upstream structural checks. */
11
+ gatedNodeIds?: Set<NodeId>;
12
12
  trimReport?: TrimReport;
13
13
  }
14
14
  /**
@@ -1,6 +1,7 @@
1
1
  import { judgeNodes } from '../judge/judge-node';
2
2
  import { buildSpaceGraph } from '../space-graph';
3
3
  import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
4
+ import { ruleGatesDescendants } from './gating';
4
5
  import { ruleSeverities, ruleSeverity } from './rule-severity';
5
6
  export { DEFAULT_SEVERITY_BY_CATEGORY, ruleSeverity } from './rule-severity';
6
7
  /**
@@ -18,10 +19,7 @@ export async function validateRules(nodes, rules, options = {}) {
18
19
  // Build node index for efficient lookups
19
20
  const nodeIndex = new Map();
20
21
  for (const node of nodes) {
21
- const title = node.title;
22
- if (title) {
23
- nodeIndex.set(title, node);
24
- }
22
+ nodeIndex.set(node.id, node);
25
23
  }
26
24
  const sentinel = nodes[0];
27
25
  const runnable = rules.filter((r) => r.expression !== undefined || (r.semantic !== undefined && judge));
@@ -37,7 +35,7 @@ export async function validateRules(nodes, rules, options = {}) {
37
35
  if (rule.when) {
38
36
  const when = await tryEvaluateExpression(rule.when, buildEvalContext(node, nodes, nodeIndex));
39
37
  if ('error' in when) {
40
- violations.push(expressionErrorViolation(rule, node.label, when.error));
38
+ violations.push(expressionErrorViolation(rule, node.id, when.error));
41
39
  continue;
42
40
  }
43
41
  if (when.value !== true)
@@ -49,8 +47,8 @@ export async function validateRules(nodes, rules, options = {}) {
49
47
  }
50
48
  // 1. Expressions, in rule order. A global rule is evaluated once against the full node set; a sentinel node
51
49
  // provides the evaluation context (the nodes array is what matters).
52
- const failedNodeLabels = new Set(options.gatedNodeLabels); // nodes that failed an expression (or upstream)
53
- const notEvaluable = new Set(); // `${rule.id}\0${label}` whose own expression failed or errored
50
+ const gatedNodeIds = new Set(options.gatedNodeIds); // nodes with a gating violation (or gated upstream)
51
+ const notEvaluable = new Set(); // `${rule.id}\0${id}` whose own expression failed or errored
54
52
  for (const rule of runnable) {
55
53
  if (rule.expression === undefined)
56
54
  continue;
@@ -67,24 +65,24 @@ export async function validateRules(nodes, rules, options = {}) {
67
65
  if (result.passed)
68
66
  continue;
69
67
  violations.push(result.violation);
70
- notEvaluable.add(`${rule.id}\0${node.label}`);
71
- if (result.failed)
72
- failedNodeLabels.add(node.label);
68
+ notEvaluable.add(`${rule.id}\0${node.id}`);
69
+ if (result.failed && ruleGatesDescendants(rule, result.violation.severity))
70
+ gatedNodeIds.add(node.id);
73
71
  }
74
72
  }
75
- // 2. Semantic parts, for nodes that passed their own expression. A node that failed an expression or
76
- // upstream is left out (safe cascading default).
73
+ // 2. Semantic parts, for nodes that passed their own expression. A node with a gating violation, or gated
74
+ // upstream, is left out.
77
75
  if (judge) {
78
76
  const perNode = []; // kept in node order
79
77
  const pending = [];
80
78
  for (const node of nodes) {
81
- if (options.judgeTarget !== undefined && node.label !== options.judgeTarget)
79
+ if (options.judgeTarget !== undefined && node.id !== options.judgeTarget)
82
80
  continue;
83
81
  const found = [];
84
82
  perNode.push(found);
85
- if (failedNodeLabels.has(node.label))
83
+ if (gatedNodeIds.has(node.id))
86
84
  continue;
87
- const semantic = runnable.filter((r) => r.semantic !== undefined && applicable.get(r)?.includes(node) && !notEvaluable.has(`${r.id}\0${node.label}`));
85
+ const semantic = runnable.filter((r) => r.semantic !== undefined && applicable.get(r)?.includes(node) && !notEvaluable.has(`${r.id}\0${node.id}`));
88
86
  if (semantic.length > 0)
89
87
  pending.push({ node, rules: semantic, found });
90
88
  }
@@ -92,10 +90,10 @@ export async function validateRules(nodes, rules, options = {}) {
92
90
  const judged = await judgeNodes(judge, pending, buildSpaceGraph(nodes, undefined), options.trimReport);
93
91
  pending.forEach(({ node, found }, i) => {
94
92
  for (const j of judged[i] ?? []) {
95
- if (!j.failed)
93
+ if (!j.violated)
96
94
  continue;
97
95
  found.push({
98
- file: node.label,
96
+ file: node.id,
99
97
  ruleId: j.rule.id,
100
98
  category: j.rule.category,
101
99
  severity: j.severity ?? ruleSeverities(j.rule)[0] ?? ruleSeverity(j.rule),
@@ -34,7 +34,7 @@
34
34
  "category": "best-practice",
35
35
  "description": "Explore multiple candidate solutions (aim for at least three) for the target opportunity",
36
36
  "type": "opportunity",
37
- "expression": "(current.status != 'exploring' and current.status != 'active') or $count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution']) >= 3"
37
+ "expression": "(current.status != 'exploring' and current.status != 'active') or $count(nodes[resolvedParentId=$$.current.id and resolvedType='solution']) >= 3"
38
38
  },
39
39
  {
40
40
  "id": "opportunity-customer-voice",
@@ -1,5 +1,5 @@
1
- import { type Config } from './config.js';
2
- import type { SpaceContext } from './types.js';
1
+ import { type Config } from './config';
2
+ import type { SpaceContext } from './types';
3
3
  /** Thrown by createSpaceContext when no space matches the given name. */
4
4
  export declare class SpaceNotFoundError extends Error {
5
5
  readonly spaceName: string;
@@ -1,6 +1,6 @@
1
- import { resolveSchema } from './config.js';
2
- import { resolveSpacePluginSchemas } from './plugins/util.js';
3
- import { loadSchema } from './schema/schema.js';
1
+ import { resolveSchema } from './config';
2
+ import { resolveSpacePluginSchemas } from './plugins/util';
3
+ import { loadSchema } from './schema/schema';
4
4
  /** Thrown by createSpaceContext when no space matches the given name. */
5
5
  export class SpaceNotFoundError extends Error {
6
6
  spaceName;