structured-context 0.13.1 → 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/commands/dump.js +1 -1
- package/dist/commands/validate.js +8 -8
- 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/judge/context.js +12 -12
- package/dist/judge/judge-node.d.ts +3 -3
- package/dist/judge/judge-node.js +7 -7
- 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/types.d.ts +4 -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/util.d.ts +11 -0
- 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/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-graph.d.ts +9 -9
- package/dist/space-graph.js +16 -16
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +3 -3
- package/dist/types.d.ts +14 -10
- package/dist/validate.d.ts +2 -2
- package/dist/validate.js +18 -19
- package/docs/concepts.md +25 -7
- package/docs/config.md +8 -0
- package/docs/rules.md +17 -13
- package/package.json +1 -1
- package/schemas/_ost_strict.json +1 -1
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-graph.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Hierarchy, SpaceNode } from './types';
|
|
1
|
+
import type { Hierarchy, NodeId, SpaceNode } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* A navigable graph over a set of SpaceNodes.
|
|
4
4
|
*
|
|
@@ -9,20 +9,20 @@ import type { Hierarchy, SpaceNode } from './types';
|
|
|
9
9
|
* so consumers don't need to build their own indexes.
|
|
10
10
|
*/
|
|
11
11
|
export type SpaceGraph = {
|
|
12
|
-
/** All nodes, keyed by
|
|
13
|
-
readonly nodes: ReadonlyMap<
|
|
12
|
+
/** All nodes, keyed by id. Preserves insertion order. */
|
|
13
|
+
readonly nodes: ReadonlyMap<NodeId, SpaceNode>;
|
|
14
14
|
/** Hierarchy roots: nodes of the root type with no valid hierarchy parents. */
|
|
15
15
|
readonly hierarchyRoots: readonly SpaceNode[];
|
|
16
16
|
/** Orphans: hierarchy-typed nodes with no valid hierarchy parents in the node set. */
|
|
17
17
|
readonly orphans: readonly SpaceNode[];
|
|
18
18
|
/** Non-hierarchy nodes: type not in the hierarchy levels definition. */
|
|
19
19
|
readonly nonHierarchy: readonly SpaceNode[];
|
|
20
|
-
/** Hierarchy children map: parent
|
|
21
|
-
readonly hierarchyChildren: ReadonlyMap<
|
|
22
|
-
/** All-edges children map: parent
|
|
23
|
-
readonly children: ReadonlyMap<
|
|
24
|
-
/** Set of all node
|
|
25
|
-
readonly
|
|
20
|
+
/** Hierarchy children map: parent id → direct children connected via this hierarchy's edges only. */
|
|
21
|
+
readonly hierarchyChildren: ReadonlyMap<NodeId, readonly SpaceNode[]>;
|
|
22
|
+
/** All-edges children map: parent id → direct children connected via any edge (hierarchy + relationship). */
|
|
23
|
+
readonly children: ReadonlyMap<NodeId, readonly SpaceNode[]>;
|
|
24
|
+
/** Set of all node ids that are part of the hierarchy (roots + their descendants + orphans). */
|
|
25
|
+
readonly hierarchyIds: ReadonlySet<NodeId>;
|
|
26
26
|
/** The hierarchy used to build this graph, if the schema has one. */
|
|
27
27
|
readonly hierarchy: Hierarchy | undefined;
|
|
28
28
|
};
|
package/dist/space-graph.js
CHANGED
|
@@ -12,22 +12,22 @@ export function buildSpaceGraph(nodes, hierarchy) {
|
|
|
12
12
|
const nonHierarchy = [];
|
|
13
13
|
// First pass: register all nodes and init adjacency lists
|
|
14
14
|
for (const node of nodes) {
|
|
15
|
-
nodesMap.set(node.
|
|
16
|
-
hierarchyChildrenMap.set(node.
|
|
17
|
-
childrenMap.set(node.
|
|
15
|
+
nodesMap.set(node.id, node);
|
|
16
|
+
hierarchyChildrenMap.set(node.id, []);
|
|
17
|
+
childrenMap.set(node.id, []);
|
|
18
18
|
}
|
|
19
19
|
// Second pass: build children maps (inverted from resolvedParents)
|
|
20
20
|
for (const node of nodes) {
|
|
21
21
|
for (const parentRef of node.resolvedParents) {
|
|
22
22
|
// All-edges map
|
|
23
|
-
if (!childrenMap.has(parentRef.
|
|
24
|
-
childrenMap.set(parentRef.
|
|
25
|
-
childrenMap.get(parentRef.
|
|
23
|
+
if (!childrenMap.has(parentRef.id))
|
|
24
|
+
childrenMap.set(parentRef.id, []);
|
|
25
|
+
childrenMap.get(parentRef.id).push(node);
|
|
26
26
|
// Hierarchy-only map
|
|
27
27
|
if (isHierarchyEdge(parentRef)) {
|
|
28
|
-
if (!hierarchyChildrenMap.has(parentRef.
|
|
29
|
-
hierarchyChildrenMap.set(parentRef.
|
|
30
|
-
hierarchyChildrenMap.get(parentRef.
|
|
28
|
+
if (!hierarchyChildrenMap.has(parentRef.id))
|
|
29
|
+
hierarchyChildrenMap.set(parentRef.id, []);
|
|
30
|
+
hierarchyChildrenMap.get(parentRef.id).push(node);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -50,20 +50,20 @@ export function buildSpaceGraph(nodes, hierarchy) {
|
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
// Check if at least one hierarchy parent is actually in the node set
|
|
53
|
-
const hasValidParent = hierarchyParents.some((r) => nodesMap.has(r.
|
|
53
|
+
const hasValidParent = hierarchyParents.some((r) => nodesMap.has(r.id));
|
|
54
54
|
if (!hasValidParent) {
|
|
55
55
|
// All hierarchy parents are dangling — treat as orphan
|
|
56
56
|
orphans.push(node);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
// Build
|
|
61
|
-
const
|
|
60
|
+
// Build hierarchyIds: BFS from roots + orphans through hierarchyChildren
|
|
61
|
+
const hierarchyIds = new Set();
|
|
62
62
|
function addHierarchySubtree(node) {
|
|
63
|
-
if (
|
|
63
|
+
if (hierarchyIds.has(node.id))
|
|
64
64
|
return;
|
|
65
|
-
|
|
66
|
-
for (const child of hierarchyChildrenMap.get(node.
|
|
65
|
+
hierarchyIds.add(node.id);
|
|
66
|
+
for (const child of hierarchyChildrenMap.get(node.id) ?? []) {
|
|
67
67
|
addHierarchySubtree(child);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -78,7 +78,7 @@ export function buildSpaceGraph(nodes, hierarchy) {
|
|
|
78
78
|
nonHierarchy,
|
|
79
79
|
hierarchyChildren: hierarchyChildrenMap,
|
|
80
80
|
children: childrenMap,
|
|
81
|
-
|
|
81
|
+
hierarchyIds,
|
|
82
82
|
hierarchy,
|
|
83
83
|
};
|
|
84
84
|
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { BaseNode, Hierarchy, HierarchyLevel, Relationship, ResolvedParentR
|
|
|
3
3
|
* Creates a ResolvedParentRef with sensible defaults for use in tests.
|
|
4
4
|
* Override any field to represent specific edge contexts.
|
|
5
5
|
*/
|
|
6
|
-
export declare const makeParentRef: (
|
|
6
|
+
export declare const makeParentRef: (id: string, overrides?: Partial<ResolvedParentRef>) => ResolvedParentRef;
|
|
7
7
|
/**
|
|
8
8
|
* Creates a HierarchyLevel with defaults matching schema metadata normalization.
|
|
9
9
|
*/
|
package/dist/testing.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Creates a ResolvedParentRef with sensible defaults for use in tests.
|
|
3
3
|
* Override any field to represent specific edge contexts.
|
|
4
4
|
*/
|
|
5
|
-
export const makeParentRef = (
|
|
5
|
+
export const makeParentRef = (id, overrides = {}) => {
|
|
6
6
|
const source = overrides.source ?? 'hierarchy';
|
|
7
7
|
return {
|
|
8
|
-
|
|
8
|
+
id: id,
|
|
9
9
|
field: 'parent',
|
|
10
10
|
source,
|
|
11
11
|
...(source === 'hierarchy' ? { hierarchy: 'main' } : {}),
|
|
@@ -55,7 +55,7 @@ export const makeRelationship = (parent, type, overrides = {}) => ({
|
|
|
55
55
|
* Creates a BaseNode with sensible defaults for use in tests.
|
|
56
56
|
*/
|
|
57
57
|
export const makeNode = (title, type, extra = {}, linkTargets) => ({
|
|
58
|
-
|
|
58
|
+
id: title,
|
|
59
59
|
title,
|
|
60
60
|
schemaData: { title, type, ...extra },
|
|
61
61
|
linkTargets: linkTargets ?? [title],
|
package/dist/types.d.ts
CHANGED
|
@@ -28,14 +28,18 @@ export type Relationship = Omit<MetadataContractRelationship, 'field' | 'fieldOn
|
|
|
28
28
|
fieldOn: 'child' | 'parent';
|
|
29
29
|
multiple: boolean;
|
|
30
30
|
};
|
|
31
|
+
/** Unique key of a node within a space. Supplied by the plugin that produced the node. */
|
|
32
|
+
export type NodeId = string & {
|
|
33
|
+
readonly __brand: 'NodeId';
|
|
34
|
+
};
|
|
31
35
|
/**
|
|
32
|
-
* A resolved parent reference, capturing not just the parent
|
|
36
|
+
* A resolved parent reference, capturing not just the parent id but the edge context
|
|
33
37
|
* from which it was resolved. All hierarchy levels and relationships produce entries of
|
|
34
38
|
* this type, forming a single labelled graph rather than separate structures.
|
|
35
39
|
*/
|
|
36
40
|
export type ResolvedParentRef = {
|
|
37
|
-
/**
|
|
38
|
-
|
|
41
|
+
/** Id of the parent node. */
|
|
42
|
+
id: NodeId;
|
|
39
43
|
/** The field name that contained the wikilink (e.g. 'parent', 'key_activities', 'produces_data'). */
|
|
40
44
|
field: string;
|
|
41
45
|
/** Whether this edge originates from a hierarchy level or a relationship definition. */
|
|
@@ -56,8 +60,8 @@ export type SourceLocation = {
|
|
|
56
60
|
column?: number;
|
|
57
61
|
};
|
|
58
62
|
export type UnresolvedRef = {
|
|
59
|
-
/**
|
|
60
|
-
|
|
63
|
+
/** Id of the node containing the broken link. */
|
|
64
|
+
id: NodeId;
|
|
61
65
|
/** Raw wikilink value (or String(rawField) for invalid_shape). */
|
|
62
66
|
ref: string;
|
|
63
67
|
/** Field name that contained the link. */
|
|
@@ -109,9 +113,9 @@ export type ResolvedContentLink = ContentLinkBase & {
|
|
|
109
113
|
* Core enriches this into a SpaceNode after parsing.
|
|
110
114
|
*/
|
|
111
115
|
export type BaseNode = {
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
/**
|
|
116
|
+
/** Unique key of the node: graph key, parent lookup, gating, duplicate detection and the file in messages. Supplied by the plugin. */
|
|
117
|
+
id: NodeId;
|
|
118
|
+
/** Display title (from schemaData.title). May repeat across nodes. */
|
|
115
119
|
title: string;
|
|
116
120
|
/** Fields validated against the active schema. */
|
|
117
121
|
schemaData: Record<string, unknown>;
|
|
@@ -138,7 +142,7 @@ export type SpaceNode = BaseNode & {
|
|
|
138
142
|
resolvedType: string;
|
|
139
143
|
/**
|
|
140
144
|
* Resolved parent references derived from all edge fields (hierarchy levels + relationships).
|
|
141
|
-
* Each entry carries the parent
|
|
145
|
+
* Each entry carries the parent id, the field it came from, and its edge context.
|
|
142
146
|
* Always present, empty if no parents resolved.
|
|
143
147
|
*/
|
|
144
148
|
resolvedParents: ResolvedParentRef[];
|
|
@@ -174,7 +178,7 @@ export type RuleSeverity = 'error' | 'warning' | 'info';
|
|
|
174
178
|
export type { CoverageGate, GatingSeed } from './schema/gating';
|
|
175
179
|
export type { Rule } from './schema/metadata-contract';
|
|
176
180
|
export type RuleViolation = {
|
|
177
|
-
/** Node
|
|
181
|
+
/** Node id, or empty string for global-scope rules. */
|
|
178
182
|
file: string;
|
|
179
183
|
ruleId: string;
|
|
180
184
|
category: RuleCategory;
|
package/dist/validate.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Config, SpaceConfig } from './config';
|
|
|
3
3
|
import type { SemanticJudgeProvider } from './judge/types';
|
|
4
4
|
import { type RuleFilter } from './schema/rule-filter';
|
|
5
5
|
import { extractEntityInfo } from './schema/schema';
|
|
6
|
-
import type { CoverageGate, FileNotInSpaceResult, FileValidationResult, GraphViolation, ParseIssue, ReadSpaceResult, RuleViolation, SchemaWithMetadata, SpaceContext, SpaceNode, ValidateFileOutput } from './types';
|
|
6
|
+
import type { CoverageGate, FileNotInSpaceResult, FileValidationResult, GraphViolation, NodeId, ParseIssue, ReadSpaceResult, RuleViolation, SchemaWithMetadata, SpaceContext, SpaceNode, ValidateFileOutput } from './types';
|
|
7
7
|
export interface FormattedError {
|
|
8
8
|
message: string;
|
|
9
9
|
dedupeKey: string;
|
|
@@ -24,7 +24,7 @@ export interface ValidationResult {
|
|
|
24
24
|
error: string;
|
|
25
25
|
}>;
|
|
26
26
|
duplicateErrors: Array<{
|
|
27
|
-
|
|
27
|
+
id: NodeId;
|
|
28
28
|
files: string[];
|
|
29
29
|
}>;
|
|
30
30
|
/** All rule violations with their effective severity. Only `error` severity fails validation. */
|