@gmickel/gno 1.30.7 → 1.32.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 +6 -5
- package/assets/skill/SKILL.md +25 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +176 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +37 -17
- package/src/mcp/tools/sections.ts +512 -0
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +238 -116
- package/src/sdk/index.ts +12 -0
- package/src/sdk/types.ts +61 -3
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +395 -77
- package/src/serve/routes/api.ts +191 -104
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
package/src/serve/server.ts
CHANGED
|
@@ -93,6 +93,10 @@ import {
|
|
|
93
93
|
handleDocSimilar,
|
|
94
94
|
} from "./routes/links";
|
|
95
95
|
import { createMcpHttpGateway } from "./routes/mcp";
|
|
96
|
+
import {
|
|
97
|
+
handleCreateSectionTarget,
|
|
98
|
+
handleResolveSectionTarget,
|
|
99
|
+
} from "./routes/section-targets";
|
|
96
100
|
import {
|
|
97
101
|
handleTraceDelete,
|
|
98
102
|
handleTraceExport,
|
|
@@ -1172,6 +1176,36 @@ export async function startServer(
|
|
|
1172
1176
|
);
|
|
1173
1177
|
},
|
|
1174
1178
|
},
|
|
1179
|
+
"/api/doc/:id/section-targets": {
|
|
1180
|
+
POST: async (req: Request) => {
|
|
1181
|
+
if (!isRequestAllowed(req, port)) {
|
|
1182
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
1183
|
+
}
|
|
1184
|
+
const parts = new URL(req.url).pathname.split("/");
|
|
1185
|
+
const id = decodeURIComponent(parts[3] || "");
|
|
1186
|
+
return withSecurityHeaders(
|
|
1187
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
1188
|
+
handleCreateSectionTarget(store, id, req)
|
|
1189
|
+
),
|
|
1190
|
+
isDev
|
|
1191
|
+
);
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
"/api/doc/:id/section-targets/resolve": {
|
|
1195
|
+
POST: async (req: Request) => {
|
|
1196
|
+
if (!isRequestAllowed(req, port)) {
|
|
1197
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
1198
|
+
}
|
|
1199
|
+
const parts = new URL(req.url).pathname.split("/");
|
|
1200
|
+
const id = decodeURIComponent(parts[3] || "");
|
|
1201
|
+
return withSecurityHeaders(
|
|
1202
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
1203
|
+
handleResolveSectionTarget(store, id, req)
|
|
1204
|
+
),
|
|
1205
|
+
isDev
|
|
1206
|
+
);
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1175
1209
|
"/api/doc/:id/backlinks": {
|
|
1176
1210
|
GET: async (req: Request) => {
|
|
1177
1211
|
const url = new URL(req.url);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: bounded, metadata-only file-refactor recovery journal.
|
|
3
|
+
*
|
|
4
|
+
* Stores IDs, plan digests, collection/relpaths, phase/state, timestamps,
|
|
5
|
+
* and fingerprints/status only. Never stores note bodies or replacements.
|
|
6
|
+
* Create prunes oldest terminal receipts under FILE_REFACTOR_JOURNAL_MAX_RECEIPTS.
|
|
7
|
+
*
|
|
8
|
+
* @module src/store/migrations/026-file-refactor-recovery-journal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Database } from "bun:sqlite";
|
|
12
|
+
|
|
13
|
+
import type { Migration } from "./runner";
|
|
14
|
+
|
|
15
|
+
export const migration: Migration = {
|
|
16
|
+
version: 26,
|
|
17
|
+
name: "file_refactor_recovery_journal",
|
|
18
|
+
|
|
19
|
+
up(db: Database): void {
|
|
20
|
+
db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS file_refactor_recovery_journal (
|
|
22
|
+
journal_id TEXT PRIMARY KEY,
|
|
23
|
+
plan_digest TEXT NOT NULL,
|
|
24
|
+
collection TEXT NOT NULL,
|
|
25
|
+
operation TEXT NOT NULL
|
|
26
|
+
CHECK (operation IN ('rename', 'move')),
|
|
27
|
+
source_rel_path TEXT NOT NULL,
|
|
28
|
+
target_rel_path TEXT NOT NULL,
|
|
29
|
+
phase TEXT NOT NULL
|
|
30
|
+
CHECK (phase IN (
|
|
31
|
+
'prepared', 'staging', 'committing', 'committed', 'sync_pending',
|
|
32
|
+
'converged', 'rolling_back', 'rolled_back', 'recovery_required',
|
|
33
|
+
'aborted'
|
|
34
|
+
)),
|
|
35
|
+
phase_ordinal INTEGER NOT NULL CHECK (phase_ordinal >= 0),
|
|
36
|
+
filesystem_state TEXT NOT NULL
|
|
37
|
+
CHECK (filesystem_state IN (
|
|
38
|
+
'unchanged', 'committed', 'rolled_back', 'recovery_required'
|
|
39
|
+
)),
|
|
40
|
+
index_state TEXT NOT NULL
|
|
41
|
+
CHECK (index_state IN (
|
|
42
|
+
'not_attempted', 'pending', 'converged', 'skipped'
|
|
43
|
+
)),
|
|
44
|
+
file_entries_json TEXT NOT NULL
|
|
45
|
+
CHECK (length(CAST(file_entries_json AS BLOB)) <= 65536),
|
|
46
|
+
created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
|
|
47
|
+
updated_at_ms INTEGER NOT NULL CHECK (updated_at_ms >= 0),
|
|
48
|
+
CHECK (length(CAST(journal_id AS BLOB)) BETWEEN 1 AND 128),
|
|
49
|
+
CHECK (
|
|
50
|
+
length(plan_digest) = 64
|
|
51
|
+
AND plan_digest NOT GLOB '*[^0-9a-f]*'
|
|
52
|
+
),
|
|
53
|
+
CHECK (length(CAST(collection AS BLOB)) BETWEEN 1 AND 256),
|
|
54
|
+
CHECK (length(CAST(source_rel_path AS BLOB)) BETWEEN 1 AND 4096),
|
|
55
|
+
CHECK (length(CAST(target_rel_path AS BLOB)) BETWEEN 1 AND 4096),
|
|
56
|
+
CHECK (updated_at_ms >= created_at_ms)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_file_refactor_journal_plan_digest
|
|
60
|
+
ON file_refactor_recovery_journal(plan_digest, updated_at_ms DESC, journal_id DESC);
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_file_refactor_journal_collection
|
|
63
|
+
ON file_refactor_recovery_journal(collection, updated_at_ms DESC);
|
|
64
|
+
`);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
down(db: Database): void {
|
|
68
|
+
db.exec("DROP INDEX IF EXISTS idx_file_refactor_journal_collection");
|
|
69
|
+
db.exec("DROP INDEX IF EXISTS idx_file_refactor_journal_plan_digest");
|
|
70
|
+
db.exec("DROP TABLE IF EXISTS file_refactor_recovery_journal");
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -39,6 +39,7 @@ import { migration as m022 } from "./022-record-export-lineage";
|
|
|
39
39
|
import { migration as m023 } from "./023-collection-egress-policy";
|
|
40
40
|
import { migration as m024 } from "./024-egress-derived-lineage";
|
|
41
41
|
import { migration as m025 } from "./025-collection-egress-policy-revision";
|
|
42
|
+
import { migration as m026 } from "./026-file-refactor-recovery-journal";
|
|
42
43
|
|
|
43
44
|
/** All migrations in order */
|
|
44
45
|
export const migrations = [
|
|
@@ -67,4 +68,5 @@ export const migrations = [
|
|
|
67
68
|
m023,
|
|
68
69
|
m024,
|
|
69
70
|
m025,
|
|
71
|
+
m026,
|
|
70
72
|
];
|
|
@@ -16,6 +16,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
16
16
|
import { basename } from "node:path";
|
|
17
17
|
|
|
18
18
|
import type { EgressLineage } from "../../core/egress-provenance";
|
|
19
|
+
import type {
|
|
20
|
+
FileRefactorJournalAdvance,
|
|
21
|
+
FileRefactorRecoveryReceipt,
|
|
22
|
+
FileRefactorRecoveryReceiptDraft,
|
|
23
|
+
} from "../../core/file-refactor-journal";
|
|
19
24
|
import type {
|
|
20
25
|
ActivationIndexDocument,
|
|
21
26
|
ActivationIndexIdentity,
|
|
@@ -55,6 +60,8 @@ import type {
|
|
|
55
60
|
EgressAuditStatusResult,
|
|
56
61
|
FtsResult,
|
|
57
62
|
FtsSearchOptions,
|
|
63
|
+
FileRefactorResolutionReferrerDocument,
|
|
64
|
+
FileRefactorResolutionSnapshot,
|
|
58
65
|
GetGraphNeighborsOptions,
|
|
59
66
|
GetGraphOptions,
|
|
60
67
|
GraphEdgeConfidence,
|
|
@@ -112,6 +119,10 @@ import {
|
|
|
112
119
|
type FtsTokenizer,
|
|
113
120
|
resolveConfiguredEgressPolicy,
|
|
114
121
|
} from "../../config/types";
|
|
122
|
+
import {
|
|
123
|
+
getDocumentCapabilities,
|
|
124
|
+
isTextLikeReferenceDocument,
|
|
125
|
+
} from "../../core/document-capabilities";
|
|
115
126
|
import { analyzeGraphCommunities } from "../../core/graph-analysis";
|
|
116
127
|
import {
|
|
117
128
|
classifyResolvedGraphEdge,
|
|
@@ -122,6 +133,7 @@ import {
|
|
|
122
133
|
buildWikiBestRankMatchCountSubquery,
|
|
123
134
|
buildWikiBestRankSubquery,
|
|
124
135
|
} from "../../core/graph-resolver";
|
|
136
|
+
import { buildContentPrefilterNeedles } from "../../core/link-relevance";
|
|
125
137
|
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
126
138
|
import {
|
|
127
139
|
parseActivationReceipt,
|
|
@@ -160,6 +172,12 @@ import {
|
|
|
160
172
|
listEgressAuditReceipts as listStoredEgressAuditReceipts,
|
|
161
173
|
purgeEgressAuditReceipts as purgeStoredEgressAuditReceipts,
|
|
162
174
|
} from "./egress-audit-store";
|
|
175
|
+
import {
|
|
176
|
+
advanceFileRefactorReceipt as advanceStoredFileRefactorReceipt,
|
|
177
|
+
createFileRefactorPreparedReceipt as createStoredFileRefactorPreparedReceipt,
|
|
178
|
+
getFileRefactorReceiptById as getStoredFileRefactorReceiptById,
|
|
179
|
+
getLatestFileRefactorReceiptByPlanDigest as getStoredLatestFileRefactorReceiptByPlanDigest,
|
|
180
|
+
} from "./file-refactor-journal-store";
|
|
163
181
|
import { loadFts5Snowball } from "./fts5-snowball";
|
|
164
182
|
import { queryGraphNeighborsForSeeds } from "./graph-neighbors";
|
|
165
183
|
import {
|
|
@@ -1960,6 +1978,38 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1960
1978
|
return purgeStoredDocumentChanges(this.ensureOpen());
|
|
1961
1979
|
}
|
|
1962
1980
|
|
|
1981
|
+
async createFileRefactorPreparedReceipt(
|
|
1982
|
+
draft: FileRefactorRecoveryReceiptDraft
|
|
1983
|
+
): Promise<StoreResult<FileRefactorRecoveryReceipt>> {
|
|
1984
|
+
return createStoredFileRefactorPreparedReceipt(this.ensureOpen(), draft);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
async advanceFileRefactorReceipt(
|
|
1988
|
+
journalId: string,
|
|
1989
|
+
update: FileRefactorJournalAdvance
|
|
1990
|
+
): Promise<StoreResult<FileRefactorRecoveryReceipt>> {
|
|
1991
|
+
return advanceStoredFileRefactorReceipt(
|
|
1992
|
+
this.ensureOpen(),
|
|
1993
|
+
journalId,
|
|
1994
|
+
update
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
async getFileRefactorReceiptById(
|
|
1999
|
+
journalId: string
|
|
2000
|
+
): Promise<StoreResult<FileRefactorRecoveryReceipt | null>> {
|
|
2001
|
+
return getStoredFileRefactorReceiptById(this.ensureOpen(), journalId);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
async getLatestFileRefactorReceiptByPlanDigest(
|
|
2005
|
+
planDigest: string
|
|
2006
|
+
): Promise<StoreResult<FileRefactorRecoveryReceipt | null>> {
|
|
2007
|
+
return getStoredLatestFileRefactorReceiptByPlanDigest(
|
|
2008
|
+
this.ensureOpen(),
|
|
2009
|
+
planDigest
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1963
2013
|
async upsertSavedCapsuleRegistration(
|
|
1964
2014
|
input: SavedCapsuleRegistrationInput
|
|
1965
2015
|
): Promise<StoreResult<SavedCapsuleRegistrationRecord>> {
|
|
@@ -3371,6 +3421,408 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3371
3421
|
}
|
|
3372
3422
|
}
|
|
3373
3423
|
|
|
3424
|
+
/**
|
|
3425
|
+
* Bounded read seam for reference-safe rename/move planning.
|
|
3426
|
+
* Loads catalog metadata plus content for indexed backlinks unioned with a
|
|
3427
|
+
* conservative SQL content prefilter (opaque embeds/HTML/code/malformed may
|
|
3428
|
+
* not appear in doc_links). Missing mirror content fails closed.
|
|
3429
|
+
*/
|
|
3430
|
+
async getFileRefactorResolutionSnapshot(input: {
|
|
3431
|
+
sourceUri: string;
|
|
3432
|
+
maxCatalogDocuments?: number;
|
|
3433
|
+
maxReferrerDocuments?: number;
|
|
3434
|
+
maxContentCharsPerDocument?: number;
|
|
3435
|
+
maxTotalContentChars?: number;
|
|
3436
|
+
}): Promise<StoreResult<FileRefactorResolutionSnapshot>> {
|
|
3437
|
+
try {
|
|
3438
|
+
const db = this.ensureOpen();
|
|
3439
|
+
const maxCatalog = Math.max(
|
|
3440
|
+
1,
|
|
3441
|
+
Math.floor(input.maxCatalogDocuments ?? 5_000)
|
|
3442
|
+
);
|
|
3443
|
+
const maxReferrers = Math.max(
|
|
3444
|
+
1,
|
|
3445
|
+
Math.floor(input.maxReferrerDocuments ?? 5_000)
|
|
3446
|
+
);
|
|
3447
|
+
const maxChars = Math.max(
|
|
3448
|
+
1,
|
|
3449
|
+
Math.floor(input.maxContentCharsPerDocument ?? 1_000_000)
|
|
3450
|
+
);
|
|
3451
|
+
const maxTotalChars = Math.max(
|
|
3452
|
+
maxChars,
|
|
3453
|
+
Math.floor(input.maxTotalContentChars ?? 20_000_000)
|
|
3454
|
+
);
|
|
3455
|
+
const truncationReasons: string[] = [];
|
|
3456
|
+
|
|
3457
|
+
const sourceRow = db
|
|
3458
|
+
.query<
|
|
3459
|
+
{
|
|
3460
|
+
id: number;
|
|
3461
|
+
uri: string;
|
|
3462
|
+
rel_path: string;
|
|
3463
|
+
collection: string;
|
|
3464
|
+
title: string | null;
|
|
3465
|
+
mirror_hash: string | null;
|
|
3466
|
+
source_ext: string;
|
|
3467
|
+
source_mime: string;
|
|
3468
|
+
record_key: string | null;
|
|
3469
|
+
},
|
|
3470
|
+
[string]
|
|
3471
|
+
>(
|
|
3472
|
+
`SELECT id, uri, rel_path, collection, title, mirror_hash,
|
|
3473
|
+
source_ext, source_mime, record_key
|
|
3474
|
+
FROM documents
|
|
3475
|
+
WHERE uri = ? AND active = 1`
|
|
3476
|
+
)
|
|
3477
|
+
.get(input.sourceUri);
|
|
3478
|
+
|
|
3479
|
+
if (!sourceRow) {
|
|
3480
|
+
return err("NOT_FOUND", `Document not found: ${input.sourceUri}`);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
const sourceCaps = getDocumentCapabilities({
|
|
3484
|
+
sourceExt: sourceRow.source_ext,
|
|
3485
|
+
sourceMime: sourceRow.source_mime,
|
|
3486
|
+
contentAvailable: Boolean(sourceRow.mirror_hash),
|
|
3487
|
+
recordKey: sourceRow.record_key,
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3490
|
+
let sourceContent: string | null = null;
|
|
3491
|
+
let sourceContentTruncated = false;
|
|
3492
|
+
if (sourceRow.mirror_hash) {
|
|
3493
|
+
const contentRow = db
|
|
3494
|
+
.query<{ markdown: string }, [number, string]>(
|
|
3495
|
+
`SELECT substr(markdown, 1, ?) AS markdown
|
|
3496
|
+
FROM content WHERE mirror_hash = ?`
|
|
3497
|
+
)
|
|
3498
|
+
.get(maxChars + 1, sourceRow.mirror_hash);
|
|
3499
|
+
if (contentRow) {
|
|
3500
|
+
sourceContentTruncated = contentRow.markdown.length > maxChars;
|
|
3501
|
+
sourceContent = sourceContentTruncated
|
|
3502
|
+
? contentRow.markdown.slice(0, maxChars)
|
|
3503
|
+
: contentRow.markdown;
|
|
3504
|
+
if (sourceContentTruncated) {
|
|
3505
|
+
truncationReasons.push("source_content_truncated");
|
|
3506
|
+
}
|
|
3507
|
+
} else {
|
|
3508
|
+
truncationReasons.push("source_content_missing");
|
|
3509
|
+
}
|
|
3510
|
+
} else {
|
|
3511
|
+
// Null mirror_hash means source bytes are unavailable — fail closed.
|
|
3512
|
+
truncationReasons.push("source_content_missing");
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
const catalogRows = db
|
|
3516
|
+
.query<
|
|
3517
|
+
{
|
|
3518
|
+
id: number;
|
|
3519
|
+
uri: string;
|
|
3520
|
+
rel_path: string;
|
|
3521
|
+
collection: string;
|
|
3522
|
+
title: string | null;
|
|
3523
|
+
},
|
|
3524
|
+
[string, number]
|
|
3525
|
+
>(
|
|
3526
|
+
`SELECT id, uri, rel_path, collection, title
|
|
3527
|
+
FROM documents
|
|
3528
|
+
WHERE active = 1 AND collection = ?
|
|
3529
|
+
ORDER BY id
|
|
3530
|
+
LIMIT ?`
|
|
3531
|
+
)
|
|
3532
|
+
.all(sourceRow.collection, maxCatalog + 1);
|
|
3533
|
+
|
|
3534
|
+
if (catalogRows.length > maxCatalog) {
|
|
3535
|
+
truncationReasons.push("catalog_truncated");
|
|
3536
|
+
}
|
|
3537
|
+
const catalog = catalogRows.slice(0, maxCatalog).map((row) => ({
|
|
3538
|
+
id: row.id,
|
|
3539
|
+
uri: row.uri,
|
|
3540
|
+
relPath: row.rel_path,
|
|
3541
|
+
collection: row.collection,
|
|
3542
|
+
title: row.title,
|
|
3543
|
+
}));
|
|
3544
|
+
|
|
3545
|
+
const occupiedRelPaths = catalog.map((doc) => doc.relPath);
|
|
3546
|
+
|
|
3547
|
+
const backlinksResult = await this.getBacklinksForDoc(sourceRow.id, {
|
|
3548
|
+
collection: sourceRow.collection,
|
|
3549
|
+
});
|
|
3550
|
+
if (!backlinksResult.ok) {
|
|
3551
|
+
return backlinksResult;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
const candidateIds = new Set<number>();
|
|
3555
|
+
for (const row of backlinksResult.value) {
|
|
3556
|
+
if (row.sourceDocId !== sourceRow.id) {
|
|
3557
|
+
candidateIds.add(row.sourceDocId);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
const needles = buildContentPrefilterNeedles({
|
|
3562
|
+
relPath: sourceRow.rel_path,
|
|
3563
|
+
title: sourceRow.title,
|
|
3564
|
+
});
|
|
3565
|
+
const escapeLike = (value: string): string =>
|
|
3566
|
+
value
|
|
3567
|
+
.replaceAll("\\", "\\\\")
|
|
3568
|
+
.replaceAll("%", "\\%")
|
|
3569
|
+
.replaceAll("_", "\\_");
|
|
3570
|
+
|
|
3571
|
+
if (needles.length > 0) {
|
|
3572
|
+
const likeClauses = needles
|
|
3573
|
+
.map(() => `c.markdown LIKE '%' || ? || '%' ESCAPE '\\'`)
|
|
3574
|
+
.join(" OR ");
|
|
3575
|
+
const prefilterRows = db
|
|
3576
|
+
.query<{ id: number }, (string | number)[]>(
|
|
3577
|
+
`SELECT DISTINCT d.id AS id
|
|
3578
|
+
FROM documents d
|
|
3579
|
+
INNER JOIN content c ON c.mirror_hash = d.mirror_hash
|
|
3580
|
+
WHERE d.active = 1
|
|
3581
|
+
AND d.collection = ?
|
|
3582
|
+
AND d.id != ?
|
|
3583
|
+
AND (${likeClauses})
|
|
3584
|
+
ORDER BY d.id
|
|
3585
|
+
LIMIT ?`
|
|
3586
|
+
)
|
|
3587
|
+
.all(
|
|
3588
|
+
sourceRow.collection,
|
|
3589
|
+
sourceRow.id,
|
|
3590
|
+
...needles.map(escapeLike),
|
|
3591
|
+
maxReferrers + 1
|
|
3592
|
+
);
|
|
3593
|
+
if (prefilterRows.length > maxReferrers) {
|
|
3594
|
+
truncationReasons.push("content_prefilter_truncated");
|
|
3595
|
+
}
|
|
3596
|
+
for (const row of prefilterRows.slice(0, maxReferrers + 1)) {
|
|
3597
|
+
candidateIds.add(row.id);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
// Completeness: union same-collection docs whose mirror content cannot be
|
|
3602
|
+
// loaded. Text-like docs (including read-only logical .md records) fail
|
|
3603
|
+
// closed; non-text binaries are omitted (they cannot hold markdown/wiki/
|
|
3604
|
+
// HTML refs). Preserve referrer caps via LIMIT + sorted slice below.
|
|
3605
|
+
const missingMirrorRows = db
|
|
3606
|
+
.query<
|
|
3607
|
+
{
|
|
3608
|
+
id: number;
|
|
3609
|
+
source_ext: string;
|
|
3610
|
+
source_mime: string;
|
|
3611
|
+
},
|
|
3612
|
+
[string, number, number]
|
|
3613
|
+
>(
|
|
3614
|
+
`SELECT d.id AS id,
|
|
3615
|
+
d.source_ext AS source_ext,
|
|
3616
|
+
d.source_mime AS source_mime
|
|
3617
|
+
FROM documents d
|
|
3618
|
+
WHERE d.active = 1
|
|
3619
|
+
AND d.collection = ?
|
|
3620
|
+
AND d.id != ?
|
|
3621
|
+
AND (
|
|
3622
|
+
d.mirror_hash IS NULL
|
|
3623
|
+
OR NOT EXISTS (
|
|
3624
|
+
SELECT 1 FROM content c WHERE c.mirror_hash = d.mirror_hash
|
|
3625
|
+
)
|
|
3626
|
+
)
|
|
3627
|
+
ORDER BY d.id
|
|
3628
|
+
LIMIT ?`
|
|
3629
|
+
)
|
|
3630
|
+
.all(sourceRow.collection, sourceRow.id, maxReferrers + 1);
|
|
3631
|
+
if (missingMirrorRows.length > maxReferrers) {
|
|
3632
|
+
truncationReasons.push("missing_mirror_scan_truncated");
|
|
3633
|
+
}
|
|
3634
|
+
for (const row of missingMirrorRows) {
|
|
3635
|
+
if (!isTextLikeReferenceDocument(row.source_ext, row.source_mime)) {
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3638
|
+
candidateIds.add(row.id);
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
const sortedCandidateIds = [...candidateIds].sort((a, b) => a - b);
|
|
3642
|
+
if (sortedCandidateIds.length > maxReferrers) {
|
|
3643
|
+
truncationReasons.push("referrers_truncated");
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
const referrers: FileRefactorResolutionReferrerDocument[] = [];
|
|
3647
|
+
let totalContentChars = sourceContent?.length ?? 0;
|
|
3648
|
+
|
|
3649
|
+
for (const referrerId of sortedCandidateIds.slice(0, maxReferrers)) {
|
|
3650
|
+
const referrerRow = db
|
|
3651
|
+
.query<
|
|
3652
|
+
{
|
|
3653
|
+
id: number;
|
|
3654
|
+
uri: string;
|
|
3655
|
+
rel_path: string;
|
|
3656
|
+
collection: string;
|
|
3657
|
+
title: string | null;
|
|
3658
|
+
mirror_hash: string | null;
|
|
3659
|
+
source_ext: string;
|
|
3660
|
+
source_mime: string;
|
|
3661
|
+
record_key: string | null;
|
|
3662
|
+
},
|
|
3663
|
+
[number]
|
|
3664
|
+
>(
|
|
3665
|
+
`SELECT id, uri, rel_path, collection, title, mirror_hash,
|
|
3666
|
+
source_ext, source_mime, record_key
|
|
3667
|
+
FROM documents
|
|
3668
|
+
WHERE id = ? AND active = 1`
|
|
3669
|
+
)
|
|
3670
|
+
.get(referrerId);
|
|
3671
|
+
if (!referrerRow) continue;
|
|
3672
|
+
|
|
3673
|
+
const caps = getDocumentCapabilities({
|
|
3674
|
+
sourceExt: referrerRow.source_ext,
|
|
3675
|
+
sourceMime: referrerRow.source_mime,
|
|
3676
|
+
contentAvailable: Boolean(referrerRow.mirror_hash),
|
|
3677
|
+
recordKey: referrerRow.record_key,
|
|
3678
|
+
});
|
|
3679
|
+
|
|
3680
|
+
if (!referrerRow.mirror_hash) {
|
|
3681
|
+
truncationReasons.push("referrer_content_missing");
|
|
3682
|
+
referrers.push({
|
|
3683
|
+
id: referrerRow.id,
|
|
3684
|
+
uri: referrerRow.uri,
|
|
3685
|
+
relPath: referrerRow.rel_path,
|
|
3686
|
+
collection: referrerRow.collection,
|
|
3687
|
+
title: referrerRow.title,
|
|
3688
|
+
content: null,
|
|
3689
|
+
contentTruncated: false,
|
|
3690
|
+
contentMissing: true,
|
|
3691
|
+
editable: caps.editable,
|
|
3692
|
+
editableReason: caps.editable
|
|
3693
|
+
? undefined
|
|
3694
|
+
: caps.reason
|
|
3695
|
+
? "read_only_document"
|
|
3696
|
+
: "capability_denied",
|
|
3697
|
+
sourceExt: referrerRow.source_ext,
|
|
3698
|
+
sourceMime: referrerRow.source_mime,
|
|
3699
|
+
recordKey: referrerRow.record_key,
|
|
3700
|
+
});
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
const contentRow = db
|
|
3705
|
+
.query<{ markdown: string }, [number, string]>(
|
|
3706
|
+
`SELECT substr(markdown, 1, ?) AS markdown
|
|
3707
|
+
FROM content WHERE mirror_hash = ?`
|
|
3708
|
+
)
|
|
3709
|
+
.get(maxChars + 1, referrerRow.mirror_hash);
|
|
3710
|
+
|
|
3711
|
+
if (!contentRow) {
|
|
3712
|
+
truncationReasons.push("referrer_content_missing");
|
|
3713
|
+
referrers.push({
|
|
3714
|
+
id: referrerRow.id,
|
|
3715
|
+
uri: referrerRow.uri,
|
|
3716
|
+
relPath: referrerRow.rel_path,
|
|
3717
|
+
collection: referrerRow.collection,
|
|
3718
|
+
title: referrerRow.title,
|
|
3719
|
+
content: null,
|
|
3720
|
+
contentTruncated: false,
|
|
3721
|
+
contentMissing: true,
|
|
3722
|
+
editable: caps.editable,
|
|
3723
|
+
editableReason: caps.editable
|
|
3724
|
+
? undefined
|
|
3725
|
+
: caps.reason
|
|
3726
|
+
? "read_only_document"
|
|
3727
|
+
: "capability_denied",
|
|
3728
|
+
sourceExt: referrerRow.source_ext,
|
|
3729
|
+
sourceMime: referrerRow.source_mime,
|
|
3730
|
+
recordKey: referrerRow.record_key,
|
|
3731
|
+
});
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
const contentTruncated = contentRow.markdown.length > maxChars;
|
|
3736
|
+
if (contentTruncated) {
|
|
3737
|
+
truncationReasons.push("referrer_content_truncated");
|
|
3738
|
+
}
|
|
3739
|
+
const content = contentTruncated
|
|
3740
|
+
? contentRow.markdown.slice(0, maxChars)
|
|
3741
|
+
: contentRow.markdown;
|
|
3742
|
+
totalContentChars += content.length;
|
|
3743
|
+
if (totalContentChars > maxTotalChars) {
|
|
3744
|
+
truncationReasons.push("total_content_truncated");
|
|
3745
|
+
referrers.push({
|
|
3746
|
+
id: referrerRow.id,
|
|
3747
|
+
uri: referrerRow.uri,
|
|
3748
|
+
relPath: referrerRow.rel_path,
|
|
3749
|
+
collection: referrerRow.collection,
|
|
3750
|
+
title: referrerRow.title,
|
|
3751
|
+
content: null,
|
|
3752
|
+
contentTruncated: false,
|
|
3753
|
+
contentMissing: true,
|
|
3754
|
+
editable: caps.editable,
|
|
3755
|
+
editableReason: caps.editable
|
|
3756
|
+
? undefined
|
|
3757
|
+
: caps.reason
|
|
3758
|
+
? "read_only_document"
|
|
3759
|
+
: "capability_denied",
|
|
3760
|
+
sourceExt: referrerRow.source_ext,
|
|
3761
|
+
sourceMime: referrerRow.source_mime,
|
|
3762
|
+
recordKey: referrerRow.record_key,
|
|
3763
|
+
});
|
|
3764
|
+
continue;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
referrers.push({
|
|
3768
|
+
id: referrerRow.id,
|
|
3769
|
+
uri: referrerRow.uri,
|
|
3770
|
+
relPath: referrerRow.rel_path,
|
|
3771
|
+
collection: referrerRow.collection,
|
|
3772
|
+
title: referrerRow.title,
|
|
3773
|
+
content,
|
|
3774
|
+
contentTruncated,
|
|
3775
|
+
contentMissing: false,
|
|
3776
|
+
editable: caps.editable,
|
|
3777
|
+
editableReason: caps.editable
|
|
3778
|
+
? undefined
|
|
3779
|
+
: caps.reason
|
|
3780
|
+
? "read_only_document"
|
|
3781
|
+
: "capability_denied",
|
|
3782
|
+
sourceExt: referrerRow.source_ext,
|
|
3783
|
+
sourceMime: referrerRow.source_mime,
|
|
3784
|
+
recordKey: referrerRow.record_key,
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
const snapshot: FileRefactorResolutionSnapshot = {
|
|
3789
|
+
source: {
|
|
3790
|
+
id: sourceRow.id,
|
|
3791
|
+
uri: sourceRow.uri,
|
|
3792
|
+
relPath: sourceRow.rel_path,
|
|
3793
|
+
collection: sourceRow.collection,
|
|
3794
|
+
title: sourceRow.title,
|
|
3795
|
+
mirrorHash: sourceRow.mirror_hash,
|
|
3796
|
+
sourceExt: sourceRow.source_ext,
|
|
3797
|
+
sourceMime: sourceRow.source_mime,
|
|
3798
|
+
recordKey: sourceRow.record_key,
|
|
3799
|
+
content: sourceContent,
|
|
3800
|
+
contentTruncated: sourceContentTruncated,
|
|
3801
|
+
editable: sourceCaps.editable,
|
|
3802
|
+
editableReason: sourceCaps.editable
|
|
3803
|
+
? undefined
|
|
3804
|
+
: sourceCaps.reason
|
|
3805
|
+
? "read_only_document"
|
|
3806
|
+
: "capability_denied",
|
|
3807
|
+
},
|
|
3808
|
+
catalog,
|
|
3809
|
+
referrers,
|
|
3810
|
+
occupiedRelPaths,
|
|
3811
|
+
truncated: truncationReasons.length > 0,
|
|
3812
|
+
truncationReasons: [...new Set(truncationReasons)].sort(),
|
|
3813
|
+
};
|
|
3814
|
+
return ok(snapshot);
|
|
3815
|
+
} catch (cause) {
|
|
3816
|
+
return err(
|
|
3817
|
+
"QUERY_FAILED",
|
|
3818
|
+
cause instanceof Error
|
|
3819
|
+
? cause.message
|
|
3820
|
+
: "Failed to load file refactor resolution snapshot",
|
|
3821
|
+
cause
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3374
3826
|
/**
|
|
3375
3827
|
* Set semantic edges for a document.
|
|
3376
3828
|
* Replaces edges from the given source.
|