dsh-plugin-wiki-tools 0.11.0 → 0.12.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.
Files changed (3) hide show
  1. package/index.js +7 -14
  2. package/lib/vault.js +72 -4
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -164,23 +164,16 @@ export function createTools(vault, options = {}) {
164
164
  },
165
165
  render: (args, value) => [{
166
166
  type: 'text',
167
- text: typeof value === 'object' && value !== null && 'path' in value
168
- ? `wiki_write: ${value.created ? 'created' : 'updated'} ${value.path}`
169
- : `wiki_write: skipped ${args.title} (source unchanged)`,
167
+ text: typeof value === 'object' && value !== null && value.alreadyIngested === true
168
+ ? `wiki_write: skipped ${args.title} source hash unchanged (pass force: true to re-ingest)`
169
+ : typeof value === 'object' && value !== null && 'path' in value
170
+ ? `wiki_write: ${value.created ? 'created' : 'updated'} ${value.path}`
171
+ : `wiki_write: skipped ${args.title} (source unchanged)`,
170
172
  }],
171
173
  },
172
174
  async execute(args) {
173
- if (args.source_path !== undefined) {
174
- const tracked = await vault.trackSource({
175
- sourcePath: args.source_path,
176
- pagesCreated: [args.title],
177
- })
178
- if (tracked.alreadyIngested && args.force !== true) {
179
- return { alreadyIngested: true, hash: tracked.hash, title: args.title }
180
- }
181
- }
182
- const { extra_frontmatter: extraFrontmatter, ...rest } = args
183
- return await vault.writePage({ ...rest, extraFrontmatter })
175
+ const { extra_frontmatter: extraFrontmatter, source_path: sourcePath, ...rest } = args
176
+ return await vault.writePage({ ...rest, extraFrontmatter, sourcePath })
184
177
  },
185
178
  presentCall: args => ({ card: 'generic', title: `Write wiki page: ${args.title}`, kind: 'other', rawInput: { title: args.title, type: args.type } }),
186
179
  })
package/lib/vault.js CHANGED
@@ -76,6 +76,18 @@ export function today() {
76
76
  return new Date().toISOString().slice(0, 10)
77
77
  }
78
78
 
79
+ /**
80
+ * Compare a stored manifest key against a normalized vault-relative path.
81
+ * Hand-written entries (the wiki-ingest skill era) may carry the platform's
82
+ * backslash separators; matching must not depend on them.
83
+ * @param {string} storedKey - key as written in `.raw/.manifest.json`.
84
+ * @param {string} rel - normalized `.raw/…` path.
85
+ * @returns {boolean}
86
+ */
87
+ function sameSourceKey(storedKey, rel) {
88
+ return storedKey === rel || storedKey.replace(/\\/g, '/') === rel
89
+ }
90
+
79
91
  /**
80
92
  * One vault root. All bookkeeping mutations go through {@link Vault.writePage},
81
93
  * which completes frontmatter, updates the master index, and prepends a log
@@ -277,9 +289,13 @@ export class Vault {
277
289
  * @param {Record<string, unknown>} [input.extraFrontmatter] - flat schema fields to merge
278
290
  * (related, sources, question, answer_quality, entity_type, aliases, …); must stay flat and
279
291
  * cannot override the managed fields.
280
- * @returns {Promise<{ path: string, created: boolean, title: string }>}
292
+ * @param {string} [input.sourcePath] - vault-relative `.raw/` source this page derives from;
293
+ * registers/refreshes the manifest entry (sha256 delta tracking) and returns `alreadyIngested`
294
+ * without writing when the hash is unchanged.
295
+ * @param {boolean} [input.force] - with sourcePath, write even when the source hash is unchanged.
296
+ * @returns {Promise<{ path: string, created: boolean, title: string } | { alreadyIngested: true, title: string, sourceHash: string, detail: string }>}
281
297
  */
282
- async writePage({ type, title, content, tags, status, summary, extraFrontmatter }) {
298
+ async writePage({ type, title, content, tags, status, summary, extraFrontmatter, sourcePath, force }) {
283
299
  const path = this.pagePath(type, title)
284
300
  return await this.enqueue(path, async () => await this.withFileLock(relative(this.root, path).split(sep).join('/'), async () => {
285
301
  await this.assertRoot()
@@ -290,6 +306,19 @@ export class Vault {
290
306
  if (status !== undefined && !PAGE_STATUSES.includes(status)) {
291
307
  throw new Error(`wiki-tools: status must be one of ${PAGE_STATUSES.join('/')} (got ${JSON.stringify(status)})`)
292
308
  }
309
+ // Source registration is mechanical bookkeeping, not model work: when the
310
+ // page summarizes a `.raw/` source, the manifest entry (sha256 delta
311
+ // tracking) is written by this tool, and an unchanged hash is rejected
312
+ // unless the caller forces a re-ingest.
313
+ if (sourcePath !== undefined) {
314
+ const { hash, alreadyIngested } = await this.inspectSource(sourcePath)
315
+ if (alreadyIngested && force !== true) {
316
+ return {
317
+ alreadyIngested: true, title: cleanTitle, sourceHash: hash,
318
+ detail: 'source hash unchanged; pass force: true to re-ingest, or omit source_path for a plain page write',
319
+ }
320
+ }
321
+ }
293
322
  const date = today()
294
323
  const fields = {
295
324
  ...(existing?.fields ?? {}),
@@ -310,6 +339,13 @@ export class Vault {
310
339
  `- ${existing === undefined ? 'Created' : 'Updated'}: [[${cleanTitle}]]`,
311
340
  ])
312
341
  this.gitCommit(`wiki: ${existing === undefined ? 'create' : 'update'} ${cleanTitle}`)
342
+ if (sourcePath !== undefined) {
343
+ await this.trackSource({
344
+ sourcePath,
345
+ pagesCreated: existing === undefined ? [cleanTitle] : [],
346
+ pagesUpdated: existing === undefined ? [] : [cleanTitle],
347
+ })
348
+ }
313
349
  return { path, created: existing === undefined, title: cleanTitle }
314
350
  }))
315
351
  }
@@ -443,7 +479,9 @@ export class Vault {
443
479
  await rm(absolute)
444
480
  const manifestPath = join(this.root, '.raw', '.manifest.json')
445
481
  const manifest = await readJson(manifestPath, { sources: {} })
446
- delete manifest.sources[rel]
482
+ for (const key of Object.keys(manifest.sources)) {
483
+ if (sameSourceKey(key, rel)) delete manifest.sources[key]
484
+ }
447
485
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
448
486
  await this.prependLog(`## [${today()}] archive | ${rel}`, [
449
487
  `- Archived: \`${rel}\` → \`.archive/${rel.slice('.raw/'.length)}\``,
@@ -559,8 +597,13 @@ export class Vault {
559
597
  const hash = createHash('sha256').update(raw).digest('hex')
560
598
  const manifestPath = join(this.root, '.raw', '.manifest.json')
561
599
  const manifest = await readJson(manifestPath, { sources: {} })
562
- const previous = manifest.sources[rel] ?? manifest.sources[sourcePath]
600
+ let previous
601
+ let twinKey
602
+ for (const [key, entry] of Object.entries(manifest.sources)) {
603
+ if (sameSourceKey(key, rel)) { previous = entry; twinKey = key; break }
604
+ }
563
605
  const alreadyIngested = previous !== undefined && previous.hash === hash
606
+ if (twinKey !== undefined && twinKey !== rel) delete manifest.sources[twinKey]
564
607
  manifest.sources[rel] = {
565
608
  hash,
566
609
  ingested_at: today(),
@@ -572,6 +615,31 @@ export class Vault {
572
615
  return { hash, alreadyIngested }
573
616
  })
574
617
  }
618
+
619
+ /**
620
+ * Hash one raw source and report whether the manifest already carries this
621
+ * exact hash — the wiki-ingest delta check's mechanical half.
622
+ * @param {string} sourcePath - vault-relative (or absolute) `.raw/` path.
623
+ * @returns {Promise<{ rel: string, hash: string, alreadyIngested: boolean }>}
624
+ */
625
+ async inspectSource(sourcePath) {
626
+ const absolute = isAbsolute(sourcePath) ? sourcePath : join(this.root, sourcePath)
627
+ const rel = relative(this.root, absolute).split(sep).join('/')
628
+ if (!rel.startsWith('.raw/')) {
629
+ throw new Error('wiki-tools: source_path must be a vault-relative path under .raw/')
630
+ }
631
+ const raw = await readFile(absolute).catch(error => {
632
+ if (error.code === 'ENOENT') throw new Error(`wiki-tools: source ${sourcePath} not found under the vault`)
633
+ throw error
634
+ })
635
+ const hash = createHash('sha256').update(raw).digest('hex')
636
+ const manifest = await readJson(join(this.root, '.raw', '.manifest.json'), { sources: {} })
637
+ let previous
638
+ for (const [key, entry] of Object.entries(manifest.sources)) {
639
+ if (sameSourceKey(key, rel)) { previous = entry; break }
640
+ }
641
+ return { rel, hash, alreadyIngested: previous !== undefined && previous.hash === hash }
642
+ }
575
643
  }
576
644
 
577
645
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wiki-tools",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",