This release is a pre-release and may not be stable for production use.
proofpack
ProofPack engine (Global Phoenix Solutions Ltd). Computes performance estimates, confidence intervals, differences and distribution comparisons from a customer-supplied test table and renders them into evidence-pack templates.
It is statistical software. It is not a certification, an audit, a regulatory
opinion, a clinical evaluation or a submission, and no regulator has reviewed,
accepted, approved or endorsed it (see src/proofpack/scope.py).
Status
Pre-release (0.1.0.dev1), still being built and not yet on sale. The command line
has six subcommands: doctor, map, run, compare, fixtures and licence
(proofpack --help). run writes the assembled document run.json from a confirmed
mapping and, with a usable licence installed, the HTML documents named by
--templates; compare writes run.json with a comparison block and T2.html;
licence show | verify | install verifies an Ed25519 licence against the shipped
public key. Each build day's record, including what is not built yet, is in
handoffs/ in the source repository, https://github.com/JoshSandhu/proofpack.
Develop
uv python install 3.12
uv venv --python 3.12
uv sync --all-groups --locked
uv run pytest -q # all tests
uv run pytest -q -m day1 # one build day
uv run ruff check . && uv run ruff format --check .
uv run pip-audit
uv build --wheel
uv run proofpack doctor --offline
Layout
schema/ JSON Schemas: input table v1, criteria.yaml, claims (skeleton), egress (skeleton)
design/guidance_map_v1.csv guidance references by internal id; section numbers are transcribed by hand
src/proofpack/io/ schema (load + type + flow), declare (criteria.yaml), mapping (roles, H07, H11),
profile (value-level column summaries, suppressed)
src/proofpack/gates.py HALT gates H01-H12 and the ingest pipeline
src/proofpack/doctor.py proofpack doctor
src/proofpack/cli.py doctor | map | run | compare
tests/ conftest.py seeded cohort factory; F12 gate tests; hypothesis schema fuzz;
fixtures/mapping/ 35 header-variant CSVs with authored expectations
scripts/ mutation_sweep.py (DEC-12 ii), coverage_bar.py, make_mapping_fixtures.py
handoffs/ one note per build session
Principles fixed on day 1
- Declarations (positive class, score orientation and type, thresholds,
prevalence, subgroup attributes, reference-standard type, indeterminates,
clustering) are read from
criteria.yamland never inferred. Missing means HALT. - Every HALT exits 3 and writes nothing.
import proofpacknever requires scipy.- Error messages and any future egress carry aggregates only: never original headers, cell values, free-text declaration fields or raw dates.
proofpack map (build day 6, lane A)
proofpack map --input test.csv [--criteria criteria.yaml] [--out mapping.json] [--yes]
What it does, in order:
- Reads the table (
io.schema.load_table), then profiles every column from its first 10,000 rows after the header (io.profile): inferred type (int / float / date / categorical / string),n_unique, the top <= 20 values with counts, min/max for numeric and date columns (dates coarsened toYYYY-MM), missing % after the missing-token normalisation, and for <= 2-unique columns the split. A value is listed only when its count is >= 10 (the egress cell floor of D1 section 6, reused here because D1 section 5 states no floor of its own); below that it prints as<suppressed>. A numeric min or max is printed only when >= 10 sampled rows hold that value; otherwise it prints as<suppressed>too (["100"] * 41 + ["7"] * 9printsmin <suppressed> max 100). A date min or max month is printed only when= 10 sampled rows fall in that month (DEC-39:
["2024-03-15"] * 49 + ["1999-01-01"]printsmin <suppressed> max 2024-03); the day/month order of a slash, dot or dash date is decided once per column from the sampled values, never per value: if any sampled value has a second field above 12 and none a first field above 12, the column is month-first; if any has a first field above 12, day-first; both or neither, day-first (["03/15/2024"] * 9 + ["04/03/2024"]is nine rows of March and one of April and printsmin <suppressed> max <suppressed>; at 4fbbf35 the order was decided per value and it printedmin 2024-03for a nine-row month;["03/15/2024"] * 9 + ["03/04/2024"]prints2024-03for both;["13/15/2024"] * 60keeps2024-15). A categorical/string column with more than half of its non-missing sample unique lists no values. - Assigns a role and a confidence to every header (
io.mapping.map_headers): exact canonical name, the synonym table, an affix-stripped synonym (pt_age,label_v2), a header token (patient_nbr), a date-like header, then the value heuristics (two-valued label sets, floats within [0, 1], parseable dates, unique integers). The confidence table is in the module docstring. Declarations (positive class, orientation, threshold, reference-standard type, indeterminates, clustering) are not read by the mapper;io.declarereads them fromcriteria.yaml. - Halts with a typed code and exit 3: H07 when two headers coincide after case-folding,
trimming and NFC normalisation; E01 (DEC-11) when two headers resolve to
case_idby name (canonical, synonym or affix -patient_id+subject_id; two headers that only carry a token,patient_nbr+mrn_local, are bothcase_id lowfor the prompt, andproofpack runon a mapping that still holds twocase_idcolumns halts E01 at apply time), whencriteria.yaml'sclustering.unitis a list of two or more, or a string that splits into two or more tokens onand(any case) or any character outside[A-Za-z0-9_](subject_id/hadm_id,patient_id, study_id,a b; the hyphenated single namepatient-idalso counts two), or whenclustering.columns/key/keys/column/units/fieldslists two or more or is a string that splits into two or more the same way (message endsreduce your case key to one column); H11 when a column is date-like by header (the H11 header regex) or typeddateby values - every sampled value one of the six shapes inio.profile.DATE_PATTERNS(ISO,15/03/2024,2024/03/15,15-03-2024,15.03.2024,17 Mar 2024; two-digit years such as3/17/24are not among them) - and noperioddeclaration covers it. - Halts H07 before the table when
--outnames an existing directory (the message carries the last path component only:--out names an existing directory (pack)) or when the directory of--outdoes not exist, then prints the tableoriginal header -> role -> confidence -> value summary(under--quietonly when stdin is a terminal, because the prompts refer to it), then:- stdin is a terminal and
--yesis absent: prompts once per non-high role. The answer is stripped of surrounding spaces and lower-cased;aoracceptaccepts,eoreditasks for a canonical role,attr_<name>/rater_<name>in lower-case letters, digits and_, orignore; an entry accepted or edited at the prompt gainsconfirmed: true(DEC-28 for the accept, DEC-42 for the edit: at 4fbbf35 an edit left itfalseand the two edited files fed were H07 under--yes);q,quitoraborthalts H07; the empty answer (Enter alone) and any other word printanswer a, e or qand ask again. An accept or an edit that would give a role a second holder among the columns already settled is refused at the prompt. An edit toignoreon the column--criteria'speriod.columnnames is refused with one line (refused: the period declaration names a column mapping.json ignores; map it to event_date or declare another column), and a mapping that ignores that column (one proposedignore highis not among the prompted entries, which are the non-high roles:visitholding one ISO date and 59 blanks) halts S03 with the same sentence here and atproofpack run, with or without a prior; at 4fbbf35run --mappingon such a file built the pack's period axis from the ignored column while counting it inn_unused_columns(tests/test_mapping_repair4.py::test_an_ignored_visit_named_by_period_column_is_s03_on_both_routes). Aperiod.columnnaming an original header mapped to a role is read under that role atproofpack run(at 4fbbf35visit -> event_datewithperiod.column: visitwas S03declared period column is not present in the table). An edit toignoreon a column whose header, after BOM stripping, trimming, NFC and case-folding, equals a role that any other column currently holds is refused with one line naming both headers and the role (DEC-31:scoreholding 0/1 besideprob, both proposed asscore low, cannot be set toignorewhileprobholdsscore; givescoreanother role,attr_score_flagory_pred, orprobanother role). The accept arm runs the same check for a role an already-ignored column is named for;map_headersproposesignoreon no header that is a role name, so that direction is reached by a hand-built mapping intests/test_mapping_repair3.py::test_accept_or_edit_to_a_role_an_ignored_column_is_named_for_is_refusedand not from this command as measured on the 19 canonical names,attr_xandrater_x. Atproofpack runan ignored column keeps its name unless that name is one the schema reads as a role (a canonical name,attr_<identifier>, arater_name), in which case it is keyedignored:<header>and counted inn_unused_columns: at b0f60a6sex(1/2/9) answerede ignorereached the pack as thesexattribute andscore(0/1) ignored besideprob -> attr_prob_rawwas read as the score (tests/test_mapping_repair3_2.py::test_an_ignored_column_named_for_a_role_does_not_reach_validate_under_that_name). When every role is high the prompt is once for the whole mapping:aoracceptaccepts,q,quitoraborthalts, the empty answer and any other word printanswer a or qand ask again. The file is written withdecided_by: interactive. Ctrl-C between the table load and the last prompt (the test raises it fromload_tableand at two prompts), or a closed stdin at a prompt, is H07 (nothing written); Ctrl-C during the write is H07mapping interrupted while writing --out(the file may be partial); anOSErroron the write (a read-only--outwas fed) is H07--out could not be written, without the path; - stdin is not a terminal and
--yesis absent: halts H07 (run interactively or pass --yes with a prior mapping.json) before any prompt; --yes: accepted only when a priormapping.jsonat--outhas the sameheader_set_sha256, itsdecided_byisinteractiveorfile(aproposedfile written byproofpack runis refused), its entries name exactly this table's headers and every role in it is a canonical role or anattr_/rater_name (DEC-29), no ignored entry is named for a role another entry holds (DEC-31, the same H07 asproofpack run --mapping), every mapped role in the file ishighorconfirmed: true, the file's role for each column equals the role computed from this table (a priorageon a column now holding[70-80)bands halts:maps a column to age but the mapping computed from this table gives it age_band) unless the file'sdecided_byisinteractive, the entry isconfirmed, the mapping computed from this table gives the columnmediumorlow(the per-role prompt iterates themediumandlowentries) and its stored value summary equals the fresh one (DEC-42: the role a human chose at the prompt stands on the values the human saw;score -> attr_score_flagedited besideprob -> scoreaccepted, onrow_id,label,score,prob, passes--yes- at 4fbbf35 it was H07 for ever,tests/test_mapping_repair4.py::test_e_attr_score_flag_beside_prob_score_passes_yes_and_e_attr_patient_code_too;decided_by,confirmedand the stored summary are read from the file and the confidence is computed from the table; who typed the file is not verified - the paragraph under the JSON below names the files fed), eachconfirmedentry's stored value summary equal to the one computed from this table ininferred_typeand in the values of a two-valuedsplit, where the file holds a summary for that column (a prior that holds no summary for a column is not compared for that column, whoever wrote it:value_summaries: {}ornullin a confirmed prior passes on a re-exported column; a hand-authoredfileprior that does hold a summary line for a confirmed column is compared like an engine-written one,tests/test_mapping_repair5.py::test_a_file_prior_with_a_typed_summary_is_compared;tests/test_mapping_repair4.py::test_a_confirmed_prior_without_summaries_is_not_compared; counts are not compared; at b0f60a6 thepatientcolumn confirmed ascategorical; 20 uniqueand re-exported as the ten strings0.0..0.9passed, andGenderconfirmed as 0/1 and re-exported as 0/1/2 passed; now H07the values of a column confirmed at the prompt changed,tests/test_mapping_repair3_2.py::test_yes_halts_h07_when_a_confirmed_columns_values_changed), and every non-high role computed from this table isconfirmed: truein the file (DEC-28); otherwise H07. The prior file's roles are used and itsdecided_byis written back as read (interactivestaysinteractive; until 9cfbdd5 it was rewrittenfile). A prior file with a leading UTF-8 BOM (PowerShell 5.1Out-File) is read; the file is written back without it.
- stdin is a terminal and
mapping.json (the one file this command writes original headers to) is written as
below. This is the shape DEC-27 fixes for the E7 manifest hash: a list of entries, each
carrying original, role, confidence, source, notes and confirmed, with the
file-level header_set_sha256, value_summaries, decided_by and timestamp.
{
"header_set_sha256": "<sha256 of the sorted, trimmed header set>",
"roles": [
{"original": "SepsisLabel", "role": "y_true", "confidence": "high",
"source": "synonym", "notes": [], "confirmed": false},
{"original": "Gender", "role": "sex", "confidence": "medium", "source": "synonym",
"notes": ["no dictionary declared for 0/1", "accepted interactively"],
"confirmed": true},
{"original": "HR", "role": "ignore", "confidence": "high", "source": "ignore",
"notes": [], "confirmed": false}
],
"value_summaries": {"<original header>": {"inferred_type": "int", "n_sampled": 50,
"n_missing": 0, "missing_pct": 0.0, "n_unique": 2, "top": [["0", 35], ["1", 15]],
"n_suppressed_values": 0, "min": "0", "max": "1", "split": [["0", 35], ["1", 15]],
"free_text": false, "values_shown": true}},
"decided_by": "interactive | file | proposed",
"timestamp": "<UTC ISO 8601>"
}
The mapper sets confirmed: true only on an entry accepted or edited at the per-role
prompt (the all-high accept leaves every entry false; at 4fbbf35 an edit did too);
--yes reads the flag, cannot verify who set it, and writes the file back with
whatever confirmed values it held (confirmed: true planted by hand on a high entry
survives map --yes:
tests/test_mapping_repair3_2.py::test_the_mapper_sets_confirmed_only_on_an_accept_or_an_edit_and_yes_writes_back_what_it_read).
A prior whose decided_by is file (or absent) and which carries confirmed: true on
an entry whose role differs from the computed one is H07 on the role difference
whether or not it holds a value summary for that column: at 9cfbdd5 the file
proofpack run writes without a prompt, copied with decided_by: file, age -> attr_age_years and confirmed: true typed in, passed map --yes and run --yes and
the pack listed attr_age_years (lens-1 fresh-attack B1 of repair 4), and a hand-built
prior with site -> ignore, confirmed: true and the one line "site": {"inferred_type": "categorical", "split": null} under value_summaries passed too
(lens-1 regression B1); both are exit 3 now, with value_summaries: {} as at 4fbbf35
(tests/test_mapping_repair4_2.py::test_a_confirmed_file_prior_is_h07_on_a_role_difference_with_or_without_a_summary).
decided_by: interactive typed by hand into that copy beside confirmed: true on
age is H07 too, because the mapping computed from the table gives age high and
the prompt asks about medium and low entries only; typed beside confirmed: true
on Gender (medium, 0/1) with the engine's summary intact it passes, and so does the
file the prompt wrote after a a with patient -> sex and Gender -> case_id swapped
by hand (map --yes exit 0; run --yes then halts H05 on the duplicate case_id
rows); that swapped file and the one the prompt writes for e sex, e case_id differ
in the two notes strings (accepted interactively / edited interactively) and the
timestamp only, and --yes reads neither
(tests/test_mapping_repair4_2.py::test_decided_by_interactive_typed_by_hand_is_read_on_a_non_high_entry_only).
Mapping.read takes true, false or the key absent (read as false) and halts H07
on any other JSON type. decided_by is interactive after the prompts and stays
interactive through --yes and proofpack run --mapping (until 9cfbdd5 both
rewrote it file; tests/test_mapping_repair4_2.py::test_yes_and_run_mapping_keep_decided_by_interactive),
file when a prior says so or has no decided_by key, and
proposed on the file map_headers computes before any confirm step (until build
day 7 proofpack run wrote that file into --out; DEC-26 ended it). proofpack run
on a proposed file, with or without --yes, halts H07 this one was not confirmed
and copies nothing into the pack
(tests/test_mapping_repair3_2.py::test_a_proposed_prior_is_not_relabelled_file_by_run_mapping).
The sha256 of the file's bytes is Mapping.file_sha256 after write() or read(),
and the same bytes are manifest.mapping_sha256 in run.json (DEC-27).
proofpack run (build day 7, lane E)
proofpack run --input test.csv --criteria criteria.yaml [--mapping mapping.json] [--out ./pack] [--offline]
- A confirmed mapping is required (DEC-26).
--mapping FILE, or<input>.mapping.jsonbeside the input (test.csv->test.csv.mapping.json); neither present, or a file that fails the--yesrule above (decided_byinteractiveorfile, the header-set hash equal, every role high or confirmed), isHALT H07: run proofpack map first ...before any statistics.runnever proposes a mapping and never writes one anywhere. - What is written:
<out>/run.json, the D1 section 4.2 document as canonical JSON (sorted keys, one-space indent, UTF-8, noNaNorInfinity- a non-finite float raises and nothing is written), plus the day-1ingest_report.json. Nothing is written on a HALT. - Criteria (
src/proofpack/criteria.py): everycriteria.yamlcriterion and the fairness bound is one ofmet/not_met/not_assessablewith a machinereason_code, comparing the statistic the customer named (ci_lower_bound=ci_lo,ci_upper_bound=ci_hi,point_estimate=est, read literally whatever the comparator) with the customer's comparator and value. The row is routed on the Number'smethodbefore any statistic is read:method: none(a typednot_estimable_reason) isnot_assessable/no_intervalunderci_lower_bound,ci_upper_boundandpoint_estimatealike, with the reason indetail(tests/test_criteria.py::test_a_number_with_method_none_is_not_assessable_under_each_of_the_three_statistics; atab729d3point_estimatewas compared onest).attainable_at_n/max_lower_bound_at_n(the Wilson lower bound atk = n,stats.attainability) are filled forci_lower_boundcriteria on proportion metrics whose Number'smethodiswilsonand whosenis an integer above 0; on any other method (a cluster bootstrap,none) both arenullanddetail.attainability_not_computedreadsmethod_not_wilson, atn0 andnulltoo (tests/test_criteria.py::test_a_method_none_number_at_n_zero_or_n_null_is_annotated_method_not_wilson; at02d00c5azero_denominatorrow atn0 carried no annotation). No default bound, statistic or comparator exists; a fairnessboundneedsstatisticandcomparatorbeside it (H08 otherwise). - Ledger (
io/ledger.py):ledger.jsonin the per-user directory (PROOFPACK_HOME, else%LOCALAPPDATA%\proofpackon Windows,~/.proofpackelsewhere) counts runs with acriteriablock per test set (SHA-256 of the analysedy_trueandscorebytes);W14when the count exceedsledger.warn_after_acceptance_runs; noledgerblock, no warning. - Licence (
src/proofpack/licence/): the file atPROOFPACK_LICENCE, else the per-userproofpack.lic, else./proofpack.lic, verified against the shipped public key (pp-2026-09, the key published on/trust).okandgraceare full runs (exit 0 / 2;graceputsLICENCE EXPIRED - not for submissioninmanifest.watermark); expired past grace, refused or absent still computes and writesrun.jsonwith that watermark and exits 4 with the one-line fix (D1 section 7: after gracerun/compareemit JSON only;doctor/map/fixturesalways work). A trial carriesTRIALand expires 30 days after issue. proofpack licence show(the installed file, no signature printed),proofpack licence verify FILE(exit 0 forok/grace, 4 otherwise) andproofpack licence install FILE(verifies, then copies to the per-user location; a refused file is not installed).- Exit codes: 0 ok, 2 warnings only, 3 HALT, 4 licence, 5 internal.
- Documents (build day 8, lane E).
--format json,html(the default) writes<out>/T8.htmlbesiderun.jsonwhen the licence isokor ingrace; after gracerun.jsonis written, no document is, and the exit code is 4.--format jsonwrites no document;docxandpdfare refused with a typed line.--templates T8(the default) names the document;T1andT7are accepted and answered with a typed "not built in E8" line. The HTML needsjinja2at run time (a runtime dependency of the wheel).run.jsoncarriesclaims,claim_rejectionsandguidance_refs({id, label, draft, url}), and everycriteria_resultsrow carriesdeclaration_index, the position of thecriteria.yamlentry it was evaluated from (nullon the fairness-bound rows). On the page a declared number (threshold, criterion value, prevalence, fairness bound) prints with every digitrun.jsoncarries; engine estimates print by D4 section 1.2's rules. - H02 reads
y_predtoo (repair 1 of build day 8): ay_predcolumn holding a non-blank value outsideclasses.positive,classes.negativeandindeterminates.valueshalts H02 naming the column, before any statistic. The two tables run:yes/nobeside classes1/0without a score column (tests/test_e8_repair1.py) and the same column beside a score column (tests/test_e8_repair2.py); each exits 3 and writes no directory. - A blank
y_predcell and an indeterminate-valued one (repair 2 of build day 8): on a table without a score column a blanky_predis a missing prediction input, excluded from every statistic, and counted underflow.excluded_missing_scorewhen the row is not adevrow and itsy_trueis present (a non-devrow whosey_trueis also blank counts underflow.excluded_missing_label, and adevrow counts underflow.dev_rows: 120 rows, 20 of themdev, withy_predblank on every tenth row andy_trueon every twentieth, givedev_rows 20,excluded_missing_label 5,excluded_missing_score 5,analysed 90intests/test_e8_repair4.py; 12 blanky_predcells, 6 of them on blank-label rows, giveexcluded_missing_label 6andexcluded_missing_score 6intests/test_e8_repair3.py; beside a score column the score is the input and a blanky_predchanges nothing); ay_predequal to a declaredindeterminates.valuesentry marks the row indeterminate (flow.indeterminate), as ay_trueequal to it does. Measured on 120 rows without a score: 60 blanky_predcells giveflow.excluded_missing_score 60,analysed 60and the two-by-two of the 60 non-blank rows; at657ef11the same table gaveanalysed 120with every blank counted as a negative prediction.
Metadata
Release files for proofpack 0.1.0.dev1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| proofpack-0.1.0.dev1-py3-none-any.whl | Python 3 | none | any | Details |
Release files / proofpack-0.1.0.dev1-py3-none-any.whl
| Download URL | proofpack-0.1.0.dev1-py3-none-any.whl |
|---|---|
| Size | 408.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d6277ab0140df7eb130c9264290b79f1f5e7617fb01713fd2fec2f20ad7815ec
|
|
BLAKE2b-256 checksum How to use checksums |
af1fd31d413a1cd47e9bcdbb8f82edfbd5bbf2197d437432d1e69d994325d6a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log