structured-context 0.13.0 → 0.14.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 +2 -2
- package/dist/api.d.ts +17 -17
- package/dist/api.js +12 -12
- package/dist/commands/dump.js +1 -1
- package/dist/commands/validate.js +8 -8
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -4
- package/dist/filter/augment-nodes.d.ts +5 -4
- package/dist/filter/augment-nodes.js +20 -19
- package/dist/filter/expand-include.d.ts +2 -2
- package/dist/filter/expand-include.js +15 -17
- package/dist/filter/filter-nodes.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -17
- package/dist/judge/context.d.ts +1 -1
- package/dist/judge/context.js +12 -12
- package/dist/judge/factory.d.ts +1 -1
- package/dist/judge/factory.js +1 -1
- package/dist/judge/index.d.ts +1 -1
- package/dist/judge/judge-node.d.ts +4 -4
- package/dist/judge/judge-node.js +7 -7
- package/dist/judge/prompt.d.ts +1 -1
- package/dist/judge/prompt.js +3 -3
- package/dist/judge/provider/fake.d.ts +1 -1
- package/dist/judge/provider/fake.js +3 -6
- package/dist/judge/provider/jev.js +1 -1
- package/dist/judge/provider/openai-decisions.js +1 -1
- package/dist/judge/trim.d.ts +1 -1
- package/dist/judge/types.d.ts +4 -1
- package/dist/judge/verdicts.d.ts +1 -1
- package/dist/load-space-graph.d.ts +3 -3
- package/dist/load-space-graph.js +4 -4
- package/dist/plugins/loader.js +1 -1
- package/dist/plugins/markdown/index.d.ts +6 -0
- package/dist/plugins/markdown/index.js +1 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +7 -2
- package/dist/plugins/markdown/parse-embedded.js +23 -21
- package/dist/plugins/markdown/read-space.js +9 -8
- package/dist/plugins/markdown/render-bullets.js +3 -3
- package/dist/plugins/markdown/render-directory.js +1 -1
- package/dist/plugins/markdown/util.d.ts +12 -1
- package/dist/plugins/markdown/util.js +21 -0
- package/dist/plugins/mermaid/render-mermaid.js +5 -6
- package/dist/plugins/miro/cache.js +1 -1
- package/dist/plugins/miro/sync.js +3 -2
- package/dist/read/resolve-graph-edges.js +12 -18
- package/dist/render/render.d.ts +1 -1
- package/dist/render/render.js +1 -1
- package/dist/schema/evaluate-rule.d.ts +4 -4
- package/dist/schema/evaluate-rule.js +14 -10
- package/dist/schema/gating.d.ts +10 -10
- package/dist/schema/gating.js +45 -44
- package/dist/schema/validate-graph.js +8 -11
- package/dist/schema/validate-rules.d.ts +4 -4
- package/dist/schema/validate-rules.js +15 -17
- package/dist/schemas/_ost_strict.json +1 -1
- package/dist/space-context.d.ts +2 -2
- package/dist/space-context.js +3 -3
- package/dist/space-graph.d.ts +9 -9
- package/dist/space-graph.js +16 -16
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/dist/types.d.ts +18 -14
- package/dist/validate.d.ts +6 -6
- package/dist/validate.js +26 -27
- package/docs/concepts.md +25 -7
- package/docs/config.md +8 -0
- package/docs/rules.md +17 -13
- package/package.json +5 -5
- package/schemas/_ost_strict.json +1 -1
package/dist/judge/judge-node.js
CHANGED
|
@@ -7,7 +7,7 @@ function resolveAll(rules) {
|
|
|
7
7
|
function errorJudgements(rules, message) {
|
|
8
8
|
return rules.map((rule) => ({
|
|
9
9
|
rule,
|
|
10
|
-
|
|
10
|
+
violated: true,
|
|
11
11
|
evidence: `Error during evaluation: ${message}`,
|
|
12
12
|
error: message,
|
|
13
13
|
}));
|
|
@@ -17,20 +17,20 @@ function interpret(rules, resolved, results) {
|
|
|
17
17
|
const result = results[rule.id];
|
|
18
18
|
if (!result || isJudgeError(result)) {
|
|
19
19
|
const message = result ? result.error : 'Model provider returned no result for this rule';
|
|
20
|
-
return { rule,
|
|
20
|
+
return { rule, violated: true, evidence: `Error during evaluation: ${message}`, error: message };
|
|
21
21
|
}
|
|
22
22
|
const severity = resolved[i].severities[result.verdict];
|
|
23
23
|
if (severity === undefined && !(result.verdict in resolved[i].spec.verdicts)) {
|
|
24
24
|
const message = `Model provider returned unknown verdict "${result.verdict}" (allowed: ${Object.keys(resolved[i].spec.verdicts).join(', ')})`;
|
|
25
|
-
return { rule,
|
|
25
|
+
return { rule, violated: true, evidence: `Error during evaluation: ${message}`, error: message };
|
|
26
26
|
}
|
|
27
|
-
const
|
|
27
|
+
const violated = severity !== undefined;
|
|
28
28
|
return {
|
|
29
29
|
rule,
|
|
30
|
-
|
|
30
|
+
violated,
|
|
31
31
|
severity,
|
|
32
32
|
verdict: result.verdict,
|
|
33
|
-
guidance:
|
|
33
|
+
guidance: violated ? resolved[i].guidance[result.verdict] : undefined,
|
|
34
34
|
evidence: result.evidence,
|
|
35
35
|
};
|
|
36
36
|
});
|
|
@@ -62,7 +62,7 @@ function planUnits(graph, items, report) {
|
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
if (fitted.trimmed && report) {
|
|
65
|
-
report.nodes.add(node.
|
|
65
|
+
report.nodes.add(node.id);
|
|
66
66
|
for (const rule of unitRules)
|
|
67
67
|
report.ruleIds.add(rule.id);
|
|
68
68
|
}
|
package/dist/judge/prompt.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SemanticJudgeInput, SemanticJudgeResult, SemanticRuleSpec } from '
|
|
1
|
+
import type { SemanticJudgeInput, SemanticJudgeResult, SemanticRuleSpec } from './types';
|
|
2
2
|
/** System prompt: a strict judge that answers with one verdict from a closed set, as JSON. */
|
|
3
3
|
export declare function buildJudgeSystemPrompt(): string;
|
|
4
4
|
/** User prompt for one rule on one node. */
|
package/dist/judge/prompt.js
CHANGED
|
@@ -34,12 +34,12 @@ export function buildJudgeUserPrompt({ node, related = [] }, rule) {
|
|
|
34
34
|
...verdictList(rule),
|
|
35
35
|
'',
|
|
36
36
|
'# Node Under Evaluation',
|
|
37
|
-
`-
|
|
37
|
+
`- Id: ${node.id}`,
|
|
38
38
|
`- Title: ${node.title}`,
|
|
39
39
|
`- Type: ${node.type} (resolved: ${node.resolvedType})`,
|
|
40
40
|
];
|
|
41
|
-
if (node.
|
|
42
|
-
lines.push(`- Parents: ${node.
|
|
41
|
+
if (node.parentTitles.length > 0) {
|
|
42
|
+
lines.push(`- Parents: ${node.parentTitles.join(', ')}`);
|
|
43
43
|
}
|
|
44
44
|
lines.push('', '## Node Content & Frontmatter:', '```json', JSON.stringify(node.schemaData, null, 2), '```', '');
|
|
45
45
|
if (related.length > 0) {
|
|
@@ -11,7 +11,7 @@ export type FakeJudgeHandler = (call: {
|
|
|
11
11
|
type FakeConfig = FakeAnswer | FakeJudgeHandler | Map<string, FakeAnswer> | Record<string, FakeAnswer>;
|
|
12
12
|
/**
|
|
13
13
|
* In-memory fake judge for offline, deterministic tests. Configure with a single answer, a handler,
|
|
14
|
-
* or answers keyed by `ruleId:title`, `ruleId:
|
|
14
|
+
* or answers keyed by `ruleId:title`, `ruleId:id` or `ruleId` (most specific wins). Unmatched calls get `pass`,
|
|
15
15
|
* or the rule's first declared verdict.
|
|
16
16
|
*/
|
|
17
17
|
export declare class FakeJudgeProvider implements SemanticJudgeProvider {
|
|
@@ -12,7 +12,7 @@ function normalise(answer) {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* In-memory fake judge for offline, deterministic tests. Configure with a single answer, a handler,
|
|
15
|
-
* or answers keyed by `ruleId:title`, `ruleId:
|
|
15
|
+
* or answers keyed by `ruleId:title`, `ruleId:id` or `ruleId` (most specific wins). Unmatched calls get `pass`,
|
|
16
16
|
* or the rule's first declared verdict.
|
|
17
17
|
*/
|
|
18
18
|
export class FakeJudgeProvider {
|
|
@@ -40,15 +40,12 @@ export class FakeJudgeProvider {
|
|
|
40
40
|
if (isHandler(config))
|
|
41
41
|
return await config({ node, rule });
|
|
42
42
|
if (config instanceof Map) {
|
|
43
|
-
return (config.get(`${rule.id}:${node.title}`) ??
|
|
44
|
-
config.get(`${rule.id}:${node.label}`) ??
|
|
45
|
-
config.get(rule.id) ??
|
|
46
|
-
fallback);
|
|
43
|
+
return (config.get(`${rule.id}:${node.title}`) ?? config.get(`${rule.id}:${node.id}`) ?? config.get(rule.id) ?? fallback);
|
|
47
44
|
}
|
|
48
45
|
if (isAnswer(config))
|
|
49
46
|
return config;
|
|
50
47
|
const rec = config;
|
|
51
|
-
return rec[`${rule.id}:${node.title}`] ?? rec[`${rule.id}:${node.
|
|
48
|
+
return rec[`${rule.id}:${node.title}`] ?? rec[`${rule.id}:${node.id}`] ?? rec[rule.id] ?? fallback;
|
|
52
49
|
}
|
|
53
50
|
clearHistory() {
|
|
54
51
|
this.callHistory = [];
|
|
@@ -109,7 +109,7 @@ function nodeState(node, related = []) {
|
|
|
109
109
|
return {
|
|
110
110
|
title: node.title,
|
|
111
111
|
type: node.resolvedType,
|
|
112
|
-
parents: node.
|
|
112
|
+
parents: node.parentTitles,
|
|
113
113
|
...node.schemaData,
|
|
114
114
|
...(related.length > 0 && {
|
|
115
115
|
relatedNodes: related.map((r) => ({
|
|
@@ -22,7 +22,7 @@ export class OpenAIDecisionsJudgeProvider {
|
|
|
22
22
|
const stateLines = [
|
|
23
23
|
`Title: ${node.title}`,
|
|
24
24
|
`Type: ${node.resolvedType}`,
|
|
25
|
-
...(node.
|
|
25
|
+
...(node.parentTitles.length ? [`Parents: ${node.parentTitles.join(', ')}`] : []),
|
|
26
26
|
...fieldLines(node.schemaData ?? {}),
|
|
27
27
|
...(input.related ?? []).flatMap((r) => [
|
|
28
28
|
'',
|
package/dist/judge/trim.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SemanticJudgeInput } from '
|
|
1
|
+
import type { SemanticJudgeInput } from './types';
|
|
2
2
|
/** The per-node input cap: estimated tokens of one node's input (the node and its related nodes), the same for every provider. */
|
|
3
3
|
export declare const PER_NODE_INPUT_CAP_TOKENS = 32000;
|
|
4
4
|
/** Estimated tokens of a value: its JSON length divided by 4. Provider-independent, and roughly right is enough. */
|
package/dist/judge/types.d.ts
CHANGED
|
@@ -26,7 +26,10 @@ export interface RelatedNode {
|
|
|
26
26
|
*/
|
|
27
27
|
export interface SemanticJudgeInput {
|
|
28
28
|
/** The evaluated node. Its body is in `schemaData.content` only when the rules' `includeContent` asks for it. */
|
|
29
|
-
node: Pick<SpaceNode, '
|
|
29
|
+
node: Pick<SpaceNode, 'id' | 'title' | 'type' | 'resolvedType' | 'schemaData'> & {
|
|
30
|
+
/** Display titles of the node's parents. */
|
|
31
|
+
parentTitles: string[];
|
|
32
|
+
};
|
|
30
33
|
/** Nodes the rules' `scope` adds, as a flat list. Absent when the scope is empty. */
|
|
31
34
|
related?: RelatedNode[];
|
|
32
35
|
rules: SemanticRuleSpec[];
|
package/dist/judge/verdicts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Rule, RuleSeverity } from '../types';
|
|
2
|
-
import type { SemanticJudgeResult, SemanticRuleSpec } from '
|
|
2
|
+
import type { SemanticJudgeResult, SemanticRuleSpec } from './types';
|
|
3
3
|
/** Verdicts for a semantic rule that declares none. */
|
|
4
4
|
export declare const DEFAULT_VERDICTS: Record<string, string>;
|
|
5
5
|
/** A semantic rule resolved for evaluation: what the judge sees, and what each verdict means for the user. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Config } from './config
|
|
2
|
-
import { type SpaceGraph } from './space-graph
|
|
3
|
-
import type { ReadSpaceResult, SpaceContext } from './types
|
|
1
|
+
import type { Config } from './config';
|
|
2
|
+
import { type SpaceGraph } from './space-graph';
|
|
3
|
+
import type { ReadSpaceResult, SpaceContext } from './types';
|
|
4
4
|
export interface AssembleSpaceGraphOptions {
|
|
5
5
|
/** Named view (resolved via space.views) or a raw filter DSL expression. */
|
|
6
6
|
filter?: string;
|
package/dist/load-space-graph.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { filterNodes } from './filter/filter-nodes
|
|
2
|
-
import { readSpace } from './read/read-space
|
|
3
|
-
import { createSpaceContext } from './space-context
|
|
4
|
-
import { buildSpaceGraph } from './space-graph
|
|
1
|
+
import { filterNodes } from './filter/filter-nodes';
|
|
2
|
+
import { readSpace } from './read/read-space';
|
|
3
|
+
import { createSpaceContext } from './space-context';
|
|
4
|
+
import { buildSpaceGraph } 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
|
package/dist/plugins/loader.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import Ajv2020 from 'ajv/dist/2020';
|
|
4
4
|
import { getConfigSourceFiles } from '../config';
|
|
5
|
-
import { builtinPlugins } from '
|
|
5
|
+
import { builtinPlugins } from '.';
|
|
6
6
|
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX, shortenPluginName, } from './util';
|
|
7
7
|
/**
|
|
8
8
|
* Walk a plugin's configSchema and resolve any string fields annotated with
|
|
@@ -32,6 +32,12 @@ export declare const MARKDOWN_CONFIG_SCHEMA: {
|
|
|
32
32
|
readonly templatePrefix: {
|
|
33
33
|
readonly type: "string";
|
|
34
34
|
};
|
|
35
|
+
readonly folderIndex: {
|
|
36
|
+
readonly type: "array";
|
|
37
|
+
readonly items: {
|
|
38
|
+
readonly type: "string";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
35
41
|
readonly typeInference: {
|
|
36
42
|
readonly type: "object";
|
|
37
43
|
readonly properties: {
|
|
@@ -18,6 +18,7 @@ export const MARKDOWN_CONFIG_SCHEMA = {
|
|
|
18
18
|
templateDir: { type: 'string', format: 'path' }, // format is hint to config loader to resolve relative directories
|
|
19
19
|
fieldMap: { type: 'object', additionalProperties: { type: 'string' } },
|
|
20
20
|
templatePrefix: { type: 'string' },
|
|
21
|
+
folderIndex: { type: 'array', items: { type: 'string' } },
|
|
21
22
|
typeInference: TYPE_INFERENCE_CONFIG_SCHEMA,
|
|
22
23
|
},
|
|
23
24
|
additionalProperties: false,
|
|
@@ -57,10 +57,15 @@ export declare function normalizeHeadingSectionTarget(rawHeadingText: string): s
|
|
|
57
57
|
export declare function defaultNodeType(stack: StackEntry[], hierarchy: readonly string[]): string;
|
|
58
58
|
export interface ExtractEmbeddedOptions {
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* The containing page. If provided (and pageType is not a space_on_a_page type),
|
|
61
61
|
* the page acts as a virtual depth-0 parent for first-level embedded headings.
|
|
62
|
+
* `id` is the page's node id (embedded node ids are `<id>#<title>`); `targets` are its
|
|
63
|
+
* link targets, most specific first (the first is used in generated parent links).
|
|
62
64
|
*/
|
|
63
|
-
|
|
65
|
+
page?: {
|
|
66
|
+
id: string;
|
|
67
|
+
targets: string[];
|
|
68
|
+
};
|
|
64
69
|
/**
|
|
65
70
|
* Node type of the containing page.
|
|
66
71
|
* - If set to a real node type: only headings with an explicit `[type:: x]` field or a
|
|
@@ -42,7 +42,7 @@ function appendParentField(parentNode, field, linkRef) {
|
|
|
42
42
|
fieldValue.push(linkRef);
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
45
|
-
throw new Error(`Cannot append child link to field '${field}' on node '${parentNode.
|
|
45
|
+
throw new Error(`Cannot append child link to field '${field}' on node '${parentNode.id}': ` +
|
|
46
46
|
`field exists but is not an array (found ${typeof fieldValue}). ` +
|
|
47
47
|
`Child link: ${linkRef}`);
|
|
48
48
|
}
|
|
@@ -219,7 +219,7 @@ function isFieldOnlyLine(line) {
|
|
|
219
219
|
return hasBracketed;
|
|
220
220
|
return /^[a-zA-Z][a-zA-Z0-9_-]*:: *.*$/.test(cleanText);
|
|
221
221
|
}
|
|
222
|
-
function processListItem(item, parentRef, contentTarget, nodes,
|
|
222
|
+
function processListItem(item, parentRef, contentTarget, nodes, makeId, buildLinkTargets, typeAliases, fieldMap, pendingType, parentFieldAppend, activeNodeFieldAppend, file, lineOffset, walk) {
|
|
223
223
|
const itemEnd = offsetsOf(item).end;
|
|
224
224
|
const firstPara = item.children.find((c) => c.type === 'paragraph');
|
|
225
225
|
if (!firstPara) {
|
|
@@ -270,7 +270,7 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
270
270
|
: undefined;
|
|
271
271
|
const linkTargets = buildLinkTargets(title);
|
|
272
272
|
const newNode = {
|
|
273
|
-
|
|
273
|
+
id: makeId(title),
|
|
274
274
|
title,
|
|
275
275
|
schemaData,
|
|
276
276
|
linkTargets,
|
|
@@ -294,7 +294,7 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
294
294
|
continue;
|
|
295
295
|
if (child.type === 'list') {
|
|
296
296
|
for (const subItem of child.children) {
|
|
297
|
-
processListItem(subItem, nestedParentRef, newNode, nodes,
|
|
297
|
+
processListItem(subItem, nestedParentRef, newNode, nodes, makeId, buildLinkTargets, typeAliases, fieldMap, pendingType, undefined, undefined, file, lineOffset, walk);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
else {
|
|
@@ -316,7 +316,7 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
316
316
|
* (directory) to find embedded sub-nodes within a page's content.
|
|
317
317
|
*/
|
|
318
318
|
export function extractEmbeddedNodes(body, options) {
|
|
319
|
-
const {
|
|
319
|
+
const { page, pageType, metadata, fieldMap } = options;
|
|
320
320
|
const levels = metadata.hierarchy?.levels ?? [];
|
|
321
321
|
const hierarchy = levels.map((l) => l.type);
|
|
322
322
|
const relationships = metadata.relationships ?? [];
|
|
@@ -325,7 +325,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
325
325
|
const nodes = [];
|
|
326
326
|
// Preamble/root content sink - never added to nodes
|
|
327
327
|
const rootNode = {
|
|
328
|
-
|
|
328
|
+
id: '_root_',
|
|
329
329
|
title: '_root_',
|
|
330
330
|
schemaData: { type: 'space_on_a_page' },
|
|
331
331
|
linkTargets: [],
|
|
@@ -335,8 +335,8 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
335
335
|
const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
|
|
336
336
|
// In typed-page mode: stack starts with the page's own virtual entry (depth 0).
|
|
337
337
|
// In space_on_a_page mode: stack starts empty (first heading has no parent).
|
|
338
|
-
const stack = !isOnAPageMode &&
|
|
339
|
-
? [{ depth: 0, title:
|
|
338
|
+
const stack = !isOnAPageMode && page !== undefined
|
|
339
|
+
? [{ depth: 0, title: page.targets[0] ?? page.id, nodeType: pageType, refTarget: page.targets[0] ?? page.id }]
|
|
340
340
|
: [];
|
|
341
341
|
let parseState = isOnAPageMode ? 'preamble' : 'active';
|
|
342
342
|
let preambleNodeCount = 0;
|
|
@@ -442,8 +442,8 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
442
442
|
}
|
|
443
443
|
return undefined;
|
|
444
444
|
}
|
|
445
|
-
function
|
|
446
|
-
return title;
|
|
445
|
+
function makeId(title) {
|
|
446
|
+
return (page ? `${page.id}#${title}` : title);
|
|
447
447
|
}
|
|
448
448
|
/**
|
|
449
449
|
* Walk the stack backwards to find the deepest typed node entry (nodeType !== '').
|
|
@@ -486,24 +486,26 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
486
486
|
return g.headingNode;
|
|
487
487
|
}
|
|
488
488
|
function buildHeadingLinkTargets(rawHeadingText, title, anchor) {
|
|
489
|
-
if (!
|
|
489
|
+
if (!page) {
|
|
490
490
|
return [title];
|
|
491
491
|
}
|
|
492
492
|
const targets = [];
|
|
493
493
|
const sectionTarget = normalizeHeadingSectionTarget(rawHeadingText);
|
|
494
494
|
if (sectionTarget) {
|
|
495
|
-
targets
|
|
495
|
+
for (const pageTarget of page.targets)
|
|
496
|
+
targets.push(`${pageTarget}#${sectionTarget}`);
|
|
496
497
|
}
|
|
497
498
|
if (anchor) {
|
|
498
|
-
targets
|
|
499
|
+
for (const pageTarget of page.targets)
|
|
500
|
+
targets.push(`${pageTarget}#^${anchor}`);
|
|
499
501
|
}
|
|
500
502
|
return targets.length > 0 ? targets : [title];
|
|
501
503
|
}
|
|
502
504
|
function buildListItemLinkTargets(title) {
|
|
503
|
-
if (!
|
|
505
|
+
if (!page)
|
|
504
506
|
return [title];
|
|
505
507
|
const normalized = normalizeHeadingSectionTarget(title);
|
|
506
|
-
return normalized ?
|
|
508
|
+
return normalized ? page.targets.map((pageTarget) => `${pageTarget}#${normalized}`) : [title];
|
|
507
509
|
}
|
|
508
510
|
let grouping = null;
|
|
509
511
|
let activeNode = rootNode;
|
|
@@ -604,7 +606,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
604
606
|
: undefined;
|
|
605
607
|
const linkTargets = buildHeadingLinkTargets(rawText, title, anchor);
|
|
606
608
|
const headingNode = {
|
|
607
|
-
|
|
609
|
+
id: makeId(title),
|
|
608
610
|
title,
|
|
609
611
|
schemaData,
|
|
610
612
|
linkTargets,
|
|
@@ -655,14 +657,14 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
655
657
|
: undefined;
|
|
656
658
|
const walk = { claims, pos: spanStart };
|
|
657
659
|
for (const item of list.children) {
|
|
658
|
-
processListItem(item, isParentSide ? undefined : semanticParent.ref, grouping.headingNode, nodes,
|
|
660
|
+
processListItem(item, isParentSide ? undefined : semanticParent.ref, grouping.headingNode, nodes, makeId, buildListItemLinkTargets, typeAliases, fieldMap, definition.type, parentFieldAppendArg, activeNodeFieldAppendArg, options.file, options.lineOffset, walk);
|
|
659
661
|
}
|
|
660
662
|
grouping = null;
|
|
661
663
|
}
|
|
662
664
|
else {
|
|
663
665
|
const walk = { claims, pos: spanStart };
|
|
664
666
|
for (const item of list.children) {
|
|
665
|
-
processListItem(item, parentRef, activeNode, nodes,
|
|
667
|
+
processListItem(item, parentRef, activeNode, nodes, makeId, buildListItemLinkTargets, typeAliases, fieldMap, undefined, undefined, undefined, options.file, options.lineOffset, walk);
|
|
666
668
|
}
|
|
667
669
|
}
|
|
668
670
|
}
|
|
@@ -755,7 +757,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
755
757
|
: undefined;
|
|
756
758
|
const linkTargets = buildListItemLinkTargets(title);
|
|
757
759
|
const rowNode = {
|
|
758
|
-
|
|
760
|
+
id: makeId(title),
|
|
759
761
|
title,
|
|
760
762
|
schemaData,
|
|
761
763
|
linkTargets,
|
|
@@ -788,7 +790,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
788
790
|
const { fields: unbracketedFields } = extractUnbracketedFields(afterBracketed);
|
|
789
791
|
const allFields = applyFieldMap({ ...unbracketedFields, ...bracketedFields }, fieldMap);
|
|
790
792
|
if ('type' in allFields) {
|
|
791
|
-
throw new Error(`Type override via paragraph field is not supported at "${activeNode.schemaData.title ?? activeNode.
|
|
793
|
+
throw new Error(`Type override via paragraph field is not supported at "${activeNode.schemaData.title ?? activeNode.id}". ` +
|
|
792
794
|
`Put [type:: ${allFields.type}] directly in the heading text.`);
|
|
793
795
|
}
|
|
794
796
|
Object.assign(activeNode.schemaData, allFields);
|
|
@@ -817,7 +819,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
817
819
|
consumeBlock(activeNode);
|
|
818
820
|
}
|
|
819
821
|
else if (Array.isArray(parsed)) {
|
|
820
|
-
throw new Error(`YAML block must be an object at "${activeNode.
|
|
822
|
+
throw new Error(`YAML block must be an object at "${activeNode.id}".`);
|
|
821
823
|
}
|
|
822
824
|
else {
|
|
823
825
|
claimBlock(activeNode);
|
|
@@ -5,7 +5,7 @@ import matter from 'gray-matter';
|
|
|
5
5
|
import { extractSchemaTypeNames } from '../../schema/schema';
|
|
6
6
|
import { extractLinksFromBody, extractLinksFromFrontmatter, getEdgeFieldNames } from './extract-content-links';
|
|
7
7
|
import { extractEmbeddedNodes, ON_A_PAGE_TYPES } from './parse-embedded';
|
|
8
|
-
import { applyFieldMap, inferTypeFromPath, MATTER_OPTIONS } from './util';
|
|
8
|
+
import { applyFieldMap, defaultPageTitle, inferTypeFromPath, MATTER_OPTIONS, pathLinkTargets } from './util';
|
|
9
9
|
export function readSpaceOnAPage(context) {
|
|
10
10
|
const { space, resolvedSchemaPath, schema: { metadata }, } = context;
|
|
11
11
|
const filePath = resolve(space.path);
|
|
@@ -20,12 +20,11 @@ export function readSpaceOnAPage(context) {
|
|
|
20
20
|
if (!hierarchyLevels || hierarchyLevels.length === 0) {
|
|
21
21
|
throw new Error(`Schema at ${resolvedSchemaPath} must define "$metadata.hierarchy.levels" to read a space_on_a_page file.`);
|
|
22
22
|
}
|
|
23
|
-
const pageTitle = basename(filePath, '.md');
|
|
24
23
|
const fmMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
25
24
|
const lineOffset = fmMatch ? (fmMatch[0].match(/\n/g) || []).length : 0;
|
|
26
25
|
const fileName = basename(filePath);
|
|
27
26
|
const { nodes, preambleNodeCount, terminatedHeadings } = extractEmbeddedNodes(body, {
|
|
28
|
-
|
|
27
|
+
page: { id: fileName, targets: pathLinkTargets(fileName) },
|
|
29
28
|
pageType: 'space_on_a_page',
|
|
30
29
|
metadata,
|
|
31
30
|
file: fileName,
|
|
@@ -98,15 +97,17 @@ export async function readSpaceDirectory(context, options) {
|
|
|
98
97
|
continue;
|
|
99
98
|
}
|
|
100
99
|
const pageType = data.type;
|
|
101
|
-
const
|
|
102
|
-
const title =
|
|
100
|
+
const explicitTitle = data.title;
|
|
101
|
+
const title = explicitTitle ?? defaultPageTitle(file, mdCfg.folderIndex);
|
|
102
|
+
const pathTargets = pathLinkTargets(file);
|
|
103
103
|
const fmMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
104
104
|
const lineOffset = fmMatch ? (fmMatch[0].match(/\n/g) || []).length : 0;
|
|
105
105
|
const pageNode = {
|
|
106
|
-
|
|
106
|
+
id: file,
|
|
107
107
|
title,
|
|
108
108
|
schemaData: { title, ...data },
|
|
109
|
-
|
|
109
|
+
// A title derived from a folder index is display only, never a link target.
|
|
110
|
+
linkTargets: [...new Set([...(explicitTitle !== undefined ? [explicitTitle] : []), ...pathTargets])],
|
|
110
111
|
type: pageType,
|
|
111
112
|
source: { file, line: 1 },
|
|
112
113
|
...(data.content !== undefined ? { content: data.content } : {}),
|
|
@@ -114,7 +115,7 @@ export async function readSpaceDirectory(context, options) {
|
|
|
114
115
|
};
|
|
115
116
|
if (!ON_A_PAGE_TYPES.includes(pageType)) {
|
|
116
117
|
const { nodes: embedded, preambleContent, preambleFields, terminatedHeadings, } = extractEmbeddedNodes(parsed.content, {
|
|
117
|
-
|
|
118
|
+
page: { id: file, targets: pathTargets },
|
|
118
119
|
pageType,
|
|
119
120
|
metadata,
|
|
120
121
|
fieldMap,
|
|
@@ -6,14 +6,14 @@ export function renderBullets(graph) {
|
|
|
6
6
|
const indent = ' '.repeat(depth);
|
|
7
7
|
const type = node.resolvedType;
|
|
8
8
|
const title = node.title;
|
|
9
|
-
const nodeChildren = children.get(
|
|
10
|
-
if (seen.has(
|
|
9
|
+
const nodeChildren = children.get(node.id) ?? [];
|
|
10
|
+
if (seen.has(node.id)) {
|
|
11
11
|
if (nodeChildren.length > 0) {
|
|
12
12
|
lines.push(`${indent}- ${type}: ${title} (*)`);
|
|
13
13
|
}
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
seen.add(
|
|
16
|
+
seen.add(node.id);
|
|
17
17
|
lines.push(`${indent}- ${type}: ${title}`);
|
|
18
18
|
for (const child of nodeChildren) {
|
|
19
19
|
renderNode(child, depth + 1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import matter from 'gray-matter';
|
|
3
|
-
import { getMarkdownConfig } from '
|
|
3
|
+
import { getMarkdownConfig } from './index';
|
|
4
4
|
import { invertFieldMap, MATTER_OPTIONS } from './util';
|
|
5
5
|
/**
|
|
6
6
|
* Sanitize a node title into a valid filesystem filename.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DumpOptions, type Schema } from 'js-yaml';
|
|
2
|
-
import type { TypeInferenceConfig } from '
|
|
2
|
+
import type { TypeInferenceConfig } from '.';
|
|
3
3
|
export declare function inferTypeFromPath(filePath: string, config: TypeInferenceConfig, knownTypes: Set<string>, typeAliases: Record<string, string> | undefined): string | undefined;
|
|
4
4
|
/**
|
|
5
5
|
* YAML schema for frontmatter and embedded YAML blocks: YAML 1.2 Core schema extended with
|
|
@@ -38,3 +38,14 @@ export declare function applyFieldMap(data: Record<string, unknown>, fieldMap: R
|
|
|
38
38
|
* Used for write operations (e.g. template-sync) to translate back to file field names.
|
|
39
39
|
*/
|
|
40
40
|
export declare function invertFieldMap(fieldMap: Record<string, string>): Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Link targets for a page, most specific first: the space-relative path without `.md` and every shorter
|
|
43
|
+
* path suffix down to the basename. Obsidian writes these when a bare name is ambiguous.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pathLinkTargets(file: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Display title for a page without an explicit `title`: the containing folder's name when the file is a
|
|
48
|
+
* folder index (its basename is listed in `folderIndex`, case-insensitively), otherwise the basename.
|
|
49
|
+
* An index at the space root has no folder to name it and keeps its basename.
|
|
50
|
+
*/
|
|
51
|
+
export declare function defaultPageTitle(file: string, folderIndex: string[] | undefined): string;
|
|
@@ -119,3 +119,24 @@ export function applyFieldMap(data, fieldMap) {
|
|
|
119
119
|
export function invertFieldMap(fieldMap) {
|
|
120
120
|
return Object.fromEntries(Object.entries(fieldMap).map(([src, canonical]) => [canonical, src]));
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Link targets for a page, most specific first: the space-relative path without `.md` and every shorter
|
|
124
|
+
* path suffix down to the basename. Obsidian writes these when a bare name is ambiguous.
|
|
125
|
+
*/
|
|
126
|
+
export function pathLinkTargets(file) {
|
|
127
|
+
const segments = file.replace(/\\/g, '/').replace(/\.md$/i, '').split('/');
|
|
128
|
+
return segments.map((_, i) => segments.slice(i).join('/'));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Display title for a page without an explicit `title`: the containing folder's name when the file is a
|
|
132
|
+
* folder index (its basename is listed in `folderIndex`, case-insensitively), otherwise the basename.
|
|
133
|
+
* An index at the space root has no folder to name it and keeps its basename.
|
|
134
|
+
*/
|
|
135
|
+
export function defaultPageTitle(file, folderIndex) {
|
|
136
|
+
const segments = file.replace(/\\/g, '/').replace(/\.md$/i, '').split('/');
|
|
137
|
+
const base = segments[segments.length - 1];
|
|
138
|
+
const folder = segments[segments.length - 2];
|
|
139
|
+
if (folder !== undefined && folderIndex?.some((name) => name.toLowerCase() === base.toLowerCase()))
|
|
140
|
+
return folder;
|
|
141
|
+
return base;
|
|
142
|
+
}
|
|
@@ -15,7 +15,7 @@ function safeNodeId(id) {
|
|
|
15
15
|
return id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
16
16
|
}
|
|
17
17
|
export function renderMermaid(graph) {
|
|
18
|
-
const { hierarchyRoots, orphans, hierarchyChildren: children,
|
|
18
|
+
const { hierarchyRoots, orphans, hierarchyChildren: children, hierarchyIds: hierarchyNodeSet } = graph;
|
|
19
19
|
const levels = graph.hierarchy?.levels ?? [];
|
|
20
20
|
let mmd = 'graph TD\n';
|
|
21
21
|
for (let i = 0; i < levels.length; i++) {
|
|
@@ -25,21 +25,20 @@ export function renderMermaid(graph) {
|
|
|
25
25
|
}
|
|
26
26
|
const addedNodes = new Set();
|
|
27
27
|
function addNodeAndChildren(node) {
|
|
28
|
-
const nodeId = node.
|
|
28
|
+
const nodeId = node.id;
|
|
29
29
|
if (addedNodes.has(nodeId))
|
|
30
30
|
return;
|
|
31
31
|
addedNodes.add(nodeId);
|
|
32
32
|
const type = node.resolvedType;
|
|
33
33
|
const priority = node.schemaData.priority;
|
|
34
|
-
const label = priority ? `${
|
|
34
|
+
const label = priority ? `${node.title} (${priority})` : node.title;
|
|
35
35
|
const safeId = safeNodeId(nodeId);
|
|
36
36
|
const escapedLabel = escapeMermaidString(label);
|
|
37
37
|
mmd += ` ${safeId}["${escapedLabel}"]:::${type}\n`;
|
|
38
38
|
const nodeChildren = children.get(nodeId) ?? [];
|
|
39
39
|
for (const child of nodeChildren) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const safeChildId = safeNodeId(childTitle);
|
|
40
|
+
if (hierarchyNodeSet.has(child.id)) {
|
|
41
|
+
const safeChildId = safeNodeId(child.id);
|
|
43
42
|
mmd += ` ${safeId} --> ${safeChildId}\n`;
|
|
44
43
|
addNodeAndChildren(child);
|
|
45
44
|
}
|
|
@@ -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.source === 'hierarchy').map((r) => r.
|
|
33
|
+
parents: node.resolvedParents.filter((r) => r.source === 'hierarchy').map((r) => r.id),
|
|
34
34
|
};
|
|
35
35
|
return createHash('sha256').update(JSON.stringify(relevant)).digest('hex').slice(0, 16);
|
|
36
36
|
}
|
|
@@ -17,7 +17,7 @@ export async function miroSync(context, graph, options) {
|
|
|
17
17
|
throw new Error('No frameId in miro plugin config. Pass --new-frame "Title" to create one.');
|
|
18
18
|
// 3. Filter to hierarchy nodes only (graph already built by caller)
|
|
19
19
|
const levels = graph.hierarchy?.levels ?? [];
|
|
20
|
-
const nodes = [...graph.nodes.values()].filter((n) => graph.
|
|
20
|
+
const nodes = [...graph.nodes.values()].filter((n) => graph.hierarchyIds.has(n.id));
|
|
21
21
|
if (nodes.length === 0) {
|
|
22
22
|
return 'No space nodes found.';
|
|
23
23
|
}
|
|
@@ -257,9 +257,10 @@ 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 {
|
|
260
|
+
for (const { id: parentId, source, hierarchy } of node.resolvedParents) {
|
|
261
261
|
if (source !== 'hierarchy' || hierarchy !== graph.hierarchy?.name)
|
|
262
262
|
continue;
|
|
263
|
+
const parentTitle = graph.nodes.get(parentId)?.title ?? parentId;
|
|
263
264
|
// Both endpoints must have verified cards on the board
|
|
264
265
|
if (verifiedCardIds.has(parentTitle) && verifiedCardIds.has(childTitle)) {
|
|
265
266
|
const key = `${parentTitle}\u2192${childTitle}`;
|