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-webandapps/internal-webnever import from
each other. Shared code lives only inpackages/@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 guardedpublic_dashagent. - Placement. UI/routes in the owning app; agent logic in
apps/{public,internal}-agent; deploy/compose/env ininfra/; specs and
plans indocs/superpowers/{specs,plans}. Match neighboring file style; one
responsibility per file. - Security invariants. Every
/api/adminmutation handler calls
requireMutationSessionand writes an audit row. Secrets live only in
git-ignoredinfra/env/*.env; update the committed*.env.examplewhen
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.sh→deploy-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:
- boundary — scan
apps/public-web/srcfor imports matching
internal-web|apps/internalandapps/internal-web/srcfor
public-web|apps/public(string scan of import/require lines). - on-prem — scan
apps/*/src,apps/public-agent/dash,
apps/internal-agent/dash, andpackages/*/srcfor 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 viaOpenAILike/OpenAIEmbedderpointed at our own hardware.
Known limitation (accepted): constructors that default to api.openai.com
whenbase_urlis unset are invisible to a grep; the deployed env always
setsEMBEDDER_BASE_URL(Plan D4) and the prose rule covers the rest. - mutation-guard — for every
apps/internal-web/src/app/api/admin/**/route.ts
that exportsPOST|PUT|PATCH|DELETE, assert the file contains
requireMutationSession. (Auth routes live underapi/auth/, not
api/admin/, so no exemptions needed.)
Component 3 — wiring
build-and-ship.shrunspnpm checkas its first step; non-zero aborts the
build (set -euo pipefailalready in place).- After building the two agent images,
build-and-ship.shruns 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.