Why this exists

The same platform definitions — catalog-info.yaml fields, gas charge defaults, queue/cron defaults, env-var names — used to live in four independent copies that drifted apart:

  1. ~/.claude/rules/*.md — the rules Claude Code loads each session (hand-maintained).
  2. tawa-docs/conventions/*.md — the canonical docs (git, PR-reviewed).
  3. The Bible (tawa_docs) + the public docs site, synced from conventions.
  4. The iec-builder code itself — the actual behavior.

Nothing tied them together, so a fact like "the default charge mode is service" could be correct in the code and wrong in three doc copies at once. Worse, the builder's skills registry was seeded from the hand-maintained rules, so the distribution system just re-spread the drift.

This system makes the code the source of truth, makes tawa-docs/conventions the single canonical doc, and gives a machine a way to detect when any of them disagree — so Claude Code (and humans) can trust what they read instead of churning on stale guidance.

The model

flowchart TD
  CODE[iec-builder code - the real behavior] -->|probes verify| CONTRACT[platform-contract.json - pinned facts]
  CONTRACT -->|drives corrections| CONV[tawa-docs/conventions - canonical docs]
  CONV -->|sync-to-koko| KOKO[Koko conventions + gates]
  CONV -->|sync-to-bible| BIBLE[Bible / tawa_docs + docs site]
  CONV -->|seed-skills| REG[builder skills registry]
  REG -->|tawa skills sync| RULES[every dev ~/.claude/rules]
  CONTRACT -->|verify-contract --publish| EP[builder /platform/contract-status]
  EP --> DASH[tawa-web admin - Platform Contract]

Two rules make it work:

  • Code is the adjudicator. When a doc and the code disagree, the code wins. Every "fact" in the contract points at the exact code symbol that defines it.
  • Conventions are the only thing humans edit. Everything downstream (Koko, Bible, the docs site, the builder skills registry, and ~/.claude/rules) is generated from tawa-docs/conventions. Editing a rule by hand is a smell — fix the convention instead.

The contract

tawa-docs/contract/platform-contract.json is a machine-readable list of facts. Each fact records the agreed value and a pointer to the code symbol that defines it.

{
  "id": "charge_default",
  "expected": "service",
  "status": "verified",
  "source": { "repo": "iec-builder", "file": "src/services/dependency-resolver.ts", "symbol": "dep.charge ?? 'service'" },
  "probe": { "type": "regex-string", "pattern": "charge\\s*\\?\\?\\s*['\"](\\w+)['\"]" },
  "docs": ["conventions/catalog-info.md"]
}

status distinguishes two cases:

statusmeaninga mismatch is...
verifiedthe code currently matches this valuea regression — fails CI
targetan agreed change not yet landed in codepending — warns only (until --strict)

So the contract doubles as a migration tracker: a target fact flips to verified the moment the code change lands.

The guard

tawa-docs/scripts/verify-contract.mjs (zero dependencies, npm run verify:contract) does three things:

  1. Code probes — for each fact, it reads the real value out of the service repo (iec-builder, iec-cron, iec-queue, iec-koko) and compares it to the contract. If a verified fact no longer matches, that is a regression (exit 1). This is what catches a developer changing CURRENT_CATALOG_VERSION, a queue default, the cron gas cost, etc., without updating the docs.
  2. Doc-prose lint — scans conventions/*.md for known-stale phrasings (e.g. callerOrg (default), retryDelayMs: 5000) and reports file:line. Set RULES_DIR=~/.claude/rules to also lint the distributed rules.
  3. Frontmatter check — every convention must have id and title, or sync-to-koko silently skips it (the doc never reaches the Bible or the site). Missing frontmatter fails the guard.

Run modes:

npm run verify:contract          # report, exit 1 on regression/error
npm run verify:contract:strict   # also fail on pending / lint / frontmatter
npm run verify:contract:json     # machine-readable snapshot -> contract/status.json
npm run verify:contract:publish  # POST snapshot to the builder dashboard

In CI, run verify:contract in each service repo (point DEV_ROOT at the checkout). If someone changes a pinned value, the build fails until the contract — and therefore the docs — are updated in the same PR.

Data-driven distribution (no hand-maintained rules)

iec-builder/scripts/seed-skills.ts reads tawa-docs/conventions/*.md (NOT ~/.claude/rules), maps each convention to a tawa-*.md rule via a small table, and pushes it to the builder skills registry. tawa skills sync then distributes the registry to every developer's ~/.claude/rules.

Two details matter:

  • Content-sensitive versions. Each skill's version is derived from a hash of its content (1.0.<n>). The builder manifest hashes name:version, so a convention edit changes the manifest hash, and tawa skills sync detects the update. (The old static 1.0.0 meant content changes were invisible to sync.)
  • Auth. Writing to the registry is admin-gated (it changes the instructions every dev's Claude follows). Seeding uses either a Bio-ID Bearer token (BUILDER_TOKEN) or the platform internal key (INTERNAL_SERVICE_KEY as X-Internal-Key).

NOTE: tawa skills sync is a one-way overwrite — it replaces the local file with the registry version, with no merge. Preview first with tawa skills list, tawa skills diff <name>, and tawa skills sync --dry-run.

The admin dashboard

tawa-web Console → Admin → Platform Contract (/console/admin/contract, super_admin only) shows live drift status: per-fact OK / regression / pending, the expected value vs what the code actually has, and any stale strings or frontmatter gaps.

The data comes from the builder endpoint GET /platform/contract-status (iec-builder/src/routes/platform.ts), which serves the latest snapshot published by verify:contract:publish. If the builder has not published yet, the page falls back to a bundled snapshot. The verifier needs filesystem access to the service repos, so it runs in CI (where the repos are checked out) and publishes the result; the pod only renders it.

Worked example: unified dependencies

The first decision landed through this system was collapsing internalDependencies / externalDependencies into a single spec.dependencies array:

  • Default transport janus (metered, JWT-verified). direct is the explicit opt-in for same-org UI/API splits; gateway is public/no-URL.
  • Scopes optional — a dependency without scopes just gets its {SERVICE}_URL injected.
  • internalDependencies / externalDependencies deprecated — still parsed (back-compat) but emit a warning pointing to dependencies.
  • charge default is service — the service's own org pays gas. callerOrg is the opt-in override (forward the end-user JWT as X-Forward-User so Janus bills the caller's org).

These were ruled in the contract first (as target facts), then the iec-builder code was changed (src/services/catalog.ts, dependency-resolver.ts), then the facts flipped to verified, and finally the conventions + distributed rules were corrected to match.

Where things live

ThingLocation
Canonical docstawa-docs/conventions/*.md
The contracttawa-docs/contract/platform-contract.json
The guardtawa-docs/scripts/verify-contract.mjs
Registry seederiec-builder/scripts/seed-skills.ts
Dashboard endpointiec-builder/src/routes/platform.ts (/platform/contract-status)
Dashboard pagetawa-web/src/app/console/admin/contract/page.tsx
Distributed rulesevery dev's ~/.claude/rules/tawa-*.md

How to use it day to day

  • Changing platform behavior? Update the code, then update the matching tawa-docs/conventions file in the same PR. If your change touches a pinned value, also update platform-contract.json (or the guard fails). CI catches you if you forget.
  • Adding a new pinned fact? Add an entry to platform-contract.json with a source pointer and a probe. Run npm run verify:contract to confirm it matches.
  • Distributing corrected guidance? Run seed-skills (populates the registry), then tawa skills sync (pulls to local rules). Preview with --dry-run first.

IMPORTANT: Never hand-edit ~/.claude/rules/tawa-*.md as the fix. That is a downstream copy. Fix the convention, re-seed, and sync — otherwise the next sync overwrites your edit.

Last updated: June 24, 2026