dsh-plugin-wiki-tools 0.10.0 → 0.11.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/lib/lint.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
10
10
  import { join } from 'node:path'
11
11
  import {
12
+ PAGE_STATUSES,
12
13
  buildAliasMap,
13
14
  collectMarkdown,
14
15
  isMachineryPage,
@@ -37,6 +38,7 @@ export async function lintVault(root) {
37
38
  const inbound = checkDeadLinks(pages, add)
38
39
  checkOrphans(pages, inbound, indexed, add)
39
40
  checkFrontmatterGaps(pages, add)
41
+ checkStatusVocabulary(pages, add)
40
42
  checkEmptySections(pages, add)
41
43
  await checkHotCacheStaleness(root, pages, add)
42
44
 
@@ -172,6 +174,25 @@ function checkFrontmatterGaps(pages, add) {
172
174
  }
173
175
  }
174
176
 
177
+ /**
178
+ * Status values outside the lifecycle vocabulary (seed/developing/mature/
179
+ * evergreen). A stray value silently breaks status-based queries and
180
+ * promotion flows, so it is flagged even though links stay intact.
181
+ * @param {Pages} pages - collected pages.
182
+ * @param {Add} add - issue recorder.
183
+ */
184
+ function checkStatusVocabulary(pages, add) {
185
+ for (const page of pages) {
186
+ if (isMachineryPage(page.name)) continue
187
+ const status = page.fields?.status
188
+ if (typeof status === 'string' && !PAGE_STATUSES.includes(status)) {
189
+ add('status-vocabulary', 'warn', page.name,
190
+ `status "${status}" is outside the lifecycle vocabulary (${PAGE_STATUSES.join('/')})`,
191
+ `Set status to one of ${PAGE_STATUSES.join('/')}`)
192
+ }
193
+ }
194
+ }
195
+
175
196
  /**
176
197
  * Headings with no content before the next heading.
177
198
  * @param {Pages} pages - collected pages.
package/lib/vault.js CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { createHash } from 'node:crypto'
15
15
  import { spawnSync } from 'node:child_process'
16
+ import { appendFileSync, readFileSync } from 'node:fs'
16
17
  import { mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
17
18
  import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path'
18
19
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
@@ -31,6 +32,9 @@ export const TYPE_FOLDERS = {
31
32
  meta: 'wiki/meta',
32
33
  }
33
34
 
35
+ /** Lifecycle status vocabulary; writePage rejects anything else and lint flags it. */
36
+ export const PAGE_STATUSES = ['seed', 'developing', 'mature', 'evergreen']
37
+
34
38
  /** Master-index section headings follow the mapped folder basename, computed per vault. */
35
39
 
36
40
  /**
@@ -227,6 +231,7 @@ export class Vault {
227
231
  gitCommit(message) {
228
232
  if (!this.gitAutoCommit) return
229
233
  const run = args => spawnSync('git', ['-C', this.root, ...args], { encoding: 'utf8' })
234
+ this.ignoreLockFiles()
230
235
  const add = run(['add', '-A'])
231
236
  if (add.status !== 0) throw new Error(`wiki-tools: git add failed in the vault: ${String(add.stderr).trim()}`)
232
237
  const pending = run(['diff', '--cached', '--quiet'])
@@ -236,6 +241,27 @@ export class Vault {
236
241
  }
237
242
  }
238
243
 
244
+ /**
245
+ * Keep advisory lock files out of vault history: cross-process locks under
246
+ * `.vault-meta/locks/` are pure runtime coordination, never vault state.
247
+ * Idempotent; creates or appends the vault's `.gitignore` as needed.
248
+ * @returns {void}
249
+ */
250
+ ignoreLockFiles() {
251
+ const marker = '.vault-meta/locks/'
252
+ const gitignore = join(this.root, '.gitignore')
253
+ let current = ''
254
+ try {
255
+ current = readFileSync(gitignore, 'utf8')
256
+ } catch (error) {
257
+ if (error.code !== 'ENOENT') throw error
258
+ }
259
+ if (!current.split('\n').some(line => line.trim() === marker)) {
260
+ const prefix = current.length === 0 || current.endsWith('\n') ? '' : '\n'
261
+ appendFileSync(gitignore, `${prefix}# wiki-tools runtime locks\n${marker}\n`, 'utf8')
262
+ }
263
+ }
264
+
239
265
  /**
240
266
  * Write one wiki page with complete bookkeeping: frontmatter completion,
241
267
  * filename-uniqueness guard, master-index entry, folder `_index.md` entry,
@@ -261,6 +287,9 @@ export class Vault {
261
287
  const existing = await this.readPage(path)
262
288
  await this.assertUniqueFilename(cleanTitle, path)
263
289
  validateExtraFrontmatter(extraFrontmatter)
290
+ if (status !== undefined && !PAGE_STATUSES.includes(status)) {
291
+ throw new Error(`wiki-tools: status must be one of ${PAGE_STATUSES.join('/')} (got ${JSON.stringify(status)})`)
292
+ }
264
293
  const date = today()
265
294
  const fields = {
266
295
  ...(existing?.fields ?? {}),
@@ -354,7 +383,12 @@ export class Vault {
354
383
  if (page.name === title || page.name.toLowerCase() === 'log' || /^lint-report-/.test(page.name)) continue
355
384
  const raw = await readFile(page.path, 'utf8').catch(() => undefined)
356
385
  if (raw === undefined) continue
357
- const pattern = new RegExp(`\\[\\[${escapeRegExp(title)}(\]\]|\||#)`, 'g')
386
+ // Exact-target boundary: the title must end the link target (`]]`),
387
+ // start an alias (`|`), or start an anchor (`#`). Escapes are doubled
388
+ // because this is a string-built RegExp, not a regex literal: single
389
+ // `\]` / `\|` would be eaten by the template literal and leave an
390
+ // empty alternation branch matching every `[[title` prefix.
391
+ const pattern = new RegExp(`\\[\\[${escapeRegExp(title)}(\\]\\]|\\||#)`, 'g')
358
392
  const updated = raw.replace(pattern, `[[${cleanNew}$1`)
359
393
  if (updated !== raw) {
360
394
  await writeFile(page.path, updated, 'utf8')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wiki-tools",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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",