diff --git a/.github/workflows/canon-marker.yml b/.github/workflows/canon-marker.yml new file mode 100644 index 0000000..e23ab28 --- /dev/null +++ b/.github/workflows/canon-marker.yml @@ -0,0 +1,21 @@ +name: Canon marker + +on: + pull_request: + +concurrency: + group: canon-marker-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + # The forge's runner carries the label `docker` (image node:22-bookworm); + # `ubuntu-latest` matches no runner there and the job is cancelled. + runs-on: docker + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: apt-get update -qq && apt-get install -y -qq python3-yaml + - run: python3 -m unittest scripts/tests/test_check_canon_marker.py + - run: python3 scripts/check_canon_marker.py "origin/${{ github.base_ref }}" diff --git a/.github/workflows/deploy-quartz.yml b/.github/workflows/deploy-quartz.yml index 62f0c1c..cf7dbb6 100644 --- a/.github/workflows/deploy-quartz.yml +++ b/.github/workflows/deploy-quartz.yml @@ -17,6 +17,9 @@ concurrency: jobs: deploy: + # The shared workflow publishes through GitHub Pages actions, which fail on + # the forge's runner (capacity 1). Run it on the GitHub mirror only. + if: github.server_url == 'https://github.com' uses: GameCult/GameCult-Quartz/.github/workflows/quartz-pages.yml@main with: content-dir: Aetheria diff --git a/.gitignore b/.gitignore index fea6634..046c00e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ Thumbs.db /quartz-site/node_modules/ /quartz-site/public/ /quartz-site/*.log +__pycache__/ diff --git a/Aetheria/Brainstorming/Corporate Roster and Item Wishlist.md b/Aetheria/Brainstorming/Corporate Roster and Item Wishlist.md index 9f6e0c4..8e5b7e3 100644 --- a/Aetheria/Brainstorming/Corporate Roster and Item Wishlist.md +++ b/Aetheria/Brainstorming/Corporate Roster and Item Wishlist.md @@ -19,16 +19,16 @@ Boss factions are marked ★, demo factions **bold**. "Ships" counts designs and | **Finch Cybernetics** | Passive sensors, character upgrades | Cybernetic transhumanism | Socialist technocratic; sleek, organic | 0 products | — | | **Aeronautics Unlimited** | All-rounder hulls, mining, dumbfire, explosives | Resource acquisition | Britain; minimalist geometric curves | 8 products | — | | **Alakrita** | Light fragile thrusters, fast hulls | Need for speed | Roman; maximalist, aerodynamic creases | 3 products | — | -| **Adrasteia** | Stealth, negative entropy | The best defense is not to need one | India; sleek, angular, faceted | 1 product | [[Worldbuilding/Post-Elysium/Technology/Running Cold|Running Cold]] | +| **Adrasteia** | Stealth, negative entropy | The best defense is not to need one | India; sleek, angular, faceted | 1 product | [[Worldbuilding/Post-Elysium/Technology/Running Cold\|Running Cold]] | | Ewan Hart Inc | Farming equipment | Money *does* grow on trees | USA Midwest; dieselpunk, tractors | 0 products | — | | Rossum & Douglas | AI, automation, compute, guided launchers | Mom's Friendly Robot Company | Germany; trapezoidal, paneled | 2 products | — | -| **Death Monkey Explosives** | Thermal management, thermal weaponry | Some people just want to watch the world burn. We're here to help! | Australia; menacing with spikes | 5 products | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives|note]], [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives Products|products]] | +| **Death Monkey Explosives** | Thermal management, thermal weaponry | Some people just want to watch the world burn. We're here to help! | Australia; menacing with spikes | 5 products | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives\|note]], [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives Products\|products]] | | **Miss Terri's Sugariffic Snack Company** | Corrosive weapons, stat consumables | Candy for all! Unexpected experimental substances for some! | Melting pot; kawaii future bass | 1 product | — | | Aya Collective | Self-repair, life support | Afro-futurism | West Africa, Akan language group, Afrihili | not in catalog | — | | Corriedales | Varied weapons | Family-friendly warfare | Lebanese; kids' toys, bubbly | not in catalog | — | | Odla Framgång | Knockoffs and pseudo-spiritual woo | Multi-level affiliate marketing | Sweden; retrofuturism, populuxe | not in catalog | — | | Pirates (name rolled per run) | Cobbled-together stolen technology | *P I R A T E S* | Emojis; junkyard, improvised | not in catalog | — | -| Megiddo | Defensive technology | *Space Zionists* | Hebrew; choral | not in catalog | [[Worldbuilding/Post-Elysium/Factions/Megiddo|note]] | +| Megiddo | Defensive technology | *Space Zionists* | Hebrew; choral | not in catalog | [[Worldbuilding/Post-Elysium/Factions/Megiddo\|note]] | Each corporation also carries a musical genre in the source table (death metal for Zhestokost, drum and bass for Alakrita, noir jazz for Adrasteia, bluegrass for Ewan Hart, muzak for Rossum & Douglas, and so on). Useful as a tone key when writing copy for one. diff --git a/Aetheria/Brainstorming/Faction Deepening.md b/Aetheria/Brainstorming/Faction Deepening.md index 88c0155..09a0bf1 100644 --- a/Aetheria/Brainstorming/Faction Deepening.md +++ b/Aetheria/Brainstorming/Faction Deepening.md @@ -12,25 +12,25 @@ Ranked from most to least legible, across the factions in [[Game Design/Faction | Rank | Faction | What we know | What is thin | | --- | --- | --- | --- | -| 1 | [[Worldbuilding/Post-Elysium/Factions/Megiddo|Megiddo]] | Founding doctrine, daily life, exit rules, Vaults, defense and trade, internal custodians | Little; the benchmark | -| 2 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Zhestokost|Zhestokost]] | Supply-tail doctrine, heavy-weapons rationale, column behavior, voice | Home life, what a Zhestokost person laughs at | -| 3 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Lucent Media|Lucent Media]] | Spectacle economy, Corriedales pipeline, duel bait (accepted 2026-10-03) | Internal ranks of headliners | -| 4 | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives|Death Monkey Explosives]] | Long note, products, cell structure, punk ethic | Fine as is | -| 5 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aya Collective|Aya Collective]] | Federation, care politics, supplier rules | Rituals, humor | -| 6 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Sol Dominion|Sol Dominion]] | Classification logic, jurisdiction | Everyday life inside the record | -| 7 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cetacean Navigators|Cetacean Navigators]] | Corridor obligations, courts, sanctuary | Fine for now | -| 8 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Framgång|Framgång]] | Retreat economy, Odla affiliates, the persistent salesman | Fine for now | -| 9 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Finch Cybernetics|Finch Cybernetics]] | Clinic dependence, passive sensing | Track-selling is still a proposal | -| 10 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cryonix|Cryonix]] | Thermal enclaves, scout doctrine | Culture outside the license office | -| 11 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aeronautics Unlimited|Aeronautics Unlimited]] | Claimshares, crews versus company | Rituals, humor | -| 12 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Rossum & Douglas|Rossum & Douglas]] | A real note; certification-and-asterisk logic | Its people | -| 13 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pirate Coalition|Pirate Coalition]] | Possession maintenance, crews, credentials | Varies by crew by design | -| 14 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Alakrita|Alakrita]] | Luxury hulls, licensed minds, Ship Mutiny link | Why it breaks off; what its people are like | -| 15 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Corriedales|Corriedales]] | Lucent's toy brand, a sharp idea | A brand, not a people; left to Lucent | -| 16 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express|Lightsail Express]] | Three paragraphs, a motto, trucking flavor | Why its haulers will die for cargo | -| 17 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Ewan Hart|Ewan Hart]] | Three paragraphs, seed lock-in | Everything human; its combat quirk was cut | -| 18 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/NiteLife Energy|NiteLife Energy]] | Three paragraphs, the cartridge | Why its guards never leave; its nightlife | -| 19 | Adrasteia | Products, [[Worldbuilding/Post-Elysium/Technology/Running Cold|Running Cold]] | No faction note at all | +| 1 | [[Worldbuilding/Post-Elysium/Factions/Megiddo\|Megiddo]] | Founding doctrine, daily life, exit rules, Vaults, defense and trade, internal custodians | Little; the benchmark | +| 2 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Zhestokost\|Zhestokost]] | Supply-tail doctrine, heavy-weapons rationale, column behavior, voice | Home life, what a Zhestokost person laughs at | +| 3 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Lucent Media\|Lucent Media]] | Spectacle economy, Corriedales pipeline, duel bait (accepted 2026-10-03) | Internal ranks of headliners | +| 4 | [[Worldbuilding/Post-Elysium/Factions/Death Monkey Explosives\|Death Monkey Explosives]] | Long note, products, cell structure, punk ethic | Fine as is | +| 5 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aya Collective\|Aya Collective]] | Federation, care politics, supplier rules | Rituals, humor | +| 6 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Sol Dominion\|Sol Dominion]] | Classification logic, jurisdiction | Everyday life inside the record | +| 7 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cetacean Navigators\|Cetacean Navigators]] | Corridor obligations, courts, sanctuary | Fine for now | +| 8 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Framgång\|Framgång]] | Retreat economy, Odla affiliates, the persistent salesman | Fine for now | +| 9 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Finch Cybernetics\|Finch Cybernetics]] | Clinic dependence, passive sensing | Track-selling is still a proposal | +| 10 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cryonix\|Cryonix]] | Thermal enclaves, scout doctrine | Culture outside the license office | +| 11 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aeronautics Unlimited\|Aeronautics Unlimited]] | Claimshares, crews versus company | Rituals, humor | +| 12 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Rossum & Douglas\|Rossum & Douglas]] | A real note; certification-and-asterisk logic | Its people | +| 13 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pirate Coalition\|Pirate Coalition]] | Possession maintenance, crews, credentials | Varies by crew by design | +| 14 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Alakrita\|Alakrita]] | Luxury hulls, licensed minds, Ship Mutiny link | Why it breaks off; what its people are like | +| 15 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Corriedales\|Corriedales]] | Lucent's toy brand, a sharp idea | A brand, not a people; left to Lucent | +| 16 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express\|Lightsail Express]] | Three paragraphs, a motto, trucking flavor | Why its haulers will die for cargo | +| 17 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Ewan Hart\|Ewan Hart]] | Three paragraphs, seed lock-in | Everything human; its combat quirk was cut | +| 18 | [[Worldbuilding/Pre-Elysium/Factions/Powers/Minor/NiteLife Energy\|NiteLife Energy]] | Three paragraphs, the cartridge | Why its guards never leave; its nightlife | +| 19 | Adrasteia | Products, [[Worldbuilding/Post-Elysium/Technology/Running Cold\|Running Cold]] | No faction note at all | | 20 | Miss Terri's Sugariffic Snack Company | Products, flavor, the tutorial role | No faction note; the protagonist faction has no lore | ## Deepened diff --git a/README.md b/README.md index 9ba652f..188cc3a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ Writers do not need custody of the whole universe. Shared notes own their stated ## Status - **Live:** the vault is the source of [aetheria.gamecult.org](https://aetheria.gamecult.org), built and deployed by `.github/workflows/deploy-quartz.yml` through the shared engine in `GameCult-Quartz`; `scripts/quartz/quartz.ps1` runs it locally. -- **Known limitation:** Quartz mishandles `\|` inside table wikilinks (it leaves a literal backslash on the target), so tables here write `[[Target|alias]]`; Obsidian may split those cells until the engine is fixed in `GameCult-Quartz`. - **Not canon:** `Brainstorming/` and `Inspiration/` branches are excluded from publication. They are working material, and notes in them become setting only when a finished story or an owner note adopts them. ## Repository Map diff --git a/scripts/check_canon_marker.py b/scripts/check_canon_marker.py new file mode 100644 index 0000000..2ad615f --- /dev/null +++ b/scripts/check_canon_marker.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Canon/proposal marker check. Usage: check_canon_marker.py + +Checks .md files under Aetheria/ added, modified or renamed against . +Proves the form and presence of ruling ids, not that the ruling exists. +""" +import re, subprocess, sys + +import yaml + +ID = re.compile(r"^[a-z0-9][a-z0-9-]*:ruling:[a-z0-9][a-z0-9._-]*$") + + +def git(*a): + return subprocess.run(["git", *a], check=True, capture_output=True, text=True).stdout + + +class UniqueKeyLoader(yaml.SafeLoader): + """safe_load that refuses a repeated key: one page, one reading of `kind`.""" + + def construct_mapping(self, node, deep=False): + seen = set() + for key_node, _ in node.value: + key = self.construct_object(key_node, deep=True) + if key in seen: + raise yaml.constructor.ConstructorError( + None, None, f"duplicate key {key!r}", key_node.start_mark) + seen.add(key) + return super().construct_mapping(node, deep) + + +FRONT = re.compile(r"---\n(?:(.*?)\n)?---[ \t]*(?:\n|\Z)", re.S) + + +def frontmatter(text): + """Return (kind, rulings or None, error). Frontmatter is real YAML.""" + if not text.startswith("---\n"): + return None, None, None + m = FRONT.match(text) + if not m: + return None, None, "unterminated frontmatter" + try: + data = yaml.load(m.group(1) or "", Loader=UniqueKeyLoader) + except yaml.YAMLError as e: + return None, None, f"frontmatter is not valid YAML ({type(e).__name__}: {e})" + if data is None: + data = {} + if not isinstance(data, dict): + return None, None, "frontmatter must be a mapping" + kind, rulings = data.get("kind"), data.get("rulings") + if kind is not None and not isinstance(kind, str): + return None, None, "kind must be a string" + if rulings is not None and not (isinstance(rulings, list) and all(isinstance(r, str) for r in rulings)): + return None, None, "rulings must be a list of strings" + return kind, rulings, None + + +def changed(base): + """(status letter, path) for each page added, modified or renamed (the new path).""" + out = git("diff", "-z", "--name-status", "--diff-filter=AMR", f"{base}...HEAD", "--", "Aetheria") + fields = out.split("\0") + rows, i = [], 0 + while i < len(fields) and fields[i]: + st = fields[i] + if st[0] == "R": + rows.append(("R", fields[i + 2])) + i += 3 + else: + rows.append((st[0], fields[i + 1])) + i += 2 + return rows + + +def main(base): + try: + rows = changed(base) + except subprocess.CalledProcessError as e: + print(f"CANON CHECK FAILED: cannot diff against {base!r}: {e.stderr.strip()}") + return 1 + failed = False + for st, path in rows: + if not path.endswith(".md"): + continue + try: + with open(path, encoding="utf-8-sig") as f: + kind, rulings, err = frontmatter(f.read()) + except (OSError, UnicodeDecodeError) as e: + kind, rulings, err = None, None, f"cannot read page ({type(e).__name__})" + problem = None + if err: + problem = err + elif kind is None: + if st == "A": + problem = "new page needs `kind: canon` or `kind: proposal`" + else: + print(f"notice: {path} has no kind (legacy); tag it when you next touch it") + elif kind == "canon": + bad = [r for r in (rulings or []) if not ID.fullmatch(r)] + if not rulings or bad: + problem = ("kind: canon requires `rulings:` with at least one id of the form " + f":ruling: