From 3d2e1de689183e91958e7e8d0e309800e0dd9c36 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 15:58:01 +0000 Subject: [PATCH 1/4] fix: lowercase 4444j99.github.io host in links GitHub Pages hosts are lowercase; normalize the uppercase host in the ecosystem and repo-card links. https://claude.ai/code/session_017iav9RLCuHkepAqnGWYMKo --- index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index fda6970..57e3b13 100644 --- a/index.html +++ b/index.html @@ -39,7 +39,7 @@

ORGANVM Ecosystem

  • ORGAN-VI (Koinonia)
  • ORGAN-VII (Kerygma)
  • META-ORGANVM
  • -
  • Personal Profile
  • +
  • Personal Profile
  • @@ -48,22 +48,22 @@

    Personal Profile

    Integrated repository directory for the 4444J99 collective.

    - + 4444J99

    Profile README — Creative Technologist building autonomous systems.

    - + application-pipeline

    Career application pipeline — structured conversion tracking, modular narrative blocks, variant testing, and analytics.

    - + domus-semper-palingenesis

    Personal dotfiles managed with chezmoi.

    - + portfolio

    Portfolio site — Astro, p5.js, 16 project case studies.

    From 006d117766d4f617e51821154ec7eae6ea06da5c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:03:38 +0000 Subject: [PATCH 2/4] feat(landing): drive organ nav + sync date from a single data source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add data/site.json as the single source of truth for the landing page's dynamic data (the organ nav URLs and the footer sync date), and scripts/sync-site.py to render it into index.html via block markers and a value marker. Pure stdlib (no deps); --check guards against drift; SITE_DATA / SITE_HTML env overrides. The site-sync workflow runs the tests, verifies sync on PRs, and auto-commits on push/dispatch. No visible change — rendered output is byte-identical. https://claude.ai/code/session_017iav9RLCuHkepAqnGWYMKo --- .github/workflows/site-sync.yml | 47 ++++++++++++++ .gitignore | 2 + data/site.json | 17 +++++ index.html | 4 +- scripts/sync-site.py | 106 ++++++++++++++++++++++++++++++++ scripts/test_sync_site.py | 70 +++++++++++++++++++++ 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/site-sync.yml create mode 100644 .gitignore create mode 100644 data/site.json create mode 100644 scripts/sync-site.py create mode 100644 scripts/test_sync_site.py diff --git a/.github/workflows/site-sync.yml b/.github/workflows/site-sync.yml new file mode 100644 index 0000000..1b0b07b --- /dev/null +++ b/.github/workflows/site-sync.yml @@ -0,0 +1,47 @@ +name: Site Sync + +on: + push: + branches: [main] + paths: + - "data/site.json" + - "scripts/sync-site.py" + - ".github/workflows/site-sync.yml" + pull_request: + paths: + - "data/site.json" + - "index.html" + - "scripts/sync-site.py" + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run tests + run: python3 scripts/test_sync_site.py + + - name: Verify index.html in sync (pull requests) + if: github.event_name == 'pull_request' + run: python3 scripts/sync-site.py --check + + - name: Sync and commit (push / manual) + if: github.event_name != 'pull_request' + run: | + python3 scripts/sync-site.py + if ! git diff --quiet index.html; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add index.html + git commit -m "chore: sync landing page from data/site.json [skip ci]" + git push + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/data/site.json b/data/site.json new file mode 100644 index 0000000..5cab01f --- /dev/null +++ b/data/site.json @@ -0,0 +1,17 @@ +{ + "_comment": "Single source of truth for the landing page's dynamic data. Run scripts/sync-site.py (or the site-sync workflow) to inject these into index.html. Edit values here, never inline in index.html.", + "values": { + "synced": "2026-05-24" + }, + "organs": [ + { "label": "ORGAN-I (Theoria)", "url": "https://organvm-i-theoria.github.io/" }, + { "label": "ORGAN-II (Poiesis)", "url": "https://organvm-ii-poiesis.github.io/" }, + { "label": "ORGAN-III (Ergon)", "url": "https://organvm-iii-ergon.github.io/" }, + { "label": "ORGAN-IV (Taxis)", "url": "https://organvm-iv-taxis.github.io/" }, + { "label": "ORGAN-V (Logos)", "url": "https://organvm-v-logos.github.io/" }, + { "label": "ORGAN-VI (Koinonia)", "url": "https://organvm-vi-koinonia.github.io/" }, + { "label": "ORGAN-VII (Kerygma)", "url": "https://organvm-vii-kerygma.github.io/" }, + { "label": "META-ORGANVM", "url": "https://meta-organvm.github.io/" }, + { "label": "Personal Profile", "url": "https://4444j99.github.io/", "active": true } + ] +} diff --git a/index.html b/index.html index ceb97dc..3a18d7c 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,7 @@
    @@ -90,7 +92,7 @@

    Personal Profile

    - ORGANVM Distributed Intelligence | System Synchronization: 2026-05-24 + ORGANVM Distributed Intelligence | System Synchronization: 2026-05-24
    diff --git a/scripts/sync-site.py b/scripts/sync-site.py new file mode 100644 index 0000000..b28629a --- /dev/null +++ b/scripts/sync-site.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +sync-site — render the landing page's dynamic data from a single source. + +Reads data/site.json and injects it into index.html: + * inline `...` markers <- data["values"][KEY] + * the `...` block <- data["organs"] + +Usage: + python3 scripts/sync-site.py # rewrite index.html in place + python3 scripts/sync-site.py --check # exit 1 if index.html is out of sync + +Environment overrides: + SITE_DATA — path to the data file (default: data/site.json) + SITE_HTML — path to the HTML file (default: index.html) + +Pure standard library — no third-party dependencies. +""" + +import argparse +import html +import json +import os +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +INDENT = " " * 12 + +VALUE = re.compile(r"()(.*?)()") +ORGANS = re.compile(r"().*?()", re.DOTALL) + + +def render_organs(organs: list[dict]) -> str: + """Render the nav
  • items at the page's 12-space indent.""" + lines = [] + for o in organs: + cls = "active" if o.get("active") else "" + url = html.escape(str(o["url"]), quote=True) + label = html.escape(str(o["label"])) + lines.append(f'{INDENT}
  • {label}
  • ') + return "\n".join(lines) + + +def render(text: str, data: dict) -> tuple[str, list[str]]: + warnings: list[str] = [] + values = {k: ("" if v is None else str(v)) for k, v in (data.get("values") or {}).items()} + seen: set[str] = set() + + def vrepl(m: re.Match) -> str: + key = m.group(2) + seen.add(key) + if key not in values: + warnings.append(f"marker '{key}' has no entry in data['values']") + return m.group(0) + return f"{m.group(1)}{values[key]}{m.group(4)}" + + text = VALUE.sub(vrepl, text) + for key in values: + if key not in seen: + warnings.append(f"value '{key}' has no matching marker in the HTML") + + if "organs" in data: + block = "\n" + render_organs(data["organs"]) + "\n" + INDENT + if ORGANS.search(text): + text = ORGANS.sub(lambda m: m.group(1) + block + m.group(2), text) + else: + warnings.append("no block found in the HTML") + + return text, warnings + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true", + help="verify index.html is in sync; exit 1 on drift (no write)") + args = parser.parse_args() + + data_path = Path(os.environ.get("SITE_DATA") or REPO_ROOT / "data/site.json") + html_path = Path(os.environ.get("SITE_HTML") or REPO_ROOT / "index.html") + + data = json.loads(data_path.read_text()) + original = html_path.read_text() + new_text, warnings = render(original, data) + for w in warnings: + print(f"warn: {w}", file=sys.stderr) + + if args.check: + if new_text != original: + print("error: index.html is out of sync with data/site.json — " + "run `python3 scripts/sync-site.py`", file=sys.stderr) + return 1 + print("index.html is in sync.") + return 0 + + if new_text != original: + html_path.write_text(new_text) + print(f"updated {html_path.relative_to(REPO_ROOT)}") + else: + print("index.html already in sync — no change.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_sync_site.py b/scripts/test_sync_site.py new file mode 100644 index 0000000..12531c9 --- /dev/null +++ b/scripts/test_sync_site.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Tests for scripts/sync-site.py. + +Run: python3 scripts/test_sync_site.py +""" + +import importlib.util +import json +import unittest +from pathlib import Path + +_HERE = Path(__file__).resolve().parent +_spec = importlib.util.spec_from_file_location("sync_site", _HERE / "sync-site.py") +ss = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(ss) + + +class RenderOrgansTests(unittest.TestCase): + def test_active_and_inactive_classes(self): + out = ss.render_organs([ + {"label": "A", "url": "https://a/"}, + {"label": "B", "url": "https://b/", "active": True}, + ]) + self.assertIn('
  • A
  • ', out) + self.assertIn('
  • B
  • ', out) + + def test_escapes_special_chars(self): + out = ss.render_organs([{"label": "X & ", "url": "https://x/?a=1&b=2"}]) + self.assertIn("X & <Y>", out) + self.assertIn("a=1&b=2", out) + + +class RenderTests(unittest.TestCase): + def test_value_marker_replaced(self): + text = "d: old" + out, warns = ss.render(text, {"values": {"synced": "2026-05-24"}}) + self.assertEqual(out, "d: 2026-05-24") + self.assertEqual(warns, []) + + def test_organs_block_regenerated(self): + text = "stale" + out, _ = ss.render(text, {"organs": [{"label": "A", "url": "https://a/"}]}) + self.assertIn("", out) + self.assertIn('
  • A
  • ', out) + self.assertIn("", out) + self.assertNotIn("stale", out) + + def test_orphan_value_and_marker_warn(self): + _, warns = ss.render("x", {"values": {"bar": "1"}}) + self.assertTrue(any("foo" in w for w in warns)) + self.assertTrue(any("bar" in w for w in warns)) + + def test_idempotent(self): + data = {"values": {"synced": "2026-05-24"}, + "organs": [{"label": "A", "url": "https://a/", "active": True}]} + seed = ("x 0 " + "z") + once, _ = ss.render(seed, data) + twice, _ = ss.render(once, data) + self.assertEqual(once, twice) + + def test_real_index_in_sync(self): + data = json.loads((ss.REPO_ROOT / "data/site.json").read_text()) + original = (ss.REPO_ROOT / "index.html").read_text() + out, _ = ss.render(original, data) + self.assertEqual(out, original, "index.html is out of sync with data/site.json") + + +if __name__ == "__main__": + unittest.main() From aa20fb9e490d42cca0a1f5107cb9dc808b14e1b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:13:38 +0000 Subject: [PATCH 3/4] address review feedback on site sync Read/write files as UTF-8 (index.html has em-dashes), and derive the nav block's indent from the matched marker instead of a hardcoded 12-space constant, so the script survives HTML reflows. Add tests for explicit indent and marker-derived indent. Addresses gemini-code-assist and sourcery-ai feedback on #3. https://claude.ai/code/session_017iav9RLCuHkepAqnGWYMKo --- scripts/sync-site.py | 27 ++++++++++++++++----------- scripts/test_sync_site.py | 19 +++++++++++++++---- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/scripts/sync-site.py b/scripts/sync-site.py index b28629a..08cdcf3 100644 --- a/scripts/sync-site.py +++ b/scripts/sync-site.py @@ -26,20 +26,22 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent -INDENT = " " * 12 -VALUE = re.compile(r"()(.*?)()") -ORGANS = re.compile(r"().*?()", re.DOTALL) +VALUE = re.compile(r"()(.*?)()", re.DOTALL) +# Capture the leading indent on the start-marker line so the regenerated block +# matches the surrounding HTML's indentation rather than a hardcoded width. +ORGANS = re.compile(r"(?P[^\S\n]*).*?", + re.DOTALL) -def render_organs(organs: list[dict]) -> str: - """Render the nav
  • items at the page's 12-space indent.""" +def render_organs(organs: list[dict], indent: str) -> str: + """Render the nav
  • items at the given indent.""" lines = [] for o in organs: cls = "active" if o.get("active") else "" url = html.escape(str(o["url"]), quote=True) label = html.escape(str(o["label"])) - lines.append(f'{INDENT}
  • {label}
  • ') + lines.append(f'{indent}
  • {label}
  • ') return "\n".join(lines) @@ -62,9 +64,12 @@ def vrepl(m: re.Match) -> str: warnings.append(f"value '{key}' has no matching marker in the HTML") if "organs" in data: - block = "\n" + render_organs(data["organs"]) + "\n" + INDENT if ORGANS.search(text): - text = ORGANS.sub(lambda m: m.group(1) + block + m.group(2), text) + def repl(m: re.Match) -> str: + indent = m.group("indent") + items = render_organs(data["organs"], indent) + return f"{indent}\n{items}\n{indent}" + text = ORGANS.sub(repl, text) else: warnings.append("no block found in the HTML") @@ -80,8 +85,8 @@ def main() -> int: data_path = Path(os.environ.get("SITE_DATA") or REPO_ROOT / "data/site.json") html_path = Path(os.environ.get("SITE_HTML") or REPO_ROOT / "index.html") - data = json.loads(data_path.read_text()) - original = html_path.read_text() + data = json.loads(data_path.read_text(encoding="utf-8")) + original = html_path.read_text(encoding="utf-8") new_text, warnings = render(original, data) for w in warnings: print(f"warn: {w}", file=sys.stderr) @@ -95,7 +100,7 @@ def main() -> int: return 0 if new_text != original: - html_path.write_text(new_text) + html_path.write_text(new_text, encoding="utf-8") print(f"updated {html_path.relative_to(REPO_ROOT)}") else: print("index.html already in sync — no change.") diff --git a/scripts/test_sync_site.py b/scripts/test_sync_site.py index 12531c9..ec26d88 100644 --- a/scripts/test_sync_site.py +++ b/scripts/test_sync_site.py @@ -20,15 +20,19 @@ def test_active_and_inactive_classes(self): out = ss.render_organs([ {"label": "A", "url": "https://a/"}, {"label": "B", "url": "https://b/", "active": True}, - ]) + ], "") self.assertIn('
  • A
  • ', out) self.assertIn('
  • B
  • ', out) def test_escapes_special_chars(self): - out = ss.render_organs([{"label": "X & ", "url": "https://x/?a=1&b=2"}]) + out = ss.render_organs([{"label": "X & ", "url": "https://x/?a=1&b=2"}], "") self.assertIn("X & <Y>", out) self.assertIn("a=1&b=2", out) + def test_indent_applied(self): + out = ss.render_organs([{"label": "A", "url": "https://a/"}], " ") + self.assertTrue(out.startswith('
  • ')) + class RenderTests(unittest.TestCase): def test_value_marker_replaced(self): @@ -45,6 +49,13 @@ def test_organs_block_regenerated(self): self.assertIn("", out) self.assertNotIn("stale", out) + def test_organs_indent_derived_from_marker(self): + text = " old" + out, _ = ss.render(text, {"organs": [{"label": "A", "url": "https://a/"}]}) + self.assertTrue(out.startswith(" ")) + self.assertIn('\n
  • A
  • \n', out) + self.assertIn("\n ", out) + def test_orphan_value_and_marker_warn(self): _, warns = ss.render("x", {"values": {"bar": "1"}}) self.assertTrue(any("foo" in w for w in warns)) @@ -60,8 +71,8 @@ def test_idempotent(self): self.assertEqual(once, twice) def test_real_index_in_sync(self): - data = json.loads((ss.REPO_ROOT / "data/site.json").read_text()) - original = (ss.REPO_ROOT / "index.html").read_text() + data = json.loads((ss.REPO_ROOT / "data/site.json").read_text(encoding="utf-8")) + original = (ss.REPO_ROOT / "index.html").read_text(encoding="utf-8") out, _ = ss.render(original, data) self.assertEqual(out, original, "index.html is out of sync with data/site.json") From a60b66ee0825528e4e48e038a4a36f08cfc568e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:28:50 +0000 Subject: [PATCH 4/4] make site sync drift-check strict + escape values + fix push workflow --check now fails on warnings (deleted marker / orphan value), closing a false-negative. HTML-escape value-marker content (parity with the organ renderer). Use a repo-relative display helper so a SITE_HTML override outside the repo root no longer crashes after writing. On the push/manual path, run the sync before the in-sync guard test so the drift it repairs can't kill the job; add index.html to the push triggers. Add a --check missing-marker test. Addresses chatgpt-codex-connector review feedback on #3. https://claude.ai/code/session_017iav9RLCuHkepAqnGWYMKo --- .github/workflows/site-sync.yml | 14 ++++++++------ scripts/sync-site.py | 24 +++++++++++++++++------- scripts/test_sync_site.py | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/.github/workflows/site-sync.yml b/.github/workflows/site-sync.yml index 1b0b07b..3d55883 100644 --- a/.github/workflows/site-sync.yml +++ b/.github/workflows/site-sync.yml @@ -5,6 +5,7 @@ on: branches: [main] paths: - "data/site.json" + - "index.html" - "scripts/sync-site.py" - ".github/workflows/site-sync.yml" pull_request: @@ -27,17 +28,18 @@ jobs: with: python-version: "3.12" - - name: Run tests - run: python3 scripts/test_sync_site.py - - - name: Verify index.html in sync (pull requests) + - name: Verify (pull requests) if: github.event_name == 'pull_request' - run: python3 scripts/sync-site.py --check + run: | + python3 scripts/test_sync_site.py + python3 scripts/sync-site.py --check - - name: Sync and commit (push / manual) + - name: Sync, test, and commit (push / manual) if: github.event_name != 'pull_request' run: | + # Sync first so the in-sync guard test passes on the exact drift it repairs. python3 scripts/sync-site.py + python3 scripts/test_sync_site.py if ! git diff --quiet index.html; then git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" diff --git a/scripts/sync-site.py b/scripts/sync-site.py index 08cdcf3..02dab72 100644 --- a/scripts/sync-site.py +++ b/scripts/sync-site.py @@ -28,6 +28,14 @@ REPO_ROOT = Path(__file__).resolve().parent.parent VALUE = re.compile(r"()(.*?)()", re.DOTALL) + + +def _disp(path: Path) -> str: + """Path relative to the repo root for messages; absolute if outside it.""" + try: + return str(path.relative_to(REPO_ROOT)) + except ValueError: + return str(path) # Capture the leading indent on the start-marker line so the regenerated block # matches the surrounding HTML's indentation rather than a hardcoded width. ORGANS = re.compile(r"(?P[^\S\n]*).*?", @@ -56,7 +64,7 @@ def vrepl(m: re.Match) -> str: if key not in values: warnings.append(f"marker '{key}' has no entry in data['values']") return m.group(0) - return f"{m.group(1)}{values[key]}{m.group(4)}" + return f"{m.group(1)}{html.escape(values[key])}{m.group(4)}" text = VALUE.sub(vrepl, text) for key in values: @@ -92,18 +100,20 @@ def main() -> int: print(f"warn: {w}", file=sys.stderr) if args.check: - if new_text != original: - print("error: index.html is out of sync with data/site.json — " - "run `python3 scripts/sync-site.py`", file=sys.stderr) + # Warnings (e.g. a deleted marker leaving a value with nowhere to render) + # are drift too, even when the text happens to be unchanged. + if warnings or new_text != original: + print(f"error: {_disp(html_path)} is out of sync with {_disp(data_path)} — " + f"run `python3 scripts/sync-site.py`", file=sys.stderr) return 1 - print("index.html is in sync.") + print(f"{_disp(html_path)} is in sync.") return 0 if new_text != original: html_path.write_text(new_text, encoding="utf-8") - print(f"updated {html_path.relative_to(REPO_ROOT)}") + print(f"updated {_disp(html_path)}") else: - print("index.html already in sync — no change.") + print(f"{_disp(html_path)} already in sync — no change.") return 0 diff --git a/scripts/test_sync_site.py b/scripts/test_sync_site.py index ec26d88..8fa3e1a 100644 --- a/scripts/test_sync_site.py +++ b/scripts/test_sync_site.py @@ -6,6 +6,8 @@ import importlib.util import json +import sys +import tempfile import unittest from pathlib import Path @@ -70,6 +72,25 @@ def test_idempotent(self): twice, _ = ss.render(once, data) self.assertEqual(once, twice) + def test_check_fails_when_marker_missing(self): + # A value with no marker in the HTML must fail --check, not pass silently. + import os + with tempfile.TemporaryDirectory() as d: + dp = Path(d) / "site.json" + hp = Path(d) / "index.html" + dp.write_text(json.dumps({"values": {"synced": "2026-05-24"}}), encoding="utf-8") + hp.write_text("no markers here", encoding="utf-8") + os.environ["SITE_DATA"] = str(dp) + os.environ["SITE_HTML"] = str(hp) + old_argv = sys.argv + try: + sys.argv = ["sync-site.py", "--check"] + self.assertEqual(ss.main(), 1) + finally: + sys.argv = old_argv + del os.environ["SITE_DATA"] + del os.environ["SITE_HTML"] + def test_real_index_in_sync(self): data = json.loads((ss.REPO_ROOT / "data/site.json").read_text(encoding="utf-8")) original = (ss.REPO_ROOT / "index.html").read_text(encoding="utf-8")