Files
ironclaw/scripts/check-type-duplicates.py
firat.sertgoz 1a58cdae0b docs(reborn): final crate/module refactor design (#5529)
* docs(reborn): final crate/module refactor design

Blueprint for decomposing the reborn stack by redistributing mass inside
crates rather than adding or removing crates. Supersedes the abandoned
6-crate composition extraction (reversed on fan-in evidence).

- 76-crate refactor map with per-crate verdicts (KEEP / INTERNAL / FOLD /
  MERGE / JIT), driven by fan-in x size analysis
- composition god-crate (153k) dissected into 10 internal modules with an
  11-PR leaf-first sequence, from a six-agent coupling analysis
- subagent-loop eval harness: machine gates G1-G6, structural gates S1-S5,
  judge rubric, plus a pub-use API-freeze baseline
- pre-move characterization test plan (T1-T8) for the leaked-block extractions
- final target architecture (L0-L6 layer map), RebornServicesApi domain-port
  split, seam registry, and a roadmap overlay mapping each planned feature to
  its landing zones and reduced footprint

Docs only; no code changes. Executed later via the 11-PR loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): make the target module count consistent (≤12)

The headline said ~11 while the S1 gate and §6.1 table said ≤15 — a subagent runs
S1 literally, so the gate could pass at 15 while the 11-module architecture is
missed. Align all three to ≤12: root + the 10 domain modules of §3/§6.2 plus the
test_support top-level mod. Reported by CodeRabbit on PR #5529.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): harden eval gates from review (G3 postgres, S4 cfg-capture)

- G3: add `postgres` to the composition test feature set so the eval catches
  postgres-backend breakage, not just libsql.
- S4: capture #[cfg(...)] attributes above each `pub use` in the freeze snapshot
  (regenerated: 170 -> 200 lines, 30 gate attrs that were previously invisible),
  so a silently dropped/changed feature gate on an export is caught.
- Note that --test-threads=1 is a flake discriminator, not the fix; the durable
  fix is lock_env().

Addresses Gemini review on PR #5529.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): name the exact bundled_skills frozen artifacts

Replace vague "embedded JSON filenames" with the concrete, verified contract from
build.rs + src/bundled_skills.rs: embedded_reborn_skill_summaries.json,
embedded_reborn_skill_bundles.json, the marker file .ironclaw-reborn-bundled.json,
and the owner string ironclaw_reborn_composition_bundled_skill. Addresses CodeRabbit;
note its suggested embedded_catalog.json / embedded_skills.json do not exist in this
crate (verified by grep).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): add type-placement rule + semantic dedup detector + judged backlog

Companion to the crate-design work: governs type location/multiplicity and the mirror-pipeline change-one-thing-edit-N-DTOs cost. .claude/rules/type-placement.md (placement ladder + mirror resolution order: pass-through->import, additive->serde(flatten), subtractive/redaction->keep manual; pub use only at a mandated contract facade). scripts/check-type-duplicates.py (field/variant-signature scan, not name grep). docs/plans/2026-07-02-type-dedup-backlog.md (all 178 pairs judged: 18 TRUE-DUP clustered with owners, 14 borderline lockstep mirrors, do-not-fix list). Docs/rule/script only; no code behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(reborn): trait audit — abstraction-must-earn-its-keep rule + judged inventory

Measured all 352 traits in crates/: 62% have 2+ prod impls; 31 cross-crate DIP seams, 30 test-seamed, 48 dyn-injection singles all judged keep; only 8 (2.3%) are ceremony/dead (agent-verified by reading defs/impls/call sites). Adds the trait clause to type-placement.md (justified by polymorphism, inversion, test seam, or dyn injection — else inline) and the removable-trait table + explicit-keep list to the backlog doc. Also documents the mechanical-audit pitfall: naive impl-grep misses blanket/generic/macro impls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:13:20 +03:00

92 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Detect cross-crate SEMANTIC duplicate-type candidates by field/variant signature.
Name matching misses the real failure mode (same DTO job, different names), so
this compares (field-name, field-type) sets for structs and variant sets for
enums across crates and reports pairs above a similarity threshold.
Output is CANDIDATES, not verdicts: a match is either a true duplicate (unify
into its owner), a justified mirror (independent wire/domain evolution), or a
coincidental shape. Judge each pair by reading the definitions before acting.
See .claude/rules/type-placement.md; judged backlog (2026-07):
docs/plans/2026-07-02-type-dedup-backlog.md.
Usage: python3 scripts/check-type-duplicates.py [--jaccard 0.6] [--min-items 3]
"""
import argparse
import itertools
import re
from pathlib import Path
DEF_RE = re.compile(r'^pub (struct|enum) ([A-Za-z0-9_]+)(?:<[^>]*>)?\s*\{', re.M)
FIELD_RE = re.compile(r'(?:pub(?:\([^)]*\))?\s+)?([a-z_][a-z0-9_]*)\s*:\s*([^,\n]+)')
VARIANT_RE = re.compile(r'^\s{4}([A-Z][A-Za-z0-9_]*)', re.M)
KEYWORDS = {'where', 'if', 'let', 'match'}
def body_of(text, start):
i = text.index('{', start)
depth, j = 0, i
while j < len(text):
if text[j] == '{':
depth += 1
elif text[j] == '}':
depth -= 1
if depth == 0:
break
j += 1
return text[i + 1:j]
def collect(min_items):
types = []
for src in Path('crates').glob('*/src'):
crate = src.parent.name.removeprefix('ironclaw_')
for f in src.rglob('*.rs'):
try:
text = f.read_text(errors='ignore')
except OSError:
continue
for m in DEF_RE.finditer(text):
kind, name = m.group(1), m.group(2)
body = body_of(text, m.start())
if kind == 'struct':
items = frozenset(
(fm.group(1), re.sub(r'\s+', '', fm.group(2)).rstrip(','))
for fm in FIELD_RE.finditer(body)
if fm.group(1) not in KEYWORDS)
else:
items = frozenset((vm.group(1), '') for vm in VARIANT_RE.finditer(body))
if len(items) >= min_items:
types.append((crate, kind, name, items, f))
return types
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--jaccard', type=float, default=0.6)
ap.add_argument('--min-items', type=int, default=3)
args = ap.parse_args()
types = collect(args.min_items)
found = 0
for a, b in itertools.combinations(types, 2):
if a[0] == b[0] or a[1] != b[1]:
continue
i1, i2 = a[3], b[3]
union = len(i1 | i2)
jac = len(i1 & i2) / union if union else 0
n1 = frozenset(x[0] for x in i1)
n2 = frozenset(x[0] for x in i2)
njac = len(n1 & n2) / len(n1 | n2) if (n1 | n2) else 0
if jac >= args.jaccard or (njac >= 0.75 and min(len(n1), len(n2)) >= 4):
found += 1
print(f"jac={jac:.2f} njac={njac:.2f} {a[1]:6} "
f"{a[0]}::{a[2]} <-> {b[0]}::{b[2]}")
print(f"\n{found} candidate pair(s) from {len(types)} types "
f"(>= {args.min_items} items). Judge before acting.")
if __name__ == '__main__':
main()