dsh-plugin-wiki-tools 0.14.0 → 0.14.1
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 +1 -1
- package/lib/lint.js +2 -2
- package/lib/vault.js +51 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The vault layout and operation contracts follow the LLM Wiki pattern (Andrej Kar
|
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| `wiki_query` | Quick mode returns `hot.md` + `index.md` verbatim (the skill's read order); standard mode is full-text search over content pages with snippets and the inbound/outbound link graph |
|
|
18
18
|
| `wiki_write` | Writes one page with complete bookkeeping: type→folder routing, frontmatter completion (keeps `created` and unknown fields on update), filename-uniqueness guard, master-index entry, log entry; with `source_path`, records the source hash and skips unchanged sources unless `force` |
|
|
19
|
-
| `wiki_lint` | Health check: duplicate filenames, dead wikilinks, orphan pages, frontmatter gaps, empty sections, stale index entries, stale hot cache — report only, with suggestions, written to `wiki/meta/
|
|
19
|
+
| `wiki_lint` | Health check: duplicate filenames, dead wikilinks, orphan pages, frontmatter gaps, empty sections, stale index entries, stale hot cache — report only, with suggestions, written to `wiki/meta/Lint Report <date>.md` |
|
|
20
20
|
|
|
21
21
|
## Install and configure
|
|
22
22
|
|
package/lib/lint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vault health checks: the mechanical half of the wiki-lint skill. Report
|
|
3
3
|
* only — auto-fixing is a human decision, so every issue carries a suggestion
|
|
4
|
-
* and the report lands in `wiki/meta/
|
|
4
|
+
* and the report lands in `wiki/meta/Lint Report YYYY-MM-DD.md`.
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-plugin-wiki-tools/lib/lint
|
|
7
7
|
*/
|
|
@@ -289,7 +289,7 @@ async function checkHotCacheStaleness(root, pages, add) {
|
|
|
289
289
|
*/
|
|
290
290
|
async function writeReport(root, issues, pagesScanned) {
|
|
291
291
|
const date = today()
|
|
292
|
-
const path = join(root, 'wiki', 'meta', `
|
|
292
|
+
const path = join(root, 'wiki', 'meta', `Lint Report ${date}.md`)
|
|
293
293
|
const sections = new Map()
|
|
294
294
|
for (const issue of issues) {
|
|
295
295
|
if (!sections.has(issue.check)) sections.set(issue.check, [])
|
package/lib/vault.js
CHANGED
|
@@ -132,9 +132,32 @@ export class Vault {
|
|
|
132
132
|
)
|
|
133
133
|
/** Per-file write chains so concurrent tool calls serialize per target. */
|
|
134
134
|
this.writeChains = new Map()
|
|
135
|
+
/** collectMarkdown result cache: { pages, ts }. Invalidated on every mutation. */
|
|
136
|
+
this._collectCache = null
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Cached wrapper around the module-level collectMarkdown. The cache lives
|
|
141
|
+
* for 30 s or until the next invalidateCollectCache call, whichever comes
|
|
142
|
+
* first. Writes always invalidate first, so a writer never sees stale data
|
|
143
|
+
* from its own session. Cross-process writes are visible on the next call
|
|
144
|
+
* after the cache expires.
|
|
145
|
+
* @returns {Promise<ReturnType<typeof collectMarkdown>>}
|
|
146
|
+
*/
|
|
147
|
+
async collectPages() {
|
|
148
|
+
if (this._collectCache !== null) {
|
|
149
|
+
const { pages, ts } = this._collectCache
|
|
150
|
+
if (Date.now() - ts < 30000) return pages
|
|
151
|
+
}
|
|
152
|
+
const pages = await collectMarkdown(join(this.root, 'wiki'))
|
|
153
|
+
this._collectCache = { pages, ts: Date.now() }
|
|
154
|
+
return pages
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Drop the collectMarkdown cache. Call after every vault mutation. */
|
|
158
|
+
invalidateCollectCache() { this._collectCache = null }
|
|
159
|
+
|
|
160
|
+
/** Absolute path of one routed page. @param {string} type - page type. @param {string} title - page title (also the filename). */
|
|
138
161
|
pagePath(type, title) {
|
|
139
162
|
const folder = this.typeFolders[type]
|
|
140
163
|
if (folder === undefined) throw new Error(`wiki-tools: unknown page type "${type}"`)
|
|
@@ -333,6 +356,7 @@ export class Vault {
|
|
|
333
356
|
const file = `---\n${stringifyYaml(fields).trimEnd()}\n---\n\n${content.replace(/^\s*\n/, '')}\n`
|
|
334
357
|
await mkdir(join(path, '..'), { recursive: true })
|
|
335
358
|
await writeFile(path, file, 'utf8')
|
|
359
|
+
this.invalidateCollectCache()
|
|
336
360
|
await this.updateIndex(type, cleanTitle, summary ?? firstContentLine(content))
|
|
337
361
|
await this.updateFolderIndex(type, cleanTitle, summary ?? firstContentLine(content))
|
|
338
362
|
await this.prependLog(`## [${date}] ${existing === undefined ? 'create' : 'update'} | ${cleanTitle}`, [
|
|
@@ -349,7 +373,11 @@ export class Vault {
|
|
|
349
373
|
// Surface dead-on-arrival wikilinks now, not at the next lint: forward
|
|
350
374
|
// links to pages about to be created are legitimate, so this is a note,
|
|
351
375
|
// never a rejection.
|
|
352
|
-
|
|
376
|
+
// All vault mutations above this point may have invalidated earlier
|
|
377
|
+
// cache entries; force a fresh scan so the unresolved-links report
|
|
378
|
+
// reflects every page the vault actually contains right now.
|
|
379
|
+
this.invalidateCollectCache()
|
|
380
|
+
const pagesNow = await this.collectPages()
|
|
353
381
|
const names = new Set(pagesNow.map(page => page.name))
|
|
354
382
|
const aliases = buildAliasMap(pagesNow)
|
|
355
383
|
const unresolvedLinks = [...new Set(extractWikilinks(content))]
|
|
@@ -414,7 +442,7 @@ export class Vault {
|
|
|
414
442
|
throw new Error(`wiki-tools: newTitle must be a plain filename without path separators (got ${JSON.stringify(newTitle)})`)
|
|
415
443
|
}
|
|
416
444
|
if (cleanNew === title) throw new Error('wiki-tools: newTitle equals the current title')
|
|
417
|
-
const pages = await
|
|
445
|
+
const pages = await this.collectPages()
|
|
418
446
|
const target = pages.find(page => page.name === title)
|
|
419
447
|
if (target === undefined) throw new Error(`wiki-tools: no page named "${title}" exists in the vault`)
|
|
420
448
|
if (isMachineryPage(title)) {
|
|
@@ -430,7 +458,7 @@ export class Vault {
|
|
|
430
458
|
// 1. Rewrite links everywhere except immutable records.
|
|
431
459
|
const rewritten = []
|
|
432
460
|
for (const page of pages) {
|
|
433
|
-
if (page.name === title || page.name.toLowerCase() === 'log' ||
|
|
461
|
+
if (page.name === title || page.name.toLowerCase() === 'log' || isMachineryPage(page.name)) continue
|
|
434
462
|
const raw = await readFile(page.path, 'utf8').catch(() => undefined)
|
|
435
463
|
if (raw === undefined) continue
|
|
436
464
|
// Exact-target boundary: the title must end the link target (`]]`),
|
|
@@ -465,6 +493,7 @@ export class Vault {
|
|
|
465
493
|
`- Renamed: [[${title}]] → [[${cleanNew}]] (${rewritten.length} files' links rewritten)`,
|
|
466
494
|
])
|
|
467
495
|
this.gitCommit(`wiki: rename ${title} -> ${cleanNew}`)
|
|
496
|
+
this.invalidateCollectCache()
|
|
468
497
|
return { from: title, to: cleanNew, path: newPath, linksRewritten: rewritten.length, filesRewritten: rewritten }
|
|
469
498
|
}))
|
|
470
499
|
}
|
|
@@ -503,6 +532,7 @@ export class Vault {
|
|
|
503
532
|
`- Archived: \`${rel}\` → \`.archive/${rel.slice('.raw/'.length)}\``,
|
|
504
533
|
])
|
|
505
534
|
this.gitCommit(`wiki: archive ${rel}`)
|
|
535
|
+
this.invalidateCollectCache()
|
|
506
536
|
return { archivedFrom: rel, archivedTo: `.archive/${rel.slice('.raw/'.length)}` }
|
|
507
537
|
}))
|
|
508
538
|
}
|
|
@@ -530,7 +560,7 @@ export class Vault {
|
|
|
530
560
|
* @param {string} path - the routed destination path.
|
|
531
561
|
*/
|
|
532
562
|
async assertUniqueFilename(title, path) {
|
|
533
|
-
const pages = await
|
|
563
|
+
const pages = await this.collectPages()
|
|
534
564
|
for (const page of pages) {
|
|
535
565
|
if (page.name.toLowerCase() === title.toLowerCase() && page.path !== path) {
|
|
536
566
|
throw new Error(`wiki-tools: filename "${title}.md" already exists at ${page.path}; wikilinks need unique filenames`)
|
|
@@ -560,7 +590,8 @@ export class Vault {
|
|
|
560
590
|
const lines = raw.split('\n')
|
|
561
591
|
let headingLine = lines.findIndex(line => line === heading)
|
|
562
592
|
if (headingLine < 0) {
|
|
563
|
-
|
|
593
|
+
const separator = dominantSeparator(raw)
|
|
594
|
+
lines.push('', heading, `- [[${title}]]${separator} ${summary.replace(/\n/g, ' ')}`)
|
|
564
595
|
await writeFile(indexPath, `${lines.join('\n')}\n`, 'utf8')
|
|
565
596
|
return
|
|
566
597
|
}
|
|
@@ -723,6 +754,20 @@ function indexSeparator(section, matched) {
|
|
|
723
754
|
return dashes > colons ? ' —' : ':'
|
|
724
755
|
}
|
|
725
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Pick the dominant separator style across all index entry lines in one body
|
|
759
|
+
* of text: ` — ` (em-dash) if it appears in more entries than `: `, else colon.
|
|
760
|
+
* Used when a new section has no prior entries to copy style from.
|
|
761
|
+
* @param {string} text - the full index text.
|
|
762
|
+
* @returns {string} either ' —' or ':'.
|
|
763
|
+
*/
|
|
764
|
+
function dominantSeparator(text) {
|
|
765
|
+
const entries = text.split('\n').filter(line => /^- \[\[/.test(line))
|
|
766
|
+
const dashCount = entries.filter(line => line.includes(' — ')).length
|
|
767
|
+
const colonCount = entries.filter(line => line.includes(': ')).length
|
|
768
|
+
return dashCount > colonCount ? ' —' : ':'
|
|
769
|
+
}
|
|
770
|
+
|
|
726
771
|
/**
|
|
727
772
|
* Read a JSON file, returning the fallback when absent.
|
|
728
773
|
* @param {string} path - absolute file path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wiki-tools",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Native DeepSeek Harness tools for an Obsidian wiki vault: wiki_query, wiki_write, and wiki_lint implement the mechanical core (path routing, delta tracking, index/log bookkeeping, health checks) of the wiki skill suite.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|