structured-context 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -198
- package/dist/api.d.ts +17 -15
- package/dist/api.js +12 -9
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/docs.js +1 -0
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +133 -22
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +24 -6
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +79 -23
- package/dist/config.d.ts +25 -1
- package/dist/config.js +106 -12
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -31
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/load-space-graph.d.ts +7 -5
- package/dist/load-space-graph.js +19 -8
- package/dist/plugins/loader.js +3 -3
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
- package/dist/plugins/markdown/parse-embedded.js +195 -24
- package/dist/plugins/markdown/read-space.js +42 -14
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +952 -183
- package/dist/schema/metadata-contract.js +177 -33
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.d.ts +14 -8
- package/dist/schema/schema.js +308 -199
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +16 -3
- package/dist/schema/validate-rules.js +91 -29
- package/dist/schemas/_knowledge_wiki.json +24 -24
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +17 -8
- package/dist/schemas/_sctx_base.json +3 -3
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/okf.json +8 -0
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +5 -2
- package/dist/space-context.js +9 -4
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +64 -16
- package/dist/validate.d.ts +62 -9
- package/dist/validate.js +229 -114
- package/docs/concepts.md +65 -9
- package/docs/config.md +78 -1
- package/docs/rules.md +168 -50
- package/docs/schemas.md +152 -52
- package/package.json +17 -11
- package/schemas/_knowledge_wiki.json +24 -24
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +17 -8
- package/schemas/_sctx_base.json +3 -3
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/okf.json +8 -0
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
package/dist/index.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { format } from 'node:util';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
|
-
import { diagram } from './commands/diagram';
|
|
5
|
-
import { docs } from './commands/docs';
|
|
6
|
-
import { dump } from './commands/dump';
|
|
7
|
-
import { miroSyncCommand } from './commands/miro-sync';
|
|
8
|
-
import { listPlugins } from './commands/plugins';
|
|
9
|
-
import { render, renderList } from './commands/render';
|
|
10
|
-
import { listSchemas, showSchema } from './commands/schemas';
|
|
11
|
-
import { show } from './commands/show';
|
|
12
|
-
import { listSpaces } from './commands/spaces';
|
|
13
|
-
import { templateSync } from './commands/template-sync';
|
|
14
|
-
import { validate, watchValidate } from './commands/validate';
|
|
15
|
-
import { validateFileCommand } from './commands/validate-file';
|
|
16
|
-
import { loadConfig, setConfigPath } from './config';
|
|
17
|
-
import { CLI_NAME } from './constants';
|
|
18
|
-
import {
|
|
5
|
+
import { diagram } from './commands/diagram.js';
|
|
6
|
+
import { docs } from './commands/docs.js';
|
|
7
|
+
import { dump } from './commands/dump.js';
|
|
8
|
+
import { miroSyncCommand } from './commands/miro-sync.js';
|
|
9
|
+
import { listPlugins } from './commands/plugins.js';
|
|
10
|
+
import { render, renderList } from './commands/render.js';
|
|
11
|
+
import { listSchemas, showSchema } from './commands/schemas.js';
|
|
12
|
+
import { show } from './commands/show.js';
|
|
13
|
+
import { listSpaces } from './commands/spaces.js';
|
|
14
|
+
import { templateSync } from './commands/template-sync.js';
|
|
15
|
+
import { validate, watchValidate } from './commands/validate.js';
|
|
16
|
+
import { validateFileCommand } from './commands/validate-file.js';
|
|
17
|
+
import { loadConfig, setConfigPath } from './config.js';
|
|
18
|
+
import { CLI_NAME } from './constants.js';
|
|
19
|
+
import { JudgeConfigError } from './judge/factory.js';
|
|
20
|
+
import { createSpaceContext, SpaceNotFoundError } from './space-context.js';
|
|
21
|
+
// Once node:process is loaded (vfile, via unified, loads it), Bun's console.log drops piped output
|
|
22
|
+
// past 64KB when the reader is slow. process.stdout.write delivers all of it before exit.
|
|
23
|
+
console.log = (...args) => {
|
|
24
|
+
process.stdout.write(`${format(...args)}\n`);
|
|
25
|
+
};
|
|
19
26
|
export function buildSpaceContext(spaceName) {
|
|
20
27
|
try {
|
|
21
28
|
return createSpaceContext(spaceName, loadConfig());
|
|
@@ -28,6 +35,18 @@ export function buildSpaceContext(spaceName) {
|
|
|
28
35
|
throw err;
|
|
29
36
|
}
|
|
30
37
|
}
|
|
38
|
+
/** Run a validate command, reporting a misconfigured model provider as a plain error rather than a stack trace. */
|
|
39
|
+
async function reportJudgeConfigErrors(run) {
|
|
40
|
+
try {
|
|
41
|
+
return await run();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (!(err instanceof JudgeConfigError))
|
|
45
|
+
throw err;
|
|
46
|
+
console.error(`Error: ${err.message}`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
const require = createRequire(import.meta.url);
|
|
32
51
|
const packageJson = require('../package.json');
|
|
33
52
|
const program = new Command();
|
|
@@ -44,9 +63,16 @@ program
|
|
|
44
63
|
.description('Validate a single file within its space')
|
|
45
64
|
.argument('<path>', 'Path to the file to validate')
|
|
46
65
|
.option('--json', 'Output results as JSON (machine-readable, for hooks)')
|
|
66
|
+
.option('--include-gated', 'Evaluate rules even when the file is gated by an upstream schema/hierarchy/rule failure (still reported as gated)')
|
|
67
|
+
.option('--rule-filter <dimension=values>', 'Evaluate only matching rules; dimensions: category, severity, type, id (repeatable, e.g. severity=error)', (v, prev = []) => [...prev, v])
|
|
68
|
+
.option('--semantic [provider]', 'Evaluate semantic rules, with the configured model provider or the one named')
|
|
47
69
|
.action(async (filePath, options) => {
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
process.exitCode = await reportJudgeConfigErrors(() => validateFileCommand(filePath, {
|
|
71
|
+
json: options.json,
|
|
72
|
+
includeGated: options.includeGated,
|
|
73
|
+
semantic: options.semantic,
|
|
74
|
+
ruleFilter: options.ruleFilter,
|
|
75
|
+
}));
|
|
50
76
|
});
|
|
51
77
|
program
|
|
52
78
|
.command('validate')
|
|
@@ -54,14 +80,24 @@ program
|
|
|
54
80
|
.argument('<space-name>', 'Space name')
|
|
55
81
|
.option('-w, --watch', 'Watch for changes and re-run validation')
|
|
56
82
|
.option('--json', 'Output results as JSON')
|
|
83
|
+
.option('--depth <n>', 'Evaluate only hierarchy nodes within N breadth-first levels from the top (e.g. 1 = root level only)', (v) => parseInt(v, 10))
|
|
84
|
+
.option('--include-gated', 'Evaluate rules even on nodes gated by an upstream schema/hierarchy/rule failure (still reported as gated)')
|
|
85
|
+
.option('--rule-filter <dimension=values>', 'Evaluate only matching rules; dimensions: category, severity, type, id (repeatable, e.g. severity=error)', (v, prev = []) => [...prev, v])
|
|
86
|
+
.option('--semantic [provider]', 'Evaluate semantic rules, with the configured model provider or the one named')
|
|
57
87
|
.action(async (spaceName, options) => {
|
|
58
88
|
const context = buildSpaceContext(spaceName);
|
|
59
89
|
if (options.watch) {
|
|
60
90
|
await watchValidate(context);
|
|
61
91
|
}
|
|
62
92
|
else {
|
|
63
|
-
|
|
64
|
-
process.
|
|
93
|
+
// exitCode rather than process.exit(): exiting straight away can cut off large piped output
|
|
94
|
+
process.exitCode = await reportJudgeConfigErrors(() => validate(context, {
|
|
95
|
+
json: options.json,
|
|
96
|
+
depth: options.depth,
|
|
97
|
+
includeGated: options.includeGated,
|
|
98
|
+
semantic: options.semantic,
|
|
99
|
+
ruleFilter: options.ruleFilter,
|
|
100
|
+
}));
|
|
65
101
|
}
|
|
66
102
|
});
|
|
67
103
|
program
|
|
@@ -69,19 +105,23 @@ program
|
|
|
69
105
|
.description('Generate mermaid diagram from space')
|
|
70
106
|
.argument('<space-name>', 'Space name')
|
|
71
107
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
108
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
72
109
|
.option('-o, --output <path>', 'Output file path (default: stdout)')
|
|
73
|
-
.action((spaceName, options) => diagram(buildSpaceContext(spaceName), options));
|
|
110
|
+
.action(async (spaceName, options) => diagram(buildSpaceContext(spaceName), options));
|
|
74
111
|
program
|
|
75
112
|
.command('show')
|
|
76
113
|
.description('Print space graph as a bullet list')
|
|
77
114
|
.argument('<space-name>', 'Space name')
|
|
78
115
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
79
|
-
.
|
|
116
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
117
|
+
.action(async (spaceName, options) => show(buildSpaceContext(spaceName), options));
|
|
80
118
|
program
|
|
81
119
|
.command('dump')
|
|
82
120
|
.description('Dump parsed space nodes as JSON')
|
|
83
121
|
.argument('<space-name>', 'Space name')
|
|
84
|
-
.
|
|
122
|
+
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
123
|
+
.option('--hierarchy <name>', 'Hierarchy used to evaluate --filter (default: the main hierarchy)')
|
|
124
|
+
.action(async (spaceName, options) => dump(buildSpaceContext(spaceName), options));
|
|
85
125
|
program
|
|
86
126
|
.command('miro-sync')
|
|
87
127
|
.description('Sync space to a Miro board')
|
|
@@ -89,16 +129,16 @@ program
|
|
|
89
129
|
.option('--new-frame <title>', 'Create a new frame on the board and sync into it')
|
|
90
130
|
.option('--dry-run', 'Show what would change without touching Miro')
|
|
91
131
|
.option('-v, --verbose', 'Detailed output')
|
|
92
|
-
.action((spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
132
|
+
.action(async (spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
93
133
|
program
|
|
94
134
|
.command('template-sync')
|
|
95
135
|
.description('Sync schema examples to templates')
|
|
96
136
|
.argument('<space-name>', 'Space name')
|
|
97
137
|
.option('--create-missing', 'Create missing template files for schema types')
|
|
98
138
|
.option('--dry-run', 'Preview changes without writing files')
|
|
99
|
-
.action((spaceName, options) => {
|
|
139
|
+
.action(async (spaceName, options) => {
|
|
100
140
|
const context = buildSpaceContext(spaceName);
|
|
101
|
-
templateSync(context, options);
|
|
141
|
+
await templateSync(context, options);
|
|
102
142
|
});
|
|
103
143
|
program
|
|
104
144
|
.command('plugins')
|
|
@@ -108,13 +148,24 @@ const spacesCmd = new Command('spaces').description('List configured spaces');
|
|
|
108
148
|
spacesCmd
|
|
109
149
|
.command('list', { isDefault: true })
|
|
110
150
|
.description('List all configured spaces and their paths')
|
|
111
|
-
.
|
|
151
|
+
.option('--json', 'Output as JSON: config files, and each space with its path and schema')
|
|
152
|
+
.action((options) => listSpaces(options));
|
|
112
153
|
program.addCommand(spacesCmd);
|
|
154
|
+
const providersCmd = new Command('providers').description('List model providers (bundled and configured) with their effective config');
|
|
155
|
+
providersCmd
|
|
156
|
+
.command('list', { isDefault: true })
|
|
157
|
+
.description('List bundled and configured model providers; keys are never shown')
|
|
158
|
+
.option('--json', 'Output as JSON')
|
|
159
|
+
.action(async (options) => {
|
|
160
|
+
const { listProviders } = await import('./commands/providers.js');
|
|
161
|
+
listProviders(options);
|
|
162
|
+
});
|
|
163
|
+
program.addCommand(providersCmd);
|
|
113
164
|
const schemasCmd = new Command('schemas').alias('schema').description('List and inspect schemas');
|
|
114
165
|
schemasCmd
|
|
115
166
|
.command('list', { isDefault: true })
|
|
116
167
|
.description('List available schemas')
|
|
117
|
-
.action(() => listSchemas());
|
|
168
|
+
.action(async () => await listSchemas());
|
|
118
169
|
schemasCmd
|
|
119
170
|
.command('show')
|
|
120
171
|
.description('Show schema structure (or raw JSON with --raw, or Mermaid ERD with --mermaid-erd)')
|
|
@@ -122,7 +173,7 @@ schemasCmd
|
|
|
122
173
|
.option('--space <name>', 'Resolve schema from space config')
|
|
123
174
|
.option('--raw', 'Output raw JSON file content')
|
|
124
175
|
.option('--mermaid-erd', 'Output Mermaid Entity Relationship Diagram')
|
|
125
|
-
.action((file, options) => showSchema(file, {
|
|
176
|
+
.action(async (file, options) => await showSchema(file, {
|
|
126
177
|
space: options.space,
|
|
127
178
|
raw: options.raw ?? false,
|
|
128
179
|
mermaidErd: options.mermaidErd ?? false,
|
|
@@ -130,7 +181,7 @@ schemasCmd
|
|
|
130
181
|
program.addCommand(schemasCmd);
|
|
131
182
|
program
|
|
132
183
|
.command('docs [topic]')
|
|
133
|
-
.description('Show documentation (no arg: README; topics: concepts, config, schema, rules)')
|
|
184
|
+
.description('Show documentation (no arg: README; topics: concepts, config, schema, rules, semantic-rules)')
|
|
134
185
|
.action((topic) => docs(topic));
|
|
135
186
|
const renderCmd = new Command('render').description('Render a space in a given format');
|
|
136
187
|
renderCmd
|
|
@@ -141,11 +192,25 @@ renderCmd
|
|
|
141
192
|
const context = spaceName ? buildSpaceContext(spaceName) : undefined;
|
|
142
193
|
await renderList(context);
|
|
143
194
|
});
|
|
195
|
+
function parseDataOptions(val, prev = {}) {
|
|
196
|
+
const eqIdx = val.indexOf('=');
|
|
197
|
+
if (eqIdx >= 0) {
|
|
198
|
+
prev[val.slice(0, eqIdx).trim()] = val.slice(eqIdx + 1).trim();
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
prev[val.trim()] = 'true';
|
|
202
|
+
}
|
|
203
|
+
return prev;
|
|
204
|
+
}
|
|
144
205
|
renderCmd
|
|
145
206
|
.argument('<space-name>', 'Space name')
|
|
146
|
-
.argument('<format>', 'Render format (e.g. markdown.bullets)')
|
|
207
|
+
.argument('<format>', 'Render format (e.g. markdown.bullets, markdown.directory)')
|
|
147
208
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
148
|
-
.option('
|
|
209
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
210
|
+
.option('-o, --output <path>', 'Output file or directory path (default: stdout)')
|
|
211
|
+
.option('--out <path>', 'Output directory or file path')
|
|
212
|
+
.option('-f, --force', 'Overwrite existing files when writing to disk')
|
|
213
|
+
.option('-d, --data <key=value...>', 'Format-specific key=value options', parseDataOptions)
|
|
149
214
|
.action(async (spaceName, format, options) => {
|
|
150
215
|
await render(buildSpaceContext(spaceName), format, options);
|
|
151
216
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SEMANTIC_SCOPES } from '../schema/metadata-contract';
|
|
2
|
+
import type { SpaceGraph } from '../space-graph';
|
|
3
|
+
import type { Rule, SpaceNode } from '../types';
|
|
4
|
+
import type { RelatedNode, SemanticJudgeInput } from '../types.js';
|
|
5
|
+
export type SemanticScope = Exclude<(typeof SEMANTIC_SCOPES)[number], 'branch'>;
|
|
6
|
+
export type ContentMode = 'none' | 'nodeOnly' | 'fullScope';
|
|
7
|
+
/** What a rule asks to send besides the evaluated node's data. */
|
|
8
|
+
export interface RuleContext {
|
|
9
|
+
scope: SemanticScope[];
|
|
10
|
+
content: ContentMode;
|
|
11
|
+
}
|
|
12
|
+
/** The related nodes and bodies a semantic rule asks for. `includeContent: true` means `fullScope`. */
|
|
13
|
+
export declare function ruleContext(rule: Rule): RuleContext;
|
|
14
|
+
/**
|
|
15
|
+
* The nodes a scope adds for a node, along main-hierarchy edges only. Selection reads the graph's structure and
|
|
16
|
+
* nothing else: not verdicts, not gating. Each node appears once, with every relation it has, nearest first.
|
|
17
|
+
*/
|
|
18
|
+
export declare function relatedNodes(graph: SpaceGraph, node: SpaceNode, context: RuleContext): RelatedNode[];
|
|
19
|
+
/** The judge input context for rules that share `context`: the node view and its related nodes. */
|
|
20
|
+
export declare function judgeContext(graph: SpaceGraph, node: SpaceNode, context: RuleContext): Pick<SemanticJudgeInput, 'node' | 'related'>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { SEMANTIC_SCOPES } from '../schema/metadata-contract';
|
|
2
|
+
import { ruleSemantic } from '../schema/rule-severity';
|
|
3
|
+
/** The related nodes and bodies a semantic rule asks for. `includeContent: true` means `fullScope`. */
|
|
4
|
+
export function ruleContext(rule) {
|
|
5
|
+
const semantic = ruleSemantic(rule);
|
|
6
|
+
const include = semantic?.includeContent;
|
|
7
|
+
const asked = new Set(semantic?.scope ?? []);
|
|
8
|
+
if (asked.has('branch'))
|
|
9
|
+
asked.add('ancestors').add('descendants');
|
|
10
|
+
// A wider scope implies the narrower one, so equal node sets give equal inputs and direct relations keep their label.
|
|
11
|
+
if (asked.has('ancestors'))
|
|
12
|
+
asked.add('parent');
|
|
13
|
+
if (asked.has('descendants'))
|
|
14
|
+
asked.add('children');
|
|
15
|
+
return {
|
|
16
|
+
scope: SEMANTIC_SCOPES.filter((s) => s !== 'branch' && asked.has(s)),
|
|
17
|
+
content: include === undefined ? 'none' : include === true ? 'fullScope' : include,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** All schema data, without the body unless asked for. */
|
|
21
|
+
function dataWithBody(node, withBody) {
|
|
22
|
+
if (withBody)
|
|
23
|
+
return node.schemaData;
|
|
24
|
+
const { content: _content, ...data } = node.schemaData;
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
/** The evaluated node as a judge sees it. */
|
|
28
|
+
function judgeNodeView(node, withBody) {
|
|
29
|
+
return {
|
|
30
|
+
label: node.label,
|
|
31
|
+
title: node.title,
|
|
32
|
+
type: node.type,
|
|
33
|
+
resolvedType: node.resolvedType,
|
|
34
|
+
schemaData: dataWithBody(node, withBody),
|
|
35
|
+
resolvedParents: node.resolvedParents,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function relatedView(node, relation, withBody) {
|
|
39
|
+
const { title: _title, type: _type, ...data } = dataWithBody(node, withBody);
|
|
40
|
+
return { relations: [relation], title: node.title, type: node.resolvedType, data };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The nodes a scope adds for a node, along main-hierarchy edges only. Selection reads the graph's structure and
|
|
44
|
+
* nothing else: not verdicts, not gating. Each node appears once, with every relation it has, nearest first.
|
|
45
|
+
*/
|
|
46
|
+
export function relatedNodes(graph, node, context) {
|
|
47
|
+
const withBody = context.content === 'fullScope';
|
|
48
|
+
const hierarchyName = graph.hierarchy?.name;
|
|
49
|
+
const parentsOf = (n) => graph.hierarchy === undefined
|
|
50
|
+
? []
|
|
51
|
+
: n.resolvedParents
|
|
52
|
+
.filter((p) => p.source === 'hierarchy' && p.hierarchy === hierarchyName)
|
|
53
|
+
.map((p) => graph.nodes.get(p.title))
|
|
54
|
+
.filter((p) => p !== undefined);
|
|
55
|
+
const childrenOf = (n) => graph.hierarchyChildren.get(n.title) ?? [];
|
|
56
|
+
const found = new Map();
|
|
57
|
+
const add = (other, relation) => {
|
|
58
|
+
if (other.title === node.title)
|
|
59
|
+
return;
|
|
60
|
+
const existing = found.get(other.title);
|
|
61
|
+
if (!existing)
|
|
62
|
+
found.set(other.title, relatedView(other, relation, withBody));
|
|
63
|
+
else if (!existing.relations.includes(relation))
|
|
64
|
+
existing.relations.push(relation);
|
|
65
|
+
};
|
|
66
|
+
const reach = (next, relation) => {
|
|
67
|
+
const seen = new Set([node.title]);
|
|
68
|
+
let level = next(node);
|
|
69
|
+
while (level.length > 0) {
|
|
70
|
+
const following = [];
|
|
71
|
+
for (const n of level) {
|
|
72
|
+
if (seen.has(n.title))
|
|
73
|
+
continue;
|
|
74
|
+
seen.add(n.title);
|
|
75
|
+
add(n, relation);
|
|
76
|
+
following.push(...next(n));
|
|
77
|
+
}
|
|
78
|
+
level = following;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const scope = new Set(context.scope);
|
|
82
|
+
if (scope.has('parent'))
|
|
83
|
+
for (const p of parentsOf(node))
|
|
84
|
+
add(p, 'parent');
|
|
85
|
+
if (scope.has('siblings'))
|
|
86
|
+
for (const p of parentsOf(node))
|
|
87
|
+
for (const s of childrenOf(p))
|
|
88
|
+
add(s, 'sibling');
|
|
89
|
+
if (scope.has('children'))
|
|
90
|
+
for (const c of childrenOf(node))
|
|
91
|
+
add(c, 'child');
|
|
92
|
+
if (scope.has('ancestors'))
|
|
93
|
+
reach(parentsOf, 'ancestor');
|
|
94
|
+
if (scope.has('descendants'))
|
|
95
|
+
reach(childrenOf, 'descendant');
|
|
96
|
+
return [...found.values()];
|
|
97
|
+
}
|
|
98
|
+
/** The judge input context for rules that share `context`: the node view and its related nodes. */
|
|
99
|
+
export function judgeContext(graph, node, context) {
|
|
100
|
+
const related = relatedNodes(graph, node, context);
|
|
101
|
+
return {
|
|
102
|
+
node: judgeNodeView(node, context.content !== 'none'),
|
|
103
|
+
...(related.length > 0 && { related }),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type HydratedProvider, type ModelProviderConfig, type SemanticJudgeProvider } from '../types.js';
|
|
2
|
+
type Env = Record<string, string | undefined>;
|
|
3
|
+
/**
|
|
4
|
+
* Bundled providers: usable as a judge by name, without a `modelProviders` entry. A user entry of the
|
|
5
|
+
* same name replaces one wholesale; fields it leaves out take the type defaults in the constructors.
|
|
6
|
+
* Every value here is explicit: no bundle reads the environment.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BUNDLED_PROVIDERS: Record<string, ModelProviderConfig>;
|
|
9
|
+
/** Semantic evaluation is misconfigured (no provider, unknown provider, missing key). Reported as a plain CLI error. */
|
|
10
|
+
export declare class JudgeConfigError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hydrate a provider definition into concrete values, in one place. Per field: the explicit value
|
|
14
|
+
* wins, then the named environment variable if it is set (a set-but-empty variable is an error, not
|
|
15
|
+
* a silent fall-through), then the definition's own default. The key is special: it must resolve.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hydrateProvider(name: string, def: ModelProviderConfig, env?: Env): HydratedProvider;
|
|
18
|
+
/**
|
|
19
|
+
* Instantiate one provider from a definition. The API is always explicit and is never inferred from
|
|
20
|
+
* API keys in the environment, so a key set for another tool is never spent on judging. Constructors
|
|
21
|
+
* apply their own API defaults for fields left undefined after hydration.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createModelProvider(name: string, def: ModelProviderConfig, env?: Env): SemanticJudgeProvider;
|
|
24
|
+
/**
|
|
25
|
+
* The provider a CLI run asks for: none without `--semantic`; the named one with `--semantic <provider>`;
|
|
26
|
+
* else the configured one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function judgeForRun(config: Parameters<typeof createJudge>[0], space: Parameters<typeof createJudge>[1], semantic?: boolean | string): SemanticJudgeProvider | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The provider for a run: the one named by `name` (the `--semantic <provider>` override), else the space's
|
|
31
|
+
* `semantic.provider`, else the top-level one. The name resolves against `modelProviders` first, then
|
|
32
|
+
* the bundled providers. Throws {@link JudgeConfigError} when it cannot be built.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createJudge(config: {
|
|
35
|
+
modelProviders?: Record<string, ModelProviderConfig>;
|
|
36
|
+
semantic?: {
|
|
37
|
+
provider?: string;
|
|
38
|
+
};
|
|
39
|
+
}, space?: {
|
|
40
|
+
semantic?: {
|
|
41
|
+
provider?: string;
|
|
42
|
+
};
|
|
43
|
+
}, name?: string, env?: Env): SemanticJudgeProvider;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { AnthropicJudgeProvider, RECOMMENDED_ANTHROPIC_MODEL } from './provider/anthropic';
|
|
2
|
+
import { FakeJudgeProvider } from './provider/fake';
|
|
3
|
+
import { JEV_LATEST, JevJudgeProvider } from './provider/jev';
|
|
4
|
+
import { OpenAIJudgeProvider, RECOMMENDED_OPENAI_MODEL } from './provider/openai';
|
|
5
|
+
import { OpenAIDecisionsJudgeProvider, RECOMMENDED_OPENAI_DECISIONS_MODEL } from './provider/openai-decisions';
|
|
6
|
+
import { MODEL_PROVIDER_APIS, } from '../types.js';
|
|
7
|
+
const OPENROUTER_COMPLETIONS_ENDPOINT = 'https://openrouter.ai/api/v1';
|
|
8
|
+
const RECOMMENDED_OPENROUTER_OPENAI_DECISIONS_MODEL = `openai/${RECOMMENDED_OPENAI_DECISIONS_MODEL}-decisions`;
|
|
9
|
+
const RECOMMENDED_OPENROUTER_OPENAI_MODEL = `openai/${RECOMMENDED_OPENAI_MODEL}`;
|
|
10
|
+
const ANTHROPIC_KEY_ENV = 'ANTHROPIC_API_KEY';
|
|
11
|
+
const OPENAI_KEY_ENV = 'OPENAI_API_KEY';
|
|
12
|
+
const OPENROUTER_KEY_ENV = 'OPENROUTER_API_KEY';
|
|
13
|
+
const ANTHROPIC_BASE = {
|
|
14
|
+
api: 'anthropic-messages',
|
|
15
|
+
model: RECOMMENDED_ANTHROPIC_MODEL,
|
|
16
|
+
apiKeyEnv: ANTHROPIC_KEY_ENV,
|
|
17
|
+
};
|
|
18
|
+
const OPENAI_BASE = {
|
|
19
|
+
api: 'openai-completions',
|
|
20
|
+
model: RECOMMENDED_OPENAI_MODEL,
|
|
21
|
+
apiKeyEnv: OPENAI_KEY_ENV,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Bundled providers: usable as a judge by name, without a `modelProviders` entry. A user entry of the
|
|
25
|
+
* same name replaces one wholesale; fields it leaves out take the type defaults in the constructors.
|
|
26
|
+
* Every value here is explicit: no bundle reads the environment.
|
|
27
|
+
*/
|
|
28
|
+
export const BUNDLED_PROVIDERS = {
|
|
29
|
+
'typesafe-jev': { api: 'typesafe-systemone', model: JEV_LATEST, apiKeyEnv: 'TYPESAFE_API_KEY' },
|
|
30
|
+
'openrouter-jev': { api: 'openrouter-decisions', model: `~typesafe/${JEV_LATEST}`, apiKeyEnv: OPENROUTER_KEY_ENV },
|
|
31
|
+
'openrouter-openai-decisions': {
|
|
32
|
+
api: 'openrouter-decisions',
|
|
33
|
+
model: RECOMMENDED_OPENROUTER_OPENAI_DECISIONS_MODEL,
|
|
34
|
+
apiKeyEnv: OPENROUTER_KEY_ENV,
|
|
35
|
+
},
|
|
36
|
+
'openai-decisions': {
|
|
37
|
+
api: 'openai-decisions',
|
|
38
|
+
model: RECOMMENDED_OPENAI_DECISIONS_MODEL,
|
|
39
|
+
apiKeyEnv: OPENAI_KEY_ENV,
|
|
40
|
+
},
|
|
41
|
+
anthropic: ANTHROPIC_BASE,
|
|
42
|
+
'anthropic-haiku-5-5': { api: 'anthropic-messages', model: 'claude-haiku-5-5', apiKeyEnv: ANTHROPIC_KEY_ENV },
|
|
43
|
+
'anthropic-env': {
|
|
44
|
+
...ANTHROPIC_BASE,
|
|
45
|
+
modelEnv: 'ANTHROPIC_MODEL',
|
|
46
|
+
},
|
|
47
|
+
openai: OPENAI_BASE,
|
|
48
|
+
'openai-env': {
|
|
49
|
+
...OPENAI_BASE,
|
|
50
|
+
modelEnv: 'OPENAI_MODEL',
|
|
51
|
+
endpointEnv: 'OPENAI_BASE_URL',
|
|
52
|
+
},
|
|
53
|
+
'openrouter-openai': {
|
|
54
|
+
api: 'openai-completions',
|
|
55
|
+
endpoint: OPENROUTER_COMPLETIONS_ENDPOINT,
|
|
56
|
+
model: RECOMMENDED_OPENROUTER_OPENAI_MODEL,
|
|
57
|
+
apiKeyEnv: OPENROUTER_KEY_ENV,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
/** Each API reads its key from this variable unless `apiKeyEnv` names another one. Model and endpoint never read env vars. */
|
|
61
|
+
const DEFAULT_KEY_ENV = {
|
|
62
|
+
'anthropic-messages': 'ANTHROPIC_API_KEY',
|
|
63
|
+
'openai-completions': 'OPENAI_API_KEY',
|
|
64
|
+
'openai-decisions': 'OPENAI_API_KEY',
|
|
65
|
+
'openrouter-decisions': 'OPENROUTER_API_KEY',
|
|
66
|
+
'typesafe-systemone': 'TYPESAFE_API_KEY',
|
|
67
|
+
};
|
|
68
|
+
/** Semantic evaluation is misconfigured (no provider, unknown provider, missing key). Reported as a plain CLI error. */
|
|
69
|
+
export class JudgeConfigError extends Error {
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Hydrate a provider definition into concrete values, in one place. Per field: the explicit value
|
|
73
|
+
* wins, then the named environment variable if it is set (a set-but-empty variable is an error, not
|
|
74
|
+
* a silent fall-through), then the definition's own default. The key is special: it must resolve.
|
|
75
|
+
*/
|
|
76
|
+
export function hydrateProvider(name, def, env = process.env) {
|
|
77
|
+
if (!MODEL_PROVIDER_APIS.includes(def.api)) {
|
|
78
|
+
throw new JudgeConfigError(`Model provider "${name}" has unknown api "${def.api}". Use one of: ${MODEL_PROVIDER_APIS.join(', ')}.`);
|
|
79
|
+
}
|
|
80
|
+
const fromEnv = (value, envName) => {
|
|
81
|
+
if (value !== undefined)
|
|
82
|
+
return value;
|
|
83
|
+
if (!envName)
|
|
84
|
+
return undefined;
|
|
85
|
+
const envValue = env[envName];
|
|
86
|
+
if (envValue !== undefined && envValue.trim() === '') {
|
|
87
|
+
throw new JudgeConfigError(`Model provider "${name}" has ${envName} set but empty; set a value or unset the variable.`);
|
|
88
|
+
}
|
|
89
|
+
return envValue;
|
|
90
|
+
};
|
|
91
|
+
const model = fromEnv(def.model, def.modelEnv);
|
|
92
|
+
const endpoint = fromEnv(def.endpoint, def.endpointEnv);
|
|
93
|
+
const keyEnv = def.apiKeyEnv ?? DEFAULT_KEY_ENV[def.api];
|
|
94
|
+
const apiKey = def.apiKey ?? (keyEnv ? env[keyEnv] : undefined);
|
|
95
|
+
return { api: def.api, model, endpoint, apiKey, keyEnv };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Instantiate one provider from a definition. The API is always explicit and is never inferred from
|
|
99
|
+
* API keys in the environment, so a key set for another tool is never spent on judging. Constructors
|
|
100
|
+
* apply their own API defaults for fields left undefined after hydration.
|
|
101
|
+
*/
|
|
102
|
+
export function createModelProvider(name, def, env = process.env) {
|
|
103
|
+
const h = hydrateProvider(name, def, env);
|
|
104
|
+
// OpenAI-compatible endpoints (Ollama, vLLM, ...) may need no key; the others must resolve one.
|
|
105
|
+
// A key never borrows another provider's variable: only the named apiKeyEnv (or literal apiKey) counts.
|
|
106
|
+
if (!h.apiKey && h.api !== 'openai-completions' && h.api !== 'fake') {
|
|
107
|
+
throw new JudgeConfigError(`Model provider "${name}" (${h.api}) has no API key. Set ${h.keyEnv} in the environment, or "apiKey" / "apiKeyEnv" on the provider.`);
|
|
108
|
+
}
|
|
109
|
+
switch (h.api) {
|
|
110
|
+
case 'fake':
|
|
111
|
+
return new FakeJudgeProvider();
|
|
112
|
+
case 'anthropic-messages':
|
|
113
|
+
return new AnthropicJudgeProvider(h);
|
|
114
|
+
case 'openrouter-decisions':
|
|
115
|
+
case 'typesafe-systemone':
|
|
116
|
+
return new JevJudgeProvider(h);
|
|
117
|
+
case 'openai-decisions':
|
|
118
|
+
return new OpenAIDecisionsJudgeProvider(h);
|
|
119
|
+
case 'openai-completions':
|
|
120
|
+
return new OpenAIJudgeProvider(h);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The provider a CLI run asks for: none without `--semantic`; the named one with `--semantic <provider>`;
|
|
125
|
+
* else the configured one.
|
|
126
|
+
*/
|
|
127
|
+
export function judgeForRun(config, space, semantic) {
|
|
128
|
+
if (!semantic)
|
|
129
|
+
return undefined;
|
|
130
|
+
return createJudge(config, space, typeof semantic === 'string' ? semantic : undefined);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The provider for a run: the one named by `name` (the `--semantic <provider>` override), else the space's
|
|
134
|
+
* `semantic.provider`, else the top-level one. The name resolves against `modelProviders` first, then
|
|
135
|
+
* the bundled providers. Throws {@link JudgeConfigError} when it cannot be built.
|
|
136
|
+
*/
|
|
137
|
+
export function createJudge(config, space = {}, name, env = process.env) {
|
|
138
|
+
const chosen = name ?? space.semantic?.provider ?? config.semantic?.provider;
|
|
139
|
+
if (!chosen) {
|
|
140
|
+
throw new JudgeConfigError('--semantic needs a model provider. Set "semantic.provider" in config.json, or pass --semantic <provider>.');
|
|
141
|
+
}
|
|
142
|
+
const def = config.modelProviders?.[chosen] ?? BUNDLED_PROVIDERS[chosen];
|
|
143
|
+
if (!def) {
|
|
144
|
+
const known = [...Object.keys(BUNDLED_PROVIDERS), ...Object.keys(config.modelProviders ?? {})];
|
|
145
|
+
throw new JudgeConfigError(`Model provider "${chosen}" is not configured.${known.length ? ` Known providers: ${known.join(', ')}.` : ''}`);
|
|
146
|
+
}
|
|
147
|
+
return createModelProvider(chosen, def, env);
|
|
148
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { createJudge, createModelProvider, JudgeConfigError } from './factory';
|
|
2
|
+
export { buildJudgeSystemPrompt, buildJudgeUserPrompt, parseJudgeResponse } from './prompt';
|
|
3
|
+
export type { AnthropicJudgeProviderOptions } from './provider/anthropic';
|
|
4
|
+
export { AnthropicJudgeProvider } from './provider/anthropic';
|
|
5
|
+
export type { FakeAnswer, FakeJudgeHandler } from './provider/fake';
|
|
6
|
+
export { FakeJudgeProvider } from './provider/fake';
|
|
7
|
+
export type { JevJudgeProviderOptions } from './provider/jev';
|
|
8
|
+
export { JevJudgeProvider } from './provider/jev';
|
|
9
|
+
export type { OpenAIJudgeProviderOptions } from './provider/openai';
|
|
10
|
+
export { OpenAIJudgeProvider } from './provider/openai';
|
|
11
|
+
export { PerRuleJudgeProvider } from './provider/per-rule';
|
|
12
|
+
export type { JudgeConfig, ModelProviderConfig, SemanticJudgeInput, SemanticJudgeProvider, SemanticJudgeResult, SemanticJudgeVerdict, SemanticRuleSpec, } from '../types.js';
|
|
13
|
+
export { DEFAULT_VERDICTS, isJudgeError, semanticRuleSpec } from './verdicts';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createJudge, createModelProvider, JudgeConfigError } from './factory';
|
|
2
|
+
export { buildJudgeSystemPrompt, buildJudgeUserPrompt, parseJudgeResponse } from './prompt';
|
|
3
|
+
export { AnthropicJudgeProvider } from './provider/anthropic';
|
|
4
|
+
export { FakeJudgeProvider } from './provider/fake';
|
|
5
|
+
export { JevJudgeProvider } from './provider/jev';
|
|
6
|
+
export { OpenAIJudgeProvider } from './provider/openai';
|
|
7
|
+
export { PerRuleJudgeProvider } from './provider/per-rule';
|
|
8
|
+
export { DEFAULT_VERDICTS, isJudgeError, semanticRuleSpec } from './verdicts';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SpaceGraph } from '../space-graph';
|
|
2
|
+
import type { Rule, RuleSeverity, SpaceNode } from '../types';
|
|
3
|
+
import { type TrimReport } from './trim';
|
|
4
|
+
import type { SemanticJudgeProvider } from '../types.js';
|
|
5
|
+
export type NodeJudgement = {
|
|
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. */
|
|
10
|
+
severity?: RuleSeverity;
|
|
11
|
+
verdict?: string;
|
|
12
|
+
/** User-facing guidance declared for the failing verdict. */
|
|
13
|
+
guidance?: string;
|
|
14
|
+
evidence?: string;
|
|
15
|
+
/** Set when the judge could not produce a verdict for this rule. */
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Judge several nodes at once. A judge with `evaluateNodes` gets every input in one call and may fold them into
|
|
20
|
+
* fewer requests; any other judge is called once per input. A node's rules that share a context (related nodes
|
|
21
|
+
* and bodies) form one input; an input that cannot fit the per-node input cap is not sent. Results align with
|
|
22
|
+
* `items`, in rule order. A failure marks only the affected rules as errors.
|
|
23
|
+
*/
|
|
24
|
+
export declare function judgeNodes(judge: SemanticJudgeProvider, items: Array<{
|
|
25
|
+
node: SpaceNode;
|
|
26
|
+
rules: Rule[];
|
|
27
|
+
}>, graph: SpaceGraph, report?: TrimReport): Promise<NodeJudgement[][]>;
|