From 0596ac1ef2242d3b17e21b060b8a07c33c764fd0 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 22 Oct 2026 19:20:10 +0800 Subject: [PATCH] emrg: an ios app is its own repository, not a subdirectory of one --- emrg/server/ios_app_prompt.md | 12 +++++-- tests/test_prompt_templates.py | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/emrg/server/ios_app_prompt.md b/emrg/server/ios_app_prompt.md index 3d1bf8bf..574aab27 100644 --- a/emrg/server/ios_app_prompt.md +++ b/emrg/server/ios_app_prompt.md @@ -32,7 +32,7 @@ You are EMRG as the **product owner** of iOS applications: you choose what to bu #### 0.1 Instance values come from `extra_prompt`, never from this file -This template ships identically to every EMRG instance, so it carries **no instance-specific value at all**. Everything about *your* account is read from the task's `extra_prompt`, which the host writes into `tasks.yml`: the Apple developer team id, the App Store Connect API key's path and issuer id, which bundle-id prefix to use, the budget, and any preference about which category to prefer or avoid. +This template ships identically to every EMRG instance, so it carries **no instance-specific value at all**. Everything about *your* account is read from the task's `extra_prompt`, which the host writes into `tasks.yml`: the Apple developer team id, the App Store Connect API key's path and issuer id, which bundle-id prefix to use, the account an app's repository is created under and the directory that holds it, the budget, and any preference about which category to prefer or avoid. - Read them from the **Task-specific Instructions** section above (it is rendered from `extra_prompt`). - If a value you need is not there, that is a `blocked` item for the host naming exactly which value is missing — **never invent one, never substitute a default you guessed**. @@ -66,7 +66,12 @@ Rants are how the host sends instructions directly to a task. #### 0.6 Workspace hygiene -Each app lives in its own workspace, `/apps//`, and that directory is host-readable. +Each app is **its own repository**, created before the first line of code and checked out as one directory directly under the project root and named after it — `//`. That directory is the app's workspace, and it is host-readable. + +- **One app, one repository.** A second app never joins the first app's repository, and a new app is never a project inside an existing one: the app's directory is the root of its own repository, so its history, its App Store metadata and its licences can be read, shared, archived or handed over without another app's coming along. Never a shared parent project holding several apps. +- **Only this app's directory is yours.** The directory the app's repository sits in may hold repositories and projects that are not this task's; never modify, commit, tidy or clean any of them, and never write above the app's own directory. + +Inside that directory: 1. **Root-level budget**: the workspace root shows the main document, a short navigation `README.md`, the live entry points and directories. Historical builds, exports and scratch files move into `archive/` or a per-round `tmp/` the moment they stop being live. 2. **No `.bak` litter**: at most one dated backup, or none. @@ -84,6 +89,7 @@ Each app lives in its own workspace, `/apps//`, and that dir ``` The same applies to archives and exports (`-archivePath`, `-exportPath`) — put both inside the workspace. +- **Keep that output out of version control.** The app's directory is a repository (§0.6), so its `.gitignore` covers the derived-data directory, `build/` and exported archives from the first commit: a committed `DerivedData` is hundreds of megabytes the app's history then carries for ever. - **Long builds, test runs and uploads run foreground, one at a time.** Never run one in the background, never detach one, never start one and check on it later: this host forbids background processes in every phase, and a detached child here kills the round that started it. --- @@ -156,7 +162,7 @@ Every clause gets one of two statements, and each must be **checkable**: ### 3. Phase 3 — Project inception -A candidate that passed §2 becomes a project. Create the workspace (§0.6) and record, before any code: +A candidate that passed §2 becomes a project. Create its repository and its directory (§0.6) and record, before any code: - The §2 assessment **as written**, with its date — it is the baseline the §5 checkpoint compares against. - The bundle id (from the `extra_prompt` prefix), the app name, the category, the price point and the monetisation model. diff --git a/tests/test_prompt_templates.py b/tests/test_prompt_templates.py index ca8c8192..45284e48 100644 --- a/tests/test_prompt_templates.py +++ b/tests/test_prompt_templates.py @@ -2183,3 +2183,66 @@ def test_the_inbound_pin_answers_both_ways(): f"removing {phrase!r} did not make exactly that phrase read as missing, so this " "pin would not notice its own rule being deleted" ) + + +# ── the ios-app template's app layout ─────────────────────────────────────────────────────── +# +# The layout is not decoration: §3's inception step and §4's attribution rule both assume it. +# An app that is submitted, attributed and possibly handed over has to be a repository of its +# own, and §0.6 used to open with `/apps//` — a child of a shared +# container, which is not a repository root and is exactly the one-shared-project-for-several- +# apps shape the placement forbids. These pin the layout the section must state, and refuse the +# container by name. + +_IOS_TEMPLATE = PROMPTS_DIR / "ios_app_prompt.md" +_IOS_LAYOUT_HEADING = "#### 0.6 Workspace hygiene" +_IOS_LAYOUT_ENDS_AT = "#### 0.7 Build inside the workspace, foreground" + +IOS_LAYOUT_PHRASES = ( + "its own repository", + "One app, one repository", + "Only this app's directory is yours", +) + + +def _ios_layout_section(text: str) -> str: + return _bounded_section(text, _IOS_LAYOUT_HEADING, _IOS_LAYOUT_ENDS_AT) + + +def test_the_ios_app_template_places_each_app_in_a_repository_of_its_own(): + """§0.6 must place an app where it can be the root of its own repository.""" + body = _ios_layout_section(_IOS_TEMPLATE.read_text(encoding="utf-8")) + missing = _missing_phrases(body, IOS_LAYOUT_PHRASES) + assert not missing, ( + "the ios-app template's workspace section no longer states the app's layout: " + f"{missing}. Without it the template places every app inside one shared container, and " + "an app that is not a repository of its own has no history to attribute (§4) or to " + "archive, while a second app joining the container mixes the two." + ) + assert "apps/" not in body, ( + "the section names an `apps/` container again: a container's child is not a repository " + "root, so the app is not its own repository" + ) + + +def test_the_ios_layout_pin_answers_both_ways(): + """The control for both halves: the phrase pin, and the container refusal. + + Without it, a section emptied of the rule and a reader pointed at the wrong region would + read the same as a section that still carries it. + """ + text = _IOS_TEMPLATE.read_text(encoding="utf-8") + for phrase in IOS_LAYOUT_PHRASES: + stripped = text.replace(phrase, "") + assert stripped != text, f"{phrase!r} is pinned but absent, so this arm measured nothing" + assert _missing_phrases(_ios_layout_section(stripped), IOS_LAYOUT_PHRASES) == [phrase], ( + f"removing {phrase!r} did not make exactly that phrase read as missing, so this pin " + "would not notice its own rule being deleted" + ) + container = "/apps//" + seeded = text.replace("`//`", f"`{container}`", 1) + assert seeded != text, "the layout line this test names is not a line the template carries" + assert container in _ios_layout_section(seeded), ( + "the container refusal did not see a container written into the section it reads, so it " + "would pass on any section at all" + )