Skip to content

Phase 1: replace legacy spreadsheet with validated, version-controlled inventory - #1

Merged
sr320 merged 3 commits into
mainfrom
phase1-migrate-legacy-inventory
Aug 3, 2026
Merged

Phase 1: replace legacy spreadsheet with validated, version-controlled inventory#1
sr320 merged 3 commits into
mainfrom
phase1-migrate-legacy-inventory

Conversation

@sr320

@sr320 sr320 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Replaces the spreadsheet in legacy/ with version-controlled, validated data, and records the plan for the rest of the system.

Two commits: the plan and governance docs, then the migration tooling and generated data.

What's here

PLAN.md Architecture and phased plan, including the optional LLM photo-description pipeline
PHOTO_POLICY.md Photo rules — mandatory because this repo is public
.github/CODEOWNERS @sr320 and @kubu4 both maintain
.github/workflows/validate.yml CI gate on every PR touching data/
scripts/migrate_legacy.py One-time xlsx → CSV migration
scripts/validate.py Schema, closed-set, referential-integrity, and date validation
data/*.csv 499 items, 173 locations, 16 rows needing a human pass

Decisions this encodes

Public repo. This puts the whole privacy burden on the photo path, so PHOTO_POLICY.md ships now rather than with Phase 4, and three guardrails are mandatory rather than nice-to-have: strip all EXIF, review the image itself and not just the items derived from it, and allow rejection on privacy grounds alone. The reasoning worth agreeing on before Phase 4: a merged photo is permanent — it survives in history, in every fork, and in whatever already scraped it — so pre-merge review is the only control that actually works.

Both maintainers can merge. Enough that nobody is a single point of failure, few enough that review actually happens.

Migration judgment calls

The migration never guesses silently. Ambiguous legacy rows go to data/review_queue.csv with a stated reason instead of into items.csv:

  • Semicolons split automatically; commas don't. Microcentrifuge tubes, conical screw top tubes, screw caps is three items, but DNeasy 96 blood and tissue kit (4) & (50) is one. Comma-only cells stay whole and carry a suggested split for a human to accept or reject.
  • Five rows on the drawers/cabinets sheet are coarse restatements of the dedicated Rm 213 shelf sheet. Importing them would have created ~40 duplicate items, so they're queued for reconciliation. Nothing dropped, nothing duplicated.
  • The date column held commentaryVery likely all expired, n.d., 08/2010 from FHL. That text moves to notes rather than being discarded, and partial dates keep their real precision (08/20102010-08) instead of having a day invented for them.

The staleness is now data

Every migrated row carries status=unverified and last_verified=2021-08-29 — the spreadsheet's own claim about when it was last touched. validate.py reports it on every run:

items                499
locations            173
unverified           499 (100%)
not verified in 1y   499 (100%)
locations w/o items  46

Five years of drift is a queryable field instead of a caveat someone has to remember. The source column (legacy-xlsx | manual | photo-llm) makes provenance permanent, so a later audit can always ask which rows a model wrote.

Two schema additions worth a look

  • glassware and office categories alongside the lab ones. Without them roughly half the inventory landed in other, which would make the planned category filter useless. Now 25%, which is honest for a drawer containing a Nintendo.
  • An owner column, since the legacy storage bins tag contents - Goetz.

Say so if you'd rather not track either.

Verification

validate.py is stdlib-only, so there's nothing to install and nothing to break when a dependency moves. Tested against nine injected faults — duplicate item ids, dangling location refs, non-ISO dates, self-parenting locations, unknown category, and a photo-llm row with no photo_id — all nine caught, exit 1.

location_id is deliberately short and stable (209-CAB-01, 213-F20-S02-D08) because it becomes a URL fragment and a QR-sticker payload; deriving ids from prose labels produced 60-character identifiers. Prose-numbered places get an X01 sequence so they can never collide with numerically-numbered ones.

Open questions

  • Are room 230 and the -80 room yours to track? 4 locations, trivial to drop.
  • Freezer boxes: v1 tracks the box, not the tube. Sample-level tracking is a different problem and probably its own tool.

Next

Phase 2 — search UI and QR stickers. Deliberately before the add-item forms: search value has to land before anyone is asked to enter data, or this goes the way of the spreadsheet.

🤖 Generated with Claude Code

sr320 and others added 3 commits August 3, 2026 11:18
Plans the replacement for the spreadsheet in legacy/: git-backed CSVs as the
source of truth, a static search site for reading, issue-form-driven PRs for
writing, and an optional Claude vision pipeline for photo intake.

Records two settled decisions:

- Public repo. This puts the entire privacy burden on the photo path, so
  PHOTO_POLICY.md ships now rather than with Phase 4, and three guardrails
  become mandatory: strip all EXIF, review the image itself and not just the
  items derived from it, and allow rejection on privacy grounds alone. A
  merged photo is permanent -- it survives in history, forks, and scrapes --
  so pre-merge review is the only real control.
- @sr320 and @kubu4 both maintain, via CODEOWNERS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Converts legacy/Roberts Lab Inventory.xlsx into data/items.csv (499 items),
data/locations.csv (173 locations), and data/review_queue.csv (16 rows), with
CI validation gating every future edit.

The migration never guesses silently. Where the legacy data is ambiguous the
row goes to review_queue.csv with a stated reason instead of into items.csv:

- Semicolons are a reliable delimiter here and are split automatically.
  Commas are not -- "Microcentrifuge tubes, conical screw top tubes, screw
  caps" is three items but "DNeasy 96 blood and tissue kit (4) & (50)" is one
  -- so comma-only cells stay whole and carry a suggested split.
- Five rows on the drawers/cabinets sheet are coarse restatements of the
  dedicated Rm 213 shelf sheet. Importing them would have produced ~40
  duplicate items, so they go to the queue for reconciliation.
- The date column held commentary ("Very likely all expired", "n.d.",
  "08/2010 from FHL"). That text moves to notes rather than being dropped,
  and partial dates keep their real precision (08/2010 -> 2010-08) instead of
  having a day invented for them.

Every migrated row carries status=unverified and last_verified=2021-08-29,
the spreadsheet's own claim about when it was last touched. Five years of
staleness becomes a queryable field that validate.py reports on every run,
rather than a caveat someone has to remember. The source column (legacy-xlsx
| manual | photo-llm) makes provenance permanent, so a later audit can always
ask which rows a model wrote.

location_id is kept short and stable (209-CAB-01, 213-F20-S02-D08) because it
becomes a URL fragment and a QR-sticker payload; deriving ids from prose
labels produced 60-character identifiers. Prose-numbered places get an X01
sequence so they can never collide with numerically-numbered ones.

Categories include glassware and office alongside the lab ones -- without
them roughly half the inventory landed in `other`, which would make the
planned category filter useless.

validate.py is stdlib-only so there is nothing to install and nothing to
break when a dependency moves. Verified against nine injected faults
(duplicate ids, dangling location refs, non-ISO dates, self-parenting,
unknown category, photo-llm row missing photo_id): all caught, exit 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scope is five rooms: 209, 213, 228, 230, and the -80C room. Declaring them in
data/rooms.csv makes scope an explicit, enforced fact rather than a side
effect of whatever the legacy spreadsheet happened to contain -- validate.py
now fails if a location names an undeclared room.

Room 228 appears nowhere in the legacy spreadsheet, so there is nothing to
migrate for it and it starts with zero locations; it gets populated by walking
in with a phone. Rather than let an in-scope-but-unrecorded room quietly be
forgotten, validate.py emits a standing warning on every run until it has
locations.

Coverage is uneven and now visible: 209=70, 213=99, 228=0, 230=2, M80=2.

Room labels are deliberately generic ("Room 228") rather than guessing at a
purpose the spreadsheet never stated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sr320
sr320 merged commit d374fd9 into main Aug 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant