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.
- package/README.md +52 -198
- package/dist/api.d.ts +17 -15
- package/dist/api.js +12 -9
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/docs.js +1 -0
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +133 -22
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +24 -6
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +79 -23
- package/dist/config.d.ts +25 -1
- package/dist/config.js +106 -12
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -31
- 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/load-space-graph.d.ts +7 -5
- package/dist/load-space-graph.js +19 -8
- package/dist/plugins/loader.js +3 -3
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
- package/dist/plugins/markdown/parse-embedded.js +195 -24
- package/dist/plugins/markdown/read-space.js +42 -14
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- 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/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +952 -183
- package/dist/schema/metadata-contract.js +177 -33
- 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.d.ts +14 -8
- package/dist/schema/schema.js +308 -199
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +16 -3
- package/dist/schema/validate-rules.js +91 -29
- package/dist/schemas/_knowledge_wiki.json +24 -24
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +17 -8
- package/dist/schemas/_sctx_base.json +3 -3
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/okf.json +8 -0
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +5 -2
- package/dist/space-context.js +9 -4
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +64 -16
- package/dist/validate.d.ts +62 -9
- package/dist/validate.js +229 -114
- package/docs/concepts.md +65 -9
- package/docs/config.md +78 -1
- package/docs/rules.md +168 -50
- package/docs/schemas.md +152 -52
- package/package.json +17 -11
- package/schemas/_knowledge_wiki.json +24 -24
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +17 -8
- package/schemas/_sctx_base.json +3 -3
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/okf.json +8 -0
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- 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
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
184
|
-
for (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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"
|
package/dist/render/render.d.ts
CHANGED
|
@@ -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<
|
|
7
|
+
}): Promise<RenderResult>;
|
package/dist/render/render.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shortenPluginName } from '
|
|
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
|
-
|
|
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>;
|