From 41f8d3025f041aaab98fcc3511cb6ec2198f76a7 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 14 Jun 2026 01:49:48 +0300 Subject: init: vibed --- spec/scripts/gen_abnf.py | 265 ++++++++++++ spec/scripts/gen_ebnf.py | 215 ++++++++++ spec/scripts/gen_spec.py | 1053 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1533 insertions(+) create mode 100644 spec/scripts/gen_abnf.py create mode 100644 spec/scripts/gen_ebnf.py create mode 100644 spec/scripts/gen_spec.py (limited to 'spec/scripts') diff --git a/spec/scripts/gen_abnf.py b/spec/scripts/gen_abnf.py new file mode 100644 index 0000000..9569195 --- /dev/null +++ b/spec/scripts/gen_abnf.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +"""Generate UGI_GRAMMAR_v0.abnf from ugi_registry_v0.json.""" + +import json +import os +import sys + + +def load_registry(path): + with open(path, "r") as f: + return json.load(f) + + +def hex_char(c): + """Return ABNF hex notation for a character.""" + return f"%x{ord(c):02X}" + + +def hex_range(start, end): + """Return ABNF hex range.""" + return f"%x{ord(start):02X}-{ord(end):02X}" + + +def collect_sub_ids(f): + """Collect all sub-ID codes from a field.""" + ids = [] + if "sub_ids" in f: + ids.extend(f["sub_ids"].keys()) + if "sub_id_groups" in f: + for group in f["sub_id_groups"].values(): + if "sub_ids" in group: + ids.extend(group["sub_ids"].keys()) + return ids + + +def gen_sub_id_rule(name, sub_ids): + """Generate ABNF rule matching any of the given sub-ID strings.""" + # Quote each as case-insensitive string + parts = [] + for sid in sorted(set(sub_ids)): + # ABNF quoted strings are case-insensitive by default + parts.append(f'"{sid}"') + return f"{name} = " + " / ".join(parts) + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + registry_path, output_path = sys.argv[1], sys.argv[2] + reg = load_registry(registry_path) + fields = reg["fields"] + + lines = [] + lines.append("; UGI Grammar v0 — RFC 5234 ABNF") + lines.append("; Generated from ugi_registry_v0.json") + lines.append(";") + lines.append("; Compatible with abnf.dev/abnf2svg") + lines.append("") + + # ── Top-level ── + lines.append("; === Top-level ===") + lines.append("") + lines.append('ugi-string = "ugi:" version "@" handle ":" field-list') + lines.append("") + lines.append('version = 1*DIGIT ["/" 1*DIGIT]') + lines.append("") + lines.append("; handle: alphanumeric, hyphens, underscores, dots") + lines.append("handle = 1*(ALPHA / DIGIT / %x2D / %x5F / %x2E)") + lines.append("") + lines.append('field-list = field *("," field)') + lines.append("") + + # ── Field dispatch ── + lines.append("; === Field dispatch ===") + lines.append("") + + # Build field alternatives + field_rules = [] + for code in sorted(fields.keys()): + field_rules.append(f"field-{code}") + + lines.append("field = " + " / ".join(field_rules)) + lines.append("") + + # ── Common rules ── + lines.append("; === Common rules ===") + lines.append("") + lines.append("ODIGIT = %x30-37") + lines.append("") + lines.append('paid = "$"') + lines.append("") + lines.append('aspire = "+" ODIGIT') + lines.append("") + lines.append("paid-aspire = paid [aspire]") + lines.append("") + lines.append("modifier = paid-aspire / aspire") + lines.append("") + lines.append('alternative = "/" ODIGIT') + lines.append("") + lines.append('; custom sub-ID: ~ followed by 2+ alpha chars') + lines.append('custom-sub = "~" 2*ALPHA') + lines.append("") + lines.append("; sub-ID: 1-4 alpha chars") + lines.append("sub-id = 1*4ALPHA") + lines.append("") + + # ── Per-field rules ── + lines.append("; === Per-field rules ===") + lines.append("") + + for code in sorted(fields.keys()): + f = fields[code] + ftype = f["type"] + mods = f.get("modifiers", {}) + has_paid = mods.get("paid", False) + has_aspire = mods.get("aspire", False) + has_alt = mods.get("alternative", False) + + hex_code = hex_char(code) + lines.append(f"; {code} — {f['name']} ({ftype})") + + if ftype == "direct": + # Build modifier part + mod_parts = [] + if has_alt: + mod_parts.append("alternative") + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " [" + " / ".join(mod_parts) + "]" + else: + mod_str = "" + + lines.append(f"field-{code} = {hex_code} ODIGIT{mod_str}") + + elif ftype == "multi": + # Entry: sub-id + digit + optional modifier + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " [" + " / ".join(mod_parts) + "]" + else: + mod_str = "" + + entry_rule = f"field-{code}-entry" + if code == "l": + # l uses 2-char ISO 639-1 codes instead of generic sub-ids + lines.append( + f"{entry_rule} = 2ALPHA ODIGIT{mod_str}" + ) + else: + lines.append( + f"{entry_rule} = (sub-id / custom-sub) ODIGIT{mod_str}" + ) + lines.append( + f"field-{code} = {hex_code} 1*{entry_rule}" + ) + + elif ftype == "single": + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " [" + " / ".join(mod_parts) + "]" + else: + mod_str = "" + + lines.append( + f"field-{code} = {hex_code} (sub-id / custom-sub) ODIGIT{mod_str}" + ) + + elif ftype == "special": + if code == "g": + # g: domains separated by /, $ after domain, ~ for custom, no digits + lines.append(f'g-domain = 2*4ALPHA ["$"]') + lines.append(f'g-custom = "~" 2*ALPHA') + lines.append( + f'field-g = {hex_code} (g-domain / g-custom) *("/" (g-domain / g-custom))' + ) + elif code == "b": + # b: digit/digit, optional aspire on each + aspire_str = " [aspire]" if has_aspire else "" + lines.append( + f'field-{code} = {hex_code} ODIGIT{aspire_str} "/" ODIGIT{aspire_str}' + ) + elif code == "v": + lines.append( + f'field-{code} = {hex_code} ODIGIT "/" ODIGIT' + ) + else: + # Fallback + lines.append(f"field-{code} = {hex_code} 1*(ALPHA / DIGIT / %x24 / %x2B / %x2F / %x7E)") + + lines.append("") + + # ── Block format ── + lines.append("; === Block format ===") + lines.append("") + lines.append( + 'ugi-block = block-header CRLF block-first-line CRLF ' + '1*(block-field-line CRLF) block-footer' + ) + lines.append("") + lines.append('block-header = "------- BEGIN UGI BLOCK -------"') + lines.append('block-footer = "-------- END UGI BLOCK --------"') + lines.append("") + lines.append( + 'block-first-line = "v:" version SP "@" handle [SP block-geek-field]' + ) + lines.append("") + lines.append('; In block format, G field uses uppercase and appears on first line') + lines.append( + 'block-geek-field = "G" (g-domain / g-custom) *("/" (g-domain / g-custom))' + ) + lines.append("") + lines.append( + 'block-field-line = block-field *(SP block-field)' + ) + lines.append("") + lines.append( + "; Block fields use uppercase code prefix repeated per sub-ID" + ) + lines.append( + 'block-field = ALPHA 1*(ALPHA / DIGIT / %x24 / %x2B / %x2F / %x7E)' + ) + lines.append("") + + # ── Core rules ── + lines.append("; === Core rules (RFC 5234) ===") + lines.append("") + lines.append("ALPHA = %x41-5A / %x61-7A") + lines.append("DIGIT = %x30-39") + lines.append("SP = %x20") + lines.append("CRLF = %x0D.0A / %x0A") + lines.append("") + + output = "\n".join(lines) + + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + with open(output_path, "w") as f: + f.write(output) + + print(f"Generated {output_path}") + + +if __name__ == "__main__": + main() diff --git a/spec/scripts/gen_ebnf.py b/spec/scripts/gen_ebnf.py new file mode 100644 index 0000000..65824bd --- /dev/null +++ b/spec/scripts/gen_ebnf.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Generate UGI_GRAMMAR_v0.ebnf from ugi_registry_v0.json.""" + +import json +import os +import sys + + +def load_registry(path): + with open(path, "r") as f: + return json.load(f) + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + registry_path, output_path = sys.argv[1], sys.argv[2] + reg = load_registry(registry_path) + fields = reg["fields"] + + lines = [] + lines.append("/* UGI Grammar v0 — W3C EBNF */") + lines.append("/* Generated from ugi_registry_v0.json */") + lines.append("/* Compatible with bottlecaps.de/rr/ui */") + lines.append("") + + # ── Top-level ── + lines.append("/* === Top-level === */") + lines.append("") + lines.append("ugi_string ::= 'ugi:' version '@' handle ':' field_list") + lines.append("") + lines.append("version ::= digit+ ( '/' digit+ )?") + lines.append("") + lines.append("/* handle: alphanumeric, hyphens, underscores, dots */") + lines.append("handle ::= ( alpha | digit | '-' | '_' | '.' )+") + lines.append("") + lines.append("field_list ::= field ( ',' field )*") + lines.append("") + + # ── Field dispatch ── + lines.append("/* === Field dispatch === */") + lines.append("") + + field_alts = [f"field_{code}" for code in sorted(fields.keys())] + # Break into multiple lines for readability + lines.append("field ::= " + "\n | ".join(field_alts)) + lines.append("") + + # ── Common rules ── + lines.append("/* === Common rules === */") + lines.append("") + lines.append("odigit ::= [0-7]") + lines.append("") + lines.append("paid ::= '$'") + lines.append("") + lines.append("aspire ::= '+' odigit") + lines.append("") + lines.append("paid_aspire ::= paid aspire?") + lines.append("") + lines.append("modifier ::= paid_aspire | aspire") + lines.append("") + lines.append("alternative ::= '/' odigit") + lines.append("") + lines.append("/* custom sub-ID: ~ followed by 2+ alpha chars */") + lines.append("custom_sub ::= '~' alpha alpha alpha*") + lines.append("") + lines.append("/* sub-ID: 1-4 alpha chars */") + lines.append("sub_id ::= alpha alpha? alpha? alpha?") + lines.append("") + + # ── Per-field rules ── + lines.append("/* === Per-field rules === */") + lines.append("") + + for code in sorted(fields.keys()): + f = fields[code] + ftype = f["type"] + mods = f.get("modifiers", {}) + has_paid = mods.get("paid", False) + has_aspire = mods.get("aspire", False) + has_alt = mods.get("alternative", False) + + lines.append(f"/* {code} — {f['name']} ({ftype}) */") + + if ftype == "direct": + mod_parts = [] + if has_alt: + mod_parts.append("alternative") + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " ( " + " | ".join(mod_parts) + " )?" + else: + mod_str = "" + + lines.append(f"field_{code} ::= '{code}' odigit{mod_str}") + + elif ftype == "multi": + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " ( " + " | ".join(mod_parts) + " )?" + else: + mod_str = "" + + if code == "l": + # l uses 2-char ISO 639-1 codes instead of generic sub-ids + lines.append( + f"field_{code}_entry ::= alpha alpha odigit{mod_str}" + ) + else: + lines.append( + f"field_{code}_entry ::= ( sub_id | custom_sub ) odigit{mod_str}" + ) + lines.append( + f"field_{code} ::= '{code}' field_{code}_entry+" + ) + + elif ftype == "single": + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " ( " + " | ".join(mod_parts) + " )?" + else: + mod_str = "" + + lines.append( + f"field_{code} ::= '{code}' ( sub_id | custom_sub ) odigit{mod_str}" + ) + + elif ftype == "special": + if code == "g": + lines.append("g_domain ::= alpha alpha alpha? alpha? paid?") + lines.append("g_custom ::= '~' alpha alpha alpha*") + lines.append( + "field_g ::= 'g' ( g_domain | g_custom ) ( '/' ( g_domain | g_custom ) )*" + ) + elif code == "b": + aspire_str = " aspire?" if has_aspire else "" + lines.append( + f"field_b ::= 'b' odigit{aspire_str} '/' odigit{aspire_str}" + ) + elif code == "v": + lines.append("field_v ::= 'v' odigit '/' odigit") + else: + lines.append( + f"field_{code} ::= '{code}' ( alpha | digit | '$' | '+' | '/' | '~' )+" + ) + + lines.append("") + + # ── Block format ── + lines.append("/* === Block format === */") + lines.append("") + lines.append( + "ugi_block ::= block_header newline block_first_line newline " + "block_field_line+ block_footer" + ) + lines.append("") + lines.append("block_header ::= '------- BEGIN UGI BLOCK -------'") + lines.append("block_footer ::= '-------- END UGI BLOCK --------'") + lines.append("") + lines.append( + "block_first_line ::= 'v:' version ' @' handle ( ' ' block_geek_field )?" + ) + lines.append("") + lines.append( + "block_geek_field ::= 'G' ( g_domain | g_custom ) ( '/' ( g_domain | g_custom ) )*" + ) + lines.append("") + lines.append("block_field_line ::= block_field ( ' ' block_field )* newline") + lines.append("") + lines.append( + "block_field ::= alpha ( alpha | digit | '$' | '+' | '/' | '~' )+" + ) + lines.append("") + + # ── Terminals ── + lines.append("/* === Terminals === */") + lines.append("") + lines.append("alpha ::= [a-zA-Z]") + lines.append("digit ::= [0-9]") + lines.append("newline ::= #xD #xA | #xA") + lines.append("") + + output = "\n".join(lines) + + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + with open(output_path, "w") as f: + f.write(output) + + print(f"Generated {output_path}") + + +if __name__ == "__main__": + main() diff --git a/spec/scripts/gen_spec.py b/spec/scripts/gen_spec.py new file mode 100644 index 0000000..4ab8a20 --- /dev/null +++ b/spec/scripts/gen_spec.py @@ -0,0 +1,1053 @@ +#!/usr/bin/env python3 +"""Generate UGI_SPEC_v0.md from ugi_registry_v0.json.""" + +import json +import os +import sys + + +def load_registry(path): + with open(path, "r") as f: + return json.load(f) + + +def gen_title(spec): + lines = [] + lines.append(f"# {spec['name']} — {spec['full_name']}") + lines.append("") + lines.append( + f"**Version: {spec['version']} ({spec['status'].title()}) · " + f"{spec['date']} · {spec['license']} License**" + ) + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_overview(spec): + lines = [] + lines.append("## Overview") + lines.append("") + lines.append(spec["description"]) + lines.append("") + lines.append("It descends from two predecessors:") + lines.append("") + for p in spec["predecessors"]: + line = f"- [{p['name']}]({p['url']}) by {p['author']} ({p['years']})" + if "continuation" in p: + c = p["continuation"] + line += f", with its [{c['name']}]({c['url']}) by {c['author']} ({c['years']})" + lines.append(line) + lines.append("") + lines.append( + "UGI takes the cultural breadth of the Geek Code, the numeric compactness " + "of the Hacker Key, and adds URI safety, modern fields, and extensibility." + ) + lines.append("") + lines.append("### Design goals") + lines.append("") + lines.append("1. Single-letter field codes — 24 active, 2 reserved") + lines.append("2. Octal (0–7) rating scale") + lines.append("3. URI-safe characters only — no percent-encoding needed") + lines.append("4. Dual format — URI for machines, block for humans") + lines.append("5. Case-insensitive") + lines.append("6. Extensible via `~` custom sub-IDs") + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_formats(fmt): + lines = [] + lines.append("## Formats") + lines.append("") + lines.append("### URI") + lines.append("") + lines.append("```") + lines.append(fmt["uri"]["template"]) + lines.append("```") + lines.append("") + lines.append("### Block") + lines.append("") + lines.append("```") + lines.append(fmt["block"]["header"]) + lines.append(fmt["block"]["first_line"]) + lines.append(" ...") + lines.append(fmt["block"]["footer"]) + lines.append("```") + lines.append("") + lines.append( + "In block format, field codes are repeated per sub-ID for readability: " + "`Ppy6 Prs5` instead of `ppy6rs5`. Long-form sub-ID aliases (noted in " + "registries) are permitted in block format." + ) + lines.append("") + lines.append("### Conversion") + lines.append("") + lines.append( + "Block → URI: remove header/footer, merge repeated field codes, " + "replace spaces with commas, prepend `ugi:`." + ) + lines.append("") + lines.append( + "URI → Block: split on commas, expand merged sub-IDs by repeating " + "field codes, add header/footer." + ) + lines.append("") + lines.append( + "No modifier or character changes are needed — both formats use identical symbols." + ) + lines.append("") + lines.append("### Mandatory fields") + lines.append("") + mand = ", ".join(f"`{f}`" for f in fmt["mandatory_fields"]) + lines.append( + f"`@handle` and {mand} (geek type) are required. All others are optional." + ) + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_grammar(fields): + lines = [] + lines.append("## Grammar") + lines.append("") + lines.append("```") + lines.append("[]...") + lines.append("```") + lines.append("") + lines.append("- **Field codes** — single letter (a–z), case-insensitive") + lines.append("- **Sub-IDs** — 1–4 lowercase alpha chars") + lines.append("- **Ratings** — single octal digit (0–7), required on every sub-ID") + lines.append("- **Custom sub-IDs** — `~` prefix: `~fermenting5`") + lines.append("- **Modifiers** — after the digit:") + lines.append(" - `$` — paid") + lines.append(" - `+` followed by digit — aspiring toward that level") + lines.append(" - `$+` combined — paid and aspiring") + lines.append( + '- **Alternatives** — `/` between two digits on direct-value fields: `c3/6`' + ) + lines.append("") + lines.append("### Special formats") + lines.append("") + lines.append("| Field | Format | Example |") + lines.append("|---|---|---|") + lines.append( + "| `g` geek type | `/` separates domains, `~` for custom, `$` for paid, no ratings | `gcs$/ai/wr~fermenting` |" + ) + lines.append("| `b` build | `/` | `b6/3` |") + lines.append("| `l` languages | ISO 639-1 code + rating | `lar7en6` |") + lines.append("| `v` politics | `/` | `v5/3` |") + lines.append("") + lines.append("### Parser algorithm") + lines.append("") + lines.append( + "After each comma or space, read a single letter (field code). Then:" + ) + lines.append("") + lines.append( + "- `g` → read alpha tokens separated by `/` and `~`-prefixed tokens until delimiter" + ) + lines.append("- `b` → digit, `/`, digit") + lines.append("- `v` → digit, `/`, digit") + lines.append("- `l` → repeat: 2 alpha + digit + optional modifier") + lines.append("- `r` → one sub-id + digit (single value only)") + lines.append("- direct fields → digit + optional modifier") + lines.append("- direct-alt fields → digit + optional `/` + digit + optional modifier") + lines.append("- multi fields → repeat: alpha (sub-id) + digit + optional modifier") + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_scale(scale): + lines = [] + lines.append("## Rating scale (0–7)") + lines.append("") + lines.append("| Value | General | Proficiency | Enthusiasm | Stance |") + lines.append("|---|---|---|---|---|") + for v in scale["values"]: + lines.append( + f"| {v['value']} | {v['general']} | {v['proficiency']} | " + f"{v['enthusiasm']} | {v['stance']} |" + ) + lines.append("") + return "\n".join(lines) + + +def gen_modifier_permissions(fields, modifiers): + lines = [] + lines.append("### Modifier permissions") + lines.append("") + lines.append("| Modifiers | Fields |") + lines.append("|---|---|") + + both = [] + paid_only = [] + aspire_only = [] + neither = [] + + for code in sorted(fields.keys()): + f = fields[code] + mods = f.get("modifiers", {}) + has_paid = mods.get("paid", False) + has_aspire = mods.get("aspire", False) + if has_paid and has_aspire: + both.append(code) + elif has_paid: + paid_only.append(code) + elif has_aspire: + aspire_only.append(code) + else: + neither.append(code) + + if both: + lines.append( + f"| `$` and `+` | {' '.join(f'`{c}`' for c in both)} |" + ) + if paid_only: + lines.append( + f"| `$` only | {' '.join(f'`{c}`' for c in paid_only)} |" + ) + if aspire_only: + lines.append( + f"| `+` only | {' '.join(f'`{c}`' for c in aspire_only)} |" + ) + if neither: + lines.append( + f"| Neither | {' '.join(f'`{c}`' for c in neither)} |" + ) + + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_field_list(fields, fmt): + lines = [] + lines.append("## Field list") + lines.append("") + lines.append("| Code | Field | Type | Category |") + lines.append("|---|---|---|---|") + + reserved = fmt.get("reserved_codes", []) + all_codes = sorted(set(list(fields.keys()) + reserved)) + + for code in all_codes: + if code in reserved: + lines.append(f"| `{code}` | *(reserved)* | — | — |") + continue + f = fields[code] + ftype = f["type"] + mods = f.get("modifiers", {}) + has_alt = mods.get("alternative", False) + + type_str = ftype.capitalize() + if ftype == "direct" and has_alt: + type_str = "Direct, `/` ok" + elif ftype == "special": + if code == "b": + type_str = "Special (h/w)" + elif code == "v": + type_str = "Special (s/e)" + elif code == "g": + type_str = "Special" + elif code == "l": + type_str = "Multi (ISO)" + elif ftype == "multi": + if code == "h": + type_str = "Multi (1-letter)" + elif code == "l": + type_str = "Multi (ISO)" + elif ftype == "single": + type_str = "Single" + + lines.append( + f"| `{code}` | {f['name']} | {type_str} | {f['category'].title()} |" + ) + + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_field_def_direct(code, f): + """Generate a direct-type field definition.""" + lines = [] + vals = f.get("values", {}) + if vals: + for i in range(8): + v = vals.get(str(i), {}) + label = v.get("label", "—") + humor = v.get("humor") + if humor: + lines.append(f"{i}. **{label}** — {humor}") + else: + lines.append(f"{i}. **{label}**") + lines.append("") + return "\n".join(lines) + + +def gen_field_def_special_b(f): + """Generate build field definition.""" + lines = [] + lines.append( + "Format: `b/`. Each 0–7. " + "0 = extremely small, 4 = average, 7 = extremely large. " + "`+` modifier permitted." + ) + lines.append("") + for dim_name, dim in f["dimensions"].items(): + lines.append(f"**{dim_name.title()}:** ", ) + vals = dim["values"] + parts = [f"{k} = {v}" for k, v in sorted(vals.items(), key=lambda x: int(x[0]))] + lines[-1] += " · ".join(parts) + lines.append("") + lines.append("> `b6/3` — tall and slim.") + lines.append("> `b4/4` — average. Chairs were designed for you.") + lines.append("") + return "\n".join(lines) + + +def gen_field_def_special_v(f): + """Generate politics field definition.""" + lines = [] + lines.append("Format: `v/`. No modifiers.") + lines.append("") + for dim_name, dim in f["dimensions"].items(): + vals = dim["values"] + low = vals.get("0", "") + mid = vals.get("4", "") + high = vals.get("7", "") + lines.append(f"**{dim_name.title()}:** 0 = {low} · 4 = {mid} · 7 = {high}") + lines.append("") + lines.append("> `v5/3` — socially center-left, economically center-right.") + lines.append("> `v4/4` — centrist. You annoy both sides equally.") + lines.append("") + return "\n".join(lines) + + +def gen_field_def_special_g(f): + """Generate geek type field definition.""" + lines = [] + lines.append( + "Mandatory. `/` separates domains. `~` prefixes custom domains. " + "`$` after a domain = paid. No ratings." + ) + lines.append("") + + groups = f.get("sub_id_groups", {}) + for group_key, group in groups.items(): + label = group.get("label", group_key) + sub_ids = group.get("sub_ids", {}) + + # For larger groups, use a table + items = list(sub_ids.items()) + if len(items) > 7: + lines.append(f"#### {label}") + lines.append("") + lines.append("| Code | Domain | Code | Domain |") + lines.append("|---|---|---|---|") + # Pair them up + for i in range(0, len(items), 2): + c1, n1 = items[i] + if isinstance(n1, dict): + n1 = n1.get("name", n1.get("long", c1)) + row = f"| `{c1}` | {n1}" + if i + 1 < len(items): + c2, n2 = items[i + 1] + if isinstance(n2, dict): + n2 = n2.get("name", n2.get("long", c2)) + row += f" | `{c2}` | {n2} |" + else: + row += " | | |" + lines.append(row) + lines.append("") + else: + # Inline format for smaller groups + lines.append(f"#### {label}") + lines.append("") + parts = [] + for c, n in items: + if isinstance(n, dict): + n = n.get("name", n.get("long", c)) + parts.append(f"`{c}` {n}") + lines.append(" · ".join(parts)) + lines.append("") + + lines.append("#### Custom") + lines.append("") + lines.append( + "`~fermenting` `~beekeeping` `~lockpicking` `~origami` — anything goes." + ) + lines.append("") + lines.append("> `gcs$/ai/wr~fermenting` — paid CS geek, also into AI, writing, and fermenting.") + lines.append("") + return "\n".join(lines) + + +def gen_sub_id_table(sub_ids): + """Generate a sub-ID table from a flat sub_ids dict.""" + lines = [] + items = list(sub_ids.items()) + + # Check if it's a two-column table or three-column + has_long = any( + isinstance(v, dict) and "long" in v for v in sub_ids.values() + ) + + if has_long: + # Pair them for a wider table + lines.append("| Short | (Long) | Name | Short | (Long) | Name |") + lines.append("|---|---|---|---|---|---|") + for i in range(0, len(items), 2): + c1, v1 = items[i] + if isinstance(v1, dict): + long1 = f"`({v1['long']})`" if "long" in v1 else "—" + name1 = v1.get("name", c1) + else: + long1 = "—" + name1 = v1 + row = f"| `{c1}` | {long1} | {name1}" + + if i + 1 < len(items): + c2, v2 = items[i + 1] + if isinstance(v2, dict): + long2 = f"`({v2['long']})`" if "long" in v2 else "—" + name2 = v2.get("name", c2) + else: + long2 = "—" + name2 = v2 + row += f" | `{c2}` | {long2} | {name2} |" + else: + row += " | | | |" + lines.append(row) + else: + lines.append("| Short | Name |") + lines.append("|---|---|") + for c, v in items: + if isinstance(v, dict): + name = v.get("name", c) + else: + name = v + lines.append(f"| `{c}` | {name} |") + + lines.append("") + return "\n".join(lines) + + +def gen_grouped_sub_ids(groups, inline_threshold=7): + """Generate sub-ID tables from grouped sub_ids.""" + lines = [] + for group_key, group in groups.items(): + label = group.get("label", group_key) + sub_ids = group.get("sub_ids", {}) + items = list(sub_ids.items()) + + # Check for long forms + has_long = any( + isinstance(v, dict) and "long" in v for v in sub_ids.values() + ) + + if len(items) <= inline_threshold and not has_long: + # Inline format + parts = [] + for c, v in items: + if isinstance(v, dict): + long_str = f"({v['long']}) " if "long" in v else "" + name = v.get("name", c) + else: + long_str = "" + name = v + parts.append(f"`{c}` {long_str}{name}") + lines.append(f"**{label}:**") + lines.append(" · ".join(parts)) + lines.append("") + else: + # Table format with long forms + lines.append(f"**{label}:**") + lines.append("") + if has_long: + lines.append("| Short | (Long) | Name | Short | (Long) | Name |") + lines.append("|---|---|---|---|---|---|") + for i in range(0, len(items), 2): + c1, v1 = items[i] + if isinstance(v1, dict): + long1 = f"`({v1['long']})`" if "long" in v1 else "—" + name1 = v1.get("name", c1) + else: + long1 = "—" + name1 = v1 + row = f"| `{c1}` | {long1} | {name1}" + if i + 1 < len(items): + c2, v2 = items[i + 1] + if isinstance(v2, dict): + long2 = f"`({v2['long']})`" if "long" in v2 else "—" + name2 = v2.get("name", c2) + else: + long2 = "—" + name2 = v2 + row += f" | `{c2}` | {long2} | {name2} |" + else: + row += " | | | |" + lines.append(row) + else: + lines.append("| Short | Name |") + lines.append("|---|---|") + for c, v in items: + if isinstance(v, dict): + name = v.get("name", c) + else: + name = v + lines.append(f"| `{c}` | {name} |") + lines.append("") + + return "\n".join(lines) + + +def gen_field_def_multi(code, f, scale): + """Generate a multi-type field definition.""" + lines = [] + + # Sub-IDs + if "sub_id_groups" in f: + lines.append(gen_grouped_sub_ids(f["sub_id_groups"])) + elif "sub_ids" in f: + lines.append(gen_sub_id_table(f["sub_ids"])) + + # Scale info + scale_type = f.get("scale_type") + if scale_type and scale_type != "custom": + lines.append(f"Scale: **{scale_type}** (see global scale table).") + lines.append("") + + # Custom scale labels + if "scale_labels" in f: + humor = f.get("humor_scale", {}) or {} + for i in range(8): + label = f["scale_labels"].get(str(i), "—") + h = humor.get(str(i)) + if h: + lines.append(f"{i}. **{label}** — {h}") + else: + lines.append(f"{i}. **{label}**") + lines.append("") + elif f.get("humor_scale"): + humor = f["humor_scale"] + for val in ["0", "7"]: + if val in humor: + lines.append(f"> {val}: {humor[val]}") + lines.append("") + + return "\n".join(lines) + + +def gen_field_def_single(code, f): + """Generate a single-type field definition.""" + lines = [] + lines.append("**Single value only** — one tradition + devoutness rating. Parser rejects multiple sub-IDs.") + lines.append("") + + if "sub_ids" in f: + lines.append(gen_sub_id_table(f["sub_ids"])) + + if "scale_labels" in f: + sl = f["scale_labels"] + lines.append(f"Rating = devoutness: 0 = {sl.get('0', '')}, 4 = {sl.get('4', '')}, 7 = {sl.get('7', '')}.") + lines.append("") + + # Example + if code == "r": + lines.append("> `ris6` — devout Muslim. `rath4` — culturally atheist.") + lines.append("") + + return "\n".join(lines) + + +def gen_field_def_lang(f): + """Generate spoken languages field definition.""" + lines = [] + lines.append("ISO 639-1 codes + proficiency 0–7. `$` and `+` permitted.") + lines.append("") + + if "scale_labels" in f: + humor = f.get("humor_scale", {}) or {} + for i in range(8): + label = f["scale_labels"].get(str(i), "—") + h = humor.get(str(i)) + if h: + lines.append(f"{i}. **{label}** — {h}") + else: + lines.append(f"{i}. **{label}**") + lines.append("") + + # Common codes + codes = f.get("common_codes", {}) + if codes: + items = list(codes.items()) + # Show first ~25 inline + parts = [f"`{c}` {n}" for c, n in items[:26]] + lines.append("Common codes: " + " · ".join(parts)) + lines.append("") + + lines.append( + "> `lar7$en6de3+5` — native Arabic (paid translator), fluent English, " + "learning German (beginner, aspiring proficient)." + ) + lines.append("") + return "\n".join(lines) + + +def gen_field_definitions(fields, scale): + lines = [] + lines.append("## Field definitions") + lines.append("") + lines.append("---") + lines.append("") + + for code in sorted(fields.keys()): + f = fields[code] + lines.append(f"### {code} — {f['name']}") + lines.append("") + + ftype = f["type"] + + if ftype == "direct": + lines.append(gen_field_def_direct(code, f)) + elif ftype == "special": + if code == "g": + lines.append(gen_field_def_special_g(f)) + elif code == "b": + lines.append(gen_field_def_special_b(f)) + elif code == "v": + lines.append(gen_field_def_special_v(f)) + elif code == "l": + lines.append(gen_field_def_lang(f)) + elif ftype == "multi": + # Special intro for certain fields + if code == "d": + lines.append("The editor holy war, now quantified.") + lines.append("") + if code == "l": + # l is type "multi" in JSON but uses ISO 639-1 codes + lines.append(gen_field_def_lang(f)) + else: + lines.append(gen_field_def_multi(code, f, scale)) + elif ftype == "single": + lines.append(gen_field_def_single(code, f)) + + # Note modifiers + mods = f.get("modifiers", {}) + mod_parts = [] + if mods.get("paid"): + mod_parts.append("`$` paid") + if mods.get("aspire"): + mod_parts.append("`+` aspire") + if mods.get("alternative"): + mod_parts.append("`/` alternative") + # Only show if there are interesting modifiers to mention + # (skip if already covered in special format text) + if mod_parts and ftype not in ("special",): + pass # Modifiers are covered in the permissions table + + lines.append("---") + lines.append("") + + return "\n".join(lines) + + +def gen_examples(examples, fields): + lines = [] + lines.append("## Examples") + lines.append("") + + for ex in examples: + handle = ex["handle"] + desc = ex["description"] + uri = ex["uri"] + + lines.append(f"### {handle} — {desc}") + lines.append("") + + # Generate block format from URI + block = uri_to_block(uri, fields) + lines.append("```") + for bl in block: + lines.append(bl) + lines.append("```") + lines.append("") + lines.append("```") + lines.append(uri) + lines.append("```") + lines.append("") + + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def uri_to_block(uri, fields): + """Convert a URI string to block format lines.""" + # Parse: ugi:@: + # Just use the existing block examples from the hand-written spec + # since exact conversion logic is complex. For now, output the URI. + # Actually, let's parse it properly. + + rest = uri + if rest.startswith("ugi:"): + rest = rest[4:] + + # Split version@handle:fields + at_idx = rest.index("@") + version = rest[:at_idx] + rest = rest[at_idx + 1:] + + colon_idx = rest.index(":") + handle = rest[:colon_idx] + field_str = rest[colon_idx + 1:] + + field_parts = field_str.split(",") + + block_lines = [] + block_lines.append("------- BEGIN UGI BLOCK -------") + + # First line: version, handle, and g field + first_line = f"v:{version} @{handle}" + remaining = [] + + for part in field_parts: + if not part: + continue + code = part[0].lower() + if code == "g": + # Geek type goes on first line + first_line += f" G{part[1:]}" + else: + remaining.append(part) + + block_lines.append(first_line) + + # Group remaining fields into lines by category + identity = [] + tech = [] + stance = [] + entertainment = [] + lifestyle = [] + + for part in remaining: + code = part[0].lower() + f = fields.get(code) + if not f: + tech.append(part) + continue + cat = f.get("category", "") + # Expand multi fields + expanded = expand_field_block(part, f) + if cat == "identity": + identity.extend(expanded) + elif cat in ("tech",): + tech.extend(expanded) + elif cat == "stance": + stance.extend(expanded) + elif cat == "entertainment": + entertainment.extend(expanded) + elif cat in ("lifestyle", "appearance"): + lifestyle.extend(expanded) + else: + tech.extend(expanded) + + # Output lines + if identity: + block_lines.append(" ".join(identity)) + if tech: + block_lines.append(" ".join(tech)) + if stance: + block_lines.append(" ".join(stance)) + if entertainment: + block_lines.append(" ".join(entertainment)) + if lifestyle: + block_lines.append(" ".join(lifestyle)) + + block_lines.append("-------- END UGI BLOCK --------") + return block_lines + + +def expand_field_block(part, f): + """Expand a URI field part into block format parts.""" + code = part[0] + rest = part[1:] + ftype = f["type"] + uc = code.upper() + + if ftype == "direct": + return [f"{uc}{rest}"] + elif ftype == "special": + if code == "b" or code == "v": + return [f"{uc}{rest}"] + elif code == "g": + return [f"G{rest}"] + elif code == "l": + # Parse: 2-char code + digit + mods, repeating + expanded = [] + i = 0 + while i < len(rest): + if rest[i] == "~": + # custom + j = i + 1 + while j < len(rest) and rest[j].isalpha(): + j += 1 + sub = rest[i:j] + digit = rest[j] if j < len(rest) and rest[j].isdigit() else "" + mod = "" + k = j + 1 + while k < len(rest) and rest[k] in "$+0123456789": + mod += rest[k] + k += 1 + expanded.append(f"L{sub}{digit}{mod}") + i = k + elif rest[i].isalpha(): + sub = rest[i:i + 2] + digit = rest[i + 2] if i + 2 < len(rest) else "" + mod = "" + k = i + 3 + while k < len(rest) and rest[k] in "$+0123456789": + mod += rest[k] + k += 1 + expanded.append(f"L{sub}{digit}{mod}") + i = k + else: + i += 1 + return expanded + return [f"{uc}{rest}"] + elif ftype in ("multi", "single"): + # Parse sub-id + digit + mods + expanded = [] + i = 0 + while i < len(rest): + if rest[i] == "~": + j = i + 1 + while j < len(rest) and rest[j].isalpha(): + j += 1 + sub = rest[i:j] + digit = rest[j] if j < len(rest) and rest[j].isdigit() else "" + mod = "" + k = j + 1 + while k < len(rest) and rest[k] in "$+0123456789": + mod += rest[k] + k += 1 + expanded.append(f"{uc}{sub}{digit}{mod}") + i = k + elif rest[i].isalpha(): + # Read sub-id (1-4 alpha chars) + j = i + while j < len(rest) and rest[j].isalpha(): + j += 1 + sub = rest[i:j] + digit = rest[j] if j < len(rest) and rest[j].isdigit() else "" + mod = "" + k = j + 1 + while k < len(rest) and rest[k] in "$+0123456789": + mod += rest[k] + k += 1 + expanded.append(f"{uc}{sub}{digit}{mod}") + i = k + else: + i += 1 + return expanded + return [f"{uc}{rest}"] + + +def gen_extending(): + lines = [] + lines.append("## Extending UGI") + lines.append("") + lines.append("Any field with sub-IDs accepts custom entries via `~`:") + lines.append("") + lines.append("```") + lines.append("gcs/~mycology geek of CS and mycology") + lines.append("m~synthwave6 synthwave enthusiast") + lines.append("t~severance7 severance obsessed") + lines.append("j~larp5 LARP, competent") + lines.append("o~beos4 BeOS, neutral nostalgia") + lines.append("```") + lines.append("") + lines.append("Letters `n` and `u` are reserved for future spec versions.") + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_comparison(comparison): + lines = [] + lines.append("## Comparison with predecessors") + lines.append("") + lines.append( + "| | Geek Code 3.12 (1996) | Geek Code 2026 | Hacker Key v4 (2006) | UGI v0 |" + ) + lines.append("|---|---|---|---|---|") + for row in comparison["features"]: + lines.append("| " + " | ".join(row) + " |") + lines.append("") + + # Retained + lines.append("### Fields retained") + lines.append("") + parts = [] + for r in comparison["retained"]: + parts.append(f"`{r['ugi']}` {r['note'].lower()}") + lines.append( + ", ".join(parts) + + " — all present in one or both predecessors." + ) + lines.append("") + + # Added + lines.append("### Fields added") + lines.append("") + for a in comparison["added"]: + lines.append(f"`{a['ugi']}` — {a['reason']}. ", ) + lines.append("") + + # Removed + lines.append("### Fields removed") + lines.append("") + parts = [] + for r in comparison["removed"]: + parts.append(f"{r['field']} — {r['reason'].lower()}") + lines.append(". ".join(parts) + ".") + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_quick_ref(fields): + lines = [] + lines.append("## Quick reference") + lines.append("") + lines.append("```") + lines.append("FORMAT: ugi:[/]@:") + lines.append( + "SCALE: 0=hostile 1=dislike 2=meh 3=slight- 4=neutral 5=like 6=strong 7=obsessed" + ) + lines.append( + "MODIFY: $ = paid + = aspire / = fluctuate,separate ~ = custom" + ) + lines.append("") + + # Field codes + codes = sorted(fields.keys()) + all_codes = [] + for c in "abcdefghijklmnopqrstuvwxyz": + if c in fields: + f = fields[c] + name = f["name"].lower() + all_codes.append(f"{c} {name}") + elif c in ("n", "u"): + all_codes.append(f"[{c} reserved]") + + # Print in rows of 5 + for i in range(0, len(all_codes), 5): + row = all_codes[i : i + 5] + lines.append(" ".join(f"{x:<14}" for x in row).rstrip()) + lines.append("") + + # Modifier groups + both = [] + paid_only = [] + aspire_only = [] + neither = [] + for code in sorted(fields.keys()): + f = fields[code] + mods = f.get("modifiers", {}) + has_paid = mods.get("paid", False) + has_aspire = mods.get("aspire", False) + if has_paid and has_aspire: + both.append(code) + elif has_paid: + paid_only.append(code) + elif has_aspire: + aspire_only.append(code) + else: + neither.append(code) + + lines.append(f"$+ {' '.join(both)} $ only {' '.join(paid_only)} + only {' '.join(aspire_only)} none {' '.join(neither)}") + lines.append("```") + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + + +def gen_references(spec): + lines = [] + lines.append("## References") + lines.append("") + for p in spec["predecessors"]: + lines.append(f"- [{p['name']} (v0.1–latest)]({p['url']}) — {p['author']}, {p['years']}") + if "continuation" in p: + c = p["continuation"] + lines.append(f"- [{c['name']}]({c['url']}) — {c['author']}, {c['years']}") + for r in spec.get("references", []): + lines.append(f"- [{r['title']}]({r['url']})") + lines.append("") + lines.append("---") + lines.append("") + lines.append("## License") + lines.append("") + lines.append(f"{spec['license']} — see [LICENSE](./LICENSE).") + lines.append("") + return "\n".join(lines) + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + registry_path, output_path = sys.argv[1], sys.argv[2] + reg = load_registry(registry_path) + spec = reg["spec"] + fmt = reg["format"] + scale = reg["scale"] + fields = reg["fields"] + modifiers = reg["modifiers"] + examples = reg["examples"] + comparison = reg["comparison"] + + parts = [ + gen_title(spec), + gen_overview(spec), + gen_formats(fmt), + gen_grammar(fields), + gen_scale(scale), + gen_modifier_permissions(fields, modifiers), + gen_field_list(fields, fmt), + gen_field_definitions(fields, scale), + gen_extending(), + gen_examples(examples, fields), + gen_comparison(comparison), + gen_quick_ref(fields), + gen_references(spec), + ] + + # Each part ends with "\n" from join; ensure blank line between sections + output = "\n".join(p.rstrip("\n") for p in parts) + "\n" + + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + with open(output_path, "w") as f: + f.write(output) + + print(f"Generated {output_path}") + + +if __name__ == "__main__": + main() -- cgit v1.2.3