diff --git a/.devostasis/targets.json b/.devostasis/targets.json index 875d388..72103eb 100644 --- a/.devostasis/targets.json +++ b/.devostasis/targets.json @@ -20,7 +20,7 @@ { "id": "B3", "title": "Durable revision history across bundles", - "state": "open" + "state": "closed" }, { "id": "B4", @@ -45,6 +45,11 @@ { "id": "v0.2", "title": "Release 0.2: phases A and B closed, one outside consumer, no unadopted research unit", + "state": "closed" + }, + { + "id": "v0.3", + "title": "Release 0.3: phases A and B closed, one outside consumer, no unadopted research unit (the bar 0.2.0 shipped before)", "state": "open" }, { diff --git a/.github/workflows/observe-self.yml b/.github/workflows/observe-self.yml index a65361a..c444ea4 100644 --- a/.github/workflows/observe-self.yml +++ b/.github/workflows/observe-self.yml @@ -4,7 +4,7 @@ # # jobs: # vitals: -# uses: drevendev/devostasis/.github/workflows/observe-self.yml@v0.1.9 +# uses: drevendev/devostasis/.github/workflows/observe-self.yml@v0.2.0 # with: # debt-labels: "type:debt" # @@ -24,7 +24,7 @@ on: devostasis-ref: description: Git ref of Devostasis to install (tag, branch or commit) type: string - default: v0.1.9 + default: v0.2.0 planning-source: description: milestones, file or none type: string diff --git a/CHANGELOG.md b/CHANGELOG.md index 9609d56..d200e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,125 @@ All notable changes to this project are documented here. Semantic changes to a contract or a policy always come with a version bump of that contract. +## 0.2.0 (2026-10-01) + +The adoption release. Every accepted research contract about the Vitals and +their history that this repository had not adopted, and that needs no owner +decision, is adopted: the repairs of Direction, Horizon and Debt that issue +#33 listed, the final T9 reconciliation, and target B3, durable Integrity +revision history (`PV-HIST-002`, #34). The owner asked for a large next +release on 2026-09-30 (#47); the candidate specification written for that +request (`PV-RELEASE-020-001`, "Evidence Observatory") has not been accepted +yet, so 0.2.0 is the part of that ambition that rests on accepted contracts. +No threshold, window, gauge constant or demand mapping changed. Four rule ids +moved, each with the judgement that moved it, so the first 0.2.0 bundle of +every project compares `INCOMPARABLE` (`RULE_VERSION_BOUNDARY`) on Horizon, +Direction, Integrity and Debt and stays comparable on the rest. + +- **Direction `direction.bands.v2`: state-neutral linkage, and incomplete + linkage without failing the project.** `PV-REV-DIRECTION-CLOSED-TARGET-001` + (cases `DIR-CLOSED-01..09`): an active change request linked to a declared + target stays linked when the target closes. Calibration finding 9 is + repaired: closing a delivered target used to un-link the work that + delivered it, so never closing targets kept the band higher than finishing + them. Measured on a copy of the fleet store: this repository reads `MIXED`, + 9 of 18 active change requests traced to a declared target, where the + open-target count said 4 of 18, `SCATTERED`. That count is still derived, + for presentation only. `PV-DIRECTION-INCOMPLETE-001` (accepted by + `PV-REV-DIRECTION-INCOMPLETE-001`, cases `DIR-INCOMPLETE-01..16`): every + active change request is `LINKED`, `UNLINKED` or `UNRESOLVED`. A reference + the complete register lacks is a broken reference, unlinked and diagnosed + `DIRECTION_TARGET_REFERENCE_MISSING`; a reference nobody could resolve, or a + title, body or milestone of the wrong type, is unresolved. Over + `L + U + R = N` the completions `k = L..L+R` go through the unchanged + predicates: one reachable band is `DEGRADED / / EXACT`, several are + `UNKNOWN` with the reachable set in `derived`. Unreadable linkage evidence no + longer fails the whole project: `LinkageEvidenceError` is gone, and a change + request names what it could not read in `linkage_unresolved`. A partial + target list no longer blocks Direction when every referenced target resolves + on its own, as a milestone embedded in the change request does. +- **Target markers are standalone tokens** (`PV-AUDIT-TARGET-MARKER-SYNTAX-001`). + `SubTarget: B1`, `NotTarget: B1` and `PreTarget:B1` linked their change + request to B1, because the configured marker was matched as a substring. + The marker now starts the text or follows a character that cannot continue + a word, and a marker ending in a letter needs a boundary after it too. The + audit's regressions `LINK-MARKER-01..08` reached this repository only as + categories; the tests cover each category and say so. +- **Horizon `horizon.bands.v2`: partial target enumeration** + (`PV-HORIZON-PARTIAL-001`, accepted by `PV-REV-HORIZON-PARTIAL-001`, cases + `HOR-PARTIAL-01..16`). A partial enumeration that returned a target proves + the capability; its counts are observed-subset lower bounds with the proof + #39 asked for; only an open target already beyond the frame forces a band, + `DEGRADED / EXTENDED / EXACT`. Every other partial shape stays `UNKNOWN`, + because Horizon declares no order to pick a representative by, and a + partial zero is never `UNDECLARED`. +- **Debt `debt.bands.v2`: partial register** (`PV-DEBT-PARTIAL-001`, accepted + by `PV-REV-DEBT-PARTIAL-001`, cases `DEBT-PARTIAL-01..16`). A confirmed open + item under a partial enumeration is `DEGRADED / PRESENT / EXACT`, the band + exact and the count a lower bound, none confirmed is `UNKNOWN`, and a + `PARTIAL` count is read as a lower bound only with its + `OBSERVED_SUBSET_COUNT` proof (the Debt part of #39). Before, a partial + positive count was `CONSERVATIVE_LOWER_BOUND` on its status alone, so an + estimate could prove `PRESENT`. +- **Integrity `integrity.bands.v1+ci-unit-004+hist-002`: durable revision + history** (`PV-HIST-002`, accepted by `PV-REV-HIST-002`, target B3). History + was reconstructed from whatever the provider still showed, so a failure + hidden behind a passing re-request of a check suite, or dropped by + retention, vanished from the counts. The union of every attempt observed per + parent per revision is now carried from bundle to bundle: a build adds its + immediate predecessor's union as `ci.revision_history_carried`, Integrity + merges it with what the provider shows now and emits the result as + `derived.revision_history`, on every path, so history also crosses a bundle + whose verification evidence could not be read. A missing, unverifiable, + foreign or later predecessor is an explicit gap (`REVISION_HISTORY_GAP`); + nothing older is ever carried in its place, and `verify` checks that the + source a bundle names is the bundle its manifest follows + (`HISTORY_SOURCE_MISMATCH`). Favorable evidence proves `PASS_ONLY_OBSERVED` + only when every attempt of every parent is observed, so a check-suite pass, + or a run whose earlier attempts are not all known, is `UNKNOWN_HISTORY` + (R52), and unknown history makes the Vital `UNKNOWN`: T9 puts it above every + conservative branch. On the stored corpus every active revision is + attempt-complete, so no fleet project loses its band to that rule. The first + 0.2.0 bundle of a project replays the revision records of its 0.1.9 + predecessor (lineage `devostasis.ci-history.v1`) instead of starting empty. + The band table and every 0.1.9 judgement are unchanged. Cases `HIST-01..07`, + `09..11`, `13`, `15`, `16` and `19` are vectors, `08`, `12`, `17`, `18` and + `20` store tests; `HIST-14` has no accepted migration to execute. +- **T9 is adopted** (`PV-TEST-004`, generator `PV-T9-GEN-003`, accepted by + `PV-REV-TEST-004`). With the repairs above, every obligation T9 consumes is + implemented; a finite generator of about 1,270 cells over the nine former + gap families holds each cell to the accepted table. The T9v2 cell-key + vectors are still the research process's to deliver. +- **Contradictory counts are refused as input.** Counts no population can + produce (`L + U + R != N`, a beyond-28-day count above the future-boundary + count) are `InadmissibleEvidence` before classification, as both totality + contracts require, and `evaluate` and `build` report an input error (exit + status 2) instead of a band. +- **The vector format learned two things.** `given.derive` runs the derivation + over stated inventories, so a case about how a change request becomes + linked, or how a partial target list is counted, is executed where that + happens; `expect.rejected` states a refusal (`HOR-PARTIAL-15`). Seventy-one + new vectors: `DIR-CLOSED`, `DIR-INCOMPLETE`, `HOR-PARTIAL`, `DEBT-PARTIAL`, + `HIST`. + +Upgrading: the first 0.2.0 bundle of every project carries four +`RULE_VERSION_BOUNDARY` rows. Durable history starts from the predecessor's +in-window records; after the fleet's pause since 2026-09-10 (#32) none are +left, so the first carried history is effectively empty, and a project whose +predecessor kept no observations member starts with one explained gap. A +saved 0.1.x observation set under file planning recorded a reference outside +the register as `UNKNOWN`; replayed, that change request is unresolved rather +than unlinked, and a fresh observation tells the two apart. The change-request +record gains `linkage_unresolved` and the reference state `MISSING`; the +revision record gains `history_complete`. The snapshot carries the durable +history, stored grouped by parent shape: 91 KB for the busiest project of the +fleet, whose 834 dispatched workflow runs in two weeks #40 already asks about. + +Not in this release: the fifth identity move (#19, the owner's decision), the +Phase C contracts of #34 (after B7 and the compatibility policy), the Pulse +part of #39 (a question for the research side), and the "Evidence +Observatory" candidate of #47, which awaits its review. + ## 0.1.9 (2026-09-30) Four things in one release. First, the adoption of the research judgements diff --git a/README.md b/README.md index d20eb2b..962fc21 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,10 @@ Devostasis is built against those failures: configuration or provider access. - **Anti-gaming rules are part of the contract.** A revision that failed verification keeps its failure in the history window even if a retry later - passes. A zero queue is not "good flow". Full milestone linkage is a fact, - not a compliment. + passes, and even after the provider stops showing the failed attempt: the + history is carried from one bundle to the next. Closing the target a change + delivered does not un-link the change. A zero queue is not "good flow". + Full milestone linkage is a fact, not a compliment. ## The seven Vitals @@ -153,7 +155,7 @@ comes first is the consumer's policy, and no accepted contract defines it. Requires Python 3.12 or newer. The runtime uses the standard library only. ```bash -pip install git+https://github.com/drevendev/devostasis@v0.1.9 +pip install git+https://github.com/drevendev/devostasis@v0.2.0 ``` Observe one repository (a GitHub token is read from `DEVOSTASIS_GITHUB_TOKEN`, @@ -212,7 +214,7 @@ permissions: jobs: vitals: - uses: drevendev/devostasis/.github/workflows/observe-self.yml@v0.1.9 + uses: drevendev/devostasis/.github/workflows/observe-self.yml@v0.2.0 decide: needs: vitals runs-on: ubuntu-latest diff --git a/ROADMAP.md b/ROADMAP.md index 9cfbd9d..0700369 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,7 +13,7 @@ standing obligations (interrupt anything) Phase A observe ourselves honestly closed │ ▼ -Phase B durability and coverage 4 of 7 done +Phase B durability and coverage 5 of 7 done │ ▼ Phase C reach: other providers and instruments @@ -45,7 +45,6 @@ statement; the fix belongs in this repository, not in the rule. | --- | --- | --- | | B1 vectors | research process, then this repository | `PV-TEST-001` is being produced as `PV-TEST-VECTORS-00n` units; their findings are [#20](https://github.com/drevendev/Devostasis/issues/20), [#21](https://github.com/drevendev/Devostasis/issues/21), [#22](https://github.com/drevendev/Devostasis/issues/22) and [#23](https://github.com/drevendev/Devostasis/issues/23), and two of them need vector kinds this repository has not built | | B7 first outside consumer | **the owner** | picking a repository; B5 has landed, so the consumer surface no longer moves | -| B3 durable revision history | **this repository** | `PV-HIST-002` was accepted by `PV-REV-HIST-002` on 2026-09-08 ([#34](https://github.com/drevendev/Devostasis/issues/34)); nothing blocks it but sequencing | | C1 GitLab adapter | **this repository, after B7 and the compatibility policy** | the requirements are accepted (`PV-GITLAB-003` by `PV-REV-GITLAB-003`, [#34](https://github.com/drevendev/Devostasis/issues/34)) | | C2 uncollected GitHub surfaces | this repository | each surface needs a contract decision first | | C3 Instruments | **this repository, after B7 and the compatibility policy** | the envelope, the carrier and four instruments are accepted ([#34](https://github.com/drevendev/Devostasis/issues/34)); the carrier moves the configuration and bundle contracts, which is why the policy comes first | @@ -55,10 +54,10 @@ statement; the fix belongs in this repository, not in the rule. | Compatibility policy | **this repository, then the owner** | `devostasis.contract-compatibility.v1` is accepted (`PV-COMPAT-002` by `PV-REV-COMPAT-002`, [#34](https://github.com/drevendev/Devostasis/issues/34)); adopting it is what makes the Phase C contract moves safe for an adopter | | `PV-CAL-004` predictive validity | elapsed time | needs calendar days of bundles, not more bundles (see the self-review) | | Judgements owed to us | delivered | all four, listed below; one of them is a required repair | -| Accepted judgements not yet adopted | **this repository, and the owner for one** | the Vital repairs of [#33](https://github.com/drevendev/Devostasis/issues/33) (Direction closed-target and incomplete, Debt partial, Horizon partial, T9; Integrity totality was adopted in 0.1.9), the Phase B and C contracts of [#34](https://github.com/drevendev/Devostasis/issues/34), and `PV-REV-RECEIPT-IDENTITY-003` on [#19](https://github.com/drevendev/Devostasis/issues/19), the fifth identity move, which the owner has to want | +| Accepted judgements not yet adopted | **this repository, and the owner for one** | the Phase C contracts of [#34](https://github.com/drevendev/Devostasis/issues/34) (GitLab, instruments, the compatibility policy), and `PV-REV-RECEIPT-IDENTITY-003` on [#19](https://github.com/drevendev/Devostasis/issues/19), the fifth identity move, which the owner has to want. The Vital repairs of [#33](https://github.com/drevendev/Devostasis/issues/33) and B3 are adopted in 0.2.0 | | Audit handoffs | this repository | about forty `REPAIR REQUIRED` static audits since 2026-09-20, catalogued in [#35](https://github.com/drevendev/Devostasis/issues/35); 0.1.9 repairs the store, transport, decoder and payload families, the timestamp and lineage families are open | | Review of 2026-09-30 | this repository, research for four | [#48](https://github.com/drevendev/Devostasis/issues/48): its defects are repaired in 0.1.9; what needs work or a decision is #38 to #46, four of them `for:researcher` (#39, #40, #42, #43) | -| 0.2.0 candidate | research review, then this repository | the owner asked for a large next release on 2026-09-30 and the research process produced a candidate specification (`PV-RELEASE-020-001`, not yet accepted); [#47](https://github.com/drevendev/Devostasis/issues/47) maps its sixteen packages onto the issues here | +| "Evidence Observatory" candidate | research review, then this repository | the owner asked for a large next release on 2026-09-30; 0.2.0 shipped the part of it that rests on accepted contracts, and the candidate specification (`PV-RELEASE-020-001`, not yet accepted) keeps the rest; [#47](https://github.com/drevendev/Devostasis/issues/47) maps its sixteen packages onto the issues here | The four judgements the research process owed this repository about work already shipped have all been delivered: @@ -67,7 +66,7 @@ already shipped have all been delivered: | --- | --- | --- | | `PV-REV-REGISTERS-001` | whether a `Target: ` marker is auditable enough under G7, and whether a bulk-editable register is gameable | ACCEPT (J1..J6, cases REG-01..08): the literal marker satisfies G7; editability stays provenance-visible; no new rule | | `PV-REV-FLEET-001` | whether `devostasis.fleet.v1` is right to declare no cross-project ordering | ACCEPT (FLEET-01..10): `aggregate` and `cross_project_order` stay exactly null; a control plane that routes across repositories owns that policy outside Devostasis | -| `PV-REV-DIRECTION-CLOSED-TARGET-001` | calibration finding 9: closing a delivered target un-links the work that delivered it | REPAIR REQUIRED: Direction linkage must be state-neutral, an active change request linked to a resolvable declared target stays linked when the target closes; versioned Direction rule, cases DIR-CLOSED-01..09; not yet adopted, tracked in [#33](https://github.com/drevendev/Devostasis/issues/33) | +| `PV-REV-DIRECTION-CLOSED-TARGET-001` | calibration finding 9: closing a delivered target un-links the work that delivered it | REPAIR REQUIRED: Direction linkage must be state-neutral, an active change request linked to a resolvable declared target stays linked when the target closes; versioned Direction rule, cases DIR-CLOSED-01..09; adopted in 0.2.0 as `direction.bands.v2` ([#33](https://github.com/drevendev/Devostasis/issues/33)) | | `PV-SPEC-001` | the conformance review of every adoption since the specification was last reviewed, including B5 | two passes on 2026-09-07: the 0.1.8 adoption reused the ORDER identifiers (repaired before the tag, #16); the post-repair pass found the runtime conformant and the public status prose stale, which 0.1.9 reconciles in `PROVENANCE.md` and here | Delivered judgements and their adoption. Under the standing obligation above @@ -83,8 +82,9 @@ an owner decision: | `PV-REV-TEST-VECTORS-004/005/007` | [#22](https://github.com/drevendev/Devostasis/issues/22) | adopted in 0.1.9: `R3`, `R4`, `T4`, `T5`, `T7` vectors; the T5 diagnostic and the T7 upper-bound path in the evaluators; whether the GitHub adapter emits `retention_semantics` is a fleet-wide decision still open there, and since the Clutter adoption it also decides whether a capped branch head resolution can ever prove a floor on GitHub | | `PV-CLUTTER-INCOMPLETE-001`, `PV-ISSUE-026-RECONCILE-001`, `PV-REV-PR-031-003` | [#26](https://github.com/drevendev/Devostasis/issues/26) | adopted in 0.1.9: `clutter.bands.v1`, cases `CLU-INCOMPLETE-01..20` executable; an incomplete component is a confirmed burden floor, never a manufactured band, and a `PARTIAL` count proves one only with its subset proof (`CLU-PARTIAL-TRUST-01..08`) | | `PV-INT-TOTALITY-001` | [#33](https://github.com/drevendev/Devostasis/issues/33) | adopted in 0.1.9 under the same Integrity rule version, in place of the unresolved superset the review of 2026-09-30 found could omit the band it emitted ([#48](https://github.com/drevendev/Devostasis/issues/48)); `INT-TOTAL-01..06` and `08..13` executable | -| `PV-REV-DIRECTION-CLOSED-TARGET-001`, `PV-DEBT-PARTIAL-001`, `PV-HORIZON-PARTIAL-001`, `PV-DIRECTION-INCOMPLETE-001`, `PV-TEST-004` | [#33](https://github.com/drevendev/Devostasis/issues/33) | **not adopted**: accepted repairs of three Vitals and the final T9 reconciliation, none of which had an issue here until the review of 2026-09-25 read the registry; each is a versioned rule adoption with named cases | -| `PV-REV-HIST-002`, `PV-REV-GITLAB-003`, the instrument contracts, `PV-COMPAT-002`, `PV-CONFORMANCE-SURFACE-001`, `PV-RENDER-CLINICAL-001` | [#34](https://github.com/drevendev/Devostasis/issues/34) | **not adopted**: the Phase B and C contracts this roadmap called blocked by research; they are ours now, sequenced after B7 and the compatibility policy | +| `PV-REV-DIRECTION-CLOSED-TARGET-001`, `PV-DEBT-PARTIAL-001`, `PV-HORIZON-PARTIAL-001`, `PV-DIRECTION-INCOMPLETE-001`, `PV-TEST-004` | [#33](https://github.com/drevendev/Devostasis/issues/33) | adopted in 0.2.0: `direction.bands.v2`, `horizon.bands.v2`, `debt.bands.v2`, one rule version each; `DIR-CLOSED-01..09`, `DIR-INCOMPLETE-01..16`, `HOR-PARTIAL-01..16`, `DEBT-PARTIAL-01..16` executable; T9 held by its generator over the nine former gap families | +| `PV-REV-HIST-002` | [#34](https://github.com/drevendev/Devostasis/issues/34) | adopted in 0.2.0 as target B3: `integrity.bands.v1+ci-unit-004+hist-002`, HIST-01..20 executable except HIST-14, which has no accepted migration to execute | +| `PV-REV-GITLAB-003`, the instrument contracts, `PV-COMPAT-002`, `PV-CONFORMANCE-SURFACE-001`, `PV-RENDER-CLINICAL-001` | [#34](https://github.com/drevendev/Devostasis/issues/34) | **not adopted**: the Phase C contracts this roadmap called blocked by research; they are ours now, sequenced after B7 and the compatibility policy | | `PV-REV-RECEIPT-IDENTITY-003` | [#19](https://github.com/drevendev/Devostasis/issues/19) | **not adopted**: the fifth identity move, a fresh receipt, observations and manifest lineage with historical verification dispatch; it needs the owner to want it, and the contract document to implement from | One judgement is ours to ask for rather than to wait on: `ORDER-01..15` are @@ -108,12 +108,12 @@ share for Direction, and its own attention order is actionable. ### B1. Executable conformance vectors — the format is built, the vectors are owed -The specification names **63 conformance cases with no test behind them** -(T3, T6, T8, T9, R5..R53, ART-05, ART-08..ART-11, ART-15, RPT-4..RPT-6, -RPT-9). It named 70 until 0.1.9 adopted the seven exact vectors the research -process has accepted so far (`T2`, `R1`, `R2`, `R3`, `R4`, `T4`, `T5`, `T7`); -the rest are still the research process's to produce, one accepted family per -unit. +The specification names **62 conformance cases with no test behind them** +(T3, T6, T8, R5..R53, ART-05, ART-08..ART-11, ART-15, RPT-4..RPT-6, RPT-9). +It named 70 until 0.1.9 adopted the seven exact vectors the research process +has accepted so far (`T2`, `R1`, `R2`, `R3`, `R4`, `T4`, `T5`, `T7`), and 63 +until 0.2.0 held T9 to its accepted table with a generator; the rest are +still the research process's to produce, one accepted family per unit. The half that was ours shipped in 0.1.8: `devostasis.vectors.v1`, a runner, a `devostasis vectors` command and a published schema @@ -144,17 +144,6 @@ one workflow, its own `GITHUB_TOKEN`, no secret. The point is not the number of adopters but the first feedback from a consumer who did not write the contract. The only remaining blocker is the owner choosing a repository. -### B3. Durable revision history across bundles — accepted by research, ours to build - -Integrity history is reconstructed from what the provider still exposes; -parent-level surfaces cannot prove earlier failures, which is diagnosed as -`HISTORY_PROVENANCE_PARENT_LEVEL_ONLY`. Persisting `revision_history_state` -per revision across bundles closes that gap, with policy provenance and -replay-or-`INCOMPARABLE` on semantic changes. The contract is `PV-HIST-002`, -accepted by `PV-REV-HIST-002` on 2026-09-08 -([#34](https://github.com/drevendev/Devostasis/issues/34)); it changes -Integrity's history source, not the consumer surface, so it can go before -the Phase C contract moves. ### Also open from the reporting review @@ -163,6 +152,15 @@ fixture for the store. Ours, and covered by B1's format. ### Done +- **B3** (0.2.0): durable revision history across bundles, `PV-HIST-002` + (accepted by `PV-REV-HIST-002`). The union of every attempt observed per + parent per revision is carried from the immediate predecessor + (`ci.revision_history_carried` in, `derived.revision_history` out), so a + failure the provider stops showing still counts while its revision is in + the window, and a gap in the chain is explicit rather than bridged by an + older bundle. Parent-level and incomplete favorable evidence is + `UNKNOWN_HISTORY`, never a reconstructed pass (R52). It changed Integrity's + history source, not the consumer surface. - **B5** (0.1.8): the band ordering of `PV-BAND-ORDER-001`, adopted. `delta.json` is `devostasis.delta.v2`, emits `IMPROVED` and `WORSENED` where a Vital declares an order over the pair, and names the ordering it applied. @@ -279,9 +277,12 @@ trustworthy. ## What ends the loop -**0.2 is reached when** Phase A and Phase B are closed, which includes one +**0.2 was defined here as** Phase A and Phase B closed, which includes one consumer outside this repository depending on a bundle, and no accepted -research unit is waiting for adoption. +research unit waiting for adoption. 0.2.0 shipped on 2026-10-01 before that, +at the owner's request for a large release (#47), as the release that adopts +every accepted Vital and history contract. What the definition still asks for, +B7 and the adoption of the Phase C contracts of #34, is now the bar for 0.3. **1.0 is reached when** the contract identifiers have a compatibility policy, a second provider is implemented, and the calibration corpus is large enough @@ -377,15 +378,17 @@ Judged since: 8. **Per-Vital rule version boundary.** Accepted by `PV-REV-RULEBOUNDARY-001`: a Vital whose `rule_id` changed is `INCOMPARABLE` on its own while the bundle stays `COMPARABLE`. +9. **Closing a delivered target un-links the work that delivered it.** + Direction counted links to *open* targets, so completing a target removed + the linkage of the pull requests that delivered it while they were still + in the 28-day window. Measured here: closing A1 and B6 took the linked + count from 2 of 6 to 0. Repaired in `direction.bands.v2` (0.2.0, + `PV-REV-DIRECTION-CLOSED-TARGET-001`): on the same store this repository + reads 9 of 18 active change requests traced, `MIXED`, where the open-target + count said 4 of 18, `SCATTERED`. Open: -9. **Closing a delivered target un-links the work that delivered it.** - Direction counts links to *open* targets, so completing a target removes - the linkage of the pull requests that delivered it while they are still in - the 28-day window. Measured here: closing A1 and B6 took the linked count - from 2 of 6 to 0. Never closing a target would keep the band higher than - finishing the work. Awaiting `PV-REV-DIRECTION-CLOSED-TARGET-001`. 10. **Activity can declare an interval wider than its evidence.** Declared since 0.1.7 with `INTERVAL_EXCEEDS_EVIDENCE_WINDOW`; whether collection should widen instead is [issue #9](https://github.com/drevendev/Devostasis/issues/9). diff --git a/docs/deployment.md b/docs/deployment.md index e76fd4d..9c87208 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -24,7 +24,7 @@ permissions: jobs: vitals: - uses: drevendev/devostasis/.github/workflows/observe-self.yml@v0.1.9 + uses: drevendev/devostasis/.github/workflows/observe-self.yml@v0.2.0 with: debt-labels: "type:debt" # optional: issue labels that mark debt items # planning-source: file # optional: targets register instead of milestones @@ -161,7 +161,7 @@ jobs: with: python-version: "3.12" - name: Install Devostasis - run: python -m pip install --quiet "git+https://github.com/drevendev/devostasis@v0.1.9" + run: python -m pip install --quiet "git+https://github.com/drevendev/devostasis@v0.2.0" - name: Observe every configured project id: run continue-on-error: true diff --git a/docs/spec/PROVENANCE.md b/docs/spec/PROVENANCE.md index fbe8580..e3f8093 100644 --- a/docs/spec/PROVENANCE.md +++ b/docs/spec/PROVENANCE.md @@ -18,6 +18,12 @@ normative here. The process is domain-neutral and documented in the | history store and reports | PV-REPORT-001 | accepted by PV-REV-REPORT-001 with the non-decisions resolved as documented; RPT-4..RPT-7/RPT-9 remain open implementation work | | `PV-BAND-ORDER-001`, `devostasis.band-order.v1` | PV-BAND-ORDER-001, which supersedes the PV-ORDER-001 research item that asked whether an order could be declared at all | accepted, adoption authorized; adopted in 0.1.8 with cases ORDER-01..15, executable as vectors. Ordering is per Vital only. PV-SPEC-001 reviewed the adoption and found the semantics conformant and the case identifiers reassigned; the repair restored ORDER-01..15 to the meanings the contract's `Required conformance cases` section gives them before 0.1.8 was tagged | | `integrity.bands.v1+ci-unit-004` | PV-REV-INTEGRITY-UNKNOWN-001 (issue #13, REPAIR REQUIRED with INT-UNKNOWN-01..06), PV-REV-TEST-003 (a PARTIAL required series is UNKNOWN), PV-REV-TEST-VECTORS-002 (sample_strength and CI_SPARSE_SAMPLE, issue #21) | accepted; adopted in 0.1.9 as one Integrity rule version. PV-INTEGRITY-TOTALITY-001 (accepted by PV-REV-INT-TOTALITY-001, INT-TOTAL-01..14; 01..06 and 08..13 executable, 07 and 14 need a RAW_RUNS path and a GitLab adapter) is part of the same unreleased version: it replaced the unresolved superset this version first derived mechanically for #12 finding 3, which the review of 2026-09-30 found could omit the band it emitted | +| `direction.bands.v2` | PV-REV-DIRECTION-CLOSED-TARGET-001 (calibration finding 9, REPAIR REQUIRED with DIR-CLOSED-01..09), PV-DIRECTION-INCOMPLETE-001 (accepted by PV-REV-DIRECTION-INCOMPLETE-001, DIR-INCOMPLETE-01..16) | accepted; adopted in 0.2.0 as one Direction rule version: state-neutral linkage, LINKED/UNLINKED/UNRESOLVED per change request and the completion-set rule; the open-target count stays derived for presentation only. The acceptance of PV-DIRECTION-INCOMPLETE-001 narrows DIR-CLOSED-04: a positively missing target is unlinked, an unresolvable one unresolved; unreadable linkage evidence no longer fails the project | +| `horizon.bands.v2` | PV-HORIZON-PARTIAL-001 (accepted by PV-REV-HORIZON-PARTIAL-001, HOR-PARTIAL-01..16) | accepted; adopted in 0.2.0: over a partial target enumeration a singleton completion set is `DEGRADED / / EXACT` and anything wider `UNKNOWN`; the counts carry the observed-subset proof of PV-REV-PR-031-003 | +| `debt.bands.v2` | PV-DEBT-PARTIAL-001 (accepted by PV-REV-DEBT-PARTIAL-001, DEBT-PARTIAL-01..16); the Debt part of issue #39 | accepted; adopted in 0.2.0: a confirmed open item under partial acquisition is `DEGRADED / PRESENT / EXACT`, none confirmed is `UNKNOWN`; a PARTIAL count is read only with its subset proof. The Pulse part of #39 waits for the research answer it asks for | +| target marker grammar | PV-AUDIT-TARGET-MARKER-SYNTAX-001 (REPAIR REQUIRED, regressions LINK-MARKER-01..08 known only by category) | repaired in 0.2.0 in the adapter; no Vital rule changes for it, the Direction rule version it lands under moves for the repairs above | +| `integrity.bands.v1+ci-unit-004+hist-002`, `devostasis.ci-history.v2` | PV-HIST-002 (accepted by PV-REV-HIST-002, HIST-01..20; supersedes the PV-HIST-001 candidate judged CHANGES_REQUIRED by PV-REV-HIST-001) | accepted; adopted in 0.2.0 as target B3: the per-revision union of observed attempts is carried from the immediate predecessor (`ci.revision_history_carried` in, `derived.revision_history` out), R52's completeness rule makes favorable incomplete evidence `UNKNOWN_HISTORY`, and 0.1.x revision records are replayed as lineage `devostasis.ci-history.v1`. HIST-14 has no accepted migration to execute; the GitLab variant of HIST-11 waits for C1 | +| T9 (`PV-T9-GEN-003`) | PV-TEST-004 (accepted by PV-REV-TEST-004): the final reconciliation of the decision-equivalence generator, zero CONTRACT_GAP terminals | adopted in 0.2.0: the five repair families it consumes are all implemented, and a finite generator over the nine former gap families holds every cell to the accepted obligation; the T9v2 cell-key vectors are PV-TEST-VECTORS-00n's to deliver | | `PV-REV-ACTIVITY-COVERAGE-001` | the judgement on issue #9 | accepted reading 3, runtime unchanged since 0.1.7; ACT-COV-01..05 adopted in 0.1.9 as `activity` vectors | | `clutter.bands.v1`, `PV-CLUTTER-INCOMPLETE-001` | PV-CLUTTER-INCOMPLETE-001 (accepted by PV-REV-CLUTTER-INCOMPLETE-001 with CLU-INCOMPLETE-01..20), PV-ISSUE-026-RECONCILE-001 (the reading of issue #26 under that contract), PV-REV-PR-031-003 (a PARTIAL count proves a floor only with machine-verifiable subset semantics, CLU-PARTIAL-TRUST-01..08) | accepted; adopted in 0.1.9 as one Clutter rule version with the twenty cases executable; the T7 upper-bound path moved under the same rule id; the subset proof (`value_semantics = OBSERVED_SUBSET_COUNT` in a PARTIAL count's coverage) is part of the same rule version, which 0.1.9 introduces and no stored bundle carries yet | | exact vector families T2/R1/R2, R3, R4, T4/T5/T7 | PV-TEST-VECTORS-002/005/004/007, accepted by PV-REV-TEST-VECTORS-002/005/004/007 | transcribed into `tests/vectors/` in 0.1.9 as the accepted expectations; T5 (`PULSE_ISSUE_ONLY_ACTIVITY`) adopted under `pulse.bands.v1` as the accepted vector requires; T7 (`retention_semantics = UNCLASSIFIED` upper bound) adopted under `clutter.bands.v1`, because the Clutter rule had to move for PV-CLUTTER-INCOMPLETE-001 in the same release and the accepted T7 semantics are unchanged | @@ -26,7 +32,7 @@ normative here. The process is domain-neutral and documented in the | `devostasis.demand.v2` | PV-ROLE-001 (minimal consumer demand interface); v1 defined by the implementation, v2 removes the cross-Vital gauge tie-break required by ROLE-01 | default table, UNRESOLVED semantics and aggregate-free projection accepted by PV-ROLE-001; the v2 repair was found conformant by PV-SPEC-001; predictive validity under calibration in PV-CAL-004, whose measurement the owner reassigned to the implementation side | | `devostasis.fleet.v1` | implementation-defined projection submitted for judgement | accepted by PV-REV-FLEET-001 (FLEET-01..10): `aggregate` and `cross_project_order` stay exactly null, no accepted contract defines a cross-project order | | `devostasis.ci-outcomes.github.v1` | implementation choice within PV-CI-NORM-001 | accepted by PV-CAL-003 (timed_out is VERIFY_FAIL, startup_failure is UNKNOWN) | -| `devostasis.targets.v1`, `devostasis.debt.v1`, register link marker | implementation choice under the explicit-register allowance of the V1 taxonomy | accepted by PV-REV-REGISTERS-001 (J1..J6, cases REG-01..08): the literal marker satisfies G7 and bulk editability stays provenance-visible; calibration finding 9 (a delivered target's closure un-links the work that delivered it) was judged separately by PV-REV-DIRECTION-CLOSED-TARGET-001 as REPAIR REQUIRED, a state-neutral Direction linkage with a versioned rule id and cases DIR-CLOSED-01..09, not yet adopted | +| `devostasis.targets.v1`, `devostasis.debt.v1`, register link marker | implementation choice under the explicit-register allowance of the V1 taxonomy | accepted by PV-REV-REGISTERS-001 (J1..J6, cases REG-01..08): the literal marker satisfies G7 and bulk editability stays provenance-visible; calibration finding 9 (a delivered target's closure un-links the work that delivered it) was judged separately by PV-REV-DIRECTION-CLOSED-TARGET-001 as REPAIR REQUIRED and is repaired in 0.2.0 by `direction.bands.v2`; REG-04 ("a marker referencing a closed target is not linked-to-open-target") still holds for the presentation count and no longer decides the band | | `devostasis.render.v4`, `devostasis.canon.v1`, `devostasis.effective-config.v2` | implementation choices within the artifact contract | local | Implementation choices the contracts left open, with their disposition after @@ -38,7 +44,7 @@ PV-CAL-002 and PV-CAL-003: the literal Flow rule with an empty queue (repaired, workflow (confirmed as the evidence-faithful reading); Pulse on bursty solo work (open observation, no rule change); Pulse on capped required enumerations (repaired, `pulse.bands.v1`); reconstruction of Integrity history -from provider-exposed attempts only (open, PV-HIST-001). +from provider-exposed attempts only (repaired in 0.2.0, PV-HIST-002). The research process continues independently. Changes flow into this repository as explicit adoption changes that name the unit they adopt; diff --git a/docs/spec/bundle.md b/docs/spec/bundle.md index 78ce32a..990bbb1 100644 --- a/docs/spec/bundle.md +++ b/docs/spec/bundle.md @@ -154,7 +154,10 @@ directory and performs, in this order: hashes to `source_receipts_digest` (`RECEIPT_DIGEST_MISMATCH`), the receipt inside `observations.json` is that receipt (`RECEIPT_COPY_MISMATCH`), `snapshot.json` names the evidence the bundle - carries (`OBSERVATIONS_DIGEST_MISMATCH`), and `semantic_config`, the field + carries (`OBSERVATIONS_DIGEST_MISMATCH`), the durable revision history the + Integrity result and `observations.json` name was carried from the bundle + the manifest follows (`HISTORY_SOURCE_MISMATCH`, PV-HIST-002), and + `semantic_config`, the field the comparison reads, is exactly the projection of the validated stored config under its schema (`SEMANTIC_CONFIG_MISMATCH`). A manifest that is not an object, or whose `members`, `receipt`, `identity_preimage` or diff --git a/docs/spec/conformance.md b/docs/spec/conformance.md index 22daf68..cd648c2 100644 --- a/docs/spec/conformance.md +++ b/docs/spec/conformance.md @@ -40,7 +40,8 @@ citation resolves; both directions are a test (`test_spec_drift.py`). ## Integrity: the newest revision and the sample (PV-REV-INTEGRITY-UNKNOWN-001, PV-REV-TEST-003, PV-REV-TEST-VECTORS-002) -Rule `integrity.bands.v1+ci-unit-004`, adopted in 0.1.9. The six cases of the +Rule `integrity.bands.v1+ci-unit-004`, adopted in 0.1.9; since 0.2.0 the same +cases hold under `integrity.bands.v1+ci-unit-004+hist-002`. The six cases of the accepted judgement on issue #13 are executable; two of them start at the provider-native normalization, because their subject is that `startup_failure` and an unsupported conclusion reach the Vital as `UNKNOWN`. @@ -57,9 +58,44 @@ and an unsupported conclusion reach the Vital as `UNKNOWN`. | sample strength | one to three decisive revisions are SPARSE with `CI_SPARSE_SAMPLE`; four are ESTABLISHED | `test_sparse_samples_declare_their_strength_and_established_ones_do_not_carry_the_diagnostic` | | unresolved newest revision | over every history, a newest revision still being verified yields FAILING exactly where the established FAILING predicate already holds and no band elsewhere; the superset this rule version first carried, which could omit the band it emitted, is gone (#12 finding 3, PV-INTEGRITY-TOTALITY-001) | `test_an_unresolved_newest_revision_never_emits_a_band_its_history_does_not_already_prove`, `test_current_unresolved_is_unknown_unless_failing_is_already_established`, `test_a_newest_revision_with_one_workflow_passed_and_one_running_is_already_counted_and_still_unresolved` | | verdict vocabulary | a current verdict outside the canonical vocabulary is read as UNKNOWN, never as a non-decisive state an older pass speaks for | `test_a_verdict_outside_the_vocabulary_fails_closed_instead_of_falling_back_to_an_older_pass` | -| record consistency | a record whose contribution contradicts its own history state is a defect, not a pass | `test_a_record_whose_contribution_contradicts_its_history_is_a_defect_not_a_pass` | +| record consistency | a record whose contribution contradicts its own history state, or that names no revision, is a defect, not a pass | `test_a_record_whose_contribution_contradicts_its_history_is_a_defect_not_a_pass`, `test_a_record_without_its_revision_identity_is_a_defect_not_a_crash` | | unusable series | an unusable required series is UNKNOWN even beside a positive `ci.configured = false` (precedence A of PV-INTEGRITY-TOTALITY-001) | `test_an_unusable_series_is_unknown_even_beside_a_positive_not_configured` | +## Integrity: durable revision history (PV-HIST-002) + +Rule `integrity.bands.v1+ci-unit-004+hist-002`, adopted in 0.2.0 (target B3). +The reconciliation cases state the carried history as the observation a build +adds (`ci.revision_history_carried`) and are vectors; the cases about the +chain, which bundle is the source and what a lost one means, need a history +store and are tests. + +| Case | Meaning | Test | +| --- | --- | --- | +| HIST-01 | a revision a prior bundle proved failing stays FAILURE_OBSERVED when the provider now shows the same revision passing; the current verdict is PASS | `vector:HIST-01` | +| HIST-02 | a revision the prior bundle recorded PASS_ONLY_OBSERVED becomes FAILURE_OBSERVED when a failure of it is newly observed, and stays there | `vector:HIST-02` | +| HIST-03 | a prior failure plus any number of same-revision retries that passed is one failed contribution, whatever the number or order of the retries | `vector:HIST-03` | +| HIST-04 | a failed revision and a newer passing revision are two samples while both are in the window | `vector:HIST-04` | +| HIST-05 | a failure the provider no longer exposes stays historical evidence while its revision is in the window | `vector:HIST-05` | +| HIST-06 | without durable history a parent-level pass cannot prove the earlier attempts: UNKNOWN_HISTORY, no favorable PASS_ONLY reconstruction | `vector:HIST-06`, `test_parent_level_provenance_is_diagnosed_and_a_favorable_one_is_unknown_history` | +| HIST-07 | complete attempt-level current evidence with no unresolved prior gap proves PASS_ONLY_OBSERVED | `vector:HIST-07` | +| HIST-08 | a required predecessor that is missing or unverifiable is a HISTORY_GAP: nothing is carried through it | `test_hist_20_and_hist_08_a_lost_predecessor_is_a_gap_and_nothing_older_is_carried` | +| HIST-09 | the same failure in durable history and in the current enumeration is one failed contribution, never two | `vector:HIST-09` | +| HIST-10 | a revision whose durable failure has aged beyond the 14-day window no longer contributes | `vector:HIST-10` | +| HIST-11 | equivalent failure histories on the Actions and the check-suite surfaces, in any enumeration order, give the identical record and result | `vector:HIST-11` | +| HIST-12 | an unrelated rule or configuration change keeps the durable Integrity history | `test_hist_12_an_unrelated_vital_rule_change_keeps_the_durable_history` | +| HIST-13 | carried history of the older lineage is replayed from the attempts it kept, never copied from the state it derived: its failure stays, its parent-level pass is unknown | `vector:HIST-13`, `test_hist_13_the_first_bundle_after_an_older_version_replays_its_revision_records` | +| HIST-14 | a history-semantics change with an accepted deterministic migration records the migration | not executable: no migration has been accepted, so the mechanism accepts none and a foreign lineage takes the HIST-15 path | +| HIST-15 | carried history of a lineage with neither replay nor an accepted migration leaves the revisions it names unknown, until complete current evidence repairs them | `vector:HIST-15` | +| HIST-16 | a prior bundle's current verdict is never inherited: the revision the provider no longer shows keeps its history and speaks for nothing current | `vector:HIST-16` | +| HIST-17 | the same current observation over different durable histories is different canonical evidence and a different bundle | `test_hist_17_different_durable_history_is_different_canonical_evidence` | +| HIST-18 | history of another immutable project at the same locator is never carried | `test_hist_18_another_project_at_the_same_locator_is_never_carried` | +| HIST-19 | a prior UNKNOWN_HISTORY does not heal because the provider now shows a pass that is complete only for what is visible | `vector:HIST-19`, `test_hist_19_complete_evidence_repairs_an_unknown_history` (the nearby repair) | +| HIST-20 | B1 pass, B2 same-revision failure, B3 pass again: B3 carries from B2 and keeps the failure; with B2 lost it is a gap, never a fallback to B1 | `test_hist_20_the_immediate_predecessor_carries_a_failure_an_older_bundle_did_not_see`, `test_hist_20_and_hist_08_a_lost_predecessor_is_a_gap_and_nothing_older_is_carried` | +| parent-level failure | an observed failure is proven on any surface; only favorable parent-level evidence is unknown | `test_a_parent_level_failure_is_still_a_proven_failure` | +| continuity | the history crosses a bundle whose verification evidence was unusable | `test_the_durable_history_crosses_a_bundle_whose_verification_evidence_was_unusable` | +| source is the predecessor | a build refuses observations that carry history from another bundle, leaves the caller's set untouched, and `verify` names a source that is not the previous bundle | `test_a_build_refuses_observations_that_carry_history_from_another_predecessor`, `test_a_build_leaves_the_callers_observation_set_as_it_was`, `test_verify_names_carried_history_that_is_not_from_the_previous_bundle`, `test_every_stored_bundle_verifies_with_its_carried_history` | +| corrupt carried history | carried history that is not its lineage's shape fails closed | `test_carried_history_that_is_not_its_lineages_shape_fails_closed` | + ## Integrity: totality (PV-INTEGRITY-TOTALITY-001) Accepted by `PV-REV-INT-TOTALITY-001` and adopted in 0.1.9 under the same @@ -124,6 +160,145 @@ identifier. Cases 13 to 16 are the accepted answer to issue #26. | CLU-PARTIAL-TRUST-08 | complete AVAILABLE/FRESH evidence and the optional UNAVAILABLE path are unchanged | `test_clu_partial_trust_08_complete_and_optional_unavailable_evidence_are_unchanged` | | invariant band under unclassified branches | when the work items alone reach the band the full unclassified count reaches, the band is DEGRADED and EXACT (section 5 of the contract) | `test_clutter_unclassified_branches_that_the_work_items_already_reach_are_an_invariant_band` | +## Direction: state-neutral and incomplete linkage (PV-REV-DIRECTION-CLOSED-TARGET-001, PV-DIRECTION-INCOMPLETE-001) + +Rule `direction.bands.v2`, adopted in 0.2.0 as one Direction rule version for +both repairs. The cases are transcribed from the judgement's and the +contract's required fixtures. A case about how one change request's evidence +becomes LINKED, UNLINKED or UNRESOLVED states the change-request and target +inventories and runs the derivation (`given.derive`); a case about the +completion rule states N, L, U and R. PV-REV-DIRECTION-INCOMPLETE-001 narrowed +DIR-CLOSED-04's "unlinked or unresolved": a positively missing target is +UNLINKED (DIR-CLOSED-04, DIR-INCOMPLETE-07), an unresolvable one UNRESOLVED +(DIR-INCOMPLETE-08, -09). + +| Case | Meaning | Test | +| --- | --- | --- | +| DIR-CLOSED-01 | an active change request with an explicit link to an open target is linked | `vector:DIR-CLOSED-01` | +| DIR-CLOSED-02 | the same change request and target id stay linked when the target moves from open to closed; the linkage share is unchanged | `vector:DIR-CLOSED-02`, `test_change_request_aggregates` | +| DIR-CLOSED-03 | a link to a closed target is linked when the target identity resolves authoritatively | `vector:DIR-CLOSED-03`, `test_a_closed_register_target_is_still_a_resolved_reference` | +| DIR-CLOSED-04 | a reference to a target the complete register positively lacks is unlinked with the missing-reference diagnostic; linkage is never fabricated | `vector:DIR-CLOSED-04` | +| DIR-CLOSED-05 | closing a target cannot by itself worsen the Direction band | `vector:DIR-CLOSED-05` | +| DIR-CLOSED-06 | reopening a target cannot by itself improve the Direction band | `vector:DIR-CLOSED-06` | +| DIR-CLOSED-07 | Horizon's open-target counts still exclude the closed target a change request stays linked to, so the two Vitals may diverge | `vector:DIR-CLOSED-07` | +| DIR-CLOSED-08 | every active change request linked to one closed target is FULLY_LINKED with the single-target diagnostic: traceability, not alignment | `vector:DIR-CLOSED-08` | +| DIR-CLOSED-09 | adopting the repair changes the Direction rule id: the first comparison across it is INCOMPARABLE for Direction alone, while an unaffected Vital still compares | `vector:DIR-CLOSED-09` | + +| Case | Meaning | Test | +| --- | --- | --- | +| DIR-INCOMPLETE-01 | N=5, L=0, U=3, R=2: every completion k in {0,1,2} is SCATTERED | `vector:DIR-INCOMPLETE-01` | +| DIR-INCOMPLETE-02 | N=5, L=3, U=1, R=1: every completion k in {3,4} is MIXED | `vector:DIR-INCOMPLETE-02` | +| DIR-INCOMPLETE-03 | N=4, L=2, U=0, R=2: completions reach MIXED and FULLY_LINKED, so no band | `vector:DIR-INCOMPLETE-03` | +| DIR-INCOMPLETE-04 | N=5, L=1, U=1, R=3: completions reach SCATTERED and MIXED, so no band | `vector:DIR-INCOMPLETE-04` | +| DIR-INCOMPLETE-05 | N=1, L=0, U=0, R=1: completions reach SCATTERED and FULLY_LINKED; never DEGRADED FULLY_LINKED | `vector:DIR-INCOMPLETE-05` | +| DIR-INCOMPLETE-06 | a marker to an authoritatively resolved closed target is LINKED; closing or reopening the target alone does not change the linkage | `vector:DIR-INCOMPLETE-06` | +| DIR-INCOMPLETE-07 | a marker to T that a complete authoritative lookup proves absent is UNLINKED: N=1, L=0, U=1, R=0 is exactly SCATTERED with the missing-reference diagnostic | `vector:DIR-INCOMPLETE-07`, `test_a_reference_the_complete_register_lacks_is_missing_and_one_the_register_could_not_answer_is_unknown` | +| DIR-INCOMPLETE-08 | a marker to T while the target list is PARTIAL and T is not in the observed subset, with no direct lookup, is UNRESOLVED: no band | `vector:DIR-INCOMPLETE-08` | +| DIR-INCOMPLETE-09 | a marker whose target resolution is unavailable, forbidden, unknown, errored or stale is UNRESOLVED, never LINKED by fallback | `vector:DIR-INCOMPLETE-09`, `test_unreadable_linkage_evidence_is_unresolved_not_unlinked_and_not_a_failed_project`, `test_a_milestone_without_a_number_is_unresolved_linkage_not_a_failed_project` | +| DIR-INCOMPLETE-10 | a PARTIAL global target enumeration does not degrade Direction when every referenced target is resolved on its own | `vector:DIR-INCOMPLETE-10` | +| DIR-INCOMPLETE-11 | partial link evidence leaves two change requests unresolved, but N/L/U/R forces SCATTERED for every completion | `vector:DIR-INCOMPLETE-11` | +| DIR-INCOMPLETE-12 | partial link evidence admits MIXED and FULLY_LINKED, so no band | `vector:DIR-INCOMPLETE-12` | +| DIR-INCOMPLETE-13 | linkage capability that is neither positively present nor absent is UNKNOWN with no band | `vector:DIR-INCOMPLETE-13` | +| DIR-INCOMPLETE-14 | an active-change inventory that is PARTIAL leaves N unproven: UNKNOWN with no band | `vector:DIR-INCOMPLETE-14` | +| DIR-INCOMPLETE-15 | reversed and reshuffled enumeration, page boundaries, marker order and target order leave L/U/R, the reachable bands, the status, the band and the diagnostics unchanged | `vector:DIR-INCOMPLETE-15` | +| DIR-INCOMPLETE-16 | several active change requests all linked to one authoritatively resolved closed target, with complete evidence, are exactly FULLY_LINKED: neutral, no concentration threshold | `vector:DIR-INCOMPLETE-16` | +| readable link dominates | a resolved reference links a change request even when another of its linkage fields is unreadable | `test_a_readable_link_is_not_undone_by_an_unreadable_field` | +| contradictory counts | `L + U + R != N` is refused before classification, and the command line reports it as an input error | `vector:HOR-PARTIAL-15` (the Horizon analogue), `test_observations_whose_counts_contradict_each_other_are_an_input_error` | + +## Target marker syntax (PV-AUDIT-TARGET-MARKER-SYNTAX-001) + +The audit's regressions `LINK-MARKER-01..08` reached this repository only as +the categories its handoff names; the tests below cover each category and do +not claim the individual case texts. + +| Category | Meaning | Test | +| --- | --- | --- | +| exact marker | a standalone marker links, at the start of the text, after a newline, a space, a tab or punctuation | `test_an_exact_marker_links_wherever_it_stands_alone` | +| embedded prefix | `NotTarget:`, `SubTarget:`, `PreTarget:` and any marker glued to a preceding word character link nothing | `test_a_marker_embedded_in_a_larger_token_links_nothing`, `test_a_marker_ending_in_a_word_character_needs_a_boundary_after_it_too` | +| custom marker | a configured marker replaces the default and is matched exactly | `test_a_custom_marker_replaces_the_default_and_is_matched_exactly` | +| multiple markers | several markers keep their order and link each target once | `test_several_markers_keep_their_order_and_link_each_target_once` | +| prose without the marker | a lowercase, pluralised or paraphrased marker links nothing | `test_prose_without_the_configured_marker_links_nothing` | +| end to end | under file planning only the standalone marker becomes a reference, and Direction counts only it | `test_embedded_markers_do_not_link_end_to_end` | + +## Horizon: partial enumeration (PV-HORIZON-PARTIAL-001) + +Rule `horizon.bands.v2`, adopted in 0.2.0. The sixteen cases of the accepted +contract, stated as target inventories and derived, so the counts, their +observed-subset proof and the capability a returned target proves are executed +where they are made. HOR-PARTIAL-15 states the contradictory counts directly +and expects their refusal (`expect.rejected`). + +| Case | Meaning | Test | +| --- | --- | --- | +| HOR-PARTIAL-01 | capability present, complete and fresh, zero open targets: exactly UNDECLARED (the positive-zero control) | `vector:HOR-PARTIAL-01` | +| HOR-PARTIAL-02 | complete and fresh, one open target with no future boundary: exactly DECLARED | `vector:HOR-PARTIAL-02` | +| HOR-PARTIAL-03 | complete and fresh, an open target exactly 28 days out and none later: exactly VISIBLE; the frame is unchanged | `vector:HOR-PARTIAL-03` | +| HOR-PARTIAL-04 | complete and fresh, an open target strictly beyond 28 days: exactly EXTENDED | `vector:HOR-PARTIAL-04` | +| HOR-PARTIAL-05 | explicit targets positively unsupported and no planning register configured: exactly UNDECLARED, a descriptive state, not missing evidence | `vector:HOR-PARTIAL-05` | +| HOR-PARTIAL-06 | capability unknown, forbidden, errored, stale, unavailable or an unresolved partial: UNKNOWN, never UNDECLARED by default | `vector:HOR-PARTIAL-06` | +| HOR-PARTIAL-07 | PH0: a partial enumeration returned no open target, so every band is reachable: UNKNOWN, never UNDECLARED | `vector:HOR-PARTIAL-07` | +| HOR-PARTIAL-08 | PH1: returned open targets without a future boundary reach DECLARED, VISIBLE and EXTENDED: UNKNOWN, DECLARED is never a representative | `vector:HOR-PARTIAL-08` | +| HOR-PARTIAL-09 | PH2: a returned open target within or exactly at 28 days reaches VISIBLE and EXTENDED: UNKNOWN, VISIBLE is never a bound | `vector:HOR-PARTIAL-09` | +| HOR-PARTIAL-10 | PH3: a returned open target strictly beyond 28 days forces EXTENDED in every completion: DEGRADED / EXTENDED / EXACT | `vector:HOR-PARTIAL-10` | +| HOR-PARTIAL-11 | a returned closed target beyond 28 days and an open target without a boundary: the closed one forces nothing, PH1, UNKNOWN | `vector:HOR-PARTIAL-11` | +| HOR-PARTIAL-12 | only closed returned targets, one beyond 28 days: PH0, UNKNOWN, never UNDECLARED or EXTENDED | `vector:HOR-PARTIAL-12` | +| HOR-PARTIAL-13 | the same PH3 evidence in reversed order, other page boundaries and an equivalent provider-neutral record shape: identical DEGRADED / EXTENDED / EXACT | `vector:HOR-PARTIAL-13` | +| HOR-PARTIAL-14 | the same PH2 subset under record and page permutations: identical UNKNOWN, pagination order cannot manufacture exactness | `vector:HOR-PARTIAL-14` | +| HOR-PARTIAL-15 | a derived tuple no target population can produce (beyond above future, future above open) fails validation before evaluation, never UNKNOWN or DEGRADED compensation | `vector:HOR-PARTIAL-15` | +| HOR-PARTIAL-16 | PH3 is the direct proof: whatever else the returned subset holds, and whatever the omitted records are, the observed open target beyond 28 days keeps every completion EXTENDED | `vector:HOR-PARTIAL-16` | + +## Debt: partial register (PV-DEBT-PARTIAL-001) + +Rule `debt.bands.v2`, adopted in 0.2.0 together with the observed-subset proof +issue #39 asked Debt to require: a PARTIAL open count is a lower bound only +when its coverage says so. + +| Case | Meaning | Test | +| --- | --- | --- | +| DEBT-PARTIAL-01 | a partial enumeration with no confirmed open item cannot manufacture CLEAR | `vector:DEBT-PARTIAL-01` | +| DEBT-PARTIAL-02 | a returned subset of closed debt items only is still not CLEAR: the closed count is a diagnostic, omitted open items stay possible | `vector:DEBT-PARTIAL-02` | +| DEBT-PARTIAL-03 | one confirmed open item forces PRESENT: DEGRADED / PRESENT / EXACT with the partial receipt retained | `vector:DEBT-PARTIAL-03` | +| DEBT-PARTIAL-04 | thirty-seven confirmed open items remain PRESENT, never ACCUMULATED or any quantitative severity | `vector:DEBT-PARTIAL-04` | +| DEBT-PARTIAL-05 | the size of the omitted tail cannot erase a confirmed open item: two different partial receipts give the same band | `vector:DEBT-PARTIAL-05` | +| DEBT-PARTIAL-06 | provider naming, item order, page order and page boundaries do not change the normalized result | `vector:DEBT-PARTIAL-06` | +| DEBT-PARTIAL-07 | positively absent debt authority stays exactly UNINSTRUMENTED; the repair does not turn positive absence into UNKNOWN | `vector:DEBT-PARTIAL-07` | +| DEBT-PARTIAL-08 | uncertain mapping authority is never inferred from a partial payload that happens to hold an apparent open debt record | `vector:DEBT-PARTIAL-08` | +| DEBT-PARTIAL-09 | a complete, fresh positive zero remains exactly CLEAR | `vector:DEBT-PARTIAL-09` | +| DEBT-PARTIAL-10 | a complete, fresh positive open count remains exactly PRESENT | `vector:DEBT-PARTIAL-10` | +| DEBT-PARTIAL-11 | a partial zero stays UNKNOWN beside one hundred closed items: closed history cannot prove current open emptiness | `vector:DEBT-PARTIAL-11` | +| DEBT-PARTIAL-12 | a forced PRESENT under partial acquisition stays visibly DEGRADED, never AVAILABLE, with the acquisition machine-visible | `vector:DEBT-PARTIAL-12` | +| DEBT-PARTIAL-13 | a stale positive count is not a confirmed current open item: UNKNOWN, as before the repair | `vector:DEBT-PARTIAL-13` | +| DEBT-PARTIAL-14 | one confirmed current open item survives other returned or tail records that stay unresolved under the partial receipt | `vector:DEBT-PARTIAL-14` | +| DEBT-PARTIAL-15 | a versioned project-local quantitative policy does not replace the canonical core under partial positive evidence | `vector:DEBT-PARTIAL-15` | +| DEBT-PARTIAL-16 | a project-local policy cannot turn a partial zero into any canonical conclusion | `vector:DEBT-PARTIAL-16` | +| subset proof | a PARTIAL open count whose coverage is missing, not a subset, an estimate, contradictory or malformed is no lower bound, and Debt is UNKNOWN (issue #39) | `test_a_partial_debt_count_without_the_subset_proof_is_not_a_lower_bound` | +| PD1 in the tests | a confirmed open item under partial acquisition is DEGRADED / PRESENT / EXACT, a stale one proves nothing | `test_partial_register_with_observed_items_is_degraded_present`, `test_a_stale_partial_register_proves_nothing_about_now` | + +## T9: the decision-equivalence reconciliation (PV-TEST-004, PV-T9-GEN-003) + +`PV-REV-TEST-004` accepted the final T9 reconciliation with zero +`CONTRACT_GAP` terminals: each of the nine gap identifiers of `PV-T9-GEN-002` +maps to one deterministic accepted obligation. 0.2.0 adopts the last of the +repairs those obligations come from, so the implementation side of T9 is a +finite generator over the acquisition tokens and the regions each family +names, which evaluates every cell and holds it to the accepted table +(`tests/conformance/test_t9_reconciliation.py`, about 1,270 cells). The T9v2 +cell-key serialization itself is the research process's to deliver as +vectors. + +| Gap identifier | Accepted obligation | Test | +| --- | --- | --- | +| T9-GAP-HORIZON-PARTIAL-BOUND-001 | PH0..PH2 `UNKNOWN`, PH3 `DEGRADED / EXTENDED / EXACT` (PV-HORIZON-PARTIAL-001) | `test_t9_gap_horizon_partial_bound_001` | +| T9-GAP-CLUTTER-UNAVAILABLE-COMPONENT-001, T9-GAP-CLUTTER-PARTIAL-FORCED-BOUND-001 | a confirmed floor from positive facts only, never `CLEAN` under incomplete coverage (PV-CLUTTER-INCOMPLETE-001) | `test_t9_gap_clutter_unavailable_component_and_partial_forced_bound` | +| T9-GAP-DIRECTION-PARTIAL-LINKAGE-001 | the completion set over k in L..L+R (PV-DIRECTION-INCOMPLETE-001) | `test_t9_gap_direction_partial_linkage_001` | +| T9-GAP-DIRECTION-UNRESOLVED-TARGET-001 | resolved identity LINKED open or closed, positive absence UNLINKED, the rest UNRESOLVED | `test_t9_gap_direction_unresolved_target_001` | +| T9-GAP-INTEGRITY-UNRESOLVED-BAND-001 | `DEGRADED / FAILING / EXACT` only over an established FAILING history, else `UNKNOWN` (PV-INTEGRITY-TOTALITY-001) | `test_t9_gap_integrity_unresolved_band_001` | +| T9-GAP-INTEGRITY-PARTIAL-SUPERSET-001 | the false gap: a PARTIAL required series is `UNKNOWN`, no `possible_bands` (PV-REV-TEST-003) | `test_t9_gap_integrity_partial_superset_001` | +| T9-GAP-INTEGRITY-UNCONFIGURED-RECENT-001 | `UNINSTRUMENTED` with the non-decisive history kept visible | `test_t9_gap_integrity_unconfigured_recent_001` | +| T9-GAP-DEBT-PARTIAL-BOUND-001 | confirmed open `DEGRADED / PRESENT / EXACT`, none confirmed `UNKNOWN` (PV-DEBT-PARTIAL-001) | `test_t9_gap_debt_partial_bound_001` | +| section 4 precedence | unknown history outranks every conservative branch | `test_t9_section_4_unknown_history_outranks_the_unresolved_failing_branch` | +| finiteness | nine identifiers, none without cells | `test_t9_generator_covers_every_family` | + ## Activity coverage (PV-REV-ACTIVITY-COVERAGE-001) Reading 3 of issue #9, accepted: the interval stays the full canonical gap and @@ -357,11 +532,14 @@ accepted case. | corpus | every vector in the corpus runs, and every declared kind is exercised | `test_conformance_vector`, `test_every_kind_the_format_declares_is_exercised_by_the_corpus` | | variants | one case over several evidence shapes passes only when every shape does, and a failure names the shape | `test_variants_hold_every_evidence_shape_to_the_one_expectation` | | ci kind | provider-native outcomes are normalized before Integrity sees them; a provider without a normalization is rejected | `test_the_ci_kind_normalizes_provider_native_outcomes_before_integrity_sees_them` | +| derive | a vital case over inventories runs the derivation first, under the two settings it reads, and no others | `test_derive_runs_the_derivation_before_the_vital_is_evaluated`, `test_derive_names_only_the_settings_the_derivation_reads` | +| rejected | a case expecting refused evidence passes only when the evaluation raises with that code, and the refusal is its whole expectation | `test_a_rejected_case_passes_only_when_the_evidence_is_refused_with_that_code`, `test_rejected_is_the_whole_expectation_and_names_a_code` | | activity kind | the interval and the coverage notes of the activity member are checked | `test_the_activity_kind_checks_the_interval_and_the_coverage_it_discloses` | -Cases not yet implemented as tests (T3, T6, T8, T9, R5..R53, ART-05, +Cases not yet implemented as tests (T3, T6, T8, R5..R53, ART-05, ART-08..ART-11, ART-15, RPT-4..RPT-6, RPT-9) are listed in the ROADMAP under the -synthetic fixture suite. The research process delivers them as executable JSON +synthetic fixture suite; T9 is implemented by its generator since 0.2.0, and +its cell-key vectors are still the research process's to deliver. The research process delivers them as executable JSON vectors in the format of [vectors.md](vectors.md), one accepted family per unit; the ones accepted so far (T2, R1, R2, R3, R4, T4, T5, T7) are in the corpus above, and so are the twenty cases of `PV-CLUTTER-INCOMPLETE-001`, diff --git a/docs/spec/github-adapter.md b/docs/spec/github-adapter.md index 96f8aab..51ddb1a 100644 --- a/docs/spec/github-adapter.md +++ b/docs/spec/github-adapter.md @@ -63,7 +63,7 @@ place (research audits `PV-AUDIT-GITHUB-*-PAYLOAD-001`). Concretely: | --- | --- | | repository metadata | an object with an integer `id`, a non-empty `default_branch`, boolean `has_issues` and `archived`, and `pushed_at` null or a timestamp; a body that does not establish them fails the project (`CollectionError`) rather than guessing `main` or reading a string's truthiness | | commits | every row an object with a non-empty `sha`, an object `commit`, object or null `committer` and `author`, and at least one readable date; a commit without a date is not skipped | -| change requests | every row an object with an integer `number`, readable `created_at` and `updated_at`, null or readable `merged_at` and `closed_at`, boolean `draft`, object or null `user`; the window predicate itself validates `updated_at`; linkage-bearing `title`, `body` and `milestone` keep their own fail-closed rule (`LinkageEvidenceError`) | +| change requests | every row an object with an integer `number`, readable `created_at` and `updated_at`, null or readable `merged_at` and `closed_at`, boolean `draft`, object or null `user`; the window predicate itself validates `updated_at`; linkage-bearing `title`, `body` and `milestone` of the wrong type are not an absent link and not a failed project: the record keeps what could be read and names the rest in `linkage_unresolved`, and Direction counts the change request unresolved unless a readable reference already links it (`PV-DIRECTION-INCOMPLETE-001`) | | issues | every row an object with an integer `number`, readable timestamps, a list of objects with string `name` as `labels`, object or null `user` | | branches | every row an object with a non-empty `name`, an object `commit` with a non-empty `sha`, boolean `protected`; a head detail that cannot be read leaves that head unresolved (`PARTIAL / BRANCH_HEADS_UNRESOLVED`), never stale or fresh | | milestones | every row an object with an integer `number`, null or readable `due_on`, integer or null `open_issues` and `closed_issues` | diff --git a/docs/spec/history-and-reports.md b/docs/spec/history-and-reports.md index 2b2a56a..ace4508 100644 --- a/docs/spec/history-and-reports.md +++ b/docs/spec/history-and-reports.md @@ -132,6 +132,12 @@ still refused (`latest pointer ... differs from index tail`). `HISTORY_GAP` therefore means what its definition says: the previous immutable bundle cannot be loaded or verified. +The same predecessor is the source of the durable Integrity revision +history a build carries (PV-HIST-002): the bundle the comparison resolves, +never an older one, so a `HISTORY_GAP` is also a gap in that history and says +so (`REVISION_HISTORY_GAP`); see +[integrity-ci.md](integrity-ci.md#durable-history-pv-hist-002). + A bundle is routed by what its canonical members say: a wrapper whose `bundle_id` or `project_key` disagrees with its own manifest is refused before anything is written. A locator derives a store path only as a diff --git a/docs/spec/integrity-ci.md b/docs/spec/integrity-ci.md index 99a6250..c36829e 100644 --- a/docs/spec/integrity-ci.md +++ b/docs/spec/integrity-ci.md @@ -45,12 +45,17 @@ Per revision, two independent facts are recorded: NON_VERIFY_TERMINAL > NOT_EXECUTED`. An unresolved sibling therefore blocks a pass but never hides an observed failure. 2. **History state** is derived from every attempt observed for the revision: - `FAILURE_OBSERVED` if any attempt was `VERIFY_FAIL`, else - `PASS_ONLY_OBSERVED` if any was `VERIFY_PASS`, else `NO_DECISIVE_OBSERVED`. + `FAILURE_OBSERVED` if any attempt was `VERIFY_FAIL`; otherwise, only when + the history is complete (every parent an Actions run whose attempts + 1..latest are all observed), `PASS_ONLY_OBSERVED` if any was `VERIFY_PASS`, + else `NO_DECISIVE_OBSERVED`; and `UNKNOWN_HISTORY` when it is not complete, + because a check suite keeps its id across re-requests and a missing attempt + may have failed (R52, `history_complete` in the record). `FAILURE_OBSERVED` is absorbing for the life of the revision in the window. The **historical contribution** is `VERIFY_FAIL` for `FAILURE_OBSERVED`, -`VERIFY_PASS` for `PASS_ONLY_OBSERVED`, none otherwise. Exactly one +`VERIFY_PASS` for `PASS_ONLY_OBSERVED`, none otherwise (`UNKNOWN_HISTORY` +included: it is neither a pass nor an empty decisive set). Exactly one contribution per revision enters `decisive_count_14d` and `failed_count_14d`; attempts are never independent samples, and retry count or order cannot improve history (rule `ANY_FAIL_ELSE_ANY_PASS_PER_IMMUTABLE_REVISION`). @@ -85,17 +90,76 @@ reading against a real repository. "current_verdict": "VERIFY_PASS", "history_state": "FAILURE_OBSERVED", "historical_contribution": "VERIFY_FAIL", - "history_provenance": "ATTEMPT_LEVEL" + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true } ``` -## Known limitation of this version - -History is reconstructed from what the provider still exposes at collection -time. Attempt-level surfaces (Actions) preserve failures; parent-level -surfaces (check suites) do not, which is diagnosed as -`HISTORY_PROVENANCE_PARENT_LEVEL_ONLY`. Persisting revision history across -bundles is on the roadmap (PV-HIST-001). +## Durable history (PV-HIST-002) + +A provider forgets: a re-requested check suite shows only its latest +outcome, and retention removes old runs. Since 0.2.0 what one bundle proved +about a revision is carried into the next one (PV-HIST-002, accepted by +PV-REV-HIST-002, target B3), under the history-semantics lineage +`devostasis.ci-history.v2`. + +**The carrier** is the union of every attempt observed per parent per +revision: `{revision, committed_at, history_state, historical_contribution, +history_complete, parent_groups: [{kind, latest_attempt, attempts: +[{attempt, state}], parent_ids}]}`. Parents of one shape are stored once +with their ids, which loses nothing and keeps the carrier small where a busy +default branch attaches hundreds of single-attempt runs to one revision (the +most active project of the fleet: 91 KB of snapshot instead of 393 KB). +Each bundle's union is its predecessor's union plus what the provider shows +now, so it holds every history fact of the chain and selects none (the +complete-chain-equivalent reduction of section C). Records that have aged +out of the window are not carried. It lives in two canonical places: + +- `derived.revision_history` of the Integrity result in `snapshot.json`, + `{lineage, source, records}`: the durable output, present on every + evaluation path, `UNKNOWN` ones included, and whatever the bundle's + observations setting; only revisions that carry history are in it; +- `ci.revision_history_carried`, the observation a build adds before + evaluating, from its immediate predecessor: `AVAILABLE` with `{lineage, + records, source_bundle_id, source_observed_at, basis}`, `UNAVAILABLE / + NO_PREVIOUS_BUNDLE` for a first bundle, `UNKNOWN / HISTORY_GAP` when the + predecessor cannot be read or verified, `UNKNOWN / + PREDECESSOR_NOT_EARLIER` when it is not earlier, `UNKNOWN / + PREDECESSOR_CARRIES_NO_HISTORY` when it keeps neither a carrier nor its + observations. + +**The source** is the verified immediately previous bundle of the same +immutable project, the one the comparison resolves, never an older one: when +that bundle is missing, unverifiable or belongs to another project, nothing +is carried and the gap is explicit (`REVISION_HISTORY_GAP`, HIST-08, -18, +-20). `verify` checks that the source a bundle names is the bundle its +manifest follows (`HISTORY_SOURCE_MISMATCH`), and a build refuses +observations that already carry history from another bundle +(`HISTORY_SOURCE_NOT_PREDECESSOR`). + +**Reconciliation**, per revision of the current 14-day inventory: the carried +union and the provider's current parents are merged by parent identity, and +the state is a function of the union alone. An observed `VERIFY_FAIL` is +`FAILURE_OBSERVED` from either side and absorbing (HIST-01, -02, -05, -09); +favorable evidence is `PASS_ONLY_OBSERVED` only when the union is complete; +a prior `UNKNOWN_HISTORY` does not heal from a later pass that is complete +only for what is visible (HIST-19), and heals when every attempt is observed. +Retries never add samples (HIST-03), a newer revision is a distinct one +(HIST-04), a revision that ages out of the window stops contributing +(HIST-10), and a revision the complete inventory no longer holds leaves it. +When the current series is partial or unusable, carried records are kept, so +history crosses a bundle whose verification evidence could not be read. A +current verdict is never carried (HIST-16). + +**Lineage.** A carried union of `devostasis.ci-history.v2` is read as it is. +The revision records a 0.1.x bundle keeps in `ci.revision_verdicts_14d` +(`devostasis.ci-history.v1`) are replayed from the attempts they kept, never +copied from the state they derived (HIST-13): the first 0.2.0 bundle of a +project carries the history of its 0.1.9 predecessor this way. Any other +lineage has neither a replay nor an accepted migration (HIST-14 has nothing +to execute): its revisions are `UNKNOWN_HISTORY` until complete current +evidence repairs them (HIST-15), and the old bundle keeps its record as +historical evidence. ## The newest revision @@ -120,7 +184,9 @@ R1 and R2 vectors, PV-REV-TEST-VECTORS-002). The strength is emitted in ## Conformance cases implemented R54 same-revision retry keeps historical failure; R55 retry-count invariance; -R56 newer revision is distinct; R57 order and surface invariance; the V0.7 +R56 newer revision is distinct; R57 order and surface invariance; HIST-01..20 +of PV-HIST-002 except HIST-14, which has no accepted migration to execute; +the V0.7 composition precedence; CI-OUTCOME-01..04 outcome mapping; unresolved current verification degrades instead of claiming `CLEAN`; INT-UNKNOWN-01..06; T2, R1 and R2 as vectors ([conformance.md](conformance.md)). The `ci` vector kind diff --git a/docs/spec/observations.md b/docs/spec/observations.md index 216b955..52d23f9 100644 --- a/docs/spec/observations.md +++ b/docs/spec/observations.md @@ -65,7 +65,7 @@ Inventories (emitted by adapters): | --- | --- | --- | | `forge.repository.metadata` | record | id, default branch, visibility, `has_issues`, archived, pushed_at | | `git.default_branch.commits_28d` | series | `{sha, committed_at, title}` for the 28-day window; coverage `complete` | -| `forge.change_requests.inventory` | series | all open change requests plus those updated in 28 days; coverage `open_complete`, `window_complete` | +| `forge.change_requests.inventory` | series | all open change requests plus those updated in 28 days; coverage `open_complete`, `window_complete`. Each record carries `target_refs`, the explicit target references with the state their resolution established (`OPEN` or `CLOSED` when the declared target resolves, `MISSING` when a complete register positively lacks the id, `UNKNOWN` when nothing could resolve it), and `linkage_unresolved`, the linkage fields that could not be read (`body:dict`, `milestone:NO_NUMBER`) | | `forge.issues.inventory` | series | all open issues plus those updated in 28 days; `UNAVAILABLE` when issues are disabled | | `git.nondefault_branches.inventory` | series | `{name, head_sha, head_committed_at, protected}`; coverage `complete`, `heads_resolved` | | `git.nondefault_branches.retention_semantics` | enum | whether the purpose of the non-default branches is known: `UNCLASSIFIED` makes a stale count an upper bound on residue ([vitals.md](vitals.md#clutter), case T7). Optional; the GitHub adapter does not emit it in this version | @@ -73,6 +73,7 @@ Inventories (emitted by adapters): | `forge.releases.inventory` | series | the 30 most recent published releases; a full page is `PARTIAL`, because the provider had at least that many | | `ci.configured` | boolean | positively observed presence or absence of verification | | `ci.revision_verdicts_14d` | series | one canonical record per default-branch revision of the 14-day window (see [integrity-ci.md](integrity-ci.md)); coverage `runs_complete`, `attempts_complete`, `suites_complete`, `suite_revisions_planned`, `suite_revisions_examined`, `suites_stop_reason`, `surface` | +| `ci.revision_history_carried` | record | added by a build, not a collector: the durable revision history of the immediate predecessor bundle, or why there is none ([integrity-ci.md](integrity-ci.md#durable-history-pv-hist-002)) | Aggregates (derived deterministically from inventories; status follows the relevant coverage flag): @@ -83,8 +84,8 @@ relevant coverage flag): | `forge.change_requests.open_count`, `.merged_count_28d`, `.updated_count_28d`, `.stale_open_count_14d`, `.oldest_open_age_days` (when open > 0), `.median_time_to_merge_seconds_28d` (when merged > 0; exact rational record `{"numerator": n, "denominator": d}` in seconds), `.median_time_to_merge_hours_28d` (whole-hour projection of the former, presentation only) | change requests | | `forge.issues.open_count`, `.stale_open_count_30d`, `.updated_count_28d` | issues | | `git.nondefault_branches.stale_count_30d` | branches (heads older than 30 days) | -| `planning.explicit_targets.capability` (`SUPPORTED`, `SUPPORTED_UNUSED`, `UNSUPPORTED`), `.open_count`, `.open_with_future_boundary_count`, `.open_beyond_28d_count`, `.nearest_future_boundary_days` | targets and configuration | -| `planning.linkage.active_change_requests_count_28d`, `.active_change_requests_linked_to_open_target_count_28d`, `.links_per_target_28d` | change requests with milestone linkage | +| `planning.explicit_targets.capability` (`SUPPORTED`, `SUPPORTED_UNUSED`, `UNSUPPORTED`), `.open_count`, `.open_with_future_boundary_count`, `.open_beyond_28d_count`, `.nearest_future_boundary_days` | targets and configuration; a fresh partial enumeration that returned a target proves `SUPPORTED` (`coverage.basis = RETURNED_TARGET_RECORD`), and its counts are observed-subset lower bounds | +| `planning.linkage.active_change_requests_count_28d`, `.active_change_requests_linked_count_28d`, `.active_change_requests_unlinked_count_28d`, `.active_change_requests_unresolved_count_28d`, `.unresolved_change_requests_28d` (record: change request number to its unresolved reasons), `.links_per_target_28d` (resolved references per target, open or closed), `.unknown_target_reference_count_28d`, `.missing_target_reference_count_28d`, `.active_change_requests_linked_to_open_target_count_28d` (presentation only since `direction.bands.v2`) | change requests with their target references | | `debt.registry.capability` (`CONFIGURED`, `UNCONFIGURED`), `debt.mapping`, `debt.items.open_count`, `.open_stale_count_30d`, `.closed_count_28d` | issues filtered by the configured label mapping | A count aggregate derived from an incomplete inventory is `PARTIAL`, and its @@ -94,15 +95,19 @@ enumeration did return, which the records it did not return can only increase. That declaration is what lets a Vital read a `PARTIAL` value as a lower bound (`PV-REV-PR-031-003`); `PARTIAL` alone never does. Every count above carries it; `oldest_open_age_days`, the two medians and -`links_per_target_28d` never do, because a maximum, a median or a record of a +`links_per_target_28d` and `unresolved_change_requests_28d` never do, because +a maximum, a median or a record of a subset is not a lower bound of anything a Vital classifies. A complete aggregate's coverage is unchanged, so this declaration moves no bundle identity built over complete evidence. Definitions: "stale" means open and not updated for the stated number of days; "active" change requests are those updated within the 28-day frame; "linked" -means the change request carries an explicit reference to an open planning -target; merge durations are computed exactly from the timestamps (fractional +means the change request carries an explicit reference to a declared planning +target that resolves, open or closed (state-neutral since +`direction.bands.v2`), "unlinked" that it carries none or only references a +complete register positively lacks, and "unresolved" that its linkage +evidence could not be read or a reference could not be resolved; merge durations are computed exactly from the timestamps (fractional seconds of an RFC 3339 timestamp are kept as exact decimal fractions, never rounded through binary floats), the "median" over an odd sample is the middle duration and over an even sample the exact arithmetic mean of the two middle diff --git a/docs/spec/registers.md b/docs/spec/registers.md index cf08e65..40bf96e 100644 --- a/docs/spec/registers.md +++ b/docs/spec/registers.md @@ -29,10 +29,23 @@ Configuration: `"planning": {"source": "file", "path": "devostasis/targets.json" `DECLARED`; with a date within 28 days `VISIBLE`; beyond, `EXTENDED`. A change request is **linked** to a target when its title or body contains -the marker followed by the target id, for example a line `Target: T-3`. Several -markers link to several targets. A reference to an id that is not in the -register counts as unlinked and is reported in -`planning.linkage.unknown_target_reference_count_28d`. +the marker followed by the target id, for example a line `Target: T-3`. The +marker is a token of its own, matched exactly, case included: it starts the +text or follows a character that cannot continue a word, so `SubTarget: T-3`, +`NotTarget: T-3` and `PreTarget:T-3` link nothing +(`PV-AUDIT-TARGET-MARKER-SYNTAX-001`); a marker that ends in a letter or digit +also needs a boundary after it. Several markers link to several targets, in +order, each once. + +Linkage is state-neutral: a closed target is still the target the work was +declared against, so a change request that references it stays linked +(`PV-REV-DIRECTION-CLOSED-TARGET-001`). Keep closed targets in the file for as +long as recent work references them. A reference to an id the register does +not contain is a broken reference: the change request counts as unlinked, +`planning.linkage.missing_target_reference_count_28d` reports it, and +Direction carries `DIRECTION_TARGET_REFERENCE_MISSING`. When the register +itself cannot be read, nothing about the id is known, so the change request is +unresolved rather than unlinked (`PV-DIRECTION-INCOMPLETE-001`). A missing file is `UNAVAILABLE / REGISTER_NOT_FOUND` and makes Horizon and Direction `UNKNOWN` (the mechanism is configured but absent). An invalid file diff --git a/docs/spec/vectors.md b/docs/spec/vectors.md index 262ef46..4098cbb 100644 --- a/docs/spec/vectors.md +++ b/docs/spec/vectors.md @@ -11,7 +11,8 @@ cases with no test behind them (debt D-1, target B1). Since 0.1.9 the accepted exact vectors of the research process are in the corpus (seven of those 70, with sixty-three still to come), beside the accepted families transcribed from their contracts (`ORDER`, `INT-UNKNOWN`, `INT-TOTAL`, `CLU-INCOMPLETE`, -`ACT-COV`). Authored against this format a case arrives executable instead of +`ACT-COV`, and since 0.2.0 `DIR-CLOSED`, `DIR-INCOMPLETE`, `HOR-PARTIAL`, +`DEBT-PARTIAL`, `HIST`). Authored against this format a case arrives executable instead of needing translation. ## A vector file @@ -81,6 +82,40 @@ list of at least two `{title, observations}` entries, all held to the one failure names the shape by its `title`. `variants` replaces `observations`; stating both is an error. +A case about how inventories become aggregates states the inventories and +`given.derive`: the provider-neutral derivation (`normalize.derive`) runs +over the envelopes before the Vital is evaluated, under the two project +settings it reads, `planning_source` (`none`, `milestones` or `file`) and +`debt_mapping` (the explicit mapping object, or `null`). Aggregates the +envelopes already state are kept, as the derivation keeps them for a saved +observation set. The Direction, Horizon and Debt families of 0.2.0 use it, +so a change request linked to a closed target or a partial target list is +executed where the linkage and the counts are made, not over counts +somebody derived by hand. + +```json +{"case": "EXAMPLE-DERIVE-01", "kind": "vital", + "title": "a returned open target beyond the frame forces EXTENDED over a partial enumeration", + "given": {"vital": "horizon", "observed_at": "2026-09-07T12:00:00Z", + "derive": {"planning_source": "milestones", "debt_mapping": null}, + "observations": [ + {"observation_id": "planning.explicit_targets.inventory", "value_type": "series", + "status": "PARTIAL", "reason_code": "PAGINATION_CAPPED", + "coverage": {"complete": false, "source": "milestones"}, + "value": [{"target_id": "1", "state": "OPEN", "due_at": "2026-12-01T00:00:00Z"}]}]}, + "expect": {"band": "EXTENDED", "evaluation_status": "DEGRADED", "band_semantics": "EXACT", + "possible_bands": ["EXTENDED"], "diagnostics": ["HORIZON_PARTIAL_BAND_INVARIANT"]}} +``` + +A case whose evidence must be refused states `{"rejected": ""}` as its +whole `expect`: the evaluation must raise `InadmissibleEvidence` with that +code (or one it is a prefix of). The totality contracts of 0.2.0 give +contradictory normalized input that outcome, before classification and +never as an `UNKNOWN` or `DEGRADED` result (HOR-PARTIAL-15; Direction's +`L + U + R != N`). A refusal combined with any other expectation, or naming +no code, is an error; a case that expects a refusal and gets a result fails +and says which result it got. + ### `kind: "delta"` Compares two snapshots given as partial Vital rows and checks transition diff --git a/docs/spec/vitals.md b/docs/spec/vitals.md index 2290c6f..b1912c5 100644 --- a/docs/spec/vitals.md +++ b/docs/spec/vitals.md @@ -27,7 +27,10 @@ Every Vital emits: `vital_id`, `vital_version`, `rule_id`, `band`, `evaluation_status` (`AVAILABLE`, `DEGRADED`, `UNKNOWN`), `band_semantics` (`EXACT`, `CONSERVATIVE_LOWER_BOUND`, `CONSERVATIVE_UPPER_BOUND`, -`NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET`), `possible_bands` when degraded, +`NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET`), `possible_bands` when degraded +(a `DEGRADED` band that every completion of the missing evidence reaches is +`EXACT`, and carries the singleton or no `possible_bands` as its contract +says), `inputs` with status and freshness, `derived` metrics actually used, `shared_signal_groups`, `dependency_group_ids`, `diagnostics` and a deterministic `explanation`. An `UNKNOWN` Vital has `band = null`. @@ -37,12 +40,12 @@ taxonomy and the policy constants. The seven rule ids of this version: | Vital | `rule_id` | Since | | --- | --- | --- | -| Horizon | `horizon.bands.v1` | 0.1.0 | +| Horizon | `horizon.bands.v2` | 0.2.0 (partial target enumeration, PV-HORIZON-PARTIAL-001) | | Clutter | `clutter.bands.v1` | 0.1.9 (the incomplete-evidence contract and its subset proof) | -| Direction | `direction.bands.v1.1` | 0.1.0 | +| Direction | `direction.bands.v2` | 0.2.0 (state-neutral and incomplete linkage, PV-REV-DIRECTION-CLOSED-TARGET-001 and PV-DIRECTION-INCOMPLETE-001) | | Flow | `flow.bands.v1` | 0.1.2 (the calibration repairs) | -| Integrity | `integrity.bands.v1+ci-unit-004` | 0.1.9 (the newest-UNKNOWN, partial-series, sample-strength and totality judgements) | -| Debt | `debt.bands.v1.1` | 0.1.0 | +| Integrity | `integrity.bands.v1+ci-unit-004+hist-002` | 0.2.0 (durable revision history, PV-HIST-002; the band table and the 0.1.9 judgements unchanged) | +| Debt | `debt.bands.v2` | 0.2.0 (partial register, PV-DEBT-PARTIAL-001) | | Pulse | `pulse.bands.v1` | 0.1.2 (the calibration repairs) | A rule version change never rewrites a @@ -167,7 +170,29 @@ to three decisive revisions, `ESTABLISHED` from four, and the diagnostic band is exact about the revisions it counts and says so, so that `FAILING` over one revision is not read as an established rate. -Degradation and refusal, under rule `integrity.bands.v1+ci-unit-004`: +History is counted over the durable revision history of **PV-HIST-002** +(rule `integrity.bands.v1+ci-unit-004+hist-002`, HIST-01..20): every attempt +one bundle observed for a revision is carried into the next one while the +revision is in the window, so a failure the provider later hides behind a +passing retry, or forgets through retention, still counts, and exactly once. +Favorable evidence that cannot prove every attempt, a parent-level check suite +or an Actions run whose earlier attempts are not all observed, is +`UNKNOWN_HISTORY` rather than a reconstructed pass (R52). The carrier and its +rules are in [integrity-ci.md](integrity-ci.md#durable-history-pv-hist-002). + +Degradation and refusal, under rule `integrity.bands.v1+ci-unit-004+hist-002`: + +- a recent revision whose history is `UNKNOWN_HISTORY` makes the Vital + `UNKNOWN` with no band, each such revision named by + `REVISION_HISTORY_UNKNOWN:` and the counts of the others kept in + `derived`: T9 puts unknown history above every conservative branch, so no + band is derived around it; +- durable history that proves verification happened while the provider shows + no current verification at all is `UNKNOWN` + (`CURRENT_VERIFICATION_NOT_OBSERVED`): a current verdict is never carried + from an earlier bundle (HIST-16); +- carried history that is not the shape its lineage declares is corrupt + persisted history and `UNKNOWN` (`REVISION_HISTORY_CARRY_MALFORMED`); - a required revision series with acquisition status `PARTIAL` is `UNKNOWN` with no band (`PV-REV-TEST-003`): the accepted chain has no degraded path @@ -320,6 +345,35 @@ Inputs: `planning.explicit_targets.capability`, `.open_count`, `EXTENDED` is not better than `VISIBLE`. Creating empty milestones improves Horizon; that is why Horizon exposes counts and never contributes to a score. +A boundary exactly 28 days out is not beyond the frame. Only open targets +count: a closed target with a distant boundary proves nothing here, even while +Direction still counts the work linked to it (DIR-CLOSED-07). + +**Partial enumeration** (PV-HORIZON-PARTIAL-001, rule `horizon.bands.v2`). A +fresh target enumeration that returned at least one target proves the +capability `SUPPORTED`, because more targets cannot remove it; one that +returned none proves neither support nor its absence, and Horizon is +`UNKNOWN`. Over a partial enumeration the three counts are observed-subset +lower bounds (`value_semantics = OBSERVED_SUBSET_COUNT`, the proof +`PV-REV-PR-031-003` requires), and the band table is evaluated over every +admissible completion of the missing targets. Horizon declares no order, so a +representative of several reachable bands is never chosen: + +| Observed subset | Reachable bands | Result | +| --- | --- | --- | +| PH0: no open target | all four | `UNKNOWN`, `HORIZON_PARTIAL_AMBIGUOUS` | +| PH1: open targets, none with a future boundary | DECLARED, VISIBLE, EXTENDED | `UNKNOWN` | +| PH2: a future boundary, none beyond 28 days | VISIBLE, EXTENDED | `UNKNOWN` | +| PH3: an open target beyond 28 days | EXTENDED | `DEGRADED` / `EXTENDED` / `EXACT`, `possible_bands = [EXTENDED]`, `HORIZON_PARTIAL_BAND_INVARIANT` | + +`EXACT` there means the band is the same in every completion of this +snapshot, which the observed target beyond the frame guarantees (existential +monotonicity, not an ordering); it never means the enumeration was complete. +The reachable set is kept in `derived.reachable_bands` as explanation, never +as a band claim, and each bounded count says `LOWER_BOUND` in `derived`. A +partial zero is never `UNDECLARED`. Counts no target population can produce +(`beyond > future` or `future > open`) are refused before classification with +`HORIZON_COUNTS_INCONSISTENT` (HOR-PARTIAL-15). Cases HOR-PARTIAL-01..16. Groups: `PLANNING_TARGETS`; dependency `HORIZON_DIRECTION_PLANNING`. @@ -329,17 +383,64 @@ Groups: `PLANNING_TARGETS`; dependency `HORIZON_DIRECTION_PLANNING`. must be explicit and auditable (milestone on the change request, or the register marker); keyword, branch-name or model heuristics are forbidden. -Inputs: `planning.linkage.active_change_requests_count_28d`, -`.active_change_requests_linked_to_open_target_count_28d`, -`planning.explicit_targets.capability`, `.links_per_target_28d`. - -| Band | Rule | +Inputs: `planning.linkage.active_change_requests_count_28d` (N), +`.active_change_requests_linked_count_28d` (L), +`.active_change_requests_unlinked_count_28d` (U), +`.active_change_requests_unresolved_count_28d` (R), +`planning.explicit_targets.capability`, `.links_per_target_28d`, +`.missing_target_reference_count_28d`, `.unresolved_change_requests_28d`. + +Every active change request is exactly one of: + +- **LINKED**: it carries an explicit reference to a declared target that + resolves, open or closed; +- **UNLINKED**: it carries no reference, or only references that a complete + register positively lacks (a broken reference, diagnosed + `DIRECTION_TARGET_REFERENCE_MISSING`); +- **UNRESOLVED**: its linkage evidence could not be read (a title, body or + milestone of the wrong type), or a reference could not be resolved + (register unreadable; resolution unavailable, forbidden, errored or stale). + Absence from a partial target list is never deletion proof. + +| Band | Rule, for k linked of N | | --- | --- | -| NO_ACTIVE_CHANGE | `active = 0` | -| UNDECLARED | `active > 0` and planning capability positively absent (`UNSUPPORTED`, `SUPPORTED_UNUSED`) | -| SCATTERED | `unlinked > linked` | -| MIXED | `unlinked > 0` and `unlinked <= linked` | -| FULLY_LINKED | `unlinked = 0` | +| NO_ACTIVE_CHANGE | `N = 0` | +| UNDECLARED | `N > 0` and planning capability positively absent (`UNSUPPORTED`, `SUPPORTED_UNUSED`) | +| SCATTERED | `2k < N`, the V1 `unlinked > linked` | +| MIXED | `k < N` and `2k >= N`, the V1 `0 < unlinked <= linked` | +| FULLY_LINKED | `k = N` | + +**State-neutral linkage** (PV-REV-DIRECTION-CLOSED-TARGET-001, rule +`direction.bands.v2`). A change request linked to a declared target stays +linked when the target closes: closing the target the work delivered must not +un-trace the work, and counting only open targets rewarded never closing them +(calibration finding 9). Horizon still counts open targets only, so the two +Vitals may diverge. The count of links to targets that are open now is still +derived, `active_change_requests_linked_to_open_target_count_28d`, for +presentation; it is never the classifier's input. + +**Incomplete linkage** (PV-DIRECTION-INCOMPLETE-001). With N complete and +fresh, the capability positively present and `L + U + R = N`, every unresolved +change request may complete either way, so k ranges over `L..L+R` and each k +is mapped through the table above: + +- `R = 0`: the exact band, `AVAILABLE`; +- `R > 0` and every k gives the same band: `DEGRADED` / band / `EXACT`, + `possible_bands` the singleton, `DIRECTION_INCOMPLETE_BAND_INVARIANT`, and + the unresolved change requests named in `derived.unresolved_change_requests`. + `FULLY_LINKED` can never be the only reachable band while `R > 0`; +- `R > 0` and the completions cross a boundary: `UNKNOWN` with no band, + `DIRECTION_INCOMPLETE_AMBIGUOUS`, the reachable set in + `derived.reachable_bands` as explanation only. Direction declares no order to + choose a representative by. + +A partial global target list does not degrade Direction when every target the +active change requests reference is resolved on its own, as a milestone +embedded in the change request is (DIR-INCOMPLETE-10). An active population +that is itself partial, or a capability neither positively present nor +absent, is `UNKNOWN`. Counts with `L + U + R != N` are refused before +classification with `DIRECTION_LINKAGE_COUNTS_INCONSISTENT`. Cases +DIR-CLOSED-01..09 and DIR-INCOMPLETE-01..16. `FULLY_LINKED` is neutral exact traceability. It is never rendered as ALIGNED, ON_TRACK or HEALTHY. Mass-linking every change to one target yields @@ -368,9 +469,30 @@ Inputs: `debt.registry.capability`, `debt.mapping`, `debt.items.open_count`, No universal quantitative thresholds exist for Debt: the retired `ACCUMULATED` band is never emitted. Configured debt evidence that is -forbidden, unknown, errored, stale or partial yields `UNKNOWN`, never `CLEAR`; -a partial register with at least one observed item yields `DEGRADED PRESENT`. -A change of `mapping_version` makes history `INCOMPARABLE`. +forbidden, unknown, errored or stale yields `UNKNOWN`, never `CLEAR`. + +**Partial register** (PV-DEBT-PARTIAL-001, rule `debt.bands.v2`). A `PARTIAL` +open count is read only as an observed-subset lower bound, which its coverage +must declare (`value_semantics = OBSERVED_SUBSET_COUNT`, issue #39), and only +under a configured mapping that is itself observed: + +- at least one confirmed open item: `DEGRADED` / `PRESENT` / `EXACT`, no + `possible_bands`, because every completion of the missing items still holds + that item. `EXACT` names the band, never the count or a severity: + `derived.open_count_semantics` is `LOWER_BOUND`, and `confirmed_open_count` + and the partial receipt (`derived.partial_enumeration`) stay visible, with + `DEBT_PARTIAL_ENUMERATION`, `DEBT_CONFIRMED_OPEN_LOWER_BOUND:` and + `DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN`; +- none confirmed: `UNKNOWN` with no band. The completions reach `CLEAR` and + `PRESENT`, Debt declares no order between them, and the observed zero is + absence of proof, not proof of absence; the zero and the receipt stay + visible. + +A partial count without the subset proof is `UNKNOWN` +(`PARTIAL_COUNT_NOT_A_LOWER_BOUND`). Stale and recently closed counts are +diagnostics and never change a result; a project-local quantitative policy +carried in the mapping never replaces the canonical bands. A change of +`mapping_version` makes history `INCOMPARABLE`. Cases DEBT-PARTIAL-01..16. Groups: `EXPLICIT_DEBT_REGISTER`, `FORGE_INVENTORY`; dependency `DEBT_CLUTTER_MAINTENANCE`. diff --git a/examples/observations.json b/examples/observations.json index 6cda0b8..37abb75 100644 --- a/examples/observations.json +++ b/examples/observations.json @@ -46,6 +46,7 @@ "committed_at": "2026-08-22T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -73,6 +74,7 @@ "committed_at": "2026-08-23T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -100,6 +102,7 @@ "committed_at": "2026-08-24T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -127,6 +130,7 @@ "committed_at": "2026-08-25T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_FAIL", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "FAILURE_OBSERVED", "parents": [ @@ -158,6 +162,7 @@ "committed_at": "2026-08-26T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -185,6 +190,7 @@ "committed_at": "2026-08-27T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -212,6 +218,7 @@ "committed_at": "2026-08-28T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -239,6 +246,7 @@ "committed_at": "2026-08-29T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -291,10 +299,17 @@ "created_at": "2026-09-02T10:00:00Z", "draft": false, "id": 40, + "linkage_unresolved": [], "merged_at": null, "number": 40, "state": "OPEN", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Refactor the pricing module", "updated_at": "2026-09-04T10:00:00Z", @@ -306,10 +321,12 @@ "created_at": "2026-09-03T10:00:00Z", "draft": true, "id": 41, + "linkage_unresolved": [], "merged_at": null, "number": 41, "state": "OPEN", "target_id": null, + "target_refs": [], "target_state": null, "title": "Add the export button", "updated_at": "2026-09-05T08:00:00Z", @@ -321,10 +338,17 @@ "created_at": "2026-08-13T10:00:00Z", "draft": false, "id": 31, + "linkage_unresolved": [], "merged_at": "2026-08-14T06:00:00Z", "number": 31, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 1", "updated_at": "2026-08-14T10:00:00Z", @@ -336,11 +360,18 @@ "created_at": "2026-08-14T10:00:00Z", "draft": false, "id": 32, + "linkage_unresolved": [], "merged_at": "2026-08-15T06:00:00Z", "number": 32, "state": "MERGED", - "target_id": null, - "target_state": null, + "target_id": "2", + "target_refs": [ + { + "state": "CLOSED", + "target_id": "2" + } + ], + "target_state": "CLOSED", "title": "Merged change 2", "updated_at": "2026-08-15T10:00:00Z", "url": "https://example.invalid/pull/32" @@ -351,10 +382,17 @@ "created_at": "2026-08-15T10:00:00Z", "draft": false, "id": 33, + "linkage_unresolved": [], "merged_at": "2026-08-16T06:00:00Z", "number": 33, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 3", "updated_at": "2026-08-16T10:00:00Z", @@ -366,10 +404,12 @@ "created_at": "2026-08-16T10:00:00Z", "draft": false, "id": 34, + "linkage_unresolved": [], "merged_at": "2026-08-17T06:00:00Z", "number": 34, "state": "MERGED", "target_id": null, + "target_refs": [], "target_state": null, "title": "Merged change 4", "updated_at": "2026-08-17T10:00:00Z", @@ -381,10 +421,17 @@ "created_at": "2026-08-17T10:00:00Z", "draft": false, "id": 35, + "linkage_unresolved": [], "merged_at": "2026-08-18T06:00:00Z", "number": 35, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 5", "updated_at": "2026-08-18T10:00:00Z", @@ -396,10 +443,12 @@ "created_at": "2026-08-18T10:00:00Z", "draft": false, "id": 36, + "linkage_unresolved": [], "merged_at": "2026-08-19T06:00:00Z", "number": 36, "state": "MERGED", "target_id": null, + "target_refs": [], "target_state": null, "title": "Merged change 6", "updated_at": "2026-08-19T10:00:00Z", @@ -411,10 +460,17 @@ "created_at": "2026-08-19T10:00:00Z", "draft": false, "id": 37, + "linkage_unresolved": [], "merged_at": "2026-08-20T06:00:00Z", "number": 37, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 7", "updated_at": "2026-08-20T10:00:00Z", @@ -426,10 +482,12 @@ "created_at": "2026-08-20T10:00:00Z", "draft": false, "id": 38, + "linkage_unresolved": [], "merged_at": "2026-08-21T06:00:00Z", "number": 38, "state": "MERGED", "target_id": null, + "target_refs": [], "target_state": null, "title": "Merged change 8", "updated_at": "2026-08-21T10:00:00Z", diff --git a/examples/sample-bundle/demand.json b/examples/sample-bundle/demand.json index 8b389dd..fe4701d 100644 --- a/examples/sample-bundle/demand.json +++ b/examples/sample-bundle/demand.json @@ -63,7 +63,7 @@ { "band": "MIXED", "evaluation_status": "AVAILABLE", - "gauge": 50, + "gauge": 60, "level": "MEDIUM", "reason": "BAND:MIXED", "vital_id": "direction" diff --git a/examples/sample-bundle/gauges.json b/examples/sample-bundle/gauges.json index 3eef643..9fd44e0 100644 --- a/examples/sample-bundle/gauges.json +++ b/examples/sample-bundle/gauges.json @@ -27,7 +27,7 @@ "contract": "devostasis.gauge.v1", "qualifier": "exact", "scale": "traceability share", - "value": 50, + "value": 60, "vital_id": "direction" }, { diff --git a/examples/sample-bundle/manifest.json b/examples/sample-bundle/manifest.json index 7e3f16d..c7e09f9 100644 --- a/examples/sample-bundle/manifest.json +++ b/examples/sample-bundle/manifest.json @@ -6,7 +6,7 @@ } ], "artifact_contract_version": "devostasis.bundle.v2", - "bundle_id": "45f84cc2652cb5d80e3b7edb689fa4b7973a5c5c76db2b5cfb2df06e8a2ec6c6", + "bundle_id": "422f6c269728988da2808817b5e72db3f3df97a330fb91c52f3f53e6ee76554c", "bundle_identity_contract": "PV-BUNDLE-ID-002", "canonical_member_profile": { "activity_json": "ENABLED", @@ -36,13 +36,13 @@ "config_version": "example-1", "delta_digest": "sha256:a36c8ad814fc8ae9c9e2d0b31f7764fc7a5ca959a39017922a389994029cf8f8", "demand_contract": "devostasis.demand.v2", - "demand_digest": "sha256:615dbc00bee70964a95bded568a13b56d8a77d18cf5db1f6e6a54ddba4b6acca", + "demand_digest": "sha256:38f9170d967c28516dcaf4ca34d77815c0b5589701c076fa1ee176a4f64f0d94", "effective_config_contract": "PV-EFFECTIVE-CONFIG-001", "effective_config_digest": "sha256:cc472c9ad93f6721ecc9a3fc14f3067ccc85af33c2acceb4489982a44eb370ac", "gauge_contract": "devostasis.gauge.v1", - "gauges_digest": "sha256:9625ff34cc358d9343d865f6900a20c357b400f47be23a414775c21261bbc628", + "gauges_digest": "sha256:f396534cc1b29a280d8680d2ce89826dfc9ef99f7cd84262cb6e80009dbad2be", "observation_contract_version": "RAW-OBS-V0", - "observations_digest": "sha256:8be202ed47fbf0fc95288df179037b061e02192f6a48f6507b6cab5eaad0ce98", + "observations_digest": "sha256:78603834c2e4cb90798cc144488c5b9e8d99407908269572a622ed13f1e58b33", "observed_at": "2026-09-05T12:00:00Z", "policy_version": "devostasis.policy.v1", "previous_bundle_id": null, @@ -54,19 +54,19 @@ "provider": "github" }, "renderer_version": "devostasis.render.v4", - "snapshot_digest": "sha256:f5b315d95227724289b459582cc4352e8a64ca9ee7a5a90457c024377e7e3e23", + "snapshot_digest": "sha256:d0274768919e75f1c6f76b6720e7612282229f7af11f050d76cc8a29bf4b85b9", "source_receipts_digest": "sha256:fda4a9bc9f4f74739ecd59c85eed55a83e95a7085ae22fa7d82d9c98f36ecde3", "vitals_contract_version": "PV-VITALS-V1-002" }, "members": { "activity.json": "sha256:d80fa63f29071d59425dad060211db5fbf6d165248c23b18169b06c37a1a0595", "delta.json": "sha256:a36c8ad814fc8ae9c9e2d0b31f7764fc7a5ca959a39017922a389994029cf8f8", - "demand.json": "sha256:615dbc00bee70964a95bded568a13b56d8a77d18cf5db1f6e6a54ddba4b6acca", + "demand.json": "sha256:38f9170d967c28516dcaf4ca34d77815c0b5589701c076fa1ee176a4f64f0d94", "effective-config.json": "sha256:cc472c9ad93f6721ecc9a3fc14f3067ccc85af33c2acceb4489982a44eb370ac", - "gauges.json": "sha256:9625ff34cc358d9343d865f6900a20c357b400f47be23a414775c21261bbc628", - "observations.json": "sha256:8be202ed47fbf0fc95288df179037b061e02192f6a48f6507b6cab5eaad0ce98", - "report.md": "sha256:560dd5d90ac4188e028870a994c5bcd0a534d83bc5fb22b889af5448fa027ed2", - "snapshot.json": "sha256:f5b315d95227724289b459582cc4352e8a64ca9ee7a5a90457c024377e7e3e23" + "gauges.json": "sha256:f396534cc1b29a280d8680d2ce89826dfc9ef99f7cd84262cb6e80009dbad2be", + "observations.json": "sha256:78603834c2e4cb90798cc144488c5b9e8d99407908269572a622ed13f1e58b33", + "report.md": "sha256:4b7c14593a029b47956d7343c839d221b9f4146a960c1a9eba3d6894cf9f2598", + "snapshot.json": "sha256:d0274768919e75f1c6f76b6720e7612282229f7af11f050d76cc8a29bf4b85b9" }, "observation_contract_version": "RAW-OBS-V0", "observed_at": "2026-09-05T12:00:00Z", @@ -167,7 +167,7 @@ "example": true, "tool": { "name": "devostasis", - "version": "0.1.9" + "version": "0.2.0" } }, "schema": "devostasis.manifest.v1", diff --git a/examples/sample-bundle/observations.json b/examples/sample-bundle/observations.json index 7827d02..415e73e 100644 --- a/examples/sample-bundle/observations.json +++ b/examples/sample-bundle/observations.json @@ -19,6 +19,22 @@ "value": true, "value_type": "boolean" }, + { + "adapter_version": "devostasis.ci-history.v2", + "collected_at": "2026-09-05T12:00:00Z", + "coverage": null, + "evidence_ref": null, + "freshness": "FRESH", + "notes": null, + "observation_id": "ci.revision_history_carried", + "provider": "history", + "reason_code": "NO_PREVIOUS_BUNDLE", + "schema_version": "RAW-OBS-V0", + "source_ref": "store:none", + "status": "UNAVAILABLE", + "value": null, + "value_type": "record" + }, { "adapter_version": "devostasis.github.v1", "collected_at": "2026-09-05T12:00:00Z", @@ -46,6 +62,7 @@ "committed_at": "2026-08-22T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -73,6 +90,7 @@ "committed_at": "2026-08-23T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -100,6 +118,7 @@ "committed_at": "2026-08-24T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -127,6 +146,7 @@ "committed_at": "2026-08-25T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_FAIL", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "FAILURE_OBSERVED", "parents": [ @@ -158,6 +178,7 @@ "committed_at": "2026-08-26T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -185,6 +206,7 @@ "committed_at": "2026-08-27T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -212,6 +234,7 @@ "committed_at": "2026-08-28T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -239,6 +262,7 @@ "committed_at": "2026-08-29T09:00:00Z", "current_verdict": "VERIFY_PASS", "historical_contribution": "VERIFY_PASS", + "history_complete": true, "history_provenance": "ATTEMPT_LEVEL", "history_state": "PASS_ONLY_OBSERVED", "parents": [ @@ -413,10 +437,17 @@ "created_at": "2026-09-02T10:00:00Z", "draft": false, "id": 40, + "linkage_unresolved": [], "merged_at": null, "number": 40, "state": "OPEN", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Refactor the pricing module", "updated_at": "2026-09-04T10:00:00Z", @@ -428,10 +459,12 @@ "created_at": "2026-09-03T10:00:00Z", "draft": true, "id": 41, + "linkage_unresolved": [], "merged_at": null, "number": 41, "state": "OPEN", "target_id": null, + "target_refs": [], "target_state": null, "title": "Add the export button", "updated_at": "2026-09-05T08:00:00Z", @@ -443,10 +476,17 @@ "created_at": "2026-08-13T10:00:00Z", "draft": false, "id": 31, + "linkage_unresolved": [], "merged_at": "2026-08-14T06:00:00Z", "number": 31, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 1", "updated_at": "2026-08-14T10:00:00Z", @@ -458,11 +498,18 @@ "created_at": "2026-08-14T10:00:00Z", "draft": false, "id": 32, + "linkage_unresolved": [], "merged_at": "2026-08-15T06:00:00Z", "number": 32, "state": "MERGED", - "target_id": null, - "target_state": null, + "target_id": "2", + "target_refs": [ + { + "state": "CLOSED", + "target_id": "2" + } + ], + "target_state": "CLOSED", "title": "Merged change 2", "updated_at": "2026-08-15T10:00:00Z", "url": "https://example.invalid/pull/32" @@ -473,10 +520,17 @@ "created_at": "2026-08-15T10:00:00Z", "draft": false, "id": 33, + "linkage_unresolved": [], "merged_at": "2026-08-16T06:00:00Z", "number": 33, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 3", "updated_at": "2026-08-16T10:00:00Z", @@ -488,10 +542,12 @@ "created_at": "2026-08-16T10:00:00Z", "draft": false, "id": 34, + "linkage_unresolved": [], "merged_at": "2026-08-17T06:00:00Z", "number": 34, "state": "MERGED", "target_id": null, + "target_refs": [], "target_state": null, "title": "Merged change 4", "updated_at": "2026-08-17T10:00:00Z", @@ -503,10 +559,17 @@ "created_at": "2026-08-17T10:00:00Z", "draft": false, "id": 35, + "linkage_unresolved": [], "merged_at": "2026-08-18T06:00:00Z", "number": 35, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 5", "updated_at": "2026-08-18T10:00:00Z", @@ -518,10 +581,12 @@ "created_at": "2026-08-18T10:00:00Z", "draft": false, "id": 36, + "linkage_unresolved": [], "merged_at": "2026-08-19T06:00:00Z", "number": 36, "state": "MERGED", "target_id": null, + "target_refs": [], "target_state": null, "title": "Merged change 6", "updated_at": "2026-08-19T10:00:00Z", @@ -533,10 +598,17 @@ "created_at": "2026-08-19T10:00:00Z", "draft": false, "id": 37, + "linkage_unresolved": [], "merged_at": "2026-08-20T06:00:00Z", "number": 37, "state": "MERGED", "target_id": "3", + "target_refs": [ + { + "state": "OPEN", + "target_id": "3" + } + ], "target_state": "OPEN", "title": "Merged change 7", "updated_at": "2026-08-20T10:00:00Z", @@ -548,10 +620,12 @@ "created_at": "2026-08-20T10:00:00Z", "draft": false, "id": 38, + "linkage_unresolved": [], "merged_at": "2026-08-21T06:00:00Z", "number": 38, "state": "MERGED", "target_id": null, + "target_refs": [], "target_state": null, "title": "Merged change 8", "updated_at": "2026-08-21T10:00:00Z", @@ -1422,6 +1496,33 @@ }, "freshness": "FRESH", "notes": null, + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "provider": "github", + "reason_code": null, + "schema_version": "RAW-OBS-V0", + "source_ref": "derived:forge.change_requests.inventory", + "status": "AVAILABLE", + "value": 6, + "value_type": "count" + }, + { + "adapter_version": "devostasis.github.v1", + "collected_at": "2026-09-05T12:00:00Z", + "coverage": { + "complete": true, + "source_coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-08T12:00:00Z" + } + }, + "evidence_ref": { + "derived_from": [ + "forge.change_requests.inventory" + ] + }, + "freshness": "FRESH", + "notes": "active change requests linked to a target that is open now; presentation only since direction.bands.v2, never a classifier input", "observation_id": "planning.linkage.active_change_requests_linked_to_open_target_count_28d", "provider": "github", "reason_code": null, @@ -1431,6 +1532,60 @@ "value": 5, "value_type": "count" }, + { + "adapter_version": "devostasis.github.v1", + "collected_at": "2026-09-05T12:00:00Z", + "coverage": { + "complete": true, + "source_coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-08T12:00:00Z" + } + }, + "evidence_ref": { + "derived_from": [ + "forge.change_requests.inventory" + ] + }, + "freshness": "FRESH", + "notes": null, + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "provider": "github", + "reason_code": null, + "schema_version": "RAW-OBS-V0", + "source_ref": "derived:forge.change_requests.inventory", + "status": "AVAILABLE", + "value": 4, + "value_type": "count" + }, + { + "adapter_version": "devostasis.github.v1", + "collected_at": "2026-09-05T12:00:00Z", + "coverage": { + "complete": true, + "source_coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-08T12:00:00Z" + } + }, + "evidence_ref": { + "derived_from": [ + "forge.change_requests.inventory" + ] + }, + "freshness": "FRESH", + "notes": null, + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "provider": "github", + "reason_code": null, + "schema_version": "RAW-OBS-V0", + "source_ref": "derived:forge.change_requests.inventory", + "status": "AVAILABLE", + "value": 0, + "value_type": "count" + }, { "adapter_version": "devostasis.github.v1", "collected_at": "2026-09-05T12:00:00Z", @@ -1456,10 +1611,38 @@ "source_ref": "derived:forge.change_requests.inventory", "status": "AVAILABLE", "value": { + "2": 1, "3": 5 }, "value_type": "record" }, + { + "adapter_version": "devostasis.github.v1", + "collected_at": "2026-09-05T12:00:00Z", + "coverage": { + "complete": true, + "source_coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-08T12:00:00Z" + } + }, + "evidence_ref": { + "derived_from": [ + "forge.change_requests.inventory" + ] + }, + "freshness": "FRESH", + "notes": null, + "observation_id": "planning.linkage.missing_target_reference_count_28d", + "provider": "github", + "reason_code": null, + "schema_version": "RAW-OBS-V0", + "source_ref": "derived:forge.change_requests.inventory", + "status": "AVAILABLE", + "value": 0, + "value_type": "count" + }, { "adapter_version": "devostasis.github.v1", "collected_at": "2026-09-05T12:00:00Z", @@ -1486,6 +1669,33 @@ "status": "AVAILABLE", "value": 0, "value_type": "count" + }, + { + "adapter_version": "devostasis.github.v1", + "collected_at": "2026-09-05T12:00:00Z", + "coverage": { + "complete": true, + "source_coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-08T12:00:00Z" + } + }, + "evidence_ref": { + "derived_from": [ + "forge.change_requests.inventory" + ] + }, + "freshness": "FRESH", + "notes": null, + "observation_id": "planning.linkage.unresolved_change_requests_28d", + "provider": "github", + "reason_code": null, + "schema_version": "RAW-OBS-V0", + "source_ref": "derived:forge.change_requests.inventory", + "status": "AVAILABLE", + "value": {}, + "value_type": "record" } ], "observed_at": "2026-09-05T12:00:00Z", diff --git a/examples/sample-bundle/report.md b/examples/sample-bundle/report.md index 5a334f6..284a3fd 100644 --- a/examples/sample-bundle/report.md +++ b/examples/sample-bundle/report.md @@ -2,13 +2,13 @@ - Observed at: 2026-09-05T12:00:00Z - Comparison: BASELINE -- Bundle: `45f84cc2652cb5d80e3b7edb689fa4b7973a5c5c76db2b5cfb2df06e8a2ec6c6` +- Bundle: `422f6c269728988da2808817b5e72db3f3df97a330fb91c52f3f53e6ee76554c` - Contracts: vitals PV-VITALS-V1-002, observations RAW-OBS-V0, policy devostasis.policy.v1 ```text Horizon ████████░░ 84 EXTENDED Clutter █░░░░░░░░░ 10 LIGHT -Direction █████░░░░░ 50 MIXED +Direction ██████░░░░ 60 MIXED Flow █░░░░░░░░░ 10 MOVING Integrity ██████░░░░ 62 FLAKY Debt █░░░░░░░░░ 11 PRESENT @@ -24,7 +24,7 @@ Demand levels come from mapping `devostasis-default-1` (devostasis.demand.v2); i | Order | Vital | Level | Band | Gauge | | --- | --- | --- | --- | --- | | 1 | Integrity | HIGH | FLAKY | 62 | -| 2 | Direction | MEDIUM | MIXED | 50 | +| 2 | Direction | MEDIUM | MIXED | 60 | | 3 | Debt | MEDIUM | PRESENT | 11 | | 4 | Clutter | LOW | LIGHT | 10 | | 5 | Flow | LOW | MOVING | 10 | @@ -37,7 +37,7 @@ Demand levels come from mapping `devostasis-default-1` (devostasis.demand.v2); i | --- | --- | --- | --- | --- | --- | | Horizon | 84 | EXTENDED | AVAILABLE | EXACT | 1 open planning targets, 1 with a future boundary, 1 reaching beyond 28 days. | | Clutter | 10 | LIGHT | AVAILABLE | EXACT | 1 stale work items out of 5 tracked open items; 1 stale non-default branches. | -| Direction | 50 | MIXED | AVAILABLE | EXACT | 5 of 10 active change requests are explicitly linked to an open planning target. | +| Direction | 60 | MIXED | AVAILABLE | EXACT | 6 of 10 active change requests are explicitly linked to a declared target, open or closed. | | Flow | 10 | MOVING | AVAILABLE | EXACT | 2 open change requests; 8 merged in 28 days; oldest open for 3 days; median time to merge 20h 0m. | | Integrity | 62 | FLAKY | AVAILABLE | EXACT | 1 of 8 decisive revisions failed verification in 14 days; latest decisive verdict is VERIFY_PASS. | | Debt | 11 | PRESENT | AVAILABLE | EXACT | 1 open registered debt items under mapping version example-1. | @@ -47,7 +47,7 @@ Demand levels come from mapping `devostasis-default-1` (devostasis.demand.v2); i _Is future work explicitly declared, and does any declaration reach beyond 28 days?_ -- Evaluation: AVAILABLE; rule `horizon.bands.v1` +- Evaluation: AVAILABLE; rule `horizon.bands.v2` - Gauge (declared future work): ████████░░ 84 - Shares signals with: HORIZON_DIRECTION_PLANNING @@ -78,19 +78,20 @@ _How much unresolved stale residue is observable?_ _Is active change work explicitly traceable to declared targets?_ -- Evaluation: AVAILABLE; rule `direction.bands.v1.1` -- Gauge (traceability share): █████░░░░░ 50 +- Evaluation: AVAILABLE; rule `direction.bands.v2` +- Gauge (traceability share): ██████░░░░ 60 - Shares signals with: HORIZON_DIRECTION_PLANNING, DIRECTION_PULSE_ACTIVITY | Metric | Value | | --- | --- | | active_change_count_28d | 10 | | capability | SUPPORTED | -| linked_active_change_count_28d | 5 | -| unlinked_active_change_count_28d | 5 | - -Diagnostics: -- `ALL_LINKS_TO_SINGLE_TARGET` +| linkage_rule | PV-REV-DIRECTION-CLOSED-TARGET-001 | +| linked_active_change_count_28d | 6 | +| linked_to_open_target_count_28d | 5 | +| missing_target_reference_count_28d | 0 | +| unlinked_active_change_count_28d | 4 | +| unresolved_active_change_count_28d | 0 | ### Flow: MOVING @@ -112,7 +113,7 @@ _What is the state and friction of the current change-request queue?_ _What does automated verification say about recent immutable revisions?_ -- Evaluation: AVAILABLE; rule `integrity.bands.v1+ci-unit-004` +- Evaluation: AVAILABLE; rule `integrity.bands.v1+ci-unit-004+hist-002` - Gauge (verification stability): ██████░░░░ 62 - Shares signals with: INTEGRITY_ONLY @@ -121,15 +122,18 @@ _What does automated verification say about recent immutable revisions?_ | decisive_count_14d | 8 | | failed_count_14d | 1 | | failure_ratio_14d | 0.13 (1/8) | +| history_rule | PV-HIST-002 | | revisions_in_window | 8 | +| revisions_with_history | 8 | | revisions_with_verification | 8 | | sample_strength | ESTABLISHED | +| unknown_history_count_14d | 0 | ### Debt: PRESENT _How much explicitly registered maintenance obligation is unresolved?_ -- Evaluation: AVAILABLE; rule `debt.bands.v1.1` +- Evaluation: AVAILABLE; rule `debt.bands.v2` - Gauge (registered debt): █░░░░░░░░░ 11 - Shares signals with: DEBT_CLUTTER_MAINTENANCE @@ -216,5 +220,5 @@ Releases: - Artifact contract: devostasis.bundle.v2; bundle identity: PV-BUNDLE-ID-002; renderer: devostasis.render.v4; gauges: devostasis.gauge.v1; demand: devostasis.demand.v2 - Effective config digest: `sha256:cc472c9ad93f6721ecc9a3fc14f3067ccc85af33c2acceb4489982a44eb370ac` (config version `example-1`) - Adapters: github devostasis.github.v1 -- Observations digest: `sha256:8be202ed47fbf0fc95288df179037b061e02192f6a48f6507b6cab5eaad0ce98` +- Observations digest: `sha256:78603834c2e4cb90798cc144488c5b9e8d99407908269572a622ed13f1e58b33` - Generated deterministically from the machine bundle without any language model. diff --git a/examples/sample-bundle/snapshot.json b/examples/sample-bundle/snapshot.json index 3e5fbae..3bc19f3 100644 --- a/examples/sample-bundle/snapshot.json +++ b/examples/sample-bundle/snapshot.json @@ -1,6 +1,6 @@ { "observation_contract_version": "RAW-OBS-V0", - "observations_digest": "sha256:8be202ed47fbf0fc95288df179037b061e02192f6a48f6507b6cab5eaad0ce98", + "observations_digest": "sha256:78603834c2e4cb90798cc144488c5b9e8d99407908269572a622ed13f1e58b33", "observed_at": "2026-09-05T12:00:00Z", "policy_version": "devostasis.policy.v1", "schema": "devostasis.snapshot.v1", @@ -60,7 +60,7 @@ ], "policy_version": "devostasis.policy.v1", "possible_bands": null, - "rule_id": "horizon.bands.v1", + "rule_id": "horizon.bands.v2", "shared_signal_groups": [ "PLANNING_TARGETS" ], @@ -155,17 +155,20 @@ "derived": { "active_change_count_28d": 10, "capability": "SUPPORTED", - "linked_active_change_count_28d": 5, + "linkage_rule": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "linked_active_change_count_28d": 6, + "linked_to_open_target_count_28d": 5, "links_per_target": { + "2": 1, "3": 5 }, - "unlinked_active_change_count_28d": 5 + "missing_target_reference_count_28d": 0, + "unlinked_active_change_count_28d": 4, + "unresolved_active_change_count_28d": 0 }, - "diagnostics": [ - "ALL_LINKS_TO_SINGLE_TARGET" - ], + "diagnostics": [], "evaluation_status": "AVAILABLE", - "explanation": "5 of 10 active change requests are explicitly linked to an open planning target.", + "explanation": "6 of 10 active change requests are explicitly linked to a declared target, open or closed.", "inputs": [ { "freshness": "FRESH", @@ -174,7 +177,17 @@ }, { "freshness": "FRESH", - "observation_id": "planning.linkage.active_change_requests_linked_to_open_target_count_28d", + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "status": "AVAILABLE" + }, + { + "freshness": "FRESH", + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "status": "AVAILABLE" + }, + { + "freshness": "FRESH", + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", "status": "AVAILABLE" }, { @@ -186,11 +199,21 @@ "freshness": "FRESH", "observation_id": "planning.linkage.links_per_target_28d", "status": "AVAILABLE" + }, + { + "freshness": "FRESH", + "observation_id": "planning.linkage.missing_target_reference_count_28d", + "status": "AVAILABLE" + }, + { + "freshness": "FRESH", + "observation_id": "planning.linkage.unresolved_change_requests_28d", + "status": "AVAILABLE" } ], "policy_version": "devostasis.policy.v1", "possible_bands": null, - "rule_id": "direction.bands.v1.1", + "rule_id": "direction.bands.v2", "shared_signal_groups": [ "PLANNING_TARGETS", "CHANGE_REQUEST_ACTIVITY" @@ -274,9 +297,201 @@ "history_provenance": { "ATTEMPT_LEVEL": 8 }, + "history_rule": "PV-HIST-002", + "revision_history": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "committed_at": "2026-08-22T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:900" + ] + } + ], + "revision": "000000000000000000000000000000000000000c" + }, + { + "committed_at": "2026-08-23T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:901" + ] + } + ], + "revision": "000000000000000000000000000000000000000d" + }, + { + "committed_at": "2026-08-24T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:902" + ] + } + ], + "revision": "000000000000000000000000000000000000000e" + }, + { + "committed_at": "2026-08-25T09:00:00Z", + "historical_contribution": "VERIFY_FAIL", + "history_complete": true, + "history_state": "FAILURE_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + }, + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 2, + "parent_ids": [ + "github_actions:workflow_run:903" + ] + } + ], + "revision": "000000000000000000000000000000000000000f" + }, + { + "committed_at": "2026-08-26T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:904" + ] + } + ], + "revision": "0000000000000000000000000000000000000010" + }, + { + "committed_at": "2026-08-27T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:905" + ] + } + ], + "revision": "0000000000000000000000000000000000000011" + }, + { + "committed_at": "2026-08-28T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:906" + ] + } + ], + "revision": "0000000000000000000000000000000000000012" + }, + { + "committed_at": "2026-08-29T09:00:00Z", + "historical_contribution": "VERIFY_PASS", + "history_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "parent_groups": [ + { + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "parent_ids": [ + "github_actions:workflow_run:907" + ] + } + ], + "revision": "0000000000000000000000000000000000000013" + } + ], + "source": { + "reason": "NO_PREVIOUS_BUNDLE", + "status": "BASELINE" + } + }, "revisions_in_window": 8, + "revisions_with_history": 8, "revisions_with_verification": 8, - "sample_strength": "ESTABLISHED" + "sample_strength": "ESTABLISHED", + "unknown_history_count_14d": 0 }, "diagnostics": [], "evaluation_status": "AVAILABLE", @@ -291,11 +506,16 @@ "freshness": "FRESH", "observation_id": "ci.revision_verdicts_14d", "status": "AVAILABLE" + }, + { + "freshness": "FRESH", + "observation_id": "ci.revision_history_carried", + "status": "UNAVAILABLE" } ], "policy_version": "devostasis.policy.v1", "possible_bands": null, - "rule_id": "integrity.bands.v1+ci-unit-004", + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", "shared_signal_groups": [ "CI_VERIFICATION" ], @@ -354,7 +574,7 @@ ], "policy_version": "devostasis.policy.v1", "possible_bands": null, - "rule_id": "debt.bands.v1.1", + "rule_id": "debt.bands.v2", "shared_signal_groups": [ "EXPLICIT_DEBT_REGISTER", "FORGE_INVENTORY" diff --git a/examples/vectors/example.json b/examples/vectors/example.json index f592fc5..f9dbc8d 100644 --- a/examples/vectors/example.json +++ b/examples/vectors/example.json @@ -111,6 +111,49 @@ ] } }, + { + "case": "EXAMPLE-DERIVE-01", + "title": "a returned open target beyond the frame forces EXTENDED over a partial enumeration", + "kind": "vital", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "source": "milestones" + }, + "value": [ + { + "target_id": "1", + "state": "OPEN", + "due_at": "2026-12-01T00:00:00Z" + } + ] + } + ] + }, + "expect": { + "band": "EXTENDED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "EXTENDED" + ], + "diagnostics": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, { "case": "EXAMPLE-DELTA-01", "title": "A Vital with a declared order and exact evidence on both sides reports a direction", diff --git a/pyproject.toml b/pyproject.toml index 17afd52..dee14f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "devostasis" -version = "0.1.9" +version = "0.2.0" description = "Deterministic, model-free vital signs for software repositories." readme = "README.md" license = "MIT" diff --git a/schemas/conformance-vector.schema.json b/schemas/conformance-vector.schema.json index 01e2355..bb40990 100644 --- a/schemas/conformance-vector.schema.json +++ b/schemas/conformance-vector.schema.json @@ -42,6 +42,16 @@ "vital": { "enum": ["horizon", "clutter", "direction", "flow", "integrity", "debt", "pulse"] }, "observed_at": { "type": "string" }, "subject": { "type": "object" }, + "derive": { + "type": "object", + "description": "run the provider-neutral derivation over the stated inventories before evaluating, under these two project settings", + "required": ["planning_source"], + "additionalProperties": false, + "properties": { + "planning_source": { "enum": ["none", "milestones", "file"] }, + "debt_mapping": { "type": ["object", "null"], "description": "the explicit debt mapping (labels or a register file), or null when none is configured" } + } + }, "observations": { "type": "array", "minItems": 1, @@ -246,9 +256,12 @@ "$defs": { "vital_expect": { "title": "What one Vital evaluation must yield", - "description": "The same shape for a vital case and for the Integrity evaluation a ci case may chain into.", + "description": "The same shape for a vital case and for the Integrity evaluation a ci case may chain into: a result to check, or the refusal a case about contradictory evidence expects.", "type": "object", - "required": ["band", "evaluation_status"], + "oneOf": [ + { "required": ["band", "evaluation_status"], "not": { "required": ["rejected"] } }, + { "required": ["rejected"], "maxProperties": 1 } + ], "additionalProperties": false, "properties": { "band": { "type": ["string", "null"] }, @@ -262,7 +275,8 @@ "diagnostics_absent": { "type": "array", "items": { "type": "string" } }, "explanation_contains": { "type": "array", "items": { "type": "string" } }, "shared_signal_groups": { "type": "array", "items": { "type": "string" } }, - "dependency_group_ids": { "type": "array", "items": { "type": "string" } } + "dependency_group_ids": { "type": "array", "items": { "type": "string" } }, + "rejected": { "type": "string", "minLength": 1, "description": "the evidence must be refused before classification with this code (InadmissibleEvidence); the whole expectation of such a case" } } }, "delta_expect": { diff --git a/scripts/make_examples.py b/scripts/make_examples.py index b7fa6d6..e565d6d 100644 --- a/scripts/make_examples.py +++ b/scripts/make_examples.py @@ -48,15 +48,26 @@ def obs(oid: str, value, value_type: str, coverage=None) -> Observation: ) +def _example_link(i: int) -> dict: + """Odd changes link to the open release 1.2; change 32 delivered release 1.1, closed since, and stays linked.""" + if i % 2: + target, state = "3", "OPEN" + elif i == 2: + target, state = "2", "CLOSED" + else: + return {"target_id": None, "target_state": None, "target_refs": [], "linkage_unresolved": []} + return {"target_id": target, "target_state": state, "target_refs": [{"target_id": target, "state": state}], "linkage_unresolved": []} + + def build_observation_set() -> ObservationSet: commits = [ {"sha": f"{i:040x}", "committed_at": f"2026-08-{10 + i:02d}T09:00:00Z", "title": f"feat: change number {i}"} for i in range(1, 20) ] change_requests = [ - {"number": 40, "id": 40, "title": "Refactor the pricing module", "state": "OPEN", "draft": False, "created_at": "2026-09-02T10:00:00Z", "updated_at": "2026-09-04T10:00:00Z", "merged_at": None, "closed_at": None, "target_id": "3", "target_state": "OPEN", "author": "dev", "url": "https://example.invalid/pull/40"}, - {"number": 41, "id": 41, "title": "Add the export button", "state": "OPEN", "draft": True, "created_at": "2026-09-03T10:00:00Z", "updated_at": "2026-09-05T08:00:00Z", "merged_at": None, "closed_at": None, "target_id": None, "target_state": None, "author": "dev", "url": "https://example.invalid/pull/41"}, + {"number": 40, "id": 40, "title": "Refactor the pricing module", "state": "OPEN", "draft": False, "created_at": "2026-09-02T10:00:00Z", "updated_at": "2026-09-04T10:00:00Z", "merged_at": None, "closed_at": None, "target_id": "3", "target_state": "OPEN", "target_refs": [{"target_id": "3", "state": "OPEN"}], "linkage_unresolved": [], "author": "dev", "url": "https://example.invalid/pull/40"}, + {"number": 41, "id": 41, "title": "Add the export button", "state": "OPEN", "draft": True, "created_at": "2026-09-03T10:00:00Z", "updated_at": "2026-09-05T08:00:00Z", "merged_at": None, "closed_at": None, "target_id": None, "target_state": None, "target_refs": [], "linkage_unresolved": [], "author": "dev", "url": "https://example.invalid/pull/41"}, ] + [ - {"number": 30 + i, "id": 30 + i, "title": f"Merged change {i}", "state": "MERGED", "draft": False, "created_at": f"2026-08-{12 + i:02d}T10:00:00Z", "updated_at": f"2026-08-{13 + i:02d}T10:00:00Z", "merged_at": f"2026-08-{13 + i:02d}T06:00:00Z", "closed_at": f"2026-08-{13 + i:02d}T06:00:00Z", "target_id": "3" if i % 2 else None, "target_state": "OPEN" if i % 2 else None, "author": "dev", "url": f"https://example.invalid/pull/{30 + i}"} + {"number": 30 + i, "id": 30 + i, "title": f"Merged change {i}", "state": "MERGED", "draft": False, "created_at": f"2026-08-{12 + i:02d}T10:00:00Z", "updated_at": f"2026-08-{13 + i:02d}T10:00:00Z", "merged_at": f"2026-08-{13 + i:02d}T06:00:00Z", "closed_at": f"2026-08-{13 + i:02d}T06:00:00Z", **_example_link(i), "author": "dev", "url": f"https://example.invalid/pull/{30 + i}"} for i in range(1, 9) ] issues = [ @@ -104,11 +115,12 @@ def build_observation_set() -> ObservationSet: "history_state": history, "historical_contribution": "VERIFY_FAIL" if history == "FAILURE_OBSERVED" else "VERIFY_PASS", "history_provenance": "ATTEMPT_LEVEL", + "history_complete": True, } ) window_revisions = [c for c in commits if c["committed_at"] >= "2026-08-22"] by_sha = {r["revision"]: r for r in revisions} - series = [by_sha.get(c["sha"], {"revision": c["sha"], "committed_at": c["committed_at"], "parents": [], "current_verdict": None, "history_state": "NO_DECISIVE_OBSERVED", "historical_contribution": None, "history_provenance": None}) for c in window_revisions] + series = [by_sha.get(c["sha"], {"revision": c["sha"], "committed_at": c["committed_at"], "parents": [], "current_verdict": None, "history_state": "NO_DECISIVE_OBSERVED", "historical_contribution": None, "history_provenance": None, "history_complete": True}) for c in window_revisions] result = ObservationSet(subject=SUBJECT, observed_at=OBSERVED_AT) result.add(obs(normalize.INV_REPO, {"id": 123456, "full_name": "acme/widget", "default_branch": "master", "visibility": "public", "has_issues": True, "archived": False, "pushed_at": "2026-09-05T08:00:00Z", "html_url": "https://example.invalid/acme/widget"}, "record")) diff --git a/src/devostasis/__init__.py b/src/devostasis/__init__.py index bc151f8..92d65bc 100644 --- a/src/devostasis/__init__.py +++ b/src/devostasis/__init__.py @@ -1,3 +1,3 @@ """Devostasis: deterministic, model-free vital signs for software repositories.""" -__version__ = "0.1.9" +__version__ = "0.2.0" diff --git a/src/devostasis/adapters/github.py b/src/devostasis/adapters/github.py index 54dcb95..7a998b9 100644 --- a/src/devostasis/adapters/github.py +++ b/src/devostasis/adapters/github.py @@ -71,18 +71,6 @@ class CollectionError(Exception): """The subject could not be identified; no observation set can be produced.""" -class LinkageEvidenceError(CollectionError): - """A change request or issue carries linkage evidence that cannot be read. - - Title, body and milestone are where a target link lives. Provider payload of - the wrong type there is not an absent link: reading it as one would report a - project as unlinked on evidence nobody could parse, which is the silent - absence the register and Direction contracts both forbid. It is a - ``CollectionError`` so the project fails explicitly, keeping its reason, - while every other project in the fleet is still observed. - """ - - @dataclass class ApiFailure(Exception): status_code: int @@ -554,27 +542,43 @@ def _register_title(where: str, value: Any) -> str: return _title(value) -def _linkage_text(where: str, item: dict[str, Any], fields: tuple[str, ...]) -> str: - """Join the free-text fields a target marker can live in, or refuse to guess.""" - parts = [] +def _linkage_text(item: dict[str, Any], fields: tuple[str, ...]) -> tuple[str, list[str]]: + """The readable free-text fields a target marker can live in, and the ones that cannot be read. + + Payload of the wrong type in a title or body is not an absent link: read as + one it would report the change request unlinked on evidence nobody could + parse. It is not a reason to fail the project either. Since + ``PV-DIRECTION-INCOMPLETE-001`` the change request keeps what could be read + and names what could not, and Direction treats its linkage as unresolved + unless a readable reference already links it. + """ + parts: list[str] = [] + unreadable: list[str] = [] for field in fields: value = item.get(field) if value is None or value == "": continue if not isinstance(value, str): - raise LinkageEvidenceError(f"{where}: {field} is {type(value).__name__}, not text, so a target marker cannot be read from it") + unreadable.append(f"{field}:{type(value).__name__}") + continue parts.append(value) - return "\n".join(parts) + return "\n".join(parts), unreadable + +def _milestone_ref(milestone: Any) -> tuple[dict[str, str] | None, str | None]: + """A milestone is a target link: ``(reference, None)``, ``(None, None)`` when absent, ``(None, reason)`` when unreadable. -def _milestone_ref(where: str, milestone: Any) -> dict[str, str] | None: - """A milestone is a target link. Absent is a fact; unreadable is not.""" + The provider embeds the milestone it links, state included, so the + reference is resolved by the change request's own payload: a closed + milestone is still the milestone the work was declared against + (``PV-REV-DIRECTION-CLOSED-TARGET-001``). + """ if not milestone: - return None - if not isinstance(milestone, dict) or milestone.get("number") is None: - raise LinkageEvidenceError(f"{where}: milestone {milestone!r} carries no number, so the target it links to cannot be named") + return None, None + if not isinstance(milestone, dict) or milestone.get("number") is None or isinstance(milestone.get("number"), bool): + return None, "milestone:NO_NUMBER" state = "CLOSED" if str(milestone.get("state") or "").lower() == "closed" else "OPEN" - return {"target_id": str(milestone["number"]), "state": state} + return {"target_id": str(milestone["number"]), "state": state}, None def _normalize_date(value: Any) -> str | None: @@ -593,10 +597,20 @@ def _normalize_date(value: Any) -> str | None: def marker_target_ids(text: str | None, marker: str) -> list[str]: - """Explicit target references: every `` `` occurrence, in order, without duplicates.""" + """Explicit target references: every standalone `` `` occurrence, in order, without duplicates. + + The configured marker is a token of its own, never text inside a larger one + (``PV-AUDIT-TARGET-MARKER-SYNTAX-001``): it starts the text or follows a + character that cannot continue a word, and a marker that ends in a word + character is not followed by another. ``SubTarget: B1``, ``NotTarget: B1`` + and ``PreTarget:B1`` therefore link nothing under ``Target:``, while + ``Target: B1`` at the start of a line, after a space or inside + ``(Target: B1)`` does. The marker is matched exactly, case included. + """ if not text or not marker: return [] - pattern = re.compile(re.escape(marker) + r"[ \t]*([A-Za-z0-9][A-Za-z0-9._/-]*)") + tail = r"(?!\w)" if (marker[-1].isalnum() or marker[-1] == "_") else "" + pattern = re.compile(r"(? dict[str, Any]: raise _payload_failure(path, f"commit {sha} carries no committer or author date") return {"sha": sha, "committed_at": committed, "title": _title(info.get("message"))} - def _target_refs(self, pull: dict[str, Any], project: ResolvedProject, target_states: dict[str, str] | None) -> list[dict[str, str]]: + def _target_refs(self, pull: dict[str, Any], project: ResolvedProject, target_states: dict[str, str] | None) -> tuple[list[dict[str, str]], list[str]]: + """The explicit target references of a change request and what of its linkage evidence could not be read. + + Each reference carries the state its resolution established: + ``OPEN`` or ``CLOSED`` when the declared target resolves (state-neutral + linkage, ``PV-REV-DIRECTION-CLOSED-TARGET-001``), ``MISSING`` when the + complete register positively lacks the id (a broken reference, not + missing evidence), ``UNKNOWN`` when the register could not be read, so + nothing about the id is known (``PV-DIRECTION-INCOMPLETE-001``). + """ source = project.planning["source"] - where = f"change request #{pull.get('number')}" if source == "milestones": - ref = _milestone_ref(where, pull.get("milestone") or None) - return [ref] if ref else [] + ref, problem = _milestone_ref(pull.get("milestone") or None) + return ([ref] if ref else []), ([problem] if problem else []) if source == "file": marker = project.planning.get("link_marker") or "Target:" - text = _linkage_text(where, pull, ("title", "body")) - states = target_states or {} - return [{"target_id": tid, "state": states.get(tid, "UNKNOWN")} for tid in marker_target_ids(text, marker)] - return [] + text, unreadable = _linkage_text(pull, ("title", "body")) + ids = marker_target_ids(text, marker) + if target_states is None: + return [{"target_id": tid, "state": "UNKNOWN"} for tid in ids], unreadable + return [{"target_id": tid, "state": target_states.get(tid, "MISSING")} for tid in ids], unreadable + return [], [] def _collect_change_requests(self, obs: ObservationSet, base: str, project: ResolvedProject, target_states: dict[str, str] | None) -> None: since = timeutil.minus_days(self.now, FLOW["window_days"]) @@ -958,7 +982,7 @@ def _change_request_record(self, path: str, pull: dict[str, Any], project: Resol state = "OPEN" user = pull.get("user") author = None if user is None else _optional_text(path, _object(path, user, f"{where} user").get("login"), f"{where} user.login") - refs = self._target_refs(pull, project, target_states) + refs, linkage_unresolved = self._target_refs(pull, project, target_states) return { "number": number, "id": _optional_integer(path, pull.get("id"), f"{where} id"), @@ -972,6 +996,7 @@ def _change_request_record(self, path: str, pull: dict[str, Any], project: Resol "target_id": refs[0]["target_id"] if refs else None, "target_state": refs[0]["state"] if refs else None, "target_refs": refs, + "linkage_unresolved": sorted(linkage_unresolved), "author": author, "url": _optional_text(path, pull.get("html_url"), f"{where} html_url"), } @@ -989,7 +1014,7 @@ def _issue_record(path: str, issue: dict[str, Any]) -> dict[str, Any]: names.append(_text(path, label.get("name"), f"{where} labels[{position}].name")) user = issue.get("user") author = None if user is None else _optional_text(path, _object(path, user, f"{where} user").get("login"), f"{where} user.login") - milestone = _milestone_ref(where, issue.get("milestone") or None) + milestone, milestone_problem = _milestone_ref(issue.get("milestone") or None) state_text = _optional_text(path, issue.get("state"), f"{where} state") return { "number": number, @@ -1001,6 +1026,7 @@ def _issue_record(path: str, issue: dict[str, Any]) -> dict[str, Any]: "closed_at": _optional_timestamp(path, issue.get("closed_at"), f"{where} closed_at"), "labels": sorted(names), "target_id": milestone["target_id"] if milestone else None, + "linkage_unresolved": [milestone_problem] if milestone_problem else [], "author": author, "url": _optional_text(path, issue.get("html_url"), f"{where} html_url"), } diff --git a/src/devostasis/adapters/github_ci.py b/src/devostasis/adapters/github_ci.py index d72f3da..d5430b2 100644 --- a/src/devostasis/adapters/github_ci.py +++ b/src/devostasis/adapters/github_ci.py @@ -6,7 +6,12 @@ * the greatest observed attempt governs a parent's current state; * history is failure-sticky: any observed VERIFY_FAIL for a revision keeps its historical contribution VERIFY_FAIL while the revision is in the window; -* attempts are never independent samples. +* attempts are never independent samples; +* history is known only where every attempt is: an Actions run whose attempts + cover 1..latest. A check suite keeps its id across re-requests and shows + only the latest outcome, so favorable parent-level or incomplete evidence is + ``UNKNOWN_HISTORY``, never a reconstructed pass (PV-CI-UNIT-004 R52, + PV-HIST-002 HIST-06). An observed failure is proven on any surface. """ from __future__ import annotations @@ -36,6 +41,8 @@ ATTEMPT_LEVEL = "ATTEMPT_LEVEL" PARENT_LEVEL_ONLY = "PARENT_LEVEL_ONLY" +ACTIONS_KIND = "github_actions_workflow_run" +UNKNOWN_HISTORY = "UNKNOWN_HISTORY" def normalize_outcome(status: str | None, conclusion: str | None) -> str: @@ -63,6 +70,15 @@ def history_state(states: list[str]) -> str: return "NO_DECISIVE_OBSERVED" +def known_history_state(states: list[str], complete: bool) -> str: + """The state evidence proves: a failure always, anything favorable only over complete attempts.""" + if VERIFY_FAIL in states: + return "FAILURE_OBSERVED" + if not complete: + return UNKNOWN_HISTORY + return history_state(states) + + def contribution(state: str) -> str | None: return {"FAILURE_OBSERVED": VERIFY_FAIL, "PASS_ONLY_OBSERVED": VERIFY_PASS}.get(state) @@ -77,6 +93,7 @@ def actions_parent(run: dict[str, Any], prior_attempts: list[dict[str, Any]]) -> attempts.sort(key=lambda a: a["attempt"]) current = attempts[-1]["state"] states = [a["state"] for a in attempts] + complete = len(attempts) == latest_attempt return { "parent_id": f"github_actions:workflow_run:{run['id']}", "kind": "github_actions_workflow_run", @@ -85,8 +102,8 @@ def actions_parent(run: dict[str, Any], prior_attempts: list[dict[str, Any]]) -> "current_attempt": latest_attempt, "current_state": current, "attempts_observed": attempts, - "attempts_complete": len(attempts) == latest_attempt, - "history_state": history_state(states), + "attempts_complete": complete, + "history_state": known_history_state(states, complete), "url": run.get("html_url"), } @@ -103,7 +120,7 @@ def check_suite_parent(suite: dict[str, Any]) -> dict[str, Any]: "current_state": state, "attempts_observed": [{"attempt": None, "state": state}], "attempts_complete": False, - "history_state": history_state([state]), + "history_state": known_history_state([state], False), "url": suite.get("url"), } @@ -118,10 +135,11 @@ def build_revision_records( parents = sorted(parents_by_sha.get(commit["sha"], []), key=lambda p: p["parent_id"]) current_states = [p["current_state"] for p in parents] all_states = [a["state"] for p in parents for a in p["attempts_observed"]] - history = history_state(all_states) if parents else "NO_DECISIVE_OBSERVED" + complete = all(p["kind"] == ACTIONS_KIND and p.get("attempts_complete") is True for p in parents) + history = known_history_state(all_states, complete) if parents else "NO_DECISIVE_OBSERVED" provenance = None if parents: - provenance = ATTEMPT_LEVEL if all(p["kind"] == "github_actions_workflow_run" for p in parents) else PARENT_LEVEL_ONLY + provenance = ATTEMPT_LEVEL if all(p["kind"] == ACTIONS_KIND for p in parents) else PARENT_LEVEL_ONLY records.append( { "revision": commit["sha"], @@ -131,6 +149,7 @@ def build_revision_records( "history_state": history, "historical_contribution": contribution(history), "history_provenance": provenance, + "history_complete": complete, } ) return records diff --git a/src/devostasis/bundle.py b/src/devostasis/bundle.py index aa2d689..298d06d 100644 --- a/src/devostasis/bundle.py +++ b/src/devostasis/bundle.py @@ -67,6 +67,7 @@ IDENTITY_FIELD_MISMATCH = "IDENTITY_FIELD_MISMATCH" RECEIPT_DIGEST_MISMATCH = "RECEIPT_DIGEST_MISMATCH" RECEIPT_COPY_MISMATCH = "RECEIPT_COPY_MISMATCH" +HISTORY_SOURCE_MISMATCH = "HISTORY_SOURCE_MISMATCH" OBSERVATIONS_DIGEST_MISMATCH = "OBSERVATIONS_DIGEST_MISMATCH" UNSUPPORTED_LINEAGE = "UNSUPPORTED_ARTIFACT_LINEAGE" RENDERER_NOT_IN_LINEAGE = "RENDERER_VERSION_NOT_IN_LINEAGE" @@ -452,6 +453,48 @@ def _check_evidence_binding(members: dict[str, bytes], manifest: dict[str, Any], declared = manifest["members"]["observations.json"] if isinstance(snapshot, dict) and snapshot.get("observations_digest") != declared: problems.append(f"{OBSERVATIONS_DIGEST_MISMATCH}: snapshot.json was evaluated over {snapshot.get('observations_digest')}, the bundle carries {declared}") + problems.extend(_check_history_source(members, manifest, observations)) + return problems + + +def _history_source_named(snapshot: Any) -> tuple[str | None, str | None]: + """The status and bundle of the durable history source the Integrity result names, if it names one.""" + if not isinstance(snapshot, dict): + return None, None + for vital in snapshot.get("vitals") or []: + if isinstance(vital, dict) and vital.get("vital_id") == "integrity": + history = (vital.get("derived") or {}).get("revision_history") if isinstance(vital.get("derived"), dict) else None + source = history.get("source") if isinstance(history, dict) else None + if isinstance(source, dict): + return source.get("status"), source.get("bundle_id") + return None, None + + +def _check_history_source(members: dict[str, bytes], manifest: dict[str, Any], observations: Any) -> list[str]: + """PV-HIST-002 section C: the carried history came from the bundle this one follows, and says so. + + The selection of the source is auditable only if the bundle names it; a + bundle whose carried history names another bundle than the one its + manifest says it follows consumed history from outside its own chain. + Bundles older than the carrier name nothing and are not checked. + """ + problems: list[str] = [] + previous = manifest.get("previous_bundle_id") + try: + snapshot = canonical.loads(members["snapshot.json"].decode("utf-8")) if "snapshot.json" in members else None + except Exception: # noqa: BLE001 + snapshot = None + status, source = _history_source_named(snapshot) + if status == "CARRIED" and source != previous: + problems.append(f"{HISTORY_SOURCE_MISMATCH}: the Integrity history was carried from {source}, the manifest follows {previous}") + if isinstance(observations, dict): + for item in observations.get("observations") or []: + if not isinstance(item, dict) or item.get("observation_id") != "ci.revision_history_carried": + continue + value = item.get("value") + named = value.get("source_bundle_id") if isinstance(value, dict) else None + if named is not None and named != previous: + problems.append(f"{HISTORY_SOURCE_MISMATCH}: observations.json carries history from {named}, the manifest follows {previous}") return problems diff --git a/src/devostasis/cli.py b/src/devostasis/cli.py index 14eadea..2888904 100644 --- a/src/devostasis/cli.py +++ b/src/devostasis/cli.py @@ -19,6 +19,7 @@ from .history import FilesystemHistoryStore, HistoryStoreError from .observations import ObservationSet from .runner import FleetSurfaceError, build_from_observations, evaluate, observe, run_all, write_fleet_index +from .vitals.common import InadmissibleEvidence TOKEN_ENVS = ("DEVOSTASIS_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN") @@ -525,5 +526,11 @@ def main(argv: list[str] | None = None) -> int: except InputError as exc: print(f"input error: {exc}", file=sys.stderr) return 2 + except InadmissibleEvidence as exc: + # Counts that cannot all be true are refused before classification + # (PV-DIRECTION-INCOMPLETE-001, PV-HORIZON-PARTIAL-001): the saved + # observation set is invalid input, not a Vital without a band. + print(f"input error: the observations contradict themselves: {exc}", file=sys.stderr) + return 2 except KeyboardInterrupt: return 130 diff --git a/src/devostasis/contracts.py b/src/devostasis/contracts.py index 08a161e..e1cd2c1 100644 --- a/src/devostasis/contracts.py +++ b/src/devostasis/contracts.py @@ -10,9 +10,13 @@ OBSERVATION_CONTRACT_VERSION = "RAW-OBS-V0" # Seven-Vital taxonomy: accepted PV-VITALS-V1-002 (units PV-VIT-010/012). -# Per-Vital rule versions (``rule_id`` in the snapshot) carry the calibration -# repairs adopted on top of it: flow.bands.v1 (PV-FLOW-EMPTY-QUEUE-001, -# PV-FLOW-MERGE-LATENCY-001) and pulse.bands.v1 (PV-PULSE-REQUIRED-LOWER-BOUND-001). +# Per-Vital rule versions (``rule_id`` in the snapshot) carry the repairs +# adopted on top of it: flow.bands.v1 (PV-FLOW-EMPTY-QUEUE-001, +# PV-FLOW-MERGE-LATENCY-001), pulse.bands.v1 (PV-PULSE-REQUIRED-LOWER-BOUND-001), +# clutter.bands.v1 (PV-CLUTTER-INCOMPLETE-001), and since 0.2.0 +# direction.bands.v2 (PV-REV-DIRECTION-CLOSED-TARGET-001, PV-DIRECTION-INCOMPLETE-001), +# horizon.bands.v2 (PV-HORIZON-PARTIAL-001), debt.bands.v2 (PV-DEBT-PARTIAL-001) +# and integrity.bands.v1+ci-unit-004+hist-002 (PV-HIST-002); vitals.md lists them. VITALS_CONTRACT_VERSION = "PV-VITALS-V1-002" # Integrity revision/verification semantics: accepted PV-CI-UNIT-004 (PV-VIT-011). diff --git a/src/devostasis/history.py b/src/devostasis/history.py index 15baa6b..5caf7d0 100644 --- a/src/devostasis/history.py +++ b/src/devostasis/history.py @@ -89,6 +89,10 @@ class LatestState: manifest: dict[str, Any] | None snapshot: dict[str, Any] | None problems: list[str] + # The observation set the bundle was evaluated over, when it keeps one: the + # replay source of durable revision history for a bundle older than the + # carrier in its snapshot (PV-HIST-002, HIST-13). + observations: dict[str, Any] | None = None @dataclass @@ -405,6 +409,7 @@ def latest(self, project_key: str, immutable_project_id: str | None = None) -> L try: manifest = canonical.loads(members["manifest.json"].decode("utf-8")) snapshot = canonical.loads(members["snapshot.json"].decode("utf-8")) if "snapshot.json" in members else None + observations = canonical.loads(members["observations.json"].decode("utf-8")) if "observations.json" in members else None except Exception as exc: # noqa: BLE001 return LatestState(True, False, expected_id, None, None, problems + [f"immutable bundle unreadable: {type(exc).__name__}"]) if not isinstance(manifest, dict): @@ -423,7 +428,7 @@ def latest(self, project_key: str, immutable_project_id: str | None = None) -> L copied_id = None if copied_id is not None and copied_id != manifest.get("bundle_id") and not self._is_stale_copy(index, copied_id, manifest): problems.append(f"latest pointer {copied_id} differs from index tail {manifest.get('bundle_id')}") - return LatestState(True, not problems, manifest.get("bundle_id"), manifest, snapshot, problems) + return LatestState(True, not problems, manifest.get("bundle_id"), manifest, snapshot, problems, observations if isinstance(observations, dict) else None) @staticmethod def _is_stale_copy(index: dict[str, Any], copied_id: str, tail_manifest: dict[str, Any]) -> bool: diff --git a/src/devostasis/normalize.py b/src/devostasis/normalize.py index 6995c92..a552e80 100644 --- a/src/devostasis/normalize.py +++ b/src/devostasis/normalize.py @@ -12,7 +12,7 @@ from . import canonical, timeutil from .config import ResolvedProject -from .observations import AVAILABLE, OBSERVED_SUBSET_COUNT, PARTIAL, UNAVAILABLE, VALUE_BEARING, VALUE_SEMANTICS, Observation, ObservationSet +from .observations import AVAILABLE, FRESH, OBSERVED_SUBSET_COUNT, PARTIAL, UNAVAILABLE, VALUE_BEARING, VALUE_SEMANTICS, Observation, ObservationSet from .policy import CLUTTER, FLOW, PLANNING, PULSE INV_REPO = "forge.repository.metadata" @@ -34,6 +34,16 @@ MEDIAN_HOURS = "forge.change_requests.median_time_to_merge_hours_28d" MEDIAN_HOURS_NOTE = "whole-hour projection of median_time_to_merge_seconds_28d for presentation; never a classifier input" +# Direction's classifier reads state-neutral linkage (direction.bands.v2). The +# open-target count it read before is kept for presentation only, as the +# accepted judgement allows (PV-REV-DIRECTION-CLOSED-TARGET-001). +LINKAGE_LINKED = "planning.linkage.active_change_requests_linked_count_28d" +LINKAGE_UNLINKED = "planning.linkage.active_change_requests_unlinked_count_28d" +LINKAGE_UNRESOLVED = "planning.linkage.active_change_requests_unresolved_count_28d" +LINKAGE_UNRESOLVED_ITEMS = "planning.linkage.unresolved_change_requests_28d" +LINKAGE_MISSING_REFS = "planning.linkage.missing_target_reference_count_28d" +LINKED_TO_OPEN_NOTE = "active change requests linked to a target that is open now; presentation only since direction.bands.v2, never a classifier input" + def _derived( obs: ObservationSet, @@ -116,6 +126,40 @@ def target_refs(item: dict[str, Any]) -> list[dict[str, Any]]: return [] +# Direction linkage evidence per active change request (PV-DIRECTION-INCOMPLETE-001, +# section 3). A reference resolves when its declared target does, open or closed +# alike (PV-REV-DIRECTION-CLOSED-TARGET-001); MISSING is a complete register +# positively lacking the id, a broken reference and therefore unlinked; every +# other state, an unreadable title, body or milestone, or a reference of an +# older record shape without a state, is evidence nobody could resolve. +LINKED = "LINKED" +UNLINKED = "UNLINKED" +UNRESOLVED = "UNRESOLVED" +RESOLVED_TARGET_STATES = ("OPEN", "CLOSED") +MISSING_TARGET = "MISSING" + + +def linkage_state(item: dict[str, Any]) -> str: + """LINKED, UNLINKED or UNRESOLVED for one active change request.""" + refs = target_refs(item) + if any(ref.get("state") in RESOLVED_TARGET_STATES for ref in refs): + return LINKED + if item.get("linkage_unresolved") or any(ref.get("state") != MISSING_TARGET for ref in refs): + return UNRESOLVED + return UNLINKED + + +def unresolved_linkage_reasons(item: dict[str, Any]) -> list[str]: + """Why an UNRESOLVED change request is unresolved, in a stable order.""" + reasons = [f"TARGET_UNRESOLVED:{ref['target_id']}:{ref.get('state')}" for ref in target_refs(item) if ref.get("state") not in RESOLVED_TARGET_STATES + (MISSING_TARGET,)] + unreadable = item.get("linkage_unresolved") + if isinstance(unreadable, list): + reasons.extend(f"LINKAGE_UNREADABLE:{reason}" for reason in unreadable) + elif unreadable: + reasons.append(f"LINKAGE_UNREADABLE:{unreadable}") + return sorted(set(reasons)) + + def derive(obs: ObservationSet, project: ResolvedProject) -> ObservationSet: observed_at = timeutil.parse_ts(obs.observed_at) since_pulse = timeutil.minus_days(observed_at, PULSE["window_days"]) @@ -182,26 +226,40 @@ def _updated_in_window(items): def _active(items): return [item for item in items if item.get("updated_at") and timeutil.parse_ts(item["updated_at"]) >= since_planning] - def _open_refs(item): - return [ref for ref in target_refs(item) if ref.get("state") == "OPEN"] + def _in_state(items, state): + return [item for item in _active(items) if linkage_state(item) == state] - def _linked(items): - return [item for item in _active(items) if _open_refs(item)] + def _linked_to_open(items): + return [item for item in _active(items) if any(ref.get("state") == "OPEN" for ref in target_refs(item))] def _links_per_target(items): + # State-neutral: a closed target still carries the links of the work + # declared against it (PV-REV-DIRECTION-CLOSED-TARGET-001, DIR-CLOSED-08). counts: dict[str, int] = {} for item in _active(items): - for ref in _open_refs(item): - counts[str(ref["target_id"])] = counts.get(str(ref["target_id"]), 0) + 1 + for ref in target_refs(item): + if ref.get("state") in RESOLVED_TARGET_STATES: + counts[str(ref["target_id"])] = counts.get(str(ref["target_id"]), 0) + 1 return dict(sorted(counts.items())) - def _unknown_refs(items): - return len([ref for item in _active(items) for ref in target_refs(item) if ref.get("state") == "UNKNOWN"]) + def _refs_in_state(items, state): + return len([ref for item in _active(items) for ref in target_refs(item) if ref.get("state") == state]) + + def _unresolved_items(items): + return {str(item.get("number")): unresolved_linkage_reasons(item) for item in _in_state(items, UNRESOLVED)} _derived(obs, "planning.linkage.active_change_requests_count_28d", "count", crs, lambda items: len(_active(items)), complete=window_ok, subset_count=True) - _derived(obs, "planning.linkage.active_change_requests_linked_to_open_target_count_28d", "count", crs, lambda items: len(_linked(items)), complete=window_ok, subset_count=True) + _derived(obs, LINKAGE_LINKED, "count", crs, lambda items: len(_in_state(items, LINKED)), complete=window_ok, subset_count=True) + _derived(obs, LINKAGE_UNLINKED, "count", crs, lambda items: len(_in_state(items, UNLINKED)), complete=window_ok, subset_count=True) + _derived(obs, LINKAGE_UNRESOLVED, "count", crs, lambda items: len(_in_state(items, UNRESOLVED)), complete=window_ok, subset_count=True) + _derived(obs, LINKAGE_UNRESOLVED_ITEMS, "record", crs, _unresolved_items, complete=window_ok) + _derived( + obs, "planning.linkage.active_change_requests_linked_to_open_target_count_28d", "count", crs, + lambda items: len(_linked_to_open(items)), complete=window_ok, subset_count=True, notes=LINKED_TO_OPEN_NOTE, + ) _derived(obs, "planning.linkage.links_per_target_28d", "record", crs, _links_per_target, complete=window_ok) - _derived(obs, "planning.linkage.unknown_target_reference_count_28d", "count", crs, _unknown_refs, complete=window_ok, subset_count=True) + _derived(obs, "planning.linkage.unknown_target_reference_count_28d", "count", crs, lambda items: _refs_in_state(items, "UNKNOWN"), complete=window_ok, subset_count=True) + _derived(obs, LINKAGE_MISSING_REFS, "count", crs, lambda items: _refs_in_state(items, MISSING_TARGET), complete=window_ok, subset_count=True) issues = obs.get(INV_ISSUES) if issues is not None: @@ -315,13 +373,17 @@ def _derive_planning(obs: ObservationSet, project: ResolvedProject, observed_at) if targets is None: obs.add(Observation(observation_id="planning.explicit_targets.capability", status="UNKNOWN", value_type="enum", reason_code="TARGETS_NOT_COLLECTED", **base)) return - if not targets.good: + # A fresh incomplete enumeration that returned a target proves support: more + # records can only add targets (PV-HORIZON-PARTIAL-001, section 3). One that + # returned none proves neither support nor its absence. + proven_subset = targets.status == PARTIAL and targets.freshness == FRESH and isinstance(targets.value, list) and bool(targets.value) + if not targets.good and not proven_subset: obs.add( Observation( observation_id="planning.explicit_targets.capability", status=targets.status if targets.status not in VALUE_BEARING else PARTIAL, value_type="enum", - value="SUPPORTED" if targets.status == PARTIAL else None, + value="SUPPORTED" if (targets.status == PARTIAL and targets.value) else None, reason_code=targets.reason_code or "TARGETS_INCOMPLETE", provider=targets.provider, collected_at=targets.collected_at, source_ref=f"derived:{INV_TARGETS}", adapter_version=targets.adapter_version, freshness=targets.freshness, @@ -335,14 +397,18 @@ def _derive_planning(obs: ObservationSet, project: ResolvedProject, observed_at) observation_id="planning.explicit_targets.capability", status=AVAILABLE, value_type="enum", value=capability, provider=targets.provider, collected_at=targets.collected_at, source_ref=f"derived:{INV_TARGETS}", adapter_version=targets.adapter_version, freshness=targets.freshness, evidence_ref={"derived_from": [INV_TARGETS]}, + coverage=None if targets.good else {"complete": True, "basis": "RETURNED_TARGET_RECORD", "source_coverage": targets.coverage}, + notes=None if targets.good else "support is proved by a returned target record; the target enumeration itself is incomplete", ) ) open_items = [i for i in items if i.get("state") == "OPEN"] future = [i for i in open_items if i.get("due_at") and timeutil.parse_ts(i["due_at"]) > observed_at] beyond = [i for i in future if timeutil.parse_ts(i["due_at"]) > horizon_edge] - _derived(obs, "planning.explicit_targets.open_count", "count", targets, lambda _: len(open_items)) - _derived(obs, "planning.explicit_targets.open_with_future_boundary_count", "count", targets, lambda _: len(future)) - _derived(obs, "planning.explicit_targets.open_beyond_28d_count", "count", targets, lambda _: len(beyond)) + # Counts over the returned targets; over an incomplete enumeration each is an + # observed-subset lower bound and says so (PV-REV-PR-031-003, issue #39). + _derived(obs, "planning.explicit_targets.open_count", "count", targets, lambda _: len(open_items), subset_count=True) + _derived(obs, "planning.explicit_targets.open_with_future_boundary_count", "count", targets, lambda _: len(future), subset_count=True) + _derived(obs, "planning.explicit_targets.open_beyond_28d_count", "count", targets, lambda _: len(beyond), subset_count=True) if future: nearest = min(int((timeutil.parse_ts(i["due_at"]) - observed_at).total_seconds() // 86400) for i in future) _derived(obs, "planning.explicit_targets.nearest_future_boundary_days", "duration", targets, lambda _: nearest) diff --git a/src/devostasis/revision_history.py b/src/devostasis/revision_history.py new file mode 100644 index 0000000..518e203 --- /dev/null +++ b/src/devostasis/revision_history.py @@ -0,0 +1,465 @@ +"""Durable Integrity revision history across bundles (PV-HIST-002, accepted by PV-REV-HIST-002). + +A provider forgets: a re-run hides the attempt that failed behind the one that +passed on a parent-level surface, and retention removes old runs altogether. +Integrity's history is a set property of one immutable revision +(``ANY_FAIL_ELSE_ANY_PASS_PER_IMMUTABLE_REVISION``, PV-CI-UNIT-004), so what +one bundle proved about a revision must survive into the next one while the +revision is in the 14-day window. + +The carrier is the union of every attempt observed per parent per revision. +Each bundle's union is the previous bundle's union plus what the provider +shows now, which makes it the complete-chain-equivalent reduction section C +of the contract allows: it holds every history fact of the intervening chain +and selects nothing. It is carried in two canonical places: + +* ``derived.revision_history`` of the Integrity result in ``snapshot.json``, + the durable output, present in every bundle whatever its observations + setting; +* ``ci.revision_history_carried``, the input observation a build adds from its + immediate predecessor before evaluating, so the bundle names the source it + consumed and a replay of its observations reproduces its evaluation. + +The state of a revision is a function of its union alone: + +* a ``VERIFY_FAIL`` attempt anywhere is ``FAILURE_OBSERVED``, absorbing, from + current evidence or durable history alike (HIST-01, -02, -05, -09); +* otherwise the history is known only when every parent's attempts are + complete: an Actions run whose attempts cover 1..latest. A check suite never + is, because a re-request keeps its id and hides the earlier outcome, so + favorable parent-level or incomplete evidence is ``UNKNOWN_HISTORY``, never + a reconstructed pass (R52, HIST-06, HIST-19); +* then ``PASS_ONLY_OBSERVED`` or ``NO_DECISIVE_OBSERVED``. + +A revision whose prior history could not be carried under the current +lineage stays ``UNKNOWN_HISTORY`` until complete current evidence repairs it +(HIST-15). Current verdicts are never carried: they belong to the current +observation (HIST-16). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from . import timeutil +from .observations import AVAILABLE, FRESH, PARTIAL, UNAVAILABLE, UNKNOWN, Observation, ObservationSet +from .policy import INTEGRITY + +# The history-semantics lineage of this implementation: provider outcome +# normalization (devostasis.ci-outcomes.github.v1), parent and attempt +# identity, the union reduction and the PV-CI-UNIT-004 state mapping with +# R52's completeness rule. Only a change to one of those moves it (section 5). +LINEAGE = "devostasis.ci-history.v2" +# The revision records 0.1.x bundles carry in ci.revision_verdicts_14d: the +# same outcome map and parent identity with every observed attempt kept, so a +# union can be replayed from them deterministically (HIST-13). +REPLAYABLE_LINEAGE = "devostasis.ci-history.v1" + +CARRIED = "ci.revision_history_carried" +REVISIONS = "ci.revision_verdicts_14d" + +FAILURE_OBSERVED = "FAILURE_OBSERVED" +PASS_ONLY_OBSERVED = "PASS_ONLY_OBSERVED" +NO_DECISIVE_OBSERVED = "NO_DECISIVE_OBSERVED" +UNKNOWN_HISTORY = "UNKNOWN_HISTORY" +STATES = (FAILURE_OBSERVED, PASS_ONLY_OBSERVED, NO_DECISIVE_OBSERVED, UNKNOWN_HISTORY) +CONTRIBUTION = {FAILURE_OBSERVED: "VERIFY_FAIL", PASS_ONLY_OBSERVED: "VERIFY_PASS", NO_DECISIVE_OBSERVED: None, UNKNOWN_HISTORY: None} + +ACTIONS_KIND = "github_actions_workflow_run" + +# Where the carried history came from. +SOURCE_CARRIED = "CARRIED" +SOURCE_BASELINE = "BASELINE" +SOURCE_GAP = "HISTORY_GAP" +SOURCE_NONE = "NOT_STATED" +SOURCE_MALFORMED = "MALFORMED" +BASIS_SNAPSHOT = "SNAPSHOT" +BASIS_SERIES = "REPLAYED_SERIES" + +NO_PREVIOUS_BUNDLE = "NO_PREVIOUS_BUNDLE" +PREDECESSOR_NOT_EARLIER = "PREDECESSOR_NOT_EARLIER" +PREDECESSOR_CARRIES_NO_HISTORY = "PREDECESSOR_CARRIES_NO_HISTORY" +HISTORY_GAP = "HISTORY_GAP" + + +class HistoryShapeError(ValueError): + """Carried history that is not the shape its lineage declares: corrupt persisted history.""" + + +# --------------------------------------------------------------------------- the union + + +def _attempt_key(attempt: dict[str, Any]) -> tuple[int, int, str]: + number = attempt.get("attempt") + return (0 if number is None else 1, number if isinstance(number, int) else 0, str(attempt.get("state"))) + + +def _attempts(raw: Any, where: str) -> list[dict[str, Any]]: + if not isinstance(raw, list): + raise HistoryShapeError(f"{where}: attempts is not a list") + seen: dict[tuple[Any, str], dict[str, Any]] = {} + for attempt in raw: + if not isinstance(attempt, dict) or not isinstance(attempt.get("state"), str): + raise HistoryShapeError(f"{where}: an attempt carries no state") + number = attempt.get("attempt") + if number is not None and (isinstance(number, bool) or not isinstance(number, int) or number < 1): + raise HistoryShapeError(f"{where}: attempt number {number!r} is not a positive integer") + seen[(number, attempt["state"])] = {"attempt": number, "state": attempt["state"]} + return sorted(seen.values(), key=_attempt_key) + + +def _parent(parent_id: Any, kind: Any, latest: Any, attempts: Any, where: str) -> dict[str, Any]: + if not isinstance(parent_id, str) or not parent_id: + raise HistoryShapeError(f"{where}: a parent carries no id") + if not isinstance(kind, str) or not kind: + raise HistoryShapeError(f"{where}: parent {parent_id} carries no kind") + if latest is not None and (isinstance(latest, bool) or not isinstance(latest, int) or latest < 1): + raise HistoryShapeError(f"{where}: parent {parent_id} latest attempt {latest!r} is not a positive integer") + return {"parent_id": parent_id, "kind": kind, "latest_attempt": latest, "attempts": _attempts(attempts, f"{where} parent {parent_id}")} + + +def parent_from_current(parent: Any, where: str) -> dict[str, Any]: + """A parent of a current revision record (github_ci shape) as durable evidence.""" + if not isinstance(parent, dict): + raise HistoryShapeError(f"{where}: a parent is not an object") + return _parent(parent.get("parent_id"), parent.get("kind"), parent.get("current_attempt"), parent.get("attempts_observed"), where) + + +def parents_from_groups(groups: Any, where: str) -> list[dict[str, Any]]: + """The parents of a carried union record, which the carrier stores grouped by shape.""" + if not isinstance(groups, list): + raise HistoryShapeError(f"{where}: parent_groups is not a list") + parents: list[dict[str, Any]] = [] + seen: set[str] = set() + for group in groups: + if not isinstance(group, dict) or not isinstance(group.get("parent_ids"), list) or not group["parent_ids"]: + raise HistoryShapeError(f"{where}: a parent group names no parents") + for parent_id in group["parent_ids"]: + if parent_id in seen: + raise HistoryShapeError(f"{where}: parent {parent_id} is in two groups") + seen.add(parent_id) + parents.append(_parent(parent_id, group.get("kind"), group.get("latest_attempt"), group.get("attempts"), where)) + return parents + + +def parent_groups(parents: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Parents of one shape (kind, greatest attempt, observed attempts) stored once with their ids. + + A busy default branch attaches hundreds of single-attempt runs to one + revision; written one object per run, the carrier of such a repository + was hundreds of kilobytes. The grouping loses nothing: every parent keeps + its identity and exactly its own attempts. + """ + groups: dict[tuple, dict[str, Any]] = {} + for parent in parents: + key = (parent["kind"], -1 if parent["latest_attempt"] is None else parent["latest_attempt"], tuple(_attempt_key(a) for a in parent["attempts"])) + group = groups.setdefault(key, {"kind": parent["kind"], "latest_attempt": parent["latest_attempt"], "attempts": parent["attempts"], "parent_ids": []}) + group["parent_ids"].append(parent["parent_id"]) + ordered = [groups[key] for key in sorted(groups)] + for group in ordered: + group["parent_ids"] = sorted(group["parent_ids"]) + return ordered + + +def merge_parents(*groups: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[str]]: + """The union of parents by identity: every attempt either side observed, the greatest attempt number either saw.""" + merged: dict[str, dict[str, Any]] = {} + conflicts: list[str] = [] + for group in groups: + for parent in group: + known = merged.get(parent["parent_id"]) + if known is None: + merged[parent["parent_id"]] = {**parent, "attempts": list(parent["attempts"])} + continue + if known["kind"] != parent["kind"]: + conflicts.append(f"PARENT_KIND_CONFLICT:{parent['parent_id']}") + numbers = [n for n in (known["latest_attempt"], parent["latest_attempt"]) if n is not None] + known["latest_attempt"] = max(numbers) if numbers else None + known["attempts"] = _attempts(known["attempts"] + parent["attempts"], parent["parent_id"]) + return [merged[key] for key in sorted(merged)], sorted(set(conflicts)) + + +def parent_complete(parent: dict[str, Any]) -> bool: + """Every attempt of an Actions run, 1..latest, is in the union. A check suite never is.""" + latest = parent.get("latest_attempt") + if parent.get("kind") != ACTIONS_KIND or not isinstance(latest, int) or latest < 1: + return False + numbers = {a["attempt"] for a in parent["attempts"] if isinstance(a.get("attempt"), int)} + return set(range(1, latest + 1)) <= numbers + + +def union_state(parents: list[dict[str, Any]], unresolved: list[str]) -> tuple[str, bool]: + """The PV-CI-UNIT-004 state of a revision's union, and whether its history is complete.""" + states = [a["state"] for p in parents for a in p["attempts"]] + complete = not unresolved and all(parent_complete(p) for p in parents) + if "VERIFY_FAIL" in states: + return FAILURE_OBSERVED, complete + if not complete: + return UNKNOWN_HISTORY, False + if "VERIFY_PASS" in states: + return PASS_ONLY_OBSERVED, True + return NO_DECISIVE_OBSERVED, True + + +def union_record(revision: str, committed_at: str, parents: list[dict[str, Any]], unresolved: list[str]) -> dict[str, Any]: + state, complete = union_state(parents, unresolved) + record: dict[str, Any] = { + "revision": revision, + "committed_at": committed_at, + "parents": parents, + "history_state": state, + "historical_contribution": CONTRIBUTION[state], + "history_complete": complete, + } + if unresolved: + record["unresolved"] = sorted(set(unresolved)) + return record + + +def encode(record: dict[str, Any]) -> dict[str, Any]: + """A union record as the carrier stores it: its parents grouped by shape.""" + encoded = {key: value for key, value in record.items() if key != "parents"} + encoded["parent_groups"] = parent_groups(record["parents"]) + return encoded + + +def current_history(parents: list[dict[str, Any]]) -> tuple[str, bool]: + """The state current evidence proves on its own (no durable history), for the per-bundle revision record.""" + durable = [parent_from_current(p, "current") for p in parents] + return union_state(durable, []) + + +# --------------------------------------------------------------------------- reading carried history + + +def _carried_records(value: Any) -> tuple[list[dict[str, Any]], dict[str, Any]]: + """The carried union records, replayed or marked unresolved according to their lineage.""" + if not isinstance(value, dict): + raise HistoryShapeError("the carried history is not an object") + lineage = value.get("lineage") + raw = value.get("records") + if not isinstance(lineage, str) or not lineage or not isinstance(raw, list): + raise HistoryShapeError("the carried history names no lineage or no records") + source = { + "lineage": lineage, + "bundle_id": value.get("source_bundle_id"), + "observed_at": value.get("source_observed_at"), + "basis": value.get("basis"), + } + records: list[dict[str, Any]] = [] + for position, record in enumerate(raw): + where = f"carried record {position}" + if not isinstance(record, dict) or not isinstance(record.get("revision"), str) or not isinstance(record.get("committed_at"), str): + raise HistoryShapeError(f"{where}: no revision or committed_at") + timeutil.parse_ts(record["committed_at"]) + if lineage == LINEAGE: + parents = parents_from_groups(record.get("parent_groups") or [], where) + unresolved = record.get("unresolved") or [] + if not isinstance(unresolved, list) or not all(isinstance(reason, str) for reason in unresolved): + raise HistoryShapeError(f"{where}: unresolved is not a list of reasons") + records.append(union_record(record["revision"], record["committed_at"], merge_parents(parents)[0], unresolved)) + elif lineage == REPLAYABLE_LINEAGE: + # HIST-13: replayed from the canonical attempts the older record kept, + # never copied from the state it derived under the older semantics. + parents = [parent_from_current(p, where) for p in record.get("parents") or []] + records.append(union_record(record["revision"], record["committed_at"], merge_parents(parents)[0], [])) + else: + # HIST-15: no replay and no accepted migration from this lineage; the + # old record stays historical evidence in its own bundle, and the + # revision's history is unknown until current evidence repairs it. + records.append(union_record(record["revision"], record["committed_at"], [], [f"LINEAGE_INCOMPATIBLE:{lineage}"])) + return records, source + + +# --------------------------------------------------------------------------- reconciliation + + +@dataclass +class Reconciled: + """The reconciled history of one evaluation.""" + + records: dict[str, dict[str, Any]] + source: dict[str, Any] + problems: list[str] = field(default_factory=list) + + def output(self) -> dict[str, Any]: + """The durable carrier the next bundle consumes: ``derived.revision_history``. + + Only revisions that carry history are in it: a revision without a + verification parent and without an unresolved prior adds nothing a + later bundle could need. + """ + bearing = [r for r in self.records.values() if r["parents"] or r.get("unresolved")] + ordered = sorted(bearing, key=lambda r: (r["committed_at"], r["revision"])) + return {"lineage": LINEAGE, "source": dict(self.source), "records": [encode(record) for record in ordered]} + + +def _series_records(series: Observation | None) -> tuple[list[dict[str, Any]] | None, bool]: + """The current revision records and whether they are the whole 14-day inventory.""" + if series is None or series.status not in (AVAILABLE, PARTIAL) or series.freshness != FRESH or not isinstance(series.value, list): + return None, False + return [r for r in series.value if isinstance(r, dict) and isinstance(r.get("revision"), str)], series.status == AVAILABLE + + +def reconcile(obs: ObservationSet) -> Reconciled: + """Combine the carried union with what the provider shows now (section D of the contract).""" + observed_at = timeutil.parse_ts(obs.observed_at) + window_start = timeutil.minus_days(observed_at, INTEGRITY["window_days"]) + carried_obs = obs.get(CARRIED) + carried: list[dict[str, Any]] = [] + problems: list[str] = [] + if carried_obs is None: + source: dict[str, Any] = {"status": SOURCE_NONE} + elif carried_obs.status == UNAVAILABLE and carried_obs.reason_code == NO_PREVIOUS_BUNDLE: + source = {"status": SOURCE_BASELINE, "reason": NO_PREVIOUS_BUNDLE} + elif carried_obs.status != AVAILABLE or carried_obs.freshness != FRESH: + # HIST-08: a required predecessor that is missing, unverifiable or not + # earlier is a gap; nothing is carried across it and nothing older is + # chosen instead (HIST-20). + source = {"status": SOURCE_GAP, "reason": carried_obs.reason_code or carried_obs.status} + if isinstance(carried_obs.evidence_ref, dict) and carried_obs.evidence_ref.get("previous_bundle_id"): + source["bundle_id"] = carried_obs.evidence_ref["previous_bundle_id"] + else: + try: + carried, detail = _carried_records(carried_obs.value) + source = {"status": SOURCE_CARRIED, **detail} + except (HistoryShapeError, ValueError, TypeError) as exc: + source = {"status": SOURCE_MALFORMED} + problems.append(f"REVISION_HISTORY_CARRY_MALFORMED:{exc}") + + current, inventory_complete = _series_records(obs.get(REVISIONS)) + current_by_rev = {r["revision"]: r for r in current or []} + records: dict[str, dict[str, Any]] = {} + + for prior in carried: + if timeutil.parse_ts(prior["committed_at"]) < window_start: + continue # HIST-10: aged out of the 14-day window + if inventory_complete and prior["revision"] not in current_by_rev: + continue # the complete inventory no longer holds it: off the default branch + records[prior["revision"]] = prior + + for revision, record in current_by_rev.items(): + committed_at = record.get("committed_at") + if not isinstance(committed_at, str): + problems.append(f"REVISION_RECORD_INCONSISTENT:{revision}") + continue + try: + visible = [parent_from_current(p, f"revision {revision}") for p in record.get("parents") or []] + except HistoryShapeError as exc: + problems.append(f"REVISION_RECORD_INCONSISTENT:{revision}:{exc}") + continue + prior = records.get(revision) + prior_parents = prior["parents"] if prior else [] + unresolved = list(prior.get("unresolved") or []) if prior else [] + if unresolved and visible and all(parent_complete(p) for p in visible): + # HIST-15, HIST-19: complete current evidence repairs a prior that + # could not be carried; anything less leaves the revision unknown. + unresolved = [] + parents, conflicts = merge_parents(prior_parents, visible) + records[revision] = union_record(revision, committed_at, parents, unresolved + conflicts) + return Reconciled(records=records, source=source, problems=problems) + + +# --------------------------------------------------------------------------- the carried observation + + +def _in_window(records: Any, observed_at: str) -> Any: + """The records a build can consume: those still inside the 14-day window it evaluates. + + A record outside the window would be dropped by the reconciliation anyway + (HIST-10), so carrying it adds bytes and no evidence. A record whose time + cannot be read is kept, so the reconciliation reports it as malformed + instead of it disappearing here. + """ + if not isinstance(records, list): + return records + window_start = timeutil.minus_days(timeutil.parse_ts(observed_at), INTEGRITY["window_days"]) + kept = [] + for record in records: + try: + if timeutil.parse_ts(record["committed_at"]) < window_start: + continue + except (KeyError, TypeError, ValueError): + pass + kept.append(record) + return kept + + +def _replay_source(record: Any) -> Any: + """A 0.1.x revision record reduced to what its replay reads: identity, time and every observed attempt.""" + if not isinstance(record, dict) or not isinstance(record.get("parents"), list): + return record + parents = [] + for parent in record["parents"]: + if not isinstance(parent, dict): + parents.append(parent) + continue + parents.append({key: parent.get(key) for key in ("parent_id", "kind", "current_attempt", "attempts_observed")}) + return {"revision": record.get("revision"), "committed_at": record.get("committed_at"), "parents": parents} + + +def carried_observation( + comparison_status: str, + previous_bundle_id: str | None, + previous_manifest: dict[str, Any] | None, + previous_snapshot: dict[str, Any] | None, + previous_observations: dict[str, Any] | None, + reasons: list[str], + observed_at: str, +) -> Observation: + """The history a build consumes: its immediate predecessor's union, or why there is none. + + Section C: the source is the verified immediately previous successful + bundle of the same immutable project, the one the comparison already + resolved; when it is missing, unverifiable or not earlier, the answer is a + gap, never an older bundle. + """ + common = { + "provider": "history", + "collected_at": observed_at, + "adapter_version": LINEAGE, + "source_ref": f"bundle:{previous_bundle_id}" if previous_bundle_id else "store:none", + } + if comparison_status == "BASELINE" or previous_bundle_id is None: + return Observation(observation_id=CARRIED, status=UNAVAILABLE, value_type="record", reason_code=NO_PREVIOUS_BUNDLE, **common) + evidence = {"previous_bundle_id": previous_bundle_id} + if comparison_status == "HISTORY_GAP" or previous_manifest is None or previous_snapshot is None: + return Observation(observation_id=CARRIED, status=UNKNOWN, value_type="record", reason_code=HISTORY_GAP, evidence_ref=evidence, **common) + if any(str(reason).startswith("NON_MONOTONIC_OBSERVATION") for reason in reasons): + return Observation(observation_id=CARRIED, status=UNKNOWN, value_type="record", reason_code=PREDECESSOR_NOT_EARLIER, evidence_ref=evidence, **common) + source = {"source_bundle_id": previous_bundle_id, "source_observed_at": previous_manifest.get("observed_at")} + integrity = next((v for v in previous_snapshot.get("vitals") or [] if isinstance(v, dict) and v.get("vital_id") == "integrity"), None) + durable = (integrity or {}).get("derived", {}).get("revision_history") if isinstance((integrity or {}).get("derived"), dict) else None + if isinstance(durable, dict) and "lineage" in durable: + value = {"lineage": durable.get("lineage"), "records": _in_window(durable.get("records"), observed_at), "basis": BASIS_SNAPSHOT, **source} + return Observation(observation_id=CARRIED, status=AVAILABLE, value_type="record", value=value, evidence_ref=evidence, **common) + series = None + if isinstance(previous_observations, dict): + series = next((o for o in previous_observations.get("observations") or [] if isinstance(o, dict) and o.get("observation_id") == REVISIONS), None) + if series is not None: + # A bundle older than this carrier: replay its revision records. + records = series.get("value") if series.get("status") in (AVAILABLE, PARTIAL) and isinstance(series.get("value"), list) else [] + replayable = [_replay_source(record) for record in _in_window(records, observed_at)] + value = {"lineage": REPLAYABLE_LINEAGE, "records": replayable, "basis": BASIS_SERIES, "source_series_status": series.get("status"), **source} + return Observation(observation_id=CARRIED, status=AVAILABLE, value_type="record", value=value, evidence_ref=evidence, **common) + return Observation(observation_id=CARRIED, status=UNKNOWN, value_type="record", reason_code=PREDECESSOR_CARRIES_NO_HISTORY, evidence_ref=evidence, **common) + + +def with_carried_history(obs: ObservationSet, carried: Observation) -> ObservationSet: + """A copy of ``obs`` with the carried history added; the caller's set is left as it was.""" + copy = ObservationSet(subject=obs.subject, observed_at=obs.observed_at, observations=obs.sorted(), receipt=obs.receipt) + copy.add(carried) + return copy + + +def carried_source_bundle(obs: ObservationSet) -> str | None: + """The bundle a stated carried history came from, when it names one.""" + carried = obs.get(CARRIED) + if carried is None: + return None + if isinstance(carried.value, dict) and carried.value.get("source_bundle_id"): + return str(carried.value["source_bundle_id"]) + if isinstance(carried.evidence_ref, dict) and carried.evidence_ref.get("previous_bundle_id"): + return str(carried.evidence_ref["previous_bundle_id"]) + return None diff --git a/src/devostasis/runner.py b/src/devostasis/runner.py index e422f77..b81684f 100644 --- a/src/devostasis/runner.py +++ b/src/devostasis/runner.py @@ -10,7 +10,7 @@ from . import activity as activity_mod from . import delta as delta_mod -from . import canonical, fleet, normalize, render, timeutil +from . import canonical, fleet, normalize, render, revision_history, timeutil from .adapters.cache import ConditionalCache from .adapters.github import CollectionError, GitHubAdapter, GitHubClient, UrllibTransport from .bundle import Bundle, BundleError, build_bundle @@ -52,13 +52,36 @@ def evaluate(obs: ObservationSet, project: ResolvedProject | None = None) -> dic return build_snapshot(obs, evaluate_all(obs)) +@dataclass +class Predecessor: + """The bundle a new one follows: what the comparison and the carried history both consume.""" + + status: str + bundle_id: str | None + manifest: dict[str, Any] | None + snapshot: dict[str, Any] | None + observations: dict[str, Any] | None + reasons: list[str] + + def decide_comparison( store: FilesystemHistoryStore, project: ResolvedProject, immutable_project_id: str | None = None, observed_at: str | None = None, ) -> tuple[str, str | None, dict[str, Any] | None, dict[str, Any] | None, list[str]]: - """Return (comparison_status, previous_bundle_id, previous_manifest, previous_snapshot, reasons). + """Return (comparison_status, previous_bundle_id, previous_manifest, previous_snapshot, reasons).""" + found = find_predecessor(store, project, immutable_project_id, observed_at) + return found.status, found.bundle_id, found.manifest, found.snapshot, found.reasons + + +def find_predecessor( + store: FilesystemHistoryStore, + project: ResolvedProject, + immutable_project_id: str | None = None, + observed_at: str | None = None, +) -> Predecessor: + """The immediate predecessor of a new bundle and how the two compare. The immutable project id locates history across a rename or transfer (RPT-7); without it the locator is the identity and a renamed project @@ -74,9 +97,9 @@ def decide_comparison( """ latest = store.latest(project.project_key, immutable_project_id) if not latest.exists: - return delta_mod.BASELINE, None, None, None, [] + return Predecessor(delta_mod.BASELINE, None, None, None, None, []) if not latest.verified or latest.manifest is None or latest.snapshot is None: - return delta_mod.HISTORY_GAP, latest.bundle_id, None, None, latest.problems + return Predecessor(delta_mod.HISTORY_GAP, latest.bundle_id, None, None, None, latest.problems) current_fields = { "vitals_contract_version": VITALS_CONTRACT_VERSION, "observation_contract_version": OBSERVATION_CONTRACT_VERSION, @@ -89,8 +112,8 @@ def decide_comparison( if timeutil.parse_ts(observed_at) <= timeutil.parse_ts(previous_observed_at): reasons.append(f"{NON_MONOTONIC_OBSERVATION}:{previous_observed_at}->{observed_at}") if reasons: - return delta_mod.INCOMPARABLE, latest.bundle_id, latest.manifest, latest.snapshot, reasons - return delta_mod.COMPARABLE, latest.bundle_id, latest.manifest, latest.snapshot, [] + return Predecessor(delta_mod.INCOMPARABLE, latest.bundle_id, latest.manifest, latest.snapshot, latest.observations, reasons) + return Predecessor(delta_mod.COMPARABLE, latest.bundle_id, latest.manifest, latest.snapshot, latest.observations, []) def check_receipt_config(project: ResolvedProject, obs: ObservationSet) -> None: @@ -114,11 +137,46 @@ def check_receipt_config(project: ResolvedProject, obs: ObservationSet) -> None: ) +HISTORY_SOURCE_NOT_PREDECESSOR = "HISTORY_SOURCE_NOT_PREDECESSOR" + + +def attach_revision_history(obs: ObservationSet, predecessor: Predecessor) -> ObservationSet: + """The observations with the durable revision history the predecessor carries (PV-HIST-002, section C). + + The source is the bundle the comparison resolved, the immediate + predecessor, never an older one. A set that already states carried history + (rebuilt from a bundle's own observations) is kept only when it names that + same predecessor: carried history is canonical input, and consuming it + against another chain would break the chain it records. + """ + carried = revision_history.carried_observation( + predecessor.status, + predecessor.bundle_id, + predecessor.manifest, + predecessor.snapshot, + predecessor.observations, + predecessor.reasons, + obs.observed_at, + ) + stated = obs.get(revision_history.CARRIED) + if stated is None: + return revision_history.with_carried_history(obs, carried) + if revision_history.carried_source_bundle(obs) != predecessor.bundle_id or stated.status != carried.status: + raise BundleError( + f"{HISTORY_SOURCE_NOT_PREDECESSOR}: the observations carry revision history from " + f"{revision_history.carried_source_bundle(obs) or 'no bundle'} ({stated.status}), this store's predecessor is " + f"{predecessor.bundle_id or 'none'} ({predecessor.status}); durable history is consumed only from the immediate predecessor" + ) + return obs + + def build_from_observations(project: ResolvedProject, obs: ObservationSet, store: FilesystemHistoryStore, run_meta: dict[str, Any] | None = None) -> Bundle: check_receipt_config(project, obs) + predecessor = find_predecessor(store, project, obs.subject.get("immutable_project_id"), obs.observed_at) + obs = attach_revision_history(obs, predecessor) snapshot = build_snapshot(obs, evaluate_all(obs)) - status, previous_id, previous_manifest, previous_snapshot, reasons = decide_comparison(store, project, obs.subject.get("immutable_project_id"), obs.observed_at) - delta = delta_mod.compare(snapshot, previous_snapshot, status, previous_id, reasons) + status, previous_id, previous_manifest, reasons = predecessor.status, predecessor.bundle_id, predecessor.manifest, predecessor.reasons + delta = delta_mod.compare(snapshot, predecessor.snapshot, status, previous_id, reasons) activity = None if project.activity_enabled: interval_start = previous_manifest.get("observed_at") if (status == delta_mod.COMPARABLE and previous_manifest) else None diff --git a/src/devostasis/vectors.py b/src/devostasis/vectors.py index 645dec5..fa09e92 100644 --- a/src/devostasis/vectors.py +++ b/src/devostasis/vectors.py @@ -21,7 +21,13 @@ * **vital** evaluates one Vital over an observation set built from raw observation envelopes (``RAW_OBSERVATION_CONTRACT_V0``), so a vector states - evidence exactly as a collector would emit it. + evidence exactly as a collector would emit it. With ``derive`` the + provider-neutral derivation runs first, so a case about how inventories + become aggregates (a change request linked to a closed target, a partial + target enumeration) is executed at that boundary rather than over counts + somebody pre-derived. ``expect.rejected`` states that the evidence must be + refused before classification (``InadmissibleEvidence``), the outcome the + totality contracts give contradictory normalized input. * **delta** compares two snapshots given as partial Vital rows and checks the transition classes and reason codes. An accepted case often names more than one pair — "``GRIDLOCKED -> CONGESTED -> MOVING`` follows WORSENED/IMPROVED @@ -70,8 +76,10 @@ from .adapters import github_ci from .contracts import CORE_VITAL_IDS, VECTOR_SCHEMA from .delta import BASELINE, COMPARABLE, HISTORY_GAP, INCOMPARABLE, compare +from .normalize import derive as derive_aggregates from .observations import AVAILABLE, PARTIAL, STATUSES, Observation, ObservationSet from .vitals import EVALUATORS +from .vitals.common import InadmissibleEvidence KINDS = ("vital", "delta", "ci", "activity") COMPARISON_STATUSES = (BASELINE, COMPARABLE, HISTORY_GAP, INCOMPARABLE) @@ -84,8 +92,12 @@ # vector, so the kind validators require it rather than this set. REQUIRED_VECTOR_KEYS = {"case", "title", "kind", "given"} -VITAL_GIVEN_KEYS = {"vital", "observations", "observed_at", "subject", "variants"} +VITAL_GIVEN_KEYS = {"vital", "observations", "observed_at", "subject", "variants", "derive"} VITAL_VARIANT_KEYS = {"title", "observations"} +# The configuration the derivation reads: where planning targets come from and +# the explicit debt mapping, the two project settings normalize.derive consults. +DERIVE_KEYS = {"planning_source", "debt_mapping"} +PLANNING_SOURCES = ("none", "milestones", "file") VITAL_EXPECT_KEYS = { "band", "evaluation_status", @@ -99,6 +111,7 @@ "explanation_contains", "shared_signal_groups", "dependency_group_ids", + "rejected", } REQUIRED_VITAL_EXPECT_KEYS = {"band", "evaluation_status"} @@ -231,14 +244,33 @@ def _require_statement(where: str, holder: dict[str, Any], keys: tuple[str, ...] def _validate_vital_expect(where: str, expect: Any) -> None: if expect is None: raise VectorError(f"{where}: missing keys ['expect']") + if isinstance(expect, dict) and "rejected" in expect: + # A refused evaluation has no band, status or metrics to check: the + # rejection code is the whole expectation. + if set(expect) != {"rejected"}: + raise VectorError(f"{where} expect: rejected is the whole expectation of a refused evaluation, not combined with {sorted(set(expect) - {'rejected'})}") + if not isinstance(expect["rejected"], str) or not expect["rejected"]: + raise VectorError(f"{where} expect: rejected must name the rejection code (an empty prefix matches everything)") + return _require_keys(f"{where} expect", expect, VITAL_EXPECT_KEYS, REQUIRED_VITAL_EXPECT_KEYS) _require_statement(f"{where} expect", expect, ("derived",) + CODE_LIST_KEYS) +def _validate_derive(where: str, derive: Any) -> None: + _require_keys(f"{where} given.derive", derive, DERIVE_KEYS, {"planning_source"}) + if derive["planning_source"] not in PLANNING_SOURCES: + raise VectorError(f"{where} given.derive: planning_source {derive['planning_source']!r} is not one of {list(PLANNING_SOURCES)}") + mapping = derive.get("debt_mapping") + if mapping is not None and not isinstance(mapping, dict): + raise VectorError(f"{where} given.derive: debt_mapping is an object or null") + + def _validate_vital(where: str, given: dict[str, Any], expect: Any) -> None: _require_keys(f"{where} given", given, VITAL_GIVEN_KEYS, {"vital"}) if given["vital"] not in EVALUATORS: raise VectorError(f"{where} given: unknown vital {given['vital']!r}") + if "derive" in given: + _validate_derive(where, given["derive"]) if "variants" in given: _require_variants(where, given, {"observations"}, VITAL_VARIANT_KEYS, {"observations"}) for index, variant in enumerate(given["variants"]): @@ -500,16 +532,40 @@ def _vital_shapes(given: dict[str, Any]) -> list[tuple[str, list[dict[str, Any]] return [("", given["observations"])] +@dataclass(frozen=True) +class _DeriveScope: + """The two project settings the derivation reads, stated by a vector instead of a configuration file.""" + + planning_source: str + debt_mapping: dict[str, Any] | None + + def _run_vital(vector: Vector) -> list[str]: """One case, every evidence shape it names. A case passes only when all of them do.""" failures: list[str] = [] + derive = vector.given.get("derive") for where, observations in _vital_shapes(vector.given): obs = _observation_set(vector.given, vector.case, observations) - result = EVALUATORS[vector.given["vital"]](obs).to_dict() - _check_vital_result(result, vector.expect, failures, where) + if derive is not None: + derive_aggregates(obs, _DeriveScope(derive["planning_source"], derive.get("debt_mapping"))) + _evaluate_and_check(EVALUATORS[vector.given["vital"]], obs, vector.expect, failures, where) return failures +def _evaluate_and_check(evaluator: Any, obs: ObservationSet, expect: dict[str, Any], failures: list[str], where: str) -> None: + """Evaluate one Vital and check the result, or check that the evidence is refused when the case says it must be.""" + if "rejected" not in expect: + _check_vital_result(evaluator(obs).to_dict(), expect, failures, where) + return + try: + result = evaluator(obs).to_dict() + except InadmissibleEvidence as exc: + if not _matches(expect["rejected"], [exc.code]): + failures.append(f"{where}rejected: expected {expect['rejected']!r}, the evidence was refused with {exc.code!r}") + return + failures.append(f"{where}rejected: expected the evidence to be refused with {expect['rejected']!r}, it evaluated to {result['band']} / {result['evaluation_status']}") + + def _delta_row(row: dict[str, Any]) -> dict[str, Any]: """A partial Vital row completed with the defaults a vector may leave out.""" band = row.get("band") @@ -639,8 +695,7 @@ def _run_ci(vector: Vector) -> list[str]: **common, ) ) - result = EVALUATORS["integrity"](obs).to_dict() - _check_vital_result(result, vector.expect["integrity"], failures, f"{where}integrity.") + _evaluate_and_check(EVALUATORS["integrity"], obs, vector.expect["integrity"], failures, f"{where}integrity.") return failures diff --git a/src/devostasis/vitals/common.py b/src/devostasis/vitals/common.py index 93aa249..c2b0b14 100644 --- a/src/devostasis/vitals/common.py +++ b/src/devostasis/vitals/common.py @@ -19,6 +19,25 @@ SEM_SUPERSET = "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" +class InadmissibleEvidence(ValueError): + """Normalized inputs that contradict each other, so no admissible completion exists. + + The totality contracts adopted in 0.2.0 name this case separately from + missing evidence: counts that cannot all be true at once (``L + U + R != N`` + for Direction, a beyond-28-day count above the future-boundary count for + Horizon) are invalid normalized input and fail validation *before* + classification, never ``UNKNOWN`` or ``DEGRADED`` compensation + (PV-DIRECTION-INCOMPLETE-001 section 7, PV-HORIZON-PARTIAL-001 case + HOR-PARTIAL-15). The derivation never produces such a set; a saved or + hand-written one that does is refused as input. ``code`` is the stable + reason a caller or a vector can match. + """ + + def __init__(self, code: str, detail: str) -> None: + super().__init__(f"{code}: {detail}") + self.code = code + + @dataclass class VitalResult: vital_id: str diff --git a/src/devostasis/vitals/debt.py b/src/devostasis/vitals/debt.py index 28054ea..cd9c249 100644 --- a/src/devostasis/vitals/debt.py +++ b/src/devostasis/vitals/debt.py @@ -4,16 +4,31 @@ exists only through an explicit, versioned mapping (for example issue labels) and without an accepted calibrated policy its bands are PRESENT, CLEAR and UNINSTRUMENTED plus raw diagnostics (PV-VITALS-V1-002). + +Rule ``debt.bands.v2`` adopts **PV-DEBT-PARTIAL-001** (accepted by +PV-REV-DEBT-PARTIAL-001, DEBT-PARTIAL-01..16). A configured register whose +enumeration is PARTIAL is an observed subset: its open count is a lower bound, +admitted only with the observed-subset proof of PV-REV-PR-031-003 (issue #39). +With at least one confirmed open item every completion of the missing items +is still PRESENT, so the result is ``DEGRADED / PRESENT / EXACT``: the band is +exact, the acquisition is not, and neither the count nor any severity is +claimed. With none confirmed, completions reach both CLEAR and PRESENT, and +Debt declares no order between them, so the result is ``UNKNOWN`` with the +partial receipt kept visible. A partial zero is never CLEAR, and PARTIAL +evidence never establishes UNINSTRUMENTED, which needs positive absence of +configured debt authority. """ from __future__ import annotations -from ..observations import FRESH, PARTIAL, ObservationSet +from typing import Any + +from ..observations import FRESH, PARTIAL, ObservationSet, subset_count_problem from .common import ( EVAL_AVAILABLE, EVAL_DEGRADED, + EVAL_UNKNOWN, SEM_EXACT, - SEM_LOWER, VitalResult, as_int, input_meta, @@ -22,7 +37,8 @@ VITAL_ID = "debt" VITAL_VERSION = "PV-VITALS-V1-002/debt" -RULE_ID = "debt.bands.v1.1" +RULE_ID = "debt.bands.v2" +PARTIAL_RULE = "PV-DEBT-PARTIAL-001" BANDS = ["UNINSTRUMENTED", "CLEAR", "PRESENT"] CAPABILITY = "debt.registry.capability" @@ -38,8 +54,12 @@ CAP_CONFIGURED = "CONFIGURED" CAP_UNCONFIGURED = "UNCONFIGURED" +PARTIAL_ENUMERATION = "DEBT_PARTIAL_ENUMERATION" +LOWER_BOUND = "DEBT_CONFIRMED_OPEN_LOWER_BOUND" +FORCED_PRESENT = "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + -def _result(obs: ObservationSet, band: str, status: str, semantics: str, possible, derived: dict, explanation: str, diagnostics: list[str]) -> VitalResult: +def _result(obs: ObservationSet, band: str | None, status: str, semantics: str | None, derived: dict, explanation: str, diagnostics: list[str]) -> VitalResult: return VitalResult( vital_id=VITAL_ID, vital_version=VITAL_VERSION, @@ -47,7 +67,7 @@ def _result(obs: ObservationSet, band: str, status: str, semantics: str, possibl band=band, evaluation_status=status, band_semantics=semantics, - possible_bands=possible, + possible_bands=None, inputs=input_meta(obs, IDS), derived=derived, shared_signal_groups=SHARED, @@ -57,8 +77,25 @@ def _result(obs: ObservationSet, band: str, status: str, semantics: str, possibl ) +def _mapping_version(mapping: Any) -> str: + return mapping.get("mapping_version") if isinstance(mapping, dict) else "n/a" + + +def _partial_receipt(obs: ObservationSet, item: Any) -> dict[str, Any]: + """What the incomplete enumeration was: its reason and the inventory it was counted over.""" + sources = (item.evidence_ref or {}).get("derived_from") if isinstance(item.evidence_ref, dict) else None + source = obs.get(sources[0]) if isinstance(sources, list) and sources else None + return { + "status": item.status, + "reason_code": item.reason_code, + "source": source.observation_id if source is not None else None, + "source_reason_code": source.reason_code if source is not None else None, + } + + def evaluate(obs: ObservationSet) -> VitalResult: if not obs.is_good(CAPABILITY): + # DEBT-PARTIAL-08: uncertain authority is never inferred from records. return unknown_result( VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, [f"MISSING_REQUIRED:{CAPABILITY}:{obs.status_of(CAPABILITY)}/{obs.freshness_of(CAPABILITY)}"], @@ -67,8 +104,9 @@ def evaluate(obs: ObservationSet) -> VitalResult: capability = obs.value_of(CAPABILITY) mapping = obs.value_of(MAPPING) if obs.is_good(MAPPING) else None if capability == CAP_UNCONFIGURED: + # DEBT-PARTIAL-07: positive absence of configured debt authority. return _result( - obs, "UNINSTRUMENTED", EVAL_AVAILABLE, SEM_EXACT, None, + obs, "UNINSTRUMENTED", EVAL_AVAILABLE, SEM_EXACT, {"capability": capability, "mapping": None}, "No debt register or category mapping is configured; debt is not instrumented.", [], @@ -81,23 +119,58 @@ def evaluate(obs: ObservationSet) -> VitalResult: open_count = as_int(obs.value_of(OPEN)) band = "PRESENT" if open_count > 0 else "CLEAR" derived = dict(derived_base, open_count=open_count) - explanation = f"{open_count} open registered debt items under mapping version {mapping.get('mapping_version') if isinstance(mapping, dict) else 'n/a'}." - return _result(obs, band, EVAL_AVAILABLE, SEM_EXACT, None, derived, explanation, []) + explanation = f"{open_count} open registered debt items under mapping version {_mapping_version(mapping)}." + return _result(obs, band, EVAL_AVAILABLE, SEM_EXACT, derived, explanation, []) open_obs = obs.get(OPEN) - # A stale or freshness-unknown partial register proves nothing about now - # (vitals.md: configured debt evidence that is stale yields UNKNOWN). - if open_obs is not None and open_obs.status == PARTIAL and open_obs.freshness == FRESH and open_obs.has_value and as_int(open_obs.value) > 0: - open_count = as_int(open_obs.value) - derived = dict(derived_base, open_count=open_count, open_count_semantics="LOWER_BOUND") + if open_obs is None or open_obs.status != PARTIAL or open_obs.freshness != FRESH or not open_obs.has_value: + # DEBT-PARTIAL-13: stale, unavailable, forbidden, unknown or errored + # configured evidence stays outside the partial repair. + return unknown_result( + VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, + [f"MISSING_REQUIRED:{OPEN}:{obs.status_of(OPEN)}/{obs.freshness_of(OPEN)}"], + SHARED, GROUPS, + explanation="Debt is configured but its register could not be observed completely; CLEAR is never assumed.", + ) + problem = subset_count_problem(open_obs) + if problem is not None: + # Issue #39: status PARTIAL alone proves nothing about the value. + return unknown_result( + VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, + [f"MISSING_REQUIRED:{OPEN}:{PARTIAL}/{open_obs.freshness}", f"PARTIAL_COUNT_NOT_A_LOWER_BOUND:{OPEN}:{problem}"], + SHARED, GROUPS, + explanation="Debt is configured, but the partial open count does not prove it is a count over returned register items; no bound is read from it.", + ) + if mapping is None: + # A PD1 result must name the authority it counted under (section 5). + return unknown_result( + VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, + [f"MISSING_REQUIRED:{MAPPING}:{obs.status_of(MAPPING)}/{obs.freshness_of(MAPPING)}"], + SHARED, GROUPS, + explanation="Debt is configured and partially enumerated, but the mapping the items were counted under is not observed; no band is claimed.", + ) + + confirmed = as_int(open_obs.value) + derived = dict( + derived_base, + open_count=confirmed, + open_count_semantics="LOWER_BOUND", + confirmed_open_count=confirmed, + partial_enumeration=_partial_receipt(obs, open_obs), + partial_rule=PARTIAL_RULE, + ) + diagnostics = [PARTIAL_ENUMERATION, f"{LOWER_BOUND}:{confirmed}"] + if confirmed > 0: + # PD1: a confirmed current open item survives every completion. return _result( - obs, "PRESENT", EVAL_DEGRADED, SEM_LOWER, ["PRESENT"], derived, - f"At least {open_count} open registered debt items were observed; the register enumeration is incomplete.", - [f"PARTIAL_REGISTER:{OPEN}"], + obs, "PRESENT", EVAL_DEGRADED, SEM_EXACT, derived, + f"At least {confirmed} open registered debt items were observed under mapping version {_mapping_version(mapping)}; " + "the register enumeration is incomplete, so the count is a lower bound and the band is PRESENT in every completion.", + diagnostics + [FORCED_PRESENT], ) - return unknown_result( - VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, - [f"MISSING_REQUIRED:{OPEN}:{obs.status_of(OPEN)}/{obs.freshness_of(OPEN)}"], - SHARED, GROUPS, - explanation="Debt is configured but its register could not be observed completely; CLEAR is never assumed.", + # PD0: zero confirmed open items is absence of proof, not proof of absence. + return _result( + obs, None, EVAL_UNKNOWN, None, derived, + "The debt register enumeration is incomplete and returned no open item; the missing items could make Debt CLEAR or PRESENT, and no band is claimed.", + diagnostics, ) diff --git a/src/devostasis/vitals/direction.py b/src/devostasis/vitals/direction.py index f03cd05..ea66a63 100644 --- a/src/devostasis/vitals/direction.py +++ b/src/devostasis/vitals/direction.py @@ -2,37 +2,109 @@ FULLY_LINKED is neutral exact traceability; it is not strategy, prioritization or target quality, and renderers must never alias it to ALIGNED or ON_TRACK. + +Rule ``direction.bands.v2`` adopts two accepted repairs on top of the V1 band +table, neither of which moves a threshold or the 28-day frame: + +* **PV-REV-DIRECTION-CLOSED-TARGET-001** (DIR-CLOSED-01..09): linkage is + state-neutral. An active change request linked to a declared target stays + linked when the target closes, because closing the target the work delivered + must not un-trace the work (calibration finding 9). Horizon still counts open + targets only, so the two Vitals may legitimately diverge. +* **PV-DIRECTION-INCOMPLETE-001** (DIR-INCOMPLETE-01..16): over a complete + active population of N change requests, each one is LINKED, UNLINKED or + UNRESOLVED (L + U + R = N). Every unresolved one may complete either way, so + the linked count k ranges over L..L+R and each k is mapped through the + unchanged predicates (k = N FULLY_LINKED, 2k >= N MIXED, else SCATTERED). + With R = 0 the band is exact as before. With R > 0 a band that every + completion reaches is ``DEGRADED / / EXACT``; completions that cross a + boundary are ``UNKNOWN`` with no band, because Direction declares no order to + pick a representative by. A singleton FULLY_LINKED is impossible with R > 0. + +The open-target count the V1 rule classified by is still derived, for +presentation only; the judgement forbids it as the classifier's authority. """ from __future__ import annotations +from typing import Any + from ..observations import ObservationSet -from .common import EVAL_AVAILABLE, SEM_EXACT, VitalResult, as_int, input_meta, unknown_result +from .common import ( + EVAL_AVAILABLE, + EVAL_DEGRADED, + EVAL_UNKNOWN, + SEM_EXACT, + SEM_SUPERSET, + InadmissibleEvidence, + VitalResult, + as_int, + input_meta, + unknown_result, +) from .horizon import CAP_SUPPORTED_UNUSED, CAP_UNSUPPORTED, CAPABILITY VITAL_ID = "direction" VITAL_VERSION = "PV-VITALS-V1-002/direction" -RULE_ID = "direction.bands.v1.1" +RULE_ID = "direction.bands.v2" +CLOSED_TARGET_RULE = "PV-REV-DIRECTION-CLOSED-TARGET-001" +INCOMPLETE_RULE = "PV-DIRECTION-INCOMPLETE-001" BANDS = ["NO_ACTIVE_CHANGE", "UNDECLARED", "SCATTERED", "MIXED", "FULLY_LINKED"] ACTIVE = "planning.linkage.active_change_requests_count_28d" -LINKED = "planning.linkage.active_change_requests_linked_to_open_target_count_28d" +LINKED = "planning.linkage.active_change_requests_linked_count_28d" +UNLINKED = "planning.linkage.active_change_requests_unlinked_count_28d" +UNRESOLVED = "planning.linkage.active_change_requests_unresolved_count_28d" +UNRESOLVED_ITEMS = "planning.linkage.unresolved_change_requests_28d" +MISSING_REFS = "planning.linkage.missing_target_reference_count_28d" TARGET_LINKS = "planning.linkage.links_per_target_28d" -IDS = [ACTIVE, LINKED, CAPABILITY, TARGET_LINKS] +# Presentation only since this rule version: never read to classify. +LINKED_TO_OPEN = "planning.linkage.active_change_requests_linked_to_open_target_count_28d" +IDS = [ACTIVE, LINKED, UNLINKED, UNRESOLVED, CAPABILITY, TARGET_LINKS, MISSING_REFS, UNRESOLVED_ITEMS] SHARED = ["PLANNING_TARGETS", "CHANGE_REQUEST_ACTIVITY"] GROUPS = ["HORIZON_DIRECTION_PLANNING", "DIRECTION_PULSE_ACTIVITY"] +BAND_INVARIANT = "DIRECTION_INCOMPLETE_BAND_INVARIANT" +AMBIGUOUS = "DIRECTION_INCOMPLETE_AMBIGUOUS" +REFERENCE_MISSING = "DIRECTION_TARGET_REFERENCE_MISSING" +SINGLE_TARGET = "ALL_LINKS_TO_SINGLE_TARGET" +COUNTS_INCONSISTENT = "DIRECTION_LINKAGE_COUNTS_INCONSISTENT" + + +def band_for(linked: int, active: int) -> str: + """The V1 predicates over complete linkage, with unlinked = active - linked.""" + if linked == active: + return "FULLY_LINKED" + if 2 * linked >= active: + return "MIXED" + return "SCATTERED" -def _result(obs: ObservationSet, band: str, derived: dict, explanation: str, diagnostics: list[str]) -> VitalResult: + +def reachable_bands(linked: int, unresolved: int, active: int) -> list[str]: + """Every band some completion of the unresolved change requests reaches, in canonical order.""" + reached = {band_for(k, active) for k in range(linked, linked + unresolved + 1)} + return [band for band in BANDS if band in reached] + + +def _result( + obs: ObservationSet, + band: str | None, + status: str, + semantics: str | None, + possible: list[str] | None, + derived: dict[str, Any], + explanation: str, + diagnostics: list[str], +) -> VitalResult: return VitalResult( vital_id=VITAL_ID, vital_version=VITAL_VERSION, rule_id=RULE_ID, band=band, - evaluation_status=EVAL_AVAILABLE, - band_semantics=SEM_EXACT, - possible_bands=None, + evaluation_status=status, + band_semantics=semantics, + possible_bands=possible, inputs=input_meta(obs, IDS), derived=derived, shared_signal_groups=SHARED, @@ -42,56 +114,94 @@ def _result(obs: ObservationSet, band: str, derived: dict, explanation: str, dia ) +def _missing(obs: ObservationSet, ids: list[str]) -> list[str]: + return [f"MISSING_REQUIRED:{oid}:{obs.status_of(oid)}/{obs.freshness_of(oid)}" for oid in ids if not obs.is_good(oid)] + + +def _count(obs: ObservationSet, oid: str) -> int: + value = as_int(obs.value_of(oid)) + if value < 0: + raise InadmissibleEvidence(COUNTS_INCONSISTENT, f"{oid} is {value}, a count below zero") + return value + + def evaluate(obs: ObservationSet) -> VitalResult: - if not obs.is_good(ACTIVE): - return unknown_result( - VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, - [f"MISSING_REQUIRED:{ACTIVE}:{obs.status_of(ACTIVE)}/{obs.freshness_of(ACTIVE)}"], - SHARED, GROUPS, - ) - active = as_int(obs.value_of(ACTIVE)) + missing = _missing(obs, [ACTIVE]) + if missing: + return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, missing, SHARED, GROUPS) + active = _count(obs, ACTIVE) if active == 0: - return _result(obs, "NO_ACTIVE_CHANGE", {"active_change_count_28d": 0}, "No active change requests in the 28-day frame.", []) + return _result(obs, "NO_ACTIVE_CHANGE", EVAL_AVAILABLE, SEM_EXACT, None, {"active_change_count_28d": 0}, "No active change requests in the 28-day frame.", []) - if not obs.is_good(CAPABILITY): - return unknown_result( - VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, - [f"MISSING_REQUIRED:{CAPABILITY}:{obs.status_of(CAPABILITY)}/{obs.freshness_of(CAPABILITY)}"], - SHARED, GROUPS, - ) + missing = _missing(obs, [CAPABILITY]) + if missing: + # DIR-INCOMPLETE-13: linkage capability neither positively present nor absent. + return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, missing, SHARED, GROUPS) capability = obs.value_of(CAPABILITY) if capability in (CAP_UNSUPPORTED, CAP_SUPPORTED_UNUSED): return _result( obs, "UNDECLARED", + EVAL_AVAILABLE, + SEM_EXACT, + None, {"active_change_count_28d": active, "capability": capability}, f"{active} active change requests, but no explicit planning target or linkage mechanism is declared.", [f"PLANNING_CAPABILITY:{capability}"], ) - if not obs.is_good(LINKED): - return unknown_result( - VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, - [f"MISSING_REQUIRED:{LINKED}:{obs.status_of(LINKED)}/{obs.freshness_of(LINKED)}"], - SHARED, GROUPS, - ) - linked = as_int(obs.value_of(LINKED)) - unlinked = active - linked - if unlinked > linked: - band = "SCATTERED" - elif unlinked > 0: - band = "MIXED" - else: - band = "FULLY_LINKED" + + missing = _missing(obs, [LINKED, UNLINKED, UNRESOLVED]) + if missing: + return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, missing, SHARED, GROUPS) + linked, unlinked, unresolved = _count(obs, LINKED), _count(obs, UNLINKED), _count(obs, UNRESOLVED) + if linked + unlinked + unresolved != active: + # PV-DIRECTION-INCOMPLETE-001 section 7: invalid normalized input, never classified. + raise InadmissibleEvidence(COUNTS_INCONSISTENT, f"linked {linked} + unlinked {unlinked} + unresolved {unresolved} != active {active}") + links = obs.value_of(TARGET_LINKS) if obs.is_good(TARGET_LINKS) else None - derived = { + missing_refs = as_int(obs.value_of(MISSING_REFS)) if obs.is_good(MISSING_REFS) else None + derived: dict[str, Any] = { "active_change_count_28d": active, "linked_active_change_count_28d": linked, "unlinked_active_change_count_28d": unlinked, + "unresolved_active_change_count_28d": unresolved, "capability": capability, "links_per_target": links, + "linkage_rule": CLOSED_TARGET_RULE, } + if obs.is_good(LINKED_TO_OPEN): + derived["linked_to_open_target_count_28d"] = as_int(obs.value_of(LINKED_TO_OPEN)) + if missing_refs is not None: + derived["missing_target_reference_count_28d"] = missing_refs + diagnostics: list[str] = [] if isinstance(links, dict) and len(links) == 1 and linked > 1: - diagnostics.append("ALL_LINKS_TO_SINGLE_TARGET") - explanation = f"{linked} of {active} active change requests are explicitly linked to an open planning target." - return _result(obs, band, derived, explanation, diagnostics) + diagnostics.append(SINGLE_TARGET) + if missing_refs: + # DIR-INCOMPLETE-07: a positively missing target is a broken reference, counted unlinked. + diagnostics.append(f"{REFERENCE_MISSING}:{missing_refs}") + + if unresolved == 0: + band = band_for(linked, active) + explanation = f"{linked} of {active} active change requests are explicitly linked to a declared target, open or closed." + return _result(obs, band, EVAL_AVAILABLE, SEM_EXACT, None, derived, explanation, diagnostics) + + reachable = reachable_bands(linked, unresolved, active) + derived["reachable_bands"] = reachable + derived["incomplete_rule"] = INCOMPLETE_RULE + items = obs.value_of(UNRESOLVED_ITEMS) if obs.is_good(UNRESOLVED_ITEMS) else None + if isinstance(items, dict): + derived["unresolved_change_requests"] = items + if len(reachable) == 1: + band = reachable[0] + derived["possible_bands_semantics"] = SEM_SUPERSET + return _result( + obs, band, EVAL_DEGRADED, SEM_EXACT, [band], derived, + f"{linked} of {active} active change requests are linked and {unresolved} unresolved; every completion of the unresolved ones is {band}.", + diagnostics + [BAND_INVARIANT], + ) + return _result( + obs, None, EVAL_UNKNOWN, None, None, derived, + f"{linked} of {active} active change requests are linked and {unresolved} unresolved; completing them could give {', '.join(reachable)}, and Direction declares no order to choose one.", + diagnostics + [AMBIGUOUS], + ) diff --git a/src/devostasis/vitals/horizon.py b/src/devostasis/vitals/horizon.py index 3760029..22183e6 100644 --- a/src/devostasis/vitals/horizon.py +++ b/src/devostasis/vitals/horizon.py @@ -2,16 +2,44 @@ Horizon measures forward visibility, not roadmap quality. EXTENDED is not better than VISIBLE; UNDECLARED may be entirely appropriate. + +Rule ``horizon.bands.v2`` adopts **PV-HORIZON-PARTIAL-001** (accepted by +PV-REV-HORIZON-PARTIAL-001, HOR-PARTIAL-01..16) on top of the V1 band table. +When the target enumeration is incomplete, its counts are lower bounds over +the returned targets (each carrying the observed-subset proof of +PV-REV-PR-031-003), and every admissible completion of the missing targets is +classified with the unchanged predicates. Horizon declares no order, so a +completion set that reaches several bands names none of them (``UNKNOWN``); +only a set of one band is a band, ``DEGRADED / / EXACT``. Under an +ordinary partial enumeration that happens exactly when a returned open target +already lies beyond the 28-day frame, because adding targets cannot remove it +(existential monotonicity, not an ordering). A partial zero is never +UNDECLARED. Counts that contradict each other are refused before +classification (HOR-PARTIAL-15). """ from __future__ import annotations -from ..observations import ObservationSet -from .common import EVAL_AVAILABLE, SEM_EXACT, VitalResult, as_int, input_meta, unknown_result +from typing import Any + +from ..observations import FRESH, PARTIAL, ObservationSet, subset_count_problem +from .common import ( + EVAL_AVAILABLE, + EVAL_DEGRADED, + EVAL_UNKNOWN, + SEM_EXACT, + SEM_SUPERSET, + InadmissibleEvidence, + VitalResult, + as_int, + input_meta, + unknown_result, +) VITAL_ID = "horizon" VITAL_VERSION = "PV-VITALS-V1-002/horizon" -RULE_ID = "horizon.bands.v1" +RULE_ID = "horizon.bands.v2" +PARTIAL_RULE = "PV-HORIZON-PARTIAL-001" BANDS = ["UNDECLARED", "DECLARED", "VISIBLE", "EXTENDED"] CAPABILITY = "planning.explicit_targets.capability" @@ -20,6 +48,7 @@ BEYOND = "planning.explicit_targets.open_beyond_28d_count" NEAREST = "planning.explicit_targets.nearest_future_boundary_days" IDS = [CAPABILITY, OPEN, FUTURE, BEYOND, NEAREST] +COUNTS = (OPEN, FUTURE, BEYOND) SHARED = ["PLANNING_TARGETS"] GROUPS = ["HORIZON_DIRECTION_PLANNING"] @@ -28,16 +57,64 @@ CAP_SUPPORTED_UNUSED = "SUPPORTED_UNUSED" CAP_UNSUPPORTED = "UNSUPPORTED" +BAND_INVARIANT = "HORIZON_PARTIAL_BAND_INVARIANT" +AMBIGUOUS = "HORIZON_PARTIAL_AMBIGUOUS" +COUNTS_INCONSISTENT = "HORIZON_COUNTS_INCONSISTENT" + + +def classify(open_count: int, future: int, beyond: int) -> str: + """The V1 first-match table over complete counts.""" + if beyond > 0: + return "EXTENDED" + if future > 0: + return "VISIBLE" + if open_count > 0: + return "DECLARED" + return "UNDECLARED" + + +def reachable_bands(open_count: int, future: int, beyond: int, exact: tuple[bool, bool, bool]) -> list[str]: + """Every band some admissible completion reaches, in canonical order. + + A count that is exact keeps its value; a lower bound may grow. Every + completion keeps beyond <= future <= open. The table reads only whether + each count is positive, so raising a lower bound by one, or to the count + it must cover, reaches every region a larger completion could. + """ + exact_open, exact_future, exact_beyond = exact + + def grow(value: int, is_exact: bool, floor: int = 0) -> set[int]: + if is_exact: + return {value} + return {max(value, floor), max(value, floor) + 1} + + reached: set[str] = set() + for b in grow(beyond, exact_beyond): + for f in grow(future, exact_future, b): + for o in grow(open_count, exact_open, f): + if b <= f <= o: + reached.add(classify(o, f, b)) + return [band for band in BANDS if band in reached] + -def _result(obs: ObservationSet, band: str, derived: dict, explanation: str, diagnostics: list[str]) -> VitalResult: +def _result( + obs: ObservationSet, + band: str | None, + status: str, + semantics: str | None, + possible: list[str] | None, + derived: dict[str, Any], + explanation: str, + diagnostics: list[str], +) -> VitalResult: return VitalResult( vital_id=VITAL_ID, vital_version=VITAL_VERSION, rule_id=RULE_ID, band=band, - evaluation_status=EVAL_AVAILABLE, - band_semantics=SEM_EXACT, - possible_bands=None, + evaluation_status=status, + band_semantics=semantics, + possible_bands=possible, inputs=input_meta(obs, IDS), derived=derived, shared_signal_groups=SHARED, @@ -47,8 +124,28 @@ def _result(obs: ObservationSet, band: str, derived: dict, explanation: str, dia ) +def _subset_lower_bound(obs: ObservationSet, oid: str) -> str | None: + """None when a PARTIAL count is a fresh observed-subset lower bound, else why it is not.""" + item = obs.get(oid) + if item is None or item.status != PARTIAL or item.freshness != FRESH: + return f"MISSING_REQUIRED:{oid}:{obs.status_of(oid)}/{obs.freshness_of(oid)}" + problem = subset_count_problem(item) + return None if problem is None else f"PARTIAL_COUNT_NOT_A_LOWER_BOUND:{oid}:{problem}" + + +def _checked(values: dict[str, int]) -> None: + """HOR-PARTIAL-15: a derived tuple no target population can produce is refused, not compensated.""" + open_count, future, beyond = values[OPEN], values[FUTURE], values[BEYOND] + if min(open_count, future, beyond) < 0 or not beyond <= future <= open_count: + raise InadmissibleEvidence( + COUNTS_INCONSISTENT, + f"open {open_count}, with a future boundary {future}, beyond 28 days {beyond}: every target beyond the frame has a future boundary and every such target is open", + ) + + def evaluate(obs: ObservationSet) -> VitalResult: if not obs.is_good(CAPABILITY): + # HOR-PARTIAL-06: support versus positive absence is not established. return unknown_result( VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, [f"MISSING_REQUIRED:{CAPABILITY}:{obs.status_of(CAPABILITY)}/{obs.freshness_of(CAPABILITY)}"], @@ -59,32 +156,58 @@ def evaluate(obs: ObservationSet) -> VitalResult: return _result( obs, "UNDECLARED", + EVAL_AVAILABLE, + SEM_EXACT, + None, {"capability": capability, "open_count": 0, "open_with_future_boundary_count": 0, "open_beyond_28d_count": 0}, "No explicit planning targets are declared for this repository.", [f"PLANNING_CAPABILITY:{capability}"], ) - missing = [f"MISSING_REQUIRED:{oid}:{obs.status_of(oid)}/{obs.freshness_of(oid)}" for oid in (OPEN, FUTURE, BEYOND) if not obs.is_good(oid)] - if missing: - return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, missing, SHARED, GROUPS) - open_count = as_int(obs.value_of(OPEN)) - future = as_int(obs.value_of(FUTURE)) - beyond = as_int(obs.value_of(BEYOND)) + problems = [] + for oid in COUNTS: + if not obs.is_good(oid): + problem = _subset_lower_bound(obs, oid) + if problem is not None: + problems.append(problem) + if problems: + return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, problems, SHARED, GROUPS) + + values = {oid: as_int(obs.value_of(oid)) for oid in COUNTS} + _checked(values) + open_count, future, beyond = values[OPEN], values[FUTURE], values[BEYOND] nearest = obs.value_of(NEAREST) if obs.is_good(NEAREST) else None - if beyond > 0: - band = "EXTENDED" - elif future > 0: - band = "VISIBLE" - elif open_count > 0: - band = "DECLARED" - else: - band = "UNDECLARED" - derived = { + derived: dict[str, Any] = { "capability": capability, "open_count": open_count, "open_with_future_boundary_count": future, "open_beyond_28d_count": beyond, "nearest_future_boundary_days": nearest, } - explanation = f"{open_count} open planning targets, {future} with a future boundary, {beyond} reaching beyond 28 days." - return _result(obs, band, derived, explanation, []) + exact = tuple(obs.is_good(oid) for oid in COUNTS) + if all(exact): + band = classify(open_count, future, beyond) + explanation = f"{open_count} open planning targets, {future} with a future boundary, {beyond} reaching beyond 28 days." + return _result(obs, band, EVAL_AVAILABLE, SEM_EXACT, None, derived, explanation, []) + + # PV-HORIZON-PARTIAL-001: the counts of a partial enumeration are lower bounds. + for oid, is_exact in zip(COUNTS, exact): + if not is_exact: + derived[f"{oid.rsplit('.', 1)[1]}_semantics"] = "LOWER_BOUND" + reachable = reachable_bands(open_count, future, beyond, exact) + derived["reachable_bands"] = reachable + derived["partial_rule"] = PARTIAL_RULE + partial = [f"PARTIAL_ENUMERATION:{oid}:{obs.get(oid).reason_code or 'INCOMPLETE'}" for oid, is_exact in zip(COUNTS, exact) if not is_exact] + if len(reachable) == 1: + band = reachable[0] + derived["possible_bands_semantics"] = SEM_SUPERSET + return _result( + obs, band, EVAL_DEGRADED, SEM_EXACT, [band], derived, + f"At least {open_count} open planning targets were returned by an incomplete enumeration, {beyond} of them beyond 28 days; every completion of the missing targets is {band}.", + partial + [BAND_INVARIANT], + ) + return _result( + obs, None, EVAL_UNKNOWN, None, None, derived, + f"At least {open_count} open planning targets were returned by an incomplete enumeration; the missing ones could make Horizon {', '.join(reachable)}, and Horizon declares no order to choose one.", + partial + [AMBIGUOUS], + ) diff --git a/src/devostasis/vitals/integrity.py b/src/devostasis/vitals/integrity.py index 6772a24..1430dc2 100644 --- a/src/devostasis/vitals/integrity.py +++ b/src/devostasis/vitals/integrity.py @@ -36,6 +36,19 @@ A required series that cannot be used is ``UNKNOWN`` whatever ``ci.configured`` says, because "no verification evidence" is exactly what it cannot establish, and a verdict outside the canonical vocabulary is read as ``UNKNOWN``. + +Rule ``integrity.bands.v1+ci-unit-004+hist-002`` (0.2.0) counts history over +the durable union of **PV-HIST-002** (accepted by PV-REV-HIST-002, HIST-01..20) +instead of over what the provider still shows: a failure one bundle proved +stays in the counts while its revision is in the window, whatever the provider +shows later, and favorable evidence that cannot prove every attempt +(parent-level check suites, incomplete attempts) is ``UNKNOWN_HISTORY``, never +a pass (R52). Any revision whose history is unknown makes the Vital +``UNKNOWN``: T9 puts unknown history above every conservative branch, so no +band is derived around it. The current verdict is still the current +observation's alone (HIST-16), and the reconciled union is emitted in +``derived.revision_history`` on every path, so the next bundle can carry it +even across an evaluation that claimed no band (see ``revision_history``). """ from __future__ import annotations @@ -45,6 +58,7 @@ from ..canonical import ratio from ..observations import AVAILABLE, FRESH, PARTIAL, ObservationSet from ..policy import INTEGRITY +from ..revision_history import CARRIED, SOURCE_GAP, UNKNOWN_HISTORY, Reconciled, reconcile from .common import ( EVAL_AVAILABLE, EVAL_DEGRADED, @@ -52,12 +66,12 @@ SEM_EXACT, VitalResult, input_meta, - unknown_result, ) VITAL_ID = "integrity" VITAL_VERSION = "PV-VITALS-V1-002/integrity" -RULE_ID = "integrity.bands.v1+ci-unit-004" +RULE_ID = "integrity.bands.v1+ci-unit-004+hist-002" +HISTORY_RULE = "PV-HIST-002" UNKNOWN_RULE = "PV-REV-INTEGRITY-UNKNOWN-001" PARTIAL_RULE = "PV-REV-TEST-003" SPARSE_RULE = "PV-REV-TEST-VECTORS-002" @@ -68,7 +82,7 @@ CONFIGURED = "ci.configured" REVISIONS = "ci.revision_verdicts_14d" -IDS = [CONFIGURED, REVISIONS] +IDS = [CONFIGURED, REVISIONS, CARRIED] SHARED = ["CI_VERIFICATION"] GROUPS = ["INTEGRITY_ONLY"] @@ -143,12 +157,14 @@ def sample_strength(n: int) -> str | None: return SAMPLE_SPARSE if n < INTEGRITY["established_sample"] else SAMPLE_ESTABLISHED -HISTORY_CONTRIBUTION = {"FAILURE_OBSERVED": VERIFY_FAIL, "PASS_ONLY_OBSERVED": VERIFY_PASS, "NO_DECISIVE_OBSERVED": None} +HISTORY_CONTRIBUTION = {"FAILURE_OBSERVED": VERIFY_FAIL, "PASS_ONLY_OBSERVED": VERIFY_PASS, "NO_DECISIVE_OBSERVED": None, UNKNOWN_HISTORY: None} def _record_consistent(record: Any) -> bool: - """A revision record's history state is in the vocabulary and its contribution is the one PV-CI-UNIT-004 derives from it.""" - if not isinstance(record, dict) or not isinstance(record.get("history_state"), str) or record["history_state"] not in HISTORY_CONTRIBUTION: + """A revision record names its immutable revision, its history state is in the vocabulary, and its contribution is the one PV-CI-UNIT-004 derives from it.""" + if not isinstance(record, dict) or not isinstance(record.get("revision"), str) or not record["revision"]: + return False + if not isinstance(record.get("history_state"), str) or record["history_state"] not in HISTORY_CONTRIBUTION: return False if not isinstance(record.get("current_verdict"), (str, type(None))): return False @@ -167,8 +183,24 @@ def established_failing(n: int, f: int) -> bool: return n >= INTEGRITY["established_sample"] and f * fail_den >= n * fail_num +def _unknown(obs: ObservationSet, history: dict[str, Any], diagnostics: list[str], explanation: str = "Evidence is insufficient for a deterministic band; no band is fabricated.") -> VitalResult: + """UNKNOWN with no band, still carrying the durable history for the next bundle.""" + return _result(obs, None, EVAL_UNKNOWN, None, None, dict(history), diagnostics, explanation) + + +def _bearing(history: Reconciled, revisions: list[dict[str, Any]]) -> list[dict[str, Any]]: + """The reconciled records of the current inventory that carry any verification history.""" + records = [history.records[r.get("revision")] for r in revisions if isinstance(r, dict) and r.get("revision") in history.records] + return [r for r in records if r["parents"] or r.get("unresolved")] + + def evaluate(obs: ObservationSet) -> VitalResult: diagnostics: list[str] = [] + reconciled = reconcile(obs) + history = {"revision_history": reconciled.output()} + if reconciled.source["status"] == SOURCE_GAP: + # HIST-08: explicit, and nothing older is carried in its place. + diagnostics.append(f"REVISION_HISTORY_GAP:{reconciled.source.get('reason')}") configured_obs = obs.get(CONFIGURED) series = obs.get(REVISIONS) configured_known = configured_obs is not None and configured_obs.good @@ -188,19 +220,40 @@ def evaluate(obs: ObservationSet) -> VitalResult: diagnostics.append(f"MISSING_REQUIRED:{REVISIONS}:{obs.status_of(REVISIONS)}/{obs.freshness_of(REVISIONS)}") if not configured_known: diagnostics.append(f"MISSING_REQUIRED:{CONFIGURED}:{obs.status_of(CONFIGURED)}/{obs.freshness_of(CONFIGURED)}") - return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, diagnostics, SHARED, GROUPS) + return _unknown(obs, history, diagnostics) revisions: list[dict[str, Any]] = list(series.value) inconsistent = [r for r in revisions if not _record_consistent(r)] - if inconsistent: + if inconsistent or reconciled.problems: # A record whose contribution contradicts its own history (or names a - # state outside the vocabulary) is a coverage defect, not a pass. + # state outside the vocabulary), or carried history that is not the + # shape its lineage declares, is a coverage defect, not a pass. diagnostics.extend(f"REVISION_RECORD_INCONSISTENT:{_record_label(r)}" for r in inconsistent) - return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, diagnostics, SHARED, GROUPS) + diagnostics.extend(reconciled.problems) + return _unknown(obs, history, diagnostics) active = [r for r in revisions if r.get("parents")] - decisive = [r for r in active if r.get("historical_contribution") in DECISIVE] + provenance: dict[str, int] = {} + for r in active: + key = r.get("history_provenance") or "UNKNOWN" + provenance[key] = provenance.get(key, 0) + 1 + if provenance.get("PARENT_LEVEL_ONLY"): + # Parent-level surfaces cannot prove an earlier failure; since R52 a + # favorable one is unknown history, and this names the surface. + diagnostics.append(f"HISTORY_PROVENANCE_PARENT_LEVEL_ONLY:{provenance['PARENT_LEVEL_ONLY']}") + bearing = _bearing(reconciled, revisions) + decisive = [r for r in bearing if r.get("historical_contribution") in DECISIVE] failed = [r for r in decisive if r.get("history_state") == "FAILURE_OBSERVED"] + unknown_history = [r for r in bearing if r.get("history_state") == UNKNOWN_HISTORY] n, f = len(decisive), len(failed) + counts: dict[str, Any] = { + "revisions_in_window": len(revisions), + "revisions_with_verification": len(active), + "revisions_with_history": len(bearing), + "decisive_count_14d": n, + "failed_count_14d": f, + "unknown_history_count_14d": len(unknown_history), + "history_rule": HISTORY_RULE, + } if series.status == PARTIAL: # PV-REV-TEST-003: a truncated required series has no accepted degraded @@ -214,19 +267,29 @@ def evaluate(obs: ObservationSet) -> VitalResult: EVAL_UNKNOWN, None, None, - { - "series_status": PARTIAL, - "revisions_in_window": len(revisions), - "revisions_with_verification": len(active), - "decisive_count_14d": n, - "failed_count_14d": f, - "partial_series_rule": PARTIAL_RULE, - }, + {**counts, "series_status": PARTIAL, "partial_series_rule": PARTIAL_RULE, **history}, diagnostics, f"The revision series is incomplete ({series.reason_code or 'truncated'}); {n} decisive revisions were observed, and no band is claimed over evidence known to be short.", ) + if unknown_history: + # R52 and PV-HIST-002: history nobody can prove is neither a pass nor a + # zero, and T9 puts it above every conservative branch. + diagnostics.extend(f"REVISION_HISTORY_UNKNOWN:{r['revision']}" for r in unknown_history) + return _result( + obs, None, EVAL_UNKNOWN, None, None, {**counts, **history}, diagnostics, + f"The verification history of {len(unknown_history)} recent revisions cannot be proven complete; {f} of {n} decisive revisions failed, and no band is claimed around the unknown ones.", + ) + if not active: + if bearing: + # Durable history proves verification happened, but nothing current + # was observed, and a current verdict is never carried (HIST-16). + diagnostics.append("CURRENT_VERIFICATION_NOT_OBSERVED") + return _result( + obs, None, EVAL_UNKNOWN, None, None, {**counts, **history}, diagnostics, + f"{len(bearing)} recent revisions carry verification history from earlier bundles, but the provider shows no current verification, so no current band is claimed.", + ) if configured_known and configured is False: return _result( obs, @@ -234,20 +297,20 @@ def evaluate(obs: ObservationSet) -> VitalResult: EVAL_AVAILABLE, SEM_EXACT, None, - {"decisive_count_14d": 0, "failed_count_14d": 0, "revisions_in_window": len(revisions), "revisions_with_verification": 0}, + {**counts, **history}, diagnostics, "No automated verification is configured and no verification evidence exists for recent revisions.", ) if not configured_known: diagnostics.append(f"MISSING_REQUIRED:{CONFIGURED}:{obs.status_of(CONFIGURED)}/{obs.freshness_of(CONFIGURED)}") - return unknown_result(VITAL_ID, VITAL_VERSION, RULE_ID, obs, IDS, diagnostics, SHARED, GROUPS) + return _unknown(obs, history, diagnostics) return _result( obs, "NO_RECENT_RUNS", EVAL_AVAILABLE, SEM_EXACT, None, - {"decisive_count_14d": 0, "failed_count_14d": 0, "revisions_in_window": len(revisions), "revisions_with_verification": 0}, + {**counts, **history}, diagnostics, f"Verification is configured but none of the {len(revisions)} recent default-branch revisions has a verification execution.", ) @@ -264,30 +327,21 @@ def evaluate(obs: ObservationSet) -> VitalResult: diagnostics.append(f"CURRENT_VERDICT_UNRECOGNIZED:{current}") current = VERDICT_UNKNOWN - provenance: dict[str, int] = {} - for r in active: - key = r.get("history_provenance") or "UNKNOWN" - provenance[key] = provenance.get(key, 0) + 1 - if provenance.get("PARENT_LEVEL_ONLY"): - diagnostics.append(f"HISTORY_PROVENANCE_PARENT_LEVEL_ONLY:{provenance['PARENT_LEVEL_ONLY']}") - strength = sample_strength(n) if strength == SAMPLE_SPARSE: diagnostics.append(SPARSE_DIAGNOSTIC) derived: dict[str, Any] = { - "revisions_in_window": len(revisions), - "revisions_with_verification": len(active), - "decisive_count_14d": n, - "failed_count_14d": f, + **counts, "failure_ratio_14d": ratio(f, n) if n else None, "current_revision": { "revision": latest.get("revision"), "committed_at": latest.get("committed_at"), "current_verdict": current, - "history_state": latest.get("history_state"), + "history_state": reconciled.records.get(latest.get("revision"), {}).get("history_state", latest.get("history_state")), }, "history_provenance": dict(sorted(provenance.items())), + **history, } if strength is not None: derived["sample_strength"] = strength diff --git a/tests/conformance/test_integrity.py b/tests/conformance/test_integrity.py index 7d2157d..1604aa7 100644 --- a/tests/conformance/test_integrity.py +++ b/tests/conformance/test_integrity.py @@ -160,7 +160,7 @@ def test_partial_revision_series_is_unknown_with_its_evidence_preserved(): assert result.band is None and result.evaluation_status == "UNKNOWN" and result.possible_bands is None assert result.derived["decisive_count_14d"] == 5 and result.derived["series_status"] == "PARTIAL" assert "REVISION_SERIES_PARTIAL:PAGINATION_CAPPED" in result.diagnostics - assert result.rule_id == "integrity.bands.v1+ci-unit-004" + assert result.rule_id == "integrity.bands.v1+ci-unit-004+hist-002" def test_sparse_samples_declare_their_strength_and_established_ones_do_not_carry_the_diagnostic(): @@ -277,9 +277,37 @@ def test_latest_non_decisive_revision_falls_back_to_latest_decisive_verdict(): assert any(code.startswith("LATEST_REVISION_NON_DECISIVE") for code in result.diagnostics) -def test_parent_level_provenance_is_diagnosed(): +def test_parent_level_provenance_is_diagnosed_and_a_favorable_one_is_unknown_history(): + """R52 and PV-HIST-002 HIST-06: a check suite hides its earlier outcomes, so its pass proves no history. + + The record states PASS_ONLY_OBSERVED, as a 0.1.x record would; the union + is rebuilt from its parents, and a parent-level pass is not a complete + history, so the Vital claims no band around it. + """ revs = [revision("s", "2026-09-01T00:00:00Z", [parent("suite", "VERIFY_PASS", kind="github_check_suite")], "VERIFY_PASS", "PASS_ONLY_OBSERVED", provenance="PARENT_LEVEL_ONLY")] obs = obs_set() integrity_inputs(obs, True, revs) result = integrity.evaluate(obs) - assert "HISTORY_PROVENANCE_PARENT_LEVEL_ONLY:1" in result.diagnostics + assert "HISTORY_PROVENANCE_PARENT_LEVEL_ONLY:1" in result.diagnostics and "REVISION_HISTORY_UNKNOWN:s" in result.diagnostics + assert (result.band, result.evaluation_status) == (None, "UNKNOWN") + assert result.derived["revision_history"]["records"][0]["history_state"] == "UNKNOWN_HISTORY" + + +def test_a_parent_level_failure_is_still_a_proven_failure(): + """Only favorable parent-level evidence is unknown: an observed failure is proven on any surface.""" + revs = [revision("s", "2026-09-01T00:00:00Z", [parent("suite", "VERIFY_FAIL", kind="github_check_suite")], "VERIFY_FAIL", "FAILURE_OBSERVED", provenance="PARENT_LEVEL_ONLY")] + obs = obs_set() + integrity_inputs(obs, True, revs) + result = integrity.evaluate(obs) + assert (result.band, result.evaluation_status) == ("FAILING", "AVAILABLE") and result.derived["failed_count_14d"] == 1 + + +def test_a_record_without_its_revision_identity_is_a_defect_not_a_crash(): + """Durable history is keyed by immutable revision (PV-HIST-002): a record that names none cannot be counted or carried.""" + anonymous = revision("x", "2026-09-01T00:00:00Z", [parent("p", "VERIFY_PASS")], "VERIFY_PASS", "PASS_ONLY_OBSERVED") + del anonymous["revision"] + obs = obs_set() + integrity_inputs(obs, True, passing_revisions(4) + [anonymous]) + result = integrity.evaluate(obs) + assert (result.band, result.evaluation_status) == (None, "UNKNOWN") + assert any(code.startswith("REVISION_RECORD_INCONSISTENT") for code in result.diagnostics) diff --git a/tests/conformance/test_planning_and_debt.py b/tests/conformance/test_planning_and_debt.py index 599d2c4..c5e753e 100644 --- a/tests/conformance/test_planning_and_debt.py +++ b/tests/conformance/test_planning_and_debt.py @@ -1,5 +1,7 @@ """Horizon, Direction and Debt (V1-03..V1-07, V1-13..V1-15).""" +import pytest + from devostasis.observations import FORBIDDEN, PARTIAL from devostasis.vitals import debt, direction, horizon from helpers import add, debt_inputs, direction_inputs, obs_set, planning_inputs @@ -89,22 +91,39 @@ def test_configured_debt_with_forbidden_register_is_unknown_never_clear(): assert result.band is None and result.evaluation_status == "UNKNOWN" +SUBSET = {"complete": False, "value_semantics": "OBSERVED_SUBSET_COUNT"} +MAPPING = {"source": "labels", "labels": ["debt"], "mapping_version": "1"} + + def test_partial_register_with_observed_items_is_degraded_present(): + """PD1 of PV-DEBT-PARTIAL-001: the band is exact, the acquisition is not.""" obs = obs_set() add(obs, "debt.registry.capability", "CONFIGURED", "enum") - add(obs, "debt.items.open_count", 3, status=PARTIAL, reason_code="PAGINATION_CAPPED") + add(obs, "debt.mapping", MAPPING, "record") + add(obs, "debt.items.open_count", 3, status=PARTIAL, reason_code="PAGINATION_CAPPED", coverage=SUBSET) result = debt.evaluate(obs) - assert result.band == "PRESENT" and result.evaluation_status == "DEGRADED" + assert (result.band, result.evaluation_status, result.band_semantics, result.possible_bands) == ("PRESENT", "DEGRADED", "EXACT", None) + assert result.derived["open_count_semantics"] == "LOWER_BOUND" and result.derived["confirmed_open_count"] == 3 def test_a_stale_partial_register_proves_nothing_about_now(): """vitals.md: configured debt evidence that is stale yields UNKNOWN; a fresh partial one is a lower bound.""" - from devostasis.observations import PARTIAL - for freshness, expected in (("FRESH", "PRESENT"), ("STALE", None), ("UNKNOWN", None)): obs = obs_set() add(obs, "debt.registry.capability", "CONFIGURED", "enum") - add(obs, "debt.mapping", {"source": "labels", "labels": ["debt"], "mapping_version": "1"}, "record") - add(obs, "debt.items.open_count", 2, status=PARTIAL, freshness=freshness, reason_code="PAGINATION_CAPPED") + add(obs, "debt.mapping", MAPPING, "record") + add(obs, "debt.items.open_count", 2, status=PARTIAL, freshness=freshness, reason_code="PAGINATION_CAPPED", coverage=SUBSET) result = debt.evaluate(obs) assert result.band == expected, freshness + + +@pytest.mark.parametrize("coverage", [None, {"complete": False}, {"complete": False, "value_semantics": "ESTIMATE"}, {"complete": True, "value_semantics": "OBSERVED_SUBSET_COUNT"}, "not a record"]) +def test_a_partial_debt_count_without_the_subset_proof_is_not_a_lower_bound(coverage): + """Issue #39: status PARTIAL proves nothing about the value; only a proven observed-subset count is a floor.""" + obs = obs_set() + add(obs, "debt.registry.capability", "CONFIGURED", "enum") + add(obs, "debt.mapping", MAPPING, "record") + add(obs, "debt.items.open_count", 4, status=PARTIAL, reason_code="PAGINATION_CAPPED", coverage=coverage) + result = debt.evaluate(obs) + assert (result.band, result.evaluation_status) == (None, "UNKNOWN") + assert any(code.startswith("PARTIAL_COUNT_NOT_A_LOWER_BOUND:debt.items.open_count") for code in result.diagnostics) diff --git a/tests/conformance/test_t9_reconciliation.py b/tests/conformance/test_t9_reconciliation.py new file mode 100644 index 0000000..0c857e4 --- /dev/null +++ b/tests/conformance/test_t9_reconciliation.py @@ -0,0 +1,357 @@ +"""T9, the final decision-equivalence reconciliation (PV-TEST-004, generator PV-T9-GEN-003). + +PV-REV-TEST-004 accepted T9 with zero CONTRACT_GAP terminals: each of the nine +gap identifiers PV-T9-GEN-002 exposed maps to exactly one deterministic +accepted obligation (sections 3.1 to 3.9), and higher-precedence fail-closed +conditions stay higher (section 4). This is the implementation side of that +reconciliation: a finite generator over the acquisition tokens (CF, +POS_ABSENT, PARTIAL, UNAVAILABLE, FORBIDDEN, UNKNOWN, ERROR, STALE) and the +threshold and relational regions each former gap family names. Every cell is +evaluated, and must reach the one obligation the accepted table gives it. + +It is not the T9v2 cell-key serialization: the research process delivers that +as vectors (PV-TEST-VECTORS-00n), and each family's accepted fixtures are +already vectors here (HOR-PARTIAL, CLU-INCOMPLETE, DIR-INCOMPLETE, INT-TOTAL, +DEBT-PARTIAL). What this proves is the property T9 exists for: no cell of the +nine families is left without its obligation, and none reaches another one. +The oracles below restate the accepted table, written from the contract text +rather than from the evaluators. +""" + +from __future__ import annotations + +import itertools + +import pytest + +from devostasis.normalize import linkage_state +from devostasis.observations import Observation, ObservationSet +from devostasis.vitals import clutter, debt, direction, horizon, integrity + +OBSERVED_AT = "2026-09-10T12:00:00Z" +SUBSET = {"complete": False, "value_semantics": "OBSERVED_SUBSET_COUNT"} +UNCERTAIN = ("UNAVAILABLE", "FORBIDDEN", "UNKNOWN", "ERROR") +TOKENS = ("CF", "PARTIAL") + UNCERTAIN + ("STALE",) + + +def _obs() -> ObservationSet: + return ObservationSet(subject={"provider": "t9", "immutable_project_id": "t9"}, observed_at=OBSERVED_AT) + + +def _put(obs: ObservationSet, oid: str, token: str, value, value_type: str = "count") -> None: + common = {"provider": "t9", "collected_at": OBSERVED_AT, "source_ref": "t9", "adapter_version": "t9"} + if token == "CF": + obs.add(Observation(oid, "AVAILABLE", value_type, value, **common)) + elif token == "STALE": + obs.add(Observation(oid, "AVAILABLE", value_type, value, freshness="STALE", **common)) + elif token == "PARTIAL": + obs.add(Observation(oid, "PARTIAL", value_type, value, coverage=SUBSET if value_type == "count" else None, reason_code="PAGINATION_CAPPED", **common)) + else: + obs.add(Observation(oid, token, value_type, None, reason_code=f"T9_{token}", **common)) + + +def _outcome(result) -> tuple: + return (result.evaluation_status, result.band, result.band_semantics, result.possible_bands) + + +UNKNOWN = ("UNKNOWN", None, None, None) + + +def _assert_one_obligation(result, expected, cell): + outcome = _outcome(result) + assert outcome[0] in ("AVAILABLE", "DEGRADED", "UNKNOWN"), cell + assert (outcome[1] is None) == (outcome[0] == "UNKNOWN"), f"{cell}: a band exactly when the status is not UNKNOWN" + assert not any("CONTRACT_GAP" in code for code in result.diagnostics), f"{cell}: CONTRACT_GAP is not a terminal" + assert outcome == expected, f"{cell}: expected {expected}, got {outcome}" + + +# --------------------------------------------------------------------------- 3.1 Horizon partial bound + +HORIZON_SHAPES = {"PH0": (0, 0, 0), "PH1": (2, 0, 0), "PH2": (2, 1, 0), "PH3": (2, 1, 1)} +HORIZON_CAPABILITIES = ("SUPPORTED", "UNSUPPORTED", "SUPPORTED_UNUSED") + UNCERTAIN + ("STALE", "PARTIAL") + + +def _horizon_expected(capability: str, token: str, shape: str): + if capability in ("UNSUPPORTED", "SUPPORTED_UNUSED"): + return ("AVAILABLE", "UNDECLARED", "EXACT", None) + if capability != "SUPPORTED": + return UNKNOWN + open_count, future, beyond = HORIZON_SHAPES[shape] + if token == "CF": + band = "EXTENDED" if beyond else "VISIBLE" if future else "DECLARED" if open_count else "UNDECLARED" + return ("AVAILABLE", band, "EXACT", None) + if token == "PARTIAL": + return ("DEGRADED", "EXTENDED", "EXACT", ["EXTENDED"]) if shape == "PH3" else UNKNOWN + return UNKNOWN + + +HORIZON_CELLS = list(itertools.product(HORIZON_CAPABILITIES, TOKENS, HORIZON_SHAPES)) + + +@pytest.mark.parametrize("capability, token, shape", HORIZON_CELLS) +def test_t9_gap_horizon_partial_bound_001(capability, token, shape): + obs = _obs() + if capability in ("SUPPORTED", "UNSUPPORTED", "SUPPORTED_UNUSED"): + _put(obs, horizon.CAPABILITY, "CF", capability, "enum") + elif capability == "STALE": + _put(obs, horizon.CAPABILITY, "STALE", "SUPPORTED", "enum") + elif capability == "PARTIAL": + _put(obs, horizon.CAPABILITY, "PARTIAL", None, "enum") + else: + _put(obs, horizon.CAPABILITY, capability, None, "enum") + for oid, value in zip(horizon.COUNTS, HORIZON_SHAPES[shape]): + _put(obs, oid, token, value) + _assert_one_obligation(horizon.evaluate(obs), _horizon_expected(capability, token, shape), (capability, token, shape)) + + +# --------------------------------------------------------------------------- 3.2 / 3.3 Clutter incomplete components and forced bounds + +CLUTTER_ISSUES = ("CF", "PARTIAL", "UNAVAILABLE", "FORBIDDEN") +CLUTTER_CRS = ("CF", "PARTIAL", "UNAVAILABLE") +CLUTTER_BRANCHES = ("CF", "PARTIAL", "UNAVAILABLE", "STALE") +STALE_WORK = (0, 3, 30) +STALE_BRANCHES = (0, 7, 25) +CLUTTER_CELLS = list(itertools.product(CLUTTER_ISSUES, CLUTTER_CRS, CLUTTER_BRANCHES, STALE_WORK, STALE_BRANCHES)) +DEGRADED_SHAPES = [ + ("DEGRADED", "HEAVY", "EXACT", None), + ("DEGRADED", "CLUTTERED", "CONSERVATIVE_LOWER_BOUND", ["CLUTTERED", "HEAVY"]), + ("DEGRADED", "LIGHT", "CONSERVATIVE_LOWER_BOUND", ["LIGHT", "CLUTTERED", "HEAVY"]), +] + + +@pytest.mark.parametrize("issues, crs, branches, stale_issues, stale_branches", CLUTTER_CELLS) +def test_t9_gap_clutter_unavailable_component_and_partial_forced_bound(issues, crs, branches, stale_issues, stale_branches): + """3.2 and 3.3: an incomplete cell is a confirmed floor from positive facts only, never CLEAN, never zero-filled.""" + obs = _obs() + _put(obs, clutter.ISSUES_OPEN, issues, 100) + _put(obs, clutter.ISSUES_STALE, issues, stale_issues) + _put(obs, clutter.CR_OPEN, crs, 10) + _put(obs, clutter.CR_STALE, crs, 0) + _put(obs, clutter.BRANCHES_STALE, branches, stale_branches) + if branches == "PARTIAL": + _put(obs, clutter.BRANCHES_RETENTION, "CF", "CLASSIFIED", "enum") + result = clutter.evaluate(obs) + outcome = _outcome(result) + cell = (issues, crs, branches, stale_issues, stale_branches) + assert outcome[0] in ("AVAILABLE", "DEGRADED", "UNKNOWN") and (outcome[1] is None) == (outcome[0] == "UNKNOWN"), cell + fail_closed = crs == "UNAVAILABLE" or issues == "FORBIDDEN" or branches == "STALE" + complete = issues == crs == branches == "CF" + if fail_closed: + assert outcome == UNKNOWN, f"{cell}: a required, forbidden or stale component is never bounded around" + return + if complete: + assert outcome[0] == "AVAILABLE" and outcome[2] == "EXACT", cell + return + assert outcome[1] != "CLEAN" and outcome[0] != "AVAILABLE", f"{cell}: incomplete coverage is never exact, never CLEAN" + if outcome[0] == "DEGRADED": + assert outcome in DEGRADED_SHAPES, f"{cell}: {outcome} is not an accepted floor shape" + proven_work = stale_issues if issues in ("CF", "PARTIAL") else 0 + proven_branches = stale_branches if branches in ("CF", "PARTIAL") else 0 + if proven_work >= 25 or proven_branches >= 20: + assert outcome == ("DEGRADED", "HEAVY", "EXACT", None), f"{cell}: a proven HEAVY fact forces HEAVY" + if proven_work == 0 and proven_branches == 0: + assert outcome == UNKNOWN, f"{cell}: without a positive fact there is no floor" + + +# --------------------------------------------------------------------------- 3.4 Direction partial linkage + + +def _direction_band(k: int, n: int) -> str: + return "FULLY_LINKED" if k == n else "MIXED" if 2 * k >= n else "SCATTERED" + + +def _direction_expected(capability: str, n_token: str, n: int, linked: int, unresolved: int): + if n_token != "CF": + return UNKNOWN + if capability == "POS_ABSENT": + return ("AVAILABLE", "UNDECLARED", "EXACT", None) + if capability != "SUPPORTED": + return UNKNOWN + if unresolved == 0: + return ("AVAILABLE", _direction_band(linked, n), "EXACT", None) + reachable = {_direction_band(k, n) for k in range(linked, linked + unresolved + 1)} + if len(reachable) == 1: + band = reachable.pop() + return ("DEGRADED", band, "EXACT", [band]) + return UNKNOWN + + +DIRECTION_SPLITS = [(n, l, u, r) for n in range(1, 6) for l in range(n + 1) for u in range(n + 1 - l) for r in [n - l - u]] +DIRECTION_CELLS = list(itertools.product(("SUPPORTED", "POS_ABSENT", "UNKNOWN", "PARTIAL"), ("CF", "PARTIAL"), DIRECTION_SPLITS)) + + +@pytest.mark.parametrize("capability, n_token, split", DIRECTION_CELLS) +def test_t9_gap_direction_partial_linkage_001(capability, n_token, split): + n, linked, unlinked, unresolved = split + obs = _obs() + if capability == "SUPPORTED": + _put(obs, horizon.CAPABILITY, "CF", "SUPPORTED", "enum") + elif capability == "POS_ABSENT": + _put(obs, horizon.CAPABILITY, "CF", "UNSUPPORTED", "enum") + else: + _put(obs, horizon.CAPABILITY, capability, None, "enum") + _put(obs, direction.ACTIVE, n_token, n) + for oid, value in ((direction.LINKED, linked), (direction.UNLINKED, unlinked), (direction.UNRESOLVED, unresolved)): + _put(obs, oid, n_token, value) + expected = _direction_expected(capability, n_token, n, linked, unresolved) + _assert_one_obligation(direction.evaluate(obs), expected, (capability, n_token, split)) + if unresolved and expected[0] == "DEGRADED": + assert expected[1] != "FULLY_LINKED", "a singleton FULLY_LINKED is impossible while R > 0" + + +# --------------------------------------------------------------------------- 3.5 Direction unresolved target + +REFERENCE_STATES = ("OPEN", "CLOSED", "MISSING", "UNKNOWN", "UNAVAILABLE", "FORBIDDEN", "ERROR", "STALE", None) + + +@pytest.mark.parametrize("state, readable", list(itertools.product(REFERENCE_STATES, (True, False)))) +def test_t9_gap_direction_unresolved_target_001(state, readable): + """Resolved identity is LINKED open or closed, positive absence UNLINKED, anything unresolvable UNRESOLVED.""" + item = {"target_refs": [{"target_id": "T", "state": state}], "linkage_unresolved": [] if readable else ["body:dict"]} + if state in ("OPEN", "CLOSED"): + expected = "LINKED" + elif state == "MISSING" and readable: + expected = "UNLINKED" + else: + expected = "UNRESOLVED" + assert linkage_state(item) == expected + obs = _obs() + _put(obs, horizon.CAPABILITY, "CF", "SUPPORTED", "enum") + for oid, value in ((direction.ACTIVE, 1), (direction.LINKED, int(expected == "LINKED")), (direction.UNLINKED, int(expected == "UNLINKED")), (direction.UNRESOLVED, int(expected == "UNRESOLVED"))): + _put(obs, oid, "CF", value) + outcome = {"LINKED": ("AVAILABLE", "FULLY_LINKED", "EXACT", None), "UNLINKED": ("AVAILABLE", "SCATTERED", "EXACT", None), "UNRESOLVED": UNKNOWN}[expected] + _assert_one_obligation(direction.evaluate(obs), outcome, (state, readable)) + + +# --------------------------------------------------------------------------- 3.6 / 3.7 / 3.8 Integrity + + +def _parent(pid: str, state: str) -> dict: + return { + "parent_id": pid, "kind": "github_actions_workflow_run", "name": "ci", "event": "push", + "current_attempt": 1, "current_state": state, "attempts_observed": [{"attempt": 1, "state": state}], + "attempts_complete": True, "history_state": "", "url": None, + } + + +def _revision(sha: str, day: int, state: str | None) -> dict: + parents = [_parent(f"github_actions:workflow_run:{sha}", state)] if state else [] + history = "FAILURE_OBSERVED" if state == "VERIFY_FAIL" else "PASS_ONLY_OBSERVED" if state == "VERIFY_PASS" else "NO_DECISIVE_OBSERVED" + for parent in parents: + parent["history_state"] = history + contribution = {"FAILURE_OBSERVED": "VERIFY_FAIL", "PASS_ONLY_OBSERVED": "VERIFY_PASS"}.get(history) + return { + "revision": sha, "committed_at": f"2026-08-{day:02d}T10:00:00Z" if day > 27 else f"2026-09-{day:02d}T10:00:00Z", + "parents": parents, "current_verdict": state, "history_state": history, "historical_contribution": contribution, + "history_provenance": "ATTEMPT_LEVEL" if parents else None, "history_complete": True, + } + + +def _integrity(configured, revisions, series_token="CF"): + obs = _obs() + if configured is not None: + _put(obs, integrity.CONFIGURED, "CF", configured, "boolean") + _put(obs, integrity.REVISIONS, series_token, revisions, "series") + return integrity.evaluate(obs) + + +def _history(n: int, f: int) -> list[dict]: + return [_revision(f"d{i}", 1 + i, "VERIFY_FAIL" if i < f else "VERIFY_PASS") for i in range(n)] + + +INTEGRITY_REGIONS = [(n, f) for n in range(0, 7) for f in range(0, n + 1)] + + +@pytest.mark.parametrize("n, f", INTEGRITY_REGIONS) +def test_t9_gap_integrity_unresolved_band_001(n, f): + """3.6: a newest VERIFY_UNRESOLVED names FAILING only where the established predicate already holds.""" + revisions = _history(n, f) + [_revision("newest", 9, "VERIFY_UNRESOLVED")] + established = n >= 4 and 4 * f >= n + expected = ("DEGRADED", "FAILING", "EXACT", None) if established else UNKNOWN + _assert_one_obligation(_integrity(True, revisions), expected, (n, f)) + + +@pytest.mark.parametrize("n, f", INTEGRITY_REGIONS) +def test_t9_gap_integrity_partial_superset_001(n, f): + """3.7, the false gap: a PARTIAL required series is UNKNOWN with no band and no possible_bands, whatever it holds.""" + revisions = _history(n, f) + [_revision("newest", 9, "VERIFY_UNRESOLVED")] + _assert_one_obligation(_integrity(True, revisions, "PARTIAL"), UNKNOWN, (n, f)) + _assert_one_obligation(_integrity(True, _history(n, f), "PARTIAL"), UNKNOWN, (n, f, "decided")) + + +@pytest.mark.parametrize("states", [("NON_VERIFY_TERMINAL",), ("NOT_EXECUTED",), ("NOT_EXECUTED", "NON_VERIFY_TERMINAL"), (None, "NOT_EXECUTED")]) +def test_t9_gap_integrity_unconfigured_recent_001(states): + """3.8: positively unconfigured with only non-decisive recent evidence is exactly UNINSTRUMENTED, the evidence kept.""" + revisions = [_revision(f"r{i}", 3 + i, state) for i, state in enumerate(states)] + result = _integrity(False, revisions) + _assert_one_obligation(result, ("AVAILABLE", "UNINSTRUMENTED", "EXACT", None), states) + assert "CI_UNINSTRUMENTED_WITH_RECENT_NONDECISIVE_HISTORY" in result.diagnostics + _assert_one_obligation(_integrity(True, revisions), ("AVAILABLE", "NO_DECISIVE_RUNS", "EXACT", None), (states, "configured")) + newest_unknown = revisions + [_revision("newest", 9, "UNKNOWN")] + _assert_one_obligation(_integrity(False, newest_unknown), UNKNOWN, (states, "newest UNKNOWN")) + + +def test_t9_section_4_unknown_history_outranks_the_unresolved_failing_branch(): + """Section 4: unknown history is never routed through a conservative branch to obtain a band.""" + suite = { + "parent_id": "github_checks:check_suite:1", "kind": "github_check_suite", "name": "app", "event": None, + "current_attempt": None, "current_state": "VERIFY_PASS", "attempts_observed": [{"attempt": None, "state": "VERIFY_PASS"}], + "attempts_complete": False, "history_state": "UNKNOWN_HISTORY", "url": None, + } + unknown_revision = { + "revision": "suite", "committed_at": "2026-09-07T10:00:00Z", "parents": [suite], "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", "historical_contribution": None, "history_provenance": "PARENT_LEVEL_ONLY", "history_complete": False, + } + established = _history(6, 3) + [unknown_revision, _revision("newest", 9, "VERIFY_UNRESOLVED")] + assert _outcome(_integrity(True, _history(6, 3) + [_revision("newest", 9, "VERIFY_UNRESOLVED")]))[1] == "FAILING" + _assert_one_obligation(_integrity(True, established), UNKNOWN, "established FAILING beside unknown history") + + +# --------------------------------------------------------------------------- 3.9 Debt partial bound + +DEBT_CAPABILITIES = ("CONFIGURED", "UNCONFIGURED", "UNKNOWN", "FORBIDDEN", "STALE") +DEBT_CELLS = list(itertools.product(DEBT_CAPABILITIES, TOKENS, (0, 3))) + + +def _debt_expected(capability: str, token: str, confirmed: int): + if capability == "UNCONFIGURED": + return ("AVAILABLE", "UNINSTRUMENTED", "EXACT", None) + if capability != "CONFIGURED": + return UNKNOWN + if token == "CF": + return ("AVAILABLE", "PRESENT" if confirmed else "CLEAR", "EXACT", None) + if token == "PARTIAL": + return ("DEGRADED", "PRESENT", "EXACT", None) if confirmed else UNKNOWN + return UNKNOWN + + +@pytest.mark.parametrize("capability, token, confirmed", DEBT_CELLS) +def test_t9_gap_debt_partial_bound_001(capability, token, confirmed): + obs = _obs() + if capability in ("CONFIGURED", "UNCONFIGURED"): + _put(obs, debt.CAPABILITY, "CF", capability, "enum") + elif capability == "STALE": + _put(obs, debt.CAPABILITY, "STALE", "CONFIGURED", "enum") + else: + _put(obs, debt.CAPABILITY, capability, None, "enum") + _put(obs, debt.MAPPING, "CF", {"source": "labels", "labels": ["debt"], "mapping_version": "1"}, "record") + _put(obs, debt.OPEN, token, confirmed) + result = debt.evaluate(obs) + _assert_one_obligation(result, _debt_expected(capability, token, confirmed), (capability, token, confirmed)) + assert result.band != "ACCUMULATED" + + +def test_t9_generator_covers_every_family(): + """The generator is finite and names every former gap family: nine identifiers, none left without cells.""" + families = { + "T9-GAP-HORIZON-PARTIAL-BOUND-001": len(HORIZON_CELLS), + "T9-GAP-CLUTTER-UNAVAILABLE-COMPONENT-001": len(CLUTTER_CELLS), + "T9-GAP-CLUTTER-PARTIAL-FORCED-BOUND-001": len(CLUTTER_CELLS), + "T9-GAP-DIRECTION-PARTIAL-LINKAGE-001": len(DIRECTION_CELLS), + "T9-GAP-DIRECTION-UNRESOLVED-TARGET-001": len(REFERENCE_STATES) * 2, + "T9-GAP-INTEGRITY-UNRESOLVED-BAND-001": len(INTEGRITY_REGIONS), + "T9-GAP-INTEGRITY-PARTIAL-SUPERSET-001": len(INTEGRITY_REGIONS), + "T9-GAP-INTEGRITY-UNCONFIGURED-RECENT-001": 4, + "T9-GAP-DEBT-PARTIAL-BOUND-001": len(DEBT_CELLS), + } + assert len(families) == 9 and all(count > 0 for count in families.values()) diff --git a/tests/helpers.py b/tests/helpers.py index 16538f8..3d2349e 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -164,9 +164,13 @@ def planning_inputs(obs: ObservationSet, capability: str, open_count: int = 0, f add(obs, "planning.explicit_targets.nearest_future_boundary_days", nearest, "duration") -def direction_inputs(obs: ObservationSet, active: int, linked: int | None = None, links: dict[str, int] | None = None) -> None: +def direction_inputs(obs: ObservationSet, active: int, linked: int | None = None, links: dict[str, int] | None = None, unresolved: int = 0) -> None: + """Direction's inputs: the active population and, when ``linked`` is given, its state-neutral L/U/R split.""" add(obs, "planning.linkage.active_change_requests_count_28d", active) if linked is not None: + add(obs, "planning.linkage.active_change_requests_linked_count_28d", linked) + add(obs, "planning.linkage.active_change_requests_unlinked_count_28d", active - linked - unresolved) + add(obs, "planning.linkage.active_change_requests_unresolved_count_28d", unresolved) add(obs, "planning.linkage.active_change_requests_linked_to_open_target_count_28d", linked) if links is not None: add(obs, "planning.linkage.links_per_target_28d", links, "record") diff --git a/tests/test_bundle_history.py b/tests/test_bundle_history.py index afad62b..bad60b8 100644 --- a/tests/test_bundle_history.py +++ b/tests/test_bundle_history.py @@ -9,6 +9,7 @@ from devostasis.contracts import ARTIFACT_CONTRACT_VERSION, RENDERER_VERSION from devostasis.config import single_project from devostasis.history import FilesystemHistoryStore, ImmutabilityError +from devostasis.observations import Observation from devostasis.runner import build_from_observations from helpers import OBSERVED_AT, add, finalize, full_inputs, obs_set @@ -184,7 +185,9 @@ def test_rule_version_boundary_makes_one_vital_incomparable_inside_a_comparable_ def test_art_06_neutral_rendering_of_fully_linked_and_present(tmp_path): obs = obs_set() full_inputs(obs) - obs.replace(type(obs.get("planning.linkage.active_change_requests_linked_to_open_target_count_28d"))(**dict(obs.get("planning.linkage.active_change_requests_linked_to_open_target_count_28d").to_dict(), value=10))) + for key, value in (("linked", 10), ("unlinked", 0)): + observation_id = f"planning.linkage.active_change_requests_{key}_count_28d" + obs.replace(Observation.from_dict(dict(obs.get(observation_id).to_dict(), value=value))) bundle = build_from_observations(_project(), obs, FilesystemHistoryStore(tmp_path)) report = bundle.members["report.md"].decode("utf-8") assert "FULLY_LINKED" in report and "PRESENT" in report diff --git a/tests/test_demand_display_registers.py b/tests/test_demand_display_registers.py index 375b7f3..bf484ce 100644 --- a/tests/test_demand_display_registers.py +++ b/tests/test_demand_display_registers.py @@ -160,14 +160,20 @@ def test_file_registers_feed_horizon_direction_and_debt(): assert obs.get(INV_TARGETS).value[1]["target_id"] == "T-3" and obs.get(INV_DEBT_REGISTER).value[0]["id"] == "D-1" crs = {c["number"]: c for c in obs.value_of("forge.change_requests.inventory")} assert crs[7]["target_refs"] == [{"target_id": "T-3", "state": "OPEN"}] - assert crs[8]["target_refs"] == [{"target_id": "T-9", "state": "UNKNOWN"}] + # T-9 is not in the complete register: a broken reference, not missing evidence (DIR-INCOMPLETE-07). + assert crs[8]["target_refs"] == [{"target_id": "T-9", "state": "MISSING"}] derive(obs, project) assert obs.value_of("planning.explicit_targets.capability") == "SUPPORTED" + assert obs.value_of("planning.linkage.active_change_requests_linked_count_28d") == 1 + assert obs.value_of("planning.linkage.active_change_requests_unlinked_count_28d") == 1 assert obs.value_of("planning.linkage.active_change_requests_linked_to_open_target_count_28d") == 1 - assert obs.value_of("planning.linkage.unknown_target_reference_count_28d") == 1 + assert obs.value_of("planning.linkage.unknown_target_reference_count_28d") == 0 + assert obs.value_of("planning.linkage.missing_target_reference_count_28d") == 1 assert obs.value_of("debt.items.open_count") == 1 and obs.value_of("debt.items.open_stale_count_30d") == 1 and obs.value_of("debt.items.closed_count_28d") == 1 bands = {r.vital_id: r.band for r in evaluate_all(obs)} assert bands["horizon"] == "EXTENDED" and bands["direction"] == "MIXED" and bands["debt"] == "PRESENT" + direction = {r.vital_id: r for r in evaluate_all(obs)}["direction"] + assert "DIRECTION_TARGET_REFERENCE_MISSING:1" in direction.diagnostics def test_missing_register_is_explicit_not_undeclared(): diff --git a/tests/test_github_adapter.py b/tests/test_github_adapter.py index 20664b7..7a0a260 100644 --- a/tests/test_github_adapter.py +++ b/tests/test_github_adapter.py @@ -4,7 +4,7 @@ import pytest -from devostasis.adapters.github import GitHubAdapter, GitHubClient, LinkageEvidenceError, classify_http_error +from devostasis.adapters.github import GitHubAdapter, GitHubClient, classify_http_error from devostasis.config import single_project from devostasis.normalize import CI_CONFIGURED, CI_REVISIONS, INV_BRANCHES, INV_CRS, INV_ISSUES, INV_TARGETS, derive from devostasis.observations import AVAILABLE, ERROR, FORBIDDEN, PARTIAL, UNAVAILABLE @@ -301,7 +301,7 @@ def __exit__(self, *exc): raise AssertionError("a 200 with a non-JSON body must be a declared failure") -# --------------------------------------------------------------------------- linkage evidence (PV-REV-PR-015) +# --------------------------------------------------------------------------- linkage evidence (PV-REV-PR-015, PV-DIRECTION-INCOMPLETE-001) def _pull(number=7, **fields): @@ -332,24 +332,56 @@ def _observe(routes, project): return GitHubAdapter(GitHubClient(FakeTransport(routes)), NOW).collect(project) +def _register_route(ids_and_states): + import base64 + import json as _json + + document = {"schema": "devostasis.targets.v1", "targets": [{"id": tid, "state": state} for tid, state in ids_and_states]} + content = base64.b64encode(_json.dumps(document).encode("utf-8")).decode("ascii") + return 200, {}, {"type": "file", "size": len(content), "encoding": "base64", "content": content} + + +def _file_planning_routes(pulls, register=(("B1", "open"),)): + return _routes(**{f"{BASE}/pulls": _paged(pulls), f"{BASE}/contents/.devostasis/targets.json": _register_route(register)}) + + @pytest.mark.parametrize("field, value", [("title", 7), ("title", ["a"]), ("body", {"a": 1}), ("body", 3.5)]) -def test_unreadable_linkage_evidence_is_declared_not_read_as_unlinked(field, value): - """`Target: ` lives in the title and the body, so payload of the wrong type there is not an absent link.""" - routes = _routes(**{f"{BASE}/pulls": _paged([_pull(**{field: value})])}) - try: - _observe(routes, _file_planning_project()) - except LinkageEvidenceError as exc: - assert "#7" in str(exc) and field in str(exc) - else: - raise AssertionError(f"a {type(value).__name__} {field} must not silently become an unlinked change request") +def test_unreadable_linkage_evidence_is_unresolved_not_unlinked_and_not_a_failed_project(field, value): + """`Target: ` lives in the title and the body, so payload of the wrong type there is not an absent link. + + Since PV-DIRECTION-INCOMPLETE-001 it is not a failed project either: the + change request keeps what could be read, names what could not, and enters + Direction as UNRESOLVED instead of UNLINKED. + """ + project = _file_planning_project() + obs = _observe(_file_planning_routes([_pull(**{field: value})]), project) + record = obs.get(INV_CRS).value[0] + assert record["target_refs"] == [] and record["linkage_unresolved"] == [f"{field}:{type(value).__name__}"] + derive(obs, project) + assert obs.value_of("planning.linkage.active_change_requests_unresolved_count_28d") == 1 + assert obs.value_of("planning.linkage.active_change_requests_unlinked_count_28d") == 0 + assert obs.value_of("planning.linkage.unresolved_change_requests_28d") == {"7": [f"LINKAGE_UNREADABLE:{field}:{type(value).__name__}"]} + direction = {r.vital_id: r for r in evaluate_all(obs)}["direction"] + assert (direction.band, direction.evaluation_status) == (None, "UNKNOWN"), "one unresolved of one could be FULLY_LINKED or SCATTERED" + + +def test_a_readable_link_is_not_undone_by_an_unreadable_field(): + """Unreadable evidence can only add references, and a resolved one already links the change request.""" + project = _file_planning_project() + obs = _observe(_file_planning_routes([_pull(title="Target: B1", body={"not": "text"})]), project) + record = obs.get(INV_CRS).value[0] + assert record["target_refs"] == [{"target_id": "B1", "state": "OPEN"}] and record["linkage_unresolved"] == ["body:dict"] + derive(obs, project) + assert obs.value_of("planning.linkage.active_change_requests_linked_count_28d") == 1 + assert obs.value_of("planning.linkage.active_change_requests_unresolved_count_28d") == 0 def test_a_change_request_with_no_marker_is_unlinked_and_that_is_not_an_error(): - """The distinction the error exists to preserve: absent evidence is a fact, unreadable evidence is not.""" + """The distinction the unresolved state exists to preserve: absent evidence is a fact, unreadable evidence is not.""" routes = _routes(**{f"{BASE}/pulls": _paged([_pull(title="nothing to see", body="no marker here")])}) item = _observe(routes, _file_planning_project()).get(INV_CRS) assert item.status == AVAILABLE - assert item.value[0]["target_refs"] == [] and item.value[0]["target_id"] is None + assert item.value[0]["target_refs"] == [] and item.value[0]["target_id"] is None and item.value[0]["linkage_unresolved"] == [] def test_a_marker_in_the_body_still_links_after_the_repair(): @@ -358,22 +390,35 @@ def test_a_marker_in_the_body_still_links_after_the_repair(): assert item.value[0]["target_id"] == "B1" -def test_a_milestone_without_a_number_cannot_name_the_target_it_links_to(): - """Under planning.source=milestones the milestone *is* the link; a malformed one is unreadable evidence.""" +def test_a_milestone_without_a_number_is_unresolved_linkage_not_a_failed_project(): + """Under planning.source=milestones the milestone *is* the link; a malformed one is unreadable evidence, kept as such.""" + project = single_project("acme/widget") routes = _routes(**{f"{BASE}/pulls": _paged([_pull(milestone={"state": "open"})])}) - try: - _observe(routes, single_project("acme/widget")) - except LinkageEvidenceError as exc: - assert "#7" in str(exc) - else: - raise AssertionError("a milestone with no number must not be read as no milestone") - - -def test_unreadable_linkage_evidence_is_a_collection_error_so_one_project_fails_alone(): - """It reaches run_project's declared boundary, not run_all's catch-all for the unanticipated.""" - from devostasis.adapters.github import CollectionError - - assert issubclass(LinkageEvidenceError, CollectionError) + obs = _observe(routes, project) + record = obs.get(INV_CRS).value[0] + assert record["target_refs"] == [] and record["linkage_unresolved"] == ["milestone:NO_NUMBER"] + derive(obs, project) + assert obs.value_of("planning.linkage.active_change_requests_unresolved_count_28d") == 1 + + +def test_a_reference_the_complete_register_lacks_is_missing_and_one_the_register_could_not_answer_is_unknown(): + """DIR-INCOMPLETE-07 against DIR-INCOMPLETE-09: positive absence is a broken reference, an unreadable register is no evidence.""" + project = _file_planning_project() + present = _observe(_file_planning_routes([_pull(body="Target: T-9")]), project) + assert present.get(INV_CRS).value[0]["target_refs"] == [{"target_id": "T-9", "state": "MISSING"}] + absent = _observe(_routes(**{f"{BASE}/pulls": _paged([_pull(body="Target: T-9")])}), project) + assert absent.get(INV_TARGETS).reason_code == "REGISTER_NOT_FOUND" + assert absent.get(INV_CRS).value[0]["target_refs"] == [{"target_id": "T-9", "state": "UNKNOWN"}] + + +def test_a_closed_register_target_is_still_a_resolved_reference(): + """DIR-CLOSED-03 at the adapter: the register resolves the id, and its lifecycle is carried, not used to unlink.""" + project = _file_planning_project() + obs = _observe(_file_planning_routes([_pull(body="Target: B1")], register=(("B1", "closed"),)), project) + assert obs.get(INV_CRS).value[0]["target_refs"] == [{"target_id": "B1", "state": "CLOSED"}] + derive(obs, project) + assert obs.value_of("planning.linkage.active_change_requests_linked_count_28d") == 1 + assert obs.value_of("planning.linkage.active_change_requests_linked_to_open_target_count_28d") == 0 # --------------------------------------------------------------------------- register shapes (#12 finding 5) diff --git a/tests/test_linkage_markers.py b/tests/test_linkage_markers.py new file mode 100644 index 0000000..e7883f9 --- /dev/null +++ b/tests/test_linkage_markers.py @@ -0,0 +1,93 @@ +"""Target markers and contradictory planning evidence (0.2.0). + +``PV-AUDIT-TARGET-MARKER-SYNTAX-001`` found the configured marker matched as a +substring, so ``NotTarget: B1`` and ``PreTarget:B1`` linked a change request to +B1. The audit's regressions ``LINK-MARKER-01..08`` are known here only by the +categories its handoff names (exact marker, embedded-prefix negatives, custom +markers, multiple markers, prose without the marker, end-to-end file +planning); these tests cover each category and do not claim the individual +case texts, which never reached this repository. +""" + +from __future__ import annotations + +import json + +import pytest + +from devostasis.adapters.github import GitHubAdapter, GitHubClient, marker_target_ids +from devostasis.cli import main +from devostasis.config import single_project +from devostasis.normalize import INV_CRS, derive +from devostasis.observations import ObservationSet +from devostasis.vitals import evaluate_all +from helpers import full_inputs, obs_set +from test_github_adapter import BASE, NOW, FakeTransport, _paged, _pull, _register_route, _routes + + +def test_an_exact_marker_links_wherever_it_stands_alone(): + for text in ("Target: B1", "Target:B1", "intro\nTarget: B1", "(Target: B1)", "- Target: B1", "done.\tTarget: B1", "Target: B1."): + assert marker_target_ids(text, "Target:") == ["B1"], text + + +@pytest.mark.parametrize("text", ["NotTarget: B1", "SubTarget: B1", "PreTarget:B1", "xTarget: B1", "_Target: B1", "2Target: B1", "ЯTarget: B1"]) +def test_a_marker_embedded_in_a_larger_token_links_nothing(text): + assert marker_target_ids(text, "Target:") == [] + + +def test_a_marker_ending_in_a_word_character_needs_a_boundary_after_it_too(): + assert marker_target_ids("Target B1", "Target") == ["B1"] + assert marker_target_ids("Targeted B1", "Target") == [], "the id would otherwise be read out of the word" + assert marker_target_ids("Targets: B1", "Target") == [] + + +def test_a_custom_marker_replaces_the_default_and_is_matched_exactly(): + assert marker_target_ids("Goal: release/1.2", "Goal:") == ["release/1.2"] + assert marker_target_ids("Target: B1", "Goal:") == [] + assert marker_target_ids("SubGoal: B1\nGoal: B2", "Goal:") == ["B2"] + hash_marker = "#target" + assert marker_target_ids(f"{hash_marker} B7", hash_marker) == ["B7"] + assert marker_target_ids(f"x{hash_marker} B7", hash_marker) == [] + + +def test_several_markers_keep_their_order_and_link_each_target_once(): + assert marker_target_ids("Target: B2\nTarget: B1\nTarget: B2", "Target:") == ["B2", "B1"] + assert marker_target_ids("NotTarget: B9 and Target: B3", "Target:") == ["B3"] + + +@pytest.mark.parametrize("text", ["target: B1", "TARGET: B1", "targets B1", "Fixes 12", "the target is B1", ""]) +def test_prose_without_the_configured_marker_links_nothing(text): + assert marker_target_ids(text, "Target:") == [] + + +def test_embedded_markers_do_not_link_end_to_end(): + """File planning end to end: only the standalone marker becomes a reference, and Direction counts only it.""" + project = single_project("acme/widget", planning={"source": "file", "path": ".devostasis/targets.json", "link_marker": "Target:"}) + pulls = [_pull(number=7, body="SubTarget: B1\nTarget: B2"), _pull(number=8, body="NotTarget: B1")] + register = _register_route((("B1", "open"), ("B2", "closed"))) + routes = _routes(**{f"{BASE}/pulls": _paged(pulls), f"{BASE}/contents/.devostasis/targets.json": register}) + obs = GitHubAdapter(GitHubClient(FakeTransport(routes)), NOW).collect(project) + refs = {item["number"]: item["target_refs"] for item in obs.get(INV_CRS).value} + assert refs == {7: [{"target_id": "B2", "state": "CLOSED"}], 8: []} + derive(obs, project) + direction = {r.vital_id: r for r in evaluate_all(obs)}["direction"] + assert (direction.band, direction.derived["linked_active_change_count_28d"], direction.derived["unlinked_active_change_count_28d"]) == ("MIXED", 1, 1) + + +@pytest.mark.parametrize("command", ["evaluate", "build"]) +def test_observations_whose_counts_contradict_each_other_are_an_input_error(tmp_path, capsys, command): + """PV-DIRECTION-INCOMPLETE-001 section 7: L + U + R != N is invalid normalized input, refused before classification.""" + data = full_inputs(obs_set()).to_dict() + for item in data["observations"]: + if item["observation_id"] == "planning.linkage.active_change_requests_unresolved_count_28d": + item["value"] = 5 + path = tmp_path / "observations.json" + path.write_text(json.dumps(data), encoding="utf-8") + ObservationSet.load(path) + if command == "evaluate": + argv = ["evaluate", "--observations", str(path), "--out", str(tmp_path / "snapshot.json")] + else: + argv = ["build", "--observations", str(path), "--store", str(tmp_path / "store")] + assert main(argv) == 2 + err = capsys.readouterr().err + assert "input error" in err and "DIRECTION_LINKAGE_COUNTS_INCONSISTENT" in err diff --git a/tests/test_normalize.py b/tests/test_normalize.py index 3e9c854..984e247 100644 --- a/tests/test_normalize.py +++ b/tests/test_normalize.py @@ -46,8 +46,15 @@ def test_change_request_aggregates(): assert obs.value_of("forge.change_requests.stale_open_count_14d") == 1 assert obs.value_of("forge.change_requests.updated_count_28d") == 3 assert obs.value_of("planning.linkage.active_change_requests_count_28d") == 3 + # Change request 4 delivered target 9, which has closed since; it stays + # linked (PV-REV-DIRECTION-CLOSED-TARGET-001), and only the presentation + # count of links to open targets leaves it out. + assert obs.value_of("planning.linkage.active_change_requests_linked_count_28d") == 3 + assert obs.value_of("planning.linkage.active_change_requests_unlinked_count_28d") == 0 + assert obs.value_of("planning.linkage.active_change_requests_unresolved_count_28d") == 0 assert obs.value_of("planning.linkage.active_change_requests_linked_to_open_target_count_28d") == 2 - assert obs.value_of("planning.linkage.links_per_target_28d") == {"5": 2} + assert obs.get("planning.linkage.active_change_requests_linked_to_open_target_count_28d").notes == normalize.LINKED_TO_OPEN_NOTE + assert obs.value_of("planning.linkage.links_per_target_28d") == {"5": 2, "9": 1} def test_flow_prec_01_sub_hour_medians_are_preserved(): diff --git a/tests/test_revision_history.py b/tests/test_revision_history.py new file mode 100644 index 0000000..8bfe3ed --- /dev/null +++ b/tests/test_revision_history.py @@ -0,0 +1,308 @@ +"""Durable Integrity revision history across bundles (PV-HIST-002, target B3). + +The reconciliation cases are vectors (tests/vectors/revision-history.json). +These are the cases about the chain: which bundle a build carries history +from, what happens when that bundle cannot be read, and what the carried +history does to the identity of the bundle that consumed it. +""" + +from __future__ import annotations + +import shutil + +import pytest + +from devostasis.bundle import BundleError, verify_dir +from devostasis.config import single_project +from devostasis.history import FilesystemHistoryStore +from devostasis.observations import Observation, ObservationSet +from devostasis.revision_history import CARRIED, LINEAGE, REPLAYABLE_LINEAGE +from devostasis.runner import build_from_observations +from helpers import add, finalize, full_inputs, obs_set, parent, revision + +LOCATOR = "acme/widget" +PROJECT_ID = "42" + + +def _run(run_id, attempts, latest=None): + """An Actions parent; ``attempts`` are the (number, state) pairs the provider shows.""" + latest = latest or max(n for n, _ in attempts) + record = parent(f"github_actions:workflow_run:{run_id}", attempts[-1][1], [s for _, s in attempts]) + record["attempts_observed"] = [{"attempt": n, "state": s} for n, s in sorted(attempts)] + record["current_attempt"] = latest + record["attempts_complete"] = len(attempts) == latest + return record + + +def _record(sha, day, parents): + states = [a["state"] for p in parents for a in p["attempts_observed"]] + complete = all(p["kind"] == "github_actions_workflow_run" and p["attempts_complete"] for p in parents) + if not parents: + history = "NO_DECISIVE_OBSERVED" + elif "VERIFY_FAIL" in states: + history = "FAILURE_OBSERVED" + elif not complete: + history = "UNKNOWN_HISTORY" + else: + history = "PASS_ONLY_OBSERVED" + current = [p["current_state"] for p in parents] + verdict = "VERIFY_FAIL" if "VERIFY_FAIL" in current else ("VERIFY_PASS" if current else None) + record = revision(sha, f"2026-09-{day:02d}T10:00:00Z", parents, verdict, history) + record["history_complete"] = complete + return record + + +def _passing(days): + return [_record(f"p{day}", day, [_run(800 + day, [(1, "VERIFY_PASS")])]) for day in days] + + +def _obs(observed_at, revisions, project_id=PROJECT_ID, series_status="AVAILABLE"): + obs = full_inputs(obs_set(observed_at)) + obs.subject = dict(obs.subject, owner="acme", repo="widget", display_locator=LOCATOR, immutable_project_id=project_id) + item = obs.get("ci.revision_verdicts_14d") + value = revisions if series_status in ("AVAILABLE", "PARTIAL") else None + obs.replace(Observation.from_dict(dict(item.to_dict(), value=value, status=series_status, reason_code=None if series_status == "AVAILABLE" else "PROVIDER_ERROR"))) + return obs + + +def _build(store, observed_at, revisions, **kwargs): + project = single_project(LOCATOR, config_version="1") + bundle = build_from_observations(project, _obs(observed_at, revisions, **kwargs), store) + path = store.commit(bundle) + return bundle, path + + +def _integrity(bundle): + import json + + snapshot = json.loads(bundle.members["snapshot.json"].decode("utf-8")) + return next(v for v in snapshot["vitals"] if v["vital_id"] == "integrity") + + +def _carried(bundle): + import json + + observations = json.loads(bundle.members["observations.json"].decode("utf-8")) + return next(o for o in observations["observations"] if o["observation_id"] == CARRIED) + + +def _states(bundle): + return {r["revision"]: r["history_state"] for r in _integrity(bundle)["derived"]["revision_history"]["records"]} + + +RUN_R = 41 + + +def _chain(store): + """B1 sees r pass, B2 sees a same-revision failure, B3 sees only the pass again.""" + b1, _ = _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_PASS")])])]) + b2, b2_path = _build(store, "2026-09-07T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_PASS"), (2, "VERIFY_FAIL")])])]) + return b1, b2, b2_path + + +def test_hist_20_the_immediate_predecessor_carries_a_failure_an_older_bundle_did_not_see(tmp_path): + store = FilesystemHistoryStore(tmp_path) + b1, b2, _ = _chain(store) + assert _states(b1)["r"] == "PASS_ONLY_OBSERVED" and _states(b2)["r"] == "FAILURE_OBSERVED" + # B3: the provider retained only the passing retry, attempt 3, and cannot show 1 and 2. + b3, _ = _build(store, "2026-09-08T12:00:00Z", _passing([1, 2, 3, 5]) + [_record("r", 4, [_run(RUN_R, [(3, "VERIFY_PASS")], latest=3)])]) + assert _carried(b3)["value"]["source_bundle_id"] == b2.bundle_id, "the source is B2, never B1" + assert _states(b3)["r"] == "FAILURE_OBSERVED" + integrity = _integrity(b3) + assert integrity["derived"]["failed_count_14d"] == 1 and integrity["band"] == "FLAKY" + assert integrity["derived"]["revision_history"]["source"] == { + "status": "CARRIED", "lineage": LINEAGE, "bundle_id": b2.bundle_id, "observed_at": "2026-09-07T12:00:00Z", "basis": "SNAPSHOT", + } + + +def test_hist_20_and_hist_08_a_lost_predecessor_is_a_gap_and_nothing_older_is_carried(tmp_path): + store = FilesystemHistoryStore(tmp_path) + b1, b2, b2_path = _chain(store) + for member in b2_path.iterdir(): + member.unlink() + shutil.rmtree(store.project_dir(b2.project_key) / "latest") + b3, _ = _build(store, "2026-09-08T12:00:00Z", _passing([1, 2, 3, 5]) + [_record("r", 4, [])]) + carried = _carried(b3) + assert (carried["status"], carried["reason_code"]) == ("UNKNOWN", "HISTORY_GAP") + assert carried["evidence_ref"] == {"previous_bundle_id": b2.bundle_id} + integrity = _integrity(b3) + assert integrity["derived"]["revision_history"]["source"]["status"] == "HISTORY_GAP" + assert "REVISION_HISTORY_GAP:HISTORY_GAP" in integrity["diagnostics"] + assert "r" not in _states(b3), "B1's PASS_ONLY for r is not reached for across the gap" + assert b3.manifest["comparison_status"] == "HISTORY_GAP" + + +def test_the_durable_history_crosses_a_bundle_whose_verification_evidence_was_unusable(tmp_path): + """B2 claims no Integrity band, and still hands B1's failure on to B3.""" + store = FilesystemHistoryStore(tmp_path) + _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_FAIL")])])]) + b2, _ = _build(store, "2026-09-07T12:00:00Z", [], series_status="ERROR") + assert _integrity(b2)["band"] is None and _states(b2)["r"] == "FAILURE_OBSERVED" + b3, _ = _build(store, "2026-09-08T12:00:00Z", _passing([1, 2, 3, 5]) + [_record("r", 4, [])]) + assert _states(b3)["r"] == "FAILURE_OBSERVED" and _integrity(b3)["derived"]["failed_count_14d"] == 1 + + +def test_hist_12_an_unrelated_vital_rule_change_keeps_the_durable_history(tmp_path): + """A predecessor that compares INCOMPARABLE for another reason still hands its history on.""" + store = FilesystemHistoryStore(tmp_path) + _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_FAIL")])])]) + project = single_project(LOCATOR, config_version="1", debt={"source": "labels", "labels": ["tech-debt"], "mapping_version": "2"}) + obs = _obs("2026-09-07T12:00:00Z", _passing([1, 2, 3, 5]) + [_record("r", 4, [])]) + bundle = build_from_observations(project, obs, store) + assert bundle.manifest["comparison_status"] == "INCOMPARABLE" + assert _states(bundle)["r"] == "FAILURE_OBSERVED" + + +def _older_bundle(project, obs): + """A bundle as 0.1.x wrote it: observations without carried history, an Integrity result without revision_history.""" + from devostasis import activity as activity_mod + from devostasis import delta as delta_mod + from devostasis.bundle import build_bundle + from devostasis.vitals import build_snapshot, evaluate_all + + snapshot = build_snapshot(obs, evaluate_all(obs)) + for vital in snapshot["vitals"]: + if vital["vital_id"] == "integrity": + vital["derived"].pop("revision_history", None) + delta = delta_mod.compare(snapshot, None, delta_mod.BASELINE, None, []) + activity = activity_mod.build_activity(obs, None, None, project.activity_list_cap) if project.activity_enabled else None + return build_bundle(project, obs, snapshot, delta, activity, None, delta_mod.BASELINE, None) + + +def test_hist_13_the_first_bundle_after_an_older_version_replays_its_revision_records(tmp_path): + """A 0.1.x predecessor has no carrier in its snapshot; its revision records are replayed, never copied.""" + store = FilesystemHistoryStore(tmp_path) + project = single_project(LOCATOR, config_version="1") + older = _older_bundle(project, _obs("2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_FAIL"), (2, "VERIFY_PASS")])])])) + store.commit(older) + b2, _ = _build(store, "2026-09-07T12:00:00Z", _passing([1, 2, 3, 5]) + [_record("r", 4, [])]) + carried = _carried(b2) + assert carried["value"]["lineage"] == REPLAYABLE_LINEAGE and carried["value"]["source_bundle_id"] == older.bundle_id + assert _integrity(b2)["derived"]["revision_history"]["source"]["basis"] == "REPLAYED_SERIES" + assert _states(b2)["r"] == "FAILURE_OBSERVED" + + +def test_hist_17_different_durable_history_is_different_canonical_evidence(tmp_path): + """The same current observation over two different histories never yields one bundle.""" + current = _passing([1, 2, 3, 5]) + [_record("r", 4, [])] + ids, states = [], [] + for name, prior in (("failed", [(1, "VERIFY_FAIL")]), ("passed", [(1, "VERIFY_PASS")])): + store = FilesystemHistoryStore(tmp_path / name) + _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, prior)])]) + bundle, _ = _build(store, "2026-09-07T12:00:00Z", current) + ids.append(bundle.bundle_id) + states.append(_states(bundle)["r"]) + assert states == ["FAILURE_OBSERVED", "PASS_ONLY_OBSERVED"] + assert ids[0] != ids[1], "hidden store state never changes an evaluation without a canonical trace" + + +def test_hist_18_another_project_at_the_same_locator_is_never_carried(tmp_path): + store = FilesystemHistoryStore(tmp_path) + _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_FAIL")])])]) + project = single_project(LOCATOR, config_version="1") + obs = _obs("2026-09-07T12:00:00Z", _passing([1, 2, 3, 5]) + [_record("r", 4, [])], project_id="7777") + bundle = build_from_observations(project, obs, store) + carried = next(o for o in __import__("json").loads(bundle.members["observations.json"])["observations"] if o["observation_id"] == CARRIED) + assert carried["status"] != "AVAILABLE", "a foreign project's history is never merged" + assert "r" not in _states(bundle) + + +def test_hist_19_complete_evidence_repairs_an_unknown_history(tmp_path): + """The nearby valid repair: once every attempt is observed, the prior unknown history is proven pass-only.""" + store = FilesystemHistoryStore(tmp_path) + b1, _ = _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(3, "VERIFY_PASS")], latest=3)])]) + assert _states(b1)["r"] == "UNKNOWN_HISTORY" and _integrity(b1)["band"] is None + b2, _ = _build(store, "2026-09-07T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_PASS"), (2, "VERIFY_PASS"), (3, "VERIFY_PASS")])])]) + assert _states(b2)["r"] == "PASS_ONLY_OBSERVED" and _integrity(b2)["band"] == "CLEAN" + + +def test_a_build_refuses_observations_that_carry_history_from_another_predecessor(tmp_path): + store = FilesystemHistoryStore(tmp_path) + b1, _ = _build(store, "2026-09-06T12:00:00Z", _passing([1, 2, 3])) + import json + + replayed = ObservationSet.from_dict(json.loads(b1.members["observations.json"])) + assert replayed.get(CARRIED).reason_code == "NO_PREVIOUS_BUNDLE" + replayed.observed_at = "2026-09-07T12:00:00Z" + with pytest.raises(BundleError, match="HISTORY_SOURCE_NOT_PREDECESSOR"): + build_from_observations(single_project(LOCATOR, config_version="1"), replayed, store) + + +def test_a_build_leaves_the_callers_observation_set_as_it_was(tmp_path): + store = FilesystemHistoryStore(tmp_path) + obs = _obs("2026-09-06T12:00:00Z", _passing([1, 2, 3])) + bundle = build_from_observations(single_project(LOCATOR, config_version="1"), obs, store) + assert CARRIED not in obs + assert _carried(bundle)["reason_code"] == "NO_PREVIOUS_BUNDLE" + + +def test_carried_history_that_is_not_its_lineages_shape_fails_closed(): + obs = finalize(obs_set("2026-09-10T12:00:00Z")) + add(obs, "ci.configured", True, "boolean") + add(obs, "ci.revision_verdicts_14d", _passing([1, 2, 3, 4]), "series") + add(obs, CARRIED, {"lineage": LINEAGE, "records": [{"revision": "r", "committed_at": "not a time", "parents": []}]}, "record") + from devostasis.vitals import integrity + + result = integrity.evaluate(obs) + assert (result.band, result.evaluation_status) == (None, "UNKNOWN") + assert any(code.startswith("REVISION_HISTORY_CARRY_MALFORMED") for code in result.diagnostics) + + +def test_every_stored_bundle_verifies_with_its_carried_history(tmp_path): + store = FilesystemHistoryStore(tmp_path) + b1, b2, b2_path = _chain(store) + assert verify_dir(b2_path) == [] + + +def test_verify_names_carried_history_that_is_not_from_the_previous_bundle(tmp_path): + """Section C: the selection of the source is auditable, so a bundle that names another source does not verify.""" + import json + + from devostasis.bundle import _check_history_source + + store = FilesystemHistoryStore(tmp_path) + _, b2, _ = _chain(store) + observations = json.loads(b2.members["observations.json"]) + assert _check_history_source(b2.members, b2.manifest, observations) == [] + problems = _check_history_source(b2.members, dict(b2.manifest, previous_bundle_id="c" * 64), observations) + assert len(problems) == 2 and all(problem.startswith("HISTORY_SOURCE_MISMATCH") for problem in problems) + + +def test_the_carrier_holds_only_what_the_build_can_consume(tmp_path): + """Records that have aged out of the window are not carried, and replayed 0.1.x records keep only what replay reads.""" + store = FilesystemHistoryStore(tmp_path) + project = single_project(LOCATOR, config_version="1") + older = _older_bundle(project, _obs("2026-09-06T12:00:00Z", _passing([1, 2, 3]) + [_record("r", 4, [_run(RUN_R, [(1, "VERIFY_FAIL")])]), _record("s", 5, [_run(RUN_R + 1, [(1, "VERIFY_PASS")])])])) + store.commit(older) + # Observed on 2026-09-19 the window starts at 09-05T12:00: r (09-04) has aged out, s (09-05T10:00) too, nothing else is carried. + b2, _ = _build(store, "2026-09-19T12:00:00Z", _passing([6, 7])) + assert _carried(b2)["value"]["records"] == [] + # On 2026-09-20 the window starts at 09-06T12:00, so p6 (09-06T10:00) is no longer carried either. + b3, _ = _build(store, "2026-09-20T12:00:00Z", _passing([6, 7, 8])) + assert [r["revision"] for r in _carried(b3)["value"]["records"]] == ["p7"] + b4_store = FilesystemHistoryStore(tmp_path / "replay") + b4_store.commit(older) + b4, _ = _build(b4_store, "2026-09-10T12:00:00Z", _passing([6])) + replayed = _carried(b4)["value"]["records"] + assert {r["revision"] for r in replayed} == {"p1", "p2", "p3", "r", "s"} + assert all(set(r) == {"revision", "committed_at", "parents"} for r in replayed) + assert all(set(p) == {"parent_id", "kind", "current_attempt", "attempts_observed"} for r in replayed for p in r["parents"]) + + +def test_the_carrier_groups_parents_by_shape_without_losing_one(): + """Hundreds of single-attempt runs on one revision are stored once per shape, and every parent comes back.""" + from devostasis.revision_history import HistoryShapeError, parent_groups, parents_from_groups + + parents = [ + {"parent_id": f"github_actions:workflow_run:{n}", "kind": "github_actions_workflow_run", "latest_attempt": 1, "attempts": [{"attempt": 1, "state": "VERIFY_PASS"}]} + for n in range(300) + ] + [ + {"parent_id": "github_actions:workflow_run:900", "kind": "github_actions_workflow_run", "latest_attempt": 2, "attempts": [{"attempt": 1, "state": "VERIFY_FAIL"}, {"attempt": 2, "state": "VERIFY_PASS"}]}, + {"parent_id": "github_checks:check_suite:7", "kind": "github_check_suite", "latest_attempt": None, "attempts": [{"attempt": None, "state": "VERIFY_PASS"}]}, + ] + groups = parent_groups(parents) + assert len(groups) == 3 and sum(len(group["parent_ids"]) for group in groups) == 302 + restored = parents_from_groups(groups, "test") + assert sorted(restored, key=lambda p: p["parent_id"]) == sorted(parents, key=lambda p: p["parent_id"]) + with pytest.raises(HistoryShapeError): + parents_from_groups([groups[0], {**groups[0]}], "test") diff --git a/tests/test_vector_runner.py b/tests/test_vector_runner.py index dc2cd96..cd3341c 100644 --- a/tests/test_vector_runner.py +++ b/tests/test_vector_runner.py @@ -162,7 +162,11 @@ def test_the_published_vector_schema_and_the_runner_agree_on_the_shape(): vital_expect = schema["$defs"]["vital_expect"] assert vital["expect"] == {"$ref": "#/$defs/vital_expect"} assert set(vital_expect["properties"]) == vectors.VITAL_EXPECT_KEYS - assert set(vital_expect["required"]) == vectors.REQUIRED_VITAL_EXPECT_KEYS + result_shape, refusal_shape = vital_expect["oneOf"] + assert set(result_shape["required"]) == vectors.REQUIRED_VITAL_EXPECT_KEYS and result_shape["not"] == {"required": ["rejected"]} + assert refusal_shape == {"required": ["rejected"], "maxProperties": 1}, "a refusal is the whole expectation" + assert set(vital["given"]["properties"]["derive"]["properties"]) == vectors.DERIVE_KEYS + assert set(vital["given"]["properties"]["derive"]["properties"]["planning_source"]["enum"]) == set(vectors.PLANNING_SOURCES) assert set(ci["given"]["properties"]) == vectors.CI_GIVEN_KEYS assert set(ci["given"]["properties"]["variants"]["items"]["properties"]) == vectors.CI_VARIANT_KEYS assert set(ci["given"]["properties"]["provider"]["enum"]) == set(vectors.CI_PROVIDERS) @@ -303,3 +307,48 @@ def test_an_exactly_empty_metric_delta_is_still_a_statement(): } vector = parse(kind="delta", given=comparison, expect={"vitals": {"clutter": {"transition_class": "UNCHANGED", "metric_deltas": []}}}) assert vector.kind == "delta" + + +# --------------------------------------------------------------------------- derive and rejected (0.2.0) + +HORIZON_COUNTS = [ + {"observation_id": "planning.explicit_targets.capability", "value_type": "enum", "value": "SUPPORTED"}, + {"observation_id": "planning.explicit_targets.open_count", "value": 1}, + {"observation_id": "planning.explicit_targets.open_with_future_boundary_count", "value": 1}, + {"observation_id": "planning.explicit_targets.open_beyond_28d_count", "value": 2}, +] + + +def test_derive_runs_the_derivation_before_the_vital_is_evaluated(): + """A case about an inventory is executed over the derivation, not over counts somebody pre-derived.""" + inventory = [{ + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [{"target_id": "1", "state": "OPEN", "due_at": None}], + "coverage": {"complete": True, "source": "milestones"}, + }] + given = {"vital": "horizon", "observations": inventory, "derive": {"planning_source": "milestones", "debt_mapping": None}} + assert vectors.run(parse(given=given, expect={"band": "DECLARED", "evaluation_status": "AVAILABLE", "derived": {"open_count": 1}})).ok + without = vectors.run(parse(given={"vital": "horizon", "observations": inventory}, expect={"band": "DECLARED", "evaluation_status": "AVAILABLE"})) + assert not without.ok, "without derive the inventory alone is not Horizon's input" + + +def test_derive_names_only_the_settings_the_derivation_reads(): + for derive in ({"planning_source": "jira"}, {"debt_mapping": None}, {"planning_source": "none", "extra": 1}, {"planning_source": "none", "debt_mapping": ["debt"]}): + with pytest.raises(vectors.VectorError): + parse(given={"vital": "horizon", "observations": HORIZON_COUNTS, "derive": derive}) + + +def test_a_rejected_case_passes_only_when_the_evidence_is_refused_with_that_code(): + given = {"vital": "horizon", "observations": HORIZON_COUNTS} + assert vectors.run(parse(given=given, expect={"rejected": "HORIZON_COUNTS_INCONSISTENT"})).ok + wrong_code = vectors.run(parse(given=given, expect={"rejected": "DIRECTION_LINKAGE_COUNTS_INCONSISTENT"})) + assert not wrong_code.ok and "HORIZON_COUNTS_INCONSISTENT" in wrong_code.failures[0] + admissible = vectors.run(parse(expect={"rejected": "HORIZON_COUNTS_INCONSISTENT"})) + assert not admissible.ok and "it evaluated to LIGHT / AVAILABLE" in admissible.failures[0] + + +def test_rejected_is_the_whole_expectation_and_names_a_code(): + for expect in ({"rejected": "X", "band": None}, {"rejected": ""}, {"rejected": 3}): + with pytest.raises(vectors.VectorError): + parse(given={"vital": "horizon", "observations": HORIZON_COUNTS}, expect=expect) diff --git a/tests/vectors/debt-partial.json b/tests/vectors/debt-partial.json new file mode 100644 index 0000000..8e34962 --- /dev/null +++ b/tests/vectors/debt-partial.json @@ -0,0 +1,1269 @@ +{ + "schema": "devostasis.vectors.v1", + "notes": "Cases DEBT-PARTIAL-01..16 of PV-DEBT-PARTIAL-001 (accepted by PV-REV-DEBT-PARTIAL-001), transcribed from the contract's required fixtures and adopted in 0.2.0 as debt.bands.v2 together with the observed-subset proof issue #39 asked for: a PARTIAL open count is a lower bound only when its coverage says OBSERVED_SUBSET_COUNT. Cases about the enumeration itself state the issue inventory under a label mapping and derive the counts; the others state the counts.", + "vectors": [ + { + "case": "DEBT-PARTIAL-01", + "title": "a partial enumeration with no confirmed open item cannot manufacture CLEAR", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 0, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 0, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 0 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:0" + ], + "diagnostics_absent": [ + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-02", + "title": "a returned subset of closed debt items only is still not CLEAR: the closed count is a diagnostic, omitted open items stay possible", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "none", + "debt_mapping": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + "observations": [ + { + "observation_id": "forge.issues.inventory", + "value_type": "series", + "value": [ + { + "number": 4, + "title": "issue 4", + "state": "CLOSED", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": "2026-09-02T00:00:00Z", + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 5, + "title": "issue 5", + "state": "CLOSED", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": "2026-09-03T00:00:00Z", + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 6, + "title": "issue 6", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "bug" + ], + "target_id": null, + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": false, + "window_complete": true + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 0, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 0 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:0" + ], + "diagnostics_absent": [ + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + }, + "notes": "Every count over an incomplete issue inventory is PARTIAL, the closed one included, so it stays out of the result's metrics; it could not prove open emptiness even when complete (DEBT-PARTIAL-11)." + }, + { + "case": "DEBT-PARTIAL-03", + "title": "one confirmed open item forces PRESENT: DEGRADED / PRESENT / EXACT with the partial receipt retained", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 1, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 1, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 1, + "partial_enumeration": { + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "source": null, + "source_reason_code": null + } + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:1", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-04", + "title": "thirty-seven confirmed open items remain PRESENT, never ACCUMULATED or any quantitative severity", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 37, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + }, + { + "observation_id": "debt.items.open_stale_count_30d", + "value": 29 + }, + { + "observation_id": "debt.items.closed_count_28d", + "value": 3 + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 37, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 37, + "open_stale_count_30d": 29, + "closed_count_28d": 3 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:37", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-05", + "title": "the size of the omitted tail cannot erase a confirmed open item: two different partial receipts give the same band", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "variants": [ + { + "title": "a page cap", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 1, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + { + "title": "an exhausted request budget", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 1, + "status": "PARTIAL", + "reason_code": "REQUEST_BUDGET_EXHAUSTED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 1, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 1 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:1", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-06", + "title": "provider naming, item order, page order and page boundaries do not change the normalized result", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "none", + "debt_mapping": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + "variants": [ + { + "title": "provider order", + "observations": [ + { + "observation_id": "forge.issues.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "issue 1", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "issue 2", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt", + "ops" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "issue 3", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "bug" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "issue 4", + "state": "CLOSED", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": "2026-09-02T00:00:00Z", + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": false, + "window_complete": true + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "reversed", + "observations": [ + { + "observation_id": "forge.issues.inventory", + "value_type": "series", + "value": [ + { + "number": 4, + "title": "issue 4", + "state": "CLOSED", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": "2026-09-02T00:00:00Z", + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "issue 3", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "bug" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "issue 2", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt", + "ops" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 1, + "title": "issue 1", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": false, + "window_complete": true + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "interleaved, another cap", + "observations": [ + { + "observation_id": "forge.issues.inventory", + "value_type": "series", + "value": [ + { + "number": 3, + "title": "issue 3", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "bug" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 1, + "title": "issue 1", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "issue 4", + "state": "CLOSED", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": "2026-09-02T00:00:00Z", + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "issue 2", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt", + "ops" + ], + "target_id": null, + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": false, + "window_complete": true + }, + "status": "PARTIAL", + "reason_code": "REQUEST_BUDGET_EXHAUSTED" + } + ] + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 2, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 2 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:2", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-07", + "title": "positively absent debt authority stays exactly UNINSTRUMENTED; the repair does not turn positive absence into UNKNOWN", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "UNCONFIGURED" + } + ] + }, + "expect": { + "band": "UNINSTRUMENTED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2" + } + }, + { + "case": "DEBT-PARTIAL-08", + "title": "uncertain mapping authority is never inferred from a partial payload that happens to hold an apparent open debt record", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "variants": [ + { + "title": "capability unknown", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "status": "UNKNOWN", + "reason_code": "NOT_OBSERVED" + }, + { + "observation_id": "debt.items.open_count", + "value": 3, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + { + "title": "mapping unknown", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "status": "UNKNOWN", + "reason_code": "NOT_OBSERVED" + }, + { + "observation_id": "debt.items.open_count", + "value": 3, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "debt.bands.v2", + "diagnostics": [ + "MISSING_REQUIRED:debt." + ], + "diagnostics_absent": [ + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-09", + "title": "a complete, fresh positive zero remains exactly CLEAR", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 0 + } + ] + }, + "expect": { + "band": "CLEAR", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 0 + } + } + }, + { + "case": "DEBT-PARTIAL-10", + "title": "a complete, fresh positive open count remains exactly PRESENT", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 4 + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 4 + } + } + }, + { + "case": "DEBT-PARTIAL-11", + "title": "a partial zero stays UNKNOWN beside one hundred closed items: closed history cannot prove current open emptiness", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 0, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + }, + { + "observation_id": "debt.items.closed_count_28d", + "value": 100 + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 0, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 0, + "closed_count_28d": 100 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:0" + ], + "diagnostics_absent": [ + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-12", + "title": "a forced PRESENT under partial acquisition stays visibly DEGRADED, never AVAILABLE, with the acquisition machine-visible", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "forge.issues.inventory", + "value_type": "series", + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "value": [ + { + "number": 9, + "title": "issue 9", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": false, + "window_complete": true + } + }, + { + "observation_id": "debt.items.open_count", + "value": 1, + "status": "PARTIAL", + "reason_code": "SOURCE_COVERAGE_INCOMPLETE", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + }, + "evidence_ref": { + "derived_from": [ + "forge.issues.inventory", + "debt.mapping" + ] + } + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 1, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 1, + "partial_enumeration": { + "status": "PARTIAL", + "reason_code": "SOURCE_COVERAGE_INCOMPLETE", + "source": "forge.issues.inventory", + "source_reason_code": "PAGINATION_CAPPED" + } + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:1", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-13", + "title": "a stale positive count is not a confirmed current open item: UNKNOWN, as before the repair", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "variants": [ + { + "title": "partial and stale", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 5, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + }, + "freshness": "STALE" + } + ] + }, + { + "title": "complete and stale", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + { + "observation_id": "debt.items.open_count", + "value": 5, + "freshness": "STALE" + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "debt.bands.v2", + "diagnostics": [ + "MISSING_REQUIRED:debt.items.open_count" + ], + "diagnostics_absent": [ + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-14", + "title": "one confirmed current open item survives other returned or tail records that stay unresolved under the partial receipt", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "none", + "debt_mapping": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1" + } + }, + "observations": [ + { + "observation_id": "forge.issues.inventory", + "value_type": "series", + "status": "PARTIAL", + "reason_code": "REQUEST_BUDGET_EXHAUSTED", + "value": [ + { + "number": 11, + "title": "issue 11", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 12, + "title": "issue 12", + "state": "OPEN", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": null, + "labels": [], + "target_id": null, + "linkage_unresolved": [] + }, + { + "number": 13, + "title": "issue 13", + "state": "CLOSED", + "created_at": "2026-06-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "closed_at": "2026-09-01T00:00:00Z", + "labels": [ + "debt" + ], + "target_id": null, + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": false, + "window_complete": false + } + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 1, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 1 + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:1", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-15", + "title": "a versioned project-local quantitative policy does not replace the canonical core under partial positive evidence", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1", + "local_policy": { + "policy_version": "acme-debt-1", + "accumulated_open_count": 5 + } + } + }, + { + "observation_id": "debt.items.open_count", + "value": 12, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + "expect": { + "band": "PRESENT", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 12, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 12, + "mapping": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1", + "local_policy": { + "policy_version": "acme-debt-1", + "accumulated_open_count": 5 + } + } + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:12", + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + }, + { + "case": "DEBT-PARTIAL-16", + "title": "a project-local policy cannot turn a partial zero into any canonical conclusion", + "kind": "vital", + "source_unit": "PV-DEBT-PARTIAL-001", + "given": { + "vital": "debt", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "debt.registry.capability", + "value_type": "enum", + "value": "CONFIGURED" + }, + { + "observation_id": "debt.mapping", + "value_type": "record", + "value": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1", + "local_policy": { + "policy_version": "acme-debt-1", + "accumulated_open_count": 5 + } + } + }, + { + "observation_id": "debt.items.open_count", + "value": 0, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "debt.bands.v2", + "derived": { + "open_count": 0, + "open_count_semantics": "LOWER_BOUND", + "confirmed_open_count": 0, + "mapping": { + "source": "labels", + "labels": [ + "debt" + ], + "mapping_version": "1", + "local_policy": { + "policy_version": "acme-debt-1", + "accumulated_open_count": 5 + } + } + }, + "diagnostics": [ + "DEBT_PARTIAL_ENUMERATION", + "DEBT_CONFIRMED_OPEN_LOWER_BOUND:0" + ], + "diagnostics_absent": [ + "DEBT_BAND_FORCED_PRESENT_BY_CONFIRMED_OPEN" + ] + } + } + ] +} diff --git a/tests/vectors/direction.json b/tests/vectors/direction.json new file mode 100644 index 0000000..ece86b3 --- /dev/null +++ b/tests/vectors/direction.json @@ -0,0 +1,3188 @@ +{ + "schema": "devostasis.vectors.v1", + "notes": "Cases DIR-CLOSED-01..09 of PV-REV-DIRECTION-CLOSED-TARGET-001 and DIR-INCOMPLETE-01..16 of PV-DIRECTION-INCOMPLETE-001 (accepted by PV-REV-DIRECTION-INCOMPLETE-001), transcribed from the contracts' required fixtures and adopted in 0.2.0 as one Direction rule version, direction.bands.v2. Cases about per-change evidence state the change-request and target inventories and run the derivation (given.derive), so the state-neutral linkage and the LINKED/UNLINKED/UNRESOLVED reduction are executed where they happen; cases about the completion rule itself state the N/L/U/R counts. A reference's state is what its resolution established: OPEN or CLOSED when the declared target resolves, MISSING when a complete register positively lacks the id, anything else unresolved.", + "vectors": [ + { + "case": "DIR-CLOSED-01", + "title": "an active change request with an explicit link to an open target is linked", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "FULLY_LINKED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 1, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 0 + } + } + }, + { + "case": "DIR-CLOSED-02", + "title": "the same change request and target id stay linked when the target moves from open to closed; the linkage share is unchanged", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "target 1 open", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "target 1 closed", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "1", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": "MIXED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 2, + "linked_active_change_count_28d": 1, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 0 + } + } + }, + { + "case": "DIR-CLOSED-03", + "title": "a link to a closed target is linked when the target identity resolves authoritatively", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "milestone embedded in the change request", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "4", + "title": "target 4", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "4", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "4", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "register-backed target", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-4", + "title": "target T-4", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-4", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-4", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": "FULLY_LINKED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 1, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 0 + } + } + }, + { + "case": "DIR-CLOSED-04", + "title": "a reference to a target the complete register positively lacks is unlinked with the missing-reference diagnostic; linkage is never fabricated", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-9", + "target_state": "MISSING", + "target_refs": [ + { + "target_id": "T-9", + "state": "MISSING" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "SCATTERED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 0, + "missing_target_reference_count_28d": 1 + }, + "diagnostics": [ + "DIRECTION_TARGET_REFERENCE_MISSING" + ] + }, + "notes": "The judgement's 'unlinked or unresolved' was narrowed by PV-REV-DIRECTION-INCOMPLETE-001: a positively missing id is UNLINKED (this case, DIR-INCOMPLETE-07); an id nobody could resolve is UNRESOLVED (DIR-INCOMPLETE-08, -09). Neither is ever linked." + }, + { + "case": "DIR-CLOSED-05", + "title": "closing a target cannot by itself worsen the Direction band", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "before: target 1 open", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "after: target 1 closed", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "1", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "1", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": "MIXED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 3, + "linked_active_change_count_28d": 2, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 0 + } + }, + "notes": "Under direction.bands.v1.1 the closed variant counted 0 of 3 linked and was SCATTERED: calibration finding 9." + }, + { + "case": "DIR-CLOSED-06", + "title": "reopening a target cannot by itself improve the Direction band", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "before: target 2 closed", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "2", + "title": "target 2", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "after: target 2 reopened", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "2", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "2", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": "SCATTERED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 3, + "linked_active_change_count_28d": 1, + "unlinked_active_change_count_28d": 2, + "unresolved_active_change_count_28d": 0 + } + } + }, + { + "case": "DIR-CLOSED-07", + "title": "Horizon's open-target counts still exclude the closed target a change request stays linked to, so the two Vitals may diverge", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "1", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "UNDECLARED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 0, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0 + } + }, + "notes": "The same evidence is FULLY_LINKED for Direction (DIR-CLOSED-03): Direction traces work to declared targets, Horizon counts what is still open." + }, + { + "case": "DIR-CLOSED-08", + "title": "every active change request linked to one closed target is FULLY_LINKED with the single-target diagnostic: traceability, not alignment", + "kind": "vital", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "5", + "title": "target 5", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "5", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "5", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "5", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "5", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "5", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "5", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "FULLY_LINKED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 3, + "linked_active_change_count_28d": 3, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 0, + "links_per_target": { + "5": 3 + } + }, + "diagnostics": [ + "ALL_LINKS_TO_SINGLE_TARGET" + ] + } + }, + { + "case": "DIR-CLOSED-09", + "title": "adopting the repair changes the Direction rule id: the first comparison across it is INCOMPARABLE for Direction alone, while an unaffected Vital still compares", + "kind": "delta", + "source_unit": "PV-REV-DIRECTION-CLOSED-TARGET-001", + "given": { + "previous": { + "vitals": [ + { + "vital_id": "direction", + "band": "SCATTERED", + "rule_id": "direction.bands.v1.1" + }, + { + "vital_id": "pulse", + "band": "STEADY", + "rule_id": "pulse.bands.v1" + } + ] + }, + "current": { + "vitals": [ + { + "vital_id": "direction", + "band": "MIXED", + "rule_id": "direction.bands.v2" + }, + { + "vital_id": "pulse", + "band": "STEADY", + "rule_id": "pulse.bands.v1" + } + ] + } + }, + "expect": { + "comparison_status": "COMPARABLE", + "vitals": { + "direction": { + "transition_class": "INCOMPARABLE", + "reason_codes": [ + "RULE_VERSION_BOUNDARY:direction.bands.v1.1->direction.bands.v2" + ], + "metric_deltas": {} + }, + "pulse": { + "transition_class": "UNCHANGED" + } + } + } + }, + { + "case": "DIR-INCOMPLETE-01", + "title": "N=5, L=0, U=3, R=2: every completion k in {0,1,2} is SCATTERED", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 5 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 0 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 3 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 2 + } + ] + }, + "expect": { + "band": "SCATTERED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "SCATTERED" + ], + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 5, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 3, + "unresolved_active_change_count_28d": 2, + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ] + } + }, + { + "case": "DIR-INCOMPLETE-02", + "title": "N=5, L=3, U=1, R=1: every completion k in {3,4} is MIXED", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 5 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 3 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 1 + } + ] + }, + "expect": { + "band": "MIXED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "MIXED" + ], + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 5, + "linked_active_change_count_28d": 3, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 1, + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ] + } + }, + { + "case": "DIR-INCOMPLETE-03", + "title": "N=4, L=2, U=0, R=2: completions reach MIXED and FULLY_LINKED, so no band", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 4 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 2 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 0 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 2 + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 4, + "linked_active_change_count_28d": 2, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 2, + "reachable_bands": [ + "MIXED", + "FULLY_LINKED" + ] + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ] + } + }, + { + "case": "DIR-INCOMPLETE-04", + "title": "N=5, L=1, U=1, R=3: completions reach SCATTERED and MIXED, so no band", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 5 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 3 + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 5, + "linked_active_change_count_28d": 1, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 3, + "reachable_bands": [ + "SCATTERED", + "MIXED" + ] + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ] + } + }, + { + "case": "DIR-INCOMPLETE-05", + "title": "N=1, L=0, U=0, R=1: completions reach SCATTERED and FULLY_LINKED; never DEGRADED FULLY_LINKED", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 0 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 0 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 1 + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 1, + "reachable_bands": [ + "SCATTERED", + "FULLY_LINKED" + ] + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ] + } + }, + { + "case": "DIR-INCOMPLETE-06", + "title": "a marker to an authoritatively resolved closed target is LINKED; closing or reopening the target alone does not change the linkage", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "variants": [ + { + "title": "target T-2 closed", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-2", + "title": "target T-2", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "target T-2 reopened", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-2", + "title": "target T-2", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-2", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-2", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": "FULLY_LINKED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 2, + "linked_active_change_count_28d": 2, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 0 + } + } + }, + { + "case": "DIR-INCOMPLETE-07", + "title": "a marker to T that a complete authoritative lookup proves absent is UNLINKED: N=1, L=0, U=1, R=0 is exactly SCATTERED with the missing-reference diagnostic", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "T-2", + "title": "target T-2", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T", + "target_state": "MISSING", + "target_refs": [ + { + "target_id": "T", + "state": "MISSING" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "SCATTERED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 0, + "missing_target_reference_count_28d": 1 + }, + "diagnostics": [ + "DIRECTION_TARGET_REFERENCE_MISSING" + ] + } + }, + { + "case": "DIR-INCOMPLETE-08", + "title": "a marker to T while the target list is PARTIAL and T is not in the observed subset, with no direct lookup, is UNRESOLVED: no band", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "2", + "title": "target 2", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 1, + "unresolved_change_requests": { + "1": [ + "TARGET_UNRESOLVED:T:UNKNOWN" + ] + }, + "reachable_bands": [ + "SCATTERED", + "FULLY_LINKED" + ] + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ] + }, + "notes": "The incomplete target list still proves the planning capability (a returned target exists), so Direction reaches the completion rule; absence from the partial list is never deletion proof." + }, + { + "case": "DIR-INCOMPLETE-09", + "title": "a marker whose target resolution is unavailable, forbidden, unknown, errored or stale is UNRESOLVED, never LINKED by fallback", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "variants": [ + { + "title": "resolution UNAVAILABLE", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "UNAVAILABLE", + "target_refs": [ + { + "target_id": "T-1", + "state": "UNAVAILABLE" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "resolution FORBIDDEN", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "FORBIDDEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "FORBIDDEN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "resolution UNKNOWN", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T-1", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "resolution ERROR", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "ERROR", + "target_refs": [ + { + "target_id": "T-1", + "state": "ERROR" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "resolution STALE", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "STALE", + "target_refs": [ + { + "target_id": "T-1", + "state": "STALE" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "link evidence unreadable", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [ + "body:dict" + ] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 1, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 1, + "reachable_bands": [ + "SCATTERED", + "FULLY_LINKED" + ] + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ] + } + }, + { + "case": "DIR-INCOMPLETE-10", + "title": "a PARTIAL global target enumeration does not degrade Direction when every referenced target is resolved on its own", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "7", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "7", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "3", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "3", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "FULLY_LINKED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 2, + "linked_active_change_count_28d": 2, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 0 + } + }, + "notes": "Targets 7 and 3 are resolved by the milestones embedded in the change requests, not by the capped target list; Horizon over the same evidence stays incomplete (HOR-PARTIAL-08)." + }, + { + "case": "DIR-INCOMPLETE-11", + "title": "partial link evidence leaves two change requests unresolved, but N/L/U/R forces SCATTERED for every completion", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "change 4", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T-1", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 5, + "title": "change 5", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [ + "title:int" + ] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "SCATTERED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "SCATTERED" + ], + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 5, + "linked_active_change_count_28d": 0, + "unlinked_active_change_count_28d": 3, + "unresolved_active_change_count_28d": 2, + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ] + } + }, + { + "case": "DIR-INCOMPLETE-12", + "title": "partial link evidence admits MIXED and FULLY_LINKED, so no band", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T-2", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 3, + "linked_active_change_count_28d": 2, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 1, + "reachable_bands": [ + "MIXED", + "FULLY_LINKED" + ] + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ] + } + }, + { + "case": "DIR-INCOMPLETE-13", + "title": "linkage capability that is neither positively present nor absent is UNKNOWN with no band", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "variants": [ + { + "title": "capability UNKNOWN", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "status": "UNKNOWN", + "reason_code": "TARGETS_INCOMPLETE" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 3 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 1 + } + ] + }, + { + "title": "capability PARTIAL", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "status": "PARTIAL", + "reason_code": "TARGETS_INCOMPLETE" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 3 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 1 + } + ] + }, + { + "title": "capability UNAVAILABLE", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "status": "UNAVAILABLE", + "reason_code": "TARGETS_INCOMPLETE" + }, + { + "observation_id": "planning.linkage.active_change_requests_count_28d", + "value": 3 + }, + { + "observation_id": "planning.linkage.active_change_requests_linked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unlinked_count_28d", + "value": 1 + }, + { + "observation_id": "planning.linkage.active_change_requests_unresolved_count_28d", + "value": 1 + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "diagnostics": [ + "MISSING_REQUIRED:planning.explicit_targets.capability" + ] + } + }, + { + "case": "DIR-INCOMPLETE-14", + "title": "an active-change inventory that is PARTIAL leaves N unproven: UNKNOWN with no band", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": false, + "window_start": "2026-08-10T12:00:00Z" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "direction.bands.v2", + "diagnostics": [ + "MISSING_REQUIRED:planning.linkage.active_change_requests_count_28d:PARTIAL" + ] + } + }, + { + "case": "DIR-INCOMPLETE-15", + "title": "reversed and reshuffled enumeration, page boundaries, marker order and target order leave L/U/R, the reachable bands, the status, the band and the diagnostics unchanged", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "variants": [ + { + "title": "provider order", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "T-2", + "title": "target T-2", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + }, + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "change 4", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 5, + "title": "change 5", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-3", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T-3", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "reversed, markers and targets reordered", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-2", + "title": "target T-2", + "state": "CLOSED", + "due_at": null + }, + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 5, + "title": "change 5", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-3", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T-3", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "change 4", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "OPEN" + }, + { + "target_id": "T-2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + { + "title": "interleaved", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "T-2", + "title": "target T-2", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-1", + "target_state": "OPEN", + "target_refs": [ + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 5, + "title": "change 5", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-3", + "target_state": "UNKNOWN", + "target_refs": [ + { + "target_id": "T-3", + "state": "UNKNOWN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-2", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-2", + "state": "CLOSED" + }, + { + "target_id": "T-1", + "state": "OPEN" + } + ], + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "change 4", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": null, + "target_state": null, + "target_refs": [], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + } + ] + }, + "expect": { + "band": "MIXED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "MIXED" + ], + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 5, + "linked_active_change_count_28d": 3, + "unlinked_active_change_count_28d": 1, + "unresolved_active_change_count_28d": 1, + "links_per_target": { + "T-1": 2, + "T-2": 2 + }, + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ] + } + }, + { + "case": "DIR-INCOMPLETE-16", + "title": "several active change requests all linked to one authoritatively resolved closed target, with complete evidence, are exactly FULLY_LINKED: neutral, no concentration threshold", + "kind": "vital", + "source_unit": "PV-DIRECTION-INCOMPLETE-001", + "given": { + "vital": "direction", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "file", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-9", + "title": "target T-9", + "state": "CLOSED", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "file" + } + }, + { + "observation_id": "forge.change_requests.inventory", + "value_type": "series", + "value": [ + { + "number": 1, + "title": "change 1", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-9", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-9", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 2, + "title": "change 2", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-9", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-9", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 3, + "title": "change 3", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-9", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-9", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + }, + { + "number": 4, + "title": "change 4", + "state": "OPEN", + "draft": false, + "created_at": "2026-08-20T10:00:00Z", + "updated_at": "2026-09-01T10:00:00Z", + "merged_at": null, + "closed_at": null, + "target_id": "T-9", + "target_state": "CLOSED", + "target_refs": [ + { + "target_id": "T-9", + "state": "CLOSED" + } + ], + "linkage_unresolved": [] + } + ], + "coverage": { + "open_complete": true, + "window_complete": true, + "window_start": "2026-08-10T12:00:00Z" + } + } + ] + }, + "expect": { + "band": "FULLY_LINKED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "direction.bands.v2", + "derived": { + "active_change_count_28d": 4, + "linked_active_change_count_28d": 4, + "unlinked_active_change_count_28d": 0, + "unresolved_active_change_count_28d": 0, + "links_per_target": { + "T-9": 4 + } + }, + "diagnostics": [ + "ALL_LINKS_TO_SINGLE_TARGET" + ], + "diagnostics_absent": [ + "DIRECTION_INCOMPLETE_BAND_INVARIANT", + "DIRECTION_INCOMPLETE_AMBIGUOUS" + ] + } + } + ] +} diff --git a/tests/vectors/horizon-partial.json b/tests/vectors/horizon-partial.json new file mode 100644 index 0000000..4ecc3fb --- /dev/null +++ b/tests/vectors/horizon-partial.json @@ -0,0 +1,1330 @@ +{ + "schema": "devostasis.vectors.v1", + "notes": "Cases HOR-PARTIAL-01..16 of PV-HORIZON-PARTIAL-001 (accepted by PV-REV-HORIZON-PARTIAL-001), transcribed from the contract's required fixtures and adopted in 0.2.0 as horizon.bands.v2. Target evidence is stated as the provider-neutral target inventory and derived (given.derive), so the counts, their observed-subset proof and the capability a returned target proves are executed where they are made. observed_at is 2026-09-07T12:00:00Z; the 28-day frame ends at 2026-10-05T12:00:00Z, which is not beyond it. HOR-PARTIAL-15 states the contradictory counts directly: the derivation cannot produce them, and the contract makes them a validation failure, which a vector states with expect.rejected.", + "vectors": [ + { + "case": "HOR-PARTIAL-01", + "title": "capability present, complete and fresh, zero open targets: exactly UNDECLARED (the positive-zero control)", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z" + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + } + ] + }, + "expect": { + "band": "UNDECLARED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 0, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0 + } + } + }, + { + "case": "HOR-PARTIAL-02", + "title": "complete and fresh, one open target with no future boundary: exactly DECLARED", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + } + ] + }, + "expect": { + "band": "DECLARED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 1, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0 + } + } + }, + { + "case": "HOR-PARTIAL-03", + "title": "complete and fresh, an open target exactly 28 days out and none later: exactly VISIBLE; the frame is unchanged", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:00Z" + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + } + ] + }, + "expect": { + "band": "VISIBLE", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 1, + "open_with_future_boundary_count": 1, + "open_beyond_28d_count": 0, + "nearest_future_boundary_days": 28 + } + } + }, + { + "case": "HOR-PARTIAL-04", + "title": "complete and fresh, an open target strictly beyond 28 days: exactly EXTENDED", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z" + } + ], + "coverage": { + "complete": true, + "source": "milestones" + } + } + ] + }, + "expect": { + "band": "EXTENDED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 1, + "open_with_future_boundary_count": 1, + "open_beyond_28d_count": 1 + } + } + }, + { + "case": "HOR-PARTIAL-05", + "title": "explicit targets positively unsupported and no planning register configured: exactly UNDECLARED, a descriptive state, not missing evidence", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "none", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "status": "UNAVAILABLE", + "reason_code": "PLANNING_SOURCE_NONE" + } + ] + }, + "expect": { + "band": "UNDECLARED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "UNSUPPORTED" + }, + "diagnostics": [ + "PLANNING_CAPABILITY:UNSUPPORTED" + ] + } + }, + { + "case": "HOR-PARTIAL-06", + "title": "capability unknown, forbidden, errored, stale, unavailable or an unresolved partial: UNKNOWN, never UNDECLARED by default", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "targets UNKNOWN", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "status": "UNKNOWN", + "reason_code": "CONNECTOR_AMBIGUOUS" + } + ] + }, + { + "title": "targets FORBIDDEN", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "status": "FORBIDDEN", + "reason_code": "FORBIDDEN" + } + ] + }, + { + "title": "targets ERROR", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "status": "ERROR", + "reason_code": "UNEXPECTED_PAYLOAD" + } + ] + }, + { + "title": "targets UNAVAILABLE", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "status": "UNAVAILABLE", + "reason_code": "REGISTER_NOT_FOUND" + } + ] + }, + { + "title": "targets STALE", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z" + } + ], + "coverage": { + "complete": true, + "source": "milestones" + }, + "freshness": "STALE" + } + ] + }, + { + "title": "partial with no returned target", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "diagnostics": [ + "MISSING_REQUIRED:planning.explicit_targets.capability" + ] + } + }, + { + "case": "HOR-PARTIAL-07", + "title": "PH0: a partial enumeration returned no open target, so every band is reachable: UNKNOWN, never UNDECLARED", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 0, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "UNDECLARED", + "DECLARED", + "VISIBLE", + "EXTENDED" + ] + }, + "diagnostics": [ + "HORIZON_PARTIAL_AMBIGUOUS", + "PARTIAL_ENUMERATION:planning.explicit_targets.open_count" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, + { + "case": "HOR-PARTIAL-08", + "title": "PH1: returned open targets without a future boundary reach DECLARED, VISIBLE and EXTENDED: UNKNOWN, DECLARED is never a representative", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 2, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "DECLARED", + "VISIBLE", + "EXTENDED" + ] + }, + "diagnostics": [ + "HORIZON_PARTIAL_AMBIGUOUS", + "PARTIAL_ENUMERATION:planning.explicit_targets.open_count" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, + { + "case": "HOR-PARTIAL-09", + "title": "PH2: a returned open target within or exactly at 28 days reaches VISIBLE and EXTENDED: UNKNOWN, VISIBLE is never a bound", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "within the frame", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "exactly at 28 days", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 1, + "open_with_future_boundary_count": 1, + "open_beyond_28d_count": 0, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "VISIBLE", + "EXTENDED" + ] + }, + "diagnostics": [ + "HORIZON_PARTIAL_AMBIGUOUS", + "PARTIAL_ENUMERATION:planning.explicit_targets.open_count" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, + { + "case": "HOR-PARTIAL-10", + "title": "PH3: a returned open target strictly beyond 28 days forces EXTENDED in every completion: DEGRADED / EXTENDED / EXACT", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": "EXTENDED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "EXTENDED" + ], + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 2, + "open_with_future_boundary_count": 1, + "open_beyond_28d_count": 1, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "EXTENDED" + ], + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_AMBIGUOUS" + ] + } + }, + { + "case": "HOR-PARTIAL-11", + "title": "a returned closed target beyond 28 days and an open target without a boundary: the closed one forces nothing, PH1, UNKNOWN", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 1, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "DECLARED", + "VISIBLE", + "EXTENDED" + ] + }, + "diagnostics": [ + "HORIZON_PARTIAL_AMBIGUOUS", + "PARTIAL_ENUMERATION:planning.explicit_targets.open_count" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, + { + "case": "HOR-PARTIAL-12", + "title": "only closed returned targets, one beyond 28 days: PH0, UNKNOWN, never UNDECLARED or EXTENDED", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 0, + "open_with_future_boundary_count": 0, + "open_beyond_28d_count": 0, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "UNDECLARED", + "DECLARED", + "VISIBLE", + "EXTENDED" + ] + }, + "diagnostics": [ + "HORIZON_PARTIAL_AMBIGUOUS", + "PARTIAL_ENUMERATION:planning.explicit_targets.open_count" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, + { + "case": "HOR-PARTIAL-13", + "title": "the same PH3 evidence in reversed order, other page boundaries and an equivalent provider-neutral record shape: identical DEGRADED / EXTENDED / EXACT", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "provider order", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + }, + { + "target_id": "3", + "title": "target 3", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "reversed, a different cap", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "3", + "title": "target 3", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + }, + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "REQUEST_BUDGET_EXHAUSTED" + } + ] + }, + { + "title": "register-shaped records with provider extras", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "T-3", + "title": "target T-3", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z", + "url": "https://example.invalid/t3" + }, + { + "target_id": "T-1", + "title": "target T-1", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z", + "open_items": 4, + "closed_items": 1 + }, + { + "target_id": "T-2", + "title": "target T-2", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z", + "url": "https://example.invalid/t2" + } + ], + "coverage": { + "complete": false, + "source": "file" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + } + ] + }, + "expect": { + "band": "EXTENDED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "EXTENDED" + ], + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 2, + "open_with_future_boundary_count": 2, + "open_beyond_28d_count": 1, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "EXTENDED" + ], + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_AMBIGUOUS" + ] + } + }, + { + "case": "HOR-PARTIAL-14", + "title": "the same PH2 subset under record and page permutations: identical UNKNOWN, pagination order cannot manufacture exactness", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "provider order", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "3", + "title": "target 3", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "reversed", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "3", + "title": "target 3", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "interleaved, another cap", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + }, + { + "target_id": "3", + "title": "target 3", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "REQUEST_BUDGET_EXHAUSTED" + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "horizon.bands.v2", + "derived": { + "capability": "SUPPORTED", + "open_count": 2, + "open_with_future_boundary_count": 1, + "open_beyond_28d_count": 0, + "open_count_semantics": "LOWER_BOUND", + "open_with_future_boundary_count_semantics": "LOWER_BOUND", + "open_beyond_28d_count_semantics": "LOWER_BOUND", + "reachable_bands": [ + "VISIBLE", + "EXTENDED" + ] + }, + "diagnostics": [ + "HORIZON_PARTIAL_AMBIGUOUS", + "PARTIAL_ENUMERATION:planning.explicit_targets.open_count" + ], + "diagnostics_absent": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + } + }, + { + "case": "HOR-PARTIAL-15", + "title": "a derived tuple no target population can produce (beyond above future, future above open) fails validation before evaluation, never UNKNOWN or DEGRADED compensation", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "variants": [ + { + "title": "partial, beyond above future", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.explicit_targets.open_count", + "value": 2, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + }, + { + "observation_id": "planning.explicit_targets.open_with_future_boundary_count", + "value": 1, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + }, + { + "observation_id": "planning.explicit_targets.open_beyond_28d_count", + "value": 2, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + { + "title": "partial, future above open", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.explicit_targets.open_count", + "value": 1, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + }, + { + "observation_id": "planning.explicit_targets.open_with_future_boundary_count", + "value": 2, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + }, + { + "observation_id": "planning.explicit_targets.open_beyond_28d_count", + "value": 0, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED", + "coverage": { + "complete": false, + "value_semantics": "OBSERVED_SUBSET_COUNT" + } + } + ] + }, + { + "title": "complete, beyond above future", + "observations": [ + { + "observation_id": "planning.explicit_targets.capability", + "value_type": "enum", + "value": "SUPPORTED" + }, + { + "observation_id": "planning.explicit_targets.open_count", + "value": 3 + }, + { + "observation_id": "planning.explicit_targets.open_with_future_boundary_count", + "value": 0 + }, + { + "observation_id": "planning.explicit_targets.open_beyond_28d_count", + "value": 1 + } + ] + } + ] + }, + "expect": { + "rejected": "HORIZON_COUNTS_INCONSISTENT" + } + }, + { + "case": "HOR-PARTIAL-16", + "title": "PH3 is the direct proof: whatever else the returned subset holds, and whatever the omitted records are, the observed open target beyond 28 days keeps every completion EXTENDED", + "kind": "vital", + "source_unit": "PV-HORIZON-PARTIAL-001", + "given": { + "vital": "horizon", + "observed_at": "2026-09-07T12:00:00Z", + "derive": { + "planning_source": "milestones", + "debt_mapping": null + }, + "variants": [ + { + "title": "beside closed targets", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "CLOSED", + "due_at": "2026-08-01T00:00:00Z" + }, + { + "target_id": "3", + "title": "target 3", + "state": "CLOSED", + "due_at": "2026-12-01T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "beside an open target without a boundary", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": null + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "beside an open target within the frame", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": "2026-09-20T00:00:00Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + }, + { + "title": "beside another open target beyond the frame", + "observations": [ + { + "observation_id": "planning.explicit_targets.inventory", + "value_type": "series", + "value": [ + { + "target_id": "1", + "title": "target 1", + "state": "OPEN", + "due_at": "2026-12-01T00:00:00Z" + }, + { + "target_id": "2", + "title": "target 2", + "state": "OPEN", + "due_at": "2026-10-05T12:00:01Z" + } + ], + "coverage": { + "complete": false, + "source": "milestones" + }, + "status": "PARTIAL", + "reason_code": "PAGINATION_CAPPED" + } + ] + } + ] + }, + "expect": { + "band": "EXTENDED", + "evaluation_status": "DEGRADED", + "band_semantics": "EXACT", + "possible_bands": [ + "EXTENDED" + ], + "rule_id": "horizon.bands.v2", + "derived": { + "reachable_bands": [ + "EXTENDED" + ], + "possible_bands_semantics": "NON_AUTHORITATIVE_CONSERVATIVE_SUPERSET" + }, + "diagnostics": [ + "HORIZON_PARTIAL_BAND_INVARIANT" + ] + }, + "notes": "The omitted records of a partial enumeration are not observable, so no vector can list them; each variant is a different returned subset, and the contract's argument is that none of them, nor anything added to them, removes the observed beyond-28-day open target." + } + ] +} diff --git a/tests/vectors/integrity-totality.json b/tests/vectors/integrity-totality.json index 7993302..26e632a 100644 --- a/tests/vectors/integrity-totality.json +++ b/tests/vectors/integrity-totality.json @@ -68,7 +68,7 @@ "failed_count_14d": 0, "unresolved_verdict_rule": "PV-INTEGRITY-TOTALITY-001" }, - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -191,7 +191,7 @@ "failed_count_14d": 0, "unresolved_verdict_rule": "PV-INTEGRITY-TOTALITY-001" }, - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -314,7 +314,7 @@ "failed_count_14d": 1, "unresolved_verdict_rule": "PV-INTEGRITY-TOTALITY-001" }, - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -491,7 +491,7 @@ "failed_count_14d": 0, "unresolved_verdict_rule": "PV-INTEGRITY-TOTALITY-001" }, - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -695,7 +695,7 @@ "failed_count_14d": 1, "unresolved_verdict_rule": "PV-INTEGRITY-TOTALITY-001" }, - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -876,7 +876,7 @@ "LATEST_REVISION_NON_DECISIVE", "CURRENT_VERIFICATION_UNRESOLVED" ], - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -941,7 +941,7 @@ "diagnostics": [ "CI_UNINSTRUMENTED_WITH_RECENT_NONDECISIVE_HISTORY" ], - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -1006,7 +1006,7 @@ "diagnostics": [ "CI_UNINSTRUMENTED_WITH_RECENT_NONDECISIVE_HISTORY" ], - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -1093,7 +1093,7 @@ "diagnostics_absent": [ "CI_UNINSTRUMENTED_WITH_RECENT_NONDECISIVE_HISTORY" ], - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -1268,7 +1268,7 @@ "diagnostics_absent": [ "CI_CURRENT_VERIFY_UNRESOLVED" ], - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -1444,7 +1444,7 @@ "diagnostics_absent": [ "CI_CURRENT_VERIFY_UNRESOLVED" ], - "rule_id": "integrity.bands.v1+ci-unit-004" + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002" } }, { @@ -1614,7 +1614,7 @@ "evaluation_status": "DEGRADED", "band_semantics": "EXACT", "possible_bands": null, - "rule_id": "integrity.bands.v1+ci-unit-004", + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", "derived": { "decisive_count_14d": 4, "failed_count_14d": 1 diff --git a/tests/vectors/integrity-unknown.json b/tests/vectors/integrity-unknown.json index 109536a..ec4fc41 100644 --- a/tests/vectors/integrity-unknown.json +++ b/tests/vectors/integrity-unknown.json @@ -30,7 +30,7 @@ "evaluation_status": "UNKNOWN", "band_semantics": null, "possible_bands": null, - "rule_id": "integrity.bands.v1+ci-unit-004", + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", "derived": {"decisive_count_14d": 4, "failed_count_14d": 0}, "diagnostics": ["CURRENT_VERDICT_UNKNOWN:r5"], "diagnostics_absent": ["LATEST_REVISION_NON_DECISIVE"] @@ -88,7 +88,7 @@ "integrity": { "band": null, "evaluation_status": "UNKNOWN", - "rule_id": "integrity.bands.v1+ci-unit-004", + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", "derived": {"decisive_count_14d": 4, "failed_count_14d": 0}, "diagnostics": ["CURRENT_VERDICT_UNKNOWN:r5"], "diagnostics_absent": ["LATEST_REVISION_NON_DECISIVE"] diff --git a/tests/vectors/revision-history.json b/tests/vectors/revision-history.json new file mode 100644 index 0000000..63805a7 --- /dev/null +++ b/tests/vectors/revision-history.json @@ -0,0 +1,3969 @@ +{ + "schema": "devostasis.vectors.v1", + "notes": "The reconciliation cases of PV-HIST-002 (accepted by PV-REV-HIST-002), transcribed from its permanent conformance cases and adopted in 0.2.0 under integrity.bands.v1+ci-unit-004+hist-002. Each states the carried history as the observation a build adds from its immediate predecessor (ci.revision_history_carried, lineage devostasis.ci-history.v2 unless the case is about another lineage) and the current revision records in the adapter's shape. The chain cases, HIST-08, -12, -17, -18 and -20, and the store replay of HIST-13 need a history store and are tests (tests/test_revision_history.py). HIST-14 needs an accepted migration, and none exists. observed_at is 2026-09-10T12:00:00Z, so the window starts at 2026-08-27T12:00:00Z.", + "vectors": [ + { + "case": "HIST-01", + "title": "a revision a prior bundle proved failing stays FAILURE_OBSERVED when the provider now shows the same revision passing; the current verdict is PASS", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_checks:check_suite:41", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "PARENT_LEVEL_ONLY", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_check_suite", + "latest_attempt": null, + "attempts": [ + { + "attempt": null, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_checks:check_suite:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "FLAKY", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 1, + "current_revision": { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "current_verdict": "VERIFY_PASS", + "history_state": "FAILURE_OBSERVED" + } + }, + "diagnostics": [ + "HISTORY_PROVENANCE_PARENT_LEVEL_ONLY:1" + ] + }, + "notes": "A re-requested check suite keeps its id and shows only its latest outcome; the failure the previous bundle observed is what keeps the revision failing (R54's rule across bundles)." + }, + { + "case": "HIST-02", + "title": "a revision the prior bundle recorded PASS_ONLY_OBSERVED becomes FAILURE_OBSERVED when a failure of it is newly observed, and stays there", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:42", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_FAIL", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "attempts_complete": true, + "history_state": "FAILURE_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_FAIL", + "history_state": "FAILURE_OBSERVED", + "historical_contribution": "VERIFY_FAIL", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p5", + "committed_at": "2026-09-05T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:905", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-01T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "FLAKY", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 1 + } + }, + "notes": "The run the previous bundle saw pass (41) is no longer listed; the new run 42 failed on the same revision." + }, + { + "case": "HIST-03", + "title": "a prior failure plus any number of same-revision retries that passed is one failed contribution, whatever the number or order of the retries", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "variants": [ + { + "title": "one retry", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "five retries", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 6, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 2, + "state": "VERIFY_PASS" + }, + { + "attempt": 3, + "state": "VERIFY_PASS" + }, + { + "attempt": 4, + "state": "VERIFY_PASS" + }, + { + "attempt": 5, + "state": "VERIFY_PASS" + }, + { + "attempt": 6, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "five retries listed newest first", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 6, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 6, + "state": "VERIFY_PASS" + }, + { + "attempt": 5, + "state": "VERIFY_PASS" + }, + { + "attempt": 4, + "state": "VERIFY_PASS" + }, + { + "attempt": 3, + "state": "VERIFY_PASS" + }, + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 6, + "attempts": [ + { + "attempt": 6, + "state": "VERIFY_PASS" + }, + { + "attempt": 5, + "state": "VERIFY_PASS" + }, + { + "attempt": 4, + "state": "VERIFY_PASS" + }, + { + "attempt": 3, + "state": "VERIFY_PASS" + }, + { + "attempt": 2, + "state": "VERIFY_PASS" + }, + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + } + ] + }, + "expect": { + "band": "SPARSE_MIXED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 1, + "failed_count_14d": 1, + "revisions_with_history": 1 + } + } + }, + { + "case": "HIST-04", + "title": "a failed revision and a newer passing revision are two samples while both are in the window", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [], + "current_verdict": null, + "history_state": "NO_DECISIVE_OBSERVED", + "historical_contribution": null, + "history_provenance": null, + "history_complete": true + }, + { + "revision": "r2", + "committed_at": "2026-09-07T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:43", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "SPARSE_MIXED", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 2, + "failed_count_14d": 1, + "revisions_with_history": 2, + "revisions_with_verification": 1 + } + } + }, + { + "case": "HIST-05", + "title": "a failure the provider no longer exposes stays historical evidence while its revision is in the window", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [], + "current_verdict": null, + "history_state": "NO_DECISIVE_OBSERVED", + "historical_contribution": null, + "history_provenance": null, + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "FLAKY", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 1, + "revisions_with_history": 5, + "revisions_with_verification": 4 + } + } + }, + { + "case": "HIST-06", + "title": "without durable history a parent-level pass cannot prove the earlier attempts: UNKNOWN_HISTORY, no favorable PASS_ONLY reconstruction", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "variants": [ + { + "title": "a first bundle", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_checks:check_suite:41", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "PARENT_LEVEL_ONLY", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "status": "UNAVAILABLE", + "reason_code": "NO_PREVIOUS_BUNDLE" + } + ] + }, + { + "title": "no carried history stated", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_checks:check_suite:41", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "PARENT_LEVEL_ONLY", + "history_complete": false + } + ] + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "diagnostics": [ + "REVISION_HISTORY_UNKNOWN:r", + "HISTORY_PROVENANCE_PARENT_LEVEL_ONLY:1" + ], + "derived": { + "unknown_history_count_14d": 1, + "decisive_count_14d": 4 + } + } + }, + { + "case": "HIST-07", + "title": "complete attempt-level current evidence with no unresolved prior gap proves PASS_ONLY_OBSERVED", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "variants": [ + { + "title": "a first bundle", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + }, + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "status": "UNAVAILABLE", + "reason_code": "NO_PREVIOUS_BUNDLE" + } + ] + }, + { + "title": "carried history without this revision", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + }, + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "parent_ids": [ + "github_actions:workflow_run:901" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + } + ] + }, + "expect": { + "band": "CLEAN", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 0, + "unknown_history_count_14d": 0 + } + } + }, + { + "case": "HIST-09", + "title": "the same failure in durable history and in the current enumeration is one failed contribution, never two", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_FAIL", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "attempts_complete": true, + "history_state": "FAILURE_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_FAIL", + "history_state": "FAILURE_OBSERVED", + "historical_contribution": "VERIFY_FAIL", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p5", + "committed_at": "2026-09-05T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:905", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-01T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "FLAKY", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 1 + } + } + }, + { + "case": "HIST-10", + "title": "a revision whose durable failure has aged beyond the 14-day window no longer contributes", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "old", + "committed_at": "2026-08-20T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:40" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "CLEAN", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 4, + "failed_count_14d": 0, + "revisions_with_history": 4 + } + }, + "notes": "The aged revision is also absent from the durable output of this bundle; the bundle that proved it keeps it." + }, + { + "case": "HIST-11", + "title": "equivalent failure histories on the Actions and the check-suite surfaces, in any enumeration order, give the identical record and result", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "variants": [ + { + "title": "Actions, attempts oldest first", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "Actions, records and attempts reversed", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 2, + "attempts": [ + { + "attempt": 2, + "state": "VERIFY_PASS" + }, + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "check suite", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_checks:check_suite:41", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "PARENT_LEVEL_ONLY", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_check_suite", + "latest_attempt": null, + "attempts": [ + { + "attempt": null, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_checks:check_suite:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + } + ] + }, + "expect": { + "band": "FLAKY", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 1 + } + }, + "notes": "GitLab-equivalent evidence (the third surface the case names) needs a GitLab adapter this version does not have." + }, + { + "case": "HIST-13", + "title": "carried history of the older lineage is replayed from the attempts it kept, never copied from the state it derived: its failure stays, its parent-level pass is unknown", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "r1", + "committed_at": "2026-09-05T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:51", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + }, + { + "revision": "r2", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [], + "current_verdict": null, + "history_state": "NO_DECISIVE_OBSERVED", + "historical_contribution": null, + "history_provenance": null, + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v1", + "records": [ + { + "revision": "r1", + "committed_at": "2026-09-05T10:00:00Z", + "history_state": "FAILURE_OBSERVED", + "historical_contribution": "VERIFY_FAIL", + "parents": [ + { + "parent_id": "github_actions:workflow_run:51", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 2, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + }, + { + "attempt": 2, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "FAILURE_OBSERVED", + "url": null + } + ] + }, + { + "revision": "r2", + "committed_at": "2026-09-06T10:00:00Z", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "parents": [ + { + "parent_id": "github_checks:check_suite:52", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "diagnostics": [ + "REVISION_HISTORY_UNKNOWN:r2" + ], + "derived": { + "failed_count_14d": 1, + "unknown_history_count_14d": 1 + } + }, + "notes": "devostasis.ci-history.v1 is the revision record 0.1.x bundles carry in ci.revision_verdicts_14d: its attempts are canonical, so the union is rebuilt from them under the current lineage. The 0.1.x record called r2 PASS_ONLY_OBSERVED from a check suite; R52 does not, and replay applies it." + }, + { + "case": "HIST-15", + "title": "carried history of a lineage with neither replay nor an accepted migration leaves the revisions it names unknown, until complete current evidence repairs them", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "variants": [ + { + "title": "no current verification", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [], + "current_verdict": null, + "history_state": "NO_DECISIVE_OBSERVED", + "historical_contribution": null, + "history_provenance": null, + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v9", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "a parent-level pass now", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_checks:check_suite:41", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "PARENT_LEVEL_ONLY", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v9", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "diagnostics": [ + "REVISION_HISTORY_UNKNOWN:r" + ], + "derived": { + "unknown_history_count_14d": 1 + } + } + }, + { + "case": "HIST-16", + "title": "a prior bundle's current verdict is never inherited: the revision the provider no longer shows keeps its history and speaks for nothing current", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-09T10:00:00Z", + "parents": [], + "current_verdict": null, + "history_state": "NO_DECISIVE_OBSERVED", + "historical_contribution": null, + "history_provenance": null, + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-09T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 1, + "attempts": [ + { + "attempt": 1, + "state": "VERIFY_FAIL" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + "expect": { + "band": "FLAKY", + "evaluation_status": "AVAILABLE", + "band_semantics": "EXACT", + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "derived": { + "decisive_count_14d": 5, + "failed_count_14d": 1, + "current_revision": { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED" + } + } + }, + "notes": "r is the newest revision and failed in the previous bundle; with no current verification it contributes its failure and is not the current reference." + }, + { + "case": "HIST-19", + "title": "a prior UNKNOWN_HISTORY does not heal because the provider now shows a pass that is complete only for what is visible", + "kind": "vital", + "source_unit": "PV-HIST-002", + "given": { + "vital": "integrity", + "observed_at": "2026-09-10T12:00:00Z", + "variants": [ + { + "title": "attempts 1 and 2 still unseen", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:41", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 3, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 3, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_actions_workflow_run", + "latest_attempt": 3, + "attempts": [ + { + "attempt": 3, + "state": "VERIFY_PASS" + } + ], + "parent_ids": [ + "github_actions:workflow_run:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "a check suite that passes now", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [ + { + "parent_id": "github_checks:check_suite:41", + "kind": "github_check_suite", + "name": "ci-app", + "event": null, + "current_attempt": null, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": null, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": false, + "history_state": "UNKNOWN_HISTORY", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "UNKNOWN_HISTORY", + "historical_contribution": null, + "history_provenance": "PARENT_LEVEL_ONLY", + "history_complete": false + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [ + { + "kind": "github_check_suite", + "latest_attempt": null, + "attempts": [ + { + "attempt": null, + "state": "VERIFY_UNRESOLVED" + } + ], + "parent_ids": [ + "github_checks:check_suite:41" + ] + } + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + }, + { + "title": "an unresolved gap marker", + "observations": [ + { + "observation_id": "ci.configured", + "value_type": "boolean", + "value": true + }, + { + "observation_id": "ci.revision_verdicts_14d", + "value_type": "series", + "value": [ + { + "revision": "p1", + "committed_at": "2026-09-01T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:901", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p2", + "committed_at": "2026-09-02T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:902", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p3", + "committed_at": "2026-09-03T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:903", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "p4", + "committed_at": "2026-09-04T10:00:00Z", + "parents": [ + { + "parent_id": "github_actions:workflow_run:904", + "kind": "github_actions_workflow_run", + "name": "ci", + "event": "push", + "current_attempt": 1, + "current_state": "VERIFY_PASS", + "attempts_observed": [ + { + "attempt": 1, + "state": "VERIFY_PASS" + } + ], + "attempts_complete": true, + "history_state": "PASS_ONLY_OBSERVED", + "url": null + } + ], + "current_verdict": "VERIFY_PASS", + "history_state": "PASS_ONLY_OBSERVED", + "historical_contribution": "VERIFY_PASS", + "history_provenance": "ATTEMPT_LEVEL", + "history_complete": true + }, + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parents": [], + "current_verdict": null, + "history_state": "NO_DECISIVE_OBSERVED", + "historical_contribution": null, + "history_provenance": null, + "history_complete": true + } + ] + }, + { + "observation_id": "ci.revision_history_carried", + "value_type": "record", + "value": { + "lineage": "devostasis.ci-history.v2", + "records": [ + { + "revision": "r", + "committed_at": "2026-09-06T10:00:00Z", + "parent_groups": [], + "unresolved": [ + "LINEAGE_INCOMPATIBLE:devostasis.ci-history.v9" + ] + } + ], + "source_bundle_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_observed_at": "2026-09-09T12:00:00Z", + "basis": "SNAPSHOT" + } + } + ] + } + ] + }, + "expect": { + "band": null, + "evaluation_status": "UNKNOWN", + "band_semantics": null, + "possible_bands": null, + "rule_id": "integrity.bands.v1+ci-unit-004+hist-002", + "diagnostics": [ + "REVISION_HISTORY_UNKNOWN:r" + ], + "derived": { + "unknown_history_count_14d": 1, + "decisive_count_14d": 4 + } + }, + "notes": "The nearby repair, complete canonical evidence of every attempt making the history PASS_ONLY_OBSERVED, is test_hist_19_complete_evidence_repairs_an_unknown_history." + } + ] +}