From 6528ce78b97ada6f5a17474c01cbdcda15a99561 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 22 Apr 2026 09:09:15 +0000 Subject: [PATCH] Scripts: page migration helpers for one-per-file layout Python + shell tooling used to split grouped index.sx files into one-directory-per-page layout (see the hyperscript gallery migration). name-mapping.json records the rename table; strip_names.py is a helper for extracting component names from .sx sources. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/migrate-pages.sh | 389 ++++++++++++++++++++++++++++ scripts/migrate_one_per_file.py | 446 ++++++++++++++++++++++++++++++++ scripts/name-mapping.json | 256 ++++++++++++++++++ scripts/strip_names.py | 170 ++++++++++++ 4 files changed, 1261 insertions(+) create mode 100755 scripts/migrate-pages.sh create mode 100644 scripts/migrate_one_per_file.py create mode 100644 scripts/name-mapping.json create mode 100644 scripts/strip_names.py diff --git a/scripts/migrate-pages.sh b/scripts/migrate-pages.sh new file mode 100755 index 00000000..2fed7295 --- /dev/null +++ b/scripts/migrate-pages.sh @@ -0,0 +1,389 @@ +#!/bin/bash +# migrate-pages.sh — Restructure SX page files so file paths match URL structure +# All moves use git mv. Directories created with mkdir -p. +# If source doesn't exist, skip with warning. If target exists, skip. +set -euo pipefail + +cd /root/rose-ash + +# Base directory for all moves +B="sx/sx" + +# First, stage all current changes in sx/sx/ so that the one-per-file split +# files become tracked by git (they are currently untracked). +echo "=== Staging current sx/sx/ changes ===" +git add sx/sx/ +echo " Done. $(git diff --cached --name-only | wc -l) files staged." + +MOVED=0 +SKIPPED=0 +WARNINGS=0 + +safe_mv() { + local src="$B/$1" + local dst="$B/$2" + if [ ! -f "$src" ]; then + echo " WARN: source missing: $1" + WARNINGS=$((WARNINGS + 1)) + return + fi + if [ -f "$dst" ]; then + echo " SKIP: target exists: $2" + SKIPPED=$((SKIPPED + 1)) + return + fi + local dir + dir=$(dirname "$dst") + mkdir -p "$dir" + git mv "$src" "$dst" + echo " $1 -> $2" + MOVED=$((MOVED + 1)) +} + +echo "" +echo "=== HOME ===" +safe_mv "docs-content/home-content.sx" "home/index.sx" + +echo "" +echo "=== GEOGRAPHY: core pages ===" +safe_mv "geography/capabilities.sx" "geography/capabilities/index.sx" +safe_mv "geography/modules.sx" "geography/modules/index.sx" +safe_mv "geography/eval-rules.sx" "geography/eval-rules/index.sx" + +echo "" +echo "=== GEOGRAPHY: CEK ===" +safe_mv "geography/cek/cek-content.sx" "geography/cek/index.sx" +safe_mv "geography/cek/cek-demo-content.sx" "geography/cek/demo/index.sx" +safe_mv "geography/cek/cek-freeze-content.sx" "geography/cek/freeze/index.sx" +safe_mv "geography/cek/cek-content-address-content.sx" "geography/cek/content/index.sx" + +# CEK islands (demo-* files) +for f in $B/geography/cek/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "geography/cek/$base" "geography/cek/_islands/$base" +done +safe_mv "geography/cek/content-address-demo.sx" "geography/cek/_islands/content-address-demo.sx" +safe_mv "geography/cek/freeze-demo.sx" "geography/cek/_islands/freeze-demo.sx" + +echo "" +echo "=== GEOGRAPHY: Reactive Islands index ===" +safe_mv "reactive-islands/index/reactive-islands-index-content.sx" "geography/reactive/index.sx" +for f in $B/reactive-islands/index/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "reactive-islands/index/$base" "geography/reactive/_islands/$base" +done + +echo "" +echo "=== GEOGRAPHY: Reactive Islands demo (examples) ===" +safe_mv "reactive-islands/demo/reactive-islands-demo-content.sx" "geography/reactive/examples/index.sx" +for f in $B/reactive-islands/demo/example-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + slug="${base#example-}" + slug="${slug%.sx}" + safe_mv "reactive-islands/demo/$base" "geography/reactive/examples/$slug/index.sx" +done + +echo "" +echo "=== GEOGRAPHY: Marshes ===" +safe_mv "reactive-islands/marshes/reactive-islands-marshes-content.sx" "geography/marshes/index.sx" +for f in $B/reactive-islands/marshes/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "reactive-islands/marshes/$base" "geography/marshes/_islands/$base" +done +for f in $B/reactive-islands/marshes/example-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + slug="${base#example-}" + slug="${slug%.sx}" + safe_mv "reactive-islands/marshes/$base" "geography/marshes/$slug/index.sx" +done + +echo "" +echo "=== GEOGRAPHY: Reactive lib files ===" +safe_mv "reactive-islands/event-bridge.sx" "geography/reactive/_lib/event-bridge.sx" +safe_mv "reactive-islands/named-stores.sx" "geography/reactive/_lib/named-stores.sx" +safe_mv "reactive-islands/phase2.sx" "geography/reactive/_lib/phase2.sx" +safe_mv "reactive-islands/plan.sx" "geography/reactive/_lib/plan.sx" +safe_mv "reactive-islands/test-runner.sx" "geography/reactive/_lib/test-runner.sx" +safe_mv "reactive-islands/test-temperature.sx" "geography/reactive/_lib/test-temperature.sx" +safe_mv "reactive-islands/runner-placeholder/test-runner-placeholder.sx" "geography/reactive/_lib/test-runner-placeholder.sx" + +# demo-cyst and demo-reactive-expressions → _lib +for f in $B/reactive-islands/demo-cyst/*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "reactive-islands/demo-cyst/$base" "geography/reactive/_lib/$base" +done +for f in $B/reactive-islands/demo-reactive-expressions/*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "reactive-islands/demo-reactive-expressions/$base" "geography/reactive/_lib/$base" +done + +echo "" +echo "=== GEOGRAPHY: Scopes ===" +safe_mv "scopes/scopes-content.sx" "geography/scopes/index.sx" +for f in $B/scopes/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "scopes/$base" "geography/scopes/_islands/$base" +done +safe_mv "scopes/scopes-demo-example.sx" "geography/scopes/_islands/scopes-demo-example.sx" + +echo "" +echo "=== GEOGRAPHY: Provide ===" +safe_mv "provide/provide-content.sx" "geography/provide/index.sx" +for f in $B/provide/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "provide/$base" "geography/provide/_islands/$base" +done +safe_mv "provide/provide-demo-example.sx" "geography/provide/_islands/provide-demo-example.sx" + +echo "" +echo "=== GEOGRAPHY: Spreads ===" +safe_mv "spreads/spreads-content.sx" "geography/spreads/index.sx" +for f in $B/spreads/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "spreads/$base" "geography/spreads/_islands/$base" +done + +echo "" +echo "=== GEOGRAPHY: Reactive Runtime ===" +safe_mv "reactive-runtime/overview-content.sx" "geography/reactive-runtime/index.sx" + +# Other content files → {slug}/index.sx (strip -content suffix) +for f in $B/reactive-runtime/*-content.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + [ "$base" = "overview-content.sx" ] && continue + slug="${base%-content.sx}" + safe_mv "reactive-runtime/$base" "geography/reactive-runtime/$slug/index.sx" +done + +# demo-* → _islands +for f in $B/reactive-runtime/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "reactive-runtime/$base" "geography/reactive-runtime/_islands/$base" +done + +echo "" +echo "=== GEOGRAPHY: Hypermedia Reference ===" +safe_mv "reference/attrs-content.sx" "geography/hypermedia/reference/attributes/index.sx" +safe_mv "reference/headers-content.sx" "geography/hypermedia/reference/headers/index.sx" +safe_mv "reference/events-content.sx" "geography/hypermedia/reference/events/index.sx" +safe_mv "reference/js-api-content.sx" "geography/hypermedia/reference/js-api/index.sx" + +# Reference shared files +safe_mv "reference/attr-detail-content.sx" "geography/hypermedia/reference/_shared/attr-detail-content.sx" +safe_mv "reference/attr-not-found.sx" "geography/hypermedia/reference/_shared/attr-not-found.sx" +safe_mv "reference/header-detail-content.sx" "geography/hypermedia/reference/_shared/header-detail-content.sx" +safe_mv "reference/event-detail-content.sx" "geography/hypermedia/reference/_shared/event-detail-content.sx" + +# Reference index +safe_mv "examples/reference-index-content.sx" "geography/hypermedia/reference/index.sx" + +# Example page shared +safe_mv "examples/page-content.sx" "geography/hypermedia/example/_shared/page-content.sx" + +# Examples-content → example/{slug}/index.sx +for f in $B/examples-content/example-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + slug="${base#example-}" + slug="${slug%.sx}" + safe_mv "examples-content/$base" "geography/hypermedia/example/$slug/index.sx" +done + +echo "" +echo "=== GEOGRAPHY: Isomorphism ===" +safe_mv "analyzer/bundle-analyzer-content.sx" "geography/isomorphism/bundle-analyzer/index.sx" +# Analyzer islands (non-content files) +for f in $B/analyzer/*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + [ "$base" = "bundle-analyzer-content.sx" ] && continue + safe_mv "analyzer/$base" "geography/isomorphism/bundle-analyzer/_islands/$base" +done + +safe_mv "routing-analyzer/content.sx" "geography/isomorphism/routing-analyzer/index.sx" +safe_mv "routing-analyzer/routing-row.sx" "geography/isomorphism/routing-analyzer/_islands/routing-row.sx" + +safe_mv "async-io-demo.sx" "geography/isomorphism/async-io/index.sx" + +safe_mv "affinity-demo/content.sx" "geography/isomorphism/affinity/index.sx" +for f in $B/affinity-demo/aff-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "affinity-demo/$base" "geography/isomorphism/affinity/_islands/$base" +done + +safe_mv "optimistic-demo.sx" "geography/isomorphism/optimistic/index.sx" +safe_mv "offline-demo.sx" "geography/isomorphism/offline/index.sx" + +echo "" +echo "=== LANGUAGE: Docs ===" +safe_mv "docs-content/docs-introduction-content.sx" "language/doc/introduction/index.sx" +safe_mv "docs-content/docs-getting-started-content.sx" "language/doc/getting-started/index.sx" +safe_mv "docs-content/docs-components-content.sx" "language/doc/components/index.sx" +safe_mv "docs-content/docs-evaluator-content.sx" "language/doc/evaluator/index.sx" +safe_mv "docs-content/docs-primitives-content.sx" "language/doc/primitives/index.sx" +safe_mv "docs-content/docs-special-forms-content.sx" "language/doc/special-forms/index.sx" +safe_mv "docs-content/docs-server-rendering-content.sx" "language/doc/server-rendering/index.sx" + +echo "" +echo "=== LANGUAGE: Spec ===" +safe_mv "specs/architecture-content.sx" "language/spec/index.sx" +safe_mv "specs/overview-content.sx" "language/spec/_shared/overview-content.sx" +safe_mv "specs/detail-content.sx" "language/spec/_shared/detail-content.sx" +safe_mv "specs/not-found.sx" "language/spec/_shared/not-found.sx" + +echo "" +echo "=== LANGUAGE: Bootstrappers ===" +safe_mv "specs/bootstrappers-index-content.sx" "language/bootstrapper/index.sx" +safe_mv "specs/bootstrapper-js-content.sx" "language/bootstrapper/javascript/index.sx" +safe_mv "specs/bootstrapper-py-content.sx" "language/bootstrapper/python/index.sx" +safe_mv "specs/bootstrapper-self-hosting-content.sx" "language/bootstrapper/self-hosting/index.sx" +safe_mv "specs/bootstrapper-self-hosting-js-content.sx" "language/bootstrapper/self-hosting-js/index.sx" + +echo "" +echo "=== LANGUAGE: Page helpers ===" +safe_mv "page-helpers-demo/content.sx" "language/bootstrapper/page-helpers/index.sx" +for f in $B/page-helpers-demo/demo-*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + safe_mv "page-helpers-demo/$base" "language/bootstrapper/page-helpers/_islands/$base" +done + +echo "" +echo "=== LANGUAGE: Testing ===" +safe_mv "testing/overview-content.sx" "language/test/index.sx" +safe_mv "testing/runners-content.sx" "language/test/runners/index.sx" +safe_mv "testing/spec-content.sx" "language/test/_shared/spec-content.sx" + +echo "" +echo "=== APPLICATIONS: HTMX ===" +safe_mv "htmx/demo-content.sx" "applications/htmx/index.sx" +safe_mv "htmx/_test/demo-content.sx" "applications/htmx/_test/demo-content.sx" + +echo "" +echo "=== APPLICATIONS: Hyperscript ===" +safe_mv "hyperscript/playground-content.sx" "applications/hyperscript/index.sx" +safe_mv "hyperscript/htmx-content.sx" "applications/hyperscript/htmx/index.sx" +safe_mv "hyperscript/compile-result.sx" "applications/hyperscript/_islands/compile-result.sx" +safe_mv "hyperscript/example.sx" "applications/hyperscript/_islands/example.sx" +safe_mv "hyperscript/live-demo.sx" "applications/hyperscript/_islands/live-demo.sx" +safe_mv "hyperscript/playground.sx" "applications/hyperscript/_islands/playground.sx" +safe_mv "hyperscript/translation.sx" "applications/hyperscript/_islands/translation.sx" + +echo "" +echo "=== APPLICATIONS: CSSX ===" +safe_mv "cssx/overview-content.sx" "applications/cssx/index.sx" +for f in $B/cssx/*-content.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + [ "$base" = "overview-content.sx" ] && continue + slug="${base%-content.sx}" + safe_mv "cssx/$base" "applications/cssx/$slug/index.sx" +done + +echo "" +echo "=== APPLICATIONS: Protocols ===" +safe_mv "protocols/wire-format-content.sx" "applications/protocol/index.sx" +for f in $B/protocols/*-content.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + [ "$base" = "wire-format-content.sx" ] && continue + slug="${base%-content.sx}" + safe_mv "protocols/$base" "applications/protocol/$slug/index.sx" +done + +echo "" +echo "=== APPLICATIONS: Standalone pages ===" +safe_mv "sx-urls.sx" "applications/sx-urls/index.sx" +safe_mv "native-browser.sx" "applications/native-browser/index.sx" +safe_mv "sxtp.sx" "applications/sxtp/index.sx" + +echo "" +echo "=== APPLICATIONS: GraphQL ===" +safe_mv "graphql/demo-content.sx" "applications/graphql/index.sx" +safe_mv "graphql/parse-island.sx" "applications/graphql/_islands/parse-island.sx" + +echo "" +echo "=== APPLICATIONS: Pretext ===" +safe_mv "pretext-demo/content.sx" "applications/pretext/index.sx" +safe_mv "pretext-demo/live.sx" "applications/pretext/_islands/live.sx" +safe_mv "pretext-demo/render-paragraph.sx" "applications/pretext/_islands/render-paragraph.sx" + +echo "" +echo "=== ETC: Essays ===" +safe_mv "essays/index.sx" "etc/essay/index.sx" + +# Philosophy essays → etc/philosophy/ +safe_mv "essays/philosophy-index.sx" "etc/philosophy/index.sx" +safe_mv "essays/sx-manifesto.sx" "etc/philosophy/sx-manifesto/index.sx" +safe_mv "essays/godel-escher-bach.sx" "etc/philosophy/godel-escher-bach/index.sx" +safe_mv "essays/sx-and-wittgenstein.sx" "etc/philosophy/wittgenstein/index.sx" +safe_mv "essays/sx-and-dennett.sx" "etc/philosophy/dennett/index.sx" +safe_mv "essays/s-existentialism.sx" "etc/philosophy/existentialism/index.sx" +safe_mv "essays/platonic-sx.sx" "etc/philosophy/platonic-sx/index.sx" + +# Regular essays → etc/essay/{name}/index.sx +PHILOSOPHY_ESSAYS="sx-manifesto godel-escher-bach sx-and-wittgenstein sx-and-dennett s-existentialism platonic-sx" +for f in $B/essays/*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + name="${base%.sx}" + [ "$name" = "index" ] && continue + [ "$name" = "philosophy-index" ] && continue + skip=false + for p in $PHILOSOPHY_ESSAYS; do + if [ "$name" = "$p" ]; then + skip=true + break + fi + done + $skip && continue + safe_mv "essays/$base" "etc/essay/$name/index.sx" +done + +echo "" +echo "=== ETC: Plans ===" +safe_mv "plans/index.sx" "etc/plan/index.sx" + +# Theorem prover (subdirectory) +safe_mv "plans/theorem-prover/plan-theorem-prover-content.sx" "etc/plan/theorem-prover/index.sx" +safe_mv "plans/theorem-prover/prove-phase1-row.sx" "etc/plan/theorem-prover/_islands/prove-phase1-row.sx" +safe_mv "plans/theorem-prover/prove-phase2-row.sx" "etc/plan/theorem-prover/_islands/prove-phase2-row.sx" + +# Regular plan files → etc/plan/{name}/index.sx +for f in $B/plans/*.sx; do + [ -f "$f" ] || continue + base=$(basename "$f") + name="${base%.sx}" + [ "$name" = "index" ] && continue + safe_mv "plans/$base" "etc/plan/$name/index.sx" +done + +echo "" +echo "=== TOOLS ===" +safe_mv "sx-tools/overview-content.sx" "tools/sx-tools/index.sx" +safe_mv "sx-tools-demos.sx" "tools/sx-tools/_lib/sx-tools-demos.sx" +safe_mv "sx-tools-editor.sx" "tools/sx-tools/_lib/sx-tools-editor.sx" +safe_mv "services-tools.sx" "tools/services/index.sx" +safe_mv "playground/content.sx" "tools/playground/index.sx" +safe_mv "playground/repl.sx" "tools/playground/_islands/repl.sx" + +echo "" +echo "=======================================" +echo "Migration complete!" +echo " Moved: $MOVED" +echo " Skipped: $SKIPPED" +echo " Warnings: $WARNINGS" +echo "=======================================" diff --git a/scripts/migrate_one_per_file.py b/scripts/migrate_one_per_file.py new file mode 100644 index 00000000..744f9d74 --- /dev/null +++ b/scripts/migrate_one_per_file.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +"""Migrate sx_docs components to one-definition-per-file convention. + +Reads all .sx files under sx/sx/ and sx/sxc/, splits multi-definition +files into one file per definition. + +Usage: + python3 scripts/migrate_one_per_file.py --dry-run # preview + python3 scripts/migrate_one_per_file.py # execute +""" +import os +import sys +import json +import argparse +from pathlib import Path + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from shared.sx.parser import parse_all, serialize +from shared.sx.types import Symbol, Keyword + +NAMED_DEFS = {"defcomp", "defisland", "defmacro", "defpage", + "defhandler", "defstyle", "deftype", "defeffect", + "defrelation", "deftest"} + +SKIP_FILES = {"boundary.sx"} + + +def get_def_info(expr): + """Return (keyword, name) for a definition, or None.""" + if not isinstance(expr, list) or not expr: + return None + head = expr[0] + if not isinstance(head, Symbol): + return None + + kw = head.name + + if kw in NAMED_DEFS: + if len(expr) < 2 or not isinstance(expr[1], Symbol): + return None + return (kw, expr[1].name.lstrip("~")) + + if kw == "define": + if len(expr) < 2: + return None + if isinstance(expr[1], Symbol): + return ("define", expr[1].name) + elif (isinstance(expr[1], list) and expr[1] + and isinstance(expr[1][0], Symbol)): + return ("define", expr[1][0].name) + return None + + return None + + +def derive_local_name(def_name, file_rel_path): + """Derive short filename for a definition within the file's directory. + + Strategy: + 1. If name contains '/', split on LAST '/' → namespace + local. + Then strip redundant namespace-as-prefix from local. + 2. If no '/', try stripping the file's full path (as hyphens) prefix. + 3. Else use the full name. + + Examples: + name: examples/card file: examples.sx → card + name: layouts/doc file: layouts.sx → doc + name: reactive-islands-demo/example-counter + file: reactive-islands/demo.sx → example-counter + name: geography-cek/geography-cek-cek-content + file: geography/cek.sx → cek-content + name: docs-nav-items file: nav-data.sx → docs-nav-items + """ + if '/' in def_name: + namespace, local = def_name.rsplit('/', 1) + # Strip redundant namespace prefix from local part + # e.g. geography-cek/geography-cek-cek-content → cek-content + ns_prefix = namespace + '-' + if local.startswith(ns_prefix): + stripped = local[len(ns_prefix):] + if stripped: + return stripped + return local + + # No / in name — try stripping file path prefix + stem = os.path.splitext(file_rel_path)[0] + path_prefix = stem.replace('/', '-').replace('\\', '-') + '-' + if def_name.startswith(path_prefix): + remainder = def_name[len(path_prefix):] + if remainder: + return remainder + + # Try just the file stem + file_stem = Path(file_rel_path).stem + stem_prefix = file_stem + '-' + if def_name.startswith(stem_prefix): + remainder = def_name[len(stem_prefix):] + if remainder: + return remainder + + return def_name + + +def extract_form_sources(source, exprs): + """Extract original source text for each top-level form. + + Walks the source text tracking paren depth to find form boundaries. + Returns list of (source_text, is_comment_block) for each expression + plus any preceding comments. + """ + results = [] + pos = 0 + n = len(source) + + for expr_idx in range(len(exprs)): + # Collect leading whitespace and comments + comment_lines = [] + form_start = pos + + while pos < n: + # Skip whitespace + while pos < n and source[pos] in ' \t\r\n': + pos += 1 + if pos >= n: + break + + if source[pos] == ';': + # Comment line + line_start = pos + while pos < n and source[pos] != '\n': + pos += 1 + if pos < n: + pos += 1 # skip newline + comment_lines.append(source[line_start:pos].rstrip()) + continue + + # Found start of form + break + + if pos >= n: + break + + # Extract the form + if source[pos] == '(': + depth = 0 + in_string = False + escape = False + form_body_start = pos + + while pos < n: + c = source[pos] + if escape: + escape = False + elif c == '\\' and in_string: + escape = True + elif c == '"': + in_string = not in_string + elif not in_string: + if c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth == 0: + pos += 1 + break + pos += 1 + + form_text = source[form_body_start:pos] + # Include preceding comments + if comment_lines: + full_text = '\n'.join(comment_lines) + '\n' + form_text + else: + full_text = form_text + + results.append(full_text) + else: + # Non-paren form (symbol, etc.) + start = pos + while pos < n and source[pos] not in ' \t\r\n': + pos += 1 + results.append(source[start:pos]) + + return results + + +def process_directory(base_dir, dry_run=True): + """Process all .sx files, return split plan.""" + + splits = [] # (source_file, target_file, content, kw, old_name) + single = [] # (source_file, kw, old_name) + no_defs = [] # files with no definitions + errors = [] + + for root, dirs, files in os.walk(base_dir): + dirs[:] = [d for d in dirs if d not in ('__pycache__', '.cache', '.pytest_cache')] + + for filename in sorted(files): + if not filename.endswith('.sx') or filename in SKIP_FILES: + continue + + filepath = os.path.join(root, filename) + rel_path = os.path.relpath(filepath, base_dir) + + try: + with open(filepath, encoding='utf-8') as f: + source = f.read() + except Exception as e: + errors.append((rel_path, str(e))) + continue + + try: + exprs = parse_all(source) + except Exception as e: + errors.append((rel_path, f"Parse: {e}")) + continue + + # Classify + defs = [] + non_defs = [] + for expr in exprs: + info = get_def_info(expr) + if info: + defs.append((expr, info)) + else: + non_defs.append(expr) + + if not defs: + no_defs.append(rel_path) + continue + + if len(defs) == 1 and not non_defs: + # Single definition — stays as-is + _, (kw, name) = defs[0] + single.append((rel_path, kw, name)) + continue + + # Multiple definitions — split + file_stem = Path(filename).stem + file_dir = os.path.dirname(rel_path) + target_dir = os.path.join(file_dir, file_stem) + + # Get original source for each form + form_sources = extract_form_sources(source, exprs) + + all_exprs = [] + for expr in exprs: + info = get_def_info(expr) + all_exprs.append((expr, info)) + + # Deduplicate: keep only the LAST definition for each name + seen_names = {} + for idx, (expr, info) in enumerate(all_exprs): + if info: + seen_names[info[1]] = idx + last_idx_for_name = set(seen_names.values()) + + for idx, (expr, info) in enumerate(all_exprs): + if info is None: + # Non-def form + continue + if idx not in last_idx_for_name: + # Earlier duplicate — skip + continue + + kw, name = info + local = derive_local_name(name, rel_path) + safe_local = local.replace('/', '-') + target_file = os.path.join(target_dir, safe_local + '.sx') + + # Use original source if available, else serialize + if idx < len(form_sources): + content = form_sources[idx] + else: + content = serialize(expr, pretty=True) + + splits.append((rel_path, target_file, content, kw, name)) + + # Non-def forms: collect into _init.sx + if non_defs: + init_parts = [] + # Find their original source + non_def_idx = 0 + for idx, (expr, info) in enumerate(all_exprs): + if info is None: + if idx < len(form_sources): + init_parts.append(form_sources[idx]) + else: + init_parts.append(serialize(expr, pretty=True)) + non_def_idx += 1 + + if init_parts: + init_content = '\n\n'.join(init_parts) + init_file = os.path.join(target_dir, '_init.sx') + splits.append((rel_path, init_file, init_content, 'init', '_init')) + + return splits, single, no_defs, errors + + +def main(): + parser = argparse.ArgumentParser(description="Migrate SX to one-per-file") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--dir", default=None) + args = parser.parse_args() + + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + os.chdir(project_root) + + dirs = [args.dir] if args.dir else ["sx/sx", "sx/sxc"] + + all_splits = [] + all_single = [] + all_no_defs = [] + all_errors = [] + + for d in dirs: + if not os.path.isdir(d): + print(f"Skip {d}") + continue + + print(f"\n{'='*60}") + print(f" {d}") + print(f"{'='*60}") + + splits, single, no_defs, errors = process_directory(d, args.dry_run) + + # Prefix with base dir for full paths + for s in splits: + src, tgt, content, kw, name = s + all_splits.append((d, os.path.join(d, src), os.path.join(d, tgt), + content, kw, name)) + for s in single: + path, kw, name = s + all_single.append((d, os.path.join(d, path), kw, name)) + all_no_defs.extend(os.path.join(d, p) for p in no_defs) + all_errors.extend((os.path.join(d, p), e) for p, e in errors) + + # Check conflicts + target_map = {} + conflicts = [] + for _, src, tgt, content, kw, name in all_splits: + if tgt in target_map: + conflicts.append((tgt, target_map[tgt], (kw, name, src))) + else: + target_map[tgt] = (kw, name, src) + + # Group splits by source file + by_source = {} + for _, src, tgt, content, kw, name in all_splits: + by_source.setdefault(src, []).append((tgt, kw, name)) + + # Report + if all_errors: + print(f"\n--- Errors ({len(all_errors)}) ---") + for p, e in all_errors: + print(f" {p}: {e}") + + if conflicts: + print(f"\n--- {len(conflicts)} Conflicts ---") + for tgt, existing, new in conflicts: + print(f" {tgt}") + print(f" existing: {existing[1]} from {existing[2]}") + print(f" new: {new[1]} from {new[2]}") + + total_new = len(all_splits) + print(f"\n{'='*60}") + print(f" Summary") + print(f"{'='*60}") + print(f" Files to split: {len(by_source)}") + print(f" New files: {total_new}") + print(f" Single-def (keep): {len(all_single)}") + print(f" No-defs (skip): {len(all_no_defs)}") + print(f" Conflicts: {len(conflicts)}") + + if args.dry_run: + print(f"\n--- Split plan ---") + for src in sorted(by_source.keys()): + targets = by_source[src] + print(f"\n {src} → {len(targets)} files:") + for tgt, kw, name in sorted(targets): + print(f" {tgt} ({kw})") + + print(f"\n--- Single-def files ---") + for _, path, kw, name in sorted(all_single)[:15]: + print(f" {path} ({kw} {name})") + if len(all_single) > 15: + print(f" ... and {len(all_single) - 15} more") + + # Show a sample + if all_splits: + _, src, tgt, content, kw, name = all_splits[0] + print(f"\n--- Sample: {tgt} ---") + lines = content.split('\n') + for line in lines[:15]: + print(f" {line}") + if len(lines) > 15: + print(f" ... ({len(lines)} lines total)") + + print(f"\nDry run. Run without --dry-run to execute.") + return + + # Execute + if conflicts: + print("Aborting due to conflicts.") + sys.exit(1) + + created = 0 + for _, src, tgt, content, kw, name in all_splits: + os.makedirs(os.path.dirname(tgt), exist_ok=True) + if os.path.exists(tgt): + print(f" SKIP (exists): {tgt}") + continue + with open(tgt, 'w', encoding='utf-8') as f: + f.write(content.rstrip() + '\n') + created += 1 + + # Delete source files + deleted = 0 + for src in by_source: + if os.path.exists(src): + os.remove(src) + deleted += 1 + + print(f"\n Created {created}, deleted {deleted} source files.") + + # Build name mapping: old_name -> new_path_name + mapping = {} + for _, src, tgt, _, kw, old_name in all_splits: + if kw in ('init', 'preamble'): + continue + # Determine which base_dir this target is in + for d in dirs: + if tgt.startswith(d + '/'): + new_name = os.path.splitext(os.path.relpath(tgt, d))[0] + if old_name != new_name: + mapping[old_name] = new_name + break + + mapping_file = "scripts/name-mapping.json" + with open(mapping_file, 'w') as f: + json.dump(mapping, f, indent=2, sort_keys=True) + print(f" Name mapping: {mapping_file} ({len(mapping)} entries)") + + +if __name__ == "__main__": + main() diff --git a/scripts/name-mapping.json b/scripts/name-mapping.json new file mode 100644 index 00000000..5f75c951 --- /dev/null +++ b/scripts/name-mapping.json @@ -0,0 +1,256 @@ +{ + "__app-config": "app-config/__app-config", + "_spec-dirs": "data/helpers/_spec-dirs", + "_spec-search-dirs": "data/helpers/_spec-search-dirs", + "_spec-slug-map": "data/helpers/_spec-slug-map", + "adapter-spec-items": "nav-language/adapter-spec-items", + "add-page-test": "pages/page-tests/add-page-test", + "affinity-demo": "pages/docs/affinity-demo", + "all-spec-items": "nav-language/all-spec-items", + "api": "handlers/dispatch/api", + "applications": "page-functions/applications", + "applications-index": "pages/docs/applications-index", + "async-io-demo": "pages/docs/async-io-demo", + "attr-detail-data": "data/helpers/attr-detail-data", + "attr-details": "data/reference-details/attr-details", + "behavior-attrs": "data/reference/behavior-attrs", + "bootstrapper": "page-functions/bootstrapper", + "bootstrapper-data": "data/helpers/bootstrapper-data", + "bootstrapper-page": "pages/docs/bootstrapper-page", + "bootstrappers-index": "pages/docs/bootstrappers-index", + "bootstrappers-nav-items": "nav-language/bootstrappers-nav-items", + "browser-spec-items": "nav-language/browser-spec-items", + "build-code-tokens": "stepper-lib/build-code-tokens", + "bundle-analyzer": "pages/docs/bundle-analyzer", + "bundle-analyzer-data": "data/helpers/bundle-analyzer-data", + "call-handler": "handlers/dispatch/call-handler", + "capabilities": "page-functions/capabilities", + "catalog-items": "handlers/reactive-api/catalog-items", + "cek": "page-functions/cek", + "cek-index": "pages/docs/cek-index", + "cek-nav-items": "nav-geography/cek-nav-items", + "cek-page": "pages/docs/cek-page", + "component-source": "data/helpers/component-source", + "core-spec-items": "nav-language/core-spec-items", + "cssx": "page-functions/cssx", + "cssx-index": "pages/docs/cssx-index", + "cssx-nav-items": "nav-data/cssx-nav-items", + "cssx-page": "pages/docs/cssx-page", + "data-test": "pages/docs/data-test", + "data-test-data": "data/helpers/data-test-data", + "doc": "page-functions/doc", + "docs-index": "pages/docs/index", + "docs-nav-items": "nav-language/docs-nav-items", + "docs-page": "pages/docs/page", + "essay": "page-functions/essay", + "essay-page": "pages/docs/essay-page", + "essays-index": "pages/docs/essays-index", + "essays-nav-items": "nav-data/essays-nav-items", + "etc": "page-functions/etc", + "etc-index": "pages/docs/etc-index", + "eval-handler-body": "handlers/dispatch/eval-handler-body", + "eval-rules": "page-functions/eval-rules", + "event-detail-data": "data/helpers/event-detail-data", + "event-details": "data/reference-details/event-details", + "events-list": "data/reference/events-list", + "example": "page-functions/example", + "examples": "page-functions/examples", + "examples-index": "pages/docs/examples-index", + "examples-nav-items": "nav-geography/examples-nav-items", + "examples-page": "pages/docs/examples-page", + "explore": "page-functions/explore", + "extension-spec-items": "nav-language/extension-spec-items", + "find-current": "nav-tree/find-current", + "find-nav-index": "nav-tree/find-nav-index", + "find-nav-match": "nav-tree/find-nav-match", + "find-spec": "nav-language/find-spec", + "flash-sale-prices": "handlers/reactive-api/flash-sale-prices", + "geography": "page-functions/geography", + "geography-index": "pages/docs/geography-index", + "geography/demo-callout": "spreads/demo-callout", + "geography/demo-cssx-tw": "spreads/demo-cssx-tw", + "geography/demo-emit-collect": "provide/demo-emit-collect", + "geography/demo-example": "spreads/demo-example", + "geography/demo-nested-provide": "provide/demo-nested-provide", + "geography/demo-provide-basic": "provide/demo-provide-basic", + "geography/demo-reactive-spread": "spreads/demo-reactive-spread", + "geography/demo-scope-basic": "scopes/demo-scope-basic", + "geography/demo-scope-dedup": "scopes/demo-scope-dedup", + "geography/demo-scope-emit": "scopes/demo-scope-emit", + "geography/demo-semantic-vars": "spreads/demo-semantic-vars", + "geography/demo-spread-basic": "spreads/demo-spread-basic", + "geography/demo-spread-mechanism": "provide/demo-spread-mechanism", + "geography/provide-content": "provide/provide-content", + "geography/provide-demo-example": "provide/provide-demo-example", + "geography/scopes-content": "scopes/scopes-content", + "geography/scopes-demo-example": "scopes/scopes-demo-example", + "geography/spreads-content": "spreads/spreads-content", + "handler-source": "data/helpers/handler-source", + "has-descendant-href?": "nav-tree/has-descendant-href?", + "header-detail-data": "data/helpers/header-detail-data", + "header-details": "data/reference-details/header-details", + "home": "pages/docs/home", + "host-spec-items": "nav-language/host-spec-items", + "hypermedia": "page-functions/hypermedia", + "hypermedia-index": "pages/docs/hypermedia-index", + "hyperscript": "page-functions/hyperscript", + "hyperscript-nav-items": "nav-data/hyperscript-nav-items", + "isomorphism": "page-functions/isomorphism", + "isomorphism-index": "pages/docs/isomorphism-index", + "isomorphism-nav-items": "nav-geography/isomorphism-nav-items", + "isomorphism-page": "pages/docs/isomorphism-page", + "js-api-list": "data/reference/js-api-list", + "language": "page-functions/language", + "language-index": "pages/docs/language-index", + "make-page-fn": "page-functions/make-page-fn", + "make-spec-files": "page-functions/make-spec-files", + "marshes": "page-functions/marshes", + "marshes-examples-nav-items": "nav-geography/marshes-examples-nav-items", + "marshes-index": "pages/docs/marshes-index", + "modules": "page-functions/modules", + "native-browser": "page-functions/native-browser", + "native-browser-nav-items": "nav-data/native-browser-nav-items", + "nav-data/section-nav": "nav-tree/section-nav", + "offline-demo": "pages/docs/offline-demo", + "optimistic-demo": "pages/docs/optimistic-demo", + "page-helpers-demo": "pages/docs/page-helpers-demo", + "page-test-specs": "pages/page-tests/page-test-specs", + "philosophy": "page-functions/philosophy", + "philosophy-index": "pages/docs/philosophy-index", + "philosophy-nav-items": "nav-data/philosophy-nav-items", + "philosophy-page": "pages/docs/philosophy-page", + "plan": "page-functions/plan", + "plan-page": "pages/docs/plan-page", + "plans-index": "pages/docs/plans-index", + "plans-nav-items": "nav-data/plans-nav-items", + "playground": "page-functions/playground", + "pretext": "page-functions/pretext", + "pretext-demo": "pages/docs/pretext-demo", + "pretext-nav-items": "nav-data/pretext-nav-items", + "primitives-by-category": "data/reference/primitives-by-category", + "primitives-data": "data/helpers/primitives-data", + "protocol": "page-functions/protocol", + "protocol-page": "pages/docs/protocol-page", + "protocols-index": "pages/docs/protocols-index", + "protocols-nav-items": "nav-data/protocols-nav-items", + "provide": "page-functions/provide", + "provide-index": "pages/docs/provide-index", + "pub-actor": "handlers/pub-api/pub-actor", + "pub-anchor": "handlers/pub-api/pub-anchor", + "pub-browse-collection": "handlers/pub-api/pub-browse-collection", + "pub-cid": "handlers/pub-api/pub-cid", + "pub-collections": "handlers/pub-api/pub-collections", + "pub-document": "handlers/pub-api/pub-document", + "pub-follow": "handlers/pub-api/pub-follow", + "pub-followers": "handlers/pub-api/pub-followers", + "pub-following": "handlers/pub-api/pub-following", + "pub-inbox": "handlers/pub-api/pub-inbox", + "pub-outbox": "handlers/pub-api/pub-outbox", + "pub-publish": "handlers/pub-api/pub-publish", + "pub-status": "handlers/pub-api/pub-status", + "pub-verify": "handlers/pub-api/pub-verify", + "pub-webfinger": "handlers/pub-api/pub-webfinger", + "reactive": "page-functions/reactive", + "reactive-api/search-results": "handlers/reactive-api/search-results", + "reactive-catalog": "handlers/reactive-api/reactive-catalog", + "reactive-examples-nav-items": "nav-geography/reactive-examples-nav-items", + "reactive-flash-sale": "handlers/reactive-api/reactive-flash-sale", + "reactive-islands-index": "pages/docs/reactive-islands-index", + "reactive-islands-nav-items": "nav-geography/reactive-islands-nav-items", + "reactive-islands-page": "pages/docs/reactive-islands-page", + "reactive-islands/demo/example-cyst": "reactive-islands/demo-cyst/example-cyst", + "reactive-islands/demo/example-reactive-expressions": "reactive-islands/demo-reactive-expressions/example-reactive-expressions", + "reactive-islands/index/demo-cyst": "reactive-islands/demo-cyst/demo-cyst", + "reactive-islands/index/demo-reactive-expressions": "reactive-islands/demo-reactive-expressions/demo-reactive-expressions", + "reactive-islands/test-runner": "reactive-islands/runner-placeholder/test-runner", + "reactive-islands/test-runner-placeholder": "reactive-islands/runner-placeholder/test-runner-placeholder", + "reactive-runtime": "page-functions/reactive-runtime", + "reactive-runtime-nav-items": "nav-data/reactive-runtime-nav-items", + "reactive-search-events": "handlers/reactive-api/reactive-search-events", + "reactive-search-posts": "handlers/reactive-api/reactive-search-posts", + "reactive-search-products": "handlers/reactive-api/reactive-search-products", + "reactive-settle-data": "handlers/reactive-api/reactive-settle-data", + "reactive-spec-items": "nav-language/reactive-spec-items", + "read-spec-file": "data/helpers/read-spec-file", + "ref-delete": "handlers/reference/ref-delete", + "ref-delete-item": "handlers/ref-api/ref-delete-item", + "ref-echo-headers": "handlers/reference/ref-echo-headers", + "ref-echo-vals": "handlers/reference/ref-echo-vals", + "ref-echo-vals-get": "handlers/ref-api/ref-echo-vals-get", + "ref-echo-vals-post": "handlers/ref-api/ref-echo-vals-post", + "ref-error-500": "handlers/ref-api/ref-error-500", + "ref-flaky": "handlers/reference/ref-flaky", + "ref-greet": "handlers/reference/ref-greet", + "ref-oob": "handlers/reference/ref-oob", + "ref-prompt-echo": "handlers/ref-api/ref-prompt-echo", + "ref-retarget": "handlers/ref-api/ref-retarget", + "ref-select-page": "handlers/reference/ref-select-page", + "ref-slow-echo": "handlers/reference/ref-slow-echo", + "ref-status": "handlers/reference/ref-status", + "ref-swap-item": "handlers/reference/ref-swap-item", + "ref-theme": "handlers/reference/ref-theme", + "ref-time": "handlers/reference/ref-time", + "ref-trigger-event": "handlers/ref-api/ref-trigger-event", + "ref-trigger-search": "handlers/reference/ref-trigger-search", + "ref-upload-name": "handlers/reference/ref-upload-name", + "reference": "page-functions/reference", + "reference-attr-detail": "pages/docs/reference-attr-detail", + "reference-data": "data/helpers/reference-data", + "reference-detail": "page-functions/reference-detail", + "reference-event-detail": "pages/docs/reference-event-detail", + "reference-header-detail": "pages/docs/reference-header-detail", + "reference-index": "pages/docs/reference-index", + "reference-nav-items": "nav-geography/reference-nav-items", + "reference-page": "pages/docs/reference-page", + "request-attrs": "data/reference/request-attrs", + "request-headers": "data/reference/request-headers", + "resolve-nav-path": "nav-tree/resolve-nav-path", + "response-headers": "data/reference/response-headers", + "routing-analyzer": "pages/docs/routing-analyzer", + "routing-analyzer-data": "data/helpers/routing-analyzer-data", + "scopes": "page-functions/scopes", + "scopes-index": "pages/docs/scopes-index", + "search-event-items": "handlers/reactive-api/search-event-items", + "search-post-items": "handlers/reactive-api/search-post-items", + "search-product-items": "handlers/reactive-api/search-product-items", + "semantics-nav-items": "nav-geography/semantics-nav-items", + "services": "page-functions/services", + "settle-items": "handlers/reactive-api/settle-items", + "slug->component": "page-functions/slug->component", + "spec": "page-functions/spec", + "spec-compute-stats": "spec-introspect/spec-compute-stats", + "spec-explore": "spec-introspect/spec-explore", + "spec-explore-define": "spec-introspect/spec-explore-define", + "spec-explorer-data": "data/helpers/spec-explorer-data", + "spec-explorer-data-by-slug": "data/helpers/spec-explorer-data-by-slug", + "spec-form-effects": "spec-introspect/spec-form-effects", + "spec-form-kind": "spec-introspect/spec-form-kind", + "spec-form-name": "spec-introspect/spec-form-name", + "spec-form-params": "spec-introspect/spec-form-params", + "spec-form-signature": "spec-introspect/spec-form-signature", + "spec-group-sections": "spec-introspect/spec-group-sections", + "special-forms-data": "data/helpers/special-forms-data", + "specs-explore-page": "pages/docs/specs-explore-page", + "specs-index": "pages/docs/specs-index", + "specs-nav-items": "nav-language/specs-nav-items", + "specs-page": "pages/docs/specs-page", + "split-tag": "stepper-lib/split-tag", + "spreads": "page-functions/spreads", + "spreads-index": "pages/docs/spreads-index", + "steps-to-preview": "stepper-lib/steps-to-preview", + "stream-colors": "streaming-demo/stream-colors", + "streaming-demo": "pages/docs/streaming-demo", + "streaming-demo-data": "streaming-demo/data", + "sx-nav-tree": "nav-tree/sx-nav-tree", + "sx-pub": "page-functions/sx-pub", + "sx-tools": "page-functions/sx-tools", + "sx-unique-attrs": "data/reference/sx-unique-attrs", + "sx-urls": "page-functions/sx-urls", + "sxtp": "page-functions/sxtp", + "sxtp-nav-items": "nav-data/sxtp-nav-items", + "test": "page-functions/test", + "testing-index": "pages/docs/testing-index", + "testing-nav-items": "nav-language/testing-nav-items", + "testing-page": "pages/docs/testing-page", + "tools": "page-functions/tools" +} \ No newline at end of file diff --git a/scripts/strip_names.py b/scripts/strip_names.py new file mode 100644 index 00000000..f96de7dd --- /dev/null +++ b/scripts/strip_names.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Phase 2: Strip names from definition forms in one-per-file .sx files. + +Transforms: + (defcomp ~name (params) body) -> (defcomp (params) body) + (define name value) -> (define value) + (define (name args) body) -> (define (args) body) + (defpage name :path ...) -> (defpage :path ...) + etc. + +Self-named components (where the name is part of the content) are left as-is +if they use a different naming convention than the file path would give. + +Usage: + python3 scripts/strip_names.py --dry-run # preview + python3 scripts/strip_names.py # execute +""" +import os +import sys +import re +import argparse + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from shared.sx.parser import parse_all +from shared.sx.types import Symbol, Keyword + +NAMED_DEFS = {"defcomp", "defisland", "defmacro", "defpage", + "defhandler", "defstyle", "deftype", "defeffect", + "defrelation", "deftest"} + +SKIP_FILES = {"boundary.sx", "_init.sx", "_preamble.sx"} + + +def strip_name_from_source(source): + """Strip the name from a definition in raw source text. + + Returns (new_source, old_name, def_type) or None if no change needed. + + Works on raw text to preserve formatting, comments, etc. + Uses the parser to understand structure, then does targeted text surgery. + """ + try: + exprs = parse_all(source) + except Exception: + return None + + if not exprs: + return None + + expr = exprs[0] + if not isinstance(expr, list) or not expr or not isinstance(expr[0], Symbol): + return None + + kw = expr[0].name + + if kw in NAMED_DEFS: + if len(expr) < 2 or not isinstance(expr[1], Symbol): + return None # Already unnamed or malformed + old_name = expr[1].name + + # Find and remove the name symbol from source text. + # Pattern: (defXXX ~name or (defXXX name + # We need to find the name token after the keyword and remove it. + pattern = re.compile( + r'(\(\s*' + re.escape(kw) + r')\s+' + re.escape(old_name) + r'(?=\s)', + re.DOTALL + ) + match = pattern.search(source) + if match: + new_source = source[:match.end(1)] + source[match.end():] + return (new_source, old_name, kw) + return None + + if kw == "define": + if len(expr) < 2: + return None + name_part = expr[1] + + if isinstance(name_part, Symbol): + old_name = name_part.name + # (define name value) -> (define value) + pattern = re.compile( + r'(\(\s*define)\s+' + re.escape(old_name) + r'(?=\s)', + re.DOTALL + ) + match = pattern.search(source) + if match: + new_source = source[:match.end(1)] + source[match.end():] + return (new_source, old_name, "define") + + elif (isinstance(name_part, list) and name_part + and isinstance(name_part[0], Symbol)): + old_name = name_part[0].name + # (define (name args...) body) -> (define (args...) body) + # Find (define (name and remove just "name " + pattern = re.compile( + r'(\(\s*define\s+\()\s*' + re.escape(old_name) + r'\s*', + re.DOTALL + ) + match = pattern.search(source) + if match: + new_source = source[:match.end(1)] + source[match.end():] + return (new_source, old_name, "define") + + return None + + return None + + +def main(): + parser = argparse.ArgumentParser(description="Strip names from definitions") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--dir", default=None) + args = parser.parse_args() + + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + os.chdir(project_root) + + dirs = [args.dir] if args.dir else ["sx/sx", "sx/sxc"] + + stripped = 0 + skipped = 0 + errors = [] + + for d in dirs: + if not os.path.isdir(d): + continue + + for root, subdirs, files in os.walk(d): + subdirs[:] = [x for x in subdirs if x not in ('__pycache__', '.cache')] + for filename in sorted(files): + if not filename.endswith('.sx') or filename in SKIP_FILES: + continue + + filepath = os.path.join(root, filename) + + try: + with open(filepath, encoding='utf-8') as f: + source = f.read() + except Exception as e: + errors.append((filepath, str(e))) + continue + + result = strip_name_from_source(source) + if result is None: + skipped += 1 + continue + + new_source, old_name, def_type = result + + if args.dry_run: + print(f" {filepath}: strip {def_type} {old_name}") + stripped += 1 + continue + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_source) + stripped += 1 + + print(f"\n{'Dry run — would strip' if args.dry_run else 'Stripped'} {stripped} names") + print(f"Skipped {skipped} (already unnamed or no definition)") + if errors: + print(f"Errors: {len(errors)}") + for fp, e in errors: + print(f" {fp}: {e}") + + +if __name__ == "__main__": + main()