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.
- package/README.md +29 -216
- package/dist/api.d.ts +17 -17
- package/dist/api.js +12 -12
- package/dist/commands/docs.js +1 -0
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/schemas.js +21 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +17 -2
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +42 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +26 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -18
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/load-space-graph.d.ts +3 -3
- package/dist/load-space-graph.js +4 -4
- package/dist/plugins/loader.js +1 -1
- package/dist/plugins/markdown/parse-embedded.d.ts +2 -0
- package/dist/plugins/markdown/parse-embedded.js +161 -24
- package/dist/plugins/markdown/read-space.js +4 -9
- package/dist/plugins/markdown/render-directory.js +5 -5
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/render/render.d.ts +1 -1
- package/dist/render/render.js +1 -1
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/metadata-contract.d.ts +625 -12
- package/dist/schema/metadata-contract.js +110 -4
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.js +31 -1
- package/dist/schema/validate-rules.d.ts +17 -8
- package/dist/schema/validate-rules.js +91 -48
- package/dist/schemas/_knowledge_wiki.json +4 -4
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +15 -6
- package/dist/schemas/_sctx_base.json +1 -1
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
- package/dist/schemas/okf.json +8 -0
- package/dist/space-context.d.ts +2 -2
- package/dist/space-context.js +3 -3
- package/dist/space-graph.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/types.d.ts +11 -4
- package/dist/validate.d.ts +59 -9
- package/dist/validate.js +225 -120
- package/docs/concepts.md +52 -6
- package/docs/config.md +70 -1
- package/docs/rules.md +140 -49
- package/docs/schemas.md +43 -18
- package/package.json +6 -5
- package/schemas/_knowledge_wiki.json +4 -4
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +15 -6
- package/schemas/_sctx_base.json +1 -1
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
- package/schemas/okf.json +8 -0
package/docs/config.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
"
|
|
18
|
-
"
|
|
26
|
+
"global": true,
|
|
27
|
+
"expression": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
|
|
19
28
|
}
|
|
20
29
|
]
|
|
21
30
|
```
|
|
22
31
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
66
|
+
## Selecting nodes
|
|
73
67
|
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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
|
-
|
|
89
|
+
An error in a `when` or `expression` is reported as a non-gating `error` violation.
|
|
79
90
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
136
|
-
"
|
|
163
|
+
"global": true,
|
|
164
|
+
"expression": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
|
|
137
165
|
}
|
|
138
166
|
```
|
|
139
167
|
|
|
140
|
-
##
|
|
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
|
-
###
|
|
100
|
+
### Dates and datetimes
|
|
79
101
|
|
|
80
|
-
YAML
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
120
|
-
"
|
|
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
|
-
| `
|
|
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 `
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
| `
|
|
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 `
|
|
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
|
-
"
|
|
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
|
-
"
|
|
348
|
-
"
|
|
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.
|
|
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.
|
|
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": "^
|
|
69
|
+
"commander": "^15.0.0",
|
|
69
70
|
"fast-glob": "^3.3.3",
|
|
70
71
|
"gray-matter": "^4.0.3",
|
|
71
|
-
"js-yaml": "^4.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
},
|