@gmickel/gno 1.8.0 → 1.10.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 +6 -2
- package/assets/skill/SKILL.md +41 -16
- package/assets/skill/cli-reference.md +39 -0
- package/assets/skill/examples.md +41 -0
- package/assets/skill/mcp-reference.md +13 -1
- package/package.json +1 -1
- package/src/cli/commands/capture.ts +9 -6
- package/src/cli/commands/graph.ts +89 -2
- package/src/cli/commands/index-cmd.ts +17 -6
- package/src/cli/commands/links.ts +237 -54
- package/src/cli/commands/query.ts +212 -0
- package/src/cli/commands/ref-parser.ts +8 -103
- package/src/cli/commands/shared.ts +17 -1
- package/src/cli/commands/update.ts +17 -6
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +176 -9
- package/src/config/content-types.ts +154 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +14 -0
- package/src/config/loader.ts +11 -2
- package/src/config/types.ts +37 -1
- package/src/core/config-mutation.ts +14 -2
- package/src/core/graph-query.ts +137 -0
- package/src/core/graph-resolver.ts +117 -0
- package/src/core/note-presets.ts +61 -5
- package/src/core/ref-parser.ts +145 -0
- package/src/ingestion/frontmatter.ts +170 -2
- package/src/ingestion/index.ts +2 -0
- package/src/ingestion/sync-options.ts +29 -0
- package/src/ingestion/sync.ts +385 -17
- package/src/ingestion/types.ts +14 -0
- package/src/mcp/tools/add-collection.ts +8 -5
- package/src/mcp/tools/capture.ts +5 -2
- package/src/mcp/tools/get.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +13 -7
- package/src/mcp/tools/index.ts +97 -10
- package/src/mcp/tools/links.ts +83 -1
- package/src/mcp/tools/multi-get.ts +1 -1
- package/src/mcp/tools/query.ts +207 -0
- package/src/mcp/tools/sync.ts +12 -6
- package/src/mcp/tools/workspace-write.ts +16 -10
- package/src/pipeline/diagnose.ts +302 -0
- package/src/pipeline/filters.ts +119 -0
- package/src/pipeline/hybrid.ts +92 -17
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +34 -0
- package/src/pipeline/vsearch.ts +4 -0
- package/src/publish/export-service.ts +1 -1
- package/src/sdk/client.ts +51 -24
- package/src/sdk/documents.ts +2 -2
- package/src/serve/background-runtime.ts +18 -4
- package/src/serve/config-sync.ts +9 -2
- package/src/serve/routes/api.ts +244 -24
- package/src/serve/routes/graph.ts +173 -1
- package/src/serve/server.ts +24 -1
- package/src/serve/watch-service.ts +12 -2
- package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
- package/src/store/migrations/010-typed-edges.ts +67 -0
- package/src/store/migrations/011-doc-edge-traversal-indexes.ts +30 -0
- package/src/store/migrations/index.ts +16 -1
- package/src/store/sqlite/adapter.ts +853 -114
- package/src/store/types.ts +147 -0
package/src/ingestion/sync.ts
CHANGED
|
@@ -10,9 +10,11 @@ import { stat } from "node:fs/promises";
|
|
|
10
10
|
// node:path for join (no Bun path utils)
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
|
+
import type { NormalizedContentTypeRule } from "../config";
|
|
13
14
|
import type { Collection } from "../config/types";
|
|
14
15
|
import type {
|
|
15
16
|
ChunkInput,
|
|
17
|
+
DocEdgeInput,
|
|
16
18
|
DocLinkInput,
|
|
17
19
|
DocumentRow,
|
|
18
20
|
IngestErrorInput,
|
|
@@ -22,6 +24,7 @@ import type {
|
|
|
22
24
|
import type {
|
|
23
25
|
ChunkerPort,
|
|
24
26
|
CollectionSyncResult,
|
|
27
|
+
ContentTypeSource,
|
|
25
28
|
FileSyncResult,
|
|
26
29
|
ProcessDecision,
|
|
27
30
|
SyncOptions,
|
|
@@ -30,6 +33,7 @@ import type {
|
|
|
30
33
|
WalkerPort,
|
|
31
34
|
} from "./types";
|
|
32
35
|
|
|
36
|
+
import { fingerprintContentTypeRules } from "../config";
|
|
33
37
|
import { getDefaultMimeDetector, type MimeDetector } from "../converters/mime";
|
|
34
38
|
import {
|
|
35
39
|
type ConversionPipeline,
|
|
@@ -40,6 +44,7 @@ import {
|
|
|
40
44
|
normalizeMarkdownPath,
|
|
41
45
|
normalizeWikiName,
|
|
42
46
|
parseLinks,
|
|
47
|
+
parseTargetParts,
|
|
43
48
|
} from "../core/links";
|
|
44
49
|
import { normalizeTag, validateTag } from "../core/tags";
|
|
45
50
|
import { defaultChunker } from "./chunker";
|
|
@@ -67,7 +72,130 @@ const MAX_CONCURRENCY = 16;
|
|
|
67
72
|
* Increment when ingestion adds new derived data (tags, metadata, etc.)
|
|
68
73
|
* Documents with ingestVersion < INGEST_VERSION will be re-processed.
|
|
69
74
|
*/
|
|
70
|
-
export const INGEST_VERSION =
|
|
75
|
+
export const INGEST_VERSION = 6;
|
|
76
|
+
const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT = fingerprintContentTypeRules([]);
|
|
77
|
+
const RELATION_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
78
|
+
|
|
79
|
+
type RelationMap = Record<string, string[]>;
|
|
80
|
+
|
|
81
|
+
function isRelationMap(value: unknown): value is RelationMap {
|
|
82
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Object.values(value).every(
|
|
87
|
+
(targets) =>
|
|
88
|
+
Array.isArray(targets) &&
|
|
89
|
+
targets.every((target) => typeof target === "string")
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeRelationTarget(raw: string): string {
|
|
94
|
+
const trimmed = raw.trim();
|
|
95
|
+
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) {
|
|
96
|
+
return trimmed.slice(2, -2).split("|")[0]?.trim() ?? "";
|
|
97
|
+
}
|
|
98
|
+
return trimmed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeRelationEdgeType(raw: string): string {
|
|
102
|
+
return raw
|
|
103
|
+
.trim()
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[-\s]+/g, "_");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findDocByWikiRef(
|
|
109
|
+
docs: DocumentRow[],
|
|
110
|
+
targetRef: string,
|
|
111
|
+
collection?: string
|
|
112
|
+
): DocumentRow | undefined {
|
|
113
|
+
const normalized = normalizeWikiName(targetRef);
|
|
114
|
+
const candidates = collection
|
|
115
|
+
? docs.filter((doc) => doc.collection === collection)
|
|
116
|
+
: docs;
|
|
117
|
+
|
|
118
|
+
return candidates.find((doc) => {
|
|
119
|
+
const title = doc.title ?? doc.relPath.split("/").pop() ?? doc.relPath;
|
|
120
|
+
const relStem = doc.relPath.replace(/\.[^/.]+$/, "");
|
|
121
|
+
return (
|
|
122
|
+
normalizeWikiName(title) === normalized ||
|
|
123
|
+
normalizeWikiName(doc.relPath) === normalized ||
|
|
124
|
+
normalizeWikiName(relStem) === normalized
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveRelationTarget(
|
|
130
|
+
docs: DocumentRow[],
|
|
131
|
+
sourceDoc: DocumentRow,
|
|
132
|
+
rawTarget: string
|
|
133
|
+
): DocumentRow | undefined {
|
|
134
|
+
const target = normalizeRelationTarget(rawTarget);
|
|
135
|
+
if (!target) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (target.startsWith("#")) {
|
|
140
|
+
return docs.find((doc) => doc.docid === target);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (target.startsWith("gno://")) {
|
|
144
|
+
return docs.find((doc) => doc.uri === target);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const parts = parseTargetParts(target);
|
|
148
|
+
const targetCollection = parts.collection;
|
|
149
|
+
const targetRef = parts.ref;
|
|
150
|
+
if (!targetRef) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (targetCollection) {
|
|
155
|
+
const exact = docs.find(
|
|
156
|
+
(doc) => doc.collection === targetCollection && doc.relPath === targetRef
|
|
157
|
+
);
|
|
158
|
+
return exact ?? findDocByWikiRef(docs, targetRef, targetCollection);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sameCollectionPath = normalizeMarkdownPath(
|
|
162
|
+
targetRef,
|
|
163
|
+
sourceDoc.relPath
|
|
164
|
+
);
|
|
165
|
+
if (sameCollectionPath) {
|
|
166
|
+
const exact = docs.find(
|
|
167
|
+
(doc) =>
|
|
168
|
+
doc.collection === sourceDoc.collection &&
|
|
169
|
+
doc.relPath === sameCollectionPath
|
|
170
|
+
);
|
|
171
|
+
if (exact) {
|
|
172
|
+
return exact;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const explicitCollPath = docs.find(
|
|
177
|
+
(doc) => `${doc.collection}/${doc.relPath}` === targetRef
|
|
178
|
+
);
|
|
179
|
+
if (explicitCollPath) {
|
|
180
|
+
return explicitCollPath;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
findDocByWikiRef(docs, targetRef, sourceDoc.collection) ??
|
|
185
|
+
findDocByWikiRef(docs, targetRef)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getPrimaryGraphHint(
|
|
190
|
+
contentType: string | null | undefined,
|
|
191
|
+
rules: NormalizedContentTypeRule[]
|
|
192
|
+
): string | undefined {
|
|
193
|
+
if (!contentType) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const rule = rules.find((candidate) => candidate.id === contentType);
|
|
197
|
+
return rule?.graphHints?.[0];
|
|
198
|
+
}
|
|
71
199
|
|
|
72
200
|
/**
|
|
73
201
|
* Decide whether to process a file or skip it.
|
|
@@ -76,7 +204,8 @@ export const INGEST_VERSION = 5;
|
|
|
76
204
|
*/
|
|
77
205
|
function decideAction(
|
|
78
206
|
existing: DocumentRow | null,
|
|
79
|
-
sourceHash: string
|
|
207
|
+
sourceHash: string,
|
|
208
|
+
contentTypeRulesFingerprint: string
|
|
80
209
|
): ProcessDecision {
|
|
81
210
|
// No existing doc - must process
|
|
82
211
|
if (!existing) {
|
|
@@ -108,6 +237,16 @@ function decideAction(
|
|
|
108
237
|
return { kind: "repair", reason: "ingest version outdated" };
|
|
109
238
|
}
|
|
110
239
|
|
|
240
|
+
const hasLegacyEmptyRulesFingerprint =
|
|
241
|
+
existing.contentTypeRulesFingerprint === null &&
|
|
242
|
+
contentTypeRulesFingerprint === EMPTY_CONTENT_TYPE_RULES_FINGERPRINT;
|
|
243
|
+
if (
|
|
244
|
+
existing.contentTypeRulesFingerprint !== contentTypeRulesFingerprint &&
|
|
245
|
+
!hasLegacyEmptyRulesFingerprint
|
|
246
|
+
) {
|
|
247
|
+
return { kind: "repair", reason: "content type rules changed" };
|
|
248
|
+
}
|
|
249
|
+
|
|
111
250
|
// All good - skip
|
|
112
251
|
return { kind: "skip", reason: "unchanged" };
|
|
113
252
|
}
|
|
@@ -143,6 +282,7 @@ function extractTags(markdown: string): string[] {
|
|
|
143
282
|
|
|
144
283
|
interface DocumentMetadata {
|
|
145
284
|
contentType?: string;
|
|
285
|
+
contentTypeSource: ContentTypeSource;
|
|
146
286
|
categories?: string[];
|
|
147
287
|
author?: string;
|
|
148
288
|
frontmatterDate?: string;
|
|
@@ -210,47 +350,95 @@ function normalizeDate(value: unknown): string | undefined {
|
|
|
210
350
|
return parsed.toISOString();
|
|
211
351
|
}
|
|
212
352
|
|
|
213
|
-
function
|
|
353
|
+
function inferPathContentType(
|
|
354
|
+
relPath: string,
|
|
355
|
+
ext: string
|
|
356
|
+
): {
|
|
357
|
+
contentType: string;
|
|
358
|
+
source: ContentTypeSource;
|
|
359
|
+
} {
|
|
214
360
|
const lowerPath = relPath.toLowerCase();
|
|
215
361
|
if (CODE_EXTENSIONS.has(ext.toLowerCase())) {
|
|
216
|
-
return "code";
|
|
362
|
+
return { contentType: "code", source: "path-ext" };
|
|
217
363
|
}
|
|
218
364
|
if (/(meeting|standup|retro|minutes)/.test(lowerPath)) {
|
|
219
|
-
return "meeting";
|
|
365
|
+
return { contentType: "meeting", source: "path-ext" };
|
|
220
366
|
}
|
|
221
367
|
if (/(spec|rfc|adr|design)/.test(lowerPath)) {
|
|
222
|
-
return "spec";
|
|
368
|
+
return { contentType: "spec", source: "path-ext" };
|
|
223
369
|
}
|
|
224
370
|
if (/(notes|journal|log)/.test(lowerPath)) {
|
|
225
|
-
return "notes";
|
|
371
|
+
return { contentType: "notes", source: "path-ext" };
|
|
372
|
+
}
|
|
373
|
+
return { contentType: "prose", source: "fallback" };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function normalizeFrontmatterScalar(value: string): string {
|
|
377
|
+
const trimmed = value.trim();
|
|
378
|
+
if (trimmed.length < 2) {
|
|
379
|
+
return trimmed;
|
|
226
380
|
}
|
|
227
|
-
|
|
381
|
+
const first = trimmed[0];
|
|
382
|
+
const last = trimmed.at(-1);
|
|
383
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
384
|
+
return trimmed.slice(1, -1).trim();
|
|
385
|
+
}
|
|
386
|
+
return trimmed;
|
|
228
387
|
}
|
|
229
388
|
|
|
230
389
|
function parseCategories(input: unknown): string[] {
|
|
231
390
|
if (Array.isArray(input)) {
|
|
232
391
|
return input
|
|
233
392
|
.filter((v): v is string => typeof v === "string")
|
|
234
|
-
.map((v) => v
|
|
393
|
+
.map((v) => normalizeFrontmatterScalar(v).toLowerCase())
|
|
235
394
|
.filter((v) => v.length > 0);
|
|
236
395
|
}
|
|
237
396
|
if (typeof input === "string") {
|
|
238
397
|
return input
|
|
239
398
|
.split(",")
|
|
240
|
-
.map((v) => v
|
|
399
|
+
.map((v) => normalizeFrontmatterScalar(v).toLowerCase())
|
|
241
400
|
.filter((v) => v.length > 0);
|
|
242
401
|
}
|
|
243
402
|
return [];
|
|
244
403
|
}
|
|
245
404
|
|
|
246
|
-
function
|
|
405
|
+
function matchPrefixContentType(
|
|
406
|
+
relPath: string,
|
|
407
|
+
rules: NormalizedContentTypeRule[]
|
|
408
|
+
): string | undefined {
|
|
409
|
+
for (const rule of rules) {
|
|
410
|
+
if (rule.prefixes.some((prefix) => relPath.startsWith(prefix))) {
|
|
411
|
+
return rule.id;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function extractDocumentMetadata(
|
|
247
418
|
markdown: string,
|
|
248
419
|
relPath: string,
|
|
249
|
-
ext: string
|
|
420
|
+
ext: string,
|
|
421
|
+
contentTypeRules: NormalizedContentTypeRule[] = []
|
|
250
422
|
): DocumentMetadata {
|
|
251
423
|
const parsed = parseFrontmatter(markdown);
|
|
252
424
|
const metadata = parsed.metadata;
|
|
253
|
-
const
|
|
425
|
+
const typedRules = new Map(contentTypeRules.map((rule) => [rule.id, rule]));
|
|
426
|
+
const rawFrontmatterType =
|
|
427
|
+
typeof metadata.type === "string"
|
|
428
|
+
? normalizeFrontmatterScalar(metadata.type)
|
|
429
|
+
: "";
|
|
430
|
+
const frontmatterType = typedRules.get(rawFrontmatterType)?.id;
|
|
431
|
+
const prefixType =
|
|
432
|
+
frontmatterType === undefined
|
|
433
|
+
? matchPrefixContentType(relPath, contentTypeRules)
|
|
434
|
+
: undefined;
|
|
435
|
+
const inferred = inferPathContentType(relPath, ext);
|
|
436
|
+
const contentType = frontmatterType ?? prefixType ?? inferred.contentType;
|
|
437
|
+
const contentTypeSource: ContentTypeSource = frontmatterType
|
|
438
|
+
? "frontmatter-type"
|
|
439
|
+
: prefixType
|
|
440
|
+
? "prefix"
|
|
441
|
+
: inferred.source;
|
|
254
442
|
const categories = new Set<string>([contentType]);
|
|
255
443
|
|
|
256
444
|
const fmCategories = parseCategories(
|
|
@@ -299,6 +487,7 @@ function extractDocumentMetadata(
|
|
|
299
487
|
|
|
300
488
|
return {
|
|
301
489
|
contentType,
|
|
490
|
+
contentTypeSource,
|
|
302
491
|
categories: [...categories],
|
|
303
492
|
author,
|
|
304
493
|
frontmatterDate,
|
|
@@ -488,6 +677,10 @@ export class SyncService {
|
|
|
488
677
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
489
678
|
hasher.update(bytes);
|
|
490
679
|
const sourceHash = hasher.digest("hex");
|
|
680
|
+
const contentTypeRules = options.contentTypeRules ?? [];
|
|
681
|
+
const contentTypeRulesFingerprint =
|
|
682
|
+
options.contentTypeRulesFingerprint ??
|
|
683
|
+
fingerprintContentTypeRules(contentTypeRules);
|
|
491
684
|
|
|
492
685
|
// 4. Check existing doc for skip/repair decision
|
|
493
686
|
const existingResult = await store.getDocument(
|
|
@@ -495,7 +688,11 @@ export class SyncService {
|
|
|
495
688
|
entry.relPath
|
|
496
689
|
);
|
|
497
690
|
const existing = existingResult.ok ? existingResult.value : null;
|
|
498
|
-
const decision = decideAction(
|
|
691
|
+
const decision = decideAction(
|
|
692
|
+
existing,
|
|
693
|
+
sourceHash,
|
|
694
|
+
contentTypeRulesFingerprint
|
|
695
|
+
);
|
|
499
696
|
|
|
500
697
|
if (decision.kind === "skip") {
|
|
501
698
|
return { relPath: entry.relPath, status: "unchanged" };
|
|
@@ -565,7 +762,8 @@ export class SyncService {
|
|
|
565
762
|
const extractedMetadata = extractDocumentMetadata(
|
|
566
763
|
artifact.markdown,
|
|
567
764
|
entry.relPath,
|
|
568
|
-
mime.ext
|
|
765
|
+
mime.ext,
|
|
766
|
+
contentTypeRules
|
|
569
767
|
);
|
|
570
768
|
|
|
571
769
|
// 7. Upsert document - EXPLICITLY clear error fields on success
|
|
@@ -584,10 +782,12 @@ export class SyncService {
|
|
|
584
782
|
converterVersion: artifact.meta.converterVersion,
|
|
585
783
|
languageHint: artifact.languageHint ?? collection.languageHint,
|
|
586
784
|
contentType: extractedMetadata.contentType,
|
|
785
|
+
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
587
786
|
categories: extractedMetadata.categories,
|
|
588
787
|
author: extractedMetadata.author,
|
|
589
788
|
frontmatterDate: extractedMetadata.frontmatterDate,
|
|
590
789
|
dateFields: extractedMetadata.dateFields,
|
|
790
|
+
contentTypeRulesFingerprint,
|
|
591
791
|
// Clear error fields on success (requires store to handle undefined → null)
|
|
592
792
|
lastErrorCode: undefined,
|
|
593
793
|
lastErrorMessage: undefined,
|
|
@@ -711,6 +911,8 @@ export class SyncService {
|
|
|
711
911
|
status,
|
|
712
912
|
docid,
|
|
713
913
|
mirrorHash: artifact.mirrorHash,
|
|
914
|
+
contentType: extractedMetadata.contentType,
|
|
915
|
+
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
714
916
|
};
|
|
715
917
|
} catch (error) {
|
|
716
918
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -815,9 +1017,160 @@ export class SyncService {
|
|
|
815
1017
|
results.push(result);
|
|
816
1018
|
}
|
|
817
1019
|
|
|
1020
|
+
await this.projectTypedEdges(collection, store, options);
|
|
1021
|
+
|
|
818
1022
|
return results;
|
|
819
1023
|
}
|
|
820
1024
|
|
|
1025
|
+
private async projectTypedEdges(
|
|
1026
|
+
collection: Collection,
|
|
1027
|
+
store: StorePort,
|
|
1028
|
+
options: SyncOptions
|
|
1029
|
+
): Promise<Array<{ relPath: string; code: string; message: string }>> {
|
|
1030
|
+
const errors: Array<{ relPath: string; code: string; message: string }> =
|
|
1031
|
+
[];
|
|
1032
|
+
|
|
1033
|
+
const backfillResult = await store.backfillDocEdges();
|
|
1034
|
+
if (!backfillResult.ok) {
|
|
1035
|
+
return [
|
|
1036
|
+
{
|
|
1037
|
+
relPath: "(typed edge backfill)",
|
|
1038
|
+
code: backfillResult.error.code,
|
|
1039
|
+
message: backfillResult.error.message,
|
|
1040
|
+
},
|
|
1041
|
+
];
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const docsResult = await store.listDocuments();
|
|
1045
|
+
if (!docsResult.ok) {
|
|
1046
|
+
return [
|
|
1047
|
+
{
|
|
1048
|
+
relPath: "(typed edge projection)",
|
|
1049
|
+
code: docsResult.error.code,
|
|
1050
|
+
message: docsResult.error.message,
|
|
1051
|
+
},
|
|
1052
|
+
];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const activeDocs = docsResult.value.filter((doc) => doc.active);
|
|
1056
|
+
|
|
1057
|
+
for (const doc of activeDocs) {
|
|
1058
|
+
if (!doc.mirrorHash) {
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const contentResult = await store.getContent(doc.mirrorHash);
|
|
1063
|
+
if (!contentResult.ok || contentResult.value === null) {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const relationsValue = parseFrontmatter(contentResult.value).metadata
|
|
1068
|
+
.relations;
|
|
1069
|
+
const relationEdges: DocEdgeInput[] = [];
|
|
1070
|
+
|
|
1071
|
+
if (isRelationMap(relationsValue)) {
|
|
1072
|
+
for (const [rawEdgeType, targets] of Object.entries(relationsValue)) {
|
|
1073
|
+
const edgeType = normalizeRelationEdgeType(rawEdgeType);
|
|
1074
|
+
if (!RELATION_EDGE_TYPE_PATTERN.test(edgeType)) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
for (const target of targets) {
|
|
1078
|
+
const targetDoc = resolveRelationTarget(activeDocs, doc, target);
|
|
1079
|
+
if (targetDoc) {
|
|
1080
|
+
relationEdges.push({
|
|
1081
|
+
targetDocId: targetDoc.id,
|
|
1082
|
+
edgeType,
|
|
1083
|
+
confidence: "manual",
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const relationTargetIds = new Set(
|
|
1090
|
+
relationEdges.map((edge) => edge.targetDocId)
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
const relationsResult = await store.setDocEdges(
|
|
1094
|
+
doc.id,
|
|
1095
|
+
relationEdges,
|
|
1096
|
+
"frontmatter-relation"
|
|
1097
|
+
);
|
|
1098
|
+
if (!relationsResult.ok) {
|
|
1099
|
+
errors.push({
|
|
1100
|
+
relPath: doc.relPath,
|
|
1101
|
+
code: relationsResult.error.code,
|
|
1102
|
+
message: relationsResult.error.message,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const primaryHint = getPrimaryGraphHint(
|
|
1107
|
+
doc.contentType,
|
|
1108
|
+
options.contentTypeRules ?? []
|
|
1109
|
+
);
|
|
1110
|
+
if (!primaryHint || !RELATION_EDGE_TYPE_PATTERN.test(primaryHint)) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const linksResult = await store.getLinksForDoc(doc.id);
|
|
1115
|
+
if (!linksResult.ok) {
|
|
1116
|
+
errors.push({
|
|
1117
|
+
relPath: doc.relPath,
|
|
1118
|
+
code: linksResult.error.code,
|
|
1119
|
+
message: linksResult.error.message,
|
|
1120
|
+
});
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const wikiEdges: DocEdgeInput[] = [];
|
|
1125
|
+
const markdownEdges: DocEdgeInput[] = [];
|
|
1126
|
+
for (const link of linksResult.value) {
|
|
1127
|
+
const targetRef =
|
|
1128
|
+
link.linkType === "markdown"
|
|
1129
|
+
? `${doc.collection}/${link.targetRefNorm}`
|
|
1130
|
+
: link.targetCollection
|
|
1131
|
+
? `${link.targetCollection}:${link.targetRef}`
|
|
1132
|
+
: link.targetRefNorm;
|
|
1133
|
+
const targetDoc = resolveRelationTarget(activeDocs, doc, targetRef);
|
|
1134
|
+
if (!targetDoc || relationTargetIds.has(targetDoc.id)) {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const edge = {
|
|
1138
|
+
targetDocId: targetDoc.id,
|
|
1139
|
+
edgeType: primaryHint,
|
|
1140
|
+
confidence: "configured" as const,
|
|
1141
|
+
};
|
|
1142
|
+
if (link.linkType === "wiki") {
|
|
1143
|
+
wikiEdges.push(edge);
|
|
1144
|
+
} else {
|
|
1145
|
+
markdownEdges.push(edge);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const wikiResult = await store.setDocEdges(doc.id, wikiEdges, "wikilink");
|
|
1150
|
+
if (!wikiResult.ok) {
|
|
1151
|
+
errors.push({
|
|
1152
|
+
relPath: doc.relPath,
|
|
1153
|
+
code: wikiResult.error.code,
|
|
1154
|
+
message: wikiResult.error.message,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
const markdownResult = await store.setDocEdges(
|
|
1158
|
+
doc.id,
|
|
1159
|
+
markdownEdges,
|
|
1160
|
+
"markdown-link"
|
|
1161
|
+
);
|
|
1162
|
+
if (!markdownResult.ok) {
|
|
1163
|
+
errors.push({
|
|
1164
|
+
relPath: doc.relPath,
|
|
1165
|
+
code: markdownResult.error.code,
|
|
1166
|
+
message: markdownResult.error.message,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return errors;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
821
1174
|
/**
|
|
822
1175
|
* Sync a single collection.
|
|
823
1176
|
*/
|
|
@@ -828,6 +1181,13 @@ export class SyncService {
|
|
|
828
1181
|
options: SyncOptions = {}
|
|
829
1182
|
): Promise<CollectionSyncResult> {
|
|
830
1183
|
const startTime = Date.now();
|
|
1184
|
+
const syncOptions: SyncOptions = {
|
|
1185
|
+
...options,
|
|
1186
|
+
contentTypeRules: options.contentTypeRules ?? [],
|
|
1187
|
+
contentTypeRulesFingerprint:
|
|
1188
|
+
options.contentTypeRulesFingerprint ??
|
|
1189
|
+
fingerprintContentTypeRules(options.contentTypeRules ?? []),
|
|
1190
|
+
};
|
|
831
1191
|
const errors: Array<{ relPath: string; code: string; message: string }> =
|
|
832
1192
|
[];
|
|
833
1193
|
|
|
@@ -892,6 +1252,7 @@ export class SyncService {
|
|
|
892
1252
|
let unchanged = 0;
|
|
893
1253
|
let errored = 0;
|
|
894
1254
|
let dynamicSkipped = 0;
|
|
1255
|
+
const fileResults: FileSyncResult[] = [];
|
|
895
1256
|
|
|
896
1257
|
if (concurrency === 1) {
|
|
897
1258
|
// Sequential processing with batched transactions (Windows perf)
|
|
@@ -905,8 +1266,9 @@ export class SyncService {
|
|
|
905
1266
|
collection,
|
|
906
1267
|
entry,
|
|
907
1268
|
store,
|
|
908
|
-
|
|
1269
|
+
syncOptions
|
|
909
1270
|
);
|
|
1271
|
+
fileResults.push(result);
|
|
910
1272
|
switch (result.status) {
|
|
911
1273
|
case "added":
|
|
912
1274
|
added += 1;
|
|
@@ -970,8 +1332,9 @@ export class SyncService {
|
|
|
970
1332
|
collection,
|
|
971
1333
|
entry,
|
|
972
1334
|
store,
|
|
973
|
-
|
|
1335
|
+
syncOptions
|
|
974
1336
|
);
|
|
1337
|
+
fileResults.push(result);
|
|
975
1338
|
results.push(result);
|
|
976
1339
|
} finally {
|
|
977
1340
|
semaphore.release();
|
|
@@ -1034,6 +1397,10 @@ export class SyncService {
|
|
|
1034
1397
|
}
|
|
1035
1398
|
}
|
|
1036
1399
|
|
|
1400
|
+
errors.push(
|
|
1401
|
+
...(await this.projectTypedEdges(collection, store, syncOptions))
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1037
1404
|
return {
|
|
1038
1405
|
collection: collection.name,
|
|
1039
1406
|
filesProcessed: entries.length,
|
|
@@ -1044,6 +1411,7 @@ export class SyncService {
|
|
|
1044
1411
|
filesSkipped: skipped.length + dynamicSkipped,
|
|
1045
1412
|
filesMarkedInactive: markedInactive,
|
|
1046
1413
|
durationMs: Date.now() - startTime,
|
|
1414
|
+
files: fileResults,
|
|
1047
1415
|
errors,
|
|
1048
1416
|
};
|
|
1049
1417
|
}
|
package/src/ingestion/types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module src/ingestion/types
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { NormalizedContentTypeRule } from "../config";
|
|
8
9
|
import type { Collection } from "../config/types";
|
|
9
10
|
|
|
10
11
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -132,8 +133,18 @@ export interface SyncOptions {
|
|
|
132
133
|
* SQLite operations are serialized regardless of this setting.
|
|
133
134
|
*/
|
|
134
135
|
concurrency?: number;
|
|
136
|
+
/** Normalized content type rules from config.contentTypes. */
|
|
137
|
+
contentTypeRules?: NormalizedContentTypeRule[];
|
|
138
|
+
/** Stable hash of the normalized content type rules, used for re-derivation. */
|
|
139
|
+
contentTypeRulesFingerprint?: string;
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
export type ContentTypeSource =
|
|
143
|
+
| "frontmatter-type"
|
|
144
|
+
| "prefix"
|
|
145
|
+
| "path-ext"
|
|
146
|
+
| "fallback";
|
|
147
|
+
|
|
137
148
|
/** Per-file sync status */
|
|
138
149
|
export type FileSyncStatus =
|
|
139
150
|
| "added"
|
|
@@ -148,6 +159,8 @@ export interface FileSyncResult {
|
|
|
148
159
|
status: FileSyncStatus;
|
|
149
160
|
docid?: string;
|
|
150
161
|
mirrorHash?: string;
|
|
162
|
+
contentType?: string;
|
|
163
|
+
contentTypeSource?: ContentTypeSource;
|
|
151
164
|
errorCode?: string;
|
|
152
165
|
errorMessage?: string;
|
|
153
166
|
}
|
|
@@ -163,6 +176,7 @@ export interface CollectionSyncResult {
|
|
|
163
176
|
filesSkipped: number;
|
|
164
177
|
filesMarkedInactive: number;
|
|
165
178
|
durationMs: number;
|
|
179
|
+
files?: FileSyncResult[];
|
|
166
180
|
errors: Array<{
|
|
167
181
|
relPath: string;
|
|
168
182
|
code: string;
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
normalizeCollectionName,
|
|
19
19
|
validateCollectionRoot,
|
|
20
20
|
} from "../../core/validation";
|
|
21
|
-
import { defaultSyncService } from "../../ingestion";
|
|
21
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
22
22
|
import { runTool, type ToolResult } from "./index";
|
|
23
23
|
|
|
24
24
|
interface AddCollectionInput {
|
|
@@ -147,10 +147,13 @@ export function handleAddCollection(
|
|
|
147
147
|
const result = await defaultSyncService.syncCollection(
|
|
148
148
|
collection,
|
|
149
149
|
ctx.store,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
withContentTypeRules(
|
|
151
|
+
{
|
|
152
|
+
gitPull: args.gitPull ?? false,
|
|
153
|
+
runUpdateCmd: false,
|
|
154
|
+
},
|
|
155
|
+
ctx.config
|
|
156
|
+
)
|
|
154
157
|
);
|
|
155
158
|
|
|
156
159
|
return {
|
package/src/mcp/tools/capture.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { writeCapturePlanFile } from "../../core/capture-write";
|
|
|
24
24
|
import { MCP_ERRORS } from "../../core/errors";
|
|
25
25
|
import { withWriteLock } from "../../core/file-lock";
|
|
26
26
|
import { normalizeCollectionName } from "../../core/validation";
|
|
27
|
-
import { defaultSyncService } from "../../ingestion";
|
|
27
|
+
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
28
28
|
import { runTool, type ToolResult } from "./index";
|
|
29
29
|
|
|
30
30
|
interface CaptureInput extends Omit<
|
|
@@ -176,7 +176,10 @@ export function handleCapture(
|
|
|
176
176
|
collection,
|
|
177
177
|
ctx.store,
|
|
178
178
|
[plan.relPath],
|
|
179
|
-
|
|
179
|
+
withContentTypeRules(
|
|
180
|
+
{ runUpdateCmd: false, gitPull: false },
|
|
181
|
+
ctx.config
|
|
182
|
+
)
|
|
180
183
|
);
|
|
181
184
|
const syncResult = results[0];
|
|
182
185
|
if (!syncResult) {
|
package/src/mcp/tools/get.ts
CHANGED
|
@@ -10,11 +10,11 @@ import type { DocumentRow, StorePort } from "../../store/types";
|
|
|
10
10
|
import type { ToolContext } from "../server";
|
|
11
11
|
|
|
12
12
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
13
|
-
import { parseRef } from "../../cli/commands/ref-parser";
|
|
14
13
|
import {
|
|
15
14
|
getDocumentCapabilities,
|
|
16
15
|
type DocumentCapabilities,
|
|
17
16
|
} from "../../core/document-capabilities";
|
|
17
|
+
import { parseRef } from "../../core/ref-parser";
|
|
18
18
|
import { runTool, type ToolResult } from "./index";
|
|
19
19
|
|
|
20
20
|
interface GetInput {
|