{
  "version": "act 2.2.1",
  "slug": "deepening-survey",
  "name": "deepening-survey",
  "description": "Use when asked to find deepening opportunities, survey a codebase for shallow modules, ask \"where is our abstraction leaking?\", \"which modules are carrying their weight?\", or \"how do we make an upcoming change easy?\". Also use before a large build to find the structure that would make it cheap. Depth 2 \u2014 investigate. Surveys for shallow modules (a wide interface hiding little implementation), measures each candidate with simulate before proposing it, and stops with a ranked candidate list. Proposes; never edits code.",
  "url": "https://act101.ai/docs/skills/deepening-survey",
  "body_md": "# Deepening Survey\n\n**Depth:** Level 2 (Investigate). **Tier:** Architecture (`simulate`,\n`analyze_interface_bloat`, `analyze_cohesion`, `analyze_chokepoints` all enforce it).\n\nSee `../analysis-protocol/references/protocol.md` for artifact directory structure,\nthe investigation loop, depth levels, summary format, token budget rules, and the\nShared Interpretation Rules. Read that document before proceeding.\n\n## What this skill is\n\nA **survey**, not a refactor. It finds modules that pay for themselves poorly \u2014\na wide interface in front of little implementation \u2014 proposes the deepening that\nwould fix each one, and stops. No file is edited during a run. The only writes are\nthe run artifacts and the project-map ledger.\n\nThe output is a ranked candidate list where **every candidate carries a measured\ndelta**, not an argued one. A proposal that cannot be measured is reported as\nunverified, and a proposal the measurement contradicts is reported as refuted.\nExecution belongs to the `architectural-refactoring` skill, which owns\n`remediation-log.md`.\n\n## Vocabulary\n\nThis skill uses one consistent vocabulary. Use these words in every candidate;\ndo not drift into \"component\", \"service\", \"layer\", or \"API\".\n\n| Term | Meaning here | How it is measured |\n|------|--------------|--------------------|\n| **module** | A file (or directory) that hides an implementation behind a callable surface | the unit `analyze_clusters` and `analyze_coupling` report on |\n| **interface** | The symbols outside code actually calls \u2014 not what is declared public | `analyze_thickness` `interface_width`; `analyze_surface` at a multi-file boundary |\n| **implementation mass** | How much behaviour sits behind that interface | `analyze_thickness` `implementation_mass` (statements) |\n| **thickness** | implementation mass \u00f7 interface width. High = a lot hidden behind a little. Low = shallow | `analyze_thickness` `thickness` |\n| **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` |\n| **seam** | A narrow point where two groups of modules communicate | `analyze_seams` read through `analyze_clusters` |\n| **locality** | How much of one concept lives in one place | `analyze_cohesion` LCOM4 `components`; `analyze_orphan_types` |\n| **leverage** | How much future change one deepening buys | `churn_hotspots` \u00d7 the simulated delta |\n| **the deletion test** | Would removing this module concentrate behaviour, or just push it to callers? | `simulate` `delete_module` \u2014 see Phase 3 |\n\n**Naming warning \u2014 do not write \"depth\" for thickness.** act101 already ships\n`analyze_depth`, which computes the **longest transitive dependency chain per file**.\nThat is a different quantity. In this skill, and in every report it writes, the\ninterface-versus-implementation quality is called **thickness**; the word \"depth\"\nrefers only to chain length and to the protocol's investigation depth levels.\n\n## Phase 0: Scope and prior state\n\n**Scope before you scan.** A deepening pays off only where change lands, so decide\nwhere to look before looking:\n\n1. If the operator named a direction \u2014 a module, a subsystem, an upcoming build, a\n   pain point \u2014 take it and skip the inference. Pointing this skill at planned work\n   (\"how do we make this change easy?\") produces the most actionable report.\n2. Otherwise use `churn_hotspots` in workspace mode to rank where the codebase is\n   actually moving, and let those paths pull the survey first. If churn is flat with\n   no concentration, widen to the whole workspace and say so in the report.\n3. Use `include` / `exclude` on every analyzer to hold the scope. Report the scope\n   in `manifest.json`.\n\n**Read the prior ledger.** If `project-map.md` exists at the workspace root, read\nits `## Refuted & Re-characterized Findings` ledger first (protocol mandate for\ndepth-2+ skills). A candidate already refuted does not come back without new\ncontradicting evidence \u2014 carry it forward as \"previously refuted (date), no new\nevidence\". Also read `remediation-log.md` if present: a module already deepened is\nnot a candidate, and a `RESOLVED` row the structure contradicts is its own finding.\n\n## Phase 1: Parallel tool dispatch\n\nDispatch in a **single parallel batch**. One subagent per tool; each saves raw JSON\nto `raw/<tool-name>.json` and returns a structured summary only.\n\n**Must-have tool** \u2014 without it there is no ranking, so report that and stop:\n\n| Tool | Supplies |\n|------|----------|\n| `analyze_thickness` | interface width, implementation mass, thickness and class per file, with the `calls_modeled` gate |\n\n**Must-have for gating** \u2014 without it, candidates can still be ranked but every one\nof them ships as `UNVERIFIED`; say so explicitly in the report:\n\n| Tool | Supplies |\n|------|----------|\n| `simulate` | the measured delta behind every candidate (Phase 3) |\n\n**Extended tools** \u2014 use if available, note in `manifest.json` if skipped:\n\n| Tool | Supplies |\n|------|----------|\n| `analyze_interface_bloat` | `export_ratio` per file: how much of the file is surface rather than hidden |\n| `analyze_surface` | width across a multi-file boundary, which `analyze_thickness` measures only per file. Reports `total_parameters` and its own `calls_modeled` gate |\n| `analyze_clusters` | current module grouping; `hub_collapse` + `top_hubs` disclosure |\n| `analyze_seams` | where a deepened boundary could sit |\n| `analyze_cohesion` | LCOM4 `components` \u2014 the named split of a module doing two things |\n| `analyze_orphan_types` | types defined away from their only consumers (a locality defect) |\n| `analyze_chokepoints` | high-centrality modules where a deepening has the widest reach |\n| `analyze_cycles` | cycles a deepening could resolve |\n| `churn_hotspots` | leverage weighting (Phase 0 scope, Phase 4 ranking) |\n| `analyze_test_gaps` | which candidates are untested \u2014 a thin interface is what makes them testable |\n| `coverage_overlay` | with an lcov / JaCoCo / coverage.py report, turns \"untested\" into evidence |\n\nInterpret `analyze_seams` through `analyze_clusters` per the protocol's Shared\nInterpretation Rules (seam / hub-collapse). `total_seams: 0` is never \"no boundary\nexists\".\n\n## Phase 2: Thickness ranking\n\n`analyze_thickness` computes this directly. Read its output; do not recompute the\nratio by hand.\n\nPer file it returns `interface_width` (symbols called from outside the file),\n`exposed_parameters`, `implementation_mass` (statements), `thickness`\n(`mass / max(width, 1)`), and a `class` of `shallow` / `proportionate` / `deep` /\n`unassessed`. Files come back ascending by thickness \u2014 shallowest first, which is\nthe order to work in.\n\n**`unassessed` is not a mild finding, it is the absence of one.** It means the\nfile was not judged: `summary.calls_modeled` is false, so interface width was\nunknowable, or the file has no measurable implementation, or nothing calls into\nit so it presents no interface at all. Never fold `unassessed` files into a\n\"nothing wrong here\" reading \u2014 count them separately, as the summary does.\n\n**Scope the run so it contains the callers.** Interface width counts calls from\noutside the file but inside the analyzed graph, so an `include` narrowed to a\nsingle module hides the very callers that give its files a width \u2014 and returns\nmostly `unassessed`. Measured on this repo, `include: [\"crates/act-analysis\"]`\njudged 4 of 38 files; the other 34 were unassessed purely because their callers\nsat outside the scope. If `unassessed_count` dominates, widen the scope and\nre-run before reporting anything about thickness.\n\n`proportionate` is likewise a real answer, not a near-miss. A small file with a\nsmall interface is in proportion; flagging it would make every helper a finding.\n\n`analyze_interface_bloat` is the corroborating second opinion, not the ratio itself:\nits `export_ratio` is exported symbols \u00f7 total symbols \u2014 \"how much of this file is\npublic\", where `analyze_thickness` asks \"how much does it hide\". A module that\nclasses `shallow` **and** carries a high `export_ratio` is the strongest shape. A\nmodule that only classes `shallow` still qualifies; say which evidence backed it.\n\n**Three shallowness shapes to name explicitly.** Each has a different deepening:\n\n| Shape | Evidence | Deepening |\n|-------|----------|-----------|\n| **Pass-through** \u2014 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 |\n| **Split personality** \u2014 one module holding two concepts, so neither is local | `analyze_cohesion` `lcom4` \u2265 2 with named `components` | split along the `components` \u2014 `split_module` proposes the cut, `move_symbol` executes it |\n| **Testability shrapnel** \u2014 logic extracted to satisfy a test, leaving the real behaviour 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 |\n\n## Phase 3: Gate every candidate with `simulate`\n\n**A candidate without a simulated delta does not get a card.** This is what\nseparates this skill from an opinion. `simulate` never touches disk, so gating is\nfree.\n\nExpress each proposed deepening as an ops script and record the returned deltas:\n\n| Proposal | Op |\n|----------|-----|\n| Remove a suspected pass-through | `delete_module{file}` |\n| Split a module along its `components` | `split_file{file, groups:[[symbol]]}` |\n| Relocate an orphan type to its consumer | `move_file{from, to}` |\n| Collapse two modules that should be one | `merge_files{files, to}` |\n| Cut a dependency a deepening would remove | `remove_edge{from, to}` |\n\nCall it as `simulate(ops=[\u2026], include=[\u2026])` and record `cycles` (resolved /\nintroduced), the per-unit `coupling` deltas, `chokepoints` centrality changes, and \u2014\nonly when an `[architecture]` contract exists in `.act/config.toml` \u2014\n`violations.cleared` / `violations.introduced`.\n\n**Reading the deletion test.** Follow the protocol's canonical order exactly:\n`surface_consumers` first (with a named consumer from `top_consumers`), then\n`surface_modeled` as the honesty gate, then `rewired_edges` / `severed_edges`.\n`surface_consumers: 0` with `surface_modeled: false` is **UNKNOWN**, never a clean\nconduit \u2014 the call channel was not modeled for that grammar.\n\n**Verdict, derived from the measurement \u2014 never asserted:**\n\n| Verdict | Earned by |\n|---------|-----------|\n| `CONFIRMED` | The simulation supports the proposal: a cycle resolves, coupling drops on the affected units, no new conformance violation, and \u2014 for a removal \u2014 `surface_consumers: 0` with `surface_modeled: true` |\n| `UNVERIFIED` | `simulate` was unavailable, or the dimension that would decide it is unmodeled (`surface_modeled: false`, a grammar absent from `modeled_kinds`). State which dimension is dark |\n| `REFUTED` | The simulation contradicts the proposal \u2014 it introduces a cycle or a conformance violation, raises coupling, or the deletion test finds load-bearing consumers |\n\nWrite each gated candidate to `investigation/candidate-N.md`.\n\n**Every `REFUTED` candidate goes into the project map's Refuted ledger** with its\ndisproving evidence, so the next survey does not re-propose it.\n\n## Phase 4: Rank and report\n\nOrder candidates by leverage: the simulated delta weighted by how much the module\nactually changes (`churn_hotspots`) and how far its blast radius reaches\n(`analyze_chokepoints`). A large delta in dormant code ranks below a modest delta in\ncode that moves every week.\n\nThe report is **markdown with ASCII diagrams**, written to\n`.act/runs/<YYYY-MM-DD-HHMMSS>/report.md`. It renders in the terminal, in a diff, and\nin a review \u2014 with no network access, no CDN, and no step where the diagram silently\nfails to load and nobody notices.\n\n```markdown\n# Deepening Survey: <project name>\n\n## Overview\nScope surveyed (include/exclude), files, date. Why this scope \u2014 operator direction\nor churn concentration.\n\n## Verdict\nOne of: **N deepening candidates** / **No deepening candidates found**.\n\"No candidates\" is a legitimate and expected outcome \u2014 report it plainly when the\nmeasurements do not support any proposal. Do not manufacture a candidate to fill\nthe report.\n\n## Thickness Ranking\nTable: module | interface width | exposed parameters | implementation mass |\nthickness | class | export_ratio, straight from `analyze_thickness`.\nReport `summary.unassessed_count` and `summary.calls_modeled` alongside it \u2014 a\nranking drawn from an unmodeled call graph is not a short list of findings, it is\nno measurement at all.\n\n## Candidates\nOne section per candidate, ordered by leverage:\n\n### C-N: <deepening stated as an action> \u2014 `CONFIRMED` / `UNVERIFIED` / `REFUTED`\n\n**Shape:** pass-through / split personality / testability shrapnel\n**Modules:** the files involved\n**Friction:** what the current structure costs, in locality and leverage terms\n**Proposal:** what would change, in plain language\n**Measured delta:** the exact `simulate` output \u2014 cycles resolved/introduced,\ncoupling before/after per unit, deletion-test counts with a named consumer\n**Tests:** which tests get simpler, and what `analyze_test_gaps` / `coverage_overlay`\nsay about the current coverage of this module\n**Execution:** the specific ops \u2014 `split_module`, `move_symbol`, `inline`,\n`extract_function` \u2014 that `architectural-refactoring` would run\n\nBefore / after, drawn as ASCII:\n\n    before                          after\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n    \u2502 caller \u2502\u2500\u2500\u2510                   \u2502 caller \u2502\u2500\u2500\u2510\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \u2502                   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \u2502\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  \u251c\u2500\u2500> shim \u2500\u2500> impl   \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> impl\n    \u2502 caller \u2502\u2500\u2500\u2518     (7 exports,    \u2502 caller \u2502\u2500\u2500\u2518            (2 exports)\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         0 logic)      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n## Refuted Candidates\nProposals this run measured and rejected, with the contradicting evidence.\nCarried into the project map's Refuted ledger.\n\n## Top Recommendation\nWhich candidate to take first and why \u2014 leverage, not size.\n```\n\n## Handoff\n\n**Stop after the report.** Return the protocol's Common Summary and ask which\ncandidate the operator wants to pursue. Do not begin designing an interface, do not\nopen an editor, and do not chain into execution on your own initiative.\n\nWhen the operator picks one, the next action is a clean-slate handoff \u2014 the\nartifacts on disk carry everything needed:\n\n> \"Clear context, then run `/architectural-refactoring` \u2014 it rehydrates from\n> `project-map.md` and this run's report, and records the remediation to\n> `remediation-log.md`.\"\n\nWork **one candidate per session**. Carrying the survey, the design, and the edit in\na single context is how the report, the reasoning, and the diff end up competing for\nthe same window.\n\n## Project Map Updates\n\nUpdates **\"Key Boundaries\"** and **\"Chokepoints & Risks\"** with confirmed candidates.\nAdds every `REFUTED` candidate to the **Refuted & Re-characterized Findings** ledger\nwith its disproving evidence and this run's date, carrying prior entries forward\nverbatim with their original \"Since\" date. Appends one row to the Analysis History\ntable. Never edits `remediation-log.md` \u2014 that ledger belongs to\n`architectural-refactoring`.\n\n## Rules\n\n1. **Propose, never edit.** A run that changes a source file has failed, regardless\n   of how good the change was.\n2. **No card without a delta.** Every candidate is gated by `simulate`, or ships as\n   `UNVERIFIED` with the dark dimension named.\n3. **\"No candidates\" is a valid report.** The measurements decide how many\n   candidates exist. Never pad the report to look productive.\n4. **Say \"thickness\", never \"depth\"** for the interface-versus-implementation\n   quality. `analyze_depth` is chain length.\n5. **Report the measurement, do not recompute it.** `analyze_thickness` owns the\n   ratio and the classification. Re-deriving either by hand invents a second,\n   unpinned definition of shallow.\n6. **Unmodeled is not clean.** `calls_modeled: false`, a `class` of `unassessed`,\n   an empty `modeled_kinds`, `surface_modeled: false`, or a skipped tool all mean\n   UNASSESSED. Never report absence of evidence as absence of a problem.\n7. **Stop at the report** and let the operator choose."
}