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
@@ -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[][]>;
@@ -0,0 +1,122 @@
1
+ import { judgeContext, ruleContext } from './context';
2
+ import { trimToCap } from './trim';
3
+ import { isJudgeError, resolveSemanticRule } from './verdicts';
4
+ function resolveAll(rules) {
5
+ return rules.map(resolveSemanticRule);
6
+ }
7
+ function errorJudgements(rules, message) {
8
+ return rules.map((rule) => ({
9
+ rule,
10
+ failed: true,
11
+ evidence: `Error during evaluation: ${message}`,
12
+ error: message,
13
+ }));
14
+ }
15
+ function interpret(rules, resolved, results) {
16
+ return rules.map((rule, i) => {
17
+ const result = results[rule.id];
18
+ if (!result || isJudgeError(result)) {
19
+ const message = result ? result.error : 'Model provider returned no result for this rule';
20
+ return { rule, failed: true, evidence: `Error during evaluation: ${message}`, error: message };
21
+ }
22
+ const severity = resolved[i].severities[result.verdict];
23
+ if (severity === undefined && !(result.verdict in resolved[i].spec.verdicts)) {
24
+ const message = `Model provider returned unknown verdict "${result.verdict}" (allowed: ${Object.keys(resolved[i].spec.verdicts).join(', ')})`;
25
+ return { rule, failed: true, evidence: `Error during evaluation: ${message}`, error: message };
26
+ }
27
+ const failed = severity !== undefined;
28
+ return {
29
+ rule,
30
+ failed,
31
+ severity,
32
+ verdict: result.verdict,
33
+ guidance: failed ? resolved[i].guidance[result.verdict] : undefined,
34
+ evidence: result.evidence,
35
+ };
36
+ });
37
+ }
38
+ const errorMessage = (err) => (err instanceof Error ? err.message : String(err));
39
+ const OVER_CAP_MESSAGE = 'input over the per-node cap after trimming content';
40
+ /**
41
+ * Split each node's rules into inputs by the context they ask for, trimmed to the per-node input cap; rules that
42
+ * share a context share an input. Inputs that cannot fit go to `overCap`; trimmed ones are added to `report`.
43
+ */
44
+ function planUnits(graph, items, report) {
45
+ const units = [];
46
+ const overCap = [];
47
+ items.forEach(({ node, rules }, item) => {
48
+ const groups = new Map();
49
+ rules.forEach((rule, position) => {
50
+ const context = ruleContext(rule);
51
+ const key = `${context.content}|${context.scope.join(',')}`;
52
+ const group = groups.get(key) ?? { context, positions: [] };
53
+ group.positions.push(position);
54
+ groups.set(key, group);
55
+ });
56
+ for (const { context, positions } of groups.values()) {
57
+ const unitRules = positions.map((p) => rules[p]);
58
+ const resolved = resolveAll(unitRules);
59
+ const fitted = trimToCap(judgeContext(graph, node, context));
60
+ if (!fitted) {
61
+ overCap.push({ item, positions, rules: unitRules });
62
+ continue;
63
+ }
64
+ if (fitted.trimmed && report) {
65
+ report.nodes.add(node.label);
66
+ for (const rule of unitRules)
67
+ report.ruleIds.add(rule.id);
68
+ }
69
+ units.push({
70
+ item,
71
+ positions,
72
+ rules: unitRules,
73
+ resolved,
74
+ input: { ...fitted.input, rules: resolved.map((r) => r.spec) },
75
+ });
76
+ }
77
+ });
78
+ return { units, overCap };
79
+ }
80
+ /**
81
+ * Judge several nodes at once. A judge with `evaluateNodes` gets every input in one call and may fold them into
82
+ * fewer requests; any other judge is called once per input. A node's rules that share a context (related nodes
83
+ * and bodies) form one input; an input that cannot fit the per-node input cap is not sent. Results align with
84
+ * `items`, in rule order. A failure marks only the affected rules as errors.
85
+ */
86
+ export async function judgeNodes(judge, items, graph, report) {
87
+ const { units, overCap } = planUnits(graph, items, report);
88
+ const out = items.map(({ rules }) => new Array(rules.length));
89
+ const place = (unit, judgements) => {
90
+ unit.positions.forEach((position, k) => {
91
+ out[unit.item][position] = judgements[k];
92
+ });
93
+ };
94
+ for (const slot of overCap)
95
+ place(slot, errorJudgements(slot.rules, OVER_CAP_MESSAGE));
96
+ if (!judge.evaluateNodes) {
97
+ for (const unit of units) {
98
+ try {
99
+ place(unit, interpret(unit.rules, unit.resolved, await judge.evaluate(unit.input)));
100
+ }
101
+ catch (err) {
102
+ place(unit, errorJudgements(unit.rules, errorMessage(err)));
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ if (units.length === 0)
108
+ return out;
109
+ let perInput;
110
+ try {
111
+ perInput = await judge.evaluateNodes(units.map((u) => u.input));
112
+ }
113
+ catch (err) {
114
+ for (const unit of units)
115
+ place(unit, errorJudgements(unit.rules, errorMessage(err)));
116
+ return out;
117
+ }
118
+ units.forEach((unit, k) => {
119
+ place(unit, interpret(unit.rules, unit.resolved, perInput[k] ?? {}));
120
+ });
121
+ return out;
122
+ }
@@ -0,0 +1,10 @@
1
+ import type { SemanticJudgeInput, SemanticJudgeResult, SemanticRuleSpec } from '../types.js';
2
+ /** System prompt: a strict judge that answers with one verdict from a closed set, as JSON. */
3
+ export declare function buildJudgeSystemPrompt(): string;
4
+ /** User prompt for one rule on one node. */
5
+ export declare function buildJudgeUserPrompt({ node, related }: Pick<SemanticJudgeInput, 'node' | 'related'>, rule: SemanticRuleSpec): string;
6
+ /**
7
+ * Parse judge output into a verdict from the rule's allowed set.
8
+ * Handles clean JSON, markdown-wrapped JSON, and a regex fallback for surrounding text.
9
+ */
10
+ export declare function parseJudgeResponse(rawText: string, allowed: string[]): SemanticJudgeResult;
@@ -0,0 +1,92 @@
1
+ function verdictList(rule) {
2
+ return Object.entries(rule.verdicts).map(([name, description]) => `- "${name}": ${description}`);
3
+ }
4
+ /** System prompt: a strict judge that answers with one verdict from a closed set, as JSON. */
5
+ export function buildJudgeSystemPrompt() {
6
+ return [
7
+ 'You are a strict, objective evaluation judge for structured planning and knowledge documents.',
8
+ 'Your task is to choose the one verdict that best describes whether a node satisfies a rule.',
9
+ '',
10
+ 'You must respond ONLY with a JSON object in this exact format:',
11
+ '{',
12
+ ' "verdict": "<exactly one of the allowed verdicts>",',
13
+ ' "confidence": <number from 0 to 1>,',
14
+ ' "evidence": "concise explanation citing specific quotes, phrases, or absent details from the node"',
15
+ '}',
16
+ '',
17
+ 'Guidelines:',
18
+ '1. "verdict" must be one of the allowed verdicts listed for the rule, spelled exactly.',
19
+ '2. "confidence" is how sure you are of the verdict.',
20
+ '3. "evidence" must directly cite or quote relevant parts of the node, explaining the reasoning concisely.',
21
+ '4. Do not include markdown code block formatting, intro text, or outro text. Respond only with the JSON object.',
22
+ ].join('\n');
23
+ }
24
+ /** User prompt for one rule on one node. */
25
+ export function buildJudgeUserPrompt({ node, related = [] }, rule) {
26
+ const rubric = rule.rubric || rule.description;
27
+ const lines = [
28
+ '# Rule to Evaluate',
29
+ `- ID: ${rule.id}`,
30
+ `- Category: ${rule.category}`,
31
+ `- Rubric: ${rubric}`,
32
+ '',
33
+ '## Allowed verdicts',
34
+ ...verdictList(rule),
35
+ '',
36
+ '# Node Under Evaluation',
37
+ `- File / Label: ${node.label}`,
38
+ `- Title: ${node.title}`,
39
+ `- Type: ${node.type} (resolved: ${node.resolvedType})`,
40
+ ];
41
+ if (node.resolvedParents && node.resolvedParents.length > 0) {
42
+ lines.push(`- Parents: ${node.resolvedParents.map((p) => p.title).join(', ')}`);
43
+ }
44
+ lines.push('', '## Node Content & Frontmatter:', '```json', JSON.stringify(node.schemaData, null, 2), '```', '');
45
+ if (related.length > 0) {
46
+ lines.push('# Related Nodes', 'Each is listed with its relation to the node under evaluation.', '');
47
+ for (const r of related) {
48
+ lines.push(`## ${r.title}`, `- Relation: ${r.relations.join(', ')}`, `- Type: ${r.type}`, '```json', JSON.stringify(r.data, null, 2), '```', '');
49
+ }
50
+ }
51
+ lines.push('Evaluate this node against the rule. Return only JSON with "verdict", "confidence" and "evidence".');
52
+ return lines.join('\n');
53
+ }
54
+ /**
55
+ * Parse judge output into a verdict from the rule's allowed set.
56
+ * Handles clean JSON, markdown-wrapped JSON, and a regex fallback for surrounding text.
57
+ */
58
+ export function parseJudgeResponse(rawText, allowed) {
59
+ let cleaned = rawText.trim();
60
+ cleaned = cleaned.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '');
61
+ let verdict;
62
+ let confidence;
63
+ let evidence;
64
+ try {
65
+ const parsed = JSON.parse(cleaned);
66
+ if (typeof parsed.verdict === 'string')
67
+ verdict = parsed.verdict.trim();
68
+ if (typeof parsed.confidence === 'number')
69
+ confidence = parsed.confidence;
70
+ if (typeof parsed.evidence === 'string')
71
+ evidence = parsed.evidence.trim();
72
+ }
73
+ catch {
74
+ const verdictMatch = /"verdict"\s*:\s*"((?:[^"\\]|\\.)*)"/i.exec(cleaned);
75
+ const confidenceMatch = /"confidence"\s*:\s*([0-9.]+)/i.exec(cleaned);
76
+ const evidenceMatch = /"evidence"\s*:\s*"((?:[^"\\]|\\.)*)"/i.exec(cleaned);
77
+ verdict = verdictMatch?.[1]?.trim();
78
+ confidence = confidenceMatch ? Number(confidenceMatch[1]) : undefined;
79
+ evidence = evidenceMatch?.[1]?.replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
80
+ }
81
+ if (verdict === undefined || !allowed.includes(verdict)) {
82
+ return {
83
+ error: `Model provider returned ${verdict === undefined ? 'no verdict' : `unknown verdict "${verdict}"`} (allowed: ${allowed.join(', ')}): ${rawText.slice(0, 200)}`,
84
+ };
85
+ }
86
+ return {
87
+ verdict,
88
+ ...(confidence !== undefined &&
89
+ Number.isFinite(confidence) && { confidence: Math.min(1, Math.max(0, confidence)) }),
90
+ ...(evidence && { evidence }),
91
+ };
92
+ }
@@ -0,0 +1,17 @@
1
+ import type { HydratedProvider, SemanticJudgeInput, SemanticJudgeResult, SemanticRuleSpec } from '../types';
2
+ import { PerRuleJudgeProvider } from './per-rule';
3
+ export interface AnthropicJudgeProviderOptions extends HydratedProvider {
4
+ /** The factory requires a resolved key before constructing a non-OpenAI provider. */
5
+ apiKey?: string;
6
+ }
7
+ export declare const RECOMMENDED_ANTHROPIC_MODEL = "claude-haiku-5-5";
8
+ export declare const ANTHROPIC_MESSAGES_ENDPOINT = "https://api.anthropic.com/v1/messages";
9
+ /** Judge provider for the Anthropic Messages API. Reads no environment: it takes hydrated values. */
10
+ export declare class AnthropicJudgeProvider extends PerRuleJudgeProvider {
11
+ readonly name = "anthropic";
12
+ private apiKey;
13
+ private model;
14
+ private endpoint;
15
+ constructor(options: AnthropicJudgeProviderOptions);
16
+ protected judgeRule(input: SemanticJudgeInput, rule: SemanticRuleSpec): Promise<SemanticJudgeResult>;
17
+ }
@@ -0,0 +1,43 @@
1
+ import { buildJudgeSystemPrompt, buildJudgeUserPrompt, parseJudgeResponse } from '../prompt';
2
+ import { PerRuleJudgeProvider } from './per-rule';
3
+ export const RECOMMENDED_ANTHROPIC_MODEL = 'claude-haiku-5-5';
4
+ export const ANTHROPIC_MESSAGES_ENDPOINT = 'https://api.anthropic.com/v1/messages';
5
+ /** Judge provider for the Anthropic Messages API. Reads no environment: it takes hydrated values. */
6
+ export class AnthropicJudgeProvider extends PerRuleJudgeProvider {
7
+ name = 'anthropic';
8
+ apiKey;
9
+ model;
10
+ endpoint;
11
+ constructor(options) {
12
+ super();
13
+ this.apiKey = options.apiKey ?? '';
14
+ this.model = options.model || RECOMMENDED_ANTHROPIC_MODEL;
15
+ this.endpoint = options.endpoint || ANTHROPIC_MESSAGES_ENDPOINT;
16
+ }
17
+ async judgeRule(input, rule) {
18
+ const res = await fetch(this.endpoint, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'content-type': 'application/json',
22
+ 'x-api-key': this.apiKey,
23
+ 'anthropic-version': '2023-06-01',
24
+ },
25
+ body: JSON.stringify({
26
+ model: this.model,
27
+ max_tokens: 1024,
28
+ system: buildJudgeSystemPrompt(),
29
+ messages: [{ role: 'user', content: buildJudgeUserPrompt(input, rule) }],
30
+ }),
31
+ });
32
+ if (!res.ok) {
33
+ const errorText = await res.text().catch(() => '');
34
+ throw new Error(`Anthropic API error (${res.status}): ${errorText}`);
35
+ }
36
+ const data = (await res.json());
37
+ const text = data.content?.find((c) => c.type === 'text')?.text;
38
+ if (!text) {
39
+ throw new Error('Anthropic API returned empty or missing text block in response.');
40
+ }
41
+ return parseJudgeResponse(text, Object.keys(rule.verdicts));
42
+ }
43
+ }
@@ -0,0 +1,26 @@
1
+ import type { SemanticJudgeInput, SemanticJudgeProvider, SemanticJudgeResult, SemanticRuleSpec } from '../types';
2
+ /** A fake answer: a full verdict, or the `{ pass }` shorthand mapped onto the default `pass`/`fail` verdicts. */
3
+ export type FakeAnswer = SemanticJudgeResult | {
4
+ pass: boolean;
5
+ evidence?: string;
6
+ };
7
+ export type FakeJudgeHandler = (call: {
8
+ node: SemanticJudgeInput['node'];
9
+ rule: SemanticRuleSpec;
10
+ }) => Promise<FakeAnswer> | FakeAnswer;
11
+ type FakeConfig = FakeAnswer | FakeJudgeHandler | Map<string, FakeAnswer> | Record<string, FakeAnswer>;
12
+ /**
13
+ * In-memory fake judge for offline, deterministic tests. Configure with a single answer, a handler,
14
+ * or answers keyed by `ruleId:title`, `ruleId:label` or `ruleId` (most specific wins). Unmatched calls get `pass`,
15
+ * or the rule's first declared verdict.
16
+ */
17
+ export declare class FakeJudgeProvider implements SemanticJudgeProvider {
18
+ private config?;
19
+ readonly name = "fake";
20
+ callHistory: SemanticJudgeInput[];
21
+ constructor(config?: FakeConfig | undefined);
22
+ evaluate(input: SemanticJudgeInput): Promise<Record<string, SemanticJudgeResult>>;
23
+ private answer;
24
+ clearHistory(): void;
25
+ }
26
+ export {};
@@ -0,0 +1,56 @@
1
+ function isHandler(c) {
2
+ return typeof c === 'function';
3
+ }
4
+ function isAnswer(c) {
5
+ return 'pass' in c || 'verdict' in c || 'error' in c;
6
+ }
7
+ function normalise(answer) {
8
+ if ('pass' in answer) {
9
+ return { verdict: answer.pass ? 'pass' : 'fail', evidence: answer.evidence };
10
+ }
11
+ return answer;
12
+ }
13
+ /**
14
+ * In-memory fake judge for offline, deterministic tests. Configure with a single answer, a handler,
15
+ * or answers keyed by `ruleId:title`, `ruleId:label` or `ruleId` (most specific wins). Unmatched calls get `pass`,
16
+ * or the rule's first declared verdict.
17
+ */
18
+ export class FakeJudgeProvider {
19
+ config;
20
+ name = 'fake';
21
+ callHistory = [];
22
+ constructor(config) {
23
+ this.config = config;
24
+ }
25
+ async evaluate(input) {
26
+ this.callHistory.push(input);
27
+ const results = {};
28
+ for (const rule of input.rules) {
29
+ results[rule.id] = normalise(await this.answer(input.node, rule));
30
+ }
31
+ return results;
32
+ }
33
+ async answer(node, rule) {
34
+ // The fake does not know severities: default to `pass`, else the first declared verdict.
35
+ const passing = { verdict: 'pass' in rule.verdicts ? 'pass' : (Object.keys(rule.verdicts)[0] ?? 'pass') };
36
+ const fallback = { ...passing, evidence: `Fake passed evaluation for rule "${rule.id}"` };
37
+ const config = this.config;
38
+ if (!config)
39
+ return fallback;
40
+ if (isHandler(config))
41
+ return await config({ node, rule });
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);
47
+ }
48
+ if (isAnswer(config))
49
+ return config;
50
+ const rec = config;
51
+ return rec[`${rule.id}:${node.title}`] ?? rec[`${rule.id}:${node.label}`] ?? rec[rule.id] ?? fallback;
52
+ }
53
+ clearHistory() {
54
+ this.callHistory = [];
55
+ }
56
+ }
@@ -0,0 +1,37 @@
1
+ import type { HydratedProvider, SemanticJudgeInput, SemanticJudgeProvider, SemanticJudgeResult } from '../types';
2
+ export interface JevJudgeProviderOptions extends HydratedProvider {
3
+ apiKey?: string;
4
+ }
5
+ export declare const JEV_LATEST = "jev-latest";
6
+ export declare const OPENROUTER_DECISIONS_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
7
+ /** Request packing limit: estimated input tokens (characters / 4) in one DecisionRequest's `state` plus `questions`. */
8
+ export declare const JEV_REQUEST_PACKING_LIMIT_TOKENS = 32000;
9
+ /** Request packing limit on the number of DecisionQuestions in one DecisionRequest. */
10
+ export declare const JEV_REQUEST_PACKING_LIMIT_QUESTIONS = 40;
11
+ /**
12
+ * Judge provider for TypeSafe's Jev decision model via OpenRouter's Decisions API.
13
+ *
14
+ * Each node is sent once as `state`, with one `choice` question per semantic rule whose criteria are
15
+ * the rule's closed verdict set. `evaluateNodes` shares one DecisionRequest between nodes with the same rule set. Jev returns the chosen verdict with confidence and probabilities,
16
+ * but no prose, so `evidence` is a summary of the distribution.
17
+ */
18
+ export declare class JevJudgeProvider implements SemanticJudgeProvider {
19
+ readonly name = "jev";
20
+ private apiKey;
21
+ private model;
22
+ private endpoint;
23
+ constructor(options: JevJudgeProviderOptions);
24
+ evaluate(input: SemanticJudgeInput): Promise<Record<string, SemanticJudgeResult>>;
25
+ /**
26
+ * Judge many nodes with fewer DecisionRequests. Nodes that share the same semantic rule set form a
27
+ * NodeRulesetGroup, sent as one DecisionRequest,
28
+ * with every node under `state.nodes` and one DecisionQuestion per node and rule. A NodeRulesetGroup is split into DecisionRequests by
29
+ * `JEV_REQUEST_PACKING_LIMIT_TOKENS` and `JEV_REQUEST_PACKING_LIMIT_QUESTIONS`, never within a node. A node alone in its DecisionRequest, even
30
+ * one over the token limit, is sent whole as a plain single-node DecisionRequest; one with more rules than the question limit has its rules split
31
+ * across DecisionRequests. A failed DecisionRequest marks only its own nodes' rules
32
+ * as errors.
33
+ */
34
+ evaluateNodes(inputs: SemanticJudgeInput[]): Promise<Array<Record<string, SemanticJudgeResult>>>;
35
+ private evaluateDecisionRequest;
36
+ private post;
37
+ }
@@ -0,0 +1,169 @@
1
+ import { estimateTokens } from '../trim';
2
+ export const JEV_LATEST = 'jev-latest';
3
+ export const OPENROUTER_DECISIONS_ENDPOINT = 'https://openrouter.ai/api/alpha/decisions';
4
+ /** Request packing limit: estimated input tokens (characters / 4) in one DecisionRequest's `state` plus `questions`. */
5
+ export const JEV_REQUEST_PACKING_LIMIT_TOKENS = 32_000;
6
+ /** Request packing limit on the number of DecisionQuestions in one DecisionRequest. */
7
+ export const JEV_REQUEST_PACKING_LIMIT_QUESTIONS = 40;
8
+ /**
9
+ * Judge provider for TypeSafe's Jev decision model via OpenRouter's Decisions API.
10
+ *
11
+ * Each node is sent once as `state`, with one `choice` question per semantic rule whose criteria are
12
+ * the rule's closed verdict set. `evaluateNodes` shares one DecisionRequest between nodes with the same rule set. Jev returns the chosen verdict with confidence and probabilities,
13
+ * but no prose, so `evidence` is a summary of the distribution.
14
+ */
15
+ export class JevJudgeProvider {
16
+ name = 'jev';
17
+ apiKey;
18
+ model;
19
+ endpoint;
20
+ constructor(options) {
21
+ this.apiKey = options.apiKey ?? '';
22
+ this.model = options.model || JEV_LATEST;
23
+ this.endpoint = options.endpoint || OPENROUTER_DECISIONS_ENDPOINT;
24
+ }
25
+ async evaluate(input) {
26
+ const { node, rules } = input;
27
+ const results = {};
28
+ // A node with more rules than one DecisionRequest allows is split by rule; the state is sent whole each time.
29
+ for (let start = 0; start < rules.length; start += JEV_REQUEST_PACKING_LIMIT_QUESTIONS) {
30
+ const part = rules.slice(start, start + JEV_REQUEST_PACKING_LIMIT_QUESTIONS);
31
+ const questions = {};
32
+ for (const rule of part)
33
+ questions[rule.id] = decisionQuestion(rule, rule.rubric || rule.description);
34
+ const data = await this.post(nodeState(node, input.related), questions);
35
+ for (const rule of part)
36
+ results[rule.id] = toResult(rule, data.answers?.[rule.id]);
37
+ }
38
+ return results;
39
+ }
40
+ /**
41
+ * Judge many nodes with fewer DecisionRequests. Nodes that share the same semantic rule set form a
42
+ * NodeRulesetGroup, sent as one DecisionRequest,
43
+ * with every node under `state.nodes` and one DecisionQuestion per node and rule. A NodeRulesetGroup is split into DecisionRequests by
44
+ * `JEV_REQUEST_PACKING_LIMIT_TOKENS` and `JEV_REQUEST_PACKING_LIMIT_QUESTIONS`, never within a node. A node alone in its DecisionRequest, even
45
+ * one over the token limit, is sent whole as a plain single-node DecisionRequest; one with more rules than the question limit has its rules split
46
+ * across DecisionRequests. A failed DecisionRequest marks only its own nodes' rules
47
+ * as errors.
48
+ */
49
+ async evaluateNodes(inputs) {
50
+ const results = inputs.map(() => ({}));
51
+ const nodeRulesetGroups = new Map();
52
+ inputs.forEach((input, i) => {
53
+ if (input.rules.length === 0)
54
+ return;
55
+ const key = JSON.stringify(input.rules);
56
+ nodeRulesetGroups.set(key, [...(nodeRulesetGroups.get(key) ?? []), i]);
57
+ });
58
+ for (const indices of nodeRulesetGroups.values()) {
59
+ for (const requestInputs of planDecisionRequests(indices, inputs)) {
60
+ try {
61
+ if (requestInputs.length === 1) {
62
+ results[requestInputs[0]] = await this.evaluate(inputs[requestInputs[0]]);
63
+ }
64
+ else {
65
+ await this.evaluateDecisionRequest(requestInputs, inputs, results);
66
+ }
67
+ }
68
+ catch (err) {
69
+ const error = err instanceof Error ? err.message : String(err);
70
+ for (const i of requestInputs) {
71
+ results[i] = Object.fromEntries(inputs[i].rules.map((r) => [r.id, { error }]));
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return results;
77
+ }
78
+ async evaluateDecisionRequest(requestInputs, inputs, results) {
79
+ const nodes = {};
80
+ const questions = {};
81
+ requestInputs.forEach((inputIndex, k) => {
82
+ const input = inputs[inputIndex];
83
+ nodes[`n${k}`] = nodeState(input.node, input.related);
84
+ for (const rule of input.rules) {
85
+ questions[`${rule.id}__n${k}`] = decisionQuestion(rule, `For node \`nodes.n${k}\`: ${rule.rubric || rule.description}`);
86
+ }
87
+ });
88
+ const data = await this.post({ nodes }, questions);
89
+ requestInputs.forEach((inputIndex, k) => {
90
+ for (const rule of inputs[inputIndex].rules) {
91
+ results[inputIndex][rule.id] = toResult(rule, data.answers?.[`${rule.id}__n${k}`]);
92
+ }
93
+ });
94
+ }
95
+ async post(state, questions) {
96
+ const res = await fetch(this.endpoint, {
97
+ method: 'POST',
98
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${this.apiKey}` },
99
+ body: JSON.stringify({ model: this.model, state, questions }),
100
+ });
101
+ if (!res.ok) {
102
+ const errorText = await res.text().catch(() => '');
103
+ throw new Error(`Jev API error (${res.status}): ${errorText}`);
104
+ }
105
+ return (await res.json());
106
+ }
107
+ }
108
+ function nodeState(node, related = []) {
109
+ return {
110
+ title: node.title,
111
+ type: node.resolvedType,
112
+ parents: node.resolvedParents?.map((p) => p.title) ?? [],
113
+ ...node.schemaData,
114
+ ...(related.length > 0 && {
115
+ relatedNodes: related.map((r) => ({
116
+ relation: r.relations.join(', '),
117
+ title: r.title,
118
+ type: r.type,
119
+ ...r.data,
120
+ })),
121
+ }),
122
+ };
123
+ }
124
+ function decisionQuestion(rule, instructions) {
125
+ return { type: 'choice', instructions, criteria: rule.verdicts };
126
+ }
127
+ function toResult(rule, answer) {
128
+ if (!answer?.choice || !(answer.choice in rule.verdicts)) {
129
+ return { error: `Jev returned no valid verdict for rule "${rule.id}" (choice: ${answer?.choice})` };
130
+ }
131
+ return {
132
+ verdict: answer.choice,
133
+ confidence: answer.confidence,
134
+ probabilities: answer.probabilities,
135
+ evidence: describeDistribution(answer),
136
+ };
137
+ }
138
+ /** Plan the DecisionRequests for one NodeRulesetGroup (inputs sharing a rule set): runs of nodes, in order, never splitting a node. */
139
+ function planDecisionRequests(indices, inputs) {
140
+ const requests = [];
141
+ let current = [];
142
+ let tokens = 0;
143
+ let questions = 0;
144
+ for (const i of indices) {
145
+ const input = inputs[i];
146
+ const cost = estimateTokens(nodeState(input.node, input.related)) + estimateTokens(input.rules);
147
+ const count = input.rules.length;
148
+ if (current.length > 0 &&
149
+ (tokens + cost > JEV_REQUEST_PACKING_LIMIT_TOKENS || questions + count > JEV_REQUEST_PACKING_LIMIT_QUESTIONS)) {
150
+ requests.push(current);
151
+ current = [];
152
+ tokens = 0;
153
+ questions = 0;
154
+ }
155
+ current.push(i);
156
+ tokens += cost;
157
+ questions += count;
158
+ }
159
+ if (current.length > 0)
160
+ requests.push(current);
161
+ return requests;
162
+ }
163
+ function describeDistribution(answer) {
164
+ const parts = Object.entries(answer.probabilities ?? {})
165
+ .sort(([, a], [, b]) => b - a)
166
+ .map(([verdict, p]) => `${verdict} ${p.toFixed(2)}`);
167
+ const confidence = answer.confidence !== undefined ? `confidence ${answer.confidence.toFixed(2)}` : undefined;
168
+ return `Jev chose "${answer.choice}"${[confidence, ...parts].filter(Boolean).length ? ` (${[confidence, ...parts].filter(Boolean).join('; ')})` : ''}`;
169
+ }
@@ -0,0 +1,21 @@
1
+ import type { HydratedProvider, SemanticJudgeInput, SemanticJudgeProvider, SemanticJudgeResult } from '../types';
2
+ export interface OpenAIDecisionsJudgeProviderOptions extends HydratedProvider {
3
+ apiKey?: string;
4
+ }
5
+ export declare const RECOMMENDED_OPENAI_DECISIONS_MODEL = "gpt-6-luna";
6
+ export declare const OPENAI_DECISIONS_ENDPOINT = "https://api.openai.com/v1/decisions";
7
+ /**
8
+ * Judge provider for OpenAI's Decisions API (https://developers.openai.com/api/docs/guides/decisions).
9
+ *
10
+ * Each node is sent once as the `input` text, with one `choice` question per semantic rule whose
11
+ * choices are the rule's closed verdict set. The API returns the chosen value with per-choice
12
+ * probabilities and a confidence, but no prose, so `evidence` is a summary of the distribution.
13
+ */
14
+ export declare class OpenAIDecisionsJudgeProvider implements SemanticJudgeProvider {
15
+ readonly name = "openai-decisions";
16
+ private apiKey;
17
+ private model;
18
+ private endpoint;
19
+ constructor(options: OpenAIDecisionsJudgeProviderOptions);
20
+ evaluate(input: SemanticJudgeInput): Promise<Record<string, SemanticJudgeResult>>;
21
+ }