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/README.md CHANGED
@@ -49,7 +49,7 @@ Rule violations:
49
49
  High tech juice press.md: Explore multiple candidate solutions (aim for at least three) for the target opportunity [best-practice: solution-quantity]
50
50
  ```
51
51
 
52
- Rule violations carry a severity (`error`, `warning` or `info`) that defaults from the rule's category. Only errors fail validation — see [docs/rules.md](docs/rules.md#categories-and-severity).
52
+ Rule violations carry a severity (`error`, `warning` or `info`) that defaults from the rule's category. Only errors fail validation — see [docs/rules.md](docs/rules.md#rule-shape).
53
53
 
54
54
  ## Installation
55
55
 
@@ -85,165 +85,63 @@ npx skills add https://github.com/mindsocket/structured-context/tree/main/plugin
85
85
 
86
86
  ## Concepts
87
87
 
88
- See [docs/concepts.md](docs/concepts.md) for the full terminology reference, including definitions of nodes, embedded nodes, spaces, schemas, rules, and more.
88
+ See [docs/concepts.md](docs/concepts.md) (`sctx docs concepts`) for terminology: spaces, nodes, embedded nodes, schemas, hierarchies, rules and more.
89
89
 
90
90
  ## Configuration
91
91
 
92
- `structured-context` looks for its config file in this order:
92
+ The config file maps space names to paths and schemas, with optional plugin settings. It is found from `--config`, then `$SCTX_CONFIG`, then `$XDG_CONFIG_HOME/structured-context/config.json` (default `~/.config/...`), then `./config.json`. Paths resolve relative to the config file, and `includeSpacesFrom` pulls in spaces from other config files.
93
93
 
94
- 1. `--config` command line argument
95
- 2. `$SCTX_CONFIG` env var
96
- 3. `~/.config/structured-context/config.json` (or `$XDG_CONFIG_HOME/structured-context/config.json`)
97
- 4. `./config.json` in the current working directory
98
-
99
- See `config.example.json` for the full structure. The config maps space names to paths, with optional additional configuration options for parsing and integrations. Paths in config files are resolved relative to the config file.
100
-
101
- **Including spaces from other configs:** Use `includeSpacesFrom` to import space definitions from other config files. This is useful for aggregating spaces from multiple projects into a central config, reducing the need to specify `--config` on CLI commands. Duplicate space names are not allowed.
102
-
103
- **Plugins and markdown plugin config:** See `sctx docs config` for the full reference including `fieldMap`, `typeInference`, `templateDir`, filter views, and plugin loading rules.
94
+ See `config.example.json` and [docs/config.md](docs/config.md) (`sctx docs config`) for the full reference, including `fieldMap`, `typeInference`, templates, filter views and plugin loading.
104
95
 
105
96
  ### Schemas
106
97
 
107
- Schemas define the structure and rules for the entities in a space, allowing customisation and extension to different models.
98
+ Schemas define the entity types, fields, hierarchy and rules for a space. Set one per space with `schema` in config. Bundled schemas:
99
+
100
+ - `strategy_general` — flexible strategy ladder (vision, mission, goals) flowing into an OST-style opportunity → solution → experiment hierarchy.
101
+ - `strict_ost` — Teresa Torres' canonical four levels: outcome → opportunity → solution → assumption test.
102
+ - `knowledge_wiki` — flat, LLM-maintained knowledge base of sources, concepts, entities, syntheses and notes, with provenance.
103
+ - `okf` — [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) v0.2 bundles: any concept type, with provenance, trust, lifecycle and Attested Computation fields checked when present.
108
104
 
109
- Several schemas (`strict_ost`, `knowledge_wiki` and `general`) are included. The general (strategy) schema combines a basic vision/mission/goals hierarchy with a hierarchy loosely based on Opportunity Solution Trees. It is intentionally flexible to support rapid initial adoption. The strict OST schema has a narrower scope, and reflects Teresa Torres' specific recommendations for Opportunity Solution Trees more closely. The knowledge wiki schema defines a flatter set of types used for knowledge bases.
105
+ Partials (`_*.json`, e.g. `_strategy_general`, `_knowledge_wiki`) are reusable building blocks for your own schemas. Plugins can contribute further schemas.
110
106
 
111
- structured-context schemas use a metaschema based on JSON Schema 2020-12 that adds a top-level `$metadata` block:
107
+ Schemas are JSON Schema 2020-12 plus a top-level `$metadata` block for hierarchy, relationships, aliases, imports and executable rules, for example:
112
108
 
113
109
  ```json5
114
110
  "$metadata": {
115
- "hierarchy": {
116
- "levels": ["outcome", { "type": "opportunity", "selfRef": true }, "solution", "assumption_test"],
117
- "allowSkipLevels": false
118
- },
119
- "relationships": [
120
- {
121
- "parent": "opportunity",
122
- "type": "assumption",
123
- "templateFormat": "table",
124
- "matchers": ["Assumptions"]
125
- }
126
- ],
111
+ "hierarchy": { "levels": ["outcome", { "type": "opportunity", "selfRef": true }, "solution", "assumption_test"] },
127
112
  "aliases": { "experiment": "assumption_test" },
128
113
  "rules": [
129
114
  {
130
115
  "id": "active-outcome-count",
131
116
  "category": "workflow",
132
117
  "description": "Only one outcome should be active at a time",
133
- "scope": "global",
134
- "check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
118
+ "global": true,
119
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
135
120
  }
136
121
  ]
137
122
  }
138
123
  ```
139
124
 
140
- Schema hierarchy levels support DAG (multi-parent) relationships via configurable edge fields. Each entry in `$metadata.hierarchy.levels` can be a plain type name string (defaults to `parent` field on child nodes) or an object:
141
-
142
- ```json5
143
- // Example fragments for hierarchy level objects:
144
- { "type": "opportunity", "selfRef": true }
145
- { "type": "solution", "field": "fulfills", "multiple": true }
146
- { "type": "requirement", "field": "generates", "fieldOn": "parent", "multiple": true }
147
- { "type": "solution", "field": "solutions", "fieldOn": "parent", "multiple": true, "selfRefField": "parent" }
148
- ```
149
-
150
- | Property | Default | Description |
151
- |---|---|---|
152
- | `type` | required | The node type at this hierarchy level |
153
- | `field` | `"parent"` | Name of the edge field |
154
- | `fieldOn` | `"child"` | Which side holds the field: `"child"` (child points up) or `"parent"` (parent points down) |
155
- | `multiple` | `false` | Whether the field is an array of wikilinks (enables multi-parent DAG) |
156
- | `selfRef` | `false` | Whether a node of this type may reference a same-type parent |
157
- | `selfRefField` | _undefined_ | Optional field for same-type parent relationships (always on child-side and singular) |
158
- | `templateFormat` | _undefined_ | Embedding hint (`"list"`, `"table"`, `"heading"`). When set alongside `matchers`, enables hierarchy embedding in typed pages |
159
- | `matchers` | _undefined_ | Heading patterns (strings or `/regex/`) to match for hierarchy embedding. Case-insensitive. |
160
- | `embeddedTemplateFields` | _undefined_ | Column names for table stubs when `template-sync` generates templates |
161
-
162
- The `selfRefField` property enables different fields for regular vs same-type relationships. For example, requirements can list solutions via `solutions` on the requirement node, while solutions can reference parent solutions via `parent` on the solution node.
163
-
164
- **Hierarchy embedding** — when `templateFormat` and `matchers` are set on a level, typed pages may include section headings that signal embedded content for that type without explicit `[type:: x]` annotations. Two patterns:
165
-
166
- - **Child-level**: heading matches the child level's type/matchers → list or table items create child nodes.
167
- - **Parent-level references**: heading matches the *parent* level's type/matchers → bare wikilink items (`- [[X]]`) populate the current node's reference field rather than creating new nodes. Useful for listing parent relationships inline.
168
-
169
- Bare wikilink items (`- [[Existing Node]]`) in any embedding section populate a field rather than creating a new node.
170
-
171
- **Adjacent Relationships** (`$metadata.relationships`) define connections between types outside the primary hierarchy — such as an `activity` having many `task` nodes. They drive embedded parsing (typed headings, lists, tables) and template generation.
172
-
173
- | Property | Default | Description |
174
- |---|---|---|
175
- | `parent` | required | Parent canonical type |
176
- | `type` | required | Child canonical type |
177
- | `field` | `"parent"` | Frontmatter field holding the wikilink(s). Required when `fieldOn: "parent"`. |
178
- | `fieldOn` | `"child"` | `"child"`: child holds a link pointing up. `"parent"`: parent holds an array of child links. |
179
- | `templateFormat` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
180
- | `matchers` | `[]` | Heading text to match for embedded parsing (strings or `/regex/`). Case-insensitive. |
181
- | `multiple` | `true` | Whether multiple children are expected |
182
- | `embeddedTemplateFields` | `[]` | Field names to include as table columns in templates |
183
-
184
- With `fieldOn: "parent"`, embedded child nodes (parsed from a matching heading's list or table) are appended as wikilinks to the parent's `field` array, rather than receiving a `parent` field. This matches schemas where the content model naturally lists children on the parent (e.g. `activity.tasks: ["[[Task A]]"]`).
185
-
186
- `$ref` reuses JSON Schema definitions for validation only; it brings no metadata. Metadata is composed only through explicit imports:
187
-
188
- ```json5
189
- "$metadata": { "imports": ["sctx://core/_strategy_general"] }
190
- ```
191
-
192
- - An import brings the whole schema's `$metadata`, and is transitive (the imported schema's own imports come too).
193
- - Merge order: imports in order, depth-first, then the schema's own metadata last.
194
- - One rule for every key: a duplicate rule `id`, alias, relationship (same `parent`/`type`/`field`) or hierarchy `name` is an error unless the later entry sets `override: true` (for an alias, write the value as `{ "type": "goal", "override": true }`).
195
- - `hierarchy`: each schema file declares at most one hierarchy, and every imported hierarchy is kept as a separate named hierarchy (see [Multiple hierarchies](#multiple-hierarchies))
196
- - Each rule's `category` supplies its default severity (`validation` → `error`, `coherence`/`workflow` → `warning`, `best-practice` → `info`); an optional `severity` field overrides it.
197
-
198
- If no file defines `hierarchy`, hierarchy-specific checks are skipped.
199
-
200
- #### Multiple hierarchies
201
-
202
- A space can have several hierarchies by importing schemas that each declare one. For example, a root schema might import a work hierarchy (`goal → task`) and a skills hierarchy (`skill_area → skill`).
203
-
204
- - **Naming**: a hierarchy's `name` defaults to the declaring schema's file stem (e.g. `_skills` for `_skills.json`). Set `"name"` in `$metadata.hierarchy` to override it.
205
- - **Main hierarchy**: if the root schema declares a hierarchy, it is the main one. If the root declares none and exactly one is imported, that one is the main hierarchy. If several are imported, the root must select one:
206
-
207
- ```json5
208
- "$metadata": {
209
- "imports": ["sctx://_work", "sctx://_skills"],
210
- "hierarchy": { "$ref": "sctx://_work#/$metadata/hierarchy" }
211
- }
212
- ```
213
-
214
- A root may either declare a hierarchy or select one with `$ref`, not both.
215
- - **Each type belongs to at most one hierarchy.** A type in two hierarchies is an error. To change an imported hierarchy's levels, declare a hierarchy with the same `name` and `"override": true`.
216
- - **Validation** (layering, parent types, skip levels, orphans) runs per hierarchy. Relationships can connect types across hierarchies.
217
- - The main hierarchy drives `space_on_a_page` parsing, hierarchy embedding, `template-sync`, and the default `show`/`diagram`/`render` output. Use `--hierarchy <name>` to render another one.
218
-
219
- **Customizing Schemas:**
220
- - **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`) are loaded and used to resolve references (using `$ref`).
221
- - **Partials as entity libraries**: Partials can define reusable entity types in `$defs` that composing schemas reference via `$ref`. Bundled partials like `_strategy_general` and `_knowledge_wiki` provide common entity sets for strategy and wiki content.
222
- - **Partials can carry metadata**: Unlike plain JSON Schema, partials may include `$metadata` (hierarchy, aliases, relationships, rules). A schema gets that metadata only by listing the partial in `$metadata.imports`.
223
- - **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "https://json-schema.org/draft/2020-12/schema"` so it validates standalone as plain JSON Schema.
224
- - **Loading priority**: Partial schemas are loaded from the default schema directory, configured plugins (`schemas/`), and the directory of your specified target schema.
225
- - **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
226
- - **`$id` namespaces**: All `$id`s use `sctx://`. Bundled schemas are `sctx://core/<name>`, plugin schemas `sctx://<pluginName>/<name>`, and local schemas a single name, `sctx://<name>`. Collisions are errors. Legacy bundled ids (`sctx://<name>`, e.g. `sctx://_sctx_base`) resolve to `sctx://core/` with a deprecation warning until 1.0. See [docs/schemas.md](docs/schemas.md#schema-id-namespaces).
227
- - **Plugin-contributed schemas**: External plugins can contribute schemas via `<pluginRoot>/schemas/` (or `"sctx": { "schemas": "..." }` in `package.json`). Spaces can reference them using `sctx-plugin/schema.json` or `plugin/schema.json`, or by simple filename if the plugin is declared in `plugins`. Schemas resolve statically without executing plugin JavaScript code.
125
+ See [docs/schemas.md](docs/schemas.md) (`sctx docs schema`) for the dialect, composition, multiple hierarchies and `$id` namespaces; [docs/rules.md](docs/rules.md) (`sctx docs rules`) for rules; and [docs/concepts.md](docs/concepts.md) for hierarchy embedding and relationships.
228
126
 
229
- Schema resolution order: space config `schema` > global config `schema`. Schema resolution fails if none is configured.
230
-
231
- **⚠️ Security Notice: Only use schemas and configuration files from trusted sources.**
232
-
233
- The tool executes JSONata expressions defined in schema files for rule validation. A maliciously crafted schema could make JSONata access JavaScript's prototype chain and execute arbitrary code. Only use schemas you've created or reviewed personally.
127
+ **⚠️ Only use schemas and configuration from trusted sources:** rules are JSONata expressions, and a malicious schema can execute arbitrary code. See [docs/config.md](docs/config.md#security-notice).
234
128
 
235
129
  ## Usage
236
130
 
237
131
  ### Validate nodes
238
132
 
239
133
  ```bash
240
- sctx validate <space> [--watch]
134
+ sctx validate <space> [--watch] [--depth <n>] [--include-gated] [--semantic [provider]] [--rule-filter <dim=values>]
241
135
  ```
242
136
 
243
137
  Validates markdown files against the JSON schema:
244
138
  - Extracts YAML frontmatter from each `.md` file
245
139
  - Skips files without frontmatter or without a `type` field
246
140
  - Reports validation results with counts and per-file errors
141
+ - Runs executable rules (`$metadata.rules`) with **cascade gating**: a schema, hierarchy, or blocking-rule failure on a node suppresses rule evaluation for that node's hierarchy subtree. Gated rules are reported in a Coverage section so you know what wasn't evaluated and why. See [docs/rules.md](docs/rules.md#cascade-gating) for details.
142
+ - `--depth <n>` limits evaluation to hierarchy nodes within `n` breadth-first levels from the top — useful for staged agentic fix loops (fix the top level, then request the next).
143
+ - `--include-gated` evaluates rules even on gated nodes (still reported as gated) — for surfacing cheap bottom-up checks on request.
144
+ - `--semantic` also evaluates the `semantic` part of rules with the configured model provider (Jev, Anthropic or OpenAI-compatible); `--semantic <provider>` uses that configured model provider instead; `--rule-filter` narrows which rules run. See [docs/semantic-rules.md](docs/semantic-rules.md) (`sctx docs semantic-rules`).
247
145
 
248
146
  ### Render space
249
147
 
@@ -262,85 +160,19 @@ Renders a space in a plugin-provided format. Use `sctx render list [space]` to l
262
160
  sctx show <space> [--filter <view-or-expression>] [--hierarchy <name>]
263
161
  ```
264
162
 
265
- Prints the space as an indented hierarchy tree. Hierarchy roots are listed first, followed by orphans (nodes in the hierarchy but with no resolved parent) and non-hierarchy nodes. The main hierarchy is shown by default; `--hierarchy <name>` selects another (see [Multiple hierarchies](#multiple-hierarchies)). `diagram` and `render` accept the same option.
163
+ Prints the space as an indented hierarchy tree. Hierarchy roots are listed first, followed by orphans (nodes in the hierarchy but with no resolved parent) and non-hierarchy nodes. The main hierarchy is shown by default; `--hierarchy <name>` selects another (see [Multiple hierarchies](docs/schemas.md#multiple-hierarchies)). `diagram` and `render` accept the same option.
266
164
 
267
165
  When a node appears under multiple parents (DAG hierarchy), it is printed in full under its first parent. Subsequent appearances with children show a `(*)` marker indicating the subtree is omitted.
268
166
 
269
- **Filtering:** The `--filter` flag accepts either a named view from the space config, or an inline filter expression. Only nodes matching the expression are shown.
167
+ **Filtering:** `--filter` (on `show` and `render`) takes a named view from the space config or an inline expression: `WHERE {jsonata}`, optionally with `SELECT {spec}` to expand results along the graph.
270
168
 
271
169
  ```bash
272
- # Inline expression
273
170
  sctx show <space> --filter "WHERE resolvedType='solution' and status='active'"
274
-
275
- # Named view from config
276
- sctx show <space> --filter active-solutions
171
+ sctx show <space> --filter "SELECT ancestors(opportunity) WHERE resolvedType='solution'"
172
+ sctx show <space> --filter active-solutions # named view
277
173
  ```
278
174
 
279
- See [Filter expressions](#filter-expressions) below for expression syntax.
280
-
281
- ### Filter expressions
282
-
283
- Filter expressions are used with `--filter` and in config `views`. They use a `SELECT ... WHERE ...` pseudo-DSL:
284
-
285
- | Form | Meaning |
286
- |------|---------|
287
- | `WHERE {jsonata}` | Return nodes where the JSONata predicate is truthy |
288
- | `SELECT {spec} WHERE {jsonata}` | Filter by WHERE, then expand result via SELECT |
289
- | `SELECT {spec}` | Expand from all nodes via SELECT (no WHERE filter — returns all nodes, expanded per spec) |
290
- | `{jsonata}` | Bare JSONata, treated as a WHERE predicate (convenience shorthand) |
291
-
292
- The WHERE predicate is a [JSONata](https://docs.jsonata.org/overview) expression evaluated per node. Within the expression, each node's fields are accessible directly (e.g. `resolvedType`, `status`, any schema fields like `title`). Two built-in fields are always available regardless of schema: `label` (relative file path, e.g. `"solutions/My Solution.md"`) and `title` (node display name). Additionally, two pre-computed traversal arrays are available:
293
-
294
- - **`ancestors[]`** — flat array of ancestor nodes, nearest first, deduplicated. Each entry includes all schema fields of the ancestor node, plus:
295
- - `_field` — the edge field name that connects to the ancestor
296
- - `_source` — `'hierarchy'` or `'relationship'`
297
- - `_hierarchy` — the hierarchy name, for hierarchy edges
298
- - `_selfRef` — whether the edge is a same-type (self-referential) link
299
- - **`descendants[]`** — same structure, for descendant nodes
300
-
301
- **SELECT spec** expands the result set by walking the graph from matched nodes. The spec is a comma-separated list of directives:
302
-
303
- | Directive | Meaning |
304
- |-----------|---------|
305
- | `ancestors` | All ancestor nodes |
306
- | `ancestors(type)` | Ancestors of the given resolved type |
307
- | `descendants` | All descendant nodes |
308
- | `descendants(type)` | Descendants of the given resolved type |
309
- | `siblings` | Nodes sharing at least one parent with matched nodes |
310
- | `relationships` | All nodes connected via a relationship (non-hierarchy) edge |
311
- | `relationships(childType)` | Relationship-connected nodes of the given child type |
312
- | `relationships(parentType:childType)` | As above, also filtering by parent type |
313
- | `relationships(parentType:field:childType)` | Fully qualified: also filtering by edge field name |
314
-
315
- Multiple directives may be combined: `SELECT ancestors(goal), siblings WHERE ...`
316
-
317
- **Examples:**
318
-
319
- ```jsonata
320
- // All solutions
321
- WHERE resolvedType='solution'
322
-
323
- // Active solutions only
324
- WHERE resolvedType='solution' and status='active'
325
-
326
- // Solutions whose nearest opportunity ancestor is active
327
- WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])
328
-
329
- // Nodes that have any ancestor goal
330
- WHERE $exists(ancestors[resolvedType='goal'])
331
-
332
- // Bare JSONata shorthand (no WHERE keyword)
333
- resolvedType='solution' and status='active'
334
-
335
- // Solutions + their opportunity ancestors
336
- SELECT ancestors(opportunity) WHERE resolvedType='solution'
337
-
338
- // Solutions + their siblings (other solutions under same opportunity)
339
- SELECT siblings WHERE resolvedType='solution' and status='active'
340
-
341
- // Opportunities + their related assumptions
342
- SELECT relationships(assumption) WHERE resolvedType='opportunity'
343
- ```
175
+ See [Filter expressions](docs/concepts.md#filter-expressions) (`sctx docs concepts`) for the full syntax.
344
176
 
345
177
  ### Generate Mermaid diagram
346
178
 
@@ -376,28 +208,9 @@ sctx schemas show strategy_general --mermaid-erd
376
208
  sctx miro-sync <space> [--new-frame <title>] [--dry-run] [--verbose]
377
209
  ```
378
210
 
379
- Syncs space nodes to a Miro board as cards with connectors. Requires `MIRO_TOKEN` env var and `boardId` set in the `miro` plugin config for the space.
380
-
381
- - `--new-frame <title>` — create a new frame on the board and sync into it; auto-saves the resulting `frameId` back to the miro plugin config
382
- - `--dry-run` — show what would change without touching Miro
383
- - `--verbose` / `-v` — detailed per-card and per-connector output
384
-
385
- Configure the miro plugin in the space's config entry:
386
-
387
- ```json
388
- {
389
- "plugins": {
390
- "miro": {
391
- "boardId": "your-board-id",
392
- "frameId": "your-frame-id"
393
- }
394
- }
395
- }
396
- ```
397
-
398
- On subsequent runs, the cached `frameId` is used automatically. Cards are colour-coded by node type and linked by parent→child connectors. A local `.miro-cache/` directory tracks Miro IDs to enable incremental updates.
211
+ Syncs space nodes to a Miro board as colour-coded cards with parent→child connectors. Requires a `MIRO_TOKEN` env var and `plugins.miro.boardId` in the space config. `--new-frame` creates a frame and saves its `frameId` to config; `--dry-run` previews changes.
399
212
 
400
- Sync is one-way (OST → Miro) and scoped to a single frame. Only cards and connectors created by this tool within that frame are managed — everything else on the board is left untouched. Card content and connectors are overwritten or recreated to match the markdown source; any edits made directly in Miro to managed cards will be lost on the next sync. Existing card positions are not changed.
213
+ Sync is one-way and scoped to one frame: only cards and connectors this tool created there are managed, and edits made in Miro to those cards are overwritten on the next sync. A local `.miro-cache/` tracks Miro IDs for incremental updates.
401
214
 
402
215
  ### Sync templates with schema
403
216
 
package/dist/api.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Import from this module to use structured-context as a library:
5
5
  *
6
- * import { loadConfig, loadSpaceGraph, validateSpace, validateFile } from 'structured-context/api';
7
- * import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/api';
6
+ * import { loadConfig, loadSpaceGraph, validateSpace, validateFile } from './api.js';
7
+ * import type { StructuredContextPlugin, PluginContext, ParseResult } from './api.js';
8
8
  *
9
9
  * Common patterns:
10
10
  *
@@ -18,18 +18,18 @@
18
18
  * const validation = await validateSpace(ctx, { readResult });
19
19
  */
20
20
  export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
21
- export type { Config, SpaceConfig } from './config';
22
- export { loadConfig, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
23
- export { PLUGIN_PREFIX } from './constants';
24
- export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
25
- export type { ParseHook, ParseResult, PluginContext, RenderFileOutput, RenderFormat, RenderHook, RenderMultiFileOutput, RenderOptions, RenderResult, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
26
- export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
27
- export { readSpace } from './read/read-space';
28
- export { buildTargetIndex, formatWikilink, type ParsedWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
29
- export type { SharedEmbeddingFields } from './schema/metadata-contract';
30
- export { bundledSchemasDir, loadSchema, type PluginSchemaSource, setBundledSchemasDir } from './schema/schema';
31
- export { createSpaceContext, SpaceNotFoundError } from './space-context';
32
- export { buildSpaceGraph, type SpaceGraph } from './space-graph';
33
- export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationIssue, FileValidationResult, Hierarchy, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, RuleCategory, RuleSeverity, RuleViolation, SchemaMetadata, SchemaWithMetadata, SourceLocation, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types';
34
- export type { ValidationResult } from './validate';
35
- export { validateFile, validateSpace } from './validate';
21
+ export type { Config, SpaceConfig } from './config.js';
22
+ export { loadConfig, resolveSchema, resolveSchemaPath, setConfigPath } from './config.js';
23
+ export { PLUGIN_PREFIX } from './constants.js';
24
+ export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph.js';
25
+ export type { ParseHook, ParseResult, PluginContext, RenderFileOutput, RenderFormat, RenderHook, RenderMultiFileOutput, RenderOptions, RenderResult, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util.js';
26
+ export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util.js';
27
+ export { readSpace } from './read/read-space.js';
28
+ export { buildTargetIndex, formatWikilink, type ParsedWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils.js';
29
+ export type { SharedEmbeddingFields } from './schema/metadata-contract.js';
30
+ export { bundledSchemasDir, loadSchema, type PluginSchemaSource, setBundledSchemasDir } from './schema/schema.js';
31
+ export { createSpaceContext, SpaceNotFoundError } from './space-context.js';
32
+ export { buildSpaceGraph, type SpaceGraph } from './space-graph.js';
33
+ export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationIssue, FileValidationResult, Hierarchy, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, RuleCategory, RuleSeverity, RuleViolation, SchemaMetadata, SchemaWithMetadata, SourceLocation, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types.js';
34
+ export type { ValidationResult } from './validate.js';
35
+ export { validateFile, validateSpace } from './validate.js';
package/dist/api.js CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Import from this module to use structured-context as a library:
5
5
  *
6
- * import { loadConfig, loadSpaceGraph, validateSpace, validateFile } from 'structured-context/api';
7
- * import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/api';
6
+ * import { loadConfig, loadSpaceGraph, validateSpace, validateFile } from './api.js';
7
+ * import type { StructuredContextPlugin, PluginContext, ParseResult } from './api.js';
8
8
  *
9
9
  * Common patterns:
10
10
  *
@@ -17,13 +17,13 @@
17
17
  * const graph = await assembleSpaceGraph(ctx, { readResult });
18
18
  * const validation = await validateSpace(ctx, { readResult });
19
19
  */
20
- export { loadConfig, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
21
- export { PLUGIN_PREFIX } from './constants';
22
- export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
23
- export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
24
- export { readSpace } from './read/read-space';
25
- export { buildTargetIndex, formatWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
26
- export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
27
- export { createSpaceContext, SpaceNotFoundError } from './space-context';
28
- export { buildSpaceGraph } from './space-graph';
29
- export { validateFile, validateSpace } from './validate';
20
+ export { loadConfig, resolveSchema, resolveSchemaPath, setConfigPath } from './config.js';
21
+ export { PLUGIN_PREFIX } from './constants.js';
22
+ export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph.js';
23
+ export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util.js';
24
+ export { readSpace } from './read/read-space.js';
25
+ export { buildTargetIndex, formatWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils.js';
26
+ export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema.js';
27
+ export { createSpaceContext, SpaceNotFoundError } from './space-context.js';
28
+ export { buildSpaceGraph } from './space-graph.js';
29
+ export { validateFile, validateSpace } from './validate.js';
@@ -5,6 +5,7 @@ const TOPICS = {
5
5
  config: 'config.md',
6
6
  schema: 'schemas.md',
7
7
  rules: 'rules.md',
8
+ 'semantic-rules': 'semantic-rules.md',
8
9
  };
9
10
  export function docs(topic) {
10
11
  let filePath;
@@ -0,0 +1,4 @@
1
+ /** List bundled and configured model providers with their effective config; keys are never shown. */
2
+ export declare function listProviders(options?: {
3
+ json?: boolean;
4
+ }): void;
@@ -0,0 +1,52 @@
1
+ import { loadConfig } from '../config';
2
+ import { BUNDLED_PROVIDERS, hydrateProvider } from '../judge/factory';
3
+ function row(name, def, source) {
4
+ try {
5
+ const h = hydrateProvider(name, def);
6
+ const needsKey = h.api !== 'openai-completions' && h.api !== 'fake';
7
+ return {
8
+ name,
9
+ source,
10
+ api: h.api,
11
+ model: h.model,
12
+ endpoint: h.endpoint,
13
+ keyEnv: h.keyEnv,
14
+ valid: !needsKey || h.apiKey !== undefined,
15
+ error: needsKey && h.apiKey === undefined ? `no API key; set ${h.keyEnv}` : undefined,
16
+ };
17
+ }
18
+ catch (err) {
19
+ return {
20
+ name,
21
+ source,
22
+ api: def.api,
23
+ model: def.model,
24
+ endpoint: def.endpoint,
25
+ keyEnv: def.apiKeyEnv,
26
+ valid: false,
27
+ error: err instanceof Error ? err.message : String(err),
28
+ };
29
+ }
30
+ }
31
+ /** List bundled and configured model providers with their effective config; keys are never shown. */
32
+ export function listProviders(options = {}) {
33
+ const config = loadConfig();
34
+ const configured = config.modelProviders ?? {};
35
+ const rows = [
36
+ ...Object.entries(configured).map(([name, def]) => row(name, def, 'config')),
37
+ ...Object.entries(BUNDLED_PROVIDERS)
38
+ .filter(([name]) => !(name in configured))
39
+ .map(([name, def]) => row(name, def, 'bundled')),
40
+ ];
41
+ if (options.json) {
42
+ console.log(JSON.stringify(rows, null, 2));
43
+ return;
44
+ }
45
+ for (const r of rows) {
46
+ const origin = r.source === 'bundled' ? '' : ' (config)';
47
+ const model = r.model ?? '(api default)';
48
+ const endpoint = r.endpoint ?? '(api default)';
49
+ const key = r.valid ? `key via ${r.keyEnv}` : (r.error ?? 'no key');
50
+ console.log(`${r.name}${origin}\n api=${r.api}\n endpoint=${endpoint}\n model=${model}\n ${key}`);
51
+ }
52
+ }
@@ -3,6 +3,7 @@ import { dirname, join, resolve } from 'node:path';
3
3
  import { loadConfig } from '../config';
4
4
  import { discoverPlugins } from '../plugins/loader';
5
5
  import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
6
+ import { ruleSemantic } from '../schema/rule-severity';
6
7
  import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema, } from '../schema/schema';
7
8
  import { mergeVariantProperties, resolveRef } from '../schema/schema-refs';
8
9
  import { DEFAULT_SEVERITY_BY_CATEGORY } from '../schema/validate-rules';
@@ -105,8 +106,7 @@ function showMetadata(metadata) {
105
106
  for (const [category, rules] of groups) {
106
107
  console.log(` ${category} (default severity: ${DEFAULT_SEVERITY_BY_CATEGORY[category]}):`);
107
108
  for (const rule of rules) {
108
- const override = rule.severity ? ` [severity: ${rule.severity}]` : '';
109
- console.log(` ${rule.id}: ${rule.description}${override}`);
109
+ console.log(` ${rule.id}: ${rule.description}${ruleAnnotations(rule)}`);
110
110
  }
111
111
  }
112
112
  }
@@ -188,6 +188,25 @@ function generateMermaidErd(metadata, entities) {
188
188
  }
189
189
  return mmd;
190
190
  }
191
+ /** Bracketed notes on a rule: semantic, severity (and verdict severities) and `when`. Empty for a plain rule. */
192
+ function ruleAnnotations(rule) {
193
+ const notes = [];
194
+ const semantic = ruleSemantic(rule);
195
+ if (rule.global)
196
+ notes.push('global');
197
+ if (semantic)
198
+ notes.push('semantic');
199
+ if (rule.severity)
200
+ notes.push(`severity: ${rule.severity}`);
201
+ if (semantic?.severity) {
202
+ notes.push(`verdict severity: ${Object.entries(semantic.severity)
203
+ .map(([verdict, sev]) => `${verdict}=${sev}`)
204
+ .join(', ')}`);
205
+ }
206
+ if (rule.when)
207
+ notes.push(`when: ${rule.when}`);
208
+ return notes.length > 0 ? ` [${notes.join('] [')}]` : '';
209
+ }
191
210
  export async function listSchemas() {
192
211
  const config = loadConfig();
193
212
  // List all schemas known to config
@@ -6,4 +6,7 @@ export { validateFile } from '../validate';
6
6
  */
7
7
  export declare function validateFileCommand(filePath: string, options?: {
8
8
  json?: boolean;
9
+ includeGated?: boolean;
10
+ semantic?: boolean | string;
11
+ ruleFilter?: string[];
9
12
  }): Promise<number>;
@@ -1,5 +1,8 @@
1
1
  import { loadConfig } from '../config';
2
- import { validateFile } from '../validate';
2
+ import { judgeForRun } from '../judge/factory';
3
+ import { parseRuleFilter } from '../schema/rule-filter';
4
+ import { createSpaceContext } from '../space-context';
5
+ import { fileNotInSpace, resolveFileSpace, validateResolvedFile } from '../validate';
3
6
  export { validateFile } from '../validate';
4
7
  /**
5
8
  * CLI command: validate a file and print results to stdout.
@@ -8,7 +11,19 @@ export { validateFile } from '../validate';
8
11
  */
9
12
  export async function validateFileCommand(filePath, options = {}) {
10
13
  const config = loadConfig();
11
- const result = await validateFile(filePath, config);
14
+ const resolved = resolveFileSpace(filePath, config);
15
+ let result;
16
+ if (resolved) {
17
+ const judge = judgeForRun(config, resolved.space, options.semantic);
18
+ result = await validateResolvedFile(filePath, resolved.label, createSpaceContext(resolved.space.name, config), {
19
+ includeGated: options.includeGated,
20
+ judge,
21
+ ruleFilter: options.ruleFilter && parseRuleFilter(options.ruleFilter),
22
+ });
23
+ }
24
+ else {
25
+ result = fileNotInSpace(filePath);
26
+ }
12
27
  if (!result.inSpace) {
13
28
  if (options.json) {
14
29
  console.log(JSON.stringify(result, null, 2));
@@ -1,5 +1,9 @@
1
1
  import type { SpaceContext } from '../types';
2
2
  export declare function validate(context: SpaceContext, options?: {
3
3
  json?: boolean;
4
+ depth?: number;
5
+ includeGated?: boolean;
6
+ semantic?: boolean | string;
7
+ ruleFilter?: string[];
4
8
  }): Promise<number>;
5
9
  export declare function watchValidate(context: SpaceContext): Promise<never>;