structured-context 0.11.0 → 0.13.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 (138) hide show
  1. package/README.md +52 -198
  2. package/dist/api.d.ts +17 -15
  3. package/dist/api.js +12 -9
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/docs.js +1 -0
  7. package/dist/commands/dump.d.ts +15 -2
  8. package/dist/commands/dump.js +18 -4
  9. package/dist/commands/providers.d.ts +4 -0
  10. package/dist/commands/providers.js +52 -0
  11. package/dist/commands/render.d.ts +4 -0
  12. package/dist/commands/render.js +64 -9
  13. package/dist/commands/schemas.d.ts +2 -2
  14. package/dist/commands/schemas.js +133 -22
  15. package/dist/commands/show.d.ts +1 -0
  16. package/dist/commands/show.js +7 -1
  17. package/dist/commands/spaces.d.ts +3 -1
  18. package/dist/commands/spaces.js +11 -2
  19. package/dist/commands/validate-file.d.ts +3 -0
  20. package/dist/commands/validate-file.js +24 -6
  21. package/dist/commands/validate.d.ts +4 -0
  22. package/dist/commands/validate.js +79 -23
  23. package/dist/config.d.ts +25 -1
  24. package/dist/config.js +106 -12
  25. package/dist/filter/augment-nodes.d.ts +3 -1
  26. package/dist/filter/augment-nodes.js +12 -7
  27. package/dist/filter/filter-nodes.js +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +96 -31
  30. package/dist/judge/context.d.ts +20 -0
  31. package/dist/judge/context.js +105 -0
  32. package/dist/judge/factory.d.ts +44 -0
  33. package/dist/judge/factory.js +148 -0
  34. package/dist/judge/index.d.ts +13 -0
  35. package/dist/judge/index.js +8 -0
  36. package/dist/judge/judge-node.d.ts +27 -0
  37. package/dist/judge/judge-node.js +122 -0
  38. package/dist/judge/prompt.d.ts +10 -0
  39. package/dist/judge/prompt.js +92 -0
  40. package/dist/judge/provider/anthropic.d.ts +17 -0
  41. package/dist/judge/provider/anthropic.js +43 -0
  42. package/dist/judge/provider/fake.d.ts +26 -0
  43. package/dist/judge/provider/fake.js +56 -0
  44. package/dist/judge/provider/jev.d.ts +37 -0
  45. package/dist/judge/provider/jev.js +169 -0
  46. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  47. package/dist/judge/provider/openai-decisions.js +84 -0
  48. package/dist/judge/provider/openai.d.ts +20 -0
  49. package/dist/judge/provider/openai.js +53 -0
  50. package/dist/judge/provider/per-rule.d.ts +10 -0
  51. package/dist/judge/provider/per-rule.js +18 -0
  52. package/dist/judge/trim.d.ts +27 -0
  53. package/dist/judge/trim.js +62 -0
  54. package/dist/judge/types.d.ts +92 -0
  55. package/dist/judge/types.js +9 -0
  56. package/dist/judge/verdicts.d.ts +24 -0
  57. package/dist/judge/verdicts.js +82 -0
  58. package/dist/load-space-graph.d.ts +7 -5
  59. package/dist/load-space-graph.js +19 -8
  60. package/dist/plugins/loader.js +3 -3
  61. package/dist/plugins/markdown/extract-content-links.js +8 -29
  62. package/dist/plugins/markdown/index.js +8 -2
  63. package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
  64. package/dist/plugins/markdown/parse-embedded.js +195 -24
  65. package/dist/plugins/markdown/read-space.js +42 -14
  66. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  67. package/dist/plugins/markdown/render-directory.js +130 -0
  68. package/dist/plugins/markdown/template-sync.js +3 -4
  69. package/dist/plugins/markdown/util.d.ts +26 -5
  70. package/dist/plugins/markdown/util.js +56 -15
  71. package/dist/plugins/mermaid/render-mermaid.js +2 -1
  72. package/dist/plugins/miro/sync.js +7 -6
  73. package/dist/plugins/util.d.ts +39 -1
  74. package/dist/plugins/util.js +139 -0
  75. package/dist/read/resolve-graph-edges.d.ts +1 -1
  76. package/dist/read/resolve-graph-edges.js +46 -27
  77. package/dist/read/wikilink-utils.d.ts +17 -0
  78. package/dist/read/wikilink-utils.js +40 -0
  79. package/dist/render/render.d.ts +3 -1
  80. package/dist/render/render.js +2 -2
  81. package/dist/schema/evaluate-rule.d.ts +33 -2
  82. package/dist/schema/evaluate-rule.js +58 -4
  83. package/dist/schema/gating.d.ts +69 -0
  84. package/dist/schema/gating.js +253 -0
  85. package/dist/schema/meta-schemas.d.ts +13 -0
  86. package/dist/schema/meta-schemas.js +50 -0
  87. package/dist/schema/metadata-contract.d.ts +952 -183
  88. package/dist/schema/metadata-contract.js +177 -33
  89. package/dist/schema/rule-filter.d.ts +12 -0
  90. package/dist/schema/rule-filter.js +46 -0
  91. package/dist/schema/rule-severity.d.ts +13 -0
  92. package/dist/schema/rule-severity.js +33 -0
  93. package/dist/schema/schema.d.ts +14 -8
  94. package/dist/schema/schema.js +308 -199
  95. package/dist/schema/validate-graph.d.ts +2 -1
  96. package/dist/schema/validate-graph.js +40 -23
  97. package/dist/schema/validate-rules.d.ts +16 -3
  98. package/dist/schema/validate-rules.js +91 -29
  99. package/dist/schemas/_knowledge_wiki.json +24 -24
  100. package/dist/schemas/_okf.json +172 -0
  101. package/dist/schemas/_ost_strict.json +17 -8
  102. package/dist/schemas/_sctx_base.json +3 -3
  103. package/dist/schemas/_strategy_general.json +38 -38
  104. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  105. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  106. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  107. package/dist/schemas/knowledge_wiki.json +10 -9
  108. package/dist/schemas/okf.json +8 -0
  109. package/dist/schemas/strategy_general.json +15 -14
  110. package/dist/schemas/strict_ost.json +17 -16
  111. package/dist/space-context.d.ts +5 -2
  112. package/dist/space-context.js +9 -4
  113. package/dist/space-graph.d.ts +9 -7
  114. package/dist/space-graph.js +7 -5
  115. package/dist/testing.d.ts +33 -0
  116. package/dist/testing.js +72 -0
  117. package/dist/types.d.ts +64 -16
  118. package/dist/validate.d.ts +62 -9
  119. package/dist/validate.js +229 -114
  120. package/docs/concepts.md +65 -9
  121. package/docs/config.md +78 -1
  122. package/docs/rules.md +168 -50
  123. package/docs/schemas.md +152 -52
  124. package/package.json +17 -11
  125. package/schemas/_knowledge_wiki.json +24 -24
  126. package/schemas/_okf.json +172 -0
  127. package/schemas/_ost_strict.json +17 -8
  128. package/schemas/_sctx_base.json +3 -3
  129. package/schemas/_strategy_general.json +38 -38
  130. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  131. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  132. package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  133. package/schemas/knowledge_wiki.json +10 -9
  134. package/schemas/okf.json +8 -0
  135. package/schemas/strategy_general.json +15 -14
  136. package/schemas/strict_ost.json +17 -16
  137. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  138. package/schemas/generated/_structured_context_schema_meta.json +0 -191
@@ -21,19 +21,27 @@ function getRefs(rawField, multiple) {
21
21
  * @param nodesByType Map of node type to nodes
22
22
  * @param targetIndex Map of link targets to nodes
23
23
  * @param edge The edge definition (child type, parent type, field, fieldOn, multiple)
24
- * @param source Whether this edge comes from hierarchy.levels or relationships
24
+ * @param source Whether this edge comes from hierarchy levels or relationships
25
+ * @param hierarchy Name of the hierarchy the edge belongs to (hierarchy edges only)
25
26
  * @param selfRef Whether child and parent are the same node type
26
27
  * @param unresolvedRefs Array to push broken/invalid link entries into
27
28
  * @param typeAliases Optional type aliases for resolution
28
29
  */
29
- function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolvedRefs, typeAliases) {
30
+ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef, unresolvedRefs, typeAliases) {
30
31
  const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
31
32
  const childType = resolveNodeType(rawChildType, typeAliases);
32
33
  const parentType = resolveNodeType(rawParentType, typeAliases);
33
34
  function pushParentRef(childNode, parentTitle) {
34
35
  // Deduplicate by (field, title) — same parent via different fields is intentional
35
36
  if (!childNode.resolvedParents.some((r) => r.field === field && r.title === parentTitle)) {
36
- childNode.resolvedParents.push({ title: parentTitle, field, fieldOn, source, selfRef });
37
+ childNode.resolvedParents.push({
38
+ title: parentTitle,
39
+ field,
40
+ fieldOn,
41
+ source,
42
+ ...(hierarchy !== undefined ? { hierarchy } : {}),
43
+ selfRef,
44
+ });
37
45
  }
38
46
  }
39
47
  function checkShape(ownerNode, rawField) {
@@ -54,6 +62,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
54
62
  field,
55
63
  reason: 'invalid_shape',
56
64
  message: `Field "${field}" must be a wikilink string, got ${typeof rawField}`,
65
+ source: ownerNode.source,
57
66
  });
58
67
  return false;
59
68
  }
@@ -69,6 +78,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
69
78
  field,
70
79
  reason: 'not_found',
71
80
  message: `Link target "${target}" in field "${field}" not found`,
81
+ source: ownerNode.source,
72
82
  });
73
83
  return null;
74
84
  }
@@ -79,6 +89,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
79
89
  field,
80
90
  reason: 'ambiguous',
81
91
  message: `Link target "${target}" in field "${field}" is ambiguous (matches multiple nodes)`,
92
+ source: ownerNode.source,
82
93
  });
83
94
  return null;
84
95
  }
@@ -155,12 +166,12 @@ function classifyContentLink(link, targetIndex) {
155
166
  }
156
167
  /**
157
168
  * Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
158
- * parent links using the hierarchy levels and relationships from schema metadata.
169
+ * parent links using the levels of every hierarchy and the relationships from schema metadata.
159
170
  *
160
171
  * Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
161
172
  */
162
173
  export function resolveGraphEdges(nodes, metadata) {
163
- const levels = metadata.hierarchy?.levels ?? [];
174
+ const hierarchies = metadata.hierarchies ?? [];
164
175
  const relationships = metadata.relationships ?? [];
165
176
  const typeAliases = metadata.typeAliases;
166
177
  const unresolvedRefs = [];
@@ -180,27 +191,35 @@ export function resolveGraphEdges(nodes, metadata) {
180
191
  }
181
192
  nodesByType.get(type).push(node);
182
193
  }
183
- // 1. Process hierarchy levels
184
- for (let i = 0; i < levels.length; i++) {
185
- const level = levels[i];
186
- // Regular relationship (child type → parent type)
187
- if (i > 0) {
188
- const parentLevel = levels[i - 1];
189
- resolveEdge(nodesByType, targetIndex, {
190
- type: level.type,
191
- parent: parentLevel.type,
192
- field: level.field,
193
- fieldOn: level.fieldOn,
194
- multiple: level.multiple,
195
- }, 'hierarchy', false, unresolvedRefs, typeAliases);
196
- }
197
- // Same-type relationship (child type → same type) via primary field
198
- if (level.selfRef) {
199
- resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.field, fieldOn: level.fieldOn, multiple: level.multiple }, 'hierarchy', true, unresolvedRefs, typeAliases);
200
- }
201
- // Same-type relationship via explicit selfRefField
202
- if (level.selfRefField) {
203
- resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.selfRefField, fieldOn: 'child', multiple: false }, 'hierarchy', true, unresolvedRefs, typeAliases);
194
+ // 1. Process the levels of each hierarchy
195
+ for (const { name, levels } of hierarchies) {
196
+ for (let i = 0; i < levels.length; i++) {
197
+ const level = levels[i];
198
+ // Regular relationship (child type → parent type)
199
+ if (i > 0) {
200
+ const parentLevel = levels[i - 1];
201
+ resolveEdge(nodesByType, targetIndex, {
202
+ type: level.type,
203
+ parent: parentLevel.type,
204
+ field: level.field,
205
+ fieldOn: level.fieldOn,
206
+ multiple: level.multiple,
207
+ }, 'hierarchy', name, false, unresolvedRefs, typeAliases);
208
+ }
209
+ // Same-type relationship (child type → same type) via primary field
210
+ if (level.selfRef) {
211
+ resolveEdge(nodesByType, targetIndex, {
212
+ type: level.type,
213
+ parent: level.type,
214
+ field: level.field,
215
+ fieldOn: level.fieldOn,
216
+ multiple: level.multiple,
217
+ }, 'hierarchy', name, true, unresolvedRefs, typeAliases);
218
+ }
219
+ // Same-type relationship via explicit selfRefField
220
+ if (level.selfRefField) {
221
+ resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.selfRefField, fieldOn: 'child', multiple: false }, 'hierarchy', name, true, unresolvedRefs, typeAliases);
222
+ }
204
223
  }
205
224
  }
206
225
  // 2. Process additional relationships
@@ -212,7 +231,7 @@ export function resolveGraphEdges(nodes, metadata) {
212
231
  fieldOn: rel.fieldOn,
213
232
  multiple: rel.multiple,
214
233
  };
215
- resolveEdge(nodesByType, targetIndex, edge, 'relationship', rel.type === rel.parent, unresolvedRefs, typeAliases);
234
+ resolveEdge(nodesByType, targetIndex, edge, 'relationship', undefined, rel.type === rel.parent, unresolvedRefs, typeAliases);
216
235
  }
217
236
  // 3. Resolve content links — classify each raw ContentLink into a ResolvedContentLink.
218
237
  for (const node of spaceNodes) {
@@ -1,4 +1,21 @@
1
1
  import type { BaseNode } from '../types';
2
+ export interface ParsedWikilink {
3
+ target: string;
4
+ anchor?: string;
5
+ displayText?: string;
6
+ }
7
+ /**
8
+ * Format a wikilink string.
9
+ * formatWikilink('My Goal') -> '[[My Goal]]'
10
+ * formatWikilink('My Goal', 'Custom Display') -> '[[My Goal|Custom Display]]'
11
+ */
12
+ export declare function formatWikilink(target: string, alias?: string): string;
13
+ /**
14
+ * Parse a wikilink or link inner string into target, anchor, and displayText.
15
+ * Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
16
+ * Also handles bare forms without enclosing brackets.
17
+ */
18
+ export declare function parseWikilink(wikilink: string): ParsedWikilink;
2
19
  /**
3
20
  * Extract the lookup key from a wikilink string such as:
4
21
  * [[Personal Vision]] → "Personal Vision"
@@ -1,3 +1,43 @@
1
+ /**
2
+ * Format a wikilink string.
3
+ * formatWikilink('My Goal') -> '[[My Goal]]'
4
+ * formatWikilink('My Goal', 'Custom Display') -> '[[My Goal|Custom Display]]'
5
+ */
6
+ export function formatWikilink(target, alias) {
7
+ const trimmedTarget = target.trim();
8
+ const trimmedAlias = alias?.trim();
9
+ if (trimmedAlias && trimmedAlias !== trimmedTarget) {
10
+ return `[[${trimmedTarget}|${trimmedAlias}]]`;
11
+ }
12
+ return `[[${trimmedTarget}]]`;
13
+ }
14
+ /**
15
+ * Parse a wikilink or link inner string into target, anchor, and displayText.
16
+ * Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
17
+ * Also handles bare forms without enclosing brackets.
18
+ */
19
+ export function parseWikilink(wikilink) {
20
+ const cleaned = wikilink.replace(/^"|"$/g, '').trim();
21
+ const inner = cleaned.startsWith('[[') && cleaned.endsWith(']]') ? cleaned.slice(2, -2).trim() : cleaned;
22
+ // Alias: [[target|alias]] or [[target#anchor|alias]]
23
+ const pipeIdx = inner.indexOf('|');
24
+ let core = inner;
25
+ let displayText;
26
+ if (pipeIdx >= 0) {
27
+ core = inner.slice(0, pipeIdx);
28
+ const alias = inner.slice(pipeIdx + 1).trim();
29
+ if (alias)
30
+ displayText = alias;
31
+ }
32
+ // Anchor: [[target#anchor]] or [[target#^block]]
33
+ const hashIdx = core.indexOf('#');
34
+ if (hashIdx >= 0) {
35
+ const target = core.slice(0, hashIdx).trim();
36
+ const anchor = core.slice(hashIdx + 1).trim() || undefined;
37
+ return { target, anchor, displayText };
38
+ }
39
+ return { target: core.trim(), displayText };
40
+ }
1
41
  /**
2
42
  * Extract the lookup key from a wikilink string such as:
3
43
  * [[Personal Vision]] → "Personal Vision"
@@ -1,5 +1,7 @@
1
+ import { type RenderResult } from '../plugins/util.js';
1
2
  import type { SpaceContext } from '../types';
2
3
  export declare function executeRender(formatName: string, context: SpaceContext, options: {
3
4
  filter?: string;
5
+ hierarchy?: string;
4
6
  data?: Record<string, unknown>;
5
- }): Promise<string>;
7
+ }): Promise<RenderResult>;
@@ -1,4 +1,4 @@
1
- import { shortenPluginName } from '@/plugins/util';
1
+ import { shortenPluginName } from '../plugins/util.js';
2
2
  import { updateSpaceField } from '../config';
3
3
  import { assembleSpaceGraph } from '../load-space-graph';
4
4
  import { loadPlugins } from '../plugins/loader';
@@ -12,7 +12,7 @@ export async function executeRender(formatName, context, options) {
12
12
  const available = registry.map((r) => r.qualifiedName).join(', ');
13
13
  throw new Error(`Unknown render format: "${formatName}".${available ? ` Available: ${available}` : ' No formats registered.'}`);
14
14
  }
15
- const graph = await assembleSpaceGraph(context, { filter: options.filter });
15
+ const graph = await assembleSpaceGraph(context, { filter: options.filter, hierarchy: options.hierarchy });
16
16
  const shortName = shortenPluginName(entry.plugin.plugin.name);
17
17
  const pluginContext = {
18
18
  ...context,
@@ -1,4 +1,4 @@
1
- import type { SpaceNode } from '../types';
1
+ import type { 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) */
@@ -12,13 +12,26 @@ export interface EvalContext {
12
12
  }
13
13
  /**
14
14
  * Evaluate a JSONata expression against a context.
15
- * Returns the result of the expression (boolean for rule checks).
15
+ * Returns the result of the expression (boolean for rule checks). Throws {@link ExpressionError} if it fails.
16
16
  *
17
17
  * @param expr - JSONata expression string
18
18
  * @param context - Evaluation context with $, $$, parent, $lookup
19
19
  * @returns Result of expression evaluation
20
20
  */
21
21
  export declare function evaluateExpression(expr: string, context: EvalContext): Promise<boolean | string | number>;
22
+ /** A JSONata expression could not be compiled or evaluated. */
23
+ export declare class ExpressionError extends Error {
24
+ readonly expression: string;
25
+ constructor(expression: string, cause: unknown);
26
+ }
27
+ /** Result of evaluating a rule's `expression` or `when`: a value, or the error that stopped it (never thrown). */
28
+ export type ExpressionOutcome = {
29
+ value: boolean | string | number;
30
+ } | {
31
+ error: string;
32
+ };
33
+ /** Like {@link evaluateExpression} but reports a failure as data, so callers can turn it into a violation. */
34
+ export declare function tryEvaluateExpression(expr: string, context: EvalContext): Promise<ExpressionOutcome>;
22
35
  /**
23
36
  * Build an evaluation context for a given node.
24
37
  *
@@ -28,3 +41,21 @@ export declare function evaluateExpression(expr: string, context: EvalContext):
28
41
  * @returns Evaluation context for the node
29
42
  */
30
43
  export declare function buildEvalContext(node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<string, SpaceNode>): EvalContext;
44
+ /** The violation reported when a rule's `expression` or `when` cannot be evaluated: an authoring error, so always `error`. */
45
+ export declare function expressionErrorViolation(rule: Rule, file: string, error: string): RuleViolation;
46
+ /** The outcome of a rule's `expression` on one node. */
47
+ export type ExpressionRuleResult = {
48
+ passed: true;
49
+ }
50
+ /** `failed` is the expression returning something other than true; otherwise it could not be evaluated. */
51
+ | {
52
+ passed: false;
53
+ violation: RuleViolation;
54
+ failed: boolean;
55
+ };
56
+ /**
57
+ * Evaluate a rule's `expression` against a node (`file` empty and `node` a sentinel for a global rule). A failure
58
+ * reports the rule's severity; a rule with a semantic part names the expression in the evidence, so the
59
+ * violation is not mistaken for a verdict. An evaluation error reports `error` and is not a `failed` result.
60
+ */
61
+ export declare function evaluateRuleExpression(rule: Rule, node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<string, SpaceNode>, file?: string): Promise<ExpressionRuleResult>;
@@ -1,8 +1,9 @@
1
1
  import jsonata from 'jsonata';
2
+ import { ruleSeverity } from './rule-severity';
2
3
  const expressionCache = new Map();
3
4
  /**
4
5
  * Evaluate a JSONata expression against a context.
5
- * Returns the result of the expression (boolean for rule checks).
6
+ * Returns the result of the expression (boolean for rule checks). Throws {@link ExpressionError} if it fails.
6
7
  *
7
8
  * @param expr - JSONata expression string
8
9
  * @param context - Evaluation context with $, $$, parent, $lookup
@@ -34,9 +35,26 @@ export async function evaluateExpression(expr, context) {
34
35
  return result;
35
36
  }
36
37
  catch (error) {
37
- // Log warning and return false (fail safe)
38
- console.warn(`Warning: Error evaluating expression "${expr}":`, error);
39
- return false;
38
+ throw new ExpressionError(expr, error);
39
+ }
40
+ }
41
+ /** A JSONata expression could not be compiled or evaluated. */
42
+ export class ExpressionError extends Error {
43
+ expression;
44
+ constructor(expression, cause) {
45
+ // JSONata throws plain objects ({ code, message, position }), not Error instances.
46
+ const reason = cause?.message ?? cause;
47
+ super(`Expression "${expression}" failed: ${String(reason)}`);
48
+ this.expression = expression;
49
+ }
50
+ }
51
+ /** Like {@link evaluateExpression} but reports a failure as data, so callers can turn it into a violation. */
52
+ export async function tryEvaluateExpression(expr, context) {
53
+ try {
54
+ return { value: await evaluateExpression(expr, context) };
55
+ }
56
+ catch (error) {
57
+ return { error: error instanceof ExpressionError ? error.message : String(error) };
40
58
  }
41
59
  }
42
60
  /**
@@ -81,3 +99,39 @@ export function buildEvalContext(node, allNodes, nodeIndex) {
81
99
  parents: flattenedParents.length > 0 ? flattenedParents : undefined,
82
100
  };
83
101
  }
102
+ /** The violation reported when a rule's `expression` or `when` cannot be evaluated: an authoring error, so always `error`. */
103
+ export function expressionErrorViolation(rule, file, error) {
104
+ return {
105
+ file,
106
+ ruleId: rule.id,
107
+ category: rule.category,
108
+ severity: 'error',
109
+ description: `${rule.description} (Rule error: ${error})`,
110
+ };
111
+ }
112
+ /**
113
+ * Evaluate a rule's `expression` against a node (`file` empty and `node` a sentinel for a global rule). A failure
114
+ * reports the rule's severity; a rule with a semantic part names the expression in the evidence, so the
115
+ * violation is not mistaken for a verdict. An evaluation error reports `error` and is not a `failed` result.
116
+ */
117
+ export async function evaluateRuleExpression(rule, node, allNodes, nodeIndex, file = node.label) {
118
+ const expression = rule.expression;
119
+ const outcome = await tryEvaluateExpression(expression, buildEvalContext(node, allNodes, nodeIndex));
120
+ if ('error' in outcome) {
121
+ return { passed: false, failed: false, violation: expressionErrorViolation(rule, file, outcome.error) };
122
+ }
123
+ if (outcome.value === true)
124
+ return { passed: true };
125
+ return {
126
+ passed: false,
127
+ failed: true,
128
+ violation: {
129
+ file,
130
+ ruleId: rule.id,
131
+ category: rule.category,
132
+ severity: ruleSeverity(rule),
133
+ description: rule.description,
134
+ ...(rule.semantic !== undefined && { evidence: `expression failed: ${expression}` }),
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,69 @@
1
+ import type { TrimReport } from '../judge/trim';
2
+ import type { SemanticJudgeProvider } from '../judge/types';
3
+ import type { SpaceGraph } from '../space-graph';
4
+ import type { Rule, RuleSeverity, RuleViolation, SpaceNode } from '../types';
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.
8
+ */
9
+ export declare function ruleGatesDescendants(rule: Rule, severity?: RuleSeverity): boolean;
10
+ /**
11
+ * A node-level failure that seeds cascade gating: a deterministic, always-run check
12
+ * (schema validation or hierarchy structure) that failed on a specific node.
13
+ */
14
+ export type GatingSeed = {
15
+ /** Label (file) of the failing node. */
16
+ file: string;
17
+ /** Title of the failing node. */
18
+ title: string;
19
+ /** Which always-run layer produced the failure. */
20
+ reason: 'schema' | 'hierarchy';
21
+ };
22
+ /**
23
+ * Records one cascade: a node whose failure or blocking rule violation suppressed
24
+ * rule evaluation for itself (if schema/hierarchy caused it) and/or its hierarchy subtree.
25
+ */
26
+ export type CoverageGate = {
27
+ /** Label (file) of the node whose failure triggered this gate. */
28
+ file: string;
29
+ /** Title of the blocking node. */
30
+ nodeTitle: string;
31
+ /** What kind of check triggered the gate. */
32
+ reason: 'schema' | 'hierarchy' | 'rule';
33
+ /** Rule id responsible, when `reason` is `'rule'`. */
34
+ ruleId?: string;
35
+ /** Labels of nodes whose rule evaluation was suppressed as a result of this gate. */
36
+ gatedFiles: string[];
37
+ };
38
+ export type GatedRuleEvaluation = {
39
+ violations: RuleViolation[];
40
+ gates: CoverageGate[];
41
+ };
42
+ export interface GatingOptions {
43
+ trimReport?: TrimReport;
44
+ /** Evaluate rules on gated nodes anyway (gates are still reported). */
45
+ includeGated?: boolean;
46
+ /** Evaluates `semantic` rules. Without it, a rule's semantic part is not evaluated. */
47
+ provider?: SemanticJudgeProvider;
48
+ /**
49
+ * Label of the one node whose result is wanted (`validate-file`). Gating is still computed over the
50
+ * whole space, but the judge is only consulted for that node and for its hierarchy ancestors' gate-capable
51
+ * semantic rules (top-down, so a gated ancestor stops the walk). Other nodes' semantic rules are not evaluated.
52
+ */
53
+ judgeTarget?: string;
54
+ }
55
+ /**
56
+ * Compute breadth-first depth (1-based, from hierarchy roots and orphans) for every
57
+ * hierarchy-typed node in the graph. Non-hierarchy nodes are not included.
58
+ */
59
+ export declare function computeHierarchyDepths(graph: SpaceGraph): Map<string, number>;
60
+ /**
61
+ * Evaluate executable rules with cascade gating: a schema, hierarchy, or blocking-rule
62
+ * failure on a node suppresses rule evaluation for that node (when the failure is
63
+ * schema/hierarchy) and its hierarchy subtree, unless `includeGated` is set.
64
+ *
65
+ * Traversal is breadth-first from hierarchy roots and orphans. Non-hierarchy nodes
66
+ * (not part of the hierarchy DAG) are always evaluated — cascade gating only applies
67
+ * within hierarchy subtrees. Global rules always run, unaffected by gating.
68
+ */
69
+ export declare function evaluateRulesWithGating(nodes: SpaceNode[], rules: Rule[], graph: SpaceGraph, seeds: GatingSeed[], options?: GatingOptions): Promise<GatedRuleEvaluation>;