structured-context 0.12.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.
Files changed (94) hide show
  1. package/README.md +29 -216
  2. package/dist/api.d.ts +17 -17
  3. package/dist/api.js +12 -12
  4. package/dist/commands/docs.js +1 -0
  5. package/dist/commands/providers.d.ts +4 -0
  6. package/dist/commands/providers.js +52 -0
  7. package/dist/commands/schemas.js +21 -2
  8. package/dist/commands/validate-file.d.ts +3 -0
  9. package/dist/commands/validate-file.js +17 -2
  10. package/dist/commands/validate.d.ts +4 -0
  11. package/dist/commands/validate.js +42 -3
  12. package/dist/config.d.ts +7 -0
  13. package/dist/config.js +26 -3
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +59 -18
  16. package/dist/judge/context.d.ts +20 -0
  17. package/dist/judge/context.js +105 -0
  18. package/dist/judge/factory.d.ts +44 -0
  19. package/dist/judge/factory.js +148 -0
  20. package/dist/judge/index.d.ts +13 -0
  21. package/dist/judge/index.js +8 -0
  22. package/dist/judge/judge-node.d.ts +27 -0
  23. package/dist/judge/judge-node.js +122 -0
  24. package/dist/judge/prompt.d.ts +10 -0
  25. package/dist/judge/prompt.js +92 -0
  26. package/dist/judge/provider/anthropic.d.ts +17 -0
  27. package/dist/judge/provider/anthropic.js +43 -0
  28. package/dist/judge/provider/fake.d.ts +26 -0
  29. package/dist/judge/provider/fake.js +56 -0
  30. package/dist/judge/provider/jev.d.ts +37 -0
  31. package/dist/judge/provider/jev.js +169 -0
  32. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  33. package/dist/judge/provider/openai-decisions.js +84 -0
  34. package/dist/judge/provider/openai.d.ts +20 -0
  35. package/dist/judge/provider/openai.js +53 -0
  36. package/dist/judge/provider/per-rule.d.ts +10 -0
  37. package/dist/judge/provider/per-rule.js +18 -0
  38. package/dist/judge/trim.d.ts +27 -0
  39. package/dist/judge/trim.js +62 -0
  40. package/dist/judge/types.d.ts +92 -0
  41. package/dist/judge/types.js +9 -0
  42. package/dist/judge/verdicts.d.ts +24 -0
  43. package/dist/judge/verdicts.js +82 -0
  44. package/dist/load-space-graph.d.ts +3 -3
  45. package/dist/load-space-graph.js +4 -4
  46. package/dist/plugins/loader.js +1 -1
  47. package/dist/plugins/markdown/parse-embedded.d.ts +2 -0
  48. package/dist/plugins/markdown/parse-embedded.js +161 -24
  49. package/dist/plugins/markdown/read-space.js +4 -9
  50. package/dist/plugins/markdown/render-directory.js +5 -5
  51. package/dist/plugins/markdown/template-sync.js +3 -4
  52. package/dist/plugins/markdown/util.d.ts +26 -5
  53. package/dist/plugins/markdown/util.js +56 -15
  54. package/dist/render/render.d.ts +1 -1
  55. package/dist/render/render.js +1 -1
  56. package/dist/schema/evaluate-rule.d.ts +33 -2
  57. package/dist/schema/evaluate-rule.js +58 -4
  58. package/dist/schema/gating.d.ts +69 -0
  59. package/dist/schema/gating.js +253 -0
  60. package/dist/schema/metadata-contract.d.ts +625 -12
  61. package/dist/schema/metadata-contract.js +110 -4
  62. package/dist/schema/rule-filter.d.ts +12 -0
  63. package/dist/schema/rule-filter.js +46 -0
  64. package/dist/schema/rule-severity.d.ts +13 -0
  65. package/dist/schema/rule-severity.js +33 -0
  66. package/dist/schema/schema.js +31 -1
  67. package/dist/schema/validate-rules.d.ts +17 -8
  68. package/dist/schema/validate-rules.js +91 -48
  69. package/dist/schemas/_knowledge_wiki.json +4 -4
  70. package/dist/schemas/_okf.json +172 -0
  71. package/dist/schemas/_ost_strict.json +15 -6
  72. package/dist/schemas/_sctx_base.json +1 -1
  73. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  74. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
  75. package/dist/schemas/okf.json +8 -0
  76. package/dist/space-context.d.ts +2 -2
  77. package/dist/space-context.js +3 -3
  78. package/dist/space-graph.d.ts +1 -1
  79. package/dist/testing.d.ts +1 -1
  80. package/dist/types.d.ts +11 -4
  81. package/dist/validate.d.ts +59 -9
  82. package/dist/validate.js +225 -120
  83. package/docs/concepts.md +52 -6
  84. package/docs/config.md +70 -1
  85. package/docs/rules.md +140 -49
  86. package/docs/schemas.md +43 -18
  87. package/package.json +6 -5
  88. package/schemas/_knowledge_wiki.json +4 -4
  89. package/schemas/_okf.json +172 -0
  90. package/schemas/_ost_strict.json +15 -6
  91. package/schemas/_sctx_base.json +1 -1
  92. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  93. package/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
  94. package/schemas/okf.json +8 -0
@@ -1,12 +1,19 @@
1
1
  import { dirname } from 'node:path';
2
2
  import chokidar from 'chokidar';
3
3
  import { getConfigSourceFiles } from '../config';
4
+ import { judgeForRun } from '../judge/factory';
4
5
  import { resolveSpacePluginSchemas } from '../plugins/util';
6
+ import { parseRuleFilter } from '../schema/rule-filter';
5
7
  import { bundledSchemasDir } from '../schema/schema';
6
- import { formatErrors, validateSpace } from '../validate';
8
+ import { formatErrors, ruleViolationMessage, validateSpace } from '../validate';
7
9
  export async function validate(context, options = {}) {
8
10
  const { schema, schemaRefRegistry } = context;
9
- const result = await validateSpace(context);
11
+ const result = await validateSpace(context, {
12
+ depth: options.depth,
13
+ includeGated: options.includeGated,
14
+ judge: judgeForRun(context.config, context.space, options.semantic),
15
+ ruleFilter: options.ruleFilter && parseRuleFilter(options.ruleFilter),
16
+ });
10
17
  const ruleCounts = { error: 0, warning: 0, info: 0 };
11
18
  for (const v of result.ruleViolations)
12
19
  ruleCounts[v.severity]++;
@@ -35,7 +42,7 @@ export async function validate(context, options = {}) {
35
42
  }
36
43
  for (const v of result.ruleViolations) {
37
44
  if (v.file && v.severity === 'error') {
38
- addError(v.file, `rule:${v.ruleId}`, 'rule', `[${v.ruleId}] ${v.description}`, v.severity);
45
+ addError(v.file, `rule:${v.ruleId}`, 'rule', ruleViolationMessage(v), v.severity);
39
46
  }
40
47
  }
41
48
  const globalRuleErrorCount = result.ruleViolations.filter((v) => !v.file && v.severity === 'error').length;
@@ -56,6 +63,10 @@ export async function validate(context, options = {}) {
56
63
  orphanCount: result.orphans.length,
57
64
  parseIssues: result.parseIssues,
58
65
  unresolvedContentLinks: result.unresolvedContentLinks,
66
+ coverageGates: result.coverageGates,
67
+ contextTrimmed: result.contextTrimmed,
68
+ skippedByDepth: result.skippedByDepth,
69
+ depthLimit: result.depthLimit,
59
70
  }, null, 2));
60
71
  return errorCount > 0 ? 1 : 0;
61
72
  }
@@ -95,6 +106,11 @@ export async function validate(context, options = {}) {
95
106
  console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
96
107
  const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
97
108
  console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
109
+ if (result.depthLimit !== undefined) {
110
+ console.log(fmt(' Skipped by depth limit', result.skippedByDepth.length, true, true));
111
+ }
112
+ const gatedFileCount = result.coverageGates.reduce((sum, g) => sum + g.gatedFiles.length, 0);
113
+ console.log(fmt(' Rule checks gated (cascade)', gatedFileCount, true, true));
98
114
  if (result.orphans.length > 0) {
99
115
  console.log(`\nOrphans (hierarchy nodes - no parent):`);
100
116
  for (const node of result.orphans)
@@ -159,6 +175,12 @@ export async function validate(context, options = {}) {
159
175
  console.log(` ${severityColor[severity]}${severity.toUpperCase()}${reset} (${violations.length}):`);
160
176
  for (const v of violations) {
161
177
  console.log(` ${v.file ? `${v.file}: ` : ''}${v.description} [${v.category}: ${v.ruleId}]`);
178
+ if (v.evidence) {
179
+ console.log(` Evidence: ${v.evidence}`);
180
+ }
181
+ if (v.guidance) {
182
+ console.log(` Guidance: ${v.guidance}`);
183
+ }
162
184
  }
163
185
  }
164
186
  }
@@ -168,6 +190,23 @@ export async function validate(context, options = {}) {
168
190
  console.log(` ${v.file}: ${v.description}`);
169
191
  }
170
192
  }
193
+ if (result.coverageGates.length > 0) {
194
+ console.log(`\nCoverage — rule checks gated by cascade (use --include-gated to evaluate anyway):`);
195
+ for (const gate of result.coverageGates) {
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):`);
198
+ for (const file of gate.gatedFiles)
199
+ console.log(` ${file}`);
200
+ }
201
+ }
202
+ if (result.skippedByDepth.length > 0) {
203
+ console.log(`\nSkipped by depth limit (--depth ${result.depthLimit}):`);
204
+ for (const { file } of result.skippedByDepth)
205
+ console.log(` ${file}`);
206
+ }
207
+ if (result.contextTrimmed.nodeCount > 0) {
208
+ console.log(`\nContext trimmed for ${result.contextTrimmed.nodeCount} node(s) across rules ${result.contextTrimmed.ruleIds.join(', ')}.`);
209
+ }
171
210
  console.log(`\n`);
172
211
  if (result.nodeErrorCount > 0 ||
173
212
  result.refErrors.length > 0 ||
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type JudgeConfig, type ModelProviderConfig } from './judge/types.js';
1
2
  export type SpaceConfig = {
2
3
  name: string;
3
4
  path: string;
@@ -8,6 +9,8 @@ export type SpaceConfig = {
8
9
  views?: Record<string, {
9
10
  expression: string;
10
11
  }>;
12
+ /** Model provider for this space's semantic rules; overrides the top-level `semantic`. */
13
+ semantic?: JudgeConfig;
11
14
  /**
12
15
  * Directory of the config file that defined this space, used to anchor relative plugin-config
13
16
  * paths. Set by loadConfig; absent for a hand-assembled Config, whose callers must pass an
@@ -19,6 +22,10 @@ export type Config = {
19
22
  spaces: SpaceConfig[];
20
23
  schema?: string;
21
24
  includeSpacesFrom?: string[];
25
+ /** Model providers by name: how to reach each model. */
26
+ modelProviders?: Record<string, ModelProviderConfig>;
27
+ /** Model provider for semantic rules (a provider name). A space's `semantic` overrides it. */
28
+ semantic?: JudgeConfig;
22
29
  };
23
30
  /** Override the config file path used by loadConfig/updateSpaceField. */
24
31
  export declare function setConfigPath(path: string | undefined): void;
package/dist/config.js CHANGED
@@ -3,9 +3,29 @@ import { homedir } from 'node:os';
3
3
  import { dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import Ajv2020 from 'ajv/dist/2020';
5
5
  import JSON5 from 'json5';
6
- import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants';
7
- import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from './plugins/util';
8
- import { bundledSchemasDir } from './schema/schema';
6
+ import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants.js';
7
+ import { MODEL_PROVIDER_APIS } from './judge/types.js';
8
+ import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from './plugins/util.js';
9
+ import { bundledSchemasDir } from './schema/schema.js';
10
+ const MODEL_PROVIDER_SCHEMA = {
11
+ type: 'object',
12
+ properties: {
13
+ api: { type: 'string', enum: MODEL_PROVIDER_APIS },
14
+ model: { type: 'string' },
15
+ modelEnv: { type: 'string' },
16
+ endpoint: { type: 'string' },
17
+ endpointEnv: { type: 'string' },
18
+ apiKey: { type: 'string' },
19
+ apiKeyEnv: { type: 'string' },
20
+ },
21
+ required: ['api'],
22
+ additionalProperties: false,
23
+ };
24
+ const SEMANTIC_CONFIG_SCHEMA = {
25
+ type: 'object',
26
+ properties: { provider: { type: 'string', minLength: 1 } },
27
+ additionalProperties: false,
28
+ };
9
29
  const CONFIG_SCHEMA = {
10
30
  type: 'object',
11
31
  properties: {
@@ -27,6 +47,7 @@ const CONFIG_SCHEMA = {
27
47
  additionalProperties: false,
28
48
  },
29
49
  },
50
+ semantic: SEMANTIC_CONFIG_SCHEMA,
30
51
  },
31
52
  required: ['name', 'path'],
32
53
  additionalProperties: false,
@@ -34,6 +55,8 @@ const CONFIG_SCHEMA = {
34
55
  },
35
56
  schema: { type: 'string' },
36
57
  includeSpacesFrom: { type: 'array', items: { type: 'string' } },
58
+ modelProviders: { type: 'object', additionalProperties: MODEL_PROVIDER_SCHEMA },
59
+ semantic: SEMANTIC_CONFIG_SCHEMA,
37
60
  },
38
61
  anyOf: [{ required: ['spaces'] }, { required: ['includeSpacesFrom'] }],
39
62
  additionalProperties: false,
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
2
- import type { SpaceContext } from './types';
2
+ import type { SpaceContext } from './types.js';
3
3
  export declare function buildSpaceContext(spaceName: string): SpaceContext;
package/dist/index.js CHANGED
@@ -2,21 +2,22 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { format } from 'node:util';
4
4
  import { Command } from 'commander';
5
- import { diagram } from './commands/diagram';
6
- import { docs } from './commands/docs';
7
- import { dump } from './commands/dump';
8
- import { miroSyncCommand } from './commands/miro-sync';
9
- import { listPlugins } from './commands/plugins';
10
- import { render, renderList } from './commands/render';
11
- import { listSchemas, showSchema } from './commands/schemas';
12
- import { show } from './commands/show';
13
- import { listSpaces } from './commands/spaces';
14
- import { templateSync } from './commands/template-sync';
15
- import { validate, watchValidate } from './commands/validate';
16
- import { validateFileCommand } from './commands/validate-file';
17
- import { loadConfig, setConfigPath } from './config';
18
- import { CLI_NAME } from './constants';
19
- import { createSpaceContext, SpaceNotFoundError } from './space-context';
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';
20
21
  // Once node:process is loaded (vfile, via unified, loads it), Bun's console.log drops piped output
21
22
  // past 64KB when the reader is slow. process.stdout.write delivers all of it before exit.
22
23
  console.log = (...args) => {
@@ -34,6 +35,18 @@ export function buildSpaceContext(spaceName) {
34
35
  throw err;
35
36
  }
36
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
+ }
37
50
  const require = createRequire(import.meta.url);
38
51
  const packageJson = require('../package.json');
39
52
  const program = new Command();
@@ -50,8 +63,16 @@ program
50
63
  .description('Validate a single file within its space')
51
64
  .argument('<path>', 'Path to the file to validate')
52
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')
53
69
  .action(async (filePath, options) => {
54
- process.exitCode = await validateFileCommand(filePath, { json: options.json });
70
+ process.exitCode = await reportJudgeConfigErrors(() => validateFileCommand(filePath, {
71
+ json: options.json,
72
+ includeGated: options.includeGated,
73
+ semantic: options.semantic,
74
+ ruleFilter: options.ruleFilter,
75
+ }));
55
76
  });
56
77
  program
57
78
  .command('validate')
@@ -59,6 +80,10 @@ program
59
80
  .argument('<space-name>', 'Space name')
60
81
  .option('-w, --watch', 'Watch for changes and re-run validation')
61
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')
62
87
  .action(async (spaceName, options) => {
63
88
  const context = buildSpaceContext(spaceName);
64
89
  if (options.watch) {
@@ -66,7 +91,13 @@ program
66
91
  }
67
92
  else {
68
93
  // exitCode rather than process.exit(): exiting straight away can cut off large piped output
69
- process.exitCode = await validate(context, { json: options.json });
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
+ }));
70
101
  }
71
102
  });
72
103
  program
@@ -120,6 +151,16 @@ spacesCmd
120
151
  .option('--json', 'Output as JSON: config files, and each space with its path and schema')
121
152
  .action((options) => listSpaces(options));
122
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);
123
164
  const schemasCmd = new Command('schemas').alias('schema').description('List and inspect schemas');
124
165
  schemasCmd
125
166
  .command('list', { isDefault: true })
@@ -140,7 +181,7 @@ schemasCmd
140
181
  program.addCommand(schemasCmd);
141
182
  program
142
183
  .command('docs [topic]')
143
- .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)')
144
185
  .action((topic) => docs(topic));
145
186
  const renderCmd = new Command('render').description('Render a space in a given format');
146
187
  renderCmd
@@ -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';