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 of state.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 into research/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 handlers
scripts/state.py — two-realm state layer (PRIVATE_FIELDS defines the sync boundary)
scripts/ops.py — the single state-mutation write path (shared by CLI + dashboard)
scripts/knowledge.py — knowledge graph load + edge resolution
scripts/risk.py — exposure-risk scoring engine
scripts/report.py — self-contained HTML report generator (Category-C)
scripts/server.py — swapit serve live dashboard (stdlib http.server, localhost-only)
scripts/anonymize.py — the privacy gate: allowlist fact builders + the forbidden-field scan
scripts/sync.py — commons sync client (queue, --dry-run preview, push/pull, merge)
scripts/selfheal.py — integrity validator
templates/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 knowledge
tests/ — 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) and item_class (corroboration-gated taxonomy growth) fact kinds; procure
surfaces + 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.
1---2name: swapit3description: Stateful, local-first household toxics inventory + swap engine. Identify the items in a home that carry endocrine disruptors and persistent chemicals (BPA/BPS, phthalates, PFAS/PTFE, parabens, flame retardants, VOCs, microplastics), score each by *real* exposure (severity x presence x how it's used x condition), and track the swap to a safer alternative from "flagged" -> "sourced" -> "swapped". Ships a grounded, cited knowledge graph of ~20 hazards, ~40 item-classes, and ~40 alternatives. Hands sourcing off to the `procurer` skill. The skill's state is the source of truth — the agent is the app.4license: MIT5---67# swapit — household toxics inventory + swap engine89## What this skill is1011`swapit` turns a vague worry ("there's probably a lot of plastic and BPA in my house") into12a **concrete, prioritized, trackable plan**. It is stateful: your household inventory lives on13disk and persists across sessions. It is local-first: zero paid services, nothing leaves your14device unless you explicitly opt into the (M3) commons.1516One pass produces:171. An **inventory** of the items you own, each tagged with its generic *item-class*.182. A **risk score** per item that reflects *actual exposure* — a scratched non-stick pan used19 daily at high heat ranks far above an unopened plastic bin in the garage.203. A **ranked "swap-first" list** — the 5% of changes that cut the most exposure.214. **Swap tracking** — chosen alternative, procurement status, checklist, bookmarks.225. A shareable **HTML report** (and, in M2, a live dashboard at `swapit serve`).2324## The three realms (and the privacy invariant)2526| Realm | What it holds | Sharing |27|---|---|---|28| **1 · Knowledge** | hazards → item-classes → alternatives (generic, cited facts) | shareable; cached + (M3) commons-synced |29| **2 · Inventory** | your items, rooms, quantities, brands, photos, swaps, bookmarks | **PRIVATE — never leaves the device** |30| **3 · Commons** (M3) | anonymized contributions enriching the shared knowledge graph | opt-in, reviewed, generic facts only |3132> **Privacy invariant (binding):** Realm-2 inventory (items, rooms, quantities, brands, photos,33> purchase info, location) **never** crosses the sync boundary. Only Realm-1 generic facts are34> shareable, via explicit opt-in + a reviewable `swapit sync --dry-run`. Enforced by an allowlist35> serializer (`scripts/anonymize.py`) on the client **and** a backstop on the commons server, plus36> fuzz tests asserting every real inventory item is rejected by the gate. Forbidden-field list:37> `anonymize.CONTRIBUTION_FORBIDDEN` (the inventory-structural subset of `state.PRIVATE_FIELDS`).3839## Data model4041State lives at `~/.config/swapit/` (override with `$SWAPIT_HOME`; honors `$XDG_CONFIG_HOME`).4243```44~/.config/swapit/45├── knowledge/ hazards.jsonl · item-classes.jsonl · alternatives.jsonl · products.jsonl · procurement.jsonl46├── inventory/ events.jsonl(append-only audit) · items.json · swaps.json · rooms.json · bookmarks.json47├── contributions/ queue.jsonl (M3)48├── sync/ config.json · sync-log.jsonl (M3)49└── photos/50```5152**Node schemas** (see `seed/*.jsonl` for the full, cited dataset):53- **hazard** — `id · name · aliases · class · mechanism · exposure_routes · regulatory · severity(0-3) · evidence_strength · sources`54- **item_class** — `id · name · category · description · hazards[]{hazard_id, presence_likelihood, rationale} · detection_hints · sources`55- **alternative** — `id · name · replaces[] · material · rationale · tradeoffs · caveats · avoids_hazards · residual_concerns · sources`56- **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`57- **item** (private) — `id · name · item_class · room · quantity · brand · condition · usage{frequency, food_contact, heat, child_contact} · status · notes · photos`58- **swap** (private) — `id · item_id · chosen_alternative · procurement{status, cost, vendor} · checklist[] · bookmarks[]`5960## Risk model6162```63risk = severity × presence_likelihood × evidence × exposure_relevance × frequency × condition64```65- **exposure_relevance** activates a hazard's route against how the item is *used* (food-contact +66 heat maxes out an ingestion/food-contact-heat hazard; dermal matters for personal care; inhalation67 for cleaning/furniture; child-contact amplifies).68- Item score = scaled sum of per-hazard risk → band **high / medium / low**.69- This is the *prioritization* intelligence — the analogue of procurer's "dominant failure mode":70 fix the few items that drive most of the exposure first, not a guilt list of everything plastic.7172## Modes7374| Mode | What it does |75|---|---|76| `init` | create state + load the seed knowledge graph |77| `add` | add a household item (name, class, room, condition, usage) → prints its assessment |78| `assess` | assess an item or an ad-hoc item-class → hazards + risk + ranked alternatives |79| `list` | list inventory, filter by `--room/--band/--status/--class/--hazard`, sort by risk |80| `swap` | create/update a swap plan: choose alternative, set status, checklist, bookmark, cost |81| `score` | household exposure summary + the swap-first ranking |82| `report` | generate the self-contained HTML report (Category-C) |83| `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 |84| `knowledge` | browse/search the knowledge graph (`list`/`search`/`show`) |85| `rooms` | list/add rooms |86| `selfheal` | validate knowledge edges + inventory refs + grounding; exit non-zero on errors |87| `serve` | **live local dashboard** at `http://127.0.0.1:8731` — kanban board, checklist, bookmarks, status; every click writes back to state |88| `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 |89| `sync` | push the contribution queue + pull community knowledge (opt-in); `--dry-run` previews exactly what would be sent; `--configure` sets the endpoint |9091### Typical flow92```bash93swapit init94swapit add --name "Old Teflon pan" --class nonstick-cookware --room kitchen \95 --condition scratched --frequency daily --food-contact --heat96swapit score # what to swap first97swapit swap itm_xxxx --to cast-iron-skillet --status sourcing --add-task "buy 10in skillet"98swapit procure itm_xxxx # -> hand the brief to the `procurer` skill99swapit report --open # shareable HTML100```101102## Compounding with other skills103104- **`procurer`** — `swapit procure <item>` builds the need ("replace 3 polycarbonate bottles with105 glass/steel") and the ready procurer prompt; procurer returns cited sources + a budget envelope.106- **`bookkeeping` (P6)** — a novel, durable hazard/product finding (e.g. a newly characterized107 item-class) is filed proactively into `research/entities/` and can later flow to the commons.108- **`health`** — personal exposure context. **`content-creation`** — educational posts from the graph.109110## Grounding discipline111112The seed knowledge cites only authoritative bodies (NIEHS, ATSDR/CDC, US EPA, US FDA, ECHA/EU REACH,113CA OEHHA Prop 65, WHO, EWG). Every record carries ≥1 source; `verified: false` marks these as114reference-grade (not freshly fetched) — run a sourced pass (or the commons) to refresh. `selfheal`115fails if any node loses its citation. This is consumer guidance grounded in public-health science,116**not** medical or legal advice.117118## Resources119120- `scripts/swapit.py` — CLI entrypoint + command handlers121- `scripts/state.py` — two-realm state layer (`PRIVATE_FIELDS` defines the sync boundary)122- `scripts/ops.py` — the single state-mutation write path (shared by CLI + dashboard)123- `scripts/knowledge.py` — knowledge graph load + edge resolution124- `scripts/risk.py` — exposure-risk scoring engine125- `scripts/report.py` — self-contained HTML report generator (Category-C)126- `scripts/server.py` — `swapit serve` live dashboard (stdlib http.server, localhost-only)127- `scripts/anonymize.py` — the privacy gate: allowlist fact builders + the forbidden-field scan128- `scripts/sync.py` — commons sync client (queue, `--dry-run` preview, push/pull, merge)129- `scripts/selfheal.py` — integrity validator130- `templates/dashboard.html` — the live dashboard (inline CSS/JS, Category-C)131- `commons/` — the networked commons reference server (FastAPI + SQLite; deploy gated)132- `seed/{hazards,item-classes,alternatives}.jsonl` — grounded starter knowledge133- `tests/` — risk, knowledge, self-heal, CLI, privacy/anonymize, sync, report, and server tests134135## Collaboration (the commons)136137Contributions are **generic facts only** — `swapit contribute product|hazard|alternative|procurement|item-class`138builds an anonymized fact (a public product→item-class mapping, a hazard-edge correction, an139alternative, a **where-to-buy offer**, or a **new taxonomy node**), applies it locally, and queues it.140`swapit sync --dry-run` shows *exactly* what would be sent; `swapit sync` pushes the queue and pulls141community knowledge (opt-in, after `--configure`). Identical facts from different users **corroborate**142(content-addressed) rather than duplicate; the commons serves a fact once **two distinct contributors143corroborate it** (`corroboration >= 2`) — moderation is corroboration-gated, *not* confidence-gated,144because `confidence` is caller-supplied and a lone submitter could otherwise self-approve. The privacy145invariant is enforced on **both** sides (client `anonymize` gate + server backstop) — inventory never crosses.146147### Where-to-buy (procurement) commons148149A `procurement_option` fact is a public offer: a safer `alternative` sold by a `retailer` in a150`region` (ISO-3166-1 alpha-2), with optional `url`, `price_min`/`price_max` + `currency` + `as_of`,151`area`, and `availability`. It is content-addressed on **`(alternative, retailer, region)`** — the152same offer corroborates across users; a different region is a different fact (the geographic scale153axis). Price/url/area are refinable market data: a corroboration with a strictly newer `as_of`154**freshens the price forward**, never regressing fresher data. The privacy seam is sharp — the public155offer uses `retailer` + `price_*` and **never** the private `vendor`/`cost` (where *you* bought and156what *you* paid stay in Realm 2). `swapit procure <item> --region <CC>` surfaces known offers and157records new ones you find — growing an open, detailed, geo-scoped dataset anyone can use.158159## Roadmap160161- **M1 (shipped, 0.1.0)** — data model, seed knowledge, CLI, risk engine, static HTML report, self-heal.162- **M2 (shipped, 0.2.0)** — `swapit serve`: live local dashboard with read/write back to state.163- **M3 (0.3.0)** — anonymized collaboration + networked commons (`commons/`, FastAPI + SQLite).164 Privacy invariant enforced by allowlist serialization + fuzz tests on both client and server.165- **M4 (this release, 0.4.0)** — **geo-scaled procurement commons + taxonomy growth**: the166 `procurement_option` (public where-to-buy, keyed by `(alternative, retailer, region)`, forward-only167 price freshening) and `item_class` (corroboration-gated taxonomy growth) fact kinds; `procure`168 surfaces + records offers; seed offers across US/CO/DE/GB; cross-language hash parity locked by169 pinned vectors. **Live deploy to broomva.tech infra is gated on explicit go** (creds/DNS); the170 skill is fully functional offline without it.