structured-context 0.12.0 → 0.13.1

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 (83) hide show
  1. package/README.md +29 -216
  2. package/dist/commands/docs.js +1 -0
  3. package/dist/commands/providers.d.ts +4 -0
  4. package/dist/commands/providers.js +52 -0
  5. package/dist/commands/schemas.js +21 -2
  6. package/dist/commands/validate-file.d.ts +3 -0
  7. package/dist/commands/validate-file.js +17 -2
  8. package/dist/commands/validate.d.ts +4 -0
  9. package/dist/commands/validate.js +42 -3
  10. package/dist/config.d.ts +7 -0
  11. package/dist/config.js +23 -0
  12. package/dist/index.js +44 -3
  13. package/dist/judge/context.d.ts +20 -0
  14. package/dist/judge/context.js +105 -0
  15. package/dist/judge/factory.d.ts +44 -0
  16. package/dist/judge/factory.js +148 -0
  17. package/dist/judge/index.d.ts +13 -0
  18. package/dist/judge/index.js +8 -0
  19. package/dist/judge/judge-node.d.ts +27 -0
  20. package/dist/judge/judge-node.js +122 -0
  21. package/dist/judge/prompt.d.ts +10 -0
  22. package/dist/judge/prompt.js +92 -0
  23. package/dist/judge/provider/anthropic.d.ts +17 -0
  24. package/dist/judge/provider/anthropic.js +43 -0
  25. package/dist/judge/provider/fake.d.ts +26 -0
  26. package/dist/judge/provider/fake.js +56 -0
  27. package/dist/judge/provider/jev.d.ts +37 -0
  28. package/dist/judge/provider/jev.js +169 -0
  29. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  30. package/dist/judge/provider/openai-decisions.js +84 -0
  31. package/dist/judge/provider/openai.d.ts +20 -0
  32. package/dist/judge/provider/openai.js +53 -0
  33. package/dist/judge/provider/per-rule.d.ts +10 -0
  34. package/dist/judge/provider/per-rule.js +18 -0
  35. package/dist/judge/trim.d.ts +27 -0
  36. package/dist/judge/trim.js +62 -0
  37. package/dist/judge/types.d.ts +92 -0
  38. package/dist/judge/types.js +9 -0
  39. package/dist/judge/verdicts.d.ts +24 -0
  40. package/dist/judge/verdicts.js +82 -0
  41. package/dist/plugins/markdown/parse-embedded.d.ts +2 -0
  42. package/dist/plugins/markdown/parse-embedded.js +161 -24
  43. package/dist/plugins/markdown/read-space.js +4 -9
  44. package/dist/plugins/markdown/render-directory.js +4 -4
  45. package/dist/plugins/markdown/template-sync.js +3 -4
  46. package/dist/plugins/markdown/util.d.ts +25 -4
  47. package/dist/plugins/markdown/util.js +56 -15
  48. package/dist/render/render.js +1 -1
  49. package/dist/schema/evaluate-rule.d.ts +33 -2
  50. package/dist/schema/evaluate-rule.js +58 -4
  51. package/dist/schema/gating.d.ts +69 -0
  52. package/dist/schema/gating.js +253 -0
  53. package/dist/schema/metadata-contract.d.ts +625 -12
  54. package/dist/schema/metadata-contract.js +110 -4
  55. package/dist/schema/rule-filter.d.ts +12 -0
  56. package/dist/schema/rule-filter.js +46 -0
  57. package/dist/schema/rule-severity.d.ts +13 -0
  58. package/dist/schema/rule-severity.js +33 -0
  59. package/dist/schema/schema.js +31 -1
  60. package/dist/schema/validate-rules.d.ts +17 -8
  61. package/dist/schema/validate-rules.js +91 -48
  62. package/dist/schemas/_knowledge_wiki.json +4 -4
  63. package/dist/schemas/_okf.json +172 -0
  64. package/dist/schemas/_ost_strict.json +15 -6
  65. package/dist/schemas/_sctx_base.json +1 -1
  66. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  67. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
  68. package/dist/schemas/okf.json +8 -0
  69. package/dist/types.d.ts +8 -1
  70. package/dist/validate.d.ts +58 -8
  71. package/dist/validate.js +219 -114
  72. package/docs/concepts.md +52 -6
  73. package/docs/config.md +70 -1
  74. package/docs/rules.md +140 -49
  75. package/docs/schemas.md +43 -18
  76. package/package.json +10 -9
  77. package/schemas/_knowledge_wiki.json +4 -4
  78. package/schemas/_okf.json +172 -0
  79. package/schemas/_ost_strict.json +15 -6
  80. package/schemas/_sctx_base.json +1 -1
  81. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  82. package/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
  83. package/schemas/okf.json +8 -0
@@ -5,12 +5,12 @@ import matter from 'gray-matter';
5
5
  import { extractSchemaTypeNames } from '../../schema/schema';
6
6
  import { extractLinksFromBody, extractLinksFromFrontmatter, getEdgeFieldNames } from './extract-content-links';
7
7
  import { extractEmbeddedNodes, ON_A_PAGE_TYPES } from './parse-embedded';
8
- import { applyFieldMap, coerceDates, inferTypeFromPath } from './util';
8
+ import { applyFieldMap, inferTypeFromPath, MATTER_OPTIONS } from './util';
9
9
  export function readSpaceOnAPage(context) {
10
10
  const { space, resolvedSchemaPath, schema: { metadata }, } = context;
11
11
  const filePath = resolve(space.path);
12
12
  const raw = readFileSync(filePath, 'utf-8');
13
- const { data: frontmatter, content: body } = matter(raw);
13
+ const { data: frontmatter, content: body } = matter(raw, MATTER_OPTIONS);
14
14
  const pageType = frontmatter.type;
15
15
  if (pageType !== undefined && !ON_A_PAGE_TYPES.includes(pageType)) {
16
16
  throw new Error(`Expected a space_on_a_page file but got type "${pageType}" in ${filePath}. ` +
@@ -60,14 +60,9 @@ export async function readSpaceDirectory(context, options) {
60
60
  const content = readFileSync(join(directory, file), 'utf-8');
61
61
  let parsed;
62
62
  try {
63
- parsed = matter(content);
63
+ parsed = matter(content, MATTER_OPTIONS);
64
64
  }
65
65
  catch (err) {
66
- // gray-matter caches parsed YAML and has a known bug where a caught exception
67
- // corrupts its internal cache, causing subsequent parses to silently return {}.
68
- // Clear the cache after any parse error to avoid stale state.
69
- // See: https://github.com/jonschlinkert/gray-matter/issues/166
70
- matter.clearCache();
71
66
  const yamlErr = err;
72
67
  const line = yamlErr.mark ? yamlErr.mark.line + 1 : undefined;
73
68
  const column = yamlErr.mark ? yamlErr.mark.column + 1 : undefined;
@@ -91,7 +86,7 @@ export async function readSpaceDirectory(context, options) {
91
86
  });
92
87
  continue;
93
88
  }
94
- const data = coerceDates(applyFieldMap(parsed.data, fieldMap));
89
+ const data = applyFieldMap(parsed.data, fieldMap);
95
90
  if (!data.type && typeInferenceCfg && knownTypes) {
96
91
  data.type = inferTypeFromPath(file, typeInferenceCfg, knownTypes, context.schema.metadata.typeAliases);
97
92
  }
@@ -1,7 +1,7 @@
1
1
  import { posix } from 'node:path';
2
2
  import matter from 'gray-matter';
3
3
  import { getMarkdownConfig } from './index';
4
- import { coerceDates, invertFieldMap } from './util';
4
+ import { invertFieldMap, MATTER_OPTIONS } from './util';
5
5
  /**
6
6
  * Sanitize a node title into a valid filesystem filename.
7
7
  * Strips Obsidian/OS forbidden characters: \ / : * ? " < > | # ^ [ ]
@@ -112,11 +112,11 @@ export function renderMarkdownDirectory(context, graph, options) {
112
112
  const typeKey = reverseFieldMap.type ?? 'type';
113
113
  frontmatterData[typeKey] = nodeType;
114
114
  }
115
- const coercedData = coerceDates(sortFrontmatterKeys(frontmatterData));
115
+ const sortedData = sortFrontmatterKeys(frontmatterData);
116
116
  const body = (node.content ?? '').trim();
117
117
  let fileContent;
118
- if (Object.keys(coercedData).length > 0) {
119
- fileContent = matter.stringify(body ? `\n${body}\n` : '', coercedData);
118
+ if (Object.keys(sortedData).length > 0) {
119
+ fileContent = matter.stringify(body ? `\n${body}\n` : '', sortedData, MATTER_OPTIONS);
120
120
  }
121
121
  else {
122
122
  fileContent = body ? `${body}\n` : '';
@@ -2,9 +2,8 @@ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import fg from 'fast-glob';
4
4
  import matter from 'gray-matter';
5
- import yaml from 'js-yaml';
6
5
  import { mergeVariantProperties, resolveRef } from '../../schema/schema-refs';
7
- import { invertFieldMap } from './util';
6
+ import { dumpYaml, invertFieldMap, MATTER_OPTIONS } from './util';
8
7
  // Fields derived from the filesystem — present at validation time but not written to frontmatter
9
8
  const DERIVED_FIELDS = new Set(['title', 'content']);
10
9
  function enumPlaceholder(def) {
@@ -76,7 +75,7 @@ export function generateNewContent(nodeType, variant, schema, schemaRefRegistry,
76
75
  const toCanonicalKey = (k) => fieldMap[k] ?? k;
77
76
  const exampleWithPlaceholders = withEnumPlaceholders(example, properties, schema, schemaRefRegistry);
78
77
  const remappedExample = Object.fromEntries(Object.entries(exampleWithPlaceholders).map(([k, v]) => [toFileKey(k), v]));
79
- let frontmatterYaml = yaml.dump(remappedExample, { lineWidth: -1 }).trim();
78
+ let frontmatterYaml = dumpYaml(remappedExample, { lineWidth: -1 }).trim();
80
79
  // Append property descriptions as comments
81
80
  // YAML lines use file field names; look up properties using canonical key.
82
81
  const lines = frontmatterYaml.split('\n');
@@ -206,7 +205,7 @@ export async function templateSync(context, options) {
206
205
  continue;
207
206
  }
208
207
  const body = content.slice(fmMatch[0].length);
209
- const parsed = matter(content);
208
+ const parsed = matter(content, MATTER_OPTIONS);
210
209
  const nodeType = parsed.data.type;
211
210
  if (!nodeType) {
212
211
  continue;
@@ -1,11 +1,32 @@
1
+ import { type DumpOptions, type Schema } from 'js-yaml';
1
2
  import type { TypeInferenceConfig } from '.';
2
3
  export declare function inferTypeFromPath(filePath: string, config: TypeInferenceConfig, knownTypes: Set<string>, typeAliases: Record<string, string> | undefined): string | undefined;
3
4
  /**
4
- * Coerce Date objects in frontmatter/YAML data to ISO date strings (YYYY-MM-DD).
5
- * gray-matter and js-yaml parse unquoted ISO dates (e.g. `date: 2026-03-31`) as
6
- * JavaScript Date objects, which are not valid JSON and fail string type validation.
5
+ * YAML schema for frontmatter and embedded YAML blocks: YAML 1.2 Core schema extended with
6
+ * YAML 1.1 merge keys (`<<`), collections (binary, omap, pairs, set), and custom Date scalar
7
+ * handling that avoids js-yaml's timestamp type. Dates and datetimes stay as the strings
8
+ * written (e.g. `2026-03-31`, `2026-06-20T22:53:05+10:00`) rather than becoming Date objects,
9
+ * which lose precision and offset and are not valid JSON.
7
10
  */
8
- export declare function coerceDates(data: Record<string, unknown>): Record<string, unknown>;
11
+ export declare const YAML_SCHEMA: Schema;
12
+ export declare function parseYaml(text: string): unknown;
13
+ export type DumpYamlOptions = DumpOptions & {
14
+ replacer?: (key: string, value: unknown) => unknown;
15
+ };
16
+ /** Dumps with YAML_SCHEMA. Date values (e.g. from non-markdown plugins) are written as ISO 8601 strings. */
17
+ export declare function dumpYaml(data: unknown, options?: DumpYamlOptions): string;
18
+ /**
19
+ * gray-matter options using YAML_SCHEMA. Passing options also bypasses gray-matter's parse cache,
20
+ * which is corrupted by a caught parse error (https://github.com/jonschlinkert/gray-matter/issues/166).
21
+ */
22
+ export declare const MATTER_OPTIONS: {
23
+ engines: {
24
+ yaml: {
25
+ parse: (text: string) => object;
26
+ stringify: (data: object) => string;
27
+ };
28
+ };
29
+ };
9
30
  /**
10
31
  * Apply field remapping to a data object.
11
32
  * Renames keys according to fieldMap (file field name → canonical field name).
@@ -1,4 +1,5 @@
1
1
  import { posix } from 'node:path';
2
+ import { binaryTag, CORE_SCHEMA, defineScalarTag, dump, load, mergeTag, omapTag, pairsTag, setTag, visit, } from 'js-yaml';
2
3
  export function inferTypeFromPath(filePath, config, knownTypes, typeAliases) {
3
4
  if (config.mode === 'off')
4
5
  return undefined;
@@ -37,26 +38,66 @@ export function inferTypeFromPath(filePath, config, knownTypes, typeAliases) {
37
38
  }
38
39
  return undefined;
39
40
  }
41
+ const dateTag = defineScalarTag('tag:yaml.org,2002:timestamp', {
42
+ identify: (data) => data instanceof Date,
43
+ represent: (data) => data.toISOString(),
44
+ resolve: (source) => source,
45
+ });
40
46
  /**
41
- * Coerce Date objects in frontmatter/YAML data to ISO date strings (YYYY-MM-DD).
42
- * gray-matter and js-yaml parse unquoted ISO dates (e.g. `date: 2026-03-31`) as
43
- * JavaScript Date objects, which are not valid JSON and fail string type validation.
47
+ * YAML schema for frontmatter and embedded YAML blocks: YAML 1.2 Core schema extended with
48
+ * YAML 1.1 merge keys (`<<`), collections (binary, omap, pairs, set), and custom Date scalar
49
+ * handling that avoids js-yaml's timestamp type. Dates and datetimes stay as the strings
50
+ * written (e.g. `2026-03-31`, `2026-06-20T22:53:05+10:00`) rather than becoming Date objects,
51
+ * which lose precision and offset and are not valid JSON.
44
52
  */
45
- export function coerceDates(data) {
46
- const result = {};
47
- for (const [key, value] of Object.entries(data)) {
48
- if (value instanceof Date) {
49
- result[key] = value.toISOString().slice(0, 10);
50
- }
51
- else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
52
- result[key] = coerceDates(value);
53
- }
54
- else {
55
- result[key] = value;
53
+ export const YAML_SCHEMA = CORE_SCHEMA.withTags(mergeTag, binaryTag, omapTag, pairsTag, setTag, dateTag);
54
+ export function parseYaml(text) {
55
+ return load(text, { schema: YAML_SCHEMA });
56
+ }
57
+ function applyReplacer(key, value, replacer) {
58
+ const replaced = replacer(key, value);
59
+ if (Array.isArray(replaced)) {
60
+ return replaced.map((item, index) => applyReplacer(String(index), item, replacer));
61
+ }
62
+ if (replaced !== null && typeof replaced === 'object' && Object.getPrototypeOf(replaced) === Object.prototype) {
63
+ const res = {};
64
+ for (const [k, v] of Object.entries(replaced)) {
65
+ const vReplaced = applyReplacer(k, v, replacer);
66
+ if (vReplaced !== undefined) {
67
+ res[k] = vReplaced;
68
+ }
56
69
  }
70
+ return res;
57
71
  }
58
- return result;
72
+ return replaced;
73
+ }
74
+ /** Dumps with YAML_SCHEMA. Date values (e.g. from non-markdown plugins) are written as ISO 8601 strings. */
75
+ export function dumpYaml(data, options) {
76
+ const input = options?.replacer ? applyReplacer('', data, options.replacer) : data;
77
+ const { replacer: _ignored, transform, ...dumpOpts } = options ?? {};
78
+ return dump(input, {
79
+ ...dumpOpts,
80
+ schema: YAML_SCHEMA,
81
+ transform: (docs) => {
82
+ visit(docs, (node) => {
83
+ if (node.kind === 'scalar' && (node.tag === '!!timestamp' || node.tag === 'tag:yaml.org,2002:timestamp')) {
84
+ node.tagged = false;
85
+ node.tag = 'tag:yaml.org,2002:str';
86
+ }
87
+ });
88
+ transform?.(docs);
89
+ },
90
+ });
59
91
  }
92
+ /**
93
+ * gray-matter options using YAML_SCHEMA. Passing options also bypasses gray-matter's parse cache,
94
+ * which is corrupted by a caught parse error (https://github.com/jonschlinkert/gray-matter/issues/166).
95
+ */
96
+ export const MATTER_OPTIONS = {
97
+ engines: {
98
+ yaml: { parse: (text) => parseYaml(text), stringify: (data) => dumpYaml(data) },
99
+ },
100
+ };
60
101
  /**
61
102
  * Apply field remapping to a data object.
62
103
  * Renames keys according to fieldMap (file field name → canonical field name).
@@ -1,7 +1,7 @@
1
- import { shortenPluginName } from '../plugins/util';
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 { 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>;