Extract state type
Skill KhurrumMahmood/senior-vibe-engineer/.claude/skills/extract-state-type
Read-only EXPLAIN skill that converts a function operating on an implicit mutable dict (or ad-hoc kwargs/namespace) into a typed @dataclass or TypedDict migration plan. Produces reports/extract-state-type/<target>/proposal.md with the current-shape table, proposed type definition, caller-by-caller migration plan, characterization-test matrix, and stop condition. Hands off to /fix-workflow or manual migration.From its SKILL.md
npx -y skills add KhurrumMahmood/senior-vibe-engineer --skill extract-state-typeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.9 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
/extract-state-type
You are the orchestrator for an EXPLAIN skill that converts an
implicit-dict state contract into a typed proposal. A function
operates on a dict whose shape lives in its reads and writes rather
than in a @dataclass or TypedDict; your job is to read the
function + callers, infer the shape, and emit a proposal that
/fix-workflow (or a human) can execute.
This skill pairs with /find-implicit-state (SUSPECT — detects the
smell) and hands off to /fix-workflow (REFACTOR — executes the
migration). You write the proposal; you do NOT edit production code.
Procedural detail lives in the knowledge files:
knowledge/state-conventions.md— scout-facing rules for dataclass vsTypedDict, state-type location, and dynamic-key stop conditions.knowledge/proposal-template.md— the exact shape ofreports/extract-state-type/<target>/proposal.md.agents/state-profiler.md— scout brief for dict-shape inference.
How success is judged
proposal.mdmatchesknowledge/proposal-template.md: current-shape key table, complete@dataclass/TypedDictdefinition, one caller row per file from the scout's grep pass, and a stop condition.- Stage 1 pasted output includes the exact
collect_target.pycommand and itswrote ... (dict_candidates=..., callers=...)line; Stage 2 is judged by theprofile.mdfile the scout writes, not by a claim. - The characterization-test section is present and concrete — the
shape
/fix-workflowmust write before any edit is non-optional. - One scout, one proposal, one target; extras go to Follow-on findings.
- Nothing outside
reports/extract-state-type/<slug>/was touched — the proposal is the handoff artifact, never the execution. Write toward these gates from Stage 0.
Core beliefs
- The dict IS the contract. Every read of a key is a caller promise; every write is an implementer commitment. The proposal writes those promises down in a type, nothing more, nothing less.
@dataclassfor mutable state,TypedDictfor boundary state. Mutable state flowing through a pipeline →@dataclasswithfield(default_factory=...). State that crosses a boundary (API payload, cache entry, log record) →TypedDict. The profiler flags which shape applies; the proposal uses that flag.- Caller impact is the migration's real cost. A dataclass that's clean in the target but breaks ten callers is a worse proposal than a slightly messier dataclass that only touches the target. The proposal enumerates every caller and its change set.
- Characterization tests pin behavior. The proposal names the
test shape
/fix-workflowmust write BEFORE any edit. Same pattern as/refactor-subsystem's Phase 2.1 — capture the current behavior, migrate, confirm unchanged. - Scouts read, orchestrator synthesizes. One scout profiles the function + callers; the orchestrator consolidates into the proposal.
Scope
- Project root: this worktree's root.
- Python:
.venv/bin/pythonfor helper scripts. - Output:
reports/extract-state-type/<target-slug>/. Never touches any other file. - Project-specific conventions (known implicit-state targets,
dataclass location, naming):
knowledge/state-conventions.md. Scouts read that file; the orchestrator does not.
Argument parsing
Two forms:
Form A — finding reference
Pattern: implicit-state:<candidate-id> (the candidate-id format
/find-implicit-state emits in its candidates.jsonl).
Resolve manually against reports/implicit-state/latest/candidates.jsonl;
collect_target.py does not implement --from-finding. Strip the
optional implicit-state: prefix, read the matching JSONL record,
extract its target file and symbol, and then continue as Form B by
calling collect_target.py --file ... --symbol ....
RAW_FINDING="<implicit-state-id-or-prefixed-id>"
FINDING_ID="${RAW_FINDING#implicit-state:}"
.venv/bin/python -c '
import json, sys
from pathlib import Path
finding_id = sys.argv[1]
path = Path("reports/implicit-state/latest/candidates.jsonl")
for line in path.read_text(encoding="utf-8").splitlines():
record = json.loads(line)
if record.get("candidate_id") == finding_id or record.get("id") == finding_id:
print(json.dumps({
"file": record.get("file"),
"symbol": (record.get("symbols") or [None])[0],
"pattern": record.get("pattern"),
"recommendation_hint": record.get("recommendation_hint"),
}))
break
else:
raise SystemExit(f"finding not found: {finding_id}")
' "${FINDING_ID}"
Only candidates whose sub-shape is implicit_dict_state are valid
for this skill — other sub-shapes (stringly-typed state, tuple-
inferred identity) belong to /extract-enum or /introduce-fk.
Note: as of the current /find-implicit-state version,
implicit_dict_state is NOT surfaced as a distinct sub-shape — the
detector targets stringly-typed state and tuple-inferred identity
only. If the Form-A candidate's sub-shape isn't implicit_dict_state,
abort with a one-line error redirecting to /extract-enum or
/introduce-fk. If no matching detector ships, fall through to
Form B and ask the user to specify the target explicitly.
Form B — explicit target
Pattern: <file>::<function> or <file>::<Class>.<method>.
Examples:
core/services/agentic_discovery_service.py::AgenticDiscoveryService.discovercore/services/field_discovery_pipeline_service.py::discover_fieldscore/services/agentic_discovery_service.py::discover(shorthand — the first function nameddiscoverin the file)
Derive the slug: <file-stem>__<bare-symbol>. Examples:
agentic_discovery_service__discoverfield_discovery_pipeline_service__discover_fields
Budget
One scout, one proposal, one target. This skill does NOT fan out
across N targets. If the finding references multiple implicit-dict
states (rare), pick the highest-confidence one and note the rest in
## Follow-on findings.
Pipeline
Stage 0 — Setup
Pre: argument parsed. Post: $REPORT_DIR exists,
latest symlink updated.
TARGET_SLUG="<derived slug>"
REPORT_DIR="reports/extract-state-type/${TARGET_SLUG}"
mkdir -p "${REPORT_DIR}"
ln -sfn "${TARGET_SLUG}" reports/extract-state-type/latest
Target-keyed path (not timestamped) — re-runs against the same
target converge, and the git history of proposal.md is the
historical record.
Stage 1 — Collect target + callers
Pre: argument parsed. Post: ${REPORT_DIR}/targets.json with
the function signature, the dict-state parameter (by inference), and
the caller list.
.venv/bin/python .claude/skills/extract-state-type/scripts/collect_target.py \
--file "<file-path>" \
--symbol "<qualified-symbol>" \
--project-root "$(pwd)" \
--output "${REPORT_DIR}/targets.json"
The helper does three things:
- Reads the function and records its signature, line range, and docstring.
- Heuristically identifies the dict-state variable — the first
local assignment in the body of the form
<name> = {...}or<name> = dict(...), or a parameter annotated asdict/Dict[str, ...]. If ambiguous, lists all candidates; the scout disambiguates at Stage 2. - Greps inbound callers (
from <module> import <symbol>,<Cls>().<method>(, bare calls for module functions) and records file + symbol + approximate line for each.
Output schema is documented in the helper's module docstring.
If the helper returns zero callers, the proposal will note "no external callers — migration affects only the target function and its private helpers"; that's a valid and cheaper migration.
Stage 2 — Profile (single scout)
Pre: targets.json. Post:
${REPORT_DIR}/profile.md — the full dict-shape inference + caller
impact.
Dispatch one scout with agents/state-profiler.md. Substitute
{{target_slug}}, {{file_path}}, {{symbol}}, {{dict_variable}},
{{callers_path}}, {{project_root}}, {{skill_root}},
{{output_path}}, {{targets_json_path}}.
Tell the scout its output is judged only by {{output_path}}: it must
write the profile shape from agents/state-profiler.md, including the
metadata status and caller impact table. A message saying the profile is
complete does not satisfy Stage 2 without the file.
The scout:
- Reads the target function in full.
- Walks the body AST (via
Read+ manual inspection), listing every key read (state['k'],state.get('k'),state.get('k', d)) and every key write (state['k'] = v,state.setdefault('k', v), nested mutations likestate['budget']['k'] = v). - Classifies each key as required (always written before first
read) or optional (read via
.getwith a default, or written only on some branches). - Infers each key's type from the literal(s) assigned or the shape of the values written.
- Detects nested dict structure — a key whose value is itself a
dict with its own implicit shape becomes a nested dataclass or
a nested
TypedDict. - Greps every caller file listed in
targets.jsonand records whether the caller constructs the dict, mutates it, reads keys from the return value, or all three. - Classifies the recommended shape:
dataclass(mutable state),TypedDict(boundary state — crosses process / cache / log). - Writes
profile.mdper the template in the scout brief.
If the scout returns profile_incomplete, re-dispatch once with a
stricter "respond only with file-write confirmation" nudge. If it
fails twice, write a minimal proposal that documents the failure
and suggests re-running.
Stage 3 — Synthesize the proposal
Pre: profile.md exists. Post:
${REPORT_DIR}/proposal.md.
Read profile.md and targets.json. Write proposal.md following
knowledge/proposal-template.md exactly. Top-level structure:
- Target metadata (path, symbol, target kind, regenerated timestamp).
- Current shape (implicit dict) — table of every key with type, required/optional, default, mutation sites.
- Proposed type definition — a complete
@dataclassorTypedDictblock, ready to paste into a new<module>/state.pyfile. Include imports. - Migration plan — numbered steps, from "add type" through "update target" through "update each caller" through "remove dict-only code paths."
- Caller table — one row per caller file with the exact change (construction, key read, key write). Pull from the scout's grep pass in Stage 2.
- Characterization tests — the exact test shape
/fix-workflowmust write before migration. Based on the current return shape and side-effects. This section is non-optional. - Test matrix — baseline (from
_common/skill-conventions.md) plus the file-specific suites from theFile(s) touchedtable. - Stop condition — checklist. Must include: type added,
imports updated, callers updated, characterization tests pass
unchanged,
/find-implicit-statere-run shows zeroimplicit_dict_statehits for this target. - Follow-on findings — adjacent implicit-dict-state candidates
surfaced but not addressed here. Seeds for future
/extract-state-typeruns. - Authorization — one line: "Human review required before
/fix-workflow extract-state-type:<target>or manual execution."
Stage 4 — Effectiveness log
Pre: proposal.md written. Post: one line appended to
reports/_meta/effectiveness.jsonl.
FIELD_COUNT=$(grep -c '^| `' "${REPORT_DIR}/proposal.md" || echo 0)
CALLER_COUNT=$(.venv/bin/python -c 'import json,sys; print(len(json.load(open(sys.argv[1]))["callers"]))' "${REPORT_DIR}/targets.json")
SHAPE=$(grep -oE '(dataclass|TypedDict)' "${REPORT_DIR}/proposal.md" | head -1 || echo unknown)
.venv/bin/python scripts/log_effectiveness.py \
--skill extract-state-type \
--scan-id "extract-state-type-${TARGET_SLUG}-$(date -u +%Y%m%d-%H%M%S)" \
--target "<original-target-spec>" \
--findings-total "${FIELD_COUNT}" \
--buckets "{\"shape\": \"${SHAPE}\", \"fields\": ${FIELD_COUNT}, \"callers\": ${CALLER_COUNT}}"
Stage 5 — Summarize
Report to the user in ≤10 lines:
- Target + shape (
dataclassorTypedDict). - Field count + caller count.
- Path to
${REPORT_DIR}/proposal.md. - Test matrix summary (baseline + per-subsystem).
- Stop-condition checklist count.
- Recommended next step:
/fix-workflow extract-state-type:<slug>for the human-approved execution path, OR manual migration if the proposal reviewer prefers. Do NOT start/fix-workflowyourself — the proposal is the handoff artifact.
Do not enumerate fields in the summary — the proposal is the source of truth.
Replay / smoke
For a no-production-code replay, create a tiny Python fixture with one
function that initializes and mutates a state = {...} dict, then run
collect_target.py --file <fixture> --symbol <function> --project-root <fixture-root> --output <tmp>/targets.json. Paste the command output
showing dict_candidates=1 and the output JSON path. This replay proves
the executable Form-B contract that Form A resolves into.
Non-goals
- Executing the migration (that's
/fix-workflow). - Detecting new implicit-dict states elsewhere (that's
/find-implicit-state; the sub-shape isn't shipped yet — Form B remains the robust entry for now). - Proposing a dataclass when the dict is a boundary object (use
TypedDict); proposing aTypedDictwhen the dict is mutated in place across a pipeline (usedataclass). The scout classifies; the orchestrator respects that. - Touching files outside
reports/extract-state-type/<slug>/. - Running tests — the proposal lists the matrix;
/fix-workflowruns it. - Rewriting the target's private helpers. The proposal covers the
public contract (dict → type); private helpers change as a
consequence, and their line-by-line edits belong to
/fix-workflow.
When things go sideways
| Symptom | Action |
|---|---|
Form-A candidate sub-shape is not implicit_dict_state | Abort with a one-line error: "that candidate is a <sub-shape> — use /extract-enum or /introduce-fk"; don't silently fall back |
| Form-B target file doesn't exist | Abort with a one-line error + suggest correct path |
| Target function has no dict-state — it's just a function | Abort with a one-line "target has no implicit-dict-state; re-check the target"; don't produce an empty proposal |
targets.json lists 0 callers | Proceed; the proposal notes "no external callers" in the caller table and the migration is cheaper |
Scout returns profile_incomplete first try | Re-dispatch once with a stricter nudge |
Scout disagrees on dataclass vs TypedDict between runs | Rare, but if it happens: prefer dataclass for any state that is mutated after construction; TypedDict only for state that is constructed once and read-only thereafter |
| The dict's shape is too dynamic to type (arbitrary string keys) | The scout flags it; proposal says "dict is dynamic — do NOT typeify; document the string-key convention in a docstring instead" and stops |
| Nested dict is deeply conditional (5+ branches each write different keys) | Propose a union of dataclasses discriminated on a single field; the scout's profile lists the discriminator candidates |
Repository layout
.claude/skills/extract-state-type/
├── SKILL.md # this file — orchestrator
├── scripts/
│ └── collect_target.py # Stage 1 target+caller inventory (stdlib-only)
├── agents/
│ └── state-profiler.md # Stage 2 scout brief
└── knowledge/ # scout context, never loaded by orchestrator
├── state-conventions.md # shape decision rules
└── proposal-template.md # Stage 3 output template + worked example
The orchestrator (you) never reads files in knowledge/. Those
are for the scout sub-agent. Keeping them out of your context is the
whole point of this architecture.
What ships with it: 4 files
44.7 KB alongside SKILL.md, 1 of them executable
agents/
- state-profiler.md10.5 KB
knowledge/
- proposal-template.md18.3 KB
- state-conventions.md1.6 KB
scripts/
- collect_target.pyruns14.3 KB