Deepening Survey — act101 Agent Skill
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:
- 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.
- Otherwise run
churn_hotspotsin 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. - Hold the scope with
include/excludeon every analyzer and record it inmanifest.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 fromproject-map.mdand this run's report, and records the remediation toremediation-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.