structured-context 0.16.0 → 0.17.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/LICENSE +21 -0
- package/README.md +13 -13
- package/dist/api.d.ts +4 -2
- package/dist/api.js +2 -0
- package/dist/browser.d.ts +14 -0
- package/dist/browser.js +13 -0
- package/dist/commands/dump.d.ts +2 -0
- package/dist/commands/dump.js +3 -1
- package/dist/commands/schemas.js +3 -2
- package/dist/commands/spaces.js +4 -6
- package/dist/commands/validate.js +20 -19
- package/dist/config/health.d.ts +10 -6
- package/dist/config/health.js +11 -44
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +15 -6
- package/dist/filter/augment-nodes.d.ts +2 -2
- package/dist/filter/augment-nodes.js +2 -2
- package/dist/filter/expand-include.js +2 -2
- package/dist/index.js +9 -1
- package/dist/judge/context.js +1 -1
- package/dist/load-space-graph.js +1 -13
- package/dist/plugins/markdown/parse-embedded.js +4 -4
- package/dist/plugins/miro/cache.js +1 -1
- package/dist/plugins/miro/sync.js +2 -2
- package/dist/read/resolve-graph-edges.js +3 -3
- package/dist/schema/list-schemas.js +2 -2
- package/dist/schema/metadata-contract.d.ts +34 -0
- package/dist/schema/metadata-contract.js +2 -2
- package/dist/schema/validate-graph.d.ts +2 -2
- package/dist/schema/validate-graph.js +4 -4
- package/dist/schemas/generated/space-graph/1.json +216 -0
- package/dist/schemas/generated/space-graph/latest.json +216 -0
- package/dist/space-context.js +11 -1
- package/dist/space-graph-data.d.ts +237 -0
- package/dist/space-graph-data.js +129 -0
- package/dist/space-graph.d.ts +12 -1
- package/dist/space-graph.js +25 -1
- package/dist/testing.js +3 -3
- package/dist/types.d.ts +2 -2
- package/docs/concepts.md +8 -4
- package/package.json +5 -1
- package/schemas/generated/space-graph/1.json +216 -0
- package/schemas/generated/space-graph/latest.json +216 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roger Barnes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -21,6 +21,18 @@ Command line tools:
|
|
|
21
21
|
```
|
|
22
22
|
> sctx validate strategy
|
|
23
23
|
|
|
24
|
+
Orphans (hierarchy nodes - no parent):
|
|
25
|
+
Solution looking for a problem.md
|
|
26
|
+
|
|
27
|
+
Broken links:
|
|
28
|
+
Steal underpants.md: [[Gnome goals]] → Link target "Gnome goals" in field "related" not found
|
|
29
|
+
|
|
30
|
+
Rule violations:
|
|
31
|
+
WARNING (1):
|
|
32
|
+
Build Death Star.md: Opportunities should contribute to at least one Outcome [workflow: opportunity-has-outcome]
|
|
33
|
+
INFO (1):
|
|
34
|
+
High tech juice press.md: Explore multiple candidate solutions (aim for at least three) for the target opportunity [best-practice: solution-quantity]
|
|
35
|
+
|
|
24
36
|
Space Validation Results
|
|
25
37
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
38
|
Content and structure
|
|
@@ -35,18 +47,6 @@ Content and structure
|
|
|
35
47
|
Orphans (hierarchy nodes - no parent) 1
|
|
36
48
|
Unresolved content links 0
|
|
37
49
|
Excluded during parsing 0
|
|
38
|
-
|
|
39
|
-
Orphans (hierarchy nodes - no parent):
|
|
40
|
-
Solution looking for a problem.md
|
|
41
|
-
|
|
42
|
-
Broken links:
|
|
43
|
-
Steal underpants.md: [[Gnome goals]] → Link target "Gnome goals" in field "related" not found
|
|
44
|
-
|
|
45
|
-
Rule violations:
|
|
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]
|
|
50
50
|
```
|
|
51
51
|
|
|
52
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#rule-shape).
|
|
@@ -237,7 +237,7 @@ Keeps Obsidian template files in sync with schema examples:
|
|
|
237
237
|
## Programmatic API
|
|
238
238
|
|
|
239
239
|
The package exposes a library entry point at `structured-context/api` for embedding the tooling
|
|
240
|
-
in other applications. See
|
|
240
|
+
in other applications, and `structured-context/browser` for loading an exported space graph in a browser. See
|
|
241
241
|
[docs/api.md](docs/api.md) for the full reference and usage examples.
|
|
242
242
|
|
|
243
243
|
## Development
|
package/dist/api.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* const validation = await validateSpace(ctx, { readResult });
|
|
19
19
|
*/
|
|
20
20
|
export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
|
|
21
|
+
export { type SpaceGraph, type SpaceNode, spaceGraphFrom } from './browser';
|
|
21
22
|
export type { Config, SpaceConfig } from './config';
|
|
22
23
|
export { loadConfig, resolveConfigPath, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
|
|
23
24
|
export { addSpace } from './config/write';
|
|
@@ -31,7 +32,8 @@ export { listSchemas } from './schema/list-schemas';
|
|
|
31
32
|
export type { SharedEmbeddingFields } from './schema/metadata-contract';
|
|
32
33
|
export { bundledSchemasDir, loadSchema, type PluginSchemaSource, setBundledSchemasDir } from './schema/schema';
|
|
33
34
|
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
34
|
-
export { buildSpaceGraph
|
|
35
|
-
export
|
|
35
|
+
export { buildSpaceGraph } from './space-graph';
|
|
36
|
+
export { exportSpaceGraph } from './space-graph-data';
|
|
37
|
+
export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationIssue, FileValidationResult, Hierarchy, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, RuleCategory, RuleSeverity, RuleViolation, SchemaMetadata, SchemaWithMetadata, SourceLocation, SpaceContext, UnresolvedRef, ValidateFileOutput, } from './types';
|
|
36
38
|
export type { ValidationResult } from './validate';
|
|
37
39
|
export { validateFile, validateSpace } from './validate';
|
package/dist/api.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* const graph = await assembleSpaceGraph(ctx, { readResult });
|
|
19
19
|
* const validation = await validateSpace(ctx, { readResult });
|
|
20
20
|
*/
|
|
21
|
+
export { spaceGraphFrom } from './browser';
|
|
21
22
|
export { loadConfig, resolveConfigPath, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
|
|
22
23
|
export { addSpace } from './config/write';
|
|
23
24
|
export { LOCAL_CONFIG_FILE, PLUGIN_PREFIX } from './constants';
|
|
@@ -29,4 +30,5 @@ export { listSchemas } from './schema/list-schemas';
|
|
|
29
30
|
export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
|
|
30
31
|
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
31
32
|
export { buildSpaceGraph } from './space-graph';
|
|
33
|
+
export { exportSpaceGraph } from './space-graph-data';
|
|
32
34
|
export { validateFile, validateSpace } from './validate';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe API for structured-context: nothing here imports Node built-ins.
|
|
3
|
+
*
|
|
4
|
+
* Rebuild a graph that a server exported with exportSpaceGraph:
|
|
5
|
+
*
|
|
6
|
+
* import { spaceGraphFrom } from 'structured-context/browser';
|
|
7
|
+
* const graph = spaceGraphFrom(await (await fetch('/graph.json')).json());
|
|
8
|
+
*
|
|
9
|
+
* Everything here is also exported from `structured-context/api`. Derive other types from these, e.g.
|
|
10
|
+
* `Parameters<typeof spaceGraphFrom>[0]` for the exported data or `SpaceNode['id']` for a node id.
|
|
11
|
+
*/
|
|
12
|
+
export type { SpaceGraph } from './space-graph';
|
|
13
|
+
export { spaceGraphFrom } from './space-graph-data';
|
|
14
|
+
export type { SpaceNode } from './types';
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Public entry point: do not add exports without the maintainer's agreement (AGENTS.md, "Public API surface").
|
|
2
|
+
/**
|
|
3
|
+
* Browser-safe API for structured-context: nothing here imports Node built-ins.
|
|
4
|
+
*
|
|
5
|
+
* Rebuild a graph that a server exported with exportSpaceGraph:
|
|
6
|
+
*
|
|
7
|
+
* import { spaceGraphFrom } from 'structured-context/browser';
|
|
8
|
+
* const graph = spaceGraphFrom(await (await fetch('/graph.json')).json());
|
|
9
|
+
*
|
|
10
|
+
* Everything here is also exported from `structured-context/api`. Derive other types from these, e.g.
|
|
11
|
+
* `Parameters<typeof spaceGraphFrom>[0]` for the exported data or `SpaceNode['id']` for a node id.
|
|
12
|
+
*/
|
|
13
|
+
export { spaceGraphFrom } from './space-graph-data';
|
package/dist/commands/dump.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface DumpOptions {
|
|
|
4
4
|
filter?: string;
|
|
5
5
|
/** Hierarchy used when evaluating the filter (default: the main hierarchy). */
|
|
6
6
|
hierarchy?: string;
|
|
7
|
+
/** Emit SpaceGraphData (nodes plus every hierarchy) instead of nodes, source and diagnostics. */
|
|
8
|
+
graph?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Build the dump payload: every parsed node, or only nodes matched by a filter.
|
package/dist/commands/dump.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assembleSpaceGraph } from '../load-space-graph';
|
|
2
2
|
import { readSpace } from '../read/read-space';
|
|
3
|
+
import { exportSpaceGraph } from '../space-graph-data';
|
|
3
4
|
/**
|
|
4
5
|
* Build the dump payload: every parsed node, or only nodes matched by a filter.
|
|
5
6
|
*
|
|
@@ -16,5 +17,6 @@ export async function buildDump(context, options = {}) {
|
|
|
16
17
|
}
|
|
17
18
|
export async function dump(context, options = {}) {
|
|
18
19
|
const { nodes, source, diagnostics } = await buildDump(context, options);
|
|
19
|
-
|
|
20
|
+
const output = options.graph ? exportSpaceGraph(nodes, context.schema.metadata) : { nodes, source, diagnostics };
|
|
21
|
+
console.log(JSON.stringify(output, null, 2));
|
|
20
22
|
}
|
package/dist/commands/schemas.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import { loadConfig } from '../config';
|
|
3
|
+
import { loadConfig, loadConfigOrEmpty } from '../config';
|
|
4
4
|
import { discoverPlugins } from '../plugins/loader';
|
|
5
5
|
import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
|
|
6
6
|
import { listSchemas } from '../schema/list-schemas';
|
|
@@ -231,7 +231,8 @@ export async function printSchemas(options = {}) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
export async function showSchema(file, options) {
|
|
234
|
-
|
|
234
|
+
// A schema named by file needs no config; a space does, so a missing one is worth a warning.
|
|
235
|
+
const config = options.space ? loadConfig() : loadConfigOrEmpty();
|
|
235
236
|
let schemaPath;
|
|
236
237
|
let schema;
|
|
237
238
|
let schemaRefRegistry;
|
package/dist/commands/spaces.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import { checkConfig, hasBlockingProblem } from '../config/health';
|
|
2
|
+
import { hasBlockingProblem, inspectConfig } from '../config/health';
|
|
4
3
|
import { addSpace } from '../config/write';
|
|
5
4
|
import { SctxError } from '../errors';
|
|
6
5
|
function renderConfigValue(v) {
|
|
@@ -9,7 +8,7 @@ function renderConfigValue(v) {
|
|
|
9
8
|
return String(v);
|
|
10
9
|
}
|
|
11
10
|
export function listSpaces(options = {}) {
|
|
12
|
-
const health =
|
|
11
|
+
const { health, config } = inspectConfig();
|
|
13
12
|
if (options.json) {
|
|
14
13
|
console.log(JSON.stringify(health));
|
|
15
14
|
if (hasBlockingProblem(health))
|
|
@@ -27,10 +26,9 @@ export function listSpaces(options = {}) {
|
|
|
27
26
|
console.log('');
|
|
28
27
|
if (hasBlockingProblem(health))
|
|
29
28
|
process.exitCode = 1;
|
|
30
|
-
if (!health.config.exists)
|
|
31
|
-
return;
|
|
32
29
|
}
|
|
33
|
-
|
|
30
|
+
if (!config)
|
|
31
|
+
return;
|
|
34
32
|
if (health.problems.length === 0)
|
|
35
33
|
console.log(`Config: ${path} (${health.config.source})\n`);
|
|
36
34
|
for (const space of config.spaces) {
|
|
@@ -91,26 +91,7 @@ export async function validate(context, options = {}) {
|
|
|
91
91
|
const countStr = String(count).padStart(3);
|
|
92
92
|
return `${label.padEnd(40)} ${color}${countStr}${reset}`;
|
|
93
93
|
};
|
|
94
|
-
console.log(`\nSpace Validation Results`);
|
|
95
|
-
console.log(`━`.repeat(45));
|
|
96
|
-
console.log('Content and structure');
|
|
97
|
-
console.log(fmt(' Valid', result.validCount));
|
|
98
|
-
console.log(fmt(' Schema validation errors', result.nodeErrorCount, true));
|
|
99
|
-
console.log(fmt(' Broken links', result.refErrors.length, true));
|
|
100
|
-
console.log(fmt(' Duplicate ids', result.duplicateErrors.length, true));
|
|
101
|
-
console.log(fmt(' Rule violations (error)', ruleCounts.error, true));
|
|
102
|
-
console.log(fmt(' Rule violations (warning)', ruleCounts.warning, true, true));
|
|
103
|
-
console.log(fmt(' Rule violations (info)', ruleCounts.info, true, true));
|
|
104
|
-
console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
|
|
105
|
-
console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
|
|
106
|
-
console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
|
|
107
94
|
const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
|
|
108
|
-
console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
|
|
109
|
-
if (result.depthLimit !== undefined) {
|
|
110
|
-
console.log(fmt(' Skipped by depth limit', result.skippedByDepth.length, true, true));
|
|
111
|
-
}
|
|
112
|
-
const gatedFileCount = result.coverageGates.reduce((sum, g) => sum + g.gatedFiles.length, 0);
|
|
113
|
-
console.log(fmt(' Rule checks gated (cascade)', gatedFileCount, true, true));
|
|
114
95
|
if (result.orphans.length > 0) {
|
|
115
96
|
console.log(`\nOrphans (hierarchy nodes - no parent):`);
|
|
116
97
|
for (const node of result.orphans)
|
|
@@ -207,6 +188,26 @@ export async function validate(context, options = {}) {
|
|
|
207
188
|
if (result.contextTrimmed.nodeCount > 0) {
|
|
208
189
|
console.log(`\nContext trimmed for ${result.contextTrimmed.nodeCount} node(s) across rules ${result.contextTrimmed.ruleIds.join(', ')}.`);
|
|
209
190
|
}
|
|
191
|
+
// Last, so a truncated tail (`| tail`) still shows the counts.
|
|
192
|
+
console.log(`\nSpace Validation Results`);
|
|
193
|
+
console.log(`━`.repeat(45));
|
|
194
|
+
console.log('Content and structure');
|
|
195
|
+
console.log(fmt(' Valid', result.validCount));
|
|
196
|
+
console.log(fmt(' Schema validation errors', result.nodeErrorCount, true));
|
|
197
|
+
console.log(fmt(' Broken links', result.refErrors.length, true));
|
|
198
|
+
console.log(fmt(' Duplicate ids', result.duplicateErrors.length, true));
|
|
199
|
+
console.log(fmt(' Rule violations (error)', ruleCounts.error, true));
|
|
200
|
+
console.log(fmt(' Rule violations (warning)', ruleCounts.warning, true, true));
|
|
201
|
+
console.log(fmt(' Rule violations (info)', ruleCounts.info, true, true));
|
|
202
|
+
console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
|
|
203
|
+
console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
|
|
204
|
+
console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
|
|
205
|
+
console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
|
|
206
|
+
if (result.depthLimit !== undefined) {
|
|
207
|
+
console.log(fmt(' Skipped by depth limit', result.skippedByDepth.length, true, true));
|
|
208
|
+
}
|
|
209
|
+
const gatedFileCount = result.coverageGates.reduce((sum, g) => sum + g.gatedFiles.length, 0);
|
|
210
|
+
console.log(fmt(' Rule checks gated (cascade)', gatedFileCount, true, true));
|
|
210
211
|
console.log(`\n`);
|
|
211
212
|
if (result.nodeErrorCount > 0 ||
|
|
212
213
|
result.refErrors.length > 0 ||
|
package/dist/config/health.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SctxErrorCode } from '../errors';
|
|
2
|
-
import { type ResolvedConfigPath } from '.';
|
|
2
|
+
import { type Config, type ResolvedConfigPath } from '.';
|
|
3
3
|
export type ConfigProblem = {
|
|
4
4
|
code: SctxErrorCode;
|
|
5
5
|
severity: 'error' | 'warning' | 'info';
|
|
@@ -21,11 +21,15 @@ export type ConfigHealth = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare function fixFor(code: SctxErrorCode): string;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Find and load the config for the working directory, reporting failures as problems with stable
|
|
25
|
+
* codes instead of throwing. It reports on the config only: what a space's `path` means belongs to
|
|
26
|
+
* the plugin that reads it, and a schema is checked when it is loaded.
|
|
26
27
|
*/
|
|
27
|
-
export declare function
|
|
28
|
+
export declare function inspectConfig(options?: {
|
|
28
29
|
startDir?: string;
|
|
29
|
-
}):
|
|
30
|
-
|
|
30
|
+
}): {
|
|
31
|
+
health: ConfigHealth;
|
|
32
|
+
config?: Config;
|
|
33
|
+
};
|
|
34
|
+
/** True when the config problems stop validation from running. */
|
|
31
35
|
export declare function hasBlockingProblem(health: ConfigHealth): boolean;
|
package/dist/config/health.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import JSON5 from 'json5';
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
3
2
|
import { LOCAL_CONFIG_FILE } from '../constants';
|
|
4
3
|
import { SctxError } from '../errors';
|
|
5
|
-
import { getConfigSourceFiles, loadConfig, resolveConfigPath
|
|
4
|
+
import { getConfigSourceFiles, loadConfig, resolveConfigPath } from '.';
|
|
6
5
|
const FIXES = {
|
|
7
6
|
E_CONFIG_NOT_FOUND: `Create a config with \`sctx spaces add <name> <path> --schema <schema> --config <folder>/${LOCAL_CONFIG_FILE}\`, or use the setup-space skill.`,
|
|
8
7
|
E_CONFIG_INVALID: 'Fix the JSON in the config file named in the message; `sctx docs config` describes the shape.',
|
|
@@ -11,7 +10,7 @@ const FIXES = {
|
|
|
11
10
|
E_SPACE_PATH_MISSING: 'The folder or file was moved or deleted. Correct `path` in the config, or remove the space.',
|
|
12
11
|
E_SCHEMA_MISSING: 'Set `schema` on the space or at the top level of the config file.',
|
|
13
12
|
E_SCHEMA_NOT_FOUND: 'Check the schema name or path. `sctx schemas list` shows the bundled schemas.',
|
|
14
|
-
E_SCHEMA_INVALID: 'The schema
|
|
13
|
+
E_SCHEMA_INVALID: 'The schema could not be loaded. The message says what to fix; `sctx docs schema` describes the format.',
|
|
15
14
|
};
|
|
16
15
|
export function fixFor(code) {
|
|
17
16
|
return FIXES[code] ?? '';
|
|
@@ -20,10 +19,11 @@ function problem(code, severity, message, space) {
|
|
|
20
19
|
return { code, severity, message, fix: fixFor(code), ...(space ? { space } : {}) };
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Find and load the config for the working directory, reporting failures as problems with stable
|
|
23
|
+
* codes instead of throwing. It reports on the config only: what a space's `path` means belongs to
|
|
24
|
+
* the plugin that reads it, and a schema is checked when it is loaded.
|
|
25
25
|
*/
|
|
26
|
-
export function
|
|
26
|
+
export function inspectConfig(options = {}) {
|
|
27
27
|
const resolved = resolveConfigPath(options.startDir);
|
|
28
28
|
const exists = existsSync(resolved.path);
|
|
29
29
|
const health = { config: { ...resolved, exists }, configFiles: [], spaces: [], problems: [] };
|
|
@@ -32,7 +32,7 @@ export function checkConfig(options = {}) {
|
|
|
32
32
|
? `No ${LOCAL_CONFIG_FILE} found from the working directory upward, and no global config at ${resolved.path}`
|
|
33
33
|
: `Config file not found: ${resolved.path}`;
|
|
34
34
|
health.problems.push(problem('E_CONFIG_NOT_FOUND', resolved.source === 'none' ? 'info' : 'error', detail));
|
|
35
|
-
return health;
|
|
35
|
+
return { health };
|
|
36
36
|
}
|
|
37
37
|
let config;
|
|
38
38
|
try {
|
|
@@ -41,46 +41,13 @@ export function checkConfig(options = {}) {
|
|
|
41
41
|
catch (err) {
|
|
42
42
|
const code = err instanceof SctxError ? err.code : 'E_CONFIG_INVALID';
|
|
43
43
|
health.problems.push(problem(code, 'error', err.message));
|
|
44
|
-
return health;
|
|
44
|
+
return { health };
|
|
45
45
|
}
|
|
46
46
|
health.configFiles = [...getConfigSourceFiles()];
|
|
47
47
|
health.spaces = config.spaces.map((s) => ({ name: s.name, path: s.path, schema: s.schema ?? config.schema }));
|
|
48
|
-
|
|
49
|
-
health.problems.push(problem('E_NO_SPACES', 'warning', `Config ${resolved.path} defines no spaces`));
|
|
50
|
-
}
|
|
51
|
-
for (const space of config.spaces) {
|
|
52
|
-
if (!existsSync(space.path)) {
|
|
53
|
-
health.problems.push(problem('E_SPACE_PATH_MISSING', 'error', `Space "${space.name}" path does not exist: ${space.path}`, space.name));
|
|
54
|
-
}
|
|
55
|
-
const schemaRef = space.schema ?? config.schema;
|
|
56
|
-
if (!schemaRef) {
|
|
57
|
-
health.problems.push(problem('E_SCHEMA_MISSING', 'error', `Space "${space.name}" has no schema`, space.name));
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
let schemaPath;
|
|
61
|
-
try {
|
|
62
|
-
schemaPath = resolveSchema(config, space);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
health.problems.push(problem('E_SCHEMA_NOT_FOUND', 'error', err.message, space.name));
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (/^https?:\/\//i.test(schemaPath))
|
|
69
|
-
continue;
|
|
70
|
-
if (!existsSync(schemaPath)) {
|
|
71
|
-
health.problems.push(problem('E_SCHEMA_NOT_FOUND', 'error', `Space "${space.name}" schema not found: ${schemaPath}`, space.name));
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
JSON5.parse(readFileSync(schemaPath, 'utf-8'));
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
health.problems.push(problem('E_SCHEMA_INVALID', 'error', `Space "${space.name}" schema ${schemaPath} is not valid: ${err.message}`, space.name));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return health;
|
|
48
|
+
return { health, config };
|
|
82
49
|
}
|
|
83
|
-
/** True when the
|
|
50
|
+
/** True when the config problems stop validation from running. */
|
|
84
51
|
export function hasBlockingProblem(health) {
|
|
85
52
|
return health.problems.some((p) => p.severity === 'error');
|
|
86
53
|
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export declare function configPath(startDir?: string): string;
|
|
|
48
48
|
export declare function loadConfig(options?: {
|
|
49
49
|
startDir?: string;
|
|
50
50
|
}): Config;
|
|
51
|
+
/** Like `loadConfig`, for commands that work without a config: a missing config is empty, with no warning. */
|
|
52
|
+
export declare function loadConfigOrEmpty(options?: {
|
|
53
|
+
startDir?: string;
|
|
54
|
+
}): Config;
|
|
51
55
|
/** Get the full space config entry by name. Throws if not found. */
|
|
52
56
|
export declare function getSpaceConfig(name: string, config: Config): SpaceConfig;
|
|
53
57
|
/**
|
package/dist/config/index.js
CHANGED
|
@@ -72,7 +72,7 @@ export function setConfigPath(path) {
|
|
|
72
72
|
/** Get all config file paths that were loaded (main config + included configs). */
|
|
73
73
|
export function getConfigSourceFiles() {
|
|
74
74
|
if (_spaceSourceFiles.size === 0) {
|
|
75
|
-
|
|
75
|
+
loadConfigOrEmpty();
|
|
76
76
|
}
|
|
77
77
|
return new Set(_spaceSourceFiles.values());
|
|
78
78
|
}
|
|
@@ -169,12 +169,21 @@ function expandHome(p) {
|
|
|
169
169
|
return p === '~' || p.startsWith('~/') ? join(process.env.HOME ?? homedir(), p.slice(1)) : p;
|
|
170
170
|
}
|
|
171
171
|
export function loadConfig(options = {}) {
|
|
172
|
-
|
|
172
|
+
return loadConfigFile(options.startDir, true);
|
|
173
|
+
}
|
|
174
|
+
/** Like `loadConfig`, for commands that work without a config: a missing config is empty, with no warning. */
|
|
175
|
+
export function loadConfigOrEmpty(options = {}) {
|
|
176
|
+
return loadConfigFile(options.startDir, false);
|
|
177
|
+
}
|
|
178
|
+
function loadConfigFile(startDir, warnIfMissing) {
|
|
179
|
+
const { path, source } = resolveConfigPath(startDir);
|
|
173
180
|
if (!existsSync(path)) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
if (warnIfMissing) {
|
|
182
|
+
const where = source === 'none'
|
|
183
|
+
? `no ${LOCAL_CONFIG_FILE} found from ${resolve(startDir ?? process.cwd())} upward, and none at ${path}`
|
|
184
|
+
: path;
|
|
185
|
+
console.warn(`E_CONFIG_NOT_FOUND: Config file not found (${where})`);
|
|
186
|
+
}
|
|
178
187
|
return { spaces: [] };
|
|
179
188
|
}
|
|
180
189
|
const config = _loadConfig(path);
|
|
@@ -2,7 +2,7 @@ import type { NodeId, SpaceNode } from '../types';
|
|
|
2
2
|
/** Edge metadata merged into each ancestor/descendant entry. */
|
|
3
3
|
export type EdgeMetadata = {
|
|
4
4
|
_field: string;
|
|
5
|
-
|
|
5
|
+
_kind: 'hierarchy' | 'relationship';
|
|
6
6
|
/** Hierarchy name, present only for hierarchy edges. */
|
|
7
7
|
_hierarchy?: string;
|
|
8
8
|
_selfRef: boolean;
|
|
@@ -21,6 +21,6 @@ export type AugmentedFlatNode = Record<string, unknown> & {
|
|
|
21
21
|
*
|
|
22
22
|
* - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by id.
|
|
23
23
|
* - descendants: BFS via childrenIndex, nearest first, deduplicated by id.
|
|
24
|
-
* - Each entry merges the parent/child node's fields with edge metadata (_field,
|
|
24
|
+
* - Each entry merges the parent/child node's fields with edge metadata (_field, _kind, _hierarchy, _selfRef).
|
|
25
25
|
*/
|
|
26
26
|
export declare function augmentNode(node: SpaceNode, nodeIndex: ReadonlyMap<NodeId, SpaceNode>, childrenIndex: ReadonlyMap<NodeId, readonly SpaceNode[]>): AugmentedFlatNode;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
function edgeMetadata(ref) {
|
|
3
3
|
return {
|
|
4
4
|
_field: ref.field,
|
|
5
|
-
|
|
5
|
+
_kind: ref.kind,
|
|
6
6
|
...(ref.hierarchy !== undefined ? { _hierarchy: ref.hierarchy } : {}),
|
|
7
7
|
_selfRef: ref.selfRef,
|
|
8
8
|
};
|
|
@@ -22,7 +22,7 @@ function flattenData(node) {
|
|
|
22
22
|
*
|
|
23
23
|
* - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by id.
|
|
24
24
|
* - descendants: BFS via childrenIndex, nearest first, deduplicated by id.
|
|
25
|
-
* - Each entry merges the parent/child node's fields with edge metadata (_field,
|
|
25
|
+
* - Each entry merges the parent/child node's fields with edge metadata (_field, _kind, _hierarchy, _selfRef).
|
|
26
26
|
*/
|
|
27
27
|
export function augmentNode(node, nodeIndex, childrenIndex) {
|
|
28
28
|
const ancestors = buildAncestors(node, nodeIndex);
|
|
@@ -139,13 +139,13 @@ function applyDirective(node, aug, directive, childrenIndex, addById) {
|
|
|
139
139
|
case 'relationships': {
|
|
140
140
|
// Relationship-sourced ancestors (matched node is the child side)
|
|
141
141
|
for (const a of aug.ancestors) {
|
|
142
|
-
if (a.
|
|
142
|
+
if (a._kind === 'relationship' && matchesRelSpec(node, a, directive, true)) {
|
|
143
143
|
addById(a.id);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
// Relationship-sourced descendants (matched node is the parent side)
|
|
147
147
|
for (const d of aug.descendants) {
|
|
148
|
-
if (d.
|
|
148
|
+
if (d._kind === 'relationship' && matchesRelSpec(node, d, directive, false)) {
|
|
149
149
|
addById(d.id);
|
|
150
150
|
}
|
|
151
151
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { validate, watchValidate } from './commands/validate';
|
|
|
16
16
|
import { validateFileCommand } from './commands/validate-file';
|
|
17
17
|
import { loadConfig, setConfigPath } from './config';
|
|
18
18
|
import { CLI_NAME } from './constants';
|
|
19
|
+
import { SctxError } from './errors';
|
|
19
20
|
import { JudgeConfigError } from './judge/factory';
|
|
20
21
|
import { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
21
22
|
// Once node:process is loaded (vfile, via unified, loads it), Bun's console.log drops piped output
|
|
@@ -121,6 +122,7 @@ program
|
|
|
121
122
|
.argument('<space-name>', 'Space name')
|
|
122
123
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
123
124
|
.option('--hierarchy <name>', 'Hierarchy used to evaluate --filter (default: the main hierarchy)')
|
|
125
|
+
.option('--graph', 'Output the space graph format: nodes plus every hierarchy')
|
|
124
126
|
.action(async (spaceName, options) => dump(buildSpaceContext(spaceName), options));
|
|
125
127
|
program
|
|
126
128
|
.command('miro-sync')
|
|
@@ -227,4 +229,10 @@ renderCmd
|
|
|
227
229
|
await render(buildSpaceContext(spaceName), format, options);
|
|
228
230
|
});
|
|
229
231
|
program.addCommand(renderCmd);
|
|
230
|
-
|
|
232
|
+
// Expected errors print as one line; anything else keeps its stack trace
|
|
233
|
+
program.parseAsync().catch((err) => {
|
|
234
|
+
if (!(err instanceof SctxError))
|
|
235
|
+
throw err;
|
|
236
|
+
console.error(`${err.code}: ${err.message}`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
});
|
package/dist/judge/context.js
CHANGED
|
@@ -49,7 +49,7 @@ export function relatedNodes(graph, node, context) {
|
|
|
49
49
|
const parentsOf = (n) => graph.hierarchy === undefined
|
|
50
50
|
? []
|
|
51
51
|
: n.resolvedParents
|
|
52
|
-
.filter((p) => p.
|
|
52
|
+
.filter((p) => p.kind === 'hierarchy' && p.hierarchy === hierarchyName)
|
|
53
53
|
.map((p) => graph.nodes.get(p.id))
|
|
54
54
|
.filter((p) => p !== undefined);
|
|
55
55
|
const childrenOf = (n) => graph.hierarchyChildren.get(n.id) ?? [];
|
package/dist/load-space-graph.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { filterNodes } from './filter/filter-nodes';
|
|
2
2
|
import { readSpace } from './read/read-space';
|
|
3
3
|
import { createSpaceContext } from './space-context';
|
|
4
|
-
import { buildSpaceGraph } from './space-graph';
|
|
4
|
+
import { buildSpaceGraph, selectHierarchy } from './space-graph';
|
|
5
5
|
/**
|
|
6
6
|
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
7
7
|
* fail schema validation, build the graph for the selected hierarchy (the main
|
|
@@ -26,18 +26,6 @@ export async function assembleSpaceGraph(context, options = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
return graph;
|
|
28
28
|
}
|
|
29
|
-
/** Resolve a hierarchy by name, or the main hierarchy when no name is given. */
|
|
30
|
-
function selectHierarchy(metadata, name) {
|
|
31
|
-
if (name === undefined)
|
|
32
|
-
return metadata.hierarchy;
|
|
33
|
-
const hierarchies = metadata.hierarchies ?? [];
|
|
34
|
-
const hierarchy = hierarchies.find((h) => h.name === name);
|
|
35
|
-
if (!hierarchy) {
|
|
36
|
-
const available = hierarchies.map((h) => h.name).join(', ');
|
|
37
|
-
throw new Error(`Unknown hierarchy: "${name}".${available ? ` Available: ${available}` : ' The schema defines no hierarchies.'}`);
|
|
38
|
-
}
|
|
39
|
-
return hierarchy;
|
|
40
|
-
}
|
|
41
29
|
/**
|
|
42
30
|
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
43
31
|
*
|
|
@@ -364,7 +364,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
364
364
|
fieldOn: rel.fieldOn,
|
|
365
365
|
multiple: rel.multiple,
|
|
366
366
|
templateFormat: rel.templateFormat ?? 'heading',
|
|
367
|
-
|
|
367
|
+
kind: 'relationship',
|
|
368
368
|
matchers: rel.matchers,
|
|
369
369
|
embeddedTemplateFields: rel.embeddedTemplateFields,
|
|
370
370
|
};
|
|
@@ -378,7 +378,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
378
378
|
fieldOn: level.fieldOn,
|
|
379
379
|
multiple: level.multiple,
|
|
380
380
|
templateFormat: level.templateFormat ?? 'heading',
|
|
381
|
-
|
|
381
|
+
kind: 'hierarchy',
|
|
382
382
|
matchers: level.matchers,
|
|
383
383
|
embeddedTemplateFields: level.embeddedTemplateFields,
|
|
384
384
|
};
|
|
@@ -435,7 +435,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
435
435
|
fieldOn: currentLevel.fieldOn,
|
|
436
436
|
multiple: currentLevel.multiple,
|
|
437
437
|
templateFormat: currentLevel.templateFormat ?? 'list',
|
|
438
|
-
|
|
438
|
+
kind: 'hierarchy',
|
|
439
439
|
matchers: immediateParentLevel.matchers,
|
|
440
440
|
};
|
|
441
441
|
}
|
|
@@ -651,7 +651,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
651
651
|
// e.g. definition.type='capabilities', definition.parent='application' → capabilities is above application.
|
|
652
652
|
const typeIdx = hierarchy.indexOf(definition.type);
|
|
653
653
|
const parentIdx = hierarchy.indexOf(definition.parent);
|
|
654
|
-
const isParentLevelMatch = definition.
|
|
654
|
+
const isParentLevelMatch = definition.kind === 'hierarchy' && typeIdx !== -1 && parentIdx !== -1 && typeIdx < parentIdx;
|
|
655
655
|
const activeNodeFieldAppendArg = isParentLevelMatch && semanticParent.node
|
|
656
656
|
? { node: semanticParent.node, field: definition.field }
|
|
657
657
|
: undefined;
|
|
@@ -30,7 +30,7 @@ export function computeNodeHash(node) {
|
|
|
30
30
|
status: node.schemaData.status,
|
|
31
31
|
summary: node.schemaData.summary,
|
|
32
32
|
priority: node.schemaData.priority,
|
|
33
|
-
parents: node.resolvedParents.filter((r) => r.
|
|
33
|
+
parents: node.resolvedParents.filter((r) => r.kind === 'hierarchy').map((r) => r.id),
|
|
34
34
|
};
|
|
35
35
|
return createHash('sha256').update(JSON.stringify(relevant)).digest('hex').slice(0, 16);
|
|
36
36
|
}
|
|
@@ -257,8 +257,8 @@ export async function miroSync(context, graph, options) {
|
|
|
257
257
|
const desiredEdges = new Map();
|
|
258
258
|
for (const node of nodes) {
|
|
259
259
|
const childTitle = node.title;
|
|
260
|
-
for (const { id: parentId,
|
|
261
|
-
if (
|
|
260
|
+
for (const { id: parentId, kind, hierarchy } of node.resolvedParents) {
|
|
261
|
+
if (kind !== 'hierarchy' || hierarchy !== graph.hierarchy?.name)
|
|
262
262
|
continue;
|
|
263
263
|
const parentTitle = graph.nodes.get(parentId)?.title ?? parentId;
|
|
264
264
|
// Both endpoints must have verified cards on the board
|