artifacty 0.2.0 → 0.4.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/AGENTS.md +2 -0
- package/README.md +22 -0
- package/docs/integrations.md +34 -1
- package/docs/mcp-public-api.md +68 -0
- package/package.json +2 -1
- package/src/cli.js +44 -6
- package/src/lib/check.js +29 -2
- package/src/lib/i18n.js +8 -0
- package/src/lib/render.js +88 -2
- package/src/lib/storage.js +438 -30
- package/src/mcp-server.js +337 -7
- package/src/server.js +28 -8
package/src/lib/storage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -112,6 +112,7 @@ export async function writeIndex(store, index) {
|
|
|
112
112
|
const db = openDatabase(store);
|
|
113
113
|
try {
|
|
114
114
|
transaction(db, () => {
|
|
115
|
+
clearSearchIndex(db);
|
|
115
116
|
db.prepare("DELETE FROM artifact_versions").run();
|
|
116
117
|
db.prepare("DELETE FROM artifacts").run();
|
|
117
118
|
for (const artifact of index.artifacts) {
|
|
@@ -120,6 +121,7 @@ export async function writeIndex(store, index) {
|
|
|
120
121
|
insertVersionRecord(db, artifact.id, version);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
124
|
+
rebuildSearchIndexInDb(db, store);
|
|
123
125
|
});
|
|
124
126
|
} finally {
|
|
125
127
|
db.close();
|
|
@@ -155,6 +157,7 @@ export async function createArtifact(store = createStore(), input = {}) {
|
|
|
155
157
|
|
|
156
158
|
insertArtifactRecord(db, artifact);
|
|
157
159
|
insertVersionRecord(db, id, version);
|
|
160
|
+
upsertSearchIndex(db, artifact, version, normalized.content);
|
|
158
161
|
insertAuditRecord(db, {
|
|
159
162
|
action: input.auditAction || "create",
|
|
160
163
|
artifactId: id,
|
|
@@ -209,6 +212,7 @@ export async function updateArtifact(store = createStore(), id, input = {}) {
|
|
|
209
212
|
artifact.id
|
|
210
213
|
);
|
|
211
214
|
insertVersionRecord(db, artifact.id, version);
|
|
215
|
+
upsertSearchIndex(db, artifact, version, normalized.content);
|
|
212
216
|
insertAuditRecord(db, {
|
|
213
217
|
action: input.auditAction || "update",
|
|
214
218
|
artifactId: artifact.id,
|
|
@@ -280,30 +284,316 @@ export async function restoreArtifact(store = createStore(), id, options = {}) {
|
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
export async function listArtifacts(store = createStore(), filters = {}) {
|
|
283
|
-
|
|
287
|
+
return (await listArtifactsPage(store, filters)).artifacts;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function listArtifactsPage(store = createStore(), filters = {}) {
|
|
291
|
+
const db = openDatabase(store);
|
|
284
292
|
const limit = clampInteger(filters.limit, 1, 200, 50);
|
|
285
|
-
const
|
|
293
|
+
const offset = clampInteger(filters.offset, 0, 1_000_000, 0);
|
|
294
|
+
const query = normalizeOptionalString(filters.query);
|
|
295
|
+
const normalizedQuery = query.toLowerCase();
|
|
286
296
|
const tag = normalizeOptionalString(filters.tag).toLowerCase();
|
|
287
297
|
const sourceAgent = normalizeOptionalString(filters.sourceAgent).toLowerCase();
|
|
288
298
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
299
|
+
try {
|
|
300
|
+
if (query && searchIndexAvailable(db)) {
|
|
301
|
+
const ftsQuery = toFtsQuery(query);
|
|
302
|
+
if (ftsQuery) {
|
|
303
|
+
try {
|
|
304
|
+
const page = listArtifactsPageWithFts(db, {
|
|
305
|
+
ftsQuery,
|
|
306
|
+
tag,
|
|
307
|
+
sourceAgent,
|
|
308
|
+
includeArchived: filters.includeArchived,
|
|
309
|
+
limit,
|
|
310
|
+
offset
|
|
311
|
+
});
|
|
312
|
+
if (page.total > 0) {
|
|
313
|
+
return page;
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// Keep search usable even if the SQLite FTS parser rejects a query.
|
|
317
|
+
}
|
|
293
318
|
}
|
|
294
|
-
|
|
295
|
-
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return listArtifactsPageWithSql(db, {
|
|
322
|
+
query: normalizedQuery,
|
|
323
|
+
tag,
|
|
324
|
+
sourceAgent,
|
|
325
|
+
includeArchived: filters.includeArchived,
|
|
326
|
+
limit,
|
|
327
|
+
offset
|
|
328
|
+
});
|
|
329
|
+
} finally {
|
|
330
|
+
db.close();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function rebuildSearchIndex(store = createStore()) {
|
|
335
|
+
const db = openDatabase(store);
|
|
336
|
+
try {
|
|
337
|
+
if (!searchIndexAvailable(db)) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
fts5: false,
|
|
341
|
+
indexed: 0,
|
|
342
|
+
skipped: [],
|
|
343
|
+
message: "SQLite FTS5 is unavailable; metadata search fallback remains active."
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return transaction(db, () => rebuildSearchIndexInDb(db, store));
|
|
348
|
+
} finally {
|
|
349
|
+
db.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function checkStoreIntegrity(store = createStore()) {
|
|
354
|
+
const db = openDatabase(store);
|
|
355
|
+
const checkedAt = new Date().toISOString();
|
|
356
|
+
try {
|
|
357
|
+
const artifacts = loadArtifacts(db);
|
|
358
|
+
const referencedPaths = new Set();
|
|
359
|
+
const missingFiles = [];
|
|
360
|
+
const hashMismatches = [];
|
|
361
|
+
const sizeMismatches = [];
|
|
362
|
+
const dbInconsistencies = [];
|
|
363
|
+
let totalBytes = 0;
|
|
364
|
+
let versionCount = 0;
|
|
365
|
+
|
|
366
|
+
for (const artifact of artifacts) {
|
|
367
|
+
if (!artifact.versions.length) {
|
|
368
|
+
dbInconsistencies.push({
|
|
369
|
+
artifactId: artifact.id,
|
|
370
|
+
issue: "artifact has no version rows"
|
|
371
|
+
});
|
|
296
372
|
}
|
|
297
|
-
if (
|
|
298
|
-
|
|
373
|
+
if (!artifact.versions.some((version) => version.version === artifact.latestVersion)) {
|
|
374
|
+
dbInconsistencies.push({
|
|
375
|
+
artifactId: artifact.id,
|
|
376
|
+
issue: `latest version ${artifact.latestVersion} has no version row`
|
|
377
|
+
});
|
|
299
378
|
}
|
|
300
|
-
|
|
301
|
-
|
|
379
|
+
|
|
380
|
+
for (const version of artifact.versions) {
|
|
381
|
+
versionCount += 1;
|
|
382
|
+
const absolutePath = path.resolve(store.home, version.path);
|
|
383
|
+
referencedPaths.add(absolutePath);
|
|
384
|
+
if (!existsSync(absolutePath)) {
|
|
385
|
+
missingFiles.push({
|
|
386
|
+
artifactId: artifact.id,
|
|
387
|
+
version: version.version,
|
|
388
|
+
path: version.path
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const content = readFileSync(absolutePath);
|
|
394
|
+
const actualSize = content.byteLength;
|
|
395
|
+
const actualSha256 = createHash("sha256").update(content).digest("hex");
|
|
396
|
+
totalBytes += actualSize;
|
|
397
|
+
|
|
398
|
+
if (actualSize !== version.sizeBytes) {
|
|
399
|
+
sizeMismatches.push({
|
|
400
|
+
artifactId: artifact.id,
|
|
401
|
+
version: version.version,
|
|
402
|
+
path: version.path,
|
|
403
|
+
expected: version.sizeBytes,
|
|
404
|
+
actual: actualSize
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (actualSha256 !== version.sha256) {
|
|
408
|
+
hashMismatches.push({
|
|
409
|
+
artifactId: artifact.id,
|
|
410
|
+
version: version.version,
|
|
411
|
+
path: version.path,
|
|
412
|
+
expected: version.sha256,
|
|
413
|
+
actual: actualSha256
|
|
414
|
+
});
|
|
415
|
+
}
|
|
302
416
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.
|
|
306
|
-
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const orphanFiles = listStoreFiles(store.artifactsDir)
|
|
420
|
+
.filter((filePath) => !referencedPaths.has(filePath))
|
|
421
|
+
.map((filePath) => {
|
|
422
|
+
const stat = statSync(filePath);
|
|
423
|
+
return {
|
|
424
|
+
path: path.relative(store.home, filePath),
|
|
425
|
+
sizeBytes: stat.size
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
const orphanBytes = orphanFiles.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
429
|
+
const ok =
|
|
430
|
+
missingFiles.length === 0 &&
|
|
431
|
+
hashMismatches.length === 0 &&
|
|
432
|
+
sizeMismatches.length === 0 &&
|
|
433
|
+
orphanFiles.length === 0 &&
|
|
434
|
+
dbInconsistencies.length === 0;
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
ok,
|
|
438
|
+
checkedAt,
|
|
439
|
+
store: store.home,
|
|
440
|
+
artifactCount: artifacts.length,
|
|
441
|
+
versionCount,
|
|
442
|
+
totalBytes,
|
|
443
|
+
orphanBytes,
|
|
444
|
+
missingFiles,
|
|
445
|
+
hashMismatches,
|
|
446
|
+
sizeMismatches,
|
|
447
|
+
orphanFiles,
|
|
448
|
+
dbInconsistencies
|
|
449
|
+
};
|
|
450
|
+
} finally {
|
|
451
|
+
db.close();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function listArtifactsPageWithSql(db, filters) {
|
|
456
|
+
const { clauses, params } = artifactWhereClauses(filters);
|
|
457
|
+
if (filters.query) {
|
|
458
|
+
const like = `%${escapeLike(filters.query)}%`;
|
|
459
|
+
clauses.push(`(
|
|
460
|
+
LOWER(id) LIKE ? ESCAPE '\\' OR
|
|
461
|
+
LOWER(title) LIKE ? ESCAPE '\\' OR
|
|
462
|
+
LOWER(source_agent) LIKE ? ESCAPE '\\' OR
|
|
463
|
+
LOWER(tags_json) LIKE ? ESCAPE '\\'
|
|
464
|
+
)`);
|
|
465
|
+
params.push(like, like, like, like);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const whereSql = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
469
|
+
const total = db.prepare(`SELECT COUNT(*) AS total FROM artifacts ${whereSql}`).get(...params).total;
|
|
470
|
+
const rows = db.prepare(`
|
|
471
|
+
SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
472
|
+
FROM artifacts
|
|
473
|
+
${whereSql}
|
|
474
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
475
|
+
LIMIT ? OFFSET ?
|
|
476
|
+
`).all(...params, filters.limit, filters.offset);
|
|
477
|
+
|
|
478
|
+
return pagedResult({
|
|
479
|
+
artifacts: rows.map((row) => toArtifactSummary(artifactFromRow(db, row))),
|
|
480
|
+
total,
|
|
481
|
+
limit: filters.limit,
|
|
482
|
+
offset: filters.offset,
|
|
483
|
+
searchBackend: filters.query ? "metadata" : "sqlite"
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function listArtifactsPageWithFts(db, filters) {
|
|
488
|
+
const { clauses, params } = artifactWhereClauses(filters, "a");
|
|
489
|
+
clauses.unshift("artifact_search MATCH ?");
|
|
490
|
+
params.unshift(filters.ftsQuery);
|
|
491
|
+
const whereSql = `WHERE ${clauses.join(" AND ")}`;
|
|
492
|
+
const total = db.prepare(`
|
|
493
|
+
SELECT COUNT(*) AS total
|
|
494
|
+
FROM artifact_search
|
|
495
|
+
JOIN artifacts a ON a.id = artifact_search.artifact_id
|
|
496
|
+
${whereSql}
|
|
497
|
+
`).get(...params).total;
|
|
498
|
+
const rows = db.prepare(`
|
|
499
|
+
SELECT
|
|
500
|
+
a.id,
|
|
501
|
+
a.title,
|
|
502
|
+
a.artifact_type,
|
|
503
|
+
a.schema_version,
|
|
504
|
+
a.source_agent,
|
|
505
|
+
a.tags_json,
|
|
506
|
+
a.created_at,
|
|
507
|
+
a.updated_at,
|
|
508
|
+
a.latest_version,
|
|
509
|
+
a.archived_at,
|
|
510
|
+
bm25(artifact_search) AS search_rank,
|
|
511
|
+
snippet(artifact_search, 7, '', '', '...', 24) AS search_snippet
|
|
512
|
+
FROM artifact_search
|
|
513
|
+
JOIN artifacts a ON a.id = artifact_search.artifact_id
|
|
514
|
+
${whereSql}
|
|
515
|
+
ORDER BY search_rank ASC, a.updated_at DESC, a.created_at DESC
|
|
516
|
+
LIMIT ? OFFSET ?
|
|
517
|
+
`).all(...params, filters.limit, filters.offset);
|
|
518
|
+
|
|
519
|
+
return pagedResult({
|
|
520
|
+
artifacts: rows.map((row) => ({
|
|
521
|
+
...toArtifactSummary(artifactFromRow(db, row)),
|
|
522
|
+
searchScore: row.search_rank,
|
|
523
|
+
searchSnippet: normalizeWhitespace(row.search_snippet)
|
|
524
|
+
})),
|
|
525
|
+
total,
|
|
526
|
+
limit: filters.limit,
|
|
527
|
+
offset: filters.offset,
|
|
528
|
+
searchBackend: "fts5"
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function artifactWhereClauses(filters, alias = "") {
|
|
533
|
+
const prefix = alias ? `${alias}.` : "";
|
|
534
|
+
const clauses = [];
|
|
535
|
+
const params = [];
|
|
536
|
+
|
|
537
|
+
if (!filters.includeArchived) {
|
|
538
|
+
clauses.push(`${prefix}archived_at IS NULL`);
|
|
539
|
+
}
|
|
540
|
+
if (filters.tag) {
|
|
541
|
+
clauses.push(`LOWER(${prefix}tags_json) LIKE ? ESCAPE '\\'`);
|
|
542
|
+
params.push(`%"${escapeLike(filters.tag)}"%`);
|
|
543
|
+
}
|
|
544
|
+
if (filters.sourceAgent) {
|
|
545
|
+
clauses.push(`LOWER(${prefix}source_agent) = ?`);
|
|
546
|
+
params.push(filters.sourceAgent);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return { clauses, params };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function pagedResult({ artifacts, total, limit, offset, searchBackend }) {
|
|
553
|
+
return {
|
|
554
|
+
artifacts,
|
|
555
|
+
total,
|
|
556
|
+
limit,
|
|
557
|
+
offset,
|
|
558
|
+
hasMore: offset + artifacts.length < total,
|
|
559
|
+
nextOffset: offset + artifacts.length < total ? offset + limit : null,
|
|
560
|
+
previousOffset: offset > 0 ? Math.max(0, offset - limit) : null,
|
|
561
|
+
search: {
|
|
562
|
+
backend: searchBackend
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function artifactFromRow(db, row) {
|
|
568
|
+
return {
|
|
569
|
+
id: row.id,
|
|
570
|
+
title: row.title,
|
|
571
|
+
artifactType: row.artifact_type,
|
|
572
|
+
schemaVersion: row.schema_version,
|
|
573
|
+
sourceAgent: row.source_agent,
|
|
574
|
+
tags: parseJson(row.tags_json, []),
|
|
575
|
+
createdAt: row.created_at,
|
|
576
|
+
updatedAt: row.updated_at,
|
|
577
|
+
archivedAt: row.archived_at,
|
|
578
|
+
latestVersion: row.latest_version,
|
|
579
|
+
versions: loadVersions(db, row.id)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function escapeLike(value) {
|
|
584
|
+
return String(value).replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function normalizeWhitespace(value) {
|
|
588
|
+
return normalizeOptionalString(value).replace(/\s+/g, " ");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function toFtsQuery(value) {
|
|
592
|
+
const tokens = normalizeOptionalString(value).match(/[\p{L}\p{N}_-]+/gu) || [];
|
|
593
|
+
return tokens
|
|
594
|
+
.slice(0, 12)
|
|
595
|
+
.map((token) => `"${token.replaceAll("\"", "\"\"")}"`)
|
|
596
|
+
.join(" AND ");
|
|
307
597
|
}
|
|
308
598
|
|
|
309
599
|
export async function getArtifact(store = createStore(), id, options = {}) {
|
|
@@ -458,6 +748,7 @@ function openDatabase(store) {
|
|
|
458
748
|
`);
|
|
459
749
|
initializeSchema(db);
|
|
460
750
|
migrateJsonIndex(db, store);
|
|
751
|
+
syncSearchIndexIfEmpty(db, store);
|
|
461
752
|
return db;
|
|
462
753
|
}
|
|
463
754
|
|
|
@@ -516,6 +807,7 @@ function initializeSchema(db) {
|
|
|
516
807
|
ensureColumn(db, "artifacts", "schema_version", "INTEGER NOT NULL DEFAULT 1");
|
|
517
808
|
ensureColumn(db, "artifacts", "archived_at", "TEXT");
|
|
518
809
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('store_version', ?)").run(String(STORE_VERSION));
|
|
810
|
+
ensureSearchTable(db);
|
|
519
811
|
}
|
|
520
812
|
|
|
521
813
|
function migrateJsonIndex(db, store) {
|
|
@@ -551,6 +843,135 @@ function migrateJsonIndex(db, store) {
|
|
|
551
843
|
});
|
|
552
844
|
}
|
|
553
845
|
|
|
846
|
+
function ensureSearchTable(db) {
|
|
847
|
+
try {
|
|
848
|
+
db.exec(`
|
|
849
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS artifact_search USING fts5(
|
|
850
|
+
artifact_id UNINDEXED,
|
|
851
|
+
title,
|
|
852
|
+
source_agent,
|
|
853
|
+
artifact_type,
|
|
854
|
+
tags,
|
|
855
|
+
format,
|
|
856
|
+
metadata,
|
|
857
|
+
content
|
|
858
|
+
);
|
|
859
|
+
`);
|
|
860
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('fts5_enabled', 'true')").run();
|
|
861
|
+
return true;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('fts5_enabled', ?)").run(`false:${error.message}`);
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function searchIndexAvailable(db) {
|
|
869
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifact_search'").get();
|
|
870
|
+
return Boolean(row) || ensureSearchTable(db);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function syncSearchIndexIfEmpty(db, store) {
|
|
874
|
+
if (!searchIndexAvailable(db)) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const artifactCount = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get().count;
|
|
878
|
+
if (artifactCount === 0) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const indexedCount = db.prepare("SELECT COUNT(*) AS count FROM artifact_search").get().count;
|
|
882
|
+
if (indexedCount === 0) {
|
|
883
|
+
transaction(db, () => rebuildSearchIndexInDb(db, store));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function clearSearchIndex(db) {
|
|
888
|
+
if (searchIndexAvailable(db)) {
|
|
889
|
+
db.prepare("DELETE FROM artifact_search").run();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function rebuildSearchIndexInDb(db, store) {
|
|
894
|
+
if (!searchIndexAvailable(db)) {
|
|
895
|
+
return {
|
|
896
|
+
ok: false,
|
|
897
|
+
fts5: false,
|
|
898
|
+
indexed: 0,
|
|
899
|
+
skipped: []
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
db.prepare("DELETE FROM artifact_search").run();
|
|
904
|
+
const skipped = [];
|
|
905
|
+
let indexed = 0;
|
|
906
|
+
for (const artifact of loadArtifacts(db)) {
|
|
907
|
+
const latest = artifact.versions.find((version) => version.version === artifact.latestVersion);
|
|
908
|
+
if (!latest) {
|
|
909
|
+
skipped.push({ artifactId: artifact.id, reason: "latest version row missing" });
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const absolutePath = path.join(store.home, latest.path);
|
|
913
|
+
if (!existsSync(absolutePath)) {
|
|
914
|
+
skipped.push({ artifactId: artifact.id, version: latest.version, reason: "version file missing" });
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
upsertSearchIndex(db, artifact, latest, readFileSync(absolutePath, "utf8"));
|
|
918
|
+
indexed += 1;
|
|
919
|
+
}
|
|
920
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('search_index_built_at', ?)").run(new Date().toISOString());
|
|
921
|
+
return {
|
|
922
|
+
ok: skipped.length === 0,
|
|
923
|
+
fts5: true,
|
|
924
|
+
indexed,
|
|
925
|
+
skipped
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function upsertSearchIndex(db, artifact, version, content) {
|
|
930
|
+
if (!searchIndexAvailable(db)) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
db.prepare("DELETE FROM artifact_search WHERE artifact_id = ?").run(artifact.id);
|
|
934
|
+
db.prepare(`
|
|
935
|
+
INSERT INTO artifact_search (
|
|
936
|
+
artifact_id, title, source_agent, artifact_type, tags, format, metadata, content
|
|
937
|
+
)
|
|
938
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
939
|
+
`).run(
|
|
940
|
+
artifact.id,
|
|
941
|
+
artifact.title,
|
|
942
|
+
artifact.sourceAgent,
|
|
943
|
+
artifact.artifactType,
|
|
944
|
+
artifact.tags.join(" "),
|
|
945
|
+
version.format,
|
|
946
|
+
metadataSearchText(version.metadata),
|
|
947
|
+
content
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function metadataSearchText(metadata) {
|
|
952
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
953
|
+
return "";
|
|
954
|
+
}
|
|
955
|
+
return JSON.stringify(metadata).slice(0, 64 * 1024);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function listStoreFiles(root) {
|
|
959
|
+
if (!existsSync(root)) {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const files = [];
|
|
964
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
965
|
+
const fullPath = path.join(root, entry.name);
|
|
966
|
+
if (entry.isDirectory()) {
|
|
967
|
+
files.push(...listStoreFiles(fullPath));
|
|
968
|
+
} else if (entry.isFile()) {
|
|
969
|
+
files.push(path.resolve(fullPath));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return files;
|
|
973
|
+
}
|
|
974
|
+
|
|
554
975
|
function loadArtifacts(db) {
|
|
555
976
|
const rows = db.prepare(`
|
|
556
977
|
SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
@@ -914,19 +1335,6 @@ function makeArtifactId(title) {
|
|
|
914
1335
|
return `${slug}-${randomUUID().slice(0, 8)}`;
|
|
915
1336
|
}
|
|
916
1337
|
|
|
917
|
-
function artifactMatchesQuery(artifact, query) {
|
|
918
|
-
const haystack = [
|
|
919
|
-
artifact.id,
|
|
920
|
-
artifact.title,
|
|
921
|
-
artifact.sourceAgent,
|
|
922
|
-
...artifact.tags
|
|
923
|
-
]
|
|
924
|
-
.join(" ")
|
|
925
|
-
.toLowerCase();
|
|
926
|
-
|
|
927
|
-
return haystack.includes(query);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
1338
|
function clampInteger(value, min, max, fallback) {
|
|
931
1339
|
const parsed = Number.parseInt(value, 10);
|
|
932
1340
|
if (Number.isNaN(parsed)) {
|