think
16px
820px

Engineering Mandate + Convention Gate (2026-07-03)

The chatbot is feature-ready; this spec locks in the conventions that got it
there so future work doesn't erode them. Two components: a terse prose mandate
in CLAUDE.md (loaded into every session — the operative enforcement point) and
a mechanical gate (pnpm check) that build-and-ship.sh must pass before
shipping. Rules that matter get checked by a machine; prose covers the rest.

Component 1 — "Engineering mandate" section in CLAUDE.md

No separate CONVENTIONS.md: one source, auto-loaded, cannot drift from a twin
document. The section states, tersely:

  • Boundary. apps/public-web and apps/internal-web never import from
    each other. Shared code lives only in packages/@ahu/* — new shared code
    means a new or existing package, never a cross-app relative import. Internal
    data never flows to the public surface; public data access goes only through
    the guarded public_dash agent.
  • Placement. UI/routes in the owning app; agent logic in
    apps/{public,internal}-agent; deploy/compose/env in infra/; specs and
    plans in docs/superpowers/{specs,plans}. Match neighboring file style; one
    responsibility per file.
  • Security invariants. Every /api/admin mutation handler calls
    requireMutationSession and writes an audit row. Secrets live only in
    git-ignored infra/env/*.env; update the committed *.env.example when
    adding vars. On-prem LLMs only in production paths (the existing On-prem
    constraint section stays).
  • Workflow. Feature-sized work — a new route, page, agent tool, or schema,
    or any change touching the boundary, auth, or deploy scripts — requires
    spec → plan → completion notes. Small fixes (bug, copy, config value) go
    straight to code with tests. TDD for features; suites stay green.
  • Deploy. Only via build-and-ship.shdeploy-staging.sh; the gate
    runs first and shipping aborts on failure.

Component 2 — pnpm check gate

Root package.json gains:

"check": "pnpm -r typecheck && pnpm -r test && node scripts/check-conventions.mjs"

scripts/check-conventions.mjs — plain Node, zero dependencies, ~80 lines,
exits non-zero with a named-rule message on any violation:

  1. boundary — scan apps/public-web/src for imports matching
    internal-web|apps/internal and apps/internal-web/src for
    public-web|apps/public (string scan of import/require lines).
  2. on-prem — scan apps/*/src, apps/public-agent/dash,
    apps/internal-agent/dash, and packages/*/src for the literal hostnames
    api.openai.com / api.anthropic.com. Allowlist (path-exact):
    apps/internal-web/src/components/admin/settings/DataDashProviderSection.tsx
    (the GPT-5.2 local-dev comparison entry). The check targets external
    hostnames, not the word "OpenAI" — vLLM/TEI speak the OpenAI-compatible
    protocol via OpenAILike/OpenAIEmbedder pointed at our own hardware.
    Known limitation (accepted): constructors that default to api.openai.com
    when base_url is unset are invisible to a grep; the deployed env always
    sets EMBEDDER_BASE_URL (Plan D4) and the prose rule covers the rest.
  3. mutation-guard — for every apps/internal-web/src/app/api/admin/**/route.ts
    that exports POST|PUT|PATCH|DELETE, assert the file contains
    requireMutationSession. (Auth routes live under api/auth/, not
    api/admin/, so no exemptions needed.)

Component 3 — wiring

  • build-and-ship.sh runs pnpm check as its first step; non-zero aborts the
    build (set -euo pipefail already in place).
  • After building the two agent images, build-and-ship.sh runs each image's
    pytest in a throwaway container (docker run --rm <agent-image> pytest -q)
    — the sql-guard suite protects the most sensitive invariant and the built
    image is the exact artifact being shipped. Non-zero aborts the ship.
    If the images don't bundle pytest, the plan adds it to the agent
    requirements (small, worth it) rather than pip-installing at check time.

Error handling

The convention script prints one line per violation
(RULE file:line detail) and a summary count; it never auto-fixes. Gate
failures leave the working tree untouched.

Testing the gate itself

The implementation plan must include deliberate violations, each proven to
fail then removed: a temp cross-app import in public-web, a temp
api.openai.com literal outside the allowlist, and a temp unguarded
route.ts with a POST export under api/admin/. Plus one clean run proving
the current tree passes all three rules.

Out of scope

  • ESLint boundary plugins / dependency-cruiser (revisit if apps multiply).
  • Enforcing audit-row presence mechanically (prose-only; call sites vary).
  • CI infrastructure — the gate runs locally in the deploy path by design.