structured-context 0.9.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.
- package/README.md +348 -0
- package/dist/commands/diagram.d.ts +5 -0
- package/dist/commands/diagram.js +12 -0
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/dump.d.ts +2 -0
- package/dist/commands/dump.js +6 -0
- package/dist/commands/plugins.d.ts +1 -0
- package/dist/commands/plugins.js +23 -0
- package/dist/commands/render.d.ts +6 -0
- package/dist/commands/render.js +35 -0
- package/dist/commands/schemas.d.ts +6 -0
- package/dist/commands/schemas.js +268 -0
- package/dist/commands/show.d.ts +4 -0
- package/dist/commands/show.js +7 -0
- package/dist/commands/spaces.d.ts +1 -0
- package/dist/commands/spaces.js +36 -0
- package/dist/commands/template-sync.d.ts +3 -0
- package/dist/commands/template-sync.js +13 -0
- package/dist/commands/validate-file.d.ts +28 -0
- package/dist/commands/validate-file.js +133 -0
- package/dist/commands/validate.d.ts +16 -0
- package/dist/commands/validate.js +349 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +179 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/filter/augment-nodes.d.ts +23 -0
- package/dist/filter/augment-nodes.js +95 -0
- package/dist/filter/expand-include.d.ts +62 -0
- package/dist/filter/expand-include.js +181 -0
- package/dist/filter/filter-nodes.d.ts +21 -0
- package/dist/filter/filter-nodes.js +73 -0
- package/dist/filter/parse-expression.d.ts +20 -0
- package/dist/filter/parse-expression.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -0
- package/dist/integrations/miro/cache.d.ts +21 -0
- package/dist/integrations/miro/cache.js +55 -0
- package/dist/integrations/miro/client.d.ts +99 -0
- package/dist/integrations/miro/client.js +118 -0
- package/dist/integrations/miro/layout.d.ts +28 -0
- package/dist/integrations/miro/layout.js +72 -0
- package/dist/integrations/miro/styles.d.ts +11 -0
- package/dist/integrations/miro/styles.js +65 -0
- package/dist/integrations/miro/sync.d.ts +8 -0
- package/dist/integrations/miro/sync.js +347 -0
- package/dist/plugin-api.d.ts +12 -0
- package/dist/plugin-api.js +7 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/loader.d.ts +21 -0
- package/dist/plugins/loader.js +104 -0
- package/dist/plugins/markdown/index.d.ts +48 -0
- package/dist/plugins/markdown/index.js +51 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +90 -0
- package/dist/plugins/markdown/parse-embedded.js +663 -0
- package/dist/plugins/markdown/read-space.d.ts +7 -0
- package/dist/plugins/markdown/read-space.js +89 -0
- package/dist/plugins/markdown/render-bullets.d.ts +2 -0
- package/dist/plugins/markdown/render-bullets.js +42 -0
- package/dist/plugins/markdown/render-mermaid.d.ts +2 -0
- package/dist/plugins/markdown/render-mermaid.js +57 -0
- package/dist/plugins/markdown/template-sync.d.ts +16 -0
- package/dist/plugins/markdown/template-sync.js +294 -0
- package/dist/plugins/markdown/util.d.ts +19 -0
- package/dist/plugins/markdown/util.js +80 -0
- package/dist/plugins/util.d.ts +60 -0
- package/dist/plugins/util.js +7 -0
- package/dist/read/read-space.d.ts +2 -0
- package/dist/read/read-space.js +22 -0
- package/dist/read/resolve-graph-edges.d.ts +11 -0
- package/dist/read/resolve-graph-edges.js +201 -0
- package/dist/read/wikilink-utils.d.ts +16 -0
- package/dist/read/wikilink-utils.js +38 -0
- package/dist/render/registry.d.ts +13 -0
- package/dist/render/registry.js +22 -0
- package/dist/render/render.d.ts +4 -0
- package/dist/render/render.js +28 -0
- package/dist/schema/evaluate-rule.d.ts +30 -0
- package/dist/schema/evaluate-rule.js +82 -0
- package/dist/schema/metadata-contract.d.ts +538 -0
- package/dist/schema/metadata-contract.js +115 -0
- package/dist/schema/schema-refs.d.ts +22 -0
- package/dist/schema/schema-refs.js +168 -0
- package/dist/schema/schema.d.ts +27 -0
- package/dist/schema/schema.js +378 -0
- package/dist/schema/validate-graph.d.ts +24 -0
- package/dist/schema/validate-graph.js +141 -0
- package/dist/schema/validate-rules.d.ts +10 -0
- package/dist/schema/validate-rules.js +51 -0
- package/dist/schemas/_ost_strict.json +81 -0
- package/dist/schemas/_sctx_base.json +72 -0
- package/dist/schemas/general.json +261 -0
- package/dist/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/dist/schemas/knowledge_wiki.json +206 -0
- package/dist/schemas/strict_ost.json +97 -0
- package/dist/space-graph.d.ts +28 -0
- package/dist/space-graph.js +82 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +0 -0
- package/docs/concepts.md +391 -0
- package/docs/config.md +140 -0
- package/docs/rules.md +120 -0
- package/docs/schemas.md +340 -0
- package/package.json +69 -0
- package/schemas/_ost_strict.json +81 -0
- package/schemas/_sctx_base.json +72 -0
- package/schemas/general.json +261 -0
- package/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/schemas/knowledge_wiki.json +206 -0
- package/schemas/strict_ost.json +97 -0
package/docs/rules.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Executable Rules
|
|
2
|
+
|
|
3
|
+
Rules are JSONata expressions in schema metadata (`$metadata.rules`). They run after structural JSON Schema validation and let you enforce cross-node checks and workflow constraints.
|
|
4
|
+
|
|
5
|
+
For metadata structure and composition behavior, see [docs/schemas.md](schemas.md).
|
|
6
|
+
|
|
7
|
+
## Rule shape
|
|
8
|
+
|
|
9
|
+
Rules are a flat array.
|
|
10
|
+
|
|
11
|
+
```json5
|
|
12
|
+
"rules": [
|
|
13
|
+
{
|
|
14
|
+
"id": "active-outcome-count",
|
|
15
|
+
"category": "workflow",
|
|
16
|
+
"description": "Only one outcome should be active at a time",
|
|
17
|
+
"scope": "global",
|
|
18
|
+
"check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Field | Required | Description |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `id` | yes | Unique rule identifier |
|
|
26
|
+
| `category` | yes | `validation` \| `coherence` \| `workflow` \| `best-practice` |
|
|
27
|
+
| `description` | yes | Human-readable rule intent |
|
|
28
|
+
| `check` | yes | JSONata expression; must evaluate to `true` |
|
|
29
|
+
| `type` | no | Restrict rule to nodes of this `resolvedType` |
|
|
30
|
+
| `scope` | no | Use `"global"` to evaluate once for the whole space |
|
|
31
|
+
| `override` | no | Only for merge conflicts: allows later duplicate `id` to replace earlier |
|
|
32
|
+
|
|
33
|
+
## Categories
|
|
34
|
+
|
|
35
|
+
Categories label violations for reporting. They do not change expression execution semantics.
|
|
36
|
+
|
|
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 |
|
|
43
|
+
|
|
44
|
+
## Evaluation model
|
|
45
|
+
|
|
46
|
+
- Rules with `scope: "global"` run once.
|
|
47
|
+
- Other rules run per applicable node.
|
|
48
|
+
- If `type` is set, only nodes with matching `resolvedType` are evaluated.
|
|
49
|
+
|
|
50
|
+
## Expression context
|
|
51
|
+
|
|
52
|
+
Each evaluation receives:
|
|
53
|
+
|
|
54
|
+
| Variable | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `nodes` | All nodes in the space |
|
|
57
|
+
| `current` | Current node for this evaluation |
|
|
58
|
+
| `parent` | First resolved parent node (if any) |
|
|
59
|
+
| `parents` | All resolved parent nodes (if any) |
|
|
60
|
+
|
|
61
|
+
Useful resolved fields on nodes:
|
|
62
|
+
- `resolvedType`
|
|
63
|
+
- `resolvedParentTitle`
|
|
64
|
+
- `resolvedParentTitles`
|
|
65
|
+
|
|
66
|
+
Prefer `resolvedType` over raw `type` so aliases are handled correctly.
|
|
67
|
+
|
|
68
|
+
## Predicate scoping (`$$`)
|
|
69
|
+
|
|
70
|
+
Inside `nodes[...]`, bare names refer to each candidate node. Use `$$` to reference outer variables:
|
|
71
|
+
|
|
72
|
+
```jsonata
|
|
73
|
+
$count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution'])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Rule imports (`$ref`)
|
|
77
|
+
|
|
78
|
+
`$metadata.rules` can include `$ref` entries that import:
|
|
79
|
+
- one rule
|
|
80
|
+
- a rule-set object with `rules: []`
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```json5
|
|
85
|
+
"rules": [
|
|
86
|
+
{ "$ref": "sctx://rule-pack#/$defs/workflowRule" },
|
|
87
|
+
{ "$ref": "sctx://rule-pack#/$defs/coreRuleSet" }
|
|
88
|
+
]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Imported entries are normalized into the same flat runtime list.
|
|
92
|
+
|
|
93
|
+
## Merge and conflict behavior
|
|
94
|
+
|
|
95
|
+
When metadata is composed across `$ref`:
|
|
96
|
+
- Rules are merged by `id`.
|
|
97
|
+
- Different payloads for the same `id` are an error by default.
|
|
98
|
+
- A later rule may replace an earlier one only with `override: true`.
|
|
99
|
+
|
|
100
|
+
Example override:
|
|
101
|
+
|
|
102
|
+
```json5
|
|
103
|
+
{
|
|
104
|
+
"id": "active-outcome-count",
|
|
105
|
+
"override": true,
|
|
106
|
+
"category": "workflow",
|
|
107
|
+
"description": "Require exactly one active outcome",
|
|
108
|
+
"scope": "global",
|
|
109
|
+
"check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Common patterns
|
|
114
|
+
|
|
115
|
+
```jsonata
|
|
116
|
+
$exists(current.metric) = true
|
|
117
|
+
$count(current.sources) >= 1
|
|
118
|
+
$count(nodes[resolvedType='outcome' and status='active']) <= 1
|
|
119
|
+
current.status != 'active' or $exists(parent) = false or parent.status = 'active'
|
|
120
|
+
```
|
package/docs/schemas.md
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Schemas
|
|
2
|
+
|
|
3
|
+
This document explains schema usage, metadata shape, and composition semantics in `structured-context`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A **schema** defines the valid structure for nodes in a `space`: entity types, field constraints, hierarchy behavior, type aliases, and executable rules.
|
|
8
|
+
|
|
9
|
+
`structured-context` uses JSON Schema Draft-07 plus a custom top-level `$metadata` keyword.
|
|
10
|
+
|
|
11
|
+
## Selecting a schema
|
|
12
|
+
|
|
13
|
+
Set `schema` in the space config entry:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"name": "my-space",
|
|
18
|
+
"path": "/path/to/space",
|
|
19
|
+
"schema": "schemas/strict_ost.json"
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Resolution order: space `schema` > global `schema` > bundled `schemas/general.json`.
|
|
24
|
+
|
|
25
|
+
## Bundled schemas
|
|
26
|
+
|
|
27
|
+
### `general.json` (default)
|
|
28
|
+
|
|
29
|
+
Flexible planning schema spanning strategy + OST-like flow.
|
|
30
|
+
|
|
31
|
+
Main types:
|
|
32
|
+
- `vision`
|
|
33
|
+
- `mission`
|
|
34
|
+
- `goal` (alias: `outcome`)
|
|
35
|
+
- `opportunity`
|
|
36
|
+
- `solution`
|
|
37
|
+
- `experiment` (aliases: `assumption_test`, `test`)
|
|
38
|
+
|
|
39
|
+
### `strict_ost.json`
|
|
40
|
+
|
|
41
|
+
Canonical 4-level OST structure.
|
|
42
|
+
|
|
43
|
+
Main types:
|
|
44
|
+
- `outcome`
|
|
45
|
+
- `opportunity`
|
|
46
|
+
- `solution`
|
|
47
|
+
- `assumption_test`
|
|
48
|
+
|
|
49
|
+
This schema composes shared structural defs and strict metadata/rules from partials.
|
|
50
|
+
|
|
51
|
+
## Custom format annotations
|
|
52
|
+
|
|
53
|
+
`structured-context` registers the following `format` annotations beyond standard JSON Schema. All apply to `string` properties and are validated at schema validation time.
|
|
54
|
+
|
|
55
|
+
| Format | Validates | Example |
|
|
56
|
+
|--------|-----------|---------|
|
|
57
|
+
| `date` | ISO 8601 date (`YYYY-MM-DD`) | `"2026-03-31"` |
|
|
58
|
+
| `path` | Non-empty filesystem path — absolute, relative, or a plain name | `"notes"`, `"./subdir/file.md"`, `"/abs/path"` |
|
|
59
|
+
| `wikilink` | Obsidian wikilink syntax (`[[...]]`) | `"[[Parent Node]]"` |
|
|
60
|
+
|
|
61
|
+
`path` and `wikilink` are also available as shared `$ref` definitions in `_sctx_base.json`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "$ref": "sctx://_sctx_base#/$defs/wikilink" }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Using `format` directly is more concise when the full definition isn't needed:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{ "type": "string", "format": "wikilink" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Date coercion
|
|
74
|
+
|
|
75
|
+
YAML parsers (gray-matter, js-yaml) coerce unquoted ISO dates to JavaScript `Date` objects:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
published_date: 2026-03-31 # parsed as a Date object by gray-matter
|
|
79
|
+
```
|
|
80
|
+
|
|
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.
|
|
82
|
+
|
|
83
|
+
## Metadata dialect
|
|
84
|
+
|
|
85
|
+
Schemas use this metaschema URL:
|
|
86
|
+
|
|
87
|
+
- `https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json`
|
|
88
|
+
|
|
89
|
+
Top-level metadata shape:
|
|
90
|
+
|
|
91
|
+
```json5
|
|
92
|
+
{
|
|
93
|
+
"$metadata": {
|
|
94
|
+
"hierarchy": {
|
|
95
|
+
"levels": [
|
|
96
|
+
"outcome",
|
|
97
|
+
{ "type": "opportunity", "selfRef": true },
|
|
98
|
+
"solution",
|
|
99
|
+
"assumption_test"
|
|
100
|
+
],
|
|
101
|
+
"allowSkipLevels": false
|
|
102
|
+
},
|
|
103
|
+
"aliases": {
|
|
104
|
+
"experiment": "assumption_test"
|
|
105
|
+
},
|
|
106
|
+
"rules": [
|
|
107
|
+
{
|
|
108
|
+
"id": "active-outcome-count",
|
|
109
|
+
"category": "workflow",
|
|
110
|
+
"description": "Only one outcome should be active at a time",
|
|
111
|
+
"scope": "global",
|
|
112
|
+
"check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `$metadata` fields
|
|
120
|
+
|
|
121
|
+
| Field | Type | Notes |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `hierarchy` | object | Optional per provider; at most one provider may define it after composition |
|
|
124
|
+
| `hierarchy.levels` | `(string \| HierarchyLevel)[]` | Ordered root→leaf types |
|
|
125
|
+
| `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 |
|
|
129
|
+
|
|
130
|
+
### Relationships
|
|
131
|
+
|
|
132
|
+
Relationships define links between node types that are not part of the primary structural hierarchy. They are handled during parsing and template generation.
|
|
133
|
+
|
|
134
|
+
| Field | Type | Default | Description |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| `parent` | `string` | **Required** | The parent's canonical type name |
|
|
137
|
+
| `type` | `string` | **Required** | The child's canonical type name |
|
|
138
|
+
| `field` | `string` | `"parent"` | The frontmatter field that holds the wikilink(s) for this relationship |
|
|
139
|
+
| `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"` |
|
|
141
|
+
| `matchers` | `string[]` | `[]` | Heading text to match (strings or `/regex/`). Case-insensitive. |
|
|
142
|
+
| `embeddedTemplateFields` | `string[]` | `[]` | Field names to include in templates when `format` is `"table"` |
|
|
143
|
+
| `multiple` | `boolean` | `true` | Whether multiple children are expected |
|
|
144
|
+
|
|
145
|
+
**`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.
|
|
146
|
+
|
|
147
|
+
**`fieldOn: "parent"` — parent-side array** — the parent node has an array field (`field`) holding wikilinks to child nodes. When `field` is required, it must be specified. Embedded parsing appends `[[Child]]` entries to the parent node's field array; validation checks that each entry resolves to a node of the declared child type.
|
|
148
|
+
|
|
149
|
+
**Example — child-side (default):**
|
|
150
|
+
|
|
151
|
+
```json5
|
|
152
|
+
"relationships": [
|
|
153
|
+
{
|
|
154
|
+
"parent": "opportunity",
|
|
155
|
+
"type": "assumption",
|
|
156
|
+
"format": "table",
|
|
157
|
+
"matchers": ["Assumptions", "/assum.*/"],
|
|
158
|
+
"embeddedTemplateFields": ["assumption", "status", "confidence"]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Example — parent-side array:**
|
|
164
|
+
|
|
165
|
+
```json5
|
|
166
|
+
"relationships": [
|
|
167
|
+
{
|
|
168
|
+
"parent": "activity",
|
|
169
|
+
"type": "task",
|
|
170
|
+
"field": "tasks",
|
|
171
|
+
"fieldOn": "parent",
|
|
172
|
+
"format": "list",
|
|
173
|
+
"matchers": ["Tasks"],
|
|
174
|
+
"multiple": true
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
With this configuration, embedded task items under an Activity's "Tasks" heading populate `activity.tasks` as `[[Task Title]]` wikilinks, and validation confirms each entry resolves to a `task` node.
|
|
180
|
+
|
|
181
|
+
`HierarchyLevel` options:
|
|
182
|
+
|
|
183
|
+
| Option | Default | Meaning |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `type` | required | Canonical type name |
|
|
186
|
+
| `field` | `"parent"` | Frontmatter field holding wikilink(s) |
|
|
187
|
+
| `fieldOn` | `"child"` | `"parent"` means the parent points to children |
|
|
188
|
+
| `multiple` | `false` | Field contains array of wikilinks |
|
|
189
|
+
| `selfRef` | `false` | Allows same-type parent |
|
|
190
|
+
| `selfRefField` | _undefined_ | Separate field for same-type parent links |
|
|
191
|
+
| `format` | _undefined_ | Embedding hint: `"list"`, `"table"`, or `"heading"` — enables hierarchy embedding when set |
|
|
192
|
+
| `matchers` | _undefined_ | Heading text patterns (strings or `/regex/`) to detect embedding sections |
|
|
193
|
+
| `embeddedTemplateFields` | _undefined_ | Column/field names for table stubs generated by `template-sync` |
|
|
194
|
+
|
|
195
|
+
String shorthand (`"goal"`) normalizes to:
|
|
196
|
+
`{ "type": "goal", "field": "parent", "fieldOn": "child", "multiple": false, "selfRef": false }`.
|
|
197
|
+
|
|
198
|
+
### Hierarchy embedding
|
|
199
|
+
|
|
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:
|
|
201
|
+
|
|
202
|
+
**Child-level embedding** — a typed-page heading signals that following content should produce nodes of the child type (next level in hierarchy):
|
|
203
|
+
|
|
204
|
+
```json5
|
|
205
|
+
"levels": [
|
|
206
|
+
"goal",
|
|
207
|
+
{
|
|
208
|
+
"type": "opportunity",
|
|
209
|
+
"field": "parent",
|
|
210
|
+
"fieldOn": "child",
|
|
211
|
+
"format": "list",
|
|
212
|
+
"matchers": ["Opportunities", "User Opportunities"]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
With this config, a goal page with `### Opportunities` followed by a list creates opportunity nodes without explicit `[type:: opportunity]` annotations.
|
|
218
|
+
|
|
219
|
+
**Bare wikilinks in embedded lists** — when a list item is a bare wikilink (`- [[Node Title]]`) inside an embedding section, it populates a field on the parent without creating a new node:
|
|
220
|
+
|
|
221
|
+
```markdown
|
|
222
|
+
### tool
|
|
223
|
+
- [[Zephyr]] ← populates activity.tools = ["[[Zephyr]]"]
|
|
224
|
+
- New Custom Tool ← creates a new tool node
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
This requires `fieldOn: "parent"` (the parent node holds the array field).
|
|
228
|
+
|
|
229
|
+
**Parent-level references** (`fieldOn: "child"`, child holds the field) — a heading matching the immediate parent type lets a node reference its parents by wikilink:
|
|
230
|
+
|
|
231
|
+
```json5
|
|
232
|
+
"levels": [
|
|
233
|
+
{ "type": "capability", "field": "parent", "fieldOn": "child", "multiple": false },
|
|
234
|
+
{
|
|
235
|
+
"type": "application",
|
|
236
|
+
"field": "capabilities", // application nodes have a capabilities field
|
|
237
|
+
"fieldOn": "child",
|
|
238
|
+
"multiple": true
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
With this config, an application page may include `### capability` with wikilink items to populate `application.capabilities`:
|
|
244
|
+
|
|
245
|
+
```markdown
|
|
246
|
+
## My Application ^application1
|
|
247
|
+
### capability
|
|
248
|
+
- [[Task Management]] ← populates application.capabilities
|
|
249
|
+
### tool
|
|
250
|
+
- [[Zephyr]] ← populates application.tools (fieldOn: parent)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Composition and merge semantics
|
|
254
|
+
|
|
255
|
+
Metadata is composed across the `$ref` graph with deterministic behavior:
|
|
256
|
+
|
|
257
|
+
1. Traverse external `$ref` graph in DFS order.
|
|
258
|
+
2. Apply root schema metadata last.
|
|
259
|
+
|
|
260
|
+
Merge rules:
|
|
261
|
+
- `hierarchy`: zero or one provider allowed. Multiple providers error.
|
|
262
|
+
- `aliases`: shallow merged; later provider wins per key.
|
|
263
|
+
- `rules`: merged by `id`.
|
|
264
|
+
- Duplicate rule `id` with different payload errors by default.
|
|
265
|
+
- A later rule may replace an earlier one only with `"override": true`.
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
|
|
269
|
+
### Rule imports via `$ref`
|
|
270
|
+
|
|
271
|
+
Inside `$metadata.rules`, entries can be inline rules or `$ref` imports:
|
|
272
|
+
|
|
273
|
+
```json5
|
|
274
|
+
"rules": [
|
|
275
|
+
{ "$ref": "sctx://my-pack#/$defs/workflowRule" },
|
|
276
|
+
{ "$ref": "sctx://my-pack#/$defs/ruleSet" }
|
|
277
|
+
]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Import targets may be:
|
|
281
|
+
- a single rule object
|
|
282
|
+
- an object containing `rules: []`
|
|
283
|
+
|
|
284
|
+
Imported rules are normalized into one executable flat list before validation.
|
|
285
|
+
|
|
286
|
+
### Override example
|
|
287
|
+
|
|
288
|
+
```json5
|
|
289
|
+
{
|
|
290
|
+
"$metadata": {
|
|
291
|
+
"rules": [
|
|
292
|
+
{
|
|
293
|
+
"id": "active-outcome-count",
|
|
294
|
+
"override": true,
|
|
295
|
+
"category": "workflow",
|
|
296
|
+
"description": "Require exactly one active outcome",
|
|
297
|
+
"scope": "global",
|
|
298
|
+
"check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Partials and `$ref`
|
|
306
|
+
|
|
307
|
+
- Files starting with `_` are auto-loaded partials.
|
|
308
|
+
- Both bundled partials and local schema-directory partials are registered.
|
|
309
|
+
- Local partial `$id` values must not collide with bundled IDs.
|
|
310
|
+
- `$ref` resolution is transitive across files.
|
|
311
|
+
- Partials with no `$metadata` should prefer `$schema: "http://json-schema.org/draft-07/schema#"` so they validate standalone as plain JSON Schema fragments.
|
|
312
|
+
|
|
313
|
+
## Editor expectations
|
|
314
|
+
|
|
315
|
+
Use the shipped metaschema URL in `$schema` for best cross-tool behavior.
|
|
316
|
+
|
|
317
|
+
Notes:
|
|
318
|
+
- Custom `$id` values like `sctx://...` are still supported by the CLI registry.
|
|
319
|
+
- Some generic editors may not resolve custom URI schemes for `$ref`; CLI behavior is authoritative.
|
|
320
|
+
- Do not rely on editor-only mappings for runtime correctness.
|
|
321
|
+
|
|
322
|
+
## Breaking migration checklist (legacy -> current)
|
|
323
|
+
|
|
324
|
+
For schemas migrating from older metadata structure:
|
|
325
|
+
|
|
326
|
+
1. Move any legacy metadata from `$defs._metadata` to top-level `$metadata`.
|
|
327
|
+
2. Convert `hierarchy` array to `hierarchy.levels` object shape.
|
|
328
|
+
3. Move `allowSkipLevels` under `hierarchy`.
|
|
329
|
+
4. Convert grouped rule containers to flat `rules[]` with per-rule `category`.
|
|
330
|
+
5. If duplicate rule IDs are intentional, mark later rules with `override: true`.
|
|
331
|
+
6. Re-run `bunx structured-context schemas show --space <name>` and `validate` to confirm merged metadata/rules.
|
|
332
|
+
|
|
333
|
+
## JSON5 support
|
|
334
|
+
|
|
335
|
+
Schema files are parsed as JSON5 (`//` comments and trailing commas are allowed).
|
|
336
|
+
|
|
337
|
+
## Further reading
|
|
338
|
+
|
|
339
|
+
- [Executable Rules](rules.md)
|
|
340
|
+
- [JSON Schema](https://json-schema.org/)
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "structured-context",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Tools for working with Opportunity Solution Tree structures and other product management and strategy frameworks",
|
|
5
|
+
"module": "./dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"bin": {
|
|
9
|
+
"sctx": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"schemas/",
|
|
14
|
+
"docs/concepts.md",
|
|
15
|
+
"docs/config.md",
|
|
16
|
+
"docs/schemas.md",
|
|
17
|
+
"docs/rules.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.js",
|
|
21
|
+
"./plugin-api": {
|
|
22
|
+
"types": "./dist/plugin-api.d.ts",
|
|
23
|
+
"default": "./dist/plugin-api.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"generate:schema-meta": "bun run scripts/generate-schema-meta.ts",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"build": "bun run clean && bun run generate:schema-meta && tsc -p tsconfig.build.json && cp -r schemas dist/ && chmod +x dist/index.js",
|
|
31
|
+
"prepublishOnly": "bun run build",
|
|
32
|
+
"validate": "bun run src/index.ts validate",
|
|
33
|
+
"diagram": "bun run src/index.ts diagram",
|
|
34
|
+
"test": "bun test tests/",
|
|
35
|
+
"test:smoke": "bun test smoke/",
|
|
36
|
+
"test:hook": "bun test hook-test/unit/",
|
|
37
|
+
"test:hook:e2e": "bun test hook-test/hooks.test.ts",
|
|
38
|
+
"test:all": "bun test",
|
|
39
|
+
"lint": "biome check",
|
|
40
|
+
"lint:fix": "biome check --write",
|
|
41
|
+
"preversion": "bun run generate:schema-meta && bun run lint:fix && bun run test",
|
|
42
|
+
"version": "git add -A",
|
|
43
|
+
"postversion": "git push --follow-tags"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
47
|
+
"@biomejs/biome": "^2.4.10",
|
|
48
|
+
"@types/bun": "latest",
|
|
49
|
+
"@types/js-yaml": "^4.0.9",
|
|
50
|
+
"json-schema-to-ts": "^3.1.1",
|
|
51
|
+
"lefthook": "^2.1.4"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"typescript": "^5"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"ajv": "^8.18.0",
|
|
58
|
+
"chokidar": "^5.0.0",
|
|
59
|
+
"commander": "^14.0.3",
|
|
60
|
+
"gray-matter": "^4.0.3",
|
|
61
|
+
"js-yaml": "^4.1.1",
|
|
62
|
+
"jsonata": "^2.1.0",
|
|
63
|
+
"mdast-util-to-string": "^4.0.0",
|
|
64
|
+
"remark-gfm": "^4.0.1",
|
|
65
|
+
"remark-parse": "^11.0.0",
|
|
66
|
+
"unified": "^11.0.5",
|
|
67
|
+
"unist-util-visit": "^5.1.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json",
|
|
3
|
+
"$id": "sctx://_ost_strict",
|
|
4
|
+
// Shared definitions for the strict OST (Opportunity Solution Tree) schema.
|
|
5
|
+
// This schema follows Teresa Torres' canonical 4-level structure as described in
|
|
6
|
+
// "Continuous Discovery Habits" (2021) and at producttalk.org.
|
|
7
|
+
"$metadata": {
|
|
8
|
+
"hierarchy": {
|
|
9
|
+
"levels": ["outcome", { "type": "opportunity", "selfRef": true }, "solution", "assumption_test"]
|
|
10
|
+
},
|
|
11
|
+
"rules": [
|
|
12
|
+
{
|
|
13
|
+
"id": "active-outcome-count",
|
|
14
|
+
"category": "workflow",
|
|
15
|
+
"description": "Only one outcome should be active at a time",
|
|
16
|
+
"scope": "global",
|
|
17
|
+
"check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "active-opportunity-count",
|
|
21
|
+
"category": "workflow",
|
|
22
|
+
"description": "Only one target opportunity should be active at a time",
|
|
23
|
+
"scope": "global",
|
|
24
|
+
"check": "$count(nodes[resolvedType='opportunity' and status='active']) <= 1"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "active-node-parent-active",
|
|
28
|
+
"category": "workflow",
|
|
29
|
+
"description": "An active node's parent should also be active",
|
|
30
|
+
"check": "current.status != 'active' or $exists(parent) = false or parent.status = 'active'"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "solution-quantity",
|
|
34
|
+
"category": "best-practice",
|
|
35
|
+
"description": "Explore multiple candidate solutions (aim for at least three) for the target opportunity",
|
|
36
|
+
"type": "opportunity",
|
|
37
|
+
"check": "(current.status != 'exploring' and current.status != 'active') or $count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution']) >= 3"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"$defs": {
|
|
42
|
+
"outcomeProps": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"description": "Properties for an Outcome node (product metric, not vision/mission)",
|
|
45
|
+
"properties": {
|
|
46
|
+
"metric": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "The specific product metric to move (e.g., 'Increase % of first-time users who reach the aha moment')"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"required": ["metric"]
|
|
52
|
+
},
|
|
53
|
+
"opportunityProps": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"description": "Properties for an Opportunity, emphasizing research grounding",
|
|
56
|
+
"properties": {
|
|
57
|
+
"source": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Customer research source that grounded this opportunity (e.g., 'Interview with Jane, 2024-03-15')"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"required": ["source"]
|
|
63
|
+
},
|
|
64
|
+
"assumptionTestProps": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"description": "Properties for an Assumption Test (the fourth level of the OST)",
|
|
67
|
+
"properties": {
|
|
68
|
+
"assumption": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "The specific belief being tested"
|
|
71
|
+
},
|
|
72
|
+
"category": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"enum": ["desirability", "viability", "feasibility", "usability", "ethical"],
|
|
75
|
+
"description": "Assumption type categories"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"required": ["assumption"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "sctx://_sctx_base",
|
|
4
|
+
"description": "Shared definitions for structured-context schemas",
|
|
5
|
+
"$defs": {
|
|
6
|
+
"title": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"description": "Title of the node"
|
|
9
|
+
},
|
|
10
|
+
"content": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Body content"
|
|
13
|
+
},
|
|
14
|
+
"summary": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Short summary"
|
|
17
|
+
},
|
|
18
|
+
"status": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"enum": ["identified", "wondering", "exploring", "active", "paused", "completed", "archived"]
|
|
21
|
+
},
|
|
22
|
+
"priority": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": ["p1", "p2", "p3", "p4"]
|
|
25
|
+
},
|
|
26
|
+
"assessment": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"minimum": 1,
|
|
29
|
+
"maximum": 5,
|
|
30
|
+
"description": "Assessment score from 1-5"
|
|
31
|
+
},
|
|
32
|
+
"wikilink": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"pattern": "^\\[\\[.+\\]\\]$",
|
|
35
|
+
"description": "An Obsidian wikilink, e.g. [[Parent Node]]"
|
|
36
|
+
},
|
|
37
|
+
"baseNodeProps": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"title": { "$ref": "#/$defs/title" },
|
|
41
|
+
"content": { "$ref": "#/$defs/content" },
|
|
42
|
+
"tags": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": { "type": "string" },
|
|
45
|
+
"description": "Categorization tags"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"required": ["title"]
|
|
49
|
+
},
|
|
50
|
+
"ostEntityProps": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"status": { "$ref": "#/$defs/status" },
|
|
54
|
+
"summary": { "$ref": "#/$defs/summary" },
|
|
55
|
+
"status_tweet": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Succinct free-text status summary"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"required": ["status"]
|
|
61
|
+
},
|
|
62
|
+
"parentNodeProps": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"parent": {
|
|
66
|
+
"$ref": "#/$defs/wikilink",
|
|
67
|
+
"description": "Parent node"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|