Skip to content

pv: a shapes gate reports Pass AND armed over ZERO focus nodes — aggregate swallows decline: NoFocus #3610

Closed as not planned
1 of 1 issue completed

Description

@noahgift

P0 — a truth defect in the instrument. Measured on lambda-labs 2026-09-20 with pv 0.68.2.

pv lint --gate shapes reports verdict: Pass for a contract whose entity is type: jsonl, grading zero focus nodes. Nothing in the output says so, so a shape over a jsonl entity is decoration that reads as coverage.

Reproduction, exactly as planted

contracts/slk-inbound-v1.yaml declares:

- {path: slk:outcome, pattern: "^(answered:(status|findings|prs|fleet|red|ask)|refused:...)$"}

Plant a row the pattern forbids into the jsonl entity:

{"schema":"slk/inbound-v1","kind":"inbound","channel":"C0C37U9HWMP","user":"U1","ts":"1.0","text":"x","outcome":"answered:do"}
entity type verdict
type: jsonl Pass
same row, type: json Fail — ont:slk/outcome: "answered:do" does not match /^(answered:...)$/

focus_nodes_n stayed at 8 with four rows in the file; converting to type: json took it to 10.

Two arms

1. Vacuity refusal. Any contract whose target resolves to 0 focus nodes renders Unknown{NoFocusNodes} (exit 2), never Pass. A gate that cannot fail is not a gate — this is the fleet's signature defect inside the instrument that exists to catch it. Fixture: the jsonl case above.

2. jsonl extractor. One row per line = one focus node.

Until arm 2 lands, the workaround is one representative json entity under shape with the ledger enforced outside pv — and the contracts carry unmeasured: [jsonl-rows] so the limit is in the artifact rather than in someone's memory.

Arm 1 is the one that matters: with it, the workaround is unnecessary because the gate would have said so on day one.

Activity

  1. noahgift commented on Sep 20, 2026

    @noahgift
    MemberAuthor

    Correction and a sharper measurement (2026-09-20, pv 0.68.2)

    My original report said a type: jsonl entity silently passes. That was measured over a whole contracts/ directory and it was the wrong description of the mechanism. Measured again, in isolation and in aggregate:

    A single declining contract is LOUD. pv lint contracts --gate shapes over a directory holding only the jsonl contract prints exactly:

    decline: NoFocus
    

    That is the right failure, and I was wrong to call it silent.

    The defect is in the AGGREGATE. Put that same contract in a directory beside one passing contract and the declination disappears — not into a warning, into a claim of the opposite:

    $ ls contracts/            # good.yaml (type: json, 1 valid row) + bad.yaml (type: jsonl, 2 INVALID rows)
    $ cat fx/rows.jsonl
    {"outcome":"answered:do"}
    {"outcome":"answered:do"}     # both violate bad.yaml's own sh:pattern ^(answered:status)$
    
    $ pv lint contracts --gate shapes ; echo EXIT=$?
      "verdict": "Pass",
      "passed": true,
        "shapes_n": 2,
        "focus_nodes_n": 1,
        "violations": 0,
        "by_shape": ["bad=0", "good=1"],
        "armed_shapes": ["bad", "good"],
        "not_armed_shapes": [],
    EXIT=0
    

    So, precisely:

    1. bad graded zero focus nodes and is nonetheless listed in armed_shapes, with not_armed_shapes: []. A shape that graded nothing is reported as armed — that field is the tool's own claim about what it measured, and this case falsifies it.
    2. The per-contract decline: NoFocus does not reach the aggregate at all: no declines count, no warnings, no non-zero exit.
    3. Two rows violating the contract's own pattern produce violations: 0 and verdict: Pass.

    Why this is the P0 rather than the jsonl extractor. A jsonl extractor is a feature; a shapes gate that reports Pass and armed over zero focus nodes is a gate that cannot fail, and it is indistinguishable from one that passed for a good reason. Our repo's name for this shape is "0 violations over 0 files" — it has caused four stale inventories and a fleet-wide no-op tag here.

    Suggested acceptance, in order of value:

    • armed_shapes must not contain a shape with zero focus nodes; such a shape belongs in not_armed_shapes (which today is empty in exactly the case it exists for).
    • The aggregate must carry the declination: a declines: [{contract, reason}] list, and --gate shapes should refuse (non-zero) when any contract declined, or at minimum when focus_nodes_n == 0 for an armed shape.
    • Then the jsonl extractor, which makes the declination unnecessary for the common case.

    The original arm 2 (a jsonl extractor) stands. Arm 1 is restated above: it is an aggregate vacuity defect, not a silent-pass-per-contract one.

    Reproduction directory content is inline above; pv 0.68.2, lambda-labs, 2026-09-20.

  2. changed the title [-]pv: a shapes gate that reports Pass over ZERO focus nodes cannot fail — vacuity refusal + jsonl extractor[/-] [+]pv: a shapes gate reports Pass AND armed over ZERO focus nodes — aggregate swallows decline: NoFocus[/+] on Sep 20, 2026
  3. added this to the 0.69.0 milestone on Sep 20, 2026
  4. noahgift commented on Sep 20, 2026

    @noahgift
    MemberAuthor

    Arm 1 is built as #3622 (draft). Two findings from the reproduction that belong on this issue rather than only in the PR.

    1. The gate was carrying the evidence of its own vacuity in a field nothing read.

    by_shape has always emitted <shape>=<focus-count>. On the reproduction fixture it printed empty-shape=0 — alongside verdict: Pass and armed_shapes: ['empty-shape', 'tool-status']. The number that falsifies the claim was in the same JSON object as the claim, computed every run, and nothing consumed it for the one question it answers: did this shape grade anything at all?

    That is the computed but unexposed class (#3597) — and this is the first instance of it inside the gate that catches the class. It is the argument for running the audit pass on the auditors: a tool that reports what it measured can carry the refutation of its own summary in an adjacent field, and no amount of reading the summary will find it.

    2. The root cause is one word, and the shape generalises.

    if report.focus_nodes_n == 0 { return Some(ShapesOutcome::NoFocus { shapes_n }); }

    The vacuity question was asked globally. That is correct for the singular case and silently wrong for the plural: a lone contract is the empty case and declines properly; a directory is the full case and passes over the same hole, because the other shapes carry the total above zero.

    It is the placeholder-is-most-permissive shape rotated ninety degrees — the same inversion as #3609, where a GGUF with no vocabulary gets a synthetic <unk> and works while one with a real vocabulary lacking <unk> is refused. The better-specified input is the one that fails, and the degenerate input is the one that passes. Worth grepping for: any aggregate predicate over a collection that tests the total rather than each member.

    Arm 2 (the jsonl extractor) is not in #3622 and is stated as excluded there.

  5. added 2 commits that reference this issue on Sep 20, 2026
  6. noahgift commented on Sep 20, 2026

    @noahgift
    MemberAuthor

    Two more aggregate-swallowed refusals, same day, both found only by planting

    Both against pv 0.68.2, both in paiml/infra while writing contracts/slk-command-v1.yaml, both the shape of this issue: a per-contract refusal that the directory run turns into a silent absence.

    (1) A YAML quirk in one field → decline: NoShapes for the whole contract, no field named

    The contract had:

        test: cd arbiter && cargo test -p arbiter --lib command::

    The trailing :: makes YAML read the value as a mapping, and pv responds to the whole contract with:

    $ pv lint contracts --gate shapes        # directory holding only this contract
    decline: NoShapes
    

    No field, no line, no "could not parse falsification_tests[0].test". Bisecting took eleven runs. Quoting the value fixes it. The message should name the path that failed to parse; "NoShapes" for a contract with a 60-line shape: block is the wrong sentence.

    (2) A nested-key error is swallowed by the directory run

    Alone, the contract said what was wrong, precisely:

    error: extract:json slk-command-v1: nested key `flags` is not in vocabulary.nested — name its class or drop it
    

    That is a good error. But in the directory run:

    "verdict": "Pass", "armed_shapes": [ …13 shapes… ], "violations": 0
    

    — the contract is not in by_shape, not in not_armed_shapes, not in any error. Thirteen armed, Pass, and the fourteenth contract's extract error is gone. I planted three forbidden values into its fixture and all three passed, which is how I noticed.

    Why these belong on #3610

    Arm 1 (aprender#3622, declines: + exit 2) covers the NoFocus decline. These two are (a) a NoShapes decline and (b) an extract error — different reasons, same aggregate behaviour. The fix that carries the NoFocus decline into the report should carry every per-contract refusal: declines: [{contract, reason}] for NoShapes and NoFocus, and errors: [{contract, message}] for extraction failures, with exit 2 when either is non-empty. A --gate shapes over a directory must never report fewer contracts than the directory holds without saying which ones it dropped and why.

    Fixture for (1): a contract whose only defect is an unquoted test: x:: line, in a directory with one healthy contract; expect the aggregate to name it. Fixture for (2): a fixture with a nested array whose key is absent from vocabulary.nested; same expectation.

  7. noahgift commented on Sep 20, 2026

    @noahgift
    MemberAuthor

    Arm 1 lands in #3622; this issue stays open on arm 2.

    #3622 implements the vacuity refusal: vacuity is computed per-shape before any verdict forms, an ARMED shape at zero focus nodes declines (exit 2, Unknown(NoFocus), named in declines[]), an UNARMED shape at zero is reported in declines[] without refusing, and a vacuous shape appears in neither armed_shapes nor not_armed_shapes — filing a vacuity as a policy choice is how it hid. Both venues are covered, because the lone-contract case already declined correctly and a fix proved only there proves nothing.

    Arm 2 — the jsonl extractor, one row per line = one focus node — is not implemented. So the type: jsonl reproduction in this issue still grades zero real rows; it now declines instead of reporting Pass, which is the truth defect fixed, but it is not yet graded. The representative-json-entity workaround and the unmeasured: [jsonl-rows] annotations remain load-bearing until arm 2 lands.

    #3622 therefore carries Refs, not Closes.

  8. removed this from the 0.69.0 milestone on Sep 21, 2026
  9. 20 remaining items

  10. modified the milestones: 0.70.0, 0.71.0 on Oct 4, 2026
  11. noahgift commented on Oct 12, 2026

    @noahgift
    MemberAuthor

    Holder note (aprender-w4664). The owner:aprender-a7 label is a clerk default, not a live hold: a7 is on pre-verify. aprender-w4664 holds this issue for one slice, as look-ahead under #3269:

    • Slice: a gate-level test of arm 2 against the reproduction as filed. On main, a .jsonl ref under extract:json already gives one focus node per row (arm 2), and #3610: a shapes gate reported Pass over ZERO focus nodes — the vacuity check was global, so a directory hid it #3622 gave the vacuity refusal (arm 1). The only jsonl fixture at gate level is the torn line (Unknown{Warn}). No test plants a well-formed row that the sh:pattern forbids.
    • Branch test/3610-jsonl-gate-plant (not pushed yet, no PR):
      • Fixtures json-jsonl-ok (3 conforming rows → Pass, focus_nodes_n 3) and json-jsonl-pattern (the same plus "outcome":"answered:do" → Fail, exit 1, focus_nodes_n 4, 1 violation naming the value).

    The older PMAT-3610-vacuous-shapes* branches are arm-1 work and do not touch this.

  12. added
    P1High priority
    and removed
    P0Critical priority
    on Oct 14, 2026
  13. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    P0 to P1 (2026-10-14 P0 triage): P0 now means it blocks the next release or carries the operator's own words on it; this issue does neither, so it stays on the 0.71.0 milestone at P1. The operator quote in its history is a milestone-wide triage note, not words about this issue.

  14. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    Folded into #5308 as a checklist row (TRIAGE-100, 2026-10-14): the open-ticket cap is 100, so this ticket is now tracked on that master and closed as not planned. Nothing is lost: reopen it when its work resumes, and an open PR for it reopens it. Pushed branch: PMAT-3610-vacuous-shapes-r, test/3610-jsonl-gate-plant.

    Agent: aprender-clerk

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 priorityont-0.70Ontology spec work that must finish in 0.70 (operator 2026-09-23)owner:aprender-a7owner sessionsize:MK̂ 3-4 h

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions