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.
- package/README.md +29 -216
- package/dist/commands/docs.js +1 -0
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/schemas.js +21 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +17 -2
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +42 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +23 -0
- package/dist/index.js +44 -3
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +2 -0
- package/dist/plugins/markdown/parse-embedded.js +161 -24
- package/dist/plugins/markdown/read-space.js +4 -9
- package/dist/plugins/markdown/render-directory.js +4 -4
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +25 -4
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/render/render.js +1 -1
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/metadata-contract.d.ts +625 -12
- package/dist/schema/metadata-contract.js +110 -4
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.js +31 -1
- package/dist/schema/validate-rules.d.ts +17 -8
- package/dist/schema/validate-rules.js +91 -48
- package/dist/schemas/_knowledge_wiki.json +4 -4
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +15 -6
- package/dist/schemas/_sctx_base.json +1 -1
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
- package/dist/schemas/okf.json +8 -0
- package/dist/types.d.ts +8 -1
- package/dist/validate.d.ts +58 -8
- package/dist/validate.js +219 -114
- package/docs/concepts.md +52 -6
- package/docs/config.md +70 -1
- package/docs/rules.md +140 -49
- package/docs/schemas.md +43 -18
- package/package.json +10 -9
- package/schemas/_knowledge_wiki.json +4 -4
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +15 -6
- package/schemas/_sctx_base.json +1 -1
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
- 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,
|
|
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 =
|
|
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 {
|
|
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
|
|
115
|
+
const sortedData = sortFrontmatterKeys(frontmatterData);
|
|
116
116
|
const body = (node.content ?? '').trim();
|
|
117
117
|
let fileContent;
|
|
118
|
-
if (Object.keys(
|
|
119
|
-
fileContent = matter.stringify(body ? `\n${body}\n` : '',
|
|
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 =
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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).
|
package/dist/render/render.js
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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>;
|