@gmickel/gno 1.12.3 → 1.13.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 +57 -30
- package/assets/skill/SKILL.md +6 -1
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/context-resolver.ts +285 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/index.ts +3 -3
- package/src/pipeline/answer-prompt.ts +80 -0
- package/src/pipeline/answer.ts +12 -26
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/result-context.ts +51 -0
- package/src/pipeline/search.ts +5 -1
- package/src/pipeline/vsearch.ts +2 -0
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +320 -53
- package/src/store/types.ts +124 -0
package/src/serve/status.ts
CHANGED
|
@@ -13,9 +13,18 @@ import type {
|
|
|
13
13
|
} from "./status-model";
|
|
14
14
|
|
|
15
15
|
import { getModelsCachePath } from "../app/constants";
|
|
16
|
+
import {
|
|
17
|
+
type ActivationStatus,
|
|
18
|
+
buildActivationStatus,
|
|
19
|
+
} from "../core/activation-status";
|
|
16
20
|
import { ModelCache } from "../llm/cache";
|
|
17
21
|
import { envIsSet, resolveDownloadPolicy } from "../llm/policy";
|
|
18
22
|
import { getActivePreset, resolveModelUri } from "../llm/registry";
|
|
23
|
+
import {
|
|
24
|
+
buildActivationCheck,
|
|
25
|
+
buildConnectorActivationCheck,
|
|
26
|
+
} from "./activation-health";
|
|
27
|
+
import { getConnectorVerificationTargets } from "./connectors";
|
|
19
28
|
import { downloadState, type ServerContext } from "./context";
|
|
20
29
|
|
|
21
30
|
const GIGABYTE = 1024 * 1024 * 1024;
|
|
@@ -60,6 +69,8 @@ export interface StatusBuildDeps {
|
|
|
60
69
|
inspectDisk?: (path: string) => Promise<DiskSnapshot | null>;
|
|
61
70
|
isModelCached?: (uri: string) => Promise<boolean>;
|
|
62
71
|
listSuggestedCollections?: () => Promise<SuggestedCollection[]>;
|
|
72
|
+
buildActivation?: typeof buildActivationStatus;
|
|
73
|
+
listConnectorTargets?: typeof getConnectorVerificationTargets;
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
function formatBytes(bytes: number): string {
|
|
@@ -490,6 +501,7 @@ async function buildBootstrapState(
|
|
|
490
501
|
|
|
491
502
|
const cachedCount = modelEntries.filter((entry) => entry.cached).length;
|
|
492
503
|
const totalCount = modelEntries.length;
|
|
504
|
+
const totalSizeBytes = await cache.totalSize();
|
|
493
505
|
|
|
494
506
|
const policySummary = policy.offline
|
|
495
507
|
? "Offline mode. Cached models only."
|
|
@@ -517,8 +529,8 @@ async function buildBootstrapState(
|
|
|
517
529
|
},
|
|
518
530
|
cache: {
|
|
519
531
|
path: cache.dir,
|
|
520
|
-
totalSizeBytes
|
|
521
|
-
totalSizeLabel: formatBytes(
|
|
532
|
+
totalSizeBytes,
|
|
533
|
+
totalSizeLabel: formatBytes(totalSizeBytes),
|
|
522
534
|
},
|
|
523
535
|
models: {
|
|
524
536
|
activePresetId: preset.id,
|
|
@@ -540,12 +552,23 @@ function buildOnboarding(
|
|
|
540
552
|
status: IndexStatus,
|
|
541
553
|
modelCheck: HealthCheck,
|
|
542
554
|
suggestions: SuggestedCollection[],
|
|
543
|
-
presetName: string
|
|
555
|
+
presetName: string,
|
|
556
|
+
activation: ActivationStatus
|
|
544
557
|
): AppStatusResponse["onboarding"] {
|
|
545
|
-
const foldersReady =
|
|
558
|
+
const foldersReady = activation.collections.length > 0;
|
|
546
559
|
const modelsReady = modelCheck.status === "ok";
|
|
547
|
-
const indexedReady =
|
|
548
|
-
|
|
560
|
+
const indexedReady = activation.healthy;
|
|
561
|
+
const semanticStates = [
|
|
562
|
+
...new Set(
|
|
563
|
+
activation.collections.map(
|
|
564
|
+
({ semanticAvailability }) => semanticAvailability.code
|
|
565
|
+
)
|
|
566
|
+
),
|
|
567
|
+
];
|
|
568
|
+
const failedActivation = activation.collections.find(({ ready }) => !ready);
|
|
569
|
+
const activationDetail = failedActivation?.remediation
|
|
570
|
+
? `${failedActivation.collection}: ${failedActivation.remediation.stage}/${failedActivation.remediation.code}. Run: ${failedActivation.remediation.command}`
|
|
571
|
+
: "Run the first sync to populate a searchable lexical index.";
|
|
549
572
|
|
|
550
573
|
const steps = [
|
|
551
574
|
{
|
|
@@ -555,6 +578,7 @@ function buildOnboarding(
|
|
|
555
578
|
detail: foldersReady
|
|
556
579
|
? `${summarizeCount(status.collections.length, "folder")} connected.`
|
|
557
580
|
: "Choose the folders you want GNO to watch and index.",
|
|
581
|
+
action: "add-collection",
|
|
558
582
|
},
|
|
559
583
|
{
|
|
560
584
|
id: "preset",
|
|
@@ -567,21 +591,20 @@ function buildOnboarding(
|
|
|
567
591
|
{
|
|
568
592
|
id: "models",
|
|
569
593
|
title: "Prepare local models",
|
|
570
|
-
status:
|
|
594
|
+
status: "upcoming",
|
|
571
595
|
detail: modelCheck.detail,
|
|
596
|
+
action: "download-models",
|
|
572
597
|
},
|
|
573
598
|
] satisfies AppStatusResponse["onboarding"]["steps"])
|
|
574
599
|
: []),
|
|
575
600
|
{
|
|
576
601
|
id: "indexing",
|
|
577
|
-
title: "
|
|
602
|
+
title: "Prove lexical retrieval",
|
|
578
603
|
status: indexedReady ? "complete" : foldersReady ? "current" : "upcoming",
|
|
579
|
-
detail:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
? `${summarizeCount(status.activeDocuments, "document")} indexed so far.`
|
|
584
|
-
: "Run the first sync to scan your folders and build the search index.",
|
|
604
|
+
detail: indexedReady
|
|
605
|
+
? `${summarizeCount(activation.collections.length, "folder")} passed a corpus-derived lexical proof.`
|
|
606
|
+
: activationDetail,
|
|
607
|
+
action: "sync",
|
|
585
608
|
},
|
|
586
609
|
] satisfies AppStatusResponse["onboarding"]["steps"];
|
|
587
610
|
|
|
@@ -597,28 +620,14 @@ function buildOnboarding(
|
|
|
597
620
|
};
|
|
598
621
|
}
|
|
599
622
|
|
|
600
|
-
if (!modelsReady) {
|
|
601
|
-
return {
|
|
602
|
-
ready: false,
|
|
603
|
-
stage: "models",
|
|
604
|
-
headline: "Your folders are connected. Finish model setup next",
|
|
605
|
-
detail: modelCheck.detail,
|
|
606
|
-
suggestedCollections: suggestions,
|
|
607
|
-
steps,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
623
|
if (!indexedReady) {
|
|
612
624
|
return {
|
|
613
625
|
ready: false,
|
|
614
626
|
stage: "indexing",
|
|
615
|
-
headline:
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
: status.activeDocuments > 0
|
|
620
|
-
? "The first sync started. Let embeddings finish so semantic search and answers can fully light up."
|
|
621
|
-
: "Run the first sync to populate the index from the folders you connected.",
|
|
627
|
+
headline: activation.usable
|
|
628
|
+
? "Search works in some folders. Repair the remaining retrieval proof"
|
|
629
|
+
: "Finish the first lexical retrieval proof",
|
|
630
|
+
detail: activationDetail,
|
|
622
631
|
suggestedCollections: suggestions,
|
|
623
632
|
steps,
|
|
624
633
|
};
|
|
@@ -628,7 +637,9 @@ function buildOnboarding(
|
|
|
628
637
|
ready: true,
|
|
629
638
|
stage: "ready",
|
|
630
639
|
headline: "Workspace ready",
|
|
631
|
-
detail:
|
|
640
|
+
detail: modelsReady
|
|
641
|
+
? `Every folder passed lexical retrieval. Local model files are cached; semantic retrieval remains separate (${semanticStates.join(", ")}).`
|
|
642
|
+
: `Every folder passed lexical retrieval. Semantic retrieval remains an optional next step (${semanticStates.join(", ")}).`,
|
|
632
643
|
suggestedCollections: suggestions,
|
|
633
644
|
steps,
|
|
634
645
|
};
|
|
@@ -647,17 +658,43 @@ export async function buildAppStatus(
|
|
|
647
658
|
|
|
648
659
|
const status = result.value;
|
|
649
660
|
const preset = getActivePreset(ctx.config);
|
|
650
|
-
const
|
|
661
|
+
const cache = new ModelCache(getModelsCachePath());
|
|
662
|
+
const isModelCached =
|
|
663
|
+
deps.isModelCached ?? ((uri: string) => cache.isCached(uri));
|
|
664
|
+
const [
|
|
665
|
+
modelCheck,
|
|
666
|
+
diskCheck,
|
|
667
|
+
suggestions,
|
|
668
|
+
bootstrap,
|
|
669
|
+
embedCached,
|
|
670
|
+
connectorTargets,
|
|
671
|
+
] = await Promise.all([
|
|
651
672
|
buildModelCheck(ctx, deps),
|
|
652
673
|
buildDiskCheck(status, deps),
|
|
653
674
|
deps.listSuggestedCollections?.() ?? listSuggestedCollections(),
|
|
654
675
|
buildBootstrapState(ctx),
|
|
676
|
+
isModelCached(preset.embed),
|
|
677
|
+
(deps.listConnectorTargets ?? getConnectorVerificationTargets)(),
|
|
655
678
|
]);
|
|
679
|
+
const activation = await (deps.buildActivation ?? buildActivationStatus)(
|
|
680
|
+
ctx.store,
|
|
681
|
+
ctx.config.collections.map(({ name }) => name),
|
|
682
|
+
{
|
|
683
|
+
semantic: {
|
|
684
|
+
modelsCached: embedCached,
|
|
685
|
+
embeddingBacklog: status.embeddingBacklog,
|
|
686
|
+
vectorAvailable: ctx.capabilities.vector,
|
|
687
|
+
},
|
|
688
|
+
connectorTargets,
|
|
689
|
+
}
|
|
690
|
+
);
|
|
656
691
|
|
|
657
692
|
const backgroundCheck = buildBackgroundCheck(ctx, status);
|
|
658
693
|
const checks = [
|
|
659
694
|
buildCollectionCheck(status),
|
|
660
695
|
buildIndexingCheck(status),
|
|
696
|
+
buildActivationCheck(activation),
|
|
697
|
+
buildConnectorActivationCheck(activation),
|
|
661
698
|
modelCheck,
|
|
662
699
|
buildVectorCheck(ctx),
|
|
663
700
|
diskCheck,
|
|
@@ -698,7 +735,14 @@ export async function buildAppStatus(
|
|
|
698
735
|
name: preset.name,
|
|
699
736
|
},
|
|
700
737
|
capabilities: ctx.capabilities,
|
|
701
|
-
|
|
738
|
+
activation,
|
|
739
|
+
onboarding: buildOnboarding(
|
|
740
|
+
status,
|
|
741
|
+
modelCheck,
|
|
742
|
+
suggestions,
|
|
743
|
+
preset.name,
|
|
744
|
+
activation
|
|
745
|
+
),
|
|
702
746
|
health: {
|
|
703
747
|
state: healthState,
|
|
704
748
|
summary: healthSummary,
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationStageReceipt,
|
|
3
|
+
ActivationVerificationReceipt,
|
|
4
|
+
} from "./types";
|
|
5
|
+
|
|
6
|
+
const ACTIVATION_RECEIPT_MAX_BYTES = 16_384;
|
|
7
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
8
|
+
const RESULT_URI_PATTERN = /^gno:\/\/[^/]+\/.+/;
|
|
9
|
+
const CONNECTOR_TARGET_PATTERN =
|
|
10
|
+
/^(?:mcp|skill):[a-z0-9][a-z0-9._-]{0,63}:(?:user|project):[a-f0-9]{64}$/;
|
|
11
|
+
const DATE_TIME_PATTERN =
|
|
12
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/;
|
|
13
|
+
const INDEX_FAILURE_CODES = new Set(["no_documents", "index_out_of_sync"]);
|
|
14
|
+
const LEXICAL_FAILURE_CODES = new Set([
|
|
15
|
+
"no_probe_term",
|
|
16
|
+
"index_query_failed",
|
|
17
|
+
"retrieval_mismatch",
|
|
18
|
+
]);
|
|
19
|
+
const CONNECTOR_SKIPPED_CODES = new Set([
|
|
20
|
+
"connector_not_configured",
|
|
21
|
+
"connector_probe_unavailable",
|
|
22
|
+
"target_runtime_unverifiable",
|
|
23
|
+
]);
|
|
24
|
+
const CONNECTOR_FAILURE_CODES = new Set([
|
|
25
|
+
"connector_probe_unavailable",
|
|
26
|
+
"connector_unsupported_config",
|
|
27
|
+
"connector_start_failed",
|
|
28
|
+
"connector_timeout",
|
|
29
|
+
"connector_missing_tools",
|
|
30
|
+
"connector_status_failed",
|
|
31
|
+
"connector_search_failed",
|
|
32
|
+
"connector_result_mismatch",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function projectStage(stage: ActivationStageReceipt): ActivationStageReceipt {
|
|
36
|
+
return {
|
|
37
|
+
status: stage.status,
|
|
38
|
+
startedAt: stage.startedAt,
|
|
39
|
+
completedAt: stage.completedAt,
|
|
40
|
+
latencyMs: stage.latencyMs,
|
|
41
|
+
...(stage.code ? { code: stage.code } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function projectReceipt(
|
|
46
|
+
receipt: ActivationVerificationReceipt
|
|
47
|
+
): ActivationVerificationReceipt {
|
|
48
|
+
return {
|
|
49
|
+
schemaVersion: "1.0",
|
|
50
|
+
collection: receipt.collection,
|
|
51
|
+
fingerprint: receipt.fingerprint,
|
|
52
|
+
ready: receipt.ready,
|
|
53
|
+
generatedAt: receipt.generatedAt,
|
|
54
|
+
stages: {
|
|
55
|
+
index: projectStage(receipt.stages.index),
|
|
56
|
+
lexical: projectStage(receipt.stages.lexical),
|
|
57
|
+
semantic: projectStage(receipt.stages.semantic),
|
|
58
|
+
connector: projectStage(receipt.stages.connector),
|
|
59
|
+
},
|
|
60
|
+
evidence: {
|
|
61
|
+
...(receipt.evidence.probeHash
|
|
62
|
+
? { probeHash: receipt.evidence.probeHash }
|
|
63
|
+
: {}),
|
|
64
|
+
...(receipt.evidence.resultUri
|
|
65
|
+
? { resultUri: receipt.evidence.resultUri }
|
|
66
|
+
: {}),
|
|
67
|
+
...(receipt.evidence.resultSourceHash
|
|
68
|
+
? { resultSourceHash: receipt.evidence.resultSourceHash }
|
|
69
|
+
: {}),
|
|
70
|
+
...(receipt.evidence.connectorTarget
|
|
71
|
+
? { connectorTarget: receipt.evidence.connectorTarget }
|
|
72
|
+
: {}),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasOnlyKeys(value: Record<string, unknown>, allowed: Set<string>) {
|
|
78
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDateTime(value: unknown): value is string {
|
|
82
|
+
if (typeof value !== "string") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const match = DATE_TIME_PATTERN.exec(value);
|
|
86
|
+
if (!match) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText] =
|
|
90
|
+
match;
|
|
91
|
+
const year = Number(yearText);
|
|
92
|
+
const month = Number(monthText);
|
|
93
|
+
const day = Number(dayText);
|
|
94
|
+
const hour = Number(hourText);
|
|
95
|
+
const minute = Number(minuteText);
|
|
96
|
+
const second = Number(secondText);
|
|
97
|
+
const offsetHour = Number(match[7] ?? 0);
|
|
98
|
+
const offsetMinute = Number(match[8] ?? 0);
|
|
99
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
100
|
+
const daysInMonth = [
|
|
101
|
+
31,
|
|
102
|
+
leapYear ? 29 : 28,
|
|
103
|
+
31,
|
|
104
|
+
30,
|
|
105
|
+
31,
|
|
106
|
+
30,
|
|
107
|
+
31,
|
|
108
|
+
31,
|
|
109
|
+
30,
|
|
110
|
+
31,
|
|
111
|
+
30,
|
|
112
|
+
31,
|
|
113
|
+
];
|
|
114
|
+
return (
|
|
115
|
+
month >= 1 &&
|
|
116
|
+
month <= 12 &&
|
|
117
|
+
day >= 1 &&
|
|
118
|
+
day <= (daysInMonth[month - 1] ?? 0) &&
|
|
119
|
+
hour <= 23 &&
|
|
120
|
+
minute <= 59 &&
|
|
121
|
+
second <= 59 &&
|
|
122
|
+
offsetHour <= 23 &&
|
|
123
|
+
offsetMinute <= 59
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isNullableDateTime(value: unknown): value is string | null {
|
|
128
|
+
return value === null || isDateTime(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isStageShape(value: unknown): value is ActivationStageReceipt {
|
|
132
|
+
if (!value || typeof value !== "object") {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const stage = value as Record<string, unknown>;
|
|
136
|
+
return (
|
|
137
|
+
hasOnlyKeys(
|
|
138
|
+
stage,
|
|
139
|
+
new Set(["status", "startedAt", "completedAt", "latencyMs", "code"])
|
|
140
|
+
) &&
|
|
141
|
+
typeof stage.status === "string" &&
|
|
142
|
+
isNullableDateTime(stage.startedAt) &&
|
|
143
|
+
isNullableDateTime(stage.completedAt) &&
|
|
144
|
+
(stage.latencyMs === null ||
|
|
145
|
+
(typeof stage.latencyMs === "number" &&
|
|
146
|
+
Number.isInteger(stage.latencyMs) &&
|
|
147
|
+
stage.latencyMs >= 0)) &&
|
|
148
|
+
(stage.code === undefined || typeof stage.code === "string")
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isTimedStage(stage: ActivationStageReceipt): boolean {
|
|
153
|
+
return (
|
|
154
|
+
isDateTime(stage.startedAt) &&
|
|
155
|
+
isDateTime(stage.completedAt) &&
|
|
156
|
+
typeof stage.latencyMs === "number"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isIndexStage(stage: ActivationStageReceipt): boolean {
|
|
161
|
+
if (!isTimedStage(stage)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return (
|
|
165
|
+
(stage.status === "passed" && stage.code === undefined) ||
|
|
166
|
+
(stage.status === "failed" &&
|
|
167
|
+
stage.code !== undefined &&
|
|
168
|
+
INDEX_FAILURE_CODES.has(stage.code))
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isLexicalStage(stage: ActivationStageReceipt): boolean {
|
|
173
|
+
if (stage.status === "passed") {
|
|
174
|
+
return isTimedStage(stage) && stage.code === undefined;
|
|
175
|
+
}
|
|
176
|
+
if (stage.status === "failed") {
|
|
177
|
+
return (
|
|
178
|
+
isTimedStage(stage) &&
|
|
179
|
+
stage.code !== undefined &&
|
|
180
|
+
LEXICAL_FAILURE_CODES.has(stage.code)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
stage.status === "skipped" &&
|
|
185
|
+
stage.startedAt === null &&
|
|
186
|
+
isDateTime(stage.completedAt) &&
|
|
187
|
+
stage.latencyMs === null &&
|
|
188
|
+
stage.code !== undefined &&
|
|
189
|
+
INDEX_FAILURE_CODES.has(stage.code)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isSemanticStage(stage: ActivationStageReceipt): boolean {
|
|
194
|
+
return (
|
|
195
|
+
stage.status === "pending" &&
|
|
196
|
+
stage.startedAt === null &&
|
|
197
|
+
stage.completedAt === null &&
|
|
198
|
+
stage.latencyMs === null &&
|
|
199
|
+
stage.code === "semantic_not_checked"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isConnectorStage(stage: ActivationStageReceipt): boolean {
|
|
204
|
+
if (stage.status === "passed") {
|
|
205
|
+
return isTimedStage(stage) && stage.code === undefined;
|
|
206
|
+
}
|
|
207
|
+
if (stage.status === "failed") {
|
|
208
|
+
return (
|
|
209
|
+
isTimedStage(stage) &&
|
|
210
|
+
stage.code !== undefined &&
|
|
211
|
+
CONNECTOR_FAILURE_CODES.has(stage.code)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (stage.status !== "skipped") {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (stage.code === "connector_not_requested") {
|
|
218
|
+
return (
|
|
219
|
+
stage.startedAt === null &&
|
|
220
|
+
stage.completedAt === null &&
|
|
221
|
+
stage.latencyMs === null
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return (
|
|
225
|
+
isTimedStage(stage) &&
|
|
226
|
+
stage.code !== undefined &&
|
|
227
|
+
CONNECTOR_SKIPPED_CODES.has(stage.code)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isReceipt(value: unknown): value is ActivationVerificationReceipt {
|
|
232
|
+
if (!value || typeof value !== "object") {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const receipt = value as Record<string, unknown>;
|
|
236
|
+
const stages = receipt.stages as Record<string, unknown> | undefined;
|
|
237
|
+
const evidence = receipt.evidence as Record<string, unknown> | undefined;
|
|
238
|
+
if (
|
|
239
|
+
!hasOnlyKeys(
|
|
240
|
+
receipt,
|
|
241
|
+
new Set([
|
|
242
|
+
"schemaVersion",
|
|
243
|
+
"collection",
|
|
244
|
+
"fingerprint",
|
|
245
|
+
"ready",
|
|
246
|
+
"generatedAt",
|
|
247
|
+
"stages",
|
|
248
|
+
"evidence",
|
|
249
|
+
])
|
|
250
|
+
) ||
|
|
251
|
+
receipt.schemaVersion !== "1.0" ||
|
|
252
|
+
typeof receipt.collection !== "string" ||
|
|
253
|
+
receipt.collection.length < 1 ||
|
|
254
|
+
receipt.collection.length > 128 ||
|
|
255
|
+
typeof receipt.fingerprint !== "string" ||
|
|
256
|
+
!HASH_PATTERN.test(receipt.fingerprint) ||
|
|
257
|
+
typeof receipt.ready !== "boolean" ||
|
|
258
|
+
!isDateTime(receipt.generatedAt) ||
|
|
259
|
+
!stages ||
|
|
260
|
+
!hasOnlyKeys(
|
|
261
|
+
stages,
|
|
262
|
+
new Set(["index", "lexical", "semantic", "connector"])
|
|
263
|
+
) ||
|
|
264
|
+
!isStageShape(stages.index) ||
|
|
265
|
+
!isStageShape(stages.lexical) ||
|
|
266
|
+
!isStageShape(stages.semantic) ||
|
|
267
|
+
!isStageShape(stages.connector) ||
|
|
268
|
+
!evidence ||
|
|
269
|
+
!hasOnlyKeys(
|
|
270
|
+
evidence,
|
|
271
|
+
new Set(["probeHash", "resultUri", "resultSourceHash", "connectorTarget"])
|
|
272
|
+
)
|
|
273
|
+
) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const indexStage = stages.index as ActivationStageReceipt;
|
|
278
|
+
const lexicalStage = stages.lexical as ActivationStageReceipt;
|
|
279
|
+
const semanticStage = stages.semantic as ActivationStageReceipt;
|
|
280
|
+
const connectorStage = stages.connector as ActivationStageReceipt;
|
|
281
|
+
if (
|
|
282
|
+
!isIndexStage(indexStage) ||
|
|
283
|
+
!isLexicalStage(lexicalStage) ||
|
|
284
|
+
!isSemanticStage(semanticStage) ||
|
|
285
|
+
!isConnectorStage(connectorStage)
|
|
286
|
+
) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const expectedReady =
|
|
290
|
+
indexStage.status === "passed" && lexicalStage.status === "passed";
|
|
291
|
+
if (receipt.ready !== expectedReady) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const connectorCode = connectorStage.code;
|
|
295
|
+
const connectorNeedsLexicalProof =
|
|
296
|
+
connectorStage.status === "passed" ||
|
|
297
|
+
(connectorStage.status === "failed" &&
|
|
298
|
+
connectorCode !== "connector_unsupported_config");
|
|
299
|
+
const unavailableBeforeConnectorProbe =
|
|
300
|
+
connectorStage.status === "skipped" &&
|
|
301
|
+
connectorCode === "connector_probe_unavailable";
|
|
302
|
+
if (
|
|
303
|
+
(connectorNeedsLexicalProof && !expectedReady) ||
|
|
304
|
+
(unavailableBeforeConnectorProbe && expectedReady)
|
|
305
|
+
) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const validEvidence =
|
|
310
|
+
(evidence.probeHash === undefined ||
|
|
311
|
+
(typeof evidence.probeHash === "string" &&
|
|
312
|
+
HASH_PATTERN.test(evidence.probeHash))) &&
|
|
313
|
+
(evidence.resultUri === undefined ||
|
|
314
|
+
(typeof evidence.resultUri === "string" &&
|
|
315
|
+
evidence.resultUri.length <= 2048 &&
|
|
316
|
+
RESULT_URI_PATTERN.test(evidence.resultUri))) &&
|
|
317
|
+
(evidence.resultSourceHash === undefined ||
|
|
318
|
+
(typeof evidence.resultSourceHash === "string" &&
|
|
319
|
+
HASH_PATTERN.test(evidence.resultSourceHash))) &&
|
|
320
|
+
(evidence.connectorTarget === undefined ||
|
|
321
|
+
(typeof evidence.connectorTarget === "string" &&
|
|
322
|
+
CONNECTOR_TARGET_PATTERN.test(evidence.connectorTarget)));
|
|
323
|
+
if (!validEvidence) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const connectorWasRequested =
|
|
328
|
+
connectorStage.code !== "connector_not_requested";
|
|
329
|
+
if (
|
|
330
|
+
connectorWasRequested !==
|
|
331
|
+
(typeof evidence.connectorTarget === "string")
|
|
332
|
+
) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const hasProbeHash = typeof evidence.probeHash === "string";
|
|
337
|
+
const hasResultUri = typeof evidence.resultUri === "string";
|
|
338
|
+
const hasResultSourceHash = typeof evidence.resultSourceHash === "string";
|
|
339
|
+
if (expectedReady) {
|
|
340
|
+
return hasProbeHash && hasResultUri && hasResultSourceHash;
|
|
341
|
+
}
|
|
342
|
+
if (lexicalStage.code === "retrieval_mismatch") {
|
|
343
|
+
return hasProbeHash && !hasResultUri && !hasResultSourceHash;
|
|
344
|
+
}
|
|
345
|
+
return !hasProbeHash && !hasResultUri && !hasResultSourceHash;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function parseActivationReceipt(
|
|
349
|
+
raw: string
|
|
350
|
+
): ActivationVerificationReceipt | null {
|
|
351
|
+
try {
|
|
352
|
+
const parsed: unknown = JSON.parse(raw);
|
|
353
|
+
return isReceipt(parsed) ? projectReceipt(parsed) : null;
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function serializeActivationReceipt(
|
|
360
|
+
receipt: ActivationVerificationReceipt
|
|
361
|
+
):
|
|
362
|
+
| {
|
|
363
|
+
ok: true;
|
|
364
|
+
json: string;
|
|
365
|
+
projected: ActivationVerificationReceipt;
|
|
366
|
+
connectorTarget: string;
|
|
367
|
+
}
|
|
368
|
+
| { ok: false; error: string } {
|
|
369
|
+
let projected: ActivationVerificationReceipt;
|
|
370
|
+
try {
|
|
371
|
+
projected = projectReceipt(receipt);
|
|
372
|
+
} catch {
|
|
373
|
+
return { ok: false, error: "Activation receipt is schema-invalid" };
|
|
374
|
+
}
|
|
375
|
+
if (!isReceipt(projected)) {
|
|
376
|
+
return { ok: false, error: "Activation receipt is schema-invalid" };
|
|
377
|
+
}
|
|
378
|
+
const json = JSON.stringify(projected);
|
|
379
|
+
if (
|
|
380
|
+
new TextEncoder().encode(json).byteLength > ACTIVATION_RECEIPT_MAX_BYTES
|
|
381
|
+
) {
|
|
382
|
+
return { ok: false, error: "Activation receipt exceeds 16 KiB" };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
json,
|
|
387
|
+
projected,
|
|
388
|
+
connectorTarget: projected.evidence.connectorTarget ?? "",
|
|
389
|
+
};
|
|
390
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -19,6 +19,14 @@ export {
|
|
|
19
19
|
export { SqliteAdapter } from "./sqlite";
|
|
20
20
|
// Types and interfaces
|
|
21
21
|
export type {
|
|
22
|
+
ActivationIndexDocument,
|
|
23
|
+
ActivationIndexIdentity,
|
|
24
|
+
ActivationIndexSnapshot,
|
|
25
|
+
ActivationStageName,
|
|
26
|
+
ActivationStageReceipt,
|
|
27
|
+
ActivationStageStatus,
|
|
28
|
+
ActivationVerificationCode,
|
|
29
|
+
ActivationVerificationReceipt,
|
|
22
30
|
ChunkInput,
|
|
23
31
|
ChunkRow,
|
|
24
32
|
CleanupStats,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: privacy-bounded retrieval activation receipts.
|
|
3
|
+
*
|
|
4
|
+
* @module src/store/migrations/012-activation-receipts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
import type { Migration } from "./runner";
|
|
10
|
+
|
|
11
|
+
export const migration: Migration = {
|
|
12
|
+
version: 12,
|
|
13
|
+
name: "activation_receipts",
|
|
14
|
+
|
|
15
|
+
up(db: Database): void {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS activation_receipts (
|
|
18
|
+
collection TEXT NOT NULL,
|
|
19
|
+
connector_target TEXT NOT NULL DEFAULT '',
|
|
20
|
+
schema_version TEXT NOT NULL,
|
|
21
|
+
fingerprint TEXT NOT NULL,
|
|
22
|
+
receipt_json TEXT NOT NULL CHECK (length(receipt_json) <= 16384),
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
PRIMARY KEY (collection, connector_target),
|
|
25
|
+
FOREIGN KEY (collection) REFERENCES collections(name) ON DELETE CASCADE
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_activation_receipts_fingerprint
|
|
30
|
+
ON activation_receipts(fingerprint)
|
|
31
|
+
`);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
down(db: Database): void {
|
|
35
|
+
db.exec("DROP INDEX IF EXISTS idx_activation_receipts_fingerprint");
|
|
36
|
+
db.exec("DROP TABLE IF EXISTS activation_receipts");
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: metadata-only FTS synchronization marker.
|
|
3
|
+
*
|
|
4
|
+
* The marker records which mirror hash was transactionally written through the
|
|
5
|
+
* supported FTS writers. This one-time migration compares legacy FTS bodies
|
|
6
|
+
* before trusting them; passive activation checks remain metadata-only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Database } from "bun:sqlite";
|
|
10
|
+
|
|
11
|
+
import type { Migration } from "./runner";
|
|
12
|
+
|
|
13
|
+
export const migration: Migration = {
|
|
14
|
+
version: 13,
|
|
15
|
+
name: "fts_sync_marker",
|
|
16
|
+
|
|
17
|
+
up(db: Database): void {
|
|
18
|
+
db.exec("ALTER TABLE documents ADD COLUMN fts_mirror_hash TEXT");
|
|
19
|
+
db.exec(`
|
|
20
|
+
UPDATE documents
|
|
21
|
+
SET fts_mirror_hash = mirror_hash
|
|
22
|
+
WHERE active = 1
|
|
23
|
+
AND mirror_hash IS NOT NULL
|
|
24
|
+
AND EXISTS (
|
|
25
|
+
SELECT 1
|
|
26
|
+
FROM documents_fts f
|
|
27
|
+
JOIN content c ON c.mirror_hash = documents.mirror_hash
|
|
28
|
+
WHERE f.rowid = documents.id
|
|
29
|
+
AND f.filepath = documents.rel_path
|
|
30
|
+
AND f.title = COALESCE(documents.title, '')
|
|
31
|
+
AND f.body = c.markdown
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
down(db: Database): void {
|
|
37
|
+
db.exec("ALTER TABLE documents DROP COLUMN fts_mirror_hash");
|
|
38
|
+
},
|
|
39
|
+
};
|