Four things in one release. First, the adoption of the research judgements
that had been delivered and not adopted, which the roadmap's standing
obligation puts before queued work: one Integrity and one Clutter rule
version, one accepted diagnostic under an existing rule, eight accepted exact
vectors and thirty-one cases of three judgements, plus the vector kinds those
cases needed. Second, the review pass of 2026-09-22 (issue #30). Third, the
repairs of the research audits of 2026-09-20 to 2026-09-24, which were
handed off on Drive and had no record in this repository until the review
of 2026-09-25 catalogued them in issue #35. Fourth, the review of
2026-09-30 (issue #48), which found defects in this release's own new code
(among them an Integrity superset that could omit the band it emitted,
replaced by the accepted totality rule) and older ones in verification,
collection, the store and the command line, and filed what needs a decision
as #38 to #47. No threshold, window or gauge changed; the two band rules
that changed did so under accepted judgements, and their rule_id moved
with them.
- Integrity rule
integrity.bands.v1+ci-unit-004. Four accepted
judgements, one rule version, oneRULE_VERSION_BOUNDARYper project.
PV-REV-INTEGRITY-UNKNOWN-001(issue #13): a newest in-scope revision whose
current verdict isUNKNOWNnever inherits an older decisive verdict; the
Vital isUNKNOWNwith no band, the decisive history stays inderived,
andCURRENT_VERDICT_UNKNOWN:<revision>names the cause. Positively
observedNOT_EXECUTEDandNON_VERIFY_TERMINALkeep the accepted
fallback. Before this, four passes and a neweststartup_failureproduced
CLEAN / AVAILABLE / EXACT, and since 0.1.8 a falseIMPROVED.
PV-REV-TEST-003(issue #12 finding 3): a required revision series that is
PARTIALisUNKNOWNwith no band, its counts visible and marked; the
DEGRADEDpath with a fixed three-band tail called a superset is gone.
PV-REV-TEST-VECTORS-002(issue #21): one to three decisive revisions carry
sample_strength = SPARSEandCI_SPARSE_SAMPLE, four or more
ESTABLISHED.PV-INTEGRITY-TOTALITY-001(accepted by
PV-REV-INT-TOTALITY-001, #33): a newest revision still being verified is
spoken for by no older verdict; the history names a band only when it
already satisfies FAILING on its own (four or more decisive revisions, a
quarter or more failed),DEGRADED / FAILING / EXACT, and every other
history isUNKNOWNwith no band, diagnosedCI_CURRENT_VERIFY_UNRESOLVED.
This rule version first carried apossible_bandsderived from the
completions instead; the review of 2026-09-30 found it could omit the band
it emitted (777 of 7,029 shapes, the commonest being one workflow passed
and one still running) and could miss bands a completion reaches, and the
accepted contract defines no reachability algorithm at all. A positively
unconfigured repository with only non-decisive recent records is
UNINSTRUMENTEDwithCI_UNINSTRUMENTED_WITH_RECENT_NONDECISIVE_HISTORY;
an unusable series isUNKNOWNeven beside "not configured"; a verdict
outside the vocabulary isUNKNOWN, never a fallback to an older pass; a
record whose contribution contradicts its history is a defect. Cases
INT-UNKNOWN-01..06,INT-TOTAL-01..06and08..13,T2,R1,R2are
executable vectors. - Pulse diagnoses issue-only activity (
PULSE_ISSUE_ONLY_ACTIVITY,
permanent case T5, issue #22), underpulse.bands.v1as the accepted vector
requires: provenance, not a judgement about productivity. It is emitted
only when every channel was positively observed: with a channel unobserved
or a required enumeration capped, "every observed event came from issues"
would be a claim about evidence nobody has. - Clutter rule
clutter.bands.v1: incomplete evidence is a confirmed burden
floor, never a manufactured band. AdoptsPV-CLUTTER-INCOMPLETE-001
(accepted byPV-REV-CLUTTER-INCOMPLETE-001, casesCLU-INCOMPLETE-01..20,
all executable) and the readingPV-ISSUE-026-RECONCILE-001gave issue
#26. An explicitlyUNAVAILABLEissue or branch component, or aPARTIAL
count with an observed subset, no longer yields a band from the rest: the
band is the floor the observed facts prove. Observed stale work and
classified stale branches prove it; the ratio proves it only over a
complete issue and change-request domain; anUNCLASSIFIEDbranch count
proves nothing. APARTIALcount counts as an observed subset only when
its coverage says so (complete = false,
value_semantics = OBSERVED_SUBSET_COUNT, which the derivation now records
on every count aggregate of an incomplete inventory and on nothing else);
without that proof, with other semantics or with malformed coverage the
component is unresolved and diagnosedCLUTTER_PARTIAL_NOT_TRUSTED
(reviewPV-REV-PR-031-003, casesCLU-PARTIAL-TRUST-01..08; before, any
freshPARTIALvalue was promoted to a floor on its status alone, so a
saved observation set could proveHEAVYwith an estimate).
HEAVYisDEGRADED / EXACT(terminal),CLUTTEREDand
LIGHTareDEGRADEDlower bounds with a conservative superset, and a
floor of nothing isUNKNOWNwith no band. Before, an unavailable
component with nothing else observed producedDEGRADED CLEAN, a band
made from absence; on the fleet's store that is exactly one project, whose
issues are disabled and which has no stale residue: it becomesUNKNOWN,
which is what the evidence supports. The #26 case, a capped branch head
resolution, proves a floor only with explicitCLASSIFIEDretention
semantics (>= 20HEAVY exact,6..19CLUTTERED,1..5LIGHT, zero
UNKNOWN); the GitHub adapter does not emit retention semantics, so on
GitHub that case staysUNKNOWNuntil issue #22 decides whether it should.
Permanent case T7 (issue #22), theUNCLASSIFIEDupper bound with
CLUTTER_BRANCH_PURPOSE_UNCLASSIFIED, moves under the same rule id
unchanged, and when the work items alone reach the band the full count
reaches, that band isDEGRADED / EXACTas the contract's section 5 says.
OneRULE_VERSION_BOUNDARYon Clutter per project; no threshold or window
moved. The GitHub adapter still does not emitretention_semantics:
emittingUNCLASSIFIEDwould make ClutterDEGRADEDfor every project with
a stale branch and take the accepted ordering away from it, so that is a
fleet-wide decision left with issue #22, not a default. - The accepted exact vectors are in the corpus as the research process
wrote them:T2,R1,R2(PV-REV-TEST-VECTORS-002),R4
(PV-REV-TEST-VECTORS-004),R3(PV-REV-TEST-VECTORS-005),T4,T5,T7
(PV-REV-TEST-VECTORS-007). Seven of the seventy named cases without a test
now have one; sixty-three remain. - Two vector kinds and one shape, in answer to the format findings. The
cikind (issue #20) starts at the provider-native normalization: workflow
runs, their earlier attempts and check suites as the provider reports them,
through the outcome map to canonical revision records and, when the case
asks, into Integrity.R5..R10can now be materialized at the boundary they
are about;INT-UNKNOWN-02and03already are. Theactivitykind
carriesACT-COV-01..05ofPV-REV-ACTIVITY-COVERAGE-001(issue #9), the
runtime of which 0.1.7 already had.variantslets one case hold several
evidence shapes to one expectation, forvitalandcicases, which is
the one-identifier multi-variant mechanism T8 and R9 need (issue #23);
vitalcases may also assertshared_signal_groupsand
dependency_group_ids(part of T6). Every kind fails closed as before; a
provider without a normalization is an error, never a skip. - An observation not later than the previous bundle is
INCOMPARABLE
(issue #17,NON_MONOTONIC_OBSERVATION:<previous>-><current>). It was
COMPARABLE, its delta reportedIMPROVEDandWORSENEDwith the
direction inverted while citing the accepted order, andactivity.json
carried an interval that ended before it started. The bundle is still
written and immutable; it claims no direction and no interval, and
build_activityrefuses a backwards interval outright. Where such a bundle
lands is the monotonic-write policy of issue #12 finding 6, still to be
decided. - The comparison reads the immutable bundle the index names, not
latest/(issue #28). A compacted or damaged convenience copy no longer
turns the next run into aHISTORY_GAP; a copy that names a different
bundle than the index is still refused, so finding 6 stays visible. Without
an index the newest immutable bundle is found by scanning. The fleet
surfaces link to the immutable report when the copy is gone. RPT-3 now
corrupts the immutable copy, which is what its sentence always meant. - Verification binds the manifest's metadata to what the identity hashes
(issue #12 finding 4).semantic_config, the field the comparison reads,
must be the projection of the validated stored config
(SEMANTIC_CONFIG_MISMATCH); every identity field the manifest repeats must
be present in both copies and agree with the preimage
(IDENTITY_FIELD_MISMATCH), with the fields a bundle must carry and the
renderers it may name decided by its exact lineage
(UNSUPPORTED_ARTIFACT_LINEAGE,RENDERER_VERSION_NOT_IN_LINEAGE;
PV-AUDIT-MANIFEST-PREIMAGE-BINDING-001, whose concrete case was a bundle
that lost its manifestrenderer_version, kept its id, skipped the report
replay and verified with any report at all); the manifest receipt
must hash tosource_receipts_digestand be the receipt inside
observations.json(RECEIPT_DIGEST_MISMATCH,RECEIPT_COPY_MISMATCH);
andsnapshot.jsonmust name the evidence the bundle carries
(OBSERVATIONS_DIGEST_MISMATCH). A manifest of the wrong shape is a
problem, not anAttributeError. All 308 bundles of the fleet's store
(both lineages, all four renderers) still verify. - Verification holds the whole identity, not only what a forger left in
place (review of 2026-09-30). The preimage must have exactly its
lineage's field set (IDENTITY_PREIMAGE_SHAPE_MISMATCH): without
source_receipts_digestthe receipt binding was simply skipped. Every
member the preimage hashes must be declared (IDENTITY_MEMBER_NOT_DECLARED):
a bundle that droppeddelta.jsonorsnapshot.jsontogether with its
entry kept its id, stopped the replay and verified with any report. The
adaptersline the report prints must be the provider and collector the
identity binds (ADAPTERS_MISMATCH). Andverifyno longer says "report
reproducibility all match" for a report it did not replay: the 74 stored
bundles written by renderers v1 to v3 are bound by their report digest and
are now reported as such; replaying them needs the historical renderers,
which this version does not carry. - Collection no longer claims completeness it did not have (review of
2026-09-30). A filtered/actions/runsquery stops at 1,000 results and
answers the next page empty; that empty page was read as the end, so a busy
repository's oldest runs (and their failures) vanished from anAVAILABLE
series. The listing is now read against the provider'stotal_countand
isPARTIALwhen it ends short. A listing that repeats a commit or a run
between pages (a push during pagination) counts it once and isPARTIAL
withLISTING_SHIFTED; it used to count it twice as complete evidence. A
revision whose check-suite page the budget refused was counted as
examined, which could makeci.configureda positivefalse; it is now
UNKNOWN / SAMPLE_INCOMPLETE. Failures that escaped the inventory boundary
and cost the project its bundle are now declared:http.client
exceptions, a timeout while reading an error body, JSON nested past the
decoder's depth, instants outside the representable range (a register
date of0001-01-01T00:00:00+01:00), register files with non-string
content, nosize, invalid base64 or more bytes than the bound, and run or
suite fields (name,event,html_url,workflow_id,url) of the
wrong type, which failed the canonical encoder. - Workflows, documentation and tests (review of 2026-09-30).
released-pins.ymlno longer runs on push: the file reaches master only
in a release merge, the one moment its pins cannot resolve, and its single
push run (a8726f5, 72 seconds before v0.1.8 was tagged) kept this
repository's own IntegrityFAILINGfor two weeks. The reusable
observe-self.ymlpassesdevostasis-refto the shell through the
environment instead of interpolating it, and trims debt labels without
xargs, which rewrote quotes and backslashes and failed on an apostrophe.
The README's self-observation snippet carries thepermissionsblock it
needs (a called workflow can only narrow the caller's token, so without it
GitHub refuses the run), and deployment.md says that instead of promising
FORBIDDENVitals; its cache snippet keeps the token,idand
continue-on-errorof the step it extends; the store layout names a
.gitattributesso a Windows clone does not break every report digest.
build'sCONFIG_MISMATCHsays which part of the configuration it can
and cannot be passed. The example fleet table, the vector page, debt D-1,
CONTRIBUTING's case families and the--nowparagraph (which promised
what #19 still breaks) say what is true now. The suite no longer sleeps
three real seconds, runs from any working directory, and the pin guards
are case-insensitive; one register assertion that could not fail now can. - Vitals and vectors (review of 2026-09-30). Pulse crashed with
IndexErrorwhen a capped commit enumeration covered all 29 UTC dates a
28-day window touches (the completion grid stopped at 28 and was empty),
which cost the project its bundle; the grid now reaches 29 and includes
the one-event QUIET threshold it missed, and an empty set would be
UNKNOWNasPV-PULSE-REQUIRED-LOWER-BOUND-001says. Debt read a stale
or freshness-unknown partial register as aPRESENTlower bound; it is
UNKNOWN, as vitals.md always said. The vector runner refuses an
expectation that states nothing (an empty object or list, an empty code,
which as a prefix matched every code), and the published vector schema
requiresstatusin its value conditional: without it the schema rejected
39 of the 64 vectors the runner accepts. vitals.md lists all seven rule
ids (it said four Vitals kept their V0 ids; Clutter moved in this
release), and a drift test now reads them from the code. - The store and the command line tell the truth about what they did
(review of 2026-09-30). Without an index, the scan for the newest bundle
skipped a directory whose manifest it could not read, so the next run
compared against an older bundle, or called a store with history
BASELINE; such a directory now makes the state unverified and the
comparison aHISTORY_GAP(INDEXLESS_CANDIDATE_UNREADABLE). A
repository namedindex.jsonwas read as a project index and broke every
identity lookup; index files are now found at their exact depth. Problem
texts carried absolute store paths and exception messages into the delta
of aHISTORY_GAPbundle, so the same damage gave a different bundle id
per checkout (and a local path inside a committed store); they are now
store-relative and name exceptions by type. The fleet surfaces are moved
into place instead of being truncated first.runprints every project's
result before a fleet-surface failure, andbuildits committed bundle; a
conditional cache that cannot be saved is a warning, written after the
fleet surfaces, not a traceback that skips them; andevaluate,build,
render,gauges,demandandactions-summaryanswer a missing or
unreadable input withinput errorand exit 2. - A build over evidence derived under another configuration is refused
(issue #27,CONFIG_MISMATCH).observerecords the digest of the
configuration its aggregates were derived under;buildwith different
planning or debt options produced a verified bundle whose effective config
said one thing and whose snapshot said another. Only a real digest is
compared, so fixtures and examples with placeholders are unaffected. - Check-suite coverage is tracked, paginated and reported (issue #12
finding 1). Suites are read page by page, and the series records how many
revisions were planned and examined, whether every page was read and why
sampling stopped. Past 100 revisions, past 3 pages, after a failed fetch or
a spent budget the series isPARTIAL / CHECK_SUITES_INCOMPLETE; the
parents already collected are kept and an observed failure stays. The 101st
revision and the 101st suite are tests. - The example bundle was regenerated under the new Integrity and Clutter
rules.
The research audits of 2026-09-20 to 2026-09-24, each reproduced here before
it was repaired (the review of 2026-09-25 found twenty-six malformed
successful payloads escaping the collectors with a probe, and the rest by
the audits' own constructions):
- The index tail is bound to this project's history (review of #28's
repair on this branch). The tail is followed only along the canonical
history/YYYY/MM/DD/<bundle_id>path, resolved inside this project's
history/tree, with a basename equal to the id it claims, and the bundle
found there must carry the identity the index records; a damaged index
that names another project's bundle, by a parent reference, an absolute
path or a copy inside the tree, isINDEX_TAIL_INVALIDor
PROJECT_IDENTITY_MISMATCH, aHISTORY_GAPand never a comparison. A
malformed index is a store failure and is never appended to. - The store fails closed on what it reads about itself. An unreadable
index met while proving an immutable id lives nowhere else fails the lookup
instead of passing for absence (PV-AUDIT-HISTORYSTORE-001); an
unreadable or malformed project index, an unfollowable tail or a demand
member that is present but unreadable stops the fleet surfaces instead of
dropping the project, and therunandindexcommands report a store
error (PV-AUDIT-FLEET-INDEX-001,PV-AUDIT-FLEET-COVERAGE-001); a
wrapper whosebundle_idorproject_keydisagrees with its manifest is
refused before any write (PV-AUDIT-STORE-BUNDLE-PATH-BINDING-001,
PV-AUDIT-STORE-PROJECT-BINDING-001). - Publication is recoverable. The index is replaced through a temporary
file, never truncated in place; it is written before the convenience copy,
and the previous copy stays until the new one is in place, so an
interruption leaves a stale copy of an indexed bundle that the next commit
replaces rather than a gap. A copy of a bundle the index does not know, or
one observed after the tail, is still refused
(PV-AUDIT-HISTORYSTORE-ATOMIC-PUBLICATION-001). - A locator is a name, not a path. A project key derives a store path
only as a<forge>/<owner>/<repo>triple of letters, digits, dots, hyphens
and underscores, strictly beneathprojects/, and the configuration admits
only such owner and repository names, soC:\escape/widgetor../xnever
reaches the filesystem (PV-AUDIT-STORE-PATH-001). Two spellings of one
repository are one project in the configuration, and once the provider has
named the repository, in the run: the second is refused with
DUPLICATE_PROJECT_IDENTITYbefore it can collect twice or be mistaken for
a rename (PV-AUDIT-PROJECT-LOCATOR-ALIAS-001).activity.enabledmust be
a boolean andstorean object (PV-AUDIT-CONFIG-SHAPE-001). - Every successful payload is validated before it is read
(PV-AUDIT-GITHUB-REPO/COMMITS/CR/ISSUES/BRANCH/RELEASE/CI-PAYLOAD-001).
The two shapes 0.1.9 already caught were members of a family: every
collector dereferenced its 200 after the boundary that turns failed
requests into observations, so a scalar row, a missing field or an
unreadable timestamp escaped as a Python exception and the project produced
no bundle. Now every consumed field is typed evidence; a body that does not
establish it isERROR / UNEXPECTED_PAYLOADfor that inventory alone, the
other inventories are still collected, and nothing is coerced, defaulted
or skipped: no guessedmainfor a repository without a default branch,
no truthiness of the string"false", no commit or published release
silently omitted for lacking a date, no run skipped for lacking a head, no
check-suite count assumed, and an attempt that names another run or another
number is not this run's history. Repository metadata that does not
establish the routing facts fails the project explicitly. A branch head
whose detail cannot be read stays unresolved,PARTIAL / BRANCH_HEADS_UNRESOLVED, never stale or fresh.
(docs/spec/github-adapter.md has the table.) - A register state outside the vocabulary is an invalid register, not an
open item (PV-AUDIT-REGISTER-STATE-001):clsoed,17orfalseno
longer manufacture an open target or debt item. - A wait hint that cannot be read is no hint (
PV-AUDIT-GITHUB-RETRY-HEADER-001):
Retry-After: infor an overflowing reset epoch used to escape as
OverflowError; the bounded backoff applies and the answer's own
classification stands. - A redirect never carries the token off the API origin
(PV-AUDIT-GITHUB-REDIRECT-AUTH-001).urllibcopiesAuthorizationonto
a redirected request, to any host; the transport now follows redirects only
to the configured API origin and refuses the rest asREDIRECT_REFUSED. A
renamed repository still resolves. - The entity-tag cache is
devostasis.http-cache.v2
(PV-AUDIT-GITHUB-CACHE-INTEGRITY-001): an entry is replayed after a 304
only when its complete shape is readable and its body still hashes to the
digest stored beside the tag; a corrupt entry or a previous cache file is a
miss and one unconditional refetch, never a replayed body and never a
ValueErrorwhile loading. The first fleet run after the upgrade pays a
full quota once. - The canonical decoder is as strict as the writer
(PV-AUDIT-CANONICAL-NONFINITE/DECIMAL/UNICODE/JSON-PARSER-001): decimal
and exponent numbers,NaNand the infinities, an object naming a member
twice, and an unpaired surrogate are refused when read, as a
canonicalization error, instead of becoming a host value that a later
check may or may not catch. All 308 bundles of the fleet's store still
read and verify.
The review pass of 2026-09-22, each defect reproduced before it was fixed:
- A successful response of the wrong shape costs one inventory, not the
project.GET /actions/runsanswering 200 with no body, or
GET /releasesanswering an object instead of a list, escaped as
AttributeError;run_all's boundary caught it, so the project produced no
bundle at all. Both are nowERROR / UNEXPECTED_PAYLOADon that inventory
alone, like every other provider failure, and the rest of the evidence is
still collected. The review of this change (PV-REV-PR-029) found the same
hole one endpoint over:GET /actions/workflowsanswering 200 with a body
that is not an object, or atotal_countthat is not a non-negative
integer, raised past theWORKFLOWS_UNAVAILABLEfallback. The count is now
validated before it is read; a malformed one isUNEXPECTED_PAYLOAD, the
receipt saysWORKFLOWS_UNAVAILABLE:UNEXPECTED_PAYLOAD, check suites are
sampled as for any other failed lookup, and no count is invented. - A failed workflow lookup is no longer silent. When
GET /actions/workflowsfails (a token withoutactions: readis enough)
the collector samples check suites instead, Actions-created suites
included, and the evidence is parent-level. That was correct and invisible:
the receipt saidCI_SURFACE:GITHUB_CHECK_SUITES_SAMPLEDand nothing about
why. It now also carriesWORKFLOWS_UNAVAILABLE:<reason>, mirroring
CHECK_SUITES_UNAVAILABLE. Capability notes are identity-bearing, so a
project in exactly that state gets a new bundle id once; every other bundle
is unchanged. - The build metadata names the setuptools that can read it.
pyproject.tomluses PEP 639 (license = "MIT",license-files) but
required onlysetuptools>=69. setuptools 76 rejects the file
(project.license must be valid exactly by one definition); isolated builds
always fetched a newer setuptools, which is why CI never saw it, and
--no-build-isolationdid. Nowsetuptools>=77. - The documentation described a token order the code never used. The
configuredtoken_envis read first, beforeDEVOSTASIS_GITHUB_TOKEN,
GITHUB_TOKENandGH_TOKEN; the configuration guide and the adapter page
said last. The code was right, since a runner's ownGITHUB_TOKENmust not
override the token a fleet configuration names, and the pages now say so. - Bookkeeping: the README status paragraph still described 0.1.0; the
roadmap listed issue #9 as "filed, not yet indexed" after it had been
answered, and had no row for the accepted judgements this repository has
not adopted (#13, #19, #21, #12 finding 3, the cases of #9). It has one now,
under the standing obligation that names them. Three findings of the same
review pass are filed as issues rather than fixed here, because each is a
rule or a contract decision: ClutterUNKNOWNpast the branch lookup cap
(#26),
devostasis buildaccepting flags that disagree with the receipt
(#27), and
comparability decided from the convenience copylatest/
(#28).