@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
|
@@ -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
|
}
|
|
@@ -278,6 +309,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
278
309
|
private configPath = ""; // Set by CLI layer for status output
|
|
279
310
|
private txDepth = 0; // Transaction nesting depth
|
|
280
311
|
private txCounter = 0; // Savepoint counter for unique names
|
|
312
|
+
private contextGeneration = 0;
|
|
281
313
|
|
|
282
314
|
// ─────────────────────────────────────────────────────────────────────────
|
|
283
315
|
// Lifecycle
|
|
@@ -341,6 +373,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
341
373
|
return result;
|
|
342
374
|
}
|
|
343
375
|
|
|
376
|
+
this.contextGeneration += 1;
|
|
344
377
|
return result;
|
|
345
378
|
} catch (cause) {
|
|
346
379
|
const message =
|
|
@@ -517,6 +550,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
517
550
|
});
|
|
518
551
|
|
|
519
552
|
transaction();
|
|
553
|
+
this.contextGeneration += 1;
|
|
520
554
|
return ok(undefined);
|
|
521
555
|
} catch (cause) {
|
|
522
556
|
return err(
|
|
@@ -559,6 +593,177 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
559
593
|
}
|
|
560
594
|
}
|
|
561
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
|
+
|
|
763
|
+
getContextGeneration(): number {
|
|
764
|
+
return this.contextGeneration;
|
|
765
|
+
}
|
|
766
|
+
|
|
562
767
|
// ─────────────────────────────────────────────────────────────────────────
|
|
563
768
|
// Documents
|
|
564
769
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -571,8 +776,9 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
571
776
|
const docid = deriveDocid(doc.sourceHash);
|
|
572
777
|
const uri = buildUri(doc.collection, doc.relPath);
|
|
573
778
|
|
|
574
|
-
db.
|
|
575
|
-
|
|
779
|
+
const transaction = db.transaction((): UpsertDocumentResult => {
|
|
780
|
+
db.run(
|
|
781
|
+
`
|
|
576
782
|
INSERT INTO documents (
|
|
577
783
|
collection, rel_path, source_hash, source_mime, source_ext,
|
|
578
784
|
source_size, source_mtime, source_ctime, docid, uri, title, mirror_hash,
|
|
@@ -610,48 +816,61 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
610
816
|
ingest_version = excluded.ingest_version,
|
|
611
817
|
updated_at = datetime('now')
|
|
612
818
|
`,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
);
|
|
642
848
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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);
|
|
649
855
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
856
|
+
if (!idRow) {
|
|
857
|
+
throw new Error("Failed to get document id after upsert");
|
|
858
|
+
}
|
|
859
|
+
|
|
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
|
+
});
|
|
653
872
|
|
|
654
|
-
return ok(
|
|
873
|
+
return ok(transaction());
|
|
655
874
|
} catch (cause) {
|
|
656
875
|
return err(
|
|
657
876
|
"QUERY_FAILED",
|
|
@@ -1138,6 +1357,29 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1138
1357
|
}
|
|
1139
1358
|
}
|
|
1140
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
|
+
|
|
1141
1383
|
async getContentBatch(
|
|
1142
1384
|
mirrorHashes: string[]
|
|
1143
1385
|
): Promise<StoreResult<Map<string, string>>> {
|
|
@@ -1352,12 +1594,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1352
1594
|
params.push(`%${options.author.toLowerCase()}%`);
|
|
1353
1595
|
}
|
|
1354
1596
|
|
|
1355
|
-
|
|
1356
|
-
params.push(options.collection);
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const hasOuterFilters =
|
|
1360
|
-
tagConditions.length > 0 || Boolean(options.collection);
|
|
1597
|
+
const hasOuterFilters = tagConditions.length > 0;
|
|
1361
1598
|
const ftsLimit = hasOuterFilters ? limit * 10 : limit;
|
|
1362
1599
|
params.push(limit);
|
|
1363
1600
|
|
|
@@ -1376,6 +1613,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1376
1613
|
) as score
|
|
1377
1614
|
FROM documents_fts
|
|
1378
1615
|
WHERE documents_fts MATCH ?
|
|
1616
|
+
AND rowid IN (
|
|
1617
|
+
SELECT id FROM documents
|
|
1618
|
+
WHERE active = 1
|
|
1619
|
+
${options.collection ? "AND collection = ?" : ""}
|
|
1620
|
+
)
|
|
1379
1621
|
ORDER BY score
|
|
1380
1622
|
LIMIT ?
|
|
1381
1623
|
)
|
|
@@ -1401,7 +1643,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1401
1643
|
JOIN documents d ON d.id = fm.rowid AND d.active = 1
|
|
1402
1644
|
WHERE 1 = 1
|
|
1403
1645
|
${tagConditions.length > 0 ? `AND ${tagConditions.join(" AND ")}` : ""}
|
|
1404
|
-
${options.collection ? "AND d.collection = ?" : ""}
|
|
1405
1646
|
ORDER BY fm.score
|
|
1406
1647
|
LIMIT ?
|
|
1407
1648
|
`;
|
|
@@ -1426,7 +1667,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1426
1667
|
categories: string | null;
|
|
1427
1668
|
}
|
|
1428
1669
|
|
|
1429
|
-
const queryParams = [
|
|
1670
|
+
const queryParams = [
|
|
1671
|
+
builtQuery.query,
|
|
1672
|
+
...(options.collection ? [options.collection] : []),
|
|
1673
|
+
ftsLimit,
|
|
1674
|
+
...params,
|
|
1675
|
+
];
|
|
1430
1676
|
const rows = db
|
|
1431
1677
|
.query<FtsRow, (string | number)[]>(sql)
|
|
1432
1678
|
.all(...queryParams);
|
|
@@ -1485,12 +1731,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1485
1731
|
id: number;
|
|
1486
1732
|
rel_path: string;
|
|
1487
1733
|
title: string | null;
|
|
1734
|
+
mirror_hash: string | null;
|
|
1488
1735
|
markdown: string | null;
|
|
1489
1736
|
}
|
|
1490
1737
|
|
|
1491
1738
|
const doc = db
|
|
1492
1739
|
.query<DocWithContent, [string, string]>(
|
|
1493
|
-
`SELECT d.id, d.rel_path, d.title, c.markdown
|
|
1740
|
+
`SELECT d.id, d.rel_path, d.title, d.mirror_hash, c.markdown
|
|
1494
1741
|
FROM documents d
|
|
1495
1742
|
LEFT JOIN content c ON c.mirror_hash = d.mirror_hash
|
|
1496
1743
|
WHERE d.collection = ? AND d.rel_path = ? AND d.active = 1`
|
|
@@ -1503,13 +1750,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1503
1750
|
|
|
1504
1751
|
// Delete existing FTS entry for this doc
|
|
1505
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
|
+
]);
|
|
1506
1756
|
|
|
1507
1757
|
// Insert new FTS entry if we have content
|
|
1508
|
-
if (doc.markdown) {
|
|
1758
|
+
if (doc.markdown !== null) {
|
|
1509
1759
|
db.run(
|
|
1510
1760
|
"INSERT INTO documents_fts (rowid, filepath, title, body) VALUES (?, ?, ?, ?)",
|
|
1511
1761
|
[doc.id, doc.rel_path, doc.title ?? "", doc.markdown]
|
|
1512
1762
|
);
|
|
1763
|
+
db.run("UPDATE documents SET fts_mirror_hash = ? WHERE id = ?", [
|
|
1764
|
+
doc.mirror_hash,
|
|
1765
|
+
doc.id,
|
|
1766
|
+
]);
|
|
1513
1767
|
}
|
|
1514
1768
|
});
|
|
1515
1769
|
|
|
@@ -1536,18 +1790,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1536
1790
|
const transaction = db.transaction(() => {
|
|
1537
1791
|
// Clear FTS table
|
|
1538
1792
|
db.run("DELETE FROM documents_fts");
|
|
1793
|
+
db.run("UPDATE documents SET fts_mirror_hash = NULL");
|
|
1539
1794
|
|
|
1540
1795
|
// Get all active documents with content
|
|
1541
1796
|
interface DocWithContent {
|
|
1542
1797
|
id: number;
|
|
1543
1798
|
rel_path: string;
|
|
1544
1799
|
title: string | null;
|
|
1800
|
+
mirror_hash: string;
|
|
1545
1801
|
markdown: string;
|
|
1546
1802
|
}
|
|
1547
1803
|
|
|
1548
1804
|
const docs = db
|
|
1549
1805
|
.query<DocWithContent, []>(
|
|
1550
|
-
`SELECT d.id, d.rel_path, d.title, c.markdown
|
|
1806
|
+
`SELECT d.id, d.rel_path, d.title, d.mirror_hash, c.markdown
|
|
1551
1807
|
FROM documents d
|
|
1552
1808
|
JOIN content c ON c.mirror_hash = d.mirror_hash
|
|
1553
1809
|
WHERE d.active = 1 AND d.mirror_hash IS NOT NULL`
|
|
@@ -1558,9 +1814,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1558
1814
|
const stmt = db.prepare(
|
|
1559
1815
|
"INSERT INTO documents_fts (rowid, filepath, title, body) VALUES (?, ?, ?, ?)"
|
|
1560
1816
|
);
|
|
1817
|
+
const markSynced = db.prepare(
|
|
1818
|
+
"UPDATE documents SET fts_mirror_hash = ? WHERE id = ?"
|
|
1819
|
+
);
|
|
1561
1820
|
|
|
1562
1821
|
for (const doc of docs) {
|
|
1563
1822
|
stmt.run(doc.id, doc.rel_path, doc.title ?? "", doc.markdown);
|
|
1823
|
+
markSynced.run(doc.mirror_hash, doc.id);
|
|
1564
1824
|
count++;
|
|
1565
1825
|
}
|
|
1566
1826
|
});
|
|
@@ -1614,10 +1874,17 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1614
1874
|
// Update FTS for each document using this hash
|
|
1615
1875
|
for (const doc of docs) {
|
|
1616
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
|
+
]);
|
|
1617
1880
|
db.run(
|
|
1618
1881
|
"INSERT INTO documents_fts (rowid, filepath, title, body) VALUES (?, ?, ?, ?)",
|
|
1619
1882
|
[doc.id, doc.rel_path, doc.title ?? "", content.markdown]
|
|
1620
1883
|
);
|
|
1884
|
+
db.run("UPDATE documents SET fts_mirror_hash = ? WHERE id = ?", [
|
|
1885
|
+
mirrorHash,
|
|
1886
|
+
doc.id,
|
|
1887
|
+
]);
|
|
1621
1888
|
}
|
|
1622
1889
|
});
|
|
1623
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
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -800,6 +884,12 @@ export interface StorePort {
|
|
|
800
884
|
*/
|
|
801
885
|
syncContexts(contexts: Context[]): Promise<StoreResult<void>>;
|
|
802
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Monotonic in-process generation for the persisted context snapshot.
|
|
889
|
+
* Changes after a successful context sync or when the store is reopened.
|
|
890
|
+
*/
|
|
891
|
+
getContextGeneration(): number;
|
|
892
|
+
|
|
803
893
|
/**
|
|
804
894
|
* Get all collections from DB.
|
|
805
895
|
*/
|
|
@@ -810,6 +900,31 @@ export interface StorePort {
|
|
|
810
900
|
*/
|
|
811
901
|
getContexts(): Promise<StoreResult<ContextRow[]>>;
|
|
812
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
|
+
|
|
813
928
|
// ─────────────────────────────────────────────────────────────────────────
|
|
814
929
|
// Documents
|
|
815
930
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -922,6 +1037,15 @@ export interface StorePort {
|
|
|
922
1037
|
*/
|
|
923
1038
|
getContent(mirrorHash: string): Promise<StoreResult<string | null>>;
|
|
924
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
|
+
|
|
925
1049
|
/**
|
|
926
1050
|
* Batch fetch markdown content for multiple mirror hashes.
|
|
927
1051
|
* Returns a map of mirrorHash -> markdown for hashes that exist.
|