Skip to content

refactor(#288): architecture debt - extract health, rename list, update C4 - #303

Open
sshekhar563 wants to merge 5 commits into
qbtrix:devfrom
sshekhar563:fix/arch-debt-288
Open

refactor(#288): architecture debt - extract health, rename list, update C4#303
sshekhar563 wants to merge 5 commits into
qbtrix:devfrom
sshekhar563:fix/arch-debt-288

Conversation

@sshekhar563

Copy link
Copy Markdown
Collaborator

Resolves #288.

This PR addresses four architectural debt and maintainability items identified in the dev audit:

1. Extract Duplicated Health & Cleanup Logic

  • Problem: Health audit and cleanup logic was copy-pasted between src/soul_protocol/mcp/server.py and src/soul_protocol/cli/main.py. The two implementations had already drifted (the CLI gained orphan-node removal and backup; MCP did not).
  • Solution: Extracted shared logic to a new module src/soul_protocol/runtime/health.py.
    • Exposes dataclasses HealthReport and CleanupResult so callers can customize the formatting (JSON for MCP vs Rich panel for CLI).
    • Exposes audit_health(), plan_cleanup(), and execute_cleanup().
    • MCP Drift Fixed: The MCP cleanup tool now properly includes orphan-node pruning.

2. Solve CLI Builtin Shadowing (list)

  • Problem: The CLI's list() command shadowed Python's builtin list inside src/soul_protocol/cli/main.py, forcing 14 ugly builtins.list(...) workarounds throughout the file.
  • Solution: Renamed the Python function to list_cmd(). Kept the CLI registration name as @cli.command("list") so there is zero change to the user-facing CLI command name (soul list). Removed builtins import and replaced all workarounds with standard Python list(...).

3. Regenerate Stale C4 Model

  • Problem: docs/c4/model.json was missing 9 subsystems added since June 2026.
  • Solution: Regenerated the model to include: engine, spec, eval, optimize, cognitive, context, dream, eternal, and health, along with 10 new relationships connecting them. Updated the CLI component description to reference the command module split.

4. Document Soul Class Split Plan

  • Problem: The Soul orchestrator is a 3,300+ line class that causes constant merge conflicts across feature branches. Splitting it requires a dedicated multi-PR effort.
  • Solution: Added a clear TODO(#288) at the top of src/soul_protocol/runtime/soul.py outlining the recommended mixin split plan to keep future efforts aligned.

Verification

  • Ran python -m pytest tests/test_cli/test_health_cleanup_repair.py -v (All 21 health/cleanup tests passed).
  • Ran full test suite. (All 258 tests passed except the test_private_key_has_0600_permissions test, which is a pre-existing failure on Windows).
  • Verified soul list, soul health, and soul cleanup work identically via the CLI.

@prakashUXtech prakashUXtech 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.

This is the good kind of extraction. I checked the thing I was most worried about, which is whether pulling health and cleanup into a shared module quietly flattened the CLI down to the weaker MCP version, and it did not: orphan-node removal, --low-importance, and the .soul.bak backup all survive, every --no-* toggle is still wired, and the thresholds (dedup 0.8, importance <= 2, the "Scored " staleness check) are unchanged. You also correctly left the backup in the CLI caller rather than dragging it into the shared module, which is the right call. The list rename is spot on too: keeping the explicit @cli.command("list") means soul list is unchanged for users, and that's the detail most people miss. All 14 builtins.list(...) workarounds and the import builtins are gone.

Deferring the Soul split with a concrete TODO(#288) is the right judgment. That one shouldn't be a drive-by.

Two blockers:

  1. MCP soul_cleanup gained a destructive capability without the CLI's guard. mcp/server.py:1574 hardcodes orphan_nodes=True, so graph entities can now be permanently pruned through an LLM-invoked tool. On the CLI that same operation is deliberately paired with backup_soul_file() before the export, but the MCP path just marks the soul modified and saves on shutdown. No backup, no opt-out, no dry-run. The drift you're fixing went the other way on purpose: the CLI author added the backup because orphan pruning is irreversible. Either add backup_soul_file to the MCP path or expose orphan_nodes: bool = False so the caller opts in. Roughly five lines.

  2. This collides head-on with your own open #299. That PR touches the same files and the same lines, adding the public accessors and replacing the private-attribute access in the CLI and MCP. This PR moves those exact blocks into runtime/health.py keeping the private access verbatim, so if #299 lands first, health.py quietly reintroduces the coupling #299 was written to delete, and whichever merges second will conflict on those hunks. Let's pick an order: I'd land #299 first, then rebase this onto its public API. Worth deciding before either merges.

Also worth fixing:

  1. The new health filter can swallow real issues. cli/main.py:3422-3425 string-matches on "duplicate" and "orphan" to un-flatten the merged issues list. Skill ids are agent-learned and user-controllable, so a skill named something like orphan-cleanup with negative XP produces an issue string containing "orphan", gets filtered out, and isn't counted in issues_found. The panel can then print "No issues found, soul is healthy" while a real problem exists. Dev didn't have this hazard because it iterated separate bond_issues and skill_issues lists. Keeping those as structured fields on HealthReport (with issues as a derived property) fixes it and removes the fragile matching.

  2. Two of the new C4 relationships aren't real. memory-tiers -> engine and context -> engine: nothing under runtime/memory/ imports soul_protocol.engine (the actual importers are cli/journal.py, cli/org.py, and spike/memory_journal.py), and runtime/context/store.py uses sqlite3 directly plus spec.context.models, never engine/. This matters more than a doc nit because loom reads model.json for blast-radius and boundary rules, so a wrong edge turns "returns nothing" into "returns something wrong", which is the worse failure. Suggest cli -> engine and context -> spec instead. The nine new components all check out. Minor: the note says "Regenerated" but the diff is clearly hand-authored, which is fine (and correctly keeps authored: true), just word it that way.

  3. The MCP cleanup response renamed removed to total_removed. Any external MCP consumer reading data["removed"] now gets a KeyError. Nothing in-repo depends on it, but MCP tools get called by arbitrary agents, so either keep removed as an alias or call the rename out in the PR body.

  4. No tests import the new module. Coverage is entirely indirect through the CLI end-to-end tests (which is what gave me confidence the CLI didn't regress) and four key-presence MCP tests. That MCP shape-only assertion is exactly why the removed rename went green. For a pure refactor this is the main risk, since a green suite proves very little about whether the extraction preserved semantics. A tests/test_runtime/test_health.py covering audit_health field by field, plan_cleanup per flag, and execute_cleanup's return count would close it. There's also no orphan-node test on either side.

Smaller: execute_cleanup does removed += 1 unconditionally while remove() returns a bool, and dedup plus low-importance can select the same id, so the count can overreport (copied from dev, so not a regression, but this is the natural place to fix it). The Updated: (#288) header lines are missing on cli/main.py and mcp/server.py. And lint is red only because the new file needs formatting: uv run ruff format src/soul_protocol/runtime/health.py.

The core of this refactor is sound. Everything above is at the edges.

@prakashUXtech

Copy link
Copy Markdown
Contributor

Great response to the last round: five of the six findings are fixed, each with a regression test. The MCP soul_cleanup is now opt-in (orphan_nodes=False, dry-run default) with a best-effort backup before it prunes; the health issues are structured fields, so the orphan-cleanup skill-name hazard is gone (and there's a test using that exact adversarial name); the two fabricated C4 edges are replaced with real ones (cli -> engine, context -> spec); removed is kept as an alias alongside total_removed; and there's a real tests/test_runtime/test_health.py. Nicely done.

The one remaining item isn't a code bug, it's sequencing with #299. runtime/health.py still reaches through private internals (about 23 accesses) because it didn't adopt #299's public API, and those methods don't exist on dev yet since #299 introduces them. Both PRs edit the same regions of cli/main.py, mcp/server.py, and soul.py, so whichever merges second will conflict.

The plan I'd suggest: land #299 first (it's approved and ready), then rebase this so health.py calls the public API (episodic_entries(), graph_entities(), graph_remove_entity(), Soul.memory, eval_history). That drops the 23 private accesses to near zero and makes this PR genuinely debt-reducing rather than relocating the coupling. Once #299 is on dev and you've rebased, ping me and I'll re-review and approve. I'm holding the formal approve only on that rebase; the code itself is in good shape.

sshekhar563 added a commit to sshekhar563/soul-protocol that referenced this pull request Jul 27, 2026
@prakashUXtech

Copy link
Copy Markdown
Contributor

The thing I blocked on last round is done, and done properly. health.py is at zero private accesses (from 23), and I verified every public method it now calls actually exists on dev rather than taking the swap on faith.

I also specifically checked the failure mode this kind of refactor usually introduces, and you avoided it: the public getters return list(...) copies, so if the cleanup had mutated what it reads it would now silently no-op. It doesn't. plan_cleanup captures values (memory IDs into sets, node names into a list) rather than holding a reference to a live collection, and execute_cleanup removes by ID through methods that mutate the real store. That's the right shape.

The blocker now is different, and it's the branch rather than the code: this branch is 16 commits behind dev and is missing #299 itself — the very PR that introduces the public API you're calling. So at the current tip, mm = soul.memory raises AttributeError on the first statement of audit_health, plan_cleanup and execute_cleanup; soul health, soul cleanup and both MCP tools are broken, along with the 21 existing health tests. Merging dev fixes all of it — but it does mean nothing in this PR has ever actually run.

Which is why the PR body needs updating: "all 21 health/cleanup tests passed / all 258 tests passed" was true for the earlier commit, not this one. Also worth knowing that GitHub can't run CI on a conflicting PR at all (there's no merge ref to build), so the missing checks aren't pending, they're unobtainable until you resolve. After the merge, I'd like to see a real green run on the result rather than a local claim.

Two smaller things:

  1. One genuine behavior change is riding along in a PR billed as a pure extraction. Removal counting is now accurate — it counts actual removals via the bool return instead of incrementing unconditionally. That's more correct and you tested it well, but it means "Cleaned N items" can now report fewer than the dry-run preview's total, since the preview still uses len(item_ids). Worth a line in the body so it isn't a surprise.
  2. The new test_health.py uses 5 private accesses of its own, which slightly undercuts the PR's thesis. Two have public equivalents already (semantic_facts(), graph_entities()); only add_entity genuinely lacks a wrapper.

Perf nit while you're in there: execute_cleanup calls mm.graph_entities() inside the per-node loop, rebuilding the list each iteration. Hoist it to a set before the loop.

So: merge dev, resolve the three conflicts (they're all the same #299 private-to-public pattern you've already applied in health.py), push, and let CI go green. Ping me and I'll re-review — everything substantive is already in place.

@prakashUXtech prakashUXtech 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.

Stop before you debug the CI failures — they're not what they look like, and the cause is mechanical.

The conflict resolution deleted code that was already on dev. You merged dev correctly (it's a clean ancestor of your head), but resolving cli/main.py took your side wholesale, which removed two landed features:

                  on dev   your head
  --min-relevance     3   →    0     #249 — the --min-relevance flag
  recall_score        1   →    0     #249 — the score field in recall --json
  deprecated          2   →    0     #236 — the soul remember deprecation notice

That's why the three test jobs are red: test_recall_score.py fails with "No such option: --min-relevance" and KeyError: 'score', and test_remember_command fails asserting "deprecated" is in the output. Those tests are correct and your branch removed the features they cover. Had this merged, it would have silently reverted #249 and #236.

The fix is to redo the merge and keep BOTH sides in cli/main.py. Your health/cleanup extraction and dev's recall-score + deprecation code don't actually overlap in behaviour — they just landed near each other in the same file. Concretely:

git checkout fix/issue-288-architecture-debt
git reset --hard origin/dev            # start from dev, keep nothing of the bad resolution
git merge <your-last-good-commit>      # or cherry-pick your health.py + extraction commits
# when cli/main.py conflicts: keep dev's --min-relevance, score and deprecation blocks,
# and add your extraction changes alongside them
uv run pytest tests/test_cli/ -q       # this is the check that catches a clobber

A quick way to self-check before pushing: git diff origin/dev...HEAD -- src/soul_protocol/cli/main.py | grep '^-' and read every deleted line. If you see anything you didn't intend to remove, the resolution ate something.

The rest of the PR is in good shape, so this is purely the merge. health.py is genuinely at zero private accesses, all the public methods you call exist on dev, and I verified the copy-vs-live trap was avoided (you capture IDs and remove by ID rather than mutating a returned list). Once tests/test_cli/ is green alongside your health tests, ping me and I'll re-review.

Two smaller things from my earlier comment that still apply: the PR body's "all 258 tests passed" predates this head, so please re-run and update it; and the removal-count change (counting actual removals rather than attempts) is a real behaviour change worth a line in the body, since "Cleaned N" can now be lower than the dry-run preview.

…name list, update C4

Redone cleanly on top of upstream/dev to preserve qbtrix#249 (--min-relevance,
recall score) and qbtrix#236 (soul remember deprecation) which were lost in the
previous conflict resolution.

1. Extract health/cleanup to runtime/health.py (CLI + MCP delegate)
2. Rename def list() -> list_cmd() with @cli.command('list')
3. Add TODO(qbtrix#288) soul.py class split recommendation
4. Update C4 model.json
5. Add tests/test_runtime/test_health.py

@prakashUXtech prakashUXtech 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.

The clobber is fixed properly, and this is the first green CI this PR has had. I re-checked every dev feature the earlier resolution ate — --min-relevance 3/3, recall_score 1/1, the remember deprecation 2/2, --password 33/33 — all present byte-for-byte, dev is a full ancestor, and health.py is at zero private accesses. All the prior-round items hold too: orphan_nodes opt-in with the backup guard, the two fabricated C4 edges gone, removed kept alongside total_removed, and the CLI cleanup richness intact. Health tests are 24 + 4, all passing from a clean export. Good redo.

One blocker, and it's a new one that CI can't see: this head has an encoding corruption.

docs/c4/model.json now starts with a UTF-8 BOM (ef bb bf — dev's starts with {), and five pre-existing en-dashes were rewritten from e2 80 93 into ce 93 c3 87 c3 b4, which renders as ΓÇô — classic cp437 mojibake, the signature of a file round-tripped through a Windows console. I verified via git show | xxd, not terminal display. The consequence is concrete: json.load fails with "Unexpected UTF-8 BOM", so loom's C4 source treats the model as present-but-unparseable and silently drops the whole architecture layer. Item 3 of this PR is "regenerate the stale C4 model" — this head makes it unreadable instead. The same corruption is in tests/test_mcp/test_server.py (11 lines, including the importorskip reason string) and src/soul_protocol/runtime/health.py (5 comment lines).

The fix is mechanical: strip the BOM, restore the UTF-8 dashes, recommit. If you're on Windows, worth checking your editor/git core.autocrlf and encoding settings so this doesn't recur — it'd have gone unnoticed here if the reviewer had only looked at the diff in a terminal.

Two smaller ones while you're in there: the removal-count behaviour change (counting actual removals rather than attempts, so "Cleaned N" can be lower than the dry-run preview) is only mentioned in health.py's header — it needs a CHANGELOG line, since it's user-visible. And the PR body's test claims are stale for this head ("21 tests", "258 passed"); the health file has 24 and the suite collects 3087.

Fix the encoding and this merges.

…ELOG entry

- Stripped BOM from docs/c4/model.json (was causing json.load failure)
- Replaced 19 cp437-mojibake em-dashes (ΓÇö → —) across health.py,
  test_server.py, and model.json
- Added CHANGELOG entry documenting the cleanup removal-count
  behaviour change (actual removals vs attempted)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants