Skip to content

fix(phase-validation): 'UI/UX 100%' measured only that files exist (#17089) - #17505

Merged
mrveiss merged 12 commits into
mainfrom
issue-17089-phase-validation
Sep 26, 2026
Merged

mrveiss merged 12 commits into
mainfrom
issue-17089-phase-validation

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

"Phase 6: Enhanced UI/UX — 100%" meant autobot-frontend/src/App.vue and package.json exist.

Two causes produced that one number, and the second is why nothing ever failed.

1. Skipping the live checks shrank the denominator. A phase's figure is passed / total. #7496
skipped the endpoint, service and feature checks under --ci-mode for a sound reason — the workflow
never brings a stack up — but it removed them from total as well as the numerator. Phase 6's two
surviving checks were file-existence tests, so 2/2 read as 100% complete. The less a run could
measure, the higher it scored.
The original comment says the skip existed to stop those checks
"drag[ging] the score below the threshold", which states the inversion outright: not looked at
became nothing missing.

2. 14 of 15 feature-check paths did not exist. They pointed at the pre-reorganization layout
(src/, backend/, autobot-vue/), so even with a live stack those checks could only ever report
False. #7496's comment claims the paths were refreshed for the current layout — it refreshed the
criteria dict and left the lambdas ~700 lines below untouched. The comment was true about what it
sat above and false about the file, and the same change began skipping those checks in CI, so the
resulting zeros never surfaced.

The consequence the issue did not mention, and the reason this is a correctness defect rather
than a dashboard one.
Following the consumers rather than grepping the symbol:
autobot-backend/phase_progression_manager.py calls validate_all_phases() and then decided a
phase was completed with phase_data["completion_percentage"] >= self.config["validation_threshold"]
— and that module progresses phases from the resulting set. So a --ci-mode run could promote a
phase on "the declared files are present". A reviewer reading only the issue would take this for a
reporting fix.

What Changed

New: autobot-infrastructure/shared/scripts/phase_score.py — the policy deciding when a number
may be called completion. A phase with even one skipped group cannot be called complete at any
ratio, and its figure is named structural_presence_percentage. completion_percentage is
absent from the report when anything was skipped — not zero, not null — so a consumer reaching
for it gets a KeyError rather than a number it would have believed. overall_maturity is None in
CI mode and ready_for_production is None rather than False, because False reads as "assessed
and failed".

It is a separate module because the policy has to be testable, and phase_validation_system imports
aiohttp, psutil, requests and autobot_shared.redis_client at module scope. A policy nothing
can exercise is how this one survived.

The UI/UX phase now reports no figure at all (AC3's second option). It names the four workflows
that actually test it — frontend-test, visual-regression, stylelint-tokens,
frontend-typecheck-regression — and reports status: deferred-to-dedicated-gates. Whether a UI is
consistent, responsive and internationalised is not a question a file-existence sweep can answer at
any percentage. It also contributes nothing to the aggregate: leaving it in the weighted mean
would make "reports no score" true of the phase and false of the run, since its 100% would still
reach the CI gate through the average. overall() excludes deferring phases and discloses that
it did, via phases_excluded_from_score and verified_by_dedicated_gates — an average over fewer
phases than the run evaluated is the same silent denominator change this issue is about.

All 54 referenced paths now exist — 39 in PHASE_CRITERIA (llm_interface_pkg → llm_shared,
renamed in #6941) and 15 in the feature lambdas (autobot-vue/ → autobot-frontend/; src/ and
scripts/ → their current packages).

The gate keeps its 60/80 thresholds and the same number, now read from structural_presence and
labelled as such. .get("overall_maturity", 0) would have returned None and broken bc rather
than failing loudly.

Three defects this PR caused and its own guards caught — all mine, all recorded because the
guards are the deliverable as much as the fix is:

  • Removing the superseded _determine_phase_status with a slice bounded by the next def
    swallowed the async def that followed, deleting _validate_phase_features while its call
    site stayed
    . py_compile passed — Python resolves attribute names at call time — and in
    --ci-mode that line sits in the branch CI does not take. Three independent reasons nothing
    failed. Restored verbatim from origin/main; test_no_self_call_points_at_a_missing_method now
    fails on exactly that deletion, mutation-proved.
  • Extracting _SHARED_SCRIPTS to fix three E501s split those paths into a Name plus a Constant,
    and the path scanner — which collected string literals — saw 4 paths instead of 15. Its floor
    failed immediately. A scanner finding 4 would have reported "no missing paths" and been believed;
    the floor caught it because it asserts a population, not an absence. The scanner now resolves
    module-level string constants and flattens the root / a / b chain.
  • The ratchet pin comment then broke E501 at 130 characters, in the file that records line-count
    limits.

A duplicated threshold consolidated rather than a ceiling raised. My first version took
phase_progression_manager.py from its 851-line pin to 859. config["validation_threshold"] was
the literal 95.0 in a hard-coded dict with no external merge — a constant wearing a config key's
clothes — and phase_score.COMPLETE_AT is also 95.0. Two copies of one bar, kept in step by hand.
The policy owns it now, which brought the file to 850, so the pin is lowered to 850 in both
baseline copies in the same commit.

Single-issue rationale

#17089 is one issue and is delivered whole; there is no second issue to batch with it. The nearest
related work is deliberately not here: #17497 (a prototype-chain in allow-list in
NodeServicesPanel.vue) is a different file and risk class, and #17482 covers the
redis_client-on-import key-material incident referenced under Verification. Batching either would
have mixed a milestone fix with unrelated risk.

Verification

repo_tests/phase_validation_paths_and_skips_17089_test.py     22 passed
ratchet / glob / prepush / bare-pytest guards                 73 passed
workflow-lint guards (-k "workflow and (lint or yaml or valid)")  20 passed
flake8 (all touched files)                                    clean
scripts/check_python_file_size.py                             clean
criteria paths referenced / missing                           39 / 0
feature-check paths referenced / missing                      15 / 0
phase_progression_manager.py                                  851 -> 850 lines

What I did not verify, stated rather than implied. I did not run
phase_validation_system.py. Importing it pulls autobot_shared.redis_client at module scope,
which generates and persists key material when none is configured — #17482 is a live incident
from exactly that, where the generated key file blocked a code_source rsync until it was removed.
So a guard that imported this module would mutate the tree it guards. The tests therefore read the
system with ast and load only the policy module, and the end-to-end behaviour of the assembled
report is CI's to prove. py_compile is not a substitute and did not catch the deleted method
above, which is why the self-call guard exists.

Mutation-proved, not merely asserted: reinstating the numeric read in phase_progression_manager
fails its guard, and re-deleting _validate_phase_features fails the self-call guard naming that
method.

Model Used

Claude Opus 5 (1M context)

Closes #17089

Summary by CodeRabbit

  • Improvements
    • Phase validation reports now distinguish structural presence from measured completion, and identify checks that were skipped or handled by a dedicated workflow.
    • Completion and progression decisions use the validator’s completion status rather than a percentage threshold.
    • Overall maturity and production-readiness results are shown only when all counted phases have measured completion.
    • Validation summaries provide a structured fallback when results cannot be parsed, including structural presence and skipped-check details.

…17089)

"Phase 6: Enhanced UI/UX -- 100%" meant `App.vue` and `package.json` exist.

TWO DEFECTS PRODUCED ONE NUMBER.

1. THE DENOMINATOR SHRANK WITH THE SKIPS. #7496 skipped the endpoint, service
   and feature checks under `--ci-mode` for a sound reason -- the workflow never
   brings a stack up -- but the phase figure is `passed / total`, and skipping
   removed those checks from `total` as well. Phase 6's two surviving checks were
   file-existence tests, so 2/2 read as 100% complete. The less a run could
   measure, the higher it scored. Its own comment says the skip was to stop them
   "drag[ging] the score below the threshold", which is the inversion stated
   outright: not-looked-at became nothing-missing.

2. 14 OF 15 FEATURE-CHECK PATHS DID NOT EXIST, so even with a live stack those
   checks could only report False. #7496's comment claims the paths were
   refreshed for the current layout; it refreshed the criteria dict and left the
   lambdas 700 lines below pointing at `src/`, `backend/` and `autobot-vue/`.
   The comment was true about what it sat above and false about the file. Nothing
   failed, because the same change began skipping those checks in CI.

WHAT CHANGED

`phase_score.py` is new and holds the policy: a phase with even one skipped
group cannot be called complete at any ratio, and its figure is named
`structural_presence_percentage`. `completion_percentage` is ABSENT from the
report when anything was skipped -- not zero, not null -- so a consumer reaching
for it gets a KeyError instead of a number it would have believed.
`overall_maturity` is `None` in CI mode and `ready_for_production` is `None`
rather than False, because False reads as "assessed and failed".

A separate module because the policy must be testable, and
`phase_validation_system` imports aiohttp, psutil, requests and
`autobot_shared.redis_client` at module scope. That last one has side effects --
it writes secret key material when none is configured -- so a guard that
imported it would mutate the tree it guards. The tests read the system as data
with `ast` and load only the policy. A policy nothing can exercise is how this
one survived.

The 15 paths are corrected to their real homes (`llm_interface_pkg` -> `llm_shared`,
#6941; `autobot-vue/` -> `autobot-frontend/`; `src/` and `scripts/` -> their
current packages). The gate keeps its 60/80 thresholds and the same number, now
read from `structural_presence` and labelled as such -- `.get("overall_maturity", 0)`
would have returned None and broken `bc`.

Phase 6 now names the workflows that actually test the UI (frontend-test,
visual-regression, stylelint-tokens, frontend-typecheck-regression) rather than
claiming a UI verdict from file presence.

A GUARD ADDED BECAUSE THIS PR BROKE IT ITSELF. Removing the superseded
`_determine_phase_status` with a slice bounded by the next `    def ` swallowed
the `    async def` that followed, deleting `_validate_phase_features` while its
call site stayed. `py_compile` passed -- Python resolves attribute names at call
time, and in `--ci-mode` that line sits in the branch CI does not take. The
method is restored verbatim from `origin/main`, and
`test_no_self_call_points_at_a_missing_method` now fails on exactly that
deletion; mutation-proved by redoing it.

    repo_tests/phase_validation_paths_and_skips_17089_test.py   17 passed
    criteria paths referenced / missing                         39 / 0
    feature-check paths referenced / missing                    15 / 0
    methods defined (24 on main, 1 intentionally removed)       23

Closes #17089
…17089)

Consumer of the report shape changed in `bbc2c9a28d`, and where that defect had
consequences rather than just a misleading number.

`phase_progression_manager` calls `validate_all_phases()` and decided a phase was
completed with:

    if phase_data["completion_percentage"] >= self.config["validation_threshold"]

Under `--ci-mode` only the file/directory checks run, so that percentage was the
share of EXISTENCE checks that passed. A phase whose declared files happened to
exist scored 100 and was marked completed -- and this module PROGRESSES phases
from that set, so the consequence was automatic promotion on "the files are
present". Both decision sites now consult `complete`, which the policy sets
False whenever any check group was skipped, at any percentage.

THE THRESHOLD WAS DUPLICATED AND IS CONSOLIDATED, not dropped.
`self.config["validation_threshold"]` was the literal `95.0` in a hard-coded dict
with no external merge -- a constant wearing a config key's clothes -- and
`phase_score.COMPLETE_AT` is also `95.0`. Two copies of one bar, kept in step by
hand. The policy owns it now.

That consolidation is what let this land without raising a ceiling: the file was
pinned at 851 and my first version took it to 859. Removing the duplicate brought
it to 850, so the pin is LOWERED to 850 in both baseline copies, in this commit,
as the shrink-only ratchet requires.

THREE THINGS THIS PR'S OWN GUARDS CAUGHT, all mine:

- Extracting `_SHARED_SCRIPTS` to fix three over-long lines split those paths
  into a Name plus a Constant, and the lambda-path scanner -- which collected
  string literals -- stopped seeing them. Its floor failed immediately. The
  scanner now resolves module-level string constants and flattens the
  `root / a / b` chain, reading the path the code builds rather than the
  fragments it is written in. Back to 15 of 15.
- That floor is why this was a failure and not a silent loss of coverage: a
  scanner quietly finding 4 paths instead of 15 would have reported "no missing
  paths" and been believed.
- The pin comment itself then broke E501 at 130 characters, in the very file that
  records line-count limits.

Guarded against regression: `TestTheProgressionManagerDoesNotPromoteOnPresence`
fails if `["completion_percentage"]` returns to this file or either decision
stops consulting `complete`; mutation-proved by putting the numeric read back.

BOUNDARY, since tracing this grew the target set: `project_state_manager.py` and
`project_state_tracking/reports.py` also carry a `completion_percentage` and are
NOT affected -- that is their own SQLite-backed dataclass field, reached through
the state tracker's `summary`, not through `validate_all_phases()`. Followed the
data flow rather than matching the symbol.

    repo_tests/phase_validation_paths_and_skips_17089_test.py   18 passed
    flake8 (all touched files)                                   clean
    check_python_file_size.py                                    clean
    phase_progression_manager.py                                 851 -> 850

Refs #17089
… it (#17089)

Completes AC3, which I had only half delivered in `bbc2c9a28d`. It asks that the
UI/UX phase report the real UI gates' verdicts, "or the phase links to them and
doesn't report its own percentage". I added the links and kept publishing a
structural figure -- still a percentage next to a phase whose verdict lives
elsewhere, so the criterion was not met and I had written it up as though it
were.

A phase declaring `authoritative_gates` now reports NO figure at all:

    status: deferred-to-dedicated-gates
    authoritative_gates: [frontend-test, visual-regression, stylelint-tokens,
                          frontend-typecheck-regression]
    why_no_score: "this phase is verified by dedicated workflows, not by file
                   presence"

Whether a UI is consistent, responsive and internationalised is not a question a
file-existence sweep can answer at any percentage. The owner objected to a
number, so the fix is to stop producing one here rather than to produce a
better-labelled one.

AND IT CONTRIBUTES NOTHING TO THE AGGREGATE. Excluding it from its own report
while leaving it in the weighted average would make "reports no score" true of
the phase and false of the run -- its 100% would still reach the CI gate through
the mean. `overall()` now drops deferring phases and DISCLOSES that it did:
`phases_excluded_from_score` and `verified_by_dedicated_gates` are in the output,
because an average over fewer phases than the run evaluated is exactly the silent
denominator change this issue is about.

Four consumers needed the absent key handled, one of which would have crashed:
`_generate_recommendations` subscripted `structural_presence_percentage`
directly, so a deferring phase was a KeyError -- caught by writing the tests, not
by running the script, which cannot be run here (it imports
`autobot_shared.redis_client`, which writes key material when none is
configured). The JSON output emits `None` rather than `0` for a phase with no
score: 0 renders as "measured and found empty", the confusion this issue is about.

One consequence worth stating plainly: `phase_progression_manager` can now never
mark Phase 6 completed, because `complete` is False for a deferring phase. That
is correct -- this report cannot verify that phase -- but it means UI progression
must come from the dedicated gates rather than from here.

    repo_tests/phase_validation_paths_and_skips_17089_test.py   22 passed
    flake8 (3 touched files)                                     clean

Refs #17089
@mrveiss mrveiss added this to the v0.9.0 milestone Sep 25, 2026
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 16 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 4dc8630b-3a99-4d4b-b08b-bc4c85f015f7

📥 Commits

Reviewing files that changed from the base of the PR and between 6261b84 and 431bf8c.

⛔ Files ignored due to path filters (2)
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py
📒 Files selected for processing (9)
  • .github/workflows/phase_validation.yml
  • autobot-backend/phase_progression_manager.py
  • autobot-backend/project_state_tracking/phase_report.py
  • autobot-backend/project_state_tracking/tracker.py
  • autobot-infrastructure/shared/scripts/phase_score.py
  • autobot-infrastructure/shared/scripts/phase_validation_system.py
  • autobot-infrastructure/shared/scripts/validation_dashboard_generator.py
  • autobot-infrastructure/shared/tests/validation_dashboard_generator_test.py
  • repo_tests/phase_validation_paths_and_skips_17089_test.py
📝 Walkthrough

Walkthrough

Phase validation now separates structural presence from measured completion. It records skipped checks, delegates designated phases to authoritative workflows, and uses the complete flag for progression decisions.

Changes

Phase validation scoring

Layer / File(s) Summary
Scoring and aggregation policy
autobot-infrastructure/shared/scripts/phase_score.py, repo_tests/phase_validation_paths_and_skips_17089_test.py
PhaseScore reports structural presence separately from completion. Aggregation excludes deferred phases and reports maturity only when counted phases have no skipped checks. Tests cover skipped checks, completion, and weighted aggregation.
Validator checks and scoring inputs
autobot-infrastructure/shared/scripts/phase_validation_system.py, repo_tests/phase_validation_paths_and_skips_17089_test.py
The validator updates feature-check paths, records live-stack checks as skipped in CI, and creates phase reports with PhaseScore and authoritative gates. Tests check referenced paths, live-stack groups, and validator structure.
Aggregate results and phase reports
autobot-infrastructure/shared/scripts/phase_validation_system.py, repo_tests/phase_validation_paths_and_skips_17089_test.py
Recommendations, critical-phase checks, and text and JSON output use structural presence, completion status, skipped-check details, and authoritative gates. Tests cover deferred-phase reports and aggregate scoring.
Workflow gate and progression consumers
.github/workflows/phase_validation.yml, autobot-backend/phase_progression_manager.py, repo_tests/phase_validation_paths_and_skips_17089_test.py
The workflow gates on structural presence and reports whether completion was measured. The progression manager uses complete for completed phases and prerequisites. Tests check those progression decisions.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟠 High · up to 6261b

After this change, the phase-validation CI check fails on every run. The validator's JSON output omits the structural-presence figure that the gate now reads, so the gate treats it as 0%. The PR status comment also displays undefined% for every phase, and running the validator from the command line in CI mode exits with an error. Restore the missing JSON fields before merging. The display, exit-code, and path-guard fixes are small follow-ups.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 6261b

Separating file presence from measured completion prevents skipped checks from advancing phases. However, the new readiness calculation can omit a deferred phase without verifying its named checks, and an integration check still expects the old numeric result. These gaps affect the reliability of release decisions; no direct attacker-access or privilege expansion was established.

Retained concerns

  • Medium · security · observed: Production readiness can be calculated without verifying the workflows named as authoritative for a deferred phase. The report identifies those workflows, but the aggregate does not consume their outcomes; at least one is manual-only and another is non-blocking.
  • Medium · reliability · inferred: The release/main integration-readiness job still requires a numeric maturity value, although CI validation now produces null when counted checks are skipped. This can strand the integration control after the structural-presence gate passes.
Security review details

Security Blast Radius

  • inferred — The identified assurance gap concerns repository-level phase and release-readiness signals, not a demonstrated new public API, tenant boundary, or privilege grant. Its effective release impact depends on independently enforced checks that were not established.

Security Findings and Attack Paths

  • inferred — A repository change affecting the deferred frontend phase can receive an aggregate production-readiness verdict calculated from other phases without the validator checking its named workflow outcomes. No external attacker-controlled route or actual release bypass was established.

Trust Boundaries and Controls

  • observed — The backend treats the validator’s complete flag as its prerequisite authority. That flag rejects skipped checks; workflow names in the deferred-phase policy are declarations, not verified success results in the aggregate.

Resilience and Maintainability Implications

  • inferred — The incompatible nullable result can keep the release/main integration control from reaching its later status step, creating rollout friction even though the earlier gate assessed only structural presence.

Hardening Proposals

  • proposed — Require verified success of each intended authoritative check before representing deferred-phase coverage as release-ready, and make the integration job explicitly handle unmeasured maturity rather than comparing null as a number.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.19% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 47 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main defect addressed: UI/UX validation reported 100% based only on file presence. This matches the pull request objectives and changes.
Linked Issues check ✅ Passed Issue #17089 is directly linked and has coding requirements. The PR reports structural presence separately from completion, records skipped live-stack checks, omits completion percentage when checks a…
Out of Scope Changes check ✅ Passed The changed workflow, scoring policy, validator, progression manager, and tests support issue #17089. The fallback reporting and gate wording preserve the required distinction between structural prese…
Full details: Docstring Coverage

Explanation

Docstring coverage is 53.19% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 47 functions across 4 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (3)

🟠 Major · Include the structural score in the JSON output. · phase_validation_system.py:944-949

autobot-infrastructure/shared/scripts/phase_validation_system.py:944-949
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include the structural score in the JSON output.

validate_all_phases() adds structural_presence, measures, and skipped_detail to the result. However, _output_json_results() omits these top-level fields. The workflow parses this JSON and falls back to 0 when structural_presence is absent. The gate therefore fails every run.

Suggested fix
     output = {
         "timestamp": datetime.now().isoformat(),
-        "overall_maturity": results.get("overall_maturity", 0),
+        "overall_maturity": results.get("overall_maturity"),
+        "structural_presence": results.get("structural_presence"),
+        "measures": results.get("measures"),
+        "skipped_detail": results.get("skipped_detail", {}),
         "phases": [],
🟡 Minor · The PR comment still reads keys that were removed. · phase_validation.yml:220-226

.github/workflows/phase_validation.yml:220-226
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The PR comment still reads keys that were removed.

phase.completion_percentage no longer exists in the JSON output. results.overall_maturity is null in CI mode. The comment therefore shows undefined% for the maturity line and for every phase. The pass/fail mapping also marks structural-presence-only and deferred-to-dedicated-gates as FAIL.

Read structural_presence_percentage. For phases that report authoritative_gates, list those gates instead of a figure.

🟡 Minor · Handle an unmeasured overall_maturity. · phase_validation_system.py:1041-1047

autobot-infrastructure/shared/scripts/phase_validation_system.py:1041-1047
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle an unmeasured overall_maturity.

When counted phases contain skipped groups, overall_maturity is None. main() then evaluates None < 50, catches the resulting TypeError, and exits with status 3. Summary mode also passes None to %.1f, which causes a logging format error. The CI step ignores this status with || true, so the workflow step continues, but direct CLI use still reports failure.

Suggested fix
-    maturity = results.get("overall_maturity", 0)
-    logger.info("Overall System Maturity: %.1f%%", maturity)
+    maturity = results.get("overall_maturity")
+    if maturity is None:
+        logger.info("Overall System Maturity: not measured")
+    else:
+        logger.info("Overall System Maturity: %.1f%%", maturity)
...
-        maturity = results.get("overall_maturity", 0)
+        maturity = results.get("overall_maturity")
+        if maturity is None:
+            maturity = results.get("structural_presence", 0)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bbf9d6ba-077d-4f2e-a02f-2cbad9257085

📥 Commits

Reviewing files that changed from the base of the PR and between 5ce2ece and 6261b84.

⛔ Files ignored due to path filters (2)
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py
📒 Files selected for processing (5)
  • .github/workflows/phase_validation.yml
  • autobot-backend/phase_progression_manager.py
  • autobot-infrastructure/shared/scripts/phase_score.py
  • autobot-infrastructure/shared/scripts/phase_validation_system.py
  • repo_tests/phase_validation_paths_and_skips_17089_test.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread repo_tests/phase_validation_paths_and_skips_17089_test.py Outdated
@mrveiss

mrveiss commented Sep 25, 2026

Copy link
Copy Markdown
Owner Author

python-suite shard 8/12 is red for a real reason, and it is not a test failure: 3186 passed, 16 skipped, and the step still exited 1. The sys.modules leak guard fails the run after the summary.

============================ sys.modules leak guard ============================
LEAK: repo_tests/phase_validation_paths_and_skips_17089_test.py leaves 1 sys.modules key(s)
      installed outside repo_tests/ — first seen at the test
      tools/lint/check_spdx_header_test.py::Test…
      phase_score: was-genuine -> REPLACED-since, module, spec,
                   autobot-infrastructure/shared/scripts/phase_score.py
      keys: phase_score
1 file(s) above are NOT on repo_tests/sys_modules_leak_baseline.txt, so this run fails.

Cause

repo_tests/phase_validation_paths_and_skips_17089_test.py:170 — _policy() installs the key and never removes it:

spec = importlib.util.spec_from_file_location("phase_score", repo_root() / _POLICY)
module = importlib.util.module_from_spec(spec)
sys.modules["phase_score"] = module
spec.loader.exec_module(module)
return module

The registration itself is correct and its docstring says why — dataclasses resolves cls.__module__ through sys.modules, so the class body raises without it. What is missing is the removal. _policy() is also called about ten times, so the key is re-installed on every call and survives the file.

Note what the guard reports: was-genuine -> REPLACED-since. A genuine phase_score was already importable, and this path-load replaced it. So the leak is not only untidy — any later test that imports phase_score gets this path-loaded copy instead of the real module, and would pass or fail for a reason that has nothing to do with itself.

Fix

The guard's own instruction applies: install and remove in the same try/finally, rather than adding a baseline line. A context manager keeps the dataclasses requirement intact and restores whatever was there before:

@contextlib.contextmanager
def _policy_module():
    spec = importlib.util.spec_from_file_location("phase_score", repo_root() / _POLICY)
    module = importlib.util.module_from_spec(spec)
    previous = sys.modules.get("phase_score")
    sys.modules["phase_score"] = module
    try:
        spec.loader.exec_module(module)
        yield module
    finally:
        if previous is None:
            sys.modules.pop("phase_score", None)
        else:
            sys.modules["phase_score"] = previous

Restoring previous rather than always popping matters here precisely because the guard saw a genuine module underneath.

A session-scoped fixture wrapping that would also avoid the ten repeated path-loads, if the call sites are worth touching in the same pass.

Why I am reporting rather than fixing

ledger who issue 17089 → claimed by autobot-ai-helper-17091, expiring in ~2h. Not my branch to edit. Handing over the diagnosis instead; happy to apply it if the owner would rather I did.

One thing to check while you are in there

The leak was attributed to this file but first seen at tools/lint/check_spdx_header_test.py, i.e. the guard noticed it only when a later test ran. That is the guard working as designed, but it means the shard's ordering decides whether this surfaces at all — worth confirming the fix is verified by running the guard, not by the file's own tests passing.

Review finding on #17505, and it is the defect I caught in this same scanner
this morning, one layer out. Correct, and worse than reported.

THE BLIND SPOT. The lambda-path scanner filtered on `"/" in joined`, which
silently dropped every SINGLE-SEGMENT path -- so `root / "src"`, `root / "tests"`
and two `root / "scripts"` were never collected. `src` and `tests` are exactly
the pre-reorganisation roots this PR exists to remove. The guard reported "15 of
15 paths exist" while not looking at five more, two of them missing.

This morning the same scanner saw 4 paths instead of 15 after I extracted a
constant, and its FLOOR caught it, because a floor asserts a population. There
was no floor over single-segment paths, so this blind spot was silent. A floor
cannot supply what was missing here -- a fixture carrying a MISSING
single-segment path -- which is exactly the contrast pair the reviewer named.

Added the fixture first and watched it fail against the un-widened filter, then
widened it, rather than trusting the instrument that had just missed four roots
to confirm it now sees them. 15 -> 20 paths collected.

AND THE PART THE REVIEW DID NOT REACH, which indicts my own fix. Correcting
`autobot-vue/src/components` to `autobot-frontend/src/components` earlier in this
PR made the DIRECTORY right and left the CHECK dead: `glob` does not recurse and
those components live in subdirectories. So the path guard passed -- it asked
only whether the base directory existed -- while four checks could still only
ever report False:

    (scripts).glob('*security*')                              0 matches
    (autobot-frontend/src/components).glob('*Terminal*')      0   <- "fixed" here
    (autobot-frontend/src/components).glob('*Settings*')      0   <- "fixed" here
    (scripts).glob('*profile*')                               0

EXISTENCE OF A PATH IS NOT EVIDENCE THAT A CHECK CAN SUCCEED, and my guard
asserted the weaker property. Repaired all six dead checks, match counts
measured:

    terminal_interface   components rglob '*Terminal*'          24
    settings_panel       components rglob '*Settings*'          29
    container_security   shared scripts glob '*security*'        2
    code_quality_checks  shared scripts glob '*profile*'         5
    task_planning        autobot-backend glob '*orchestrat*'     2
    unit_testing         autobot-backend/tests                  exists

`TestNoFeatureCheckIsDeadOnArrival` now asserts every glob check matches at
least one file, with a positive control on a fixture and a test that `glob` vs
`rglob` is recorded -- that distinction is the difference between dead and alive
for two of these. Mutation-proved: reverting one `rglob` to `glob` fails the
guard naming that line.

    repo_tests/phase_validation_paths_and_skips_17089_test.py   29 passed
    lambda paths collected / missing                            20 / 0
    glob checks / dead                                          7 / 0

Refs #17089
…shape (#17089)

The strict contract from `bbc2c9a28d` -- `completion_percentage` ABSENT when a
check group was skipped -- broke every consumer that read it unconditionally. I
missed them, and the reason is worth recording: my consumer sweep ended in
`head -20`, so I read a truncated list as complete and concluded two files were
affected when five were. The command meant to enumerate the set was capped, and
I trusted its output as the whole.

Two production consumers, 14 live read sites, both invoked by the same workflow
this PR already touches:

`validation_dashboard_generator.py` -- 5 reads of
`overall_assessment["system_maturity_score"]`, renamed here to
`structural_presence_score`, plus 9 reads of `phase_data["completion_percentage"]`.
It runs as `... > validation_dashboard.html || echo "Dashboard generation
failed"`, so the KeyError would not have failed the job: it would have SILENTLY
STOPPED PRODUCING A DASHBOARD. The same defect this issue is about, wearing a
third hat.

`project_state_tracking/tracker.py` -- 5 sites, including a
`TrackingMetric.PHASE_COMPLETION` counted by thresholding that percentage, and a
snapshot storing it per phase.

WHAT THE MIGRATION DOES, rather than defaulting to 0 anywhere:

- `phase_figure()` returns `None` when a phase publishes no figure, and callers
  branch on it. 0 would read as "measured and found empty".
- "Completed phases" counts come from the `complete` FLAG in both consumers, not
  from a threshold on a percentage. That flag is False whenever a group was
  skipped, so a `--ci-mode` run cannot inflate either count.
- Unscored phases sort LAST rather than as zero -- they are not the
  worst-performing phases, they are the ones the report does not judge -- and are
  excluded from "stalled" alerts for the same reason.
- The tracker's snapshot stores only phases that published a figure: a `None`
  written there becomes a score, and an unscored phase is not a zero-scoring one.
- A deferring phase gets its own recommendation naming the workflows that verify
  it, instead of appearing as a phase needing attention.

ONE DESIGN CORRECTION MADE BY A TEST, which is the right way round. My first
version precomputed `percentage_display` and `bar_width` in the caller, and
`_generate_phase_html` then demanded keys its own existing test does not pass --
that test calls it directly with just `completion_percentage`. The formatting
decision belongs in the renderer, so it derives both locally and the existing
assertions pass unchanged. A renderer that requires pre-formatted input cannot
be tested in isolation.

AND ONE CEILING RESPECTED RATHER THAN RAISED. Inlining the two helpers took
`tracker.py` from 571 to 604 lines, past the 600 ceiling. They now live in
`project_state_tracking/phase_report.py`, which brings it to 583 -- split, not
pinned, and no baseline entry added.

Contrast pair for the new render path: a phase with no figure renders
`not scored` with a 0-width bar and explicitly NOT `0.0%`; the control asserts a
phase with a figure still renders it.

    validation_dashboard_generator_test.py   5 passed (2 new)
    tracker_test.py                          3 passed
    phase_validation_paths_and_skips         29 passed
    check_python_file_size.py                clean (tracker 583 < 600)
    flake8 (5 touched files)                 clean

Refs #17089
The `python-suite shard 8/12` red on #17505. Cause, from the shard log:

    repo_tests/phase_validation_paths_and_skips_17089_test.py leaves
    sys.modules key 'phase_score' installed outside repo_tests/
    (first seen at tools/lint/check_spdx_header_test.py)

Mine, and the guard was right. `_policy()` loads the policy module by path and
must register it in `sys.modules` WHILE the body executes -- `dataclasses`
resolves `cls.__module__` through it and the class body raises without it -- but
I left it there afterwards. A module parked under a bare name can be picked up
by anything later in the same process, which is why the file that noticed was
an unrelated one in another directory entirely.

Registration is now scoped to the exec and removed in a `finally`, and the
loaded module is cached so repeated calls neither re-execute it nor re-enter
`sys.modules`.

WHAT I COULD NOT REPRODUCE, STATED PLAINLY. I could not make the leak guard fire
locally, with or without the fix: the same two files that CI failed on pass here
and the guard prints no section at all, for a reason I did not identify. So this
is not "verified locally" -- the guard's own verdict is CI's to give. What I did
verify is the mechanism the guard named, directly:

    phase_score in sys.modules before _policy():  False
    after _policy():                              False
    policy usable:                                True
    cached (same object on 2nd call):             True

That is the property the shard complained about, checked without relying on an
instrument I could not get to run. The shard log also noted the local
environment is below four dependency floors (websockets, starlette, uvicorn,
greenlet), which is a separate pre-existing condition and not this failure.

    repo_tests/phase_validation_paths_and_skips_17089_test.py   29 passed
    with tools/lint/check_spdx_header_test.py                   50 passed

Refs #17089

@mrveiss mrveiss left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Verdict: the red is not yours, and the work is good

python-suite shard 1/12 is a genuine failure (zero cancellations on this PR, so it is not
the serialisation sweep), but it is not caused by this change. It fails 24 seconds before
any test runs, in the dependency-floor gate:

4 of 182 declared versions are NOT satisfied by the interpreter running this check (python 3.14.7).
This IS the CI job's own environment -- these are the packages CI itself installed
  opentelemetry-api:                      installed 1.45.0, declared ==1.44.0 (requirements.txt:51)
  opentelemetry-sdk:                      installed 1.45.0, declared ==1.44.0
  opentelemetry-exporter-otlp-proto-grpc: installed 1.45.0, declared ==1.44.0
  opentelemetry-proto:                    installed 1.45.0, declared ==1.44.0
##[error]Process completed with exit code 1.

Timeline in the log: the gate exits at 12:06:17; the first test output is at 12:06:41.
And this PR touches no requirements file — the 11 changed paths are the phase-validation
modules, its test, and the two size-ratchet mirrors.

Why it will keep happening until someone owns it

requirements.txt:51-58 pins opentelemetry-*==1.44.0. The CI plane
(requirements-ci.txt → requirements-ci/*.txt) never declares opentelemetry at all — I
grepped the whole tree for it. So CI resolves it transitively and unconstrained, takes whatever
is newest, and the strict floor check then compares that against the production pin. The drift
appears the moment upstream publishes, on whichever PR next builds a cold cache — which is why
it lands on an unrelated PR rather than on the change that caused it.

That is precisely the two-declaration-planes defect of #17448, and #17502 adds
constraints_differ() plus a constraint-drift baseline that detects this class. This instance
is not in that baseline, so it is worth a line on #17448 either way.

Nothing for you to do here beyond a rerun once the planes agree. Please do not chase it.

Acceptance criteria — all four met

  • CI mode no longer shows a completion %. as_report() omits completion_percentage
    entirely when anything was skipped, and emits structural_presence_percentage plus
    not_checked instead. Test: test_the_report_omits_completion_percentage_entirely_when_anything_was_skipped.
  • Feature-check paths exist, with a guard. The autobot-vue/ references survive only in
    comments describing the defect. TestEveryReferencedPathExists covers both the criteria
    paths and the lambda paths, and test_the_parse_found_* guards against the scanner
    silently finding nothing — which is the right shape, since an empty parse would make the
    other two vacuous.
  • UI/UX defers to the real gates. defers_to makes the phase report no figure and name
    the authoritative workflows. test_the_ui_phase_declares_the_workflows_that_verify_it,
    and test_a_deferring_phase_contributes_nothing_to_the_aggregate closes the hole where
    the number would still reach the gate through the average.
  • A skipped phase cannot read complete.
    test_a_phase_with_a_skipped_group_is_not_complete_even_at_100_percent.

What is notably right

Three decisions I would have got wrong:

Withholding the claim instead of capping the number. The docstring's reasoning is correct —
a cap makes 99% mean two things and invites someone to raise it later.

completion_percentage absent rather than zero or null. A consumer reaching for it gets a
KeyError instead of a number it would have believed. That is the same discipline as a floor
guard raising rather than returning an empty set.

ready_for_production: None, not False. "Not assessed" and "assessed and failed" are
different facts and False would have collapsed them — which is the exact inversion this PR
exists to remove, so it is good to see it applied to the aggregate too.

TestEveryMethodItCallsOnItselfExists and TestTheSkipListStaysInStepWithWhatIsSkipped are
both guarding the staleness failure mode rather than the current values. That is the part
that stops #17089 recurring.

One non-blocking note

PhaseScore.complete and PhaseScore.status ignore defers_to, while as_report()
short-circuits and hardcodes complete: False for a deferring phase. So a deferring phase with
no skips and a high ratio returns complete == True from the property and "complete": False
from the report.

Nothing reads the properties today — phase_progression_manager.py:453 correctly reads
phase_data["complete"] from the dict — so this is latent, not live, and I am not asking you
to hold the PR for it. But it is the same trap the PR removes elsewhere: two accessors for one
question that disagree, where the safe one happens to be the one in use. A one-line
if self.defers_to: return False in complete, and the matching branch in status, would
close it before someone reaches for the property.

@mrveiss

mrveiss commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Second review pass — e5 reviewed this first, so this is not the initial one. One thing to fix before merge, and it is a regression this PR introduced rather than a pre-existing gap. Everything else is verified sound or filed separately.

The core fix is real, and I tried to break it

PhaseScore with complete / skipped / defers_to does close the literal bug. Every one of the ten phases declares at least one LIVE_STACK_GROUPS key, so under --ci-mode — the only mode CI runs — complete can never be True anywhere, and a skipped group cannot contribute a believed figure. The skip path is honest in the way that matters: not_checked, complete=False, completion_percentage omitted rather than zeroed, and overall_maturity / ready_for_production as None rather than False. Omitting the key instead of publishing a 0 is the right call — a 0 is a measurement and an absent key is not.

TestSkippedCanNeverReadAsComplete.test_a_phase_with_a_skipped_group_is_not_complete_even_at_100_percent asserts PhaseScore(ran=2, passed=2, skipped=(...)).complete is False at percentage == 100.0. That test fails if the policy reverts to counting file existence, which is the mutation proof this needed.

phase_progression_manager.py switching both progression and prerequisite gates from >= validation_threshold to phase_data["complete"] closes exactly what #17089 named, and tracker.py filtering None figures out of snapshots rather than storing them as 0 is the same discipline carried through.

Ratchet verified compliant: phase_progression_manager.py 851 → 850 in both repo_tests/python_file_size_ratchet_baseline.py:302 and scripts/python_file_size_known_large.py:297, and 850 matches the file's actual length at this head. Down, both mirrors consistent.

Fix before merge — the schema rename broke a consumer this PR did not update

.github/workflows/phase_validation.yml, the Comment Phase Status on PR step:

comment += `**System Maturity:** ${results.overall_maturity?.toFixed(1)}%\n\n`;
comment += `${emoji} **${phase.name}**: ${phase.completion_percentage?.toFixed(1)}%\n`;

Under the new schema _output_json_results emits structural_presence_percentage, not completion_percentage, and overall_maturity is None for every --ci-mode run. Both expressions evaluate to undefined, so the comment renders **System Maturity:** undefined% and undefined% per phase.

Fix: read structural_presence_percentage, and render "not scored" when authoritative_gates is present — which is what validation_dashboard_generator.py already does correctly in this same PR. Two lines.

Why it is not visible today, and why that makes it worth fixing now. The step is gated on github.event_name == 'pull_request', and the workflow's PR trigger is:

pull_request:
  branches: [ release ]

This repo's PR base is main. So the trigger never fires for a PR to main — confirmed, gh pr checks 17505 shows no phase-validation job on this PR at all — and the broken consumer is currently unreachable. Two defects are masking each other. Fixing the trigger scope alone would immediately surface undefined% on every release PR, and whoever fixes it will not be looking at this PR. That is the argument for the two lines here rather than a follow-up.

I have deliberately not claimed "every PR comment will read undefined%". It will, for PRs targeting release; for PRs to main no comment is posted at all.

The trigger scope itself is pre-existing and out of this PR's stated scope — worth a follow-up so the gate this PR hardens actually blocks the PRs it is meant to, rather than only running post-merge on push. Stating it because "the gate is now a real failing gate" is true (exit 1, no continue-on-error) and "the gate runs on pull requests" is not.

Filed separately — #17559

_validate_single_feature ends with return True when the validator map has no entry for a feature name, so an unmeasured feature reports implemented. production_features declares containerization, scalability and deployment_automation; _get_feature_validators() returns 21 entries and none of them is any of those three.

That is the same inversion this PR fixes, one layer below it. It does not overlap with the fix — your live-stack grouping means those groups are skipped in --ci-mode, so the path is unreachable in CI while remaining live outside it. Pre-existing, untouched here, and the fix is a behaviour change to a path this PR does not otherwise enter, so it is #17559 rather than something to fold in. The criterion that matters there is the correspondence guard, not the three validators: TestTheSkipListStaysInStepWithWhatIsSkipped is already the pattern for it.

Worth separating two claims that read as one: "#17089 is fixed" is true; "an unmeasured feature cannot report implemented" is not yet.

Minor

validation_dashboard_generator.py:385 — _get_status_color(_phase_percentage(phase_data) or 0.0) colours a deferred or unscored phase the same critical red as a genuinely failing one, while the adjacent text correctly says "not scored". A neutral colour would match the text. The new dashboard test hand-supplies status_color: "#9E9E9E" rather than going through _format_phase_details, so the real computation is unguarded — that is the part worth changing, more than the colour.

phase_validation_system.py:954 — results.get("overall_maturity", 0): the key is always present (set to None or a float), so the 0 default is dead and reads as a guarded default that is not guarding anything. With --output-format summary, None through %.1f produces a logging error rather than a crash. .get("overall_maturity") with an explicit None branch, as the per-phase loop already does.

Not verified

  • The run counts in the body ("22 passed", "73 passed", "20 passed", flake8 and file-size clean) — I did not execute the suite.
  • "criteria paths referenced / missing: 39 / 0" and "feature-check paths: 15 / 0" — plausible from reading PHASE_CRITERIA and the lambda map; I did not run the scanner.
  • That phase_validation_system.py is never imported on the test collection path (the stated reason the redis_client side effect is avoided) — consistent with the design, not independently confirmed.

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

Labels

None yet

Projects

None yet

1 participant