Skip to content

pv shapes: the LONE-contract venue declines with no report — by_shape and declines exist only on the directory path #3648

Closed

Description

@noahgift

Found by a quorum lane re-reviewing #3622 (#3610 arm 1). Not a regression — an asymmetry #3622 made visible by fixing one half of it.

The two venues answer differently

decline() asks the vacuity question globally:

// crates/aprender-contracts/src/lint/shapes_gate.rs:349
if report.focus_nodes_n == 0 {
    return Some(ShapesOutcome::NoFocus { shapes_n });
}

and that returns before the result path builds by_shape, declines and armed_shapes. NoFocus then renders through skipped_gate("shapes", …) with Vec::new() findings and no extra (lint/mod.rs:772).

venue verdict exit report
directory with an armed zero-focus shape (fixed in #3622) Unknown(NoFocus) 2 full — by_shape naming the shape, declines, armed_shapes
lone contract with zero focus nodes Unknown(NoFocus) 2 none — a bare decline line

Why it matters, and why it is P1 and not P0

The report requirement is not decoration. From #3622's own test file:

"Downstream consumers (infra's SLK gate) capture stdout and parse it REGARDLESS of exit code, because pv already exits non-zero on Fail. A refusal that printed only a bare decline: line would read to them as 'no by_shape' and score UNMEASURED — indistinguishable, from their side, from a broken pv."

That is precisely what the lone venue does today.

It is P1 because the consumer that motivated the requirement runs pv over a DIRECTORY, which is the venue that now carries the report. A lone-file invocation is a developer running pv lint <file>, where the bare decline is legible on a terminal. So this is a real inconsistency with a bounded blast radius, not a live scoring defect.

done_when

  1. The lone venue emits the same evidence document as the directory venue: by_shape, declines, and a verdict of Unknown(NoFocus), on the exit-2 path.
  2. This requires building the report before decline() is consulted, or threading it through ShapesOutcome — a restructure of the outcome path, which is why it was not folded into #3610: a shapes gate reported Pass over ZERO focus nodes — the vacuity check was global, so a directory hid it #3622.
  3. shapes_vacuity.rs::the_lone_contract_venue_declines_without_a_report_and_that_is_recorded currently asserts r.extra().is_null() and carries an instruction in its own failure message: when this lands, delete that test and add json-missing-ref to the_three_answers_remain_distinct's report loop — do not loosen the assertion to keep it green.
  4. Falsifier both ways: the lone venue gains a parseable report, and the directory venue keeps hers.

How it was hidden

#3622's tests asserted the report requirement for the directory venue and not for the lone one — the_lone_contract_venue_still_declines_as_it_always_did checked only code != 0, and the_three_answers_remain_distinct omitted json-missing-ref from the loop that asserts a parseable report. Two tests that together covered one venue's requirement and quietly not the other's: the same half-checked shape #3610 was about, inside the fix for it.

Refs #3610, #3622

Activity

  1. added this to the 0.71.0 milestone on Sep 23, 2026
  2. noahgift commented on Sep 23, 2026

    @noahgift
    MemberAuthor

    milestone → 0.71.0 per docs/roadmaps/milestone-triage-2026-09-23.md (#4024), operator approval 2026-09-23 (relayed by aprender-cf). Reason: debt ratchet pillars B/C (#3997: pv at the deepest level, ontology merge), slice 2

  3. noahgift commented on Sep 24, 2026

    @noahgift
    MemberAuthor

    Closed by paiml-implement — folded into epic #2556 (triage evidence/triage-1623.md)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priority

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions