structured-context 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +52 -198
  2. package/dist/api.d.ts +17 -15
  3. package/dist/api.js +12 -9
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/docs.js +1 -0
  7. package/dist/commands/dump.d.ts +15 -2
  8. package/dist/commands/dump.js +18 -4
  9. package/dist/commands/providers.d.ts +4 -0
  10. package/dist/commands/providers.js +52 -0
  11. package/dist/commands/render.d.ts +4 -0
  12. package/dist/commands/render.js +64 -9
  13. package/dist/commands/schemas.d.ts +2 -2
  14. package/dist/commands/schemas.js +133 -22
  15. package/dist/commands/show.d.ts +1 -0
  16. package/dist/commands/show.js +7 -1
  17. package/dist/commands/spaces.d.ts +3 -1
  18. package/dist/commands/spaces.js +11 -2
  19. package/dist/commands/validate-file.d.ts +3 -0
  20. package/dist/commands/validate-file.js +24 -6
  21. package/dist/commands/validate.d.ts +4 -0
  22. package/dist/commands/validate.js +79 -23
  23. package/dist/config.d.ts +25 -1
  24. package/dist/config.js +106 -12
  25. package/dist/filter/augment-nodes.d.ts +3 -1
  26. package/dist/filter/augment-nodes.js +12 -7
  27. package/dist/filter/filter-nodes.js +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +96 -31
  30. package/dist/judge/context.d.ts +20 -0
  31. package/dist/judge/context.js +105 -0
  32. package/dist/judge/factory.d.ts +44 -0
  33. package/dist/judge/factory.js +148 -0
  34. package/dist/judge/index.d.ts +13 -0
  35. package/dist/judge/index.js +8 -0
  36. package/dist/judge/judge-node.d.ts +27 -0
  37. package/dist/judge/judge-node.js +122 -0
  38. package/dist/judge/prompt.d.ts +10 -0
  39. package/dist/judge/prompt.js +92 -0
  40. package/dist/judge/provider/anthropic.d.ts +17 -0
  41. package/dist/judge/provider/anthropic.js +43 -0
  42. package/dist/judge/provider/fake.d.ts +26 -0
  43. package/dist/judge/provider/fake.js +56 -0
  44. package/dist/judge/provider/jev.d.ts +37 -0
  45. package/dist/judge/provider/jev.js +169 -0
  46. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  47. package/dist/judge/provider/openai-decisions.js +84 -0
  48. package/dist/judge/provider/openai.d.ts +20 -0
  49. package/dist/judge/provider/openai.js +53 -0
  50. package/dist/judge/provider/per-rule.d.ts +10 -0
  51. package/dist/judge/provider/per-rule.js +18 -0
  52. package/dist/judge/trim.d.ts +27 -0
  53. package/dist/judge/trim.js +62 -0
  54. package/dist/judge/types.d.ts +92 -0
  55. package/dist/judge/types.js +9 -0
  56. package/dist/judge/verdicts.d.ts +24 -0
  57. package/dist/judge/verdicts.js +82 -0
  58. package/dist/load-space-graph.d.ts +7 -5
  59. package/dist/load-space-graph.js +19 -8
  60. package/dist/plugins/loader.js +3 -3
  61. package/dist/plugins/markdown/extract-content-links.js +8 -29
  62. package/dist/plugins/markdown/index.js +8 -2
  63. package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
  64. package/dist/plugins/markdown/parse-embedded.js +195 -24
  65. package/dist/plugins/markdown/read-space.js +42 -14
  66. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  67. package/dist/plugins/markdown/render-directory.js +130 -0
  68. package/dist/plugins/markdown/template-sync.js +3 -4
  69. package/dist/plugins/markdown/util.d.ts +26 -5
  70. package/dist/plugins/markdown/util.js +56 -15
  71. package/dist/plugins/mermaid/render-mermaid.js +2 -1
  72. package/dist/plugins/miro/sync.js +7 -6
  73. package/dist/plugins/util.d.ts +39 -1
  74. package/dist/plugins/util.js +139 -0
  75. package/dist/read/resolve-graph-edges.d.ts +1 -1
  76. package/dist/read/resolve-graph-edges.js +46 -27
  77. package/dist/read/wikilink-utils.d.ts +17 -0
  78. package/dist/read/wikilink-utils.js +40 -0
  79. package/dist/render/render.d.ts +3 -1
  80. package/dist/render/render.js +2 -2
  81. package/dist/schema/evaluate-rule.d.ts +33 -2
  82. package/dist/schema/evaluate-rule.js +58 -4
  83. package/dist/schema/gating.d.ts +69 -0
  84. package/dist/schema/gating.js +253 -0
  85. package/dist/schema/meta-schemas.d.ts +13 -0
  86. package/dist/schema/meta-schemas.js +50 -0
  87. package/dist/schema/metadata-contract.d.ts +952 -183
  88. package/dist/schema/metadata-contract.js +177 -33
  89. package/dist/schema/rule-filter.d.ts +12 -0
  90. package/dist/schema/rule-filter.js +46 -0
  91. package/dist/schema/rule-severity.d.ts +13 -0
  92. package/dist/schema/rule-severity.js +33 -0
  93. package/dist/schema/schema.d.ts +14 -8
  94. package/dist/schema/schema.js +308 -199
  95. package/dist/schema/validate-graph.d.ts +2 -1
  96. package/dist/schema/validate-graph.js +40 -23
  97. package/dist/schema/validate-rules.d.ts +16 -3
  98. package/dist/schema/validate-rules.js +91 -29
  99. package/dist/schemas/_knowledge_wiki.json +24 -24
  100. package/dist/schemas/_okf.json +172 -0
  101. package/dist/schemas/_ost_strict.json +17 -8
  102. package/dist/schemas/_sctx_base.json +3 -3
  103. package/dist/schemas/_strategy_general.json +38 -38
  104. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  105. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  106. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  107. package/dist/schemas/knowledge_wiki.json +10 -9
  108. package/dist/schemas/okf.json +8 -0
  109. package/dist/schemas/strategy_general.json +15 -14
  110. package/dist/schemas/strict_ost.json +17 -16
  111. package/dist/space-context.d.ts +5 -2
  112. package/dist/space-context.js +9 -4
  113. package/dist/space-graph.d.ts +9 -7
  114. package/dist/space-graph.js +7 -5
  115. package/dist/testing.d.ts +33 -0
  116. package/dist/testing.js +72 -0
  117. package/dist/types.d.ts +64 -16
  118. package/dist/validate.d.ts +62 -9
  119. package/dist/validate.js +229 -114
  120. package/docs/concepts.md +65 -9
  121. package/docs/config.md +78 -1
  122. package/docs/rules.md +168 -50
  123. package/docs/schemas.md +152 -52
  124. package/package.json +17 -11
  125. package/schemas/_knowledge_wiki.json +24 -24
  126. package/schemas/_okf.json +172 -0
  127. package/schemas/_ost_strict.json +17 -8
  128. package/schemas/_sctx_base.json +3 -3
  129. package/schemas/_strategy_general.json +38 -38
  130. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  131. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  132. package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  133. package/schemas/knowledge_wiki.json +10 -9
  134. package/schemas/okf.json +8 -0
  135. package/schemas/strategy_general.json +15 -14
  136. package/schemas/strict_ost.json +17 -16
  137. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  138. package/schemas/generated/_structured_context_schema_meta.json +0 -191
package/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
 
@@ -26,6 +30,12 @@ A space is a named directory or single file registered in the config. Example:
26
30
  }
27
31
  ```
28
32
 
33
+ **`schema`** — the schema used to validate nodes in the space. Can be:
34
+ - A bundled schema name (e.g. `strategy_general.json`, `strict_ost.json`)
35
+ - A local path relative to the config file (e.g. `./schemas/custom.json`) or absolute path
36
+ - A plugin schema prefixed with the plugin name (e.g. `sctx-wardley-mapping/wardley_map.json` or `wardley-mapping/wardley_map.json`). Specifying a plugin-prefixed schema automatically loads that plugin for the space.
37
+ - A simple filename (e.g. `wardley_map.json`) provided by a plugin explicitly declared under `plugins`.
38
+
29
39
  **`includeSpacesFrom`** — import space definitions from other config files. Useful for aggregating spaces from multiple projects into a central config. Duplicate space names are not allowed.
30
40
 
31
41
  A config must define at least one of `spaces` or `includeSpacesFrom`. When using `includeSpacesFrom` alone, `spaces` can be omitted entirely.
@@ -53,6 +63,73 @@ Use `plugins` to load parse plugins that read spaces from non-markdown sources.
53
63
 
54
64
  All plugin names must start with `sctx-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. External plugins are resolved in order: config-adjacent (`{configDir}/plugins/{name}`), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config file directory.
55
65
 
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.
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
+
56
133
  ## Markdown plugin config
57
134
 
58
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,40 +23,76 @@ 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 |
32
+ Every rule has a category, which describes its intent and supplies a default severity.
33
+
34
+ | Category | Typical use | Default severity |
24
35
  |---|---|---|
25
- | `id` | yes | Unique rule identifier |
26
- | `category` | yes | `validation` \| `coherence` \| `workflow` \| `best-practice` |
27
- | `description` | yes | Human-readable rule intent |
28
- | `check` | yes | JSONata expression; must evaluate to `true` |
29
- | `type` | no | Restrict rule to nodes of this `resolvedType` |
30
- | `scope` | no | Use `"global"` to evaluate once for the whole space |
31
- | `override` | no | Only for merge conflicts: allows later duplicate `id` to replace earlier |
36
+ | `validation` | Hard correctness constraints | `error` |
37
+ | `coherence` | Cross-node consistency checks | `warning` |
38
+ | `workflow` | Process/operating-discipline checks | `warning` |
39
+ | `best-practice` | Advisory quality checks | `info` |
32
40
 
33
- ## Categories
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).
34
42
 
35
- Categories label violations for reporting. They do not change expression execution semantics.
43
+ | Severity | Meaning | Fails validation |
44
+ |---|---|---|
45
+ | `error` | Content is wrong | yes |
46
+ | `warning` | Content is likely inconsistent or incomplete | no |
47
+ | `info` | Content could be better | no |
36
48
 
37
- | Category | Typical use |
38
- |---|---|
39
- | `validation` | Hard correctness constraints |
40
- | `coherence` | Cross-node consistency checks |
41
- | `workflow` | Process/operating-discipline checks |
42
- | `best-practice` | Advisory quality checks |
49
+ Only `error` violations fail validation: `sctx validate` and `sctx validate-file` exit non-zero, and JSON output reports `valid: false`. Warnings and info violations are reported but don't fail, so a work-in-progress space can still pass while advisory checks remain visible.
50
+
51
+ Prefer choosing the right category and relying on its default. Set `severity` on an individual rule only when that rule genuinely differs from the rest of its category:
52
+
53
+ ```json5
54
+ {
55
+ "id": "goal-not-blocked",
56
+ "category": "workflow",
57
+ "severity": "error",
58
+ "description": "Goals must not be left blocked",
59
+ "type": "goal",
60
+ "expression": "current.status != 'blocked'"
61
+ }
62
+ ```
63
+
64
+ `sctx schemas show` lists each category with its default severity and marks rules that override it.
65
+
66
+ ## Selecting nodes
67
+
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`)
43
74
 
44
- ## Evaluation model
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:
45
76
 
46
- - Rules with `scope: "global"` run once.
47
- - Other rules run per applicable node.
48
- - If `type` is set, only nodes with matching `resolvedType` are evaluated.
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.
88
+
89
+ An error in a `when` or `expression` is reported as a non-gating `error` violation.
49
90
 
50
- ## Expression context
91
+ ## Writing expressions
92
+
93
+ An `expression` (and `when`) is JSONata. This section covers what an expression can see and how to write expressions.
94
+
95
+ ### Expression context
51
96
 
52
97
  Each evaluation receives:
53
98
 
@@ -65,7 +110,7 @@ Useful resolved fields on nodes:
65
110
 
66
111
  Prefer `resolvedType` over raw `type` so aliases are handled correctly.
67
112
 
68
- ## Predicate scoping (`$$`)
113
+ ### Predicate scoping (`$$`)
69
114
 
70
115
  Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to reference outer variables:
71
116
 
@@ -73,29 +118,39 @@ Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to refere
73
118
  $count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution'])
74
119
  ```
75
120
 
76
- ## Rule imports (`$ref`)
121
+ ### Common patterns
77
122
 
78
- `$metadata.rules` can include `$ref` entries that import:
79
- - one rule
80
- - a rule-set object with `rules: []`
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
+ ```
81
129
 
82
- Example:
130
+ #### Dates and times
83
131
 
84
- ```json5
85
- "rules": [
86
- { "$ref": "sctx://rule-pack#/$defs/workflowRule" },
87
- { "$ref": "sctx://rule-pack#/$defs/coreRuleSet" }
88
- ]
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)
89
137
  ```
90
138
 
91
- Imported entries are normalized into the same flat runtime list.
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
92
144
 
93
- ## Merge and conflict behavior
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`.
94
146
 
95
- When metadata is composed across `$ref`:
96
- - Rules are merged by `id`.
97
- - Different payloads for the same `id` are an error by default.
98
- - A later rule may replace an earlier one only with `override: true`.
147
+ ### Overriding rules
148
+
149
+ ```json5
150
+ "$metadata": {
151
+ "imports": ["sctx://_rule-pack"]
152
+ }
153
+ ```
99
154
 
100
155
  Example override:
101
156
 
@@ -105,16 +160,79 @@ Example override:
105
160
  "override": true,
106
161
  "category": "workflow",
107
162
  "description": "Require exactly one active outcome",
108
- "scope": "global",
109
- "check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
163
+ "global": true,
164
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
110
165
  }
111
166
  ```
112
167
 
113
- ## 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).
114
238
 
115
- ```jsonata
116
- $exists(current.metric) = true
117
- $count(current.sources) >= 1
118
- $count(nodes[resolvedType='outcome' and status='active']) <= 1
119
- current.status != 'active' or $exists(parent) = false or parent.status = 'active'
120
- ```