Files
ironclaw/scripts/check-type-duplicates.py
Henry Park 3fd143933a docs(guidance): repo-wide agent-guidance audit — fix drift, prune 21.5k lines, consolidate tests/ onto AGENTS.md convention (#7797)
* docs(guidance): repo-wide agent-guidance audit — fix drift, prune 21.5k lines, consolidate tests/ onto AGENTS.md convention

Full-layer audit of the agent-guidance system (root contracts, .claude rules/
skills/commands, family and crate AGENTS.md, CONTRACT specs, tests guidance,
docs/internal), verified reference-by-reference against HEAD.

- Fix stale/ghost references: UserSandboxProcessPort, ProductSurfaceError,
  LlmError::ContextLengthExceeded, INVERTED_PORT_IMPLEMENTORS, split channel
  traits (ChannelIngress/ChannelReply/ChannelDelivery), memory-native's
  never-implemented EmbeddingProvider seam, wrong layer/crate/module counts.
- Convert unpinned prose numbers to regeneration commands or pinning-test
  citations across root, family, and crate guidance (drift-proofing).
- tests/: rename CLAUDE.md -> AGENTS.md with CLAUDE.md symlinks (crates/
  convention), extend scripts/ci/check-guidance.py discovery to tests/,
  delete stale e2e scenario tables, dedupe tier taxonomy against
  .claude/rules/testing.md.
- Commands/skills: delete six dead v1 commands (add-tool, review-pr,
  review-crate, fix-issue, respond-pr, add-sse-event) and the v1-teaching
  architecture-video skill; convert ironclaw-reborn-skill-maintainer into
  the auto-loading rule .claude/rules/guidance-maintenance.md; fix clippy
  -D warnings and portable date in surviving commands; triggers-only
  frontmatter; add automations section to reborn-feature.
- Rules: rename gateway-events.md -> events.md; revive
  scripts/check-type-duplicates.py (glob matched zero types since the
  family reorg); index all 15 rules in root AGENTS.md for Codex parity.
- docs/internal: delete 70 superseded plans/specs/design docs (~21.5k
  lines, each re-verified unreferenced); fix misleading v1-migration
  status lines; rewrite the contracts index as a recipe; restore two docs
  that proved live-referenced.
- Trim composition CONTRACT.md route-mirror sections (invariants kept).

Verified: check-guidance.py (384 files, 0 grandfathered),
docs_publication_boundary.py, cargo test -p ironclaw_architecture_tests,
scripts/ci test-plan suite (87/87) — all green.

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

* docs(guidance): address PR #7797 review comments

Verified each of the ~40 bot/reviewer findings against the tree; applied the
valid mechanical fixes, rebutted the rest with evidence (see PR comment).

- Test planner: add renamed tests/ guidance aliases to IGNORED_GUIDANCE_PATHS
  (reproduced the fail-closed abort on this PR's own changed-file list) and
  extend the planner test to all six guidance paths.
- check-guidance: add self-tests proving tests/-tree discovery and alias
  enforcement (48 tests, was 46); new self-test file for
  check-type-duplicates.py (4 tests).
- Portability: replace GNU-only date -d in triage commands with a python3
  one-liner (works on macOS BSD and Linux).
- Count/claim accuracy: product_contracts manager-port prose 4 -> 3 (matches
  INVERTED_PORT_IMPLEMENTORS), kernel grep -cF for literal #[test] (was regex
  char class, 2 vs 23), rg -o|wc -l for a true total in architecture.md,
  Rust-scoped LlmProvider count (catches 5 generic impls), measured
  1/73-crate dual-backend claim in pr-shepherd, executable wc -l in
  assistant guidance, AST/pytest recipes for the e2e test-count figures.
- Content: deslop co-author line no longer hardcodes an address; ship.md
  surfaces Postgres-skip counts; risk-label guidance documents the crates/**
  labeler blind spot; e2e authoring recipe leads with reborn_v2_* fixtures;
  stale CLAUDE.md line citation replaced with a stable anchor; ✎ provenance
  notes for two deleted-plan citations; unified-channel-model status text
  reconciled with an explicit ChannelDelivery-only exception note.

Gates: check-guidance (384 files, 0 grandfathered), docs boundary, planner
tests 87/87, check-guidance self-test 48/48, type-dup self-test 4/4 - green.

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

* fix(ci): classify scripts/test-check-type-duplicates.py in the PR test planner

The self-test added in 77bc8f8c5 was never registered in
PR_STATIC_CONTROL_PATHS, so the planner's fail-closed unmapped-path arm
aborted 'Detect Reborn test scope' and cascaded into the whole Reborn
matrix skipping. Classified like its subject (deliberately CI-unwired
local dev tool, per the existing entry's rationale) and pinned in the
static-control planner test alongside it.

Verified: planner tests OK; planner run against this PR's full
changed-file list now returns mode=selected with the path owned by
static checks.

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

* fix(docs): drop parity-QA path reference from tests/integration/AGENTS.md

The guidance dedup in this PR added a literal
tests/support/reborn_parity_qa reference to tests/integration guidance,
which scripts/ci/check-test-suite-boundaries.sh correctly flags: the
one-way dependency guard covers docs too, and origin/main's version of
this file carried no such reference. Fix the content, not the check -
the tier comparison is reworded to describe the RebornBinaryE2EHarness
seam difference without naming the parity/QA tree.

Verified: check-test-suite-boundaries.sh OK; check-guidance OK; the
full 'Detect Reborn test scope' job reproduced locally end-to-end.

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

* ci: run the type-duplicates self-test in Code Style; exercise the production report path

Address the two follow-up review findings on 77bc8f8c5/9ed30caf8:

- Wire scripts/test-check-type-duplicates.py into code_style.yml's
  Static-check self-tests step (next to test-check-guidance.py) so the
  regression test is actually enforced; update the planner's
  PR_STATIC_CONTROL_PATHS comment accordingly (classification unchanged
  - Code Style is the static lane).
- Strengthen the semantic-duplicate self-test to also drive the
  production main() report path and assert on its printed candidate
  output, instead of only re-computing similarity locally. Strictly
  stronger; the other three tests are untouched.

Verified: self-test 4/4, planner tests 87/87, workflow-contract
self-test 94/94, planner simulation over both changed paths classifies
cleanly (no unmapped-path abort).

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

* ci: trigger fast-checks on type-duplicates script changes; symbol recipe for the tier reference

- code_style.yml's has_code filter covered scripts/ci/ but not the bare
  scripts/ type-duplicates pair, so a diff touching only the new
  self-test skipped the fast-checks job that runs it (the previous
  commit's 'runs unconditionally' claim was wrong - corrected the
  planner comment too). Added the two files to the filter following the
  check_no_panics precedent and pinned them as in_scope probes in the
  ws12 workflow-contracts routing test.
- tests/integration/AGENTS.md tier reference is now re-verifiable via a
  single-hit symbol recipe (rg 'struct RebornBinaryE2EHarness') instead
  of a path citation, which the test-suite boundary guard forbids from
  this subtree.

Verified: ws12 workflow contracts 94/94, planner tests 87/87,
check-test-suite-boundaries OK, check-guidance OK.

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

---------

Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 21:26:50 +00:00

94 lines
3.4 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/internal/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 itertools.chain(
Path('crates').glob('*/*/src'),
Path('crates/extensions/packages').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()