test-spec v0.2.0 · 2026-07-02, amended 2026-07-04, 2026-07-06 — v0 ratified 2026-07-02
through item-by-item dialogue; amended 2026-07-04 to reconcile the §8.3 fresh-context
assertion review after the automated suite was built (see §8.3); v1 data-pipeline slice
appended + ratified 2026-07-06 (see §"v1 — Data-pipeline slice"; ownership record in
RATIFICATION_LOG_v1.md, Sitting 2).
Living document: amend as the design evolves; do not clobber. The diff history is the record
of what changed about what-must-be-true. Sections §1–§8 below are the v0 spec (full
pipeline); the v1 slice at the end covers the data-pipeline rebuild and reuses v0's
engine coverage at the seams rather than duplicating it.
Under test: Runway v0 — the full pipeline (conversion → engine → verify → shortlist → report → CLI error contract), per the ratified scope decision.
Design authority: the canonical Design Doc
(Notion, "§n" below) and docs/decision_log.md ("dec. #n"). Code symbols are cited where the
behavior lives. Items the design never stated are tagged [ratified-in-test-spec] — they were
decided during this spec's dialogue and the design doc / decision log must be amended to match
(see §5, Design amendments owed).
Acceptance statement: Runway v0 is trusted to ship when the automated suite is green on synthetic fixtures, and the golden real-data run (Scenario A) plus the failure-face run (Scenario B) pass on the target JP-locale Windows machine, and the human pass (spot-trace + real-user read) is done. A green suite alone is not the gate.
Where the risk concentrates (ratified):
- The filter funnel — certified/SOC/wage-level normalization; a silent mis-filter produces a confidently wrong shortlist, and the product's edge is "every number traces to a DOL filing."
- Employer normalization + aggregation — a false merge points an applicant at a company that doesn't exist; dropped/duplicated filings make the counts lie.
- The verify layer itself — the runtime green light; a check that cannot fire is worse than no check (§8).
- Manual-input handling — the two human-touch files (
hiring_now.csv,gap_read_filled.md) must never be clobbered, must fail in plain English, and must escape hostile content. - Quarter arithmetic (added by the adversarial sweep) — DOL quarterly files are cumulative FYTD; naive multi-quarter loading double-counts and fabricates the repeat-sponsor signal (F1).
⚠ = expected to fail against today's code — the test documents ratified behavior the build must catch up to. That is spec-first working as intended, and doubles as proof the test can fail.
| # | Behavior (one line) | Traces to | Method | Tier |
|---|---|---|---|---|
| M1 | Only exact Certified rows pass the status filter |
§5; dec. #2; sponsors.py:174 |
unit | MUST |
| M2 | SOC match is by base code; O*NET detail suffixes (.00, .01…) included | dec. #3; normalize_soc; F6 [ratified-in-test-spec] |
unit | MUST (+SHOULD pin for .01) |
| M3 | Wage-level spellings normalize to I–IV; unknown → None; all-None quarter stops | §5; normalize_wage_level |
unit | MUST |
| M4 | Annualization FROM×{1,2080,12,52}; unparseable wage stays counted, excluded from stats, tallied | dec. #8; sponsors.py:206 |
unit | MUST |
| M5 | Employer key conservative: case/punct/4-suffix only; LLP kept; distinct names never merge | dec. #9; §5; normalize_employer |
unit + property | MUST |
| M6 | Per-employer row correct: count, display=modal spelling, repeat⇔≥2 quarters, wage stats, sort | dec. #10; sponsors.py:219-247 |
unit | MUST |
| M7 | Funnel monotone; Σ filing_count = rows_selected; groups ≤ raw spellings | §7; verify.py |
unit + property | MUST |
| M8 | Conversion: columns by name; padding rows dropped and counted; label from filename; ~$ ignored; mtime skip + --force-convert; misnamed xlsx skipped w/ message (SHOULD) |
§5; dec. #6, #7, #16 | fixture | MUST |
| M9 | Every anticipated failure = RunwayError, plain English, exit 1, no stack trace |
§7; dec. #15; all raise sites | unit + fixture | MUST |
| M10 | --quarters = assertion, never selector; extras always used; missing named quarter stops |
§5 (gap G1); dec. #11 | unit | MUST |
| M11 | Golden check: FY2025Q4 → top must be IGAVEL; absent → SKIP not fail | dec. #12; verify.py:100 |
unit + real-data gate | MUST |
| M12 | hiring_now.csv: created blank once, never overwritten; values reach report; stale keys ignored w/o crash; broken columns → instructive error |
dec. #4; build_report.py:42 |
fixture | MUST |
| M13 | Gap read: absent → visible placeholder, run succeeds; renderer (headings demoted, lists, bold, links) escapes input; hostile markdown inert incl. ⚠ href-attribute breakout | §3; dec. #17; markdown_to_html; J5 |
unit | MUST |
| M14 | Report: every row (no truncation), 5 caveats verbatim, single-quarter note iff 1 quarter, wage-excluded note iff >0, missing wage → — | §6, §2; dec. #13; _util.CAVEATS |
unit + fixture | MUST |
| M15 | UTF-8 artifacts; cp932 console never crashes a run | dec. #14; force_utf8 |
fixture + manual | MUST |
| M16 | Provenance JSON complete and consistent with the CSV it accompanies | §7; build_shortlist.py |
fixture | MUST |
| F1 | ⚠ Overlapping same-FY (cumulative) quarter files must not double-count filings or fabricate repeat_sponsor | sweep; DOL cumulative-FYTD files; [ratified-in-test-spec — design amendment owed] | fixture | MUST |
| F2 | ⚠ Non-UTF-8 manual input (cp932-saved csv/md) stops with plain-English error naming the file and fix | sweep; dec. #15 contract; build_report.py:56,130 |
fixture | MUST |
| F3 | ⚠ Unreadable/truncated parquet stops with plain-English error naming --force-convert (never sticky-skipped) |
sweep; dec. #7, #15; sponsors.py:140 |
fixture | MUST |
| F4 | ⚠ Empty/whitespace gap_read_filled.md behaves as absent (placeholder), never a silent blank section |
sweep; §3 pending-review contract; build_report.py:128 |
unit | MUST |
| F5 | ⚠ Report-side CSV read round-trips every cell as written: blank renders blank (never "nan"); hiring join on exact strings | sweep; build_report.py:37 vs :56 |
fixture | SHOULD |
| F7 | ⚠ Report build asserts table/provenance are the same generation (len(table) == employer_groups) |
sweep; build_report.py:196-232 |
unit | note |
| V1–V4 | Each verify check demonstrably fires on corrupted input (see §8) | verify.py all checks |
unit (negative) | MUST |
| P1 | Parquet write→read→compare with unicode, blanks, long strings | promoted from SKIP #3 | unit | SHOULD |
| D1 | README advisory: don't run two invocations concurrently | promoted from SKIP #6 | docs action (no test) | SHOULD |
The index rows above are the checkable statements; detail where a wrong call is expensive:
- M1 Given rows with statuses
Certified,Certified - Withdrawn,Denied,Withdrawn, only exactCertified(case/whitespace-cleaned) survives.Certified - Withdrawnexclusion is deliberate (dec. #2) and must be pinned by an explicit test row. - M2/F6 Given
15-1255.00and15-1255.01, both match roledesign. Base-code matching is the ratified intent (the wage level certifies against the base occupation), decided in this spec's dialogue — decision log owes a line. A test pins.01inclusion so the scope is chosen, not an accident ofsplit(".")[0]. - M4 Given FROM=
"$85,000.00", unit=HOUR→ annual 176,800,000? No — 85000×2080; the test matrix covers each unit,$/comma parsing, blank FROM, andBi-Weekly(excluded from stats, present in counts, tallied inrows_wage_excluded). Dropping such a filing would understate a sponsor — the spec forbids it (dec. #8). - M5
Acme Design LLC≡ACME DESIGN, INC.≡Acme Design;Deloitte Consulting LLPkeeps LLP;Acme DesignandAcme Designsnever merge. Under-merge over over-merge, always. - M6 Two synthetic quarters exercise
repeat_sponsorboth ways andquarters_present; display name is the modal raw spelling with deterministic tie-break; sort is quarters↓, filings↓, name↑. - M12 The tool writes
hiring_now.csvonly when absent; a re-run after hand-editing leaves every byte of user work intact. Stale keys (shortlist changed) are ignored without crash — and the missing staleness warning is a logged design observation (§5), not a test. - M13 Hostile markdown:
<script>inert;javascript:never linkified; and the href-attribute breakout ([x](https://a.com/"onmouseover="…)—html.escape(quote=False)leaves"live insidehref) must be impossible. ⚠ Known-failing today; the fix is a one-liner at build time. - F1 Given two synthetic "quarters" sharing filings (modeling DOL's cumulative FYTD files), the pipeline must detect/refuse/dedupe — the mechanism is a design decision to log; the invariant (no filing counted twice, no fabricated repeat signal) is ratified now. Until amended, dec. #11's "extra quarters are harmless" and the report's "convert another quarter" note are wrong for same-FY files.
Ratified: hypothesis as a dev-only dependency (requirements-dev.txt); user-facing install
surface unchanged, honoring the spirit of dec. #17.
| # | Property (falsifiable statement) | Anchors |
|---|---|---|
| I1 | normalize_employer is idempotent: f(f(x)) == f(x) for arbitrary strings |
M5 |
| I2 | normalize_employer never merges names differing in a non-suffix token |
M5; dec. #9 |
| I3 | normalize_employer never raises; empty/punctuation-only input → "" key; such filings stay counted under one group, rendered as-is (ratified: no crash, counted, as-is) |
J7 |
| I4 | normalize_wage_level is total: any input → {I, II, III, IV, None}, never raises |
M3 |
| I5 | Aggregation partitions: every selected filing in exactly one employer group; Σ filing_count = rows_selected | M7; check_filing_count_sum |
| I6 | Where wage stats exist: min ≤ median ≤ max | M6 |
| I7 | Funnel monotone: total ≥ certified ≥ SOC-matched ≥ selected | M7 |
| I8 | No filing contributes twice to any count across loaded quarters (⚠ violated today by cumulative same-FY files — F1) | F1 |
Scored likelihood / blast / cost → tier. Every SKIP is deliberate and logged.
| Case | L / B / C | Tier | Where |
|---|---|---|---|
PERM file dropped in data/raw/ |
med / high / low | MUST | M9 (named suspicion in message) |
Empty data/raw/ on first run |
high / low / low | MUST | M9 + Scenario B |
| Requested quarter not converted | med / med / low | MUST | M10 |
| Same-FY cumulative overlap double-count | high (report suggests it!) / high / med | MUST ⚠ | F1 |
| cp932-saved manual inputs → traceback | high on target machine / med / low | MUST ⚠ | F2 |
| Truncated parquet sticky-poisons every run | med / med / low | MUST ⚠ | F3 |
| Empty gap-read file blanks flagship section | med / med / low | MUST ⚠ | F4 |
| Hostile gap-read markdown (incl. href breakout) | low / med / low | MUST ⚠ | M13/J5 |
| Blank employer name → empty-key row | very low / low / ~0 | property, as-is | I3/J7 |
Stale hiring_now.csv after shortlist change |
med / low / low | tested-as-designed; warning gap → design observation | M12/J4 |
| CSV type-inference mutations ("nan", broken join) | low / med / low | SHOULD ⚠ | F5 |
| Corrupt xlsx (bad download) | med / low / low | SHOULD | M9 |
| Misnamed xlsx silently unconverted | med / low / low | SHOULD | M8 |
| Mixed-generation CSV/provenance pair | very low / med / ~0 | note | F7 |
| Skip | Reason |
|---|---|
| S1 Perf/memory of the 100 MB stream | Regression unlikely; blast is minutes not wrongness; CI cost high. Observed incidentally in Scenario A. |
| S2 HTML visual appearance | Cosmetic; the human acceptance pass eyeballs the real report — that's the reserved human layer's job. |
| S4 Multi-sheet xlsx | DOL publishes single-sheet; a decoy first sheet fails loudly via the required-columns check (M9). We rely on that check and say so. |
| S5 Ctrl-C exit code (130) | Trivial plumbing; signal tests flaky on Windows. The dangerous consequence of Ctrl-C — the truncated parquet — was promoted to F3 (MUST); only the exit-code cosmetics are skipped. |
| S6 Concurrent runs | Single-operator CLI by design; corrupts only regenerable artifacts. Demoted to D1 README advisory (a docs action, not coverage). |
| S3 Parquet round-trip |
- Writing/running the tests — this document specifies; a separate build step implements.
- Layer A / v2 features (postings, classification, UI, more roles) — design §8.
- Gap-read content quality — Layer 3 is human-reviewed by design; the spec tests the plumbing (placeholder, rendering, escaping), never the judgment.
- Immigration accuracy of caveat wording — ratified in design §9; tests assert verbatim presence only.
- Decision-log entries that are process, not runtime behavior (#1 source choice, #5 architecture split). The engine's no-LLM/no-HTML property is enforced structurally and by review, not by tests.
F1: Design §5/§6, dec. #11, and the report's "convert another quarter" note must be amended for DOL's cumulative-FYTD reality; the dedupe/refuse/warn mechanism is a fork to log.STRUCK 2026-07-04 (dec. #21): mechanism ratified = supersede to the latest same-FY file. dec. #10 and dec. #11 amended inline; the report note now points at fiscal years;quarters_supersededannounced on console + in provenance. Remaining external action: update the Notion Design Doc §5/§6 for cumulative-FYTD reality (routes to Notion per the recording standard, not an in-repo edit).- F6: dec. #3 gains a line: base-SOC matching includes O*NET detail suffixes (ratified).
- J4: no staleness warning exists for
hiring_now.csvafter the shortlist changes — observation for the design doc's next revision, not silently absorbed.
| Method | Items | Automated? |
|---|---|---|
| unit (pytest, synthetic frames) | M1–M7, M9(engine raises), M10, M11(synthetic), M13, M14, M15(force_utf8), F4, F7, V1–V4 |
yes |
| property (hypothesis, dev-only) | I1–I8 | yes |
| fixture-integration (committed mini-xlsx → real pipeline in tmp dir) | M8, M9(CLI: exit 1, stderr, no traceback), M12, M14–M16, F1, F2, F3, F5, P1 | yes |
| real-data gate (local only, never in suite) | Scenario A incl. golden iGavel check | no — required before "ship" |
| manual/human | S2 eyeball, spot-trace, real-user read | no — reserved deliberately |
Fixture strategy (ratified J3): the suite runs entirely on synthetic data — a committed ~20-row
DOL-shaped xlsx in tests/fixtures/ plus in-memory frames — so a fresh clone tests everything
without a 100 MB download. Real data is the acceptance gate's job, not the suite's.
Scenario A — the golden run (on the JP-locale Windows machine dec. #14 exists for):
- Real
LCA_Disclosure_Data_FY2025_Q4.xlsxindata/raw/, nooutput/private/state →python scripts/run.py. - Observe: conversion runs or skips cleanly · all four
[verify]lines print, golden = iGavel, 7 filings, PASS · three artifacts exist · report shows the full table, all 5 caveats, single-quarter note, visible gap-read placeholder · blankhiring_now.csvcreated · exit 0 · zero stack traces · no mojibake. - Hand-fill one
hiring_nowrow; save a realgap_read_filled.md→ re-run → both render; the hand-filled CSV is byte-identical where the tool is concerned.
Scenario B — the failure face: fresh clone, empty data/raw/ → run.py exits 1 with the
plain-English no-data message. No traceback.
Human pass (no assertion replaces it):
- Spot-trace: pick 2 shortlist employers, filter the raw xlsx by hand, confirm filing counts match — "every number traces" verified by a person, repeated for every new quarter.
- Real-user read: the applicant (design §1's single validation user) reads the report and can act on it — names the 3 projects back, understands the caveats.
A + B green and the human pass done = trusted to ship. Nothing less.
Scoped to the load-bearing items (ratified J2):
- The verify layer must demonstrably fire (V1–V4, MUST): each of the four checks gets a
negative test —
check_nonemptyon an empty selection,check_filing_count_sumon a table with a dropped and a duplicated filing,check_employer_collapseon stats where groups > raw,check_golden_top_employeron an FY2025Q4 frame where iGavel is not top — each must raiseRunwayError. A check that can't fail manufactures confidence on every run. - The ⚠ items are the suite's own proof: every known-failing test must actually fail against today's code before its fix lands. A ⚠ test that passes untouched means the test is wrong.
- Fresh-context assertion review (one-time): after the suite is built, a different model/session reads the tests against this spec and flags tautologies — assertions that cannot fail, fixtures that never exercise the branch, mocks that assert the mock. Findings return to this document as amendments.
Ran after the automated suite was built: a fresh-context reviewer (separate session, given the spec + code-under-test + tests, no memory of writing them) hunted tautologies, false xfails, fixtures that miss their branch, mocks that assert the mock, and traceability gaps — cross-checking with
pytest --runxfail. Verdict: trustworthy as the automated leg of the v0 ship gate, no blockers.
Clean, with evidence:
- False xfails — none. All nine ⚠ tests fail at their asserted line for the exact contracted
mechanism (F1/I8
2 == 1double-count; F2×2 realUnicodeDecodeErrortraceback; F3 pyarrow traceback; F4pending is False; F5 literalnancell; F7DID NOT RAISE; M13 liveonmouseover=").xfail_strict=true, 0 xpassed — no ⚠ item passes untouched (§8.2 holds). - Tautologies — none. M14 caveats and M16 provenance assert against independently-stated
oracles (retyped caveat literals; the hand-derived
EXPECTEDdict re-verified row-by-row), not the code's own source. - Mocks / traceability — clean.
monkeypatchonly redirects path constants; every must-index row maps to a named test; §6 methods match; no scope-creep tests.
Findings reconciled into the suite:
| # | Finding | Fix |
|---|---|---|
| 1 | M15's cp932 end-to-end test was vacuous — the success path prints no non-ASCII, so it stayed green even with force_utf8 fully broken (proven by counterfactual). |
Added a unit test of force_utf8() (reconfigures both stdio streams to UTF-8 errors='replace', sets PYTHONUTF8); relabeled the end-to-end test as an environment smoke. M15 gains a unit leg (§6). |
| 2 | M14 asserted the median-wage cell only in its null (—) state; a median→min/max wiring swap would pass. | Added rendered-median assertions ($68,500, $81,800) that only match the median, pinning the wiring end-to-end. |
| 3 | F3's xfail reason over-claimed the convert-side "sticky-skip" path, which the test does not exercise. |
Trimmed the reason to the read-path traceback it actually pins. |
Findings logged, deliberately deferred (silence ≠ oversight):
- M14 wage-excluded note, absence branch (0-excluded → note vanishes) is not exercised — no 0-excluded fixture exists; low value, deferred. (The presence branch and the single-quarter note's absence branch are tested.)
- F3 convert-side sticky-skip variant (a truncated parquet newer than its xlsx, never rebuilt) is not exercised; the dangerous consequence — the read-path traceback — is covered by the F3 xfail. Deferred with the F1/F2/F3 fix step.
- Code observation (not a test defect):
verify.check_employer_collapseembeds its own spellings and reports "verified on synthetic names" — a mild self-reference at the code level. The V3 negative test still fires the realgroups > rawbranch. Logged for the design doc.
Ratification record: baseline M1–M16 (all MUST); J3 synthetic+real-gate; J1 hypothesis dev-only; J2 negative tests + assertion review; J5 MUST incl. breakout; J4 test-as-designed + log gap; J7 no-crash-counted-as-is; skips S1/S2/S4/S5 accepted, S3→P1, S6→D1; ship gate as written; sweep F1–F4 MUST, F5 SHOULD, F6 ratified base-match, F7 note. All decisions by the owner, 2026-07-02.
⚠️ SUPERSEDED 2026-07-06 (dec. #33). This slice specified tests for the scheduled-fetch / per-title-parquet-manifest architecture (fetch_quarters.py,build_shortlists.py,run_pipeline.py,data-pipeline.yml), which the converged v1 Build Plan replaced and which has been deleted. The section is kept as history — do not build from it. The v1 data path is nowrun.py→web/data/{design.json, design.provenance.json, design.csv}(Design Doc §4.2/§4.3, D3/D9); its test spec is authored fresh in the next test-spec pass. P1–P21/Q1–Q5 below no longer map to code.
test-spec slice · ratified 2026-07-06 — ownership record: RATIFICATION_LOG_v1.md, Sitting 2.
Under test (the slice): the v1 automated data pipeline that replaces v0's manual
"download-and-drop" — scripts/fetch_quarters.py (discover +
download + prune DOL quarters), scripts/build_shortlists.py
(per-title incremental shortlist build + manifest), the
data-pipeline.yml CI workflow's gating, and the
engine seams they reuse (discover_quarters, load_quarters,
supersede_cumulative_quarters, build_sponsor_table, the verify layer).
Design authority: docs/decision_log.md dec. #22 (scheduled fetch + HEAD-probe
discovery), #23 (commit processed parquet), #24 (per-title incremental shortlists),
grounded on #21 (cumulative-FYTD supersession). There is no local v1 design doc; the
Notion Design Doc is the upstream copy. Items decided during this slice's dialogue carry
their ratify-call letter (A–K) and are marked [ratified-in-test-spec]; the design
amendments they owe are listed in §v1.6.
Slice boundary (declared): this slice covers the fetch/build scripts, the CI gating, and
the engine seams. Out of slice, owed to the v1 sweep: the GitHub Pages frontend that
reads index.json; the deferred absorption of the v0 private report into the site path
(dec. #24); the manifest's generated_at_utc churn (benign — only committed when something
else changed; documented, not tested). v0 engine internals are already covered by §1–§8 M1–M16
/ F1–F8 / I1–I8 — re-affirmed at the seams, never duplicated.
Acceptance statement: the v1 pipeline is trusted when Scenario D (synthetic incremental proof) is green in the suite, the v0 suite (§P17) stays green, the extracted CI orchestrator's gating unit-tests are green, and the first real scheduled/dispatched CI run (Scenario C) downloads + converts + builds against live DOL and is a clean no-op on an already-current repo.
Where the risk concentrates (ratified):
- Committed-data destruction from a partial signal. A single flaky HEAD probe (B) or a scoping flag (G) or a killed mid-write (H) can delete or corrupt parquet the frontend serves — and the pipeline commits the damage. This is the slice's dominant blast class.
- The repeat-sponsor signal's ≥2-fiscal-year floor. The lookback window is exactly 2 FYs (dec. #22); anything that silently drops an FY collapses the product's core edge to false.
- Incrementality correctness. "Already saved" must mean "matches current definition AND window" (C) or a config edit silently no-ops; and the build must be genuinely idempotent (P9) or CI commits churn.
- The multi-title loop vs. the verify trust contract (A). One title must not take down the others (empty case) — but an integrity failure must still stop everything (engine-bug case).
⚠ = design-anchored and expected to fail against today's code — author blind to the implementation, commit red-first (dec. #20 xfail-strict pattern), drive to green.
| # | Behavior (one line) | Traces to | Anchor class | Method | Tier |
|---|---|---|---|---|---|
| P1 | current_fiscal_year: Sep 30 → FY N; Oct 1 → FY N+1 (Q1); total fn of injected date |
dec.#22; fetch_quarters.py:53 |
design | unit | MUST |
| P2 | discover_upstream: per FY, newest-first probe → highest published quarter; records its URL |
dec.#22; :79 |
design | unit (mock) | MUST |
| P3 | fetch reconcile: published-not-in-have → download; in-have → skip; changed iff disk changed |
dec.#22; :124 |
design | integ (mock) | MUST |
| P4 | Conservative prune: prune ONLY for supersession (newer same-FY seen) or out-of-window; never on a probe-miss incl. 404/5xx/429 while another FY resolves | dec.#22; B [ratified-in-test-spec]; :161 |
design | integ (mock) | MUST ⚠ |
| P5 | Total blackout (no FY published) → RunwayError naming the README URL template; prunes nothing |
dec.#22/#23; :138 |
design | unit | MUST |
| P6 | Network URLError → RunwayError, plain English, names the URL/README (self-diagnosing) |
dec.#15; :71,112 |
design | unit | MUST |
| P7 | fetch & build emit changed to $GITHUB_OUTPUT = true iff disk changed |
dec.#22/#24; :172 |
design | unit | MUST |
| P8 | build triggers: no-op when current · new quarter → all rebuild · new title → only it · removed title (from full ROLE_SOC) → pruned | dec.#24; :78 |
design | integ | MUST |
| P9 | build idempotence: 2nd run on unchanged inputs writes no parquet, changed=False |
dec.#24; :103 |
design | property/integ | MUST |
| P10 | manifest consistent + frontend-readable: window, per-title soc_codes/counts/parquet name = what was built |
dec.#24; :126 |
design | integ | MUST |
| P11 | fetch/engine agree: post-fetch data/processed = one parquet per in-window FY → supersede map empty |
dec.#22 ↔ #21 | design | integ | MUST |
| P12 | Empty-result title → isolated, marked empty in manifest, others still build; integrity-check RunwayError → aborts whole run |
dec.#24 fork; A [ratified-in-test-spec]; verify.py; sponsors.py:227 |
design | integ | MUST ⚠ |
| P13 | Saved-state key includes soc_codes+wage_level: editing a title's SOC (no new quarter) rebuilds it |
dec.#24 amended; C [ratified-in-test-spec]; :103 |
design | integ | MUST ⚠ |
| P14 | CI gating decisions (convert-if-fetch-changed · commit-if-either · push retry) live in a testable scripts/run_pipeline.py orchestrator |
dec.#22/#24; E [ratified-in-test-spec] | design | unit (orchestrator) | MUST ⚠ |
| P15 | --titles scopes the build only; prune always considers full ROLE_SOC; a scoped run never deletes an out-of-subset title |
dec.#24; G [ratified-in-test-spec]; :92,119 |
design | integ | MUST ⚠ |
| P16 | Shortlist parquet written atomically (.part→replace); up_to_date verifies readability; retry-after-crash self-heals |
F3 precedent; H [ratified-in-test-spec]; :62,103 |
design | integ | MUST ⚠ |
| P17 | v0 regression: the existing suite stays green (v0 private path untouched, dec.#24 additive) | dec.#24; v0 §1–§8 | code (pin) | suite | MUST |
| P18 | Download truncation guard: Content-Length mismatch → RunwayError + no file at dest |
dec.#7; D [ratified-in-test-spec]; :115 |
design | unit (mock) | SHOULD (green — guard already built; see §v1.7 amendment + dec. #31) |
| P19 | git push non-fast-forward → rebase-or-retry contract; a run's regenerated data is not silently discarded |
dec.#22; K [ratified-in-test-spec]; yml:66 |
design | orchestrator unit + review | SHOULD |
| P20 | discover_quarters: two files with a case-only difference mapping to one FY label → deterministic tie-break or hard error, never a silent drop |
J [ratified-in-test-spec]; sponsors.py:101 |
code (pin) | unit | SHOULD |
| P21 | quarters_superseded manifest field reports a real same-FY collapse (capture the map, don't discard it) |
dec.#16; I [ratified-in-test-spec]; :88,73 |
design | integ | SHOULD |
- P4 (B) The prune loop must never treat "this in-window FY didn't answer my probe this run"
as "delete it." DOL serves stable permanent links and never un-publishes a quarter, so a
missing probe is always a transient failure or a template change. Concretely: given committed
{FY2025Q4, FY2026Q1}and a run where the FY2025Q4 HEAD returns 503/429/404 while FY2026Q1 returns 200, FY2025Q4's parquet survives.check_nonempty… — the test matrix must include a 5xx/429, not just a 404, and the case where another FY resolves (so the total-blackout guard P5 does not fire). Blast: the ≥2-FY repeat-sponsor floor collapses; the site self-heals only on a later successful probe. - P12 (A) Two failure kinds diverge. An empty-result title (
No certified rows matched…) is a normal outcome for a thin niche role → skip it, recordstatus: emptyin its manifest entry, keep building every other title. An integrity-check failure (check_filing_count_sum,check_employer_collapse) means the engine is miscounting — which corrupts every title in the run — so it still raises and aborts the whole build; nothing ships. Isolating an integrity failure would ship corrupted siblings while the check that says "do not trust this run" is filed as "one title skipped." Today the code raises on both and aborts on both — P12 is red on the empty-isolation half. - P13 (C)
up_to_datetoday compares only the window. The manifest already storessoc_codes; the key must become (title × definition × window) so that editingROLE_SOC["design"]without a new quarter marks the title not-saved and rebuilds it. Red today. - P15 (G)
build_all(only_titles=…)currently restrictstitles, then the stale-prune loop (set(prior) − set(titles)) deletes every title not in the subset and the manifest is rewritten with only the subset — so--titles designsilently deletesengineering's parquet and manifest entry. A scoping flag must never remove. Red today. - P16 (H)
to_parquetwrites directly (no.part), andup_to_datechecks only that the file exists. A process killed mid-write leaves a truncated parquet; because the prior manifest entry still matches the window, the next run skips the rebuild and serves the corrupt file forever. Mirror F3: atomic.part→replace on write, andup_to_datemust confirm the parquet actually reads. This also makes retry-after-crash self-healing. Red today.
| # | Property (falsifiable statement) | Anchors |
|---|---|---|
| Q1 | current_fiscal_year is total and monotone across the Oct 1 boundary; same date → same FY |
P1 |
| Q2 | build_all is idempotent: a second run on unchanged inputs writes no parquet and reports changed=False |
P9 |
| Q3 | Prune safety: a committed in-window FY parquet is removed ONLY if a newer same-FY quarter was positively observed, OR the FY is out of the lookback window | P4/B |
| Q4 | fetch/engine agreement: after a completed fetch, supersede_cumulative_quarters(discover_quarters(processed)) returns an empty superseded map |
P11 |
| Q5 | --titles never shrinks the manifest's title set relative to ROLE_SOC ∩ prior-manifest |
P15/G |
| Case | L / B / C | Tier | Where |
|---|---|---|---|
| Transient single-FY probe failure (404/5xx/429) prunes committed data | med / high (kills repeat-sponsor) / low | MUST ⚠ | P4/B |
--titles <subset> deletes out-of-subset titles |
med (any scoped local run) / high / low | MUST ⚠ | P15/G |
| Killed mid-write → truncated shortlist served forever (no self-heal) | med (CI OOM/timeout) / high / low | MUST ⚠ | P16/H |
| Empty-result title aborts the whole multi-title build | high once title #2 lands / med / low | MUST ⚠ | P12/A |
| SOC edit without a new quarter silently no-ops | med / med / low | MUST ⚠ | P13/C |
| Total upstream blackout wipes all parquet | low / high / low | MUST | P5 (guard exists) |
git push non-fast-forward discards a run |
low-med / med (a week to next run) / low | SHOULD | P19/K |
discover_quarters case-only label collision drops a file |
low / med / low | SHOULD | P20/J |
quarters_superseded can't report a real collapse |
low / low / low | SHOULD | P21/I |
| Skip | Reason |
|---|---|
| SK-v1-1 Live DOL network I/O in the suite | F: suite mocks the network; the real fetch is proven by the first scheduled CI run (Scenario C). DOL changes ~4×/yr and a break gives ample response time; the control is diagnosability (P5/P6 name the URL template), not a suite test. VCR/live-smoke rejected (staleness / re-introduced flakiness). |
| SK-v1-2 Real 80–140 MB stream perf/memory | Inherits v0 S1; observed incidentally in Scenario C. |
| SK-v1-3 kill-mid-stream download atomicity (vs the Content-Length guard) | Trusted to the atomic-rename idiom + F3 downstream + P18; only the mockable truncation guard is asserted. |
SK-v1-4 concurrency-group internals + [skip ci] loop-avoidance |
GH-Actions primitives, review-only per E; the dangerous consequence (push race) is promoted to P19/K. |
| Method | Items | Automated? |
|---|---|---|
| unit (pytest) | P1, P5, P6, P7, P14, P18, P20, Q1 | yes |
integration (mocked network; fake data/processed/manifest in tmp dir) |
P3, P4, P8, P9, P10, P11, P12, P13, P15, P16, P21, P19 (orchestrator contract) | yes |
| property (hypothesis, dev-only) | Q1–Q5 | yes |
| suite-regression | P17 (the v0 suite, unchanged) | yes |
| manual / review | P14 YAML residue + P19 push (code review); Scenario C = the real-fetch acceptance leg | no |
Authorship contract (carried downstream). The design-anchored ⚠ items — P4, P12, P13,
P14, P15, P16 — must be authored by a test-writer blind to the implementation: a fresh
session given docs/decision_log.md dec. #22/#23/#24 + this v1 slice section only, never the
scripts or the diff. Commit them red-first (xfail-strict, dec. #20 pattern) and drive to
green in the build step. P17 (v0 suite) and P20 (label-collision pin) are regression pins
that may read the code. Network is mocked throughout (SK-v1-1).
- Scenario C — real fetch (the reserved real-data leg). The first scheduled or
workflow_dispatchCI run against live DOL: the URL template resolves, the current-FY quarter downloads + converts, per-title shortlists build, and — on an already-current repo — the run is a clean no-op (fetchchanged=false→ convert skipped → commit skipped). Observed once, logged. This is the v1 analogue of v0 Scenario A. - Scenario D — incremental proof (synthetic, in-suite). Seed a tmp
data/processedwith FY(n−1)+FY(n) parquet, then: runbuild_shortliststwice → first builds, second is a no-op (changed=False, no parquet rewritten); add a title → only it builds; advance the window (drop-in a new-FY parquet) → all titles rebuild; remove a title fromROLE_SOC→ its parquet is pruned. Plus the prune-safety and--titlesscoping cases (P4, P15) driven with a fake prober.
Gate: Scenario D green in the suite and P17 (v0 suite) green and the P14 orchestrator unit-tests green and Scenario C observed once = the slice is trusted. A green suite alone is not the gate — Scenario C proves the live source, which the suite deliberately never touches.
- The ⚠ items are the slice's own proof. P4, P12, P13, P14, P15, P16 must each fail against today's code before their fix lands (xfail-strict, 0 xpassed) — a ⚠ that passes untouched means the test is wrong. Because they're authored blind to the implementation, the builder's blind spot and the test author's blind spot are not the same blind spot.
- Fresh-context assertion review (one-time), after the suite is built: a different
model/session reads the v1 tests against this section and hunts tautologies — a mocked prober
that never exercises the prune branch, an
up_to_datetest that asserts the mock, a manifest oracle copied from the code rather than independently stated. Findings return here as amendments, as in v0 §8.3.
Spec amendment (2026-07-06, Test-Build reconciliation — mirrors v0 §8.3). P18 was marked ⚠
but is already green: the Content-Length truncation guard exists at scripts/fetch_quarters.py:115
(stream to .part → compare bytes vs Content-Length → unlink + RunwayError, so dest is never
created), ratification call D reads "test existing behavior," and the ratified red-first set is
{A,B,C,E,G,H} = {P12,P4,P13,P14,P15,P16} — which never included D/P18. P18's ⚠ marker is struck in
§v1.1/§v1.5/§v1.7; its test stays a plain green pin (never xfail). Logged as docs/decision_log.md dec. #31.
Route to docs/decision_log.md (the why, in-repo) and the Notion v1 Design Doc §5/§6
(pipeline shape). This skill wrote only this spec + RATIFICATION_LOG_v1.md.
- dec. #24 open fork CLOSED (A): zero-result title → isolate + mark
empty; integrity-check failure aborts the whole run. - dec. #24 saved-state key (C): (title × window) → (title × definition × window).
- dec. #22 prune rule (B): explicit — supersession or out-of-window only; never on a probe-miss (incl. 5xx/429 while another FY resolves).
- CI architecture (E): gating consolidates into a testable
scripts/run_pipeline.py; YAML shrinks to checkout → orchestrator → push. --titlessemantics (G): scope-only; never prunes out-of-subset titles.- Shortlist output write (H): atomic + readability-checked, at F3 parity.
Ratification record (v1 slice): calls A–K all ratified by the owner 2026-07-06 via
prediction-before-reveal; 10/11 predicted · 1 surprised (A) · 0 no-opinion; two owner-overrides
(E extraction, F diagnosability) and two owner-expansions (I, J fixed over noted). MUST ⚠ set =
P4/P12/P13/P14/P15/P16 (design-anchored, red-first); SHOULD = P18/P19/P20/P21; SKIPs
SK-v1-1..4 accepted. Full per-call record: RATIFICATION_LOG_v1.md, Sitting 2.