@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
|
@@ -8,7 +8,7 @@ import type { Collection } from "../config/types";
|
|
|
8
8
|
import type { DocumentRow, StorePort, TagRow } from "../store/types";
|
|
9
9
|
|
|
10
10
|
import { parseRef } from "../core/ref-parser";
|
|
11
|
-
import { parseFrontmatter } from "../ingestion/frontmatter";
|
|
11
|
+
import { parseFrontmatter, stripFrontmatter } from "../ingestion/frontmatter";
|
|
12
12
|
import { getContentBatch } from "../store/content-batch";
|
|
13
13
|
import {
|
|
14
14
|
buildEncryptedPublishArtifact,
|
|
@@ -186,11 +186,11 @@ async function exportCollectionArtifact(
|
|
|
186
186
|
if (isPublishDisabledByFrontmatter(rawMarkdown)) {
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
|
+
const frontmatter = parseFrontmatter(rawMarkdown).metadata;
|
|
189
190
|
const sanitized = sanitizeObsidianMarkdown(rawMarkdown);
|
|
190
191
|
warnings.push(...sanitized.warnings);
|
|
191
|
-
const markdown = sanitized.markdown;
|
|
192
|
+
const markdown = stripFrontmatter(sanitized.markdown);
|
|
192
193
|
const tags = tagsByDocId.get(doc.id) ?? [];
|
|
193
|
-
const frontmatter = parseFrontmatter(markdown).metadata;
|
|
194
194
|
const title = deriveExportedTitle(doc);
|
|
195
195
|
notes.push({
|
|
196
196
|
markdown,
|
|
@@ -240,7 +240,6 @@ async function exportCollectionArtifact(
|
|
|
240
240
|
encryptedPayload: encrypted.encryptedPayload,
|
|
241
241
|
routeSlug,
|
|
242
242
|
secretToken: encrypted.secretToken,
|
|
243
|
-
source: collection.name,
|
|
244
243
|
sourceType: "collection",
|
|
245
244
|
});
|
|
246
245
|
}
|
|
@@ -249,7 +248,6 @@ async function exportCollectionArtifact(
|
|
|
249
248
|
homeNoteSlug: chooseHomeNoteSlug(notes),
|
|
250
249
|
notes,
|
|
251
250
|
routeSlug,
|
|
252
|
-
source: collection.name,
|
|
253
251
|
sourceType: "collection",
|
|
254
252
|
summary,
|
|
255
253
|
title,
|
|
@@ -274,11 +272,11 @@ async function exportDocumentArtifact(
|
|
|
274
272
|
`Refused to export: ${doc.uri} has publish: false in frontmatter`
|
|
275
273
|
);
|
|
276
274
|
}
|
|
275
|
+
const frontmatter = parseFrontmatter(rawMarkdown).metadata;
|
|
277
276
|
const sanitized = sanitizeObsidianMarkdown(rawMarkdown);
|
|
278
277
|
warnings.push(...sanitized.warnings);
|
|
279
|
-
const markdown = sanitized.markdown;
|
|
278
|
+
const markdown = stripFrontmatter(sanitized.markdown);
|
|
280
279
|
const tags = await loadDocumentTags(store, doc);
|
|
281
|
-
const frontmatter = parseFrontmatter(markdown).metadata;
|
|
282
280
|
const title = options.title ?? deriveExportedTitle(doc);
|
|
283
281
|
const summary =
|
|
284
282
|
options.summary ?? deriveExportedSummary(markdown, frontmatter);
|
|
@@ -315,7 +313,6 @@ async function exportDocumentArtifact(
|
|
|
315
313
|
encryptedPayload: encrypted.encryptedPayload,
|
|
316
314
|
routeSlug,
|
|
317
315
|
secretToken: encrypted.secretToken,
|
|
318
|
-
source: doc.uri,
|
|
319
316
|
sourceType: "note",
|
|
320
317
|
});
|
|
321
318
|
}
|
|
@@ -331,7 +328,6 @@ async function exportDocumentArtifact(
|
|
|
331
328
|
},
|
|
332
329
|
],
|
|
333
330
|
routeSlug,
|
|
334
|
-
source: doc.uri,
|
|
335
331
|
sourceType: "note",
|
|
336
332
|
summary,
|
|
337
333
|
title,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader-safe publish metadata projection.
|
|
3
|
+
*
|
|
4
|
+
* @module src/publish/metadata
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DocumentRow, TagRow } from "../store/types";
|
|
8
|
+
|
|
9
|
+
const ALLOWED_FRONTMATTER_METADATA_KEYS = new Set([
|
|
10
|
+
"audience",
|
|
11
|
+
"canonical",
|
|
12
|
+
"canonicalUrl",
|
|
13
|
+
"canonicalURL",
|
|
14
|
+
"coverAlt",
|
|
15
|
+
"coverImage",
|
|
16
|
+
"icon",
|
|
17
|
+
"image",
|
|
18
|
+
"layout",
|
|
19
|
+
"publishedAt",
|
|
20
|
+
"readingTime",
|
|
21
|
+
"series",
|
|
22
|
+
"seriesOrder",
|
|
23
|
+
"status",
|
|
24
|
+
"subtitle",
|
|
25
|
+
"theme",
|
|
26
|
+
"topic",
|
|
27
|
+
"topics",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const PUBLIC_URL_METADATA_KEYS = new Set([
|
|
31
|
+
"canonical",
|
|
32
|
+
"canonicalUrl",
|
|
33
|
+
"canonicalURL",
|
|
34
|
+
"coverImage",
|
|
35
|
+
"image",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const FORBIDDEN_URI_TOKEN_PATTERN =
|
|
39
|
+
/(?:^|[^a-z0-9+.-])(?:file:(?:\/\/)?|gno:\/\/)/iu;
|
|
40
|
+
const LOCAL_PATH_TOKEN_PATTERN =
|
|
41
|
+
/(?:^|[\s([{"'=,:;])(?:~[/\\]|[a-z]:[/\\]|\\\\[^\\/\s]+[/\\]|\/(?:Applications|bin|dev|etc|home|Library|mnt|opt|private|proc|root|srv|sys|System|tmp|Users|usr|var|Volumes)(?:[/\\]|$))/iu;
|
|
42
|
+
const LOCAL_HOSTNAME_SUFFIX_PATTERN =
|
|
43
|
+
/(?:^|\.)(?:home|internal|lan|local|localhost)$/iu;
|
|
44
|
+
|
|
45
|
+
const containsLocalReference = (value: string): boolean =>
|
|
46
|
+
FORBIDDEN_URI_TOKEN_PATTERN.test(value) ||
|
|
47
|
+
LOCAL_PATH_TOKEN_PATTERN.test(value);
|
|
48
|
+
|
|
49
|
+
const isNonPublicIpv4 = (hostname: string): boolean => {
|
|
50
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(hostname)) return false;
|
|
51
|
+
const octets = hostname.split(".").map(Number);
|
|
52
|
+
if (octets.some((octet) => octet > 255)) return true;
|
|
53
|
+
const [first = 0, second = 0, third = 0] = octets;
|
|
54
|
+
return (
|
|
55
|
+
first === 0 ||
|
|
56
|
+
first === 10 ||
|
|
57
|
+
first === 127 ||
|
|
58
|
+
first >= 224 ||
|
|
59
|
+
(first === 100 && second >= 64 && second <= 127) ||
|
|
60
|
+
(first === 169 && second === 254) ||
|
|
61
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
62
|
+
(first === 192 && second === 0 && third === 0) ||
|
|
63
|
+
(first === 192 && second === 0 && third === 2) ||
|
|
64
|
+
(first === 192 && second === 168) ||
|
|
65
|
+
(first === 198 && (second === 18 || second === 19)) ||
|
|
66
|
+
(first === 198 && second === 51 && third === 100) ||
|
|
67
|
+
(first === 203 && second === 0 && third === 113)
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isNonPublicIpv6 = (hostname: string): boolean => {
|
|
72
|
+
const normalized = hostname.replace(/^\[|\]$/gu, "").toLowerCase();
|
|
73
|
+
if (!normalized.includes(":")) return false;
|
|
74
|
+
return (
|
|
75
|
+
normalized === "::" ||
|
|
76
|
+
normalized.startsWith("::") ||
|
|
77
|
+
normalized.startsWith("fc") ||
|
|
78
|
+
normalized.startsWith("fd") ||
|
|
79
|
+
/^fe[89ab]/u.test(normalized) ||
|
|
80
|
+
normalized.startsWith("ff") ||
|
|
81
|
+
normalized.startsWith("2001:db8:")
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const isPublicHttpUrl = (value: string): boolean => {
|
|
86
|
+
let url: URL;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(value);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
95
|
+
url.username.length > 0 ||
|
|
96
|
+
url.password.length > 0
|
|
97
|
+
) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hostname = url.hostname.replace(/\.$/u, "").toLowerCase();
|
|
102
|
+
if (
|
|
103
|
+
hostname.length === 0 ||
|
|
104
|
+
LOCAL_HOSTNAME_SUFFIX_PATTERN.test(hostname) ||
|
|
105
|
+
isNonPublicIpv4(hostname) ||
|
|
106
|
+
isNonPublicIpv6(hostname)
|
|
107
|
+
) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isIpLiteral =
|
|
112
|
+
hostname.includes(":") || /^\d+(?:\.\d+){3}$/u.test(hostname);
|
|
113
|
+
return isIpLiteral || hostname.includes(".");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isSafeMetadataValue = (key: string, value: string): boolean => {
|
|
117
|
+
if (containsLocalReference(value)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
PUBLIC_URL_METADATA_KEYS.has(key) ||
|
|
122
|
+
(key === "icon" && /^https?:\/\//iu.test(value))
|
|
123
|
+
) {
|
|
124
|
+
return isPublicHttpUrl(value);
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const filterReaderSafeMetadata = (
|
|
130
|
+
metadata: Record<string, string | string[]>
|
|
131
|
+
): Record<string, string | string[]> => {
|
|
132
|
+
const result: Record<string, string | string[]> = {};
|
|
133
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
if (isSafeMetadataValue(key, value)) {
|
|
136
|
+
result[key] = value;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const safeValues = value.filter((entry) => isSafeMetadataValue(key, entry));
|
|
142
|
+
if (safeValues.length > 0) {
|
|
143
|
+
result[key] = safeValues;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const buildExportedMetadata = (
|
|
150
|
+
doc: Pick<
|
|
151
|
+
DocumentRow,
|
|
152
|
+
"author" | "categories" | "contentType" | "frontmatterDate" | "languageHint"
|
|
153
|
+
>,
|
|
154
|
+
parsedFrontmatter: Record<string, unknown>,
|
|
155
|
+
tags: TagRow[]
|
|
156
|
+
): Record<string, string | string[]> => {
|
|
157
|
+
const metadata: Record<string, string | string[]> = {};
|
|
158
|
+
|
|
159
|
+
if (doc.author) metadata.author = doc.author;
|
|
160
|
+
if (doc.contentType) metadata.contentType = doc.contentType;
|
|
161
|
+
if (doc.languageHint) metadata.language = doc.languageHint;
|
|
162
|
+
if (doc.frontmatterDate) metadata.date = doc.frontmatterDate;
|
|
163
|
+
if (doc.categories?.length) metadata.categories = doc.categories;
|
|
164
|
+
|
|
165
|
+
const tagValues = tags.map((tag) => tag.tag);
|
|
166
|
+
if (tagValues.length) metadata.tags = tagValues;
|
|
167
|
+
|
|
168
|
+
for (const [key, value] of Object.entries(parsedFrontmatter)) {
|
|
169
|
+
if (
|
|
170
|
+
key === "tags" ||
|
|
171
|
+
key === "title" ||
|
|
172
|
+
key === "summary" ||
|
|
173
|
+
!ALLOWED_FRONTMATTER_METADATA_KEYS.has(key)
|
|
174
|
+
) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
typeof value === "string" &&
|
|
179
|
+
value.trim() &&
|
|
180
|
+
isSafeMetadataValue(key, value.trim())
|
|
181
|
+
) {
|
|
182
|
+
metadata[key] = value.trim();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
const cleaned = value
|
|
187
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
188
|
+
.map((entry) => entry.trim())
|
|
189
|
+
.filter((entry) => entry.length > 0 && isSafeMetadataValue(key, entry));
|
|
190
|
+
if (cleaned.length > 0) metadata[key] = cleaned;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return filterReaderSafeMetadata(metadata);
|
|
195
|
+
};
|
package/src/sdk/client.ts
CHANGED
|
@@ -40,6 +40,11 @@ import type {
|
|
|
40
40
|
GnoRenameNoteOptions,
|
|
41
41
|
GnoUpdateOptions,
|
|
42
42
|
GnoVectorSearchOptions,
|
|
43
|
+
KnowledgeChangesResult,
|
|
44
|
+
KnowledgeDiffResult,
|
|
45
|
+
KnowledgeImpactInput,
|
|
46
|
+
KnowledgeImpactResult,
|
|
47
|
+
ListKnowledgeChangesInput,
|
|
43
48
|
} from "./types";
|
|
44
49
|
|
|
45
50
|
import {
|
|
@@ -84,6 +89,12 @@ import {
|
|
|
84
89
|
planRenameRefactor,
|
|
85
90
|
} from "../core/file-refactors";
|
|
86
91
|
import { resolveEffectiveIndex } from "../core/indexed-reference";
|
|
92
|
+
import {
|
|
93
|
+
analyzeKnowledgeImpact,
|
|
94
|
+
getKnowledgeDiff,
|
|
95
|
+
listKnowledgeChanges,
|
|
96
|
+
type KnowledgeDeltaServiceResult,
|
|
97
|
+
} from "../core/knowledge-delta";
|
|
87
98
|
import { resolveNoteCreatePlan } from "../core/note-creation";
|
|
88
99
|
import { resolveNotePreset } from "../core/note-presets";
|
|
89
100
|
import { RetrievalTraceManagementService } from "../core/retrieval-trace-management";
|
|
@@ -174,6 +185,11 @@ function unwrapTraceStore<T>(result: StoreResult<T>): T {
|
|
|
174
185
|
});
|
|
175
186
|
}
|
|
176
187
|
|
|
188
|
+
function unwrapKnowledgeDelta<T>(result: KnowledgeDeltaServiceResult<T>): T {
|
|
189
|
+
if (result.success) return result.data;
|
|
190
|
+
throw sdkError(result.isValidation ? "VALIDATION" : "STORE", result.error);
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
async function resolveClientState(
|
|
178
194
|
options: GnoClientInitOptions = {}
|
|
179
195
|
): Promise<OpenedClientState> {
|
|
@@ -1067,6 +1083,32 @@ class GnoClientImpl implements GnoClient {
|
|
|
1067
1083
|
};
|
|
1068
1084
|
}
|
|
1069
1085
|
|
|
1086
|
+
async changes(
|
|
1087
|
+
options: ListKnowledgeChangesInput = {}
|
|
1088
|
+
): Promise<KnowledgeChangesResult> {
|
|
1089
|
+
this.assertOpen();
|
|
1090
|
+
return unwrapKnowledgeDelta(
|
|
1091
|
+
await listKnowledgeChanges(this.store, options)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async diff(ref: string, changeId?: string): Promise<KnowledgeDiffResult> {
|
|
1096
|
+
this.assertOpen();
|
|
1097
|
+
return unwrapKnowledgeDelta(
|
|
1098
|
+
await getKnowledgeDiff(this.store, ref, changeId)
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async impact(
|
|
1103
|
+
ref: string,
|
|
1104
|
+
options: KnowledgeImpactInput = {}
|
|
1105
|
+
): Promise<KnowledgeImpactResult> {
|
|
1106
|
+
this.assertOpen();
|
|
1107
|
+
return unwrapKnowledgeDelta(
|
|
1108
|
+
await analyzeKnowledgeImpact(this.store, ref, options)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1070
1112
|
async status(): Promise<IndexStatus> {
|
|
1071
1113
|
this.assertOpen();
|
|
1072
1114
|
return unwrapStore(
|
package/src/sdk/index.ts
CHANGED
|
@@ -54,6 +54,13 @@ export type {
|
|
|
54
54
|
GnoSkippedDocument,
|
|
55
55
|
GnoUpdateOptions,
|
|
56
56
|
GnoVectorSearchOptions,
|
|
57
|
+
KnowledgeChange,
|
|
58
|
+
KnowledgeChangesResult,
|
|
59
|
+
KnowledgeDiffResult,
|
|
60
|
+
KnowledgeImpactEvidenceStep,
|
|
61
|
+
KnowledgeImpactInput,
|
|
62
|
+
KnowledgeImpactResult,
|
|
63
|
+
ListKnowledgeChangesInput,
|
|
57
64
|
} from "./types";
|
|
58
65
|
export {
|
|
59
66
|
ContextCapsuleContractError,
|
package/src/sdk/types.ts
CHANGED
|
@@ -17,6 +17,13 @@ import type {
|
|
|
17
17
|
} from "../core/context-capsule";
|
|
18
18
|
import type { ContextEvidenceErrorCode } from "../core/context-evidence";
|
|
19
19
|
import type { ContextVerifierErrorCode } from "../core/context-verifier";
|
|
20
|
+
import type {
|
|
21
|
+
KnowledgeChangesResult,
|
|
22
|
+
KnowledgeDiffResult,
|
|
23
|
+
KnowledgeImpactInput,
|
|
24
|
+
KnowledgeImpactResult,
|
|
25
|
+
ListKnowledgeChangesInput,
|
|
26
|
+
} from "../core/knowledge-delta";
|
|
20
27
|
import type { NoteCollisionPolicy } from "../core/note-creation";
|
|
21
28
|
import type { NotePresetId } from "../core/note-presets";
|
|
22
29
|
import type {
|
|
@@ -52,6 +59,15 @@ export type {
|
|
|
52
59
|
SyncResult,
|
|
53
60
|
};
|
|
54
61
|
export type { AskOptions, HybridSearchOptions } from "../pipeline/types";
|
|
62
|
+
export type {
|
|
63
|
+
KnowledgeChange,
|
|
64
|
+
KnowledgeChangesResult,
|
|
65
|
+
KnowledgeDiffResult,
|
|
66
|
+
KnowledgeImpactEvidenceStep,
|
|
67
|
+
KnowledgeImpactInput,
|
|
68
|
+
KnowledgeImpactResult,
|
|
69
|
+
ListKnowledgeChangesInput,
|
|
70
|
+
} from "../core/knowledge-delta";
|
|
55
71
|
export type { Collection, Config as GnoConfig, Context } from "../config/types";
|
|
56
72
|
export type { GetResponse as GnoGetResult } from "../cli/commands/get";
|
|
57
73
|
export type {
|
|
@@ -233,6 +249,12 @@ export interface GnoClient {
|
|
|
233
249
|
list(
|
|
234
250
|
options?: GnoListOptions
|
|
235
251
|
): Promise<import("../cli/commands/ls").LsResponse>;
|
|
252
|
+
changes(options?: ListKnowledgeChangesInput): Promise<KnowledgeChangesResult>;
|
|
253
|
+
diff(ref: string, changeId?: string): Promise<KnowledgeDiffResult>;
|
|
254
|
+
impact(
|
|
255
|
+
ref: string,
|
|
256
|
+
options?: KnowledgeImpactInput
|
|
257
|
+
): Promise<KnowledgeImpactResult>;
|
|
236
258
|
status(): Promise<IndexStatus>;
|
|
237
259
|
listRetrievalTraces(
|
|
238
260
|
options?: RetrievalTraceListRequest
|
package/src/serve/doc-events.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type DocumentEventOrigin = "watcher" | "save" | "create";
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface DocumentChangedEvent {
|
|
4
4
|
type: "document-changed";
|
|
5
5
|
uri: string;
|
|
6
6
|
collection: string;
|
|
@@ -9,6 +9,17 @@ export interface DocumentEvent {
|
|
|
9
9
|
changedAt: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface CapsuleReverifiedEvent {
|
|
13
|
+
type: "capsule-reverified";
|
|
14
|
+
registrationId: string;
|
|
15
|
+
capsuleId: string;
|
|
16
|
+
operationStatus: "completed" | "failed";
|
|
17
|
+
affectedQuestionState: "unaffected" | "affected" | "unknown";
|
|
18
|
+
changedAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DocumentEvent = DocumentChangedEvent | CapsuleReverifiedEvent;
|
|
22
|
+
|
|
12
23
|
export interface DocumentEventBusState {
|
|
13
24
|
connectedClients: number;
|
|
14
25
|
retryMs: number;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
isInitialized,
|
|
32
32
|
loadConfig,
|
|
33
33
|
} from "../config";
|
|
34
|
+
import { SavedCapsuleReverificationScheduler } from "../core/capsule-reverification-scheduler";
|
|
34
35
|
import { acquireWriteLock } from "../core/file-lock";
|
|
35
36
|
import { JobManager } from "../core/job-manager";
|
|
36
37
|
import { recordContentMutation } from "../core/mutation-generations";
|
|
@@ -94,6 +95,7 @@ export interface ResidentRuntime {
|
|
|
94
95
|
readonly toolMutex: Mutex;
|
|
95
96
|
readonly readerGate: ReaderGate;
|
|
96
97
|
readonly jobManager: JobManager;
|
|
98
|
+
readonly capsuleReverificationScheduler: SavedCapsuleReverificationScheduler;
|
|
97
99
|
readonly modelManager: ModelManager;
|
|
98
100
|
readonly mcpContext: ToolContext;
|
|
99
101
|
readonly generations: ResidentGeneration;
|
|
@@ -266,6 +268,8 @@ export async function startResidentRuntime(
|
|
|
266
268
|
ctxHolder.current.scheduler = scheduler;
|
|
267
269
|
ctxHolder.current.eventBus = options.eventBus ?? null;
|
|
268
270
|
|
|
271
|
+
let capsuleReverificationScheduler: SavedCapsuleReverificationScheduler | null =
|
|
272
|
+
null;
|
|
269
273
|
const watchService = (
|
|
270
274
|
deps.watchServiceFactory ??
|
|
271
275
|
((watchOptions) => new DefaultCollectionWatchService(watchOptions))
|
|
@@ -282,6 +286,10 @@ export async function startResidentRuntime(
|
|
|
282
286
|
});
|
|
283
287
|
options.watchCallbacks?.onSyncComplete?.(event);
|
|
284
288
|
},
|
|
289
|
+
onSettled: () => {
|
|
290
|
+
capsuleReverificationScheduler?.notifySyncSettled();
|
|
291
|
+
options.watchCallbacks?.onSettled?.();
|
|
292
|
+
},
|
|
285
293
|
},
|
|
286
294
|
syncOptions: withContentTypeRules({}, initialConfig),
|
|
287
295
|
});
|
|
@@ -312,6 +320,17 @@ export async function startResidentRuntime(
|
|
|
312
320
|
const backgroundWork = new ResidentBackgroundWork(
|
|
313
321
|
() => !disposed && admission.accepting
|
|
314
322
|
);
|
|
323
|
+
capsuleReverificationScheduler = new SavedCapsuleReverificationScheduler({
|
|
324
|
+
deps: {
|
|
325
|
+
store,
|
|
326
|
+
get config() {
|
|
327
|
+
return ctxHolder.config;
|
|
328
|
+
},
|
|
329
|
+
indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME),
|
|
330
|
+
notify: (event) => options.eventBus?.emit(event),
|
|
331
|
+
},
|
|
332
|
+
startBackgroundWork: (operation) => backgroundWork.start(operation),
|
|
333
|
+
});
|
|
315
334
|
|
|
316
335
|
const mcpContext = createToolContext({
|
|
317
336
|
store,
|
|
@@ -355,6 +374,7 @@ export async function startResidentRuntime(
|
|
|
355
374
|
toolMutex,
|
|
356
375
|
readerGate,
|
|
357
376
|
jobManager,
|
|
377
|
+
capsuleReverificationScheduler,
|
|
358
378
|
modelManager,
|
|
359
379
|
mcpContext,
|
|
360
380
|
generations,
|
|
@@ -453,6 +473,7 @@ export async function startResidentRuntime(
|
|
|
453
473
|
syncOptions.triggerEmbed === false
|
|
454
474
|
? null
|
|
455
475
|
: await scheduler.triggerNow();
|
|
476
|
+
capsuleReverificationScheduler.notifySyncSettled();
|
|
456
477
|
return { syncResult, embedResult };
|
|
457
478
|
},
|
|
458
479
|
async dispose() {
|
|
@@ -466,6 +487,7 @@ export async function startResidentRuntime(
|
|
|
466
487
|
);
|
|
467
488
|
if (deadlineReached) shutdownState = "deadline";
|
|
468
489
|
await backgroundWork.cancelAndDrain();
|
|
490
|
+
await capsuleReverificationScheduler.dispose();
|
|
469
491
|
await jobManager.shutdown().catch(() => undefined);
|
|
470
492
|
await Promise.allSettled([
|
|
471
493
|
Promise.resolve().then(() => watchService.dispose()),
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -160,6 +160,7 @@ import {
|
|
|
160
160
|
withRetrievalTraceHeader,
|
|
161
161
|
} from "../retrieval-trace";
|
|
162
162
|
import { buildAppStatus, type StatusBuildDeps } from "../status";
|
|
163
|
+
import { handleChanges, handleDiff, handleImpact } from "./changes";
|
|
163
164
|
|
|
164
165
|
/** Mutable context holder for hot-reloading presets */
|
|
165
166
|
export interface ContextHolder {
|
|
@@ -5122,6 +5123,18 @@ export async function routeApi(
|
|
|
5122
5123
|
);
|
|
5123
5124
|
}
|
|
5124
5125
|
|
|
5126
|
+
if (path === "/api/changes" && req.method === "GET") {
|
|
5127
|
+
return handleChanges(store, url);
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
if (path === "/api/diff" && req.method === "GET") {
|
|
5131
|
+
return handleDiff(store, url);
|
|
5132
|
+
}
|
|
5133
|
+
|
|
5134
|
+
if (path === "/api/impact" && req.method === "GET") {
|
|
5135
|
+
return handleImpact(store, url);
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5125
5138
|
// Unknown API route
|
|
5126
5139
|
if (path.startsWith("/api/")) {
|
|
5127
5140
|
return errorResponse("NOT_FOUND", `Unknown API endpoint: ${path}`, 404);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Read-only REST handlers for knowledge change, diff, and impact services. */
|
|
2
|
+
|
|
3
|
+
import type { KnowledgeImpactInput } from "../../core/knowledge-delta";
|
|
4
|
+
import type { StorePort } from "../../store/types";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
analyzeKnowledgeImpact,
|
|
8
|
+
getKnowledgeDiff,
|
|
9
|
+
listKnowledgeChanges,
|
|
10
|
+
} from "../../core/knowledge-delta";
|
|
11
|
+
|
|
12
|
+
const errorResponse = (
|
|
13
|
+
code: "VALIDATION" | "RUNTIME",
|
|
14
|
+
message: string
|
|
15
|
+
): Response =>
|
|
16
|
+
Response.json(
|
|
17
|
+
{ error: { code, message } },
|
|
18
|
+
{ status: code === "VALIDATION" ? 400 : 500 }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const parsePositiveInt = (
|
|
22
|
+
url: URL,
|
|
23
|
+
name: string
|
|
24
|
+
): number | undefined | Response => {
|
|
25
|
+
const raw = url.searchParams.get(name);
|
|
26
|
+
if (raw === null) return;
|
|
27
|
+
if (!/^\d+$/.test(raw)) {
|
|
28
|
+
return errorResponse("VALIDATION", `${name} must be a positive integer`);
|
|
29
|
+
}
|
|
30
|
+
const value = Number(raw);
|
|
31
|
+
return Number.isSafeInteger(value) && value > 0
|
|
32
|
+
? value
|
|
33
|
+
: errorResponse("VALIDATION", `${name} must be a positive integer`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function handleChanges(
|
|
37
|
+
store: StorePort,
|
|
38
|
+
url: URL
|
|
39
|
+
): Promise<Response> {
|
|
40
|
+
const limit = parsePositiveInt(url, "limit");
|
|
41
|
+
if (limit instanceof Response) return limit;
|
|
42
|
+
const result = await listKnowledgeChanges(store, {
|
|
43
|
+
since: url.searchParams.get("since") ?? undefined,
|
|
44
|
+
collection: url.searchParams.get("collection") ?? undefined,
|
|
45
|
+
limit,
|
|
46
|
+
});
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
return errorResponse(
|
|
49
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
50
|
+
result.error
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return Response.json(result.data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleDiff(
|
|
57
|
+
store: StorePort,
|
|
58
|
+
url: URL
|
|
59
|
+
): Promise<Response> {
|
|
60
|
+
const ref = url.searchParams.get("ref")?.trim();
|
|
61
|
+
if (!ref) return errorResponse("VALIDATION", "ref is required");
|
|
62
|
+
const result = await getKnowledgeDiff(
|
|
63
|
+
store,
|
|
64
|
+
ref,
|
|
65
|
+
url.searchParams.get("change") ?? undefined
|
|
66
|
+
);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
return errorResponse(
|
|
69
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
70
|
+
result.error
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return Response.json(result.data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function handleImpact(
|
|
77
|
+
store: StorePort,
|
|
78
|
+
url: URL
|
|
79
|
+
): Promise<Response> {
|
|
80
|
+
const ref = url.searchParams.get("ref")?.trim();
|
|
81
|
+
if (!ref) return errorResponse("VALIDATION", "ref is required");
|
|
82
|
+
const input: KnowledgeImpactInput = {};
|
|
83
|
+
for (const [queryName, inputName] of [
|
|
84
|
+
["maxDepth", "maxDepth"],
|
|
85
|
+
["maxNodes", "maxNodes"],
|
|
86
|
+
["maxEdges", "maxEdges"],
|
|
87
|
+
["frontierLimit", "frontierLimit"],
|
|
88
|
+
["visitedLimit", "visitedLimit"],
|
|
89
|
+
] as const) {
|
|
90
|
+
const value = parsePositiveInt(url, queryName);
|
|
91
|
+
if (value instanceof Response) return value;
|
|
92
|
+
if (value !== undefined) input[inputName] = value;
|
|
93
|
+
}
|
|
94
|
+
const result = await analyzeKnowledgeImpact(store, ref, input);
|
|
95
|
+
if (!result.success) {
|
|
96
|
+
return errorResponse(
|
|
97
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
98
|
+
result.error
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return Response.json(result.data);
|
|
102
|
+
}
|
package/src/serve/server.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
handleUpdateDoc,
|
|
70
70
|
handleVerifyConnector,
|
|
71
71
|
} from "./routes/api";
|
|
72
|
+
import { handleChanges, handleDiff, handleImpact } from "./routes/changes";
|
|
72
73
|
import { handleGraph, handleGraphQuery } from "./routes/graph";
|
|
73
74
|
import {
|
|
74
75
|
handleDocBacklinks,
|
|
@@ -968,6 +969,39 @@ export async function startServer(
|
|
|
968
969
|
);
|
|
969
970
|
},
|
|
970
971
|
},
|
|
972
|
+
"/api/changes": {
|
|
973
|
+
GET: async (req: Request) => {
|
|
974
|
+
const url = new URL(req.url);
|
|
975
|
+
return withSecurityHeaders(
|
|
976
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
977
|
+
handleChanges(store, url)
|
|
978
|
+
),
|
|
979
|
+
isDev
|
|
980
|
+
);
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
"/api/diff": {
|
|
984
|
+
GET: async (req: Request) => {
|
|
985
|
+
const url = new URL(req.url);
|
|
986
|
+
return withSecurityHeaders(
|
|
987
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
988
|
+
handleDiff(store, url)
|
|
989
|
+
),
|
|
990
|
+
isDev
|
|
991
|
+
);
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
"/api/impact": {
|
|
995
|
+
GET: async (req: Request) => {
|
|
996
|
+
const url = new URL(req.url);
|
|
997
|
+
return withSecurityHeaders(
|
|
998
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
999
|
+
handleImpact(store, url)
|
|
1000
|
+
),
|
|
1001
|
+
isDev
|
|
1002
|
+
);
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
971
1005
|
},
|
|
972
1006
|
});
|
|
973
1007
|
} catch (e) {
|
|
@@ -31,6 +31,8 @@ export interface CollectionWatchCallbacks {
|
|
|
31
31
|
relPaths: string[];
|
|
32
32
|
error: unknown;
|
|
33
33
|
}) => void;
|
|
34
|
+
/** Fires after all watcher syncs and queued paths have settled. */
|
|
35
|
+
onSettled?: () => void;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
interface CollectionWatchServiceOptions {
|
|
@@ -236,6 +238,13 @@ export class CollectionWatchService {
|
|
|
236
238
|
const remaining = this.#pendingByCollection.get(collectionName);
|
|
237
239
|
if (remaining && remaining.size > 0) {
|
|
238
240
|
void this.#flushCollection(collectionName);
|
|
241
|
+
} else if (
|
|
242
|
+
this.#syncing.size === 0 &&
|
|
243
|
+
![...this.#pendingByCollection.values()].some(
|
|
244
|
+
(relPaths) => relPaths.size > 0
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
this.#callbacks?.onSettled?.();
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
}
|