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
@@ -0,0 +1,253 @@
1
+ import { judgeNodes } from '../judge/judge-node';
2
+ import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
3
+ import { ruleSeverities, ruleSeverity } from './rule-severity';
4
+ /**
5
+ * Whether a failure of this rule gates its subtree: explicit `gatesDescendants`, else true only for `error` severity.
6
+ * `severity` is that of the failure: the rule's own for an expression, the verdict's for a semantic part.
7
+ */
8
+ export function ruleGatesDescendants(rule, severity = ruleSeverity(rule)) {
9
+ return rule.gatesDescendants ?? severity === 'error';
10
+ }
11
+ /** Whether any violation of this rule could gate its subtree. */
12
+ function ruleCanGate(rule) {
13
+ return rule.gatesDescendants ?? ruleSeverities(rule).includes('error');
14
+ }
15
+ /**
16
+ * Compute breadth-first depth (1-based, from hierarchy roots and orphans) for every
17
+ * hierarchy-typed node in the graph. Non-hierarchy nodes are not included.
18
+ */
19
+ export function computeHierarchyDepths(graph) {
20
+ const depths = new Map();
21
+ const queue = [];
22
+ for (const node of [...graph.hierarchyRoots, ...graph.orphans]) {
23
+ queue.push({ title: node.title, depth: 1 });
24
+ }
25
+ let i = 0;
26
+ while (i < queue.length) {
27
+ const { title, depth } = queue[i++];
28
+ if (depths.has(title))
29
+ continue;
30
+ depths.set(title, depth);
31
+ for (const child of graph.hierarchyChildren.get(title) ?? []) {
32
+ if (!depths.has(child.title))
33
+ queue.push({ title: child.title, depth: depth + 1 });
34
+ }
35
+ }
36
+ return depths;
37
+ }
38
+ /**
39
+ * Evaluate executable rules with cascade gating: a schema, hierarchy, or blocking-rule
40
+ * failure on a node suppresses rule evaluation for that node (when the failure is
41
+ * schema/hierarchy) and its hierarchy subtree, unless `includeGated` is set.
42
+ *
43
+ * Traversal is breadth-first from hierarchy roots and orphans. Non-hierarchy nodes
44
+ * (not part of the hierarchy DAG) are always evaluated — cascade gating only applies
45
+ * within hierarchy subtrees. Global rules always run, unaffected by gating.
46
+ */
47
+ export async function evaluateRulesWithGating(nodes, rules, graph, seeds, options = {}) {
48
+ const includeGated = options.includeGated ?? false;
49
+ const nodeIndex = new Map();
50
+ for (const node of nodes) {
51
+ if (node.title)
52
+ nodeIndex.set(node.title, node);
53
+ }
54
+ // When a node has both a schema and hierarchy failure, report schema as the reason.
55
+ const seedByTitle = new Map();
56
+ for (const seed of seeds) {
57
+ if (!seedByTitle.has(seed.title) || seed.reason === 'schema') {
58
+ seedByTitle.set(seed.title, seed);
59
+ }
60
+ }
61
+ const judge = options.provider;
62
+ const violations = [];
63
+ const gateOrigins = new Map();
64
+ function originFor(title, seed) {
65
+ let origin = gateOrigins.get(title);
66
+ if (!origin) {
67
+ origin = { file: seed.file, nodeTitle: title, reason: seed.reason, ruleId: seed.ruleId, gatedFiles: [] };
68
+ gateOrigins.set(title, origin);
69
+ }
70
+ return origin;
71
+ }
72
+ const globalRules = rules.filter((r) => r.global && r.expression !== undefined);
73
+ const sentinel = nodes[0];
74
+ if (sentinel) {
75
+ for (const rule of globalRules) {
76
+ const result = await evaluateRuleExpression(rule, sentinel, nodes, nodeIndex, '');
77
+ if (!result.passed)
78
+ violations.push(result.violation);
79
+ }
80
+ }
81
+ const perNodeRules = rules.filter((r) => !r.global && (r.expression !== undefined || (r.semantic !== undefined && judge)));
82
+ // With a judge target, only the target and its hierarchy ancestors can need judging.
83
+ const targetAncestors = new Set();
84
+ if (options.judgeTarget !== undefined) {
85
+ const parentsOf = new Map();
86
+ for (const [parent, children] of graph.hierarchyChildren) {
87
+ for (const child of children)
88
+ parentsOf.set(child.title, [...(parentsOf.get(child.title) ?? []), parent]);
89
+ }
90
+ const target = nodes.find((n) => n.label === options.judgeTarget);
91
+ const pending = target ? [target.title] : [];
92
+ while (pending.length > 0) {
93
+ for (const parent of parentsOf.get(pending.pop()) ?? []) {
94
+ if (!targetAncestors.has(parent)) {
95
+ targetAncestors.add(parent);
96
+ pending.push(parent);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ /** Rules that apply to this node: matching `type`, passing their `when` test, and (for a judge target) worth judging. */
102
+ async function applicableRules(node, onWhenError) {
103
+ const applicable = [];
104
+ for (const r of perNodeRules) {
105
+ if (r.type && r.type !== node.resolvedType)
106
+ continue;
107
+ if (judge &&
108
+ r.semantic !== undefined &&
109
+ options.judgeTarget !== undefined &&
110
+ node.label !== options.judgeTarget) {
111
+ // Ancestors only matter through the gates they could raise.
112
+ if (!(targetAncestors.has(node.title) && ruleCanGate(r)))
113
+ continue;
114
+ }
115
+ if (r.when) {
116
+ const outcome = await tryEvaluateExpression(r.when, buildEvalContext(node, nodes, nodeIndex));
117
+ if ('error' in outcome) {
118
+ onWhenError?.(r, outcome.error);
119
+ continue;
120
+ }
121
+ if (outcome.value !== true)
122
+ continue;
123
+ }
124
+ applicable.push(r);
125
+ }
126
+ return applicable;
127
+ }
128
+ async function prepareNode(node) {
129
+ const local = [];
130
+ const prepared = { violations: local, semantic: [], judged: () => { } };
131
+ const applicable = await applicableRules(node, (rule, error) => local.push(expressionErrorViolation(rule, node.label, error)));
132
+ function report(rule, detail = {}) {
133
+ const severity = detail.severity ?? ruleSeverities(rule)[0] ?? ruleSeverity(rule);
134
+ local.push({
135
+ file: node.label,
136
+ ruleId: rule.id,
137
+ category: rule.category,
138
+ severity,
139
+ description: detail.error
140
+ ? `${rule.description} (Semantic evaluation error: ${detail.error})`
141
+ : rule.description,
142
+ ...(detail.evidence !== undefined && { evidence: detail.evidence }),
143
+ ...(detail.verdict !== undefined && { verdict: detail.verdict }),
144
+ ...(detail.guidance !== undefined && { guidance: detail.guidance }),
145
+ });
146
+ // An evaluation error says nothing about the node, so it never gates.
147
+ if (!detail.error && ruleGatesDescendants(rule, severity) && !prepared.blockingRuleId) {
148
+ prepared.blockingRuleId = rule.id;
149
+ }
150
+ }
151
+ // Cheap expressions first; a semantic part is only evaluated when its own expression passes.
152
+ for (const rule of applicable) {
153
+ if (rule.expression !== undefined) {
154
+ const result = await evaluateRuleExpression(rule, node, nodes, nodeIndex);
155
+ if (!result.passed) {
156
+ local.push(result.violation);
157
+ if (result.failed && ruleGatesDescendants(rule, result.violation.severity) && !prepared.blockingRuleId) {
158
+ prepared.blockingRuleId = rule.id;
159
+ }
160
+ continue;
161
+ }
162
+ }
163
+ if (rule.semantic !== undefined && judge)
164
+ prepared.semantic.push(rule);
165
+ }
166
+ prepared.judged = (judgements) => {
167
+ for (const j of judgements) {
168
+ if (j.failed) {
169
+ report(j.rule, {
170
+ severity: j.severity,
171
+ verdict: j.verdict,
172
+ guidance: j.guidance,
173
+ evidence: j.evidence,
174
+ error: j.error,
175
+ });
176
+ }
177
+ }
178
+ };
179
+ return prepared;
180
+ }
181
+ /**
182
+ * Evaluate one set of nodes together, so their semantic rules go to the judge in one pass and a judge
183
+ * with `evaluateNodes` can share model calls. Gating for the set is already decided; violations are
184
+ * recorded in node order.
185
+ */
186
+ async function evaluateNodeSet(nodes) {
187
+ const prepared = [];
188
+ for (const node of nodes)
189
+ prepared.push(await prepareNode(node));
190
+ if (judge) {
191
+ const pending = nodes.map((node, i) => ({ node, rules: prepared[i].semantic }));
192
+ const judgements = await judgeNodes(judge, pending, graph, options.trimReport);
193
+ prepared.forEach((p, i) => {
194
+ p.judged(judgements[i] ?? []);
195
+ });
196
+ }
197
+ for (const p of prepared)
198
+ violations.push(...p.violations);
199
+ return prepared.map((p) => ({ blockingRuleId: p.blockingRuleId }));
200
+ }
201
+ // Non-hierarchy nodes sit outside the subtree cascade — always evaluated, never gated.
202
+ await evaluateNodeSet(graph.nonHierarchy);
203
+ const visited = new Set();
204
+ // Hierarchy nodes are judged one depth at a time: a node's verdicts decide whether its children are evaluated.
205
+ let depthNodes = [];
206
+ for (const node of [...graph.hierarchyRoots, ...graph.orphans]) {
207
+ depthNodes.push({ node, ancestorOrigin: undefined });
208
+ }
209
+ while (depthNodes.length > 0) {
210
+ const current = [];
211
+ for (const entry of depthNodes) {
212
+ if (visited.has(entry.node.title))
213
+ continue;
214
+ visited.add(entry.node.title);
215
+ current.push(entry);
216
+ }
217
+ const isGated = (e) => e.ancestorOrigin !== undefined || seedByTitle.has(e.node.title);
218
+ const toEvaluate = current.filter((e) => !isGated(e) || includeGated);
219
+ const evaluated = await evaluateNodeSet(toEvaluate.map((e) => e.node));
220
+ const blockingByTitle = new Map(toEvaluate.map((e, i) => [e.node.title, evaluated[i].blockingRuleId]));
221
+ const next = [];
222
+ for (const { node, ancestorOrigin } of current) {
223
+ const selfSeed = seedByTitle.get(node.title);
224
+ const isGatedByAncestor = ancestorOrigin !== undefined;
225
+ const isSelfBlocked = selfSeed !== undefined;
226
+ const blockingRuleId = blockingByTitle.get(node.title);
227
+ let childOrigin = ancestorOrigin;
228
+ if (isGatedByAncestor) {
229
+ gateOrigins.get(ancestorOrigin).gatedFiles.push(node.label);
230
+ }
231
+ else if (isSelfBlocked) {
232
+ const origin = originFor(node.title, { file: selfSeed.file, reason: selfSeed.reason });
233
+ origin.gatedFiles.push(node.label);
234
+ childOrigin = node.title;
235
+ }
236
+ else if (blockingRuleId) {
237
+ originFor(node.title, { file: node.label, reason: 'rule', ruleId: blockingRuleId });
238
+ childOrigin = node.title;
239
+ }
240
+ else {
241
+ childOrigin = undefined;
242
+ }
243
+ for (const child of graph.hierarchyChildren.get(node.title) ?? []) {
244
+ next.push({ node: child, ancestorOrigin: childOrigin });
245
+ }
246
+ }
247
+ depthNodes = next;
248
+ }
249
+ return {
250
+ violations,
251
+ gates: [...gateOrigins.values()].filter((g) => g.gatedFiles.length > 0),
252
+ };
253
+ }
@@ -0,0 +1,13 @@
1
+ import type { AnySchemaObject } from 'ajv';
2
+ /** Version folders under `generatedDir` (named after the release that first shipped the dialect), oldest first. */
3
+ export declare function listMetaVersions(generatedDir: string): string[];
4
+ /** The newest released meta-schema and its version folder, if any. */
5
+ export declare function readNewestMetaSchema(generatedDir: string): {
6
+ version: string;
7
+ schema: AnySchemaObject;
8
+ } | undefined;
9
+ /**
10
+ * Meta-schemas shipped in `<schemasDir>/generated/`, keyed by `$id`: every versioned folder at its own `$id`, and
11
+ * `latest` registered under the `latest` URL so `$schema` references to either form resolve.
12
+ */
13
+ export declare function loadMetaSchemas(schemasDir: string): Map<string, AnySchemaObject>;
@@ -0,0 +1,50 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { SCHEMA_META_FILE, SCHEMA_META_ID } from './metadata-contract';
4
+ const LATEST_FOLDER = 'latest';
5
+ const VERSION_FOLDER = /^\d+\.\d+\.\d+$/;
6
+ function compareVersions(a, b) {
7
+ const [pa, pb] = [a, b].map((v) => v.split('.').map(Number));
8
+ for (let i = 0; i < 3; i++) {
9
+ const diff = (pa?.[i] ?? 0) - (pb?.[i] ?? 0);
10
+ if (diff !== 0)
11
+ return diff;
12
+ }
13
+ return 0;
14
+ }
15
+ /** Version folders under `generatedDir` (named after the release that first shipped the dialect), oldest first. */
16
+ export function listMetaVersions(generatedDir) {
17
+ if (!existsSync(generatedDir))
18
+ return [];
19
+ return readdirSync(generatedDir)
20
+ .filter((name) => VERSION_FOLDER.test(name))
21
+ .sort(compareVersions);
22
+ }
23
+ function readMetaSchema(generatedDir, folder) {
24
+ const path = join(generatedDir, folder, SCHEMA_META_FILE);
25
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf-8')) : undefined;
26
+ }
27
+ /** The newest released meta-schema and its version folder, if any. */
28
+ export function readNewestMetaSchema(generatedDir) {
29
+ const version = listMetaVersions(generatedDir).at(-1);
30
+ const schema = version && readMetaSchema(generatedDir, version);
31
+ return version && schema ? { version, schema } : undefined;
32
+ }
33
+ /**
34
+ * Meta-schemas shipped in `<schemasDir>/generated/`, keyed by `$id`: every versioned folder at its own `$id`, and
35
+ * `latest` registered under the `latest` URL so `$schema` references to either form resolve.
36
+ */
37
+ export function loadMetaSchemas(schemasDir) {
38
+ const generatedDir = join(schemasDir, 'generated');
39
+ const schemas = new Map();
40
+ for (const version of listMetaVersions(generatedDir)) {
41
+ const schema = readMetaSchema(generatedDir, version);
42
+ if (schema && typeof schema.$id === 'string')
43
+ schemas.set(schema.$id, schema);
44
+ }
45
+ // The `latest` copy carries its versioned `$id`, which a version folder may already own; register it under its own URL.
46
+ const latest = readMetaSchema(generatedDir, LATEST_FOLDER);
47
+ if (latest)
48
+ schemas.set(SCHEMA_META_ID, { ...latest, $id: SCHEMA_META_ID });
49
+ return schemas;
50
+ }