diff --git a/stacklets/memory/lib.py b/stacklets/memory/lib.py index e5076bf1..a31b3e0a 100644 --- a/stacklets/memory/lib.py +++ b/stacklets/memory/lib.py @@ -112,7 +112,6 @@ ) BRAIN_SEED_COMMIT_MESSAGE = "seed: initial brain projection scaffold" BRAIN_MIGRATION_TOKEN_NAME = "memory-brain-migration" -GENERATED_PAGE_MARKER = "" ONTOLOGY_PATH_IN_REPO = "ontology.toml" INSTALL_COMMIT_MESSAGE = "seed: initial memory from famstack {version}" @@ -1227,6 +1226,25 @@ def seed_brain( return {"created": created, "skipped": skipped} +def _declares_generated(content: str) -> bool: + """True when a page's frontmatter declares the whole file generated. + + The splice marker only brackets a *region*, and a hand-written page may + reserve one for the wiki to fill in later — the seeded root `index.md` + does exactly that. Treating the marker as proof of a generated page + deleted that welcome page on every start, which left a fresh install + serving 404 at the wiki root until the first nightly sweep. + + `generated: true` is the whole-page declaration, and the generator + stamps it on every page it publishes (`_with_generated_marker`), so + every real projection artifact still matches. + """ + value = _parse_frontmatter(content).get("generated") + if value is True: + return True + return isinstance(value, str) and value.strip().lower() == "true" + + def purge_generated_memory_pages( client: ForgejoClient, *, @@ -1236,9 +1254,10 @@ def purge_generated_memory_pages( B1 moves generation to `family/brain`, but upgraded instances can already have generated wiki pages in the source repo from pre-B1 - runs. The migration is marker-based so it does not depend on stale - path conventions: any markdown file with the generated splice marker - is a projection artifact and is removed from memory. + runs. The migration keys off the page's own frontmatter rather than + stale path conventions: any markdown file declaring `generated: true` + is a projection artifact and is removed from memory. See + `_declares_generated` for why the splice marker alone is not enough. """ deleted: list[str] = [] tree = client.list_tree(REPO_OWNER, REPO_NAME) @@ -1252,7 +1271,7 @@ def purge_generated_memory_pages( if not existing: continue content = existing.get("content", "") - if GENERATED_PAGE_MARKER not in content: + if not _declares_generated(content): continue client.delete_file( REPO_OWNER, REPO_NAME, path, @@ -1281,7 +1300,7 @@ def purge_local_generated_memory_pages(vault_path: Path) -> dict: content = md.read_text(encoding="utf-8", errors="ignore") except OSError: continue - if GENERATED_PAGE_MARKER not in content: + if not _declares_generated(content): continue try: rel = str(md.relative_to(vault_path)) diff --git a/stacklets/memory/seeds/index.md b/stacklets/memory/seeds/index.md index 0283bbdd..3ba164ab 100644 --- a/stacklets/memory/seeds/index.md +++ b/stacklets/memory/seeds/index.md @@ -15,6 +15,15 @@ To add or edit a page, open the **family/memory** repository in Forgejo. Every change is saved as a version you can always look back on. - +## Your family overview + +Nothing has been summarised yet. The wiki writes this section for you from +the documents you file and the notes you save, and refreshes it every night. + +To build it now instead of waiting, run: + +```sh +stack memory wiki +``` diff --git a/tests/stacklets/test_memory_install_seeds.py b/tests/stacklets/test_memory_install_seeds.py index f01913da..3841531e 100644 --- a/tests/stacklets/test_memory_install_seeds.py +++ b/tests/stacklets/test_memory_install_seeds.py @@ -382,7 +382,7 @@ def test_deletes_generated_markdown_from_memory(self, httpserver): method="GET", ).respond_with_json(_file_response( "index.md", - "---\ntitle: Home\n---\n\nbody\n\n", + "---\ntitle: Home\ngenerated: true\n---\n\nbody\n\n", )) httpserver.expect_request( f"/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/contents/index.md", @@ -393,7 +393,7 @@ def test_deletes_generated_markdown_from_memory(self, httpserver): method="GET", ).respond_with_json(_file_response( "homer/about.md", - "---\ntitle: Homer\n---\n\nbody\n\n", + "---\ntitle: Homer\ngenerated: true\n---\n\nbody\n\n", )) httpserver.expect_request( f"/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/contents/homer/about.md", @@ -419,7 +419,7 @@ def test_deletes_generated_markdown_but_keeps_readme_and_sources(self, tmp_path) generated = tmp_path / "homer" / "about.md" generated.parent.mkdir(parents=True) generated.write_text( - "---\ntitle: Homer\n---\n\nbody\n", + "---\ntitle: Homer\ngenerated: true\n---\n\nbody\n", encoding="utf-8", ) readme = tmp_path / "family" / "correspondents" / "README.md" @@ -435,3 +435,27 @@ def test_deletes_generated_markdown_but_keeps_readme_and_sources(self, tmp_path) assert not generated.exists() assert readme.exists() assert source.exists() + + def test_keeps_a_seeded_page_that_merely_reserves_a_generated_region( + self, tmp_path, + ): + """The seeded root `index.md` is hand-written: a welcome page that + reserves an empty generated region for the wiki to fill in later. + + It is not a projection artifact, and deleting it is what left a + fresh install serving 404 at the wiki root until the first nightly + sweep. Only frontmatter saying `generated: true` marks a whole page + as generated, and the generator writes that on every page it + publishes. + """ + seeded = tmp_path / "index.md" + seeded.write_text( + "---\ntitle: Family Memory\n---\n\nWelcome to your family's memory.\n" + "\n\n", + encoding="utf-8", + ) + + result = purge_local_generated_memory_pages(tmp_path) + + assert result == {"deleted": []} + assert seeded.exists()