The drafter block and the register: /profile/, rebuilt (§16) - #12
Merged
Merged
Conversation
… (§16) The complaint was that the profile sheet had stopped being readable, and it measured: `<main>` in the export held 1260 words, eleven `<h2>` panels and twenty form controls before a single React island mounted, with the two controls a reader comes here for about 700 words apart. Three things were invisible without counting them — the mark picker was rendered twice on one page (`IdentityPanel` and `RolePanel`'s `MarkOffer`) with a third copy on `/sign-in/alias/`; four surfaces wrote `identity.name`; and the identity card's five-row glossary explained five fields whose values were not on it. So the page is now two blocks. The drafter block is open: the drawing with its MARK and SEED lines, then the alias field and one compact mark row, then sign-in — one box holding the two answers to how this record is known, a 1.5px painted rule apart. Everything else is a register of ten folds, closed. The rule that makes folding honest is §16.4.1: a closed row prints the number its panel exists to report, so folding removes prose and never a fact. It is §10.4's contract on a silent gauge applied to a disclosure, `RegisterRow` refuses a blank reading at render time, and three e2e gates hold it — the strongest being that a row's closed reading must equal the reading its opened body prints. Every reading comes from the selector its own body already uses; none is a new count. The mark picker is one implementation with two modes, because `AliasSheet`'s KEEP THIS ALIAS must not claim an act already performed: uncontrolled it writes on change, controlled the parent holds the draft. `data-hl-mark` stays on the label with the 44px touch floor, and `idPrefix` keeps all three radio group names as they were. An email sign-in now names the alias once, from the local part, with a line saying where the name came from. It writes, so every guard is load-bearing: inside the claim's `.then` from a fresh snapshot, only when the name is null, only with an email identity, and only once — `prefs.aliasNamedFor` is the durable flag, because `AccountSync`'s effect remints its user object on every token refresh and without it clearing your name would not be final. Measured after: 1060 words in `<main>` counting every closed body still in the DOM, 426 on arrival. Four gates green, plus the sub-path build: typecheck clean, 94 files / 2838 tests, 57 pages exported, 365 e2e passed. Spec: §16, docs/superpowers/specs/2026-09-02-lms-phase6-drafter-block-design.md
…ay in Five review lenses over 884b603 produced nineteen findings; eight were refuted by a verifier whose job was to refute them, and these are the eleven that survived plus one whose verifier crashed and which measured itself. **The alias offer overruled REMOVE NAME.** `prefs.aliasNamedFor` recorded that an offer had been TAKEN, never that one had been DECIDED, so a reader whose record already carried a name at sign-in never got the flag written — guard one returned before the flag's only writer was reached. Clear the name afterwards and the next claim wrote the local part of the address over it. Two agents reproduced it independently, and the file's own docblock claimed the opposite invariant. The offer is now decided once per account: the flag is written whenever the question is settled, and the name only when there is a null name to fill. An erase keeps the flag, because `carriesNothing` deliberately ignores `prefs`, so keeping it cannot resurrect an erased record — and without it a record the reader had just erased came back holding their name and a day. **The §16.4 gates could not tell a row from its reading.** A reviewer swapped uptime's reading with stamps', hard-coded the storage reading to `PERSISTENT · QUERIED` with its hydration gate dropped, froze the role reading and inverted the keyboard's — four simultaneously lying readings, one of them asserting persistence before `navigator.storage` had been asked — and the whole suite stayed green: 2782 unit, 365 e2e, byte-identical. The gates were one-way implications, vacuous for a reading with no digits. There is now a per-row table of the expected reading at two fixed seeds, and a row in `REGISTER_ROWS` with no entry fails. Measured after: the same swap turns that gate red and leaves the other four green, which is what the finding said and where it said it. That gate then earned itself before shipping: with the storage row closed and the browser having answered, the summary printed `UNKNOWN` while the body one line below printed `BEST-EFFORT`. `storageReadout()` was a bare call, and the grant is deliberately not written through the reducer, so the summary subscribed to nothing and froze. `useStorageReadout` subscribes it. The rest: the Organisation row's closed reading contradicted its own body under the default flag; `ACCOUNTS NOT ENABLED` and `ACCOUNTS NOT ENABLED YET` were two spellings of one status five lines apart in one box; `OFFERED FOR YOUR ROLE` printed inside a 54px cell wrapped to four lines and made every cell 110px tall, which is the compact row's whole purpose undone, so the offer is now the dashed cell plus the words in the shared description line; the print block could not open a `<details>`, so paper got ten readings and no bodies; `.hl-drafter-note` was dead CSS quoting a string the site had rejected; and three docblocks stated things the code does not do — a heading level that moved, a writer that writes nothing, and 163 words measured off the mockup rather than the build. Arrival is 351 words, from 1260. Five gates green: typecheck clean, 94 files / 2847 tests, 57 pages exported, 366 e2e, and the sub-path build.
There was a problem hiding this comment.
🟡 Changes recommended
Alias decision persistence, provenance, account switching, and profile synchronization have unresolved correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Rebuilds /profile/ around an open drafter block and a ten-row disclosure register while consolidating mark selection and alias naming.
Changes:
- Introduces the drafter/register layout and responsive styling.
- Unifies mark pickers and adds account-derived alias handling.
- Expands unit and end-to-end coverage for disclosures, authentication, accessibility, and record behavior.
File summaries
| File | Description |
|---|---|
tests/unit/record/validate.test.ts |
Derives expected schema keys from EMPTY_RECORD. |
tests/unit/record/stamp.test.ts |
Updates preference fixtures. |
tests/unit/record/schema.test.ts |
Verifies the widened preference schema. |
tests/unit/record/register.test.tsx |
Tests register structure and readings. |
tests/unit/record/merge.test.ts |
Updates merge fixtures. |
tests/unit/record/boot.test.ts |
Updates boot-state fixtures. |
tests/unit/record/alias-naming.test.ts |
Tests alias decisions and erase behavior. |
tests/unit/identity/alias-offer.test.ts |
Tests email alias extraction and schema handling. |
tests/unit/copy-register.test.ts |
Extends reader-visible copy scanning. |
tests/unit/auth/chrome.test.tsx |
Tests inline and panel auth variants. |
tests/e2e/responsive.spec.ts |
Adds responsive profile coverage. |
tests/e2e/record.ts |
Adds register helpers and preference typing. |
tests/e2e/colour-not-alone.spec.ts |
Covers forced-color profile states. |
tests/e2e/alias.spec.ts |
Uses label-driven mark selection. |
tests/e2e/accounts.spec.ts |
Opens the register before erasing. |
tests/e2e/accounts-disabled.spec.ts |
Verifies disabled authentication on profile. |
src/lib/record/validate.ts |
Coerces the alias-decision preference. |
src/lib/record/store.ts |
Adds a subscribed storage readout. |
src/lib/record/scope.ts |
Defines alias provenance copy. |
src/lib/record/schema.ts |
Adds aliasNamedFor to records. |
src/lib/record/events.ts |
Adds the alias-decision writer. |
src/lib/record/erase.ts |
Preserves alias state during in-memory erase. |
src/lib/identity/alias-offer.ts |
Derives aliases from email addresses. |
src/components/record/RolePanel.tsx |
Moves mark offers to the shared picker. |
src/components/record/Register.tsx |
Implements native disclosure rows. |
src/components/record/ProfilePanels.tsx |
Adds register readings and reorganizes panels. |
src/components/record/MarkPicker.tsx |
Consolidates controlled and immediate-write modes. |
src/components/record/IdentityPanel.tsx |
Adds alias provenance display. |
src/components/record/DrafterBlock.tsx |
Composes identity and account controls. |
src/components/record/DataPanel.tsx |
Uses the new erased-record state. |
src/components/record/AccountSync.tsx |
Applies account-derived alias decisions. |
src/components/identity/AliasSheet.tsx |
Reuses the shared mark picker. |
src/components/auth/SignInPanel.tsx |
Adds reusable inline auth chrome. |
src/components/auth/AuthPanels.tsx |
Supports inline account and organization panels. |
src/app/profile/page.tsx |
Rebuilds the profile page composition. |
src/app/profile.css |
Styles the drafter, picker, and register. |
src/app/globals.css |
Loads profile-specific styles. |
Review details
- Files reviewed: 39/39 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…aiming Copilot's review of #12 raised four points against `prefs.aliasNamedFor`. Three were real; the fourth was already argued in the file it was raised on. **§14.8.2 — the profile row was built from a stale identity.** It was pushed on the statement after `claim()` was STARTED, so on a first email sign-in it read the record before §16.3 derived a name from the address. `profileRowFor` omitted `display_name`, and every manager panel printed `USER 1a2b3c4d` for that whole session, healing only on the next mount. Extracted as `pushProfileRow`, which takes the identity at call time; the immediate push is kept, so a hung claim still cannot cost the row, and the decider pushes a second time when — and only when — it actually wrote a name. `upsert` on the primary key makes the second call idempotent. Two guards, each mutation-proved to fail alone: deleting the deferred push reddens one case, moving the immediate push behind the claim reddens the other. **Two comments claimed more than the code delivers.** `erasedRecord` carries the flag in the record, and `eraseStored()` removes that record's key a line later — so the tombstone holds for this tab and not across a load. The limit is now written down beside the argument for accepting it (a durable tombstone is a durable fact about the reader, under a key §12.15's panel prints as empty), and asserted in both directions rather than left unstated. `prefs.aliasNamedFor`'s docblock argued that an id beats a boolean because it tells two accounts apart. One slot only defers the collision: A clears the name, B settles, B clears, A returns — and A's removal is offered over. Filed as §16.11, which is a schema widening with a `merge.ts` question of its own. **Not changed.** The provenance note in `IdentityPanel` tests a value and not its provenance, and lines 136-145 there already record that as a measured imprecision with its cost priced. typecheck 0 · 2851 unit · 57 pages · 366 e2e
Enhanced the `manager-queries.md` to include the new `prefs.aliasNamedFor` field, clarifying its purpose in the context of account aliases. Additionally, removed outdated CSS rules from `profile.css` to streamline the layout of drafter actions, ensuring a more consistent user interface. Updated text in various components for clarity and accuracy, including adjustments to the legend and account sync documentation.
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.
The profile sheet had stopped being readable. The complaint was that there was
too much text and that finding anything was hard, and it measured:
<main>inthe export held 1260 words, eleven
<h2>panels and twenty form controlsbefore a single React island mounted, with the two controls a reader comes here
for about 700 words apart.
Three things were invisible without counting them:
IdentityPanel'sMarkPickerand, directly below it,RolePanel'sMarkOfferdrawing thesame eight options again with two confirm buttons of its own — with a third
copy on
/sign-in/alias/;identity.name;were not on it — five paragraphs, zero readings.
What ships
One open drafter block. The drawing with its
MARKandSEEDlines; thenthe alias field and one compact mark row; then sign-in. One box holding the two
answers to how this record is known — the alias in this browser, the account
across browsers — a 1.5px painted rule apart. The rule is the argument: same
subject, different reach.
One register of ten folds, closed. Readout, Uptime, Stamps, Submittal
register, Role and path, Organisation, Storage, Stored values, Export/import/
erase, Keyboard.
The rule that makes folding honest (§16.4.1). A closed row prints the number
its panel exists to report, so folding removes prose and never a fact — §10.4's
contract on a silent gauge, applied to a disclosure.
RegisterRowrefuses ablank reading at render time, and the strongest of the five e2e gates is that a
row's closed reading equals the reading its opened body prints. Every reading
comes from the selector its own body already uses; none is a new count. Readings
that are reader state print
--in the prerender, which is the house spellingfor "no reading taken yet".
One mark picker, two modes. Uncontrolled it writes on change, which is what
/profile/wants; controlled the parent holds the draft, becauseAliasSheet'sKEEP THIS ALIASmust not claim an act already performed.data-hl-markstayson the label with the 44px touch floor, and
idPrefixkeeps all three radiogroup names exactly as they were.
An email sign-in names the alias once, from the local part, with a line
saying where the name came from. It writes, so every guard is load-bearing.
Review
Five lenses — correctness, truthfulness, design and accessibility, test quality,
and a reader's own errands — produced nineteen findings. Eight were refuted by
verifiers whose job was to refute them. Twelve were fixed. Two mattered:
The alias offer overruled
REMOVE NAME. The flag recorded that an offer hadbeen taken, never that one had been decided, so a reader whose record already
carried a name at sign-in never got the flag written — the first guard returned
before the flag's only writer was reached. Clearing the name then let the next
claim write the address's local part over it. Reproduced independently twice; the
file's own docblock claimed the opposite invariant. The offer is now decided once
per account, and an erase keeps the flag, because
carriesNothingdeliberatelyignores
prefsso keeping it cannot resurrect an erased record — without it, arecord the reader had just erased came back holding their name.
The §16.4 gates could not tell a row from its reading. A reviewer swapped
uptime's reading with stamps', hard-coded the storage reading to
PERSISTENT · QUERIEDwith its hydration gate dropped, froze the role readingand inverted the keyboard's — four simultaneously lying readings, one asserting
persistence before
navigator.storagehad been asked — and the suite stayedgreen: 2782 unit, 365 e2e, byte-identical to a clean run. The gates were one-way
implications and vacuous for a reading with no digits. There is now a per-row
table of expected readings at two fixed seeds, and a row with no entry fails.
Verified: the same swap turns that gate red and leaves the other four green.
That gate earned itself before shipping. With the storage row closed and the
browser having answered, the summary printed
UNKNOWNwhile the body one linebelow printed
BEST-EFFORT—storageReadout()was a bare call and the grant isdeliberately not written through the reducer, so the summary subscribed to
nothing and froze.
useStorageReadoutsubscribes it.Gates
npm run typecheck— cleannpm test— 94 files, 2847 testsnpm run build— 57 pages exportednpm run test:e2e— 366 passed, 15 credentialed tests skipping cleanlySITE_BASE_PATH=/ai-engineering-bazaar npm run build— the deployed sub-pathMutation-proven, not asserted: eleven deliberate breaks across the two commits,
each rebuilt before its run and restored after — a row rendered
open, areading deleted,
data-hl-selecteddropped, the offer marker reduced to a tint,inline chrome re-emitting a heading id, a readout given a full stop, and the
reading swap above.
Schema
prefsgains one nullable field,aliasNamedFor. A widening, not a migration:coerceRecordDatadefaults it, records written before it still load, andcarriesNothingstill ignoresprefs. No table, no policy, no RLS change.Spec: §16,
docs/superpowers/specs/2026-09-02-lms-phase6-drafter-block-design.md