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
package/docs/config.md CHANGED
@@ -1,4 +1,8 @@
1
- # structured-context Configuration Reference
1
+ ---
2
+ title: "Configuration Reference"
3
+ ---
4
+
5
+ # Configuration Reference
2
6
 
3
7
  ## Config file location
4
8
 
@@ -61,6 +65,71 @@ All plugin names must start with `sctx-` (the prefix is optional in config and n
61
65
 
62
66
  Plugins can also contribute schemas and partials via a `<pluginRoot>/schemas/` directory (or `"sctx": { "schemas": "..." }` in `package.json`). If a space specifies a plugin-prefixed schema (e.g. `"sctx-wardley-mapping/wardley_map.json"`), schemas are resolved statically without executing plugin code, and the plugin's code hooks are loaded when parsing or rendering.
63
67
 
68
+ ## Model providers and semantic rules
69
+
70
+ structured-context evaluates [semantic rules](semantic-rules.md) with a **model provider**: a named definition that says which wire API to speak, which model to use and where. A provider definition combines an `api`, explicit values or environment-variable names, and the key source.
71
+
72
+ **Bundled providers** work without any configuration: a fixed model per vendor chosen by this project, plus `-env` variants that also read the vendor's usual model and endpoint variables (`OPENAI_MODEL`, `OPENAI_BASE_URL`, `ANTHROPIC_MODEL`), falling back to the bundle's defaults where a variable is unset. Bundles without `-env` read no environment beyond the key: they behave the same on every machine.
73
+
74
+ Run `sctx providers` for the authoritative list of bundled and configured providers with their effective API, endpoint, model and key variable. Keys are never shown. `bun run test:models` checks that each bundled model resolves on its endpoint.
75
+
76
+ ### Wire APIs and bundled-provider conventions
77
+
78
+ A provider's `api` names the **request shape**, potentially supported by multiple vendors.
79
+
80
+ | API | Shape | Bundled providers |
81
+ |---|---|---|
82
+ | `openrouter-decisions` | Decisions API, closed-set `choice` questions, answers with confidence and probabilities | `openrouter-jev`, `openrouter-openai-decisions` |
83
+ | `typesafe-systemone` | Same decisions shape (currently) | `typesafe-jev` |
84
+ | `openai-decisions` | OpenAI's Decisions API: same concept, its own request/response shape | `openai-decisions` |
85
+ | `anthropic-messages` | Chat: structured JSON verdict | `anthropic`, `anthropic-haiku-5-5`, `anthropic-env` |
86
+ | `openai-completions` | Chat: OpenAI-compatible; works with Ollama, vLLM and OpenRouter | `openai`, `openai-env`, `openrouter-openai` |
87
+
88
+ Bundled provider names follow the pattern:
89
+
90
+ - `<vendor>` — this project's recommended model for that vendor; can change between releases.
91
+ - `<vendor>-<model>` (e.g. `anthropic-haiku-5-5`) — pins a model id, so it does not drift when the `<vendor>` bundle's recommendation moves.
92
+ - `<vendor>-env` — hydrates model, endpoint etc from the vendor's usual environment variables.
93
+ - `openrouter-<vendor>-<purpose>` (e.g. `openrouter-openai-decisions`) — aggregating vendor's endpoint variant
94
+
95
+ **Configured providers** live in `modelProviders` at the top level, and replace a bundled provider of the same name. Fields a configured provider leaves out still take the API defaults:
96
+
97
+ ```json
98
+ {
99
+ "modelProviders": {
100
+ "mine": { "api": "openrouter-decisions", "apiKeyEnv": "MY_JEV_KEY" },
101
+ "local": { "api": "openai-completions", "endpoint": "http://localhost:11434/v1" }
102
+ }
103
+ }
104
+ ```
105
+
106
+ | Field | Description |
107
+ |---|---|
108
+ | `api` | **Required.** The wire API (see the table above and [Choosing a model provider](semantic-rules.md#choosing-a-model-provider)), or `fake` (for tests). |
109
+ | `model` | A fixed model. Its default depends on the API. |
110
+ | `modelEnv` | Environment variable holding a model override. An explicit `model` wins; a variable set but empty is an error. |
111
+ | `endpoint` | A fixed API URL. |
112
+ | `endpointEnv` | Environment variable holding an endpoint override. Same precedence as `modelEnv`. |
113
+ | `apiKeyEnv` | The environment variable that holds the key. Defaults to the API's usual variable (see the table above). A local `openai-completions` endpoint can need no key. |
114
+ | `apiKey` | The key itself. Use `apiKeyEnv` instead, so that keys stay out of config files. |
115
+
116
+ Resolution per field: the explicit value wins, then the named environment variable, then the API default. Provider implementations read no environment; all hydration happens before one runs.
117
+
118
+ The environment holds only keys and the variables a `-env` bundle names. No environment variable selects a provider.
119
+
120
+ The top-level `semantic.provider` names the model provider that evaluates semantic rules: a bundled provider or an entry in `modelProviders`. A space can set its own `semantic`, which replaces the top-level one for that space, for example to keep a private space on a local model:
121
+
122
+ ```json
123
+ {
124
+ "semantic": { "provider": "openrouter-jev" },
125
+ "spaces": [
126
+ { "name": "private-notes", "path": "/path/to/notes", "semantic": { "provider": "local" } }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ Config does not turn semantic evaluation on. Each run asks for it with `--semantic`. `--semantic <provider>` also selects a different provider for that run.
132
+
64
133
  ## Markdown plugin config
65
134
 
66
135
  Set under `plugins.markdown` per space.
package/docs/rules.md CHANGED
@@ -1,6 +1,15 @@
1
+ ---
2
+ title: "Executable Rules"
3
+ ---
4
+
1
5
  # Executable Rules
2
6
 
3
- Rules are JSONata expressions in schema metadata (`$metadata.rules`). They run after structural JSON Schema validation and let you enforce cross-node checks and workflow constraints.
7
+ Rules check content after JSON Schema validation, for example cross-node checks and workflow constraints. A schema declares rules in `$metadata.rules`. A rule has an `expression`, a `semantic` part, or both:
8
+
9
+ - An **`expression`** is a JSONata test that must return `true`.
10
+ - A **`semantic`** part asks a model to evaluate a node. See [Semantic rules](semantic-rules.md).
11
+
12
+ A rule with neither is a schema error.
4
13
 
5
14
  For metadata structure and composition behavior, see [docs/schemas.md](schemas.md).
6
15
 
@@ -14,26 +23,13 @@ Rules are a flat array.
14
23
  "id": "active-outcome-count",
15
24
  "category": "workflow",
16
25
  "description": "Only one outcome should be active at a time",
17
- "scope": "global",
18
- "check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
26
+ "global": true,
27
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
19
28
  }
20
29
  ]
21
30
  ```
22
31
 
23
- | Field | Required | Description |
24
- |---|---|---|
25
- | `id` | yes | Unique rule identifier |
26
- | `category` | yes | `validation` \| `coherence` \| `workflow` \| `best-practice` |
27
- | `severity` | no | `error` \| `warning` \| `info`; overrides the category default (see [Severity](#severity)) |
28
- | `description` | yes | Human-readable rule intent |
29
- | `check` | yes | JSONata expression; must evaluate to `true` |
30
- | `type` | no | Restrict rule to nodes of this `resolvedType` |
31
- | `scope` | no | Use `"global"` to evaluate once for the whole space |
32
- | `override` | no | Only for merge conflicts: allows later duplicate `id` to replace earlier |
33
-
34
- ## Categories and severity
35
-
36
- Categories describe a rule's intent and supply its default severity. They do not change expression execution semantics.
32
+ Every rule has a category, which describes its intent and supplies a default severity.
37
33
 
38
34
  | Category | Typical use | Default severity |
39
35
  |---|---|---|
@@ -42,9 +38,7 @@ Categories describe a rule's intent and supply its default severity. They do not
42
38
  | `workflow` | Process/operating-discipline checks | `warning` |
43
39
  | `best-practice` | Advisory quality checks | `info` |
44
40
 
45
- ### Severity
46
-
47
- Every rule violation is reported with a severity:
41
+ Severity can also be set per rule. It applies to a failed `expression` and to the default `fail` verdict of a [semantic rule](semantic-rules.md#severity).
48
42
 
49
43
  | Severity | Meaning | Fails validation |
50
44
  |---|---|---|
@@ -63,25 +57,42 @@ Prefer choosing the right category and relying on its default. Set `severity` on
63
57
  "severity": "error",
64
58
  "description": "Goals must not be left blocked",
65
59
  "type": "goal",
66
- "check": "current.status != 'blocked'"
60
+ "expression": "current.status != 'blocked'"
67
61
  }
68
62
  ```
69
63
 
70
64
  `sctx schemas show` lists each category with its default severity and marks rules that override it.
71
65
 
72
- ### Output
66
+ ## Selecting nodes
73
67
 
74
- - **`sctx validate`** — the summary counts rule violations per severity, and details are grouped by severity with each line showing the rule's category and id.
75
- - **`sctx validate --json`** — `errors` includes only `error` severity rule violations (keyed `rule:<id>`, with a `severity` field). A `ruleViolations` array lists every violation with `file`, `ruleId`, `category`, `severity` and `description` (`file` is empty for global rules).
76
- - **`sctx validate-file`** — `error` severity violations appear under `errors`; `warning` and `info` violations appear under `warnings`. Rule entries carry a `severity` field.
68
+ - A rule with `global: true` runs once for the space. It needs an `expression`, and it cannot have `when`, `type` or `semantic`.
69
+ - Other rules run once for each node.
70
+ - `type` limits a rule to nodes with that `resolvedType`.
71
+ - `when` limits a rule to nodes where the applicability condition returns `true`. See [Conditional rules](#conditional-rules-when).
72
+
73
+ ### Conditional rules (`when`)
74
+
75
+ `when` is the rule's applicability condition: a JSONata test that determines whether a rule applies to a node. It has the same context as `expression` and runs before it. Use `when` when `type` cannot select the nodes a rule applies to:
76
+
77
+ ```json
78
+ { "id": "ready-has-owner", "category": "workflow", "type": "task",
79
+ "description": "Ready tasks have an owner",
80
+ "when": "current.status = 'ready'", "expression": "$exists(current.owner)" }
81
+ ```
82
+
83
+ `when` costs almost nothing, so use it to stop an expensive rule from running where it does not apply. For a semantic rule, this saves a model call on each node that `when` excludes.
84
+
85
+ ### Order within a rule
86
+
87
+ A rule runs its parts in this order: `when`, then `expression`, then the `semantic` part. If the `expression` fails, the `semantic` part does not run on that node.
77
88
 
78
- ## Evaluation model
89
+ An error in a `when` or `expression` is reported as a non-gating `error` violation.
79
90
 
80
- - Rules with `scope: "global"` run once.
81
- - Other rules run per applicable node.
82
- - If `type` is set, only nodes with matching `resolvedType` are evaluated.
91
+ ## Writing expressions
83
92
 
84
- ## Expression context
93
+ An `expression` (and `when`) is JSONata. This section covers what an expression can see and how to write expressions.
94
+
95
+ ### Expression context
85
96
 
86
97
  Each evaluation receives:
87
98
 
@@ -99,7 +110,7 @@ Useful resolved fields on nodes:
99
110
 
100
111
  Prefer `resolvedType` over raw `type` so aliases are handled correctly.
101
112
 
102
- ## Predicate scoping (`$$`)
113
+ ### Predicate scoping (`$$`)
103
114
 
104
115
  Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to reference outer variables:
105
116
 
@@ -107,9 +118,33 @@ Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to refere
107
118
  $count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution'])
108
119
  ```
109
120
 
110
- ## Reusing rules
121
+ ### Common patterns
122
+
123
+ ```jsonata
124
+ $exists(current.metric) = true
125
+ $count(current.sources) >= 1
126
+ $count(nodes[resolvedType='outcome' and status='active']) <= 1
127
+ current.status != 'active' or $exists(parent) = false or parent.status = 'active'
128
+ ```
129
+
130
+ #### Dates and times
131
+
132
+ Dates and datetimes reach rules as ISO 8601 strings. Convert with `$toMillis()` to compare them, including date against datetime, and use `$millis()` for the current time:
133
+
134
+ ```jsonata
135
+ $not($exists(current.due)) or $toMillis(current.due) > $millis()
136
+ $toMillis(current.stale_after) > $toMillis(current.generated.at)
137
+ ```
138
+
139
+ A datetime without an offset (`2026-07-01T00:00:00`) is read in the machine's local time zone, so require an offset with `format: "date-time"` when results must not depend on where validation runs. `$toMillis()` also needs seconds: it cannot parse a minute-precision datetime such as `2026-07-01T09:15`, which `format: "date-or-date-time"` accepts. A value `$toMillis()` cannot parse makes the rule fail.
140
+
141
+ The same applies to [filter expressions](concepts.md#filter-expressions): compare dates with `$toMillis()`, not as strings.
142
+
143
+ ## Sharing rules
111
144
 
112
- To share rules, put them in a schema's `$metadata.rules` and list that schema's `$id` in `$metadata.imports`. An import brings the whole schema's metadata, including its own imports:
145
+ To share rules, put them in a schema's `$metadata.rules` and list that schema's `$id` in `$metadata.imports`. An import brings the whole schema's metadata, including its own imports. Merged rules combine by `id`; a duplicate `id` is an error, and a later rule replaces an earlier one only with `override: true`.
146
+
147
+ ### Overriding rules
113
148
 
114
149
  ```json5
115
150
  "$metadata": {
@@ -117,13 +152,6 @@ To share rules, put them in a schema's `$metadata.rules` and list that schema's
117
152
  }
118
153
  ```
119
154
 
120
- ## Merge and conflict behavior
121
-
122
- When metadata is merged from imports (imports in order, then the schema's own metadata):
123
- - Rules are merged by `id`.
124
- - A duplicate `id` is an error.
125
- - A later rule may replace an earlier one only with `override: true`.
126
-
127
155
  Example override:
128
156
 
129
157
  ```json5
@@ -132,16 +160,79 @@ Example override:
132
160
  "override": true,
133
161
  "category": "workflow",
134
162
  "description": "Require exactly one active outcome",
135
- "scope": "global",
136
- "check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
163
+ "global": true,
164
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
137
165
  }
138
166
  ```
139
167
 
140
- ## Common patterns
168
+ ## Cascade gating
169
+
170
+ When a schema uses `$metadata.hierarchy`, rule evaluation cascades top-down through the
171
+ hierarchy tree (breadth-first from roots and orphans). A **gate** occurs when a node fails
172
+ in a way that makes its descendants' rules untrustworthy or wasteful to run:
173
+
174
+ - **Schema validation failure** on a node — always gates. The node's own rules are
175
+ suppressed too, since its data is structurally invalid.
176
+ - **Hierarchy structure violation** on a node (e.g. an invalid parent) — always gates the
177
+ node's subtree, though the node's own rules still run.
178
+ - **A rule failure with `gatesDescendants: true`** — gates the node's subtree, though the
179
+ failing node's own other rules still run.
180
+
181
+ Gating exists so that expensive or semantic downstream rules (see [Semantic rules](semantic-rules.md)) aren't wasted
182
+ evaluating nodes whose ancestors are already known-bad, and so noise doesn't drown out the
183
+ actionable failure. It is per-subtree: once a node is gated, its descendants inherit the same
184
+ gate (the original blocker), rather than each generating a new one. Cascade gating only applies to nodes within the
185
+ hierarchy DAG.
186
+
187
+ A rule has one `gatesDescendants` setting. It applies to whichever part failed, the `expression` or the `semantic` part. Its default comes from the severity of that failure: `true` for `error`, and `false` for `warning` and `info`. Set it explicitly when that default doesn't fit.
188
+
189
+ ### Coverage output
190
+
191
+ `sctx validate` reports gating as **coverage gates**: which node's failure (schema,
192
+ hierarchy, or a specific `ruleId`) suppressed rule evaluation, and the list of files gated
193
+ as a result. This makes the validation "edge" visible — e.g. "this blocked goal means all
194
+ opportunities beneath it were not evaluated" — for both human reading and agent consumption
195
+ (JSON output via `--json`, under `coverageGates`).
196
+
197
+ Use `--include-gated` to evaluate rules on gated nodes anyway (still reported as gated) —
198
+ useful for surfacing cheap, bottom-up checks (e.g. orphan detection, content quality) that
199
+ don't depend on an ancestor being valid.
200
+
201
+ ### Single-file validation
202
+
203
+ `sctx validate-file <path>` applies the same gating as `sctx validate`, so a file's rule results match its entry in a full run.
204
+
205
+ - Expressions are evaluated for the whole space; the output is restricted to the file.
206
+ - Semantic parts are evaluated only for the file and its gating ancestors (see [Semantic rules](semantic-rules.md#how-semantic-evaluation-works)).
207
+ - If the file is gated, its rules are skipped and a `gated:<file>` entry under `warnings` names the blocker, so a gated file doesn't read as clean. `--include-gated` evaluates the file's rules anyway.
208
+
209
+ ## Controlling validation runs
210
+
211
+ These options restrict which rules run, without changing rule definitions.
212
+
213
+ ### Filtering rules
214
+
215
+ `--rule-filter <dimension>=<value>[,<value>]` runs only the rules that match. It works on `sctx validate` and `sctx validate-file`.
216
+
217
+ - Dimensions: `category`, `severity`, `type` and `id`.
218
+ - Values in one dimension combine with OR. Repeat the option to add a dimension; dimensions combine with AND.
219
+ - A rule matches each severity that it can report, including the severities in a semantic verdict map.
220
+ - Gating comes only from the rules that ran.
221
+
222
+ For example, `--rule-filter category=workflow,coherence --rule-filter severity=error` runs only workflow and coherence rules that can report an error.
223
+
224
+ ### Depth-limited evaluation
225
+
226
+ `--depth <n>` restricts validation to hierarchy nodes within `n` breadth-first levels from
227
+ the top (roots and orphans are depth 1). This is a manual, coarser control than cascade
228
+ gating — intended for staged workflows (e.g. an agent fixes the top level, then requests
229
+ the next). Excluded nodes are reported under `skippedByDepth`, separate from gated nodes.
230
+
231
+ ## Output
232
+
233
+ - **`sctx validate`** — the summary counts rule violations per severity, and details are grouped by severity with each line showing the rule's category and id.
234
+ - **`sctx validate --json`** — `errors` includes only `error` severity rule violations (keyed `rule:<id>`, with a `severity` field). A `ruleViolations` array lists every violation with `file`, `ruleId`, `category`, `severity` and `description` (`file` is empty for global rules). A violation from a failed `expression` has the evidence `expression failed: <expression>` when the rule also has a `semantic` part, and no verdict.
235
+ - **`sctx validate-file`** — `error` severity violations appear under `errors`; `warning` and `info` violations appear under `warnings`. Rule entries carry a `severity` field.
236
+
237
+ Gated files: see [Single-file validation](#single-file-validation).
141
238
 
142
- ```jsonata
143
- $exists(current.metric) = true
144
- $count(current.sources) >= 1
145
- $count(nodes[resolvedType='outcome' and status='active']) <= 1
146
- current.status != 'active' or $exists(parent) = false or parent.status = 'active'
147
- ```
package/docs/schemas.md CHANGED
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Schemas"
3
+ ---
4
+
1
5
  # Schemas
2
6
 
3
7
  This document explains schema usage, metadata shape, and composition semantics in `structured-context`.
@@ -53,13 +57,31 @@ Main types:
53
57
 
54
58
  This schema reuses shared structural defs from partials via `$ref` and imports its hierarchy and rules from `_ost_strict` via `$metadata.imports`.
55
59
 
60
+ ### `knowledge_wiki.json`
61
+
62
+ Flat, LLM-maintained knowledge base (after Karpathy's LLM Wiki pattern). No hierarchy or required parent links; wikilinks between any pages are valid. Validates structure and provenance, not meaning.
63
+
64
+ Main types:
65
+ - `source`
66
+ - `concept`
67
+ - `entity`
68
+ - `synthesis`
69
+ - `note`
70
+ - `index`
71
+
72
+ ### `okf.json`
73
+
74
+ [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) (OKF v0.2) bundles, validated as plain markdown spaces. Any concept `type` is accepted and unknown fields are tolerated; provenance (`sources`), trust (`generated`, `verified`), lifecycle (`status`, `stale_after`) and the Attested Computation contract are checked when present. An Attested Computation needs its computation: a `computation` field, or a `# Computation` section in the body (found by matching that line in the body text, so a `# Computation` line inside a code fence also counts). Reserved `index.md` and `log.md` are reported as excluded during parsing.
75
+
56
76
  ## Custom format annotations
57
77
 
58
78
  `structured-context` registers the following `format` annotations beyond standard JSON Schema. All apply to `string` properties and are validated at schema validation time.
59
79
 
60
80
  | Format | Validates | Example |
61
81
  |--------|-----------|---------|
62
- | `date` | ISO 8601 date (`YYYY-MM-DD`) | `"2026-03-31"` |
82
+ | `date` | ISO 8601 calendar date (`YYYY-MM-DD`) | `"2026-03-31"` |
83
+ | `date-time` | ISO 8601 datetime with seconds and a UTC offset (`Z` or `±HH:MM`) | `"2026-03-31T09:15:00+10:00"` |
84
+ | `date-or-date-time` | A date, or a datetime where seconds and offset are optional (as Obsidian's datetime property writes) | `"2026-03-31"`, `"2026-03-31T09:15"`, `"2026-03-31T09:15:00Z"` |
63
85
  | `path` | Non-empty filesystem path — absolute, relative, or a plain name | `"notes"`, `"./subdir/file.md"`, `"/abs/path"` |
64
86
  | `wikilink` | Obsidian wikilink syntax (`[[...]]`) | `"[[Parent Node]]"` |
65
87
 
@@ -75,15 +97,16 @@ Using `format` directly is more concise when the full definition isn't needed:
75
97
  { "type": "string", "format": "wikilink" }
76
98
  ```
77
99
 
78
- ### Date coercion
100
+ ### Dates and datetimes
79
101
 
80
- YAML parsers (gray-matter, js-yaml) coerce unquoted ISO dates to JavaScript `Date` objects:
102
+ The markdown plugin reads frontmatter and embedded YAML blocks without YAML's timestamp type, so unquoted dates and datetimes stay as the strings written, at any depth (including inside lists):
81
103
 
82
104
  ```yaml
83
- published_date: 2026-03-31 # parsed as a Date object by gray-matter
105
+ published_date: 2026-03-31 # "2026-03-31"
106
+ updated: 2026-06-20T22:53:05+10:00 # "2026-06-20T22:53:05+10:00", offset kept
84
107
  ```
85
108
 
86
- The markdown plugin automatically coerces `Date` objects to `YYYY-MM-DD` strings before validation, so unquoted dates in frontmatter and embedded YAML blocks work correctly with `format: "date"` fields.
109
+ Validate them with `format: "date"` or `format: "date-time"` when a field needs one precision, or `format: "date-or-date-time"` to accept either, and compare them in rules with JSONata's `$toMillis()` (see [Rules](rules.md#dates-and-times)).
87
110
 
88
111
  ## Metadata dialect
89
112
 
@@ -116,8 +139,8 @@ Top-level metadata shape:
116
139
  "id": "active-outcome-count",
117
140
  "category": "workflow",
118
141
  "description": "Only one outcome should be active at a time",
119
- "scope": "global",
120
- "check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
142
+ "global": true,
143
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
121
144
  }
122
145
  ]
123
146
  }
@@ -136,7 +159,7 @@ Top-level metadata shape:
136
159
  | `hierarchy.allowSkipLevels` | `boolean` | Optional; allows parent to be any ancestor level |
137
160
  | `relationships` | `Relationship[]` | Optional; defines related node links outside the primary hierarchy. A relationship may set `override: true` |
138
161
  | `aliases` | `Record<string, string \| { type, override }>` | Optional type alias map. Use `{ "type": "goal", "override": true }` to replace an imported alias |
139
- | `rules` | `Rule[]` | Optional flat rule array |
162
+ | `rules` | `Rule[]` | Optional flat rule array. A rule has an `expression` (a JSONata test), a `semantic` part (evaluated by a model against a rubric), or both; see [Rules](rules.md) |
140
163
 
141
164
  ### Relationships
142
165
 
@@ -148,9 +171,9 @@ Relationships define links between node types that are not part of the primary s
148
171
  | `type` | `string` | **Required** | The child's canonical type name |
149
172
  | `field` | `string` | `"parent"` | The frontmatter field that holds the wikilink(s) for this relationship |
150
173
  | `fieldOn` | `string` | `"child"` | `"child"` (child holds a link to parent) or `"parent"` (parent holds an array of child links) |
151
- | `format` | `string` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
174
+ | `templateFormat` | `string` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
152
175
  | `matchers` | `string[]` | `[]` | Heading text to match (strings or `/regex/`). Case-insensitive. |
153
- | `embeddedTemplateFields` | `string[]` | `[]` | Field names to include in templates when `format` is `"table"` |
176
+ | `embeddedTemplateFields` | `string[]` | `[]` | Field names to include in templates when `templateFormat` is `"table"` |
154
177
  | `multiple` | `boolean` | `true` | Whether multiple children are expected |
155
178
 
156
179
  **`fieldOn: "child"` (default)** — child node has a field pointing to its parent. Embedded parsing sets this field on each child node; validation checks that it resolves to a node of the declared parent type.
@@ -164,7 +187,7 @@ Relationships define links between node types that are not part of the primary s
164
187
  {
165
188
  "parent": "opportunity",
166
189
  "type": "assumption",
167
- "format": "table",
190
+ "templateFormat": "table",
168
191
  "matchers": ["Assumptions", "/assum.*/"],
169
192
  "embeddedTemplateFields": ["assumption", "status", "confidence"]
170
193
  }
@@ -180,7 +203,7 @@ Relationships define links between node types that are not part of the primary s
180
203
  "type": "task",
181
204
  "field": "tasks",
182
205
  "fieldOn": "parent",
183
- "format": "list",
206
+ "templateFormat": "list",
184
207
  "matchers": ["Tasks"],
185
208
  "multiple": true
186
209
  }
@@ -199,7 +222,7 @@ With this configuration, embedded task items under an Activity's "Tasks" heading
199
222
  | `multiple` | `false` | Field contains array of wikilinks |
200
223
  | `selfRef` | `false` | Allows same-type parent |
201
224
  | `selfRefField` | _undefined_ | Separate field for same-type parent links |
202
- | `format` | _undefined_ | Embedding hint: `"list"`, `"table"`, or `"heading"` — enables hierarchy embedding when set |
225
+ | `templateFormat` | _undefined_ | Embedding hint: `"list"`, `"table"`, or `"heading"` — enables hierarchy embedding when set |
203
226
  | `matchers` | _undefined_ | Heading text patterns (strings or `/regex/`) to detect embedding sections |
204
227
  | `embeddedTemplateFields` | _undefined_ | Column/field names for table stubs generated by `template-sync` |
205
228
 
@@ -208,7 +231,7 @@ String shorthand (`"goal"`) normalizes to:
208
231
 
209
232
  ### Hierarchy embedding
210
233
 
211
- When a hierarchy level has `format` and `matchers`, it participates in **hierarchy embedding** — the same section-based parsing used for relationships. Two patterns are supported:
234
+ When a hierarchy level has `templateFormat` and `matchers`, it participates in **hierarchy embedding** — the same section-based parsing used for relationships. Two patterns are supported:
212
235
 
213
236
  **Child-level embedding** — a typed-page heading signals that following content should produce nodes of the child type (next level in hierarchy):
214
237
 
@@ -219,7 +242,7 @@ When a hierarchy level has `format` and `matchers`, it participates in **hierarc
219
242
  "type": "opportunity",
220
243
  "field": "parent",
221
244
  "fieldOn": "child",
222
- "format": "list",
245
+ "templateFormat": "list",
223
246
  "matchers": ["Opportunities", "User Opportunities"]
224
247
  }
225
248
  ]
@@ -261,6 +284,8 @@ With this config, an application page may include `### capability` with wikilink
261
284
  - [[Zephyr]] ← populates application.tools (fieldOn: parent)
262
285
  ```
263
286
 
287
+ A node's body becomes its `content`; see [Node content](concepts.md#node-content).
288
+
264
289
  ## Composition and merge semantics
265
290
 
266
291
  `$ref` and `$defs` are for **validation only**: a `$ref` to another schema (or to one of its `$defs`) reuses its JSON Schema definitions but brings none of its `$metadata`.
@@ -344,8 +369,8 @@ How hierarchies are used:
344
369
  "override": true,
345
370
  "category": "workflow",
346
371
  "description": "Require exactly one active outcome",
347
- "scope": "global",
348
- "check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
372
+ "global": true,
373
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
349
374
  }
350
375
  ]
351
376
  }
@@ -373,7 +398,7 @@ Every schema `$id` uses the `sctx://` scheme. The shape of the `$id` says where
373
398
  - `$id` values follow the [namespace rules](#schema-id-namespaces) and must not collide across bundled, plugin and local schemas.
374
399
  - `$ref` resolution is transitive across files.
375
400
  - Partials with no `$metadata` should prefer `$schema: "https://json-schema.org/draft/2020-12/schema"` so they validate standalone as plain JSON Schema fragments.
376
- - **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference their entity types via `$ref` rather than redefining them.
401
+ - **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, `_okf.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference their entity types via `$ref` rather than redefining them.
377
402
  - **Partials can carry metadata**: Partials may include `$metadata` (hierarchy, aliases, relationships, rules). A schema gets it only by listing the partial in `$metadata.imports`; `$ref` alone brings none.
378
403
 
379
404
  ## Plugin-contributed schemas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "structured-context",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Keep your content clean and your thinking clear with assistive tools for structured knowledge",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -41,6 +41,8 @@
41
41
  "diagram": "bun run src/index.ts diagram",
42
42
  "test": "bun test tests/",
43
43
  "test:smoke": "bun test smoke/",
44
+ "test:judge": "SCTX_LIVE_JUDGE_TESTS=1 bun test tests/judge/live.test.ts",
45
+ "test:models": "SCTX_MODEL_VALIDATION=1 bun test tests/judge/model-validation.test.ts",
44
46
  "test:hook": "bun test hook-test/unit/",
45
47
  "test:hook:e2e": "bun test hook-test/hooks.test.ts",
46
48
  "test:all": "bun test",
@@ -52,9 +54,8 @@
52
54
  },
53
55
  "devDependencies": {
54
56
  "@anthropic-ai/claude-agent-sdk": "^0.3.191",
55
- "@biomejs/biome": "^2.5.1",
57
+ "@biomejs/biome": "^2.5.15",
56
58
  "@types/bun": "latest",
57
- "@types/js-yaml": "^4.0.9",
58
59
  "json-schema-to-ts": "^3.1.1",
59
60
  "lefthook": "^2.1.9",
60
61
  "tsc-alias": "^1.9.1"
@@ -65,10 +66,10 @@
65
66
  "dependencies": {
66
67
  "ajv": "^8.20.0",
67
68
  "chokidar": "^5.0.0",
68
- "commander": "^14.0.3",
69
+ "commander": "^15.0.0",
69
70
  "fast-glob": "^3.3.3",
70
71
  "gray-matter": "^4.0.3",
71
- "js-yaml": "^4.1.1",
72
+ "js-yaml": "^5.4.3",
72
73
  "json5": "^2.2.3",
73
74
  "jsonata": "^2.2.1",
74
75
  "mdast-util-to-string": "^4.0.0",
@@ -50,14 +50,14 @@
50
50
  "category": "coherence",
51
51
  "description": "Syntheses should reference at least one contributing source in their sources array",
52
52
  "type": "synthesis",
53
- "check": "$exists(current.sources) and $count(current.sources) >= 1"
53
+ "expression": "$exists(current.sources) and $count(current.sources) >= 1"
54
54
  },
55
55
  {
56
56
  "id": "concept-has-summary",
57
57
  "category": "best-practice",
58
58
  "description": "Concept pages should have a short summary for index discoverability",
59
59
  "type": "concept",
60
- "check": "$exists(current.summary) and $length(current.summary) > 0"
60
+ "expression": "$exists(current.summary) and $length(current.summary) > 0"
61
61
  }
62
62
  ]
63
63
  },
@@ -89,7 +89,7 @@
89
89
  },
90
90
  "published_date": {
91
91
  "type": "string",
92
- "format": "date",
92
+ "format": "date-or-date-time",
93
93
  "description": "Original publication date"
94
94
  },
95
95
  "source_type": {
@@ -213,7 +213,7 @@
213
213
  "type": { "const": "note" },
214
214
  "date": {
215
215
  "type": "string",
216
- "format": "date",
216
+ "format": "date-or-date-time",
217
217
  "description": "Date this entry was created or relates to"
218
218
  }
219
219
  },