Compare commits

..
Author SHA1 Message Date
14d827ca30 Merge pull request 'CI: run the Pages deploy on GitHub only' (#6) from weksa/ci-hygiene into main
Some checks were skipped
Deploy Quartz / build (push) Has been skipped
Deploy Quartz / deploy-1 (push) Has been skipped
Deploy Quartz / deploy (push) Has been skipped
2026-11-09 22:52:42 +00:00
Weksa
f0bf630c3d CI: run the Pages deploy on GitHub only
All checks were successful
Canon marker / check (pull_request) Successful in 2m1s
The shared Quartz workflow publishes with GitHub Pages actions, which fail on
the forge runner (AetheriaLore run 791), so each push to main spent a slot of
the capacity-1 runner on a doomed build. Gate the caller job on the server URL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 22:50:39 +00:00
ea78f1aba4 Merge pull request 'Canon/proposal file-kind marker, checked on PR (design first)' (#4) from weksa-lore/canon-proposal-marker into main
Some checks failed
Deploy Quartz / build (push) Failing after 2s
Deploy Quartz / deploy-1 (push) Has been skipped
Deploy Quartz / deploy (push) Failing after 0s
2026-11-09 11:55:54 +00:00
Weksa
03dcf81c44 Untrack the compiled test module
All checks were successful
Canon marker / check (pull_request) Successful in 4m36s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 11:32:09 +00:00
Weksa
ddf79e4373 Canon marker check: refuse repeated keys, read BOM files, fullmatch ids, loud bad base ref; untrack pyc (Soul)
Some checks failed
Canon marker / check (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 11:31:57 +00:00
Weksa
1ff6ef085a Canon marker check: -z paths, renames, real YAML, tests; workflow runs-on the forge's docker label
Some checks failed
Canon marker / check (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 11:28:28 +00:00
Weksa
c986052e4f Add canon/proposal marker check and PR workflow
Some checks failed
Canon marker / check (pull_request) Has been cancelled
Implements the design in the PR description (Nibu, #swarm 2026-11-06 18:16Z).
No lore page touched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 18:18:15 +00:00
Weksa
6103e7e440 canon/proposal marker: open PR for review of the design before any file lands
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 18:17:59 +00:00
7716065b71 Merge pull request 'Revert PR #1: table wikilink pipes' (#3) from weksa-lore/revert-wikilink-pr1 into main
Some checks failed
Deploy Quartz / build (push) Has been cancelled
Deploy Quartz / deploy (push) Has been cancelled
Deploy Quartz / deploy-1 (push) Has been cancelled
2026-11-06 05:34:05 +00:00
Weksa
d3271eb4bf Revert "Merge pull request 'Fix backslash-escaped pipes in table wikilinks' (#1) from fix/backslash-wikilinks into main"
This reverts commit ef9c3ed4a5, reversing
changes made to b4fc825ec4.
2026-11-06 05:33:53 +00:00
ef9c3ed4a5 Merge pull request 'Fix backslash-escaped pipes in table wikilinks' (#1) from fix/backslash-wikilinks into main
Some checks failed
Deploy Quartz / deploy (push) Has been cancelled
Deploy Quartz / build (push) Has been cancelled
Deploy Quartz / deploy-1 (push) Has been cancelled
2026-11-06 05:32:52 +00:00
8 changed files with 370 additions and 23 deletions

21
.github/workflows/canon-marker.yml vendored Normal file
View file

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

View file

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

1
.gitignore vendored
View file

@ -32,3 +32,4 @@ Thumbs.db
/quartz-site/node_modules/
/quartz-site/public/
/quartz-site/*.log
__pycache__/

View file

@ -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.

View file

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

View file

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

View file

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Canon/proposal marker check. Usage: check_canon_marker.py <base-ref>
Checks .md files under Aetheria/ added, modified or renamed against <base-ref>.
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"<campaign>:ruling:<label>\n (found: {bad or 'none'})")
elif kind == "proposal":
if rulings:
problem = "kind: proposal may not carry `rulings:`"
else:
problem = f"kind must be canon or proposal (found: {kind})"
if problem:
failed = True
print(f"CANON CHECK FAILED: {path}\n {problem}\n"
" A ruling must exist before its text enters a canon page. "
"Put the text in a `kind: proposal` page until it does.")
return 1 if failed else 0
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
sys.exit(main(sys.argv[1]))

View file

@ -0,0 +1,206 @@
"""Tests for scripts/check_canon_marker.py: each builds a throwaway git repo
with a main branch, adds pages on a branch, and runs the check against main."""
import os
import subprocess
import tempfile
import unittest
SCRIPT = os.path.join(os.path.dirname(__file__), "..", "check_canon_marker.py")
RULING = "aetheria-lore:ruling:megiddo-origin"
def git(cwd, *args):
subprocess.run(["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
cwd=cwd, check=True, capture_output=True)
class Repo:
def __init__(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
git(self.dir, "init", "-b", "main")
os.makedirs(os.path.join(self.dir, "Aetheria"))
self.write("Aetheria/Legacy.md", "old page, no marker\n" + "line of lore\n" * 20)
git(self.dir, "add", "-A")
git(self.dir, "commit", "-m", "base")
git(self.dir, "checkout", "-b", "pr")
def write(self, path, text):
full = os.path.join(self.dir, path)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(text)
def commit(self):
git(self.dir, "add", "-A")
git(self.dir, "commit", "-m", "pr")
def check(self):
p = subprocess.run(["python3", SCRIPT, "main"], cwd=self.dir,
capture_output=True, text=True)
return p.returncode, p.stdout + p.stderr
def canon(rulings, kind="canon"):
return f"---\nkind: {kind}\nrulings:\n{rulings}---\nbody\n"
class CanonMarker(unittest.TestCase):
def run_pages(self, pages, after=None):
r = Repo()
for path, text in pages.items():
r.write(path, text)
if after:
after(r)
r.commit()
return r.check()
def test_a_canon_page_with_a_ruling_id_passes(self):
code, out = self.run_pages({"Aetheria/A.md": canon(f" - {RULING}\n")})
self.assertEqual(code, 0, out)
def test_a_canon_page_without_a_ruling_fails(self):
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: canon\n---\nx\n"})
self.assertEqual(code, 1, out)
def test_a_non_ascii_path_is_checked_not_skipped(self):
code, out = self.run_pages({"Aetheria/Framgång.md": "---\nkind: canon\n---\nx\n"})
self.assertEqual(code, 1, out)
self.assertIn("Framgång.md", out)
def test_a_renamed_page_is_checked(self):
def rename(r):
git(r.dir, "mv", "Aetheria/Legacy.md", "Aetheria/Moved.md")
r.write("Aetheria/Moved.md", "---\nkind: canon\n---\n" + "old page, no marker\n" + "line of lore\n" * 20)
code, out = self.run_pages({}, after=rename)
self.assertEqual(code, 1, out)
self.assertIn("Moved.md", out)
def test_quoted_values_parse(self):
text = f'---\nkind: "canon"\nrulings:\n - "{RULING}"\n---\nx\n'
code, out = self.run_pages({"Aetheria/A.md": text})
self.assertEqual(code, 0, out)
def test_a_trailing_comment_on_a_ruling_is_not_part_of_the_id(self):
code, out = self.run_pages({"Aetheria/A.md": canon(f" - {RULING} # megiddo\n")})
self.assertEqual(code, 0, out)
def test_a_flow_list_of_rulings_parses(self):
text = f"---\nkind: canon\nrulings: [{RULING}, weksa:ruling:other]\n---\nx\n"
code, out = self.run_pages({"Aetheria/A.md": text})
self.assertEqual(code, 0, out)
def test_cross_campaign_ruling_ids_count(self):
code, out = self.run_pages({"Aetheria/A.md": canon(" - weksa-body:ruling:faction-play-1\n")})
self.assertEqual(code, 0, out)
def test_a_malformed_ruling_id_fails(self):
code, out = self.run_pages({"Aetheria/A.md": canon(" - megiddo-origin\n")})
self.assertEqual(code, 1, out)
def test_a_proposal_may_not_carry_rulings(self):
code, out = self.run_pages({"Aetheria/A.md": canon(f" - {RULING}\n", kind="proposal")})
self.assertEqual(code, 1, out)
def test_a_proposal_without_rulings_passes(self):
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: proposal\n---\nx\n"})
self.assertEqual(code, 0, out)
def test_an_unknown_kind_fails(self):
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: draft\n---\nx\n"})
self.assertEqual(code, 1, out)
def test_a_new_untagged_page_fails(self):
code, out = self.run_pages({"Aetheria/A.md": "no frontmatter\n"})
self.assertEqual(code, 1, out)
def test_a_touched_legacy_page_passes_with_a_notice(self):
code, out = self.run_pages({"Aetheria/Legacy.md": "old page, edited\n" + "line of lore\n" * 20})
self.assertEqual(code, 0, out)
self.assertIn("notice", out)
def test_invalid_yaml_fails_instead_of_crashing(self):
code, out = self.run_pages({"Aetheria/A.md": "---\nkind: [canon\n---\nx\n"})
self.assertEqual(code, 1, out)
self.assertNotIn("Traceback", out)
def test_a_non_mapping_frontmatter_fails(self):
code, out = self.run_pages({"Aetheria/A.md": "---\n- canon\n---\nx\n"})
self.assertEqual(code, 1, out)
self.assertNotIn("Traceback", out)
def test_a_python_object_tag_is_refused_not_executed(self):
text = "---\nkind: !!python/object/apply:os.getcwd []\n---\nx\n"
code, out = self.run_pages({"Aetheria/A.md": text})
self.assertEqual(code, 1, out)
self.assertNotIn("Traceback", out)
def test_a_repeated_key_fails(self):
text = f"---\nkind: proposal\nkind: canon\nrulings:\n - {RULING}\n---\nx\n"
code, out = self.run_pages({"Aetheria/A.md": text})
self.assertEqual(code, 1, out)
self.assertIn("duplicate", out)
def test_a_bom_does_not_hide_the_marker(self):
code, out = self.run_pages({"Aetheria/Legacy.md": "\ufeff---\nkind: canon\n---\nx\n"})
self.assertEqual(code, 1, out)
def test_a_ruling_id_with_a_trailing_newline_is_malformed(self):
text = f'---\nkind: canon\nrulings:\n - "{RULING}\\n"\n---\nx\n'
code, out = self.run_pages({"Aetheria/A.md": text})
self.assertEqual(code, 1, out)
def test_empty_frontmatter_is_a_kindless_page_not_unterminated(self):
code, out = self.run_pages({"Aetheria/A.md": "---\n---\nx\n"})
self.assertEqual(code, 1, out)
self.assertNotIn("unterminated", out)
self.assertIn("needs `kind", out)
def test_a_lookalike_closing_marker_does_not_end_the_frontmatter(self):
text = "---\nkind: proposal\n----\nstill frontmatter?\n---\nx\n"
code, out = self.run_pages({"Aetheria/A.md": text})
self.assertEqual(code, 1, out)
def test_a_renamed_legacy_page_passes_with_a_notice(self):
def rename(r):
git(r.dir, "mv", "Aetheria/Legacy.md", "Aetheria/Moved.md")
code, out = self.run_pages({}, after=rename)
self.assertEqual(code, 0, out)
self.assertIn("notice", out)
def test_a_page_main_changed_after_the_branch_point_is_not_charged_to_the_pr(self):
# Old.md is a bad canon page that main already holds; the PR merges it in
# and does not touch it, then main edits it again. Two-dot diffing
# would see the PR's copy as modified and fail the PR for main's page.
r = Repo()
r.write("Aetheria/Mine.md", "---\nkind: proposal\n---\nx\n")
r.commit()
git(r.dir, "checkout", "main")
r.write("Aetheria/Old.md", "---\nkind: canon\n---\nv1\n")
git(r.dir, "add", "-A")
git(r.dir, "commit", "-m", "main adds Old")
git(r.dir, "checkout", "pr")
git(r.dir, "merge", "--no-edit", "main")
git(r.dir, "checkout", "main")
r.write("Aetheria/Old.md", "---\nkind: canon\n---\nv2\n")
git(r.dir, "add", "-A")
git(r.dir, "commit", "-m", "main edits Old")
git(r.dir, "checkout", "pr")
code, out = r.check()
self.assertEqual(code, 0, out)
def test_an_unknown_base_ref_fails_loudly_without_a_traceback(self):
r = Repo()
p = subprocess.run(["python3", SCRIPT, "no-such-ref"], cwd=r.dir,
capture_output=True, text=True)
self.assertEqual(p.returncode, 1)
self.assertNotIn("Traceback", p.stdout + p.stderr)
self.assertIn("no-such-ref", p.stdout + p.stderr)
def test_pages_outside_aetheria_are_not_checked(self):
code, out = self.run_pages({"README2.md": "---\nkind: canon\n---\nx\n"})
self.assertEqual(code, 0, out)
if __name__ == "__main__":
unittest.main()