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 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/lint-report-<date>.md` |
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/lint-report-YYYY-MM-DD.md`.
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', `lint-report-${date}.md`)
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
- /** Absolute path of one routed page. @param {string} type - page type. @param {string} title - page title (also the filename). */
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
- const pagesNow = await collectMarkdown(join(this.root, 'wiki'))
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 collectMarkdown(join(this.root, 'wiki'))
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' || /^lint-report-/.test(page.name)) continue
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 collectMarkdown(join(this.root, 'wiki'))
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
- lines.push('', heading, `- [[${title}]]: ${summary.replace(/\n/g, ' ')}`)
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.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",