structured-context 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +52 -198
  2. package/dist/api.d.ts +17 -15
  3. package/dist/api.js +12 -9
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/docs.js +1 -0
  7. package/dist/commands/dump.d.ts +15 -2
  8. package/dist/commands/dump.js +18 -4
  9. package/dist/commands/providers.d.ts +4 -0
  10. package/dist/commands/providers.js +52 -0
  11. package/dist/commands/render.d.ts +4 -0
  12. package/dist/commands/render.js +64 -9
  13. package/dist/commands/schemas.d.ts +2 -2
  14. package/dist/commands/schemas.js +133 -22
  15. package/dist/commands/show.d.ts +1 -0
  16. package/dist/commands/show.js +7 -1
  17. package/dist/commands/spaces.d.ts +3 -1
  18. package/dist/commands/spaces.js +11 -2
  19. package/dist/commands/validate-file.d.ts +3 -0
  20. package/dist/commands/validate-file.js +24 -6
  21. package/dist/commands/validate.d.ts +4 -0
  22. package/dist/commands/validate.js +79 -23
  23. package/dist/config.d.ts +25 -1
  24. package/dist/config.js +106 -12
  25. package/dist/filter/augment-nodes.d.ts +3 -1
  26. package/dist/filter/augment-nodes.js +12 -7
  27. package/dist/filter/filter-nodes.js +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +96 -31
  30. package/dist/judge/context.d.ts +20 -0
  31. package/dist/judge/context.js +105 -0
  32. package/dist/judge/factory.d.ts +44 -0
  33. package/dist/judge/factory.js +148 -0
  34. package/dist/judge/index.d.ts +13 -0
  35. package/dist/judge/index.js +8 -0
  36. package/dist/judge/judge-node.d.ts +27 -0
  37. package/dist/judge/judge-node.js +122 -0
  38. package/dist/judge/prompt.d.ts +10 -0
  39. package/dist/judge/prompt.js +92 -0
  40. package/dist/judge/provider/anthropic.d.ts +17 -0
  41. package/dist/judge/provider/anthropic.js +43 -0
  42. package/dist/judge/provider/fake.d.ts +26 -0
  43. package/dist/judge/provider/fake.js +56 -0
  44. package/dist/judge/provider/jev.d.ts +37 -0
  45. package/dist/judge/provider/jev.js +169 -0
  46. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  47. package/dist/judge/provider/openai-decisions.js +84 -0
  48. package/dist/judge/provider/openai.d.ts +20 -0
  49. package/dist/judge/provider/openai.js +53 -0
  50. package/dist/judge/provider/per-rule.d.ts +10 -0
  51. package/dist/judge/provider/per-rule.js +18 -0
  52. package/dist/judge/trim.d.ts +27 -0
  53. package/dist/judge/trim.js +62 -0
  54. package/dist/judge/types.d.ts +92 -0
  55. package/dist/judge/types.js +9 -0
  56. package/dist/judge/verdicts.d.ts +24 -0
  57. package/dist/judge/verdicts.js +82 -0
  58. package/dist/load-space-graph.d.ts +7 -5
  59. package/dist/load-space-graph.js +19 -8
  60. package/dist/plugins/loader.js +3 -3
  61. package/dist/plugins/markdown/extract-content-links.js +8 -29
  62. package/dist/plugins/markdown/index.js +8 -2
  63. package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
  64. package/dist/plugins/markdown/parse-embedded.js +195 -24
  65. package/dist/plugins/markdown/read-space.js +42 -14
  66. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  67. package/dist/plugins/markdown/render-directory.js +130 -0
  68. package/dist/plugins/markdown/template-sync.js +3 -4
  69. package/dist/plugins/markdown/util.d.ts +26 -5
  70. package/dist/plugins/markdown/util.js +56 -15
  71. package/dist/plugins/mermaid/render-mermaid.js +2 -1
  72. package/dist/plugins/miro/sync.js +7 -6
  73. package/dist/plugins/util.d.ts +39 -1
  74. package/dist/plugins/util.js +139 -0
  75. package/dist/read/resolve-graph-edges.d.ts +1 -1
  76. package/dist/read/resolve-graph-edges.js +46 -27
  77. package/dist/read/wikilink-utils.d.ts +17 -0
  78. package/dist/read/wikilink-utils.js +40 -0
  79. package/dist/render/render.d.ts +3 -1
  80. package/dist/render/render.js +2 -2
  81. package/dist/schema/evaluate-rule.d.ts +33 -2
  82. package/dist/schema/evaluate-rule.js +58 -4
  83. package/dist/schema/gating.d.ts +69 -0
  84. package/dist/schema/gating.js +253 -0
  85. package/dist/schema/meta-schemas.d.ts +13 -0
  86. package/dist/schema/meta-schemas.js +50 -0
  87. package/dist/schema/metadata-contract.d.ts +952 -183
  88. package/dist/schema/metadata-contract.js +177 -33
  89. package/dist/schema/rule-filter.d.ts +12 -0
  90. package/dist/schema/rule-filter.js +46 -0
  91. package/dist/schema/rule-severity.d.ts +13 -0
  92. package/dist/schema/rule-severity.js +33 -0
  93. package/dist/schema/schema.d.ts +14 -8
  94. package/dist/schema/schema.js +308 -199
  95. package/dist/schema/validate-graph.d.ts +2 -1
  96. package/dist/schema/validate-graph.js +40 -23
  97. package/dist/schema/validate-rules.d.ts +16 -3
  98. package/dist/schema/validate-rules.js +91 -29
  99. package/dist/schemas/_knowledge_wiki.json +24 -24
  100. package/dist/schemas/_okf.json +172 -0
  101. package/dist/schemas/_ost_strict.json +17 -8
  102. package/dist/schemas/_sctx_base.json +3 -3
  103. package/dist/schemas/_strategy_general.json +38 -38
  104. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  105. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  106. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  107. package/dist/schemas/knowledge_wiki.json +10 -9
  108. package/dist/schemas/okf.json +8 -0
  109. package/dist/schemas/strategy_general.json +15 -14
  110. package/dist/schemas/strict_ost.json +17 -16
  111. package/dist/space-context.d.ts +5 -2
  112. package/dist/space-context.js +9 -4
  113. package/dist/space-graph.d.ts +9 -7
  114. package/dist/space-graph.js +7 -5
  115. package/dist/testing.d.ts +33 -0
  116. package/dist/testing.js +72 -0
  117. package/dist/types.d.ts +64 -16
  118. package/dist/validate.d.ts +62 -9
  119. package/dist/validate.js +229 -114
  120. package/docs/concepts.md +65 -9
  121. package/docs/config.md +78 -1
  122. package/docs/rules.md +168 -50
  123. package/docs/schemas.md +152 -52
  124. package/package.json +17 -11
  125. package/schemas/_knowledge_wiki.json +24 -24
  126. package/schemas/_okf.json +172 -0
  127. package/schemas/_ost_strict.json +17 -8
  128. package/schemas/_sctx_base.json +3 -3
  129. package/schemas/_strategy_general.json +38 -38
  130. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  131. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  132. package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  133. package/schemas/knowledge_wiki.json +10 -9
  134. package/schemas/okf.json +8 -0
  135. package/schemas/strategy_general.json +15 -14
  136. package/schemas/strict_ost.json +17 -16
  137. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  138. package/schemas/generated/_structured_context_schema_meta.json +0 -191
package/docs/schemas.md CHANGED
@@ -1,3 +1,7 @@
1
+ ---
2
+ title: "Schemas"
3
+ ---
4
+
1
5
  # Schemas
2
6
 
3
7
  This document explains schema usage, metadata shape, and composition semantics in `structured-context`.
@@ -6,7 +10,7 @@ This document explains schema usage, metadata shape, and composition semantics i
6
10
 
7
11
  A **schema** defines the valid structure for nodes in a `space`: entity types, field constraints, hierarchy behavior, type aliases, and executable rules.
8
12
 
9
- `structured-context` uses JSON Schema Draft-07 plus a custom top-level `$metadata` keyword.
13
+ `structured-context` uses JSON Schema 2020-12 plus a custom top-level `$metadata` keyword.
10
14
 
11
15
  ## Selecting a schema
12
16
 
@@ -20,6 +24,11 @@ Set `schema` in the space config entry:
20
24
  }
21
25
  ```
22
26
 
27
+ A schema can be specified as:
28
+ - **Bundled schema**: simple filename like `strategy_general.json` or `strict_ost.json`.
29
+ - **Local file**: relative path starting with `./` or `../` (resolved against config file directory), or absolute path.
30
+ - **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.
31
+
23
32
  Resolution order: space `schema` > global `schema`. Schema resolution fails if none is configured.
24
33
 
25
34
  ## Bundled schemas
@@ -46,7 +55,23 @@ Main types:
46
55
  - `solution`
47
56
  - `assumption_test`
48
57
 
49
- This schema composes shared structural defs and strict metadata/rules from partials.
58
+ This schema reuses shared structural defs from partials via `$ref` and imports its hierarchy and rules from `_ost_strict` via `$metadata.imports`.
59
+
60
+ ### `knowledge_wiki.json`
61
+
62
+ Flat, LLM-maintained knowledge base (after Karpathy's LLM Wiki pattern). No hierarchy or required parent links; wikilinks between any pages are valid. Validates structure and provenance, not meaning.
63
+
64
+ Main types:
65
+ - `source`
66
+ - `concept`
67
+ - `entity`
68
+ - `synthesis`
69
+ - `note`
70
+ - `index`
71
+
72
+ ### `okf.json`
73
+
74
+ [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) (OKF v0.2) bundles, validated as plain markdown spaces. Any concept `type` is accepted and unknown fields are tolerated; provenance (`sources`), trust (`generated`, `verified`), lifecycle (`status`, `stale_after`) and the Attested Computation contract are checked when present. An Attested Computation needs its computation: a `computation` field, or a `# Computation` section in the body (found by matching that line in the body text, so a `# Computation` line inside a code fence also counts). Reserved `index.md` and `log.md` are reported as excluded during parsing.
50
75
 
51
76
  ## Custom format annotations
52
77
 
@@ -54,14 +79,16 @@ This schema composes shared structural defs and strict metadata/rules from parti
54
79
 
55
80
  | Format | Validates | Example |
56
81
  |--------|-----------|---------|
57
- | `date` | ISO 8601 date (`YYYY-MM-DD`) | `"2026-03-31"` |
82
+ | `date` | ISO 8601 calendar date (`YYYY-MM-DD`) | `"2026-03-31"` |
83
+ | `date-time` | ISO 8601 datetime with seconds and a UTC offset (`Z` or `±HH:MM`) | `"2026-03-31T09:15:00+10:00"` |
84
+ | `date-or-date-time` | A date, or a datetime where seconds and offset are optional (as Obsidian's datetime property writes) | `"2026-03-31"`, `"2026-03-31T09:15"`, `"2026-03-31T09:15:00Z"` |
58
85
  | `path` | Non-empty filesystem path — absolute, relative, or a plain name | `"notes"`, `"./subdir/file.md"`, `"/abs/path"` |
59
86
  | `wikilink` | Obsidian wikilink syntax (`[[...]]`) | `"[[Parent Node]]"` |
60
87
 
61
88
  `path` and `wikilink` are also available as shared `$ref` definitions in `_sctx_base.json`:
62
89
 
63
90
  ```json
64
- { "$ref": "sctx://_sctx_base#/$defs/wikilink" }
91
+ { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" }
65
92
  ```
66
93
 
67
94
  Using `format` directly is more concise when the full definition isn't needed:
@@ -70,21 +97,25 @@ Using `format` directly is more concise when the full definition isn't needed:
70
97
  { "type": "string", "format": "wikilink" }
71
98
  ```
72
99
 
73
- ### Date coercion
100
+ ### Dates and datetimes
74
101
 
75
- YAML parsers (gray-matter, js-yaml) coerce unquoted ISO dates to JavaScript `Date` objects:
102
+ The markdown plugin reads frontmatter and embedded YAML blocks without YAML's timestamp type, so unquoted dates and datetimes stay as the strings written, at any depth (including inside lists):
76
103
 
77
104
  ```yaml
78
- published_date: 2026-03-31 # parsed as a Date object by gray-matter
105
+ published_date: 2026-03-31 # "2026-03-31"
106
+ updated: 2026-06-20T22:53:05+10:00 # "2026-06-20T22:53:05+10:00", offset kept
79
107
  ```
80
108
 
81
- The markdown plugin automatically coerces `Date` objects to `YYYY-MM-DD` strings before validation, so unquoted dates in frontmatter and embedded YAML blocks work correctly with `format: "date"` fields.
109
+ Validate them with `format: "date"` or `format: "date-time"` when a field needs one precision, or `format: "date-or-date-time"` to accept either, and compare them in rules with JSONata's `$toMillis()` (see [Rules](rules.md#dates-and-times)).
82
110
 
83
111
  ## Metadata dialect
84
112
 
85
- Schemas use this metaschema URL:
113
+ The dialect is JSON Schema 2020-12 plus the top-level `$metadata` keyword. Schemas set `$schema` to the metaschema URL:
114
+
115
+ - `https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/latest/_structured_context_schema_meta.json` follows the newest dialect.
116
+ - `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
117
 
87
- - `https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json`
118
+ Because the dialect is 2020-12, keywords may sit beside `$ref`, and `unevaluatedProperties: false` can close an object composed through `allOf`.
88
119
 
89
120
  Top-level metadata shape:
90
121
 
@@ -108,8 +139,8 @@ Top-level metadata shape:
108
139
  "id": "active-outcome-count",
109
140
  "category": "workflow",
110
141
  "description": "Only one outcome should be active at a time",
111
- "scope": "global",
112
- "check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
142
+ "global": true,
143
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
113
144
  }
114
145
  ]
115
146
  }
@@ -120,12 +151,15 @@ Top-level metadata shape:
120
151
 
121
152
  | Field | Type | Notes |
122
153
  |---|---|---|
123
- | `hierarchy` | object | Optional per provider; at most one provider may define it after composition |
154
+ | `imports` | `string[]` | Optional; `$id`s of schemas whose metadata is merged into this one. See [Composition and merge semantics](#composition-and-merge-semantics) |
155
+ | `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) |
156
+ | `hierarchy.name` | `string` | Optional short name; defaults to the declaring schema's file stem |
157
+ | `hierarchy.override` | `boolean` | Optional; replaces an imported hierarchy with the same name |
124
158
  | `hierarchy.levels` | `(string \| HierarchyLevel)[]` | Ordered root→leaf types |
125
159
  | `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 |
128
- | `rules` | `Rule[]` | Optional flat rule array |
160
+ | `relationships` | `Relationship[]` | Optional; defines related node links outside the primary hierarchy. A relationship may set `override: true` |
161
+ | `aliases` | `Record<string, string \| { type, override }>` | Optional type alias map. Use `{ "type": "goal", "override": true }` to replace an imported alias |
162
+ | `rules` | `Rule[]` | Optional flat rule array. A rule has an `expression` (a JSONata test), a `semantic` part (evaluated by a model against a rubric), or both; see [Rules](rules.md) |
129
163
 
130
164
  ### Relationships
131
165
 
@@ -137,9 +171,9 @@ Relationships define links between node types that are not part of the primary s
137
171
  | `type` | `string` | **Required** | The child's canonical type name |
138
172
  | `field` | `string` | `"parent"` | The frontmatter field that holds the wikilink(s) for this relationship |
139
173
  | `fieldOn` | `string` | `"child"` | `"child"` (child holds a link to parent) or `"parent"` (parent holds an array of child links) |
140
- | `format` | `string` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
174
+ | `templateFormat` | `string` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
141
175
  | `matchers` | `string[]` | `[]` | Heading text to match (strings or `/regex/`). Case-insensitive. |
142
- | `embeddedTemplateFields` | `string[]` | `[]` | Field names to include in templates when `format` is `"table"` |
176
+ | `embeddedTemplateFields` | `string[]` | `[]` | Field names to include in templates when `templateFormat` is `"table"` |
143
177
  | `multiple` | `boolean` | `true` | Whether multiple children are expected |
144
178
 
145
179
  **`fieldOn: "child"` (default)** — child node has a field pointing to its parent. Embedded parsing sets this field on each child node; validation checks that it resolves to a node of the declared parent type.
@@ -153,7 +187,7 @@ Relationships define links between node types that are not part of the primary s
153
187
  {
154
188
  "parent": "opportunity",
155
189
  "type": "assumption",
156
- "format": "table",
190
+ "templateFormat": "table",
157
191
  "matchers": ["Assumptions", "/assum.*/"],
158
192
  "embeddedTemplateFields": ["assumption", "status", "confidence"]
159
193
  }
@@ -169,7 +203,7 @@ Relationships define links between node types that are not part of the primary s
169
203
  "type": "task",
170
204
  "field": "tasks",
171
205
  "fieldOn": "parent",
172
- "format": "list",
206
+ "templateFormat": "list",
173
207
  "matchers": ["Tasks"],
174
208
  "multiple": true
175
209
  }
@@ -188,7 +222,7 @@ With this configuration, embedded task items under an Activity's "Tasks" heading
188
222
  | `multiple` | `false` | Field contains array of wikilinks |
189
223
  | `selfRef` | `false` | Allows same-type parent |
190
224
  | `selfRefField` | _undefined_ | Separate field for same-type parent links |
191
- | `format` | _undefined_ | Embedding hint: `"list"`, `"table"`, or `"heading"` — enables hierarchy embedding when set |
225
+ | `templateFormat` | _undefined_ | Embedding hint: `"list"`, `"table"`, or `"heading"` — enables hierarchy embedding when set |
192
226
  | `matchers` | _undefined_ | Heading text patterns (strings or `/regex/`) to detect embedding sections |
193
227
  | `embeddedTemplateFields` | _undefined_ | Column/field names for table stubs generated by `template-sync` |
194
228
 
@@ -197,7 +231,7 @@ String shorthand (`"goal"`) normalizes to:
197
231
 
198
232
  ### Hierarchy embedding
199
233
 
200
- When a hierarchy level has `format` and `matchers`, it participates in **hierarchy embedding** — the same section-based parsing used for relationships. Two patterns are supported:
234
+ When a hierarchy level has `templateFormat` and `matchers`, it participates in **hierarchy embedding** — the same section-based parsing used for relationships. Two patterns are supported:
201
235
 
202
236
  **Child-level embedding** — a typed-page heading signals that following content should produce nodes of the child type (next level in hierarchy):
203
237
 
@@ -208,7 +242,7 @@ When a hierarchy level has `format` and `matchers`, it participates in **hierarc
208
242
  "type": "opportunity",
209
243
  "field": "parent",
210
244
  "fieldOn": "child",
211
- "format": "list",
245
+ "templateFormat": "list",
212
246
  "matchers": ["Opportunities", "User Opportunities"]
213
247
  }
214
248
  ]
@@ -250,39 +284,79 @@ With this config, an application page may include `### capability` with wikilink
250
284
  - [[Zephyr]] ← populates application.tools (fieldOn: parent)
251
285
  ```
252
286
 
287
+ A node's body becomes its `content`; see [Node content](concepts.md#node-content).
288
+
253
289
  ## Composition and merge semantics
254
290
 
255
- Metadata is composed across the `$ref` graph with deterministic behavior:
291
+ `$ref` and `$defs` are for **validation only**: a `$ref` to another schema (or to one of its `$defs`) reuses its JSON Schema definitions but brings none of its `$metadata`.
292
+
293
+ Metadata travels only through **`$metadata.imports`**, a list of schema `$id`s:
294
+
295
+ ```json5
296
+ "$metadata": {
297
+ "imports": ["sctx://core/_strategy_general"],
298
+ "rules": [ /* this schema's own rules */ ]
299
+ }
300
+ ```
301
+
302
+ - An import brings the imported schema's whole `$metadata` (hierarchy, relationships, aliases, rules).
303
+ - Imports are transitive: an imported schema's own imports come too.
304
+ - Merge order: imports in the order listed, depth-first, then the schema's own metadata last. Each schema is merged at most once.
305
+ - An unresolvable import `$id` is an error.
256
306
 
257
- 1. Traverse external `$ref` graph in DFS order.
258
- 2. Apply root schema metadata last.
307
+ 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.
259
308
 
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`.
309
+ | Key | Identity | How to mark `override` |
310
+ |---|---|---|
311
+ | `rules` | `id` | `"override": true` on the rule |
312
+ | `relationships` | `parent` + `type` + `field` (default `"parent"`) | `"override": true` on the relationship |
313
+ | `aliases` | alias name | `{ "type": "<target>", "override": true }` as the alias value |
314
+ | `hierarchy` | `name` | `"override": true` on the hierarchy declaration |
315
+
316
+ 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
317
 
268
318
  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
319
 
270
- ### Rule imports via `$ref`
320
+ ### Multiple hierarchies
321
+
322
+ A space has several hierarchies when its schema imports several schemas that each declare one. Each schema declares at most one hierarchy.
271
323
 
272
- Inside `$metadata.rules`, entries can be inline rules or `$ref` imports:
324
+ - **Every imported schema contributes its hierarchy.** Each is kept as a separate named hierarchy; levels are never merged.
325
+ - **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`.
326
+ - **Main hierarchy.**
327
+ - If the root schema declares a hierarchy, it is the main hierarchy.
328
+ - 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`).
329
+ - 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.
330
+ - A root that both declares a hierarchy and uses `$ref` is an error.
331
+ - **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.
332
+ - **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
333
 
274
334
  ```json5
275
- "rules": [
276
- { "$ref": "sctx://my-pack#/$defs/workflowRule" },
277
- { "$ref": "sctx://my-pack#/$defs/ruleSet" }
278
- ]
335
+ {
336
+ "$id": "sctx://team",
337
+ "oneOf": [
338
+ { "$ref": "sctx://_work#/$defs/goal" },
339
+ { "$ref": "sctx://_work#/$defs/task" },
340
+ { "$ref": "sctx://_skills#/$defs/skill_area" },
341
+ { "$ref": "sctx://_skills#/$defs/skill" }
342
+ ],
343
+ "$metadata": {
344
+ // _work.json and _skills.json each declare a hierarchy; select the main one.
345
+ "imports": ["sctx://_work", "sctx://_skills"],
346
+ "hierarchy": { "$ref": "sctx://_work#/$metadata/hierarchy" }
347
+ }
348
+ }
279
349
  ```
280
350
 
281
- Import targets may be:
282
- - a single rule object
283
- - an object containing `rules: []`
351
+ How hierarchies are used:
284
352
 
285
- Imported rules are normalized into one executable flat list before validation.
353
+ | Behaviour | Hierarchies used |
354
+ |---|---|
355
+ | Layering, parent-type, skip-level and orphan checks | Each hierarchy, independently. A hierarchy edge whose parent belongs to another hierarchy is an invalid parent. |
356
+ | Relationships | Connect types across hierarchies, as before |
357
+ | `space_on_a_page` parsing, hierarchy embedding, `template-sync` | Main hierarchy |
358
+ | `show`, `diagram`, `render` | Main hierarchy by default; `--hierarchy <name>` selects another |
359
+ | `resolvedParents` | Hierarchy edges carry the `hierarchy` name |
286
360
 
287
361
  ### Override example
288
362
 
@@ -295,30 +369,56 @@ Imported rules are normalized into one executable flat list before validation.
295
369
  "override": true,
296
370
  "category": "workflow",
297
371
  "description": "Require exactly one active outcome",
298
- "scope": "global",
299
- "check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
372
+ "global": true,
373
+ "expression": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
300
374
  }
301
375
  ]
302
376
  }
303
377
  }
304
378
  ```
305
379
 
380
+ ## Schema `$id` namespaces
381
+
382
+ Every schema `$id` uses the `sctx://` scheme. The shape of the `$id` says where the schema comes from:
383
+
384
+ | `$id` shape | Source | Example |
385
+ |---|---|---|
386
+ | `sctx://core/<name>` | Bundled with structured-context | `sctx://core/_sctx_base` |
387
+ | `sctx://<plugin>/<name>` | Contributed by a plugin (its short name, without `sctx-`) | `sctx://wardley-mapping/_wardley` |
388
+ | `sctx://<name>` | Local to a space's schema directory — a single name, no path | `sctx://team` |
389
+
390
+ - Two schemas loaded for the same space may not share an `$id`; a collision is an error naming both sources.
391
+ - `core` is reserved: no plugin may use it as its namespace.
392
+ - **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.
393
+
306
394
  ## Partials and `$ref`
307
395
 
308
396
  - 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.
397
+ - Bundled partials, local schema-directory partials, and partials from loaded plugins are all registered.
398
+ - `$id` values follow the [namespace rules](#schema-id-namespaces) and must not collide across bundled, plugin and local schemas.
311
399
  - `$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.
400
+ - Partials with no `$metadata` should prefer `$schema: "https://json-schema.org/draft/2020-12/schema"` so they validate standalone as plain JSON Schema fragments.
401
+ - **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, `_okf.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference their entity types via `$ref` rather than redefining them.
402
+ - **Partials can carry metadata**: Partials may include `$metadata` (hierarchy, aliases, relationships, rules). A schema gets it only by listing the partial in `$metadata.imports`; `$ref` alone brings none.
403
+
404
+ ## Plugin-contributed schemas
405
+
406
+ Plugins contribute schemas and partials by standard directory convention: placing schema files in `<pluginRoot>/schemas/` (or declaring `"sctx": { "schemas": "./path" }` in `package.json`).
407
+
408
+ 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.
409
+
410
+ When a space uses a plugin schema:
411
+ - **Registry registration**: All partials (`_*`) in the plugin's `schemas/` directory are automatically indexed in the schema registry.
412
+ - **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).
413
+ - **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.
414
+ - **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
415
 
316
416
  ## Editor expectations
317
417
 
318
- Use the shipped metaschema URL in `$schema` for best cross-tool behavior.
418
+ 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
419
 
320
420
  Notes:
321
- - Custom `$id` values like `sctx://...` are still supported by the CLI registry.
421
+ - `sctx://` `$id`s are resolved by the CLI registry (see [Schema `$id` namespaces](#schema-id-namespaces)).
322
422
  - Some generic editors may not resolve custom URI schemes for `$ref`; CLI behavior is authoritative.
323
423
  - Do not rely on editor-only mappings for runtime correctness.
324
424
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "structured-context",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Keep your content clean and your thinking clear with assistive tools for structured knowledge",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -25,47 +25,53 @@
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",
38
42
  "test": "bun test tests/",
39
43
  "test:smoke": "bun test smoke/",
44
+ "test:judge": "SCTX_LIVE_JUDGE_TESTS=1 bun test tests/judge/live.test.ts",
45
+ "test:models": "SCTX_MODEL_VALIDATION=1 bun test tests/judge/model-validation.test.ts",
40
46
  "test:hook": "bun test hook-test/unit/",
41
47
  "test:hook:e2e": "bun test hook-test/hooks.test.ts",
42
48
  "test:all": "bun test",
43
49
  "lint": "biome check",
44
50
  "lint:fix": "biome check --write",
45
51
  "preversion": "bun run generate:schema-meta && bun run lint:fix && bun run test",
46
- "version": "git add -A",
52
+ "version": "bun run generate:schema-meta --release && git add -A",
47
53
  "postversion": "git push --follow-tags"
48
54
  },
49
55
  "devDependencies": {
50
- "@anthropic-ai/claude-agent-sdk": "^0.2.92",
51
- "@biomejs/biome": "^2.4.10",
56
+ "@anthropic-ai/claude-agent-sdk": "^0.3.191",
57
+ "@biomejs/biome": "^2.5.15",
52
58
  "@types/bun": "latest",
53
- "@types/js-yaml": "^4.0.9",
54
59
  "json-schema-to-ts": "^3.1.1",
55
- "lefthook": "^2.1.4"
60
+ "lefthook": "^2.1.9",
61
+ "tsc-alias": "^1.9.1"
56
62
  },
57
63
  "peerDependencies": {
58
64
  "typescript": "^5"
59
65
  },
60
66
  "dependencies": {
61
- "ajv": "^8.18.0",
67
+ "ajv": "^8.20.0",
62
68
  "chokidar": "^5.0.0",
63
- "commander": "^14.0.3",
69
+ "commander": "^15.0.0",
64
70
  "fast-glob": "^3.3.3",
65
71
  "gray-matter": "^4.0.3",
66
- "js-yaml": "^4.1.1",
72
+ "js-yaml": "^5.4.3",
67
73
  "json5": "^2.2.3",
68
- "jsonata": "^2.1.0",
74
+ "jsonata": "^2.2.1",
69
75
  "mdast-util-to-string": "^4.0.0",
70
76
  "remark-gfm": "^4.0.1",
71
77
  "remark-parse": "^11.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json",
3
- "$id": "sctx://_knowledge_wiki",
2
+ "$schema": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/latest/_structured_context_schema_meta.json",
3
+ "$id": "sctx://core/_knowledge_wiki",
4
4
  // Shared entity definitions for knowledge wiki schemas.
5
5
  // Provides aliases, relationships, rules, and entity type $defs.
6
6
  // This partial has no hierarchy — wiki entities are flat with no required parent links.
@@ -50,14 +50,14 @@
50
50
  "category": "coherence",
51
51
  "description": "Syntheses should reference at least one contributing source in their sources array",
52
52
  "type": "synthesis",
53
- "check": "$exists(current.sources) and $count(current.sources) >= 1"
53
+ "expression": "$exists(current.sources) and $count(current.sources) >= 1"
54
54
  },
55
55
  {
56
56
  "id": "concept-has-summary",
57
57
  "category": "best-practice",
58
58
  "description": "Concept pages should have a short summary for index discoverability",
59
59
  "type": "concept",
60
- "check": "$exists(current.summary) and $length(current.summary) > 0"
60
+ "expression": "$exists(current.summary) and $length(current.summary) > 0"
61
61
  }
62
62
  ]
63
63
  },
@@ -65,18 +65,18 @@
65
65
  "source": {
66
66
  "type": "object",
67
67
  "description": "A processed summary of a raw source (article, paper, book, video, thread, podcast, etc.). Captures key information from the source in structured form with provenance.",
68
- "allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
68
+ "allOf": [{ "$ref": "sctx://core/_sctx_base#/$defs/baseNodeProps" }],
69
69
  "properties": {
70
70
  "type": { "const": "source" },
71
71
  "summary": {
72
- "$ref": "sctx://_sctx_base#/$defs/summary"
72
+ "$ref": "sctx://core/_sctx_base#/$defs/summary"
73
73
  },
74
74
  "url": {
75
75
  "type": "string",
76
76
  "description": "URL of the original source"
77
77
  },
78
78
  "local_copy": {
79
- "$ref": "sctx://_sctx_base#/$defs/wikilink",
79
+ "$ref": "sctx://core/_sctx_base#/$defs/wikilink",
80
80
  "description": "Wikilink to a local copy of the source (e.g. a saved PDF or clipped note)"
81
81
  },
82
82
  "author": {
@@ -89,7 +89,7 @@
89
89
  },
90
90
  "published_date": {
91
91
  "type": "string",
92
- "format": "date",
92
+ "format": "date-or-date-time",
93
93
  "description": "Original publication date"
94
94
  },
95
95
  "source_type": {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  "sources": {
100
100
  "type": "array",
101
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
101
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
102
102
  "description": "Wikilinks to other source pages that this one references or extends"
103
103
  }
104
104
  },
@@ -117,20 +117,20 @@
117
117
  "concept": {
118
118
  "type": "object",
119
119
  "description": "A concept page — defines and frames a specific idea, mechanism, or domain term. May draw from multiple sources. The building blocks of the wiki's knowledge graph.",
120
- "allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
120
+ "allOf": [{ "$ref": "sctx://core/_sctx_base#/$defs/baseNodeProps" }],
121
121
  "properties": {
122
122
  "type": { "const": "concept" },
123
123
  "summary": {
124
- "$ref": "sctx://_sctx_base#/$defs/summary"
124
+ "$ref": "sctx://core/_sctx_base#/$defs/summary"
125
125
  },
126
126
  "sources": {
127
127
  "type": "array",
128
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
128
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
129
129
  "description": "Wikilinks to source pages that contributed to this concept"
130
130
  },
131
131
  "related": {
132
132
  "type": "array",
133
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
133
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
134
134
  "description": "Wikilinks to related concept or synthesis pages"
135
135
  }
136
136
  },
@@ -147,20 +147,20 @@
147
147
  "entity": {
148
148
  "type": "object",
149
149
  "description": "A concrete entity — a person, organisation, product, model, or other named thing. The building blocks of the wiki's knowledge graph alongside concepts. Entities are distinct from concepts: a concept frames an idea or mechanism; an entity is a specific, identifiable thing in the world.",
150
- "allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
150
+ "allOf": [{ "$ref": "sctx://core/_sctx_base#/$defs/baseNodeProps" }],
151
151
  "properties": {
152
152
  "type": { "const": "entity" },
153
153
  "summary": {
154
- "$ref": "sctx://_sctx_base#/$defs/summary"
154
+ "$ref": "sctx://core/_sctx_base#/$defs/summary"
155
155
  },
156
156
  "sources": {
157
157
  "type": "array",
158
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
158
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
159
159
  "description": "Wikilinks to source pages that contributed to this entity page"
160
160
  },
161
161
  "related": {
162
162
  "type": "array",
163
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
163
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
164
164
  "description": "Wikilinks to related entity, concept, or synthesis pages"
165
165
  }
166
166
  },
@@ -177,20 +177,20 @@
177
177
  "synthesis": {
178
178
  "type": "object",
179
179
  "description": "A synthesis page — connects multiple concepts and sources into a coherent model, framework, or thesis. The highest-value pages in a knowledge wiki. Must reference contributing sources.",
180
- "allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
180
+ "allOf": [{ "$ref": "sctx://core/_sctx_base#/$defs/baseNodeProps" }],
181
181
  "properties": {
182
182
  "type": { "const": "synthesis" },
183
183
  "summary": {
184
- "$ref": "sctx://_sctx_base#/$defs/summary"
184
+ "$ref": "sctx://core/_sctx_base#/$defs/summary"
185
185
  },
186
186
  "sources": {
187
187
  "type": "array",
188
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
188
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
189
189
  "description": "Wikilinks to source pages this synthesis draws from"
190
190
  },
191
191
  "concepts": {
192
192
  "type": "array",
193
- "items": { "$ref": "sctx://_sctx_base#/$defs/wikilink" },
193
+ "items": { "$ref": "sctx://core/_sctx_base#/$defs/wikilink" },
194
194
  "description": "Wikilinks to concept pages this synthesis connects"
195
195
  }
196
196
  },
@@ -208,12 +208,12 @@
208
208
  "note": {
209
209
  "type": "object",
210
210
  "description": "Personal notes, journal entries, talk notes, or rough thoughts not drawn from an external URL.",
211
- "allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
211
+ "allOf": [{ "$ref": "sctx://core/_sctx_base#/$defs/baseNodeProps" }],
212
212
  "properties": {
213
213
  "type": { "const": "note" },
214
214
  "date": {
215
215
  "type": "string",
216
- "format": "date",
216
+ "format": "date-or-date-time",
217
217
  "description": "Date this entry was created or relates to"
218
218
  }
219
219
  },
@@ -230,7 +230,7 @@
230
230
  "index": {
231
231
  "type": "object",
232
232
  "description": "The wiki index — a catalog of all pages with links, summaries, and metadata. Updated by the agent on ingest. Used as the primary navigation entry point.",
233
- "allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
233
+ "allOf": [{ "$ref": "sctx://core/_sctx_base#/$defs/baseNodeProps" }],
234
234
  "properties": {
235
235
  "type": { "const": "index" }
236
236
  },