DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path inspection-briefs/loomworks-b49-step-0-findings-v0_1.md

Loomworks — B-49 Step 0 findings — gating what is clean — v0.1

Version. 0.1 Date. 2026-08-04 Author. Claude Code (executing session), DUNIN7-M4. Brief. inspection-briefs/loomworks-b49-step-0-inspection-brief-v0_1.md — confirmed present and the only B-49 brief in the directory (numeric sort: no v0_2 or higher exists). Charter. standing-notes/dunin7-standing-authorization-charter-v0_1 — R-5 inspection run. Status. Read-only, as fenced. Produces no change, no recommendation on gate design.


0. Environment


1. Question one — what runs today, in both repositories (§3)

3.1 Every workflow file

Engine — exactly one: .github/workflows/api-docs.yml, job check-api-docs.

Surfacezero workflow files. find /Users/dunin7/loomworks/.github -type f returned nothing; there is no .github directory at all. Confirmed independently via gh api repos/DUNIN7/loomworks/actions/workflows, which returned an empty workflow list, and gh api repos/DUNIN7/loomworks/actions/runs, whose total_count is 0. Nothing has ever run in CI for the surface, at any time.

3.2 What the triggers actually cover

The engine's job triggers on pull_request as well as push: main — by the YAML alone, it would run before a merge, if a merge arrived as a PR. It never does. gh pr list --repo DUNIN7/loomworks-engine --state all --limit 5 returned zero PRs. git log --oneline --merges -5 shows the actual mechanism: local git merge --no-ff producing a merge commit (e.g. 7bdbf8b Merge CR-2026-166 (engine) into main: the clean-report cluster, D-1), pushed directly to main. The pull_request trigger has no branches to fire on — it is not dead configuration in the abstract, it is configuration for a merge path this project does not use. The only trigger that ever fires is push: main, which by construction runs after the code is already on main.

Answer to 3.2's question directly: nothing runs before code reaches main, in either repository. The engine's workflow fires only after the merge; the surface has no workflow to fire at all.

3.3 Anything outside the workflow files

Checked in both repositories: no Makefile, no .pre-commit-config.yaml, no .husky directory, no lint-staged config, no active git hook (core.hooksPath unset in both; .git/hooks/ contains only the stock .sample files, none installed). Branch protection on main was checked via gh api repos/<owner>/<repo>/branches/main/protection for both repositories — both return 404 Branch not protected. There is no gate of any kind, CI or otherwise, in front of either repository's main.

3.4 The engine's doc-freshness job, read properly

This is the central finding of this inspection, and it corrects an assumption in the brief itself.

The brief's Section 5.1 frames the doc-freshness job as the candidate for a vacuous pass — "it runs, it is green." It is not green. Run locally at HEAD (7bdbf8b), with dependencies synced exactly as CI syncs them (uv sync --extra dev):


$ uv run python scripts/check_api_docs_fresh.py
API docs are STALE — run scripts/generate_api_docs.py and commit:
  - docs/api/api-reference.md
  - docs/api/api-detail-blocks.html

Exit code 1. Diffing the generated content against the committed files (in-process, no file written) shows real, substantive drift — not a whitespace artifact: the committed docs/api/api-reference.md describes 206 operations · 289 schemas; the live schema has 228 operations · 318 schemas. Fields such as voice_interpretation_disabled, alternatives, requires_operator_comment, operator_comment, and a widened source_mode enum (discovery, external_credential) exist in the live schema and are absent from the committed docs.

git log --oneline 9265c97..HEAD -- src/loomworks/api/9265c97 being the last commit to touch docs/api/ (2026-06-28) — counts 46 commits touching the API surface since, up to and including HEAD.

This was checked against GitHub's own run history, not inferred from the local run alone. Via gh api repos/DUNIN7/loomworks-engine/actions/workflows/291008209/runs:

So: does it fail when it should? Yes — it is failing right now, correctly, on real drift, and has apparently never once passed since it was created two months ago. But the brief's framing needs correcting: the hazard here is not a check that passes vacuously. It is a check that has been red for its entire life, on every run, and that redness has not stopped a single merge, because nothing consumes its result — no branch protection, no PR gate (no PRs exist), nothing blocking push: main. A red check that blocks nothing is functionally identical to a vacuous green one: neither has ever prevented anything from landing. The corrected framing for whatever gate design follows: this is not "install a gate over what's clean," for this specific check — it is "a check already exists, has already caught real and repeated drift, and has been ignored 112 times in a row." Grandfathering here does not mean tolerating debt accrued before a new rule; the debt (docs/api/ two months and 46 commits stale) already exists under the current rule, unenforced.


2. Question two — cost of a gate over the current clean state (§4)

Four commands, each run twice, locally, against the throwaway database where a database was needed. Both runs of every command produced identical pass/skip/fail counts; no test flipped between runs; no output differed once non-deterministic content (elapsed-time strings) was stripped.

| Command | Run 1 | Run 2 | Deterministic | |---|---|---|---| | Engine suite (uv run pytest -q) | 3522 passed, 68 skipped, 486.73s | 3522 passed, 68 skipped, 496.19s | Yes — identical counts, grep -c FAILED\|ERROR = 0 both logs | | Surface lint (npm run lint) | 0 findings, 5.29s wall | 0 findings, 4.52s wall | Yes — byte-identical output | | Surface tests (npm run test, vitest) | 112 files / 696 tests passed, 17.37s wall | 112 files / 696 tests passed, 17.31s wall | Yes — identical file/test counts | | Surface build (npm run build) | Compiled + typechecked + 21 routes generated, 6.35s wall | Same, 5.69s wall | Yes — outputs identical once timing numbers stripped |

4.1 — does it currently pass? All four: yes, as shown above.

4.2 — how long? The engine suite is the outlier at roughly 8 minutes; every surface command is under 20 seconds. A gate combining all four is dominated entirely by the engine suite's wall-clock, not the surface's.

4.3 — what does each require?

  1. ALEMBIC_URL=... uv run alembic upgrade 0033 (engine chain to the pre-break revision — required first; skipping to head from empty fails at revision 0034 and rolls back the whole invocation, per the README's own documented trap).
  2. DATABASE_URL=... uv run python scripts/bootstrap_engagements.py (plants the administrative-engagement row and three siblings; the chain cannot pass 0034 without them).
  3. ALEMBIC_URL=... uv run alembic upgrade head (engine chain to head).
  4. STELE_DATABASE_URL=... uv run alembic stamp 0001_baseline, run from Stele's own checkout (/Users/dunin7/stele) with Stele's own alembic.iniuv run --project from the engine's directory is documented as not equivalent.
  5. STELE_DATABASE_URL=... uv run alembic upgrade head, same checkout.

All five ran clean against the throwaway database this session, with no deviation from the documented sequence and no failure at any step. A fresh runner needs: a Postgres instance with a role holding CREATEDB (or a database pre-created by one), a checkout of the Stele repository alongside the engine's (path-dependent — the engine's pyproject.toml sources Stele via { path = "../stele", editable = true }), and LOOMWORKS_SECRET_KEY — either as an environment variable or via .env fallback (three migrations, 00620064, raise a RuntimeError naming the fix if neither is present). This session's .env already carried the key; a genuinely fresh runner (CI) would need it supplied as a secret.

One environment trap surfaced during this session that is itself a finding worth carrying into gate design: setting LOOMWORKS_ENV=test to run the suite is actively wrong. src/loomworks/api/app.py calls create_app() at module import time, and create_app() invokes a posture guard (enforce_startup_posture) that refuses to boot when loomworks_env looks like a dev posture (which "test" triggers) and the configured WebAuthn origins are public (.env's WEBAUTHN_RP_ORIGIN=https://app.dunin7.com, a leftover of the real deployment config this repo's .env otherwise carries). The result was 18 test-collection errors, all the same PostureConfigurationError, naming the 2026-07-19 dev-endpoint-exposure incident. Removing that override and leaving LOOMWORKS_ENV unset (falling back to .env's production, which the posture guard accepts) and setting only DATABASE_URL_TEST is what actually ran clean. Any CI job standing up this suite needs to know this, or it will fail on a posture guard rather than on a real test failure — a failure mode indistinguishable from a real one without reading the traceback.

4.4 — determinism. Established above: every command, run twice, produced identical results. No flake surfaced in either repository's suite.


3. Question three — what the gate must not do (§5)

5.1 — a gate that passes vacuously. Addressed at length in §1 above (3.4): the engine's one existing check is not this hazard's example — it fails, correctly, on real drift, every time it has ever run. The hazard as stated (a check that is green and checks nothing anyone worried about) was not found in either repository's current configuration, because the surface has no checks at all and the engine's one check is red, not green. For any new candidate gate (the four commands in §2), the construction check is: each one currently exits non-zero on a real defect by design — pytest fails a test, eslint fails a lint rule, vitest fails an assertion, next build fails a type error or a build error — none of these is a check that structurally cannot fail. No candidate gate examined here is a check with no failure mode.

5.2 — a gate that hardcodes a moving number. None of the four commands in §2 needs to encode a count to function — pytest, eslint, vitest, and next build all gate on exit code, not on a printed total. The one place a moving number appears adjacent to a gate is inside the doc-freshness check's own output: docs/api/api-reference.md's header line states 206 operations · 289 schemas, a committed, generated figure, not a hardcoded assertion in the check itself — the check compares full file content, not this one line in isolation, so a schema count changing does not by itself require touching the check's logic. Nothing in the four commands, or in the one existing workflow, hardcodes a test count, a coverage percentage, or any other figure that would break the next time a test or route is added.


4. Question four — what is deliberately not gated (§6)

Not decided here (B-48's ground, and this brief's own fence) — reported as options only, per the brief's instruction to choose none.

What a truthful statement of current coverage would need to say, if written today:

Options for making coverage legible, none chosen:

  1. A line in each repository's README stating exactly which commands are gated and which are not, updated whenever the gate changes.
  2. A comment inside each workflow file, next to the job definition, naming what it does not run (mirroring how api-docs.yml's existing header comment already explains what the job does — extending that pattern to also state its limits).
  3. A badge per check (docs-freshness, tests, lint, typecheck) rather than one aggregate CI badge, so a reader sees which specific claims are green versus absent, rather than inferring coverage from a single pass/fail.

5. e2e — reported, not run (§7)

The surface's Playwright config (playwright.config.ts, read in full) states plainly, in its own header comment: "E2E tests run against the live substrate at localhost:8000 and the dev server at localhost:3001. Neither server is started by Playwright — bring them up manually." fullyParallel: false, workers: 1, single Chromium project. It cannot run in CI as configured — there is no step, in any workflow (there are none in this repo) or in the config itself, that starts either the engine (which itself needs the five-step database stand-up from §2) or the Next dev server. Making it CI-capable would require authoring that orchestration from scratch; this brief does not do that, and this session did not run npm run test:e2e.


6. Corrections preserved


7. Unread and unrun


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — B-49 Step 0 findings — v0.1 — 2026-08-04 The doc-freshness check is not the gate design's vacuous-pass worry. It is the gate design's already-ignored-112-times worry.