structured-context 0.10.0 → 0.12.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 (107) hide show
  1. package/README.md +85 -25
  2. package/dist/api.d.ts +25 -5
  3. package/dist/api.js +22 -2
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/dump.d.ts +15 -2
  7. package/dist/commands/dump.js +18 -4
  8. package/dist/commands/miro-sync.d.ts +3 -0
  9. package/dist/commands/miro-sync.js +8 -0
  10. package/dist/commands/render.d.ts +4 -0
  11. package/dist/commands/render.js +64 -9
  12. package/dist/commands/schemas.d.ts +2 -2
  13. package/dist/commands/schemas.js +117 -24
  14. package/dist/commands/show.d.ts +1 -0
  15. package/dist/commands/show.js +7 -1
  16. package/dist/commands/spaces.d.ts +3 -1
  17. package/dist/commands/spaces.js +11 -4
  18. package/dist/commands/validate-file.js +20 -5
  19. package/dist/commands/validate.js +54 -21
  20. package/dist/config.d.ts +26 -13
  21. package/dist/config.js +104 -31
  22. package/dist/filter/augment-nodes.d.ts +3 -1
  23. package/dist/filter/augment-nodes.js +12 -7
  24. package/dist/filter/filter-nodes.js +1 -1
  25. package/dist/index.js +51 -35
  26. package/dist/load-space-graph.d.ts +42 -0
  27. package/dist/load-space-graph.js +53 -0
  28. package/dist/plugins/index.js +3 -1
  29. package/dist/plugins/loader.js +4 -4
  30. package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
  31. package/dist/plugins/markdown/extract-content-links.js +123 -0
  32. package/dist/plugins/markdown/index.js +5 -5
  33. package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
  34. package/dist/plugins/markdown/parse-embedded.js +50 -7
  35. package/dist/plugins/markdown/read-space.js +41 -5
  36. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  37. package/dist/plugins/markdown/render-directory.js +130 -0
  38. package/dist/plugins/mermaid/index.d.ts +2 -0
  39. package/dist/plugins/mermaid/index.js +14 -0
  40. package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -15
  41. package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
  42. package/dist/plugins/miro/index.d.ts +2 -0
  43. package/dist/plugins/miro/index.js +27 -0
  44. package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
  45. package/dist/{integrations → plugins}/miro/layout.js +3 -16
  46. package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
  47. package/dist/plugins/miro/sync.d.ts +7 -0
  48. package/dist/{integrations → plugins}/miro/sync.js +22 -39
  49. package/dist/plugins/util.d.ts +48 -1
  50. package/dist/plugins/util.js +143 -0
  51. package/dist/read/resolve-graph-edges.d.ts +1 -1
  52. package/dist/read/resolve-graph-edges.js +78 -27
  53. package/dist/read/wikilink-utils.d.ts +17 -0
  54. package/dist/read/wikilink-utils.js +40 -0
  55. package/dist/render/render.d.ts +4 -1
  56. package/dist/render/render.js +20 -16
  57. package/dist/schema/evaluate-rule.js +1 -0
  58. package/dist/schema/meta-schemas.d.ts +13 -0
  59. package/dist/schema/meta-schemas.js +50 -0
  60. package/dist/schema/metadata-contract.d.ts +343 -187
  61. package/dist/schema/metadata-contract.js +68 -30
  62. package/dist/schema/schema.d.ts +17 -9
  63. package/dist/schema/schema.js +283 -200
  64. package/dist/schema/validate-graph.d.ts +2 -1
  65. package/dist/schema/validate-graph.js +40 -23
  66. package/dist/schema/validate-rules.d.ts +5 -1
  67. package/dist/schema/validate-rules.js +20 -1
  68. package/dist/schemas/_knowledge_wiki.json +21 -21
  69. package/dist/schemas/_ost_strict.json +2 -2
  70. package/dist/schemas/_sctx_base.json +2 -2
  71. package/dist/schemas/_strategy_general.json +38 -38
  72. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  73. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  74. package/dist/schemas/knowledge_wiki.json +10 -9
  75. package/dist/schemas/strategy_general.json +15 -14
  76. package/dist/schemas/strict_ost.json +17 -16
  77. package/dist/space-context.d.ts +27 -0
  78. package/dist/space-context.js +46 -0
  79. package/dist/space-graph.d.ts +9 -7
  80. package/dist/space-graph.js +7 -5
  81. package/dist/testing.d.ts +33 -0
  82. package/dist/testing.js +72 -0
  83. package/dist/types.d.ts +101 -8
  84. package/dist/validate.d.ts +15 -3
  85. package/dist/validate.js +55 -20
  86. package/docs/concepts.md +37 -4
  87. package/docs/config.md +10 -0
  88. package/docs/rules.md +49 -22
  89. package/docs/schemas.md +110 -35
  90. package/package.json +13 -8
  91. package/schemas/_knowledge_wiki.json +21 -21
  92. package/schemas/_ost_strict.json +2 -2
  93. package/schemas/_sctx_base.json +2 -2
  94. package/schemas/_strategy_general.json +38 -38
  95. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  96. package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  97. package/schemas/knowledge_wiki.json +10 -9
  98. package/schemas/strategy_general.json +15 -14
  99. package/schemas/strict_ost.json +17 -16
  100. package/dist/integrations/miro/sync.d.ts +0 -8
  101. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  102. package/schemas/generated/_structured_context_schema_meta.json +0 -191
  103. /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
  104. /package/dist/{integrations → plugins}/miro/cache.js +0 -0
  105. /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
  106. /package/dist/{integrations → plugins}/miro/client.js +0 -0
  107. /package/dist/{integrations → plugins}/miro/styles.js +0 -0
package/docs/concepts.md CHANGED
@@ -93,6 +93,8 @@ A **space node** (or **node** for short) is a single entity in a `space` — a n
93
93
 
94
94
  Node types are defined by the schema in use and may vary across schemas. Examples from the default schema: `vision`, `mission`, `goal`, `opportunity`, `solution`. The tooling is not prescriptive about which types exist — schemas are designed to be extended and replaced.
95
95
 
96
+ Each node retains its physical `source` location (`file`, and `line` / `column` where available) so that validation diagnostics, edge errors, and downstream tooling pinpoint the exact location in the vault.
97
+
96
98
  > `space_on_a_page` and `dashboard` are not `space node` types — they are `tooling types`.
97
99
 
98
100
  ### Embedded node
@@ -111,7 +113,7 @@ A `space node`'s resolved type (`resolvedType`) is its canonical type after alia
111
113
 
112
114
  ## Typed page
113
115
 
114
- A **typed page** is a markdown file whose frontmatter declares a `space node` type (e.g. `type: goal`). The file itself represents one node, and its body may additionally contain `embedded nodes`.
116
+ A **typed page** is a markdown file whose frontmatter declares a `space node` type (e.g. `type: goal`). The file itself represents one node, and its body may additionally contain `embedded nodes`. Body content appearing before any embedded headings (or the entire body when no embedded headings are present) is preserved as the page node's `content`.
115
117
 
116
118
  Typed pages are distinct from `space on a page` files: a typed page *is* a `space node`; a `space_on_a_page` file is merely a container.
117
119
 
@@ -123,7 +125,7 @@ A **schema** defines the valid structure for nodes in a `space`: the fields, typ
123
125
 
124
126
  The schema handles structural validation. Cross-node and workflow checks are handled by executable `rules` defined in `$metadata.rules`.
125
127
 
126
- Schemas are composable: structural definitions and metadata can be sourced across `$ref` graphs, then merged deterministically (root metadata applied last, single hierarchy provider, aliases merged, rules merged by `id` with explicit override semantics).
128
+ Schemas are composable. Structural definitions are reused with `$ref` (validation only). Metadata is reused only by listing schema `$id`s in `$metadata.imports`: imports merge transitively, in order, with the schema's own metadata last. Hierarchies, relationships, aliases and rules all follow one rule — a duplicate is an error unless the later entry sets `override: true` — and each declared hierarchy is kept as a separate named hierarchy.
127
129
 
128
130
  ### Rules
129
131
 
@@ -141,6 +143,8 @@ Rules may be:
141
143
 
142
144
  Rules are distinct from schema validation: the schema checks structure; rules check meaning and quality.
143
145
 
146
+ Each executable rule has a **category** (`validation`, `coherence`, `workflow`, `best-practice`) that supplies its default **severity** (`error`, `warning`, `info`). A rule may override its category's severity, but categories are the preferred grouping. Only `error` severity violations fail validation.
147
+
144
148
  See [docs/rules.md](rules.md) for the rules reference, including JSONata expression syntax and the full `$metadata` field reference.
145
149
 
146
150
  ---
@@ -160,6 +164,10 @@ The **hierarchy** is the ordered list of node types in a space, from root to lea
160
164
 
161
165
  The hierarchy is modelled as a layered DAG: a non-root node may have zero parents (orphaned), one parent, or multiple parents. The `show` command renders this as an indented tree, marking repeated nodes with `(*)` where the subtree is already shown elsewhere.
162
166
 
167
+ A space may have **several hierarchies**, one for each imported schema that declares one. Each hierarchy has a **name** (defaulting to the declaring schema's file stem), and each type belongs to at most one hierarchy. Structural validation runs per hierarchy; relationships may connect types across hierarchies.
168
+
169
+ The **main hierarchy** is the one declared by the root schema; failing that, the only imported hierarchy; failing that, the one the root selects with `"hierarchy": { "$ref": ... }`. The main hierarchy drives `space on a page` parsing, hierarchy embedding, template sync, and default rendering. `show`, `diagram` and `render` take `--hierarchy <name>` to render another. See [docs/schemas.md](schemas.md#multiple-hierarchies).
170
+
163
171
  Each non-root level uses the shared `field`, `fieldOn`, and `multiple` edge options (see [Graph edges](#graph-edges)). Hierarchy-specific options:
164
172
 
165
173
  | Option | Default | Meaning |
@@ -250,9 +258,33 @@ Each entry is a `ResolvedParentRef` object:
250
258
  | `title` | `string` | The parent node's title |
251
259
  | `field` | `string` | The frontmatter field that held the wikilink |
252
260
  | `source` | `'hierarchy' \| 'relationship'` | Whether the edge came from a hierarchy level or a relationship |
261
+ | `hierarchy` | `string` | Name of the hierarchy the edge belongs to; present only when `source` is `'hierarchy'` |
253
262
  | `selfRef` | `boolean` | Whether the edge is a same-type (self-referential) parent link |
254
263
 
255
- The `source` label lets downstream consumers distinguish edge types without re-inspecting the schema. Validation routes `hierarchy` edges to structural checks (parent-type rules, skip-level detection) and `relationship` edges to field reference checks (type-match, missing-target). Tree rendering and rule evaluation use the full set.
264
+ The `source` and `hierarchy` labels let downstream consumers distinguish edge types and hierarchies without re-inspecting the schema. Validation routes `hierarchy` edges to structural checks within their own hierarchy (parent-type rules, skip-level detection) and `relationship` edges to field reference checks (type-match, missing-target). Tree rendering and rule evaluation use the full set.
265
+
266
+ ### Content links
267
+
268
+ **Content links** (`resolvedLinks` on `SpaceNode`) are all links extracted from a node's content at parse time — both from frontmatter non-edge fields and from the body text. They are distinct from graph edges: they are never used to infer structure, but are available to consumers for rendering, auditing, or navigation.
269
+
270
+ Each entry is a `ResolvedContentLink` object:
271
+
272
+ | Field | Type | Description |
273
+ |---|---|---|
274
+ | `text` | `string` | Display text of the link |
275
+ | `target` | `string` | Raw link target (URL or wikilink path) |
276
+ | `action` | `'link' \| 'embed'` | Whether the link navigates or transcludes |
277
+ | `anchor` | `string?` | Fragment component (heading or block ref) if present |
278
+ | `location` | `'node' \| 'internal' \| 'external' \| 'system' \| 'protocol'` | Resolved location classification |
279
+
280
+ **Location classification:**
281
+ - `node` — wikilink resolved to a known space node
282
+ - `internal` — wikilink or relative path to an in-source target that is not a space node
283
+ - `external` — `http://` or `https://` URL
284
+ - `system` — `file://` URL
285
+ - `protocol` — any other scheme (e.g. `obsidian://`)
286
+
287
+ **Sources:** Both wikilink syntax (`[[Target]]`, `![[Image]]`) and markdown link syntax (`[text](url)`, `![alt](url)`) are extracted. Bare URLs in frontmatter string fields are also captured. Graph edge fields (hierarchy `parent`, relationship fields) are excluded — those are graph edges, not content links.
256
288
 
257
289
  ### Anchor
258
290
 
@@ -292,7 +324,8 @@ Each entry in `ancestors[]` or `descendants[]` includes all schema fields of the
292
324
  | Metadata field | Type | Description |
293
325
  |----------------|------|-------------|
294
326
  | `_field` | `string` | The edge field name that connects to this ancestor/descendant |
295
- | `_source` | `'hierarchy' \| 'relationship'` | Whether the edge came from the hierarchy or a relationship |
327
+ | `_source` | `'hierarchy' \| 'relationship'` | Whether the edge came from a hierarchy or a relationship |
328
+ | `_hierarchy` | `string` | Hierarchy name; present only for hierarchy edges |
296
329
  | `_selfRef` | `boolean` | Whether the edge is a same-type (self-referential) link |
297
330
 
298
331
  ### SELECT spec
package/docs/config.md CHANGED
@@ -26,8 +26,16 @@ A space is a named directory or single file registered in the config. Example:
26
26
  }
27
27
  ```
28
28
 
29
+ **`schema`** — the schema used to validate nodes in the space. Can be:
30
+ - A bundled schema name (e.g. `strategy_general.json`, `strict_ost.json`)
31
+ - A local path relative to the config file (e.g. `./schemas/custom.json`) or absolute path
32
+ - 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.
33
+ - A simple filename (e.g. `wardley_map.json`) provided by a plugin explicitly declared under `plugins`.
34
+
29
35
  **`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
36
 
37
+ A config must define at least one of `spaces` or `includeSpacesFrom`. When using `includeSpacesFrom` alone, `spaces` can be omitted entirely.
38
+
31
39
  ## Plugins
32
40
 
33
41
  Use `plugins` to load parse plugins that read spaces from non-markdown sources. The built-in markdown plugin is always available without any declaration. Plugins are tried in order; the first to return a result wins. The `plugins` field is a map of plugin name to plugin config, and can be declared at the top level (applies to all spaces) or per-space (overrides the top level):
@@ -51,6 +59,8 @@ Use `plugins` to load parse plugins that read spaces from non-markdown sources.
51
59
 
52
60
  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.
53
61
 
62
+ Plugins can also contribute schemas and partials via a `<pluginRoot>/schemas/` directory (or `"sctx": { "schemas": "..." }` in `package.json`). If a space specifies a plugin-prefixed schema (e.g. `"sctx-wardley-mapping/wardley_map.json"`), schemas are resolved statically without executing plugin code, and the plugin's code hooks are loaded when parsing or rendering.
63
+
54
64
  ## Markdown plugin config
55
65
 
56
66
  Set under `plugins.markdown` per space.
package/docs/rules.md CHANGED
@@ -24,22 +24,56 @@ Rules are a flat array.
24
24
  |---|---|---|
25
25
  | `id` | yes | Unique rule identifier |
26
26
  | `category` | yes | `validation` \| `coherence` \| `workflow` \| `best-practice` |
27
+ | `severity` | no | `error` \| `warning` \| `info`; overrides the category default (see [Severity](#severity)) |
27
28
  | `description` | yes | Human-readable rule intent |
28
29
  | `check` | yes | JSONata expression; must evaluate to `true` |
29
30
  | `type` | no | Restrict rule to nodes of this `resolvedType` |
30
31
  | `scope` | no | Use `"global"` to evaluate once for the whole space |
31
32
  | `override` | no | Only for merge conflicts: allows later duplicate `id` to replace earlier |
32
33
 
33
- ## Categories
34
+ ## Categories and severity
34
35
 
35
- Categories label violations for reporting. They do not change expression execution semantics.
36
+ Categories describe a rule's intent and supply its default severity. They do not change expression execution semantics.
36
37
 
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 |
38
+ | Category | Typical use | Default severity |
39
+ |---|---|---|
40
+ | `validation` | Hard correctness constraints | `error` |
41
+ | `coherence` | Cross-node consistency checks | `warning` |
42
+ | `workflow` | Process/operating-discipline checks | `warning` |
43
+ | `best-practice` | Advisory quality checks | `info` |
44
+
45
+ ### Severity
46
+
47
+ Every rule violation is reported with a severity:
48
+
49
+ | Severity | Meaning | Fails validation |
50
+ |---|---|---|
51
+ | `error` | Content is wrong | yes |
52
+ | `warning` | Content is likely inconsistent or incomplete | no |
53
+ | `info` | Content could be better | no |
54
+
55
+ 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.
56
+
57
+ 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:
58
+
59
+ ```json5
60
+ {
61
+ "id": "goal-not-blocked",
62
+ "category": "workflow",
63
+ "severity": "error",
64
+ "description": "Goals must not be left blocked",
65
+ "type": "goal",
66
+ "check": "current.status != 'blocked'"
67
+ }
68
+ ```
69
+
70
+ `sctx schemas show` lists each category with its default severity and marks rules that override it.
71
+
72
+ ### Output
73
+
74
+ - **`sctx validate`** — the summary counts rule violations per severity, and details are grouped by severity with each line showing the rule's category and id.
75
+ - **`sctx validate --json`** — `errors` includes only `error` severity rule violations (keyed `rule:<id>`, with a `severity` field). A `ruleViolations` array lists every violation with `file`, `ruleId`, `category`, `severity` and `description` (`file` is empty for global rules).
76
+ - **`sctx validate-file`** — `error` severity violations appear under `errors`; `warning` and `info` violations appear under `warnings`. Rule entries carry a `severity` field.
43
77
 
44
78
  ## Evaluation model
45
79
 
@@ -73,28 +107,21 @@ Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to refere
73
107
  $count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution'])
74
108
  ```
75
109
 
76
- ## Rule imports (`$ref`)
110
+ ## Reusing rules
77
111
 
78
- `$metadata.rules` can include `$ref` entries that import:
79
- - one rule
80
- - a rule-set object with `rules: []`
81
-
82
- Example:
112
+ To share rules, put them in a schema's `$metadata.rules` and list that schema's `$id` in `$metadata.imports`. An import brings the whole schema's metadata, including its own imports:
83
113
 
84
114
  ```json5
85
- "rules": [
86
- { "$ref": "sctx://rule-pack#/$defs/workflowRule" },
87
- { "$ref": "sctx://rule-pack#/$defs/coreRuleSet" }
88
- ]
115
+ "$metadata": {
116
+ "imports": ["sctx://_rule-pack"]
117
+ }
89
118
  ```
90
119
 
91
- Imported entries are normalized into the same flat runtime list.
92
-
93
120
  ## Merge and conflict behavior
94
121
 
95
- When metadata is composed across `$ref`:
122
+ When metadata is merged from imports (imports in order, then the schema's own metadata):
96
123
  - Rules are merged by `id`.
97
- - Different payloads for the same `id` are an error by default.
124
+ - A duplicate `id` is an error.
98
125
  - A later rule may replace an earlier one only with `override: true`.
99
126
 
100
127
  Example override:
package/docs/schemas.md CHANGED
@@ -6,7 +6,7 @@ This document explains schema usage, metadata shape, and composition semantics i
6
6
 
7
7
  A **schema** defines the valid structure for nodes in a `space`: entity types, field constraints, hierarchy behavior, type aliases, and executable rules.
8
8
 
9
- `structured-context` uses JSON Schema Draft-07 plus a custom top-level `$metadata` keyword.
9
+ `structured-context` uses JSON Schema 2020-12 plus a custom top-level `$metadata` keyword.
10
10
 
11
11
  ## Selecting a schema
12
12
 
@@ -20,6 +20,11 @@ Set `schema` in the space config entry:
20
20
  }
21
21
  ```
22
22
 
23
+ A schema can be specified as:
24
+ - **Bundled schema**: simple filename like `strategy_general.json` or `strict_ost.json`.
25
+ - **Local file**: relative path starting with `./` or `../` (resolved against config file directory), or absolute path.
26
+ - **Plugin schema**: prefixed with the plugin name, e.g. `sctx-wardley-mapping/wardley_map.json` or `wardley-mapping/wardley_map.json`. When prefixed, structured-context automatically loads the plugin for that space. Alternatively, if the plugin is already declared under `plugins`, the simple filename (e.g. `wardley_map.json`) resolves directly from that plugin.
27
+
23
28
  Resolution order: space `schema` > global `schema`. Schema resolution fails if none is configured.
24
29
 
25
30
  ## Bundled schemas
@@ -46,7 +51,7 @@ Main types:
46
51
  - `solution`
47
52
  - `assumption_test`
48
53
 
49
- This schema composes shared structural defs and strict metadata/rules from partials.
54
+ This schema reuses shared structural defs from partials via `$ref` and imports its hierarchy and rules from `_ost_strict` via `$metadata.imports`.
50
55
 
51
56
  ## Custom format annotations
52
57
 
@@ -61,7 +66,7 @@ This schema composes shared structural defs and strict metadata/rules from parti
61
66
  `path` and `wikilink` are also available as shared `$ref` definitions in `_sctx_base.json`:
62
67
 
63
68
  ```json
64
- { "$ref": "sctx://_sctx_base#/$defs/wikilink" }
69
+ { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" }
65
70
  ```
66
71
 
67
72
  Using `format` directly is more concise when the full definition isn't needed:
@@ -82,9 +87,12 @@ The markdown plugin automatically coerces `Date` objects to `YYYY-MM-DD` strings
82
87
 
83
88
  ## Metadata dialect
84
89
 
85
- Schemas use this metaschema URL:
90
+ The dialect is JSON Schema 2020-12 plus the top-level `$metadata` keyword. Schemas set `$schema` to the metaschema URL:
91
+
92
+ - `https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/latest/_structured_context_schema_meta.json` follows the newest dialect.
93
+ - `https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/<version>/_structured_context_schema_meta.json` pins the dialect to the structured-context release that first shipped it. Versioned files never change once released. Releases that don't change the dialect add no folder, so a version number only appears when the dialect changed.
86
94
 
87
- - `https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json`
95
+ Because the dialect is 2020-12, keywords may sit beside `$ref`, and `unevaluatedProperties: false` can close an object composed through `allOf`.
88
96
 
89
97
  Top-level metadata shape:
90
98
 
@@ -120,11 +128,14 @@ Top-level metadata shape:
120
128
 
121
129
  | Field | Type | Notes |
122
130
  |---|---|---|
123
- | `hierarchy` | object | Optional per provider; at most one provider may define it after composition |
131
+ | `imports` | `string[]` | Optional; `$id`s of schemas whose metadata is merged into this one. See [Composition and merge semantics](#composition-and-merge-semantics) |
132
+ | `hierarchy` | object | Optional; declares this schema's hierarchy, or (root schema only) selects the main hierarchy with `{ "$ref": "<schema $id>#/$metadata/hierarchy" }`. See [Multiple hierarchies](#multiple-hierarchies) |
133
+ | `hierarchy.name` | `string` | Optional short name; defaults to the declaring schema's file stem |
134
+ | `hierarchy.override` | `boolean` | Optional; replaces an imported hierarchy with the same name |
124
135
  | `hierarchy.levels` | `(string \| HierarchyLevel)[]` | Ordered root→leaf types |
125
136
  | `hierarchy.allowSkipLevels` | `boolean` | Optional; allows parent to be any ancestor level |
126
- | `relationships` | `Relationship[]` | Optional; defines related node links outside the primary hierarchy |
127
- | `aliases` | `Record<string, string>` | Optional type alias map |
137
+ | `relationships` | `Relationship[]` | Optional; defines related node links outside the primary hierarchy. A relationship may set `override: true` |
138
+ | `aliases` | `Record<string, string \| { type, override }>` | Optional type alias map. Use `{ "type": "goal", "override": true }` to replace an imported alias |
128
139
  | `rules` | `Rule[]` | Optional flat rule array |
129
140
 
130
141
  ### Relationships
@@ -252,37 +263,75 @@ With this config, an application page may include `### capability` with wikilink
252
263
 
253
264
  ## Composition and merge semantics
254
265
 
255
- Metadata is composed across the `$ref` graph with deterministic behavior:
266
+ `$ref` and `$defs` are for **validation only**: a `$ref` to another schema (or to one of its `$defs`) reuses its JSON Schema definitions but brings none of its `$metadata`.
267
+
268
+ Metadata travels only through **`$metadata.imports`**, a list of schema `$id`s:
269
+
270
+ ```json5
271
+ "$metadata": {
272
+ "imports": ["sctx://core/_strategy_general"],
273
+ "rules": [ /* this schema's own rules */ ]
274
+ }
275
+ ```
256
276
 
257
- 1. Traverse external `$ref` graph in DFS order.
258
- 2. Apply root schema metadata last.
277
+ - An import brings the imported schema's whole `$metadata` (hierarchy, relationships, aliases, rules).
278
+ - Imports are transitive: an imported schema's own imports come too.
279
+ - Merge order: imports in the order listed, depth-first, then the schema's own metadata last. Each schema is merged at most once.
280
+ - An unresolvable import `$id` is an error.
259
281
 
260
- Merge rules:
261
- - `hierarchy`: may be defined in partials; **last one wins**. This allows partials to define a default hierarchy that composing schemas can override.
262
- - `aliases`: shallow merged; later file wins per key.
263
- - `relationships`: collected from all files; order preserved.
264
- - `rules`: merged by `id`.
265
- - Duplicate rule `id` with different payload errors by default.
266
- - A later rule may replace an earlier one only with `"override": true`.
282
+ One merge rule applies to every key. **A duplicate is an error unless the later entry sets `"override": true`**, in which case it replaces the earlier entry.
283
+
284
+ | Key | Identity | How to mark `override` |
285
+ |---|---|---|
286
+ | `rules` | `id` | `"override": true` on the rule |
287
+ | `relationships` | `parent` + `type` + `field` (default `"parent"`) | `"override": true` on the relationship |
288
+ | `aliases` | alias name | `{ "type": "<target>", "override": true }` as the alias value |
289
+ | `hierarchy` | `name` | `"override": true` on the hierarchy declaration |
290
+
291
+ Overriding a hierarchy by name is how a schema replaces the levels of an imported hierarchy (declare it with the same `name` and `"override": true`).
267
292
 
268
293
  When no provider defines `hierarchy`, hierarchy-based behavior is disabled (`show` tree shape, hierarchy validation, parent-edge checks). `space_on_a_page` parsing still requires hierarchy and will error without it.
269
294
 
270
- ### Rule imports via `$ref`
295
+ ### Multiple hierarchies
271
296
 
272
- Inside `$metadata.rules`, entries can be inline rules or `$ref` imports:
297
+ A space has several hierarchies when its schema imports several schemas that each declare one. Each schema declares at most one hierarchy.
298
+
299
+ - **Every imported schema contributes its hierarchy.** Each is kept as a separate named hierarchy; levels are never merged.
300
+ - **Naming.** `hierarchy.name` defaults to the declaring schema's file stem (`_ost_strict` for `_ost_strict.json`). A duplicate name is an error unless the later hierarchy sets `"override": true`.
301
+ - **Main hierarchy.**
302
+ - If the root schema declares a hierarchy, it is the main hierarchy.
303
+ - If the root declares none and exactly one hierarchy is imported, that one is the main hierarchy (for example `strict_ost`, which imports `_ost_strict`).
304
+ - If the root declares none and several are imported, the root must select one with `"hierarchy": { "$ref": "<schema $id>#/$metadata/hierarchy" }`, otherwise loading the schema fails.
305
+ - A root that both declares a hierarchy and uses `$ref` is an error.
306
+ - **Each type belongs to at most one hierarchy.** A type claimed by two hierarchies is an error. To change an imported hierarchy's levels, override it by name.
307
+ - **Selections are not imported.** A `hierarchy: { "$ref" }` selection only applies when its schema is the root, so an imported schema stays usable standalone as another space's root.
273
308
 
274
309
  ```json5
275
- "rules": [
276
- { "$ref": "sctx://my-pack#/$defs/workflowRule" },
277
- { "$ref": "sctx://my-pack#/$defs/ruleSet" }
278
- ]
310
+ {
311
+ "$id": "sctx://team",
312
+ "oneOf": [
313
+ { "$ref": "sctx://_work#/$defs/goal" },
314
+ { "$ref": "sctx://_work#/$defs/task" },
315
+ { "$ref": "sctx://_skills#/$defs/skill_area" },
316
+ { "$ref": "sctx://_skills#/$defs/skill" }
317
+ ],
318
+ "$metadata": {
319
+ // _work.json and _skills.json each declare a hierarchy; select the main one.
320
+ "imports": ["sctx://_work", "sctx://_skills"],
321
+ "hierarchy": { "$ref": "sctx://_work#/$metadata/hierarchy" }
322
+ }
323
+ }
279
324
  ```
280
325
 
281
- Import targets may be:
282
- - a single rule object
283
- - an object containing `rules: []`
326
+ How hierarchies are used:
284
327
 
285
- Imported rules are normalized into one executable flat list before validation.
328
+ | Behaviour | Hierarchies used |
329
+ |---|---|
330
+ | Layering, parent-type, skip-level and orphan checks | Each hierarchy, independently. A hierarchy edge whose parent belongs to another hierarchy is an invalid parent. |
331
+ | Relationships | Connect types across hierarchies, as before |
332
+ | `space_on_a_page` parsing, hierarchy embedding, `template-sync` | Main hierarchy |
333
+ | `show`, `diagram`, `render` | Main hierarchy by default; `--hierarchy <name>` selects another |
334
+ | `resolvedParents` | Hierarchy edges carry the `hierarchy` name |
286
335
 
287
336
  ### Override example
288
337
 
@@ -303,22 +352,48 @@ Imported rules are normalized into one executable flat list before validation.
303
352
  }
304
353
  ```
305
354
 
355
+ ## Schema `$id` namespaces
356
+
357
+ Every schema `$id` uses the `sctx://` scheme. The shape of the `$id` says where the schema comes from:
358
+
359
+ | `$id` shape | Source | Example |
360
+ |---|---|---|
361
+ | `sctx://core/<name>` | Bundled with structured-context | `sctx://core/_sctx_base` |
362
+ | `sctx://<plugin>/<name>` | Contributed by a plugin (its short name, without `sctx-`) | `sctx://wardley-mapping/_wardley` |
363
+ | `sctx://<name>` | Local to a space's schema directory — a single name, no path | `sctx://team` |
364
+
365
+ - Two schemas loaded for the same space may not share an `$id`; a collision is an error naming both sources.
366
+ - `core` is reserved: no plugin may use it as its namespace.
367
+ - **Legacy bundled ids:** before 1.0, bundled schemas used `sctx://<name>` (e.g. `sctx://_sctx_base`, `sctx://strategy_general`). References to those ids in `$ref` and `$metadata.imports` still resolve to their `sctx://core/` equivalents with a deprecation warning, and local schemas may not use them as their own `$id`. The aliases will be removed in 1.0.
368
+
306
369
  ## Partials and `$ref`
307
370
 
308
371
  - Files starting with `_` are auto-loaded partials.
309
- - Both bundled partials and local schema-directory partials are registered.
310
- - Local partial `$id` values must not collide with bundled IDs.
372
+ - Bundled partials, local schema-directory partials, and partials from loaded plugins are all registered.
373
+ - `$id` values follow the [namespace rules](#schema-id-namespaces) and must not collide across bundled, plugin and local schemas.
311
374
  - `$ref` resolution is transitive across files.
312
- - Partials with no `$metadata` should prefer `$schema: "http://json-schema.org/draft-07/schema#"` so they validate standalone as plain JSON Schema fragments.
313
- - **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference these via `$ref` rather than redefining common entity types.
314
- - **Partials can carry metadata**: Partials may include `$metadata` (hierarchy, aliases, relationships, rules). This makes them self-contained units that bundle both type definitions and behavioral metadata.
375
+ - Partials with no `$metadata` should prefer `$schema: "https://json-schema.org/draft/2020-12/schema"` so they validate standalone as plain JSON Schema fragments.
376
+ - **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference their entity types via `$ref` rather than redefining them.
377
+ - **Partials can carry metadata**: Partials may include `$metadata` (hierarchy, aliases, relationships, rules). A schema gets it only by listing the partial in `$metadata.imports`; `$ref` alone brings none.
378
+
379
+ ## Plugin-contributed schemas
380
+
381
+ Plugins contribute schemas and partials by standard directory convention: placing schema files in `<pluginRoot>/schemas/` (or declaring `"sctx": { "schemas": "./path" }` in `package.json`).
382
+
383
+ Because schemas are resolved statically by convention from the filesystem, structured-context does not need to execute plugin JavaScript code during schema validation, context creation, or schema inspection.
384
+
385
+ When a space uses a plugin schema:
386
+ - **Registry registration**: All partials (`_*`) in the plugin's `schemas/` directory are automatically indexed in the schema registry.
387
+ - **Namespace conventions**: Schemas contributed by a plugin must use `sctx://<pluginName>/...` for their `$id` (e.g. `sctx://wardley-mapping/_wardley`), where the prefix is the plugin's short name (without the `sctx-` prefix). See [Schema `$id` namespaces](#schema-id-namespaces).
388
+ - **Collision protection**: If two plugins register schemas with the same `$id`, or a plugin or local schema collides with a bundled schema, schema loading throws an error.
389
+ - **Imports and $ref**: Plugin schemas can `$ref` or `$metadata.imports` bundled schemas (e.g. `sctx://core/_sctx_base`) as well as other partials within their own namespace. Schema composition rules from #122 apply.
315
390
 
316
391
  ## Editor expectations
317
392
 
318
- Use the shipped metaschema URL in `$schema` for best cross-tool behavior.
393
+ Use the shipped metaschema URL in `$schema` for best cross-tool behavior; pin a `<version>` URL if you need the dialect to stay fixed.
319
394
 
320
395
  Notes:
321
- - Custom `$id` values like `sctx://...` are still supported by the CLI registry.
396
+ - `sctx://` `$id`s are resolved by the CLI registry (see [Schema `$id` namespaces](#schema-id-namespaces)).
322
397
  - Some generic editors may not resolve custom URI schemes for `$ref`; CLI behavior is authoritative.
323
398
  - Do not rely on editor-only mappings for runtime correctness.
324
399
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "structured-context",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Keep your content clean and your thinking clear with assistive tools for structured knowledge",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -25,13 +25,17 @@
25
25
  "./types": {
26
26
  "types": "./dist/types.d.ts",
27
27
  "default": "./dist/types.js"
28
+ },
29
+ "./testing": {
30
+ "types": "./dist/testing.d.ts",
31
+ "default": "./dist/testing.js"
28
32
  }
29
33
  },
30
34
  "scripts": {
31
35
  "clean": "rm -rf dist",
32
36
  "generate:schema-meta": "bun run scripts/generate-schema-meta.ts",
33
37
  "typecheck": "tsc --noEmit",
34
- "build": "bun run clean && bun run generate:schema-meta && tsc -p tsconfig.build.json && cp -r schemas dist/ && chmod +x dist/index.js",
38
+ "build": "bun run clean && bun run generate:schema-meta && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r schemas dist/ && chmod +x dist/index.js",
35
39
  "prepublishOnly": "bun run build",
36
40
  "validate": "bun run src/index.ts validate",
37
41
  "diagram": "bun run src/index.ts diagram",
@@ -43,29 +47,30 @@
43
47
  "lint": "biome check",
44
48
  "lint:fix": "biome check --write",
45
49
  "preversion": "bun run generate:schema-meta && bun run lint:fix && bun run test",
46
- "version": "git add -A",
50
+ "version": "bun run generate:schema-meta --release && git add -A",
47
51
  "postversion": "git push --follow-tags"
48
52
  },
49
53
  "devDependencies": {
50
- "@anthropic-ai/claude-agent-sdk": "^0.2.92",
51
- "@biomejs/biome": "^2.4.10",
54
+ "@anthropic-ai/claude-agent-sdk": "^0.3.191",
55
+ "@biomejs/biome": "^2.5.1",
52
56
  "@types/bun": "latest",
53
57
  "@types/js-yaml": "^4.0.9",
54
58
  "json-schema-to-ts": "^3.1.1",
55
- "lefthook": "^2.1.4"
59
+ "lefthook": "^2.1.9",
60
+ "tsc-alias": "^1.9.1"
56
61
  },
57
62
  "peerDependencies": {
58
63
  "typescript": "^5"
59
64
  },
60
65
  "dependencies": {
61
- "ajv": "^8.18.0",
66
+ "ajv": "^8.20.0",
62
67
  "chokidar": "^5.0.0",
63
68
  "commander": "^14.0.3",
64
69
  "fast-glob": "^3.3.3",
65
70
  "gray-matter": "^4.0.3",
66
71
  "js-yaml": "^4.1.1",
67
72
  "json5": "^2.2.3",
68
- "jsonata": "^2.1.0",
73
+ "jsonata": "^2.2.1",
69
74
  "mdast-util-to-string": "^4.0.0",
70
75
  "remark-gfm": "^4.0.1",
71
76
  "remark-parse": "^11.0.0",