All notable changes to ContextSafe are documented here. The format is based on Keep a Changelog; the project has no tagged release yet, so everything to date lives under Unreleased.
-
B-034 slice:
contextsafe renderandsrc/contextsafe/html_receipt.py, the script-free semantic HTML rendering of a receipt document. The package had no human-facing surface at all before this — every command emitted canonical JSON — which is why B-041 was blocked and why the old i18n declaration could truthfully say "N/A". The page is one self-contained file with no script, no event-handler attribute, no external stylesheet, font, or image, and no network reference; it is deterministic in the receipt document and the catalog, reads no clock and no environment, and a three-environment subprocess test pins byte equality across time zone, locale, and hash seed. Every status carries its word and a distinct symbol rather than a colour, so nothing is lost in black-and-white print or to any colour vision;<main>carriesdata-cs-payload-sha256anddata-cs-case-idso a checker can prove which receipt it examined rather than reporting zero findings against whatever page it was handed. Unpublished enum values, non-boolean scope entries, and any envelope claiming a signature or trusted time are refused rather than printed. B-034 is not closed: this is the receipt surface only, the print stylesheet has had no B-038 evidence-minimization pass, and independent accessibility review remains B-043 and B-044. -
B-041 slice: message catalogs, and the rule that an unreviewed translation says so. Every user-facing string now lives in
src/contextsafe/locales/, andsrc/contextsafe/i18n.pyhands back aMessagecarrying its text and the provenance of its wording — never a bare string — so "we forgot to check whether this was reviewed" is not a reachable state. ASurfacedeclares what it claims about the text it shows, and a surface claiminghuman_reviewedrefuses an unreviewed string by construction. B-042, the professional translation and independent community review, has not happened, so the shippedes-UScatalog is marked machine-translated on every entry and no surface claims review: the rendered page carries the notice in Spanish and in English, marks each string withdata-cs-review, and renders every mandated safety disclosure next to itsen-USoriginal, because a machine translation of "not an approved clinical oracle" is exactly the sentence a reader must not be left alone with. Limitation translations are matched by the source sentence rather than by position, so rewording a mandated limitation drops its translation and says so instead of keeping a stale one. Hash-covered artifacts stay in one fixed language and a test pins that no catalog string reaches one; CLI help is externalized but rendered only in the source locale, because--helpand usage errors are part of the byte surfacetests/test_determinism.pyguards.make i18n(tools/i18n_gate.py, inmake verify) fails on catalog-key drift, placeholder drift, empty or mismarked strings, a review record nobody signed, an unreviewed string reaching a claiming surface, a missing or spurious disclosure, and any visible text on the pseudolocalized page that no catalog message accounts for — which is how "externalize every string" is checked rather than asserted. It also fails, rather than passing, when it has examined no catalog at all. Every rule has a negative control intests/test_i18n.pythat was watched to fail. B-041 is not closed while its only translation is unreviewed;docs/I18N.mdnow records "Partial" and supersedes the 2026-07-16 "N/A" declaration. -
Full-history secret scan (SEC-19):
tools/secret-scan-full-history.sh, run bymake secret-scan, by thesecurityworkflow on every push, pull request, and the weekly schedule, and by the release workflow before anything is built at a tag. Both previous secret scans were diff-scoped — the pre-commit hook sees staged changes, the CI job saw a pull request's commit range — so neither could ever support a claim about the history as a whole. The new gate has three phases: every reachable commit on every ref; every object in the object database, which adds unreachable blobs and every commit message, neither of which phase 1 reads; and the working tree including untracked files. gitleaks is installed from one named release verified against a recorded SHA-256, by a local composite action rather than a wrapper action that would resolve the scanning binary at run time — pinning the action does not pin the ruleset. The script also refuses to run against an unpinned gitleaks version. Scanner choice is recorded in the script's header: TruffleHog's Lob detector has matched ordinarytest_-prefixed pytest function names and then promoted them to "verified" by POSTing them to a third party, and this repository contains five distinct test names of exactly that shape. -
Publication sweep (
tools/publication_sweep.py,make publication-sweep, and part ofmake verify): the readiness audit's employer / private-repo / internal-host / personal-path sweep, made executable. It was run by hand, which made it true of one commit rather than of the repository. The sweep fails on an absolute path out of somebody's machine, a hostname a public reader cannot resolve or should not probe, a pointer to a repository under this owner that is not on the published allowlist, and a relative link that resolves outside the repository — resolved against the containing file's directory, so the pull-request template's parent-relative link to the definition of done is correctly not a finding, while the README's parent-relative pointer at a sibling standards directory was. Reserved names (.invalid,.example,.test,localhost) are never flagged, because this repository uses*.contextsafe.invalidon purpose. Terms that must not appear in the repository or in the scanner, a former employer's name being the obvious one, come from a denylist file outside version control (--denylist,PUBLICATION_SWEEP_DENYLIST) and are reported by rule, file, and line only — never by content. The one exemption mechanism is apublication-sweep: allowmarker on the offending line, so every exemption is greppable.--historyextends the scan to every blob in the object database. 29 tests cover each rule in both directions, plus one that asserts the repository itself sweeps clean. It also runs as its own job in the security workflow, which carries nopaths-ignore:ci.ymlskips docs-only changes by design, and the one gate whose job is documentation hygiene must not be blind to documentation changes. -
Dependency-update automation (SEC-14):
.github/dependabot.ymlcovering the two places this repository pins — theuvlock and the SHA-pinned GitHub Actions — weekly, with a seven-day cooldown on both ecosystems (SEC-26 asks for 72 hours) and Python updates grouped into one pull request. The repository previously had neither adependabot.ymlnor arenovate.json, so no advisory against a locked dependency could open a pull request, nothing kept the action pins current, and OpenSSF Scorecard'sDependency-Update-Toolcheck scored 0 by construction. -
B-039 slice:
tests/test_privacy_canaries.py, the near-miss, log, and crash-output half of the canary suite RG-12 gates on. It pins the privacy boundary in both directions — approved codes that resemble identifiers must not be false positives, values one character from acceptable must fail closed with a named code — and records three identifier-shaped values the pattern scan does not catch (a date outside its 19xx/20xx window, a dotted date, a seven-digit local number) as blind spots for the independent security review rather than as accepted behavior; the synthetic-namespace grammar is what bounds them. It adds a structural log canary (no module importsloggingor prints, and no accepted or rejected command emits a record), a crash canary (an unexpected failure after the boundary read carries neither evidence content nor the caller's source path, and a CLI rejection prints a structured error rather than a traceback), an index canary (raw bytes stay in the content-addressed object; the queryable SQLite index carries hashes, tokens, and provenance only), and a matrix property that no rejection echoes the value that triggered it. No detector, schema, or runtime behavior changes. B-039 is not closed: pattern tuning is a security-owned decision whose independent review has not happened, FHIR/HL7/LIS sources do not exist (B-023–B-025), and the diagnostics, support bundle, and local logs RG-12 also covers are B-046. -
B-021 slice:
tests/test_determinism.py, the three-run reproducibility evidence R-10 and RG-15 ask for and the process half of status-algebra invariant 10. Each shipped command runs three times in fresh interpreters under different time zones, locales, hash seeds, UTF-8 modes, working directories, and input directories, and must produce byte-identical exit codes, stdout, stderr, and--outputartifacts. Every artifact must be one canonical UTF-8 JSON line with one terminal newline and no carriage return, the referenceevaluatedocument has a pinned SHA-256, no absolute input path or environment value may reach an artifact, a caller-declaredclaimed_generated_atmust move the envelope without movingpayload_sha256, and a fail-closed rejection must emit the same stderr bytes and error code every run. A CI matrix (ubuntu-24.04,macos-15,windows-2025) reproduces the pinned digest, and a monkeypatched test pins the documented fail-closed rejection on platforms without descriptor-relative no-follow open — Windows among them, wherepack validate,plan validate, andevidence preflighttherefore cannot run. This is byte-reproducibility evidence only: packaging and fresh-install evidence remain B-045, and B-021 stays open pending normalization (B-019/B-026) and signing (B-035). -
docs/PUBLICATION-READINESS.md: a gate-by-gate audit of whether this repository could ever be made public, with evidence. Gate 0 is the IP/inventions-agreement question created by the repository's creation date falling during prior employment, which only the maintainer's attorney can clear; Gate 1 is a dual-use and misuse assessment specific to this project — a tool that reports where transgender and nonbinary identity data is lost also reports where it is retained — including what the threat model already covers, four things it does not, and what must be decided before B-010. The verdict is technically ready pending IP clearance, not ready to publish. No tag, release, visibility change, or history rewrite accompanies it. -
docs/17-PUBLICATION-POLICY.md: the publication policy the readiness audit said had to exist, written as a decision document rather than an adopted control. It classifies everything this project could publish as method, locator, or instance; states what may be said about a governed pack and what never may (no receipt, customer, vendor, version, or small-population aggregate); names an approval owner with a community co-chair veto and an interim rule that blocks locator material entirely while the maintainer is the only available approver; says what happens to already-published material when a pack lands; and lists the conditions under which the project stops publishing. Its five open decisions carry options and a recommendation — split publication, "publish the judgment, withhold the locator" — and none is in force until the maintainer records a date. -
Publication as a first-class part of the threat model and governance, closing the four dual-use gaps the readiness audit named.
docs/06states the inversion in section 1 rather than deriving it later, adds TB-10 (publication) with its irreversibility called out, adds the reader of public project material, the party using lawful process, and the maintainer publishing under time pressure as actors, and adds T-16 through T-18, two assets, and three residual risks including that withholding locators buys friction rather than secrecy.docs/07gains publication decision rights, a RACI row, HAZ-09 and HAZ-10, a launch gate, and section 14.docs/14splits R-23, which was titled "weaken demand or increase harm" and mitigated only demand: R-23 is now demand alone, R-25 is the harm half at score 15, and R-26 is compelled disclosure.docs/13gates B-009 on an adopted policy,docs/15adds two RG-19 checks, and the README carries the inversion above the quickstart. -
CLI:
contextsafe evidence preflightnow accepts--output, matchingpack validate,plan validate, andevaluate. Previously the only way to obtain the boundary-check result was stdout, so combining--quietwithevidence preflightsilently discarded the command's only output and left nothing but the exit code.--outputwrites the same non-sensitive result document (boundary_check_status, hashes, declared scope —PreflightResultnever carries evidence content) that would otherwise print; it does not change what the command reads, copies, indexes, or logs. -
B-033 slice:
schemas/contextsafe-receipt-v0.1.schema.json, the published contract for the receipt document and its deterministic payload — the pre-1.0 shape of the receipt schema required bydocs/04-ARCHITECTURE.mdsection 8. The contract closes every object (additionalProperties: false), pins the unsigned envelope constants so a signing layer cannot relabel these documents in place, keeps the payload claim-minimal by rejecting timestamp, signature, reviewer, run-environment, and semantic-value fields, pins the mandated limitation set as a closed ordered list so a stripped, reworded, reordered, or padded disclosure fails validation (F-030) and the payload carries no unbounded free-text channel, and publishes closed status, reason, checkpoint, and concept enums. Tests enforce schema/runtime agreement on the reference document, theevaluate --outputartifact, and every Hypothesis-generated bundle; a companion test asserts that every file inschemas/is a valid, self-consistent Draft 2020-12 contract. Outcome reasons are now the typedOutcomeReasonenum, so an unpublished reason string cannot reach a receipt without a schema change. Receipt bytes are unchanged. -
B-027 slice: Hypothesis-based property tests seeding the documented property layer (
docs/09-TEST-AND-EVALUATION.mdsection 2) for the machine-checkable status-algebra invariants — no pass without exactly one affirmative evidence match, not-applicable only from a predeclared rule, fail-closed cross-concept rejection, order-independent byte-identical receipts, and value-minimized receipts that never echo generated semantic values. Invariants needing pack lifecycle, review signatures, HTML, or signature verification remain untested because those components do not exist yet. -
B-020 slice: every CLI command accepts
--quiet(suppress the stdout success payload; exit codes,--outputfiles, and stderr JSON errors unchanged) and--no-color(an explicit pin of the always-plain contract — output never contains ANSI escape sequences), and exit codes are documented and stable:0success,2fail-closed contract rejection,64command-line usage error (previously argparse's default2, which collided with contract rejections). -
V1 planning corpus (
docs/00–16): PRD, service design, architecture, data and evidence model, security/privacy threat model, governance, test strategy, operations, roadmap, backlog, risk register, and release checklist. -
Iteration 1: strict versioned case and observation-set schemas; separately typed GI, RSG, SPCU, name-to-use, and pronoun values; fail-closed cross-concept rejection; pure exact-match evaluator (missing/ambiguous evidence is indeterminate); deterministic value-minimized JSON receipts; offline
validateandevaluateCLI commands with a synthetic reference fixture. -
Iteration 2: strict pack envelope, deterministic unsigned compiler with semantic component hashes and lifecycle/withdrawal checks; strict engagement and execution-plan contracts with fail-closed non-production attestations, host allowlisting, and hash pinning.
-
Iteration 3: canonical JSON evidence boundary envelope with field allowlist, namespace pins, PHI canaries, and direct-identifier checks; read-only
evidence preflight; recoverable two-pass persistence into a SHA-256 object store with an update/delete-protected SQLite index. -
Iteration 4 (B-021 slice): receipt payload/envelope separation.
contextsafe evaluatenow emits a receipt document instead of the bare iteration-1 receipt — the byte-identical deterministic payload pluspayload_sha256over the payload only, and an untrusted envelope with caller-declaredclaimed_generated_at(optional canonical whole-second UTC, via--claimed-generated-at),signature_status: not_signed, andtrusted_time: false. Timestamps and signatures stay outside the deterministic payload (P0-14); no signing or trusted-time path exists. -
Standards-conformance baseline (2026-07-16 sweep): LICENSE (Apache-2.0), SECURITY.md, CONTRIBUTING.md, CITATION.cff, CHANGELOG, pre-commit config, Semgrep/gitleaks/pip-audit security workflow, tag-triggered release workflow, ADR log seed (existing ADRs relocated from
docs/decisions/todocs/adr/), docs/I18N.md declaration, and a README Standards Conformance table.
- Schema identity: five of the eleven published contracts claimed
$idundercontextsafe.dev, a domain nobody had registered. On a public repository an unregistered domain in a contract identity is squattable — whoever buys the name can serve documents at URIs this project publishes as canonical. All eleven now usehttps://contextsafe.invalid/schemas/<file>..invalidis reserved by RFC 2606 and can never be delegated, so the identifiers are stable and unique without depending on anyone owning anything, and nothing here is dereferenced in any case: no code fetches a schema, and every$refis local. The choice and the alternative (register a domain and serve them) are written down in the newschemas/README.md, and a test pins every$idto the reserved domain so a resolvable identity cannot come back by accident. CITATION.cffno longer advertises a release that was never cut. It carrieddate-released: 2026-07-17whilegit tag -lis empty,gh release listreturns nothing, and the repository'slatestReleaseis null. CFF treatsversionanddate-releasedas optional; both return when a release is actually tagged.SECURITY.mdno longer publishes a personal email address as the disclosure channel. It did so for a reason the file stated — private vulnerability reporting is not available on a private repository — and that reason stops applying the moment this repository is public. Reports now go through GitHub private vulnerability reporting, with a details-free public issue as the fallback if that form is unavailable. The setting has to be enabled in repository settings for the link to work; until it is, the form 404s.- Three README claims that had drifted from what the repository does.
ci.ymldoes not run on "every push/PR": it skips docs-only changes by design, and the row now says so and notes that the security workflow has no such skip. The documentation row enumerated a planning corpusdocs/00–16that no longer matches the files on disk. The release row now records that no tag and no release exist, which is the same fact the citation fix above turns on. The "last reviewed" date moved to the date of this review. - The Semgrep SAST gate (SEC-07) had been red on
mainfor every one of its fourteen runs since 2026-07-17, on four blocking findings against the two evidence-index header PRAGMAs inevidence_store.py. SQLite does not accept bound parameters in a PRAGMA, so the statements are now rendered once at module scope from their integer constants with the:dconversion — which can emit only digits and an optional sign — and_publish_new_databaseexecutes those constants instead of building a string at the call site. A new test pins the exact rendered text of both statements, requires each to matchPRAGMA [a-z_]+ = -?\d+, and asserts that SQLite rejects the parameterized form. No waiver,.semgrepignore, or# nosemgrepwas added; the registry auto config now reports 0 findings over 72 targets. Store bytes and the on-disk index header are unchanged. - Command output is written as UTF-8 bytes instead of through a text stream. Text-mode writes translate the terminal newline into the platform line separator and encode with the platform's preferred encoding, so the same receipt would have left a POSIX host and a Windows host with different bytes and different file digests — the cross-platform nondeterminism R-10 names and RG-15 gates. Artifact and payload content is unchanged on POSIX hosts.
CITATION.cffno longer advertises a release that was never cut. It carrieddate-released: 2026-07-17whilegit tag -lis empty and no GitHub release exists. CFF treatsversionanddate-releasedas optional; both return when a release is actually tagged.- The README no longer points readers at a parent-relative sibling standards directory, a path that exists only in the author's local checkout and names a repository a reader cannot open. The standards are now described rather than linked; the conformance table is unchanged. The literal path is deliberately not quoted here: the publication sweep landed in the same batch flags that string in any tracked file, correctly, and a changelog entry describing the removal is not a reason to weaken the rule or to spend its one exemption.
.gitignorecovers.hypothesis/, which was previously ignored only by the nested ignore file Hypothesis generates for itself.- Three links to ADR 0001 pointed at
docs/decisions/, which does not exist; the ADRs live indocs/adr/. Every relative link indocs/and the README now resolves. docs/PUBLICATION-READINESS.mdGate 0 records that the maintainer reviewed the IP question and decided on 2026-08-15 to proceed. The status changed; the findings did not. The audit's recommendation to wait for counsel is kept verbatim alongside the decision that departed from it, because a record that deleted the recommendation once it was overridden would be the less honest document.
- The Semgrep SAST gate (SEC-07) reported a green check on every pull request
while scanning nothing.
semgrep ciresolves a diff baseline on apull_requestevent by runninggit fetch origin --force --depth=1 <head-sha>; this repository is private and the checkout setspersist-credentials: false, so the fetch failed, Semgrep aborted before scanning, and its default--suppress-errorsturned the aborted run into exit 0. The job logs carry the scan-environment banner and the fetch error with no scan summary — no rule count, no target count, no findings line. A HIGH finding introduced by a pull request would have passed the gate. Replaced withsemgrep scan --config auto --error --strict, which needs no baseline and no credential, runs the identical full scan on push, pull request, and schedule, and fails on an analysis error so a scan that cannot run can no longer report success. See ADR 0004.