| name | anti-legacy:graph-translator |
| description | The ยงI5 domain-graph re-think. Takes the ANNOTATED wicked-estate code graph (the resolved-or-flagged evidence spine produced by anti-legacy:extraction) plus the cluster(weight="calls") capability communities and re-thinks them into the TARGET-state DOMAIN graph โ a capability-oriented requirements_graph.json, not a 1:1 code skeleton. Merges one-or-more source apps into ONE target system, honoring disposition (keep | modify | drop | new) so a reimagined-away rule is an EXPLICIT drop with a reason, never a silent omission. Emits the gate-validated requirements graph + a drop manifest + disposition-aware round-trip coverage. Use when: you reached here to "translate the call graph" / "build the requirements graph" / "re-think the annotated graph into capability domains" / orchestrate Phase 4 "Graph Translate" (after extraction, before blueprint).
|
anti-legacy:graph-translator โ the ยงI5 domain-graph re-think
This skill drives scripts/domain_graph.py (via the dispatcher). It is the ยงI5
re-think CLAUDE.md names: the second of the two graphs.
- CODE graph (the spine) โ the
wicked-estate index, annotated by
anti-legacy:extraction: every behavior-bearing node ends RESOLVED (a rule
at/above resolve_threshold) or RISK-flagged. This is the evidence.
- DOMAIN graph (what THIS skill builds) โ the TARGET requirements, re-thought
into capability-oriented domains. A capability PLAN, not a code skeleton.
The product is a behavior-preserving targeted rewrite: data shapes, interfaces,
and jobs are INVARIANT; only the implementation is reimagined. The domain graph
must COVER every code-graph requirement edge โ honoring disposition.
Business-rule EXTRACTION is NOT this skill. That is anti-legacy:extraction
(it annotates the graph). This skill consumes those annotations and re-thinks
them into the target domain graph. If you were sent here to extract rules from
source, run anti-legacy:extraction first.
Precondition: front-half coverage == 1.0
ยงI5 refuses to translate an incompletely-annotated graph. Before building, the
latest .anti-legacy/coverage-report.json must show coverage == 1.0 (every
behavior-bearing node RESOLVED or RISK). If it is below 1.0 the builder exits
non-zero listing the unaccounted SymbolIds โ go back to anti-legacy:extraction
and annotate them. Regenerate the report with:
python3 .anti-legacy/run.py coverage
Run it
The builder is dispatched through the workspace shim (never call scripts/
directly):
python3 .anti-legacy/run.py domain_graph \
--output .anti-legacy/requirements/requirements_graph.json
It reads config.source_apps + migration_mode from .anti-legacy/config.json,
resolves each app's per-app DB under .anti-legacy/graphs/<app>.db, and the
.anti-legacy/annotations.jsonl overlay.
migration_mode selects the target shape and is written verbatim into the graph's
metadata.migration_mode. The intended default is functional โ the domain
graph is a capability PLAN (capability-oriented domains from call-affinity
clusters), NOT a 1:1 code skeleton. structural produces code-equivalent nodes
for like-for-like rehost only. The builder honors migration_mode from config
(it does NOT silently default to structural); a functional config yields a
functional graph.
Useful flags:
--config <path> โ alternate config (default .anti-legacy/config.json).
--annotations <path> โ alternate overlay (default honors the
ANTI_LEGACY_ANNOTATIONS env, else .anti-legacy/annotations.jsonl).
--db <path> โ score a single DB instead of every source_apps DB.
--coverage-report <path> โ alternate front-half precondition report.
--schema <path> โ alternate enriched schema (default
schemas/requirements-graph.enriched.schema.json).
--net-new <path> โ a JSON list (or {"net_new": [...]}) of net-new TARGET
requirement specs ({domain, title, business_rules, data_access}) โ the
add-capability half of the merge (provenance="net-new", legacy_components=[],
exempt from the round-trip denominator). Also read from config.net_new.
--skip-front-half-check โ DRY-RUN/TEST ONLY; the gate requires the check in
production.
What it produces (three artifacts, under .anti-legacy/requirements/)
requirements_graph.json โ the primary, GATE_1_DESIGN-validated artifact.
Validates against schemas/requirements-graph.enriched.schema.json (Draft7,
object-form rules) with ZERO errors or the build fails.
dispositions.json โ the explicit DROP manifest. A legacy SymbolId listed
here with a drop_reason is intentionally reimagined away (not a coverage
gap). v1 emits no automatic drops; the manifest always exists so a curator can
record an explicit drop the round-trip check will honor.
roundtrip-coverage.json โ the disposition-aware round-trip gate evidence
({legacy_rule_total, represented, dropped, uncovered_symbol_ids, roundtrip_coverage}).
How the domain graph is built
Capability DOMAINS come from clusters, NEVER from files
Domains are derived from cluster(db, weight="calls") communities โ a call-affinity
community is a set of nodes that CALL each other, i.e. a behavioral unit (a
capability), independent of which file/copybook holds them.
weight="calls" is mandatory โ the only sound mode today. confidence and
data-affinity collapse to group-by-file via the engine's contains hub
(documented shim limitation in wicked_estate.cluster). The builder hardcodes
calls for that reason.
- Domains are file/copybook-derived NEVER (forbidden). A capability domain
spans merged sources; a file-derived domain cannot (files are per-repo).
cluster() / list_nodes() are single-DB; the cross-app merge happens after
per-DB clustering โ communities across apps that resolve the same capability
coalesce by domain name into one target domain.
- Entities are co-located into the capability's domain from the members'
data_access, so data_access โ own-domain entities (the T2 invariant). Data
follows the capability that uses it โ the inverse of the old "domain per data
file" anti-pattern.
Each target requirement carries
legacy_components โ MANDATORY, non-null: the sorted member SymbolIds the
capability derives from (the trace back to the code graph). [] only for
net-new.
business_rules โ OBJECT form (gate-1 {id, statement, source_ref, confidence, provenance}), re-numbered RULE-NNN per requirement, one rule per
behavior-bearing member (source_ref = that member's SymbolId). A RESOLVED
member emits its real statement; a RISK or UNACCOUNTED member emits a
REVIEW REQUIRED: <reason> rule that PRESERVES its statement / risk_reason and
its source_ref โ so the risk member's behavior is NEVER erased from the graph,
even when the requirement also has resolved rules. Any risk/unaccounted member
forces status="review".
provenance โ the source app id, or "net-new".
disposition + disposition_reason โ keep | modify | drop | new. Reason is
mandatory for modify/drop/new.
parity_hints (additive optional) โ one per detected numeric output
(money/rate/percent/count). The downstream anti-legacy:test-strategy phase
turns these into the contract's parity_rules (parity_rules live in the test
contract, NOT the requirements schema โ its rule objects are
additionalProperties:false).
data_access, dependencies (cross-cluster call edges โ that capability's
REQ_ID), validations, error_paths, status, merged_programs.
Disposition model (keep | modify | drop | new)
- keep โ โฅ1 resolved member rule, single-source.
disposition_reason: "behavior preserved from <app>".
- modify โ the same capability was contributed by >1 source app (cross-source
reconciliation) or required restructuring.
status forced to review so a
human signs at GATE_1; reason names what changed.
- drop โ a behavior-bearing legacy edge intentionally NOT made an active
requirement. NEVER an omission: it is written to
dispositions.json with
{symbol_id, app, legacy_rule_id, drop_reason, decided_by} (curator-authored).
build() READS an existing dispositions.json and honors it end-to-end โ the
round-trip treats a dropped-with-reason SymbolId as covered. A malformed manifest
is a hard error (a drop that cannot be read must not silently become "no drops").
- new โ a target capability with NO legacy origin (
provenance="net-new",
legacy_components=[]). Authored via --net-new / config.net_new. Exempt from
the round-trip denominator; still carries โฅ1 business rule and is schema-valid.
The core invariant (no silent maybe-correct)
The builder enforces disposition-aware round-trip coverage and exits non-zero
when it fails. Let:
- L = every behavior-bearing legacy requirement edge โ
{(app, symbol_id)}
for every behavior-bearing node carrying a rule (RESOLVED or RISK/UNACCOUNTED;
each is an edge that must be accounted for).
- T = the
(app, source_ref) of every emitted business_rule in an
active/review requirement โช every SymbolId in the drop manifest with a
non-empty reason (DROP).
COVERAGE_RULE: L โ T, i.e. roundtrip_coverage = |{l in L : l in T}| / |L| == 1.0.
Representation is graded at rule granularity (business_rule.source_ref), NOT
at symbol granularity (legacy_components). A member whose behavior rule is absent
from the graph is uncovered even if its symbol still rides in legacy_components โ
this is the exact silent-erasure the cardinal invariant forbids. A behavior edge in
NEITHER set is a silent drop; the builder lists each uncovered
(app, symbol_id, state) and exits non-zero, mirroring coverage.py.
The front-half precondition is enforced TWICE: the coverage-report.json scalar
(coverage == 1.0) AND a re-derivation from the SAME overlay the builder reads (so
a stale report cannot let an unannotated node through). The round-trip evidence also
carries a clustering diagnostic (degenerate=true when every behavior community
is a singleton โ a disconnected/batch estate where weight="calls" found no
call-affinity to group on; the reviewer confirms the domains are real distinct
capabilities, not a laundered file/program 1:1 partition).
This is the DESIGN-time (GATE_1) round-trip check; it is complementary to
scripts/compare_graphs.py, which re-checks per-requirement rule_coverage at
BUILD time (GATE_3_BUILD / 3B). The builder keeps REQ_IDs and business_rule object
ids stable + schema-valid so compare_graphs.split_item() joins them.
Done looks like
domains=N requirements=M roundtrip_coverage=1.0000 legacy_rule_total=K represented=K dropped=0
wrote .anti-legacy/requirements/requirements_graph.json
roundtrip_coverage == 1.0 (no silent drops).
- Zero schema errors against the enriched profile.
- Front-half
coverage == 1.0 was a precondition.
If any fails, the builder exits non-zero with the offending SymbolIds โ fix the
gap (annotate, represent, or explicitly drop with a reason); do NOT advance.
Still not done (per ยง6)
- Cross-app call dependencies are wired within a single DB โ and for the
INDEPENDENT-SYSTEMS merge case that is correct, not a gap (ISS-08). Independent
legacy systems coalesce at the capability level (domain+title), not by calling
each other's code: the two real source apps share 0 call targets and emit 0
cross-app/unresolved edges, so there is nothing to resolve across DBs. If a
config DID have one repo calling into another (a shared library), that surfaces
as an unresolved edge target and would be resolved via the engine's
cross-graph. How two MERGED capabilities INTERACT in the target is a
target-design decision made at blueprint time โ a target dependency, not a
legacy-call edge โ so it is intentionally not synthesized here.
- Automatic drops are not emitted โ every resolved edge is represented. The
drop mechanism + manifest exist for a human curator; no rule is dropped without
a person writing the reason.
- Disconnected/batch estates (independent programs chained by JCL, not by
CALL) cluster into singleton-per-program communities.
weight="calls" cannot
merge nodes that do not call each other, so the partition degenerates to one
capability per program. The builder does NOT silently pass this off as capability
domains: the round-trip evidence carries clustering.degenerate=true for the
reviewer to confirm the domains are real distinct capabilities. Coalescing
same-capability singletons by rule-text similarity is deliberately NOT done
(it would guess at capability identity โ "silent maybe-correct"); the human
confirms instead. A native call-graph that resolves JCL step ordering into call
affinity would fix this at the source.
roundtrip-coverage.json IS now a registered GATE_1_DESIGN evidence id (ISS-10):
orchestrate Phase 4b registers it (manifest register roundtrip-coverage) and
anti-legacy:gatekeeper's GATE_1_DESIGN check verifies it's registered and that
roundtrip_coverage >= 1.0 โ so the disposition-aware round-trip is a checksummed
audit seam the design gate cites, not signalling-only. This skill produces the
evidence; the gate consumes it.
PEP done-gate (AGENTS.md ยง10 โ Full PEP)
Before declaring done, run all six steps:
Step 3 โ Antagonist (pre-build, before the producer runs)
python3 .anti-legacy/run.py antagonist context --phase graph-translate
Steps 2 & 4 โ Adversarial review + resolve loop (after the producer runs)
python3 .anti-legacy/run.py refine_loop descriptor --artifact requirements-graph
Adversarially self-review the requirements graph (advisory โ AGENTS.md ยง8)
The builder's own assertions (round-trip == 1.0, zero schema errors) are mechanical;
they cannot see a domain that laundered a file 1:1 partition into "capabilities", a
RESOLVED rule whose statement drifted from its source, or a disposition reason that
does not justify the drop. Before you report done, adversarially review the
requirements_graph.json you just produced โ the round-trip is trusting; this is the
loop that distrusts it.
Run the commands from the PEP done-gate โ Steps 2 & 4 section above (steps 2 and 4
of the Phase Execution Protocol). That section resolves the single-artifact critic target
(requirements-graph) and runs the bounded refine loop. Advisory: it clears no gate
(GATE_1_DESIGN is still a human sign-off) and advances no phase.
Migration note
anti-legacy:orchestrate Phase 4 ("Graph Translate") dispatches this skill
(after anti-legacy:extraction, before anti-legacy:blueprint).
scripts/graph_normalizer.py is the OLD code-graph-JSON scaffold builder. It is
NOT extended and NOT deleted โ it stays green for its existing tests. ยงI5
SUPERSEDES its role with scripts/domain_graph.py, which reads the LIVE engine +
overlay (there is no code-graph JSON in the wicked-estate world) and emits
POPULATED object-form rules (graph_normalizer emits empty rule slots).