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.
Files changed (49) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/dump.js +1 -1
  3. package/dist/commands/validate.js +8 -8
  4. package/dist/filter/augment-nodes.d.ts +5 -4
  5. package/dist/filter/augment-nodes.js +20 -19
  6. package/dist/filter/expand-include.d.ts +2 -2
  7. package/dist/filter/expand-include.js +15 -17
  8. package/dist/filter/filter-nodes.js +2 -2
  9. package/dist/judge/context.js +12 -12
  10. package/dist/judge/judge-node.d.ts +3 -3
  11. package/dist/judge/judge-node.js +7 -7
  12. package/dist/judge/prompt.js +3 -3
  13. package/dist/judge/provider/fake.d.ts +1 -1
  14. package/dist/judge/provider/fake.js +3 -6
  15. package/dist/judge/provider/jev.js +1 -1
  16. package/dist/judge/provider/openai-decisions.js +1 -1
  17. package/dist/judge/types.d.ts +4 -1
  18. package/dist/plugins/markdown/index.d.ts +6 -0
  19. package/dist/plugins/markdown/index.js +1 -0
  20. package/dist/plugins/markdown/parse-embedded.d.ts +7 -2
  21. package/dist/plugins/markdown/parse-embedded.js +23 -21
  22. package/dist/plugins/markdown/read-space.js +9 -8
  23. package/dist/plugins/markdown/render-bullets.js +3 -3
  24. package/dist/plugins/markdown/util.d.ts +11 -0
  25. package/dist/plugins/markdown/util.js +21 -0
  26. package/dist/plugins/mermaid/render-mermaid.js +5 -6
  27. package/dist/plugins/miro/cache.js +1 -1
  28. package/dist/plugins/miro/sync.js +3 -2
  29. package/dist/read/resolve-graph-edges.js +12 -18
  30. package/dist/schema/evaluate-rule.d.ts +4 -4
  31. package/dist/schema/evaluate-rule.js +14 -10
  32. package/dist/schema/gating.d.ts +10 -10
  33. package/dist/schema/gating.js +45 -44
  34. package/dist/schema/validate-graph.js +8 -11
  35. package/dist/schema/validate-rules.d.ts +4 -4
  36. package/dist/schema/validate-rules.js +15 -17
  37. package/dist/schemas/_ost_strict.json +1 -1
  38. package/dist/space-graph.d.ts +9 -9
  39. package/dist/space-graph.js +16 -16
  40. package/dist/testing.d.ts +1 -1
  41. package/dist/testing.js +3 -3
  42. package/dist/types.d.ts +14 -10
  43. package/dist/validate.d.ts +2 -2
  44. package/dist/validate.js +18 -19
  45. package/docs/concepts.md +25 -7
  46. package/docs/config.md +8 -0
  47. package/docs/rules.md +17 -13
  48. package/package.json +1 -1
  49. package/schemas/_ost_strict.json +1 -1
package/README.md CHANGED
@@ -27,7 +27,7 @@ Content and structure
27
27
  Valid 16
28
28
  Schema validation errors 0
29
29
  Broken links 1
30
- Duplicate keys 0
30
+ Duplicate ids 0
31
31
  Rule violations (error) 0
32
32
  Rule violations (warning) 1
33
33
  Rule violations (info) 1
@@ -138,7 +138,7 @@ Validates markdown files against the JSON schema:
138
138
  - Extracts YAML frontmatter from each `.md` file
139
139
  - Skips files without frontmatter or without a `type` field
140
140
  - Reports validation results with counts and per-file errors
141
- - Runs executable rules (`$metadata.rules`) with **cascade gating**: a schema, hierarchy, or blocking-rule failure on a node suppresses rule evaluation for that node's hierarchy subtree. Gated rules are reported in a Coverage section so you know what wasn't evaluated and why. See [docs/rules.md](docs/rules.md#cascade-gating) for details.
141
+ - Runs executable rules (`$metadata.rules`) with **cascade gating**: a schema, hierarchy or gating rule violation on a node suppresses rule evaluation for that node's hierarchy subtree. Gated rules are reported in a Coverage section so you know what wasn't evaluated and why. See [docs/rules.md](docs/rules.md#cascade-gating) for details.
142
142
  - `--depth <n>` limits evaluation to hierarchy nodes within `n` breadth-first levels from the top — useful for staged agentic fix loops (fix the top level, then request the next).
143
143
  - `--include-gated` evaluates rules even on gated nodes (still reported as gated) — for surfacing cheap bottom-up checks on request.
144
144
  - `--semantic` also evaluates the `semantic` part of rules with the configured model provider (Jev, Anthropic or OpenAI-compatible); `--semantic <provider>` uses that configured model provider instead; `--rule-filter` narrows which rules run. See [docs/semantic-rules.md](docs/semantic-rules.md) (`sctx docs semantic-rules`).
@@ -11,7 +11,7 @@ export async function buildDump(context, options = {}) {
11
11
  if (!options.filter)
12
12
  return readResult;
13
13
  const graph = await assembleSpaceGraph(context, { ...options, readResult });
14
- const nodes = readResult.nodes.filter((node) => graph.nodes.get(node.title) === node);
14
+ const nodes = readResult.nodes.filter((node) => graph.nodes.get(node.id) === node);
15
15
  return { ...readResult, nodes };
16
16
  }
17
17
  export async function dump(context, options = {}) {
@@ -34,10 +34,10 @@ export async function validate(context, options = {}) {
34
34
  for (const { file, parent, error } of result.refErrors) {
35
35
  addError(file, `broken-link:${parent}`, 'broken-link', `${parent} → ${error}`);
36
36
  }
37
- for (const { title, files } of result.duplicateErrors) {
37
+ for (const { id, files } of result.duplicateErrors) {
38
38
  for (const file of files) {
39
39
  const others = files.filter((f) => f !== file);
40
- addError(file, `duplicate:${title}`, 'duplicate', `Duplicate title "${title}" also exists in: ${others.join(', ')}`);
40
+ addError(file, `duplicate:${id}`, 'duplicate', `Duplicate id "${id}" also exists in: ${others.join(', ')}`);
41
41
  }
42
42
  }
43
43
  for (const v of result.ruleViolations) {
@@ -97,7 +97,7 @@ export async function validate(context, options = {}) {
97
97
  console.log(fmt(' Valid', result.validCount));
98
98
  console.log(fmt(' Schema validation errors', result.nodeErrorCount, true));
99
99
  console.log(fmt(' Broken links', result.refErrors.length, true));
100
- console.log(fmt(' Duplicate keys', result.duplicateErrors.length, true));
100
+ console.log(fmt(' Duplicate ids', result.duplicateErrors.length, true));
101
101
  console.log(fmt(' Rule violations (error)', ruleCounts.error, true));
102
102
  console.log(fmt(' Rule violations (warning)', ruleCounts.warning, true, true));
103
103
  console.log(fmt(' Rule violations (info)', ruleCounts.info, true, true));
@@ -114,7 +114,7 @@ export async function validate(context, options = {}) {
114
114
  if (result.orphans.length > 0) {
115
115
  console.log(`\nOrphans (hierarchy nodes - no parent):`);
116
116
  for (const node of result.orphans)
117
- console.log(` ${node.label}`);
117
+ console.log(` ${node.id}`);
118
118
  }
119
119
  if (result.unresolvedContentLinks.length > 0) {
120
120
  console.log(`\nUnresolved content links (may be outside-space or broken):`);
@@ -157,9 +157,9 @@ export async function validate(context, options = {}) {
157
157
  });
158
158
  }
159
159
  if (result.duplicateErrors.length > 0) {
160
- console.log(`\nDuplicate keys (same title in multiple files):`);
161
- result.duplicateErrors.forEach(({ title, files }) => {
162
- console.log(` "${title}":`);
160
+ console.log(`\nDuplicate ids (same id on multiple nodes):`);
161
+ result.duplicateErrors.forEach(({ id, files }) => {
162
+ console.log(` "${id}":`);
163
163
  for (const f of files) {
164
164
  console.log(` ${f}`);
165
165
  }
@@ -194,7 +194,7 @@ export async function validate(context, options = {}) {
194
194
  console.log(`\nCoverage — rule checks gated by cascade (use --include-gated to evaluate anyway):`);
195
195
  for (const gate of result.coverageGates) {
196
196
  const cause = gate.reason === 'rule' ? `blocking rule "${gate.ruleId}"` : `${gate.reason} failure`;
197
- console.log(` ${gate.file} (${gate.nodeTitle}) — ${cause} gated ${gate.gatedFiles.length} node(s):`);
197
+ console.log(` ${gate.file} (${gate.nodeId}) — ${cause} gated ${gate.gatedFiles.length} node(s):`);
198
198
  for (const file of gate.gatedFiles)
199
199
  console.log(` ${file}`);
200
200
  }
@@ -1,4 +1,4 @@
1
- import type { SpaceNode } from '../types';
1
+ import type { NodeId, SpaceNode } from '../types';
2
2
  /** Edge metadata merged into each ancestor/descendant entry. */
3
3
  export type EdgeMetadata = {
4
4
  _field: string;
@@ -11,6 +11,7 @@ export type EdgeMetadata = {
11
11
  export type AugmentedFlatNode = Record<string, unknown> & {
12
12
  resolvedType: string;
13
13
  resolvedParentTitles: string[];
14
+ resolvedParentIds: NodeId[];
14
15
  ancestors: Array<Record<string, unknown> & EdgeMetadata>;
15
16
  descendants: Array<Record<string, unknown> & EdgeMetadata>;
16
17
  };
@@ -18,8 +19,8 @@ export type AugmentedFlatNode = Record<string, unknown> & {
18
19
  * Build the augmented flat representation of a node, including pre-computed
19
20
  * ancestors[] and descendants[] arrays with edge metadata.
20
21
  *
21
- * - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by title.
22
- * - descendants: BFS via childrenIndex, nearest first, deduplicated by title.
22
+ * - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by id.
23
+ * - descendants: BFS via childrenIndex, nearest first, deduplicated by id.
23
24
  * - Each entry merges the parent/child node's fields with edge metadata (_field, _source, _hierarchy, _selfRef).
24
25
  */
25
- export declare function augmentNode(node: SpaceNode, nodeIndex: ReadonlyMap<string, SpaceNode>, childrenIndex: ReadonlyMap<string, readonly SpaceNode[]>): AugmentedFlatNode;
26
+ export declare function augmentNode(node: SpaceNode, nodeIndex: ReadonlyMap<NodeId, SpaceNode>, childrenIndex: ReadonlyMap<NodeId, readonly SpaceNode[]>): AugmentedFlatNode;
@@ -11,7 +11,7 @@ function edgeMetadata(ref) {
11
11
  function flattenData(node) {
12
12
  return {
13
13
  ...node.schemaData,
14
- label: node.label,
14
+ id: node.id,
15
15
  title: node.title,
16
16
  resolvedType: node.resolvedType,
17
17
  };
@@ -20,8 +20,8 @@ function flattenData(node) {
20
20
  * Build the augmented flat representation of a node, including pre-computed
21
21
  * ancestors[] and descendants[] arrays with edge metadata.
22
22
  *
23
- * - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by title.
24
- * - descendants: BFS via childrenIndex, nearest first, deduplicated by title.
23
+ * - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by id.
24
+ * - descendants: BFS via childrenIndex, nearest first, deduplicated by id.
25
25
  * - Each entry merges the parent/child node's fields with edge metadata (_field, _source, _hierarchy, _selfRef).
26
26
  */
27
27
  export function augmentNode(node, nodeIndex, childrenIndex) {
@@ -30,7 +30,8 @@ export function augmentNode(node, nodeIndex, childrenIndex) {
30
30
  return {
31
31
  ...flattenData(node),
32
32
  resolvedType: node.resolvedType,
33
- resolvedParentTitles: node.resolvedParents.map((r) => r.title),
33
+ resolvedParentTitles: node.resolvedParents.map((r) => nodeIndex.get(r.id)?.title ?? r.id),
34
+ resolvedParentIds: node.resolvedParents.map((r) => r.id),
34
35
  ancestors,
35
36
  descendants,
36
37
  };
@@ -41,24 +42,24 @@ function buildAncestors(node, nodeIndex) {
41
42
  // BFS queue holds: { parentRef that led to this node, the node itself }
42
43
  const queue = [];
43
44
  for (const ref of node.resolvedParents) {
44
- const parentNode = nodeIndex.get(ref.title);
45
+ const parentNode = nodeIndex.get(ref.id);
45
46
  if (parentNode)
46
47
  queue.push({ ref, node: parentNode });
47
48
  }
48
49
  while (queue.length > 0) {
49
50
  const item = queue.shift();
50
- const title = item.node.title;
51
- if (visited.has(title))
51
+ const id = item.node.id;
52
+ if (visited.has(id))
52
53
  continue;
53
- visited.add(title);
54
+ visited.add(id);
54
55
  result.push({
55
56
  ...flattenData(item.node),
56
57
  ...edgeMetadata(item.ref),
57
58
  });
58
59
  // Continue BFS upward
59
60
  for (const ref of item.node.resolvedParents) {
60
- const parentNode = nodeIndex.get(ref.title);
61
- if (parentNode && !visited.has(ref.title)) {
61
+ const parentNode = nodeIndex.get(ref.id);
62
+ if (parentNode && !visited.has(ref.id)) {
62
63
  queue.push({ ref, node: parentNode });
63
64
  }
64
65
  }
@@ -66,31 +67,31 @@ function buildAncestors(node, nodeIndex) {
66
67
  return result;
67
68
  }
68
69
  function buildDescendants(node, childrenIndex) {
69
- const nodeTitle = node.title;
70
+ const nodeId = node.id;
70
71
  const visited = new Set();
71
72
  const result = [];
72
73
  // BFS queue holds: child node + the resolvedParents entry on that child that points to its parent
73
74
  const queue = [];
74
- const directChildren = childrenIndex.get(nodeTitle) ?? [];
75
+ const directChildren = childrenIndex.get(nodeId) ?? [];
75
76
  for (const child of directChildren) {
76
- const ref = child.resolvedParents.find((r) => r.title === nodeTitle);
77
+ const ref = child.resolvedParents.find((r) => r.id === nodeId);
77
78
  if (ref)
78
79
  queue.push({ childNode: child, ref });
79
80
  }
80
81
  while (queue.length > 0) {
81
82
  const item = queue.shift();
82
- const title = item.childNode.title;
83
- if (visited.has(title))
83
+ const id = item.childNode.id;
84
+ if (visited.has(id))
84
85
  continue;
85
- visited.add(title);
86
+ visited.add(id);
86
87
  result.push({
87
88
  ...flattenData(item.childNode),
88
89
  ...edgeMetadata(item.ref),
89
90
  });
90
- const grandchildren = childrenIndex.get(title) ?? [];
91
+ const grandchildren = childrenIndex.get(id) ?? [];
91
92
  for (const grandchild of grandchildren) {
92
- if (!visited.has(grandchild.title)) {
93
- const ref = grandchild.resolvedParents.find((r) => r.title === title);
93
+ if (!visited.has(grandchild.id)) {
94
+ const ref = grandchild.resolvedParents.find((r) => r.id === id);
94
95
  if (ref)
95
96
  queue.push({ childNode: grandchild, ref });
96
97
  }
@@ -1,4 +1,4 @@
1
- import type { SpaceNode } from '../types';
1
+ import type { NodeId, SpaceNode } from '../types';
2
2
  import type { AugmentedFlatNode } from './augment-nodes';
3
3
  export type AncestorsDirective = {
4
4
  kind: 'ancestors';
@@ -59,4 +59,4 @@ export declare function parseIncludeSpec(spec: string): IncludeDirective[];
59
59
  * @param childrenIndex - Title → direct children (all edges)
60
60
  * @param augmented - Title → AugmentedFlatNode with pre-computed ancestors/descendants
61
61
  */
62
- export declare function expandInclude(matchedNodes: SpaceNode[], directives: IncludeDirective[], nodeIndex: ReadonlyMap<string, SpaceNode>, childrenIndex: ReadonlyMap<string, readonly SpaceNode[]>, augmented: Map<string, AugmentedFlatNode>): SpaceNode[];
62
+ export declare function expandInclude(matchedNodes: SpaceNode[], directives: IncludeDirective[], nodeIndex: ReadonlyMap<NodeId, SpaceNode>, childrenIndex: ReadonlyMap<NodeId, readonly SpaceNode[]>, augmented: Map<NodeId, AugmentedFlatNode>): SpaceNode[];
@@ -85,34 +85,33 @@ function parseDirective(item) {
85
85
  export function expandInclude(matchedNodes, directives, nodeIndex, childrenIndex, augmented) {
86
86
  if (directives.length === 0)
87
87
  return matchedNodes;
88
- const seen = new Set(matchedNodes.map((n) => n.title));
88
+ const seen = new Set(matchedNodes.map((n) => n.id));
89
89
  const result = [...matchedNodes];
90
- function addByTitle(title) {
91
- if (seen.has(title))
90
+ function addById(id) {
91
+ if (seen.has(id))
92
92
  return;
93
- const node = nodeIndex.get(title);
93
+ const node = nodeIndex.get(id);
94
94
  if (node) {
95
- seen.add(title);
95
+ seen.add(id);
96
96
  result.push(node);
97
97
  }
98
98
  }
99
99
  for (const node of matchedNodes) {
100
- const title = node.title;
101
- const aug = augmented.get(title);
100
+ const aug = augmented.get(node.id);
102
101
  if (!aug)
103
102
  continue;
104
103
  for (const directive of directives) {
105
- applyDirective(node, aug, directive, childrenIndex, addByTitle);
104
+ applyDirective(node, aug, directive, childrenIndex, addById);
106
105
  }
107
106
  }
108
107
  return result;
109
108
  }
110
- function applyDirective(node, aug, directive, childrenIndex, addByTitle) {
109
+ function applyDirective(node, aug, directive, childrenIndex, addById) {
111
110
  switch (directive.kind) {
112
111
  case 'ancestors': {
113
112
  for (const a of aug.ancestors) {
114
113
  if (!directive.typeFilter || a.resolvedType === directive.typeFilter) {
115
- addByTitle(a.title);
114
+ addById(a.id);
116
115
  }
117
116
  }
118
117
  break;
@@ -120,7 +119,7 @@ function applyDirective(node, aug, directive, childrenIndex, addByTitle) {
120
119
  case 'descendants': {
121
120
  for (const d of aug.descendants) {
122
121
  if (!directive.typeFilter || d.resolvedType === directive.typeFilter) {
123
- addByTitle(d.title);
122
+ addById(d.id);
124
123
  }
125
124
  }
126
125
  break;
@@ -128,11 +127,10 @@ function applyDirective(node, aug, directive, childrenIndex, addByTitle) {
128
127
  case 'siblings': {
129
128
  // Nodes that share at least one parent with the matched node (any edge type)
130
129
  for (const parentRef of node.resolvedParents) {
131
- const siblings = childrenIndex.get(parentRef.title) ?? [];
130
+ const siblings = childrenIndex.get(parentRef.id) ?? [];
132
131
  for (const sibling of siblings) {
133
- const siblingTitle = sibling.title;
134
- if (siblingTitle !== node.title) {
135
- addByTitle(siblingTitle);
132
+ if (sibling.id !== node.id) {
133
+ addById(sibling.id);
136
134
  }
137
135
  }
138
136
  }
@@ -142,13 +140,13 @@ function applyDirective(node, aug, directive, childrenIndex, addByTitle) {
142
140
  // Relationship-sourced ancestors (matched node is the child side)
143
141
  for (const a of aug.ancestors) {
144
142
  if (a._source === 'relationship' && matchesRelSpec(node, a, directive, true)) {
145
- addByTitle(a.title);
143
+ addById(a.id);
146
144
  }
147
145
  }
148
146
  // Relationship-sourced descendants (matched node is the parent side)
149
147
  for (const d of aug.descendants) {
150
148
  if (d._source === 'relationship' && matchesRelSpec(node, d, directive, false)) {
151
- addByTitle(d.title);
149
+ addById(d.id);
152
150
  }
153
151
  }
154
152
  break;
@@ -30,7 +30,7 @@ export async function filterNodes(expression, graph) {
30
30
  // Pre-augment all nodes once (ancestors/descendants needed for WHERE predicates and SELECT expansion)
31
31
  const augmented = new Map();
32
32
  for (const node of nodeIndex.values()) {
33
- augmented.set(node.title, augmentNode(node, nodeIndex, childrenIndex));
33
+ augmented.set(node.id, augmentNode(node, nodeIndex, childrenIndex));
34
34
  }
35
35
  // Step 1: apply WHERE clause to get the matched set
36
36
  let matched;
@@ -48,7 +48,7 @@ export async function filterNodes(expression, graph) {
48
48
  }
49
49
  matched = [];
50
50
  for (const node of nodeIndex.values()) {
51
- const current = augmented.get(node.title);
51
+ const current = augmented.get(node.id);
52
52
  if (!current)
53
53
  continue;
54
54
  // Spread current node fields at root level so bare field names work in expressions
@@ -25,14 +25,14 @@ function dataWithBody(node, withBody) {
25
25
  return data;
26
26
  }
27
27
  /** The evaluated node as a judge sees it. */
28
- function judgeNodeView(node, withBody) {
28
+ function judgeNodeView(node, withBody, graph) {
29
29
  return {
30
- label: node.label,
30
+ id: node.id,
31
31
  title: node.title,
32
32
  type: node.type,
33
33
  resolvedType: node.resolvedType,
34
34
  schemaData: dataWithBody(node, withBody),
35
- resolvedParents: node.resolvedParents,
35
+ parentTitles: node.resolvedParents.map((p) => graph.nodes.get(p.id)?.title ?? p.id),
36
36
  };
37
37
  }
38
38
  function relatedView(node, relation, withBody) {
@@ -50,28 +50,28 @@ export function relatedNodes(graph, node, context) {
50
50
  ? []
51
51
  : n.resolvedParents
52
52
  .filter((p) => p.source === 'hierarchy' && p.hierarchy === hierarchyName)
53
- .map((p) => graph.nodes.get(p.title))
53
+ .map((p) => graph.nodes.get(p.id))
54
54
  .filter((p) => p !== undefined);
55
- const childrenOf = (n) => graph.hierarchyChildren.get(n.title) ?? [];
55
+ const childrenOf = (n) => graph.hierarchyChildren.get(n.id) ?? [];
56
56
  const found = new Map();
57
57
  const add = (other, relation) => {
58
- if (other.title === node.title)
58
+ if (other.id === node.id)
59
59
  return;
60
- const existing = found.get(other.title);
60
+ const existing = found.get(other.id);
61
61
  if (!existing)
62
- found.set(other.title, relatedView(other, relation, withBody));
62
+ found.set(other.id, relatedView(other, relation, withBody));
63
63
  else if (!existing.relations.includes(relation))
64
64
  existing.relations.push(relation);
65
65
  };
66
66
  const reach = (next, relation) => {
67
- const seen = new Set([node.title]);
67
+ const seen = new Set([node.id]);
68
68
  let level = next(node);
69
69
  while (level.length > 0) {
70
70
  const following = [];
71
71
  for (const n of level) {
72
- if (seen.has(n.title))
72
+ if (seen.has(n.id))
73
73
  continue;
74
- seen.add(n.title);
74
+ seen.add(n.id);
75
75
  add(n, relation);
76
76
  following.push(...next(n));
77
77
  }
@@ -99,7 +99,7 @@ export function relatedNodes(graph, node, context) {
99
99
  export function judgeContext(graph, node, context) {
100
100
  const related = relatedNodes(graph, node, context);
101
101
  return {
102
- node: judgeNodeView(node, context.content !== 'none'),
102
+ node: judgeNodeView(node, context.content !== 'none', graph),
103
103
  ...(related.length > 0 && { related }),
104
104
  };
105
105
  }
@@ -4,9 +4,9 @@ import { type TrimReport } from './trim';
4
4
  import type { SemanticJudgeProvider } from './types';
5
5
  export type NodeJudgement = {
6
6
  rule: Rule;
7
- /** True when the verdict fails the rule (it has a severity), or the provider errored. */
8
- failed: boolean;
9
- /** Severity of the failing verdict; unset for a judge error, which takes the rule's own severity. */
7
+ /** True when the verdict has a severity (so it reports a violation), or the provider errored. */
8
+ violated: boolean;
9
+ /** Severity of the verdict; unset for a judge error, which takes the rule's own severity. */
10
10
  severity?: RuleSeverity;
11
11
  verdict?: string;
12
12
  /** User-facing guidance declared for the failing verdict. */
@@ -7,7 +7,7 @@ function resolveAll(rules) {
7
7
  function errorJudgements(rules, message) {
8
8
  return rules.map((rule) => ({
9
9
  rule,
10
- failed: true,
10
+ violated: true,
11
11
  evidence: `Error during evaluation: ${message}`,
12
12
  error: message,
13
13
  }));
@@ -17,20 +17,20 @@ function interpret(rules, resolved, results) {
17
17
  const result = results[rule.id];
18
18
  if (!result || isJudgeError(result)) {
19
19
  const message = result ? result.error : 'Model provider returned no result for this rule';
20
- return { rule, failed: true, evidence: `Error during evaluation: ${message}`, error: message };
20
+ return { rule, violated: true, evidence: `Error during evaluation: ${message}`, error: message };
21
21
  }
22
22
  const severity = resolved[i].severities[result.verdict];
23
23
  if (severity === undefined && !(result.verdict in resolved[i].spec.verdicts)) {
24
24
  const message = `Model provider returned unknown verdict "${result.verdict}" (allowed: ${Object.keys(resolved[i].spec.verdicts).join(', ')})`;
25
- return { rule, failed: true, evidence: `Error during evaluation: ${message}`, error: message };
25
+ return { rule, violated: true, evidence: `Error during evaluation: ${message}`, error: message };
26
26
  }
27
- const failed = severity !== undefined;
27
+ const violated = severity !== undefined;
28
28
  return {
29
29
  rule,
30
- failed,
30
+ violated,
31
31
  severity,
32
32
  verdict: result.verdict,
33
- guidance: failed ? resolved[i].guidance[result.verdict] : undefined,
33
+ guidance: violated ? resolved[i].guidance[result.verdict] : undefined,
34
34
  evidence: result.evidence,
35
35
  };
36
36
  });
@@ -62,7 +62,7 @@ function planUnits(graph, items, report) {
62
62
  continue;
63
63
  }
64
64
  if (fitted.trimmed && report) {
65
- report.nodes.add(node.label);
65
+ report.nodes.add(node.id);
66
66
  for (const rule of unitRules)
67
67
  report.ruleIds.add(rule.id);
68
68
  }
@@ -34,12 +34,12 @@ export function buildJudgeUserPrompt({ node, related = [] }, rule) {
34
34
  ...verdictList(rule),
35
35
  '',
36
36
  '# Node Under Evaluation',
37
- `- File / Label: ${node.label}`,
37
+ `- Id: ${node.id}`,
38
38
  `- Title: ${node.title}`,
39
39
  `- Type: ${node.type} (resolved: ${node.resolvedType})`,
40
40
  ];
41
- if (node.resolvedParents && node.resolvedParents.length > 0) {
42
- lines.push(`- Parents: ${node.resolvedParents.map((p) => p.title).join(', ')}`);
41
+ if (node.parentTitles.length > 0) {
42
+ lines.push(`- Parents: ${node.parentTitles.join(', ')}`);
43
43
  }
44
44
  lines.push('', '## Node Content & Frontmatter:', '```json', JSON.stringify(node.schemaData, null, 2), '```', '');
45
45
  if (related.length > 0) {
@@ -11,7 +11,7 @@ export type FakeJudgeHandler = (call: {
11
11
  type FakeConfig = FakeAnswer | FakeJudgeHandler | Map<string, FakeAnswer> | Record<string, FakeAnswer>;
12
12
  /**
13
13
  * In-memory fake judge for offline, deterministic tests. Configure with a single answer, a handler,
14
- * or answers keyed by `ruleId:title`, `ruleId:label` or `ruleId` (most specific wins). Unmatched calls get `pass`,
14
+ * or answers keyed by `ruleId:title`, `ruleId:id` or `ruleId` (most specific wins). Unmatched calls get `pass`,
15
15
  * or the rule's first declared verdict.
16
16
  */
17
17
  export declare class FakeJudgeProvider implements SemanticJudgeProvider {
@@ -12,7 +12,7 @@ function normalise(answer) {
12
12
  }
13
13
  /**
14
14
  * In-memory fake judge for offline, deterministic tests. Configure with a single answer, a handler,
15
- * or answers keyed by `ruleId:title`, `ruleId:label` or `ruleId` (most specific wins). Unmatched calls get `pass`,
15
+ * or answers keyed by `ruleId:title`, `ruleId:id` or `ruleId` (most specific wins). Unmatched calls get `pass`,
16
16
  * or the rule's first declared verdict.
17
17
  */
18
18
  export class FakeJudgeProvider {
@@ -40,15 +40,12 @@ export class FakeJudgeProvider {
40
40
  if (isHandler(config))
41
41
  return await config({ node, rule });
42
42
  if (config instanceof Map) {
43
- return (config.get(`${rule.id}:${node.title}`) ??
44
- config.get(`${rule.id}:${node.label}`) ??
45
- config.get(rule.id) ??
46
- fallback);
43
+ return (config.get(`${rule.id}:${node.title}`) ?? config.get(`${rule.id}:${node.id}`) ?? config.get(rule.id) ?? fallback);
47
44
  }
48
45
  if (isAnswer(config))
49
46
  return config;
50
47
  const rec = config;
51
- return rec[`${rule.id}:${node.title}`] ?? rec[`${rule.id}:${node.label}`] ?? rec[rule.id] ?? fallback;
48
+ return rec[`${rule.id}:${node.title}`] ?? rec[`${rule.id}:${node.id}`] ?? rec[rule.id] ?? fallback;
52
49
  }
53
50
  clearHistory() {
54
51
  this.callHistory = [];
@@ -109,7 +109,7 @@ function nodeState(node, related = []) {
109
109
  return {
110
110
  title: node.title,
111
111
  type: node.resolvedType,
112
- parents: node.resolvedParents?.map((p) => p.title) ?? [],
112
+ parents: node.parentTitles,
113
113
  ...node.schemaData,
114
114
  ...(related.length > 0 && {
115
115
  relatedNodes: related.map((r) => ({
@@ -22,7 +22,7 @@ export class OpenAIDecisionsJudgeProvider {
22
22
  const stateLines = [
23
23
  `Title: ${node.title}`,
24
24
  `Type: ${node.resolvedType}`,
25
- ...(node.resolvedParents?.length ? [`Parents: ${node.resolvedParents.map((p) => p.title).join(', ')}`] : []),
25
+ ...(node.parentTitles.length ? [`Parents: ${node.parentTitles.join(', ')}`] : []),
26
26
  ...fieldLines(node.schemaData ?? {}),
27
27
  ...(input.related ?? []).flatMap((r) => [
28
28
  '',
@@ -26,7 +26,10 @@ export interface RelatedNode {
26
26
  */
27
27
  export interface SemanticJudgeInput {
28
28
  /** The evaluated node. Its body is in `schemaData.content` only when the rules' `includeContent` asks for it. */
29
- node: Pick<SpaceNode, 'label' | 'title' | 'type' | 'resolvedType' | 'schemaData' | 'resolvedParents'>;
29
+ node: Pick<SpaceNode, 'id' | 'title' | 'type' | 'resolvedType' | 'schemaData'> & {
30
+ /** Display titles of the node's parents. */
31
+ parentTitles: string[];
32
+ };
30
33
  /** Nodes the rules' `scope` adds, as a flat list. Absent when the scope is empty. */
31
34
  related?: RelatedNode[];
32
35
  rules: SemanticRuleSpec[];
@@ -32,6 +32,12 @@ export declare const MARKDOWN_CONFIG_SCHEMA: {
32
32
  readonly templatePrefix: {
33
33
  readonly type: "string";
34
34
  };
35
+ readonly folderIndex: {
36
+ readonly type: "array";
37
+ readonly items: {
38
+ readonly type: "string";
39
+ };
40
+ };
35
41
  readonly typeInference: {
36
42
  readonly type: "object";
37
43
  readonly properties: {
@@ -18,6 +18,7 @@ export const MARKDOWN_CONFIG_SCHEMA = {
18
18
  templateDir: { type: 'string', format: 'path' }, // format is hint to config loader to resolve relative directories
19
19
  fieldMap: { type: 'object', additionalProperties: { type: 'string' } },
20
20
  templatePrefix: { type: 'string' },
21
+ folderIndex: { type: 'array', items: { type: 'string' } },
21
22
  typeInference: TYPE_INFERENCE_CONFIG_SCHEMA,
22
23
  },
23
24
  additionalProperties: false,
@@ -57,10 +57,15 @@ export declare function normalizeHeadingSectionTarget(rawHeadingText: string): s
57
57
  export declare function defaultNodeType(stack: StackEntry[], hierarchy: readonly string[]): string;
58
58
  export interface ExtractEmbeddedOptions {
59
59
  /**
60
- * Title of the containing page. If provided (and pageType is not a space_on_a_page type),
60
+ * The containing page. If provided (and pageType is not a space_on_a_page type),
61
61
  * the page acts as a virtual depth-0 parent for first-level embedded headings.
62
+ * `id` is the page's node id (embedded node ids are `<id>#<title>`); `targets` are its
63
+ * link targets, most specific first (the first is used in generated parent links).
62
64
  */
63
- pageTitle?: string;
65
+ page?: {
66
+ id: string;
67
+ targets: string[];
68
+ };
64
69
  /**
65
70
  * Node type of the containing page.
66
71
  * - If set to a real node type: only headings with an explicit `[type:: x]` field or a