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 +21 -0
- package/lib/vault.js +35 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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",
|