@gmickel/gno 1.20.0 → 1.22.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 +29 -5
- package/assets/skill/SKILL.md +46 -15
- package/package.json +2 -1
- package/spec/cli.md +144 -0
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +48 -0
- package/spec/mcp.md +22 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +195 -0
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/ingestion/sync.ts +214 -165
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/index.ts +29 -0
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +42 -0
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +13 -0
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
package/src/ingestion/sync.ts
CHANGED
|
@@ -40,6 +40,13 @@ import {
|
|
|
40
40
|
getDefaultPipeline,
|
|
41
41
|
} from "../converters/pipeline";
|
|
42
42
|
import { DEFAULT_LIMITS } from "../converters/types";
|
|
43
|
+
import {
|
|
44
|
+
diffDocumentStructure,
|
|
45
|
+
extractDocumentStructure,
|
|
46
|
+
isRelationMap,
|
|
47
|
+
normalizeRelationEdgeType,
|
|
48
|
+
normalizeRelationTarget,
|
|
49
|
+
} from "../core/change-diff";
|
|
43
50
|
import {
|
|
44
51
|
normalizeMarkdownPath,
|
|
45
52
|
normalizeWikiName,
|
|
@@ -83,35 +90,6 @@ const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
|
|
|
83
90
|
"UNSUPPORTED",
|
|
84
91
|
]);
|
|
85
92
|
|
|
86
|
-
type RelationMap = Record<string, string[]>;
|
|
87
|
-
|
|
88
|
-
function isRelationMap(value: unknown): value is RelationMap {
|
|
89
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return Object.values(value).every(
|
|
94
|
-
(targets) =>
|
|
95
|
-
Array.isArray(targets) &&
|
|
96
|
-
targets.every((target) => typeof target === "string")
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function normalizeRelationTarget(raw: string): string {
|
|
101
|
-
const trimmed = raw.trim();
|
|
102
|
-
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) {
|
|
103
|
-
return trimmed.slice(2, -2).split("|")[0]?.trim() ?? "";
|
|
104
|
-
}
|
|
105
|
-
return trimmed;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function normalizeRelationEdgeType(raw: string): string {
|
|
109
|
-
return raw
|
|
110
|
-
.trim()
|
|
111
|
-
.toLowerCase()
|
|
112
|
-
.replace(/[-\s]+/g, "_");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
93
|
function findDocByWikiRef(
|
|
116
94
|
docs: DocumentRow[],
|
|
117
95
|
targetRef: string,
|
|
@@ -747,7 +725,24 @@ export class SyncService {
|
|
|
747
725
|
// Log but continue - error recording is best-effort
|
|
748
726
|
}
|
|
749
727
|
|
|
750
|
-
|
|
728
|
+
const hadRetrievableEvidence = Boolean(existing?.mirrorHash);
|
|
729
|
+
const structureDelta = hadRetrievableEvidence
|
|
730
|
+
? diffDocumentStructure(
|
|
731
|
+
await this.readPreviousStructure(store, existing),
|
|
732
|
+
{
|
|
733
|
+
headings: [],
|
|
734
|
+
links: [],
|
|
735
|
+
typedEdges: [],
|
|
736
|
+
dates: {},
|
|
737
|
+
}
|
|
738
|
+
).delta
|
|
739
|
+
: undefined;
|
|
740
|
+
|
|
741
|
+
// Upsert document with error info, explicitly clear mirrorHash. This
|
|
742
|
+
// transition is journaled only when retrievable evidence previously
|
|
743
|
+
// existed; initial/repeated error placeholders are not evidence
|
|
744
|
+
// creates. Real evidence disappearance stays in the same transaction
|
|
745
|
+
// so saved Capsule freshness checks cannot miss it.
|
|
751
746
|
const upsertResult = await store.upsertDocument({
|
|
752
747
|
collection: collection.name,
|
|
753
748
|
relPath: entry.relPath,
|
|
@@ -761,6 +756,7 @@ export class SyncService {
|
|
|
761
756
|
lastErrorMessage: convertResult.error.message,
|
|
762
757
|
ingestVersion: INGEST_VERSION,
|
|
763
758
|
contentTypeRulesFingerprint,
|
|
759
|
+
changeJournal: structureDelta ? { structureDelta } : false,
|
|
764
760
|
// mirrorHash intentionally omitted (will be null)
|
|
765
761
|
});
|
|
766
762
|
|
|
@@ -788,155 +784,186 @@ export class SyncService {
|
|
|
788
784
|
mime.ext,
|
|
789
785
|
contentTypeRules
|
|
790
786
|
);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
collection: collection.name,
|
|
795
|
-
relPath: entry.relPath,
|
|
796
|
-
sourceHash,
|
|
797
|
-
sourceMime: mime.mime,
|
|
798
|
-
sourceExt: mime.ext,
|
|
799
|
-
sourceSize,
|
|
800
|
-
sourceMtime,
|
|
801
|
-
sourceCtime,
|
|
802
|
-
title: artifact.title,
|
|
803
|
-
mirrorHash: artifact.mirrorHash,
|
|
804
|
-
converterId: artifact.meta.converterId,
|
|
805
|
-
converterVersion: artifact.meta.converterVersion,
|
|
806
|
-
languageHint: artifact.languageHint ?? collection.languageHint,
|
|
807
|
-
contentType: extractedMetadata.contentType,
|
|
808
|
-
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
809
|
-
categories: extractedMetadata.categories,
|
|
810
|
-
author: extractedMetadata.author,
|
|
811
|
-
frontmatterDate: extractedMetadata.frontmatterDate,
|
|
812
|
-
dateFields: extractedMetadata.dateFields,
|
|
813
|
-
contentTypeRulesFingerprint,
|
|
814
|
-
// Clear error fields on success (requires store to handle undefined → null)
|
|
815
|
-
lastErrorCode: undefined,
|
|
816
|
-
lastErrorMessage: undefined,
|
|
817
|
-
ingestVersion: INGEST_VERSION,
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
const { id: docId, docid } = mustOk(docidResult, "upsertDocument", {
|
|
821
|
-
collection: collection.name,
|
|
822
|
-
relPath: entry.relPath,
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
// 8. Upsert content (content-addressed dedupe) - CHECKED
|
|
826
|
-
const contentResult = await store.upsertContent(
|
|
827
|
-
artifact.mirrorHash,
|
|
828
|
-
artifact.markdown
|
|
787
|
+
const previousStructure = await this.readPreviousStructure(
|
|
788
|
+
store,
|
|
789
|
+
existing
|
|
829
790
|
);
|
|
830
|
-
|
|
831
|
-
mirrorHash: artifact.mirrorHash,
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// 9. Chunk content
|
|
835
|
-
const chunks = this.chunker.chunk(
|
|
791
|
+
const nextStructure = extractDocumentStructure(
|
|
836
792
|
artifact.markdown,
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
entry.relPath
|
|
793
|
+
entry.relPath,
|
|
794
|
+
extractedMetadata.dateFields
|
|
840
795
|
);
|
|
796
|
+
const structureDelta = diffDocumentStructure(
|
|
797
|
+
previousStructure,
|
|
798
|
+
nextStructure
|
|
799
|
+
).delta;
|
|
800
|
+
|
|
801
|
+
const persistSuccessfulFile = async (): Promise<FileSyncResult> => {
|
|
802
|
+
// 7. Upsert document - EXPLICITLY clear error fields on success
|
|
803
|
+
const docidResult = await store.upsertDocument({
|
|
804
|
+
collection: collection.name,
|
|
805
|
+
relPath: entry.relPath,
|
|
806
|
+
sourceHash,
|
|
807
|
+
sourceMime: mime.mime,
|
|
808
|
+
sourceExt: mime.ext,
|
|
809
|
+
sourceSize,
|
|
810
|
+
sourceMtime,
|
|
811
|
+
sourceCtime,
|
|
812
|
+
title: artifact.title,
|
|
813
|
+
mirrorHash: artifact.mirrorHash,
|
|
814
|
+
converterId: artifact.meta.converterId,
|
|
815
|
+
converterVersion: artifact.meta.converterVersion,
|
|
816
|
+
languageHint: artifact.languageHint ?? collection.languageHint,
|
|
817
|
+
contentType: extractedMetadata.contentType,
|
|
818
|
+
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
819
|
+
categories: extractedMetadata.categories,
|
|
820
|
+
author: extractedMetadata.author,
|
|
821
|
+
frontmatterDate: extractedMetadata.frontmatterDate,
|
|
822
|
+
dateFields: extractedMetadata.dateFields,
|
|
823
|
+
contentTypeRulesFingerprint,
|
|
824
|
+
// Clear error fields on success (requires store to handle undefined → null)
|
|
825
|
+
lastErrorCode: undefined,
|
|
826
|
+
lastErrorMessage: undefined,
|
|
827
|
+
ingestVersion: INGEST_VERSION,
|
|
828
|
+
changeJournal: { structureDelta },
|
|
829
|
+
});
|
|
841
830
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
text: c.text,
|
|
847
|
-
startLine: c.startLine,
|
|
848
|
-
endLine: c.endLine,
|
|
849
|
-
language: c.language ?? undefined,
|
|
850
|
-
tokenCount: c.tokenCount ?? undefined,
|
|
851
|
-
}));
|
|
852
|
-
|
|
853
|
-
// 11. Upsert chunks - CHECKED
|
|
854
|
-
const chunksResult = await store.upsertChunks(
|
|
855
|
-
artifact.mirrorHash,
|
|
856
|
-
chunkInputs
|
|
857
|
-
);
|
|
858
|
-
mustOk(chunksResult, "upsertChunks", {
|
|
859
|
-
mirrorHash: artifact.mirrorHash,
|
|
860
|
-
chunkCount: chunkInputs.length,
|
|
861
|
-
});
|
|
831
|
+
const { id: docId, docid } = mustOk(docidResult, "upsertDocument", {
|
|
832
|
+
collection: collection.name,
|
|
833
|
+
relPath: entry.relPath,
|
|
834
|
+
});
|
|
862
835
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
836
|
+
// 8. Upsert content (content-addressed dedupe) - CHECKED
|
|
837
|
+
const contentResult = await store.upsertContent(
|
|
838
|
+
artifact.mirrorHash,
|
|
839
|
+
artifact.markdown
|
|
840
|
+
);
|
|
841
|
+
mustOk(contentResult, "upsertContent", {
|
|
842
|
+
mirrorHash: artifact.mirrorHash,
|
|
843
|
+
});
|
|
868
844
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
);
|
|
877
|
-
mustOk(tagsResult, "setDocTags", {
|
|
878
|
-
docId,
|
|
879
|
-
tagCount: extractedTags.length,
|
|
880
|
-
});
|
|
845
|
+
// 9. Chunk content
|
|
846
|
+
const chunks = this.chunker.chunk(
|
|
847
|
+
artifact.markdown,
|
|
848
|
+
DEFAULT_CHUNK_PARAMS,
|
|
849
|
+
artifact.languageHint ?? collection.languageHint,
|
|
850
|
+
entry.relPath
|
|
851
|
+
);
|
|
881
852
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
853
|
+
// 10. Convert to ChunkInput for store
|
|
854
|
+
const chunkInputs: ChunkInput[] = chunks.map((c) => ({
|
|
855
|
+
seq: c.seq,
|
|
856
|
+
pos: c.pos,
|
|
857
|
+
text: c.text,
|
|
858
|
+
startLine: c.startLine,
|
|
859
|
+
endLine: c.endLine,
|
|
860
|
+
language: c.language ?? undefined,
|
|
861
|
+
tokenCount: c.tokenCount ?? undefined,
|
|
862
|
+
}));
|
|
863
|
+
|
|
864
|
+
// 11. Upsert chunks - CHECKED
|
|
865
|
+
const chunksResult = await store.upsertChunks(
|
|
866
|
+
artifact.mirrorHash,
|
|
867
|
+
chunkInputs
|
|
868
|
+
);
|
|
869
|
+
mustOk(chunksResult, "upsertChunks", {
|
|
870
|
+
mirrorHash: artifact.mirrorHash,
|
|
871
|
+
chunkCount: chunkInputs.length,
|
|
872
|
+
});
|
|
890
873
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
874
|
+
// 12. Rebuild FTS for this hash - CHECKED
|
|
875
|
+
const ftsResult = await store.rebuildFtsForHash(artifact.mirrorHash);
|
|
876
|
+
mustOk(ftsResult, "rebuildFtsForHash", {
|
|
877
|
+
mirrorHash: artifact.mirrorHash,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// 13. Extract and store tags from frontmatter and body hashtags
|
|
881
|
+
// Always call setDocTags to clear removed tags on re-sync
|
|
882
|
+
const extractedTags = extractTags(artifact.markdown);
|
|
883
|
+
const tagsResult = await store.setDocTags(
|
|
884
|
+
docId,
|
|
885
|
+
extractedTags,
|
|
886
|
+
"frontmatter"
|
|
887
|
+
);
|
|
888
|
+
mustOk(tagsResult, "setDocTags", {
|
|
889
|
+
docId,
|
|
890
|
+
tagCount: extractedTags.length,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// 14. Extract and store links (wiki and markdown links)
|
|
894
|
+
const excludedRanges = getExcludedRanges(artifact.markdown);
|
|
895
|
+
const lineOffsets = buildLineOffsets(artifact.markdown);
|
|
896
|
+
const parsedLinks = parseLinks(
|
|
897
|
+
artifact.markdown,
|
|
898
|
+
lineOffsets,
|
|
899
|
+
excludedRanges
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
const linkInputs: DocLinkInput[] = [];
|
|
903
|
+
for (const link of parsedLinks) {
|
|
904
|
+
// Compute target_ref_norm based on link type
|
|
905
|
+
let targetRefNorm: string;
|
|
906
|
+
if (link.kind === "wiki") {
|
|
907
|
+
targetRefNorm = normalizeWikiName(link.targetRef);
|
|
908
|
+
} else {
|
|
909
|
+
// Markdown links with collection prefix are not supported
|
|
910
|
+
// (use wiki links for cross-collection references)
|
|
911
|
+
if (link.targetCollection) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const resolved = normalizeMarkdownPath(
|
|
915
|
+
link.targetRef,
|
|
916
|
+
entry.relPath
|
|
917
|
+
);
|
|
918
|
+
if (!resolved) {
|
|
919
|
+
// Link escapes collection root - skip silently
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
targetRefNorm = resolved;
|
|
907
923
|
}
|
|
908
|
-
|
|
924
|
+
|
|
925
|
+
linkInputs.push({
|
|
926
|
+
targetRef: link.targetRef,
|
|
927
|
+
targetRefNorm,
|
|
928
|
+
targetAnchor: link.targetAnchor,
|
|
929
|
+
targetCollection: link.targetCollection,
|
|
930
|
+
linkType: link.kind,
|
|
931
|
+
linkText: link.displayText,
|
|
932
|
+
startLine: link.startLine,
|
|
933
|
+
startCol: link.startCol,
|
|
934
|
+
endLine: link.endLine,
|
|
935
|
+
endCol: link.endCol,
|
|
936
|
+
});
|
|
909
937
|
}
|
|
910
938
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
startCol: link.startCol,
|
|
920
|
-
endLine: link.endLine,
|
|
921
|
-
endCol: link.endCol,
|
|
939
|
+
const linksResult = await store.setDocLinks(
|
|
940
|
+
docId,
|
|
941
|
+
linkInputs,
|
|
942
|
+
"parsed"
|
|
943
|
+
);
|
|
944
|
+
mustOk(linksResult, "setDocLinks", {
|
|
945
|
+
docId,
|
|
946
|
+
linkCount: linkInputs.length,
|
|
922
947
|
});
|
|
923
|
-
}
|
|
924
948
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
status,
|
|
935
|
-
docid,
|
|
936
|
-
mirrorHash: artifact.mirrorHash,
|
|
937
|
-
contentType: extractedMetadata.contentType,
|
|
938
|
-
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
949
|
+
const status = existing ? "updated" : "added";
|
|
950
|
+
return {
|
|
951
|
+
relPath: entry.relPath,
|
|
952
|
+
status,
|
|
953
|
+
docid,
|
|
954
|
+
mirrorHash: artifact.mirrorHash,
|
|
955
|
+
contentType: extractedMetadata.contentType,
|
|
956
|
+
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
957
|
+
};
|
|
939
958
|
};
|
|
959
|
+
if (!store.withTransaction) {
|
|
960
|
+
return await persistSuccessfulFile();
|
|
961
|
+
}
|
|
962
|
+
const persisted = await store.withTransaction(persistSuccessfulFile);
|
|
963
|
+
return mustOk(persisted, "persistSuccessfulFile", {
|
|
964
|
+
collection: collection.name,
|
|
965
|
+
relPath: entry.relPath,
|
|
966
|
+
});
|
|
940
967
|
} catch (error) {
|
|
941
968
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
942
969
|
// Distinguish store errors from other internal errors
|
|
@@ -977,6 +1004,7 @@ export class SyncService {
|
|
|
977
1004
|
existingResult.value.sourceMtime,
|
|
978
1005
|
lastErrorCode: code,
|
|
979
1006
|
lastErrorMessage: message,
|
|
1007
|
+
changeJournal: false,
|
|
980
1008
|
});
|
|
981
1009
|
}
|
|
982
1010
|
} catch {
|
|
@@ -992,6 +1020,27 @@ export class SyncService {
|
|
|
992
1020
|
}
|
|
993
1021
|
}
|
|
994
1022
|
|
|
1023
|
+
private async readPreviousStructure(
|
|
1024
|
+
store: StorePort,
|
|
1025
|
+
existing: DocumentRow | null
|
|
1026
|
+
): Promise<ReturnType<typeof extractDocumentStructure> | null | undefined> {
|
|
1027
|
+
if (!existing) return null;
|
|
1028
|
+
if (!existing.mirrorHash) return undefined;
|
|
1029
|
+
|
|
1030
|
+
const content = await store.getContent(existing.mirrorHash);
|
|
1031
|
+
if (!content.ok) {
|
|
1032
|
+
throw new Error(`Store operation failed: ${content.error.message}`, {
|
|
1033
|
+
cause: content.error,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (content.value === null) return undefined;
|
|
1037
|
+
return extractDocumentStructure(
|
|
1038
|
+
content.value,
|
|
1039
|
+
existing.relPath,
|
|
1040
|
+
existing.dateFields
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
995
1044
|
/**
|
|
996
1045
|
* Sync a specific set of files within a collection.
|
|
997
1046
|
*/
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Read-only MCP adapters for knowledge change, diff, and impact services. */
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { ToolContext } from "../server";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
analyzeKnowledgeImpact,
|
|
9
|
+
getKnowledgeDiff,
|
|
10
|
+
listKnowledgeChanges,
|
|
11
|
+
} from "../../core/knowledge-delta";
|
|
12
|
+
import { runTool, type ToolResult } from "./index";
|
|
13
|
+
|
|
14
|
+
export const changesInputSchema = z.object({
|
|
15
|
+
since: z.string().trim().min(1).max(512).optional(),
|
|
16
|
+
collection: z.string().trim().min(1).max(256).optional(),
|
|
17
|
+
limit: z.number().int().min(1).max(1000).default(100),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const diffInputSchema = z.object({
|
|
21
|
+
ref: z.string().trim().min(1).max(4096),
|
|
22
|
+
change: z.string().trim().min(1).max(512).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const impactInputSchema = z.object({
|
|
26
|
+
ref: z.string().trim().min(1).max(4096),
|
|
27
|
+
maxDepth: z.number().int().min(1).max(6).default(3),
|
|
28
|
+
maxNodes: z.number().int().min(1).max(1000).default(100),
|
|
29
|
+
maxEdges: z.number().int().min(1).max(5000).default(250),
|
|
30
|
+
frontierLimit: z.number().int().min(1).max(1000).default(100),
|
|
31
|
+
visitedLimit: z.number().int().min(1).max(5000).default(500),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const unwrap = <T>(
|
|
35
|
+
result:
|
|
36
|
+
| { success: true; data: T }
|
|
37
|
+
| { success: false; error: string; isValidation?: boolean }
|
|
38
|
+
): T => {
|
|
39
|
+
if (result.success) return result.data;
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${result.isValidation ? "VALIDATION" : "RUNTIME"}: ${result.error}`
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const handleChanges = (
|
|
46
|
+
args: z.infer<typeof changesInputSchema>,
|
|
47
|
+
ctx: ToolContext
|
|
48
|
+
): Promise<ToolResult> =>
|
|
49
|
+
runTool(
|
|
50
|
+
ctx,
|
|
51
|
+
"gno_changes",
|
|
52
|
+
async () => unwrap(await listKnowledgeChanges(ctx.store, args)),
|
|
53
|
+
(data) =>
|
|
54
|
+
`${data.changes.length} retained document changes${data.page.truncated ? " (more available)" : ""}`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const handleDiff = (
|
|
58
|
+
args: z.infer<typeof diffInputSchema>,
|
|
59
|
+
ctx: ToolContext
|
|
60
|
+
): Promise<ToolResult> =>
|
|
61
|
+
runTool(
|
|
62
|
+
ctx,
|
|
63
|
+
"gno_diff",
|
|
64
|
+
async () =>
|
|
65
|
+
unwrap(await getKnowledgeDiff(ctx.store, args.ref, args.change)),
|
|
66
|
+
(data) =>
|
|
67
|
+
`Structural diff for ${data.document.uri}: ${data.status}; history ${data.history.status}; source bodies not retained`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const handleImpact = (
|
|
71
|
+
args: z.infer<typeof impactInputSchema>,
|
|
72
|
+
ctx: ToolContext
|
|
73
|
+
): Promise<ToolResult> =>
|
|
74
|
+
runTool(
|
|
75
|
+
ctx,
|
|
76
|
+
"gno_impact",
|
|
77
|
+
async () => unwrap(await analyzeKnowledgeImpact(ctx.store, args.ref, args)),
|
|
78
|
+
(data) =>
|
|
79
|
+
`${data.impacted.length} documents depend on ${data.root.uri}${data.meta.truncated ? " (truncated)" : ""}`
|
|
80
|
+
);
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -21,6 +21,14 @@ import { normalizeTag } from "../../core/tags";
|
|
|
21
21
|
import { handleAddCollection } from "./add-collection";
|
|
22
22
|
import { askInputSchema, handleAsk } from "./ask";
|
|
23
23
|
import { handleCapture } from "./capture";
|
|
24
|
+
import {
|
|
25
|
+
changesInputSchema,
|
|
26
|
+
diffInputSchema,
|
|
27
|
+
handleChanges,
|
|
28
|
+
handleDiff,
|
|
29
|
+
handleImpact,
|
|
30
|
+
impactInputSchema,
|
|
31
|
+
} from "./changes";
|
|
24
32
|
import { handleClearCollectionEmbeddings } from "./clear-collection-embeddings";
|
|
25
33
|
import { handleContext, handleContextVerify } from "./context";
|
|
26
34
|
import { handleEmbed } from "./embed";
|
|
@@ -1036,6 +1044,27 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1036
1044
|
(args) => handleStatus(args, ctx)
|
|
1037
1045
|
);
|
|
1038
1046
|
|
|
1047
|
+
server.tool(
|
|
1048
|
+
"gno_changes",
|
|
1049
|
+
"List retained metadata-only document changes with opaque cursor pagination and retention disclosure.",
|
|
1050
|
+
changesInputSchema.shape,
|
|
1051
|
+
(args) => handleChanges(args, ctx)
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
server.tool(
|
|
1055
|
+
"gno_diff",
|
|
1056
|
+
"Inspect one retained metadata-only structural document change. Source bodies are never returned.",
|
|
1057
|
+
diffInputSchema.shape,
|
|
1058
|
+
(args) => handleDiff(args, ctx)
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
server.tool(
|
|
1062
|
+
"gno_impact",
|
|
1063
|
+
"Find bounded inbound typed, wiki, and Markdown dependencies with deterministic evidence paths.",
|
|
1064
|
+
impactInputSchema.shape,
|
|
1065
|
+
(args) => handleImpact(args, ctx)
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1039
1068
|
server.tool(
|
|
1040
1069
|
"gno_trace_list",
|
|
1041
1070
|
"List bounded metadata-only summaries of private local retrieval traces. Raw replay queries are omitted from history.",
|