@viloforge/vfkb 0.2.3 → 0.4.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.
@@ -0,0 +1,134 @@
1
+ // ADR-0063 §3 — `vfkb broadcast`: write one cross-repo record fact into each
2
+ // target repo's brain, through the engine, stamping mechanically what the v1
3
+ // convention leaves to discipline (the `cross-repo` tag, the CROSS-REPO marker
4
+ // with origin + date). Refusals are per-target and loud — a partial broadcast
5
+ // must be visible, never silent — and the writer NEVER commits the target
6
+ // (§4: the entry rides the target's own ADR-0033 discipline).
7
+ import { execFileSync } from 'node:child_process';
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { basename, join, resolve } from 'node:path';
10
+ import { addEntry } from './engine.js';
11
+ import { writeManifest } from './manifest.js';
12
+ import { defaultProject } from './storage.js';
13
+ import { SCHEMA_VERSION } from './version.js';
14
+ /** The resident-pin tags a broadcast record must never carry (ADR-0063 §1). */
15
+ const FORBIDDEN_TAGS = new Set(['handoff', 'next']);
16
+ function targetBrainDir(target) {
17
+ const abs = resolve(target);
18
+ return basename(abs) === '.vfkb' ? abs : join(abs, '.vfkb');
19
+ }
20
+ function gitPosture(repoDir) {
21
+ const git = (...a) => execFileSync('git', ['-C', repoDir, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
22
+ try {
23
+ const branch = git('rev-parse', '--abbrev-ref', 'HEAD');
24
+ const parked = branch === 'main' || branch === 'master';
25
+ return `uncommitted; target on ${branch}${parked ? ' (parked — rides until a topic-branch brain commit)' : ''}`;
26
+ }
27
+ catch {
28
+ // rev-parse HEAD fails on a repo with no commits yet — distinguish that
29
+ // from "not a repo at all" (review finding: unborn HEAD was misreported).
30
+ try {
31
+ if (git('rev-parse', '--is-inside-work-tree') === 'true') {
32
+ return 'uncommitted; target git repository on an unborn branch (no commits yet)';
33
+ }
34
+ }
35
+ catch {
36
+ /* fall through */
37
+ }
38
+ return 'uncommitted; target not a git repository';
39
+ }
40
+ }
41
+ /**
42
+ * Broadcast one record to N target brains. Origin is derived from the INVOKING
43
+ * repo's project label, captured before any env switching. Targets without a
44
+ * brain are refused (never bootstrap a wire-less brain — ADR-0063 §3): no
45
+ * `entries.jsonl` means wire-less; a wired brain merely missing its
46
+ * `manifest.json` (plugin-born, vfkb#193) is healed, visibly. Brains whose
47
+ * schema the running engine does not support are refused (promoting doctor's
48
+ * diagnostic to a hard refusal).
49
+ */
50
+ export function broadcast(text, targets, opts = {}) {
51
+ const origin = defaultProject();
52
+ const date = new Date().toISOString().slice(0, 10);
53
+ const extraTags = opts.tags ?? [];
54
+ for (const t of extraTags) {
55
+ if (FORBIDDEN_TAGS.has(t)) {
56
+ throw new Error(`broadcast: tag '${t}' is forbidden on cross-repo records — it claims the resident's ADR-0049 continuity pin (ADR-0063 §1)`);
57
+ }
58
+ }
59
+ const op = (opts.op || 'OPERATION').toUpperCase();
60
+ // No-double-stamp applies only to a COMPLETE marker (op + date + origin) —
61
+ // a bare "CROSS-REPO " prefix must not waive the stamping this command
62
+ // exists to mechanize (review finding: the loose prefix test let a record
63
+ // land with no origin and no date, the ADR-0051 §3 quiet-success shape).
64
+ const MARKER = /^CROSS-REPO \S+ \(\d{4}-\d{2}-\d{2}, from [^)]+\):/;
65
+ const stamped = MARKER.test(text) ? text : `CROSS-REPO ${op} (${date}, from ${origin}): ${text}`;
66
+ const results = [];
67
+ const written = new Set();
68
+ for (const target of targets) {
69
+ const brain = targetBrainDir(target);
70
+ // "Exactly one record per affected repo" (ADR-0063 §1) also holds across
71
+ // path spellings of the same target within one broadcast.
72
+ if (written.has(brain)) {
73
+ results.push({ target, ok: false, reason: 'duplicate target (already written in this broadcast)' });
74
+ continue;
75
+ }
76
+ const repoDir = resolve(brain, '..');
77
+ const manifestPath = join(brain, 'manifest.json');
78
+ let healed = false;
79
+ if (!existsSync(manifestPath)) {
80
+ // §3's rule is "never bootstrap a WIRE-LESS brain". A brain with a live
81
+ // entries.jsonl is wired — the manifest is just missing because only
82
+ // `vfkb init` ever writes it (plugin-born brains, vfkb#193). Heal it
83
+ // (engine-written stamp) instead of refusing; refuse only when there are
84
+ // no entries at all, which is the true wire-less case.
85
+ if (!existsSync(join(brain, 'entries.jsonl'))) {
86
+ results.push({ target, ok: false, reason: `no brain (no entries.jsonl in ${brain}) — never bootstrap a wire-less brain (ADR-0063 §3)` });
87
+ continue;
88
+ }
89
+ try {
90
+ writeManifest(brain);
91
+ }
92
+ catch (err) {
93
+ // Per-target and loud, never a thrown abort — a heal failure on one
94
+ // target must not swallow the rest of a partial broadcast.
95
+ results.push({ target, ok: false, reason: `manifest heal failed: ${err.message}` });
96
+ continue;
97
+ }
98
+ healed = true;
99
+ }
100
+ let schema;
101
+ try {
102
+ schema = JSON.parse(readFileSync(manifestPath, 'utf8')).schema_version;
103
+ }
104
+ catch {
105
+ results.push({ target, ok: false, reason: 'unreadable manifest.json' });
106
+ continue;
107
+ }
108
+ if (schema !== SCHEMA_VERSION) {
109
+ results.push({ target, ok: false, reason: `brain schema ${JSON.stringify(schema)} unsupported by engine schema v${SCHEMA_VERSION}` });
110
+ continue;
111
+ }
112
+ // brainDir() resolves VFKB_DATA_DIR fresh on every call (the RFC-013/033
113
+ // verified transport), so an env swap scopes the engine write to the target.
114
+ const prev = process.env.VFKB_DATA_DIR;
115
+ try {
116
+ process.env.VFKB_DATA_DIR = brain;
117
+ const e = addEntry('fact', stamped, { role: 'executor', tags: [...new Set(['cross-repo', ...extraTags])] });
118
+ written.add(brain);
119
+ results.push({ target, ok: true, id: e.id, posture: gitPosture(repoDir), ...(healed ? { healed } : {}) });
120
+ }
121
+ catch (err) {
122
+ // healed still reported on failure — the manifest stamp happened even
123
+ // though the record did not land (heal is never silent).
124
+ results.push({ target, ok: false, reason: err.message, ...(healed ? { healed } : {}) });
125
+ }
126
+ finally {
127
+ if (prev === undefined)
128
+ delete process.env.VFKB_DATA_DIR;
129
+ else
130
+ process.env.VFKB_DATA_DIR = prev;
131
+ }
132
+ }
133
+ return results;
134
+ }
@@ -3648,49 +3648,49 @@ var require_fast_uri = __commonJS({
3648
3648
  schemelessOptions.skipEscape = true;
3649
3649
  return serialize(resolved, schemelessOptions);
3650
3650
  }
3651
- function resolveComponent(base, relative, options, skipNormalization) {
3651
+ function resolveComponent(base, relative2, options, skipNormalization) {
3652
3652
  const target = {};
3653
3653
  if (!skipNormalization) {
3654
3654
  base = parse3(serialize(base, options), options);
3655
- relative = parse3(serialize(relative, options), options);
3655
+ relative2 = parse3(serialize(relative2, options), options);
3656
3656
  }
3657
3657
  options = options || {};
3658
- if (!options.tolerant && relative.scheme) {
3659
- target.scheme = relative.scheme;
3660
- target.userinfo = relative.userinfo;
3661
- target.host = relative.host;
3662
- target.port = relative.port;
3663
- target.path = removeDotSegments(relative.path || "");
3664
- target.query = relative.query;
3658
+ if (!options.tolerant && relative2.scheme) {
3659
+ target.scheme = relative2.scheme;
3660
+ target.userinfo = relative2.userinfo;
3661
+ target.host = relative2.host;
3662
+ target.port = relative2.port;
3663
+ target.path = removeDotSegments(relative2.path || "");
3664
+ target.query = relative2.query;
3665
3665
  } else {
3666
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
3667
- target.userinfo = relative.userinfo;
3668
- target.host = relative.host;
3669
- target.port = relative.port;
3670
- target.path = removeDotSegments(relative.path || "");
3671
- target.query = relative.query;
3666
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
3667
+ target.userinfo = relative2.userinfo;
3668
+ target.host = relative2.host;
3669
+ target.port = relative2.port;
3670
+ target.path = removeDotSegments(relative2.path || "");
3671
+ target.query = relative2.query;
3672
3672
  } else {
3673
- if (!relative.path) {
3673
+ if (!relative2.path) {
3674
3674
  target.path = base.path;
3675
- if (relative.query !== void 0) {
3676
- target.query = relative.query;
3675
+ if (relative2.query !== void 0) {
3676
+ target.query = relative2.query;
3677
3677
  } else {
3678
3678
  target.query = base.query;
3679
3679
  }
3680
3680
  } else {
3681
- if (relative.path[0] === "/") {
3682
- target.path = removeDotSegments(relative.path);
3681
+ if (relative2.path[0] === "/") {
3682
+ target.path = removeDotSegments(relative2.path);
3683
3683
  } else {
3684
3684
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3685
- target.path = "/" + relative.path;
3685
+ target.path = "/" + relative2.path;
3686
3686
  } else if (!base.path) {
3687
- target.path = relative.path;
3687
+ target.path = relative2.path;
3688
3688
  } else {
3689
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
3689
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
3690
3690
  }
3691
3691
  target.path = removeDotSegments(target.path);
3692
3692
  }
3693
- target.query = relative.query;
3693
+ target.query = relative2.query;
3694
3694
  }
3695
3695
  target.userinfo = base.userinfo;
3696
3696
  target.host = base.host;
@@ -3698,7 +3698,7 @@ var require_fast_uri = __commonJS({
3698
3698
  }
3699
3699
  target.scheme = base.scheme;
3700
3700
  }
3701
- target.fragment = relative.fragment;
3701
+ target.fragment = relative2.fragment;
3702
3702
  return target;
3703
3703
  }
3704
3704
  function equal(uriA, uriB, options) {
@@ -30956,7 +30956,7 @@ var StdioServerTransport = class {
30956
30956
  import { randomBytes as randomBytes2 } from "node:crypto";
30957
30957
 
30958
30958
  // src/storage.ts
30959
- import { basename, dirname, join as join3, resolve } from "node:path";
30959
+ import { basename, dirname as dirname2, join as join4, resolve } from "node:path";
30960
30960
  import { homedir } from "node:os";
30961
30961
  import { createHash } from "node:crypto";
30962
30962
 
@@ -31154,6 +31154,23 @@ function storageBackend() {
31154
31154
  return current;
31155
31155
  }
31156
31156
 
31157
+ // src/journal.ts
31158
+ import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
31159
+ import { dirname, join as join3, relative, sep } from "node:path";
31160
+ var walPath = (brain) => join3(brain, ".journal", "wal.jsonl");
31161
+ function journalAppend(brain, rec) {
31162
+ if (process.env.VFKB_NO_JOURNAL) return;
31163
+ try {
31164
+ mkdirSync3(join3(brain, ".journal"), { recursive: true });
31165
+ appendFileSync2(walPath(brain), JSON.stringify(rec) + "\n", "utf8");
31166
+ } catch (err) {
31167
+ process.stderr.write(
31168
+ `vfkb: journal mirror failed (primary append proceeds unprotected): ${err.message}
31169
+ `
31170
+ );
31171
+ }
31172
+ }
31173
+
31157
31174
  // src/validate.ts
31158
31175
  var ROLE = external_exports.enum(["architect", "pm", "executor", "judge", "human", "init", "import"]);
31159
31176
  var PROV_STATUS = external_exports.enum(["verified", "unverified", "stale", "expired"]);
@@ -31205,7 +31222,7 @@ function isTombstone(r) {
31205
31222
  return r.deleted === true;
31206
31223
  }
31207
31224
  function brainDir() {
31208
- return process.env.VFKB_DATA_DIR || process.env.VFKB_DIR || join3(homedir(), ".vfkb");
31225
+ return process.env.VFKB_DATA_DIR || process.env.VFKB_DIR || join4(homedir(), ".vfkb");
31209
31226
  }
31210
31227
  function defaultProject() {
31211
31228
  const raw = (() => {
@@ -31214,7 +31231,7 @@ function defaultProject() {
31214
31231
  if (explicit) {
31215
31232
  const abs = resolve(explicit);
31216
31233
  const name = basename(abs);
31217
- return name.startsWith(".") ? basename(dirname(abs)) : name;
31234
+ return name.startsWith(".") ? basename(dirname2(abs)) : name;
31218
31235
  }
31219
31236
  const root = process.env.CLAUDE_PROJECT_DIR;
31220
31237
  if (root) return basename(resolve(root));
@@ -31223,7 +31240,9 @@ function defaultProject() {
31223
31240
  return raw.replace(/["<>&]/g, "") || "spike";
31224
31241
  }
31225
31242
  function appendRecord(rec) {
31226
- storageBackend().append(rec);
31243
+ const be = storageBackend();
31244
+ if (be.name === "jsonl-fs") journalAppend(be.location(), rec);
31245
+ be.append(rec);
31227
31246
  writeMeta();
31228
31247
  }
31229
31248
  function withExclusive(fn) {
@@ -31387,15 +31406,15 @@ function assertNoSecrets(text2) {
31387
31406
  }
31388
31407
 
31389
31408
  // src/counters.ts
31390
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
31391
- import { join as join4 } from "node:path";
31409
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
31410
+ import { join as join5 } from "node:path";
31392
31411
  function signalsFile() {
31393
- return join4(brainDir(), ".signals", "counters.jsonl");
31412
+ return join5(brainDir(), ".signals", "counters.jsonl");
31394
31413
  }
31395
31414
  function readSignals() {
31396
31415
  const f = signalsFile();
31397
- if (!existsSync2(f)) return [];
31398
- return readFileSync3(f, "utf8").split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
31416
+ if (!existsSync3(f)) return [];
31417
+ return readFileSync4(f, "utf8").split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
31399
31418
  }
31400
31419
  function tally(entryId, signals = readSignals()) {
31401
31420
  let helpful = 0;
@@ -31740,6 +31759,22 @@ function latestHandoff(all = readAll(), today = nowIso().slice(0, 10), supersede
31740
31759
  }
31741
31760
  return latest;
31742
31761
  }
31762
+ var CROSS_REPO_PIN_CAP_CHARS = 2e3;
31763
+ function latestCrossRepo(all = readAll(), today = nowIso().slice(0, 10), superseded = supersededIds(all)) {
31764
+ let latest = null;
31765
+ for (const e of all) {
31766
+ if (!e.tags.includes("cross-repo")) continue;
31767
+ if (e.tags.includes("handoff") || e.tags.includes("next")) continue;
31768
+ if (!isInjectable(e, today, superseded)) continue;
31769
+ if (!latest) {
31770
+ latest = e;
31771
+ continue;
31772
+ }
31773
+ const cmp = e.updated.localeCompare(latest.updated) || e.created.localeCompare(latest.created);
31774
+ if (cmp >= 0) latest = e;
31775
+ }
31776
+ return latest;
31777
+ }
31743
31778
  function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET_CHARS) {
31744
31779
  const all = readAll();
31745
31780
  const today = nowIso().slice(0, 10);
@@ -31767,25 +31802,57 @@ function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET
31767
31802
  body += `## Last handoff
31768
31803
  - [${handoff.type} ${trustGlyph(handoff)}] ${text2}
31769
31804
 
31805
+ `;
31806
+ }
31807
+ const crossRepo = latestCrossRepo(all, today, superseded);
31808
+ if (crossRepo && !constitutionalIds.has(crossRepo.id)) {
31809
+ const text2 = crossRepo.text.length > CROSS_REPO_PIN_CAP_CHARS ? `${crossRepo.text.slice(0, CROSS_REPO_PIN_CAP_CHARS)}\u2026 (truncated \u2014 kb_get ${crossRepo.id} for the rest)` : crossRepo.text;
31810
+ body += `## Cross-repo operations
31811
+ - [${crossRepo.type} ${trustGlyph(crossRepo)}] ${text2}
31812
+
31770
31813
  `;
31771
31814
  }
31772
31815
  body += renderContextMap() + "\n\n";
31816
+ const kept = [];
31817
+ let keptLen = 0;
31773
31818
  let dropped = 0;
31819
+ const fixedLen = () => header.length + body.length + footer.length;
31774
31820
  for (const e of injectable) {
31775
31821
  if (constitutionalIds.has(e.id)) continue;
31776
31822
  if (handoff && e.id === handoff.id) continue;
31823
+ if (crossRepo && e.id === crossRepo.id) continue;
31777
31824
  const line2 = `- [${e.type} ${trustGlyph(e)}] ${e.text}
31778
31825
  `;
31779
- if (header.length + body.length + line2.length + footer.length > budget) {
31826
+ if (fixedLen() + keptLen + line2.length > budget) {
31780
31827
  dropped++;
31781
31828
  continue;
31782
31829
  }
31783
- body += line2;
31830
+ kept.push(line2);
31831
+ keptLen += line2.length;
31784
31832
  }
31785
31833
  if (dropped > 0) {
31786
- const note = `<!-- ${dropped} lower-ranked entries omitted for the ${budget}-char budget -->
31834
+ const note = () => `(+ ${dropped} lower-ranked entries omitted for the ${budget}-char budget \u2014 kb_search / kb_list pulls them)
31787
31835
  `;
31788
- if (header.length + body.length + note.length + footer.length <= budget) body += note;
31836
+ const evicted = [];
31837
+ while (kept.length > 0 && fixedLen() + keptLen + note().length > budget) {
31838
+ const line2 = kept.pop();
31839
+ keptLen -= line2.length;
31840
+ evicted.push(line2);
31841
+ dropped++;
31842
+ }
31843
+ if (fixedLen() + keptLen + note().length <= budget) {
31844
+ body += kept.join("") + note();
31845
+ } else {
31846
+ while (evicted.length > 0) {
31847
+ const line2 = evicted.pop();
31848
+ kept.push(line2);
31849
+ keptLen += line2.length;
31850
+ dropped--;
31851
+ }
31852
+ body += kept.join("");
31853
+ }
31854
+ } else {
31855
+ body += kept.join("");
31789
31856
  }
31790
31857
  return header + body + footer;
31791
31858
  }
@@ -31940,7 +32007,7 @@ function queryExplained(opts = {}) {
31940
32007
  }
31941
32008
 
31942
32009
  // src/version.ts
31943
- var ENGINE_VERSION = true ? "0.2.3" : ownPackageVersion();
32010
+ var ENGINE_VERSION = true ? "0.4.1" : ownPackageVersion();
31944
32011
 
31945
32012
  // src/mcp-server.ts
31946
32013
  var SEARCH_DEFAULT_LIMIT = 25;