structured-context 0.12.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 (94) hide show
  1. package/README.md +29 -216
  2. package/dist/api.d.ts +17 -17
  3. package/dist/api.js +12 -12
  4. package/dist/commands/docs.js +1 -0
  5. package/dist/commands/providers.d.ts +4 -0
  6. package/dist/commands/providers.js +52 -0
  7. package/dist/commands/schemas.js +21 -2
  8. package/dist/commands/validate-file.d.ts +3 -0
  9. package/dist/commands/validate-file.js +17 -2
  10. package/dist/commands/validate.d.ts +4 -0
  11. package/dist/commands/validate.js +42 -3
  12. package/dist/config.d.ts +7 -0
  13. package/dist/config.js +26 -3
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +59 -18
  16. package/dist/judge/context.d.ts +20 -0
  17. package/dist/judge/context.js +105 -0
  18. package/dist/judge/factory.d.ts +44 -0
  19. package/dist/judge/factory.js +148 -0
  20. package/dist/judge/index.d.ts +13 -0
  21. package/dist/judge/index.js +8 -0
  22. package/dist/judge/judge-node.d.ts +27 -0
  23. package/dist/judge/judge-node.js +122 -0
  24. package/dist/judge/prompt.d.ts +10 -0
  25. package/dist/judge/prompt.js +92 -0
  26. package/dist/judge/provider/anthropic.d.ts +17 -0
  27. package/dist/judge/provider/anthropic.js +43 -0
  28. package/dist/judge/provider/fake.d.ts +26 -0
  29. package/dist/judge/provider/fake.js +56 -0
  30. package/dist/judge/provider/jev.d.ts +37 -0
  31. package/dist/judge/provider/jev.js +169 -0
  32. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  33. package/dist/judge/provider/openai-decisions.js +84 -0
  34. package/dist/judge/provider/openai.d.ts +20 -0
  35. package/dist/judge/provider/openai.js +53 -0
  36. package/dist/judge/provider/per-rule.d.ts +10 -0
  37. package/dist/judge/provider/per-rule.js +18 -0
  38. package/dist/judge/trim.d.ts +27 -0
  39. package/dist/judge/trim.js +62 -0
  40. package/dist/judge/types.d.ts +92 -0
  41. package/dist/judge/types.js +9 -0
  42. package/dist/judge/verdicts.d.ts +24 -0
  43. package/dist/judge/verdicts.js +82 -0
  44. package/dist/load-space-graph.d.ts +3 -3
  45. package/dist/load-space-graph.js +4 -4
  46. package/dist/plugins/loader.js +1 -1
  47. package/dist/plugins/markdown/parse-embedded.d.ts +2 -0
  48. package/dist/plugins/markdown/parse-embedded.js +161 -24
  49. package/dist/plugins/markdown/read-space.js +4 -9
  50. package/dist/plugins/markdown/render-directory.js +5 -5
  51. package/dist/plugins/markdown/template-sync.js +3 -4
  52. package/dist/plugins/markdown/util.d.ts +26 -5
  53. package/dist/plugins/markdown/util.js +56 -15
  54. package/dist/render/render.d.ts +1 -1
  55. package/dist/render/render.js +1 -1
  56. package/dist/schema/evaluate-rule.d.ts +33 -2
  57. package/dist/schema/evaluate-rule.js +58 -4
  58. package/dist/schema/gating.d.ts +69 -0
  59. package/dist/schema/gating.js +253 -0
  60. package/dist/schema/metadata-contract.d.ts +625 -12
  61. package/dist/schema/metadata-contract.js +110 -4
  62. package/dist/schema/rule-filter.d.ts +12 -0
  63. package/dist/schema/rule-filter.js +46 -0
  64. package/dist/schema/rule-severity.d.ts +13 -0
  65. package/dist/schema/rule-severity.js +33 -0
  66. package/dist/schema/schema.js +31 -1
  67. package/dist/schema/validate-rules.d.ts +17 -8
  68. package/dist/schema/validate-rules.js +91 -48
  69. package/dist/schemas/_knowledge_wiki.json +4 -4
  70. package/dist/schemas/_okf.json +172 -0
  71. package/dist/schemas/_ost_strict.json +15 -6
  72. package/dist/schemas/_sctx_base.json +1 -1
  73. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  74. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
  75. package/dist/schemas/okf.json +8 -0
  76. package/dist/space-context.d.ts +2 -2
  77. package/dist/space-context.js +3 -3
  78. package/dist/space-graph.d.ts +1 -1
  79. package/dist/testing.d.ts +1 -1
  80. package/dist/types.d.ts +11 -4
  81. package/dist/validate.d.ts +59 -9
  82. package/dist/validate.js +225 -120
  83. package/docs/concepts.md +52 -6
  84. package/docs/config.md +70 -1
  85. package/docs/rules.md +140 -49
  86. package/docs/schemas.md +43 -18
  87. package/package.json +6 -5
  88. package/schemas/_knowledge_wiki.json +4 -4
  89. package/schemas/_okf.json +172 -0
  90. package/schemas/_ost_strict.json +15 -6
  91. package/schemas/_sctx_base.json +1 -1
  92. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  93. package/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
  94. package/schemas/okf.json +8 -0
@@ -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>;
@@ -0,0 +1,253 @@
1
+ import { judgeNodes } from '../judge/judge-node';
2
+ import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
3
+ import { ruleSeverities, ruleSeverity } from './rule-severity';
4
+ /**
5
+ * Whether a failure of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
6
+ * `severity` is that of the failure: the rule's own for an expression, the verdict's for a semantic part.
7
+ */
8
+ export function ruleGatesDescendants(rule, severity = ruleSeverity(rule)) {
9
+ return rule.gatesDescendants ?? severity === 'error';
10
+ }
11
+ /** Whether any violation of this rule could gate its subtree. */
12
+ function ruleCanGate(rule) {
13
+ return rule.gatesDescendants ?? ruleSeverities(rule).includes('error');
14
+ }
15
+ /**
16
+ * Compute breadth-first depth (1-based, from hierarchy roots and orphans) for every
17
+ * hierarchy-typed node in the graph. Non-hierarchy nodes are not included.
18
+ */
19
+ export function computeHierarchyDepths(graph) {
20
+ const depths = new Map();
21
+ const queue = [];
22
+ for (const node of [...graph.hierarchyRoots, ...graph.orphans]) {
23
+ queue.push({ title: node.title, depth: 1 });
24
+ }
25
+ let i = 0;
26
+ while (i < queue.length) {
27
+ const { title, depth } = queue[i++];
28
+ if (depths.has(title))
29
+ continue;
30
+ depths.set(title, depth);
31
+ for (const child of graph.hierarchyChildren.get(title) ?? []) {
32
+ if (!depths.has(child.title))
33
+ queue.push({ title: child.title, depth: depth + 1 });
34
+ }
35
+ }
36
+ return depths;
37
+ }
38
+ /**
39
+ * Evaluate executable rules with cascade gating: a schema, hierarchy, or blocking-rule
40
+ * failure on a node suppresses rule evaluation for that node (when the failure is
41
+ * schema/hierarchy) and its hierarchy subtree, unless `includeGated` is set.
42
+ *
43
+ * Traversal is breadth-first from hierarchy roots and orphans. Non-hierarchy nodes
44
+ * (not part of the hierarchy DAG) are always evaluated — cascade gating only applies
45
+ * within hierarchy subtrees. Global rules always run, unaffected by gating.
46
+ */
47
+ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, options = {}) {
48
+ const includeGated = options.includeGated ?? false;
49
+ const nodeIndex = new Map();
50
+ for (const node of nodes) {
51
+ if (node.title)
52
+ nodeIndex.set(node.title, node);
53
+ }
54
+ // When a node has both a schema and hierarchy failure, report schema as the reason.
55
+ const seedByTitle = new Map();
56
+ for (const seed of seeds) {
57
+ if (!seedByTitle.has(seed.title) || seed.reason === 'schema') {
58
+ seedByTitle.set(seed.title, seed);
59
+ }
60
+ }
61
+ const judge = options.provider;
62
+ const violations = [];
63
+ const gateOrigins = new Map();
64
+ function originFor(title, seed) {
65
+ let origin = gateOrigins.get(title);
66
+ if (!origin) {
67
+ origin = { file: seed.file, nodeTitle: title, reason: seed.reason, ruleId: seed.ruleId, gatedFiles: [] };
68
+ gateOrigins.set(title, origin);
69
+ }
70
+ return origin;
71
+ }
72
+ const globalRules = rules.filter((r) => r.global && r.expression !== undefined);
73
+ const sentinel = nodes[0];
74
+ if (sentinel) {
75
+ for (const rule of globalRules) {
76
+ const result = await evaluateRuleExpression(rule, sentinel, nodes, nodeIndex, '');
77
+ if (!result.passed)
78
+ violations.push(result.violation);
79
+ }
80
+ }
81
+ const perNodeRules = rules.filter((r) => !r.global && (r.expression !== undefined || (r.semantic !== undefined && judge)));
82
+ // With a judge target, only the target and its hierarchy ancestors can need judging.
83
+ const targetAncestors = new Set();
84
+ if (options.judgeTarget !== undefined) {
85
+ const parentsOf = new Map();
86
+ for (const [parent, children] of graph.hierarchyChildren) {
87
+ for (const child of children)
88
+ parentsOf.set(child.title, [...(parentsOf.get(child.title) ?? []), parent]);
89
+ }
90
+ const target = nodes.find((n) => n.label === options.judgeTarget);
91
+ const pending = target ? [target.title] : [];
92
+ while (pending.length > 0) {
93
+ for (const parent of parentsOf.get(pending.pop()) ?? []) {
94
+ if (!targetAncestors.has(parent)) {
95
+ targetAncestors.add(parent);
96
+ pending.push(parent);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ /** Rules that apply to this node: matching `type`, passing their `when` test, and (for a judge target) worth judging. */
102
+ async function applicableRules(node, onWhenError) {
103
+ const applicable = [];
104
+ for (const r of perNodeRules) {
105
+ if (r.type && r.type !== node.resolvedType)
106
+ continue;
107
+ if (judge &&
108
+ r.semantic !== undefined &&
109
+ options.judgeTarget !== undefined &&
110
+ node.label !== options.judgeTarget) {
111
+ // Ancestors only matter through the gates they could raise.
112
+ if (!(targetAncestors.has(node.title) && ruleCanGate(r)))
113
+ continue;
114
+ }
115
+ if (r.when) {
116
+ const outcome = await tryEvaluateExpression(r.when, buildEvalContext(node, nodes, nodeIndex));
117
+ if ('error' in outcome) {
118
+ onWhenError?.(r, outcome.error);
119
+ continue;
120
+ }
121
+ if (outcome.value !== true)
122
+ continue;
123
+ }
124
+ applicable.push(r);
125
+ }
126
+ return applicable;
127
+ }
128
+ async function prepareNode(node) {
129
+ const local = [];
130
+ const prepared = { violations: local, semantic: [], judged: () => { } };
131
+ const applicable = await applicableRules(node, (rule, error) => local.push(expressionErrorViolation(rule, node.label, error)));
132
+ function report(rule, detail = {}) {
133
+ const severity = detail.severity ?? ruleSeverities(rule)[0] ?? ruleSeverity(rule);
134
+ local.push({
135
+ file: node.label,
136
+ ruleId: rule.id,
137
+ category: rule.category,
138
+ severity,
139
+ description: detail.error
140
+ ? `${rule.description} (Semantic evaluation error: ${detail.error})`
141
+ : rule.description,
142
+ ...(detail.evidence !== undefined && { evidence: detail.evidence }),
143
+ ...(detail.verdict !== undefined && { verdict: detail.verdict }),
144
+ ...(detail.guidance !== undefined && { guidance: detail.guidance }),
145
+ });
146
+ // An evaluation error says nothing about the node, so it never gates.
147
+ if (!detail.error && ruleGatesDescendants(rule, severity) && !prepared.blockingRuleId) {
148
+ prepared.blockingRuleId = rule.id;
149
+ }
150
+ }
151
+ // Cheap expressions first; a semantic part is only evaluated when its own expression passes.
152
+ for (const rule of applicable) {
153
+ if (rule.expression !== undefined) {
154
+ const result = await evaluateRuleExpression(rule, node, nodes, nodeIndex);
155
+ if (!result.passed) {
156
+ local.push(result.violation);
157
+ if (result.failed && ruleGatesDescendants(rule, result.violation.severity) && !prepared.blockingRuleId) {
158
+ prepared.blockingRuleId = rule.id;
159
+ }
160
+ continue;
161
+ }
162
+ }
163
+ if (rule.semantic !== undefined && judge)
164
+ prepared.semantic.push(rule);
165
+ }
166
+ prepared.judged = (judgements) => {
167
+ for (const j of judgements) {
168
+ if (j.failed) {
169
+ report(j.rule, {
170
+ severity: j.severity,
171
+ verdict: j.verdict,
172
+ guidance: j.guidance,
173
+ evidence: j.evidence,
174
+ error: j.error,
175
+ });
176
+ }
177
+ }
178
+ };
179
+ return prepared;
180
+ }
181
+ /**
182
+ * Evaluate one set of nodes together, so their semantic rules go to the judge in one pass and a judge
183
+ * with `evaluateNodes` can share model calls. Gating for the set is already decided; violations are
184
+ * recorded in node order.
185
+ */
186
+ async function evaluateNodeSet(nodes) {
187
+ const prepared = [];
188
+ for (const node of nodes)
189
+ prepared.push(await prepareNode(node));
190
+ if (judge) {
191
+ const pending = nodes.map((node, i) => ({ node, rules: prepared[i].semantic }));
192
+ const judgements = await judgeNodes(judge, pending, graph, options.trimReport);
193
+ prepared.forEach((p, i) => {
194
+ p.judged(judgements[i] ?? []);
195
+ });
196
+ }
197
+ for (const p of prepared)
198
+ violations.push(...p.violations);
199
+ return prepared.map((p) => ({ blockingRuleId: p.blockingRuleId }));
200
+ }
201
+ // Non-hierarchy nodes sit outside the subtree cascade — always evaluated, never gated.
202
+ await evaluateNodeSet(graph.nonHierarchy);
203
+ const visited = new Set();
204
+ // Hierarchy nodes are judged one depth at a time: a node's verdicts decide whether its children are evaluated.
205
+ let depthNodes = [];
206
+ for (const node of [...graph.hierarchyRoots, ...graph.orphans]) {
207
+ depthNodes.push({ node, ancestorOrigin: undefined });
208
+ }
209
+ while (depthNodes.length > 0) {
210
+ const current = [];
211
+ for (const entry of depthNodes) {
212
+ if (visited.has(entry.node.title))
213
+ continue;
214
+ visited.add(entry.node.title);
215
+ current.push(entry);
216
+ }
217
+ const isGated = (e) => e.ancestorOrigin !== undefined || seedByTitle.has(e.node.title);
218
+ const toEvaluate = current.filter((e) => !isGated(e) || includeGated);
219
+ const evaluated = await evaluateNodeSet(toEvaluate.map((e) => e.node));
220
+ const blockingByTitle = new Map(toEvaluate.map((e, i) => [e.node.title, evaluated[i].blockingRuleId]));
221
+ const next = [];
222
+ for (const { node, ancestorOrigin } of current) {
223
+ const selfSeed = seedByTitle.get(node.title);
224
+ const isGatedByAncestor = ancestorOrigin !== undefined;
225
+ const isSelfBlocked = selfSeed !== undefined;
226
+ const blockingRuleId = blockingByTitle.get(node.title);
227
+ let childOrigin = ancestorOrigin;
228
+ if (isGatedByAncestor) {
229
+ gateOrigins.get(ancestorOrigin).gatedFiles.push(node.label);
230
+ }
231
+ else if (isSelfBlocked) {
232
+ const origin = originFor(node.title, { file: selfSeed.file, reason: selfSeed.reason });
233
+ origin.gatedFiles.push(node.label);
234
+ childOrigin = node.title;
235
+ }
236
+ else if (blockingRuleId) {
237
+ originFor(node.title, { file: node.label, reason: 'rule', ruleId: blockingRuleId });
238
+ childOrigin = node.title;
239
+ }
240
+ else {
241
+ childOrigin = undefined;
242
+ }
243
+ for (const child of graph.hierarchyChildren.get(node.title) ?? []) {
244
+ next.push({ node: child, ancestorOrigin: childOrigin });
245
+ }
246
+ }
247
+ depthNodes = next;
248
+ }
249
+ return {
250
+ violations,
251
+ gates: [...gateOrigins.values()].filter((g) => g.gatedFiles.length > 0),
252
+ };
253
+ }