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
53 changes: 53 additions & 0 deletions emrg/server/ios_app_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ For each candidate record, in the form a later round can re-check:
- **Implementation shape**: the screens, the data it needs, whether it needs a server, and roughly how many rounds it would take
- **Compute and cost**: what building and running it costs in money (a server, a domain, a certificate renewal) and in rounds

#### 1.1 Two gates, and a candidate that fails either is replaced

§2 refuses a direction Apple would reject. It does not refuse a direction nobody would pay for, so selection carries two gates of its own and a candidate needs both.

1. **Evidence that people pay for this**, measured and written into the candidate record: what the nearest apps charge, whether they charge at all, and how many ratings they carry. A category in which no paid app holds a meaningful number of ratings is a refusal rather than a maybe, and a feeling about the market is not the measure.
2. **A structural difference, stated in one sentence** that names who it is for and what they get that they cannot get today. "It is well engineered" is not a difference, and adding features to a direction that has none does not create one.

**Prefer a product that gives a conclusion to one that reports activity.** A counter of days, words or sessions has hundreds of competitors, and being the best of them still makes it the next one; a product that tells its user something they could not work out for themselves is the shape that can hold a niche.

**Both gates or a different candidate.** Passing §2 is not graduation from this section: an app with no rejection risk can still be one nobody pays for.

**Do not start building from this phase.** A candidate graduates only by passing §2.

---
Expand Down Expand Up @@ -173,6 +184,24 @@ A candidate that passed §2 becomes a project. Create its repository and its dir

Read the current App Store Connect state before choosing a version number: never reuse a version or build number that has already been uploaded.

#### 3.1 The loop, segment by segment

Write the product as the four or five segments a user passes through, in the order they pass through them, and mark each one **have** or **missing** in the inception record.

- **A loop with a missing segment promises an outcome the app does not deliver.** It ships the verdict without the result, which is the failure that looks finished from a distance. The missing segment outranks every new feature, and it outranks a version number: a round that would ship a half-built core ships no version at all.
- Asked whether the product is "too simple", the answer is the core made deeper, not an unrelated feature bolted to the side.

#### 3.2 The design is agreed before the code

- **A static mockup of the screens comes before implementation, and the host confirms it screen by screen.** A mockup is cheap to change and a built screen is not; the confirmation is where a misunderstanding costs a minute instead of a round.
- The mockup is the acceptance surface, so it carries what the host will check: every requirement visible somewhere, the core effect on a screen of its own, one concept per screen, and the user's own vocabulary rather than words the app invented. **A requirement that cannot be pointed at is not on the screen, and a feature nobody can see has not been built.**
- **An example value in a mockup is an example, not a requirement** — do not hard-code the sample into the build.
- **The default path is the product.** The first screen is the value itself, never a questionnaire, a calibration, a permission request or a sign-in, and whatever must exist anyway is a side door rather than the way in. Keep the navigation to what the product needs, with two destinations as the budget and everything else in settings.

#### 3.3 The order of the screens

Order the screens by the order a user uses them, not by the order the code is organised in. Where the order is a preference, the user may choose it — but content is not hidden behind a switch: a screen the user cannot reach is a screen the app does not have.

---

### 4. Phase 4 — Implementation
Expand All @@ -183,6 +212,16 @@ One phase per round, and one coherent piece of work per round.
- Run the app in the simulator and exercise the flow you changed — a build that compiles is not a feature that works.
- Keep the monetisation path real from the first buildable version: StoreKit products defined and purchasable in the sandbox, not stubbed behind a TODO.
- Record what changed and what was verified, in the app's memory entry (§7).
- **A version is not a promise about what was changed; it is a promise about what a user can now do.** Before choosing one, name the capability a user sees. If there is none, the version does not exist yet.

#### 4.1 Gates before features, and each gate is seen red

- **Write the gate that fails first, then the feature that makes it pass.** "Done" with no gate that would have refused it is a claim, and this task has nowhere else to keep one.
- **A rule a comment claims is not a rule.** The comment says what the code must do; if nothing executes it, the gap between the claim and the implementation is where the defect gets through. Where the two differ, the code is the rule.
- **Watch a gate fail before trusting it.** Add a case that must be refused, confirm it is refused, and do it again whenever a judgement is widened — a gate that has only ever been seen green has not been measured.
- **A gate that fails open is not a gate.** Reading a missing value as permission, or a failed read as a pass, is a defect in the gate rather than a tolerance in the check.
- Test the things that must not happen as well as the ones that must: a tap on inert text opens nothing, an unselected path writes no file, a refused permission does not fall back to the network.
- Assert real numbers — except a number a generator owns, which is derived at read time rather than written down, or a correct regeneration reads as a regression.

**Attribution before submission**: for anything worth sharing later, note in the memory entry where the idea came from and under what licence any third-party code, font or asset is used, so the record can be taken apart honestly if it is submitted or criticised.

Expand All @@ -198,6 +237,20 @@ One phase per round, and one coherent piece of work per round.

Then **continue with other work in the same round**. Review takes days and a round takes minutes; waiting is not a phase.

#### 5.1 Every version ships to the host's hands

- **Every version goes to TestFlight**, which is the channel the host actually opens. A version that exists only in App Store Connect has not been shipped.
- **Open the build and look at it before reporting it.** A screen nobody opened is a screen nobody verified, and the round's summary says which screens it saw rather than implying it saw them all.
- **One change at a time.** Navigation, data and copy changed in one submission cannot be diagnosed when one of them breaks.
- **Do not rename an app after it is submitted.** A rename buys nothing and slows the review.
- **Do not wait for a review to finish before releasing something better.** If a better build exists, submit it and withdraw the older one from the queue.

#### 5.2 The store page is where a small app is found

At low install volume, acquisition happens on the store page rather than inside the app, and what it has to work with is **one screenshot and one sentence**. The sentence is concrete and belongs to the user — a named situation with a number in it — not a description of the software. Everything the copy claims has to be shown by the build it describes, so the assets are made from a build that exists.

**At this stage installs are the only hard number.** Price and metadata are not the bottleneck while nobody sees the app, so a pricing experiment is judged by the installs it produces rather than by the argument for it — including a decision to make the app free.

---

### 6. Phase 6 — Result tracking and monetisation iteration
Expand Down
122 changes: 122 additions & 0 deletions tests/test_prompt_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2535,6 +2535,128 @@ def test_the_promote_method_pin_reads_position_and_absence() -> None:
)


# ── the product method the ios-app template owed its readers ────────────────────────────────
#
# An installation's `extra_prompt` for this task carried the product discipline its rounds were
# actually run by: two gates before a direction is chosen, the closed loop marked segment by
# segment, a static mockup the host confirms before any code, gates written before the features
# they gate, and a version that reaches TestFlight and a store page. Measured against the
# template before this change: `TestFlight`, `static`, `fail-closed`, `fail-open`, `store page`,
# `onboarding` and `calibrat` each returned **0** occurrences, while the six phases, the
# rejection gate, the credentials rules and cross-round state were already there. A general
# method an installation has to file as its own configuration is a gap in the template: every
# other installation's rounds run without it.
#
# Each pin names the section that owns the subject, and the section is checked rather than
# assumed — a phrase moved into an appendix leaves a presence assertion green while the rule has
# left the place a reader looks for it. The control below moves one phrase to the end of the file
# and requires the positional reading to notice, which a presence-only pin cannot do.

#: (phrase the rule is stated as, heading that owns its subject).
IOS_METHOD_RULES = (
(
"Evidence that people pay for this",
"#### 1.1 Two gates, and a candidate that fails either is replaced",
),
(
"A structural difference, stated in one sentence",
"#### 1.1 Two gates, and a candidate that fails either is replaced",
),
(
"Prefer a product that gives a conclusion to one that reports activity",
"#### 1.1 Two gates, and a candidate that fails either is replaced",
),
(
"mark each one **have** or **missing**",
"#### 3.1 The loop, segment by segment",
),
(
"A loop with a missing segment",
"#### 3.1 The loop, segment by segment",
),
(
"A static mockup of the screens comes before implementation",
"#### 3.2 The design is agreed before the code",
),
(
"The default path is the product",
"#### 3.2 The design is agreed before the code",
),
(
"Write the gate that fails first",
"#### 4.1 Gates before features, and each gate is seen red",
),
(
"A gate that fails open is not a gate",
"#### 4.1 Gates before features, and each gate is seen red",
),
(
"Every version goes to TestFlight",
"#### 5.1 Every version ships to the host's hands",
),
(
"Open the build and look at it before reporting it",
"#### 5.1 Every version ships to the host's hands",
),
(
"one screenshot and one sentence",
"#### 5.2 The store page is where a small app is found",
),
(
"At this stage installs are the only hard number",
"#### 5.2 The store page is where a small app is found",
),
)


def test_the_ios_template_carries_the_product_method_it_was_missing() -> None:
"""Each rule is stated once, in the section that owns its subject.

What this measures and what it does not: it pins that the rules are **stated where a reader
looks for them**. Whether a round obeyed them is that round's own closing summary, which no
text assertion can answer.
"""
text = _IOS_TEMPLATE.read_text(encoding="utf-8")

assert "### 1. Phase 1 — Research and niche selection" in text, (
"the phases this block pins have been renumbered or renamed, so a positional reading "
"below would compare against nothing"
)

for phrase, expected in IOS_METHOD_RULES:
found = _heading_above(text, phrase)
assert _heading_matches(found, expected), (
f"the ios-app template no longer states {phrase!r} under {expected!r} "
f"(found: {found!r}). A rule stated nowhere, or stated in a section that does not "
"own its subject, is one a round reads past."
)
assert text.count(phrase) == 1, (
f"{phrase!r} appears {text.count(phrase)} times — a rule written twice is two "
"answers that will disagree, and the reader has no way to pick one"
)


def test_the_ios_method_pin_reads_position_and_absence() -> None:
"""The controls: a deleted rule and a moved rule must each redden, and they are not the same
arm — deletion is what a rewrite does, relocation is what a busy edit does."""
text = _IOS_TEMPLATE.read_text(encoding="utf-8")
phrase, expected = IOS_METHOD_RULES[0]

deleted = text.replace(phrase, "")
assert deleted != text, f"{phrase!r} is pinned but absent, so this arm measured nothing"
assert _heading_above(deleted, phrase) is None, (
"a deleted rule still reads as present, so the pin would not notice its own rule going"
)

moved = text.replace(phrase, "").rstrip() + f"\n\n{phrase}\n"
found = _heading_above(moved, phrase)
assert found is not None, "the relocation arm lost the phrase it moved, not the position"
assert not _heading_matches(found, expected), (
f"moving {phrase!r} to the end of the file left it reading as {expected!r} — the pin is "
"reading presence, not position"
)


# ── the reflection heading's count is a claim, so it is measured ──────────────────────────
#
# `promote_prompt.md` opens its reflection section with a sentence that both mandates the
Expand Down
Loading