Generate the facts that go stale, and record the fortnight in the changelog

CLAUDE.md is read at the start of every session and claimed contract 0.16.0 and
1159 tests while the code was at 0.18.0 and 1567, along with a plugin count and
a chain head that were both wrong. Hand-written facts about a moving codebase
are wrong within a fortnight, and a reader who cannot tell which lines are stale
has to re-derive all of them.

So they are derived. scripts/gen_project_map.py reads the versions, the plugin
inventory, every Alembic chain head, the ADR index with status and the size of
the codebase out of the code, and --check fails a build where the committed map
no longer matches. CLAUDE.md now points at it, leads with the multi-site rule,
and lists the gates to run instead of restating the conventions they enforce.

The changelog's Unreleased section covered 8 of the 46 commits since 0.9.0 and
had no Changed, Fixed or Security sections at all. It now carries the whole
fortnight, including both contract bumps - which had never been recorded even
though ADR-002 makes contract versions their own series.
This commit is contained in:
cproudlock
2026-08-14 13:47:48 -04:00
parent 035419fa51
commit 8f9805d964
4 changed files with 543 additions and 124 deletions

View File

@@ -66,6 +66,106 @@ ADR-007 and ADR-002.
components nobody wrote. Cards are ordered by severity, hide themselves when
there is nothing to report, gate on a permission, and each fetches
independently so one broken endpoint cannot blank the board.
- Tech Tools: a section for the small conversions and code generation that
otherwise happen in a browser tab on somebody's phone. Starts with barcode and
QR codes laid out for the label stock actually in the printers.
- Kiosks report what they are. The dispatcher already read
`C:\Enrollment\display-type.txt` to pick its target; now it tells ShopDB, so
the Asset column on an enforcement report shows a reported value instead of
one inferred from a mapping table nobody filled in.
- Enforcement reports carry what the PC is, where it is, and whether it is
backed up: the host links to its own page, the Asset column names the machine
or instrument the PC controls, a map pin shows the location on hover, and the
backup column judges the chain rather than printing its last date.
- `flask relationships audit`, read-only, listing links that cannot both be
true, plus an inverse guard that refuses to create one.
- An employees endpoint that says WHY a name did not resolve, rather than
returning nothing and leaving the caller to guess.
- The display client module updates itself through the manifest, so a client
change no longer means hands on every kiosk.
- `docs/PROJECT-MAP.md`, generated by `scripts/gen_project_map.py`: versions,
the plugin inventory, every Alembic chain head, the ADR index and the size of
the codebase, derived from the code. The hand-written equivalents in CLAUDE.md
had drifted by two contract releases and 400 tests.
### Changed
- Plugin contract **0.19.0**: `get_dashboard_widgets` returns data and shape,
not a component name. The old shape named a Vue component per widget, which
cannot survive a lean build (ADR-013) and had five plugins pointing at
components nobody wrote. The change itself shipped earlier without a bump and
with `BasePlugin` and PLUGIN-HOOKS.md still teaching the dead shape; this
records it and corrects both.
- Plugin contract **0.17.0**: `DashboardDefault` is part of the `shopdb.api`
surface, so a plugin can resolve a display without reaching into core.
- Plugin contract **0.18.0**: one name per display role, and it is the kiosk's
own (`Dashboard`, `Lobby`, `3DPrintRoom`). Server and dispatcher had three
vocabularies for the same three kiosks, and a display could report a role core
could not store. `normalize_display_role` resolves case and the retired
spelling; an unknown value is kept verbatim so a typo stays visible.
Contract versions are their own series (ADR-002) and belong in this file;
0.17.0 and 0.18.0 shipped without an entry, which is what this note corrects.
- Publishing a GE-Enforce manifest is refused when PCs reporting for that scope
run an older client library than the manifest needs, naming the hosts. A minor
version bump that NARROWS behaviour is not backward compatible: an old library
reads a gated entry as ungated and installs every version it cannot detect.
- Shadow mode runs inside the enforce cycle instead of on its own schedule. The
share is mounted only for the length of a cycle, so a task on its own clock
woke to a missing drive and reported 0 installed / 0 skipped / 0 failed, which
is indistinguishable from a healthy no-op.
- A GE-Enforce report is judged stale on both clocks, the server's and the
client's asserted check-in, so a PC cannot mask real silence with a future
timestamp. A PC that has gone quiet stops reading as healthy.
### Fixed
- **Every plugin migration chain re-ran its head on each MySQL deploy.** Core's
Alembic env got a `connection.commit()`; the per-plugin template did not, so
the last migration of every run lost its version stamp while its DDL survived.
Invisible for as long as every head happened to be idempotent - and two were
not. The backups head cleared a column for every row, and the geenforce head
added a column unconditionally and failed on the second attempt. Both are now
bounded and guarded, and the template commits.
- A part-marker PC reported 200 on the cycle that created its marker and 500 on
every cycle after. Both of the links it makes were looked up by label, so a row
made by hand or by the legacy import was invisible and the insert violated the
uniqueness of (source, target, type); and the marker's asset number is derived
from the PC, so it could already be taken.
- A CMM reported the same way: the instrument IS the reported bay, so adoption
was relabelling the row the machine sync owns and finding it by that label.
- A reported identifier is matched exactly rather than with `ilike`, so `_` and
`%` in a file on a shopfloor PC are no longer wildcards. `MT-600_` had been
adopting MT-6001, and a bare `%` adopted whatever active asset came first.
- Measuring tools are adopted before they are minted. Minting derived the asset
number from the HOSTNAME, so a permanent instrument took the identity of
whichever PC drove it that week, and the collector could not see a tool it had
not created - which left legacy instruments shadowed by minted twins.
- An unanchored rsync exclude in the export was matching at any depth, so a
whole plugin was silently dropped from the published payload. Root excludes
are anchored and the export now aborts if a plugin manifest fails to survive.
- Backup revisions are ordered in Python, not with `ORDER BY ... NULLS LAST`,
which SQLite accepts and MySQL rejects.
### Security
- The Windows installer breaks ACL inheritance on the roots it creates. A
directory made directly under `C:\` inherits an inherit-only Modify grant for
Authenticated Users, so `C:\shopdb-flask`, `C:\Python314` and `C:\MySQL84`
were writable by any logged-on user. Two files inside them make that a local
privilege escalation: `shopdb-admin.ps1`, which self-elevates, and
`instance/config.py`, which the app executes unconditionally. The MySQL data
directory is locked down as well, since it holds the password hashes. A Python
installation this installer did not create is reported rather than seized.
- Site-specific values no longer ship in product code (ADR-015). The backups
share root defaults to blank instead of one site's file server, the shadow
client no longer overwrites a site's own ShopDB URL in the registry on every
cycle, the kiosk dispatcher takes its URL from the `site_base_url` setting and
refuses rather than guessing, and one site's hostname, FQDN and internal
subnets are out of the documentation, the generated API specs and the UI
placeholders. The ADR-015 scanner now covers PowerShell, the installer, the
seeds and generated JSON, case-insensitively, and FAILS the build; deliberate
organisation-wide defaults are declared with an `ADR-015-OK` marker. Both
publication gates carry the site patterns.
## [0.9.0] - 2026-08-11