swapit — household toxics inventory + swap engine
What this skill is
swapit turns a vague worry ("there's probably a lot of plastic and BPA in my house") into
a concrete, prioritized, trackable plan. It is stateful: your household inventory lives on
disk and persists across sessions. It is local-first: zero paid services, nothing leaves your
device unless you explicitly opt into the (M3) commons.
One pass produces:
- An inventory of the items you own, each tagged with its generic item-class.
- A risk score per item that reflects actual exposure — a scratched non-stick pan used daily at high heat ranks far above an unopened plastic bin in the garage.
- A ranked "swap-first" list — the 5% of changes that cut the most exposure.
- Swap tracking — chosen alternative, procurement status, checklist, bookmarks.
- A shareable HTML report (and, in M2, a live dashboard at
swapit serve).
The three realms (and the privacy invariant)
| Realm | What it holds | Sharing |
|---|---|---|
| 1 · Knowledge | hazards → item-classes → alternatives (generic, cited facts) | shareable; cached + (M3) commons-synced |
| 2 · Inventory | your items, rooms, quantities, brands, photos, swaps, bookmarks | PRIVATE — never leaves the device |
| 3 · Commons (M3) | anonymized contributions enriching the shared knowledge graph | opt-in, reviewed, generic facts only |
Privacy invariant (binding): Realm-2 inventory (items, rooms, quantities, brands, photos, purchase info, location) never crosses the sync boundary. Only Realm-1 generic facts are shareable, via explicit opt-in + a reviewable
swapit sync --dry-run. Enforced by an allowlist serializer (scripts/anonymize.py) on the client and a backstop on the commons server, plus fuzz tests asserting every real inventory item is rejected by the gate. Forbidden-field list:anonymize.CONTRIBUTION_FORBIDDEN(the inventory-structural subset ofstate.PRIVATE_FIELDS).
Data model
State lives at ~/.config/swapit/ (override with $SWAPIT_HOME; honors $XDG_CONFIG_HOME).
~/.config/swapit/
├── knowledge/ hazards.jsonl · item-classes.jsonl · alternatives.jsonl · products.jsonl · procurement.jsonl
├── inventory/ events.jsonl(append-only audit) · items.json · swaps.json · rooms.json · bookmarks.json
├── contributions/ queue.jsonl (M3)
├── sync/ config.json · sync-log.jsonl (M3)
└── photos/
Node schemas (see seed/*.jsonl for the full, cited dataset):
- hazard —
id · name · aliases · class · mechanism · exposure_routes · regulatory · severity(0-3) · evidence_strength · sources - item_class —
id · name · category · description · hazards[]{hazard_id, presence_likelihood, rationale} · detection_hints · sources - alternative —
id · name · replaces[] · material · rationale · tradeoffs · caveats · avoids_hazards · residual_concerns · sources - procurement_option (where-to-buy, public) —
id · alternative · item_class · retailer · region(ISO-3166-1 a2) · area · url · price_min · price_max · currency · as_of · availability · confidence · corroboration_count - item (private) —
id · name · item_class · room · quantity · brand · condition · usage{frequency, food_contact, heat, child_contact} · status · notes · photos - swap (private) —
id · item_id · chosen_alternative · procurement{status, cost, vendor} · checklist[] · bookmarks[]
Risk model
risk = severity × presence_likelihood × evidence × exposure_relevance × frequency × condition
- exposure_relevance activates a hazard's route against how the item is used (food-contact + heat maxes out an ingestion/food-contact-heat hazard; dermal matters for personal care; inhalation for cleaning/furniture; child-contact amplifies).
- Item score = scaled sum of per-hazard risk → band high / medium / low.
- This is the prioritization intelligence — the analogue of procurer's "dominant failure mode": fix the few items that drive most of the exposure first, not a guilt list of everything plastic.
Modes
| Mode | What it does |
|---|---|
init | create state + load the seed knowledge graph |
add | add a household item (name, class, room, condition, usage) → prints its assessment |
assess | assess an item or an ad-hoc item-class → hazards + risk + ranked alternatives |
list | list inventory, filter by --room/--band/--status/--class/--hazard, sort by risk |
swap | create/update a swap plan: choose alternative, set status, checklist, bookmark, cost |
score | household exposure summary + the swap-first ranking |
report | generate the self-contained HTML report (Category-C) |
procure | procurer handoff brief + known where-to-buy offers for the swap target (filter by --region); record a found offer (--retailer/--url/--price-*) to the public commons |
knowledge | browse/search the knowledge graph (list/search/show) |
rooms | list/add rooms |
selfheal | validate knowledge edges + inventory refs + grounding; exit non-zero on errors |
serve | live local dashboard at http://127.0.0.1:8731 — kanban board, checklist, bookmarks, status; every click writes back to state |
contribute | queue an anonymized fact — product / hazard (item-class→hazard) / alternative / procurement (public where-to-buy offer) / item-class (new taxonomy node); applied locally + gated for the commons |
sync | push the contribution queue + pull community knowledge (opt-in); --dry-run previews exactly what would be sent; --configure sets the endpoint |
Typical flow
swapit init
swapit add --name "Old Teflon pan" --class nonstick-cookware --room kitchen \
--condition scratched --frequency daily --food-contact --heat
swapit score # what to swap first
swapit swap itm_xxxx --to cast-iron-skillet --status sourcing --add-task "buy 10in skillet"
swapit procure itm_xxxx # -> hand the brief to the `procurer` skill
swapit report --open # shareable HTML
Compounding with other skills
procurer—swapit procure <item>builds the need ("replace 3 polycarbonate bottles with glass/steel") and the ready procurer prompt; procurer returns cited sources + a budget envelope.bookkeeping(P6) — a novel, durable hazard/product finding (e.g. a newly characterized item-class) is filed proactively intoresearch/entities/and can later flow to the commons.health— personal exposure context.content-creation— educational posts from the graph.
Grounding discipline
The seed knowledge cites only authoritative bodies (NIEHS, ATSDR/CDC, US EPA, US FDA, ECHA/EU REACH,
CA OEHHA Prop 65, WHO, EWG). Every record carries ≥1 source; verified: false marks these as
reference-grade (not freshly fetched) — run a sourced pass (or the commons) to refresh. selfheal
fails if any node loses its citation. This is consumer guidance grounded in public-health science,
not medical or legal advice.
Resources
scripts/swapit.py— CLI entrypoint + command handlersscripts/state.py— two-realm state layer (PRIVATE_FIELDSdefines the sync boundary)scripts/ops.py— the single state-mutation write path (shared by CLI + dashboard)scripts/knowledge.py— knowledge graph load + edge resolutionscripts/risk.py— exposure-risk scoring enginescripts/report.py— self-contained HTML report generator (Category-C)scripts/server.py—swapit servelive dashboard (stdlib http.server, localhost-only)scripts/anonymize.py— the privacy gate: allowlist fact builders + the forbidden-field scanscripts/sync.py— commons sync client (queue,--dry-runpreview, push/pull, merge)scripts/selfheal.py— integrity validatortemplates/dashboard.html— the live dashboard (inline CSS/JS, Category-C)commons/— the networked commons reference server (FastAPI + SQLite; deploy gated)seed/{hazards,item-classes,alternatives}.jsonl— grounded starter knowledgetests/— risk, knowledge, self-heal, CLI, privacy/anonymize, sync, report, and server tests
Collaboration (the commons)
Contributions are generic facts only — swapit contribute product|hazard|alternative|procurement|item-class
builds an anonymized fact (a public product→item-class mapping, a hazard-edge correction, an
alternative, a where-to-buy offer, or a new taxonomy node), applies it locally, and queues it.
swapit sync --dry-run shows exactly what would be sent; swapit sync pushes the queue and pulls
community knowledge (opt-in, after --configure). Identical facts from different users corroborate
(content-addressed) rather than duplicate; the commons serves a fact once two distinct contributors
corroborate it (corroboration >= 2) — moderation is corroboration-gated, not confidence-gated,
because confidence is caller-supplied and a lone submitter could otherwise self-approve. The privacy
invariant is enforced on both sides (client anonymize gate + server backstop) — inventory never crosses.
Where-to-buy (procurement) commons
A procurement_option fact is a public offer: a safer alternative sold by a retailer in a
region (ISO-3166-1 alpha-2), with optional url, price_min/price_max + currency + as_of,
area, and availability. It is content-addressed on (alternative, retailer, region) — the
same offer corroborates across users; a different region is a different fact (the geographic scale
axis). Price/url/area are refinable market data: a corroboration with a strictly newer as_of
freshens the price forward, never regressing fresher data. The privacy seam is sharp — the public
offer uses retailer + price_* and never the private vendor/cost (where you bought and
what you paid stay in Realm 2). swapit procure <item> --region <CC> surfaces known offers and
records new ones you find — growing an open, detailed, geo-scoped dataset anyone can use.
Roadmap
- M1 (shipped, 0.1.0) — data model, seed knowledge, CLI, risk engine, static HTML report, self-heal.
- M2 (shipped, 0.2.0) —
swapit serve: live local dashboard with read/write back to state. - M3 (0.3.0) — anonymized collaboration + networked commons (
commons/, FastAPI + SQLite). Privacy invariant enforced by allowlist serialization + fuzz tests on both client and server. - M4 (this release, 0.4.0) — geo-scaled procurement commons + taxonomy growth: the
procurement_option(public where-to-buy, keyed by(alternative, retailer, region), forward-only price freshening) anditem_class(corroboration-gated taxonomy growth) fact kinds;procuresurfaces + records offers; seed offers across US/CO/DE/GB; cross-language hash parity locked by pinned vectors. Live deploy to broomva.tech infra is gated on explicit go (creds/DNS); the skill is fully functional offline without it.