tools: close the last Shape-C1 item — the PI manifest asserts were not the check - #1096
Conversation
…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.
There was a problem hiding this comment.
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
python3andpython3 -Ofor malformed shapes. - Added an AST-based test to detect reintroduction of bare
assertin 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.
| 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}" |
| 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.
|
Disposition (merge-gate review) Blocker fixed in 84a92be. On the materialized merged tree (main + #1099 + this PR), Verified on the merged tree: both cases pass, the two PI suites are 30 passed, and full The three inline notes (assert-scope vs docstring, |
There was a problem hiding this comment.
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 anyassertanywhere 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 noassertat all”), or (b) narrow the AST check to only the specific assert patterns/locations you intend to forbid (e.g.,assert isinstance(...)inmain()).
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 thissubprocess.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
returncodemismatches, this assertion only reportsstdout, but the validator’s diagnostics are written tostderr(ERR: ...). For easier triage on CI failures, pass a message that includesstderr(or bothstdoutandstderr). This pattern repeats in several tests in this file.
assert proc.returncode == 2, proc.stdout
* 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.
Closes the last deferred Shape-C1 item, registered in #1084:
tools/validate_professional_intelligence_manifest.pyL97/L119/L125 used bareassertfor shape checks, whichpython -Ostrips.Probing it first changed the verdict. These three asserts are not
load-bearing, so #1084's remedy (
require()/raise TypeError) does not applyhere — 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 alreadyrejects a non-mapping.
expect_mapping()is a plainisinstancetest thatprints
ERR:and returnsFalse— not an assert, so-Oleaves it intact.if not expect_mapping(data, "manifest"): return 2expect_mapping(...)→bad += 1; capabilities = {}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/mainbefore any editMalformed manifests (top level a list;
capabilitiesa list;capabilities.workspaceOSa list), run underpython3 -Oagainst theunmodified
maintool:All three already fail closed under
-O. There was no evaporating check.Converting them to
raisewould additionally have broken deliberate behaviour:L119/L125 normalise to
{}specifically so a run keeps accumulating everydownstream error instead of stopping at the first.
What this PR does instead
establishes the invariant — so the next Shape-C sweep does not re-flag this
file for a fourth time.
tools/tests/test_professional_intelligence_manifest_shape.py,turning "the guard happens to survive
-O" into an enforced property. Itruns the validator under
python3andpython3 -Oagainst eachmalformed shape, plus an AST check that fails if any bare
assertreturns.No other test in
tools/testsexercises-O.Behaviour is unchanged on every input. Pre- vs post-change output across
4 manifests x {
python3,python3 -O} is byte-identical (diff: empty). Thereal manifest still validates
rc=0under both interpreters and viamake validate-professional-intelligence-manifest.The new test has teeth — verified both ways
[97, 119, 125]expect_mappingassert-based (the real fail-open)-O/plain shape teststools/tests: 298 passed (289 + 9 new).Also found in the file — reported, not changed
demoAcceptancemisdiagnoses a bad shape (L164). IfdemoAcceptanceispresent but not a mapping, the ternary yields
Noneand 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_mappingfor this.metadata.namecheck is skipped whenmetadatais not a mapping (L117elif) —badis already incremented so it fails closed, but the specificdiagnostic is lost.
contractPaths/controlRefslist certain paths, but never checks theyexist. 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 touchvalidate-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).