# /data/job-check.json — Job Check's one data file (version 1)

Written by every build (`npm run marketing`, scripts/marketing/jobCheck.mjs) from the repo's researched data:
`scripts/marketing/data/job-check-nyc.json` (19 New York City jobs), `job-check-nyc-disposal.json` (where to take the waste, NYC),
`license-limits.json` (each state's license answers) and `sales-tax-labor.json` (each state's sales tax on labor). Never edit the
JSON by hand: edit the data and rebuild. Every text in it has already been held to the sourcing rule (each sentence backed by a quote
the same card shows), so a reader of this file shows the texts as they are and never re-words them. No AI made any of it.

**Answering job + place → card, with no transform:** `import { cardOf, cardHtml, normSel, parseQuery, selQuery } from '/site-jobcheck.js'`
(public/site-jobcheck.js runs in Node and in the browser). `cardOf(data, { job, place, answers, work })` returns
`{ sel, job, place, state, nyc, checked }` — `nyc` is the NYC job record or `null` outside New York City; `cardHtml(data, sel)` is the
card the site shows. `answers` maps a question id to one of that question's option strings; `work` is one of the sales tax fields or ''.

## Top level

| key | what |
|---|---|
| `version` | 1 — bumped when a field changes meaning or is removed |
| `schema` | this file's path |
| `checked` | `{ nyc, licenses, salesTax, waste }` — ISO date each data set was last checked against its sources |
| `honesty` | the line every card ends with (not legal or tax advice; confirm with the departments named) |
| `salesTaxQuestion` | `{ id: 'work', q, options: [{ value, label }] }` — value '' = "Not sure: show all four", else `repairTPP` · `repairRealProperty` · `installation` · `newConstruction`. The tool never picks one for the reader. |
| `jobs` | `[{ id, name, trade, stateLicense: [field…] }]` — the pickable list; `stateLicense` names the fields of `states.XX.license.fields` this job reads outside NYC |
| `places` | `[{ id, label, where, state, researched }]` — `nyc` (researched: true) and one per state (`al` … `wy`, `dc`; `ny` = New York outside NYC) |
| `nyc` | the researched place, below |
| `states` | `{ XX: State }` for the 50 states and DC, below |

## `nyc`

`{ id, name, state, checked, departments: { buildings, consumer } (each { name, short, url }), general: [{ id, heading, quotes }],
jobs: { <jobId>: Job }, waste: { checked, placesOnCard, materials: { <materialId>: Material } } }`

### Job (`nyc.jobs[id]`)

| key | what |
|---|---|
| `id`, `name`, `trade`, `page` | `page` = its static page, `/job-check/nyc/<id>/` |
| `checked`, `confirmed` | ISO date; `false` = the card says "Not fully confirmed" |
| `notConfirmed`, `notCovered` | public words for what the research could not settle / did not cover |
| `questions` | `[{ id, q, options: [string] }]` — the follow-ups that decide the answer (ids never `job`, `place` or `work`) |
| `permit` | `{ answer: 'yes'|'no'|'depends', word, department: 'buildings', rules: [Rule], conditions: [text], exceptions: [text], filing, quotes: [Quote] }` |
| `license` | `{ who, conditions: [text], quotes: [Quote], hic: { applies: true|false|'depends'|'likely'|'unknown', word, text: [text], rules: [Rule], quotes: [Quote] } }` — `hic` is DCWP's home improvement contractor license |
| `forms` | `[{ name, url }]` — official forms |
| `twoAnswers` | `[{ topic, notes: [text], sides: [Quote, Quote] }]` — where the city's own pages disagree; always show both sides |
| `disposal` | `null` (nothing that needs special handling listed) or `{ materials: [{ id, if, via }] }` — `if`: the condition that brings the material ("an old unit is taken away"); `via`: the material that points to this one |

**Rule:** `{ when, label, text, verdict, info? }`. `text` is `null` when no sentence survived the sourcing rule (say so and point to the quotes).
`verdict` is the short answer the rule opens with ("No permit", "Permit needed", …) or `null`. The headline is a verdict only when no
rule is still open and every rule that applies has that same verdict; a rule that applies with no verdict keeps it at the data's
`word`. `info: true` marks a rule that only informs (its text is conditional on a permit): it neither sets nor blocks the headline. A verdict is printed on its own only where the
research's sentence ends there; otherwise `text` keeps the whole sentence, scope included.
`when` is one of `{ always: true }` · `{ anyOf: [[{ q, is }…]…] }` (any group whose every `q` was answered `is`) ·
`{ otherwise: <index of the rule before>, allOf?: [{ q, is }] }` (true when that rule is false and `allOf` holds). With answers
missing a rule is "open" (it turns on what was not answered): `ruleStates(rules, answers)` in site-jobcheck.js returns
`'yes' | 'no' | 'open'` for each.

**Quote:** `{ quote, title, url, pageDate }` — `pageDate` is the date the page itself carries, or `null` when it shows none.

### Material (`nyc.waste.materials[id]`)

`{ id, name, confirmed, seeMaterial, page, rules: [{ text, quote, checked }], warnings: [Warning], programs: [{ name, url, text,
quote, checked }], business: [Place], notSettled: [text], twoAnswers: [{ topic, notes, sides }] }`

- **`business`** — places that take it FROM A BUSINESS (`business`: 'Yes' or 'Depends'). Household-only places are never here.
- **`warnings`** — NOT FOR CONTRACTORS: `{ text, quote, checked }` (a household-only city route) or `{ place: Place }` (a
  household-only place). Show them only as warnings, never as somewhere to take a job's waste.
- **Place:** `{ name, business, householdOnly, words, quote, quantityLimit, vehicleLimit, fee, hours, appointment, address, borough,
  phone, link, checked }` — every place is shown with "call ahead".
- `page` — the material's own page (`/job-check/nyc/waste/<id>/`, every place by borough); a material with `seeMaterial` is said on
  that material's page.
- A card shows the waste COMPACT (warnings, the first `placesOnCard` business places, a link to `page`); the material page shows it all.
  Rules the build holds back on purpose (with their reason) are in `WASTE_RULE_SKIP` in scripts/marketing/jobCheck.mjs.

## State (`states.XX`)

```
{ code, name,
  license: { checked, confirmed, board: { name, url }, page,
             fields: { handyman | generalContractor | plumbing | electrical | hvac: { label, cell, note, why, quotes } } },
  salesTax: { checked, confirmed, kind: 'sales'|'gross-receipts'|'none', tax, local, department: { name, url }, page, kindQuotes,
              answers: { repairTPP | repairRealProperty | installation | newConstruction:
                         { word, label, plain, note, dependsOn, caveats: [text], twoAnswers: [{ sides }], sources: [Quote] } } } }
```

`cell` and `label` are the exact words of the license and sales tax pages (public/site-licenses.js, public/site-salestax.js
`jobAnswer`); `label` carries every hedge ("not fully confirmed", "see the caveat", local taxes not checked) — show `label`, not
`word`, unless `plain` is true. `why` = why a license field is not fully confirmed. Outside NYC the permit is not researched: say
"we haven't researched <place> yet" and send the reader to the city or county building department.
