structured-context 0.13.1 → 0.15.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 +5 -3
- package/dist/api.d.ts +3 -2
- package/dist/api.js +3 -2
- package/dist/commands/dump.js +1 -1
- package/dist/commands/spaces.d.ts +5 -0
- package/dist/commands/spaces.js +46 -10
- package/dist/commands/validate.js +8 -8
- package/dist/config/health.d.ts +31 -0
- package/dist/config/health.js +86 -0
- package/dist/{config.d.ts → config/index.d.ts} +18 -3
- package/dist/{config.js → config/index.js} +76 -37
- package/dist/config/write.d.ts +27 -0
- package/dist/config/write.js +91 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +8 -0
- 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.js +13 -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 +10 -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 +26 -8
- package/docs/config.md +31 -6
- package/docs/rules.md +17 -13
- package/package.json +4 -4
- package/schemas/_ost_strict.json +1 -1
package/dist/constants.js
CHANGED
|
@@ -4,3 +4,5 @@ export const CLI_NAME = 'sctx';
|
|
|
4
4
|
export const PLUGIN_PREFIX = `${CLI_NAME}-`;
|
|
5
5
|
export const ENV_CONFIG_VAR = 'SCTX_CONFIG';
|
|
6
6
|
export const SCHEMA_URI_SCHEME = CLI_NAME;
|
|
7
|
+
/** Name of the config file discovered by walking up from the working directory. */
|
|
8
|
+
export const LOCAL_CONFIG_FILE = '.sctx.json';
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable codes for configuration and environment problems. Hooks and agent skills map a code to a
|
|
3
|
+
* fix, so the message wording can change without breaking them.
|
|
4
|
+
*/
|
|
5
|
+
export type SctxErrorCode = 'E_CONFIG_NOT_FOUND' | 'E_CONFIG_INVALID' | 'E_INCLUDE_NOT_FOUND' | 'E_NO_SPACES' | 'E_SPACE_PATH_MISSING' | 'E_SCHEMA_MISSING' | 'E_SCHEMA_NOT_FOUND' | 'E_SCHEMA_INVALID' | 'E_SPACE_EXISTS' | 'E_TARGET_REQUIRED' | 'E_CONFIG_NEEDS_MANUAL_EDIT';
|
|
6
|
+
export declare class SctxError extends Error {
|
|
7
|
+
readonly code: SctxErrorCode;
|
|
8
|
+
constructor(code: SctxErrorCode, message: string);
|
|
9
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -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
|
|
22
|
-
* - descendants: BFS via childrenIndex, nearest first, deduplicated by
|
|
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<
|
|
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
|
-
|
|
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
|
|
24
|
-
* - descendants: BFS via childrenIndex, nearest first, deduplicated by
|
|
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.
|
|
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
|
|
51
|
-
if (visited.has(
|
|
51
|
+
const id = item.node.id;
|
|
52
|
+
if (visited.has(id))
|
|
52
53
|
continue;
|
|
53
|
-
visited.add(
|
|
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.
|
|
61
|
-
if (parentNode && !visited.has(ref.
|
|
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
|
|
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(
|
|
75
|
+
const directChildren = childrenIndex.get(nodeId) ?? [];
|
|
75
76
|
for (const child of directChildren) {
|
|
76
|
-
const ref = child.resolvedParents.find((r) => r.
|
|
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
|
|
83
|
-
if (visited.has(
|
|
83
|
+
const id = item.childNode.id;
|
|
84
|
+
if (visited.has(id))
|
|
84
85
|
continue;
|
|
85
|
-
visited.add(
|
|
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(
|
|
91
|
+
const grandchildren = childrenIndex.get(id) ?? [];
|
|
91
92
|
for (const grandchild of grandchildren) {
|
|
92
|
-
if (!visited.has(grandchild.
|
|
93
|
-
const ref = grandchild.resolvedParents.find((r) => r.
|
|
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<
|
|
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.
|
|
88
|
+
const seen = new Set(matchedNodes.map((n) => n.id));
|
|
89
89
|
const result = [...matchedNodes];
|
|
90
|
-
function
|
|
91
|
-
if (seen.has(
|
|
90
|
+
function addById(id) {
|
|
91
|
+
if (seen.has(id))
|
|
92
92
|
return;
|
|
93
|
-
const node = nodeIndex.get(
|
|
93
|
+
const node = nodeIndex.get(id);
|
|
94
94
|
if (node) {
|
|
95
|
-
seen.add(
|
|
95
|
+
seen.add(id);
|
|
96
96
|
result.push(node);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
for (const node of matchedNodes) {
|
|
100
|
-
const
|
|
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,
|
|
104
|
+
applyDirective(node, aug, directive, childrenIndex, addById);
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
return result;
|
|
109
108
|
}
|
|
110
|
-
function applyDirective(node, aug, directive, childrenIndex,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
130
|
+
const siblings = childrenIndex.get(parentRef.id) ?? [];
|
|
132
131
|
for (const sibling of siblings) {
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { listPlugins } from './commands/plugins';
|
|
|
10
10
|
import { render, renderList } from './commands/render';
|
|
11
11
|
import { listSchemas, showSchema } from './commands/schemas';
|
|
12
12
|
import { show } from './commands/show';
|
|
13
|
-
import { listSpaces } from './commands/spaces';
|
|
13
|
+
import { addSpaceCommand, listSpaces } from './commands/spaces';
|
|
14
14
|
import { templateSync } from './commands/template-sync';
|
|
15
15
|
import { validate, watchValidate } from './commands/validate';
|
|
16
16
|
import { validateFileCommand } from './commands/validate-file';
|
|
@@ -54,7 +54,7 @@ program
|
|
|
54
54
|
.name(CLI_NAME)
|
|
55
55
|
.description('Structured context validation and diagram generation tool')
|
|
56
56
|
.version(packageJson.version)
|
|
57
|
-
.option('--config <path>', 'Path to config file (overrides
|
|
57
|
+
.option('--config <path>', 'Path to config file (overrides config discovery)');
|
|
58
58
|
program.hook('preAction', () => {
|
|
59
59
|
setConfigPath(program.opts().config);
|
|
60
60
|
});
|
|
@@ -150,6 +150,17 @@ spacesCmd
|
|
|
150
150
|
.description('List all configured spaces and their paths')
|
|
151
151
|
.option('--json', 'Output as JSON: config files, and each space with its path and schema')
|
|
152
152
|
.action((options) => listSpaces(options));
|
|
153
|
+
spacesCmd
|
|
154
|
+
.command('add')
|
|
155
|
+
.description('Add a space to a config file, creating the file if needed')
|
|
156
|
+
.argument('<name>', 'Space name (lowercase letters, digits, - and _)')
|
|
157
|
+
.argument('<path>', 'Path to the space (a folder or a file); must exist')
|
|
158
|
+
.option('--schema <schema>', 'Schema name or path (required unless the config names a top-level schema)')
|
|
159
|
+
.option('--global', 'Write to the global config file')
|
|
160
|
+
.option('--json', 'Output as JSON')
|
|
161
|
+
.addHelpText('after', '\nWrites to the config found by walking up from the working directory, or the one named with --config.\n' +
|
|
162
|
+
'If only the global config (or none) exists, name the target: --config <folder>/.sctx.json or --global.')
|
|
163
|
+
.action((name, path, options) => addSpaceCommand(name, path, options));
|
|
153
164
|
program.addCommand(spacesCmd);
|
|
154
165
|
const providersCmd = new Command('providers').description('List model providers (bundled and configured) with their effective config');
|
|
155
166
|
providersCmd
|
package/dist/judge/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
53
|
+
.map((p) => graph.nodes.get(p.id))
|
|
54
54
|
.filter((p) => p !== undefined);
|
|
55
|
-
const childrenOf = (n) => graph.hierarchyChildren.get(n.
|
|
55
|
+
const childrenOf = (n) => graph.hierarchyChildren.get(n.id) ?? [];
|
|
56
56
|
const found = new Map();
|
|
57
57
|
const add = (other, relation) => {
|
|
58
|
-
if (other.
|
|
58
|
+
if (other.id === node.id)
|
|
59
59
|
return;
|
|
60
|
-
const existing = found.get(other.
|
|
60
|
+
const existing = found.get(other.id);
|
|
61
61
|
if (!existing)
|
|
62
|
-
found.set(other.
|
|
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.
|
|
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.
|
|
72
|
+
if (seen.has(n.id))
|
|
73
73
|
continue;
|
|
74
|
-
seen.add(n.
|
|
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
|
|
8
|
-
|
|
9
|
-
/** Severity of the
|
|
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. */
|
package/dist/judge/judge-node.js
CHANGED
|
@@ -7,7 +7,7 @@ function resolveAll(rules) {
|
|
|
7
7
|
function errorJudgements(rules, message) {
|
|
8
8
|
return rules.map((rule) => ({
|
|
9
9
|
rule,
|
|
10
|
-
|
|
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,
|
|
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,
|
|
25
|
+
return { rule, violated: true, evidence: `Error during evaluation: ${message}`, error: message };
|
|
26
26
|
}
|
|
27
|
-
const
|
|
27
|
+
const violated = severity !== undefined;
|
|
28
28
|
return {
|
|
29
29
|
rule,
|
|
30
|
-
|
|
30
|
+
violated,
|
|
31
31
|
severity,
|
|
32
32
|
verdict: result.verdict,
|
|
33
|
-
guidance:
|
|
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.
|
|
65
|
+
report.nodes.add(node.id);
|
|
66
66
|
for (const rule of unitRules)
|
|
67
67
|
report.ruleIds.add(rule.id);
|
|
68
68
|
}
|
package/dist/judge/prompt.js
CHANGED
|
@@ -34,12 +34,12 @@ export function buildJudgeUserPrompt({ node, related = [] }, rule) {
|
|
|
34
34
|
...verdictList(rule),
|
|
35
35
|
'',
|
|
36
36
|
'# Node Under Evaluation',
|
|
37
|
-
`-
|
|
37
|
+
`- Id: ${node.id}`,
|
|
38
38
|
`- Title: ${node.title}`,
|
|
39
39
|
`- Type: ${node.type} (resolved: ${node.resolvedType})`,
|
|
40
40
|
];
|
|
41
|
-
if (node.
|
|
42
|
-
lines.push(`- Parents: ${node.
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
+
...(node.parentTitles.length ? [`Parents: ${node.parentTitles.join(', ')}`] : []),
|
|
26
26
|
...fieldLines(node.schemaData ?? {}),
|
|
27
27
|
...(input.related ?? []).flatMap((r) => [
|
|
28
28
|
'',
|
package/dist/judge/types.d.ts
CHANGED
|
@@ -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, '
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|