SpaceWorks 0.7.5 - #15
Merged
Merged
Conversation
…rt and cutover Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…apps Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e, import and console Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ry, and per-tenant fan-out isolation Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…sons, and audit inside the completion transaction Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… recovery failures Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…d split failure cleanup out Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…bandoning the lease Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ry-file size limit CLAUDE.md was 227k chars against a threshold of max(40000, ctx*0.05*chars_per_token) — 150k on a 1M-context model, 40k on a 200k one — so it warned on every session. Invariants, the source map and the condensed changelog move to docs/, verbatim; what stays inline is what an agent must obey without knowing to look it up. Verified zero of 718 identifiers lost across the split. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChangePasswordGate had three password inputs with no autoComplete and no username field, so Chrome/Firefox/Safari could not offer to update a saved credential. Adds current-password/new-password, a visually-hidden sr-only username field (not display:none, which managers ignore), and label/input association. The Add-staff modal marks its password new-password so the actor's own credential is not autofilled into a field that sets someone else's. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ve, and close the ledger fail-open age recipient encryption authenticates nothing about the sender, so an optional expected_sha256 meant a planted archive was hashed by us and that hash recorded as authoritative. The digest is now required and validated before any upload or row write, and verify_content_ledger refuses an empty ledger for v2 archives, rejects duplicate paths, and asserts the ledger covers every payload file (manifest.json excepted). Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… silently re-enabling them Both capability reads swallowed every exception and returned permissive defaults, so a transient database fault re-enabled login methods an operator had turned off. The reads still must never lock anyone out, so they now write through a cache of the four booleans on success, return the cached values on failure, and fall back to permissive defaults only when nothing was ever cached. Failures are logged at ERROR; the read path still performs no database write. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…e ledger on a late payment _expire_checkout_best_effort only looked for a checkout session, so a native PaymentSheet charge (which sets only stripe_payment_intent_id) stayed live after staff waived or marked it paid offline, and the member could still pay a charge the ledger called waived. The closer now cancels the intent too, still best-effort so reconciliation can never be blocked and strand a charge. A paid webhook arriving after WAIVED now promotes the row to PAID_ONLINE through a narrow transaction-scoped trigger exception; after PAID_OFFLINE it preserves the prior resolution and records payment.double_paid_refund_required. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ckout feature gate, and gate printer stats The three member payment routes were mounted unconditionally in the makerspaces urlconf, so a deployment with payments tombstoned kept resolving them, kept them in the OpenAPI schema, and could still create real provider checkouts after reconciliation and the webhook were gone. They move to apps/payments/urls_member.py spliced in by config.urls.separable, with paths and route names unchanged. checkout_tool now re-checks inventory.self_checkout under the makerspace row lock, closing the window between the view's check and the issue; return_tool deliberately keeps no locked guard so an admitted return cannot be stranded mid-toggle. Public stats now require both machine_service and printing before publishing printer telemetry. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…e does not apply A superadmin holding an explicit machine-scoped membership in a makerspace that disabled superadmin access still answers ALL at the action level, so the 'if manage_scope is rbac.ALL: return queryset' shortcut handed them every machine and their role's links did nothing. Four sites now normalise the sentinel to the tenant ids the queryset represents and let role_scope's plural resolver decide EXEMPT versus role-linked per makerspace, so an ordinary superadmin is unaffected while a hidden-space membership is scoped. Covers the machine querysets, service files and consumable pools. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… and throttle presence starts NUM_PROXIES defaulted to None, so DRF keyed throttles on the whole caller-supplied X-Forwarded-For and an attacker varied it for unlimited buckets. Defaulting to 0 instead would collapse every user behind an unconfigured proxy into one bucket, so there is no safe blind default: production now refuses to boot when DEBUG is False and TRUSTED_PROXY_COUNT is unset, development defaults to 0, and django-axes derives its proxy count from the same value so the throttle and the lockout cannot disagree about who the client is. PresenceStartView gains a per-member throttle; identical-duration replays stay idempotent and do not consume it. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…staged files to their owner, refuse a bucket collision AuditLog is append-only, so raw invitation emails and claim redemption IPs written into meta were permanently readable by any VIEW_AUDIT holder. record() now sanitises metadata centrally by value shape and key token, replacing them with domain-separated HMAC fingerprints while leaving key names and the tenant_migration audit-reference paths intact. finalize_file now requires owner_user_id=actor.pk before storage promotion and revalidates under the row lock, so staff can no longer attach and read another user's staged CAD file. And equal PUBLIC_IMAGE_BUCKET/AWS_STORAGE_BUCKET_NAME now fails at compose init, as a Django system check, and in settings validation, since a check alone does not run for a direct gunicorn or Celery start. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ze refresh rotation RecoveryStateView was IsAuthenticated only, so any member could read deployment recovery state: auth generation, restore ids, the recovery principal and quarantine acknowledgements. It now admits only an active superadmin or the designated recovery principal, reusing recovery.py's own notion of the principal, and the quarantine route allowlist is unchanged so the principal can still acknowledge while quarantined. Browser and claim refresh rotation also delegated to SimpleJWT without serializing the blacklist check against the replacement mint, so a copied refresh token submitted concurrently yielded two independently rotating descendants; both paths now share one service that locks OutstandingToken by JTI inside a transaction and mints exactly one descendant, the loser getting the existing token_not_valid. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…cy-overriding subclass P5a's PresenceStartThrottle overrode allow_request and get_cache_key, which the claim-route guard refuses outright: an allow_request override can silently disable throttling on a claim-reachable route. It also resolved the makerspace inside allow_request via get_public_makerspace, whose Http404 would have rewritten the response from a throttle -- the same anti-pattern the source-gate invariant records. The view already declared throttle_scope, so MemberPrincipalRateThrottle covers it directly. Accepted tradeoff: identical-duration replays now consume the budget; the generous production rate is what protects legitimate re-checkins. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…revoked members reading machine data A Space Manager could point a Slack/Mattermost/Discord webhook at loopback, link-local or private space and have the backend POST from inside the platform network; urlopen also followed redirects. Every DNS answer is now checked and unsafe addresses rejected, at save time and again immediately before delivery, and delivery connects a socket to the validated sockaddr so the hostname is never re-resolved -- closing DNS rebinding, which a resolve-then-urlopen check cannot. TLS keeps the original hostname for SNI and certificate verification and sends it as Host; redirects are followed manually, capped at three hops, each target revalidated and pinned. No vendor allowlist, so self-hosted Mattermost still works. Separately, stale MachineOperator rows no longer grant a revoked member private machine reads in either resolver, and operator candidates and service-request requesters now require an active membership and active access status. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
The Lane D spec requires operator documentation to state that core dumps, process dumps and swap sit outside the application-level DEK guarantee. The sentence named process dumps and swap but never core dumps, so test_dek_guarantee_d8 was a standing red on dev - the test shipped without the documentation half it asserts. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scripts/import-backup.sh ends with exec "$ROOT/scripts/restore.sh", but restore.sh was committed 100644 and nothing chmods it: install.sh unpacks the release tarball with git's modes and only chmods install.sh and spaceworks-compose.sh. A backup import therefore died with Permission denied at the handoff, after it had already recorded the restore intent and released its lock. Same defect class as the spaceworks-compose.sh fix in 6c17f3d, so this adds the guard that was missing: the privileged shell scripts must be executable, and whatever a shipped script execs must be executable too. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test built its fake deployment against an older ops layout: it wrote the age identity to ops/age-identity.txt and shipped no compose wrapper, so import-backup.sh died at its second precondition with exit 1 while the test asserted 23. It was a standing red that never reached the behaviour it names. setup.sh and the script both use ops/work/age-identity.txt, so the test now builds that, plus a compose-wrapper stub that forwards to the existing docker stub. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_coverage_failures built its result in a defaultdict and called failures[makerspace_id].extend(row_errors) unconditionally, so a makerspace with no errors still created an empty list entry. dict(failures) was therefore truthy for a perfectly covered run and finalize_run marked every successful run FAILED. Also re-points two K2 fixtures at the current schema: Makerspace.slug is varchar(50) and the generated slugs reached 52 characters, and makerspace-scoped archives must carry superadmin_access_at_decision for the backup_makerspace_decision_snapshot_present constraint that landed after this work was written. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…K2 models The K2 merge added backup.BackupRun and backup.BackupRunCoverage and gave BackupArchive backup_run, build_holder and staging_object_key, which invalidates the checked-in schema/catalog digest and failed 26 tests across tenant_migration. Blessing the new digest is the explicit review act the constant's comment asks for: the graph diff against the pre-merge tree contains exactly those two models and those three fields and nothing else. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion handler The pooler-safe gate took its advisory lock on connections["default"].copy(alias=...), an alias absent from settings.DATABASES. django.contrib.postgres connects register_type_handlers to connection_created, which resolves connections[alias] to read hstore OIDs, so every lock connect raised ConnectionDoesNotExist and every gated write boundary answered 503 - in production as well as tests. Registering the alias cleared that but ran into pytest-django refusing undeclared aliases, and left a spurious extra connection per process purely for the OID lookup. This connection is only ever a transaction-scoped advisory-lock holder, so it does not belong in the ORM's connection handler at all: it is now a raw psycopg2 connection built from the default backend's own get_connection_params(), which already carries the effective database name under pytest. Backend-PID continuity, the granted pg_locks check, rollback-then-close release, failure conversion to SourceMigrationGateUnavailable and an independent connection per nested boundary are all preserved. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…r manifest (Lane D)
…ours Resolves the five-week-old filament-wip work onto the current tree. Two conflicts: models_service.py had become a thin re-export barrel under an LOC split, so the pool's new machine_type FK, color_hex, is_public and its two check constraints move into models_service_consumables.py where MachineConsumablePool now lives. public_status kept both sides' intent: machine-type identity comes from resolve_global_printer_type() as the branch intended (its PRINTER_SLUG import is gone), while tenancy stays routed through the servability policy rather than the branch's raw archived_at filter. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stries filament-wip predates the field-disposition guards, so its three new MachineConsumablePool fields had no dispositions: the Lane D field catalog and the data_export REDACTED list both refused the build. machine_type is a structural FK, color_hex a hex string and is_public a visibility flag - none is PII - so they are carried exactly like their siblings machine, color and is_active. Also recomputes the Lane D schema digest, whose only drift against the post-K2 tree is these three fields. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The checked-in snapshot predated 0022_target_import_recovery_mode, so RecoveryStateModeEnum was missing target_import in both the schema and the generated client. Regenerating after the filament merge picks that up; spectacular writes no trailing newline, so it is restored to keep the file's diff honest. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…jection policy The filament merge added MachineConsumablePool.machine_type, and every FK pointing at machines.MachineType needs a declared projection policy or the registry guard refuses the build. It remaps through the built-in/custom machine type map, exactly like the machine, pricing and service-queue edges: projecting a tenant into a target deployment must land its pools on the target's equivalent type rather than a source primary key. This is the fourth registry the three new fields touched, after the Lane D field catalog, the data_export dispositions and the schema digest. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sibility The filament merge changed what the machines module offers, and MODULES.md is the user-facing page the README links module names into. Records the either-machine-or-type scoping and states exactly what a public pool exposes: PublicPrinterPoolSerializer emits id, material and colour name only, so the hex swatch, lot code and remaining grams stay staff-only. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page read as live operator instructions for `cutover_printing_kernel`, but 237e0f3 (B7c) deleted that command with the rest of the apps/printing runtime, and 911f458 (B7d) deleted its read-only successor when it tombstoned the legacy tables. An operator following it during a real incident would have hunted for a command that has not existed since July. Reframed as a record: what the cutover did, what still exists (PrintingCutoverRepair behind a read-only admin, the provenance migration), and the forward-repair rollback boundary, which is worth keeping because the same rule governs every append-only surface here. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…make it opt-in
The operator-facing name was already "Member accounts"; only the key said `accounts`,
which read as ambiguous next to the separate `membership` module. The key is now
`member_accounts`, with a reversible data migration over Makerspace.enabled_modules -
mandatory rather than cosmetic, because _canonical_modules deliberately preserves unknown
keys, so an un-migrated row would have kept a dead `accounts` entry while the module
silently stopped working.
Both member_accounts and mobile lose default_enabled. Dropping it from member_accounts
alone would have been cosmetic: mobile declares requires_modules=("member_accounts",) and
install_module enables a key "and anything it requires", so a default install would have
pulled member accounts straight back in. A fresh makerspace now gets 8 modules - the six
core plus payments and updates - and staff sign in with email and password through
superadmin, Space Manager and custom roles, which never depended on this module. Existing
makerspaces are unaffected: migration 0057 already backfilled the key onto their rows.
Google and social auth are untouched - login methods live in PlatformLoginMethods behind
the /control/ switches, not behind a per-makerspace module.
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Written alongside the member_accounts rename rather than requested by it, so it lands separately: a module with no group vanishes from the console it is administered from, and an unknown group or a dependency on an unknown key should refuse at import. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Puts member accounts where operators look for it - under Membership in the console tick list - without coupling the two. It stays an independent module and changes only its group, because the alternative reading (making it a FeatureDefinition under membership) carries a parent_module prerequisite, which would force a space running Keycloak or walk-in records only to enable the member account ecosystem it deliberately does not want. That independence is the point: the registry comment on `membership` already says identity may instead come from external OIDC or a staff-created person record. GROUP_ACCOUNTS held only this module, so an empty group heading would have been left behind; the group is removed and the operator-facing half of its description - including "staff always sign in with a password regardless" - folds into Membership's, where it is now the reassurance that matters. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`validate-compose-wrapper.py` used Path at module scope without importing it, so it raised NameError before parsing an argument. spaceworks-compose.sh runs it under `set -euo pipefail` before `exec docker compose`, which aborted every compose command on every topology: setup, update, restore and backup import. Not on origin/main, so nothing deployed was affected. The guard executes every shipped Python script's module scope, which is the failure class; `main()` stays behind its __main__ guard so scripts with different CLI conventions are not held to argparse's exit codes. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pullable Every published tag was unpullable: `docker pull` failed with "manifest unknown" for latest, main, 0.5 and the pinned release, on both images. build-push-action leaves provenance on, so each tag is an OCI index whose platform and attestation manifests are separate UNTAGGED package versions; the tag-only keep-list deleted exactly those and left the indexes dangling. Retention moves to scripts/ghcr-retention.py, which decides by digest: a kept root protects every child in its index, and a child-discovery failure vetoes deletion for that whole package. Fail-closed is the invariant, not caution - treating an inspection error as "no children" reproduces the outage. The step is continue-on-error, so a silently-never-deleting implementation is invisible in CI; the fixture tests cover both directions and verify-release-images.sh runs after cleanup so a release that orphans its images fails at the release. Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Shaan-Shoukath
added a commit
that referenced
this pull request
Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges 66 commits from
dev. Merging this publishes a release —release.ymlfires onmain, andVERSIONchanged, so it will build and pushspaceworks-backend/spaceworks-frontendimages to ghcr and publish a GitHub Release titled SpaceWorks 0.7.5.What's in it
Events program (four phases) — cross-makerspace collaborative events with host-waiver acceptance, QR check-in, opt-in attended-events on the maker profile, and registration decoupled from presence (presence is now proven at the door by the staff-scanned QR rather than a self-declared session).
Module-architecture program — operator-facing module groups, one module map per staff tab,
payments/accounts/mobile/updateskeys, Razorpay behind a provider seam, a superadmin modules console, cloud/full deploy profiles, the account-less identity seam with staff-created walk-in members, four platform login-method switches, maker profiles and a member directory, and staff-side event registration.Notifications v2 — per-event recipient selection, per-room chat destinations, editable email/chat wording, and object-scoped recipient rules.
Plus scoped PII encryption, custom per-makerspace roles, machine-scoped
MANAGE_MACHINES, and an accessibility floor (contrast guard, focus indicators, skip links).Release mechanics
VERSION0.5.1 → 0.7.5VERSION— it had been pinned at0.1.0whileVERSIONclimbed to 0.5.1, so every schema consumer was told a version that had never been true. A test now fails if the two drift.SpaceWorks <version>instead ofv<version>. The immutable tag keeps itsv<base>-main.<run>.<sha>form.CHANGELOG.mdgains a 0.7.5 section. Note 0.3.0–0.5.1 were never recorded — generation stopped after 0.2.0 — so that window points atgit loginstead of inventing entries.Review
The final commit closes four rounds of
codex exec reviewon the events program. Each round found a defect the previous one missed, all the same shape — money or access in a state transition happening later than the code assumed:eventsstranded host-raised charges: a receipt vanished and a pending charge became unpayable.Round 4 returned no actionable defects.
Verification
Backend suite green against the seven known pre-existing failures, each verified by reason rather than name. Tombstone suite 140, vitest 76,
tsc -bclean, migration drift clean. New regression tests were each confirmed to fail when the bug they guard is reintroduced.QA
End-to-end QA signed off by the repository owner (@Shaan-Shoukath). Recorded here as the owner's attestation — it was not performed or observed by the automated review, which covered the test suites, static checks and the build only.
Automated verification for the record: backend suite green against the seven known pre-existing failures (each confirmed by failure reason, not name), tombstone suite 140, vitest 76,
tsc -bclean, migration drift clean. Each new regression test was confirmed to fail when the bug it guards is reintroduced.Known open item — follow-up in progress
A host purging
eventsstill deletes visiting members' charges:module_purge._purgeremoves every host-owned event payment, including one routed to a visitor's own space, so a paid receipt is destroyed and a pending debt silently vanishes.This predates the PR and is being fixed separately on
fix/purge-preserves-visiting-member-payments— no module purge will delete a payment at all, and asubject_labelsnapshot onPaymentkeeps a surviving receipt readable.Correction to an earlier version of this note: it claimed a subject-less pending charge would still be payable. That is false.
Payment.save()callsfull_clean()unconditionally andPayment.clean()requires the subject row to exist, so every write path — hosted checkout, mobile intent, offline reconciliation, and webhook settlement — fails once the subject is gone. Read paths degrade safely; write paths do not. Makingclean()tolerate a missing subject on an otherwise-unchanged existing payment is therefore part of the fix, not an optimisation.🤖 Generated with Claude Code