pi-para 0.6.3 → 0.6.5
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/CHANGELOG.md +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/link-utils.d.ts +4 -12
- package/dist/link-utils.d.ts.map +1 -1
- package/dist/link-utils.js +35 -16
- package/dist/link-utils.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +51 -35
- package/dist/lint.js.map +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +5 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/status.d.ts +5 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +39 -0
- package/dist/status.js.map +1 -1
- package/dist/store.d.ts +7 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +15 -0
- package/dist/store.js.map +1 -1
- package/dist/tools.d.ts +4 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +11 -5
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/link-utils.ts +40 -16
- package/src/lint.ts +49 -33
- package/src/scheduler/index.ts +4 -1
- package/src/status.ts +44 -0
- package/src/store.ts +18 -0
- package/src/tools.ts +16 -4
package/src/lint.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
extractWikilinks,
|
|
25
25
|
removeWikilink,
|
|
26
26
|
autoLinkSlugs,
|
|
27
|
+
findUnlinkedSlugs,
|
|
27
28
|
syncFrontmatterLinks,
|
|
28
29
|
} from "./link-utils.js";
|
|
29
30
|
import { normalizeTags, normalizeScopes } from "./tag-registry.js";
|
|
@@ -415,22 +416,9 @@ function checkLinkSync(
|
|
|
415
416
|
): LintIssue[] {
|
|
416
417
|
const issues: LintIssue[] = [];
|
|
417
418
|
for (const p of pages) {
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
.filter(s => s !== p.slug)
|
|
422
|
-
.sort((a, b) => b.length - a.length);
|
|
423
|
-
|
|
424
|
-
const unlinked: string[] = [];
|
|
425
|
-
for (const slug of sortedSlugs) {
|
|
426
|
-
if (bodyLinks.has(slug)) continue;
|
|
427
|
-
// Check if slug appears as plain text in body (word-boundary match)
|
|
428
|
-
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
429
|
-
const re = new RegExp(`(?<![a-z0-9-])${escaped}(?![a-z0-9-])`);
|
|
430
|
-
if (re.test(p.body)) {
|
|
431
|
-
unlinked.push(slug);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
419
|
+
// Use the exact same protected-range semantics as the fixer so every
|
|
420
|
+
// auto-fixable issue is actionable (no code-block/heading false positives).
|
|
421
|
+
const unlinked = findUnlinkedSlugs(p.body, allSlugs, p.slug);
|
|
434
422
|
|
|
435
423
|
if (unlinked.length > 0) {
|
|
436
424
|
issues.push({
|
|
@@ -603,7 +591,9 @@ async function fixSecrets(
|
|
|
603
591
|
updated: new Date().toISOString(),
|
|
604
592
|
},
|
|
605
593
|
};
|
|
606
|
-
|
|
594
|
+
p.body = fixedPage.body;
|
|
595
|
+
p.frontmatter = fixedPage.frontmatter;
|
|
596
|
+
await writePage(wikiDir, p);
|
|
607
597
|
|
|
608
598
|
fixed.push({
|
|
609
599
|
severity: "error",
|
|
@@ -650,7 +640,9 @@ async function fixLinkSync(
|
|
|
650
640
|
updated: new Date().toISOString(),
|
|
651
641
|
},
|
|
652
642
|
};
|
|
653
|
-
|
|
643
|
+
p.body = fixedPage.body;
|
|
644
|
+
p.frontmatter = fixedPage.frontmatter;
|
|
645
|
+
await writePage(wikiDir, p);
|
|
654
646
|
|
|
655
647
|
if (addedLinks.length > 0) {
|
|
656
648
|
fixed.push({
|
|
@@ -729,7 +721,9 @@ async function fixBrokenLinks(
|
|
|
729
721
|
body: newBody,
|
|
730
722
|
frontmatter: { ...p.frontmatter, links: newFmLinks },
|
|
731
723
|
};
|
|
732
|
-
|
|
724
|
+
p.body = fixedPage.body;
|
|
725
|
+
p.frontmatter = fixedPage.frontmatter;
|
|
726
|
+
await writePage(wikiDir, p);
|
|
733
727
|
|
|
734
728
|
for (const link of broken) {
|
|
735
729
|
fixed.push({
|
|
@@ -931,11 +925,10 @@ export async function lintWiki(
|
|
|
931
925
|
|
|
932
926
|
// Load all pages
|
|
933
927
|
const refs = await listPages(wikiDir);
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
928
|
+
const loadedPages = await Promise.all(
|
|
929
|
+
refs.map((ref) => readPage(wikiDir, ref.category, ref.slug)),
|
|
930
|
+
);
|
|
931
|
+
const pages = loadedPages.filter((page): page is WikiPage => page !== null);
|
|
939
932
|
|
|
940
933
|
// Build slug set
|
|
941
934
|
const allSlugs = new Set(pages.map((p) => p.slug));
|
|
@@ -978,15 +971,38 @@ export async function lintWiki(
|
|
|
978
971
|
// Auto-fix if enabled
|
|
979
972
|
const fixed: LintIssue[] = [];
|
|
980
973
|
if (autoFix) {
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
974
|
+
// Only run a fixer against pages its check identified. Previously every
|
|
975
|
+
// fixer rescanned all P pages (and link sync scanned all S slugs per page)
|
|
976
|
+
// even when that category had zero issues.
|
|
977
|
+
const affectedPages = (category: LintIssue["category"]): WikiPage[] => {
|
|
978
|
+
const paths = new Set(
|
|
979
|
+
issues
|
|
980
|
+
.filter((issue) => issue.category === category && issue.autoFixable && issue.page)
|
|
981
|
+
.map((issue) => issue.page!),
|
|
982
|
+
);
|
|
983
|
+
return paths.size === 0
|
|
984
|
+
? []
|
|
985
|
+
: pages.filter((page) => paths.has(`${page.category}/${page.slug}`));
|
|
986
|
+
};
|
|
987
|
+
const hasIssue = (category: LintIssue["category"]): boolean =>
|
|
988
|
+
issues.some((issue) => issue.category === category && issue.autoFixable);
|
|
989
|
+
|
|
990
|
+
// Fix secrets first — highest priority.
|
|
991
|
+
const secretPages = affectedPages("secrets");
|
|
992
|
+
if (secretPages.length > 0) fixed.push(...(await fixSecrets(wikiDir, secretPages)));
|
|
993
|
+
const brokenPages = affectedPages("broken-link");
|
|
994
|
+
if (brokenPages.length > 0) fixed.push(...(await fixBrokenLinks(wikiDir, brokenPages, allSlugs)));
|
|
995
|
+
const linkPages = affectedPages("link-sync");
|
|
996
|
+
if (linkPages.length > 0) fixed.push(...(await fixLinkSync(wikiDir, linkPages, allSlugs)));
|
|
997
|
+
const tagPages = affectedPages("tag-health");
|
|
998
|
+
if (tagPages.length > 0) fixed.push(...(await fixTagHealth(wikiDir, tagPages)));
|
|
999
|
+
if (hasIssue("index-drift")) fixed.push(...(await fixIndexDrift(wikiDir, pages, indexContent)));
|
|
1000
|
+
const frontmatterPages = affectedPages("frontmatter");
|
|
1001
|
+
if (frontmatterPages.length > 0) fixed.push(...(await fixFrontmatter(wikiDir, frontmatterPages)));
|
|
1002
|
+
const scopePages = affectedPages("scope-drift");
|
|
1003
|
+
if (scopePages.length > 0) fixed.push(...(await fixScopeDrift(wikiDir, scopePages)));
|
|
1004
|
+
const schemaPages = affectedPages("schema-version");
|
|
1005
|
+
if (schemaPages.length > 0) fixed.push(...(await fixSchemaVersion(wikiDir, schemaPages)));
|
|
990
1006
|
}
|
|
991
1007
|
|
|
992
1008
|
// Recount broken links for stats
|
package/src/scheduler/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QMDStore } from "qmd-engine";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { gitCommit, rebuildIndex } from "../wiki.js";
|
|
4
|
-
import { reindex } from "../store.js";
|
|
4
|
+
import { reindex, storeHasApiProviders } from "../store.js";
|
|
5
5
|
import { getParaPaths } from "../paths.js";
|
|
6
6
|
import { acquireLease, releaseLease } from "./leases.js";
|
|
7
7
|
import { SchedulerStateDB, type QueueItem } from "./state.js";
|
|
@@ -155,6 +155,9 @@ export class WikiScheduler {
|
|
|
155
155
|
private async runQmdEmbed(): Promise<void> {
|
|
156
156
|
const store = this.storeProvider();
|
|
157
157
|
if (!store) return;
|
|
158
|
+
// No real embedding provider configured — embed calls would hit the inert
|
|
159
|
+
// shim endpoint (slow TCP timeouts on Windows) and always fail. Skip.
|
|
160
|
+
if (!storeHasApiProviders(store)) return;
|
|
158
161
|
|
|
159
162
|
const status = await store.getStatus();
|
|
160
163
|
const pending = status.needsEmbedding ?? 0;
|
package/src/status.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
2
3
|
import { loadParaConfig } from "./config.js";
|
|
3
4
|
import { getParaPaths } from "./paths.js";
|
|
4
5
|
import { listPages, type ParaCategory } from "./wiki.js";
|
|
@@ -20,6 +21,8 @@ export interface PiParaStatusResult {
|
|
|
20
21
|
};
|
|
21
22
|
scheduler: Record<QueueStatus, number>;
|
|
22
23
|
qmdDbExists: boolean;
|
|
24
|
+
/** Read-only probe of the QMD DB (null when the DB doesn't exist/can't be read). */
|
|
25
|
+
embedding: { hasVectorIndex: boolean; needsEmbedding: number } | null;
|
|
23
26
|
warnings: string[];
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -46,6 +49,7 @@ export async function getPiParaStatus(options: PiParaStatusOptions = {}): Promis
|
|
|
46
49
|
pages,
|
|
47
50
|
scheduler,
|
|
48
51
|
qmdDbExists: existsSync(paths.qmdDbPath),
|
|
52
|
+
embedding: getQmdEmbeddingStats(paths.qmdDbPath, warnings),
|
|
49
53
|
warnings,
|
|
50
54
|
};
|
|
51
55
|
}
|
|
@@ -58,6 +62,9 @@ export function formatPiParaStatus(status: PiParaStatusResult): string {
|
|
|
58
62
|
` Pages: ${status.pages.total} total (${formatCategoryCounts(status.pages.byCategory)})`,
|
|
59
63
|
` Scheduler: ${status.scheduler.queued} queued, ${status.scheduler.running} running, ${status.scheduler.failed} failed`,
|
|
60
64
|
` QMD SDK DB: ${status.qmdDbExists ? status.qmdDbPath : "not initialized"}`,
|
|
65
|
+
...(status.embedding
|
|
66
|
+
? [` Embeddings: vector index ${status.embedding.hasVectorIndex ? "yes" : "no"}, pending ${status.embedding.needsEmbedding}`]
|
|
67
|
+
: []),
|
|
61
68
|
];
|
|
62
69
|
|
|
63
70
|
if (status.warnings.length > 0) {
|
|
@@ -110,6 +117,43 @@ function getSchedulerCounts(dbPath: string, warnings: string[]): Record<QueueSta
|
|
|
110
117
|
return counts;
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Cheap read-only probe of the QMD SQLite DB for embedding state — avoids
|
|
122
|
+
* opening the full store (~1s). Mirrors qmd-engine's getHashesNeedingEmbedding
|
|
123
|
+
* query (documents ⋈ content_vectors), which is stable across qmd 2.x.
|
|
124
|
+
*/
|
|
125
|
+
function getQmdEmbeddingStats(
|
|
126
|
+
dbPath: string,
|
|
127
|
+
warnings: string[],
|
|
128
|
+
): { hasVectorIndex: boolean; needsEmbedding: number } | null {
|
|
129
|
+
if (!existsSync(dbPath)) return null;
|
|
130
|
+
try {
|
|
131
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
132
|
+
try {
|
|
133
|
+
const tables = new Set(
|
|
134
|
+
(db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table','view')").all() as Array<{ name: string }>)
|
|
135
|
+
.map((row) => row.name),
|
|
136
|
+
);
|
|
137
|
+
if (!tables.has("documents") || !tables.has("content_vectors")) {
|
|
138
|
+
return { hasVectorIndex: false, needsEmbedding: 0 };
|
|
139
|
+
}
|
|
140
|
+
const vectors = db.prepare("SELECT COUNT(*) AS c FROM content_vectors").get() as { c: number };
|
|
141
|
+
const pending = db.prepare(`
|
|
142
|
+
SELECT COUNT(DISTINCT d.hash) AS c
|
|
143
|
+
FROM documents d
|
|
144
|
+
LEFT JOIN content_vectors v ON d.hash = v.hash AND v.seq = 0
|
|
145
|
+
WHERE d.active = 1 AND v.hash IS NULL
|
|
146
|
+
`).get() as { c: number };
|
|
147
|
+
return { hasVectorIndex: vectors.c > 0, needsEmbedding: pending.c };
|
|
148
|
+
} finally {
|
|
149
|
+
db.close();
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
warnings.push(`could not read QMD embedding stats: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
function formatCategoryCounts(counts: Record<ParaCategory, number>): string {
|
|
114
158
|
return `projects ${counts.projects}, areas ${counts.areas}, resources ${counts.resources}, archives ${counts.archives}`;
|
|
115
159
|
}
|
package/src/store.ts
CHANGED
|
@@ -73,6 +73,21 @@ const RAW_CONTEXT = "Immutable source material: articles, documents, notes. Not
|
|
|
73
73
|
|
|
74
74
|
const pendingEmbeds = new WeakMap<QMDStore, Promise<void>>();
|
|
75
75
|
|
|
76
|
+
// -- Inert-provider tracking ---------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** Stores opened with the NO_LOCAL_PROVIDERS shim (no real API provider). */
|
|
79
|
+
const inertProviderStores = new WeakSet<QMDStore>();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether the store was opened with a real (configured) embedding provider.
|
|
83
|
+
* When false, embedding calls would hit the inert shim endpoint and fail —
|
|
84
|
+
* slowly on platforms where localhost connects to closed ports time out
|
|
85
|
+
* (Windows firewall drops) — so background embedding must be skipped.
|
|
86
|
+
*/
|
|
87
|
+
export function storeHasApiProviders(store: QMDStore): boolean {
|
|
88
|
+
return !inertProviderStores.has(store);
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
// -- qmd provider config -----------------------------------------------------
|
|
77
92
|
|
|
78
93
|
/**
|
|
@@ -132,6 +147,7 @@ export async function openStore(wikiDir: string, opts: OpenStoreOptions = {}): P
|
|
|
132
147
|
})
|
|
133
148
|
: loadQmdProviders();
|
|
134
149
|
const providers = configuredProviders ?? NO_LOCAL_PROVIDERS;
|
|
150
|
+
const usingInertShim = !configuredProviders;
|
|
135
151
|
|
|
136
152
|
const store = await createStore({
|
|
137
153
|
dbPath: join(wikiDir, ".qmd.sqlite"),
|
|
@@ -152,6 +168,8 @@ export async function openStore(wikiDir: string, opts: OpenStoreOptions = {}): P
|
|
|
152
168
|
},
|
|
153
169
|
});
|
|
154
170
|
|
|
171
|
+
if (usingInertShim) inertProviderStores.add(store);
|
|
172
|
+
|
|
155
173
|
// Add PARA category contexts to the wiki collection
|
|
156
174
|
for (const [prefix, description] of Object.entries(PARA_CONTEXTS)) {
|
|
157
175
|
await store.addContext("wiki", prefix, description);
|
package/src/tools.ts
CHANGED
|
@@ -860,13 +860,23 @@ function createMoveExecute(
|
|
|
860
860
|
|
|
861
861
|
// -- Factory: wiki_lint execute ----------------------------------------------
|
|
862
862
|
|
|
863
|
-
function createLintExecute(
|
|
863
|
+
function createLintExecute(
|
|
864
|
+
wikiDir: string,
|
|
865
|
+
store: QMDStore,
|
|
866
|
+
markDirty: () => void,
|
|
867
|
+
getLintConfig?: () => { autoFix: boolean; staleDays: number },
|
|
868
|
+
) {
|
|
864
869
|
return async (
|
|
865
870
|
params: { autoFix?: boolean },
|
|
866
871
|
): Promise<AgentToolResult<WikiLintDetails>> => {
|
|
872
|
+
const configured = getLintConfig?.();
|
|
867
873
|
const report = await lintWiki(wikiDir, {
|
|
868
|
-
autoFix: params.autoFix ?? true,
|
|
874
|
+
autoFix: params.autoFix ?? configured?.autoFix ?? true,
|
|
875
|
+
staleDays: configured?.staleDays ?? 90,
|
|
869
876
|
});
|
|
877
|
+
if (report.fixed.length > 0) {
|
|
878
|
+
scheduleWikiMaintenance(wikiDir, store, markDirty);
|
|
879
|
+
}
|
|
870
880
|
|
|
871
881
|
const { issues, fixed, stats } = report;
|
|
872
882
|
|
|
@@ -1057,6 +1067,7 @@ export function registerTools(
|
|
|
1057
1067
|
getScope: () => ProjectScope,
|
|
1058
1068
|
markDirty: () => void,
|
|
1059
1069
|
getGraphBoost?: () => boolean,
|
|
1070
|
+
getLintConfig?: () => { autoFix: boolean; staleDays: number },
|
|
1060
1071
|
): void {
|
|
1061
1072
|
const ingestExec = createIngestExecute(wikiDir, store, getScope);
|
|
1062
1073
|
const queryExec = createQueryExecute(wikiDir, store, getScope, getGraphBoost);
|
|
@@ -1064,7 +1075,7 @@ export function registerTools(
|
|
|
1064
1075
|
const editExec = createEditExecute(wikiDir, store, markDirty);
|
|
1065
1076
|
const readExec = createReadExecute(wikiDir);
|
|
1066
1077
|
const moveExec = createMoveExecute(wikiDir, store, markDirty);
|
|
1067
|
-
const lintExec = createLintExecute(wikiDir);
|
|
1078
|
+
const lintExec = createLintExecute(wikiDir, store, markDirty, getLintConfig);
|
|
1068
1079
|
const migrateExec = createMigrateExecute(wikiDir);
|
|
1069
1080
|
const summarizeExec = createSummarizeExecute(wikiDir, getScope);
|
|
1070
1081
|
|
|
@@ -1306,7 +1317,8 @@ export function registerTools(
|
|
|
1306
1317
|
try { return await lintExec(params); } finally { ctx.ui.setStatus("pi-para", undefined); }
|
|
1307
1318
|
},
|
|
1308
1319
|
renderCall(args, theme) {
|
|
1309
|
-
const
|
|
1320
|
+
const configuredAutoFix = getLintConfig?.().autoFix ?? true;
|
|
1321
|
+
const mode = (args.autoFix ?? configuredAutoFix) ? "auto-fix" : "report-only";
|
|
1310
1322
|
return new Text(
|
|
1311
1323
|
theme.fg("toolTitle", theme.bold("wiki_lint ")) +
|
|
1312
1324
|
theme.fg("muted", `[${mode}]`),
|