Skip to content

refactor(slm): split four frozen modules so their ceilings stop blocking work (#15495, #15496) - #16099

Merged
mrveiss merged 17 commits into
Dev_new_guifrom
issue-15495-split-frozen-modules
Sep 10, 2026
Merged

mrveiss merged 17 commits into
Dev_new_guifrom
issue-15495-split-frozen-modules

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

I picked up #15495 (per-node LLM capability probe) and #15496 (llm_mode + preset resolver) and got
blocked before the first edit. Every module the pair needs to touch sits at exactly its recorded
size ceiling, and the ratchet is explicit about what that means:

autobot-slm-backend/models/database.py: 1231 lines, over its recorded ceiling of 1229.
A grandfathered file may not grow (#14236) — the exemption freezes the size it was
granted for, it does not license more.

scripts/python_file_size_known_large.py gives exactly one sanctioned response: "Never add an
entry to make a new file pass; split the file instead."
I checked whether that is enforced or
merely documented before acting on it — every one of the last fifteen commits to that baseline is
a decrease, and the audit runs as a required check in code-quality.yml:654. It is enforced.

So this PR is the splitting, and nothing else. No feature lands here. #15495/#15496 follow in a
second PR that rides the headroom this creates, which keeps each half inside one honest review pass.

Boundaries were chosen where the code was already separable, not where the line count worked out:

  • The three heartbeat builders needed no agent state beyond what is now passed explicitly. That is
    what made them safe to lift out as free functions, and it is why the move is provably behaviour-free.
  • The NPU block was banner-delimited and referenced nowhere else in schemas.py — zero remaining
    NPU references there after the move.
  • Only the seven enums preceding the first mapped class moved. The twelve further down
    database.py stay: each sits beside the model whose column it constrains. Moving them would buy
    lines and cost cohesion, and "why these seven and not those twelve" has a real answer rather than
    a convenient one.
  • The five SSH helpers build or read an ssh/sshpass invocation and touch no router, session or
    model state.

What Changed

file before after extracted to
slm/agent/agent.py 739 675 slm/agent/heartbeat_payload.py
models/schemas.py 2335 2181 models/npu_schemas.py
models/database.py 1229 1135 models/enums.py
api/nodes.py 3017 2927 api/node_ssh_helpers.py

Every block moved verbatim. models.database and models.schemas re-export the moved names, so
all 67 modules importing from them are untouched; only api/npu.py changes an import, because its
symbols moved to a module that did not previously exist.

Both baselines (scripts/python_file_size_known_large.py and
repo_tests/python_file_size_ratchet_baseline.py) are lowered in the same commit. That is required
in both directions — the audit fails on an unlowered ceiling too, not only on growth, because an
unlowered ceiling re-licenses the lines just cut.

slm/agent/heartbeat_payload.py is mirrored to the ansible copy and has its own copy: task in
roles/slm_agent/tasks/main.yml.

Verification

  • scripts/check_python_file_size.py --audit-ceilings — 5295 files walked, 0 problems
  • pipeline-scripts/detect-agent-code-drift.sh — 12 pairs byte-identical
  • pre-push pytest — pass (it caught one real break first; see below)
  • every moved symbol resolves to exactly one definition. The sweep flagged CodeStatus ×2 and
    RoleStatus ×3; both are pre-existing and unrelated — a test-local stub in
    test_node_update_availability_11964.py and role_detector.py's dataclass of the same name.
  • all four new modules and every edited file parse

The pre-push hook caught a genuine break and it is worth recording. api/nodes_test.py
AST-extracts the two SSH argv builders from nodes.py by path rather than importing them, so
moving them made it find zero. Its own assertion message is "Rename tracking broke." — the guard
was written to fail loudly on exactly this, and it did. Fixed by repointing it at
node_ssh_helpers.py (second commit).

I deliberately did not take the simplification that move now allows. The AST-extraction exists
because nodes.py registers FastAPI routes at import time; node_ssh_helpers.py does not, so the
test could import it directly. But the helper module imports models.schemas, which this suite
stubs with MagicMock Pydantic, so switching to a real import is a behaviour change I cannot verify
from here. Repointing is guaranteed-equivalent; the simplification belongs in its own change.

One thing I found and did not fix here, to keep this a pure move: the agent module deploy list
in roles/slm_agent/tasks/main.yml is hand-maintained, one copy: task per module, and nothing
checks it against the directory. A new module that is mirrored but not shipped would fail only at
runtime on a node. I verified all 7 production modules are currently covered. Filing separately —
it is the same defect class as #13148 and #15684.

Model Used

Claude Opus 5

Refs #15495, #15496 — deliberately Refs, not Closes. This PR delivers neither issue;
it removes the size ceilings that block both, and the feature lands in a follow-up.

It also carries two things the split paid for and should not lose: the fix for the migration
gate red (status_enums.py top-level, per #16019's recorded route) and
repo_tests/by_path_loaded_modules_are_package_free_test.py, which turns that twice-hit
constraint from a comment into a pre-push failure. Related: #16100, #16101.

t added 2 commits September 9, 2026 09:03
…ing work (#15495, #15496)

Four of the modules #15495 needs to touch sit at exactly their recorded size
ceiling, and the ratchet says what that means:

  models/database.py: 1231 lines, over its recorded ceiling of 1229. A
  grandfathered file may not grow (#14236) -- the exemption freezes the size
  it was granted for, it does not license more.

scripts/python_file_size_known_large.py gives the only sanctioned response:
"Never add an entry to make a new file pass; split the file instead." Every
one of the last fifteen commits to that baseline is a decrease, so this is
enforced practice, not aspiration. This commit does the splitting; no
behaviour changes and no feature lands here.

  agent.py       739 -> 675   payload builders  -> slm/agent/heartbeat_payload.py
  schemas.py    2335 -> 2181   NPU worker block  -> models/npu_schemas.py
  database.py   1229 -> 1135   status enums      -> models/enums.py
  nodes.py      3017 -> 2927   SSH test helpers  -> api/node_ssh_helpers.py

Each block moved verbatim. Boundaries were chosen where the code was already
separable rather than where the line count happened to work out:

- The three heartbeat builders needed no agent state beyond what is now
  passed explicitly, which is what made them safe to lift out as free
  functions.
- The NPU block was banner-delimited and referenced nowhere else in
  schemas.py -- zero remaining NPU references there after the move.
- Only the seven enums that preceded the first mapped class moved. The
  twelve further down database.py stay: each sits beside the model whose
  column it constrains, and separating them would cost more than it frees.
- The five SSH helpers build or read an ssh/sshpass invocation and touch no
  router, session or model state.

models.database and models.schemas keep re-exporting the moved names, so all
67 modules importing from them are untouched; only api/npu.py changes an
import, because its symbols moved to a module that did not previously exist.

Both baselines are lowered in the same commit, as
scripts/python_file_size_known_large.py requires -- an unlowered ceiling
re-licenses the lines just cut, and the audit fails on that too, not only on
growth.

Verification:
- scripts/check_python_file_size.py --audit-ceilings: 5295 files walked, 0 problems
- pipeline-scripts/detect-agent-code-drift.sh: 12 pairs byte-identical
- every moved symbol resolves to exactly one definition; the CodeStatus and
  RoleStatus duplicates the sweep found are pre-existing and unrelated (a
  test-local stub and role_detector.py's dataclass of the same name)
- all four new modules and every edited file parse

The new agent module is deployed by its own copy: task in
roles/slm_agent/tasks/main.yml. That list is hand-maintained, one task per
module, and nothing checks it against the directory -- a new module that is
mirrored but not shipped would fail only at runtime on a node. Filed
separately rather than fixed here to keep this commit a pure move.
@coderabbitai

coderabbitai Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 05126da0-843d-4e53-8164-3c3b4fd87040


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.

@mrveiss

mrveiss commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Filed the deploy-list gap I flagged in the PR body as #16100 — a guard that the slm_agent copy: task list and the canonical agent directory describe the same set, in both directions, with a positive control.

Worth naming why it belongs with #13148 and #15684 rather than beside them: the drift check for this exact directory pair went green while heartbeat_payload.py was mirrored and not deployed. The mirror is what hides it — byte-identity holds, so the guard that exists reports clean, and the module is simply absent on every node. Content parity and shipping parity are different claims about the same two directories, and only one of them is checked.

@github-actions

github-actions Bot commented Sep 9, 2026

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.

…d they go (#15495)

The migration gate went red on the first push:

  File "autobot-slm-backend/models/database.py", line 32, in <module>
      from .enums import (  # noqa: F401
  ImportError: attempted relative import with no known parent package

Three steps in .github/workflows/slm-migration-gate.yml load models/database.py
with spec_from_file_location to run Base.metadata.create_all the way production
does. Loaded that way the module has no parent package, and base's database.py
imported nothing from models/ at all -- the by-path load was package-free, and
this commit's parent was the first change to break that.

Switching to `from models.enums import` fixes the relative-import error and
walks into the next one: importing anything from the models package runs
models/__init__.py, which pulls in user_management and thence autobot_shared,
which that loader does not have.

Both of which models/database.py:302 already says, and I edited past it:

  # ServiceStatus moved to top-level service_status.py (#16019) -- this file is
  # AT its ceiling, and models/ would drag in autobot_shared. See that docstring.
  from service_status import ServiceStatus  # noqa: E402,F401

service_status.py's docstring names this failure by name -- "broke `SLM
migration runner - Postgres 16` with ModuleNotFoundError: No module named
'autobot_shared'". Same wall, same file, second time. The rule I broke is
check the decisions recorded outside the file before editing it: the constraint
was written down at the exact line I was editing, and the ceiling I was working
around is the same ceiling that put it there.

So the enums follow the established route: top-level status_enums.py,
re-exported from models.database, with the reason recorded at both ends so a
later tidy-up into models/ has to argue with it first.

  models/enums.py -> status_enums.py   (rename, contents unchanged)
  models/database.py  1135 -> 1141     (the import comment explaining why)

Verification -- the gate's exact construction, run locally:

  spec_from_file_location("_slm_models_database_14326", "models/database.py")
  -> OK, loaded by path with no package: 33 tables
  -> enums reachable via re-export: NodeStatus, CodeStatus.UNKNOWN

33 mapped classes on base and 33 on this branch: enums are not mapped, so the
schema surface cannot have moved, and it did not.

  ratchet audit: 5297 files walked, 0 problems
  agent drift:   12 pairs byte-identical
@mrveiss

mrveiss commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner Author

Migration gate went red and is fixed — the correction matters more than the failure.

First push:

File "autobot-slm-backend/models/database.py", line 32, in <module>
    from .enums import (  # noqa: F401
ImportError: attempted relative import with no known parent package

Three steps in slm-migration-gate.yml load models/database.py with spec_from_file_location to run Base.metadata.create_all the way production does. Loaded that way it has no parent package — and base's database.py imported nothing from models/ at all, so the by-path load was package-free and my re-export was the first thing to break it.

Switching to from models.enums import fixes that error and walks straight into the next: importing anything from the models package runs models/__init__.py, which pulls in user_management and thence autobot_shared, which that loader does not have.

Both of which models/database.py:302 already said, twenty lines from where I was editing:

# ServiceStatus moved to top-level service_status.py (#16019) -- this file is
# AT its ceiling, and models/ would drag in autobot_shared. See that docstring.
from service_status import ServiceStatus  # noqa: E402,F401

And service_status.py's docstring names the failure exactly: "broke SLM migration runner — Postgres 16 with ModuleNotFoundError: No module named 'autobot_shared'". Same wall, same file, second time. The rule I broke is check the decisions recorded outside the file before editing it — except it wasn't even outside the file. The constraint was written at the line I was editing, and the ceiling I was working around is the same ceiling that put it there.

Fixed by following the established route rather than inventing a third one: models/enums.py → top-level status_enums.py, contents unchanged, re-exported from models.database, with the reason recorded at both ends so a later tidy-up into models/ has to argue with it first.

Verified by running the gate's exact construction locally, not by reasoning about it:

spec_from_file_location("_slm_models_database_14326", "models/database.py")
  -> OK, loaded by path with no package: 33 tables
  -> enums reachable via re-export: NodeStatus, CodeStatus.UNKNOWN

33 mapped classes on base, 33 on this branch — enums are not mapped, so the schema surface could not have moved, and did not. Ratchet 5297/0, drift 12 pairs.

The other three extractions were unaffected: npu_schemas.py and node_ssh_helpers.py use absolute imports and neither is loaded by path.

t added 2 commits September 9, 2026 09:23
…mment (#15495)

models/database.py:302 has said this since #16019:

  # ServiceStatus moved to top-level service_status.py (#16019) -- this file is
  # AT its ceiling, and models/ would drag in autobot_shared. See that docstring.

and service_status.py's docstring names the failing job by name. Both were on
base while the parent commit of this one broke the migration gate in exactly
the way they describe, from an edit twenty lines away. The knowledge was in the
best place it could be -- the file being changed, at the point of the change --
and it was read past twice in two months. Prose is not a guard.

The predicate is measured, not asserted. Two things had to be established
before this could be a test rather than a hunch:

1. Which loads are strict. The workflow has two styles. A dotted module name
   ("models.database") means the step scaffolded a parent package in
   sys.modules first, and package imports resolve. A name with no dot
   ("_slm_models_database_14326") has no package behind it. Only the second
   kind is constrained, and the targets are derived from the workflow rather
   than listed here.

2. Which imports actually fail there. My first draft flagged every first-party
   package import and failed on base: migrations/runner.py is loaded the same
   strict way and imports `from migrations import utils` and
   `from user_management.config import ...` quite happily. The real rule is
   narrower -- an import fails only if that package's __init__ chain reaches
   autobot_shared, which the job does not install. Walking the tree:

     models       REACHES autobot_shared  via user_management.models.user
     middleware   REACHES autobot_shared  via monitoring.prometheus_metrics
     monitoring   REACHES autobot_shared
     api, migrations, services, slm, user_management   clean

   That matches the observed CI behaviour exactly, which the blanket version
   did not.

Verified by extracting the test's own functions and running them against the
tree:

  targets:     migrations/runner.py, models/database.py   -> both CLEAN
  attempt 1    from .enums import NodeStatus     -> relative import: .enums
  attempt 2    from models.enums import ...      -> unreachable package
  the fix      from status_enums import ...      -> CLEAN
  runner's own from migrations import utils      -> CLEAN

So it fails on both of the two things I actually did, and passes everything
that legitimately works there.

Two controls, because a matcher that flags nothing is indistinguishable from a
clean tree: the derivation must keep finding models/database.py among the
strict targets, and the reachability walk must keep finding `models` among the
packages that reach autobot_shared. If either stops, the guard broke rather
than the tree improving.
t and others added 3 commits September 9, 2026 10:11
Black -- 3 files, one cause. Every extraction seam left an extra blank line;
the PostToolUse auto-format never fired because these edits were made by script
rather than through the editing tool. npu_schemas.py, nodes.py, database.py.
Baselines follow the 4 lines out: database.py 1141 -> 1140, nodes.py
2927 -> 2926.

shard 6/12 -- repo_tests/enum_union_guard_test.py anchors to
models/database.py by path for BackupServiceType and
_BACKUP_SERVICE_TYPE_ALIASES, which moved to status_enums.py. Repointed. Kept
to one line so the file stays at its recorded 801: a grandfathered file may not
grow, and a four-line assertion message is not worth a ceiling.

This one is the same class I filed as #16101 an hour ago -- and my own sweep
for it MISSED this instance. The narrowed detector required the anchor literal
on the same expression (`Path(__file__).parent / "nodes.py"`); this composes
from a module-level `SLM` variable, so `SLM / "models" / "database.py"` never
matched. The broad 136-hit pass had it; narrowing to 87 dropped it, and the
single positive control survived because it happened to be of the shape that
survived. One control is a control for one shape.

shard 12/12 -- python_filter_covers_its_guards_test.py, and this one is
pointed: the guard I added last commit reads
.github/workflows/slm-migration-gate.yml by concrete literal, and
.github/filters/python-paths.yml does not cover that file. So adding a fourth
by-path load -- exactly the change the guard exists for -- would have taken the
required-context shim's green with the guard never running. A guard against
"the constraint is not enforced" that is itself unreachable by the filter.

Widened the filter by one entry rather than recording an exemption:
MAX_UNCOVERED_READS stays 39, since the rule is that the record only shrinks.
The comment two lines above my entry describes the same detector blind spot
from the other side -- it recognises `NAME / "a" / "b"` and misses `parents[1]`,
`.parent`, `project_root()`. Mine recognised the roots it missed and missed the
composition it catches.

Verification: black clean on every file touched; ratchet 5299 walked, 0
problems; agent drift 12 pairs byte-identical.

Not fixed here: `Check PR links to its issue`. The body carries
`Refs #15495, #15496` and satisfies both (a) the keyword and (b) the standalone
token for 15495. Re-checking that separately rather than editing the body again
on the assumption it is wrong.
@mrveiss

mrveiss commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

CI diagnosis — code-quality, mechanical

ERROR: autobot-slm-backend/api/nodes.py Imports are incorrectly sorted and/or formatted.
isort check failed

One file, isort only. duplication-guard is failing separately and is not this.

Base is green (26 success / 0 failure at 15689bfec), and this branch is 1 commit
behind — so neither failure is inherited. Both are the branch's own.

@mrveiss

mrveiss commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Both causes measured

1. code-quality — isort, one file

ERROR: autobot-slm-backend/api/nodes.py Imports are incorrectly sorted and/or formatted.

2. duplication-guard — the SLM scope, over by 0.7 lines

The guard's own message says "There are TWO, and this step cannot tell which
failed"
. It is the SLM scope (autobot_shared + autobot-slm-backend + autobot-slm-frontend/src):

Duplicated lines 4729
Total lines 259798
Measured 1.8203 %
SLM_THRESHOLD 1.82 % — identical on base and on this branch
Lines allowed 4728.3
Over by 0.7 lines · 0.0003 pp

The threshold is not the problem and this branch did not move it — I diffed
.github/workflows/duplication-guard.yml on both refs and SLM_THRESHOLD: '1.82'
is unchanged. The guard is correct: splitting four frozen modules added
duplication, and the scope had essentially zero headroom.

Why the log makes this look like a tie

ERROR: jscpd found too many duplicates (1.8%) over threshold (1.8%)

Both values print rounded to 1 dp, so a 0.0003 pp overage renders as 1.8 vs 1.8
— two identical numbers with a failure between them. The real comparison is
1.8203 > 1.82. Anyone reading only that line would reasonably conclude the guard
had misfired.

Options

  1. Extract the shared code the split introduced — genuinely reduces duplication.
  2. Re-pin SLM_THRESHOLD with the documented reason the workflow asks for. Note
    the pinning instruction says rounded UP to 2 dp; 1.8203 rounds up to 1.83,
    which is what a re-pin should use, not 1.82 again.

Option 1 is the right one unless the duplication is genuinely unavoidable — a re-pin
buys 0.7 lines of headroom and the next refactor trips it again.

@mrveiss

mrveiss commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

code-quality root cause: isort, one file.

Failing step is Check import sorting with isort, and the log names exactly one target:

ERROR: autobot-slm-backend/api/nodes.py  Imports are incorrectly sorted and/or formatted.

Nothing else fails — code-quality is the only red required context, and the gate confirms it is green on base, so this is the branch's own change rather than inherited.

Mechanical: isort autobot-slm-backend/api/nodes.py and push. Worth doing in the same push as anything else outstanding here rather than on its own, since each push costs a full workflow fan-out.

Flagging rather than fixing — the session that opened this PR is no longer running, and nodes.py is one of the four frozen modules this PR is splitting, so the import block is being actively restructured and I would rather not reformat under an author who is mid-design. If nobody picks this up, say so and I will take it.

github-actions Bot and others added 2 commits September 10, 2026 07:02
…py (#15495)

The code-quality check failed at step 6, isort, on one file:

  autobot-slm-backend/api/nodes.py  Imports are incorrectly sorted and/or formatted

`from autobot_shared.ssot_config import config` sat above the
`from api.node_ssh_helpers import (...)` block. With the profile and known_first_party
from pyproject.toml, first-party ordering is api < autobot_shared < models, so the
single import moves below that block.

Pure reorder, one line moved. The file is 2926 lines before and after, so the size
ratchet this PR shrinks is untouched and no baseline is regenerated.

Verified with the exact CI invocation rather than bare isort, since --settings-path=.
is what supplies the profile and src_paths:

  python3 -m isort --check-only --settings-path=. --line-length=120 \
    autobot-backend/ autobot-slm-backend/ autobot_shared/   -> clean

black --check on the same file also passes, so the two formatters agree.
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.

1 participant