Self-hosting: the engine gates its own tree#

bce grades repositories against an authored EngineeringBlueprint. This repository is itself a repository — so it carries its own blueprint (.blueprints/engine.blueprint.json) and CI runs the engine over the engine on every push and pull request (.github/workflows/self-gate.yml).

Self-hosting is not a stunt. It is the cheapest continuous proof that:

  1. the engine's authored-artifact path (schema → extraction → evaluate → score → verdict) works end-to-end on a real, non-trivial TypeScript repository;
  2. the engine's own architecture cannot silently drift — the same fail-closed gate its users get is the gate its maintainers live under;
  3. dogfooding surfaces real engine gaps (see the teeth scopePaths fix below, found the first time the engine assessed its own blueprint).

The architecture this blueprint enforces#

Every constraint below was verified against the code before being authored — the blueprint records the real architecture, not an aspiration.

ClaimConstraint(s)Mechanism
Only src/extractors.ts may import ts-morph. Everything else consumes extracted facts through the seam (below).only-extractors-may-import-ts-morphforbiddenDependency on the AST import graph, scopePaths = every src file except extractors.ts
Only src/python-module-graph.ts may import the pinned Lezer Python parser packages.only-python-module-graph-may-import-lezerforbiddenDependency on the AST import graph, scopePaths = every src file except python-module-graph.ts
Runtime dependency allowlist is exactly zod, ts-morph, @lezer/common, and @lezer/python. Any other external package import is a violation. node: builtins are allowed globally and narrowed per-file below.runtime-dep-allowlist-approved-parsersanchored per-line forbiddenPattern over src/**/*.ts
src/schema.ts (the single source of truth for the artifact shape) imports only zod plus the local safe-regex guard.schema-imports-only-zod-and-safe-regexper-file forbiddenPattern
The evaluator is pure: report.ts may import only node:crypto (deterministic hashing) + local modules; score.ts and teeth.ts import local modules only. No fs, no network, no process, no child processes.evaluator-pure--* (3 constraints)per-file forbiddenPattern
Proposal compilation and review semantics remain pure; only the assistant, SCM-authentication, quarantine, and CLI shells own I/O.review-core-no-io-imports plus explicit component relationshipsper-file forbiddenPattern + authored graph
Only the two process-owning bins (cli.ts and mcp-server.ts) are exempt from the no-exit rule; every one of the 39 library modules returns or throws.only-cli-may-call-process-exit--* (39 per-file constraints)per-file forbiddenPattern

Design notes, recorded honestly:

The extractor / facts seam#

The blueprint never talks to ts-morph. It talks to facts:

Lane A / Lane B#

Two lanes can gate this tree:

The bootstrap-0 exception ended with the provenance-backed bce-engine@0.1.0 publication. Lane A now installs the exact bce-engine@0.3.1 registry artifact independently, while Lane B continues to grade the commit under review with its own build.

The flip: the lane-a-pinned-gate job already exists in self-gate.yml — it reads the exact pin from .engine-pin.json, and is if-guarded on the pin being published AND bce-engine@<pin> actually resolving on npm. The guard is open: the job installs bce-engine@0.3.1 (exact pin, no range) and runs the same gate verb. The job is also a required branch-protection check.

The Lane-A pin ceremony (forward reference)#

The Lane-A flip is a pin ceremony, not a version range — and the distinction is the whole point of the lane. Lane A must be independent of the code under review, so it installs an exact version:

The exact steps are written up in docs/pin-ceremony.md, and the live exact pin is recorded in .engine-pin.json. On a pull request, Lane A selects that file from the event's exact base SHA; on push, it selects the merged tree. v0.1.0 completed bootstrap; subsequent pin bumps are therefore admitted by the previously published Lane-A engine.

Branch-protection incident policy (attended recovery, not a skip flag)#

A required, fail-closed check can occasionally block its own fix — the classic case is a change that reddens the gate whose only correct resolution is that very change (a corrected gate, a fixed extractor). This repository's current branch protection applies required checks to administrators; there is no ordinary admin-merge bypass. Recovery therefore requires an explicit, temporary change to branch protection (or a separately reviewed forward fix), and its policy is pre-written, not improvised:

This is an attended recovery path, not a skip flag — and the distinction is exact. bce's "no skip flag" claim is a claim about the engine: there is no --skip / --no-verify / --force in bce that turns a red green (the test suite asserts it). It is not a claim that repository administrators cannot reconfigure GitHub. The incident record and exact restoration are what keep that platform recovery visible when it is used.

Reproducing the self-gate locally#

npm ci
npm run build
node dist/cli.js validate --blueprint .blueprints/engine.blueprint.json
node dist/cli.js gate  --repo . --repo-name blueprint-conformance/bce
npm run test:self-teeth-mutations
npx vitest run tests/self-blueprint.test.ts

Expected: blueprint VALID, gate score 100 (pass), and extractor-real-proven with all 47 constraints killed by 47 separately materialized source-tree mutants. The mutation manifest is regenerated from the blueprint and is freshness-checked before the real CLI proof runs. Tests are green. To watch the gate actually bite, add import { Project } from 'ts-morph'; to src/score.ts and re-run the gate: it exits 1 with two violations (the seam constraint, via the AST import edge, and the evaluator-purity pattern) — then revert.

What self-hosting already found#

Assessing the engine's own blueprint surfaced a real engine gap on day one: the teeth reddening mutation for forbiddenDependency injected its synthetic edge at a placeholder path that could never match a scopePaths-narrowed constraint, so a genuinely enforcing, scoped constraint was mislabeled TRIVIALLY_GREEN. Fixed in src/teeth.ts (the injected edge now lands at a concrete in-scope path) with a discriminating regression test in tests/self-blueprint.test.ts. The former evaluator-only gap is now closed by .blueprints/engine.teeth-mutations.json: every self-blueprint clause maps to one real create/replace/append/delete mutation, and the CLI refuses missing, duplicate, surviving, out-of-scope, protected-surface, syntax-invalid, or collateral mutations. This proves the current 47 clauses can bite the current extraction/evaluation path; it does not prove the blueprint is a complete specification of every desirable property.

Full self-adoption#

The source checkout installs its canonical skills under .agents/skills/ and runs its built MCP server through .mcp.json and .codex/config.toml. CI checks the installed skills for drift and exercises the actual project MCP configuration. The enforced mode is explicit in .bce-mode.json.

The solo-steward ceremony describes the live GitHub decision path. .bce-governance.json identifies the steward; it establishes no independent review. The acceptance check for a completed lifecycle is node dist/cli.js doctor --repo .: real-source teeth, matching approved policy and adoption history, exact CI pin, project integrations, and full gate must agree. A missing ceremony remains a warning until its authenticated history exists. Both blueprints now carry digest-bound source mutation manifests: 47 engine clauses and 13 skill-standard clauses. The latter includes exact file evidence for forbidden files, without inventing line numbers.

Read Markdown · Source: docs/self-hosting.md