@viloforge/vfkb 0.5.0 → 0.6.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.
@@ -32021,7 +32021,7 @@ function queryExplained(opts = {}) {
32021
32021
  }
32022
32022
 
32023
32023
  // src/version.ts
32024
- var ENGINE_VERSION = true ? "0.5.0" : ownPackageVersion();
32024
+ var ENGINE_VERSION = true ? "0.6.0" : ownPackageVersion();
32025
32025
 
32026
32026
  // src/mcp-server.ts
32027
32027
  var SEARCH_DEFAULT_LIMIT = 25;
@@ -16091,8 +16091,8 @@ import { dirname as dirname5, join as join8 } from "node:path";
16091
16091
 
16092
16092
  // src/version.ts
16093
16093
  var SCHEMA_VERSION = 1;
16094
- var ENGINE_VERSION = true ? "0.5.0" : ownPackageVersion();
16095
- var ENGINE_COMMIT = true ? "aeaa5fa" : "dev";
16094
+ var ENGINE_VERSION = true ? "0.6.0" : ownPackageVersion();
16095
+ var ENGINE_COMMIT = true ? "9921586" : "dev";
16096
16096
 
16097
16097
  // src/manifest.ts
16098
16098
  function manifestPath(brainDir2) {
@@ -16850,7 +16850,8 @@ function initProject(root, opts = {}) {
16850
16850
  ".vfkb/.sessions/",
16851
16851
  ".vfkb/.signals/",
16852
16852
  ".vfkb/.journal/",
16853
- ".vfkb/.lock"
16853
+ ".vfkb/.lock",
16854
+ ".vfkb/.write-probe-*"
16854
16855
  ];
16855
16856
  const HEADER = [
16856
16857
  "# vfkb \u2014 entries.jsonl + manifest.json (the ADR-0030 engine stamp) are committed;",
@@ -16925,7 +16926,8 @@ function approvalNotice(project) {
16925
16926
 
16926
16927
  // src/doctor.ts
16927
16928
  import { execFileSync as execFileSync6 } from "node:child_process";
16928
- import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, realpathSync } from "node:fs";
16929
+ import { randomBytes as randomBytes3 } from "node:crypto";
16930
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, realpathSync, unlinkSync as unlinkSync2 } from "node:fs";
16929
16931
  import { join as join14, dirname as dirname6, relative as relative3, resolve as resolve4, isAbsolute as isAbsolute2 } from "node:path";
16930
16932
  function readJson2(path) {
16931
16933
  if (!existsSync10(path)) return void 0;
@@ -17193,6 +17195,29 @@ function runDoctor(opts) {
17193
17195
  add("engine drift", "warn", `brain last stamped by engine ${mf.engine_commit}, running ${runningCommit} \u2014 possible dual-clone drift`);
17194
17196
  }
17195
17197
  }
17198
+ {
17199
+ const probe = join14(brainDir2, `.write-probe-${process.pid}-${randomBytes3(4).toString("hex")}`);
17200
+ const payload = `vfkb write-probe ${process.pid} ${Date.now()}`;
17201
+ let failure;
17202
+ try {
17203
+ mkdirSync8(brainDir2, { recursive: true });
17204
+ writeFileSync6(probe, payload);
17205
+ if (readFileSync11(probe, "utf8") !== payload) failure = "wrote the probe file but read back different bytes";
17206
+ } catch (e) {
17207
+ failure = e.message;
17208
+ } finally {
17209
+ try {
17210
+ if (existsSync10(probe)) unlinkSync2(probe);
17211
+ } catch {
17212
+ }
17213
+ }
17214
+ const SCOPE = " \u2014 scope: this is the CLI/engine/filesystem path only; it does NOT check the MCP server, so it cannot tell you whether kb_* capture is reaching the brain (ADR-0065 \xA70: a hung MCP server loses a write with no error at all)";
17215
+ if (failure) {
17216
+ add("write-health (filesystem)", "fail", `cannot write to ${brainDir2}: ${failure}${SCOPE}`);
17217
+ } else {
17218
+ add("write-health (filesystem)", "ok", `round-trip verified in ${brainDir2}${SCOPE}`);
17219
+ }
17220
+ }
17196
17221
  {
17197
17222
  const js = journalStatus(brainDir2);
17198
17223
  if (js.suppressedInEntries > 0) {
package/dist/doctor.js CHANGED
@@ -3,7 +3,8 @@
3
3
  // or inconsistent wiring, and the dual-clone drift signal (a brain last stamped by
4
4
  // a different engine build). Deterministic; unit-tested (the inner gate per ADR-0023).
5
5
  import { execFileSync } from 'node:child_process';
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs';
6
+ import { randomBytes } from 'node:crypto';
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, unlinkSync } from 'node:fs';
7
8
  import { join, dirname, relative, resolve, isAbsolute } from 'node:path';
8
9
  import { SCHEMA_VERSION, ENGINE_VERSION, ENGINE_COMMIT } from './version.js';
9
10
  import { journalStatus } from './journal.js';
@@ -438,6 +439,65 @@ export function runDoctor(opts) {
438
439
  add('engine drift', 'warn', `brain last stamped by engine ${mf.engine_commit}, running ${runningCommit} — possible dual-clone drift`);
439
440
  }
440
441
  }
442
+ // 2a-bis. Write-health (ADR-0065 §2): can the engine actually write here?
443
+ //
444
+ // Round-trips a NON-entries file. `entries.jsonl` is append-only — there is no
445
+ // "append then remove" (the only delete is a tombstone, two permanent lines
446
+ // per doctor run), and a probe namespace inside it would dirty the working
447
+ // tree and grow the committed log on every invocation.
448
+ //
449
+ // SCOPE, and it is the whole difficulty. §0 observed that the failure which
450
+ // actually loses writes is a HUNG MCP server: the process is alive and mute,
451
+ // the call never returns, nothing errors (scenarios/probes/mcp-disconnect.md).
452
+ // Doctor is the CLI face and cannot see that at all. So a true "write-health
453
+ // ok" here is exactly the kind of statement a reader over-reads into "my
454
+ // capture is fine" — the 6ad98196b5a2 failure, where a diagnostic was made
455
+ // more CONFIDENT rather than more TRUE and an L4 went green because the claim
456
+ // became more quotable. The scope caveat below is therefore part of the
457
+ // behaviour, not decoration, and is pinned by test/doctor-write-health.test.ts.
458
+ {
459
+ // UNIQUE per run: with a fixed path, two doctors racing read each other's
460
+ // bytes and report "read back different bytes" — a false alarm of silent
461
+ // disk corruption, and report.ok=false exits 1 (reproduced: 4 processes x
462
+ // 200 runs, review of #228).
463
+ const probe = join(brainDir, `.write-probe-${process.pid}-${randomBytes(4).toString('hex')}`);
464
+ const payload = `vfkb write-probe ${process.pid} ${Date.now()}`;
465
+ let failure;
466
+ try {
467
+ mkdirSync(brainDir, { recursive: true });
468
+ writeFileSync(probe, payload);
469
+ // Read BACK: a write that silently no-ops (a full disk, an overlay that
470
+ // swallows writes) must not read as healthy.
471
+ if (readFileSync(probe, 'utf8') !== payload)
472
+ failure = 'wrote the probe file but read back different bytes';
473
+ }
474
+ catch (e) {
475
+ failure = e.message;
476
+ }
477
+ finally {
478
+ try {
479
+ if (existsSync(probe))
480
+ unlinkSync(probe);
481
+ }
482
+ catch {
483
+ /* leaving the probe behind is not itself a write-health failure */
484
+ }
485
+ }
486
+ // §0 consequence #3 says a §2 design needs its own timeout or it inherits
487
+ // the hang. These are synchronous fs calls: on a wedged NFS/FUSE mount they
488
+ // block indefinitely and so does doctor. Named, not silently dropped — the
489
+ // bounded-round-trip requirement targets an MCP-probing design, which this
490
+ // deliberately is not.
491
+ const SCOPE = ' — scope: this is the CLI/engine/filesystem path only; it does NOT check the MCP server, ' +
492
+ 'so it cannot tell you whether kb_* capture is reaching the brain (ADR-0065 §0: a hung MCP ' +
493
+ 'server loses a write with no error at all)';
494
+ if (failure) {
495
+ add('write-health (filesystem)', 'fail', `cannot write to ${brainDir}: ${failure}${SCOPE}`);
496
+ }
497
+ else {
498
+ add('write-health (filesystem)', 'ok', `round-trip verified in ${brainDir}${SCOPE}`);
499
+ }
500
+ }
441
501
  // 2b. Durable-capture journal (ADR-0064): what would recovery do, and is a
442
502
  // redaction half-done? Read-only — recovery itself runs at session start.
443
503
  {
package/dist/init.js CHANGED
@@ -213,6 +213,7 @@ export function initProject(root, opts = {}) {
213
213
  '.vfkb/.signals/',
214
214
  '.vfkb/.journal/',
215
215
  '.vfkb/.lock',
216
+ '.vfkb/.write-probe-*',
216
217
  ];
217
218
  // The header the stanza should carry. The OLD one claimed "only
218
219
  // .vfkb/entries.jsonl is committed", which is FALSE — manifest.json is
package/dist/init.test.js CHANGED
@@ -66,7 +66,7 @@ describe('vfkb init (FR-1)', () => {
66
66
  it('ignores every derived/operational path, including the lock', () => {
67
67
  initProject(root, { project: 'demo' });
68
68
  const gi = read('.gitignore');
69
- for (const p of ['.vfkb/index-meta.json', '.vfkb/.sessions/', '.vfkb/.signals/', '.vfkb/.journal/', '.vfkb/.lock']) {
69
+ for (const p of ['.vfkb/index-meta.json', '.vfkb/.sessions/', '.vfkb/.signals/', '.vfkb/.journal/', '.vfkb/.lock', '.vfkb/.write-probe-*']) {
70
70
  expect(gi, `missing ignore rule ${p}`).toContain(p);
71
71
  }
72
72
  });
@@ -96,7 +96,7 @@ describe('vfkb init (FR-1)', () => {
96
96
  return false;
97
97
  }
98
98
  };
99
- for (const p of ['.vfkb/.lock', '.vfkb/.journal/wal.jsonl', '.vfkb/index-meta.json', '.vfkb/.sessions/x', '.vfkb/.signals/y']) {
99
+ for (const p of ['.vfkb/.lock', '.vfkb/.journal/wal.jsonl', '.vfkb/index-meta.json', '.vfkb/.sessions/x', '.vfkb/.signals/y', '.vfkb/.write-probe-123-abc']) {
100
100
  expect(ignored(p), `${p} should be ignored`).toBe(true);
101
101
  }
102
102
  // The brain and the ADR-0030 engine stamp MUST stay committable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viloforge/vfkb",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "ViloForge KnowledgeBase (vfkb) — per-project knowledge substrate for the ViloForge software factory. Greenfield TypeScript.",
5
5
  "type": "module",
6
6
  "main": "dist/engine.js",