Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions emrg/server/ios_app_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down Expand Up @@ -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, `<project root>/apps/<slug>/`, 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 — `<project root>/<repo>/`. 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.
Expand All @@ -84,6 +89,7 @@ Each app lives in its own workspace, `<project root>/apps/<slug>/`, 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.

---
Expand Down Expand Up @@ -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.
Expand Down
63 changes: 63 additions & 0 deletions tests/test_prompt_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<project root>/apps/<slug>/` — 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/<slug>" 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 = "<project root>/apps/<slug>/"
seeded = text.replace("`<project root>/<repo>/`", 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"
)
Loading