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/README.md CHANGED
@@ -28,10 +28,13 @@ Content and structure
28
28
  Schema validation errors 0
29
29
  Broken links 1
30
30
  Duplicate keys 0
31
- Rule violations 2
31
+ Rule violations (error) 0
32
+ Rule violations (warning) 1
33
+ Rule violations (info) 1
32
34
  Hierarchy violations 0
33
35
  Orphans (hierarchy nodes - no parent) 1
34
- Ignored during parsing 0
36
+ Unresolved content links 0
37
+ Excluded during parsing 0
35
38
 
36
39
  Orphans (hierarchy nodes - no parent):
37
40
  Solution looking for a problem.md
@@ -40,13 +43,14 @@ Broken links:
40
43
  Steal underpants.md: [[Gnome goals]] → Link target "Gnome goals" in field "related" not found
41
44
 
42
45
  Rule violations:
43
- workflow (1):
44
- Build Death Star.md: Opportunities should contribute to at least one Outcome
45
-
46
- best-practice (1):
47
- High tech juice press.md: Explore multiple candidate solutions (aim for at least three) for an active target opportunity
46
+ WARNING (1):
47
+ Build Death Star.md: Opportunities should contribute to at least one Outcome [workflow: opportunity-has-outcome]
48
+ INFO (1):
49
+ High tech juice press.md: Explore multiple candidate solutions (aim for at least three) for the target opportunity [best-practice: solution-quantity]
48
50
  ```
49
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#rule-shape).
53
+
50
54
  ## Installation
51
55
 
52
56
  Requires [Bun](https://bun.sh) runtime.
@@ -81,230 +85,99 @@ npx skills add https://github.com/mindsocket/structured-context/tree/main/plugin
81
85
 
82
86
  ## Concepts
83
87
 
84
- 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.
85
89
 
86
90
  ## Configuration
87
91
 
88
- `structured-context` looks for its config file in this order:
89
-
90
- 1. `--config` command line argument
91
- 2. `$SCTX_CONFIG` env var
92
- 3. `~/.config/structured-context/config.json` (or `$XDG_CONFIG_HOME/structured-context/config.json`)
93
- 4. `./config.json` in the current working directory
94
-
95
- 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.
96
-
97
- **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.
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.
98
93
 
99
- **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.
100
95
 
101
96
  ### Schemas
102
97
 
103
- 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:
104
99
 
105
- 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.
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.
106
104
 
107
- structured-context schemas use a metaschema based on JSON Schema Draft-07 that adds a top-level `$metadata` block:
105
+ Partials (`_*.json`, e.g. `_strategy_general`, `_knowledge_wiki`) are reusable building blocks for your own schemas. Plugins can contribute further schemas.
106
+
107
+ Schemas are JSON Schema 2020-12 plus a top-level `$metadata` block for hierarchy, relationships, aliases, imports and executable rules, for example:
108
108
 
109
109
  ```json5
110
110
  "$metadata": {
111
- "hierarchy": {
112
- "levels": ["outcome", { "type": "opportunity", "selfRef": true }, "solution", "assumption_test"],
113
- "allowSkipLevels": false
114
- },
115
- "relationships": [
116
- {
117
- "parent": "opportunity",
118
- "type": "assumption",
119
- "templateFormat": "table",
120
- "matchers": ["Assumptions"]
121
- }
122
- ],
111
+ "hierarchy": { "levels": ["outcome", { "type": "opportunity", "selfRef": true }, "solution", "assumption_test"] },
123
112
  "aliases": { "experiment": "assumption_test" },
124
113
  "rules": [
125
114
  {
126
115
  "id": "active-outcome-count",
127
116
  "category": "workflow",
128
117
  "description": "Only one outcome should be active at a time",
129
- "scope": "global",
130
- "check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
118
+ "global": true,
119
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
131
120
  }
132
121
  ]
133
122
  }
134
123
  ```
135
124
 
136
- 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:
137
-
138
- ```json5
139
- // Example fragments for hierarchy level objects:
140
- { "type": "opportunity", "selfRef": true }
141
- { "type": "solution", "field": "fulfills", "multiple": true }
142
- { "type": "requirement", "field": "generates", "fieldOn": "parent", "multiple": true }
143
- { "type": "solution", "field": "solutions", "fieldOn": "parent", "multiple": true, "selfRefField": "parent" }
144
- ```
145
-
146
- | Property | Default | Description |
147
- |---|---|---|
148
- | `type` | required | The node type at this hierarchy level |
149
- | `field` | `"parent"` | Name of the edge field |
150
- | `fieldOn` | `"child"` | Which side holds the field: `"child"` (child points up) or `"parent"` (parent points down) |
151
- | `multiple` | `false` | Whether the field is an array of wikilinks (enables multi-parent DAG) |
152
- | `selfRef` | `false` | Whether a node of this type may reference a same-type parent |
153
- | `selfRefField` | _undefined_ | Optional field for same-type parent relationships (always on child-side and singular) |
154
- | `templateFormat` | _undefined_ | Embedding hint (`"list"`, `"table"`, `"heading"`). When set alongside `matchers`, enables hierarchy embedding in typed pages |
155
- | `matchers` | _undefined_ | Heading patterns (strings or `/regex/`) to match for hierarchy embedding. Case-insensitive. |
156
- | `embeddedTemplateFields` | _undefined_ | Column names for table stubs when `template-sync` generates templates |
157
-
158
- 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.
159
-
160
- **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:
161
-
162
- - **Child-level**: heading matches the child level's type/matchers → list or table items create child nodes.
163
- - **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.
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.
164
126
 
165
- Bare wikilink items (`- [[Existing Node]]`) in any embedding section populate a field rather than creating a new node.
166
-
167
- **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.
168
-
169
- | Property | Default | Description |
170
- |---|---|---|
171
- | `parent` | required | Parent canonical type |
172
- | `type` | required | Child canonical type |
173
- | `field` | `"parent"` | Frontmatter field holding the wikilink(s). Required when `fieldOn: "parent"`. |
174
- | `fieldOn` | `"child"` | `"child"`: child holds a link pointing up. `"parent"`: parent holds an array of child links. |
175
- | `templateFormat` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
176
- | `matchers` | `[]` | Heading text to match for embedded parsing (strings or `/regex/`). Case-insensitive. |
177
- | `multiple` | `true` | Whether multiple children are expected |
178
- | `embeddedTemplateFields` | `[]` | Field names to include as table columns in templates |
179
-
180
- 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]]"]`).
181
-
182
- Metadata is composable across `$ref` graphs:
183
- - `hierarchy`: multiple schema files may define a hierarchy; **last one wins** (root schema overrides partials)
184
- - `aliases` are shallow-merged (later wins)
185
- - `rules` merge by `id`; conflicts error unless the later rule sets `override: true`
186
- - `$metadata.rules` supports `$ref` imports for reusable rule packs
187
- - `relationships` are collected from all files
188
-
189
- If no file defines `hierarchy`, hierarchy-specific checks are skipped.
190
-
191
- **Customizing Schemas:**
192
- - **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`).
193
- - **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.
194
- - **Partials can carry metadata**: Unlike plain JSON Schema, partials may include `$metadata` (hierarchy, aliases, relationships, rules). This makes them self-contained units that bundle both type definitions and behavioral metadata.
195
- - **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "http://json-schema.org/draft-07/schema#"` so it validates standalone as plain JSON Schema.
196
- - **Loading priority**: Partial schemas are loaded from both the default schema directory and the directory of your specified target schema.
197
- - **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
198
- - **Unique IDs**: To encourage clean namespacing, local partial schemas **must** have unique `$id`s that do not collide with the default schemas. If a collision is detected, validation will fail with an error.
199
-
200
- Schema resolution order: space config `schema` > global config `schema`. Schema resolution fails if none is configured.
201
-
202
- **⚠️ Security Notice: Only use schemas and configuration files from trusted sources.**
203
-
204
- 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).
205
128
 
206
129
  ## Usage
207
130
 
208
131
  ### Validate nodes
209
132
 
210
133
  ```bash
211
- sctx validate <space> [--watch]
134
+ sctx validate <space> [--watch] [--depth <n>] [--include-gated] [--semantic [provider]] [--rule-filter <dim=values>]
212
135
  ```
213
136
 
214
137
  Validates markdown files against the JSON schema:
215
138
  - Extracts YAML frontmatter from each `.md` file
216
139
  - Skips files without frontmatter or without a `type` field
217
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`).
218
145
 
219
- ### Show space tree
146
+ ### Render space
220
147
 
221
148
  ```bash
222
- sctx show <space> [--filter <view-or-expression>]
149
+ sctx render <space> <format> [--out <dir-or-file>] [--force] [--filter <view>] [--hierarchy <name>] [--data key=value]
223
150
  ```
224
151
 
225
- 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.
152
+ Renders a space in a plugin-provided format. Use `sctx render list [space]` to list available formats.
226
153
 
227
- 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.
154
+ - `markdown.bullets` — Indented bullet list tree (outputs to stdout or `--out <file>`).
155
+ - `markdown.directory` — Markdown space directory with one file per node. Writes to `--out <dir>` (refuses to overwrite existing files unless `--force` is passed). Flat directory by default, or organized into per-type subfolders with `--data folders=type` or when configured with `typeInference.folderMap`. Without `--out`, prints a summary of files to be written.
228
156
 
229
- **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.
157
+ ### Show space tree
230
158
 
231
159
  ```bash
232
- # Inline expression
233
- sctx show <space> --filter "WHERE resolvedType='solution' and status='active'"
234
-
235
- # Named view from config
236
- sctx show <space> --filter active-solutions
160
+ sctx show <space> [--filter <view-or-expression>] [--hierarchy <name>]
237
161
  ```
238
162
 
239
- See [Filter expressions](#filter-expressions) below for expression syntax.
240
-
241
- ### Filter expressions
242
-
243
- Filter expressions are used with `--filter` and in config `views`. They use a `SELECT ... WHERE ...` pseudo-DSL:
244
-
245
- | Form | Meaning |
246
- |------|---------|
247
- | `WHERE {jsonata}` | Return nodes where the JSONata predicate is truthy |
248
- | `SELECT {spec} WHERE {jsonata}` | Filter by WHERE, then expand result via SELECT |
249
- | `SELECT {spec}` | Expand from all nodes via SELECT (no WHERE filter — returns all nodes, expanded per spec) |
250
- | `{jsonata}` | Bare JSONata, treated as a WHERE predicate (convenience shorthand) |
251
-
252
- 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:
253
-
254
- - **`ancestors[]`** — flat array of ancestor nodes, nearest first, deduplicated. Each entry includes all schema fields of the ancestor node, plus:
255
- - `_field` — the edge field name that connects to the ancestor
256
- - `_source` — `'hierarchy'` or `'relationship'`
257
- - `_selfRef` — whether the edge is a same-type (self-referential) link
258
- - **`descendants[]`** — same structure, for descendant nodes
259
-
260
- **SELECT spec** expands the result set by walking the graph from matched nodes. The spec is a comma-separated list of directives:
261
-
262
- | Directive | Meaning |
263
- |-----------|---------|
264
- | `ancestors` | All ancestor nodes |
265
- | `ancestors(type)` | Ancestors of the given resolved type |
266
- | `descendants` | All descendant nodes |
267
- | `descendants(type)` | Descendants of the given resolved type |
268
- | `siblings` | Nodes sharing at least one parent with matched nodes |
269
- | `relationships` | All nodes connected via a relationship (non-hierarchy) edge |
270
- | `relationships(childType)` | Relationship-connected nodes of the given child type |
271
- | `relationships(parentType:childType)` | As above, also filtering by parent type |
272
- | `relationships(parentType:field:childType)` | Fully qualified: also filtering by edge field name |
273
-
274
- Multiple directives may be combined: `SELECT ancestors(goal), siblings WHERE ...`
275
-
276
- **Examples:**
277
-
278
- ```jsonata
279
- // All solutions
280
- WHERE resolvedType='solution'
281
-
282
- // Active solutions only
283
- WHERE resolvedType='solution' and status='active'
284
-
285
- // Solutions whose nearest opportunity ancestor is active
286
- WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])
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.
287
164
 
288
- // Nodes that have any ancestor goal
289
- WHERE $exists(ancestors[resolvedType='goal'])
290
-
291
- // Bare JSONata shorthand (no WHERE keyword)
292
- resolvedType='solution' and status='active'
293
-
294
- // Solutions + their opportunity ancestors
295
- SELECT ancestors(opportunity) WHERE resolvedType='solution'
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.
296
166
 
297
- // Solutions + their siblings (other solutions under same opportunity)
298
- SELECT siblings WHERE resolvedType='solution' and status='active'
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.
299
168
 
300
- // Opportunities + their related assumptions
301
- SELECT relationships(assumption) WHERE resolvedType='opportunity'
169
+ ```bash
170
+ sctx show <space> --filter "WHERE resolvedType='solution' and status='active'"
171
+ sctx show <space> --filter "SELECT ancestors(opportunity) WHERE resolvedType='solution'"
172
+ sctx show <space> --filter active-solutions # named view
302
173
  ```
303
174
 
175
+ See [Filter expressions](docs/concepts.md#filter-expressions) (`sctx docs concepts`) for the full syntax.
176
+
304
177
  ### Generate Mermaid diagram
305
178
 
306
179
  ```bash
307
- sctx diagram <space> [--output path/to/output.mmd]
180
+ sctx diagram <space> [--output path/to/output.mmd] [--hierarchy <name>]
308
181
  ```
309
182
 
310
183
  Generates a Mermaid `graph TD` diagram from validated space nodes:
@@ -321,7 +194,7 @@ sctx schemas show <schema-file> [--mermaid-erd] [--space <name>]
321
194
 
322
195
  Generates a Mermaid Entity Relationship Diagram from a schema:
323
196
  - Shows all entity types and their properties
324
- - Displays parent-child relationships based on hierarchy metadata
197
+ - Displays parent-child relationships based on the levels of each hierarchy
325
198
  - Useful for visualizing schema structure during development
326
199
 
327
200
  Example:
@@ -335,28 +208,9 @@ sctx schemas show strategy_general --mermaid-erd
335
208
  sctx miro-sync <space> [--new-frame <title>] [--dry-run] [--verbose]
336
209
  ```
337
210
 
338
- 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.
339
-
340
- - `--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
341
- - `--dry-run` — show what would change without touching Miro
342
- - `--verbose` / `-v` — detailed per-card and per-connector output
343
-
344
- Configure the miro plugin in the space's config entry:
345
-
346
- ```json
347
- {
348
- "plugins": {
349
- "miro": {
350
- "boardId": "your-board-id",
351
- "frameId": "your-frame-id"
352
- }
353
- }
354
- }
355
- ```
356
-
357
- 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.
358
212
 
359
- 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.
360
214
 
361
215
  ### Sync templates with schema
362
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,16 +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, setConfigPath } from './config';
23
- export { PLUGIN_PREFIX } from './constants';
24
- export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
25
- export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
26
- export { readSpace } from './read/read-space';
27
- export type { SharedEmbeddingFields } from './schema/metadata-contract';
28
- export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
29
- export { createSpaceContext, SpaceNotFoundError } from './space-context';
30
- export type { SpaceGraph } from './space-graph';
31
- export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationResult, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types';
32
- export type { ValidationResult } from './validate';
33
- 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,10 +17,13 @@
17
17
  * const graph = await assembleSpaceGraph(ctx, { readResult });
18
18
  * const validation = await validateSpace(ctx, { readResult });
19
19
  */
20
- export { loadConfig, setConfigPath } from './config';
21
- export { PLUGIN_PREFIX } from './constants';
22
- export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
23
- export { readSpace } from './read/read-space';
24
- export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
25
- export { createSpaceContext, SpaceNotFoundError } from './space-context';
26
- 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';
@@ -2,4 +2,5 @@ import type { SpaceContext } from '../types';
2
2
  export declare function diagram(context: SpaceContext, options: {
3
3
  output?: string;
4
4
  filter?: string;
5
+ hierarchy?: string;
5
6
  }): Promise<void>;
@@ -1,7 +1,13 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { executeRender } from '../render/render';
3
3
  export async function diagram(context, options) {
4
- const result = await executeRender('mermaid.graph', context, { filter: options.filter });
4
+ const result = await executeRender('mermaid.graph', context, {
5
+ filter: options.filter,
6
+ hierarchy: options.hierarchy,
7
+ });
8
+ if (typeof result !== 'string') {
9
+ throw new Error('Expected string output from mermaid diagram renderer');
10
+ }
5
11
  if (options.output) {
6
12
  writeFileSync(options.output, result);
7
13
  console.log(`Mermaid diagram written to ${options.output}`);
@@ -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;
@@ -1,2 +1,15 @@
1
- import type { SpaceContext } from '../types';
2
- export declare function dump(context: SpaceContext): Promise<void>;
1
+ import type { ReadSpaceResult, SpaceContext } from '../types';
2
+ export interface DumpOptions {
3
+ /** Named view (resolved via space.views) or a raw filter DSL expression. */
4
+ filter?: string;
5
+ /** Hierarchy used when evaluating the filter (default: the main hierarchy). */
6
+ hierarchy?: string;
7
+ }
8
+ /**
9
+ * Build the dump payload: every parsed node, or only nodes matched by a filter.
10
+ *
11
+ * Unfiltered dumps include schema-invalid nodes, since dump is a debugging aid.
12
+ * Filtered dumps match show/render: the filter runs over the graph of schema-valid nodes.
13
+ */
14
+ export declare function buildDump(context: SpaceContext, options?: DumpOptions): Promise<ReadSpaceResult>;
15
+ export declare function dump(context: SpaceContext, options?: DumpOptions): Promise<void>;
@@ -1,6 +1,20 @@
1
- import JSON5 from 'json5';
1
+ import { assembleSpaceGraph } from '../load-space-graph';
2
2
  import { readSpace } from '../read/read-space';
3
- export async function dump(context) {
4
- const { nodes, source, diagnostics } = await readSpace(context);
5
- console.log(JSON5.stringify({ nodes, source, diagnostics }, null, 2));
3
+ /**
4
+ * Build the dump payload: every parsed node, or only nodes matched by a filter.
5
+ *
6
+ * Unfiltered dumps include schema-invalid nodes, since dump is a debugging aid.
7
+ * Filtered dumps match show/render: the filter runs over the graph of schema-valid nodes.
8
+ */
9
+ export async function buildDump(context, options = {}) {
10
+ const readResult = await readSpace(context);
11
+ if (!options.filter)
12
+ return readResult;
13
+ const graph = await assembleSpaceGraph(context, { ...options, readResult });
14
+ const nodes = readResult.nodes.filter((node) => graph.nodes.get(node.title) === node);
15
+ return { ...readResult, nodes };
16
+ }
17
+ export async function dump(context, options = {}) {
18
+ const { nodes, source, diagnostics } = await buildDump(context, options);
19
+ console.log(JSON.stringify({ nodes, source, diagnostics }, null, 2));
6
20
  }
@@ -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
+ }
@@ -1,6 +1,10 @@
1
1
  import type { SpaceContext } from '../types';
2
2
  export declare function render(context: SpaceContext, format: string, options: {
3
3
  filter?: string;
4
+ hierarchy?: string;
4
5
  output?: string;
6
+ out?: string;
7
+ force?: boolean;
8
+ data?: Record<string, unknown>;
5
9
  }): Promise<void>;
6
10
  export declare function renderList(context?: SpaceContext): Promise<void>;