Commit Graph

2 Commits

Author SHA1 Message Date
2081c1ad3c T5.4 - CR-005: a per-project location taxonomy, stored as codes
CLAUDE.md lists CR-005 among the change requests that get "silently half-built if
you treat them as frontend-only". This is the server half and the wizard half
together: a new table, four routes, an Alembic revision, and step 11.

CODES, NOT DISPLAY STRINGS, because CR-018 rolls cost up by these values and a
rollup keyed on a label breaks the day somebody fixes a typo in it. Two columns
carry that: `code` is a node's own slug, derived once at import and never
recomputed; `path` is the full slug path, unique per project, and is what a work
package will store. Renaming a value changes `name` alone - the probe renames a
floor and demands its path comes back byte-identical, with its children's paths
intact.

DEACTIVATE, NEVER DELETE. There is no DELETE route, and the probe checks for its
absence (405) rather than trusting that nobody added one. Deactivating hides a
value from new work packages and cascades DOWN, because a floor nobody can pick
must not keep offering its sectors. Reactivating walks UP only - a sector may
have been switched off for its own reasons, and silently resurrecting it would
undo a decision nobody made twice. That asymmetry is deliberate and is pinned by
a named check so it does not get "fixed" into a surprise.

Import reports rather than merges. Rejected rows come back with the SOURCE line
number and a reason; duplicates are listed as duplicates, separated into "already
in this project" and "already on line N of this import". Reusing a parent is not
a duplicate - B1/L2/1P and B1/L2/2P share a building and a floor by design, and
only the full path repeating counts. Re-importing a deactivated value brings the
same row back rather than creating a second one; the probe checks the id.

One parser, on the server. A CSV is read in the browser and posted as text
exactly as a paste is, so "what does a blank column mean" has one answer.
Comma, semicolon and tab all work - a paste out of a spreadsheet is tab
separated and a saved CSV is not, and which one somebody has is a question the
machine can answer.

No guessed floor names. IMPLEMENTATION.md section 8 says the B100 list has not
been supplied. The seeded sample has "Sample" inside every string, and the probe
greps html/ and server/ for a location-shaped assignment containing any of the
review's real names.

  server/models.py                    LocationNode
  server/alembic/versions/e2a4c7d91b30_location_taxonomy.py
  server/app.py                       GET/POST/PATCH + import, parser, slug
  html/work-package-suite.html        step 11, an 11th rail button
  html/work-package-suite-app.js      the step's logic; LAST_STEP replaces 10
  html/work-package-suite-styles.css  the list, the report
  html/theme-light.css                .field-error, now declared once
  tests/locations_check.py            new - 58 checks
  tests/stepper_check.py              STEP_COUNT 10 -> 11

Done when
  [x] CSV upload and paste both work and report rejected rows with reasons
  [x] duplicates are detected and reported rather than silently merged
  [x] values are editable after import - rename, add, deactivate
  [x] deactivating hides it from new work packages; an existing package
      referencing it still resolves, because the row is retained
  [x] values are stored as codes suitable for grouping
  [x] no guessed real-world floor names exist anywhere in the code

Two decisions worth disagreeing with

  Step 11, appended, not step 2, inserted. Locations belong beside Project by
  subject. Renumbering 2-10 would touch every sop-step-N id, every
  collectStepData case, every gate key and the analytics history - a large
  silent-mismatch surface for an ordering change. The count now lives in one
  place (LAST_STEP), so reordering later is cheap.

  Any project member may edit the list, not only a Project Admin. It matches how
  the SOP baseline itself is authored: the Project Admin gate is on CHANGING a
  completed SOP, not on writing one. If the location list should be tighter than
  the SOP it belongs to, that is a product call.

Verified one at a time
  locations_check  58/58  new
  stepper_check    70/70  (11 steps)
  browser_check    71/71
  a11y             22/22  sop now rings 38 focusable elements
  url_state        23/23
  autosave         34/34
  aggregates       16/16
  pipeline         43/43
  launcher         58/58
  f_items          F1-F5 FIXED, F6 REPRODUCES (T7.2)
  alembic          upgrade / downgrade / upgrade all clean on a throwaway SQLite
                   file, and the migrated schema matches Base.metadata.create_all
                   column for column - dev auto-creates and production migrates,
                   so a divergence between the two is invisible until it ships

.field-error was declared in two page sheets by the end of T5.2 and would have
been three by T5.8, so it moved to theme-light.css. No colour literal added
anywhere: still 0 across all page sheets and inline blocks.

Question for the PR, per CLAUDE.md: the levels are fixed at building / floor /
sector. Micron's floors behave like buildings, which this handles by letting a
project use whichever levels it needs - but a job that wants a fourth level, or
different names for the three, cannot say so. Whether that is worth a
per-project level vocabulary is a product question; the schema would take it
without a migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:06:27 -05:00
5e1f6e75ba T5.1 - A4/S9: a vertical stepper, ten real buttons, states in words
The step rail was ten div elements carrying onclick inside a horizontal
scroller. Not in the tab order, not operable by keyboard, and silent about
progress - the only thing on the page that said where you were was a "1 / 10"
pill in the app bar, detached from the control it described.

The rail is now a vertical column beside the form: ten <button> elements in an
<ol> inside a named <nav>, with arrow keys, Home and End on top of the Enter and
Space a button gives for free. All ten stay in the tab order; a roving tabindex
would have satisfied "arrow keys" by breaking "tab", which the done-when asks
for both of.

Four states, each carrying a word and a marker shape as well as a colour (C1):
Complete (green disc, tick), Current step (blue disc, aria-current="step"),
Locked (dashed outline) and a plain default. Locked steps keep aria-disabled
rather than disabled, so a keyboard user can reach one and be told what is in
the way instead of finding a control that has vanished from the tab order.

Reachability is the guard's own rule, deliberately not a stricter one: you may
leave the step you are on once its required fields are filled. The tempting rule
- lock everything after the first unmet gate anywhere - is not what
validateStep() enforces, and a padlock the Next button walks straight past is
the drift this change exists to remove. validateStep() and the rail now read one
STEP_GATES table, so they cannot disagree; T5.8 widens that table rather than
editing four functions.

Clicking a step you cannot reach announces why through a role="alert" region and
puts the cursor in the field that is missing. Saying "no" and leaving you where
you were, with no idea which of five inputs was empty, is what the dialog did.

Below 900px the rail collapses to a disclosure naming the step you are on -
ten vertical rows above the fields is most of a 390px screen before you reach an
input. 44px tap targets, since Field View is the gloved-hands surface.

Also: going backwards is no longer gated. previousStep() never validated, so a
rail that did would have trapped you on an incomplete step.

  html/work-package-suite.html        rail markup, counter removed
  html/work-package-suite-styles.css  #tool-sop grid, .step-rail*, 899px collapse
  html/work-package-suite-app.js      STEP_GATES, renderStepRail, keyboard, watcher
  tests/stepper_check.py              new - 70 checks

Done when
  [x] all 10 steps are <button> elements
  [x] keyboard: tab, arrow keys, Home/End, Enter and Space
  [x] aria-current on the current step, exactly one
  [x] complete / current / unavailable told apart without colour
  [x] the "1 / 10" counter is gone - no .step-counter, no N/10 in the app bar
  [x] app-wide <div onclick> 12 -> 2, down exactly 10

Verified one at a time
  stepper_check   70/70   new
  browser_check   71/71
  url_state       23/23
  a11y            22/22   sop now rings 37 focusable elements, all >= 3:1
  autosave        34/34
  aggregates      16/16
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)
  baseline_shots  14 shots; only the sop pair changed. The beforeunload log on
                  sop@1440 and creator@1440 is present at HEAD too - captured
                  both sides to check rather than assume.

No colour literal was added: all five page sheets and all seven inline <style>
blocks still hold zero. New spacing consumes --wp-s*; three raw font sizes were
added and three removed, so BL-010 is unchanged in kind.

Raised, not fixed
  BL-016  Back to a URL with no `step` leaves the wizard where it was. T4.2's
          popstate handler parses NaN and ignores it; its own probe never took
          that branch. stepper_check pins the current behaviour by name so the
          fix has a test waiting.
  BL-017  The native-dialog baseline counts the word `alert(` in comments. Four
          comments written here - all of them about removing a dialog - moved
          the number from 80 to 82 while two real calls were being deleted. They
          were reworded; the metric still needs a comment-stripped variant, which
          T5.8 owns.

Question for the PR, per CLAUDE.md: BL-015 leaves the creator's .step-tab
uppercase as the last forced-uppercase interactive text in the suite, on the
grounds that A5 scopes sentence case to buttons and field labels. The wizard's
rail is now buttons, so its labels are sentence case ("Sign-offs", "WP types").
The two are consistent by rule and inconsistent on screen until T7.x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 23:19:58 -05:00