@gmickel/gno 1.19.0 → 1.21.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 +28 -8
- package/assets/skill/SKILL.md +73 -27
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +1 -1
- package/spec/cli.md +142 -17
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +75 -3
- package/spec/output-schemas/ask.schema.json +198 -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/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -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/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +4 -0
- package/src/app/context-runtime.ts +5 -1
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +40 -149
- 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 +227 -1
- 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/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +33 -21
- package/src/core/context-evidence.ts +6 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/core/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/ingestion/sync.ts +214 -165
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +38 -0
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/types.ts +25 -0
- package/src/sdk/client.ts +77 -2
- 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/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +162 -3
- 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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** Bounded, coalescing resident scheduler for evidence-triggered reverification. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
SavedCapsuleRegistrationRecord,
|
|
5
|
+
StorePort,
|
|
6
|
+
StoreResult,
|
|
7
|
+
} from "../store/types";
|
|
8
|
+
import type {
|
|
9
|
+
SavedCapsuleReverificationDeps,
|
|
10
|
+
SavedCapsuleReverificationOutcome,
|
|
11
|
+
} from "./capsule-reverification";
|
|
12
|
+
|
|
13
|
+
import { SavedCapsuleRegistryError } from "./capsule-registry";
|
|
14
|
+
import { reverifySavedCapsule } from "./capsule-reverification";
|
|
15
|
+
import {
|
|
16
|
+
decodeDocumentChangeCursor,
|
|
17
|
+
encodeDocumentChangeCursor,
|
|
18
|
+
} from "./change-journal";
|
|
19
|
+
|
|
20
|
+
const MAX_REGISTRATIONS_PER_DRAIN = 10_000;
|
|
21
|
+
|
|
22
|
+
type SchedulerStore = StorePort &
|
|
23
|
+
Pick<
|
|
24
|
+
StorePort,
|
|
25
|
+
| "getSavedCapsuleReverificationState"
|
|
26
|
+
| "listDocumentChanges"
|
|
27
|
+
| "listSavedCapsuleIdsAffectedByChanges"
|
|
28
|
+
| "listSavedCapsuleRegistrations"
|
|
29
|
+
| "setSavedCapsuleReverificationSequence"
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export interface SavedCapsuleReverificationDrain {
|
|
33
|
+
fromSequence: number;
|
|
34
|
+
throughSequence: number;
|
|
35
|
+
cursorExpired: boolean;
|
|
36
|
+
affected: number;
|
|
37
|
+
completed: number;
|
|
38
|
+
failed: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SavedCapsuleReverificationSchedulerOptions {
|
|
42
|
+
deps: Omit<SavedCapsuleReverificationDeps, "store"> & {
|
|
43
|
+
store: SchedulerStore & SavedCapsuleReverificationDeps["store"];
|
|
44
|
+
};
|
|
45
|
+
startBackgroundWork: (
|
|
46
|
+
operation: (signal: AbortSignal) => Promise<void>
|
|
47
|
+
) => boolean;
|
|
48
|
+
onDrain?: (result: SavedCapsuleReverificationDrain) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const unwrapStore = <T>(result: StoreResult<T>, operation: string): T => {
|
|
52
|
+
if (result.ok) return result.value;
|
|
53
|
+
throw new SavedCapsuleRegistryError(
|
|
54
|
+
"store_failed",
|
|
55
|
+
`${operation}: ${result.error.message}`,
|
|
56
|
+
result.error.cause
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class SavedCapsuleReverificationScheduler {
|
|
61
|
+
readonly #options: SavedCapsuleReverificationSchedulerOptions;
|
|
62
|
+
#pending = false;
|
|
63
|
+
#running = false;
|
|
64
|
+
#disposed = false;
|
|
65
|
+
|
|
66
|
+
constructor(options: SavedCapsuleReverificationSchedulerOptions) {
|
|
67
|
+
this.#options = options;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
notifySyncSettled(): void {
|
|
71
|
+
if (this.#disposed) return;
|
|
72
|
+
this.#pending = true;
|
|
73
|
+
if (this.#running) return;
|
|
74
|
+
this.#running = true;
|
|
75
|
+
const started = this.#options.startBackgroundWork(async (signal) => {
|
|
76
|
+
await this.#run(signal);
|
|
77
|
+
});
|
|
78
|
+
if (!started) {
|
|
79
|
+
this.#running = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async triggerNow(signal: AbortSignal = new AbortController().signal) {
|
|
84
|
+
if (this.#disposed) return [];
|
|
85
|
+
const results: SavedCapsuleReverificationDrain[] = [];
|
|
86
|
+
do {
|
|
87
|
+
this.#pending = false;
|
|
88
|
+
results.push(await this.#drain(signal));
|
|
89
|
+
} while (this.#pending && !signal.aborted && !this.#disposed);
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async dispose(): Promise<void> {
|
|
94
|
+
this.#disposed = true;
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #run(signal: AbortSignal): Promise<void> {
|
|
99
|
+
const operation = (async () => {
|
|
100
|
+
try {
|
|
101
|
+
while (this.#pending && !signal.aborted && !this.#disposed) {
|
|
102
|
+
this.#pending = false;
|
|
103
|
+
const result = await this.#drain(signal);
|
|
104
|
+
this.#options.onDrain?.(result);
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
this.#running = false;
|
|
108
|
+
if (this.#pending && !this.#disposed) {
|
|
109
|
+
this.notifySyncSettled();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
await operation;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async #drain(signal: AbortSignal): Promise<SavedCapsuleReverificationDrain> {
|
|
117
|
+
const store = this.#options.deps.store;
|
|
118
|
+
const schedulerState = unwrapStore(
|
|
119
|
+
await store.getSavedCapsuleReverificationState(),
|
|
120
|
+
"Failed to read saved Capsule scheduler state"
|
|
121
|
+
);
|
|
122
|
+
const fromSequence = schedulerState.lastProcessedSequence;
|
|
123
|
+
const journal = unwrapStore(
|
|
124
|
+
await store.listDocumentChanges({
|
|
125
|
+
cursor: encodeDocumentChangeCursor(fromSequence),
|
|
126
|
+
limit: 1,
|
|
127
|
+
}),
|
|
128
|
+
"Failed to read document change journal"
|
|
129
|
+
);
|
|
130
|
+
const throughSequence = decodeDocumentChangeCursor(journal.latestCursor);
|
|
131
|
+
if (throughSequence <= fromSequence) {
|
|
132
|
+
return {
|
|
133
|
+
fromSequence,
|
|
134
|
+
throughSequence,
|
|
135
|
+
cursorExpired: journal.cursorExpired,
|
|
136
|
+
affected: 0,
|
|
137
|
+
completed: 0,
|
|
138
|
+
failed: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let registrations: SavedCapsuleRegistrationRecord[];
|
|
143
|
+
if (journal.cursorExpired) {
|
|
144
|
+
registrations = unwrapStore(
|
|
145
|
+
await store.listSavedCapsuleRegistrations(),
|
|
146
|
+
"Failed to list saved Context Capsules"
|
|
147
|
+
);
|
|
148
|
+
if (registrations.length > MAX_REGISTRATIONS_PER_DRAIN) {
|
|
149
|
+
throw new SavedCapsuleRegistryError(
|
|
150
|
+
"store_failed",
|
|
151
|
+
"Saved Capsule scheduler registration bound exceeded"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const affected = unwrapStore(
|
|
156
|
+
await store.listSavedCapsuleIdsAffectedByChanges(
|
|
157
|
+
fromSequence,
|
|
158
|
+
throughSequence,
|
|
159
|
+
MAX_REGISTRATIONS_PER_DRAIN
|
|
160
|
+
),
|
|
161
|
+
"Failed to resolve changed saved Capsule evidence"
|
|
162
|
+
);
|
|
163
|
+
if (affected.truncated) {
|
|
164
|
+
throw new SavedCapsuleRegistryError(
|
|
165
|
+
"store_failed",
|
|
166
|
+
"Saved Capsule scheduler registration bound exceeded"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const all = unwrapStore(
|
|
170
|
+
await store.listSavedCapsuleRegistrations(),
|
|
171
|
+
"Failed to list saved Context Capsules"
|
|
172
|
+
);
|
|
173
|
+
const ids = new Set(affected.registrationIds);
|
|
174
|
+
registrations = all.filter((registration) =>
|
|
175
|
+
ids.has(registration.registrationId)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const outcomes: SavedCapsuleReverificationOutcome[] = [];
|
|
180
|
+
for (const registration of registrations) {
|
|
181
|
+
if (signal.aborted) break;
|
|
182
|
+
if (registration.lastAttemptedSequence >= throughSequence) continue;
|
|
183
|
+
outcomes.push(
|
|
184
|
+
await reverifySavedCapsule(
|
|
185
|
+
registration.registrationId,
|
|
186
|
+
{
|
|
187
|
+
kind: "journal",
|
|
188
|
+
fromSequence,
|
|
189
|
+
throughSequence,
|
|
190
|
+
},
|
|
191
|
+
this.#options.deps
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (!signal.aborted) {
|
|
196
|
+
const advanced = unwrapStore(
|
|
197
|
+
await store.setSavedCapsuleReverificationSequence(
|
|
198
|
+
throughSequence,
|
|
199
|
+
schedulerState.registrationEpoch
|
|
200
|
+
),
|
|
201
|
+
"Failed to advance saved Capsule scheduler state"
|
|
202
|
+
);
|
|
203
|
+
if (!advanced) this.#pending = true;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
fromSequence,
|
|
207
|
+
throughSequence,
|
|
208
|
+
cursorExpired: journal.cursorExpired,
|
|
209
|
+
affected: outcomes.length,
|
|
210
|
+
completed: outcomes.filter(
|
|
211
|
+
(outcome) => outcome.verification.operationStatus === "completed"
|
|
212
|
+
).length,
|
|
213
|
+
failed: outcomes.filter(
|
|
214
|
+
(outcome) => outcome.verification.operationStatus === "failed"
|
|
215
|
+
).length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/** Non-generative reverification for metadata-only saved Capsule records. */
|
|
2
|
+
|
|
3
|
+
import type { ContextCapsuleRuntimeDeps } from "../app/context-runtime";
|
|
4
|
+
import type {
|
|
5
|
+
SavedCapsuleRegistrationRecord,
|
|
6
|
+
SavedCapsuleRegistrationSnapshot,
|
|
7
|
+
SavedCapsuleTriggerKind,
|
|
8
|
+
SavedCapsuleVerificationRecord,
|
|
9
|
+
StorePort,
|
|
10
|
+
StoreResult,
|
|
11
|
+
} from "../store/types";
|
|
12
|
+
import type { ContextCapsuleVerification } from "./context-capsule";
|
|
13
|
+
|
|
14
|
+
import { DEFAULT_INDEX_NAME } from "../app/constants";
|
|
15
|
+
import {
|
|
16
|
+
canonicalVerifiedContextCapsuleJson,
|
|
17
|
+
verifyContextCapsuleRuntime,
|
|
18
|
+
} from "../app/context-runtime";
|
|
19
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
20
|
+
import {
|
|
21
|
+
getSavedCapsule,
|
|
22
|
+
loadSavedCapsuleFile,
|
|
23
|
+
SavedCapsuleRegistryError,
|
|
24
|
+
} from "./capsule-registry";
|
|
25
|
+
import { decodeDocumentChangeCursor } from "./change-journal";
|
|
26
|
+
import { sha256Text } from "./context-capsule-validation";
|
|
27
|
+
|
|
28
|
+
type ReverificationStore = StorePort &
|
|
29
|
+
ContextCapsuleRuntimeDeps["store"] &
|
|
30
|
+
Pick<
|
|
31
|
+
StorePort,
|
|
32
|
+
| "getSavedCapsuleRegistration"
|
|
33
|
+
| "getSavedCapsuleRegistrationSnapshot"
|
|
34
|
+
| "listDocumentChanges"
|
|
35
|
+
| "upsertSavedCapsuleVerification"
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
const MAX_REGISTRATION_CONFLICT_ATTEMPTS = 2;
|
|
39
|
+
|
|
40
|
+
export interface SavedCapsuleReverificationNotification {
|
|
41
|
+
type: "capsule-reverified";
|
|
42
|
+
registrationId: string;
|
|
43
|
+
capsuleId: string;
|
|
44
|
+
operationStatus: "completed" | "failed";
|
|
45
|
+
affectedQuestionState: "unaffected" | "affected" | "unknown";
|
|
46
|
+
changedAt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SavedCapsuleReverificationOutcome {
|
|
50
|
+
registration: SavedCapsuleRegistrationRecord;
|
|
51
|
+
verification: SavedCapsuleVerificationRecord;
|
|
52
|
+
receipt: ContextCapsuleVerification | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SavedCapsuleReverificationDeps extends Omit<
|
|
56
|
+
ContextCapsuleRuntimeDeps,
|
|
57
|
+
"store" | "indexName"
|
|
58
|
+
> {
|
|
59
|
+
store: ReverificationStore;
|
|
60
|
+
indexName: string;
|
|
61
|
+
now?: () => number;
|
|
62
|
+
notify?: (event: SavedCapsuleReverificationNotification) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SavedCapsuleReverificationTrigger {
|
|
66
|
+
kind: SavedCapsuleTriggerKind;
|
|
67
|
+
fromSequence: number;
|
|
68
|
+
throughSequence: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const unwrapStore = <T>(result: StoreResult<T>, operation: string): T => {
|
|
72
|
+
if (result.ok) return result.value;
|
|
73
|
+
throw new SavedCapsuleRegistryError(
|
|
74
|
+
"store_failed",
|
|
75
|
+
`${operation}: ${result.error.message}`,
|
|
76
|
+
result.error.cause
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const currentSequence = async (store: ReverificationStore): Promise<number> => {
|
|
81
|
+
const page = unwrapStore(
|
|
82
|
+
await store.listDocumentChanges({ limit: 1 }),
|
|
83
|
+
"Failed to read document change journal"
|
|
84
|
+
);
|
|
85
|
+
return decodeDocumentChangeCursor(page.latestCursor);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getVerificationSnapshot = async (
|
|
89
|
+
store: ReverificationStore,
|
|
90
|
+
registrationId: string
|
|
91
|
+
): Promise<SavedCapsuleRegistrationSnapshot> => {
|
|
92
|
+
const snapshot = unwrapStore(
|
|
93
|
+
await store.getSavedCapsuleRegistrationSnapshot(registrationId),
|
|
94
|
+
"Failed to read saved Context Capsule verification snapshot"
|
|
95
|
+
);
|
|
96
|
+
if (!snapshot) {
|
|
97
|
+
throw new SavedCapsuleRegistryError(
|
|
98
|
+
"registration_not_found",
|
|
99
|
+
"Saved Context Capsule registration not found"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return snapshot;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const affected = (
|
|
106
|
+
receipt: ContextCapsuleVerification
|
|
107
|
+
): {
|
|
108
|
+
state: "unaffected" | "affected";
|
|
109
|
+
reasons: string[];
|
|
110
|
+
} => {
|
|
111
|
+
const reasons: string[] = [];
|
|
112
|
+
if (receipt.contentStatus === "stale") reasons.push("content_stale");
|
|
113
|
+
if (receipt.contentStatus === "missing") reasons.push("content_missing");
|
|
114
|
+
if (receipt.rankingStatus === "reranked") reasons.push("ranking_changed");
|
|
115
|
+
if (receipt.fingerprintStatus === "drifted") {
|
|
116
|
+
reasons.push("fingerprint_changed");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
state: reasons.length === 0 ? "unaffected" : "affected",
|
|
120
|
+
reasons,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const errorIdentity = (error: unknown): { code: string; message: string } => {
|
|
125
|
+
if (
|
|
126
|
+
error instanceof Error &&
|
|
127
|
+
"code" in error &&
|
|
128
|
+
typeof error.code === "string"
|
|
129
|
+
) {
|
|
130
|
+
return { code: error.code, message: error.message.slice(0, 4096) };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
code: "verification_failed",
|
|
134
|
+
message: (error instanceof Error
|
|
135
|
+
? error.message
|
|
136
|
+
: "Saved Capsule verification failed"
|
|
137
|
+
).slice(0, 4096),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const persist = async (
|
|
142
|
+
deps: SavedCapsuleReverificationDeps,
|
|
143
|
+
registration: SavedCapsuleRegistrationRecord,
|
|
144
|
+
registrationGeneration: number,
|
|
145
|
+
verification: SavedCapsuleVerificationRecord
|
|
146
|
+
): Promise<boolean> => {
|
|
147
|
+
const persisted = unwrapStore(
|
|
148
|
+
await deps.store.upsertSavedCapsuleVerification(verification, {
|
|
149
|
+
registrationGeneration,
|
|
150
|
+
}),
|
|
151
|
+
"Failed to persist saved Context Capsule verification"
|
|
152
|
+
);
|
|
153
|
+
if (!persisted) return false;
|
|
154
|
+
if (registration.notificationPreference === "local") {
|
|
155
|
+
deps.notify?.({
|
|
156
|
+
type: "capsule-reverified",
|
|
157
|
+
registrationId: registration.registrationId,
|
|
158
|
+
capsuleId: registration.capsuleId,
|
|
159
|
+
operationStatus: verification.operationStatus,
|
|
160
|
+
affectedQuestionState: verification.affectedQuestionState,
|
|
161
|
+
changedAt: new Date(verification.verifiedAtMs).toISOString(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const reverifySavedCapsuleAttempt = async (
|
|
168
|
+
registrationId: string,
|
|
169
|
+
trigger: SavedCapsuleReverificationTrigger,
|
|
170
|
+
deps: SavedCapsuleReverificationDeps
|
|
171
|
+
): Promise<SavedCapsuleReverificationOutcome | null> => {
|
|
172
|
+
const snapshot = await getVerificationSnapshot(deps.store, registrationId);
|
|
173
|
+
const registration = snapshot.registration;
|
|
174
|
+
const runtimeIndex = canonicalizeIndexName(
|
|
175
|
+
deps.indexName || DEFAULT_INDEX_NAME
|
|
176
|
+
);
|
|
177
|
+
const verifiedAtMs = (deps.now ?? Date.now)();
|
|
178
|
+
let receipt: ContextCapsuleVerification | null = null;
|
|
179
|
+
let verification: SavedCapsuleVerificationRecord;
|
|
180
|
+
try {
|
|
181
|
+
if (runtimeIndex !== registration.indexName) {
|
|
182
|
+
throw Object.assign(
|
|
183
|
+
new Error(
|
|
184
|
+
`Saved Context Capsule index ${registration.indexName} does not match runtime index ${runtimeIndex}`
|
|
185
|
+
),
|
|
186
|
+
{ code: "invalid_filter" }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const loaded = await loadSavedCapsuleFile(
|
|
190
|
+
registration.filePath,
|
|
191
|
+
registration.fileHash
|
|
192
|
+
);
|
|
193
|
+
if (loaded.capsule.capsuleId !== registration.capsuleId) {
|
|
194
|
+
throw Object.assign(
|
|
195
|
+
new Error("Saved Context Capsule file changed after registration"),
|
|
196
|
+
{ code: "capsule_file_changed" }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
receipt = await verifyContextCapsuleRuntime(loaded.capsule, {
|
|
200
|
+
...deps,
|
|
201
|
+
store: deps.store,
|
|
202
|
+
indexName: runtimeIndex,
|
|
203
|
+
});
|
|
204
|
+
const receiptJson = canonicalVerifiedContextCapsuleJson(receipt);
|
|
205
|
+
const projection = affected(receipt);
|
|
206
|
+
verification = {
|
|
207
|
+
registrationId,
|
|
208
|
+
triggerKind: trigger.kind,
|
|
209
|
+
fromSequence: trigger.fromSequence,
|
|
210
|
+
throughSequence: trigger.throughSequence,
|
|
211
|
+
operationStatus: "completed",
|
|
212
|
+
affectedQuestionState: projection.state,
|
|
213
|
+
affectedReasons: projection.reasons,
|
|
214
|
+
receiptJson,
|
|
215
|
+
receiptHash: sha256Text(receiptJson),
|
|
216
|
+
errorCode: null,
|
|
217
|
+
errorMessage: null,
|
|
218
|
+
verifiedAtMs,
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const failure = errorIdentity(error);
|
|
222
|
+
verification = {
|
|
223
|
+
registrationId,
|
|
224
|
+
triggerKind: trigger.kind,
|
|
225
|
+
fromSequence: trigger.fromSequence,
|
|
226
|
+
throughSequence: trigger.throughSequence,
|
|
227
|
+
operationStatus: "failed",
|
|
228
|
+
affectedQuestionState: "unknown",
|
|
229
|
+
affectedReasons: [],
|
|
230
|
+
receiptJson: null,
|
|
231
|
+
receiptHash: null,
|
|
232
|
+
errorCode: failure.code,
|
|
233
|
+
errorMessage: failure.message,
|
|
234
|
+
verifiedAtMs,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
!(await persist(
|
|
239
|
+
deps,
|
|
240
|
+
registration,
|
|
241
|
+
snapshot.registrationGeneration,
|
|
242
|
+
verification
|
|
243
|
+
))
|
|
244
|
+
) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const refreshed = await getSavedCapsule(deps.store, registrationId);
|
|
248
|
+
return { registration: refreshed, verification, receipt };
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const reverifySavedCapsule = async (
|
|
252
|
+
registrationId: string,
|
|
253
|
+
trigger: SavedCapsuleReverificationTrigger,
|
|
254
|
+
deps: SavedCapsuleReverificationDeps
|
|
255
|
+
): Promise<SavedCapsuleReverificationOutcome> => {
|
|
256
|
+
for (
|
|
257
|
+
let attempt = 0;
|
|
258
|
+
attempt < MAX_REGISTRATION_CONFLICT_ATTEMPTS;
|
|
259
|
+
attempt += 1
|
|
260
|
+
) {
|
|
261
|
+
const outcome = await reverifySavedCapsuleAttempt(
|
|
262
|
+
registrationId,
|
|
263
|
+
trigger,
|
|
264
|
+
deps
|
|
265
|
+
);
|
|
266
|
+
if (outcome) return outcome;
|
|
267
|
+
}
|
|
268
|
+
throw new SavedCapsuleRegistryError(
|
|
269
|
+
"store_failed",
|
|
270
|
+
"Saved Context Capsule registration changed repeatedly during verification"
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const reverifySavedCapsuleManually = async (
|
|
275
|
+
registrationId: string,
|
|
276
|
+
deps: SavedCapsuleReverificationDeps
|
|
277
|
+
): Promise<SavedCapsuleReverificationOutcome> => {
|
|
278
|
+
const sequence = await currentSequence(deps.store);
|
|
279
|
+
const registration = await getSavedCapsule(deps.store, registrationId);
|
|
280
|
+
return reverifySavedCapsule(
|
|
281
|
+
registrationId,
|
|
282
|
+
{
|
|
283
|
+
kind: "manual",
|
|
284
|
+
fromSequence: registration.lastAttemptedSequence,
|
|
285
|
+
throughSequence: sequence,
|
|
286
|
+
},
|
|
287
|
+
deps
|
|
288
|
+
);
|
|
289
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, content-free structural snapshots for document change journaling.
|
|
3
|
+
*
|
|
4
|
+
* The snapshot values are normalized summaries only. Source bodies are used
|
|
5
|
+
* transiently during sync and are never included in a journal delta.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
DocumentChangeStructureDelta,
|
|
10
|
+
DocumentChangeSet,
|
|
11
|
+
} from "../store/types";
|
|
12
|
+
|
|
13
|
+
import { parseFrontmatter } from "../ingestion/frontmatter";
|
|
14
|
+
import { buildLineOffsets } from "../ingestion/position";
|
|
15
|
+
import { getExcludedRanges } from "../ingestion/strip";
|
|
16
|
+
import { normalizeMarkdownPath, normalizeWikiName, parseLinks } from "./links";
|
|
17
|
+
import { extractSections } from "./sections";
|
|
18
|
+
|
|
19
|
+
export type RelationMap = Record<string, string[]>;
|
|
20
|
+
|
|
21
|
+
export interface DocumentStructureSnapshot {
|
|
22
|
+
headings: string[];
|
|
23
|
+
links: string[];
|
|
24
|
+
typedEdges: string[];
|
|
25
|
+
dates: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DocumentStructureDeltaResult {
|
|
29
|
+
delta: DocumentChangeStructureDelta;
|
|
30
|
+
history: "available" | "unavailable";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const RELATION_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
34
|
+
|
|
35
|
+
export function isRelationMap(value: unknown): value is RelationMap {
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return Object.values(value).every(
|
|
40
|
+
(targets) =>
|
|
41
|
+
Array.isArray(targets) &&
|
|
42
|
+
targets.every((target) => typeof target === "string")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeRelationTarget(raw: string): string {
|
|
47
|
+
const trimmed = raw.trim();
|
|
48
|
+
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) {
|
|
49
|
+
return trimmed.slice(2, -2).split("|")[0]?.trim() ?? "";
|
|
50
|
+
}
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeRelationEdgeType(raw: string): string {
|
|
55
|
+
return raw
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[-\s]+/g, "_");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const withOccurrences = (values: readonly string[]): string[] => {
|
|
62
|
+
const counts = new Map<string, number>();
|
|
63
|
+
return values
|
|
64
|
+
.map((value) => {
|
|
65
|
+
const count = (counts.get(value) ?? 0) + 1;
|
|
66
|
+
counts.set(value, count);
|
|
67
|
+
return count === 1 ? value : `${value} [${count}]`;
|
|
68
|
+
})
|
|
69
|
+
.sort();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const linkSummary = (
|
|
73
|
+
link: ReturnType<typeof parseLinks>[number],
|
|
74
|
+
relPath: string
|
|
75
|
+
): string | null => {
|
|
76
|
+
const anchor = link.targetAnchor
|
|
77
|
+
? `#${normalizeWikiName(link.targetAnchor)}`
|
|
78
|
+
: "";
|
|
79
|
+
if (link.kind === "wiki") {
|
|
80
|
+
const collection = link.targetCollection
|
|
81
|
+
? `${normalizeWikiName(link.targetCollection)}:`
|
|
82
|
+
: "";
|
|
83
|
+
return `wiki:${collection}${normalizeWikiName(link.targetRef)}${anchor}`;
|
|
84
|
+
}
|
|
85
|
+
const target = normalizeMarkdownPath(link.targetRef, relPath);
|
|
86
|
+
return target ? `markdown:${target}${anchor}` : null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const extractTypedEdges = (markdown: string): string[] => {
|
|
90
|
+
const relations = parseFrontmatter(markdown).metadata.relations;
|
|
91
|
+
if (!isRelationMap(relations)) return [];
|
|
92
|
+
|
|
93
|
+
const values: string[] = [];
|
|
94
|
+
for (const [rawEdgeType, targets] of Object.entries(relations)) {
|
|
95
|
+
const edgeType = normalizeRelationEdgeType(rawEdgeType);
|
|
96
|
+
if (!RELATION_EDGE_TYPE_PATTERN.test(edgeType)) continue;
|
|
97
|
+
for (const rawTarget of targets) {
|
|
98
|
+
const target = normalizeRelationTarget(rawTarget);
|
|
99
|
+
if (target) {
|
|
100
|
+
values.push(`${edgeType}:${normalizeWikiName(target)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...new Set(values)].sort();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const extractDocumentStructure = (
|
|
108
|
+
markdown: string,
|
|
109
|
+
relPath: string,
|
|
110
|
+
dateFields: Readonly<Record<string, string>> | null | undefined
|
|
111
|
+
): DocumentStructureSnapshot => {
|
|
112
|
+
const excludedRanges = getExcludedRanges(markdown);
|
|
113
|
+
const links = parseLinks(markdown, buildLineOffsets(markdown), excludedRanges)
|
|
114
|
+
.map((link) => linkSummary(link, relPath))
|
|
115
|
+
.filter((value): value is string => value !== null);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
headings: withOccurrences(
|
|
119
|
+
extractSections(markdown).map(
|
|
120
|
+
({ level, title }) => `${"#".repeat(level)} ${title.normalize("NFC")}`
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
links: withOccurrences(links),
|
|
124
|
+
typedEdges: extractTypedEdges(markdown),
|
|
125
|
+
dates: { ...dateFields },
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const diffValues = (
|
|
130
|
+
previous: readonly string[],
|
|
131
|
+
next: readonly string[]
|
|
132
|
+
): DocumentChangeSet => {
|
|
133
|
+
const previousValues = new Set(previous);
|
|
134
|
+
const nextValues = new Set(next);
|
|
135
|
+
return {
|
|
136
|
+
added: next.filter((value) => !previousValues.has(value)),
|
|
137
|
+
removed: previous.filter((value) => !nextValues.has(value)),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const diffDocumentStructure = (
|
|
142
|
+
previous: DocumentStructureSnapshot | null | undefined,
|
|
143
|
+
next: DocumentStructureSnapshot
|
|
144
|
+
): DocumentStructureDeltaResult => {
|
|
145
|
+
if (previous === undefined) {
|
|
146
|
+
return {
|
|
147
|
+
history: "unavailable",
|
|
148
|
+
delta: {
|
|
149
|
+
headings: { added: [], removed: [] },
|
|
150
|
+
links: { added: [], removed: [] },
|
|
151
|
+
typedEdges: { added: [], removed: [] },
|
|
152
|
+
dates: { added: [], removed: [], changed: [] },
|
|
153
|
+
truncated: true,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const prior = previous ?? {
|
|
159
|
+
headings: [],
|
|
160
|
+
links: [],
|
|
161
|
+
typedEdges: [],
|
|
162
|
+
dates: {},
|
|
163
|
+
};
|
|
164
|
+
const priorDateKeys = Object.keys(prior.dates).sort();
|
|
165
|
+
const nextDateKeys = Object.keys(next.dates).sort();
|
|
166
|
+
const dates = diffValues(priorDateKeys, nextDateKeys);
|
|
167
|
+
const changed = priorDateKeys.filter(
|
|
168
|
+
(key) =>
|
|
169
|
+
Object.hasOwn(next.dates, key) && prior.dates[key] !== next.dates[key]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
history: "available",
|
|
174
|
+
delta: {
|
|
175
|
+
headings: diffValues(prior.headings, next.headings),
|
|
176
|
+
links: diffValues(prior.links, next.links),
|
|
177
|
+
typedEdges: diffValues(prior.typedEdges, next.typedEdges),
|
|
178
|
+
dates: { ...dates, changed },
|
|
179
|
+
truncated: false,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|