Six procedures that could not be followed as written.
Eighty-nine curl examples single-quoted `Authorization: Bearer $TOKEN`, so the
shell never expanded it and the server answered 422 "Not enough segments". Nine
more did the same with X-API-Key. The other 129 examples in the same file
already used double quotes, so this was drift rather than a convention, and the
spec regenerated from it carried the fault onward.
The GE-Enforce report example put a `//` comment inside a JSON body. The server
parses with silent=True, so it saw `{}` and answered "hostname is required"
about a body that plainly has one - the worst kind of error message, one that
sends the reader to the wrong field entirely.
The IIS install ran `flask db upgrade` and a per-plugin install loop but never
`flask plugin upgrade-all`, leaving every plugin's own chain unapplied. That is
precisely the 1054 "Unknown column" a deploy then hits somewhere else, days
later, on the page that uses the new column.
The pilot runbook looped `flask plugin enable` over plugins that were not yet
installed; enable refuses those, so on a fresh database it exited 1 on every
iteration and enabled nothing. ADR-013 had already recorded that defect.
`apply-profile` installs and enables in dependency order, which is what the step
was reaching for.
DEPLOY-WINDOWS-IIS named a profile file that does not exist; the shipped ones do.
And PLUGIN-EXTERNAL-REPO never mentioned PLUGIN_TABLE_OWNERS, while the
migration engine raises for any plugin missing from it - so the guide's own
step 5 fails for any external plugin that owns a table. That the registry lives
in the framework repo is deliberate, so the guide now says so, and says what it
costs: a table-owning external plugin is a two-repository change, and a plugin
that owns no tables avoids it entirely.
Nine documents carried a hand-typed contract version and every one was stale.
One was load-bearing: PLUGIN-EXTERNAL-REPO.md told an external author to pin
">=0.13.0,<0.14.0" while the contract is at 0.19.0, so a plugin built by
following that guide is refused by the loader at startup. The plugin count was
wrong in six more.
They now point at docs/PROJECT-MAP.md, which is generated. A test enforces it:
no document may declare a version literal, a stated current version must match
the code, and a stated plugin count must match the tree. ADRs are exempt from
the current-version rule, because an ADR states the version a decision was taken
AT - that is a record of the past, and rewriting it would falsify the record
ADRs exist to keep.
CONTRACT-STABILITY.md was missing 0.17.0, 0.18.0 and 0.19.0 - including the only
BREAKING change in the series - in the one document a site reads to choose its
pin. All three are recorded, with 0.19.0 called out: it took something away, and
it shipped before it was written down, which is the argument for pinning tight
rather than trusting that a minor bump is safe.
REQ-D: restore waitress and tzdata to requirements.in. They existed ONLY in the
generated requirements.txt (hand-added in bf9e60e), so the next
`uv pip compile` would have silently removed the WSGI server and the IANA
timezone database from every Windows install.
REQ-E: split production and development requirements. requirements.txt was
installing pytest, pytest-cov, pytest-flask, coverage, iniconfig and pluggy onto
production servers. Verified on a real Windows Server box before this change.
CI, scripts/test-external-plugin.sh and the dev docs now use requirements-dev.txt.
REQ-F: standardise on Python 3.14. The repo declared four different versions
(Dockerfile 3.12, DEPLOY-WINDOWS-IIS 3.12, INSTALL-WINDOWS-IIS 3.13, CI 3.13,
plus README, web.config and PLUGIN-EXTERNAL-REPO). 3.14 is in active bugfix
support until ~Apr 2027 and supported to Oct 2030; 3.13 entered security-only in
Apr 2026. All four compiled dependencies publish win_amd64 wheels for 3.14
(cryptography via an abi3 wheel), verified by building an offline wheelhouse and
installing it on Windows Server 2025.
REQ-G: state MySQL 8.0 as the standard for new installs; 5.7+/5.6 remain
supported on an existing server.
Lockfiles regenerated with uv pip compile. Production deps 44 -> 38.
From the Fable/Opus documentation audit (8 confirmed + verified
lab-drift the run's session limit had cut short):
- HIGH: the lab's kiosk _kiosk_find_item block showed the pre-stage-17
row-id resolver as current; replace with the shipped gagelabtag /
numeric-tail resolver, fix the stale 'resolved by row id' prose and
the 'stage-7 code is corrected' note.
- MED: the badge _external_lookup block used dict-only row access that
breaks on a tuple cursor; use the tuple-or-dict form shipped. Split
'&&' command chains (fail in PowerShell 5.1) in the lab.
- LOW/link: the Windows note's [DEVELOPMENT-SETUP] link dropped the .md
and 404'd in four docs; fix. Correct the stage-6a->16a comment and
the lab-stage tag range (..16 -> ..17).
- Leaks: drop /home/camp path from ADR-006, the internal gitea host
from PLUGINS.md.
- Windows: add an mklink junction note for the external-plugin symlink
dev loop.
- CI: prime root to mysql_native_password so pymysql connects to the
MySQL 8 service without the cryptography package (and its kit wheel).
The CI workflow comment named the internal server, and
PLUGIN-EXTERNAL-REPO carried internal gitea clone URLs (it becomes a
public wiki page). Point both at the GitHub home / a generic CI
mention so the publication scrub gate passes and the wiki does not
expose internal infrastructure.
GitHub had no CI, so naming/tests/build were unenforced on the public
mirror. Add .github/workflows/ci.yml mirroring the internal pipeline:
backend pytest, the naming gate, frontend vitest+build, and the
migrations-mysql job that proves a fresh flask db upgrade + every
plugin chain on utf8mb4 MySQL 8 is idempotent. Flip the dev-setup CI
note to reflect it. Add an identical Windows/VS Code convention note to
the four developer docs (venv\Scripts vs venv/bin, $env: vs export,
pointer to DEVELOPMENT-SETUP).
Path.rglob does not descend symlinks, so a symlinked external plugin
(the ADR-003 dev loop) silently escaped the contract-purity scan. The
scanner now resolves plugin dirs before walking, a regression test
plants a symlinked plugin with a real violation and asserts it is
flagged, and the known-limitation notes in the external-repo docs are
lifted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- flask plugin new now scaffolds the frontend too: List/Detail/Form
views on the global styles, a gated route module (ADR-009), and an
api-client snippet emitted into the plugin dir. Views are written
before the route file so a partially generated plugin cannot 500 the
dev server.
- docs/PLUGIN-EXTERNAL-REPO.md + scripts/test-external-plugin.sh: how a
sister site develops a plugin in its own repo and runs the framework
contract tests in CI against a pinned framework ref (script verified
to fail on a broken core_version pin).
- docs/CONTRACT-STABILITY.md: settled vs churning contract surface and
the provisional 1.0 criteria.
- CLAUDE.md active-state refresh (contract 0.6.0, 11 plugins, 340
tests, measuringtools done).
Known limitation documented: Path.rglob does not descend symlinks, so
the import-surface contract test skips symlinked external plugins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>