structured-context 0.13.0 → 0.14.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 +2 -2
- package/dist/api.d.ts +17 -17
- package/dist/api.js +12 -12
- package/dist/commands/dump.js +1 -1
- package/dist/commands/validate.js +8 -8
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -4
- package/dist/filter/augment-nodes.d.ts +5 -4
- package/dist/filter/augment-nodes.js +20 -19
- package/dist/filter/expand-include.d.ts +2 -2
- package/dist/filter/expand-include.js +15 -17
- package/dist/filter/filter-nodes.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -17
- package/dist/judge/context.d.ts +1 -1
- package/dist/judge/context.js +12 -12
- package/dist/judge/factory.d.ts +1 -1
- package/dist/judge/factory.js +1 -1
- package/dist/judge/index.d.ts +1 -1
- package/dist/judge/judge-node.d.ts +4 -4
- package/dist/judge/judge-node.js +7 -7
- package/dist/judge/prompt.d.ts +1 -1
- package/dist/judge/prompt.js +3 -3
- package/dist/judge/provider/fake.d.ts +1 -1
- package/dist/judge/provider/fake.js +3 -6
- package/dist/judge/provider/jev.js +1 -1
- package/dist/judge/provider/openai-decisions.js +1 -1
- package/dist/judge/trim.d.ts +1 -1
- package/dist/judge/types.d.ts +4 -1
- package/dist/judge/verdicts.d.ts +1 -1
- 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/index.d.ts +6 -0
- package/dist/plugins/markdown/index.js +1 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +7 -2
- package/dist/plugins/markdown/parse-embedded.js +23 -21
- package/dist/plugins/markdown/read-space.js +9 -8
- package/dist/plugins/markdown/render-bullets.js +3 -3
- package/dist/plugins/markdown/render-directory.js +1 -1
- package/dist/plugins/markdown/util.d.ts +12 -1
- package/dist/plugins/markdown/util.js +21 -0
- package/dist/plugins/mermaid/render-mermaid.js +5 -6
- package/dist/plugins/miro/cache.js +1 -1
- package/dist/plugins/miro/sync.js +3 -2
- package/dist/read/resolve-graph-edges.js +12 -18
- package/dist/render/render.d.ts +1 -1
- package/dist/render/render.js +1 -1
- package/dist/schema/evaluate-rule.d.ts +4 -4
- package/dist/schema/evaluate-rule.js +14 -10
- package/dist/schema/gating.d.ts +10 -10
- package/dist/schema/gating.js +45 -44
- package/dist/schema/validate-graph.js +8 -11
- package/dist/schema/validate-rules.d.ts +4 -4
- package/dist/schema/validate-rules.js +15 -17
- package/dist/schemas/_ost_strict.json +1 -1
- package/dist/space-context.d.ts +2 -2
- package/dist/space-context.js +3 -3
- package/dist/space-graph.d.ts +9 -9
- package/dist/space-graph.js +16 -16
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/dist/types.d.ts +18 -14
- package/dist/validate.d.ts +6 -6
- package/dist/validate.js +26 -27
- package/docs/concepts.md +25 -7
- package/docs/config.md +8 -0
- package/docs/rules.md +17 -13
- package/package.json +5 -5
- package/schemas/_ost_strict.json +1 -1
|
@@ -31,11 +31,11 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
31
31
|
const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
|
|
32
32
|
const childType = resolveNodeType(rawChildType, typeAliases);
|
|
33
33
|
const parentType = resolveNodeType(rawParentType, typeAliases);
|
|
34
|
-
function pushParentRef(childNode,
|
|
35
|
-
// Deduplicate by (field,
|
|
36
|
-
if (!childNode.resolvedParents.some((r) => r.field === field && r.
|
|
34
|
+
function pushParentRef(childNode, parentId) {
|
|
35
|
+
// Deduplicate by (field, id) — same parent via different fields is intentional
|
|
36
|
+
if (!childNode.resolvedParents.some((r) => r.field === field && r.id === parentId)) {
|
|
37
37
|
childNode.resolvedParents.push({
|
|
38
|
-
|
|
38
|
+
id: parentId,
|
|
39
39
|
field,
|
|
40
40
|
fieldOn,
|
|
41
41
|
source,
|
|
@@ -47,7 +47,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
47
47
|
function checkShape(ownerNode, rawField) {
|
|
48
48
|
if (multiple && !Array.isArray(rawField)) {
|
|
49
49
|
unresolvedRefs.push({
|
|
50
|
-
|
|
50
|
+
id: ownerNode.id,
|
|
51
51
|
ref: String(rawField),
|
|
52
52
|
field,
|
|
53
53
|
reason: 'invalid_shape',
|
|
@@ -57,7 +57,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
57
57
|
}
|
|
58
58
|
if (!multiple && typeof rawField !== 'string') {
|
|
59
59
|
unresolvedRefs.push({
|
|
60
|
-
|
|
60
|
+
id: ownerNode.id,
|
|
61
61
|
ref: String(rawField),
|
|
62
62
|
field,
|
|
63
63
|
reason: 'invalid_shape',
|
|
@@ -73,7 +73,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
73
73
|
const resolved = targetIndex.get(target);
|
|
74
74
|
if (resolved === undefined) {
|
|
75
75
|
unresolvedRefs.push({
|
|
76
|
-
|
|
76
|
+
id: ownerNode.id,
|
|
77
77
|
ref,
|
|
78
78
|
field,
|
|
79
79
|
reason: 'not_found',
|
|
@@ -84,7 +84,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
84
84
|
}
|
|
85
85
|
if (resolved === null) {
|
|
86
86
|
unresolvedRefs.push({
|
|
87
|
-
|
|
87
|
+
id: ownerNode.id,
|
|
88
88
|
ref,
|
|
89
89
|
field,
|
|
90
90
|
reason: 'ambiguous',
|
|
@@ -109,10 +109,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
109
109
|
const childNode = checkAndRecordRef(parentNode, ref);
|
|
110
110
|
if (!childNode)
|
|
111
111
|
continue;
|
|
112
|
-
|
|
113
|
-
if (typeof parentTitle !== 'string')
|
|
114
|
-
continue;
|
|
115
|
-
pushParentRef(childNode, parentTitle);
|
|
112
|
+
pushParentRef(childNode, parentNode.id);
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
115
|
}
|
|
@@ -129,10 +126,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
129
126
|
const parentNode = checkAndRecordRef(childNode, ref);
|
|
130
127
|
if (!parentNode)
|
|
131
128
|
continue;
|
|
132
|
-
|
|
133
|
-
if (typeof parentTitle !== 'string')
|
|
134
|
-
continue;
|
|
135
|
-
pushParentRef(childNode, parentTitle);
|
|
129
|
+
pushParentRef(childNode, parentNode.id);
|
|
136
130
|
}
|
|
137
131
|
}
|
|
138
132
|
}
|
|
@@ -237,12 +231,12 @@ export function resolveGraphEdges(nodes, metadata) {
|
|
|
237
231
|
for (const node of spaceNodes) {
|
|
238
232
|
node.resolvedLinks = (node.contentLinks ?? []).map((link) => classifyContentLink(link, targetIndex));
|
|
239
233
|
}
|
|
240
|
-
// Deduplicate by (
|
|
234
|
+
// Deduplicate by (id, field, ref) — the same broken link may be encountered across
|
|
241
235
|
// multiple resolveEdge calls (e.g. selfRef + regular hierarchy share the same field).
|
|
242
236
|
const seen = new Set();
|
|
243
237
|
const deduped = [];
|
|
244
238
|
for (const u of unresolvedRefs) {
|
|
245
|
-
const key = `${u.
|
|
239
|
+
const key = `${u.id}\0${u.field}\0${u.ref}`;
|
|
246
240
|
if (!seen.has(key)) {
|
|
247
241
|
seen.add(key);
|
|
248
242
|
deduped.push(u);
|
package/dist/render/render.d.ts
CHANGED
package/dist/render/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { shortenPluginName } from '../plugins/util.js';
|
|
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 { Rule, RuleViolation, SpaceNode } from '../types';
|
|
1
|
+
import type { NodeId, 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) */
|
|
@@ -37,10 +37,10 @@ export declare function tryEvaluateExpression(expr: string, context: EvalContext
|
|
|
37
37
|
*
|
|
38
38
|
* @param node - The node to build context for
|
|
39
39
|
* @param allNodes - All nodes in the space
|
|
40
|
-
* @param nodeIndex - Map of node
|
|
40
|
+
* @param nodeIndex - Map of node ids to nodes for efficient lookup
|
|
41
41
|
* @returns Evaluation context for the node
|
|
42
42
|
*/
|
|
43
|
-
export declare function buildEvalContext(node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<
|
|
43
|
+
export declare function buildEvalContext(node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<NodeId, SpaceNode>): EvalContext;
|
|
44
44
|
/** The violation reported when a rule's `expression` or `when` cannot be evaluated: an authoring error, so always `error`. */
|
|
45
45
|
export declare function expressionErrorViolation(rule: Rule, file: string, error: string): RuleViolation;
|
|
46
46
|
/** The outcome of a rule's `expression` on one node. */
|
|
@@ -58,4 +58,4 @@ export type ExpressionRuleResult = {
|
|
|
58
58
|
* reports the rule's severity; a rule with a semantic part names the expression in the evidence, so the
|
|
59
59
|
* violation is not mistaken for a verdict. An evaluation error reports `error` and is not a `failed` result.
|
|
60
60
|
*/
|
|
61
|
-
export declare function evaluateRuleExpression(rule: Rule, node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<
|
|
61
|
+
export declare function evaluateRuleExpression(rule: Rule, node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<NodeId, SpaceNode>, file?: string): Promise<ExpressionRuleResult>;
|
|
@@ -64,12 +64,16 @@ export async function tryEvaluateExpression(expr, context) {
|
|
|
64
64
|
* @param node - The node to flatten
|
|
65
65
|
* @returns Flattened node with properties directly accessible
|
|
66
66
|
*/
|
|
67
|
-
function flattenNode(node) {
|
|
67
|
+
function flattenNode(node, nodeIndex) {
|
|
68
|
+
const parentTitle = (r) => nodeIndex.get(r.id)?.title ?? r.id;
|
|
68
69
|
return {
|
|
69
70
|
...node.schemaData,
|
|
71
|
+
id: node.id,
|
|
70
72
|
resolvedType: node.resolvedType,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
resolvedParentId: node.resolvedParents[0]?.id, // first parent or undefined, provided for convenience
|
|
74
|
+
resolvedParentIds: node.resolvedParents.map((r) => r.id), // full array of parent ids
|
|
75
|
+
resolvedParentTitle: node.resolvedParents[0] && parentTitle(node.resolvedParents[0]), // display title of the first parent
|
|
76
|
+
resolvedParentTitles: node.resolvedParents.map(parentTitle), // display titles of all parents
|
|
73
77
|
resolvedLinks: node.resolvedLinks,
|
|
74
78
|
};
|
|
75
79
|
}
|
|
@@ -78,23 +82,23 @@ function flattenNode(node) {
|
|
|
78
82
|
*
|
|
79
83
|
* @param node - The node to build context for
|
|
80
84
|
* @param allNodes - All nodes in the space
|
|
81
|
-
* @param nodeIndex - Map of node
|
|
85
|
+
* @param nodeIndex - Map of node ids to nodes for efficient lookup
|
|
82
86
|
* @returns Evaluation context for the node
|
|
83
87
|
*/
|
|
84
88
|
export function buildEvalContext(node, allNodes, nodeIndex) {
|
|
85
89
|
// Flatten all nodes for JSONata access
|
|
86
|
-
const flattenedNodes = allNodes.map(flattenNode);
|
|
90
|
+
const flattenedNodes = allNodes.map((n) => flattenNode(n, nodeIndex));
|
|
87
91
|
// Build all parent objects from resolvedParents array
|
|
88
92
|
const flattenedParents = [];
|
|
89
|
-
for (const {
|
|
90
|
-
const parentNode = nodeIndex.get(
|
|
93
|
+
for (const { id: parentId } of node.resolvedParents) {
|
|
94
|
+
const parentNode = nodeIndex.get(parentId);
|
|
91
95
|
if (parentNode) {
|
|
92
|
-
flattenedParents.push(flattenNode(parentNode));
|
|
96
|
+
flattenedParents.push(flattenNode(parentNode, nodeIndex));
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
return {
|
|
96
100
|
nodes: flattenedNodes,
|
|
97
|
-
$$: flattenNode(node),
|
|
101
|
+
$$: flattenNode(node, nodeIndex),
|
|
98
102
|
parent: flattenedParents[0],
|
|
99
103
|
parents: flattenedParents.length > 0 ? flattenedParents : undefined,
|
|
100
104
|
};
|
|
@@ -114,7 +118,7 @@ export function expressionErrorViolation(rule, file, error) {
|
|
|
114
118
|
* reports the rule's severity; a rule with a semantic part names the expression in the evidence, so the
|
|
115
119
|
* violation is not mistaken for a verdict. An evaluation error reports `error` and is not a `failed` result.
|
|
116
120
|
*/
|
|
117
|
-
export async function evaluateRuleExpression(rule, node, allNodes, nodeIndex, file = node.
|
|
121
|
+
export async function evaluateRuleExpression(rule, node, allNodes, nodeIndex, file = node.id) {
|
|
118
122
|
const expression = rule.expression;
|
|
119
123
|
const outcome = await tryEvaluateExpression(expression, buildEvalContext(node, allNodes, nodeIndex));
|
|
120
124
|
if ('error' in outcome) {
|
package/dist/schema/gating.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { TrimReport } from '../judge/trim';
|
|
2
2
|
import type { SemanticJudgeProvider } from '../judge/types';
|
|
3
3
|
import type { SpaceGraph } from '../space-graph';
|
|
4
|
-
import type { Rule, RuleSeverity, RuleViolation, SpaceNode } from '../types';
|
|
4
|
+
import type { NodeId, Rule, RuleSeverity, RuleViolation, SpaceNode } from '../types';
|
|
5
5
|
/**
|
|
6
|
-
* Whether a
|
|
7
|
-
* `severity` is that of the
|
|
6
|
+
* Whether a violation of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
|
|
7
|
+
* `severity` is that of the violation: the rule's own for an expression, the verdict's for a semantic part.
|
|
8
8
|
*/
|
|
9
9
|
export declare function ruleGatesDescendants(rule: Rule, severity?: RuleSeverity): boolean;
|
|
10
10
|
/**
|
|
@@ -12,10 +12,10 @@ export declare function ruleGatesDescendants(rule: Rule, severity?: RuleSeverity
|
|
|
12
12
|
* (schema validation or hierarchy structure) that failed on a specific node.
|
|
13
13
|
*/
|
|
14
14
|
export type GatingSeed = {
|
|
15
|
-
/**
|
|
15
|
+
/** Id (file) of the failing node. */
|
|
16
16
|
file: string;
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/** Id of the failing node. */
|
|
18
|
+
id: NodeId;
|
|
19
19
|
/** Which always-run layer produced the failure. */
|
|
20
20
|
reason: 'schema' | 'hierarchy';
|
|
21
21
|
};
|
|
@@ -24,10 +24,10 @@ export type GatingSeed = {
|
|
|
24
24
|
* rule evaluation for itself (if schema/hierarchy caused it) and/or its hierarchy subtree.
|
|
25
25
|
*/
|
|
26
26
|
export type CoverageGate = {
|
|
27
|
-
/**
|
|
27
|
+
/** Id (file) of the node whose failure triggered this gate. */
|
|
28
28
|
file: string;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** Id of the blocking node. */
|
|
30
|
+
nodeId: NodeId;
|
|
31
31
|
/** What kind of check triggered the gate. */
|
|
32
32
|
reason: 'schema' | 'hierarchy' | 'rule';
|
|
33
33
|
/** Rule id responsible, when `reason` is `'rule'`. */
|
|
@@ -56,7 +56,7 @@ export interface GatingOptions {
|
|
|
56
56
|
* Compute breadth-first depth (1-based, from hierarchy roots and orphans) for every
|
|
57
57
|
* hierarchy-typed node in the graph. Non-hierarchy nodes are not included.
|
|
58
58
|
*/
|
|
59
|
-
export declare function computeHierarchyDepths(graph: SpaceGraph): Map<
|
|
59
|
+
export declare function computeHierarchyDepths(graph: SpaceGraph): Map<NodeId, number>;
|
|
60
60
|
/**
|
|
61
61
|
* Evaluate executable rules with cascade gating: a schema, hierarchy, or blocking-rule
|
|
62
62
|
* failure on a node suppresses rule evaluation for that node (when the failure is
|
package/dist/schema/gating.js
CHANGED
|
@@ -2,8 +2,8 @@ import { judgeNodes } from '../judge/judge-node';
|
|
|
2
2
|
import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
|
|
3
3
|
import { ruleSeverities, ruleSeverity } from './rule-severity';
|
|
4
4
|
/**
|
|
5
|
-
* Whether a
|
|
6
|
-
* `severity` is that of the
|
|
5
|
+
* Whether a violation of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
|
|
6
|
+
* `severity` is that of the violation: the rule's own for an expression, the verdict's for a semantic part.
|
|
7
7
|
*/
|
|
8
8
|
export function ruleGatesDescendants(rule, severity = ruleSeverity(rule)) {
|
|
9
9
|
return rule.gatesDescendants ?? severity === 'error';
|
|
@@ -20,17 +20,17 @@ export function computeHierarchyDepths(graph) {
|
|
|
20
20
|
const depths = new Map();
|
|
21
21
|
const queue = [];
|
|
22
22
|
for (const node of [...graph.hierarchyRoots, ...graph.orphans]) {
|
|
23
|
-
queue.push({
|
|
23
|
+
queue.push({ id: node.id, depth: 1 });
|
|
24
24
|
}
|
|
25
25
|
let i = 0;
|
|
26
26
|
while (i < queue.length) {
|
|
27
|
-
const {
|
|
28
|
-
if (depths.has(
|
|
27
|
+
const { id, depth } = queue[i++];
|
|
28
|
+
if (depths.has(id))
|
|
29
29
|
continue;
|
|
30
|
-
depths.set(
|
|
31
|
-
for (const child of graph.hierarchyChildren.get(
|
|
32
|
-
if (!depths.has(child.
|
|
33
|
-
queue.push({
|
|
30
|
+
depths.set(id, depth);
|
|
31
|
+
for (const child of graph.hierarchyChildren.get(id) ?? []) {
|
|
32
|
+
if (!depths.has(child.id))
|
|
33
|
+
queue.push({ id: child.id, depth: depth + 1 });
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
return depths;
|
|
@@ -48,24 +48,24 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
48
48
|
const includeGated = options.includeGated ?? false;
|
|
49
49
|
const nodeIndex = new Map();
|
|
50
50
|
for (const node of nodes) {
|
|
51
|
-
if (node.
|
|
52
|
-
nodeIndex.set(node.
|
|
51
|
+
if (node.id)
|
|
52
|
+
nodeIndex.set(node.id, node);
|
|
53
53
|
}
|
|
54
54
|
// When a node has both a schema and hierarchy failure, report schema as the reason.
|
|
55
|
-
const
|
|
55
|
+
const seedById = new Map();
|
|
56
56
|
for (const seed of seeds) {
|
|
57
|
-
if (!
|
|
58
|
-
|
|
57
|
+
if (!seedById.has(seed.id) || seed.reason === 'schema') {
|
|
58
|
+
seedById.set(seed.id, seed);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
const judge = options.provider;
|
|
62
62
|
const violations = [];
|
|
63
63
|
const gateOrigins = new Map();
|
|
64
|
-
function originFor(
|
|
65
|
-
let origin = gateOrigins.get(
|
|
64
|
+
function originFor(id, seed) {
|
|
65
|
+
let origin = gateOrigins.get(id);
|
|
66
66
|
if (!origin) {
|
|
67
|
-
origin = { file: seed.file,
|
|
68
|
-
gateOrigins.set(
|
|
67
|
+
origin = { file: seed.file, nodeId: id, reason: seed.reason, ruleId: seed.ruleId, gatedFiles: [] };
|
|
68
|
+
gateOrigins.set(id, origin);
|
|
69
69
|
}
|
|
70
70
|
return origin;
|
|
71
71
|
}
|
|
@@ -85,10 +85,10 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
85
85
|
const parentsOf = new Map();
|
|
86
86
|
for (const [parent, children] of graph.hierarchyChildren) {
|
|
87
87
|
for (const child of children)
|
|
88
|
-
parentsOf.set(child.
|
|
88
|
+
parentsOf.set(child.id, [...(parentsOf.get(child.id) ?? []), parent]);
|
|
89
89
|
}
|
|
90
|
-
const target = nodes.find((n) => n.
|
|
91
|
-
const pending = target ? [target.
|
|
90
|
+
const target = nodes.find((n) => n.id === options.judgeTarget);
|
|
91
|
+
const pending = target ? [target.id] : [];
|
|
92
92
|
while (pending.length > 0) {
|
|
93
93
|
for (const parent of parentsOf.get(pending.pop()) ?? []) {
|
|
94
94
|
if (!targetAncestors.has(parent)) {
|
|
@@ -104,12 +104,9 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
104
104
|
for (const r of perNodeRules) {
|
|
105
105
|
if (r.type && r.type !== node.resolvedType)
|
|
106
106
|
continue;
|
|
107
|
-
if (judge &&
|
|
108
|
-
r.semantic !== undefined &&
|
|
109
|
-
options.judgeTarget !== undefined &&
|
|
110
|
-
node.label !== options.judgeTarget) {
|
|
107
|
+
if (judge && r.semantic !== undefined && options.judgeTarget !== undefined && node.id !== options.judgeTarget) {
|
|
111
108
|
// Ancestors only matter through the gates they could raise.
|
|
112
|
-
if (!(targetAncestors.has(node.
|
|
109
|
+
if (!(targetAncestors.has(node.id) && ruleCanGate(r)))
|
|
113
110
|
continue;
|
|
114
111
|
}
|
|
115
112
|
if (r.when) {
|
|
@@ -128,11 +125,11 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
128
125
|
async function prepareNode(node) {
|
|
129
126
|
const local = [];
|
|
130
127
|
const prepared = { violations: local, semantic: [], judged: () => { } };
|
|
131
|
-
const applicable = await applicableRules(node, (rule, error) => local.push(expressionErrorViolation(rule, node.
|
|
128
|
+
const applicable = await applicableRules(node, (rule, error) => local.push(expressionErrorViolation(rule, node.id, error)));
|
|
132
129
|
function report(rule, detail = {}) {
|
|
133
130
|
const severity = detail.severity ?? ruleSeverities(rule)[0] ?? ruleSeverity(rule);
|
|
134
131
|
local.push({
|
|
135
|
-
file: node.
|
|
132
|
+
file: node.id,
|
|
136
133
|
ruleId: rule.id,
|
|
137
134
|
category: rule.category,
|
|
138
135
|
severity,
|
|
@@ -148,7 +145,9 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
148
145
|
prepared.blockingRuleId = rule.id;
|
|
149
146
|
}
|
|
150
147
|
}
|
|
151
|
-
// Cheap expressions first
|
|
148
|
+
// Cheap expressions first. A semantic part is evaluated only when its own expression passes, and not at all
|
|
149
|
+
// on a node with a gating violation.
|
|
150
|
+
const semantic = [];
|
|
152
151
|
for (const rule of applicable) {
|
|
153
152
|
if (rule.expression !== undefined) {
|
|
154
153
|
const result = await evaluateRuleExpression(rule, node, nodes, nodeIndex);
|
|
@@ -161,11 +160,13 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
if (rule.semantic !== undefined && judge)
|
|
164
|
-
|
|
163
|
+
semantic.push(rule);
|
|
165
164
|
}
|
|
165
|
+
if (!prepared.blockingRuleId)
|
|
166
|
+
prepared.semantic = semantic;
|
|
166
167
|
prepared.judged = (judgements) => {
|
|
167
168
|
for (const j of judgements) {
|
|
168
|
-
if (j.
|
|
169
|
+
if (j.violated) {
|
|
169
170
|
report(j.rule, {
|
|
170
171
|
severity: j.severity,
|
|
171
172
|
verdict: j.verdict,
|
|
@@ -209,38 +210,38 @@ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, option
|
|
|
209
210
|
while (depthNodes.length > 0) {
|
|
210
211
|
const current = [];
|
|
211
212
|
for (const entry of depthNodes) {
|
|
212
|
-
if (visited.has(entry.node.
|
|
213
|
+
if (visited.has(entry.node.id))
|
|
213
214
|
continue;
|
|
214
|
-
visited.add(entry.node.
|
|
215
|
+
visited.add(entry.node.id);
|
|
215
216
|
current.push(entry);
|
|
216
217
|
}
|
|
217
|
-
const isGated = (e) => e.ancestorOrigin !== undefined ||
|
|
218
|
+
const isGated = (e) => e.ancestorOrigin !== undefined || seedById.has(e.node.id);
|
|
218
219
|
const toEvaluate = current.filter((e) => !isGated(e) || includeGated);
|
|
219
220
|
const evaluated = await evaluateNodeSet(toEvaluate.map((e) => e.node));
|
|
220
|
-
const
|
|
221
|
+
const blockingById = new Map(toEvaluate.map((e, i) => [e.node.id, evaluated[i].blockingRuleId]));
|
|
221
222
|
const next = [];
|
|
222
223
|
for (const { node, ancestorOrigin } of current) {
|
|
223
|
-
const selfSeed =
|
|
224
|
+
const selfSeed = seedById.get(node.id);
|
|
224
225
|
const isGatedByAncestor = ancestorOrigin !== undefined;
|
|
225
226
|
const isSelfBlocked = selfSeed !== undefined;
|
|
226
|
-
const blockingRuleId =
|
|
227
|
+
const blockingRuleId = blockingById.get(node.id);
|
|
227
228
|
let childOrigin = ancestorOrigin;
|
|
228
229
|
if (isGatedByAncestor) {
|
|
229
|
-
gateOrigins.get(ancestorOrigin).gatedFiles.push(node.
|
|
230
|
+
gateOrigins.get(ancestorOrigin).gatedFiles.push(node.id);
|
|
230
231
|
}
|
|
231
232
|
else if (isSelfBlocked) {
|
|
232
|
-
const origin = originFor(node.
|
|
233
|
-
origin.gatedFiles.push(node.
|
|
234
|
-
childOrigin = node.
|
|
233
|
+
const origin = originFor(node.id, { file: selfSeed.file, reason: selfSeed.reason });
|
|
234
|
+
origin.gatedFiles.push(node.id);
|
|
235
|
+
childOrigin = node.id;
|
|
235
236
|
}
|
|
236
237
|
else if (blockingRuleId) {
|
|
237
|
-
originFor(node.
|
|
238
|
-
childOrigin = node.
|
|
238
|
+
originFor(node.id, { file: node.id, reason: 'rule', ruleId: blockingRuleId });
|
|
239
|
+
childOrigin = node.id;
|
|
239
240
|
}
|
|
240
241
|
else {
|
|
241
242
|
childOrigin = undefined;
|
|
242
243
|
}
|
|
243
|
-
for (const child of graph.hierarchyChildren.get(node.
|
|
244
|
+
for (const child of graph.hierarchyChildren.get(node.id) ?? []) {
|
|
244
245
|
next.push({ node: child, ancestorOrigin: childOrigin });
|
|
245
246
|
}
|
|
246
247
|
}
|
|
@@ -5,7 +5,7 @@ import { resolveNodeType } from '../schema/schema';
|
|
|
5
5
|
*/
|
|
6
6
|
export function validateGraph(nodes, metadata, unresolvedRefs = []) {
|
|
7
7
|
const refErrors = unresolvedRefs.map((u) => ({
|
|
8
|
-
file: u.source?.file ?? u.
|
|
8
|
+
file: u.source?.file ?? u.id,
|
|
9
9
|
...(u.source?.line !== undefined ? { line: u.source.line } : {}),
|
|
10
10
|
parent: u.ref,
|
|
11
11
|
error: u.message,
|
|
@@ -69,20 +69,17 @@ export function validateHierarchyStructure(nodes, metadata) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
const nodeIndex = new Map();
|
|
72
|
-
for (const node of nodes)
|
|
73
|
-
|
|
74
|
-
if (title)
|
|
75
|
-
nodeIndex.set(title, node);
|
|
76
|
-
}
|
|
72
|
+
for (const node of nodes)
|
|
73
|
+
nodeIndex.set(node.id, node);
|
|
77
74
|
for (const node of nodes) {
|
|
78
75
|
const nodeType = node.resolvedType;
|
|
79
76
|
const nodeTitle = node.title;
|
|
80
77
|
for (const parentRef of node.resolvedParents) {
|
|
81
|
-
const parentNode = nodeIndex.get(parentRef.
|
|
78
|
+
const parentNode = nodeIndex.get(parentRef.id);
|
|
82
79
|
if (!parentNode)
|
|
83
80
|
continue;
|
|
84
81
|
const parentType = parentNode.resolvedType;
|
|
85
|
-
const parentTitle =
|
|
82
|
+
const parentTitle = parentNode.title;
|
|
86
83
|
if (parentRef.fieldOn === 'parent') {
|
|
87
84
|
// Field is on the parent node; validate that this node's type matches the expected child type.
|
|
88
85
|
// Violation is attributed to the field-owner (parent) node.
|
|
@@ -90,7 +87,7 @@ export function validateHierarchyStructure(nodes, metadata) {
|
|
|
90
87
|
if (allowedChildTypes && !allowedChildTypes.has(nodeType)) {
|
|
91
88
|
const expected = [...allowedChildTypes].join(' or ');
|
|
92
89
|
violations.push({
|
|
93
|
-
file: parentNode.
|
|
90
|
+
file: parentNode.id,
|
|
94
91
|
nodeType: parentType,
|
|
95
92
|
nodeTitle: parentTitle,
|
|
96
93
|
parentType: nodeType,
|
|
@@ -105,7 +102,7 @@ export function validateHierarchyStructure(nodes, metadata) {
|
|
|
105
102
|
if (allowedTypes && !allowedTypes.has(parentType)) {
|
|
106
103
|
const expected = [...allowedTypes].join(' or ');
|
|
107
104
|
violations.push({
|
|
108
|
-
file: node.
|
|
105
|
+
file: node.id,
|
|
109
106
|
nodeType,
|
|
110
107
|
nodeTitle,
|
|
111
108
|
parentType,
|
|
@@ -143,7 +140,7 @@ export function validateHierarchyStructure(nodes, metadata) {
|
|
|
143
140
|
}
|
|
144
141
|
if (!isValidHierarchy) {
|
|
145
142
|
violations.push({
|
|
146
|
-
file: node.
|
|
143
|
+
file: node.id,
|
|
147
144
|
nodeType,
|
|
148
145
|
nodeTitle,
|
|
149
146
|
parentType,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { TrimReport } from '../judge/trim';
|
|
2
2
|
import type { SemanticJudgeProvider } from '../judge/types';
|
|
3
|
-
import type { Rule, RuleViolation, SpaceNode } from '../types';
|
|
3
|
+
import type { NodeId, Rule, RuleViolation, SpaceNode } from '../types';
|
|
4
4
|
export { DEFAULT_SEVERITY_BY_CATEGORY, ruleSeverity } from './rule-severity';
|
|
5
5
|
export interface ValidateRulesOptions {
|
|
6
6
|
/** Evaluates semantic rules. Without one, a rule runs only its `expression`, and a semantic-only rule is skipped. */
|
|
7
7
|
judge?: SemanticJudgeProvider;
|
|
8
|
-
/**
|
|
8
|
+
/** Id of the only node to judge (`validate-file`); other nodes' semantic rules are skipped. */
|
|
9
9
|
judgeTarget?: string;
|
|
10
|
-
/** Set of node
|
|
11
|
-
|
|
10
|
+
/** Set of node ids that have already failed upstream structural checks. */
|
|
11
|
+
gatedNodeIds?: Set<NodeId>;
|
|
12
12
|
trimReport?: TrimReport;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { judgeNodes } from '../judge/judge-node';
|
|
2
2
|
import { buildSpaceGraph } from '../space-graph';
|
|
3
3
|
import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
|
|
4
|
+
import { ruleGatesDescendants } from './gating';
|
|
4
5
|
import { ruleSeverities, ruleSeverity } from './rule-severity';
|
|
5
6
|
export { DEFAULT_SEVERITY_BY_CATEGORY, ruleSeverity } from './rule-severity';
|
|
6
7
|
/**
|
|
@@ -18,10 +19,7 @@ export async function validateRules(nodes, rules, options = {}) {
|
|
|
18
19
|
// Build node index for efficient lookups
|
|
19
20
|
const nodeIndex = new Map();
|
|
20
21
|
for (const node of nodes) {
|
|
21
|
-
|
|
22
|
-
if (title) {
|
|
23
|
-
nodeIndex.set(title, node);
|
|
24
|
-
}
|
|
22
|
+
nodeIndex.set(node.id, node);
|
|
25
23
|
}
|
|
26
24
|
const sentinel = nodes[0];
|
|
27
25
|
const runnable = rules.filter((r) => r.expression !== undefined || (r.semantic !== undefined && judge));
|
|
@@ -37,7 +35,7 @@ export async function validateRules(nodes, rules, options = {}) {
|
|
|
37
35
|
if (rule.when) {
|
|
38
36
|
const when = await tryEvaluateExpression(rule.when, buildEvalContext(node, nodes, nodeIndex));
|
|
39
37
|
if ('error' in when) {
|
|
40
|
-
violations.push(expressionErrorViolation(rule, node.
|
|
38
|
+
violations.push(expressionErrorViolation(rule, node.id, when.error));
|
|
41
39
|
continue;
|
|
42
40
|
}
|
|
43
41
|
if (when.value !== true)
|
|
@@ -49,8 +47,8 @@ export async function validateRules(nodes, rules, options = {}) {
|
|
|
49
47
|
}
|
|
50
48
|
// 1. Expressions, in rule order. A global rule is evaluated once against the full node set; a sentinel node
|
|
51
49
|
// provides the evaluation context (the nodes array is what matters).
|
|
52
|
-
const
|
|
53
|
-
const notEvaluable = new Set(); // `${rule.id}\0${
|
|
50
|
+
const gatedNodeIds = new Set(options.gatedNodeIds); // nodes with a gating violation (or gated upstream)
|
|
51
|
+
const notEvaluable = new Set(); // `${rule.id}\0${id}` whose own expression failed or errored
|
|
54
52
|
for (const rule of runnable) {
|
|
55
53
|
if (rule.expression === undefined)
|
|
56
54
|
continue;
|
|
@@ -67,24 +65,24 @@ export async function validateRules(nodes, rules, options = {}) {
|
|
|
67
65
|
if (result.passed)
|
|
68
66
|
continue;
|
|
69
67
|
violations.push(result.violation);
|
|
70
|
-
notEvaluable.add(`${rule.id}\0${node.
|
|
71
|
-
if (result.failed)
|
|
72
|
-
|
|
68
|
+
notEvaluable.add(`${rule.id}\0${node.id}`);
|
|
69
|
+
if (result.failed && ruleGatesDescendants(rule, result.violation.severity))
|
|
70
|
+
gatedNodeIds.add(node.id);
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
|
-
// 2. Semantic parts, for nodes that passed their own expression. A node
|
|
76
|
-
// upstream is left out
|
|
73
|
+
// 2. Semantic parts, for nodes that passed their own expression. A node with a gating violation, or gated
|
|
74
|
+
// upstream, is left out.
|
|
77
75
|
if (judge) {
|
|
78
76
|
const perNode = []; // kept in node order
|
|
79
77
|
const pending = [];
|
|
80
78
|
for (const node of nodes) {
|
|
81
|
-
if (options.judgeTarget !== undefined && node.
|
|
79
|
+
if (options.judgeTarget !== undefined && node.id !== options.judgeTarget)
|
|
82
80
|
continue;
|
|
83
81
|
const found = [];
|
|
84
82
|
perNode.push(found);
|
|
85
|
-
if (
|
|
83
|
+
if (gatedNodeIds.has(node.id))
|
|
86
84
|
continue;
|
|
87
|
-
const semantic = runnable.filter((r) => r.semantic !== undefined && applicable.get(r)?.includes(node) && !notEvaluable.has(`${r.id}\0${node.
|
|
85
|
+
const semantic = runnable.filter((r) => r.semantic !== undefined && applicable.get(r)?.includes(node) && !notEvaluable.has(`${r.id}\0${node.id}`));
|
|
88
86
|
if (semantic.length > 0)
|
|
89
87
|
pending.push({ node, rules: semantic, found });
|
|
90
88
|
}
|
|
@@ -92,10 +90,10 @@ export async function validateRules(nodes, rules, options = {}) {
|
|
|
92
90
|
const judged = await judgeNodes(judge, pending, buildSpaceGraph(nodes, undefined), options.trimReport);
|
|
93
91
|
pending.forEach(({ node, found }, i) => {
|
|
94
92
|
for (const j of judged[i] ?? []) {
|
|
95
|
-
if (!j.
|
|
93
|
+
if (!j.violated)
|
|
96
94
|
continue;
|
|
97
95
|
found.push({
|
|
98
|
-
file: node.
|
|
96
|
+
file: node.id,
|
|
99
97
|
ruleId: j.rule.id,
|
|
100
98
|
category: j.rule.category,
|
|
101
99
|
severity: j.severity ?? ruleSeverities(j.rule)[0] ?? ruleSeverity(j.rule),
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"category": "best-practice",
|
|
35
35
|
"description": "Explore multiple candidate solutions (aim for at least three) for the target opportunity",
|
|
36
36
|
"type": "opportunity",
|
|
37
|
-
"expression": "(current.status != 'exploring' and current.status != 'active') or $count(nodes[
|
|
37
|
+
"expression": "(current.status != 'exploring' and current.status != 'active') or $count(nodes[resolvedParentId=$$.current.id and resolvedType='solution']) >= 3"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"id": "opportunity-customer-voice",
|
package/dist/space-context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Config } from './config
|
|
2
|
-
import type { SpaceContext } from './types
|
|
1
|
+
import { type Config } from './config';
|
|
2
|
+
import type { SpaceContext } from './types';
|
|
3
3
|
/** Thrown by createSpaceContext when no space matches the given name. */
|
|
4
4
|
export declare class SpaceNotFoundError extends Error {
|
|
5
5
|
readonly spaceName: string;
|
package/dist/space-context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { resolveSchema } from './config
|
|
2
|
-
import { resolveSpacePluginSchemas } from './plugins/util
|
|
3
|
-
import { loadSchema } from './schema/schema
|
|
1
|
+
import { resolveSchema } from './config';
|
|
2
|
+
import { resolveSpacePluginSchemas } from './plugins/util';
|
|
3
|
+
import { loadSchema } from './schema/schema';
|
|
4
4
|
/** Thrown by createSpaceContext when no space matches the given name. */
|
|
5
5
|
export class SpaceNotFoundError extends Error {
|
|
6
6
|
spaceName;
|