@lensmap/document 0.1.0 → 0.2.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/index.d.ts +21 -2
- package/dist/index.js +91 -6
- package/dist/v1/schema.d.ts +22 -0
- package/dist/v1/schema.js +44 -12
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@ semantic graphs, revision-bound source bindings, session identities and bounded
|
|
|
5
5
|
source-preserving content edits. Does not own layout, browser rendering or LSP.
|
|
6
6
|
|
|
7
7
|
Requires Node 24.16 or newer. ESM only. The API and `version: v1` authoring format
|
|
8
|
-
are incomplete and experimental; `0.
|
|
8
|
+
are incomplete and experimental; `0.x` is not a promise of a stable schema.
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npm install @lensmap/document
|
|
11
|
+
npm install @lensmap/document
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```js
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export {
|
|
2
|
+
import { type ConnectionType } from "./v1/schema.js";
|
|
3
|
+
export type { ConnectionType, LensmapDocument, Reference, RichNode, } from "./v1/schema.js";
|
|
4
|
+
export { ConnectionTypeSchema, DocumentSchema } from "./v1/schema.js";
|
|
4
5
|
export declare function generateJSONSchema(): z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
|
|
5
6
|
version: z.ZodLiteral<"v1">;
|
|
7
|
+
frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
6
8
|
nodes: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<import("./v1/schema.js").RichNode, unknown, z.core.$ZodTypeInternals<import("./v1/schema.js").RichNode, unknown>>]>>;
|
|
7
9
|
}, z.core.$strict>>;
|
|
10
|
+
/** Read the edge from the inspected endpoint, independently of structural kind. */
|
|
11
|
+
export declare function connectionWording(type: ConnectionType | undefined, direction: "outgoing" | "incoming"): string;
|
|
12
|
+
type AnchorThought = {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly id?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Generated spelling is private to this rendered document version. */
|
|
17
|
+
export declare function thoughtAnchors(nodes: readonly AnchorThought[]): Map<string, string>;
|
|
18
|
+
/** The host decodes the fragment once before calling; authored names stay exact. */
|
|
19
|
+
export declare function resolveThoughtAnchor(nodes: readonly AnchorThought[], decodedAnchor: string): string | undefined;
|
|
8
20
|
export type SourcePosition = {
|
|
9
21
|
line: number;
|
|
10
22
|
character: number;
|
|
@@ -28,12 +40,17 @@ export type Thought = {
|
|
|
28
40
|
y: number;
|
|
29
41
|
};
|
|
30
42
|
path: string;
|
|
43
|
+
destination?: {
|
|
44
|
+
kind: "map_link" | "external_link";
|
|
45
|
+
url: string;
|
|
46
|
+
};
|
|
31
47
|
};
|
|
32
48
|
export type Connection = {
|
|
33
49
|
key: string;
|
|
34
50
|
from: string;
|
|
35
51
|
to: string;
|
|
36
52
|
kind: "branch" | "cross";
|
|
53
|
+
type?: ConnectionType;
|
|
37
54
|
path: string;
|
|
38
55
|
};
|
|
39
56
|
export type SemanticGraph = {
|
|
@@ -51,6 +68,7 @@ export type Diagnostic = {
|
|
|
51
68
|
range: SourceRange;
|
|
52
69
|
};
|
|
53
70
|
export type DocumentSnapshot = {
|
|
71
|
+
attachmentId?: string;
|
|
54
72
|
uri: string;
|
|
55
73
|
revision: number;
|
|
56
74
|
analyzedRevision: number | null;
|
|
@@ -80,6 +98,7 @@ export declare function positionToOffset(source: string, position: SourcePositio
|
|
|
80
98
|
export declare function offsetToPosition(source: string, offset: number): SourcePosition;
|
|
81
99
|
export declare class DocumentSession {
|
|
82
100
|
readonly uri: string;
|
|
101
|
+
private readonly attachmentId;
|
|
83
102
|
private text;
|
|
84
103
|
private version;
|
|
85
104
|
private state;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
import { isAlias, isMap, isScalar, parseDocument } from "yaml";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { DocumentSchema, } from "./v1/schema.js";
|
|
4
|
-
export { DocumentSchema } from "./v1/schema.js";
|
|
4
|
+
export { ConnectionTypeSchema, DocumentSchema } from "./v1/schema.js";
|
|
5
5
|
export function generateJSONSchema() {
|
|
6
6
|
return z.toJSONSchema(DocumentSchema, { io: "input" });
|
|
7
7
|
}
|
|
8
|
+
/** Read the edge from the inspected endpoint, independently of structural kind. */
|
|
9
|
+
export function connectionWording(type, direction) {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case "precedes":
|
|
12
|
+
return direction === "outgoing" ? "precedes" : "follows";
|
|
13
|
+
case "follows":
|
|
14
|
+
return direction === "outgoing" ? "follows" : "precedes";
|
|
15
|
+
case "prevents":
|
|
16
|
+
return direction === "outgoing" ? "prevents" : "is prevented by";
|
|
17
|
+
case "optional":
|
|
18
|
+
return "optionally connected to";
|
|
19
|
+
case "map_link":
|
|
20
|
+
return direction === "outgoing" ? "links to map" : "map linked from";
|
|
21
|
+
case "external_link":
|
|
22
|
+
return direction === "outgoing"
|
|
23
|
+
? "links to resource"
|
|
24
|
+
: "resource linked from";
|
|
25
|
+
default:
|
|
26
|
+
return "connected to";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Generated spelling is private to this rendered document version. */
|
|
30
|
+
export function thoughtAnchors(nodes) {
|
|
31
|
+
const reserved = new Set(nodes.flatMap((node) => (node.id === undefined ? [] : [node.id])));
|
|
32
|
+
const anchors = new Map();
|
|
33
|
+
for (const node of nodes) {
|
|
34
|
+
if (node.id !== undefined) {
|
|
35
|
+
anchors.set(node.key, node.id);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const base = `thought-${node.key}`;
|
|
39
|
+
let anchor = base;
|
|
40
|
+
let suffix = 0;
|
|
41
|
+
while (reserved.has(anchor))
|
|
42
|
+
anchor = `${base}-${++suffix}`;
|
|
43
|
+
reserved.add(anchor);
|
|
44
|
+
anchors.set(node.key, anchor);
|
|
45
|
+
}
|
|
46
|
+
return anchors;
|
|
47
|
+
}
|
|
48
|
+
/** The host decodes the fragment once before calling; authored names stay exact. */
|
|
49
|
+
export function resolveThoughtAnchor(nodes, decodedAnchor) {
|
|
50
|
+
for (const [key, anchor] of thoughtAnchors(nodes))
|
|
51
|
+
if (anchor === decodedAnchor)
|
|
52
|
+
return key;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
let attachmentSerial = 0;
|
|
8
56
|
export class ContentEditError extends Error {
|
|
9
57
|
code;
|
|
10
58
|
constructor(code, message) {
|
|
@@ -45,6 +93,7 @@ function rejectCycles(value, ancestors = new Set()) {
|
|
|
45
93
|
}
|
|
46
94
|
export class DocumentSession {
|
|
47
95
|
uri;
|
|
96
|
+
attachmentId = `attachment:${++attachmentSerial}`;
|
|
48
97
|
text;
|
|
49
98
|
version;
|
|
50
99
|
state;
|
|
@@ -57,6 +106,7 @@ export class DocumentSession {
|
|
|
57
106
|
this.version = revision;
|
|
58
107
|
this.state = {
|
|
59
108
|
uri,
|
|
109
|
+
attachmentId: this.attachmentId,
|
|
60
110
|
revision,
|
|
61
111
|
analyzedRevision: null,
|
|
62
112
|
graphRevision: null,
|
|
@@ -111,8 +161,27 @@ export class DocumentSession {
|
|
|
111
161
|
const input = doc.toJS();
|
|
112
162
|
rejectCycles(input);
|
|
113
163
|
const result = DocumentSchema.safeParse(input);
|
|
114
|
-
if (!result.success)
|
|
115
|
-
|
|
164
|
+
if (!result.success) {
|
|
165
|
+
// Union alternatives report their paths at the union boundary. Locate
|
|
166
|
+
// a concrete failing field in the source, not an unrelated line zero.
|
|
167
|
+
const leaves = (issues, prefix = []) => issues.flatMap((issue) => {
|
|
168
|
+
const path = [...prefix, ...issue.path];
|
|
169
|
+
return issue.code === "invalid_union"
|
|
170
|
+
? leaves(issue.errors.flat(), path)
|
|
171
|
+
: [{ ...issue, path }];
|
|
172
|
+
});
|
|
173
|
+
const located = leaves(result.error.issues)
|
|
174
|
+
.map((issue) => {
|
|
175
|
+
const path = issue.path.filter((part) => typeof part !== "symbol");
|
|
176
|
+
if (issue.code === "unrecognized_keys" && issue.keys[0])
|
|
177
|
+
path.push(issue.keys[0]);
|
|
178
|
+
const ast = doc.getIn(path, true);
|
|
179
|
+
return { issue, path, offset: ast?.range?.[0] };
|
|
180
|
+
})
|
|
181
|
+
.filter((item) => item.offset !== undefined)
|
|
182
|
+
.sort((a, b) => b.path.length - a.path.length)[0];
|
|
183
|
+
throw Object.assign(new Error(located?.issue.message ?? z.prettifyError(result.error)), { offset: located?.offset ?? 0 });
|
|
184
|
+
}
|
|
116
185
|
const records = [];
|
|
117
186
|
const edges = [];
|
|
118
187
|
const bindings = [];
|
|
@@ -157,6 +226,7 @@ export class DocumentSession {
|
|
|
157
226
|
refs.push({
|
|
158
227
|
from: parent,
|
|
159
228
|
id: entry.node_id,
|
|
229
|
+
type: entry.type,
|
|
160
230
|
path: pathName,
|
|
161
231
|
span,
|
|
162
232
|
});
|
|
@@ -167,10 +237,17 @@ export class DocumentSession {
|
|
|
167
237
|
: Array.isArray(entry)
|
|
168
238
|
? { full_text: null, connections: entry }
|
|
169
239
|
: entry;
|
|
170
|
-
const
|
|
240
|
+
const destination = (content.type === "map_link" || content.type === "external_link") &&
|
|
241
|
+
content.url !== undefined
|
|
242
|
+
? { kind: content.type, url: content.url }
|
|
243
|
+
: undefined;
|
|
244
|
+
const fallback = content.summary === undefined && content.full_text === undefined
|
|
245
|
+
? (destination?.url ?? "")
|
|
246
|
+
: "";
|
|
247
|
+
const markdown = content.full_text ?? content.summary ?? fallback;
|
|
171
248
|
const title = content.summary !== undefined
|
|
172
249
|
? (content.summary ?? "")
|
|
173
|
-
: (content.full_text ??
|
|
250
|
+
: (content.full_text ?? fallback).split(/\r?\n/, 1)[0];
|
|
174
251
|
const key = `thought:${++this.serial}`;
|
|
175
252
|
const thought = {
|
|
176
253
|
key,
|
|
@@ -180,6 +257,7 @@ export class DocumentSession {
|
|
|
180
257
|
unresolved: title.trim() === "",
|
|
181
258
|
position: content.position,
|
|
182
259
|
path: pathName,
|
|
260
|
+
...(destination ? { destination } : {}),
|
|
183
261
|
};
|
|
184
262
|
records.push({ thought, span, ast });
|
|
185
263
|
bindings.push({ key, kind: "definition", span });
|
|
@@ -194,6 +272,7 @@ export class DocumentSession {
|
|
|
194
272
|
from: parent,
|
|
195
273
|
to: key,
|
|
196
274
|
kind: "branch",
|
|
275
|
+
...(content.type !== undefined ? { type: content.type } : {}),
|
|
197
276
|
path: pathName,
|
|
198
277
|
});
|
|
199
278
|
content.connections?.forEach((child, i) => {
|
|
@@ -257,7 +336,11 @@ export class DocumentSession {
|
|
|
257
336
|
for (const ref of refs) {
|
|
258
337
|
const to = ids.get(ref.id);
|
|
259
338
|
if (!to)
|
|
260
|
-
throw new Error(`${ref.path}.node_id: no node has id "${ref.id}".`);
|
|
339
|
+
throw Object.assign(new Error(`${ref.path}.node_id: no node has id "${ref.id}".`), { offset: ref.span.start });
|
|
340
|
+
if ((ref.type === "map_link" || ref.type === "external_link") &&
|
|
341
|
+
records.find((r) => r.thought.key === to)?.thought.destination
|
|
342
|
+
?.kind !== ref.type)
|
|
343
|
+
throw Object.assign(new Error(`${ref.path}.type: reference requires a ${ref.type} destination.`), { offset: ref.span.start });
|
|
261
344
|
const endpoints = `${ref.from}:${to}`;
|
|
262
345
|
const occurrence = parallelReferences.get(endpoints) ?? 0;
|
|
263
346
|
parallelReferences.set(endpoints, occurrence + 1);
|
|
@@ -266,6 +349,7 @@ export class DocumentSession {
|
|
|
266
349
|
from: ref.from,
|
|
267
350
|
to,
|
|
268
351
|
kind: "cross",
|
|
352
|
+
...(ref.type !== undefined ? { type: ref.type } : {}),
|
|
269
353
|
path: ref.path,
|
|
270
354
|
});
|
|
271
355
|
bindings.push({
|
|
@@ -279,6 +363,7 @@ export class DocumentSession {
|
|
|
279
363
|
this.validSource = this.text;
|
|
280
364
|
this.state = {
|
|
281
365
|
uri: this.uri,
|
|
366
|
+
attachmentId: this.attachmentId,
|
|
282
367
|
revision: this.version,
|
|
283
368
|
analyzedRevision: this.version,
|
|
284
369
|
graphRevision: this.version,
|
package/dist/v1/schema.d.ts
CHANGED
|
@@ -4,8 +4,27 @@ export declare const PositionSchema: z.ZodObject<{
|
|
|
4
4
|
x: z.ZodNumber;
|
|
5
5
|
y: z.ZodNumber;
|
|
6
6
|
}, z.core.$strict>;
|
|
7
|
+
export declare const ConnectionTypeSchema: z.ZodEnum<{
|
|
8
|
+
connected: "connected";
|
|
9
|
+
external_link: "external_link";
|
|
10
|
+
follows: "follows";
|
|
11
|
+
map_link: "map_link";
|
|
12
|
+
optional: "optional";
|
|
13
|
+
precedes: "precedes";
|
|
14
|
+
prevents: "prevents";
|
|
15
|
+
}>;
|
|
16
|
+
export type ConnectionType = z.infer<typeof ConnectionTypeSchema>;
|
|
7
17
|
export declare const ReferenceSchema: z.ZodObject<{
|
|
8
18
|
node_id: z.ZodString;
|
|
19
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
connected: "connected";
|
|
21
|
+
external_link: "external_link";
|
|
22
|
+
follows: "follows";
|
|
23
|
+
map_link: "map_link";
|
|
24
|
+
optional: "optional";
|
|
25
|
+
precedes: "precedes";
|
|
26
|
+
prevents: "prevents";
|
|
27
|
+
}>>;
|
|
9
28
|
}, z.core.$strict>;
|
|
10
29
|
declare const NodeContentSchema: z.ZodObject<{
|
|
11
30
|
id: z.ZodOptional<z.ZodString>;
|
|
@@ -19,6 +38,8 @@ declare const NodeContentSchema: z.ZodObject<{
|
|
|
19
38
|
export type Reference = z.infer<typeof ReferenceSchema>;
|
|
20
39
|
export type RichNode = z.infer<typeof NodeContentSchema> & {
|
|
21
40
|
connections?: Connection[];
|
|
41
|
+
type?: ConnectionType;
|
|
42
|
+
url?: string;
|
|
22
43
|
};
|
|
23
44
|
export type Connection = string | RichNode | Reference | Connection[];
|
|
24
45
|
export declare const RichNodeSchema: z.ZodType<RichNode>;
|
|
@@ -26,6 +47,7 @@ export declare const ConnectionSchema: z.ZodType<Connection>;
|
|
|
26
47
|
export declare const NodeSchema: z.ZodUnion<readonly [z.ZodString, z.ZodType<RichNode, unknown, z.core.$ZodTypeInternals<RichNode, unknown>>]>;
|
|
27
48
|
export declare const DocumentSchema: z.ZodObject<{
|
|
28
49
|
version: z.ZodLiteral<"v1">;
|
|
50
|
+
frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
29
51
|
nodes: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<RichNode, unknown, z.core.$ZodTypeInternals<RichNode, unknown>>]>>;
|
|
30
52
|
}, z.core.$strict>;
|
|
31
53
|
export type LensmapDocument = z.infer<typeof DocumentSchema>;
|
package/dist/v1/schema.js
CHANGED
|
@@ -5,31 +5,63 @@ export const PositionSchema = z.strictObject({
|
|
|
5
5
|
y: z.number().finite(),
|
|
6
6
|
});
|
|
7
7
|
const IdSchema = z.string().regex(/\S/, "An ID must not be empty.");
|
|
8
|
-
export const
|
|
8
|
+
export const ConnectionTypeSchema = z.enum([
|
|
9
|
+
"connected",
|
|
10
|
+
"precedes",
|
|
11
|
+
"follows",
|
|
12
|
+
"prevents",
|
|
13
|
+
"optional",
|
|
14
|
+
"external_link",
|
|
15
|
+
"map_link",
|
|
16
|
+
]);
|
|
17
|
+
const LocalTypeSchema = z.enum([
|
|
18
|
+
"connected",
|
|
19
|
+
"precedes",
|
|
20
|
+
"follows",
|
|
21
|
+
"prevents",
|
|
22
|
+
"optional",
|
|
23
|
+
]);
|
|
24
|
+
export const ReferenceSchema = z.strictObject({
|
|
25
|
+
node_id: IdSchema,
|
|
26
|
+
type: ConnectionTypeSchema.optional(),
|
|
27
|
+
});
|
|
9
28
|
const NodeContentSchema = z.strictObject({
|
|
10
29
|
id: IdSchema.optional(),
|
|
11
30
|
summary: z.string().nullable().optional(),
|
|
12
31
|
full_text: z.string().nullable().optional(),
|
|
13
32
|
position: PositionSchema.optional(),
|
|
14
33
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
const ordinary = (typed) => {
|
|
35
|
+
const base = typed
|
|
36
|
+
? NodeContentSchema.extend({ type: LocalTypeSchema.optional() })
|
|
37
|
+
: NodeContentSchema;
|
|
38
|
+
return z.union([
|
|
39
|
+
base.extend({
|
|
40
|
+
summary: z.string().nullable(),
|
|
41
|
+
connections: z.array(ConnectionSchema).optional(),
|
|
42
|
+
}),
|
|
43
|
+
base.extend({
|
|
44
|
+
full_text: z.string().nullable(),
|
|
45
|
+
connections: z.array(ConnectionSchema).optional(),
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
};
|
|
49
|
+
const portal = () => NodeContentSchema.extend({
|
|
50
|
+
type: z.enum(["map_link", "external_link"]),
|
|
51
|
+
url: z.string().regex(/\S/, "A portal URL must not be blank."),
|
|
52
|
+
connections: z.array(ConnectionSchema).optional(),
|
|
53
|
+
});
|
|
54
|
+
export const RichNodeSchema = z.lazy(() => z.union([ordinary(true), portal()]));
|
|
55
|
+
const RootNodeSchema = z.lazy(() => z.union([ordinary(false), portal()]));
|
|
25
56
|
export const ConnectionSchema = z.lazy(() => z.union([
|
|
26
57
|
z.string(),
|
|
27
58
|
ReferenceSchema,
|
|
28
59
|
RichNodeSchema,
|
|
29
60
|
z.array(ConnectionSchema),
|
|
30
61
|
]));
|
|
31
|
-
export const NodeSchema = z.union([z.string(),
|
|
62
|
+
export const NodeSchema = z.union([z.string(), RootNodeSchema]);
|
|
32
63
|
export const DocumentSchema = z.strictObject({
|
|
33
64
|
version: z.literal("v1"),
|
|
65
|
+
frontmatter: z.record(z.string(), z.unknown()).optional(),
|
|
34
66
|
nodes: z.array(NodeSchema),
|
|
35
67
|
});
|
package/package.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"name": "@lensmap/document",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"description": "Experimental Lensmap document engine: YAML analysis, semantic graphs and revision-bound source edits",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
8
8
|
],
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public",
|
|
11
|
-
"tag": "
|
|
11
|
+
"tag": "latest",
|
|
12
12
|
"registry": "https://registry.npmjs.org/"
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "24.19.2",
|
|
39
|
+
"ajv": "8.20.0",
|
|
39
40
|
"typescript": "7.0.2",
|
|
40
|
-
"vitest": "5.1.0"
|
|
41
|
-
"ajv": "8.20.0"
|
|
41
|
+
"vitest": "5.1.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsc",
|