Skip to content

VSS2-03: Torch-free topology-to-finite-domain adapter (SLM-67)#326

Merged
Tyler-R-Kendrick merged 1 commit into
mainfrom
slm-67-vss2-03-topology-edits
Jul 18, 2026
Merged

VSS2-03: Torch-free topology-to-finite-domain adapter (SLM-67)#326
Tyler-R-Kendrick merged 1 commit into
mainfrom
slm-67-vss2-03-topology-edits

Conversation

@Tyler-R-Kendrick

@Tyler-R-Kendrick Tyler-R-Kendrick commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Implements SLM-67.

Changes

  • Adds src/slm_training/dsl/solver/ package:
    • state.py — immutable HoleId, DomainValue, HoleDomain, FiniteDomainState, SupportVerdict.
    • topology_adapter.py — Torch-free TopologyAction, TopologyEdit, TopologyAdapterConfig, legal_topology_productions, and derive_topology_holes.
    • __init__.py exporting public contracts.
  • The adapter enumerates complete edit tuples (action, production_id, arity, slot_id) for each active topology node instead of independent logits.
  • It avoids importing torch or grammar_diffusion.py; callers pass TopologyNode instances into the Torch-free TopologyNodeLike protocol.
  • Adds tests/test_dsl/test_topology_adapter.py with regressions for completeness, finiteness, uniqueness, round-trip, fingerprint stability, and torch-free import.
  • Documents the work in docs/design/vss2-03-topology-edits.md.

Verified

  • ruff check passes.
  • python -m compileall passes.
  • pytest tests/test_dsl/test_topology_adapter.py passes (6 tests).
  • python -m scripts.repo_policy ok.
  • git diff --check clean.

Honest caveats

  • Fixture/wiring evidence only; no train/eval/bench, model, checkpoint, or ship claim.
  • Capsule-to-topology coordinate binding (mapping each hole to its owning VerificationCapsule) is scaffolded through stable HoleId paths and is a follow-up.
  • No model generation behavior was changed.

Summary by CodeRabbit

  • New Features

    • Added dependency-closed verification capsule graphs for program specifications, including serialization and deterministic dependency analysis.
    • Added finite-domain topology editing support with bounded, complete edit options for grammar nodes.
    • Added public exports for capsule graph and topology solver capabilities.
  • Documentation

    • Added design documentation covering verification capsules and topology edits.
  • Tests

    • Added coverage for graph generation, serialization, dependency handling, topology hole derivation, finite domains, and torch-free imports.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
slm-training Ready Ready Preview, Comment Jul 18, 2026 5:43am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Tyler-R-Kendrick, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d88f87cf-ba95-43cc-997e-ff0054710084

📥 Commits

Reviewing files that changed from the base of the PR and between b7d75b2 and fb92790.

📒 Files selected for processing (4)
  • docs/design/vss2-03-topology-edits.md
  • src/slm_training/dsl/solver/__init__.py
  • src/slm_training/dsl/solver/topology_adapter.py
  • tests/test_dsl/test_topology_adapter.py
📝 Walkthrough

Walkthrough

The PR adds dependency-closed verification capsule graphs for ProgramSpec data and a Torch-free finite-domain topology adapter. It also adds immutable serialization models, package exports, design documentation, and regression tests for both features.

Changes

Verification capsule graphs

Layer / File(s) Summary
Capsule graph model and public exports
src/slm_training/data/progspec/capsules.py, src/slm_training/data/progspec/__init__.py
Defines immutable graph nodes, edges, capsules, dependency kinds, serialization, versioning, and package-level exports.
Graph derivation and verification
src/slm_training/data/progspec/capsules.py, tests/test_data/test_progspec.py, docs/design/vss2-01-verification-capsules.md
Derives statement/root graphs, reference and external dependencies, forward-reference validation, and Tarjan SCC capsules; tests cover structure, round trips, determinism, nesting, and dependencies.

Finite-domain topology solver

Layer / File(s) Summary
Finite-domain state contracts
src/slm_training/dsl/solver/state.py
Adds immutable hole identifiers, domain values, finite domains, support verdicts, serialization, uniqueness validation, and deterministic fingerprints.
Topology hole derivation and exports
src/slm_training/dsl/solver/topology_adapter.py, src/slm_training/dsl/solver/__init__.py, tests/test_dsl/test_topology_adapter.py, docs/design/vss2-03-topology-edits.md
Adds bounded topology edit enumeration, legal production filtering, active-node selection, Torch-free imports, solver exports, and regression coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProgramSpec
  participant CapsuleGraphDeriver
  participant TypedAST
  participant TarjanSCC
  ProgramSpec->>CapsuleGraphDeriver: provide specification
  CapsuleGraphDeriver->>TypedAST: walk scopes and references
  TypedAST-->>CapsuleGraphDeriver: dependency edges
  CapsuleGraphDeriver->>TarjanSCC: compute reference components
  TarjanSCC-->>CapsuleGraphDeriver: verification capsules
Loading
sequenceDiagram
  participant TopologyTree
  participant TopologyAdapter
  participant GrammarCodec
  participant FiniteDomainState
  TopologyTree->>TopologyAdapter: provide bounded active nodes
  TopologyAdapter->>GrammarCodec: query legal productions
  GrammarCodec-->>TopologyAdapter: production identifiers
  TopologyAdapter->>FiniteDomainState: encode topology edit domains
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a Torch-free topology-to-finite-domain adapter for SLM-67.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slm-67-vss2-03-topology-edits

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (1)
src/slm_training/data/progspec/capsules.py (1)

13-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid publishing dependency kinds that V1 never emits.

DEFINES, CONTAINMENT, and EFFECT are exported API values, but the implementation and design caveats defer their semantics. Remove them until implemented to avoid cementing an unsupported contract.

As per coding guidelines, “Do not add abstractions, dependencies, boilerplate, or files unless they are needed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/data/progspec/capsules.py` around lines 13 - 21, Update the
DependencyKind enum to remove the unsupported DEFINES, CONTAINMENT, and EFFECT
values, retaining only dependency kinds emitted and supported by V1.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/slm_training/data/progspec/capsules.py`:
- Around line 316-337: The capsule construction around _tarjan_sccs must account
for inter-SCC reference edges: for each capsule, compute and include the
transitive reachable dependency nodes in node_ids, or explicitly represent those
capsule dependencies instead of treating node_ids as closed. Preserve
deterministic ordering and ensure external_dependencies and entry_node_id remain
consistent with the resulting closure.
- Around line 174-199: Preserve the existing path_to_node mapping for the root
statement when creating the synthetic root, so traversal and member/external
assignment still resolve to that statement node. Update the ROOT_OUTPUT
association to target the root statement explicitly rather than checking
definitions, since derive_scope_contracts excludes “root” from definitions. Keep
the synthetic root node available without replacing the root statement mapping.
- Around line 174-187: Change the statement traversal around definition_to_node
and _walk so declarations become resolvable only after their statement has been
visited, causing references to later statements to fail validation. Apply the
same ordering fix to the corresponding logic noted at the additional location,
while preserving resolution of previously declared names.
- Around line 36-58: Update ScopeNode.from_dict and the related persistence
deserialization entry points to validate serialized graph data before
constructing immutable objects: enforce supported version values, required field
types, valid AST/member paths, unique node identifiers, existing edge endpoints,
and capsule membership. Remove permissive str(...) coercions and reject
malformed or unknown data with a clear validation error rather than silently
normalizing it.

In `@src/slm_training/dsl/solver/__init__.py`:
- Around line 5-25: Update the solver package exports in __init__.py to include
the documented public contracts SupportVerdict and TopologyNodeLike, importing
them from their defining module and adding them to __all__. If they are
intentionally submodule-only instead, update the documentation accordingly, but
preserve the documented API contract.

In `@src/slm_training/dsl/solver/state.py`:
- Around line 74-84: Make the state represented by the dataclass containing
__post_init__ and to_dict genuinely immutable by defensively freezing or
recursively copying all JSON-compatible dictionary data, including nested
values, before fingerprint computation. Ensure post-construction mutations
cannot alter the fingerprinted state, and have to_dict return a detached
representation rather than exposing metadata directly. Preserve the existing
serialization shape and uniqueness validation.

In `@src/slm_training/dsl/solver/topology_adapter.py`:
- Around line 212-243: Update the finite edit enumeration in the topology
adapter to emit TopologyAction.CONTRACT alongside KEEP, DELETE, and STOP when
the node supports it, preserving the existing production, arity, and slot
fields. Update the regression test to assert exact action coverage and the
expected four structural candidates, including CONTRACT.
- Around line 150-152: Update the fallback handling in the result-building
method so it never appends mask_id when codec.unk_id is absent. Only append
unk_id after validating that it is a non-special ID present in id_to_production;
otherwise leave result empty or return the empty result.
- Around line 60-68: Update TopologyEdit.from_value to validate that value has
the expected topology-edit DomainValue tag and exactly four payload items before
unpacking value.payload. Reject invalid tags or payload lengths using the
module’s established validation/error convention, while preserving the existing
decoding for valid values.

In `@tests/test_dsl/test_topology_adapter.py`:
- Around line 135-144: Update test_importing_adapter_does_not_import_torch to
execute the import and Torch-presence assertion in a fresh subprocess rather
than the current interpreter. Start the subprocess with clean module state,
import slm_training.dsl.solver.topology_adapter there, and assert that torch is
absent without relying on the parent process’s sys.modules.

---

Nitpick comments:
In `@src/slm_training/data/progspec/capsules.py`:
- Around line 13-21: Update the DependencyKind enum to remove the unsupported
DEFINES, CONTAINMENT, and EFFECT values, retaining only dependency kinds emitted
and supported by V1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad5b7e34-52d2-4ec9-9583-345af49ca60e

📥 Commits

Reviewing files that changed from the base of the PR and between c7cb099 and b7d75b2.

📒 Files selected for processing (9)
  • docs/design/vss2-01-verification-capsules.md
  • docs/design/vss2-03-topology-edits.md
  • src/slm_training/data/progspec/__init__.py
  • src/slm_training/data/progspec/capsules.py
  • src/slm_training/dsl/solver/__init__.py
  • src/slm_training/dsl/solver/state.py
  • src/slm_training/dsl/solver/topology_adapter.py
  • tests/test_data/test_progspec.py
  • tests/test_dsl/test_topology_adapter.py

Comment on lines +36 to +58
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["ast_path"] = list(self.ast_path)
data["member_paths"] = [list(p) for p in self.member_paths]
data["definitions"] = list(self.definitions)
data["external_dependencies"] = list(self.external_dependencies)
return data

@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> ScopeNode:
return cls(
node_id=str(data["node_id"]),
scope_id=str(data["scope_id"]) if data.get("scope_id") else None,
kind=str(data["kind"]),
ast_path=tuple(data.get("ast_path") or ()),
member_paths=tuple(
tuple(p) for p in (data.get("member_paths") or ())
),
definitions=tuple(str(v) for v in data.get("definitions") or ()),
external_dependencies=tuple(
str(v) for v in data.get("external_dependencies") or ()
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Validate serialized graph data instead of coercing malformed input.

These persistence entry points silently coerce arbitrary values with str(...) and accept unsupported versions, unknown endpoints, and malformed paths. Validate types, version, node uniqueness, edge endpoints, and capsule membership before constructing the immutable graph.

As per coding guidelines, “Always perform input validation at trust boundaries.”

Also applies to: 78-85, 105-114, 140-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/data/progspec/capsules.py` around lines 36 - 58, Update
ScopeNode.from_dict and the related persistence deserialization entry points to
validate serialized graph data before constructing immutable objects: enforce
supported version values, required field types, valid AST/member paths, unique
node identifiers, existing edge endpoints, and capsule membership. Remove
permissive str(...) coercions and reject malformed or unknown data with a clear
validation error rather than silently normalizing it.

Source: Coding guidelines

Comment on lines +174 to +187
for contract in statements:
node_id = f"{contract.scope_id}:node"
nodes[node_id] = ScopeNode(
node_id=node_id,
scope_id=contract.scope_id,
kind="statement",
ast_path=contract.ast_path,
member_paths=(),
definitions=contract.definitions,
external_dependencies=(),
)
path_to_node[contract.ast_path] = node_id
for name in contract.definitions:
definition_to_node[name] = node_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Forward references are currently accepted.

definition_to_node is populated with every declaration before _walk, so a reference to a later statement resolves successfully. The added validation only rejects undefined binders. Either track declarations in traversal order or rename the documented contract to undefined-reference validation.

Also applies to: 251-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/data/progspec/capsules.py` around lines 174 - 187, Change
the statement traversal around definition_to_node and _walk so declarations
become resolvable only after their statement has been visited, causing
references to later statements to fail validation. Apply the same ordering fix
to the corresponding logic noted at the additional location, while preserving
resolution of previously declared names.

Comment on lines +174 to +199
for contract in statements:
node_id = f"{contract.scope_id}:node"
nodes[node_id] = ScopeNode(
node_id=node_id,
scope_id=contract.scope_id,
kind="statement",
ast_path=contract.ast_path,
member_paths=(),
definitions=contract.definitions,
external_dependencies=(),
)
path_to_node[contract.ast_path] = node_id
for name in contract.definitions:
definition_to_node[name] = node_id

# Synthetic root node.
nodes[root_id] = ScopeNode(
node_id=root_id,
scope_id=None,
kind="root",
ast_path=(),
member_paths=(),
definitions=(),
external_dependencies=(),
)
path_to_node[()] = root_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the root statement mapping and connect ROOT_OUTPUT to it.

The synthetic root overwrites path_to_node[()], orphaning the root statement from traversal and member/external assignment. Additionally, derive_scope_contracts excludes "root" from definitions, so the current ROOT_OUTPUT condition can never succeed.

Proposed fix
     for contract in statements:
         ...
         path_to_node[contract.ast_path] = node_id
         ...

+    root_statement_id = path_to_node.get(())
+
     nodes[root_id] = ScopeNode(
         ...
     )
-    path_to_node[()] = root_id
+    path_to_node.setdefault((), root_id)

-    if "root" in definition_to_node:
+    if root_statement_id is not None:
         edges.append(
             ScopeEdge(
                 source=root_id,
-                target=definition_to_node["root"],
+                target=root_statement_id,
                 kind=DependencyKind.ROOT_OUTPUT,
                 role="root",
             )
         )

Also applies to: 302-311

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/data/progspec/capsules.py` around lines 174 - 199, Preserve
the existing path_to_node mapping for the root statement when creating the
synthetic root, so traversal and member/external assignment still resolve to
that statement node. Update the ROOT_OUTPUT association to target the root
statement explicitly rather than checking definitions, since
derive_scope_contracts excludes “root” from definitions. Keep the synthetic root
node available without replacing the root statement mapping.

Comment on lines +316 to +337
# Compute SCCs over statement nodes using reference edges.
sccs = _tarjan_sccs({n.node_id: n for n in node_list}, edge_list)

capsules: list[VerificationCapsule] = []
for index, component in enumerate(sccs):
node_ids = tuple(sorted(component))
entry = node_ids[0]
external = sorted(
{
dep
for nid in node_ids
for dep in nodes[nid].external_dependencies
}
)
capsules.append(
VerificationCapsule(
capsule_id=f"{spec.id}:capsule:{index}",
node_ids=node_ids,
entry_node_id=entry,
external_dependencies=tuple(external),
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

SCC membership does not make capsules dependency-closed.

For an edge A → B where the nodes are not cyclic, Tarjan returns separate capsules and A’s capsule omits B. Compute each SCC’s reachable dependency closure, or explicitly model dependencies between capsules instead of claiming closed node_ids.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/data/progspec/capsules.py` around lines 316 - 337, The
capsule construction around _tarjan_sccs must account for inter-SCC reference
edges: for each capsule, compute and include the transitive reachable dependency
nodes in node_ids, or explicitly represent those capsule dependencies instead of
treating node_ids as closed. Preserve deterministic ordering and ensure
external_dependencies and entry_node_id remain consistent with the resulting
closure.

Comment thread src/slm_training/dsl/solver/__init__.py Outdated
Comment on lines +5 to +25
from slm_training.dsl.solver.state import DomainValue, FiniteDomainState, HoleDomain, HoleId
from slm_training.dsl.solver.topology_adapter import (
TopologyAction,
TopologyAdapterConfig,
TopologyEdit,
TopologyHole,
derive_topology_holes,
legal_topology_productions,
)

__all__ = [
"DomainValue",
"FiniteDomainState",
"HoleDomain",
"HoleId",
"TopologyAction",
"TopologyAdapterConfig",
"TopologyEdit",
"TopologyHole",
"derive_topology_holes",
"legal_topology_productions",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Export the remaining documented public contracts.

SupportVerdict and TopologyNodeLike are listed as added contracts in the design document but are omitted from the package API. Export them here or explicitly document them as submodule-only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/dsl/solver/__init__.py` around lines 5 - 25, Update the
solver package exports in __init__.py to include the documented public contracts
SupportVerdict and TopologyNodeLike, importing them from their defining module
and adding them to __all__. If they are intentionally submodule-only instead,
update the documentation accordingly, but preserve the documented API contract.

Comment thread src/slm_training/dsl/solver/state.py Outdated
Comment on lines +60 to +68
@classmethod
def from_value(cls, value: DomainValue) -> TopologyEdit:
action_name, production_id, arity, slot_id = value.payload
return cls(
action=TopologyAction[action_name],
production_id=int(production_id),
arity=int(arity),
slot_id=int(slot_id),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate the tagged value before decoding it.

A different DomainValue tag with a four-item payload is silently accepted as a topology edit. Reject incorrect tags and malformed payload lengths before unpacking.

Proposed fix
     `@classmethod`
     def from_value(cls, value: DomainValue) -> TopologyEdit:
+        if value.tag != "topology_edit":
+            raise ValueError(f"expected topology_edit, got {value.tag!r}")
+        if len(value.payload) != 4:
+            raise ValueError("topology_edit payload must contain four fields")
         action_name, production_id, arity, slot_id = value.payload

As per coding guidelines, “Always perform input validation at trust boundaries.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@classmethod
def from_value(cls, value: DomainValue) -> TopologyEdit:
action_name, production_id, arity, slot_id = value.payload
return cls(
action=TopologyAction[action_name],
production_id=int(production_id),
arity=int(arity),
slot_id=int(slot_id),
)
`@classmethod`
def from_value(cls, value: DomainValue) -> TopologyEdit:
if value.tag != "topology_edit":
raise ValueError(f"expected topology_edit, got {value.tag!r}")
if len(value.payload) != 4:
raise ValueError("topology_edit payload must contain four fields")
action_name, production_id, arity, slot_id = value.payload
return cls(
action=TopologyAction[action_name],
production_id=int(production_id),
arity=int(arity),
slot_id=int(slot_id),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/dsl/solver/topology_adapter.py` around lines 60 - 68, Update
TopologyEdit.from_value to validate that value has the expected topology-edit
DomainValue tag and exactly four payload items before unpacking value.payload.
Reject invalid tags or payload lengths using the module’s established
validation/error convention, while preserving the existing decoding for valid
values.

Source: Coding guidelines

Comment on lines +150 to +152
if not result:
unk_id = getattr(codec, "unk_id", mask_id)
result.append(unk_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not return an explicitly illegal fallback production.

When unk_id is absent, this appends mask_id, even though Line 129 excludes that ID as special. Return an empty result or only append a validated, non-special unk_id present in id_to_production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/dsl/solver/topology_adapter.py` around lines 150 - 152,
Update the fallback handling in the result-building method so it never appends
mask_id when codec.unk_id is absent. Only append unk_id after validating that it
is a non-special ID present in id_to_production; otherwise leave result empty or
return the empty result.

Comment on lines +212 to +243
# Complete edit tuples for structural actions.
for action in (TopologyAction.KEEP, TopologyAction.DELETE, TopologyAction.STOP):
values.append(
TopologyEdit(
action=action,
production_id=node.production_id,
arity=len(node.children),
slot_id=node.slot_id,
).to_value()
)

# EXPAND actions: enumerate legal complete edits (production, arity, slot).
if node.node_type != "leaf":
legal_pids = legal_topology_productions(codec, node.node_type)
max_arity = min(config.topology_max_arity, 8)
for pid in legal_pids:
token = codec.id_to_production.get(pid, "")
is_container = token.startswith("+") or token in {
"[",
FRAGMENT_CHUNK,
}
arity_range = range(max_arity + 1) if is_container else range(1)
for arity in arity_range:
for slot_id in ([0, *range(1, len(slots) + 1)]) if slots else [0]:
values.append(
TopologyEdit(
action=TopologyAction.EXPAND,
production_id=pid,
arity=arity,
slot_id=slot_id,
).to_value()
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include CONTRACT in the finite edit domain.

TopologyAction.CONTRACT is public but never emitted, so derived holes cannot represent that action. The test’s “4 structural” bound also indicates four structural candidates were intended. Enumerate CONTRACT where legal and assert exact action coverage in the regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/dsl/solver/topology_adapter.py` around lines 212 - 243,
Update the finite edit enumeration in the topology adapter to emit
TopologyAction.CONTRACT alongside KEEP, DELETE, and STOP when the node supports
it, preserving the existing production, arity, and slot fields. Update the
regression test to assert exact action coverage and the expected four structural
candidates, including CONTRACT.

Comment on lines +135 to +144
def test_importing_adapter_does_not_import_torch():
import importlib
import sys

# Snapshot current torch presence, then fresh-import the adapter module.
had_torch = "torch" in sys.modules
if "slm_training.dsl.solver.topology_adapter" in sys.modules:
del sys.modules["slm_training.dsl.solver.topology_adapter"]
importlib.import_module("slm_training.dsl.solver.topology_adapter")
assert ("torch" in sys.modules) == had_torch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run the Torch-free assertion in a fresh interpreter.

The adapter is already imported through Lines 6–12. If that initial import loads Torch, had_torch is True and this test still passes. Use a subprocess so the assertion begins with clean module state.

Proposed fix
 def test_importing_adapter_does_not_import_torch():
-    import importlib
+    import subprocess
     import sys
 
-    # Snapshot current torch presence, then fresh-import the adapter module.
-    had_torch = "torch" in sys.modules
-    if "slm_training.dsl.solver.topology_adapter" in sys.modules:
-        del sys.modules["slm_training.dsl.solver.topology_adapter"]
-    importlib.import_module("slm_training.dsl.solver.topology_adapter")
-    assert ("torch" in sys.modules) == had_torch
+    code = """
+import sys
+assert "torch" not in sys.modules
+import slm_training.dsl.solver.topology_adapter
+assert "torch" not in sys.modules
+"""
+    subprocess.run([sys.executable, "-c", code], check=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_importing_adapter_does_not_import_torch():
import importlib
import sys
# Snapshot current torch presence, then fresh-import the adapter module.
had_torch = "torch" in sys.modules
if "slm_training.dsl.solver.topology_adapter" in sys.modules:
del sys.modules["slm_training.dsl.solver.topology_adapter"]
importlib.import_module("slm_training.dsl.solver.topology_adapter")
assert ("torch" in sys.modules) == had_torch
def test_importing_adapter_does_not_import_torch():
import subprocess
import sys
code = """
import sys
assert "torch" not in sys.modules
import slm_training.dsl.solver.topology_adapter
assert "torch" not in sys.modules
"""
subprocess.run([sys.executable, "-c", code], check=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_dsl/test_topology_adapter.py` around lines 135 - 144, Update
test_importing_adapter_does_not_import_torch to execute the import and
Torch-presence assertion in a fresh subprocess rather than the current
interpreter. Start the subprocess with clean module state, import
slm_training.dsl.solver.topology_adapter there, and assert that torch is absent
without relying on the parent process’s sys.modules.

Tyler-R-Kendrick pushed a commit that referenced this pull request Jul 18, 2026
The fleet independently shipped SLM-59 (Torch-free finite-domain support
lattice) to main via #321/#326. This branch had its own SLM-59
implementation, now redundant. Per "do not reimplement what already
exists," resolve the add/add collision by taking main's canonical
dsl/solver/ (state.py, adapters.py, __init__.py) and test_solver_state.py
verbatim, dropping this branch's duplicate. #322 collapses to its unique,
solver-independent LDI contribution (DecisionEventV2 + diagnostics,
causal capture, removable adapter). Verified: main's solver test + all
LDI suites pass together (141), repo_policy ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5Fy8PnMTV54p675y9T3B8
VSS2-03: adapts GrammarDiffusionModel's bounded production-tree topology nodes
into the canonical finite-domain solver state (main's DomainValue tag/payload_json
API), so each active topology node becomes a stable semantic hole whose values are
complete edit tuples. Carrier + hard-domain enumeration only; does not alter model
generation. Torch-free; verified locally (topology + existing solver tests pass).
No ship claim.
@Tyler-R-Kendrick
Tyler-R-Kendrick force-pushed the slm-67-vss2-03-topology-edits branch from b7d75b2 to fb92790 Compare July 18, 2026 05:42
@Tyler-R-Kendrick
Tyler-R-Kendrick merged commit 4066c30 into main Jul 18, 2026
2 of 4 checks passed
@Tyler-R-Kendrick
Tyler-R-Kendrick deleted the slm-67-vss2-03-topology-edits branch July 18, 2026 05:43
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