dsh-plugin-wiki-tools 0.13.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 +21 -3
- package/lib/vault.js +54 -7
- 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,12 +1,12 @@
|
|
|
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
|
*/
|
|
8
8
|
|
|
9
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
9
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import {
|
|
12
12
|
PAGE_STATUSES,
|
|
@@ -39,6 +39,7 @@ export async function lintVault(root) {
|
|
|
39
39
|
checkOrphans(pages, inbound, indexed, add)
|
|
40
40
|
checkFrontmatterGaps(pages, add)
|
|
41
41
|
checkStatusVocabulary(pages, add)
|
|
42
|
+
await checkStructurePages(root, add)
|
|
42
43
|
checkEmptySections(pages, add)
|
|
43
44
|
await checkHotCacheStaleness(root, pages, add)
|
|
44
45
|
|
|
@@ -193,6 +194,23 @@ function checkStatusVocabulary(pages, add) {
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
/**
|
|
198
|
+
* The narrative structure page every vault layout expects. `overview.md` is
|
|
199
|
+
* human-maintained, so its absence is info — a nudge, not a defect.
|
|
200
|
+
* @param {string} root - absolute vault root.
|
|
201
|
+
* @param {Add} add - issue recorder.
|
|
202
|
+
*/
|
|
203
|
+
async function checkStructurePages(root, add) {
|
|
204
|
+
const overview = join(root, 'wiki', 'overview.md')
|
|
205
|
+
const exists = await stat(overview).then(() => true, () => false)
|
|
206
|
+
if (!exists) {
|
|
207
|
+
add('missing-structure', 'info', 'overview',
|
|
208
|
+
'wiki/overview.md does not exist',
|
|
209
|
+
'Run wiki_scaffold (generic) or write a ~200-word vault overview: what the vault is for, its areas, and where things go',
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
196
214
|
/**
|
|
197
215
|
* Headings with no content before the next heading.
|
|
198
216
|
* @param {Pages} pages - collected pages.
|
|
@@ -271,7 +289,7 @@ async function checkHotCacheStaleness(root, pages, add) {
|
|
|
271
289
|
*/
|
|
272
290
|
async function writeReport(root, issues, pagesScanned) {
|
|
273
291
|
const date = today()
|
|
274
|
-
const path = join(root, 'wiki', 'meta', `
|
|
292
|
+
const path = join(root, 'wiki', 'meta', `Lint Report ${date}.md`)
|
|
275
293
|
const sections = new Map()
|
|
276
294
|
for (const issue of issues) {
|
|
277
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 (`]]`),
|
|
@@ -439,7 +467,9 @@ export class Vault {
|
|
|
439
467
|
// `\]` / `\|` would be eaten by the template literal and leave an
|
|
440
468
|
// empty alternation branch matching every `[[title` prefix.
|
|
441
469
|
const pattern = new RegExp(`\\[\\[${escapeRegExp(title)}(\\]\\]|\\||#)`, 'g')
|
|
442
|
-
|
|
470
|
+
let updated = raw.replace(pattern, `[[${cleanNew}$1`)
|
|
471
|
+
// A page whose links changed today is not "current" as of yesterday.
|
|
472
|
+
if (updated !== raw) updated = updated.replace(/^(updated:\s*).+$/m, `$1${today()}`)
|
|
443
473
|
if (updated !== raw) {
|
|
444
474
|
await writeFile(page.path, updated, 'utf8')
|
|
445
475
|
rewritten.push(page.name)
|
|
@@ -463,6 +493,7 @@ export class Vault {
|
|
|
463
493
|
`- Renamed: [[${title}]] → [[${cleanNew}]] (${rewritten.length} files' links rewritten)`,
|
|
464
494
|
])
|
|
465
495
|
this.gitCommit(`wiki: rename ${title} -> ${cleanNew}`)
|
|
496
|
+
this.invalidateCollectCache()
|
|
466
497
|
return { from: title, to: cleanNew, path: newPath, linksRewritten: rewritten.length, filesRewritten: rewritten }
|
|
467
498
|
}))
|
|
468
499
|
}
|
|
@@ -501,6 +532,7 @@ export class Vault {
|
|
|
501
532
|
`- Archived: \`${rel}\` → \`.archive/${rel.slice('.raw/'.length)}\``,
|
|
502
533
|
])
|
|
503
534
|
this.gitCommit(`wiki: archive ${rel}`)
|
|
535
|
+
this.invalidateCollectCache()
|
|
504
536
|
return { archivedFrom: rel, archivedTo: `.archive/${rel.slice('.raw/'.length)}` }
|
|
505
537
|
}))
|
|
506
538
|
}
|
|
@@ -528,7 +560,7 @@ export class Vault {
|
|
|
528
560
|
* @param {string} path - the routed destination path.
|
|
529
561
|
*/
|
|
530
562
|
async assertUniqueFilename(title, path) {
|
|
531
|
-
const pages = await
|
|
563
|
+
const pages = await this.collectPages()
|
|
532
564
|
for (const page of pages) {
|
|
533
565
|
if (page.name.toLowerCase() === title.toLowerCase() && page.path !== path) {
|
|
534
566
|
throw new Error(`wiki-tools: filename "${title}.md" already exists at ${page.path}; wikilinks need unique filenames`)
|
|
@@ -558,7 +590,8 @@ export class Vault {
|
|
|
558
590
|
const lines = raw.split('\n')
|
|
559
591
|
let headingLine = lines.findIndex(line => line === heading)
|
|
560
592
|
if (headingLine < 0) {
|
|
561
|
-
|
|
593
|
+
const separator = dominantSeparator(raw)
|
|
594
|
+
lines.push('', heading, `- [[${title}]]${separator} ${summary.replace(/\n/g, ' ')}`)
|
|
562
595
|
await writeFile(indexPath, `${lines.join('\n')}\n`, 'utf8')
|
|
563
596
|
return
|
|
564
597
|
}
|
|
@@ -721,6 +754,20 @@ function indexSeparator(section, matched) {
|
|
|
721
754
|
return dashes > colons ? ' —' : ':'
|
|
722
755
|
}
|
|
723
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
|
+
|
|
724
771
|
/**
|
|
725
772
|
* Read a JSON file, returning the fallback when absent.
|
|
726
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.
|
|
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",
|