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/README.md
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# structured-context
|
|
2
|
+
|
|
3
|
+
Tools for working with Opportunity Solution Tree structures and other product management and strategy frameworks
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Requires [Bun](https://bun.sh) runtime.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install -g structured-context
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or use directly via `bunx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bunx structured-context validate <space>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup for AI Agents
|
|
20
|
+
|
|
21
|
+
A Claude Code plugin is included at `plugin/`. It provides validation hooks, slash commands, and agent skills. Install it with:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
claude plugin install mindsocket/structured-context
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Skills can also be installed standalone without the plugin:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx skills add https://github.com/mindsocket/structured-context/tree/main/plugin/skills/structured-context
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Concepts
|
|
34
|
+
|
|
35
|
+
See [docs/concepts.md](docs/concepts.md) for the full terminology reference, including definitions of nodes, embedded nodes, spaces, schemas, rules, and more.
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
`structured-context` looks for its config file in this order:
|
|
40
|
+
|
|
41
|
+
1. `$SCTX_CONFIG` — explicit path override
|
|
42
|
+
2. `~/.config/structured-context/config.json` (or `$XDG_CONFIG_HOME/structured-context/config.json`)
|
|
43
|
+
3. `./config.json` in the current working directory
|
|
44
|
+
|
|
45
|
+
See `config.example.json` for the full structure. The config maps space names to paths, with optional Miro integration fields and global defaults. Paths in config files are resolved relative to the config file.
|
|
46
|
+
|
|
47
|
+
**Including spaces from other configs:** Use `includeSpacesFrom` to import space definitions from other config files. This is useful for aggregating spaces from multiple projects into a central config, reducing the need to specify `--config` on CLI commands. Duplicate space names are not allowed.
|
|
48
|
+
|
|
49
|
+
**Plugins and markdown plugin config:** See `sctx docs config` for the full reference including `fieldMap`, `typeInference`, `templateDir`, filter views, and plugin loading rules.
|
|
50
|
+
|
|
51
|
+
### Spaces
|
|
52
|
+
|
|
53
|
+
A space is a named directory or single file registered in the config. Spaces let you reference content by name instead of path:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sctx validate ProductX
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Schemas
|
|
60
|
+
|
|
61
|
+
Schemas define the structure and rules for the entities in a space, allowing customisation and extension to different models.
|
|
62
|
+
|
|
63
|
+
Two schemas (`general` and `strict_ost`) are included. The general schema combines a basic vision/mission/goals hierarchy with a hierarchy loosely based on Opportunity Solution Trees. It is intentionally flexible to support rapid initial adoption. The strict OST schema has a narrower scope, and reflects Teresa Torres' specific recommendations for Opportunity Solution Trees more closely.
|
|
64
|
+
|
|
65
|
+
sctx schemas use a metaschema based on JSON Schema Draft-07 that adds a top-level `$metadata` block:
|
|
66
|
+
|
|
67
|
+
```json5
|
|
68
|
+
"$metadata": {
|
|
69
|
+
"hierarchy": {
|
|
70
|
+
"levels": ["outcome", { "type": "opportunity", "selfRef": true }, "solution", "assumption_test"],
|
|
71
|
+
"allowSkipLevels": false
|
|
72
|
+
},
|
|
73
|
+
"relationships": [
|
|
74
|
+
{
|
|
75
|
+
"parent": "opportunity",
|
|
76
|
+
"type": "assumption",
|
|
77
|
+
"templateFormat": "table",
|
|
78
|
+
"matchers": ["Assumptions"]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"aliases": { "experiment": "assumption_test" },
|
|
82
|
+
"rules": [
|
|
83
|
+
{
|
|
84
|
+
"id": "active-outcome-count",
|
|
85
|
+
"category": "workflow",
|
|
86
|
+
"description": "Only one outcome should be active at a time",
|
|
87
|
+
"scope": "global",
|
|
88
|
+
"check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Rules are a flat array (`rules[]`) with per-rule `category`.
|
|
95
|
+
|
|
96
|
+
Schema hierarchy levels support DAG (multi-parent) relationships via configurable edge fields. Each entry in `$metadata.hierarchy.levels` can be a plain type name string (defaults to `parent` field on child nodes) or an object:
|
|
97
|
+
|
|
98
|
+
```json5
|
|
99
|
+
// Example fragments for hierarchy level objects:
|
|
100
|
+
{ "type": "opportunity", "selfRef": true }
|
|
101
|
+
{ "type": "solution", "field": "fulfills", "multiple": true }
|
|
102
|
+
{ "type": "requirement", "field": "generates", "fieldOn": "parent", "multiple": true }
|
|
103
|
+
{ "type": "solution", "field": "solutions", "fieldOn": "parent", "multiple": true, "selfRefField": "parent" }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Property | Default | Description |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `type` | required | The node type at this hierarchy level |
|
|
109
|
+
| `field` | `"parent"` | Name of the edge field |
|
|
110
|
+
| `fieldOn` | `"child"` | Which side holds the field: `"child"` (child points up) or `"parent"` (parent points down) |
|
|
111
|
+
| `multiple` | `false` | Whether the field is an array of wikilinks (enables multi-parent DAG) |
|
|
112
|
+
| `selfRef` | `false` | Whether a node of this type may reference a same-type parent |
|
|
113
|
+
| `selfRefField` | _undefined_ | Optional field for same-type parent relationships (always on child-side and singular) |
|
|
114
|
+
| `templateFormat` | _undefined_ | Embedding hint (`"list"`, `"table"`, `"heading"`). When set alongside `matchers`, enables hierarchy embedding in typed pages |
|
|
115
|
+
| `matchers` | _undefined_ | Heading patterns (strings or `/regex/`) to match for hierarchy embedding. Case-insensitive. |
|
|
116
|
+
| `embeddedTemplateFields` | _undefined_ | Column names for table stubs when `template-sync` generates templates |
|
|
117
|
+
|
|
118
|
+
The `selfRefField` property enables different fields for regular vs same-type relationships. For example, requirements can list solutions via `solutions` on the requirement node, while solutions can reference parent solutions via `parent` on the solution node.
|
|
119
|
+
|
|
120
|
+
**Hierarchy embedding** — when `templateFormat` and `matchers` are set on a level, typed pages may include section headings that signal embedded content for that type without explicit `[type:: x]` annotations. Two patterns:
|
|
121
|
+
|
|
122
|
+
- **Child-level**: heading matches the child level's type/matchers → list or table items create child nodes.
|
|
123
|
+
- **Parent-level references**: heading matches the *parent* level's type/matchers → bare wikilink items (`- [[X]]`) populate the current node's reference field rather than creating new nodes. Useful for listing parent relationships inline.
|
|
124
|
+
|
|
125
|
+
Bare wikilink items (`- [[Existing Node]]`) in any embedding section populate a field rather than creating a new node.
|
|
126
|
+
|
|
127
|
+
**Adjacent Relationships** (`$metadata.relationships`) define connections between types outside the primary hierarchy — such as an `activity` having many `task` nodes. They drive embedded parsing (typed headings, lists, tables) and template generation.
|
|
128
|
+
|
|
129
|
+
| Property | Default | Description |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `parent` | required | Parent canonical type |
|
|
132
|
+
| `type` | required | Child canonical type |
|
|
133
|
+
| `field` | `"parent"` | Frontmatter field holding the wikilink(s). Required when `fieldOn: "parent"`. |
|
|
134
|
+
| `fieldOn` | `"child"` | `"child"`: child holds a link pointing up. `"parent"`: parent holds an array of child links. |
|
|
135
|
+
| `templateFormat` | `"page"` | Hint for `template-sync`: `"table"`, `"list"`, or `"heading"` |
|
|
136
|
+
| `matchers` | `[]` | Heading text to match for embedded parsing (strings or `/regex/`). Case-insensitive. |
|
|
137
|
+
| `multiple` | `true` | Whether multiple children are expected |
|
|
138
|
+
| `embeddedTemplateFields` | `[]` | Field names to include as table columns in templates |
|
|
139
|
+
|
|
140
|
+
With `fieldOn: "parent"`, embedded child nodes (parsed from a matching heading's list or table) are appended as wikilinks to the parent's `field` array, rather than receiving a `parent` field. This matches schemas where the content model naturally lists children on the parent (e.g. `activity.tasks: ["[[Task A]]"]`).
|
|
141
|
+
|
|
142
|
+
Metadata is composable across `$ref` graphs:
|
|
143
|
+
- zero or one metadata provider may define `hierarchy`
|
|
144
|
+
- `aliases` are shallow-merged (later wins)
|
|
145
|
+
- `rules` merge by `id`; conflicts error unless the later rule sets `override: true`
|
|
146
|
+
- `$metadata.rules` supports `$ref` imports for reusable rule packs
|
|
147
|
+
|
|
148
|
+
If no provider defines `hierarchy`, hierarchy-specific checks are skipped. Reading a `space_on_a_page` file still requires `hierarchy.levels`.
|
|
149
|
+
|
|
150
|
+
**Customizing Schemas:**
|
|
151
|
+
- **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`) are loaded and used to resolve references (using `$ref`).
|
|
152
|
+
- **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "http://json-schema.org/draft-07/schema#"` so it validates standalone as plain JSON Schema.
|
|
153
|
+
- **Loading priority**: Partial schemas are loaded from both the default schema directory and the directory of your specified target schema.
|
|
154
|
+
- **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
|
|
155
|
+
- **Unique IDs**: To encourage clean namespacing, local partial schemas **must** have unique `$id`s that do not collide with the default schemas. If a collision is detected, validation will fail with an error.
|
|
156
|
+
|
|
157
|
+
Schema resolution order: space config `schema` > global config `schema` > bundled `schemas/general.json`
|
|
158
|
+
|
|
159
|
+
**⚠️ Security Notice: Only use schemas and configuration files from trusted sources.**
|
|
160
|
+
|
|
161
|
+
The tool executes JSONata expressions defined in schema files for rule validation. A maliciously crafted schema could make JSONata access JavaScript's prototype chain and execute arbitrary code. Only use schemas you've created or reviewed personally.
|
|
162
|
+
|
|
163
|
+
## Usage
|
|
164
|
+
|
|
165
|
+
### Validate nodes
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
sctx validate <space> [--watch]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Validates markdown files against the JSON schema:
|
|
172
|
+
- Extracts YAML frontmatter from each `.md` file
|
|
173
|
+
- Skips files without frontmatter or without a `type` field
|
|
174
|
+
- Reports validation results with counts and per-file errors
|
|
175
|
+
|
|
176
|
+
### Show space tree
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
sctx show <space> [--filter <view-or-expression>]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Prints the space as an indented hierarchy tree. Hierarchy roots are listed first, followed by orphans (nodes in the hierarchy but with no resolved parent) and non-hierarchy nodes.
|
|
183
|
+
|
|
184
|
+
When a node appears under multiple parents (DAG hierarchy), it is printed in full under its first parent. Subsequent appearances with children show a `(*)` marker indicating the subtree is omitted.
|
|
185
|
+
|
|
186
|
+
**Filtering:** The `--filter` flag accepts either a named view from the space config, or an inline filter expression. Only nodes matching the expression are shown.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Inline expression
|
|
190
|
+
sctx show <space> --filter "WHERE resolvedType='solution' and status='active'"
|
|
191
|
+
|
|
192
|
+
# Named view from config
|
|
193
|
+
sctx show <space> --filter active-solutions
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
See [Filter expressions](#filter-expressions) below for expression syntax.
|
|
197
|
+
|
|
198
|
+
### Filter expressions
|
|
199
|
+
|
|
200
|
+
Filter expressions are used with `--filter` and in config `views`. They use a `SELECT ... WHERE ...` pseudo-DSL:
|
|
201
|
+
|
|
202
|
+
| Form | Meaning |
|
|
203
|
+
|------|---------|
|
|
204
|
+
| `WHERE {jsonata}` | Return nodes where the JSONata predicate is truthy |
|
|
205
|
+
| `SELECT {spec} WHERE {jsonata}` | Filter by WHERE, then expand result via SELECT |
|
|
206
|
+
| `SELECT {spec}` | Expand from all nodes via SELECT (no WHERE filter — returns all nodes, expanded per spec) |
|
|
207
|
+
| `{jsonata}` | Bare JSONata, treated as a WHERE predicate (convenience shorthand) |
|
|
208
|
+
|
|
209
|
+
The WHERE predicate is a [JSONata](https://docs.jsonata.org/overview) expression evaluated per node. Within the expression, each node's fields are accessible directly (e.g. `resolvedType`, `status`, any schema fields like `title`). Two built-in fields are always available regardless of schema: `label` (relative file path, e.g. `"solutions/My Solution.md"`) and `title` (node display name). Additionally, two pre-computed traversal arrays are available:
|
|
210
|
+
|
|
211
|
+
- **`ancestors[]`** — flat array of ancestor nodes, nearest first, deduplicated. Each entry includes all schema fields of the ancestor node, plus:
|
|
212
|
+
- `_field` — the edge field name that connects to the ancestor
|
|
213
|
+
- `_source` — `'hierarchy'` or `'relationship'`
|
|
214
|
+
- `_selfRef` — whether the edge is a same-type (self-referential) link
|
|
215
|
+
- **`descendants[]`** — same structure, for descendant nodes
|
|
216
|
+
|
|
217
|
+
**SELECT spec** expands the result set by walking the graph from matched nodes. The spec is a comma-separated list of directives:
|
|
218
|
+
|
|
219
|
+
| Directive | Meaning |
|
|
220
|
+
|-----------|---------|
|
|
221
|
+
| `ancestors` | All ancestor nodes |
|
|
222
|
+
| `ancestors(type)` | Ancestors of the given resolved type |
|
|
223
|
+
| `descendants` | All descendant nodes |
|
|
224
|
+
| `descendants(type)` | Descendants of the given resolved type |
|
|
225
|
+
| `siblings` | Nodes sharing at least one parent with matched nodes |
|
|
226
|
+
| `relationships` | All nodes connected via a relationship (non-hierarchy) edge |
|
|
227
|
+
| `relationships(childType)` | Relationship-connected nodes of the given child type |
|
|
228
|
+
| `relationships(parentType:childType)` | As above, also filtering by parent type |
|
|
229
|
+
| `relationships(parentType:field:childType)` | Fully qualified: also filtering by edge field name |
|
|
230
|
+
|
|
231
|
+
Multiple directives may be combined: `SELECT ancestors(goal), siblings WHERE ...`
|
|
232
|
+
|
|
233
|
+
**Examples:**
|
|
234
|
+
|
|
235
|
+
```jsonata
|
|
236
|
+
// All solutions
|
|
237
|
+
WHERE resolvedType='solution'
|
|
238
|
+
|
|
239
|
+
// Active solutions only
|
|
240
|
+
WHERE resolvedType='solution' and status='active'
|
|
241
|
+
|
|
242
|
+
// Solutions whose nearest opportunity ancestor is active
|
|
243
|
+
WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])
|
|
244
|
+
|
|
245
|
+
// Nodes that have any ancestor goal
|
|
246
|
+
WHERE $exists(ancestors[resolvedType='goal'])
|
|
247
|
+
|
|
248
|
+
// Bare JSONata shorthand (no WHERE keyword)
|
|
249
|
+
resolvedType='solution' and status='active'
|
|
250
|
+
|
|
251
|
+
// Solutions + their opportunity ancestors
|
|
252
|
+
SELECT ancestors(opportunity) WHERE resolvedType='solution'
|
|
253
|
+
|
|
254
|
+
// Solutions + their siblings (other solutions under same opportunity)
|
|
255
|
+
SELECT siblings WHERE resolvedType='solution' and status='active'
|
|
256
|
+
|
|
257
|
+
// Opportunities + their related assumptions
|
|
258
|
+
SELECT relationships(assumption) WHERE resolvedType='opportunity'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Generate Mermaid diagram
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
sctx diagram <space> [--output path/to/output.mmd]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Generates a Mermaid `graph TD` diagram from validated space nodes:
|
|
268
|
+
- Uses parent→child relationships from wikilinks
|
|
269
|
+
- Applies type-based styling (different colours per node type and status)
|
|
270
|
+
- Handles orphan nodes (no parent) as a separate cluster
|
|
271
|
+
- Outputs to file or stdout
|
|
272
|
+
|
|
273
|
+
### Show schema ERD
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
sctx schemas show <schema-file> [--mermaid-erd] [--space <name>]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Generates a Mermaid Entity Relationship Diagram from a schema:
|
|
280
|
+
- Shows all entity types and their properties
|
|
281
|
+
- Displays parent-child relationships based on hierarchy metadata
|
|
282
|
+
- Useful for visualizing schema structure during development
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
```bash
|
|
286
|
+
sctx schemas show general --mermaid-erd
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Sync space to Miro
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
sctx miro-sync <space> [--new-frame <title>] [--dry-run] [--verbose]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Syncs space nodes to a Miro board as cards with connectors. Requires `MIRO_TOKEN` env var and `miroBoardId` set in the space's config entry.
|
|
296
|
+
|
|
297
|
+
- `--new-frame <title>` — create a new frame on the board and sync into it; auto-saves the resulting `miroFrameId` back to the config file
|
|
298
|
+
- `--dry-run` — show what would change without touching Miro
|
|
299
|
+
- `--verbose` / `-v` — detailed per-card and per-connector output
|
|
300
|
+
|
|
301
|
+
On subsequent runs, the cached `miroFrameId` is used automatically. Cards are colour-coded by node type and linked by parent→child connectors. A local `.miro-cache/` directory tracks Miro IDs to enable incremental updates.
|
|
302
|
+
|
|
303
|
+
Sync is one-way (OST → Miro) and scoped to a single frame. Only cards and connectors created by this tool within that frame are managed — everything else on the board is left untouched. Card content and connectors are overwritten or recreated to match the markdown source; any edits made directly in Miro to managed cards will be lost on the next sync. Existing card positions are not changed.
|
|
304
|
+
|
|
305
|
+
### Sync templates with schema
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
sctx template-sync <space> [--create-missing] [--dry-run]
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Keeps Obsidian template files in sync with schema examples:
|
|
312
|
+
- Matches markdown files in the template directory (defined in config) by `type` field
|
|
313
|
+
- Rewrites frontmatter using description fields and property `examples`
|
|
314
|
+
- `templatePrefix` in `plugins.markdown` config (default blank) sets a naming convention for templates (`{templatePrefix}{type}.md`). This will be used to check existing filenames, and create new templates with `--create-missing`.
|
|
315
|
+
- `--dry-run` previews changes without writing files
|
|
316
|
+
|
|
317
|
+
## Development
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
# Run a command against a configured space
|
|
321
|
+
bun run src/index.ts validate personal
|
|
322
|
+
|
|
323
|
+
# Run type checking (checks all code including tests)
|
|
324
|
+
bun run typecheck
|
|
325
|
+
|
|
326
|
+
# Run core unit tests
|
|
327
|
+
bun run test
|
|
328
|
+
|
|
329
|
+
# Run occasional smoke tests against all locally configured spaces
|
|
330
|
+
bun run test:smoke
|
|
331
|
+
|
|
332
|
+
# Build compiled output
|
|
333
|
+
bun run build
|
|
334
|
+
|
|
335
|
+
# Link built package locally so `bunx structured-context` picks up changes
|
|
336
|
+
bun link
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Releasing
|
|
340
|
+
```bash
|
|
341
|
+
npm login # authenticate with npm registry if needed
|
|
342
|
+
bun pm version patch # or minor / major — runs lint, tests, then pushes with tags
|
|
343
|
+
npm publish
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { executeRender } from '../render/render';
|
|
3
|
+
export async function diagram(context, options) {
|
|
4
|
+
const result = await executeRender('markdown.mermaid', context, { filter: options.filter });
|
|
5
|
+
if (options.output) {
|
|
6
|
+
writeFileSync(options.output, result);
|
|
7
|
+
console.log(`Mermaid diagram written to ${options.output}`);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.log(result);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function docs(topic?: string): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const TOPICS = {
|
|
4
|
+
concepts: 'concepts.md',
|
|
5
|
+
config: 'config.md',
|
|
6
|
+
schema: 'schemas.md',
|
|
7
|
+
rules: 'rules.md',
|
|
8
|
+
};
|
|
9
|
+
export function docs(topic) {
|
|
10
|
+
let filePath;
|
|
11
|
+
if (!topic) {
|
|
12
|
+
filePath = join(import.meta.dir, '..', '..', 'README.md');
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const file = TOPICS[topic];
|
|
16
|
+
if (!file) {
|
|
17
|
+
const available = Object.keys(TOPICS).join(', ');
|
|
18
|
+
console.error(`Unknown topic "${topic}". Available: ${available}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
filePath = join(import.meta.dir, '..', '..', 'docs', file);
|
|
22
|
+
}
|
|
23
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
24
|
+
const cols = process.stdout.columns ?? 80;
|
|
25
|
+
const rendered = Bun.markdown.render(content, {
|
|
26
|
+
heading: (children, { level }) => {
|
|
27
|
+
const prefix = '#'.repeat(level);
|
|
28
|
+
if (level === 1)
|
|
29
|
+
return `\x1b[1;4m${prefix} ${children}\x1b[0m\n\n`;
|
|
30
|
+
if (level === 2)
|
|
31
|
+
return `\n\x1b[1m${prefix} ${children}\x1b[0m\n${'─'.repeat(Math.min(children.length + level + 1, cols))}\n`;
|
|
32
|
+
return `\n\x1b[1m${prefix} ${children}\x1b[0m\n`;
|
|
33
|
+
},
|
|
34
|
+
paragraph: (children) => `${children}\n\n`,
|
|
35
|
+
strong: (children) => `\x1b[1m**${children}**\x1b[22m`,
|
|
36
|
+
emphasis: (children) => `\x1b[3m*${children}*\x1b[23m`,
|
|
37
|
+
codespan: (children) => `\x1b[96m\`${children}\`\x1b[39m`,
|
|
38
|
+
code: (children, meta) => {
|
|
39
|
+
const lang = meta?.language ?? '';
|
|
40
|
+
return `\x1b[96m\`\`\`${lang}\n${children}\`\`\`\x1b[0m\n`;
|
|
41
|
+
},
|
|
42
|
+
blockquote: (children) => `${children
|
|
43
|
+
.split('\n')
|
|
44
|
+
.map((l) => `\x1b[2m> ${l}\x1b[0m`)
|
|
45
|
+
.join('\n')}\n`,
|
|
46
|
+
table: (children) => `${children}\n`,
|
|
47
|
+
thead: (children) => {
|
|
48
|
+
const colCount = (children.split('\n')[0] ?? '').split('|').length - 2;
|
|
49
|
+
const sep = `\x1b[2m| ${Array(colCount).fill('---').join(' | ')} |\x1b[0m\n`;
|
|
50
|
+
return `${children}${sep}`;
|
|
51
|
+
},
|
|
52
|
+
tbody: (children) => children,
|
|
53
|
+
tr: (children) => `${children}|\n`,
|
|
54
|
+
th: (children) => `| \x1b[1m${children}\x1b[22m `,
|
|
55
|
+
td: (children) => `| ${children} `,
|
|
56
|
+
list: (children) => `${children}\n`,
|
|
57
|
+
listItem: (children, meta) => {
|
|
58
|
+
const { depth = 0, ordered = false, index = 0, } = meta;
|
|
59
|
+
const indent = ' '.repeat(depth);
|
|
60
|
+
const bullet = ordered ? `${index + 1}.` : '-';
|
|
61
|
+
return `${indent}${bullet} ${children.trimEnd()}\n`;
|
|
62
|
+
},
|
|
63
|
+
hr: () => `\x1b[2m---\x1b[0m\n`,
|
|
64
|
+
link: (children, { href }) => children === href ? `\x1b[4;34m${href}\x1b[0m` : `[${children}](\x1b[4;34m${href}\x1b[0m)`,
|
|
65
|
+
});
|
|
66
|
+
process.stdout.write(rendered);
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listPlugins(): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { builtinPlugins } from '../plugins';
|
|
2
|
+
import { discoverPlugins } from '../plugins/loader';
|
|
3
|
+
function showConfigSchema(plugin) {
|
|
4
|
+
console.log(JSON.stringify(plugin.configSchema, null, 2));
|
|
5
|
+
}
|
|
6
|
+
export async function listPlugins() {
|
|
7
|
+
const builtinNames = new Set(builtinPlugins.map((p) => p.name));
|
|
8
|
+
const plugins = await discoverPlugins();
|
|
9
|
+
const builtins = plugins.filter((p) => builtinNames.has(p.name));
|
|
10
|
+
const external = plugins.filter((p) => !builtinNames.has(p.name));
|
|
11
|
+
console.log('Built-in plugins:');
|
|
12
|
+
for (const plugin of builtins) {
|
|
13
|
+
console.log(` ${plugin.name}`);
|
|
14
|
+
showConfigSchema(plugin);
|
|
15
|
+
}
|
|
16
|
+
if (external.length > 0) {
|
|
17
|
+
console.log('\nConfig-adjacent plugins:');
|
|
18
|
+
for (const plugin of external) {
|
|
19
|
+
console.log(` ${plugin.name}`);
|
|
20
|
+
showConfigSchema(plugin);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { discoverPlugins, loadPlugins } from '../plugins/loader';
|
|
3
|
+
import { buildFormatRegistry } from '../render/registry';
|
|
4
|
+
import { executeRender } from '../render/render';
|
|
5
|
+
export async function render(context, format, options) {
|
|
6
|
+
const result = await executeRender(format, context, { filter: options.filter });
|
|
7
|
+
if (options.output) {
|
|
8
|
+
writeFileSync(options.output, result);
|
|
9
|
+
console.error(`Written to ${options.output}`);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
process.stdout.write(result);
|
|
13
|
+
if (!result.endsWith('\n'))
|
|
14
|
+
process.stdout.write('\n');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function renderList(context) {
|
|
18
|
+
let loaded;
|
|
19
|
+
if (context) {
|
|
20
|
+
const pluginMap = context.space?.plugins ?? {};
|
|
21
|
+
loaded = await loadPlugins(pluginMap, context.configDir);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const discovered = await discoverPlugins();
|
|
25
|
+
loaded = discovered.map((plugin) => ({ plugin, pluginConfig: {} }));
|
|
26
|
+
}
|
|
27
|
+
const registry = buildFormatRegistry(loaded);
|
|
28
|
+
if (registry.length === 0) {
|
|
29
|
+
console.log('No render formats available.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const entry of registry) {
|
|
33
|
+
console.log(` ${entry.qualifiedName.padEnd(24)} ${entry.format.description}`);
|
|
34
|
+
}
|
|
35
|
+
}
|