Skip to content

tools: close the last Shape-C1 item — the PI manifest asserts were not the check - #1096

Merged
mdheller merged 2 commits into
mainfrom
fix/shape-c1-pi-manifest-assert-evaporation
Jul 31, 2026
Merged

tools: close the last Shape-C1 item — the PI manifest asserts were not the check#1096
mdheller merged 2 commits into
mainfrom
fix/shape-c1-pi-manifest-assert-evaporation

Conversation

@mdheller

Copy link
Copy Markdown
Member

Closes the last deferred Shape-C1 item, registered in #1084:
tools/validate_professional_intelligence_manifest.py L97/L119/L125 used bare
assert for shape checks, which python -O strips.

Probing it first changed the verdict. These three asserts are not
load-bearing, so #1084's remedy (require() / raise TypeError) does not apply
here — and applying it would have been a regression. Reporting that rather than
shipping a fix whose teeth I could not demonstrate.

Why they were never the check

Each assert sits immediately after an expect_mapping() guard that already
rejects a non-mapping. expect_mapping() is a plain isinstance test that
prints ERR: and returns Falsenot an assert, so -O leaves it intact.

line guard that actually enforces it
L97 if not expect_mapping(data, "manifest"): return 2
L119 expect_mapping(...)bad += 1; capabilities = {}
L125 expect_mapping(...)bad += 1; workspace_os = {}

Each assert was therefore unreachable-with-a-false-condition: a narrowing hint
for a type checker this repo does not run (no mypy/pyright config, no CI
type-check step anywhere).

Proof, measured on origin/main before any edit

Malformed manifests (top level a list; capabilities a list;
capabilities.workspaceOS a list), run under python3 -O against the
unmodified main tool:

rc=2  -O  A: top-level is a LIST      ERR: manifest must be a mapping/object
rc=2  -O  B: capabilities is a LIST   ERR: capabilities must be a mapping/object
rc=2  -O  C: workspaceOS is a LIST    ERR: capabilities.workspaceOS must be a mapping/object

All three already fail closed under -O. There was no evaporating check.

Converting them to raise would additionally have broken deliberate behaviour:
L119/L125 normalise to {} specifically so a run keeps accumulating every
downstream error instead of stopping at the first.

What this PR does instead

  1. Removes the three vestigial asserts, recording at each site which guard
    establishes the invariant — so the next Shape-C sweep does not re-flag this
    file for a fourth time.
  2. Adds tools/tests/test_professional_intelligence_manifest_shape.py,
    turning "the guard happens to survive -O" into an enforced property. It
    runs the validator under python3 and python3 -O against each
    malformed shape, plus an AST check that fails if any bare assert returns.
    No other test in tools/tests exercises -O.

Behaviour is unchanged on every input. Pre- vs post-change output across
4 manifests x {python3, python3 -O} is byte-identical (diff: empty). The
real manifest still validates rc=0 under both interpreters and via
make validate-professional-intelligence-manifest.

The new test has teeth — verified both ways

mutation result
restore the three asserts AST test fails, naming [97, 119, 125]
make expect_mapping assert-based (the real fail-open) 7 of 9 fail, incl. all six -O/plain shape tests
restored 9 passed, file byte-identical to pre-mutation

tools/tests: 298 passed (289 + 9 new).

Also found in the file — reported, not changed

  • demoAcceptance misdiagnoses a bad shape (L164). If demoAcceptance is
    present but not a mapping, the ternary yields None and the tool reports
    "demoAcceptance.required must be a non-empty list" — pointing at the wrong
    field. It still fails closed (rc=2); this is message quality, not teeth.
    Every other field in the file uses expect_mapping for this.
  • metadata.name check is skipped when metadata is not a mapping (L117
    elif) — bad is already incremented so it fails closed, but the specific
    diagnostic is lost.
  • Declared-but-never-stat'd evidence. The validator enforces that
    contractPaths / controlRefs list certain paths, but never checks they
    exist. The manifest could claim contract alignment against deleted files and
    still print "OK: workspaceOS contract-aligned evidence present". Cross-repo
    refs can't be checked locally, but five in-repo ones could be. I verified all
    five currently exist, so this is latent, not currently broken.

No other bare asserts remain in the file (AST-verified, and now CI-enforced).

Lane

tools/ only — two files. Does not touch validate-target-diagnostics.yml
(#1080 and #1082 were both still open when this was written, which is why this
item was deferred in the first place; the tool change is independent of them).

…not the check)

Registered in #1084 as the last deferred Shape-C1 item: three bare asserts in
tools/validate_professional_intelligence_manifest.py (L97, L119, L125) that
`python -O` would strip.

Probing it first changed the verdict. These three are NOT load-bearing, so the
#1084 remedy (convert to require()/raise) does not apply and would have been a
regression. Each assert sits immediately after an expect_mapping() guard that
already rejects a non-mapping -- and expect_mapping() is a plain isinstance
test that prints ERR and returns False, not an assert, so -O leaves it intact:

  L97   guarded by `if not expect_mapping(data, "manifest"): return 2`
  L119  guarded by expect_mapping() + `capabilities = {}` fallback
  L125  guarded by expect_mapping() + `workspace_os = {}` fallback

Measured on origin/main BEFORE any edit, under `python3 -O`, against a manifest
whose top level / capabilities / capabilities.workspaceOS is a list: all three
already exit rc=2 with the correct "must be a mapping/object" diagnostic. There
was no evaporating check to fix.

Converting them to `raise TypeError` would also have broken the deliberate
error-accumulation: L119/L125 normalise to {} precisely so a run reports every
downstream error rather than stopping at the first.

So: remove the three vestigial asserts (narrowing hints for a type checker this
repo does not run -- no mypy/pyright config or CI step exists) and record at
each site which guard establishes the invariant, so the next Shape-C sweep does
not re-flag this file for a fourth time.

Behaviour is unchanged on every input: pre- and post-change output over
4 manifests x {python3, python3 -O} is byte-identical, and the real manifest
still validates rc=0 under both interpreters and via `make
validate-professional-intelligence-manifest`.

Adds tools/tests/test_professional_intelligence_manifest_shape.py, which turns
"the guard happens to survive -O" into an enforced property: it runs the
validator under python3 AND python3 -O against each malformed shape, and an AST
check fails if any bare assert returns to the file. No other test in tools/tests
exercises -O. Both directions verified by mutation -- restoring the asserts
fails the AST test naming L97/L119/L125; making expect_mapping assert-based
fails 7 of 9.

Lane: tools/ only. Does not touch validate-target-diagnostics.yml (#1080/#1082
still open at time of writing).

tools/tests: 298 passed.
Copilot AI review requested due to automatic review settings July 30, 2026 04:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Removes vestigial assert isinstance(...) shape “checks” from the manifest validator (which would be stripped under python -O), and adds regression tests to ensure malformed shapes fail consistently under both normal and -O interpreters.

Changes:

  • Replaced three bare shape asserts with comments documenting the existing expect_mapping() enforcement.
  • Added subprocess-based tests that run the validator under python3 and python3 -O for malformed shapes.
  • Added an AST-based test to detect reintroduction of bare assert in the validator.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tools/validate_professional_intelligence_manifest.py Removes bare asserts and documents why guards remain effective under -O.
tools/tests/test_professional_intelligence_manifest_shape.py Adds -O parity tests and an AST guard against bare asserts in the validator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +103 to +113
def test_validator_carries_no_bare_asserts() -> None:
"""Guard the guard: no shape check in this tool may be a bare `assert`.

Catches a regression at the source rather than only through behaviour --
if someone reintroduces `assert isinstance(...)` here, -O would strip it.
"""
import ast

tree = ast.parse(TOOL.read_text(encoding="utf-8"))
asserts = [n.lineno for n in ast.walk(tree) if isinstance(n, ast.Assert)]
assert not asserts, f"{TOOL.name} regained bare assert(s) at line(s) {asserts}"
Comment on lines +58 to +62
return subprocess.run(
argv_prefix + [str(root / "tools" / TOOL.name)],
capture_output=True,
text=True,
)
@pytest.mark.parametrize("argv_prefix", INTERPRETERS)
def test_non_mapping_manifest_is_rejected(tmp_path: Path, argv_prefix: list[str]) -> None:
proc = run_against(tmp_path, [{"apiVersion": "x"}, {"kind": "y"}], argv_prefix)
assert proc.returncode == 2, proc.stdout
… its fixture root

run_against() copied only the validator into the synthetic root. Once the
validator stats the manifest's in-repo contractPaths/controlRefs (the
evidence-paths change), feeding it the real manifest there fails: the declared
in-repo paths are absent from the fixture, not from the repo. Mirror
build_root() in the evidence-paths suite and copy those paths in; cross-repo
refs are not present in any checkout and stay skipped. Fixes
test_real_manifest_still_validates on the merged tree. Addresses Copilot on #1096.
Copilot AI review requested due to automatic review settings July 30, 2026 17:30
@mdheller

Copy link
Copy Markdown
Member Author

Disposition (merge-gate review)

Blocker fixed in 84a92be. On the materialized merged tree (main + #1099 + this PR), test_real_manifest_still_validates[python3] and [python3-O] were failing: once #1099 teaches the validator to stat the manifest's in-repo control-ref/contract paths, run_against() — which copied only the validator into its synthetic root — made the tool reject the real manifest for paths absent from the fixture, not from the repo. Mirrored build_root() from the evidence-paths suite: run_against now copies the manifest's in-repo evidence into the synthetic root; cross-repo refs stay skipped.

Verified on the merged tree: both cases pass, the two PI suites are 30 passed, and full tools/tests is 307 passed / 12 skipped / 0 failed. (Testing this PR alone — 9 passed — is exactly how the merged-tree failure was missed; please merge #1099 first, then this.)

The three inline notes (assert-scope vs docstring, cwd=/timeout= on the subprocess, stderr in the assertion message) are valid minor test-quality improvements — reasonable as a follow-up; left out here to keep this change scoped to the merged-tree failure.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

tools/tests/test_professional_intelligence_manifest_shape.py:136

  • The docstring says “no shape check … may be a bare assert”, but the implementation rejects any assert anywhere in the tool (even if it were unrelated to shape checks). Either (a) update the docstring/message to match the stricter invariant (“the tool must contain no assert at all”), or (b) narrow the AST check to only the specific assert patterns/locations you intend to forbid (e.g., assert isinstance(...) in main()).
def test_validator_carries_no_bare_asserts() -> None:
    """Guard the guard: no shape check in this tool may be a bare `assert`.

    Catches a regression at the source rather than only through behaviour --
    if someone reintroduces `assert isinstance(...)` here, -O would strip it.
    """
    import ast

    tree = ast.parse(TOOL.read_text(encoding="utf-8"))
    asserts = [n.lineno for n in ast.walk(tree) if isinstance(n, ast.Assert)]
    assert not asserts, f"{TOOL.name} regained bare assert(s) at line(s) {asserts}"

tools/tests/test_professional_intelligence_manifest_shape.py:85

  • Consider adding a timeout= to this subprocess.run() call. If the validator ever regresses into a hang (e.g., unexpected blocking I/O), the test suite can stall indefinitely; a small timeout makes such failures deterministic and easier to debug.
    return subprocess.run(
        argv_prefix + [str(root / "tools" / TOOL.name)],
        capture_output=True,
        text=True,
    )

tools/tests/test_professional_intelligence_manifest_shape.py:99

  • When returncode mismatches, this assertion only reports stdout, but the validator’s diagnostics are written to stderr (ERR: ...). For easier triage on CI failures, pass a message that includes stderr (or both stdout and stderr). This pattern repeats in several tests in this file.
    assert proc.returncode == 2, proc.stdout

@mdheller
mdheller merged commit 1e14c38 into main Jul 31, 2026
68 checks passed
mdheller added a commit that referenced this pull request Aug 2, 2026
* tools: four ways a check reported success without checking

Every fix below is paired with an observed failure: break the guarded thing,
show red, restore, show green. Evidence is local — Actions is spend-capped.

1. An absent validator was a silent pass
   `run_optional_validator` ran a validator only `if validator.exists()`. All 11
   call sites sit inside `tools/validate_repo.py`, which is `make validate-repo`,
   a leg of the REQUIRED diagnostics-gate. Deleting or renaming any of the 11
   disarmed that check while the gate stayed green.

   Renamed to `run_validator`; absence is now a failure. The only way to tolerate
   it is an explicit `OPTIONAL_VALIDATORS` entry naming the reason, and even that
   prints a SKIP line. The dict ships empty — all 11 validators exist today.

   Red/green: with `validate_cell_gateway_api.py` renamed away, before rc=0
   "OK: validate passed" -> after rc=2 naming the missing file -> restored rc=0.

2. `|| true` inside a required-gate leg
   Makefile:100, last recipe line of `lattice-studio-smoke`, which feeds
   diagnostics-gate via smoke-target-diagnostics. Stripping `|| true` showed what
   it hid: `--catalog-asset .../services_demo-inference-service.json` is missing
   its `/catalog-asset.json` segment, so the step exited 2 and neither
   studio-platform-records.json nor its enrichments were ever written.

   Same commit (847d4c3, #633) also severed the target's 14 `test -s` output
   assertions into a bogus `test -s:` target — a make target named `test` with
   prerequisite `-s`. `make -n lattice-studio-smoke` confirmed 0 assertions ran.
   Fixed the path, dropped `|| true`, re-attached the 14 assertions, deleted the
   bogus target and the two dead emit lines it carried.

   Red/green: re-break the path with no `|| true` -> `make lattice-studio-smoke`
   rc=2; restored -> rc=0 with all 14 assertions executing.

3. Empty globs read as pass
   Seven validators drive every check from a glob and print success when it
   matches nothing. Six say so literally ("0 <name> checks passed", rc=0); the
   seventh, adr-035, kept one non-glob check and reported 1 of 6.

   Each now materialises the glob, declares a MIN_FIXTURES floor set to what
   ships today, and fails before the loop if the match is short.

   Red/green: with each fixture directory emptied, all seven go rc=1 with a named
   diagnostic; restored, all seven pass with their original check counts.

   The audit called this four validators; measured against emptied directories it
   is seven. All seven are fixed.

4. Verification that evaporates under `python -O`
   Of the 44 bare asserts across 6 files, 41 are already claimed by open PRs:
   38 by #1084 (the pattern followed here) and 3 by #1096/#1099, which owns
   validate_professional_intelligence_manifest.py. Only
   tools/test_liberty_stack_runtime_demo.py was unclaimed.

   Its 3 asserts are the whole check, so under -O it printed {"ok": true}
   regardless. None was vestigial and none sat behind an isinstance guard, so all
   three were converted rather than deleted. It also could not pass at all: the
   demo emits two JSON documents and the tool called json.loads on the stream,
   raising JSONDecodeError before any check. Now parses the readout document and
   raises DemoCheckFailure.

   Red/green under `python3 -O`: original asserts against a wrong payload -> rc=0
   {"ok": true}; converted check -> DemoCheckFailure. End to end with the readout
   status sabotaged -> rc=1; restored -> rc=0 under both python3 and python3 -O.

Guard tests: tools/tests/test_tools_silent_pass_guards.py (27 tests) locks in all
four. Verified to have teeth both ways — reverting any one of validate_repo.py,
the Makefile, a glob validator, or the liberty demo turns the relevant tests red.

pytest tools/tests: 289 -> 316 passed.

Untouched: validate-target-diagnostics.yml, gitops-promote.yml, images.yml,
apps/health-twin/**, compute_gateway/**, infra/tofu/**,
tools/validate_professional_intelligence_manifest.py.

* fix(tools): adr-035 fixture-floor f-string parses on Python 3.11

The nested double-quotes in f"{(CONTRACTS / "examples").relative_to(ROOT)}"
are a SyntaxError on 3.11 (PEP 701 nested same-quotes are 3.12+). CI runs 3.11,
so validate_adr_035_contracts.py failed to parse — a fixture-floor guard that
cannot run. Single-quote the inner string. Guard suite: 1 failed/26 -> 27 passed
on 3.11. Addresses Copilot inline on #1111.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants