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.
- package/README.md +29 -216
- package/dist/api.d.ts +17 -17
- package/dist/api.js +12 -12
- 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 +26 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -18
- 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 +3 -3
- package/dist/load-space-graph.js +4 -4
- package/dist/plugins/loader.js +1 -1
- 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 +5 -5
- 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/render/render.d.ts +1 -1
- 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/space-context.d.ts +2 -2
- package/dist/space-context.js +3 -3
- package/dist/space-graph.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/types.d.ts +11 -4
- package/dist/validate.d.ts +59 -9
- package/dist/validate.js +225 -120
- 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 +6 -5
- 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
|
@@ -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>;
|
|
@@ -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
|
+
}
|