Spread the Rules table's treatment to every table that carries verbs
(record-table-actions, archived 2026-09-19): the table is align-middle,
the Actions header and cells are text-end, every button in an Actions
cell carries ms-1, and the header's word is for assistive technology
only (a visually-hidden span; Primer: row actions do not require a
visible column header). The two integrations tables and the member
domains list, which headed their controls with an empty cell, take the
hidden word too. A new anatomy-lint rule, actions-column, refuses a
table that drifts from any of it.
Decisions on the way: a row tint marks the row in play (the current
workspace, a staged change), never a record's status; the member catalog
lists only what can be bought, so a Listed product without an active,
synced, recurring price is left out instead of shown with "Not available
for purchase"; two Actions cells that carried text in a verb's place are
empty (the Placements column and the pending panel already say why); and
four record tables gain their width floor.
Specs: page-anatomy "Tables share one density" modified, ui-quality-gate
gains "Lint refuses an Actions column without its treatment",
member-product-discovery's Extras bucket and truthful-copy requirements
modified. docs/design-system.md §6 states the treatment and its reasons.
The Rules section is one record table grouped by kind, Limit then
Boolean, on fixed columns, edited in place: Edit opens a row's controls
in their columns, Add rule opens a dense row above the table, and every
change is staged into a tray that lists the deltas with Undo and applies
them as one rule-change act. The reduction policy is a column of the
rule beside its limit. History shows counts only. Group rows are a quiet
heading rather than a divider, the maintainer's pick from four rounds of
outside-model ideation.
Dense rows align to the top and render each error under its control in
every form family (design D16), replacing the below-row error block; the
forms library gains the batch form (rows plus one tray) and the RowField
dense and label-hidden options. Migration 00019 records the governing
reduction policy on effect rows.
Archive staged-rule-changes with its spec updates (entitlement-set-
management, entitlement-set-history, entitlements, form-library,
form-conventions, ui-quality-gate). Screens accepted 2026-09-19.
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
Derive Stripe test/live mode from the API key prefix at boot, failing on
unrecognized prefixes, and drop the separate `stripe-mode` config key.
Refine disabled controls to render through the shared `disabledControl`
part with the not-allowed cursor, and add a lint rule refusing
hand-rolled disabled buttons.
Adjust plan cards to offer no purchase control on free rungs, fix bound
checkbox Bool handling, and rename "Public/Private" to "Listed/Unlisted"
with enhanced readiness verdicts.
Archives openspec change slice3-walk-fixes and syncs its five delta
specs (fedwiki-sites, entitlements, operator-panel-navigation,
operator-list-scale, ui-quality-gate).
- FedWiki site usage is read from active site rows in both quota
readers; the reservation counter converges on the rows: raise-only
after farm sync and inside the create quota check, exact at boot.
The understated production counters repair on the first boot.
- The People tile caption excludes the reserved system person through
the same query parameter the directory uses.
- The operator Domains live-claims list is a governed list: pages of
50, true total, search over root name and organization, a
pending/active facet.
- New lint rule table-without-list-controls refuses an unpaged
page-body table unless it carries a list-scale exempt marker with a
reason; six curated or detail tables carry one. Its first run caught
the operator FedWiki sites list, which is now governed the same way.
- Entitlement-set rule copy: "Per unit", "Multiplied by the quantity
purchased or granted."
Below lg the top bar now holds the brand and one toggler; the account
items render in the drawer as a second, labelled list from a shared
partial, so both surfaces cannot drift. Desktop unchanged.
- Rotate the session token at the OIDC callback and restore the full
lifetime; cap pre-auth sessions at 15 minutes and write no session
for bare anonymous requests
- Treat db-dsn as a secret: accept db-dsn-file, log only host, port,
database and user, and never echo a malformed DSN in an error
- Guard the logout callback with a state cookie so a forged visit
cannot end a live session
- Collapse FedWiki site actions on a foreign tenant's domain to the
not-found answer, as for a domain that does not exist
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
Role extraction now reads only the verified ID token; the
access token is no longer parsed. Log a warning when the ID
token carries none of the role claim locations. Update
identity-provider docs and the oidc-login spec.
Extract ExtendGrantTx, RevokeGrantTx, and ExpireGrantTx into
internal/entitlements so the operator handlers, expiry activity, and
demo seed all run the same conferral path. The seed plays a five-grant
history on Carlos's org; pin-timestamps now uses dense-rank offsets
and transition list ordering ties break by transition_id.
Align the landing surface with the shared parts: sectionHeader for the
regions, the new readout part for every headline count, flush lists for
the System sheet, chevron-marked linked cards, and a boxed paginated
activity feed. Add the raw-section-title lint rule and classify the
remaining hand-written titles; archive the overview-consistency change.
Width becomes a one-rung step from the control's natural fraction,
and invariants bound the ladder. The grant's free text is renamed
to GrantNote, and both grant ledgers show it as a muted note line.
Parse every config value through a single declared type at save, boot,
and validate, and expose the settings seam from the terminal so
operators can list, set, clear, and validate overrides without SQL.
Every form on both surfaces is now a declared FormSpec in
internal/forms, rendered through the shared form part and parsed
by its handler through the same declaration. Create and edit
share one field list, refusals answer 422 with values carried
back, and layout, buttons and errors come from one part.
Also adds the form registry with its invariant and route-mapping
tests, the raw-form, filler-copy and Go em-dash lint rules,
declared action triggers, and the capture-coverage cross-check.
The forms-library change is archived under
openspec/changes/archive/2026-09-05-forms-library.
Six review rounds on the September walk's 40 findings, executed as the
acceptance-fixes change (design D1 to D31) and archived as
openspec/changes/archive/2026-09-03-acceptance-fixes/ with its 20
deltas synced into openspec/specs.
Shell and conventions: the location trail on every page rooted at the
surface; click-opened help popovers; buttons by role with the pressed
rule retired and tertiary openers with a rotating plus; the code chip
and white card headers; Remove versus Delete on row actions.
Operator surface: three overview tiles, one Integrations card, the
getting-started banner as the first instance setting
(core.instance_settings, migration 00015); record creation on its own
page for products, entitlement sets, and plan ladders; Visibility as
one Public checkbox; the Stripe provider page; the plan ladder
validation page retired and ranks ascending; org-types default change
with dispositions, Discard, and the settle-cloning fix; the rebuilt
composite billing card; hints instead of placeholders; the person page's
identity-provider glyph on its field labels; non-UUID ids answer 404.
Found by review and fixed: a session now ends when its person no longer
exists (the foreign-key failure on a ladder reorder after a snapshot
rebuild); the CSRF cookie lives as long as the session and its toast
says the page is out of date; htmx's settle delay is 0 app-wide.
Verification: unit suites, browser walkthroughs, the screens baseline
(31 screens, accepted), and Lighthouse at 100 on both widths, recorded
in docs/operator-a11y-baseline.md. Three stale issue entries archived.
The outside walk's evidence and synthesis for the 10k.4 acceptance:
missions, evaluator and heuristic reviews, outside reads, the
findings ACC-1 to ACC-40 with their loci, the review guide, and the
verification of ground truth per finding. The raw page-text dumps the
walkers read (screen-texts/) are left out of git from here on; nothing
cites them and they are reproducible from the app at this commit.
The ui-acceptance change carries the walk's proposal and task list;
its remaining tasks stay open.
Vendor htmx 4.0.0 and complete the migration with no compat shim:
mark inherited attributes with :inherited, rename hx-disabled-elt to
hx-disable, and drop hx-disinherit. Update static scripts to the
colon-separated event names and detail.ctx shape, and move error-swap
suppression to declarative noSwap config so 422 swaps natively while
403/5xx responses leave the target unchanged.
Handlers that branch on HX-Request now declare Vary: HX-Request. Add
lint guards for htmx 2 residue: hx-disinherit, camelCase event names,
and htmx-2-compat references.
Rebuild every operator, member, and integration template on the shared
page-anatomy parts, empty the lint allowlist, and move the confirm modal
into both shells. Replace back links with location trails and make row
identifiers the primary link. Make screen captures deterministic by
resetting the app database from a pinned demo snapshot. Send the logout
id_token_hint only while valid.
- Add shared ui_*.html parts (pageHeader, sectionHeader, statusBadge,
emptyState) parsed into every template set
- Add anatomy lint rules with a shrinking allowlist and screen-coverage
check
- Add make screens capture harness with contact sheets and baseline diff
- Compose member and FedWiki regions server-side so pages arrive
complete
- Rebuild Domains and Integrations on the parts as pilots
Extract the top bar and account menu into shell_topbar.html and the
member rail into shell_rail_member.html, backed by a single
server.Shell value. Move session controls into the account menu, add
the mirrored Operator panel/Member dashboard surface switch, and turn
the rail into an offcanvas drawer below lg with shell.js closing it on
navigation. Update docs, specs, and tests.
Unify application shell across both surfaces
Extract the top bar and member rail into shared partials and introduce
server.Shell as the single data value for page chrome. Move session
controls into an account menu, make the rail an offcanvas drawer below
lg, and add the mirrored surface switch.
Replace the entity slugs on organizations, workspaces, resource pools,
and
plan ladders with nullable `key` columns and add keys to products,
prices,
and entitlement sets. Rename `providers.slug` to `provider` and add
partial
unique indexes for system and org role names.
Assign invoice numbers per billing account from a gapless transactional
counter; Stripe's number moves to the invoice mapping as an external
reference.
Seeds, fixtures, and the operator lookup address rows by key, and the
returning-login resync no longer blanks a display name when the IdP
sends
no `name` claim.
Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.
People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.
Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.
Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.
Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
Move sub-surfaces into their sections: billing views get a pill row,
org types a header button. Replace inline IdP handoff copy with an SVG
icon and tooltip, add help icons to dense form rows, and delete the
registry-driven sidebar nav plumbing. Update specs and tests.
- Restructure operator sidebar into a flat task list with indented
children; fold plan topology into plan ladders
- Expand member catalog non-plan section to all published non-tier
products; require recurring Stripe-mapped prices for purchase
- Add operator domains placements and terminal-claims ledger; redirect
/domains to the FedWiki Sites Domains anchor
- Apply canonical vocabulary and chrome/form conventions; migrate seeded
FedWiki Sites display name
Implement the ux-first-run change: a state-derived setup checklist on
/operator/setup with a landing region that recedes once required steps
are done, and empty states that distinguish blocked from empty across
operator and member surfaces. Also add production deployment and
environment reference docs, plus a config-key completeness test.
- Add deployment-name branding to titles, mastheads, and OG tags
- Share one grant delivery-state query with lineage across grants
surfaces
- Show pool status/usage, org owners, and config readiness
- Make billing views projection-aware with recency and sync vocabulary
- Guard FedWiki creation without domains and render route-aware 404s
Enforce 10j's verified gaps (schema-hardening change):
- Migration 00010: partial unique indexes for one default pool and one
primary assignment per workspace, plus CHECKs pinning
pool/provider/subscription vocabularies and provider lifecycle
timestamps.
- Workspace creation shares a transactional provisioning function;
extension validates its target pool; last-tier deletion of a defaulted
ladder is guarded; signup completes plan-less on a broken ladder.
- Boot asserts integration slug parity and validates declared config
enums; Stripe invoice amounts are range-checked; domain cancellation
runs a final evidence probe; rule authoring is additive-only.
Add lifecycle_status = 'published' to the public-catalog queries
(plans and add-ons listings) and reject checkout before any Stripe
call unless the product behind the price clears the shared member
gate (published + active + public). The currently-enrolled ladder
rung stays renderable even if its product is later drafted or
retired, fetched directly so members keep seeing what they are on.
Introduce a single evaluateMemberGate definition shared by the
catalog paths and the operator readiness panel so the surfaces
cannot disagree about what is publishable for members.
Write the eight domain-model cards under `docs/models/`, mark M10 10i
Done, and update the design docs to v16. Also records Doc 44, which
ratifies Decisions 129-133 as amended, and the Doc-39 conformance
check that satisfied its gate.
Introduce `docs/models/` with the catalog index and its first card
(product-catalog), recording invariants, dimensions, and drift traps
per the Decision 141 documentation division. Add the model-cards
openspec change (proposal, design, spec, tasks) and sync the design
docs to v15: product kind taxonomy dissolved (Decisions 134–139),
Decisions 140–141 ratified, and product/pricing helpers updated. The
catalog is the single descriptive home for as-built models.
Put the fedwiki chain (init, render, farm, caddy) behind a fedwiki
compose profile symmetric with discourse's; the default composition is
neither, selected via COMPOSE_PROFILES in test/.env, so a default stack
no longer binds host 443.
Guard every script and walkthrough on service presence: shared
skipUnlessIntegrationEndpointReachable helper, seed-stack presence
checks (also repairing its unsourced .env and container-native render
invocation), generic root-owned testdata reclaim in teardown, discourse
coverage in verify-stack-isolation, and fedwiki's 8090 base in the port
probe.
Update stack docs and finalize status bookkeeping for all three
changes; archives the test-stack-integration-profiles change.
Guard cluster-global CREATE ROLE in all five migration streams with
pg_roles checks so multiple databases can migrate in one cluster, and
tolerate still-referenced roles on Down.
Add test/reset-test-db.sh to drop and recreate member_console_test and
member_console_e2e per run, emit their DSNs from bootstrap, and add a
make test target that resets then runs the suite serialized; parallel
unit packages sharing one database still interfered even after the e2e
split.
Fix customdomain_db_test.go, stale since 0affda7 and previously passing
only through pollution. Bootstrap and the Makefile carry small forward
references to the compose-profile knob introduced next.
Archives the test-db-isolation change.
Register bool and duration ConfigSpec keys from the Default's type, move
fedwiki's four sync knobs and discourse's two into their integrations'
ConfigSpecs, and replace core's read of fedwiki-custom-domain-target
with a core domains-connect-target key resolved once and threaded
through server and worker config.
Generate init's optional-integration scaffold sections from each
registered ConfigSpec instead of the hand-maintained list, and reword
the Temporal boot warning generically.
Archives the integration-config-parity change; status bookkeeping and
the verify-skill doc follow with the test-stack commit.
Run Docker runtime stage as non-root user app (UID 65532).
Add styled full-page 404/500 error rendering for navigation requests
while preserving plain-text responses for HTMX partials.
Reuse recent unconsumed OIDC login state to avoid state mismatch on
parallel login hits, and merge resource_access in role extraction.
Re-level template headings, add autocomplete tokens, and resolve
catalog resource display names.
Self-label test-stack secrets and document CSRF secret rotation.
Replace domain claims with open invoices, show monthly recurring
revenue,
count team organizations, and add more informative trend captions.
Use standard bordered cards and add a README screenshot with reversible
sample-data tooling.
Registry.ClaimExternal now enforces the plan gate itself via an injected
domains.ExternalClaimGate (pre-lock, typed refusals), so every entry
point — and any future consumer — inherits it from the allocation API.
One constructor in internal/server builds the gate from the entitlements
querier and connect target; it is injected into the member-facing
registry constructions in server.go and fedwiki.go and drives affordance
rendering on both surfaces. The duplicated helpers and resource-key
constants in fedwiki web and member_domains are gone; fedwiki no longer
reads entitlement tables for this gate at all.
Archives the change with the domains-registry spec delta (enforcement
location is now requirement-level: registry-inherited, surfaces derive).
Closes the entitlement-gate placement debt in issues.md; files the
separately-discovered operator force-release dead-end affordance bug
that a placed claim exposed in the domains walkthrough.
Domains leaves the member nav everywhere; GET /domains 302s to the
dashboard and domains.html is deleted. Claims are managed where they are
used: the fedwiki sites card embeds the core claims partial, a
server-conditional dashboard notice carries pending verifications (the
durable re-entry now that the page is gone), and the member_domains
partials retarget to 'closest .domains-surface' so multiple hosts coexist
on one page. Adding an external domain starts only from the create form;
the fedwiki banner slims to verified-unplaced one-click creates, since
the notice and embedded section own the pending state.
Verified at the surface end-to-end (stack + Chrome): nav absence,
redirect, notice lifecycle through claim-cancel, and cross-host swap
isolation with two claim views open. Archives the change with spec
deltas synced (domains-registry point-of-use rewrite, fedwiki-sites and
member-dashboard additions); files the entitlement-gate placement debt
in issues.md; adds the repo verify skill.
Move member domain management affordances to point-of-use within site
flows and retire the standalone member `/domains` page. Document
integration interdependence exploration.
Documents the merged 10f operator overview (56a743f, 4cec386) as a
spec-driven change: delta merged into operator-panel-navigation (at-a-glance
region with per-source em-dash degradation and provisions-backed delivering
count, registry-driven system rail with dead-letter-only alarm, brand-neutral
shell, no-skip heading outline). Milestones: 10f code-shipped; 10a screenshot
deferred pending the overview polish follow-up.
Sweep stranded pending claims at boot and on a Temporal schedule while
preserving evidence-based abandonment semantics.
Apply occupancy and name-policy checks to carves by operator-root owners
without affecting direct operator placements.
Make claim windows and workspace caps configurable, and enforce
initiation
and abandonment budgets without penalizing DNS evidence or system
failures.
Add operator visibility into live claims and default verification to 24
hours.
Domain names become an allocatable resource with one authority. A new
core module (schema `domains`, own migration stream between core and the
integrations) owns claims — a DNS node plus its whole subtree, mutually
disjoint: operator shared-domain roots, member claims carved from them,
and bring-your-own names proven by TXT verification — and placements,
which bind a name inside a claim to a provider slug and resource ref.
Verification moves to the claim and decouples from creation. A member
proves control of a domain once; afterwards every name inside it places
instantly, wildcard-CNAME friendly, with no further DNS work. The claim
workflow activates the claim and stops — it no longer creates a site —
so the sites list offers a one-click create once a domain verifies.
/domains/ask answers from placements and is registered by core rather
than the FedWiki adapter; its HTTP contract is unchanged. A configured
`domains-ask-fallback-url` forwards names the registry does not know to
a legacy answerer, the strangler seam wiki.cafe's migration needs; a
name the registry knows but has archived is refused locally.
FedWiki's create saga reserves the name before the farm call, carrying a
workflow-minted site id so retries are idempotent, and compensates on
failure. Sync places only names it owns, never stealing a member's;
lifecycle transitions and the retention purge maintain servability. An
unconditional boot pass seeds operator roots, releases orphaned
placements, and adopts pre-existing sites — grandfathering member-owned
external domains shortest-name-first, and skipping name policy, so a
live single-letter site cannot lose its certificate.
Members manage domains at /domains: claims with verification status, DNS
records including an optional wildcard row, check-now, cancel, release.
Name policy (reserved, blocked, premium, plus a single-letter guard) is
operator data; refusals collapse to a plain "unavailable" so the console
never becomes an oracle for who holds what.
BREAKING (pre-release): `fedwiki.custom_domain_verifications` and
`sites.is_custom_domain` are dropped, the flag now derived from the
placement's claim kind; resource key `fedwiki_custom_domains` migrates
to the platform-owned `external_domain_claims`; running
verify-custom-domain workflows must be terminated before deploy.
Each poll now probes both required DNS records (TXT challenge + connect)
and records per-record state with observed values, so the pending view
shows found/missing/mismatch diagnostics per record with copy-on-click.
Check now signals the workflow to probe immediately and reset backoff;
Cancel frees the domain instantly via a new canceled status. The pending
banner reopens the instructions view. Hardening from adversarial review:
poll timer clamped to the deadline with a final probe before expiry,
ContinueAsNew on suggestion, per-workspace pending cap, clipboard
failure feedback. Archives OpenSpec change custom-domain-verification-ux;
records the 10d domain exploration and wiki.cafe census in status/.
Verify-then-create: a fedwiki_custom_domains boolean entitlement gates a
custom-domain branch on site creation; a Temporal workflow polls a TXT
challenge (7-day window) and creates the site only after domain control
is proven. GET /domains/ask answers the on-demand-TLS ask contract from
registered sites (provider-neutral seam). Archives OpenSpec change
custom-domain-support.