@gmickel/gno 1.12.4 → 1.14.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 +126 -59
- package/assets/skill/SKILL.md +8 -1
- package/assets/skill/cli-reference.md +18 -7
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +3 -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/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/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 +313 -53
- package/src/store/types.ts +118 -0
|
@@ -25,6 +25,8 @@ import { migration as m008 } from "./008-vector-fingerprints";
|
|
|
25
25
|
import { migration as m009 } from "./009-content-type-rule-fingerprint";
|
|
26
26
|
import { migration as m010 } from "./010-typed-edges";
|
|
27
27
|
import { migration as m011 } from "./011-doc-edge-traversal-indexes";
|
|
28
|
+
import { migration as m012 } from "./012-activation-receipts";
|
|
29
|
+
import { migration as m013 } from "./013-fts-sync-marker";
|
|
28
30
|
|
|
29
31
|
/** All migrations in order */
|
|
30
32
|
export const migrations = [
|
|
@@ -39,4 +41,6 @@ export const migrations = [
|
|
|
39
41
|
m009,
|
|
40
42
|
m010,
|
|
41
43
|
m011,
|
|
44
|
+
m012,
|
|
45
|
+
m013,
|
|
42
46
|
];
|
|
@@ -15,6 +15,10 @@ import { basename } from "node:path";
|
|
|
15
15
|
|
|
16
16
|
import type { Collection, Context, FtsTokenizer } from "../../config/types";
|
|
17
17
|
import type {
|
|
18
|
+
ActivationIndexDocument,
|
|
19
|
+
ActivationIndexIdentity,
|
|
20
|
+
ActivationIndexSnapshot,
|
|
21
|
+
ActivationVerificationReceipt,
|
|
18
22
|
BacklinkRow,
|
|
19
23
|
ChunkInput,
|
|
20
24
|
ChunkRow,
|
|
@@ -63,7 +67,11 @@ import {
|
|
|
63
67
|
buildWikiBestRankSubquery,
|
|
64
68
|
} from "../../core/graph-resolver";
|
|
65
69
|
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
66
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
parseActivationReceipt,
|
|
72
|
+
serializeActivationReceipt,
|
|
73
|
+
} from "../activation-receipts";
|
|
74
|
+
import { getSchemaVersion, migrations, runMigrations } from "../migrations";
|
|
67
75
|
import { err, ok } from "../types";
|
|
68
76
|
import { getStoredEmbeddingFingerprint } from "../vector/freshness";
|
|
69
77
|
import { modelTableName } from "../vector/sqlite-vec";
|
|
@@ -84,6 +92,29 @@ const FTS5_FIELD_WEIGHTS = {
|
|
|
84
92
|
body: 1.0,
|
|
85
93
|
} as const;
|
|
86
94
|
|
|
95
|
+
/** Content-free activation snapshot query; kept exported for contract tests. */
|
|
96
|
+
export const ACTIVATION_INDEX_SNAPSHOT_SQL = `SELECT
|
|
97
|
+
d.id,
|
|
98
|
+
d.uri,
|
|
99
|
+
d.source_hash,
|
|
100
|
+
d.mirror_hash,
|
|
101
|
+
d.active,
|
|
102
|
+
CASE WHEN f.rowid IS NULL THEN 0 ELSE 1 END AS fts_present,
|
|
103
|
+
CASE
|
|
104
|
+
WHEN d.mirror_hash IS NULL AND f.rowid IS NULL THEN 1
|
|
105
|
+
WHEN
|
|
106
|
+
f.rowid IS NOT NULL
|
|
107
|
+
AND f.filepath = d.rel_path
|
|
108
|
+
AND f.title = COALESCE(d.title, '')
|
|
109
|
+
AND d.fts_mirror_hash = d.mirror_hash
|
|
110
|
+
THEN 1
|
|
111
|
+
ELSE 0
|
|
112
|
+
END AS fts_current
|
|
113
|
+
FROM documents d
|
|
114
|
+
LEFT JOIN documents_fts f ON f.rowid = d.id
|
|
115
|
+
WHERE d.collection = ? AND d.active = 1
|
|
116
|
+
ORDER BY d.uri ASC, d.id ASC`;
|
|
117
|
+
|
|
87
118
|
function sanitizeFts5Term(term: string): string {
|
|
88
119
|
return term.replace(/[^\p{L}\p{N}'_]/gu, "").toLowerCase();
|
|
89
120
|
}
|
|
@@ -562,6 +593,173 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
562
593
|
}
|
|
563
594
|
}
|
|
564
595
|
|
|
596
|
+
async getActivationIndexIdentity(
|
|
597
|
+
collection: string
|
|
598
|
+
): Promise<StoreResult<ActivationIndexIdentity>> {
|
|
599
|
+
const snapshot = await this.getActivationIndexSnapshot(collection);
|
|
600
|
+
return snapshot.ok ? ok(snapshot.value.identity) : snapshot;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async getActivationIndexSnapshot(
|
|
604
|
+
collection: string
|
|
605
|
+
): Promise<StoreResult<ActivationIndexSnapshot>> {
|
|
606
|
+
try {
|
|
607
|
+
const db = this.ensureOpen();
|
|
608
|
+
const indexName =
|
|
609
|
+
basename(this.dbPath)
|
|
610
|
+
.replace(SQLITE_EXT_REGEX, "")
|
|
611
|
+
.replace(INDEX_PREFIX_REGEX, "") || "default";
|
|
612
|
+
const rows = db
|
|
613
|
+
.query<
|
|
614
|
+
{
|
|
615
|
+
id: number;
|
|
616
|
+
uri: string;
|
|
617
|
+
source_hash: string;
|
|
618
|
+
mirror_hash: string | null;
|
|
619
|
+
active: number;
|
|
620
|
+
fts_present: number;
|
|
621
|
+
fts_current: number;
|
|
622
|
+
},
|
|
623
|
+
[string]
|
|
624
|
+
>(ACTIVATION_INDEX_SNAPSHOT_SQL)
|
|
625
|
+
.all(collection);
|
|
626
|
+
const ftsHasher = new Bun.CryptoHasher("sha256");
|
|
627
|
+
ftsHasher.update(
|
|
628
|
+
JSON.stringify(
|
|
629
|
+
rows.map(({ uri, fts_present, fts_current }) => ({
|
|
630
|
+
uri,
|
|
631
|
+
present: fts_present,
|
|
632
|
+
current: fts_current,
|
|
633
|
+
}))
|
|
634
|
+
)
|
|
635
|
+
);
|
|
636
|
+
const documents: ActivationIndexDocument[] = rows.map((row) => ({
|
|
637
|
+
id: row.id,
|
|
638
|
+
uri: row.uri,
|
|
639
|
+
sourceHash: row.source_hash,
|
|
640
|
+
mirrorHash: row.mirror_hash,
|
|
641
|
+
active: row.active === 1,
|
|
642
|
+
}));
|
|
643
|
+
return ok({
|
|
644
|
+
identity: {
|
|
645
|
+
indexName,
|
|
646
|
+
schemaVersion: getSchemaVersion(db),
|
|
647
|
+
ftsTokenizer: this.ftsTokenizer,
|
|
648
|
+
ftsStateHash: ftsHasher.digest("hex"),
|
|
649
|
+
activeDocumentCount: documents.length,
|
|
650
|
+
ftsSynchronized: rows.every((row) => row.fts_current === 1),
|
|
651
|
+
},
|
|
652
|
+
documents,
|
|
653
|
+
});
|
|
654
|
+
} catch (cause) {
|
|
655
|
+
return err(
|
|
656
|
+
"QUERY_FAILED",
|
|
657
|
+
cause instanceof Error
|
|
658
|
+
? cause.message
|
|
659
|
+
: "Failed to get activation index identity",
|
|
660
|
+
cause
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async getActivationReceipt(
|
|
666
|
+
collection: string,
|
|
667
|
+
expectedFingerprint: string,
|
|
668
|
+
connectorTarget = ""
|
|
669
|
+
): Promise<StoreResult<ActivationVerificationReceipt | null>> {
|
|
670
|
+
try {
|
|
671
|
+
const db = this.ensureOpen();
|
|
672
|
+
const row = db
|
|
673
|
+
.query<
|
|
674
|
+
{
|
|
675
|
+
collection: string;
|
|
676
|
+
connector_target: string;
|
|
677
|
+
fingerprint: string;
|
|
678
|
+
receipt_json: string;
|
|
679
|
+
},
|
|
680
|
+
[string, string]
|
|
681
|
+
>(
|
|
682
|
+
`SELECT collection, connector_target, fingerprint, receipt_json
|
|
683
|
+
FROM activation_receipts
|
|
684
|
+
WHERE collection = ? AND connector_target = ?`
|
|
685
|
+
)
|
|
686
|
+
.get(collection, connectorTarget);
|
|
687
|
+
|
|
688
|
+
if (!row) {
|
|
689
|
+
return ok(null);
|
|
690
|
+
}
|
|
691
|
+
if (row.fingerprint !== expectedFingerprint) {
|
|
692
|
+
db.run(
|
|
693
|
+
"DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
|
|
694
|
+
[collection, connectorTarget]
|
|
695
|
+
);
|
|
696
|
+
return ok(null);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const receipt = parseActivationReceipt(row.receipt_json);
|
|
700
|
+
if (
|
|
701
|
+
!receipt ||
|
|
702
|
+
receipt.fingerprint !== expectedFingerprint ||
|
|
703
|
+
receipt.collection !== row.collection ||
|
|
704
|
+
(receipt.evidence.connectorTarget ?? "") !== row.connector_target
|
|
705
|
+
) {
|
|
706
|
+
db.run(
|
|
707
|
+
"DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
|
|
708
|
+
[collection, connectorTarget]
|
|
709
|
+
);
|
|
710
|
+
return ok(null);
|
|
711
|
+
}
|
|
712
|
+
return ok(receipt);
|
|
713
|
+
} catch (cause) {
|
|
714
|
+
return err(
|
|
715
|
+
"QUERY_FAILED",
|
|
716
|
+
cause instanceof Error
|
|
717
|
+
? cause.message
|
|
718
|
+
: "Failed to get activation receipt",
|
|
719
|
+
cause
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async upsertActivationReceipt(
|
|
725
|
+
receipt: ActivationVerificationReceipt
|
|
726
|
+
): Promise<StoreResult<void>> {
|
|
727
|
+
try {
|
|
728
|
+
const db = this.ensureOpen();
|
|
729
|
+
const serialized = serializeActivationReceipt(receipt);
|
|
730
|
+
if (!serialized.ok) {
|
|
731
|
+
return err("INVALID_INPUT", serialized.error);
|
|
732
|
+
}
|
|
733
|
+
db.run(
|
|
734
|
+
`INSERT INTO activation_receipts (
|
|
735
|
+
collection, connector_target, schema_version, fingerprint,
|
|
736
|
+
receipt_json, updated_at
|
|
737
|
+
) VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
738
|
+
ON CONFLICT(collection, connector_target) DO UPDATE SET
|
|
739
|
+
schema_version = excluded.schema_version,
|
|
740
|
+
fingerprint = excluded.fingerprint,
|
|
741
|
+
receipt_json = excluded.receipt_json,
|
|
742
|
+
updated_at = datetime('now')`,
|
|
743
|
+
[
|
|
744
|
+
serialized.projected.collection,
|
|
745
|
+
serialized.connectorTarget,
|
|
746
|
+
serialized.projected.schemaVersion,
|
|
747
|
+
serialized.projected.fingerprint,
|
|
748
|
+
serialized.json,
|
|
749
|
+
]
|
|
750
|
+
);
|
|
751
|
+
return ok(undefined);
|
|
752
|
+
} catch (cause) {
|
|
753
|
+
return err(
|
|
754
|
+
"QUERY_FAILED",
|
|
755
|
+
cause instanceof Error
|
|
756
|
+
? cause.message
|
|
757
|
+
: "Failed to persist activation receipt",
|
|
758
|
+
cause
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
565
763
|
getContextGeneration(): number {
|
|
566
764
|
return this.contextGeneration;
|
|
567
765
|
}
|
|
@@ -578,8 +776,9 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
578
776
|
const docid = deriveDocid(doc.sourceHash);
|
|
579
777
|
const uri = buildUri(doc.collection, doc.relPath);
|
|
580
778
|
|
|
581
|
-
db.
|
|
582
|
-
|
|
779
|
+
const transaction = db.transaction((): UpsertDocumentResult => {
|
|
780
|
+
db.run(
|
|
781
|
+
`
|
|
583
782
|
INSERT INTO documents (
|
|
584
783
|
collection, rel_path, source_hash, source_mime, source_ext,
|
|
585
784
|
source_size, source_mtime, source_ctime, docid, uri, title, mirror_hash,
|
|
@@ -617,48 +816,61 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
617
816
|
ingest_version = excluded.ingest_version,
|
|
618
817
|
updated_at = datetime('now')
|
|
619
818
|
`,
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
819
|
+
[
|
|
820
|
+
doc.collection,
|
|
821
|
+
doc.relPath,
|
|
822
|
+
doc.sourceHash,
|
|
823
|
+
doc.sourceMime,
|
|
824
|
+
doc.sourceExt,
|
|
825
|
+
doc.sourceSize,
|
|
826
|
+
doc.sourceMtime,
|
|
827
|
+
doc.sourceCtime ?? doc.sourceMtime,
|
|
828
|
+
docid,
|
|
829
|
+
uri,
|
|
830
|
+
doc.title ?? null,
|
|
831
|
+
doc.mirrorHash ?? null,
|
|
832
|
+
doc.converterId ?? null,
|
|
833
|
+
doc.converterVersion ?? null,
|
|
834
|
+
doc.languageHint ?? null,
|
|
835
|
+
doc.contentType ?? null,
|
|
836
|
+
doc.categories ? JSON.stringify(doc.categories) : null,
|
|
837
|
+
doc.contentTypeSource ?? null,
|
|
838
|
+
doc.author ?? null,
|
|
839
|
+
doc.frontmatterDate ?? null,
|
|
840
|
+
doc.dateFields ? JSON.stringify(doc.dateFields) : null,
|
|
841
|
+
doc.contentTypeRulesFingerprint ?? null,
|
|
842
|
+
doc.lastErrorCode ?? null,
|
|
843
|
+
doc.lastErrorMessage ?? null,
|
|
844
|
+
doc.lastErrorCode ? new Date().toISOString() : null,
|
|
845
|
+
doc.ingestVersion ?? null,
|
|
846
|
+
]
|
|
847
|
+
);
|
|
649
848
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
849
|
+
// Get the row id (either inserted or updated)
|
|
850
|
+
const idRow = db
|
|
851
|
+
.query<{ id: number }, [string, string]>(
|
|
852
|
+
"SELECT id FROM documents WHERE collection = ? AND rel_path = ?"
|
|
853
|
+
)
|
|
854
|
+
.get(doc.collection, doc.relPath);
|
|
656
855
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
856
|
+
if (!idRow) {
|
|
857
|
+
throw new Error("Failed to get document id after upsert");
|
|
858
|
+
}
|
|
660
859
|
|
|
661
|
-
|
|
860
|
+
// Conversion failures deliberately drop mirror ownership. Remove the
|
|
861
|
+
// old lexical projection in the same transaction so stale content can
|
|
862
|
+
// never join against the newly updated document metadata.
|
|
863
|
+
if (doc.mirrorHash == null) {
|
|
864
|
+
db.run("DELETE FROM documents_fts WHERE rowid = ?", [idRow.id]);
|
|
865
|
+
db.run("UPDATE documents SET fts_mirror_hash = NULL WHERE id = ?", [
|
|
866
|
+
idRow.id,
|
|
867
|
+
]);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return { id: idRow.id, docid };
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
return ok(transaction());
|
|
662
874
|
} catch (cause) {
|
|
663
875
|
return err(
|
|
664
876
|
"QUERY_FAILED",
|
|
@@ -1145,6 +1357,29 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1145
1357
|
}
|
|
1146
1358
|
}
|
|
1147
1359
|
|
|
1360
|
+
async getContentPrefix(
|
|
1361
|
+
mirrorHash: string,
|
|
1362
|
+
maxChars: number
|
|
1363
|
+
): Promise<StoreResult<string | null>> {
|
|
1364
|
+
try {
|
|
1365
|
+
const db = this.ensureOpen();
|
|
1366
|
+
const boundedChars = Math.max(0, Math.floor(maxChars));
|
|
1367
|
+
const row = db
|
|
1368
|
+
.query<{ markdown: string }, [number, string]>(
|
|
1369
|
+
"SELECT substr(markdown, 1, ?) AS markdown FROM content WHERE mirror_hash = ?"
|
|
1370
|
+
)
|
|
1371
|
+
.get(boundedChars, mirrorHash);
|
|
1372
|
+
|
|
1373
|
+
return ok(row?.markdown ?? null);
|
|
1374
|
+
} catch (cause) {
|
|
1375
|
+
return err(
|
|
1376
|
+
"QUERY_FAILED",
|
|
1377
|
+
cause instanceof Error ? cause.message : "Failed to get content prefix",
|
|
1378
|
+
cause
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1148
1383
|
async getContentBatch(
|
|
1149
1384
|
mirrorHashes: string[]
|
|
1150
1385
|
): Promise<StoreResult<Map<string, string>>> {
|
|
@@ -1359,12 +1594,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1359
1594
|
params.push(`%${options.author.toLowerCase()}%`);
|
|
1360
1595
|
}
|
|
1361
1596
|
|
|
1362
|
-
|
|
1363
|
-
params.push(options.collection);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
const hasOuterFilters =
|
|
1367
|
-
tagConditions.length > 0 || Boolean(options.collection);
|
|
1597
|
+
const hasOuterFilters = tagConditions.length > 0;
|
|
1368
1598
|
const ftsLimit = hasOuterFilters ? limit * 10 : limit;
|
|
1369
1599
|
params.push(limit);
|
|
1370
1600
|
|
|
@@ -1383,6 +1613,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1383
1613
|
) as score
|
|
1384
1614
|
FROM documents_fts
|
|
1385
1615
|
WHERE documents_fts MATCH ?
|
|
1616
|
+
AND rowid IN (
|
|
1617
|
+
SELECT id FROM documents
|
|
1618
|
+
WHERE active = 1
|
|
1619
|
+
${options.collection ? "AND collection = ?" : ""}
|
|
1620
|
+
)
|
|
1386
1621
|
ORDER BY score
|
|
1387
1622
|
LIMIT ?
|
|
1388
1623
|
)
|
|
@@ -1408,7 +1643,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1408
1643
|
JOIN documents d ON d.id = fm.rowid AND d.active = 1
|
|
1409
1644
|
WHERE 1 = 1
|
|
1410
1645
|
${tagConditions.length > 0 ? `AND ${tagConditions.join(" AND ")}` : ""}
|
|
1411
|
-
${options.collection ? "AND d.collection = ?" : ""}
|
|
1412
1646
|
ORDER BY fm.score
|
|
1413
1647
|
LIMIT ?
|
|
1414
1648
|
`;
|
|
@@ -1433,7 +1667,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1433
1667
|
categories: string | null;
|
|
1434
1668
|
}
|
|
1435
1669
|
|
|
1436
|
-
const queryParams = [
|
|
1670
|
+
const queryParams = [
|
|
1671
|
+
builtQuery.query,
|
|
1672
|
+
...(options.collection ? [options.collection] : []),
|
|
1673
|
+
ftsLimit,
|
|
1674
|
+
...params,
|
|
1675
|
+
];
|
|
1437
1676
|
const rows = db
|
|
1438
1677
|
.query<FtsRow, (string | number)[]>(sql)
|
|
1439
1678
|
.all(...queryParams);
|
|
@@ -1492,12 +1731,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1492
1731
|
id: number;
|
|
1493
1732
|
rel_path: string;
|
|
1494
1733
|
title: string | null;
|
|
1734
|
+
mirror_hash: string | null;
|
|
1495
1735
|
markdown: string | null;
|
|
1496
1736
|
}
|
|
1497
1737
|
|
|
1498
1738
|
const doc = db
|
|
1499
1739
|
.query<DocWithContent, [string, string]>(
|
|
1500
|
-
`SELECT d.id, d.rel_path, d.title, c.markdown
|
|
1740
|
+
`SELECT d.id, d.rel_path, d.title, d.mirror_hash, c.markdown
|
|
1501
1741
|
FROM documents d
|
|
1502
1742
|
LEFT JOIN content c ON c.mirror_hash = d.mirror_hash
|
|
1503
1743
|
WHERE d.collection = ? AND d.rel_path = ? AND d.active = 1`
|
|
@@ -1510,13 +1750,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1510
1750
|
|
|
1511
1751
|
// Delete existing FTS entry for this doc
|
|
1512
1752
|
db.run("DELETE FROM documents_fts WHERE rowid = ?", [doc.id]);
|
|
1753
|
+
db.run("UPDATE documents SET fts_mirror_hash = NULL WHERE id = ?", [
|
|
1754
|
+
doc.id,
|
|
1755
|
+
]);
|
|
1513
1756
|
|
|
1514
1757
|
// Insert new FTS entry if we have content
|
|
1515
|
-
if (doc.markdown) {
|
|
1758
|
+
if (doc.markdown !== null) {
|
|
1516
1759
|
db.run(
|
|
1517
1760
|
"INSERT INTO documents_fts (rowid, filepath, title, body) VALUES (?, ?, ?, ?)",
|
|
1518
1761
|
[doc.id, doc.rel_path, doc.title ?? "", doc.markdown]
|
|
1519
1762
|
);
|
|
1763
|
+
db.run("UPDATE documents SET fts_mirror_hash = ? WHERE id = ?", [
|
|
1764
|
+
doc.mirror_hash,
|
|
1765
|
+
doc.id,
|
|
1766
|
+
]);
|
|
1520
1767
|
}
|
|
1521
1768
|
});
|
|
1522
1769
|
|
|
@@ -1543,18 +1790,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1543
1790
|
const transaction = db.transaction(() => {
|
|
1544
1791
|
// Clear FTS table
|
|
1545
1792
|
db.run("DELETE FROM documents_fts");
|
|
1793
|
+
db.run("UPDATE documents SET fts_mirror_hash = NULL");
|
|
1546
1794
|
|
|
1547
1795
|
// Get all active documents with content
|
|
1548
1796
|
interface DocWithContent {
|
|
1549
1797
|
id: number;
|
|
1550
1798
|
rel_path: string;
|
|
1551
1799
|
title: string | null;
|
|
1800
|
+
mirror_hash: string;
|
|
1552
1801
|
markdown: string;
|
|
1553
1802
|
}
|
|
1554
1803
|
|
|
1555
1804
|
const docs = db
|
|
1556
1805
|
.query<DocWithContent, []>(
|
|
1557
|
-
`SELECT d.id, d.rel_path, d.title, c.markdown
|
|
1806
|
+
`SELECT d.id, d.rel_path, d.title, d.mirror_hash, c.markdown
|
|
1558
1807
|
FROM documents d
|
|
1559
1808
|
JOIN content c ON c.mirror_hash = d.mirror_hash
|
|
1560
1809
|
WHERE d.active = 1 AND d.mirror_hash IS NOT NULL`
|
|
@@ -1565,9 +1814,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1565
1814
|
const stmt = db.prepare(
|
|
1566
1815
|
"INSERT INTO documents_fts (rowid, filepath, title, body) VALUES (?, ?, ?, ?)"
|
|
1567
1816
|
);
|
|
1817
|
+
const markSynced = db.prepare(
|
|
1818
|
+
"UPDATE documents SET fts_mirror_hash = ? WHERE id = ?"
|
|
1819
|
+
);
|
|
1568
1820
|
|
|
1569
1821
|
for (const doc of docs) {
|
|
1570
1822
|
stmt.run(doc.id, doc.rel_path, doc.title ?? "", doc.markdown);
|
|
1823
|
+
markSynced.run(doc.mirror_hash, doc.id);
|
|
1571
1824
|
count++;
|
|
1572
1825
|
}
|
|
1573
1826
|
});
|
|
@@ -1621,10 +1874,17 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1621
1874
|
// Update FTS for each document using this hash
|
|
1622
1875
|
for (const doc of docs) {
|
|
1623
1876
|
db.run("DELETE FROM documents_fts WHERE rowid = ?", [doc.id]);
|
|
1877
|
+
db.run("UPDATE documents SET fts_mirror_hash = NULL WHERE id = ?", [
|
|
1878
|
+
doc.id,
|
|
1879
|
+
]);
|
|
1624
1880
|
db.run(
|
|
1625
1881
|
"INSERT INTO documents_fts (rowid, filepath, title, body) VALUES (?, ?, ?, ?)",
|
|
1626
1882
|
[doc.id, doc.rel_path, doc.title ?? "", content.markdown]
|
|
1627
1883
|
);
|
|
1884
|
+
db.run("UPDATE documents SET fts_mirror_hash = ? WHERE id = ?", [
|
|
1885
|
+
mirrorHash,
|
|
1886
|
+
doc.id,
|
|
1887
|
+
]);
|
|
1628
1888
|
}
|
|
1629
1889
|
});
|
|
1630
1890
|
|
package/src/store/types.ts
CHANGED
|
@@ -732,6 +732,90 @@ export interface MigrationResult {
|
|
|
732
732
|
ftsTokenizer: FtsTokenizer;
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
736
|
+
// Activation Verification
|
|
737
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
export type ActivationStageName =
|
|
740
|
+
| "index"
|
|
741
|
+
| "lexical"
|
|
742
|
+
| "semantic"
|
|
743
|
+
| "connector";
|
|
744
|
+
|
|
745
|
+
export type ActivationStageStatus = "passed" | "pending" | "failed" | "skipped";
|
|
746
|
+
|
|
747
|
+
export type ActivationVerificationCode =
|
|
748
|
+
| "no_documents"
|
|
749
|
+
| "index_out_of_sync"
|
|
750
|
+
| "no_probe_term"
|
|
751
|
+
| "index_query_failed"
|
|
752
|
+
| "retrieval_mismatch"
|
|
753
|
+
| "semantic_not_checked"
|
|
754
|
+
| "connector_not_requested"
|
|
755
|
+
| "connector_not_configured"
|
|
756
|
+
| "connector_probe_unavailable"
|
|
757
|
+
| "connector_unsupported_config"
|
|
758
|
+
| "connector_start_failed"
|
|
759
|
+
| "connector_timeout"
|
|
760
|
+
| "connector_missing_tools"
|
|
761
|
+
| "connector_status_failed"
|
|
762
|
+
| "connector_search_failed"
|
|
763
|
+
| "connector_result_mismatch"
|
|
764
|
+
| "target_runtime_unverifiable";
|
|
765
|
+
|
|
766
|
+
export interface ActivationStageReceipt {
|
|
767
|
+
status: ActivationStageStatus;
|
|
768
|
+
startedAt: string | null;
|
|
769
|
+
completedAt: string | null;
|
|
770
|
+
latencyMs: number | null;
|
|
771
|
+
code?: ActivationVerificationCode;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** Privacy-bounded, per-collection proof that the local index can retrieve. */
|
|
775
|
+
export interface ActivationVerificationReceipt {
|
|
776
|
+
schemaVersion: "1.0";
|
|
777
|
+
collection: string;
|
|
778
|
+
fingerprint: string;
|
|
779
|
+
ready: boolean;
|
|
780
|
+
generatedAt: string;
|
|
781
|
+
stages: Record<ActivationStageName, ActivationStageReceipt>;
|
|
782
|
+
evidence: {
|
|
783
|
+
/** Corpus-keyed SHA-256 probe digest. The term/key are never persisted. */
|
|
784
|
+
probeHash?: string;
|
|
785
|
+
resultUri?: string;
|
|
786
|
+
resultSourceHash?: string;
|
|
787
|
+
connectorTarget?: string;
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Stable index inputs included in activation fingerprints. */
|
|
792
|
+
export interface ActivationIndexIdentity {
|
|
793
|
+
indexName: string;
|
|
794
|
+
schemaVersion: number;
|
|
795
|
+
ftsTokenizer: FtsTokenizer;
|
|
796
|
+
/** Hash of collection-scoped active FTS synchronization state. */
|
|
797
|
+
ftsStateHash: string;
|
|
798
|
+
/** Number of active documents represented by the snapshot. */
|
|
799
|
+
activeDocumentCount: number;
|
|
800
|
+
/** True only when every active document has a current owned FTS row. */
|
|
801
|
+
ftsSynchronized: boolean;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Content-free document identity used by passive activation fingerprints. */
|
|
805
|
+
export interface ActivationIndexDocument {
|
|
806
|
+
id: number;
|
|
807
|
+
uri: string;
|
|
808
|
+
sourceHash: string;
|
|
809
|
+
mirrorHash: string | null;
|
|
810
|
+
active: boolean;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** One metadata-only snapshot; never contains source or indexed body text. */
|
|
814
|
+
export interface ActivationIndexSnapshot {
|
|
815
|
+
identity: ActivationIndexIdentity;
|
|
816
|
+
documents: ActivationIndexDocument[];
|
|
817
|
+
}
|
|
818
|
+
|
|
735
819
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
736
820
|
// Transaction Types
|
|
737
821
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -816,6 +900,31 @@ export interface StorePort {
|
|
|
816
900
|
*/
|
|
817
901
|
getContexts(): Promise<StoreResult<ContextRow[]>>;
|
|
818
902
|
|
|
903
|
+
/** Schema/tokenizer identity used to invalidate activation receipts. */
|
|
904
|
+
getActivationIndexIdentity(
|
|
905
|
+
collection: string
|
|
906
|
+
): Promise<StoreResult<ActivationIndexIdentity>>;
|
|
907
|
+
|
|
908
|
+
/** Metadata/hash-only identity and documents for passive activation checks. */
|
|
909
|
+
getActivationIndexSnapshot(
|
|
910
|
+
collection: string
|
|
911
|
+
): Promise<StoreResult<ActivationIndexSnapshot>>;
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Load the current activation receipt. A row with a different fingerprint is
|
|
915
|
+
* deleted and returned as null so stale readiness cannot escape the store.
|
|
916
|
+
*/
|
|
917
|
+
getActivationReceipt(
|
|
918
|
+
collection: string,
|
|
919
|
+
expectedFingerprint: string,
|
|
920
|
+
connectorTarget?: string
|
|
921
|
+
): Promise<StoreResult<ActivationVerificationReceipt | null>>;
|
|
922
|
+
|
|
923
|
+
/** Persist a strictly projected, privacy-bounded activation receipt. */
|
|
924
|
+
upsertActivationReceipt(
|
|
925
|
+
receipt: ActivationVerificationReceipt
|
|
926
|
+
): Promise<StoreResult<void>>;
|
|
927
|
+
|
|
819
928
|
// ─────────────────────────────────────────────────────────────────────────
|
|
820
929
|
// Documents
|
|
821
930
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -928,6 +1037,15 @@ export interface StorePort {
|
|
|
928
1037
|
*/
|
|
929
1038
|
getContent(mirrorHash: string): Promise<StoreResult<string | null>>;
|
|
930
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* Read at most maxChars from stored markdown. Used by bounded activation
|
|
1042
|
+
* probes so readiness checks never materialize whole documents.
|
|
1043
|
+
*/
|
|
1044
|
+
getContentPrefix(
|
|
1045
|
+
mirrorHash: string,
|
|
1046
|
+
maxChars: number
|
|
1047
|
+
): Promise<StoreResult<string | null>>;
|
|
1048
|
+
|
|
931
1049
|
/**
|
|
932
1050
|
* Batch fetch markdown content for multiple mirror hashes.
|
|
933
1051
|
* Returns a map of mirrorHash -> markdown for hashes that exist.
|