mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* docs: enforce the docs/ publication boundary with a frozen .mintignore and CI gate docs/ mixes the public Mintlify site with internal engineering docs, and omission from docs.json navigation is not a publication boundary: a page left out of navigation is still deployed, reachable by URL, and indexable. docs/design/ and docs/research/ were never added to docs/.mintignore, so both internal docs have been served as hidden pages on the public site. Close the gap and the process hole behind it: * Move docs/design/ and docs/research/ under docs/internal/, the one growing home for internal material — new internal docs now land inside the fence by default instead of requiring a .mintignore edit. * Freeze docs/.mintignore: scripts/ci/docs_publication_boundary.py rejects any new entry (legacy directories stay listed until consolidated into internal/; entries may only be removed). * Gate in CI (Code Style): every .md/.mdx under docs/ must be in docs.json navigation, matched by .mintignore, or carry `hidden: true` frontmatter marking a deliberately unlisted public page; navigation entries must have a source file. The gate has its own has_docs trigger because docs-only PRs skip every Rust lane, and it is checked in the roll-up before the has_code early exit so it blocks docs-only PRs too. Regression coverage: scripts/ci/test_docs_publication_boundary.py (16 cases, run by the CI job before the check; one pins the real docs/ tree as clean). Red/green verified: the checker flagged exactly docs/design/agent-activity-streaming.md and docs/research/pi-agent-deep-dive.md before the fix and passes after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: relocate legacy internal doc directories under docs/internal/ — text-only Move plans/, superpowers/, qa/, adr/, architecture-video/, and reborn-binary.md from docs/ into docs/internal/, and rewrite every repo reference to the old paths (guidance files, script and workflow comments, Rust doc comments, the render-architecture-video VIDEO_DIR, the architecture-video skill, .coderabbit.yaml). Behavior unchanged: all references to these directories were textual except the video script's VIDEO_DIR, the skill paths, and the .coderabbit.yaml ignore, which are updated in step. docs/reborn/ deliberately stays put: its path is load-bearing (ironclaw_capabilities and ironclaw_architecture_tests read contract files from it at test time, and reborn-e2e.yml scope filters match it — pinned by scripts/ci/ws12_workflow_contracts.py). It consolidates into internal/ in a follow-up when those consumers can move with it; docs/.mintignore and FROZEN_MINTIGNORE_PATTERNS shrink to internal/ + reborn/ accordingly. Verified: docs publication boundary check green, its 16 self-tests green, ws12 workflow contracts green (46), touched YAML parses, zero references to the old paths remain outside git history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover the docs gate's entry point and pin its trigger in ws12 contracts Fixes the three findings from the multi-agent code review of this branch (security/bugs/performance/conventions clean; tests reviewer found 3): * main() was never called by any test — the exit-code contract, the three stderr violation blocks, and main()'s MintignoreSyntaxError handling were uncovered, so a regression returning 0 despite violations would have passed all tests while turning the CI gate into a no-op. Three new tests drive main() directly (clean tree, all violation classes, syntax error). * The has_docs trigger grep and the fail-closed roll-up guard had no pin. ws12_workflow_contracts.py now carries a has_docs CrateScopeFilter (docs/, the gate's own files, and the workflow in scope; crates and README out) plus code_style.yml REQUIRED_MARKERS for the job, both steps, and the roll-up guard — with sabotage tests proving narrowing the grep or removing a marker fails loudly. Red/green verified. * is_ignored()'s slash-glob pattern branch (contains '/' but not trailing) had no fixture; covered with design/*.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin the docs-gate guard's ordering, not just its presence Fixes the three findings from review round 2 (security/bugs/performance clean; tests found 2, conventions found 1): * REQUIRED_MARKERS is presence-only, so relocating the docs-gate roll-up guard to after the has_code early exit — the exact silent-skip bug the guard exists to prevent — passed every contract check. New validate_code_style_docs_guard_order() pins guard-before-early-exit in code_style.yml, with a sabotage test that relocates the guard line and a checked-in-order pass test. Red/green verified. * CrateScopeFilterSabotageTests' docstring still described "the three remaining crate-keyed filters"; updated for the fourth, non-crate-keyed has_docs pin (review-discipline.md: guardrail docs must match the code). * is_ignored()'s nested-directory pattern branch (a trailing-slash entry with an internal slash, e.g. `design/sub/`) never executed under the suite; covered by test_mintignore_nested_directory_pattern_fences. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix relative ADR links missed by the path sweep; align checker hint with the frozen fence Addresses the Copilot review on #7259: * The reference sweep rewrote literal `docs/adr` strings but not relative markdown links: `../../adr/` in target-architecture/{CHECKLIST,PROPOSAL}.md, `../../../adr/` in families/domains.md, and the hooks CLAUDE.md link (which also carried a pre-existing wrong depth from the WS7 family move) all resolved to the old location. A repo-wide relative-link scan found exactly these seven move-caused breaks; the remaining broken links predate this branch (WS7 crate-move fallout in testing-playbook.md, one dead June plan link) or are Mintlify extensionless links that resolve on the site. * The checker's remediation hint said "add its directory to docs/.mintignore", contradicting the frozen-fence rule the same script enforces; it now directs authors to move internal material under docs/internal/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: only executable guard occurrences satisfy the docs-gate order pin CodeRabbit (Major, #7259): validate_code_style_docs_guard_order used a raw str.find, so a commented-out copy of the guard above the has_code early exit — a realistic refactor leftover — satisfied the pin while the executable guard sat below the exit, silently unhooking the gate for docs-only PRs. The validator now strips comment lines before matching and requires EVERY live guard occurrence to precede the first early-exit occurrence; a comment-only occurrence reports the order as unassertable. New decoy sabotage test red/green verified. Also parenthesized the implicit string concatenations Ruff flagged (ISC004). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docs): align all Remotion packages to 4.0.499 CodeRabbit flagged the moved architecture-video project's manifest: Remotion requires every @remotion/* package at one identical version, but dependabot #6658 bumped only @remotion/cli and @remotion/tailwind-v4 to 4.0.499, leaving remotion, @remotion/transitions, and @remotion/eslint-config-flat at 4.0.447 — a pre-existing break on main that surfaced here because the directory rename presents as a new project. Aligned all five to 4.0.499 and regenerated the lockfile; `npx remotion versions` now reports all packages at the correct version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: assert both signals in the literal-pattern fencing case Copilot (#7259): test_mintignore_literal_file_fences used a pattern outside the frozen allowlist but discarded the `unexpected` result, so it passed while the checker it pins would fail — a misleading regression pin. The case now asserts both independent signals explicitly: the literal entry still fences its file (no publication leak) AND trips the frozen-list rule, with the interplay documented in the test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: mark the architecture video as stale pre-Reborn content Copilot flagged the relocated video scenes for citing crates/ironclaw_engine paths that no longer exist. The scenes are untouched April 2026 content (#2365) presenting as new because of the directory rename; regenerating them against the Reborn architecture is deliberately out of scope for this move-only PR. Until that regeneration happens, a prominent README banner states what the video describes, why it is wrong today, where current docs live (openwiki/), and how to regenerate (architecture-video skill) — so the content cannot mislead contributors in the meantime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: probe docs/docs.json in the has_docs scope pin Copilot (#7259): navigation is half the publication-boundary contract — a nav-only edit can orphan a page into hidden-page territory or reference a missing source file — but no has_docs probe covered docs.json, so a future markdown-only narrowing of the trigger grep (e.g. ^docs/.*\.(md|mdx)$) would silently skip the gate for nav changes while every existing probe stayed green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): map the two sweep-touched dev scripts and satisfy rustfmt Two root causes behind the red CI on #7259, both fallout from the docs path sweep touching files no docs-only change normally touches: * reborn_composition_boundaries.rs reads the (moved) composition pub-use snapshot; the longer docs/internal/plans/ path pushed the line past rustfmt's width. Reformatted. * The Reborn PR test planner fails closed on unmapped repo-root scripts. The sweep touched two local dev tools no workflow invokes — check-type-duplicates.py (docstring path) and render-architecture-video.sh (VIDEO_DIR path) — mapped both with per-file decisions in the planner's established style. Verified by running the planner against this branch's full 173-file changed list (green) plus its 61 self-tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: commit the messaging-framework path rewrites dropped by the previous merge The prior merge commit staged files before running the path sweep, so its rewrites of #6831's new files (docs/superpowers -> docs/internal/superpowers in three Rust doc comments, the plan, and standard-operations.md) were left unstaged and its message wrongly called the sweep a no-op. This commit is those rewrites. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
92 lines
3.3 KiB
Python
Executable File
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/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 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()
|