OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / autobase-guards.js

87 lines · javascript ref:
// Are the two autobase patches this daemon depends on actually present?
//
// ⛔ WHY A BOOT CHECK AND NOT TRUST. `scripts/patch-autobase-warmup.cjs` re-applies both patches on
// every `npm install`, so a reinstall cannot revert them — proven 2026-09-15 against a pristine
// upstream copy. What it does NOT survive is an autobase BUMP: the anchors move, the script prints
//
//     anchor not found (autobase version changed?) — SKIPPING; re-verify manually
//
// and the install SUCCEEDS. Both guards vanish, and what they prevent is a SIGABRT crash-loop on boot
// (pear-git #3) and a daemon wedged in a microtask chain that accepts nothing and reaps nothing
// (measured on stortron: 800,000 `_advance` calls, no banner). A console line at install time is not
// a defence against either — nobody reads install output, and the symptom arrives days later wearing
// a different face.
//
// `overrides: { "autobase": "7.28.1" }` pins the version and is the only thing holding this shut
// today. This makes the pin's failure LOUD instead of silent.
//
// ⛔ FAIL CLOSED ON A POSITIVE ANSWER ONLY. A file we can read whose marker is missing is a fact, and
// we refuse. A file we cannot read at all is NOT knowledge — the layout could have changed, this
// could be a bundled tree, the check itself could be wrong — and refusing to boot a production forge
// over our own uncertainty is worse than the thing we are guarding against. Unknown proceeds, loudly.
// Same posture as chooseBundle's addon guard, for the same reason.
//
// PURE + INJECTED: `read` is a parameter, so every branch is testable with no node_modules.

export const GUARDS = [
  {
    file: 'node_modules/autobase/lib/apply-state.js',
    marker: 'GUARD (pgv3 patch)',
    protects: 'null view.core during _drain warmup — without it the daemon SIGABRT-crash-loops on boot (pear-git #3)'
  },
  {
    file: 'node_modules/autobase/index.js',
    marker: 'BOUND (pgv3 patch)',
    protects: 'the _appendBatch flush loop — without it a mirror whose writer the origin has not enrolled spins forever and the daemon never serves (stortron, 800k _advance calls)'
  }
]

/**
 * @param {object} o
 *   read  (relativePath) => string | null   — source text, or null when it cannot be read
 * @returns {{ ok: boolean, missing: object[], unreadable: object[] }}
 */
export function checkAutobaseGuards ({ read } = {}) {
  if (typeof read !== 'function') throw new Error('checkAutobaseGuards requires read(path)')
  const missing = []
  const unreadable = []
  for (const g of GUARDS) {
    let src = null
    try { src = read(g.file) } catch { src = null }
    if (src == null) { unreadable.push(g); continue }
    if (!src.includes(g.marker)) missing.push(g)
  }
  return { ok: missing.length === 0, missing, unreadable }
}

/**
 * Boot gate. Throws when a guard is positively absent; warns when it cannot tell.
 *
 * ⚠ `PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE=1` overrides, for the operator who has upstreamed the patches
 * or is deliberately testing without them. An override that must be typed is a decision; a check that
 * can be skipped by accident is not a check.
 */
export function assertAutobaseGuards ({ read, env = {}, log = console.error } = {}) {
  const { ok, missing, unreadable } = checkAutobaseGuards({ read })

  for (const g of unreadable) {
    log('[autobase-guards] cannot read ' + g.file + ' — NOT verifying: ' + g.protects)
  }
  if (ok) return { ok: true, missing, unreadable }

  const detail = missing.map((g) => '  ' + g.file + ' is missing "' + g.marker + '"\n    guards: ' + g.protects).join('\n')
  const msg = 'autobase is not patched for this daemon.\n' + detail +
    '\n\nRun `npm install` (postinstall re-applies both), or if autobase was bumped the anchors have' +
    '\nmoved and scripts/patch-autobase-warmup.cjs needs re-pointing — it warns and succeeds in that' +
    '\ncase, which is why this check exists.' +
    '\n\nTo boot anyway: PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE=1'

  if (env.PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE === '1') {
    log('[autobase-guards] OVERRIDDEN — booting unpatched.\n' + msg)
    return { ok: false, overridden: true, missing, unreadable }
  }
  throw new Error('[autobase-guards] ' + msg)
}

export default { GUARDS, checkAutobaseGuards, assertAutobaseGuards }