Infer the 3-tier domain → capability → feature catalog (scope/features.yaml) from scan-index.json during /codify by enumerating per-capability modules, routes, and entry-point wiring, then assigning a 5-point status (planned | development | rollout | released | cleanup) from release tags, churn, test presence, and deprecation markers. Used exclusively by product-keeper after infer-enriched-capabilities-from-code.
Infer the 3-tier domain → capability → feature catalog (scope/features.yaml) from scan-index.json during /codify by enumerating per-capability modules, routes, and entry-point wiring, then assigning a 5-point status (planned | development | rollout | released | cleanup) from release tags, churn, test presence, and deprecation markers. Used exclusively by product-keeper after infer-enriched-capabilities-from-code.
version
0.1.0
user-invocable
false
model
sonnet
allowed-tools
Read, Write, Grep, Glob
deprecated
true
deprecated_note
#434 ProductOS realignment — superseded by the command model; retained for Phase E reference, not installed
infer-features-from-code
Called by product-keeper during /codify, after
infer-enriched-capabilities-from-code. Produces scope/features.yaml at
{stm_base}/{issue}/evidence/codify/proposals/scope/features.yaml.
Purpose
Once domains and enriched capabilities are proposed, the spec trunk needs
the canonical 3-tier feature catalog — the same artifact manage-features
writes in /specify Stage 4b. In /codify the feature list and per-feature
status must be reverse-engineered from code. This skill reads
scan-index.json, walks every selected capability from the sibling
proposals, enumerates feature candidates from modules / routes / entry-point
wiring, assigns each feature a 5-point status via the heuristic below, and
records every status decision in a status_inferences manifest for
downstream surfacing.
scan_index_path parses as JSON with keys manifests, trees,
entry_points, docs, config_files, frontend_detection, patterns,
git. Missing → scan_index_missing.
Every file in related_proposal_paths exists and is valid YAML. Missing
any → missing_related_proposal. Parent dirs for the four output paths
exist or are creatable.
2. Resolution Protocol walk
Per core/components/memory/standards/rules/resolution.md, record every
probe in resolution_trace_path. R1 skipped (codify bootstrap; reason
"codify-bootstrap"). R2 probes {product_base}/scope/features.yaml — if
present, read informationally only; prior IDs, name shapes, and status
values land in the trace. The skill still emits a fresh proposal; it does
NOT short-circuit to alignment_confirmed. Conflicts between an existing
catalog and the inferred proposal are surfaced as status_inferences
entries at tier=mid. R3 / R4 skipped (feature taxonomy is project-specific).
3. Read related proposals
Pull selected domain + capability slugs from scope.yaml +
domain-selection.yaml. Pull per-capability notes (module hints, anchored
framework idioms) from enriched-capabilities.yaml. These feed Step 4.
4. Enumerate feature candidates per capability
For each selected capability, walk the scan index through these signal lanes:
Top-level modules under src/. Direct children of the source root
from trees. When convention nests deeper (src/{capability}/*), descend
one more level under the capability subfolder.
Per-capability subfolders. When the capability slug matches a
directory name, scope enumeration to that subtree. Each direct subfolder
is a feature candidate.
Route / handler counts. From entry_points and
patterns.framework_idioms, count routes (Express / FastAPI / Gin /
Spring / Rails) per module. Multiple distinct route groups → multiple
features; a single route group → one feature.
Entry-point wiring. From entry_points, identify modules registered
in application bootstrap (main, app, server, cmd). Unregistered folders
land as scaffold-only candidates (feeds planned status in Step 5).
patterns.naming_suffix_counts. Suffix counts (*Repository,
*Service, *Controller, *Handler) confirm the feature exists but do
NOT on their own raise the status above planned.
Name each feature from the module name in title case, plus — when available —
the nearest matching README header (docs.readme_preview). Names are noun
phrases of 3–10 words per manage-features R2. ID regex ^[A-Z]{2,4}-F\d{3}$;
PREFIX is the capability's canonical abbreviation from
enriched-capabilities.yaml. Number features within a capability from
F001 in enumeration order.
5. Assign per-feature status — heuristic (first match wins)
cleanup — module present AND a deprecation marker is hit. Markers:
literal @deprecated annotation in any module file, DEPRECATED in
module README, or deprecated: true frontmatter. Evidence: the exact
file path + ≤200-char excerpt.
released — module present AND a release tag in git.tags_recent
references the module path OR a semver ≥ 1.0 tag exists for the repo
AND test signal is present for the module
(patterns.test_framework_signals intersects the module path) AND the
module does NOT appear in git.churn_top (churn is below the hot-file
threshold → "stable").
rollout — module present AND test signal present AND either no
release tag yet OR all tags are pre-1.0 (built, not fully released).
development — module present AND either partial test coverage
(tests exist for some files in the module but not all) OR the module
appears in git.churn_top (active churn). Active work-in-flight signal.
planned — module referenced in README, config, or an entry-point
import path BUT the directory is absent OR contains only scaffolding
(empty index.ts, lone types.ts, naming-suffix stubs with zero
route/handler wiring).
Fallback → planned — no rule resolves. Recorded at tier=low
with grounding_source=insufficient-signal.
Confidence per status:
high — ≥ 2 signal lanes agree (e.g., release tag + test + low churn).
medium — 1 strong signal OR 2 weak signals.
low — fallback or single weak signal.
6. Build the 3-tier tree
Nest features under their capability; nest capabilities under their domain.
Slugs come verbatim from domain-selection.yaml.selected_domains[].id and
enriched-capabilities.yaml.capabilities[].id — never relocate, never
invent.
Capability-level status rollup (per manage-features precedence): any
released → released; else any rollout → rollout; else any
development → development; else any cleanup → cleanup; else
planned. Domain-level status is freeform (pass through from enriched
record or leave blank) — C4 of manage-features forbids coercing it to
the 5-point enum.
Write order (manifests first so mid-write crashes leave an audit trail):
resolution trace → status inferences → decision manifest → primary artifact.
Every per-feature status lands in status_inferences_path with
feature_id, assigned_status, heuristic_rule (1–6 above), signals
(concrete evidence paths), confidence, alternatives_considered, tier.
Output
Primary artifact — scope/features.yaml
meta:source_type:"inferred_from_code"evidence:-"scan-index.json#/trees"-"scan-index.json#/entry_points"-"scan-index.json#/patterns/framework_idioms"-"scan-index.json#/patterns/naming_suffix_counts"-"scan-index.json#/patterns/test_framework_signals"-"scan-index.json#/git/tags_recent"-"scan-index.json#/git/churn_top"-"scan-index.json#/docs/readme_preview"confidence:"high"|"medium"|"low"# aggregate over featureslearning_category:"product"sub_category:nulltier:2summary:domains:<int>capabilities:<int>features_total:<int>features_planned:<int>features_development:<int>features_rollout:<int>features_released:<int>features_cleanup:<int>domains:-id:"<domain-slug>"status:"<freeform or blank>"capabilities:-id:"<capability-slug>"status:"planned | development | rollout | released | cleanup"features:-id:"<PREFIX-Fnnn>"name:"<noun phrase, 3–10 words>"status:"planned | development | rollout | released | cleanup"description:"<1–2 sentences from module README/docstring>"evidence_paths:-"<repo-relative module path>"-"scan-index.json#<pointer>"confidence:"high | medium | low"
Standard Garura shape (schema_version, skill, generated_at,
decisions[]). One entry per inferred status, per name massaged away from
the raw module identifier, and per capability-level rollup. Each carries
decision_id, decision_type, tier, grounding_source,
recommendation, alternatives, chosen_reason.
missing_related_proposal — scope.yaml / enriched-capabilities.yaml /
domain-selection.yaml absent or unparseable.
scan_index_missing — scan_index_path absent or not valid JSON.
ltm_resolution_failed — R2 errored (existing features.yaml not valid
YAML, or product_base resolves outside the repo sandbox).
insufficient_signal — scan present but zero modules under any selected
capability AND zero entry-point wiring AND zero README references. No
candidate list is inferable; product-keeper falls back to human input.
Boundaries
Read-only against scan_index_path, related_proposal_paths, and (for
R2) {product_base}/scope/features.yaml. Emits a proposal only — writing
to .garura/product/scope/features.yaml is a downstream /garura:enrich action.
Does NOT re-run scan.py, invent capabilities or domains outside the
sibling proposals, or produce intent epics.