Deepening Survey — act101 Agent Skill

Deepening Survey

Use when asked to find deepening opportunities, survey a codebase for shallow modules, ask "where is our abstraction leaking?" or "how do we make an upcoming change easy?", or before a large build to find the structure that would make it cheap. Proposes measured candidates; never edits code.

Deepening Survey

Depth: Level 2 (Investigate). Tier: Architecture (simulate, analyze_interface_bloat, analyze_cohesion, and analyze_chokepoints all enforce it). Read ../analysis-protocol/references/protocol.md first: it defines the run artifacts, the investigation loop, the Shared Interpretation Rules, and the summary format.

What this skill is

A survey, not a refactor. It finds modules that pay for themselves poorly, a wide interface in front of little implementation, proposes the deepening that would fix each one, and stops. The only writes are the run artifacts and the project-map ledger. Every candidate carries a measured delta: a proposal that cannot be measured is reported UNVERIFIED, and one the measurement contradicts is reported REFUTED. Execution belongs to architectural-refactoring, which owns remediation-log.md.

Vocabulary

Use these words in every candidate; "component", "service", "layer", and "API" are not substitutes.

Term Meaning here How it is measured
module A file (or directory) that hides an implementation behind a callable surface the unit analyze_clusters and analyze_coupling report on
interface The symbols outside code actually calls, not what is declared public analyze_thickness interface_width; analyze_surface at a multi-file boundary
implementation mass How much behavior sits behind that interface analyze_thickness implementation_mass (statements)
thickness implementation mass ÷ interface width. High hides a lot behind a little; low is shallow analyze_thickness thickness
shallow module Low thickness with non-trivial mass: the interface costs nearly as much to learn as the code it hides analyze_thickness class: shallow
seam A narrow point where two groups of modules communicate analyze_seams read through analyze_clusters
locality How much of one concept lives in one place analyze_cohesion LCOM4 components; analyze_orphan_types
leverage How much future change one deepening buys churn_hotspots × the simulated delta
the deletion test Would removing this module concentrate behavior, or just push it to callers? simulate delete_module (Phase 3)

"Thickness", not "depth". act101 ships analyze_depth, which computes the longest transitive dependency chain per file, a different quantity. In this skill and its reports, the interface-versus-implementation quality is thickness; "depth" means chain length or the protocol's investigation depth levels.

Phase 0: Scope and prior state

Scope before scanning. A deepening pays off only where change lands:

  1. If the operator named a direction (a module, a subsystem, an upcoming build, a pain point), take it. Pointing this skill at planned work produces the most actionable report.
  2. Otherwise run churn_hotspots in workspace mode to rank where the codebase is moving and let those paths lead the survey. If churn is flat, widen to the whole workspace and say so in the report.
  3. Hold the scope with include / exclude on every analyzer and record it in manifest.json.

Read the prior ledger. If project-map.md exists, read its ## Refuted & Re-characterized Findings ledger first. A refuted candidate returns only with new contradicting evidence; otherwise carry it as "previously refuted (date), no new evidence". Also read remediation-log.md if present: a module already deepened is not a candidate, and a RESOLVED row the structure contradicts is its own finding.

Phase 1: Parallel tool dispatch

Dispatch in one parallel batch, one subagent per tool; each saves raw JSON to raw/<tool-name>.json and returns a structured summary.

Must-have: analyze_thickness (interface width, implementation mass, thickness, and class per file, with the calls_modeled gate). Without it there is no ranking: report that and stop.

Must-have for gating: simulate, the measured delta behind every candidate (Phase 3). Without it, candidates can still be ranked but every one ships UNVERIFIED; say so in the report.

Extended (use if available; note skips in manifest.json):

Tool Supplies
analyze_interface_bloat export_ratio per file: how much of the file is surface rather than hidden
analyze_surface Width across a multi-file boundary (thickness measures per file); reports total_parameters and its own calls_modeled gate
analyze_clusters Current module grouping; hub_collapse and top_hubs disclosure
analyze_seams Where a deepened boundary could sit
analyze_cohesion LCOM4 components: the named split of a module doing two things
analyze_orphan_types Types defined away from their only consumers (a locality defect)
analyze_chokepoints High-centrality modules where a deepening has the widest reach
analyze_cycles Cycles a deepening could resolve
churn_hotspots Leverage weighting (Phase 0 scope, Phase 4 ranking)
analyze_test_gaps Which candidates are untested; a thin interface is what makes them testable
coverage_overlay With an lcov, JaCoCo, or coverage.py report, turns "untested" into evidence

Interpret analyze_seams through analyze_clusters per the protocol's seam / hub-collapse rule; total_seams: 0 alone is not "no boundary". A skipped tool leaves its dimension UNASSESSED in the report; absence of evidence is never absence of a problem.

Phase 2: Thickness ranking

analyze_thickness computes the ratio; read its output rather than recomputing. Per file it returns interface_width (symbols called from outside the file), exposed_parameters, implementation_mass (statements), thickness (mass / max(width, 1)), and a class of shallow / proportionate / deep / unassessed. Files come back ascending by thickness, shallowest first, which is the order to work in.

unassessed is the absence of a finding. The file was not judged: summary.calls_modeled is false, so interface width was unknowable; or the file has no measurable implementation; or nothing calls into it. Count unassessed files separately, as the summary does; they are never part of a "nothing wrong here" reading.

Scope the run so it contains the callers. Interface width counts calls from outside the file but inside the analyzed graph, so an include narrowed to a single module hides the callers that give its files a width and returns mostly unassessed. If unassessed_count dominates, widen the scope and re-run before reporting anything about thickness.

proportionate is a real answer, not a near-miss: a small file with a small interface is in proportion, and flagging it would make every helper a finding.

analyze_interface_bloat is the corroborating second opinion, not the ratio: its export_ratio is exported symbols ÷ total symbols ("how much of this file is public"), where thickness asks "how much does it hide". A module that classes shallow and carries a high export_ratio is the strongest shape; a module that only classes shallow still qualifies. Say which evidence backed each candidate.

Three shallowness shapes, each with its own deepening:

Shape Evidence Deepening
Pass-through: the module forwards and adds nothing simulate delete_module: surface_consumers: 0 with surface_modeled: true, high rewired_edges, severed_edges: 0 Remove it; inline the forwarders into callers
Split personality: one module holding two concepts, so neither is local analyze_cohesion lcom4 of 2 or more with named components Split along the components; split_module proposes the cut, move_symbol executes it
Testability shrapnel: logic extracted to satisfy a test, leaving the real behavior in the caller A symbol with exactly one caller (references) in a file whose analyze_test_gaps status is covered while the caller is not Fold it back with inline, then test through the deepened interface

Phase 3: Gate every candidate with simulate

A candidate without a simulated delta does not get a card. simulate never touches disk, so gating is free. Express each proposed deepening as an ops script and record the returned deltas:

Proposal Op
Remove a suspected pass-through delete_module{file}
Split a module along its components split_file{file, groups:[[symbol]]}
Relocate an orphan type to its consumer move_file{from, to}
Collapse two modules that should be one merge_files{files, to}
Cut a dependency a deepening would remove remove_edge{from, to}

Call simulate(ops=[…], include=[…]) and record cycles (resolved / introduced), the per-unit coupling deltas, chokepoints centrality changes, and, only when an [architecture] contract exists in .act/config.toml, violations.cleared / violations.introduced.

Reading the deletion test. Follow the protocol's canonical order: surface_consumers first (with a named consumer from top_consumers), then surface_modeled as the honesty gate, then rewired_edges / severed_edges. surface_consumers: 0 with surface_modeled: false is UNKNOWN: the call channel was not modeled for that grammar.

Verdict, derived from the measurement:

Verdict Earned by
CONFIRMED The simulation supports the proposal: a cycle resolves, coupling drops on the affected units, no new conformance violation, and, for a removal, surface_consumers: 0 with surface_modeled: true
UNVERIFIED simulate was unavailable, or the deciding dimension is unmodeled (surface_modeled: false, a grammar absent from modeled_kinds). State which dimension is dark
REFUTED The simulation contradicts the proposal: it introduces a cycle or a conformance violation, raises coupling, or the deletion test finds load-bearing consumers

Write each gated candidate to investigation/candidate-N.md. Every REFUTED candidate goes into the project map's Refuted ledger with its disproving evidence, so the next survey does not re-propose it.

Phase 4: Rank and report

Order candidates by leverage: the simulated delta weighted by how much the module changes (churn_hotspots) and how far its blast radius reaches (analyze_chokepoints). A large delta in dormant code ranks below a modest delta in code that moves every week.

The report is markdown with ASCII diagrams, written to .act/runs/<YYYY-MM-DD-HHMMSS>/report.md, so it renders in a terminal, a diff, and a review with no network dependency.

# Deepening Survey: <project name>

## Overview
Scope surveyed (include/exclude), files, date, and why this scope: operator
direction or churn concentration.

## Verdict
**N deepening candidates** or **No deepening candidates found**. "No candidates"
is a legitimate outcome when the measurements support no proposal.

## Thickness Ranking
Table: module | interface width | exposed parameters | implementation mass |
thickness | class | export_ratio, straight from `analyze_thickness`, with
`summary.unassessed_count` and `summary.calls_modeled` beside it. A ranking drawn
from an unmodeled call graph is no measurement at all.

## Candidates
One section per candidate, ordered by leverage:

### C-N: <deepening stated as an action> — `CONFIRMED` / `UNVERIFIED` / `REFUTED`

**Shape:** pass-through / split personality / testability shrapnel
**Modules:** the files involved
**Friction:** what the current structure costs, in locality and leverage terms
**Proposal:** what would change, in plain language
**Measured delta:** the exact `simulate` output: cycles resolved/introduced,
coupling before/after per unit, deletion-test counts with a named consumer
**Tests:** which tests get simpler, and what `analyze_test_gaps` / `coverage_overlay`
say about current coverage of this module
**Execution:** the ops (`split_module`, `move_symbol`, `inline`, `extract_function`)
that `architectural-refactoring` would run

Before / after, drawn as ASCII:

    before                          after
    ┌────────┐                      ┌────────┐
    │ caller │──┐                   │ caller │──┐
    └────────┘  │                   └────────┘  │
    ┌────────┐  ├──> shim ──> impl   ┌────────┐  ├──────────> impl
    │ caller │──┘     (7 exports,    │ caller │──┘            (2 exports)
    └────────┘         0 logic)      └────────┘

## Refuted Candidates
Proposals this run measured and rejected, with the contradicting evidence.

## Top Recommendation
Which candidate to take first and why: leverage, not size.

Handoff

Stop after the report. Return the protocol's Common Summary and ask which candidate the operator wants to pursue. Designing the interface, opening an editor, or chaining into execution waits for the operator's next instruction.

When the operator picks one, hand off clean-slate; the artifacts on disk carry everything needed:

"Clear context, then run /architectural-refactoring; it rehydrates from project-map.md and this run's report, and records the remediation to remediation-log.md."

One candidate per session: carrying the survey, the design, and the edit in a single context makes the report, the reasoning, and the diff compete for the same window.

Project map updates

Updates Key Boundaries and Chokepoints & Risks with confirmed candidates. Adds every REFUTED candidate to the Refuted & Re-characterized Findings ledger with its evidence and this run's date, carrying prior entries forward verbatim with their original "Since" date. Appends one row to the Analysis History table. remediation-log.md belongs to architectural-refactoring and is never edited here.