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.
Files changed (138) hide show
  1. package/README.md +52 -198
  2. package/dist/api.d.ts +17 -15
  3. package/dist/api.js +12 -9
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/docs.js +1 -0
  7. package/dist/commands/dump.d.ts +15 -2
  8. package/dist/commands/dump.js +18 -4
  9. package/dist/commands/providers.d.ts +4 -0
  10. package/dist/commands/providers.js +52 -0
  11. package/dist/commands/render.d.ts +4 -0
  12. package/dist/commands/render.js +64 -9
  13. package/dist/commands/schemas.d.ts +2 -2
  14. package/dist/commands/schemas.js +133 -22
  15. package/dist/commands/show.d.ts +1 -0
  16. package/dist/commands/show.js +7 -1
  17. package/dist/commands/spaces.d.ts +3 -1
  18. package/dist/commands/spaces.js +11 -2
  19. package/dist/commands/validate-file.d.ts +3 -0
  20. package/dist/commands/validate-file.js +24 -6
  21. package/dist/commands/validate.d.ts +4 -0
  22. package/dist/commands/validate.js +79 -23
  23. package/dist/config.d.ts +25 -1
  24. package/dist/config.js +106 -12
  25. package/dist/filter/augment-nodes.d.ts +3 -1
  26. package/dist/filter/augment-nodes.js +12 -7
  27. package/dist/filter/filter-nodes.js +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +96 -31
  30. package/dist/judge/context.d.ts +20 -0
  31. package/dist/judge/context.js +105 -0
  32. package/dist/judge/factory.d.ts +44 -0
  33. package/dist/judge/factory.js +148 -0
  34. package/dist/judge/index.d.ts +13 -0
  35. package/dist/judge/index.js +8 -0
  36. package/dist/judge/judge-node.d.ts +27 -0
  37. package/dist/judge/judge-node.js +122 -0
  38. package/dist/judge/prompt.d.ts +10 -0
  39. package/dist/judge/prompt.js +92 -0
  40. package/dist/judge/provider/anthropic.d.ts +17 -0
  41. package/dist/judge/provider/anthropic.js +43 -0
  42. package/dist/judge/provider/fake.d.ts +26 -0
  43. package/dist/judge/provider/fake.js +56 -0
  44. package/dist/judge/provider/jev.d.ts +37 -0
  45. package/dist/judge/provider/jev.js +169 -0
  46. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  47. package/dist/judge/provider/openai-decisions.js +84 -0
  48. package/dist/judge/provider/openai.d.ts +20 -0
  49. package/dist/judge/provider/openai.js +53 -0
  50. package/dist/judge/provider/per-rule.d.ts +10 -0
  51. package/dist/judge/provider/per-rule.js +18 -0
  52. package/dist/judge/trim.d.ts +27 -0
  53. package/dist/judge/trim.js +62 -0
  54. package/dist/judge/types.d.ts +92 -0
  55. package/dist/judge/types.js +9 -0
  56. package/dist/judge/verdicts.d.ts +24 -0
  57. package/dist/judge/verdicts.js +82 -0
  58. package/dist/load-space-graph.d.ts +7 -5
  59. package/dist/load-space-graph.js +19 -8
  60. package/dist/plugins/loader.js +3 -3
  61. package/dist/plugins/markdown/extract-content-links.js +8 -29
  62. package/dist/plugins/markdown/index.js +8 -2
  63. package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
  64. package/dist/plugins/markdown/parse-embedded.js +195 -24
  65. package/dist/plugins/markdown/read-space.js +42 -14
  66. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  67. package/dist/plugins/markdown/render-directory.js +130 -0
  68. package/dist/plugins/markdown/template-sync.js +3 -4
  69. package/dist/plugins/markdown/util.d.ts +26 -5
  70. package/dist/plugins/markdown/util.js +56 -15
  71. package/dist/plugins/mermaid/render-mermaid.js +2 -1
  72. package/dist/plugins/miro/sync.js +7 -6
  73. package/dist/plugins/util.d.ts +39 -1
  74. package/dist/plugins/util.js +139 -0
  75. package/dist/read/resolve-graph-edges.d.ts +1 -1
  76. package/dist/read/resolve-graph-edges.js +46 -27
  77. package/dist/read/wikilink-utils.d.ts +17 -0
  78. package/dist/read/wikilink-utils.js +40 -0
  79. package/dist/render/render.d.ts +3 -1
  80. package/dist/render/render.js +2 -2
  81. package/dist/schema/evaluate-rule.d.ts +33 -2
  82. package/dist/schema/evaluate-rule.js +58 -4
  83. package/dist/schema/gating.d.ts +69 -0
  84. package/dist/schema/gating.js +253 -0
  85. package/dist/schema/meta-schemas.d.ts +13 -0
  86. package/dist/schema/meta-schemas.js +50 -0
  87. package/dist/schema/metadata-contract.d.ts +952 -183
  88. package/dist/schema/metadata-contract.js +177 -33
  89. package/dist/schema/rule-filter.d.ts +12 -0
  90. package/dist/schema/rule-filter.js +46 -0
  91. package/dist/schema/rule-severity.d.ts +13 -0
  92. package/dist/schema/rule-severity.js +33 -0
  93. package/dist/schema/schema.d.ts +14 -8
  94. package/dist/schema/schema.js +308 -199
  95. package/dist/schema/validate-graph.d.ts +2 -1
  96. package/dist/schema/validate-graph.js +40 -23
  97. package/dist/schema/validate-rules.d.ts +16 -3
  98. package/dist/schema/validate-rules.js +91 -29
  99. package/dist/schemas/_knowledge_wiki.json +24 -24
  100. package/dist/schemas/_okf.json +172 -0
  101. package/dist/schemas/_ost_strict.json +17 -8
  102. package/dist/schemas/_sctx_base.json +3 -3
  103. package/dist/schemas/_strategy_general.json +38 -38
  104. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  105. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  106. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  107. package/dist/schemas/knowledge_wiki.json +10 -9
  108. package/dist/schemas/okf.json +8 -0
  109. package/dist/schemas/strategy_general.json +15 -14
  110. package/dist/schemas/strict_ost.json +17 -16
  111. package/dist/space-context.d.ts +5 -2
  112. package/dist/space-context.js +9 -4
  113. package/dist/space-graph.d.ts +9 -7
  114. package/dist/space-graph.js +7 -5
  115. package/dist/testing.d.ts +33 -0
  116. package/dist/testing.js +72 -0
  117. package/dist/types.d.ts +64 -16
  118. package/dist/validate.d.ts +62 -9
  119. package/dist/validate.js +229 -114
  120. package/docs/concepts.md +65 -9
  121. package/docs/config.md +78 -1
  122. package/docs/rules.md +168 -50
  123. package/docs/schemas.md +152 -52
  124. package/package.json +17 -11
  125. package/schemas/_knowledge_wiki.json +24 -24
  126. package/schemas/_okf.json +172 -0
  127. package/schemas/_ost_strict.json +17 -8
  128. package/schemas/_sctx_base.json +3 -3
  129. package/schemas/_strategy_general.json +38 -38
  130. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  131. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  132. package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  133. package/schemas/knowledge_wiki.json +10 -9
  134. package/schemas/okf.json +8 -0
  135. package/schemas/strategy_general.json +15 -14
  136. package/schemas/strict_ost.json +17 -16
  137. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  138. 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 { 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';
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
- const exitCode = await validateFileCommand(filePath, { json: options.json });
49
- process.exit(exitCode);
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
- const exitCode = await validate(context, { json: options.json });
64
- process.exit(exitCode);
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
- .action((spaceName, options) => show(buildSpaceContext(spaceName), options));
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
- .action((spaceName) => dump(buildSpaceContext(spaceName)));
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
- .action(listSpaces);
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('-o, --output <path>', 'Output file path (default: stdout)')
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[][]>;