structured-context 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -22
- package/dist/api.d.ts +7 -5
- package/dist/api.js +4 -1
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +114 -22
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -2
- package/dist/commands/validate-file.js +7 -4
- package/dist/commands/validate.js +38 -21
- package/dist/config.d.ts +18 -1
- package/dist/config.js +81 -10
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.js +39 -15
- package/dist/load-space-graph.d.ts +4 -2
- package/dist/load-space-graph.js +15 -4
- package/dist/plugins/loader.js +2 -2
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
- package/dist/plugins/markdown/parse-embedded.js +41 -7
- package/dist/plugins/markdown/read-space.js +38 -5
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +343 -187
- package/dist/schema/metadata-contract.js +68 -30
- package/dist/schema/schema.d.ts +14 -8
- package/dist/schema/schema.js +277 -198
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +5 -1
- package/dist/schema/validate-rules.js +20 -1
- package/dist/schemas/_knowledge_wiki.json +20 -20
- package/dist/schemas/_ost_strict.json +2 -2
- package/dist/schemas/_sctx_base.json +2 -2
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +3 -0
- package/dist/space-context.js +7 -2
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +54 -13
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +19 -9
- package/docs/concepts.md +14 -4
- package/docs/config.md +8 -0
- package/docs/rules.md +49 -22
- package/docs/schemas.md +110 -35
- package/package.json +13 -8
- package/schemas/_knowledge_wiki.json +20 -20
- package/schemas/_ost_strict.json +2 -2
- package/schemas/_sctx_base.json +2 -2
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
package/README.md
CHANGED
|
@@ -28,10 +28,13 @@ Content and structure
|
|
|
28
28
|
Schema validation errors 0
|
|
29
29
|
Broken links 1
|
|
30
30
|
Duplicate keys 0
|
|
31
|
-
Rule violations
|
|
31
|
+
Rule violations (error) 0
|
|
32
|
+
Rule violations (warning) 1
|
|
33
|
+
Rule violations (info) 1
|
|
32
34
|
Hierarchy violations 0
|
|
33
35
|
Orphans (hierarchy nodes - no parent) 1
|
|
34
|
-
|
|
36
|
+
Unresolved content links 0
|
|
37
|
+
Excluded during parsing 0
|
|
35
38
|
|
|
36
39
|
Orphans (hierarchy nodes - no parent):
|
|
37
40
|
Solution looking for a problem.md
|
|
@@ -40,13 +43,14 @@ Broken links:
|
|
|
40
43
|
Steal underpants.md: [[Gnome goals]] → Link target "Gnome goals" in field "related" not found
|
|
41
44
|
|
|
42
45
|
Rule violations:
|
|
43
|
-
|
|
44
|
-
Build Death Star.md: Opportunities should contribute to at least one Outcome
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
High tech juice press.md: Explore multiple candidate solutions (aim for at least three) for an active target opportunity
|
|
46
|
+
WARNING (1):
|
|
47
|
+
Build Death Star.md: Opportunities should contribute to at least one Outcome [workflow: opportunity-has-outcome]
|
|
48
|
+
INFO (1):
|
|
49
|
+
High tech juice press.md: Explore multiple candidate solutions (aim for at least three) for the target opportunity [best-practice: solution-quantity]
|
|
48
50
|
```
|
|
49
51
|
|
|
52
|
+
Rule violations carry a severity (`error`, `warning` or `info`) that defaults from the rule's category. Only errors fail validation — see [docs/rules.md](docs/rules.md#categories-and-severity).
|
|
53
|
+
|
|
50
54
|
## Installation
|
|
51
55
|
|
|
52
56
|
Requires [Bun](https://bun.sh) runtime.
|
|
@@ -104,7 +108,7 @@ Schemas define the structure and rules for the entities in a space, allowing cus
|
|
|
104
108
|
|
|
105
109
|
Several schemas (`strict_ost`, `knowledge_wiki` and `general`) are included. The general (strategy) schema combines a basic vision/mission/goals hierarchy with a hierarchy loosely based on Opportunity Solution Trees. It is intentionally flexible to support rapid initial adoption. The strict OST schema has a narrower scope, and reflects Teresa Torres' specific recommendations for Opportunity Solution Trees more closely. The knowledge wiki schema defines a flatter set of types used for knowledge bases.
|
|
106
110
|
|
|
107
|
-
structured-context schemas use a metaschema based on JSON Schema
|
|
111
|
+
structured-context schemas use a metaschema based on JSON Schema 2020-12 that adds a top-level `$metadata` block:
|
|
108
112
|
|
|
109
113
|
```json5
|
|
110
114
|
"$metadata": {
|
|
@@ -179,23 +183,48 @@ Bare wikilink items (`- [[Existing Node]]`) in any embedding section populate a
|
|
|
179
183
|
|
|
180
184
|
With `fieldOn: "parent"`, embedded child nodes (parsed from a matching heading's list or table) are appended as wikilinks to the parent's `field` array, rather than receiving a `parent` field. This matches schemas where the content model naturally lists children on the parent (e.g. `activity.tasks: ["[[Task A]]"]`).
|
|
181
185
|
|
|
182
|
-
Metadata is
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
`$ref` reuses JSON Schema definitions for validation only; it brings no metadata. Metadata is composed only through explicit imports:
|
|
187
|
+
|
|
188
|
+
```json5
|
|
189
|
+
"$metadata": { "imports": ["sctx://core/_strategy_general"] }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- An import brings the whole schema's `$metadata`, and is transitive (the imported schema's own imports come too).
|
|
193
|
+
- Merge order: imports in order, depth-first, then the schema's own metadata last.
|
|
194
|
+
- One rule for every key: a duplicate rule `id`, alias, relationship (same `parent`/`type`/`field`) or hierarchy `name` is an error unless the later entry sets `override: true` (for an alias, write the value as `{ "type": "goal", "override": true }`).
|
|
195
|
+
- `hierarchy`: each schema file declares at most one hierarchy, and every imported hierarchy is kept as a separate named hierarchy (see [Multiple hierarchies](#multiple-hierarchies))
|
|
196
|
+
- Each rule's `category` supplies its default severity (`validation` → `error`, `coherence`/`workflow` → `warning`, `best-practice` → `info`); an optional `severity` field overrides it.
|
|
188
197
|
|
|
189
198
|
If no file defines `hierarchy`, hierarchy-specific checks are skipped.
|
|
190
199
|
|
|
200
|
+
#### Multiple hierarchies
|
|
201
|
+
|
|
202
|
+
A space can have several hierarchies by importing schemas that each declare one. For example, a root schema might import a work hierarchy (`goal → task`) and a skills hierarchy (`skill_area → skill`).
|
|
203
|
+
|
|
204
|
+
- **Naming**: a hierarchy's `name` defaults to the declaring schema's file stem (e.g. `_skills` for `_skills.json`). Set `"name"` in `$metadata.hierarchy` to override it.
|
|
205
|
+
- **Main hierarchy**: if the root schema declares a hierarchy, it is the main one. If the root declares none and exactly one is imported, that one is the main hierarchy. If several are imported, the root must select one:
|
|
206
|
+
|
|
207
|
+
```json5
|
|
208
|
+
"$metadata": {
|
|
209
|
+
"imports": ["sctx://_work", "sctx://_skills"],
|
|
210
|
+
"hierarchy": { "$ref": "sctx://_work#/$metadata/hierarchy" }
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A root may either declare a hierarchy or select one with `$ref`, not both.
|
|
215
|
+
- **Each type belongs to at most one hierarchy.** A type in two hierarchies is an error. To change an imported hierarchy's levels, declare a hierarchy with the same `name` and `"override": true`.
|
|
216
|
+
- **Validation** (layering, parent types, skip levels, orphans) runs per hierarchy. Relationships can connect types across hierarchies.
|
|
217
|
+
- The main hierarchy drives `space_on_a_page` parsing, hierarchy embedding, `template-sync`, and the default `show`/`diagram`/`render` output. Use `--hierarchy <name>` to render another one.
|
|
218
|
+
|
|
191
219
|
**Customizing Schemas:**
|
|
192
220
|
- **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`) are loaded and used to resolve references (using `$ref`).
|
|
193
221
|
- **Partials as entity libraries**: Partials can define reusable entity types in `$defs` that composing schemas reference via `$ref`. Bundled partials like `_strategy_general` and `_knowledge_wiki` provide common entity sets for strategy and wiki content.
|
|
194
|
-
- **Partials can carry metadata**: Unlike plain JSON Schema, partials may include `$metadata` (hierarchy, aliases, relationships, rules).
|
|
195
|
-
- **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "
|
|
196
|
-
- **Loading priority**: Partial schemas are loaded from
|
|
222
|
+
- **Partials can carry metadata**: Unlike plain JSON Schema, partials may include `$metadata` (hierarchy, aliases, relationships, rules). A schema gets that metadata only by listing the partial in `$metadata.imports`.
|
|
223
|
+
- **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "https://json-schema.org/draft/2020-12/schema"` so it validates standalone as plain JSON Schema.
|
|
224
|
+
- **Loading priority**: Partial schemas are loaded from the default schema directory, configured plugins (`schemas/`), and the directory of your specified target schema.
|
|
197
225
|
- **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
|
|
198
|
-
-
|
|
226
|
+
- **`$id` namespaces**: All `$id`s use `sctx://`. Bundled schemas are `sctx://core/<name>`, plugin schemas `sctx://<pluginName>/<name>`, and local schemas a single name, `sctx://<name>`. Collisions are errors. Legacy bundled ids (`sctx://<name>`, e.g. `sctx://_sctx_base`) resolve to `sctx://core/` with a deprecation warning until 1.0. See [docs/schemas.md](docs/schemas.md#schema-id-namespaces).
|
|
227
|
+
- **Plugin-contributed schemas**: External plugins can contribute schemas via `<pluginRoot>/schemas/` (or `"sctx": { "schemas": "..." }` in `package.json`). Spaces can reference them using `sctx-plugin/schema.json` or `plugin/schema.json`, or by simple filename if the plugin is declared in `plugins`. Schemas resolve statically without executing plugin JavaScript code.
|
|
199
228
|
|
|
200
229
|
Schema resolution order: space config `schema` > global config `schema`. Schema resolution fails if none is configured.
|
|
201
230
|
|
|
@@ -216,13 +245,24 @@ Validates markdown files against the JSON schema:
|
|
|
216
245
|
- Skips files without frontmatter or without a `type` field
|
|
217
246
|
- Reports validation results with counts and per-file errors
|
|
218
247
|
|
|
248
|
+
### Render space
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
sctx render <space> <format> [--out <dir-or-file>] [--force] [--filter <view>] [--hierarchy <name>] [--data key=value]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Renders a space in a plugin-provided format. Use `sctx render list [space]` to list available formats.
|
|
255
|
+
|
|
256
|
+
- `markdown.bullets` — Indented bullet list tree (outputs to stdout or `--out <file>`).
|
|
257
|
+
- `markdown.directory` — Markdown space directory with one file per node. Writes to `--out <dir>` (refuses to overwrite existing files unless `--force` is passed). Flat directory by default, or organized into per-type subfolders with `--data folders=type` or when configured with `typeInference.folderMap`. Without `--out`, prints a summary of files to be written.
|
|
258
|
+
|
|
219
259
|
### Show space tree
|
|
220
260
|
|
|
221
261
|
```bash
|
|
222
|
-
sctx show <space> [--filter <view-or-expression>]
|
|
262
|
+
sctx show <space> [--filter <view-or-expression>] [--hierarchy <name>]
|
|
223
263
|
```
|
|
224
264
|
|
|
225
|
-
Prints the space as an indented hierarchy tree. Hierarchy roots are listed first, followed by orphans (nodes in the hierarchy but with no resolved parent) and non-hierarchy nodes.
|
|
265
|
+
Prints the space as an indented hierarchy tree. Hierarchy roots are listed first, followed by orphans (nodes in the hierarchy but with no resolved parent) and non-hierarchy nodes. The main hierarchy is shown by default; `--hierarchy <name>` selects another (see [Multiple hierarchies](#multiple-hierarchies)). `diagram` and `render` accept the same option.
|
|
226
266
|
|
|
227
267
|
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.
|
|
228
268
|
|
|
@@ -254,6 +294,7 @@ The WHERE predicate is a [JSONata](https://docs.jsonata.org/overview) expression
|
|
|
254
294
|
- **`ancestors[]`** — flat array of ancestor nodes, nearest first, deduplicated. Each entry includes all schema fields of the ancestor node, plus:
|
|
255
295
|
- `_field` — the edge field name that connects to the ancestor
|
|
256
296
|
- `_source` — `'hierarchy'` or `'relationship'`
|
|
297
|
+
- `_hierarchy` — the hierarchy name, for hierarchy edges
|
|
257
298
|
- `_selfRef` — whether the edge is a same-type (self-referential) link
|
|
258
299
|
- **`descendants[]`** — same structure, for descendant nodes
|
|
259
300
|
|
|
@@ -304,7 +345,7 @@ SELECT relationships(assumption) WHERE resolvedType='opportunity'
|
|
|
304
345
|
### Generate Mermaid diagram
|
|
305
346
|
|
|
306
347
|
```bash
|
|
307
|
-
sctx diagram <space> [--output path/to/output.mmd]
|
|
348
|
+
sctx diagram <space> [--output path/to/output.mmd] [--hierarchy <name>]
|
|
308
349
|
```
|
|
309
350
|
|
|
310
351
|
Generates a Mermaid `graph TD` diagram from validated space nodes:
|
|
@@ -321,7 +362,7 @@ sctx schemas show <schema-file> [--mermaid-erd] [--space <name>]
|
|
|
321
362
|
|
|
322
363
|
Generates a Mermaid Entity Relationship Diagram from a schema:
|
|
323
364
|
- Shows all entity types and their properties
|
|
324
|
-
- Displays parent-child relationships based on hierarchy
|
|
365
|
+
- Displays parent-child relationships based on the levels of each hierarchy
|
|
325
366
|
- Useful for visualizing schema structure during development
|
|
326
367
|
|
|
327
368
|
Example:
|
package/dist/api.d.ts
CHANGED
|
@@ -19,15 +19,17 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
|
|
21
21
|
export type { Config, SpaceConfig } from './config';
|
|
22
|
-
export { loadConfig, setConfigPath } from './config';
|
|
22
|
+
export { loadConfig, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
|
|
23
23
|
export { PLUGIN_PREFIX } from './constants';
|
|
24
24
|
export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
|
|
25
|
-
export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
|
|
25
|
+
export type { ParseHook, ParseResult, PluginContext, RenderFileOutput, RenderFormat, RenderHook, RenderMultiFileOutput, RenderOptions, RenderResult, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
|
|
26
|
+
export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
|
|
26
27
|
export { readSpace } from './read/read-space';
|
|
28
|
+
export { buildTargetIndex, formatWikilink, type ParsedWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
|
|
27
29
|
export type { SharedEmbeddingFields } from './schema/metadata-contract';
|
|
28
|
-
export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
|
|
30
|
+
export { bundledSchemasDir, loadSchema, type PluginSchemaSource, setBundledSchemasDir } from './schema/schema';
|
|
29
31
|
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
30
|
-
export type
|
|
31
|
-
export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationResult, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types';
|
|
32
|
+
export { buildSpaceGraph, type SpaceGraph } from './space-graph';
|
|
33
|
+
export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationIssue, FileValidationResult, Hierarchy, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, RuleCategory, RuleSeverity, RuleViolation, SchemaMetadata, SchemaWithMetadata, SourceLocation, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types';
|
|
32
34
|
export type { ValidationResult } from './validate';
|
|
33
35
|
export { validateFile, validateSpace } from './validate';
|
package/dist/api.js
CHANGED
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* const graph = await assembleSpaceGraph(ctx, { readResult });
|
|
18
18
|
* const validation = await validateSpace(ctx, { readResult });
|
|
19
19
|
*/
|
|
20
|
-
export { loadConfig, setConfigPath } from './config';
|
|
20
|
+
export { loadConfig, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
|
|
21
21
|
export { PLUGIN_PREFIX } from './constants';
|
|
22
22
|
export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
|
|
23
|
+
export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
|
|
23
24
|
export { readSpace } from './read/read-space';
|
|
25
|
+
export { buildTargetIndex, formatWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
|
|
24
26
|
export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
|
|
25
27
|
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
28
|
+
export { buildSpaceGraph } from './space-graph';
|
|
26
29
|
export { validateFile, validateSpace } from './validate';
|
package/dist/commands/diagram.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
2
|
import { executeRender } from '../render/render';
|
|
3
3
|
export async function diagram(context, options) {
|
|
4
|
-
const result = await executeRender('mermaid.graph', context, {
|
|
4
|
+
const result = await executeRender('mermaid.graph', context, {
|
|
5
|
+
filter: options.filter,
|
|
6
|
+
hierarchy: options.hierarchy,
|
|
7
|
+
});
|
|
8
|
+
if (typeof result !== 'string') {
|
|
9
|
+
throw new Error('Expected string output from mermaid diagram renderer');
|
|
10
|
+
}
|
|
5
11
|
if (options.output) {
|
|
6
12
|
writeFileSync(options.output, result);
|
|
7
13
|
console.log(`Mermaid diagram written to ${options.output}`);
|
package/dist/commands/dump.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
import type { SpaceContext } from '../types';
|
|
2
|
-
export
|
|
1
|
+
import type { ReadSpaceResult, SpaceContext } from '../types';
|
|
2
|
+
export interface DumpOptions {
|
|
3
|
+
/** Named view (resolved via space.views) or a raw filter DSL expression. */
|
|
4
|
+
filter?: string;
|
|
5
|
+
/** Hierarchy used when evaluating the filter (default: the main hierarchy). */
|
|
6
|
+
hierarchy?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build the dump payload: every parsed node, or only nodes matched by a filter.
|
|
10
|
+
*
|
|
11
|
+
* Unfiltered dumps include schema-invalid nodes, since dump is a debugging aid.
|
|
12
|
+
* Filtered dumps match show/render: the filter runs over the graph of schema-valid nodes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDump(context: SpaceContext, options?: DumpOptions): Promise<ReadSpaceResult>;
|
|
15
|
+
export declare function dump(context: SpaceContext, options?: DumpOptions): Promise<void>;
|
package/dist/commands/dump.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { assembleSpaceGraph } from '../load-space-graph';
|
|
2
2
|
import { readSpace } from '../read/read-space';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Build the dump payload: every parsed node, or only nodes matched by a filter.
|
|
5
|
+
*
|
|
6
|
+
* Unfiltered dumps include schema-invalid nodes, since dump is a debugging aid.
|
|
7
|
+
* Filtered dumps match show/render: the filter runs over the graph of schema-valid nodes.
|
|
8
|
+
*/
|
|
9
|
+
export async function buildDump(context, options = {}) {
|
|
10
|
+
const readResult = await readSpace(context);
|
|
11
|
+
if (!options.filter)
|
|
12
|
+
return readResult;
|
|
13
|
+
const graph = await assembleSpaceGraph(context, { ...options, readResult });
|
|
14
|
+
const nodes = readResult.nodes.filter((node) => graph.nodes.get(node.title) === node);
|
|
15
|
+
return { ...readResult, nodes };
|
|
16
|
+
}
|
|
17
|
+
export async function dump(context, options = {}) {
|
|
18
|
+
const { nodes, source, diagnostics } = await buildDump(context, options);
|
|
19
|
+
console.log(JSON.stringify({ nodes, source, diagnostics }, null, 2));
|
|
6
20
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SpaceContext } from '../types';
|
|
2
2
|
export declare function render(context: SpaceContext, format: string, options: {
|
|
3
3
|
filter?: string;
|
|
4
|
+
hierarchy?: string;
|
|
4
5
|
output?: string;
|
|
6
|
+
out?: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
data?: Record<string, unknown>;
|
|
5
9
|
}): Promise<void>;
|
|
6
10
|
export declare function renderList(context?: SpaceContext): Promise<void>;
|
package/dist/commands/render.js
CHANGED
|
@@ -1,17 +1,72 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
3
|
import { discoverPlugins, loadPlugins } from '../plugins/loader';
|
|
4
|
+
import { isRenderMultiFileOutput } from '../plugins/util';
|
|
3
5
|
import { buildFormatRegistry } from '../render/registry';
|
|
4
6
|
import { executeRender } from '../render/render';
|
|
5
7
|
export async function render(context, format, options) {
|
|
6
|
-
const result = await executeRender(format, context, {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const result = await executeRender(format, context, {
|
|
9
|
+
filter: options.filter,
|
|
10
|
+
hierarchy: options.hierarchy,
|
|
11
|
+
data: options.data,
|
|
12
|
+
});
|
|
13
|
+
const targetPath = options.out ?? options.output;
|
|
14
|
+
const force = Boolean(options.force);
|
|
15
|
+
if (typeof result === 'string') {
|
|
16
|
+
if (targetPath) {
|
|
17
|
+
const resolved = resolve(targetPath);
|
|
18
|
+
if (existsSync(resolved)) {
|
|
19
|
+
if (statSync(resolved).isDirectory()) {
|
|
20
|
+
throw new Error(`Output path "${targetPath}" is a directory. Please specify a file path.`);
|
|
21
|
+
}
|
|
22
|
+
if (!force) {
|
|
23
|
+
throw new Error(`File "${targetPath}" already exists. Use --force to overwrite.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
27
|
+
writeFileSync(resolved, result, 'utf8');
|
|
28
|
+
console.error(`Written to ${targetPath}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
process.stdout.write(result);
|
|
32
|
+
if (!result.endsWith('\n'))
|
|
33
|
+
process.stdout.write('\n');
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
10
36
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
37
|
+
if (isRenderMultiFileOutput(result)) {
|
|
38
|
+
if (targetPath) {
|
|
39
|
+
const targetDir = resolve(targetPath);
|
|
40
|
+
const conflicts = [];
|
|
41
|
+
for (const file of result.files) {
|
|
42
|
+
const fullPath = join(targetDir, file.path);
|
|
43
|
+
if (existsSync(fullPath)) {
|
|
44
|
+
conflicts.push(file.path);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (conflicts.length > 0 && !force) {
|
|
48
|
+
const preview = conflicts
|
|
49
|
+
.slice(0, 5)
|
|
50
|
+
.map((f) => ` ${f}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
const extra = conflicts.length > 5 ? `\n ... and ${conflicts.length - 5} more` : '';
|
|
53
|
+
throw new Error(`Refusing to overwrite ${conflicts.length} existing file(s) in "${targetPath}" without --force:\n${preview}${extra}`);
|
|
54
|
+
}
|
|
55
|
+
for (const file of result.files) {
|
|
56
|
+
const fullPath = join(targetDir, file.path);
|
|
57
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
58
|
+
writeFileSync(fullPath, file.content, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
console.error(`Written ${result.files.length} file(s) to ${targetPath}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log(`Rendered ${result.files.length} file(s):`);
|
|
64
|
+
for (const file of result.files) {
|
|
65
|
+
const bytes = Buffer.byteLength(file.content, 'utf8');
|
|
66
|
+
console.log(` ${file.path} (${bytes} bytes)`);
|
|
67
|
+
}
|
|
68
|
+
console.log('\nSpecify --out <dir> to write files to disk.');
|
|
69
|
+
}
|
|
15
70
|
}
|
|
16
71
|
}
|
|
17
72
|
export async function renderList(context) {
|
package/dist/commands/schemas.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { loadConfig
|
|
4
|
-
import {
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { loadConfig } from '../config';
|
|
4
|
+
import { discoverPlugins } from '../plugins/loader';
|
|
5
|
+
import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
|
|
6
|
+
import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema, } from '../schema/schema';
|
|
5
7
|
import { mergeVariantProperties, resolveRef } from '../schema/schema-refs';
|
|
8
|
+
import { DEFAULT_SEVERITY_BY_CATEGORY } from '../schema/validate-rules';
|
|
9
|
+
import { createSpaceContext } from '../space-context';
|
|
6
10
|
function isBundledPath(schemaPath) {
|
|
7
11
|
return dirname(schemaPath) === bundledSchemasDir;
|
|
8
12
|
}
|
|
@@ -73,9 +77,14 @@ function showDefs(defs) {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
function showMetadata(metadata) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(
|
|
80
|
+
const hierarchies = metadata.hierarchies ?? [];
|
|
81
|
+
if (hierarchies.length > 0) {
|
|
82
|
+
console.log('');
|
|
83
|
+
for (const hierarchy of hierarchies) {
|
|
84
|
+
const parts = hierarchy.levels.map((l) => (l.selfRef ? `${l.type}(+)` : l.type));
|
|
85
|
+
const main = hierarchy === metadata.hierarchy ? ' (main)' : '';
|
|
86
|
+
console.log(`hierarchy "${hierarchy.name}"${main}: ${parts.join(' → ')}`);
|
|
87
|
+
}
|
|
79
88
|
}
|
|
80
89
|
else {
|
|
81
90
|
console.log('\nhierarchy: (none)');
|
|
@@ -88,15 +97,16 @@ function showMetadata(metadata) {
|
|
|
88
97
|
const groups = new Map();
|
|
89
98
|
for (const rule of metadata.rules) {
|
|
90
99
|
const rules = groups.get(rule.category) ?? [];
|
|
91
|
-
rules.push(
|
|
100
|
+
rules.push(rule);
|
|
92
101
|
groups.set(rule.category, rules);
|
|
93
102
|
}
|
|
94
103
|
if (groups.size > 0) {
|
|
95
104
|
console.log('\nRules:');
|
|
96
|
-
for (const [
|
|
97
|
-
console.log(` ${
|
|
98
|
-
for (const
|
|
99
|
-
|
|
105
|
+
for (const [category, rules] of groups) {
|
|
106
|
+
console.log(` ${category} (default severity: ${DEFAULT_SEVERITY_BY_CATEGORY[category]}):`);
|
|
107
|
+
for (const rule of rules) {
|
|
108
|
+
const override = rule.severity ? ` [severity: ${rule.severity}]` : '';
|
|
109
|
+
console.log(` ${rule.id}: ${rule.description}${override}`);
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
}
|
|
@@ -110,7 +120,7 @@ function showMetadata(metadata) {
|
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
|
-
function showRegistry(schemaPath, schemaRefRegistry) {
|
|
123
|
+
function showRegistry(schemaPath, schemaRefRegistry, pluginSchemas) {
|
|
114
124
|
const bundledIds = new Set();
|
|
115
125
|
if (existsSync(bundledSchemasDir)) {
|
|
116
126
|
for (const file of readdirSync(bundledSchemasDir).filter((f) => f.endsWith('.json'))) {
|
|
@@ -119,9 +129,22 @@ function showRegistry(schemaPath, schemaRefRegistry) {
|
|
|
119
129
|
bundledIds.add(s.$id);
|
|
120
130
|
}
|
|
121
131
|
}
|
|
132
|
+
const pluginIds = new Set();
|
|
133
|
+
if (pluginSchemas) {
|
|
134
|
+
for (const { schemasDir } of pluginSchemas) {
|
|
135
|
+
if (!existsSync(schemasDir))
|
|
136
|
+
continue;
|
|
137
|
+
for (const file of readdirSync(schemasDir).filter((f) => f.endsWith('.json'))) {
|
|
138
|
+
const s = readRawSchema(join(schemasDir, file));
|
|
139
|
+
if (typeof s.$id === 'string')
|
|
140
|
+
pluginIds.add(s.$id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
122
144
|
console.log(`\nRegistry (${schemaPath}):`);
|
|
123
145
|
for (const [id] of schemaRefRegistry) {
|
|
124
|
-
|
|
146
|
+
const origin = bundledIds.has(id) ? 'bundled' : pluginIds.has(id) ? 'plugin' : 'local';
|
|
147
|
+
console.log(` [${origin}] ${id}`);
|
|
125
148
|
}
|
|
126
149
|
}
|
|
127
150
|
/**
|
|
@@ -130,7 +153,6 @@ function showRegistry(schemaPath, schemaRefRegistry) {
|
|
|
130
153
|
*/
|
|
131
154
|
function generateMermaidErd(metadata, entities) {
|
|
132
155
|
let mmd = 'erDiagram\n';
|
|
133
|
-
const hierarchyLevels = metadata.hierarchy?.levels ?? [];
|
|
134
156
|
// Generate entity definitions with properties
|
|
135
157
|
for (const entity of entities) {
|
|
136
158
|
const safeName = entity.type.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
@@ -145,8 +167,8 @@ function generateMermaidErd(metadata, entities) {
|
|
|
145
167
|
}
|
|
146
168
|
mmd += ' }\n';
|
|
147
169
|
}
|
|
148
|
-
// Generate relationships based on hierarchy
|
|
149
|
-
|
|
170
|
+
// Generate relationships based on the levels of each hierarchy
|
|
171
|
+
for (const { levels: hierarchyLevels } of metadata.hierarchies ?? []) {
|
|
150
172
|
for (let i = 0; i < hierarchyLevels.length - 1; i++) {
|
|
151
173
|
const current = hierarchyLevels[i];
|
|
152
174
|
const next = hierarchyLevels[i + 1];
|
|
@@ -166,7 +188,7 @@ function generateMermaidErd(metadata, entities) {
|
|
|
166
188
|
}
|
|
167
189
|
return mmd;
|
|
168
190
|
}
|
|
169
|
-
export function listSchemas() {
|
|
191
|
+
export async function listSchemas() {
|
|
170
192
|
const config = loadConfig();
|
|
171
193
|
// List all schemas known to config
|
|
172
194
|
if (existsSync(bundledSchemasDir)) {
|
|
@@ -182,6 +204,23 @@ export function listSchemas() {
|
|
|
182
204
|
}
|
|
183
205
|
}
|
|
184
206
|
}
|
|
207
|
+
// List schemas provided by discovered plugins
|
|
208
|
+
const plugins = await discoverPlugins();
|
|
209
|
+
for (const p of plugins) {
|
|
210
|
+
if (p.schemasDir && existsSync(p.schemasDir)) {
|
|
211
|
+
const files = readdirSync(p.schemasDir)
|
|
212
|
+
.filter((f) => f.endsWith('.json'))
|
|
213
|
+
.sort();
|
|
214
|
+
if (files.length > 0) {
|
|
215
|
+
console.log(`\nPlugin schemas (${p.name}):`);
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
const schema = readRawSchema(join(p.schemasDir, file));
|
|
218
|
+
const id = typeof schema.$id === 'string' ? schema.$id : '(no $id)';
|
|
219
|
+
console.log(` ${file}${file.startsWith('_') ? ' [partial]' : ''} (${id})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
185
224
|
const configured = [];
|
|
186
225
|
const seen = new Set();
|
|
187
226
|
if (config.schema && !isBundledPath(config.schema)) {
|
|
@@ -201,16 +240,23 @@ export function listSchemas() {
|
|
|
201
240
|
}
|
|
202
241
|
}
|
|
203
242
|
}
|
|
204
|
-
export function showSchema(file, options) {
|
|
243
|
+
export async function showSchema(file, options) {
|
|
205
244
|
const config = loadConfig();
|
|
206
245
|
let schemaPath;
|
|
246
|
+
let schema;
|
|
247
|
+
let schemaRefRegistry;
|
|
248
|
+
let pluginSchemas;
|
|
207
249
|
if (options.space) {
|
|
208
250
|
const space = config.spaces.find((s) => s.name === options.space);
|
|
209
251
|
if (!space) {
|
|
210
252
|
console.error(`Error: Unknown space "${options.space}"`);
|
|
211
253
|
process.exit(1);
|
|
212
254
|
}
|
|
213
|
-
|
|
255
|
+
const context = createSpaceContext(options.space, config);
|
|
256
|
+
schemaPath = context.resolvedSchemaPath;
|
|
257
|
+
schema = context.schema;
|
|
258
|
+
schemaRefRegistry = context.schemaRefRegistry;
|
|
259
|
+
pluginSchemas = resolveSpacePluginSchemas(context.space, config, context.configDir);
|
|
214
260
|
}
|
|
215
261
|
else if (!file) {
|
|
216
262
|
console.error('Error: specify a file argument or use --space');
|
|
@@ -220,7 +266,49 @@ export function showSchema(file, options) {
|
|
|
220
266
|
schemaPath = file;
|
|
221
267
|
}
|
|
222
268
|
else {
|
|
223
|
-
|
|
269
|
+
// Check discovered plugins
|
|
270
|
+
const discovered = await discoverPlugins();
|
|
271
|
+
pluginSchemas = discovered
|
|
272
|
+
.filter((p) => p.schemasDir)
|
|
273
|
+
.map((p) => ({ pluginName: shortenPluginName(p.name), schemasDir: p.schemasDir }));
|
|
274
|
+
let candidatePath;
|
|
275
|
+
if (file.includes('/')) {
|
|
276
|
+
const slashIdx = file.indexOf('/');
|
|
277
|
+
const prefix = file.slice(0, slashIdx);
|
|
278
|
+
const subPath = file.slice(slashIdx + 1);
|
|
279
|
+
const norm = normalizePluginName(prefix);
|
|
280
|
+
const short = shortenPluginName(prefix);
|
|
281
|
+
const pluginDir = resolvePluginSchemasDir(prefix, process.cwd());
|
|
282
|
+
if (pluginDir) {
|
|
283
|
+
candidatePath = resolve(pluginDir, subPath.endsWith('.json') ? subPath : `${subPath}.json`);
|
|
284
|
+
if (!pluginSchemas.some((ps) => ps.pluginName === short)) {
|
|
285
|
+
pluginSchemas.push({ pluginName: short, schemasDir: pluginDir });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const match = discovered.find((p) => (p.name === norm || p.name === short) && p.schemasDir);
|
|
290
|
+
if (match?.schemasDir) {
|
|
291
|
+
candidatePath = resolve(match.schemasDir, subPath.endsWith('.json') ? subPath : `${subPath}.json`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
for (const p of discovered) {
|
|
297
|
+
if (p.schemasDir) {
|
|
298
|
+
const c = resolve(p.schemasDir, file.endsWith('.json') ? file : `${file}.json`);
|
|
299
|
+
if (existsSync(c)) {
|
|
300
|
+
candidatePath = c;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (candidatePath && existsSync(candidatePath)) {
|
|
307
|
+
schemaPath = candidatePath;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
schemaPath = join(bundledSchemasDir, file.endsWith('.json') ? file : `${file}.json`);
|
|
311
|
+
}
|
|
224
312
|
}
|
|
225
313
|
if (!existsSync(schemaPath)) {
|
|
226
314
|
console.error(`Schema not found: ${schemaPath}`);
|
|
@@ -237,7 +325,11 @@ export function showSchema(file, options) {
|
|
|
237
325
|
process.stdout.write(content);
|
|
238
326
|
return;
|
|
239
327
|
}
|
|
240
|
-
|
|
328
|
+
if (!schema || !schemaRefRegistry) {
|
|
329
|
+
const loaded = loadSchema(schemaPath, pluginSchemas);
|
|
330
|
+
schema = loaded.schema;
|
|
331
|
+
schemaRefRegistry = loaded.schemaRefRegistry;
|
|
332
|
+
}
|
|
241
333
|
// Handle --mermaid-erd: generate ERD and exit
|
|
242
334
|
if (options.mermaidErd) {
|
|
243
335
|
const entityInfo = extractEntityInfo(schema, schemaRefRegistry);
|
|
@@ -265,5 +357,5 @@ export function showSchema(file, options) {
|
|
|
265
357
|
console.log(` ${ref}`);
|
|
266
358
|
}
|
|
267
359
|
}
|
|
268
|
-
showRegistry(schemaPath, schemaRefRegistry);
|
|
360
|
+
showRegistry(schemaPath, schemaRefRegistry, pluginSchemas);
|
|
269
361
|
}
|
package/dist/commands/show.d.ts
CHANGED
package/dist/commands/show.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { executeRender } from '../render/render';
|
|
2
2
|
export async function show(context, options) {
|
|
3
|
-
const result = await executeRender('markdown.bullets', context, {
|
|
3
|
+
const result = await executeRender('markdown.bullets', context, {
|
|
4
|
+
filter: options?.filter,
|
|
5
|
+
hierarchy: options?.hierarchy,
|
|
6
|
+
});
|
|
7
|
+
if (typeof result !== 'string') {
|
|
8
|
+
throw new Error('Expected string output from markdown.bullets renderer');
|
|
9
|
+
}
|
|
4
10
|
process.stdout.write(result);
|
|
5
11
|
if (!result.endsWith('\n'))
|
|
6
12
|
process.stdout.write('\n');
|