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) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 09:09:15 +00:00
parent bfe4727edf
commit 6528ce78b9
4 changed files with 1261 additions and 0 deletions

389
scripts/migrate-pages.sh Executable file
View File

@@ -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 "======================================="

View File

@@ -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()

256
scripts/name-mapping.json Normal file
View File

@@ -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"
}

170
scripts/strip_names.py Normal file
View File

@@ -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()