mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Merge batch-start published governance and release-page frontier
Change-source: pulse-maintainer
This commit is contained in:
@@ -75,25 +75,23 @@ that already belong to the canonical system.
|
||||
surface, prefer the largest same-surface slice that still has one coherent
|
||||
proof story. Split work only when there is a real risk, concept, or proof
|
||||
boundary, not merely because smaller residue items can be named.
|
||||
13. Treat scores and evidence as signals, not targets.
|
||||
Lane scores, missing evidence references, and proof counts are diagnostic
|
||||
signals that help identify the real runtime, product, ownership, or
|
||||
governance gap. They are not the work item by themselves. Fix the actual
|
||||
gap first, then record the proof that the gap is closed.
|
||||
13. Evidence counts do not determine the work target.
|
||||
Check the actual runtime, product, ownership or governance gap first, then
|
||||
record proof that the behavior is correct.
|
||||
14. Use worktrees when a mutating slice needs physical isolation.
|
||||
Claims and lane ownership reduce logical overlap, but they do not isolate
|
||||
hooks, dirty state, or staged scope. Use a dedicated worktree when a slice
|
||||
needs that isolation — for example when a subagent needs to mutate
|
||||
independently and land back cleanly. A shared checkout is fine for normal
|
||||
single-session work.
|
||||
15. Let the active target choose the default queue.
|
||||
15. Use the active target and current evidence to choose work.
|
||||
When the active target is a lane-expansion initiative and
|
||||
`status_audit.py --pretty` exposes an `available_candidate_lane_queue`,
|
||||
treat that queue as the default next-pick surface.
|
||||
`status_audit.py --pretty` exposes `available_candidate_lane_options`,
|
||||
use those scope and ownership facts as context for choosing work.
|
||||
Cleanup, presentation polish, and guardrail-only residue should normally
|
||||
support a selected candidate lane or an active blocker rather than
|
||||
displacing the queue.
|
||||
That default queue does not override the product-quality stop gate: if a
|
||||
displacing the active user direction.
|
||||
This selection context does not override the product-quality stop gate: if a
|
||||
customer-facing baseline is still not good enough to deserve iteration,
|
||||
step back to the owning redesign or canonical model first.
|
||||
16. Make every governed slice explicit.
|
||||
|
||||
@@ -139,8 +139,8 @@ model that active and future release profiles reuse.
|
||||
Reserve its governed claim in the checkout where the work will run.
|
||||
17. Lane-expansion targets must steer slice selection.
|
||||
When the active target is a lane-expansion or bridge-foundation target and
|
||||
`status_audit.py --pretty` exposes an `available_candidate_lane_queue`,
|
||||
agents should choose from that queue before local cleanup, bounded
|
||||
`status_audit.py --pretty` exposes `available_candidate_lane_options`,
|
||||
agents should assess those scope and ownership facts before local cleanup, bounded
|
||||
stabilization residue, or presentation-only polish unless the user
|
||||
explicitly overrides the priority or a release-blocking surface needs
|
||||
immediate containment.
|
||||
|
||||
@@ -31,7 +31,6 @@ the current lane map does not model a durable surface cleanly enough yet.
|
||||
Use `status.json.candidate_lanes` for the machine-readable promotion plan that
|
||||
turns durable coverage gaps into explicit future lane splits, additions, or
|
||||
expansions.
|
||||
The current highest-ranked surfaced candidate is policy-aware data governance.
|
||||
Use `status.json.work_claims` for the active lease-style reservation list that
|
||||
keeps multiple agents from picking the same governed slice at the same time.
|
||||
Record the claim immediately once the slice is chosen, and if an agent rolls
|
||||
@@ -45,9 +44,9 @@ Candidate lanes should point at the owning target from
|
||||
`docs/release-control/control_plane.json` so lane-expansion work is routed to
|
||||
an explicit initiative.
|
||||
Use `python3 scripts/release_control/status_audit.py --pretty` to see any
|
||||
derived `candidate_lane_queue` entries when agents need a ranked next pick for
|
||||
the lane-expansion target.
|
||||
Use that same audit output to see the `available_candidate_lane_queue`, active
|
||||
`candidate_lane_options` with scope and demand evidence for the lane-expansion
|
||||
target. Options use stable identifier order, with no inferred priority.
|
||||
Use that same audit output to see the `available_candidate_lane_options`, active
|
||||
claims, expired claims, and any claim conflicts before taking a new slice.
|
||||
Use `status.json.readiness_assertions` for the active required assertion set, its proof references, and any executable `proof_commands` that readiness assertion guardrails run.
|
||||
Use `docs/release-control/control_plane.json` for the active profile and active
|
||||
|
||||
@@ -108,10 +108,10 @@ completion records, the active readiness assertion catalog, readiness
|
||||
derivation rules, executable proof commands, structured evidence references,
|
||||
typed lane/subsystem decision records, active work claims, and the canonical machine-readable
|
||||
workspace repo catalog.
|
||||
Lane scores reaching target in `status.json` mean the tracked architecture and
|
||||
governance work for those lanes is at target; they are not, by themselves,
|
||||
approval to cut an RC or promote stable/GA if phase-blocking readiness
|
||||
assertions, `open_decisions`, or `release_gates` still remain.
|
||||
Lane `completion.state`, its evidence and typed residuals describe the tracked
|
||||
work. A `bounded-residual` lane remains distinct from `complete`. Neither state
|
||||
approves an RC or stable/GA promotion while phase-blocking readiness assertions,
|
||||
`open_decisions`, or `release_gates` remain.
|
||||
When the agent starts from a sibling repo or the shared workspace root, these
|
||||
same files are still authoritative; resolve them from `pulse/docs/release-control/v6/`
|
||||
rather than recreating release control in the current repo.
|
||||
@@ -419,7 +419,7 @@ Every substantial task must finish by checking these questions:
|
||||
state, or scope boundary, keep it internal and continue the work.
|
||||
9. Did I learn that the current lane map is hiding a durable product surface?
|
||||
If yes: record a `coverage_gaps` entry with evidence, an explicit coverage
|
||||
impact deduction, and the intended promotion path instead of burying the
|
||||
scope evidence, and the intended promotion path instead of burying the
|
||||
discovery inside generic residual text.
|
||||
When the intended destination is already clear, also add or update the
|
||||
matching `candidate_lanes` record so the lane-expansion plan is typed,
|
||||
@@ -453,10 +453,8 @@ Every substantial task must finish by checking these questions:
|
||||
If the lane state is being advanced in `status.json`, the same slice
|
||||
should normally also change an owned runtime or product surface for that
|
||||
lane unless the remaining lane gap is explicitly governance-only.
|
||||
10aa. Am I treating score or evidence deltas as the task instead of as
|
||||
diagnostics?
|
||||
Missing evidence items, current-vs-target score, and proof counts are
|
||||
signals that help locate the real same-lane gap.
|
||||
10aa. Am I treating evidence counts as the task instead of checking the
|
||||
actual remaining behavior and its proof?
|
||||
10ab. Have I checked real browser truth for a customer-facing surface?
|
||||
For customer-facing UI, code-level proof is not enough. Complete the
|
||||
Frontend Browser Verification Gate against the current build, exercise the
|
||||
@@ -495,9 +493,8 @@ Every substantial task must finish by checking these questions:
|
||||
Pair lane `status` and `completion.state` coherently: `complete` goes with
|
||||
`target-met`, `bounded-residual` goes with `partial`, and `open` must not
|
||||
be paired with `target-met`.
|
||||
`partial` means measurable progress, so it must not sit at zero score.
|
||||
`not-started` means zero score and `open`. `blocked` remains an `open`
|
||||
lane below target rather than a residual or complete state.
|
||||
`not-started` and `blocked` remain `open`, rather than residual or complete
|
||||
states. `partial` describes evidenced work that is still incomplete.
|
||||
Blocked lanes must declare typed same-lane blocker references to unresolved
|
||||
readiness assertions, release gates, or open decisions.
|
||||
`completion.tracking` is only for bounded residuals; if the lane is still
|
||||
@@ -528,8 +525,8 @@ residual work, the same slice must record that residual in
|
||||
`status.json.lanes[*].completion` with a short rationale and explicit tracking
|
||||
references to the governing lane followup, readiness assertion, release gate,
|
||||
or open decision that owns the follow-up.
|
||||
When feature work fits an existing lane, reopen that lane's completion state or
|
||||
score instead of pretending the map is still complete.
|
||||
When feature work fits an existing lane, reopen that lane's completion state
|
||||
instead of pretending the map is still complete.
|
||||
Existing lanes are not grandfathered out of the modernization rules.
|
||||
If a previously touched lane still relies on a non-canonical v6 path, lower
|
||||
its claimed completeness and continue from the canonical model rather than
|
||||
@@ -569,22 +566,13 @@ residual in `status.json.lanes[*].completion`, `status.json.lane_followups`,
|
||||
`status.json.coverage_gaps`, or another owning governed surface before you replace or release the claim.
|
||||
Treat a broad lane claim as blocking narrower same-lane blocker or follow-up
|
||||
claims until that broad claim is released or expires.
|
||||
Treat lane completion, lane coverage, and coverage planning as separate
|
||||
derived signals.
|
||||
`status_audit.py --pretty` may derive a lane-completion score from
|
||||
`current_score/target_score`, a lane-coverage score from unresolved
|
||||
`coverage_gaps`, a coverage-planning score from unresolved `coverage_gaps`
|
||||
that still lack explicit `candidate_lanes`, and a conservative
|
||||
governed-surface score as the lower of completion and coverage.
|
||||
Treat those scores and any missing evidence counts as diagnostic summaries,
|
||||
not as the task itself.
|
||||
Use them to choose the real remaining gap on the owned surface rather than
|
||||
optimizing directly for score movement or evidence-count closure unless the
|
||||
remaining work is explicitly governance-only.
|
||||
When lane-expansion work is the intended target, that same audit output may
|
||||
also derive a ranked `candidate_lane_queue` so agents can take the highest
|
||||
impact valid candidate lane first.
|
||||
That same audit output may also derive `available_candidate_lane_queue`,
|
||||
Keep declared completion, unresolved coverage gaps and proposed work distinct.
|
||||
`status_audit.py --pretty` exposes these records and their evidence without
|
||||
grading completion or assigning priority from proxy counts. Choose work from
|
||||
current user direction and the actual product, runtime and ownership evidence.
|
||||
For lane expansion, `candidate_lane_options` lists scope and demand evidence in
|
||||
stable identifier order. This order does not recommend one option over another.
|
||||
The audit also derives `available_candidate_lane_options`,
|
||||
active-versus-expired `work_claims`, and claim-conflict records so agents have
|
||||
an executable "do not overlap" surface instead of only prose.
|
||||
Those claim surfaces reduce logical overlap.
|
||||
|
||||
@@ -10,11 +10,11 @@ Evergreen control-plane governance now lives in
|
||||
`docs/release-control/CONTROL_PLANE.md`, and the active profile plus active
|
||||
target are machine-selected by `docs/release-control/control_plane.json`.
|
||||
|
||||
Current lane scores, evidence references, coverage-gap discovery records,
|
||||
Current lane states, evidence references, coverage-gap discovery records,
|
||||
candidate-lane planning records, and typed operational decision records live
|
||||
only in `docs/release-control/v6/internal/status.json`.
|
||||
Lane completion state, residual-gap summaries, normalized follow-up tracking,
|
||||
and derived coverage scores also live only in
|
||||
and typed blocker references also live only in
|
||||
`docs/release-control/v6/internal/status.json`.
|
||||
Non-blocking same-lane residuals that are concrete enough to name directly
|
||||
live in `status.json.lane_followups`.
|
||||
@@ -48,7 +48,7 @@ live in `status.json.readiness_assertions`.
|
||||
|
||||
This file must not contain:
|
||||
|
||||
1. hand-maintained lane score tables
|
||||
1. hand-maintained copies of lane state
|
||||
2. hand-maintained evidence lists
|
||||
3. ephemeral session recaps or task logs
|
||||
|
||||
@@ -387,7 +387,7 @@ Assertion design rules:
|
||||
Contract ratchets, proof-routing cleanup, registry tightening, and
|
||||
guardrail-only tests may strengthen confidence, but they must not be
|
||||
treated as substantive lane movement by themselves.
|
||||
If `status.json` advances a lane's score, status, or completion state, the
|
||||
If `status.json` advances a lane's status or completion state, the
|
||||
same slice should normally include an owned runtime or product-surface
|
||||
delta for that lane unless the remaining gap is explicitly governance-only.
|
||||
21. Large machine control files are canonical databases, not startup prompt
|
||||
@@ -415,12 +415,11 @@ Assertion design rules:
|
||||
tiny commits just because each residue item can be named separately. Split
|
||||
only when there is a real risk boundary, concept boundary, or proof
|
||||
boundary.
|
||||
24. Lane scores, missing evidence items, and proof counts are signals, not the
|
||||
work target.
|
||||
24. Evidence counts do not determine the work target.
|
||||
Use them to find the remaining runtime, product, ownership, or governance
|
||||
gap that still keeps the lane below floor.
|
||||
Do not frame a slice as "get two more evidence items" or "raise the lane
|
||||
score" unless the remaining gap is explicitly governance-only.
|
||||
Do not frame a slice as "get two more evidence items" unless the remaining
|
||||
gap is explicitly governance-only.
|
||||
For ordinary lane work, identify the real same-lane gap first, then add
|
||||
the proof that demonstrates the gap is actually closed.
|
||||
25. Worktrees are available for isolated mutating slices when explicit isolation
|
||||
@@ -475,7 +474,7 @@ Assertion design rules:
|
||||
page-shell contract and still present inconsistent top-level framing,
|
||||
header chrome, or section treatment without explicit justification.
|
||||
13. Do not keep polishing strong lanes while weak lanes remain behind.
|
||||
14. Do not treat `status.json` lane scores reaching target as sufficient release approval by themselves; open operational decisions, machine-derived unresolved readiness assertions, and unresolved release gates still apply.
|
||||
14. Do not treat `status.json` lane completion as sufficient release approval by itself; open operational decisions, machine-derived unresolved readiness assertions, and unresolved release gates still apply.
|
||||
15. Do not promote v6 to stable or GA without an exercised prerelease, a real
|
||||
release-pipeline proof run, a recorded rollback target plus exact
|
||||
reinstall command, and a written v5 maintenance-only support policy.
|
||||
@@ -494,9 +493,8 @@ Assertion design rules:
|
||||
Pair lane `status` and `completion.state` coherently: `complete` goes with
|
||||
`target-met`, `bounded-residual` goes with `partial`, and `open` must not
|
||||
be paired with `target-met`.
|
||||
`partial` means measurable progress, so it must not sit at zero score.
|
||||
`not-started` means zero score and `open`. `blocked` remains an `open`
|
||||
lane below target rather than a residual or complete state.
|
||||
`not-started` and `blocked` remain `open`, rather than residual or complete
|
||||
states. `partial` describes evidenced work that is still incomplete.
|
||||
Blocked lanes must declare typed same-lane blocker references to unresolved
|
||||
readiness assertions, release gates, or open decisions.
|
||||
`completion.tracking` is only for bounded residuals; open or complete lanes
|
||||
@@ -517,9 +515,9 @@ Assertion design rules:
|
||||
resources, canonical shared types, explicit subsystem ownership, or a
|
||||
modernized proof path, the lane is still below floor until that migration
|
||||
is made or the remaining gap is governed explicitly.
|
||||
20. Do not treat missing evidence count or a score delta as the task itself.
|
||||
20. Do not treat missing evidence counts as the task itself.
|
||||
If the real remaining gap is runtime, product, ownership, or behavior
|
||||
coherence, fix that gap and let the score/evidence catch up as proof.
|
||||
coherence, fix that gap and record evidence of the resulting behavior.
|
||||
Only treat evidence-count closure itself as the task when the remaining
|
||||
gap is explicitly governance-only.
|
||||
21. Do not run parallel mutating agents out of one dirty checkout and call
|
||||
@@ -918,20 +916,15 @@ For canonical subsystem work:
|
||||
13. If the current lane map no longer models a durable product surface cleanly,
|
||||
record that discovery in `status.json.coverage_gaps` with evidence and an
|
||||
intended promotion path instead of burying it in generic residual text.
|
||||
14. Treat lane completion, lane coverage, and coverage planning as separate
|
||||
derived signals.
|
||||
`status_audit.py --pretty` may derive a lane-completion score from
|
||||
`current_score/target_score`, a lane-coverage score from unresolved
|
||||
`coverage_gaps`, a coverage-planning score from unresolved
|
||||
`coverage_gaps` that still lack explicit `candidate_lanes`, and a
|
||||
conservative governed-surface score as the lower of completion and
|
||||
coverage.
|
||||
Those scores are decision aids, not proof that unknown work does not
|
||||
exist.
|
||||
14. Keep declared lane completion, unresolved coverage gaps and proposed work
|
||||
distinct. The audit exposes scope, demand evidence and ownership availability
|
||||
without grading completion or deriving priority from proxy counts. Candidate
|
||||
options use stable identifier order. Choose work from current user direction
|
||||
and actual evidence, not the position of an option in the output.
|
||||
15. Treat active work claiming as its own live control surface.
|
||||
`status.json.work_claims` should hold only live lease-style claims, not a
|
||||
historical log, and `status_audit.py --pretty` should make active claims,
|
||||
expired claims, claim conflicts, and the `available_candidate_lane_queue`
|
||||
expired claims, claim conflicts, and the `available_candidate_lane_options`
|
||||
visible enough that agents can avoid overlapping picks.
|
||||
Use `scripts/release_control/work_claim.py` as the default reservation
|
||||
path rather than hand-editing claim objects when the system can reserve
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
],
|
||||
"execution_model": "direct-repo-sessions",
|
||||
"readiness": {
|
||||
"repo_ready_rule": "all lanes target-met and evidence-present plus all repo-ready assertions passed",
|
||||
"repo_ready_rule": "all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed",
|
||||
"rc_ready_rule": "repo_ready plus all rc-ready assertions passed plus zero rc-ready open_decisions plus all rc-ready release_gates passed",
|
||||
"release_ready_rule": "rc_ready plus all release-ready assertions passed plus zero release-ready open_decisions plus all release-ready release_gates passed"
|
||||
},
|
||||
@@ -5095,36 +5095,10 @@
|
||||
"absolute_paths_forbidden": true,
|
||||
"local_repo": "pulse"
|
||||
},
|
||||
"priority_engine": {
|
||||
"formula": "behind_score = (max(0,target-current)*4) + criticality + staleness + dependency + blocker_bonus",
|
||||
"floor_rule": {
|
||||
"release_critical_lanes": [
|
||||
"L1",
|
||||
"L2",
|
||||
"L3",
|
||||
"L7",
|
||||
"L8",
|
||||
"L9",
|
||||
"L10",
|
||||
"L11",
|
||||
"L12"
|
||||
],
|
||||
"minimum_score": 6
|
||||
},
|
||||
"weights": {
|
||||
"gap_multiplier": 4,
|
||||
"criticality_range": "0-5",
|
||||
"staleness_range": "0-3",
|
||||
"dependency_range": "0-3",
|
||||
"blocker_bonus": 8
|
||||
}
|
||||
},
|
||||
"lanes": [
|
||||
{
|
||||
"id": "L1",
|
||||
"name": "Self-hosted release confidence",
|
||||
"target_score": 9,
|
||||
"current_score": 9,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5211,8 +5185,6 @@
|
||||
{
|
||||
"id": "L2",
|
||||
"name": "Conversion/commercial readiness",
|
||||
"target_score": 9,
|
||||
"current_score": 9,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5283,8 +5255,6 @@
|
||||
{
|
||||
"id": "L3",
|
||||
"name": "Cloud paid readiness",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5331,8 +5301,6 @@
|
||||
{
|
||||
"id": "L4",
|
||||
"name": "Hosted MSP readiness",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "target-met",
|
||||
"completion": {
|
||||
"state": "complete",
|
||||
@@ -5352,8 +5320,6 @@
|
||||
{
|
||||
"id": "L5",
|
||||
"name": "Mobile go-live readiness",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5398,8 +5364,6 @@
|
||||
{
|
||||
"id": "L6",
|
||||
"name": "Architecture coherence",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5490,8 +5454,6 @@
|
||||
{
|
||||
"id": "L7",
|
||||
"name": "Relay infrastructure readiness",
|
||||
"target_score": 9,
|
||||
"current_score": 9,
|
||||
"status": "target-met",
|
||||
"completion": {
|
||||
"state": "complete",
|
||||
@@ -5533,8 +5495,6 @@
|
||||
{
|
||||
"id": "L8",
|
||||
"name": "First-session & UX polish",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5596,8 +5556,6 @@
|
||||
{
|
||||
"id": "L9",
|
||||
"name": "Documentation readiness",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5637,8 +5595,6 @@
|
||||
{
|
||||
"id": "L10",
|
||||
"name": "Performance & scalability",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5725,8 +5681,6 @@
|
||||
{
|
||||
"id": "L11",
|
||||
"name": "v5-to-v6 migration safety",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5796,8 +5750,6 @@
|
||||
{
|
||||
"id": "L12",
|
||||
"name": "E2E journey coverage",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -5832,8 +5784,6 @@
|
||||
{
|
||||
"id": "L13",
|
||||
"name": "Core monitoring runtime",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "target-met",
|
||||
"completion": {
|
||||
"state": "complete",
|
||||
@@ -5976,8 +5926,6 @@
|
||||
{
|
||||
"id": "L14",
|
||||
"name": "Security, identity, and privacy",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "target-met",
|
||||
"completion": {
|
||||
"state": "complete",
|
||||
@@ -6080,8 +6028,6 @@
|
||||
{
|
||||
"id": "L15",
|
||||
"name": "Storage and recovery",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -6133,8 +6079,6 @@
|
||||
{
|
||||
"id": "L16",
|
||||
"name": "Agent lifecycle and fleet operations",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "target-met",
|
||||
"completion": {
|
||||
"state": "complete",
|
||||
@@ -6386,8 +6330,6 @@
|
||||
{
|
||||
"id": "L17",
|
||||
"name": "Customer account portal",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -6462,8 +6404,6 @@
|
||||
{
|
||||
"id": "L18",
|
||||
"name": "Policy-aware data governance",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -6538,8 +6478,6 @@
|
||||
{
|
||||
"id": "L19",
|
||||
"name": "Resource change intelligence",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -6714,8 +6652,6 @@
|
||||
{
|
||||
"id": "L20",
|
||||
"name": "Action governance and auditability",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -7133,8 +7069,6 @@
|
||||
{
|
||||
"id": "L21",
|
||||
"name": "Platform admission execution",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -7314,8 +7248,6 @@
|
||||
{
|
||||
"id": "L22",
|
||||
"name": "Fleet governance and rollout control",
|
||||
"target_score": 6,
|
||||
"current_score": 6,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -7465,8 +7397,6 @@
|
||||
{
|
||||
"id": "L23",
|
||||
"name": "Monitor-first Patrol operations",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "target-met",
|
||||
"completion": {
|
||||
"state": "complete",
|
||||
@@ -7648,8 +7578,6 @@
|
||||
{
|
||||
"id": "L24",
|
||||
"name": "Localized product surfaces",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -9734,7 +9662,6 @@
|
||||
"agent-lifecycle"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -9770,7 +9697,6 @@
|
||||
"unified-resources"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 6,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -9809,7 +9735,6 @@
|
||||
"security-privacy"
|
||||
],
|
||||
"proposed_resolution": "new-lane",
|
||||
"coverage_impact": 8,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -9868,7 +9793,6 @@
|
||||
"unified-resources"
|
||||
],
|
||||
"proposed_resolution": "new-lane",
|
||||
"coverage_impact": 9,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -9902,7 +9826,6 @@
|
||||
"monitoring"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 6,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -9941,7 +9864,6 @@
|
||||
"security-privacy"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -9977,7 +9899,6 @@
|
||||
"patrol-intelligence"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -10021,7 +9942,6 @@
|
||||
"patrol-intelligence"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -10055,7 +9975,6 @@
|
||||
"patrol-intelligence"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 4,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -10093,7 +10012,6 @@
|
||||
"notifications"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -10130,7 +10048,6 @@
|
||||
"security-privacy"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
@@ -10172,7 +10089,6 @@
|
||||
"unified-resources"
|
||||
],
|
||||
"proposed_resolution": "lane-expansion",
|
||||
"coverage_impact": 4,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
|
||||
@@ -5197,6 +5197,32 @@ is about 250 CPU-seconds, so under the pin it fails at 180 seconds with
|
||||
`signal: killed` (reproduced; the first beta's release failed on it twice),
|
||||
while a warmed cache makes the same pinned run a 16-second test. The race
|
||||
build of the test binary does not warm that plain build.
|
||||
|
||||
The release renderer appends a `## Source comparison` section after Roll back: a link to
|
||||
GitHub's comparison of the release tag with the stable rollback target it was given
|
||||
(`/compare/<rollback target>...<tag>`, from the candidate tag and rollback target the
|
||||
workflow already passes; the base is whatever lower stable target was supplied, not
|
||||
necessarily the immediately previous release). The link names bare refs, so a branch that shares
|
||||
a tag's name would win on GitHub; no such branch exists today. A `tags/` prefix, which
|
||||
GitHub's documentation describes for choosing the tag, returned HTTP 404 on the web
|
||||
comparison URL for tags that exist (checked on 2026-10-13 against v6.5.0 and
|
||||
v6.6.0-beta.2, with a nonexistent tag as a control), so the bare form is used. It is a pointer and not a
|
||||
complete list, because GitHub shows at most 250 commits in a comparison, so a larger release
|
||||
is only partly visible through it and the section says so. It is advisory. The comparison
|
||||
helper skips a missing or malformed tag or base, including a trailing newline, non-ASCII
|
||||
digits or numbers too large to convert, and raises nothing. The renderer's existing argument
|
||||
and body checks still apply to the whole render, so a rollback target that smuggles in a
|
||||
heading still fails the existing duplicate-Install check. A body that validated
|
||||
before still validates without the section, and an expected-body comparison still rejects a
|
||||
changed or deleted section when its expected body contains one. This does not establish that
|
||||
every historical published body meets today's other requirements. The post-update changelog
|
||||
dialog reads only recognised category headings, so a separate heading placed after Roll back
|
||||
is ignored by it; a `<details>` block placed inside a recognised category would be retained,
|
||||
and the sanitizer drops its collapse behaviour. Making the section required, and adding a
|
||||
generated row-level inventory that does prove completeness, needs a declarative contract
|
||||
record in the preparation snapshot: a marker in the body alone could be deleted to restore
|
||||
legacy acceptance, and a frozen in-flight release keeps its own renderer.
|
||||
|
||||
As of 2026-10-11, the governed release floor is Go `1.26.9`. It supersedes
|
||||
`1.26.8`, whose standard library is reachable through twelve vulnerable Pulse
|
||||
call paths reported by `govulncheck` after the 2026-10-08 advisories, including
|
||||
|
||||
@@ -362,7 +362,7 @@ page, API, alert policy or operator permission is introduced.
|
||||
|
||||
The existing workload drawer distinguishes assigned Pulse Agent action ownership,
|
||||
observed agent version metadata and Proxmox-reported guest-read state. Neither a
|
||||
version string nor a parent-node Discovery target may be called **connected**.
|
||||
version string nor a parent-node execution target may be called **connected**.
|
||||
Reported available, deferred, expected-unreachable, not-running, disabled and
|
||||
unknown states remain visible even before a version or filesystem sample exists.
|
||||
A current operation lock/backup takes precedence over a cached available flag.
|
||||
@@ -377,7 +377,8 @@ requires independent thaw and writes to covered filesystems. Running power
|
||||
state, an OK backup or reported agent availability establishes none of those.
|
||||
Fresh same-guest evidence withdraws the precaution without a new diagnostic or
|
||||
collector request. Existing filesystem provenance, metric values, History,
|
||||
Discovery/action permissions and monitoring policy are unchanged. Memory qualification uses its own authoritative observation; disk-read state
|
||||
action permissions and monitoring policy are unchanged. Memory qualification
|
||||
uses its own authoritative observation; disk-read state
|
||||
cannot qualify a different metric or renew its observation time.
|
||||
|
||||
Verification: `GuestDrawerOverview.agent-state.test.tsx` covers each current state
|
||||
@@ -1647,7 +1648,7 @@ The Backups surface passes its complete deduplicated route inventory to the PBS
|
||||
|
||||
One agent can be surfaced twice for a single PBS host: folded into its PVE guest and as a standalone `source=pbs` host row. Those two rows are one machine, not an ambiguous pair. Correlation must collapse candidates that share an agent identity and prefer the guest representation, whose canonical metrics target carries the persisted host history; the PBS service target has no stored host series and renders the no-history state. Two candidates with distinct agent identities remain ambiguous, and a candidate with no agent identity must not be treated as proof of sameness.
|
||||
|
||||
A live snapshot can briefly omit the correlated host row while the PBS server row remains, for example while a realtime refresh replaces the merged estate. The correlation must retain the last resolved host per PBS server across that omission instead of falling back to the PBS service target, so the drawer's Discovery and Metrics Target rows and its History series do not flicker. Reuse the remembered host only while it is still fresh relative to the server, and drop it once stale so a removed or replaced host is not advertised indefinitely; a host row that is present but ambiguous still declines.
|
||||
A live snapshot can briefly omit the correlated host row while the PBS server row remains, for example while a realtime refresh replaces the merged estate. The correlation must retain the last resolved host per PBS server across that omission instead of falling back to the PBS service target, so the drawer's linked host and History series do not flicker. Reuse the remembered host only while it is still fresh relative to the server, and drop it once stale so a removed or replaced host is not advertised indefinitely; a host row that is present but ambiguous still declines.
|
||||
|
||||
Verification: ProxmoxBackupServersTable.drawer.test.tsx covers standalone and
|
||||
merged guest targets, missing disks, duplicate guest/host representations of one
|
||||
@@ -2602,7 +2603,7 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
This contract also covers secondary drawer facts in availability status and
|
||||
suggestion cards, resource change-history entries, Docker/PBS/PMG service
|
||||
support panels, Docker container-update management cards, action history,
|
||||
Discovery summaries, and the specialized Proxmox Mail Gateway drawer.
|
||||
and the specialized Proxmox Mail Gateway drawer.
|
||||
Headers, status summaries, actions, disk capacity summaries, and RAID state
|
||||
pairs may retain intentional endpoint alignment; ordinary label/value facts
|
||||
in those surfaces may not.
|
||||
@@ -4399,13 +4400,8 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
self-hosted plan surfaces must hand
|
||||
off to Pulse Account/public Cloud ownership rather than route to an
|
||||
in-product Cloud trial/signup page.
|
||||
40. Keep AI Discovery retired. The model-backed service-identification layer
|
||||
(Discovery tab, identified-service cards, readiness badges, provenance
|
||||
markers, suggested web-interface URLs, and the AI Service Context settings
|
||||
page) was removed on 2026-10-11. Do not reintroduce drawer, row, or
|
||||
Assistant-briefing surfaces for AI-identified services without a new
|
||||
evidence-backed `FEATURE_REQUESTS.md` entry; persisted/manual web-interface
|
||||
metadata is the only row-link source.
|
||||
40. Follow the AI runtime contract for retired AI Discovery behavior.
|
||||
Persisted/manual web-interface metadata remains the only row-link source.
|
||||
41. Keep settings sidebar search able to find pages by the vocabulary users
|
||||
actually type, not only by rendered copy. `SettingsNavItem.keywords` in
|
||||
`frontend-modern/src/components/Settings/settingsNavigationModel.ts` is the
|
||||
@@ -4598,28 +4594,11 @@ their per-suite fixture reuses only identical source/path renders, returns
|
||||
isolated clones and invalidates changed input. No renderer, guide bytes,
|
||||
assertions or test deadlines are weakened.
|
||||
|
||||
### AI Discovery removed (2026-10-11)
|
||||
### Retired frontend ownership
|
||||
|
||||
AI Discovery, the model-backed service-identification layer, is gone. The
|
||||
`internal/servicediscovery` store and scanner, `internal/ai/infradiscovery`,
|
||||
the `pulse_discovery` Assistant/Patrol tool, the `/api/discovery*` routes, the
|
||||
mock Discovery fixtures, the resource/guest/node/Docker drawer Discovery tabs,
|
||||
identified-service cards, readiness badges, provenance markers, suggested
|
||||
web-interface URLs, the Workloads AI Context column, the chat "Discovery is
|
||||
off" hint and the AI Service Context settings page were all removed. No
|
||||
telemetry counter ever measured its use (the `discovery_enabled` telemetry
|
||||
field folded it together with network subnet discovery), the maintainer judged it a flop, and
|
||||
it carried a separate scheduler, store and model route for context that
|
||||
`pulse_query`, `pulse_read` and the unified-resource context sections already
|
||||
supply.
|
||||
Network subnet discovery (`internal/discovery`, `/api/discover`, the
|
||||
Infrastructure discovery settings) and the `discoveryTarget` agent coordinates
|
||||
are separate features and stay. Stored `discovery_enabled`,
|
||||
`discovery_interval_hours` and `discovery_model` AI settings are ignored on
|
||||
load. The old `/settings/pulse-intelligence/discovery` route redirects to
|
||||
Assistant settings.
|
||||
|
||||
`DiscoveryLoadingFallback`, `DiscoveryReadinessBadge`, `DiscoveryProvenanceMarker` and the now-orphaned `CopyableCodeRow` were deleted with their registry rules; the drawer spinner guard now names `LoadingSpinner` as its canonical owner.
|
||||
AI Discovery retirement and its compatibility boundaries are owned by
|
||||
`ai-runtime.md`. Deleted Discovery primitives have no current UI obligations.
|
||||
`LoadingSpinner` owns the remaining drawer loading boundary.
|
||||
|
||||
### Manual update freshness
|
||||
|
||||
@@ -4717,10 +4696,6 @@ by the shared-template registry rather than page-local discretion.
|
||||
Update progress status indicators are included in that loading boundary:
|
||||
progress-stage loading must compose `LoadingSpinner` rather than local spinner
|
||||
SVGs.
|
||||
`DiscoveryLoadingFallback` owns the discovery-tab Suspense fallback row for
|
||||
resource, workload, and Docker host drawers: centered row layout, status
|
||||
semantics, discovery loading copy, and canonical `LoadingSpinner` composition
|
||||
live there rather than in drawer-local fallback markup.
|
||||
`FilterButtonGroup` owns feature table view toggles as well as settings
|
||||
segmented selectors: page-specific labels and selected values stay in the
|
||||
owning feature model, but the visible segmented selector shell must come from
|
||||
@@ -4886,11 +4861,6 @@ Patrol status payload. That text is page-owned operational metadata inside the
|
||||
existing header row, not a new shared primitive, nested status card, or
|
||||
secondary verdict band.
|
||||
|
||||
`frontend-modern/src/utils/discoveryPresentation.ts` owns resource discovery
|
||||
command guidance targets. Discovery surfaces that need to tell operators where
|
||||
to enable command execution or verify `agent:exec` scope must use that helper's
|
||||
canonical `Settings → Infrastructure` and `Settings → API Access` handoffs
|
||||
instead of hard-coding legacy settings labels or old route paths.
|
||||
Shared frontend empty states, thresholds empty states, and discovery guidance
|
||||
that mention the Infrastructure settings destination now consume
|
||||
`frontend-modern/src/utils/infrastructureSettingsPresentation.ts` for the
|
||||
@@ -5487,13 +5457,6 @@ automatic-action policy, saved access configuration, and action audit belong
|
||||
to a dedicated `Manage` tab and must not render inside Overview. Guest, node,
|
||||
Docker-host, and unified-resource drawers must compose these shared primitives
|
||||
instead of defining provider-local attention or technical disclosure shells.
|
||||
The same drawer family must consume the canonical
|
||||
`useDiscoveryFeatureAvailability` boundary before exposing Discovery chrome or
|
||||
content. Until the shared AI runtime settings explicitly report Discovery as
|
||||
enabled, drawers render no Discovery tab, readiness state, explanatory copy,
|
||||
analysis reveal, or identified-service suggestion and issue no passive
|
||||
discovery-record read. Provider-specific drawer shells must not reinterpret a
|
||||
technical discovery target as operator consent to enable the feature.
|
||||
Drawer headers reserve their limited space for the subject and the canonical
|
||||
full-row collapse control, plus a direct object-specific lifecycle action only
|
||||
where the object contract requires one. `ObjectDrawerHeader` owns that
|
||||
@@ -5787,10 +5750,9 @@ Actions audit; the audit remains the authority before any later retry.
|
||||
The shared web interface URL field now follows that same owner split.
|
||||
`frontend-modern/src/components/shared/WebInterfaceUrlField.tsx` stays the
|
||||
render shell, `frontend-modern/src/components/shared/useWebInterfaceUrlFieldState.ts`
|
||||
owns metadata fetch/save/remove lifecycle, success/error state, and suggested
|
||||
URL runtime, and `frontend-modern/src/components/shared/webInterfaceUrlFieldModel.ts`
|
||||
owns URL validation, target-label normalization, and suggested-URL presentation
|
||||
rules. The shared primitive now also supports an embedded mode with a caller-
|
||||
owns metadata fetch/save/remove lifecycle and success/error state.
|
||||
`frontend-modern/src/components/shared/webInterfaceUrlFieldModel.ts` owns URL
|
||||
validation and target-label normalization. The shared primitive now also supports an embedded mode with a caller-
|
||||
owned title so feature drawers can place web-interface controls inside a larger
|
||||
access surface without forking the save/remove/runtime behavior. Future
|
||||
web-interface URL work should extend those owners instead of pushing metadata
|
||||
@@ -5800,12 +5762,8 @@ transport or validation back into the shared shell.
|
||||
metadata write gate: browser sessions always pass, while API-token sessions
|
||||
such as kiosk links need `monitoring:write` or the wildcard scope. Read-only
|
||||
sessions keep the saved URL with its open and copy controls and a one-line
|
||||
explanation, but get no input, Save, Remove, or suggested-URL adoption that the
|
||||
explanation, but get no input, Save, or Remove controls that the
|
||||
server would refuse.
|
||||
Missing-suggested-URL diagnostics remain useful only when the operator has no
|
||||
saved or entered URL; once a custom web-interface URL is present, the shared
|
||||
field must suppress "no suggested URL" warnings so Discovery does not make a
|
||||
valid manual endpoint look broken.
|
||||
Saved web-interface launch affordances must also stay on a shared primitive
|
||||
instead of page-local table columns or one-off external-link anchors.
|
||||
`frontend-modern/src/components/shared/WebInterfaceLink.tsx` owns URL
|
||||
@@ -7681,7 +7639,7 @@ maintenance routes through `AIChatMaintenanceSection.tsx`, and readiness plus
|
||||
save/test actions route through `AISettingsStatusAndActions.tsx`.
|
||||
`AISettingsStatusAndActions.tsx` may expose provider connection status and
|
||||
test actions only for the Provider & Models page; section pages reuse the save
|
||||
bar without making Patrol, Assistant, or Discovery look like provider setup
|
||||
bar without making Patrol or Assistant look like provider setup
|
||||
screens. Future AI settings work must extend those section owners instead of
|
||||
re-inlining large runtime subsections into the shell.
|
||||
Provider-specific settings fields inside
|
||||
@@ -7799,40 +7757,11 @@ skips its optional sign-in delivery. No frontend runtime file changes in this
|
||||
slice; the API proofs are
|
||||
`TestContract_HostedMagicLinkRequestValidatesOriginBeforeMutation` and
|
||||
`TestStripeWebhook_CheckoutMagicLinkValidatesOriginBeforeMutation`.
|
||||
`frontend-modern/src/utils/discoveryPresentation.ts` now owns the
|
||||
customer-facing discovery-section framing copy, scan-scope labels, subnet
|
||||
guidance, command-execution settings targets, API Access handoff labels, and
|
||||
environment-lock messaging so
|
||||
`frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx` stays a
|
||||
shared presentation shell instead of re-accumulating that wording inline.
|
||||
Resource discovery command guidance must use that same presentation owner for
|
||||
settings handoffs. `frontend-modern/src/utils/discoveryPresentation.ts` owns
|
||||
the shared command-execution and `agent:exec` token-scope links; discovery
|
||||
surfaces may explain those states, but the visible links must remain
|
||||
`Settings → Infrastructure` and `Settings → API Access` through that helper,
|
||||
not inline legacy labels or old settings paths.
|
||||
That same presentation owner also packages the identified-service summary
|
||||
consumed by surfaces outside the Discovery sub-tab.
|
||||
`getDiscoveryIdentifiedSummary` is the canonical reducer that turns a stored
|
||||
`ResourceDiscovery` into the compact card payload (service name, category,
|
||||
confidence percent, port and path counts, cli access hint, service version,
|
||||
observed timestamp, provenance label, and suggested web-interface URL
|
||||
metadata). New surfaces that want to label a workload with its identified
|
||||
service or offer a Discovery-sourced endpoint candidate must read through that
|
||||
helper rather than re-implementing the empty/low-signal gate, so the
|
||||
Discovery tab and out-of-tab surfaces collapse the same records and avoid
|
||||
surfacing "Unknown" rows or zero-confidence noise. Manual/persisted
|
||||
web-interface URLs still win: Discovery suggestions may be copied, opened, or
|
||||
adopted through the shared `WebInterfaceUrlField`, but they must not silently
|
||||
replace metadata or make row-name links active until the operator saves them.
|
||||
No-URL diagnostics and command access hints are not endpoint candidates; they
|
||||
can explain a Discovery result inside Discovery-owned surfaces, but they must
|
||||
not trigger out-of-tab identified-service cards or suggested-URL panels without
|
||||
another meaningful service signal.
|
||||
The visible provenance marker for those values is the shared
|
||||
`DiscoveryProvenanceMarker`; local surfaces may choose the labelled or
|
||||
icon-only variant, but must not invent alternate Discovery badges or hide the
|
||||
source on compact cards.
|
||||
`frontend-modern/src/utils/infrastructureSettingsPresentation.ts` owns network
|
||||
discovery framing, scan-scope labels, subnet guidance and environment-lock
|
||||
copy consumed by `DiscoverySettingsForm.tsx`. Network discovery remains an
|
||||
Infrastructure feature, separate from the retired AI service scanner.
|
||||
|
||||
That same settings-shell boundary now also owns the shared settings
|
||||
presentation helpers that those panels consume. `frontend-modern/src/utils/systemSettingsPresentation.ts`
|
||||
is the canonical owner for shared system-settings presets, summaries, and
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"readiness",
|
||||
"readiness_assertions",
|
||||
"evidence_reference_policy",
|
||||
"priority_engine",
|
||||
"lanes",
|
||||
"release_gates",
|
||||
"lane_followups",
|
||||
@@ -85,9 +84,6 @@
|
||||
"evidence_reference_policy": {
|
||||
"$ref": "#/$defs/evidence_reference_policy"
|
||||
},
|
||||
"priority_engine": {
|
||||
"$ref": "#/$defs/priority_engine"
|
||||
},
|
||||
"lanes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
@@ -258,7 +254,7 @@
|
||||
"properties": {
|
||||
"repo_ready_rule": {
|
||||
"type": "string",
|
||||
"const": "all lanes target-met and evidence-present plus all repo-ready assertions passed"
|
||||
"const": "all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed"
|
||||
},
|
||||
"rc_ready_rule": {
|
||||
"type": "string",
|
||||
@@ -270,96 +266,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority_engine": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"formula",
|
||||
"floor_rule",
|
||||
"weights"
|
||||
],
|
||||
"properties": {
|
||||
"formula": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"floor_rule": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"release_critical_lanes",
|
||||
"minimum_score"
|
||||
],
|
||||
"properties": {
|
||||
"release_critical_lanes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^L[0-9]+$"
|
||||
}
|
||||
},
|
||||
"minimum_score": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"weights": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"gap_multiplier",
|
||||
"blocker_bonus",
|
||||
"criticality_range",
|
||||
"staleness_range",
|
||||
"dependency_range"
|
||||
],
|
||||
"properties": {
|
||||
"gap_multiplier": {
|
||||
"type": "number"
|
||||
},
|
||||
"blocker_bonus": {
|
||||
"type": "number"
|
||||
},
|
||||
"criticality_range": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"staleness_range": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"dependency_range": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lane": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"const": "partial"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"current_score": {
|
||||
"not": {
|
||||
"const": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
@@ -370,9 +280,6 @@
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"current_score": {
|
||||
"const": 0
|
||||
},
|
||||
"completion": {
|
||||
"properties": {
|
||||
"state": {
|
||||
@@ -492,8 +399,6 @@
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"target_score",
|
||||
"current_score",
|
||||
"status",
|
||||
"completion",
|
||||
"blockers",
|
||||
@@ -509,16 +414,6 @@
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"target_score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
},
|
||||
"current_score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -1167,7 +1062,6 @@
|
||||
"lane_ids",
|
||||
"subsystem_ids",
|
||||
"proposed_resolution",
|
||||
"coverage_impact",
|
||||
"evidence"
|
||||
],
|
||||
"properties": {
|
||||
@@ -1221,11 +1115,6 @@
|
||||
"target-update"
|
||||
]
|
||||
},
|
||||
"coverage_impact": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
||||
@@ -376,40 +376,11 @@ func TestCanonicalDevelopmentProtocolExists(t *testing.T) {
|
||||
content := readRepoFile(t, rel)
|
||||
assertContainsAll(t, rel, content, []string{
|
||||
"# Pulse v6 Canonical Development Protocol",
|
||||
"## Core Rule",
|
||||
"## V6 Modernization Invariants",
|
||||
"## Required Operating Files",
|
||||
"## Subsystem Contracts",
|
||||
"## Task Completion Protocol",
|
||||
"## Guardrails",
|
||||
"## Boundary Rule",
|
||||
"AGENT_VALUES.md",
|
||||
"control_plane.py --agent-entrypoint --pretty",
|
||||
"status_lookup.py --lane <LANE_ID> --pretty",
|
||||
"work_claim.py --kind ... --id ... --summary ... --agent-id ... --write",
|
||||
"contract_audit.py --check",
|
||||
"Did the user state a durable product truth or change the current priority?",
|
||||
"consistency, seamlessness, drift, bypass",
|
||||
"Did the user casually raise a new quality bar or product invariant?",
|
||||
"status.json.lanes[*].completion",
|
||||
"references must also reference the same lane",
|
||||
"already-passed",
|
||||
"lane followup",
|
||||
"rather than a broad target reference",
|
||||
"candidate_lane_queue",
|
||||
"status.json.work_claims",
|
||||
"available_candidate_lane_queue",
|
||||
"replace the old claim with the new claim in the same `status.json` edit",
|
||||
"route through unified resources when that is the canonical shape",
|
||||
"These rules are retrospective.",
|
||||
"Existing lanes are not grandfathered out of the modernization rules.",
|
||||
"do not mark such a gap `planned`",
|
||||
"`target-update` coverage gaps",
|
||||
"Guardrail-only proof routing, contract wording, registry cleanup, and",
|
||||
"substantive lane movement by themselves.",
|
||||
"preserving legacy internals when only boundary compatibility is",
|
||||
"Legacy support is allowed only for explicit boundaries",
|
||||
"that is drift. Retire it or record the exception",
|
||||
"status_lookup.py",
|
||||
"work_claim.py",
|
||||
"status.json",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -515,52 +486,18 @@ func TestReleaseControlPlaneFilesExist(t *testing.T) {
|
||||
values := readRepoFile(t, valuesRel)
|
||||
assertContainsAll(t, valuesRel, values, []string{
|
||||
"# Pulse Agent Values",
|
||||
"## Purpose",
|
||||
"## Values",
|
||||
"## Delegation Rule",
|
||||
"## Current State",
|
||||
"prompt-like guidance should express posture, priorities, and decision heuristics",
|
||||
"the canonical system, not the prompt, carry the detailed operating burden",
|
||||
"governance routing should usually be automatic and mostly invisible to the user",
|
||||
"Do that resolution quietly unless the result materially changes the plan",
|
||||
"control_plane.py --agent-entrypoint --pretty",
|
||||
"work_claim.py --kind ... --id ... --summary ... --agent-id ... --pretty",
|
||||
"Proof routing, registry cleanup, contract ratchets, and guardrail-only test",
|
||||
"Treat guardrail work as support work.",
|
||||
"owned runtime or product-surface delta in the same slice",
|
||||
"Keep legacy support at the boundary.",
|
||||
"Do not preserve or polish legacy-primary internal paths",
|
||||
"Prefer the largest coherent slice.",
|
||||
"Treat scores and evidence as signals, not targets.",
|
||||
"gap first, then record the proof",
|
||||
"Use worktrees when a mutating slice needs physical isolation.",
|
||||
"work_claim.py",
|
||||
})
|
||||
|
||||
docRel := "docs/release-control/internal/CONTROL_PLANE.md"
|
||||
doc := readRepoFile(t, docRel)
|
||||
assertContainsAll(t, docRel, doc, []string{
|
||||
"# Pulse Release Control Plane",
|
||||
"AGENT_VALUES.md",
|
||||
"control_plane.py --agent-entrypoint --pretty",
|
||||
"control_plane.json",
|
||||
"active target",
|
||||
"v6 is the current active release profile",
|
||||
"control_plane_audit.py --check",
|
||||
"Direction changes must be normalized",
|
||||
"Do not wait for a special governance prompt",
|
||||
"stable or GA promotion readiness",
|
||||
"v6-release-reliability",
|
||||
"v6-ga-promotion",
|
||||
"v6-rc-stabilization",
|
||||
"available_candidate_lane_queue",
|
||||
"should always be true",
|
||||
"Lane work must follow canonical v6 modernization rules",
|
||||
"These modernization rules are retrospective as well as forward-looking",
|
||||
"Governance routing should be quiet by default",
|
||||
"status_lookup.py",
|
||||
"raw `status.json` or `registry.json`",
|
||||
})
|
||||
|
||||
jsonRel := "docs/release-control/control_plane.json"
|
||||
jsonContent := readRepoFile(t, jsonRel)
|
||||
assertContainsAll(t, jsonRel, jsonContent, []string{
|
||||
@@ -664,71 +601,18 @@ func TestV6ControlDocsReferenceCanonicalDevelopmentProtocol(t *testing.T) {
|
||||
"CONTROL_PLANE.md",
|
||||
"control_plane.json",
|
||||
"CANONICAL_DEVELOPMENT_PROTOCOL.md",
|
||||
"HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md",
|
||||
"status.schema.json",
|
||||
"registry.schema.json",
|
||||
"subsystems/*.md",
|
||||
"structured evidence references",
|
||||
"ready to cut a prerelease or approve stable/GA",
|
||||
"control_plane_audit.py --check",
|
||||
"status_audit.py --pretty",
|
||||
"status.json.coverage_gaps",
|
||||
"status.json.candidate_lanes",
|
||||
"status.json.work_claims",
|
||||
"status.json.readiness_assertions",
|
||||
"proof_commands",
|
||||
"available_candidate_lane_queue",
|
||||
})
|
||||
|
||||
source := readRepoFile(t, "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md")
|
||||
assertContainsAll(t, "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", source, []string{
|
||||
"AGENT_VALUES.md",
|
||||
"control_plane.py --agent-entrypoint --pretty",
|
||||
"work_claim.py",
|
||||
"CONTROL_PLANE.md",
|
||||
"control_plane.json",
|
||||
"CANONICAL_DEVELOPMENT_PROTOCOL.md",
|
||||
"HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md",
|
||||
"status.schema.json",
|
||||
"registry.schema.json",
|
||||
"docs/release-control/v6/internal/subsystems/",
|
||||
"## Evergreen Readiness Assertions",
|
||||
"## Development Governance",
|
||||
"Do not treat `status.json` lane scores reaching target as sufficient release approval by themselves",
|
||||
"Do not promote v6 to stable or GA without an exercised prerelease",
|
||||
"v5 maintenance-only support policy",
|
||||
"control_plane_audit.py --check",
|
||||
"When a user states a durable product truth",
|
||||
"classify that as an active-target update or another control-plane",
|
||||
"Do not wait for a special governance prompt",
|
||||
"Lanes that are at target but intentionally not closed must record a",
|
||||
"Those references must belong to that same lane",
|
||||
"status.json.coverage_gaps",
|
||||
"status.json.candidate_lanes",
|
||||
"status.json.work_claims",
|
||||
"lane-coverage score",
|
||||
"coverage-planning score",
|
||||
"active claims",
|
||||
"available_candidate_lane_queue",
|
||||
"The claim is an audit trail",
|
||||
"Lane work must be evaluated against the canonical v6 model",
|
||||
"These modernization rules are retrospective.",
|
||||
"Do not treat a lane as healthy just because it has local fixes on a legacy path.",
|
||||
"consistency, seamlessness, drift, bypass",
|
||||
"status_audit.py --pretty",
|
||||
"status_lookup.py",
|
||||
"canonical databases, not startup prompt payload",
|
||||
"Claimed lane work should prefer the largest coherent same-surface slice.",
|
||||
"Lane scores, missing evidence items, and proof counts are signals, not the",
|
||||
"Do not frame a slice as \"get two more evidence items\"",
|
||||
"Worktrees are available for isolated mutating slices",
|
||||
"Do not run parallel mutating agents out of one dirty checkout",
|
||||
"status.json.readiness_assertions",
|
||||
"`coverage_gaps.status` is not cosmetic",
|
||||
"`target-update` coverage gaps",
|
||||
"Keep governance routing quiet by default",
|
||||
})
|
||||
|
||||
matrix := readRepoFile(t, "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md")
|
||||
assertContainsAll(t, "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", matrix, []string{
|
||||
"status.json.release_gates",
|
||||
@@ -764,7 +648,6 @@ func TestStatusSchemaExistsAndDeclaresTypedStatusContract(t *testing.T) {
|
||||
"\"agent_id\"",
|
||||
"\"heartbeat_at\"",
|
||||
"\"expires_at\"",
|
||||
"\"coverage_impact\"",
|
||||
"\"proposed_resolution\"",
|
||||
"\"blocking_level\"",
|
||||
"\"proof_type\"",
|
||||
@@ -796,48 +679,11 @@ func TestSourceOfTruthStaysStableAndNonOperational(t *testing.T) {
|
||||
rel := "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md"
|
||||
content := readRepoFile(t, rel)
|
||||
assertContainsAll(t, rel, content, []string{
|
||||
"## Purpose",
|
||||
"## Canonical Control Files",
|
||||
"## Scope",
|
||||
"## Release Definition",
|
||||
"## Evergreen Readiness Assertions",
|
||||
"## Non-Negotiable Release Gates",
|
||||
"## Locked Decisions",
|
||||
"## Development Governance",
|
||||
"## Source Domains",
|
||||
"It is not a live progress dashboard.",
|
||||
"coverage-gap discovery records",
|
||||
"candidate-lane planning records",
|
||||
"Active multi-agent lease records",
|
||||
"normalized follow-up tracking",
|
||||
"derived coverage scores",
|
||||
"status.json.lane_followups",
|
||||
"status.json.coverage_gaps",
|
||||
"status.json.candidate_lanes",
|
||||
"status.json.work_claims",
|
||||
"coverage-planning score",
|
||||
"`complete` goes with",
|
||||
"`bounded-residual` goes with",
|
||||
"`partial` means measurable progress",
|
||||
"`not-started` means zero score",
|
||||
"`blocked` remains an `open`",
|
||||
"typed same-lane blocker references",
|
||||
"Those references must belong to that same lane",
|
||||
"already-passed assertions, gates, or completed targets",
|
||||
"open or complete lanes",
|
||||
"rather than a broad target reference",
|
||||
"active residual records",
|
||||
"Lane work must be evaluated against the canonical v6 model",
|
||||
"These modernization rules are retrospective.",
|
||||
"Do not treat a lane as healthy just because it has local fixes on a legacy path.",
|
||||
"Legacy compatibility must be named as a boundary exception",
|
||||
"Preserve legacy behavior only at explicit boundaries",
|
||||
"Within an active claimed lane, prefer the largest coherent same-surface slice",
|
||||
"Lane scores, missing evidence items, and proof counts are signals, not the",
|
||||
"Do not frame a slice as \"get two more evidence items\"",
|
||||
"Do not treat missing evidence count or a score delta as the task itself.",
|
||||
"Worktrees are available for isolated mutating slices",
|
||||
"Do not run parallel mutating agents out of one dirty checkout",
|
||||
"status.json",
|
||||
"status.schema.json",
|
||||
"registry.schema.json",
|
||||
"CANONICAL_DEVELOPMENT_PROTOCOL.md",
|
||||
})
|
||||
assertContainsNone(t, rel, content, []string{
|
||||
"## Priority Engine",
|
||||
@@ -882,9 +728,6 @@ func TestStatusJSONHasStrictTopLevelSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := status["priority_engine"].(map[string]any); !ok {
|
||||
t.Fatalf("status.json missing priority_engine object")
|
||||
}
|
||||
if _, ok := status["open_decisions"].([]any); !ok {
|
||||
t.Fatalf("status.json missing open_decisions list")
|
||||
}
|
||||
@@ -925,8 +768,8 @@ func TestStatusJSONHasStrictTopLevelSchema(t *testing.T) {
|
||||
if gates, ok := status["release_gates"].([]any); !ok || len(gates) == 0 {
|
||||
t.Fatalf("status.json release_gates must be a non-empty list")
|
||||
}
|
||||
if got, ok := readiness["repo_ready_rule"].(string); !ok || got != "all lanes target-met and evidence-present plus all repo-ready assertions passed" {
|
||||
t.Fatalf("status.json readiness.repo_ready_rule = %#v, want %q", readiness["repo_ready_rule"], "all lanes target-met and evidence-present plus all repo-ready assertions passed")
|
||||
if got, ok := readiness["repo_ready_rule"].(string); !ok || got != "all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed" {
|
||||
t.Fatalf("status.json readiness.repo_ready_rule = %#v, want %q", readiness["repo_ready_rule"], "all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed")
|
||||
}
|
||||
if got, ok := readiness["rc_ready_rule"].(string); !ok || got != "repo_ready plus all rc-ready assertions passed plus zero rc-ready open_decisions plus all rc-ready release_gates passed" {
|
||||
t.Fatalf("status.json readiness.rc_ready_rule = %#v, want %q", readiness["rc_ready_rule"], "repo_ready plus all rc-ready assertions passed plus zero rc-ready open_decisions plus all rc-ready release_gates passed")
|
||||
@@ -1060,20 +903,6 @@ func TestStatusJSONLaneEvidenceReferencesAreStructured(t *testing.T) {
|
||||
if _, ok := lane["name"].(string); !ok {
|
||||
t.Fatalf("status.json lane %q missing string name", laneID)
|
||||
}
|
||||
targetScore, ok := lane["target_score"].(float64)
|
||||
if !ok {
|
||||
t.Fatalf("status.json lane %q missing numeric target_score", laneID)
|
||||
}
|
||||
currentScore, ok := lane["current_score"].(float64)
|
||||
if !ok {
|
||||
t.Fatalf("status.json lane %q missing numeric current_score", laneID)
|
||||
}
|
||||
if targetScore < 0 || targetScore > 10 || currentScore < 0 || currentScore > 10 {
|
||||
t.Fatalf("status.json lane %q score out of range", laneID)
|
||||
}
|
||||
if currentScore > targetScore {
|
||||
t.Fatalf("status.json lane %q current_score %.0f exceeds target_score %.0f", laneID, currentScore, targetScore)
|
||||
}
|
||||
statusValue, ok := lane["status"].(string)
|
||||
if !ok || !slices.Contains([]string{"not-started", "partial", "target-met", "blocked"}, statusValue) {
|
||||
t.Fatalf("status.json lane %q has invalid status %#v", laneID, lane["status"])
|
||||
@@ -1103,21 +932,12 @@ func TestStatusJSONLaneEvidenceReferencesAreStructured(t *testing.T) {
|
||||
if completionState == "open" && statusValue == "target-met" {
|
||||
t.Fatalf("status.json lane %q must not pair completion.state open with status target-met", laneID)
|
||||
}
|
||||
if statusValue == "partial" && currentScore == 0 {
|
||||
t.Fatalf("status.json lane %q must keep current_score above 0 while status is partial", laneID)
|
||||
}
|
||||
if statusValue == "not-started" && currentScore != 0 {
|
||||
t.Fatalf("status.json lane %q must keep current_score at 0 while status is not-started", laneID)
|
||||
}
|
||||
if statusValue == "not-started" && completionState != "open" {
|
||||
t.Fatalf("status.json lane %q must pair status not-started with completion.state open", laneID)
|
||||
}
|
||||
if statusValue == "blocked" && completionState != "open" {
|
||||
t.Fatalf("status.json lane %q must pair status blocked with completion.state open", laneID)
|
||||
}
|
||||
if statusValue == "blocked" && currentScore >= targetScore {
|
||||
t.Fatalf("status.json lane %q must stay below target_score while status is blocked", laneID)
|
||||
}
|
||||
if statusValue == "blocked" && len(rawBlockers) == 0 {
|
||||
t.Fatalf("status.json lane %q must declare blockers while status is blocked", laneID)
|
||||
}
|
||||
|
||||
@@ -378,9 +378,9 @@ def agent_entrypoint(*, staged: bool = False) -> dict[str, Any]:
|
||||
]
|
||||
)
|
||||
if active_target_id == "v6-product-lane-expansion":
|
||||
default_pick_surface = "available_candidate_lane_queue"
|
||||
default_pick_surface = "available_candidate_lane_options"
|
||||
selection_rule = (
|
||||
"Pick from available candidate lanes and linked coverage gaps before local prerelease cleanup unless the user overrides the priority or a release-blocking surface needs immediate containment."
|
||||
"Use current user direction and the evidence for available candidate lanes and linked coverage gaps to choose work. Options are listed by stable identifier, with ownership availability, and carry no priority ranking."
|
||||
)
|
||||
else:
|
||||
default_pick_surface = "lanes"
|
||||
|
||||
@@ -122,7 +122,7 @@ class ControlPlaneAuditTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
if entrypoint["active_target_id"] == "v6-product-lane-expansion":
|
||||
self.assertEqual(entrypoint["default_pick_surface"], "available_candidate_lane_queue")
|
||||
self.assertEqual(entrypoint["default_pick_surface"], "available_candidate_lane_options")
|
||||
self.assertIn("candidate lanes", entrypoint["selection_rule"])
|
||||
self.assertIn(
|
||||
"python3 scripts/release_control/status_lookup.py --candidate-lane <CANDIDATE_LANE_ID> --pretty",
|
||||
|
||||
@@ -589,6 +589,41 @@ def build_rollback_section(args: argparse.Namespace) -> str:
|
||||
)
|
||||
|
||||
|
||||
# ASCII digits only, bounded, and matched against the whole string: `$` would accept a final newline
|
||||
# (which would land inside the URL), `\d` accepts non-ASCII digits, and an unbounded number would make
|
||||
# the comparison below raise on conversion.
|
||||
_COMPARISON_HEAD = re.compile(r"v([0-9]{1,6})\.([0-9]{1,6})\.([0-9]{1,6})(?:-(?:alpha|beta|rc)\.[1-9][0-9]{0,5})?")
|
||||
_COMPARISON_BASE = re.compile(r"v([0-9]{1,6})\.([0-9]{1,6})\.([0-9]{1,6})")
|
||||
|
||||
|
||||
def build_source_comparison(args: argparse.Namespace) -> str | None:
|
||||
"""A pointer to GitHub's own comparison of this release with the stable rollback target it was given.
|
||||
|
||||
This is a pointer, not a complete list: GitHub shows at most 250 commits in a comparison, so a
|
||||
release with more changes than that is only partly visible through it. It is also advisory. It
|
||||
exists so a reader can look up a change the summary does not name, and it renders nothing, and
|
||||
raises nothing, for a missing or malformed tag or for a base that is not a lower stable release.
|
||||
"""
|
||||
head = _COMPARISON_HEAD.fullmatch(args.candidate_tag or "")
|
||||
base = _COMPARISON_BASE.fullmatch(args.rollback_target or "")
|
||||
if not head or not base:
|
||||
return None
|
||||
if tuple(map(int, base.groups())) >= tuple(map(int, head.groups())):
|
||||
return None
|
||||
return "\n".join(
|
||||
[
|
||||
"## Source comparison",
|
||||
"",
|
||||
f"[Browse source changes from `{args.rollback_target}` to `{args.candidate_tag}`]"
|
||||
f"(https://github.com/rcourtman/Pulse/compare/{args.rollback_target}...{args.candidate_tag}) "
|
||||
"on GitHub for more detail behind this summary.",
|
||||
"",
|
||||
"This comparison includes internal maintenance. GitHub shows at most 250 commits, "
|
||||
"so larger comparisons may omit commits.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--version", required=True)
|
||||
@@ -688,6 +723,9 @@ def main() -> int:
|
||||
build_rollback_section(args),
|
||||
]
|
||||
)
|
||||
comparison = build_source_comparison(args)
|
||||
if comparison:
|
||||
sections.append(comparison)
|
||||
rendered = "\n\n".join(sections) + "\n"
|
||||
validate_release_body_shape(rendered, args.version)
|
||||
Path(args.output).write_text(rendered, encoding="utf-8")
|
||||
|
||||
@@ -1074,5 +1074,146 @@ class AssetValidationWordingTest(unittest.TestCase):
|
||||
self.assertIn("<!-- VALIDATION_STATUS_END -->", banner)
|
||||
|
||||
|
||||
class SourceComparisonTests(unittest.TestCase):
|
||||
"""The advisory tag-bound pointer to GitHub's comparison, and its place in the rendered body."""
|
||||
|
||||
NOTES = _REPO_ROOT / "docs" / "releases" / "RELEASE_NOTES_v6.5.0.md"
|
||||
|
||||
@staticmethod
|
||||
def args(tag: str | None, base: str | None) -> "argparse.Namespace":
|
||||
import argparse
|
||||
|
||||
return argparse.Namespace(candidate_tag=tag, rollback_target=base)
|
||||
|
||||
def render(self, version: str, tag: str, base: str, notes: Path | None = None) -> subprocess.CompletedProcess:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "body.md"
|
||||
result = subprocess.run(
|
||||
["python3", str(_REPO_ROOT / "scripts/release_control/render_release_body.py"),
|
||||
"--version", version, "--release-notes-file", str(notes or self.NOTES),
|
||||
"--output", str(output), "--promotion-channel", "stable-rc-promotion",
|
||||
"--candidate-tag", tag, "--promoted-prerelease-tag", "", "--rollback-target", base,
|
||||
"--rollback-command", f"sudo /bin/update --version {base}",
|
||||
"--planned-ga-date", "", "--planned-v5-eos-date", "", "--hotfix-exception", "false",
|
||||
"--hotfix-reason", "", "--require-windows-signing", "false",
|
||||
"--unsigned-windows-exception", "false", "--unsigned-windows-reason", ""],
|
||||
capture_output=True, text=True, cwd=_REPO_ROOT)
|
||||
result.body = output.read_text(encoding="utf-8") if output.exists() else ""
|
||||
return result
|
||||
|
||||
def test_section_binds_the_exact_tag_and_stable_base(self) -> None:
|
||||
section = render_release_body.build_source_comparison(self.args("v6.6.0-beta.3", "v6.5.0"))
|
||||
self.assertIsNotNone(section)
|
||||
self.assertTrue(section.startswith("## Source comparison\n"))
|
||||
self.assertIn("https://github.com/rcourtman/Pulse/compare/v6.5.0...v6.6.0-beta.3)", section)
|
||||
# GitHub caps a comparison at 250 commits, so this must not claim to be a full list.
|
||||
self.assertIn("at most 250 commits", section)
|
||||
self.assertIn("may omit commits", section)
|
||||
self.assertNotIn("Full list", section)
|
||||
# Stable promotion compares the same stable base with the final tag.
|
||||
self.assertIn("compare/v6.5.0...v6.6.0)", render_release_body.build_source_comparison(self.args("v6.6.0", "v6.5.0")))
|
||||
|
||||
def test_missing_or_unusable_context_renders_nothing_and_never_raises(self) -> None:
|
||||
huge = "v" + "1" * 5000 + ".0.0"
|
||||
for tag, base in [(None, "v6.5.0"), ("v6.6.0-beta.3", None), ("", ""), ("6.6.0", "v6.5.0"),
|
||||
("v6.6.0-beta.3", "6.5.0"), ("v6.6.0-beta.3", "v6.5.0-rc.1"),
|
||||
("v6.6.0-beta.3", "v6.6.0"), ("v6.5.0", "v6.5.0"), ("v6.5.0", "v6.6.0"),
|
||||
("v6.6.0-beta.0", "v6.5.0"), ("v6.6.0-beta.3\n#", "v6.5.0"),
|
||||
# a final newline would put a line break inside the URL
|
||||
("v6.5.0\n", "v6.4.5"), ("v6.5.0", "v6.4.5\n"), ("v6.6.0-beta.3\n", "v6.5.0"),
|
||||
# non-ASCII digits, and numbers too large to convert, must not raise
|
||||
("v\uff16.\uff15.\uff10", "v6.4.5"), ("v6.5.0", "v\uff16.\uff14.\uff15"),
|
||||
(huge, "v6.4.5"), ("v6.5.0", huge), ("v6.5.0", "v6.4." + "9" * 7),
|
||||
("v6.6.0-beta." + "9" * 7, "v6.5.0")]:
|
||||
with self.subTest(tag=tag, base=base):
|
||||
self.assertIsNone(render_release_body.build_source_comparison(self.args(tag, base)))
|
||||
|
||||
def test_rendered_body_ends_with_the_comparison_after_roll_back_and_validates(self) -> None:
|
||||
result = self.render("6.5.0", "v6.5.0", "v6.4.5")
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
headings = re.findall(r"(?m)^## (.+)$", result.body)
|
||||
self.assertEqual(["Install", "Roll back", "Source comparison"], headings[-3:])
|
||||
self.assertEqual(1, result.body.count("## Source comparison"))
|
||||
self.assertIn("compare/v6.4.5...v6.5.0)", result.body)
|
||||
# The stored body round-trips through the validator the edit and recovery workflows use.
|
||||
render_release_body.validate_release_body_shape(result.body, "6.5.0")
|
||||
clean = render_release_body.validate_release_body_shape(result.body, "6.5.0", expected_body=result.body)
|
||||
self.assertIn("## Source comparison", clean)
|
||||
|
||||
def test_the_base_is_the_supplied_stable_rollback_target_and_orders_numerically(self) -> None:
|
||||
build = render_release_body.build_source_comparison
|
||||
# Numeric, not lexical: patch 10 follows patch 9. Alpha and release candidate heads work like betas.
|
||||
self.assertIn("compare/v6.4.9...v6.4.10)", build(self.args("v6.4.10", "v6.4.9")))
|
||||
self.assertIsNone(build(self.args("v6.4.9", "v6.4.10")))
|
||||
self.assertIn("compare/v6.5.0...v6.6.0-rc.2)", build(self.args("v6.6.0-rc.2", "v6.5.0")))
|
||||
self.assertIn("compare/v6.5.0...v6.6.0-alpha.1)", build(self.args("v6.6.0-alpha.1", "v6.5.0")))
|
||||
# A hotfix and an older, non-immediate rollback target are used exactly as supplied.
|
||||
self.assertIn("compare/v6.4.1...v6.5.1)", build(self.args("v6.5.1", "v6.4.1")))
|
||||
|
||||
def test_section_text_is_separated_by_one_blank_line_and_ends_with_one_newline(self) -> None:
|
||||
result = self.render("6.5.0", "v6.5.0", "v6.4.5")
|
||||
self.assertTrue(result.body.endswith("larger comparisons may omit commits.\n"), result.body[-120:])
|
||||
self.assertFalse(result.body.endswith("\n\n"))
|
||||
self.assertIn("\n\n## Source comparison\n\n[Browse source changes", result.body)
|
||||
# Exactly one blank line before the section, as between every other section.
|
||||
self.assertNotIn("\n\n\n## Source comparison", result.body)
|
||||
before = result.body.split("\n\n## Source comparison")[0]
|
||||
self.assertTrue(before.rstrip("\n").endswith("recreate the container."), before[-80:])
|
||||
self.assertFalse(before.endswith("\n"))
|
||||
self.assertIn(") on GitHub for more detail behind this summary.", result.body)
|
||||
|
||||
def test_edit_and_annotation_cycles_preserve_the_section_exactly(self) -> None:
|
||||
# validate-release-assets.yml re-reads the stored body, strips its status block, prepends a
|
||||
# fresh one and checks equality with the cleaned body. Repeat that, with the extra newline
|
||||
# `jq -r` adds and with CRLF line endings, and the section must survive unchanged.
|
||||
result = self.render("6.5.0", "v6.5.0", "v6.4.5")
|
||||
block = ("<!-- VALIDATION_STATUS_START -->\n## Release Asset Validation: PASSED\n\n"
|
||||
"**Validated**: 2026-10-13 00:00:00 UTC\n\n<!-- VALIDATION_STATUS_END -->\n\n")
|
||||
clean = render_release_body.validate_release_body_shape(result.body, "6.5.0")
|
||||
for cycle in range(10):
|
||||
stored = block + clean + ("\n" if cycle % 2 else "")
|
||||
clean = render_release_body.validate_release_body_shape(stored, "6.5.0", expected_body=None)
|
||||
crlf = render_release_body.validate_release_body_shape(stored.replace("\n", "\r\n"), "6.5.0")
|
||||
self.assertEqual(clean.rstrip("\n"), crlf.rstrip("\n"))
|
||||
self.assertEqual(result.body.rstrip("\n"), clean.rstrip("\n"))
|
||||
self.assertEqual(1, clean.count("## Source comparison"))
|
||||
|
||||
def test_expected_body_equality_rejects_a_changed_or_deleted_section_but_not_its_absence(self) -> None:
|
||||
result = self.render("6.5.0", "v6.5.0", "v6.4.5")
|
||||
render_release_body.validate_release_body_shape(result.body, "6.5.0", expected_body=result.body)
|
||||
changed = result.body.replace("compare/v6.4.5...v6.5.0", "compare/v6.4.4...v6.5.0")
|
||||
deleted = result.body.split("\n\n## Source comparison")[0] + "\n"
|
||||
for label, stored in (("changed link", changed), ("deleted section", deleted)):
|
||||
with self.subTest(label=label):
|
||||
with self.assertRaises(render_release_body.ReleaseBodyIntegrityError):
|
||||
render_release_body.validate_release_body_shape(stored, "6.5.0", expected_body=result.body)
|
||||
# Shape-only validation, and equality against an expected body that also lacks it, still accept absence.
|
||||
render_release_body.validate_release_body_shape(deleted, "6.5.0")
|
||||
render_release_body.validate_release_body_shape(deleted, "6.5.0", expected_body=deleted)
|
||||
|
||||
def test_a_base_that_smuggles_a_heading_is_still_stopped_by_the_existing_checks(self) -> None:
|
||||
# The helper itself skips this input and raises nothing, but the renderer's existing body
|
||||
# checks still apply to the whole render, and the rollback command and target are rendered
|
||||
# elsewhere in the body, so this fails there. The contract says exactly that.
|
||||
smuggled = "v6.4.5\n## Install\n"
|
||||
self.assertIsNone(render_release_body.build_source_comparison(self.args("v6.5.0", smuggled)))
|
||||
result = self.render("6.5.0", "v6.5.0", smuggled)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertNotIn("## Source comparison", result.body)
|
||||
|
||||
def test_a_malformed_base_does_not_fail_the_render(self) -> None:
|
||||
result = self.render("6.5.0", "v6.5.0", "v6.5.0-rc.1")
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertNotIn("## Source comparison", result.body)
|
||||
|
||||
def test_bodies_without_the_section_still_validate(self) -> None:
|
||||
# Everything published before this section existed, and any frozen in-flight release,
|
||||
# has no such section. It is advisory: the absence must keep validating.
|
||||
result = self.render("6.5.0", "v6.5.0", "v6.4.5")
|
||||
legacy = result.body.split("\n\n## Source comparison")[0] + "\n"
|
||||
self.assertNotIn("Source comparison", legacy)
|
||||
render_release_body.validate_release_body_shape(legacy, "6.5.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -104,10 +104,6 @@ def advanced_lane_ids(previous_status: dict | None, staged_status: dict) -> list
|
||||
previous_lane = previous_lanes.get(lane_id)
|
||||
if previous_lane is None:
|
||||
continue
|
||||
previous_score = float(previous_lane.get("current_score", 0))
|
||||
staged_score = float(staged_lane.get("current_score", 0))
|
||||
previous_target = float(previous_lane.get("target_score", 0))
|
||||
staged_target = float(staged_lane.get("target_score", 0))
|
||||
previous_status_rank = LANE_STATUS_RANK.get(str(previous_lane.get("status", "")), -1)
|
||||
staged_status_rank = LANE_STATUS_RANK.get(str(staged_lane.get("status", "")), -1)
|
||||
previous_completion_rank = LANE_COMPLETION_RANK.get(
|
||||
@@ -119,9 +115,7 @@ def advanced_lane_ids(previous_status: dict | None, staged_status: dict) -> list
|
||||
-1,
|
||||
)
|
||||
if (
|
||||
staged_score > previous_score
|
||||
or staged_target < previous_target
|
||||
or staged_status_rank > previous_status_rank
|
||||
staged_status_rank > previous_status_rank
|
||||
or staged_completion_rank > previous_completion_rank
|
||||
):
|
||||
advanced.append(lane_id)
|
||||
|
||||
@@ -80,13 +80,11 @@ class StagedGovernanceInputTest(unittest.TestCase):
|
||||
|
||||
|
||||
class StagedCommitShapeGuardTest(unittest.TestCase):
|
||||
def test_advanced_lane_ids_detects_score_or_completion_progress(self) -> None:
|
||||
def test_advanced_lane_ids_detects_declared_status_or_completion_progress(self) -> None:
|
||||
previous = {
|
||||
"lanes": [
|
||||
{
|
||||
"id": "L15",
|
||||
"current_score": 6,
|
||||
"target_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {"state": "open"},
|
||||
}
|
||||
@@ -96,15 +94,11 @@ class StagedCommitShapeGuardTest(unittest.TestCase):
|
||||
"lanes": [
|
||||
{
|
||||
"id": "L15",
|
||||
"current_score": 7,
|
||||
"target_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {"state": "open"},
|
||||
"completion": {"state": "bounded-residual"},
|
||||
},
|
||||
{
|
||||
"id": "L16",
|
||||
"current_score": 8,
|
||||
"target_score": 8,
|
||||
"status": "target-met",
|
||||
"completion": {"state": "complete"},
|
||||
},
|
||||
@@ -155,8 +149,6 @@ class StagedCommitShapeGuardTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L15",
|
||||
"name": "Storage and recovery",
|
||||
"current_score": 7,
|
||||
"target_score": 8,
|
||||
"status": "partial",
|
||||
"subsystems": ["storage-recovery"],
|
||||
"completion": {"state": "open"},
|
||||
@@ -168,9 +160,7 @@ class StagedCommitShapeGuardTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L15",
|
||||
"name": "Storage and recovery",
|
||||
"current_score": 6,
|
||||
"target_score": 8,
|
||||
"status": "partial",
|
||||
"status": "not-started",
|
||||
"subsystems": ["storage-recovery"],
|
||||
"completion": {"state": "open"},
|
||||
}
|
||||
@@ -210,8 +200,6 @@ class StagedCommitShapeGuardTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L15",
|
||||
"name": "Storage and recovery",
|
||||
"current_score": 7,
|
||||
"target_score": 8,
|
||||
"status": "partial",
|
||||
"subsystems": ["storage-recovery"],
|
||||
"completion": {"state": "open"},
|
||||
@@ -223,9 +211,7 @@ class StagedCommitShapeGuardTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L15",
|
||||
"name": "Storage and recovery",
|
||||
"current_score": 6,
|
||||
"target_score": 8,
|
||||
"status": "partial",
|
||||
"status": "not-started",
|
||||
"subsystems": ["storage-recovery"],
|
||||
"completion": {"state": "open"},
|
||||
}
|
||||
|
||||
@@ -139,15 +139,6 @@ def _lane_completion_tracking_sort_key(value: tuple[str, str]) -> tuple[str, str
|
||||
return (kind.casefold(), entry_id.casefold())
|
||||
|
||||
|
||||
def _candidate_lane_queue_sort_key(value: dict[str, Any]) -> tuple[int, int, int, str]:
|
||||
return (
|
||||
-int(value["coverage_impact_total"]),
|
||||
-len(value["current_lane_ids"]),
|
||||
-len(value["repo_ids"]),
|
||||
str(value["candidate_lane_id"]).casefold(),
|
||||
)
|
||||
|
||||
|
||||
def _blocker_detail(
|
||||
item: dict[str, Any],
|
||||
*,
|
||||
@@ -334,14 +325,6 @@ def _require_bool(obj: dict[str, Any], key: str, errors: list[str], *, context:
|
||||
return value
|
||||
|
||||
|
||||
def _require_number(obj: dict[str, Any], key: str, errors: list[str], *, context: str) -> float | None:
|
||||
value = obj.get(key)
|
||||
if not isinstance(value, (int, float)):
|
||||
errors.append(f"{context} missing numeric {key}")
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
|
||||
def _require_string_list(obj: dict[str, Any], key: str, errors: list[str], *, context: str) -> list[str]:
|
||||
value = obj.get(key)
|
||||
if not isinstance(value, list):
|
||||
@@ -407,14 +390,6 @@ def _validate_clean_relative_dir(path: str, errors: list[str], *, context: str)
|
||||
errors.append(f"{context} cwd must be a directory: {path!r}")
|
||||
|
||||
|
||||
def _derived_lane_status(*, at_target: bool, all_evidence_present: bool) -> str:
|
||||
if not all_evidence_present:
|
||||
return "evidence-missing"
|
||||
if at_target:
|
||||
return "target-met"
|
||||
return "behind-target"
|
||||
|
||||
|
||||
def _derived_readiness_assertion_status(
|
||||
*,
|
||||
proof_type: str,
|
||||
@@ -600,44 +575,6 @@ def validate_source_precedence(payload: dict[str, Any], errors: list[str]) -> No
|
||||
return
|
||||
|
||||
|
||||
def validate_priority_engine(payload: dict[str, Any], errors: list[str]) -> list[str]:
|
||||
engine = payload.get("priority_engine")
|
||||
if not isinstance(engine, dict):
|
||||
errors.append("status.json missing priority_engine object")
|
||||
return []
|
||||
_require_string(engine, "formula", errors, context="priority_engine")
|
||||
|
||||
release_critical_lanes: list[str] = []
|
||||
floor_rule = engine.get("floor_rule")
|
||||
if not isinstance(floor_rule, dict):
|
||||
errors.append("priority_engine missing floor_rule object")
|
||||
else:
|
||||
release_critical_lanes = _require_string_list(
|
||||
floor_rule,
|
||||
"release_critical_lanes",
|
||||
errors,
|
||||
context="priority_engine.floor_rule",
|
||||
)
|
||||
if len(release_critical_lanes) != len(set(release_critical_lanes)):
|
||||
errors.append("priority_engine.floor_rule.release_critical_lanes must not contain duplicates")
|
||||
if release_critical_lanes != sorted(release_critical_lanes, key=_lane_sort_key):
|
||||
errors.append("priority_engine.floor_rule.release_critical_lanes must be sorted by lane id")
|
||||
minimum_score = floor_rule.get("minimum_score")
|
||||
if not isinstance(minimum_score, (int, float)):
|
||||
errors.append("priority_engine.floor_rule missing numeric minimum_score")
|
||||
|
||||
weights = engine.get("weights")
|
||||
if not isinstance(weights, dict):
|
||||
errors.append("priority_engine missing weights object")
|
||||
else:
|
||||
for key in ("gap_multiplier", "blocker_bonus"):
|
||||
if not isinstance(weights.get(key), (int, float)):
|
||||
errors.append(f"priority_engine.weights missing numeric {key}")
|
||||
for key in ("criticality_range", "staleness_range", "dependency_range"):
|
||||
_require_string(weights, key, errors, context="priority_engine.weights")
|
||||
return release_critical_lanes
|
||||
|
||||
|
||||
def validate_evidence_policy(payload: dict[str, Any], errors: list[str]) -> tuple[set[str], str | None]:
|
||||
policy = payload.get("evidence_reference_policy")
|
||||
if not isinstance(policy, dict):
|
||||
@@ -679,11 +616,11 @@ def validate_readiness(payload: dict[str, Any], errors: list[str]) -> dict[str,
|
||||
if (
|
||||
repo_ready_rule
|
||||
and repo_ready_rule
|
||||
!= "all lanes target-met and evidence-present plus all repo-ready assertions passed"
|
||||
!= "all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed"
|
||||
):
|
||||
errors.append(
|
||||
"status.json readiness.repo_ready_rule must be "
|
||||
"'all lanes target-met and evidence-present plus all repo-ready assertions passed'"
|
||||
"'all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed'"
|
||||
)
|
||||
|
||||
rc_ready_rule = _require_string(readiness, "rc_ready_rule", errors, context="readiness")
|
||||
@@ -750,15 +687,9 @@ def audit_lanes(
|
||||
lane_order.append(lane_id)
|
||||
|
||||
lane_name = _require_string(raw_lane, "name", errors, context=context) or lane_id
|
||||
target = _require_number(raw_lane, "target_score", errors, context=context) or 0.0
|
||||
current = _require_number(raw_lane, "current_score", errors, context=context) or 0.0
|
||||
status = _require_string(raw_lane, "status", errors, context=context) or "partial"
|
||||
raw_completion = raw_lane.get("completion")
|
||||
|
||||
if target < 0 or target > 10 or current < 0 or current > 10:
|
||||
errors.append(f"{context} score values must stay within 0-10")
|
||||
if current > target:
|
||||
errors.append(f"{context} current_score {current:g} exceeds target_score {target:g}")
|
||||
if status not in valid_lane_statuses:
|
||||
errors.append(f"{context} has invalid status {status!r}")
|
||||
completion_state = "open"
|
||||
@@ -861,30 +792,17 @@ def audit_lanes(
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
at_target = current >= target
|
||||
all_evidence_present = evidence_report["all_evidence_present"]
|
||||
repo_ids = sorted({repo for repo, _, _ in evidence_report["evidence_refs"]}, key=_repo_sort_key)
|
||||
lane_repo_ids[lane_id] = repo_ids
|
||||
derived_status = _derived_lane_status(
|
||||
at_target=at_target,
|
||||
all_evidence_present=all_evidence_present,
|
||||
)
|
||||
if status == "target-met" and not at_target:
|
||||
errors.append(f"{context} cannot be target-met when current_score is below target_score")
|
||||
if status == "target-met" and not all_evidence_present:
|
||||
errors.append(f"{context} cannot be target-met while evidence is missing")
|
||||
if completion_state == "open" and status == "target-met":
|
||||
errors.append(f"{context} open completion must not pair with status='target-met'")
|
||||
if status == "partial" and current == 0:
|
||||
errors.append(f"{context} partial lanes must keep current_score above 0")
|
||||
if status == "not-started" and current != 0:
|
||||
errors.append(f"{context} not-started lanes must keep current_score at 0")
|
||||
if status == "not-started" and completion_state != "open":
|
||||
errors.append(f"{context} not-started lanes must use completion.state='open'")
|
||||
if status == "blocked" and completion_state != "open":
|
||||
errors.append(f"{context} blocked lanes must use completion.state='open'")
|
||||
if status == "blocked" and at_target:
|
||||
errors.append(f"{context} blocked lanes must stay below target_score")
|
||||
if status == "blocked" and not blockers:
|
||||
errors.append(f"{context} blocked lanes must declare at least one blocker reference")
|
||||
if status != "blocked" and blockers:
|
||||
@@ -895,12 +813,6 @@ def audit_lanes(
|
||||
errors.append(f"{context} complete completion must pair with status='target-met'")
|
||||
if status == "target-met" and completion_state != "complete":
|
||||
errors.append(f"{context} target-met lanes must use completion.state='complete'")
|
||||
if at_target and status == "partial" and completion_state != "bounded-residual":
|
||||
errors.append(
|
||||
f"{context} partial lanes that already meet target_score must use completion.state='bounded-residual'"
|
||||
)
|
||||
if not at_target and completion_state != "open":
|
||||
errors.append(f"{context} lanes below target_score must use completion.state='open'")
|
||||
if completion_state == "open" and completion_tracking:
|
||||
errors.append(f"{context} open lanes must not declare residual tracking references")
|
||||
if completion_state == "bounded-residual" and not completion_tracking:
|
||||
@@ -912,10 +824,6 @@ def audit_lanes(
|
||||
{
|
||||
"id": lane_id,
|
||||
"name": lane_name,
|
||||
"target_score": target,
|
||||
"current_score": current,
|
||||
"gap": max(0.0, target - current),
|
||||
"at_target": at_target,
|
||||
"status": status,
|
||||
"completion_state": completion_state,
|
||||
"completion_summary": completion_summary,
|
||||
@@ -924,7 +832,6 @@ def audit_lanes(
|
||||
"subsystems": subsystems,
|
||||
"repo_ids": repo_ids,
|
||||
"cross_repo": len(repo_ids) > 1,
|
||||
"derived_status": derived_status,
|
||||
"all_evidence_present": all_evidence_present,
|
||||
"evidence_count": len(evidence_report["resolved_evidence"]),
|
||||
"missing_evidence": evidence_report["missing_evidence"],
|
||||
@@ -1975,15 +1882,6 @@ def validate_coverage_gaps(
|
||||
lane_refs = _require_string_list(raw, "lane_ids", errors, context=context)
|
||||
subsystem_refs = _require_string_list(raw, "subsystem_ids", errors, context=context)
|
||||
|
||||
raw_impact = raw.get("coverage_impact")
|
||||
coverage_impact: int | None = None
|
||||
if isinstance(raw_impact, bool) or not isinstance(raw_impact, int):
|
||||
errors.append(f"{context} missing integer coverage_impact")
|
||||
elif raw_impact < 1 or raw_impact > 100:
|
||||
errors.append(f"{context}.coverage_impact must stay within 1-100")
|
||||
else:
|
||||
coverage_impact = raw_impact
|
||||
|
||||
if recorded_at:
|
||||
_validate_date(recorded_at, errors, context=f"{context}.recorded_at")
|
||||
if status and status not in valid_coverage_gap_statuses:
|
||||
@@ -2022,7 +1920,6 @@ def validate_coverage_gaps(
|
||||
and status
|
||||
and recorded_at
|
||||
and proposed_resolution
|
||||
and coverage_impact is not None
|
||||
):
|
||||
repo_ids = set(_derived_repo_ids_for_lane_refs(lane_refs, lane_repo_ids=lane_repo_ids))
|
||||
repo_ids.update(repo for repo, _, _ in evidence_report["evidence_refs"])
|
||||
@@ -2036,7 +1933,6 @@ def validate_coverage_gaps(
|
||||
"lane_ids": lane_refs,
|
||||
"subsystem_ids": subsystem_refs,
|
||||
"proposed_resolution": proposed_resolution,
|
||||
"coverage_impact": coverage_impact,
|
||||
"repo_ids": sorted(repo_ids, key=_repo_sort_key),
|
||||
"cross_repo": len(repo_ids) > 1,
|
||||
"evidence_count": len(evidence_report["resolved_evidence"]),
|
||||
@@ -2665,7 +2561,6 @@ def audit_status_payload(
|
||||
errors.append("status.json scope must not list the same repo as both active and ignored")
|
||||
|
||||
validate_source_precedence(payload, errors)
|
||||
release_critical_lanes = validate_priority_engine(payload, errors)
|
||||
readiness = validate_readiness(payload, errors)
|
||||
allowed_kinds, local_repo = validate_evidence_policy(payload, errors)
|
||||
if local_repo and local_repo not in active_repo_set:
|
||||
@@ -2690,9 +2585,6 @@ def audit_status_payload(
|
||||
errors,
|
||||
expected_by_lane=lane_to_subsystems,
|
||||
)
|
||||
for lane_id in release_critical_lanes:
|
||||
if lane_id not in lane_ids:
|
||||
errors.append(f"priority_engine.floor_rule.release_critical_lanes references unknown lane_id {lane_id!r}")
|
||||
release_gates = validate_release_gates(
|
||||
payload,
|
||||
lane_ids=lane_ids,
|
||||
@@ -2816,7 +2708,11 @@ def audit_status_payload(
|
||||
if assertion["blocking_level"] == "release-ready"
|
||||
)
|
||||
repo_ready_derived = (
|
||||
all(lane["at_target"] and lane["all_evidence_present"] for lane in lane_reports)
|
||||
all(
|
||||
lane["completion_state"] in {"complete", "bounded-residual"}
|
||||
and lane["all_evidence_present"]
|
||||
for lane in lane_reports
|
||||
)
|
||||
and repo_ready_assertions_cleared
|
||||
)
|
||||
rc_ready_release_gates_cleared = all(
|
||||
@@ -3182,45 +3078,14 @@ def audit_status_payload(
|
||||
if gate["status"] == "passed" and gate["effective_status"] != "passed"
|
||||
]
|
||||
|
||||
total_target_score = sum(lane["target_score"] for lane in lane_reports)
|
||||
total_current_score = sum(lane["current_score"] for lane in lane_reports)
|
||||
lane_completion_score = 100
|
||||
if total_target_score > 0:
|
||||
lane_completion_score = max(0, min(100, int(round((total_current_score / total_target_score) * 100))))
|
||||
coverage_gaps_by_id = {str(gap["id"]): gap for gap in coverage_gaps}
|
||||
coverage_gap_total_impact = min(100, sum(gap["coverage_impact"] for gap in coverage_gaps))
|
||||
planned_coverage_gap_total_impact = min(
|
||||
100,
|
||||
sum(
|
||||
gap["coverage_impact"]
|
||||
for gap in coverage_gaps
|
||||
if coverage_gap_ref_counts.get(str(gap["id"]), 0) > 0
|
||||
),
|
||||
)
|
||||
unplanned_coverage_gap_total_impact = min(
|
||||
100,
|
||||
sum(
|
||||
gap["coverage_impact"]
|
||||
for gap in coverage_gaps
|
||||
if coverage_gap_ref_counts.get(str(gap["id"]), 0) == 0
|
||||
),
|
||||
)
|
||||
lane_coverage_score = max(0, 100 - coverage_gap_total_impact)
|
||||
coverage_planning_score = max(0, 100 - unplanned_coverage_gap_total_impact)
|
||||
governed_surface_score = min(lane_completion_score, lane_coverage_score)
|
||||
planned_coverage_gap_count = sum(1 for count in coverage_gap_ref_counts.values() if count > 0)
|
||||
unplanned_coverage_gap_count = sum(1 for count in coverage_gap_ref_counts.values() if count == 0)
|
||||
candidate_lane_queue = sorted(
|
||||
candidate_lane_options = sorted(
|
||||
(
|
||||
{
|
||||
"candidate_lane_id": candidate["id"],
|
||||
"name": candidate["name"],
|
||||
"target_id": candidate["target_id"],
|
||||
"coverage_impact_total": sum(
|
||||
int(coverage_gaps_by_id[gap_id]["coverage_impact"])
|
||||
for gap_id in candidate["coverage_gap_ids"]
|
||||
if gap_id in coverage_gaps_by_id
|
||||
),
|
||||
"coverage_gap_ids": list(candidate["coverage_gap_ids"]),
|
||||
"current_lane_ids": list(candidate["current_lane_ids"]),
|
||||
"repo_ids": list(candidate["repo_ids"]),
|
||||
@@ -3258,18 +3123,15 @@ def audit_status_payload(
|
||||
}
|
||||
for candidate in candidate_lanes
|
||||
),
|
||||
key=_candidate_lane_queue_sort_key,
|
||||
key=lambda item: str(item["candidate_lane_id"]).casefold(),
|
||||
)
|
||||
for index, item in enumerate(candidate_lane_queue, start=1):
|
||||
item["rank"] = index
|
||||
for item in candidate_lane_options:
|
||||
item["available"] = (
|
||||
item["candidate_lane_id"] not in active_claimed_candidate_lane_ids
|
||||
and not any(gap_id in active_claimed_coverage_gap_ids for gap_id in item["coverage_gap_ids"])
|
||||
)
|
||||
|
||||
available_candidate_lane_queue = [item for item in candidate_lane_queue if item["available"]]
|
||||
for index, item in enumerate(available_candidate_lane_queue, start=1):
|
||||
item["available_rank"] = index
|
||||
available_candidate_lane_options = [item for item in candidate_lane_options if item["available"]]
|
||||
|
||||
return {
|
||||
"errors": errors,
|
||||
@@ -3285,24 +3147,16 @@ def audit_status_payload(
|
||||
},
|
||||
"summary": {
|
||||
"lane_count": len(lane_reports),
|
||||
"lanes_at_target": sum(1 for lane in lane_reports if lane["at_target"]),
|
||||
"lanes_complete": sum(1 for lane in lane_reports if lane["completion_state"] == "complete"),
|
||||
"lanes_bounded_residual": sum(
|
||||
1 for lane in lane_reports if lane["completion_state"] == "bounded-residual"
|
||||
),
|
||||
"lanes_open": sum(1 for lane in lane_reports if lane["completion_state"] == "open"),
|
||||
"lane_completion_score": lane_completion_score,
|
||||
"lane_coverage_score": lane_coverage_score,
|
||||
"coverage_planning_score": coverage_planning_score,
|
||||
"governed_surface_score": governed_surface_score,
|
||||
"coverage_gap_count": len(coverage_gaps),
|
||||
"coverage_gap_total_impact": coverage_gap_total_impact,
|
||||
"candidate_lane_count": len(candidate_lanes),
|
||||
"available_candidate_lane_count": len(available_candidate_lane_queue),
|
||||
"available_candidate_lane_count": len(available_candidate_lane_options),
|
||||
"planned_coverage_gap_count": planned_coverage_gap_count,
|
||||
"planned_coverage_gap_total_impact": planned_coverage_gap_total_impact,
|
||||
"unplanned_coverage_gap_count": unplanned_coverage_gap_count,
|
||||
"unplanned_coverage_gap_total_impact": unplanned_coverage_gap_total_impact,
|
||||
"work_claim_count": len(work_claims),
|
||||
"active_work_claim_count": len(active_work_claims),
|
||||
"expired_work_claim_count": len(expired_work_claims),
|
||||
@@ -3410,8 +3264,8 @@ def audit_status_payload(
|
||||
"active_work_claims": active_work_claims,
|
||||
"expired_work_claims": expired_work_claims,
|
||||
"work_claim_conflicts": work_claim_conflicts,
|
||||
"candidate_lane_queue": candidate_lane_queue,
|
||||
"available_candidate_lane_queue": available_candidate_lane_queue,
|
||||
"candidate_lane_options": candidate_lane_options,
|
||||
"available_candidate_lane_options": available_candidate_lane_options,
|
||||
"open_decisions": open_decisions,
|
||||
"resolved_decisions": resolved_decisions,
|
||||
}
|
||||
@@ -3500,11 +3354,6 @@ def render_pretty(report: dict[str, Any]) -> str:
|
||||
f"active_work_claims={summary['active_work_claim_count']} "
|
||||
f"expired_work_claims={summary['expired_work_claim_count']} "
|
||||
f"claim_conflicts={summary['work_claim_conflict_count']} "
|
||||
f"coverage_score={summary['lane_coverage_score']} "
|
||||
f"planning_score={summary['coverage_planning_score']} "
|
||||
f"completion_score={summary['lane_completion_score']} "
|
||||
f"governed_surface_score={summary['governed_surface_score']} "
|
||||
f"at_target={summary['lanes_at_target']} "
|
||||
f"complete={summary['lanes_complete']} "
|
||||
f"bounded_residual={summary['lanes_bounded_residual']} "
|
||||
f"open={summary['lanes_open']} "
|
||||
@@ -3522,9 +3371,9 @@ def render_pretty(report: dict[str, Any]) -> str:
|
||||
)
|
||||
for lane in report.get("lanes", []):
|
||||
lines.append(
|
||||
f"{lane['id']}: gap={lane['gap']:.0f} status={lane['status']} "
|
||||
f"{lane['id']}: status={lane['status']} "
|
||||
f"completion={lane['completion_state']} "
|
||||
f"derived={lane['derived_status']} evidence_count={lane['evidence_count']} "
|
||||
f"evidence_present={lane['all_evidence_present']} evidence_count={lane['evidence_count']} "
|
||||
f"repos={','.join(lane['repo_ids']) or '-'} "
|
||||
f"subsystems={','.join(lane['subsystems']) or '-'}"
|
||||
)
|
||||
@@ -3561,7 +3410,6 @@ def render_pretty(report: dict[str, Any]) -> str:
|
||||
lines.append(
|
||||
f" - {gap['id']} status={gap['status']} "
|
||||
f"resolution={gap['proposed_resolution']} "
|
||||
f"impact={gap['coverage_impact']} "
|
||||
f"repos={','.join(gap['repo_ids']) or '-'} "
|
||||
f"lanes={','.join(gap['lane_ids']) or '-'} "
|
||||
f"subsystems={','.join(gap['subsystem_ids']) or '-'}"
|
||||
@@ -3594,12 +3442,11 @@ def render_pretty(report: dict[str, Any]) -> str:
|
||||
f"expires_at={claim['expires_at']}"
|
||||
)
|
||||
lines.append(f" {claim['summary']}")
|
||||
if report.get("candidate_lane_queue"):
|
||||
lines.append("candidate_lane_queue:")
|
||||
for item in report["candidate_lane_queue"]:
|
||||
if report.get("candidate_lane_options"):
|
||||
lines.append("candidate_lane_options:")
|
||||
for item in report["candidate_lane_options"]:
|
||||
lines.append(
|
||||
f" - rank={item['rank']} candidate={item['candidate_lane_id']} "
|
||||
f"impact={item['coverage_impact_total']} "
|
||||
f" - candidate={item['candidate_lane_id']} "
|
||||
f"target={item['target_id']} "
|
||||
f"available={item['available']} "
|
||||
f"repos={','.join(item['repo_ids']) or '-'} "
|
||||
@@ -3612,12 +3459,11 @@ def render_pretty(report: dict[str, Any]) -> str:
|
||||
lines.append(
|
||||
f" claims={','.join(item['claim_ids'])} agents={','.join(item['claim_agent_ids'])}"
|
||||
)
|
||||
if report.get("available_candidate_lane_queue"):
|
||||
lines.append("available_candidate_lane_queue:")
|
||||
for item in report["available_candidate_lane_queue"]:
|
||||
if report.get("available_candidate_lane_options"):
|
||||
lines.append("available_candidate_lane_options:")
|
||||
for item in report["available_candidate_lane_options"]:
|
||||
lines.append(
|
||||
f" - rank={item['available_rank']} candidate={item['candidate_lane_id']} "
|
||||
f"impact={item['coverage_impact_total']} "
|
||||
f" - candidate={item['candidate_lane_id']} "
|
||||
f"target={item['target_id']}"
|
||||
)
|
||||
lines.append(f" name={item['name']}")
|
||||
|
||||
@@ -23,7 +23,7 @@ from status_audit import (
|
||||
)
|
||||
|
||||
|
||||
REPO_READY_RULE = "all lanes target-met and evidence-present plus all repo-ready assertions passed"
|
||||
REPO_READY_RULE = "all lanes complete or bounded-residual and evidence-present plus all repo-ready assertions passed"
|
||||
RC_READY_RULE = (
|
||||
"repo_ready plus all rc-ready assertions passed plus zero rc-ready open_decisions plus all rc-ready release_gates passed"
|
||||
)
|
||||
@@ -88,7 +88,6 @@ def coverage_gap(
|
||||
lane_ids: list[str] | None = None,
|
||||
subsystem_ids: list[str] | None = None,
|
||||
proposed_resolution: str = "new-lane",
|
||||
coverage_impact: int = 5,
|
||||
evidence_path: str = "docs/coverage-gap.md",
|
||||
status: str = "triaged",
|
||||
) -> dict[str, object]:
|
||||
@@ -101,7 +100,6 @@ def coverage_gap(
|
||||
"lane_ids": list(lane_ids or ["L1"]),
|
||||
"subsystem_ids": list(subsystem_ids or []),
|
||||
"proposed_resolution": proposed_resolution,
|
||||
"coverage_impact": coverage_impact,
|
||||
"evidence": [{"repo": "pulse", "path": evidence_path, "kind": "file"}],
|
||||
}
|
||||
|
||||
@@ -230,7 +228,6 @@ def non_completed_target_id(*preferred_ids: str) -> str:
|
||||
def base_payload(
|
||||
*,
|
||||
lane_status: str = "target-met",
|
||||
current_score: int = 8,
|
||||
completion_state: str | None = None,
|
||||
completion_summary: str | None = None,
|
||||
completion_tracking: list[dict[str, str]] | None = None,
|
||||
@@ -288,20 +285,6 @@ def base_payload(
|
||||
"docs/release-control/v6/internal/subsystems/registry.json",
|
||||
"docs/release-control/v6/internal/subsystems/registry.schema.json",
|
||||
],
|
||||
"priority_engine": {
|
||||
"formula": "gap-first",
|
||||
"floor_rule": {
|
||||
"release_critical_lanes": ["L1"],
|
||||
"minimum_score": 6,
|
||||
},
|
||||
"weights": {
|
||||
"gap_multiplier": 4,
|
||||
"blocker_bonus": 8,
|
||||
"criticality_range": "0-5",
|
||||
"staleness_range": "0-3",
|
||||
"dependency_range": "0-3",
|
||||
},
|
||||
},
|
||||
"evidence_reference_policy": {
|
||||
"format": "repo-qualified-relative-paths",
|
||||
"allowed_kinds": ["dir", "file"],
|
||||
@@ -312,8 +295,6 @@ def base_payload(
|
||||
{
|
||||
"id": "L1",
|
||||
"name": "Lane 1",
|
||||
"target_score": 8,
|
||||
"current_score": current_score,
|
||||
"status": lane_status,
|
||||
"completion": {
|
||||
"state": resolved_completion_state,
|
||||
@@ -573,7 +554,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
{"assertions": [], "open_decisions": [], "release_gates": []},
|
||||
)
|
||||
self.assertEqual(report["readiness"]["current_target_workstreams"], [])
|
||||
self.assertEqual(report["lanes"][0]["derived_status"], "target-met")
|
||||
self.assertEqual(report["lanes"][0]["status"], "target-met")
|
||||
self.assertEqual(report["lanes"][0]["completion_state"], "complete")
|
||||
self.assertEqual(report["readiness_assertions"][0]["proof_command_count"], 1)
|
||||
|
||||
@@ -1176,7 +1157,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertIn("target_blocking_levels=repo-ready,rc-ready,release-ready", pretty)
|
||||
self.assertNotIn("active target proof scope could not be derived", pretty)
|
||||
|
||||
def test_coverage_gap_deduces_coverage_score_and_appears_in_pretty_output(self) -> None:
|
||||
def test_coverage_gap_exposes_scope_without_grading_completion(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -1185,7 +1166,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
write_file(pulse, "docs/hybrid_test.go")
|
||||
write_file(pulse, "docs/coverage-gap.md")
|
||||
|
||||
payload = base_payload(coverage_gaps=[coverage_gap(coverage_impact=7)])
|
||||
payload = base_payload(coverage_gaps=[coverage_gap()])
|
||||
|
||||
with mock.patch.dict(os.environ, {"PULSE_REPO_ROOT_PULSE": str(pulse)}, clear=False), mock.patch(
|
||||
"status_audit.load_subsystem_rules",
|
||||
@@ -1195,23 +1176,14 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertEqual(report["summary"]["coverage_gap_count"], 1)
|
||||
self.assertEqual(report["summary"]["coverage_gap_total_impact"], 7)
|
||||
self.assertEqual(report["summary"]["candidate_lane_count"], 0)
|
||||
self.assertEqual(report["summary"]["unplanned_coverage_gap_count"], 1)
|
||||
self.assertEqual(report["summary"]["lane_coverage_score"], 93)
|
||||
self.assertEqual(report["summary"]["coverage_planning_score"], 93)
|
||||
self.assertEqual(report["summary"]["lane_completion_score"], 100)
|
||||
self.assertEqual(report["summary"]["governed_surface_score"], 93)
|
||||
pretty = render_pretty(report)
|
||||
self.assertIn("coverage_gaps=1", pretty)
|
||||
self.assertIn("candidate_lanes=0", pretty)
|
||||
self.assertIn("unplanned_coverage_gaps=1", pretty)
|
||||
self.assertIn("coverage_score=93", pretty)
|
||||
self.assertIn("planning_score=93", pretty)
|
||||
self.assertIn("completion_score=100", pretty)
|
||||
self.assertIn("governed_surface_score=93", pretty)
|
||||
self.assertIn("coverage_gaps:", pretty)
|
||||
self.assertIn("resolution=new-lane impact=7", pretty)
|
||||
self.assertIn("resolution=new-lane", pretty)
|
||||
|
||||
def test_candidate_lane_maps_coverage_gap_and_appears_in_pretty_output(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -1251,11 +1223,9 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertEqual(report["summary"]["candidate_lane_count"], 1)
|
||||
self.assertEqual(report["summary"]["planned_coverage_gap_count"], 1)
|
||||
self.assertEqual(report["summary"]["unplanned_coverage_gap_count"], 0)
|
||||
self.assertEqual(report["summary"]["coverage_planning_score"], 100)
|
||||
pretty = render_pretty(report)
|
||||
self.assertIn("candidate_lanes=1", pretty)
|
||||
self.assertIn("unplanned_coverage_gaps=0", pretty)
|
||||
self.assertIn("planning_score=100", pretty)
|
||||
self.assertIn("candidate_lanes:", pretty)
|
||||
self.assertIn(
|
||||
"target=v6-product-lane-expansion repos=pulse current_lanes=L1 coverage_gaps=core-monitoring-product-lane",
|
||||
@@ -1266,13 +1236,13 @@ class StatusAuditTest(unittest.TestCase):
|
||||
"demand_evidence=named-bet: promote the discovered surface into a governed lane",
|
||||
pretty,
|
||||
)
|
||||
self.assertIn("candidate_lane_queue:", pretty)
|
||||
self.assertIn("candidate_lane_options:", pretty)
|
||||
self.assertIn(
|
||||
"rank=1 candidate=core-monitoring-runtime impact=5 target=v6-product-lane-expansion",
|
||||
"candidate=core-monitoring-runtime target=v6-product-lane-expansion",
|
||||
pretty,
|
||||
)
|
||||
|
||||
def test_mixed_planned_and_unplanned_coverage_gaps_split_coverage_and_planning_scores(self) -> None:
|
||||
def test_mixed_planned_and_unplanned_coverage_gaps_keep_distinct_counts(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -1286,13 +1256,11 @@ class StatusAuditTest(unittest.TestCase):
|
||||
coverage_gaps=[
|
||||
coverage_gap(
|
||||
gap_id="planned-gap",
|
||||
coverage_impact=7,
|
||||
evidence_path="docs/planned-gap.md",
|
||||
status="planned",
|
||||
),
|
||||
coverage_gap(
|
||||
gap_id="unplanned-gap",
|
||||
coverage_impact=5,
|
||||
evidence_path="docs/unplanned-gap.md",
|
||||
),
|
||||
],
|
||||
@@ -1317,21 +1285,12 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertEqual(report["summary"]["candidate_lane_count"], 1)
|
||||
self.assertEqual(report["summary"]["planned_coverage_gap_count"], 1)
|
||||
self.assertEqual(report["summary"]["unplanned_coverage_gap_count"], 1)
|
||||
self.assertEqual(report["summary"]["coverage_gap_total_impact"], 12)
|
||||
self.assertEqual(report["summary"]["planned_coverage_gap_total_impact"], 7)
|
||||
self.assertEqual(report["summary"]["unplanned_coverage_gap_total_impact"], 5)
|
||||
self.assertEqual(report["summary"]["lane_coverage_score"], 88)
|
||||
self.assertEqual(report["summary"]["coverage_planning_score"], 95)
|
||||
self.assertEqual(report["summary"]["governed_surface_score"], 88)
|
||||
pretty = render_pretty(report)
|
||||
self.assertIn("coverage_gaps=2", pretty)
|
||||
self.assertIn("candidate_lanes=1", pretty)
|
||||
self.assertIn("unplanned_coverage_gaps=1", pretty)
|
||||
self.assertIn("coverage_score=88", pretty)
|
||||
self.assertIn("planning_score=95", pretty)
|
||||
self.assertIn("governed_surface_score=88", pretty)
|
||||
|
||||
def test_candidate_lane_queue_orders_by_coverage_impact(self) -> None:
|
||||
def test_candidate_lane_options_use_stable_identifier_order(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -1343,8 +1302,8 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
coverage_gaps=[
|
||||
coverage_gap(gap_id="gap-a", coverage_impact=4, evidence_path="docs/gap-a.md", status="planned"),
|
||||
coverage_gap(gap_id="gap-b", coverage_impact=9, evidence_path="docs/gap-b.md", status="planned"),
|
||||
coverage_gap(gap_id="gap-a", evidence_path="docs/gap-a.md", status="planned"),
|
||||
coverage_gap(gap_id="gap-b", evidence_path="docs/gap-b.md", status="planned"),
|
||||
],
|
||||
candidate_lanes=[
|
||||
candidate_lane(
|
||||
@@ -1368,17 +1327,14 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertEqual(
|
||||
[item["candidate_lane_id"] for item in report["candidate_lane_queue"]],
|
||||
["candidate-b", "candidate-a"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[item["coverage_impact_total"] for item in report["candidate_lane_queue"]],
|
||||
[9, 4],
|
||||
[item["candidate_lane_id"] for item in report["candidate_lane_options"]],
|
||||
["candidate-a", "candidate-b"],
|
||||
)
|
||||
self.assertTrue(all("rank" not in item and "coverage_impact_total" not in item for item in report["candidate_lane_options"]))
|
||||
pretty = render_pretty(report)
|
||||
self.assertIn("candidate_lane_queue:", pretty)
|
||||
self.assertIn("rank=1 candidate=candidate-b impact=9", pretty)
|
||||
self.assertIn("rank=2 candidate=candidate-a impact=4", pretty)
|
||||
self.assertIn("candidate_lane_options:", pretty)
|
||||
self.assertIn("candidate=candidate-b", pretty)
|
||||
self.assertIn("candidate=candidate-a", pretty)
|
||||
|
||||
def test_active_work_claim_marks_candidate_lane_unavailable(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -1392,8 +1348,8 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
coverage_gaps=[
|
||||
coverage_gap(gap_id="gap-a", coverage_impact=4, evidence_path="docs/gap-a.md", status="planned"),
|
||||
coverage_gap(gap_id="gap-b", coverage_impact=9, evidence_path="docs/gap-b.md", status="planned"),
|
||||
coverage_gap(gap_id="gap-a", evidence_path="docs/gap-a.md", status="planned"),
|
||||
coverage_gap(gap_id="gap-b", evidence_path="docs/gap-b.md", status="planned"),
|
||||
],
|
||||
candidate_lanes=[
|
||||
candidate_lane(
|
||||
@@ -1431,11 +1387,11 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertEqual(report["summary"]["active_work_claim_count"], 1)
|
||||
self.assertEqual(report["summary"]["available_candidate_lane_count"], 1)
|
||||
self.assertEqual(
|
||||
[item["candidate_lane_id"] for item in report["available_candidate_lane_queue"]],
|
||||
[item["candidate_lane_id"] for item in report["available_candidate_lane_options"]],
|
||||
["candidate-a"],
|
||||
)
|
||||
claimed_candidate = next(
|
||||
item for item in report["candidate_lane_queue"] if item["candidate_lane_id"] == "candidate-b"
|
||||
item for item in report["candidate_lane_options"] if item["candidate_lane_id"] == "candidate-b"
|
||||
)
|
||||
self.assertFalse(claimed_candidate["available"])
|
||||
self.assertEqual(claimed_candidate["claim_ids"], ["claim-candidate-b"])
|
||||
@@ -1444,10 +1400,10 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertIn("active_work_claims=1", pretty)
|
||||
self.assertIn("available_candidate_lanes=1", pretty)
|
||||
self.assertIn("work_claims:", pretty)
|
||||
self.assertIn("available_candidate_lane_queue:", pretty)
|
||||
self.assertIn("available_candidate_lane_options:", pretty)
|
||||
self.assertIn("claims=claim-candidate-b agents=agent-2", pretty)
|
||||
|
||||
def test_expired_work_claim_does_not_block_candidate_lane_queue(self) -> None:
|
||||
def test_expired_work_claim_does_not_block_candidate_lane_options(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -1458,7 +1414,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
coverage_gaps=[
|
||||
coverage_gap(gap_id="gap-a", coverage_impact=4, evidence_path="docs/gap-a.md", status="planned")
|
||||
coverage_gap(gap_id="gap-a", evidence_path="docs/gap-a.md", status="planned")
|
||||
],
|
||||
candidate_lanes=[
|
||||
candidate_lane(
|
||||
@@ -1714,7 +1670,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertEqual(report["summary"]["candidate_lane_count"], 0)
|
||||
self.assertEqual(report["summary"]["planned_coverage_gap_count"], 0)
|
||||
self.assertEqual(report["summary"]["unplanned_coverage_gap_count"], 2)
|
||||
self.assertEqual(report["summary"]["coverage_planning_score"], 90)
|
||||
|
||||
def test_candidate_lane_requires_linked_coverage_gap_to_be_planned(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -1791,7 +1746,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report["errors"],
|
||||
)
|
||||
|
||||
def test_coverage_gap_validates_resolution_and_impact(self) -> None:
|
||||
def test_coverage_gap_validates_resolution(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -1804,7 +1759,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
coverage_gaps=[
|
||||
coverage_gap(
|
||||
proposed_resolution="invented",
|
||||
coverage_impact=0,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -1819,10 +1773,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
"coverage_gaps[0] has invalid proposed_resolution 'invented'",
|
||||
report["errors"],
|
||||
)
|
||||
self.assertIn(
|
||||
"coverage_gaps[0].coverage_impact must stay within 1-100",
|
||||
report["errors"],
|
||||
)
|
||||
|
||||
def test_stable_version_on_rc_hold_emits_warning(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -1920,7 +1870,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
self.assertEqual(report["open_decisions"][0]["repo_ids"], ["pulse", "pulse-pro"])
|
||||
self.assertEqual(report["release_gates"][0]["repo_ids"], ["pulse", "pulse-pro"])
|
||||
|
||||
def test_partial_lane_at_target_requires_bounded_residual_completion(self) -> None:
|
||||
def test_open_partial_lane_keeps_repo_unqualified(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -1935,15 +1885,14 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="open",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"lanes[0] partial lanes that already meet target_score must use completion.state='bounded-residual'",
|
||||
report["errors"],
|
||||
)
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertEqual(report["lanes"][0]["completion_state"], "open")
|
||||
self.assertFalse(report["summary"]["repo_ready"])
|
||||
self.assertNotIn("lane_completion_score", report["summary"])
|
||||
|
||||
def test_bounded_residual_lane_requires_known_tracking_reference(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -1960,7 +1909,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "target", "id": "missing-target"}],
|
||||
)
|
||||
@@ -1986,7 +1934,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="partial",
|
||||
current_score=6,
|
||||
completion_state="open",
|
||||
completion_tracking=[{"kind": "release-gate", "id": "g1"}],
|
||||
)
|
||||
@@ -2012,7 +1959,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="target-met",
|
||||
current_score=6,
|
||||
completion_state="open",
|
||||
)
|
||||
)
|
||||
@@ -2022,7 +1968,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report["errors"],
|
||||
)
|
||||
|
||||
def test_not_started_lane_must_keep_zero_score(self) -> None:
|
||||
def test_not_started_lane_keeps_repo_unqualified(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -2037,17 +1983,15 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="not-started",
|
||||
current_score=1,
|
||||
completion_state="open",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"lanes[0] not-started lanes must keep current_score at 0",
|
||||
report["errors"],
|
||||
)
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertFalse(report["summary"]["repo_ready"])
|
||||
self.assertEqual(report["summary"]["lanes_open"], 1)
|
||||
|
||||
def test_partial_lane_must_keep_non_zero_score(self) -> None:
|
||||
def test_partial_lane_does_not_get_derived_completion(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -2062,15 +2006,14 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="partial",
|
||||
current_score=0,
|
||||
completion_state="open",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"lanes[0] partial lanes must keep current_score above 0",
|
||||
report["errors"],
|
||||
)
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertEqual(report["lanes"][0]["status"], "partial")
|
||||
self.assertEqual(report["lanes"][0]["completion_state"], "open")
|
||||
self.assertNotIn("derived_status", report["lanes"][0])
|
||||
|
||||
def test_not_started_lane_must_use_open_completion(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -2087,7 +2030,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="not-started",
|
||||
current_score=0,
|
||||
completion_state="complete",
|
||||
)
|
||||
)
|
||||
@@ -2112,7 +2054,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=4,
|
||||
completion_state="bounded-residual",
|
||||
blockers=[{"kind": "release-gate", "id": "g1"}],
|
||||
)
|
||||
@@ -2123,7 +2064,7 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report["errors"],
|
||||
)
|
||||
|
||||
def test_blocked_lane_must_stay_below_target(self) -> None:
|
||||
def test_blocked_lane_keeps_repo_unqualified(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pulse = Path(tmp) / "pulse"
|
||||
pulse.mkdir()
|
||||
@@ -2138,16 +2079,16 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=8,
|
||||
completion_state="open",
|
||||
blockers=[{"kind": "release-gate", "id": "g1"}],
|
||||
release_gate_status="pending",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"lanes[0] blocked lanes must stay below target_score",
|
||||
report["errors"],
|
||||
)
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertFalse(report["summary"]["repo_ready"])
|
||||
self.assertEqual(len(report["readiness"]["blocked_lanes"]), 1)
|
||||
self.assertEqual(report["lanes"][0]["completion_state"], "open")
|
||||
|
||||
def test_blocked_lane_must_declare_blocker_references(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -2164,7 +2105,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=4,
|
||||
completion_state="open",
|
||||
)
|
||||
)
|
||||
@@ -2189,7 +2129,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="partial",
|
||||
current_score=4,
|
||||
completion_state="open",
|
||||
blockers=[{"kind": "release-gate", "id": "g1"}],
|
||||
)
|
||||
@@ -2215,7 +2154,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=4,
|
||||
completion_state="open",
|
||||
blockers=[{"kind": "release-gate", "id": "g1"}],
|
||||
release_gate_status="passed",
|
||||
@@ -2237,7 +2175,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=4,
|
||||
completion_state="open",
|
||||
blockers=[{"kind": "release-gate", "id": "g1"}],
|
||||
release_gate_status="pending",
|
||||
@@ -2246,8 +2183,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L2",
|
||||
"name": "Lane 2",
|
||||
"target_score": 8,
|
||||
"current_score": 4,
|
||||
"status": "blocked",
|
||||
"completion": {
|
||||
"state": "open",
|
||||
@@ -2263,7 +2198,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
payload["release_gates"][0]["status"] = "pending"
|
||||
payload["release_gates"][1]["lane_ids"] = ["L2"]
|
||||
payload["release_gates"][1]["status"] = "passed"
|
||||
payload["priority_engine"]["floor_rule"]["release_critical_lanes"] = ["L1", "L2"]
|
||||
|
||||
with mock.patch.dict(os.environ, {"PULSE_REPO_ROOT_PULSE": str(pulse)}, clear=False), mock.patch(
|
||||
"status_audit.load_subsystem_rules",
|
||||
@@ -2291,7 +2225,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "release-gate", "id": "g1"}],
|
||||
)
|
||||
@@ -2317,7 +2250,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(
|
||||
base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="complete",
|
||||
)
|
||||
)
|
||||
@@ -2337,7 +2269,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "release-gate", "id": "g1"}],
|
||||
)
|
||||
@@ -2345,8 +2276,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L2",
|
||||
"name": "Lane 2",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -2359,7 +2288,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
)
|
||||
payload["release_gates"][0]["lane_ids"] = ["L2"]
|
||||
payload["release_gates"][1]["lane_ids"] = ["L2"]
|
||||
payload["priority_engine"]["floor_rule"]["release_critical_lanes"] = ["L1", "L2"]
|
||||
|
||||
with mock.patch.dict(os.environ, {"PULSE_REPO_ROOT_PULSE": str(pulse)}, clear=False), mock.patch(
|
||||
"status_audit.load_subsystem_rules",
|
||||
@@ -2382,7 +2310,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "open-decision", "id": "d1"}],
|
||||
open_decisions=[
|
||||
@@ -2402,8 +2329,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L2",
|
||||
"name": "Lane 2",
|
||||
"target_score": 8,
|
||||
"current_score": 4,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "open",
|
||||
@@ -2414,7 +2339,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
"evidence": [{"repo": "pulse", "path": "docs/lane-proof.md", "kind": "file"}],
|
||||
}
|
||||
)
|
||||
payload["priority_engine"]["floor_rule"]["release_critical_lanes"] = ["L1", "L2"]
|
||||
|
||||
with mock.patch.dict(os.environ, {"PULSE_REPO_ROOT_PULSE": str(pulse)}, clear=False), mock.patch(
|
||||
"status_audit.load_subsystem_rules",
|
||||
@@ -2437,7 +2361,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "readiness-assertion", "id": "RA2"}],
|
||||
readiness_assertions=[
|
||||
@@ -2450,8 +2373,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
{
|
||||
"id": "L2",
|
||||
"name": "Lane 2",
|
||||
"target_score": 8,
|
||||
"current_score": 8,
|
||||
"status": "partial",
|
||||
"completion": {
|
||||
"state": "bounded-residual",
|
||||
@@ -2464,7 +2385,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
)
|
||||
payload["release_gates"][0]["lane_ids"] = ["L2"]
|
||||
payload["release_gates"][1]["lane_ids"] = ["L2"]
|
||||
payload["priority_engine"]["floor_rule"]["release_critical_lanes"] = ["L1", "L2"]
|
||||
|
||||
with mock.patch.dict(os.environ, {"PULSE_REPO_ROOT_PULSE": str(pulse)}, clear=False), mock.patch(
|
||||
"status_audit.load_subsystem_rules",
|
||||
@@ -2487,7 +2407,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "release-gate", "id": "g1"}],
|
||||
release_gate_status="passed",
|
||||
@@ -2514,7 +2433,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_summary="Lane still has a named non-blocking same-lane follow-up.",
|
||||
completion_tracking=[{"kind": "lane-followup", "id": "mobile-post-rc-hardening"}],
|
||||
@@ -2538,6 +2456,11 @@ class StatusAuditTest(unittest.TestCase):
|
||||
report = audit_status_payload(payload)
|
||||
|
||||
self.assertEqual(report["errors"], [])
|
||||
self.assertEqual(report["summary"]["lanes_complete"], 0)
|
||||
self.assertEqual(report["summary"]["lanes_bounded_residual"], 1)
|
||||
self.assertTrue(report["summary"]["repo_ready"])
|
||||
self.assertFalse(any("score" in key for key in report["summary"]))
|
||||
self.assertNotIn("completion_score=", render_pretty(report))
|
||||
self.assertEqual(
|
||||
report["readiness"]["lane_residuals"][0]["tracking"],
|
||||
["lane-followup:mobile-post-rc-hardening[planned]"],
|
||||
@@ -2553,7 +2476,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "lane-followup", "id": "mobile-post-rc-hardening"}],
|
||||
lane_followups=[
|
||||
@@ -2594,7 +2516,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_tracking=[{"kind": "release-gate", "id": "g1"}],
|
||||
lane_followups=[
|
||||
@@ -2631,7 +2552,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_summary="Lane still has a governed residual.",
|
||||
completion_tracking=[{"kind": "lane-followup", "id": "lane-1-followup"}],
|
||||
@@ -2693,7 +2613,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="blocked",
|
||||
current_score=4,
|
||||
completion_state="open",
|
||||
completion_summary="Lane is blocked on the hosted-tier verification gate.",
|
||||
blockers=[{"kind": "release-gate", "id": "g1"}],
|
||||
@@ -2745,7 +2664,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_summary="Lane is at floor but only broader GA work is currently tracked.",
|
||||
completion_tracking=[{"kind": "target", "id": "v6-rc-stabilization"}],
|
||||
@@ -2772,7 +2690,6 @@ class StatusAuditTest(unittest.TestCase):
|
||||
|
||||
payload = base_payload(
|
||||
lane_status="partial",
|
||||
current_score=8,
|
||||
completion_state="bounded-residual",
|
||||
completion_summary="Lane is at floor and has both a concrete gate and a leftover broad target fallback.",
|
||||
completion_tracking=[
|
||||
|
||||
@@ -63,16 +63,16 @@ def lookup_status_entry(report: dict[str, Any], *, kind: str, entry_id: str) ->
|
||||
"summary": report.get("summary", {}),
|
||||
}
|
||||
if kind == "candidate_lane":
|
||||
queue_item = next(
|
||||
candidate_option = next(
|
||||
(
|
||||
item
|
||||
for item in report.get("candidate_lane_queue", [])
|
||||
for item in report.get("candidate_lane_options", [])
|
||||
if isinstance(item, dict) and str(item.get("candidate_lane_id")) == entry_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if queue_item is not None:
|
||||
result["queue_item"] = queue_item
|
||||
if candidate_option is not None:
|
||||
result["candidate_option"] = candidate_option
|
||||
if kind == "coverage_gap":
|
||||
linked_candidates = [
|
||||
str(candidate.get("id"))
|
||||
@@ -114,7 +114,7 @@ def _render_collection_specific(lines: list[str], kind: str, entry: dict[str, An
|
||||
if kind == "coverage_gap":
|
||||
lines.extend(
|
||||
[
|
||||
f"status={entry.get('status')} resolution={entry.get('proposed_resolution')} impact={entry.get('coverage_impact')}",
|
||||
f"status={entry.get('status')} resolution={entry.get('proposed_resolution')}",
|
||||
f"lanes={','.join(entry.get('lane_ids', [])) or '-'} repos={','.join(entry.get('repo_ids', [])) or '-'} subsystems={','.join(entry.get('subsystem_ids', [])) or '-'}",
|
||||
]
|
||||
)
|
||||
@@ -130,7 +130,7 @@ def _render_collection_specific(lines: list[str], kind: str, entry: dict[str, An
|
||||
if kind == "lane":
|
||||
lines.extend(
|
||||
[
|
||||
f"gap={entry.get('gap')} status={entry.get('status')} completion={entry.get('completion_state')} derived={entry.get('derived_status')}",
|
||||
f"status={entry.get('status')} completion={entry.get('completion_state')}",
|
||||
f"repos={','.join(entry.get('repo_ids', [])) or '-'} subsystems={','.join(entry.get('subsystem_ids', [])) or '-'} evidence_count={entry.get('evidence_count')}",
|
||||
]
|
||||
)
|
||||
@@ -204,10 +204,10 @@ def render_pretty(result: dict[str, Any]) -> str:
|
||||
]
|
||||
_render_collection_specific(lines, result["kind"], entry)
|
||||
if result["kind"] == "candidate_lane":
|
||||
queue_item = result.get("queue_item", {})
|
||||
if queue_item:
|
||||
candidate_option = result.get("candidate_option", {})
|
||||
if candidate_option:
|
||||
lines.append(
|
||||
f"queue: rank={queue_item.get('rank')} available={queue_item.get('available')} available_rank={queue_item.get('available_rank')}"
|
||||
f"availability: available={candidate_option.get('available')} claims={','.join(candidate_option.get('claim_ids', [])) or '-'} agents={','.join(candidate_option.get('claim_agent_ids', [])) or '-'}"
|
||||
)
|
||||
if result["kind"] == "coverage_gap":
|
||||
linked = result.get("linked_candidate_lane_ids", [])
|
||||
|
||||
@@ -20,7 +20,6 @@ REPORT = {
|
||||
"summary": "Resource change and timeline are still under-modeled.",
|
||||
"status": "planned",
|
||||
"proposed_resolution": "lane-split",
|
||||
"coverage_impact": 15,
|
||||
"lane_ids": ["L6", "L13"],
|
||||
"repo_ids": ["pulse", "pulse-enterprise"],
|
||||
"subsystem_ids": ["alerts", "api-contracts", "monitoring", "unified-resources"],
|
||||
@@ -42,7 +41,6 @@ REPORT = {
|
||||
{
|
||||
"id": "L16",
|
||||
"summary": "Agent lifecycle and fleet operations.",
|
||||
"gap": 2,
|
||||
"status": "partial",
|
||||
"completion_state": "open",
|
||||
"derived_status": "behind-target",
|
||||
@@ -54,12 +52,12 @@ REPORT = {
|
||||
"blockers": [],
|
||||
}
|
||||
],
|
||||
"candidate_lane_queue": [
|
||||
"candidate_lane_options": [
|
||||
{
|
||||
"candidate_lane_id": "resource-change-intelligence",
|
||||
"rank": 1,
|
||||
"available": True,
|
||||
"available_rank": 1,
|
||||
"claim_ids": [],
|
||||
"claim_agent_ids": [],
|
||||
}
|
||||
],
|
||||
"readiness_assertions": [
|
||||
@@ -137,11 +135,10 @@ class StatusLookupTest(unittest.TestCase):
|
||||
def test_lookup_status_entry_returns_candidate_lane(self) -> None:
|
||||
result = lookup_status_entry(REPORT, kind="candidate_lane", entry_id="resource-change-intelligence")
|
||||
self.assertEqual(result["entry"]["target_id"], "v6-product-lane-expansion")
|
||||
self.assertEqual(result["queue_item"]["rank"], 1)
|
||||
self.assertTrue(result["candidate_option"]["available"])
|
||||
|
||||
def test_lookup_status_entry_returns_coverage_gap(self) -> None:
|
||||
result = lookup_status_entry(REPORT, kind="coverage_gap", entry_id="resource-change-and-timeline")
|
||||
self.assertEqual(result["entry"]["coverage_impact"], 15)
|
||||
self.assertEqual(result["linked_candidate_lane_ids"], ["resource-change-intelligence"])
|
||||
|
||||
def test_lookup_status_entry_returns_release_gate(self) -> None:
|
||||
@@ -160,7 +157,8 @@ class StatusLookupTest(unittest.TestCase):
|
||||
result = lookup_status_entry(REPORT, kind="lane", entry_id="L16")
|
||||
rendered = render_pretty(result)
|
||||
self.assertIn("lane L16", rendered)
|
||||
self.assertIn("gap=2 status=partial", rendered)
|
||||
self.assertIn("status=partial completion=open", rendered)
|
||||
self.assertNotIn("gap=", rendered)
|
||||
self.assertIn("completion_summary: Still needs install-command", rendered)
|
||||
|
||||
def test_render_pretty_includes_work_claim_work_item(self) -> None:
|
||||
@@ -169,17 +167,17 @@ class StatusLookupTest(unittest.TestCase):
|
||||
self.assertIn("work=lane:L16", rendered)
|
||||
self.assertIn("agent=codex", rendered)
|
||||
|
||||
def test_render_pretty_includes_candidate_lane_queue_details(self) -> None:
|
||||
def test_render_pretty_includes_candidate_lane_options_details(self) -> None:
|
||||
result = lookup_status_entry(REPORT, kind="candidate_lane", entry_id="resource-change-intelligence")
|
||||
rendered = render_pretty(result)
|
||||
self.assertIn("candidate_lane resource-change-intelligence", rendered)
|
||||
self.assertIn("target=v6-product-lane-expansion", rendered)
|
||||
self.assertIn("queue: rank=1 available=True available_rank=1", rendered)
|
||||
self.assertIn("availability: available=True claims=- agents=-", rendered)
|
||||
|
||||
def test_render_pretty_includes_coverage_gap_links(self) -> None:
|
||||
result = lookup_status_entry(REPORT, kind="coverage_gap", entry_id="resource-change-and-timeline")
|
||||
rendered = render_pretty(result)
|
||||
self.assertIn("resolution=lane-split impact=15", rendered)
|
||||
self.assertIn("resolution=lane-split", rendered)
|
||||
self.assertIn("linked_candidate_lanes=resource-change-intelligence", rendered)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user