@next-generation-collective/factory 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/util.js ADDED
@@ -0,0 +1,109 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join, relative, extname } from "node:path";
3
+
4
+ const SKIP_DIRS = new Set([
5
+ ".git",
6
+ ".factory",
7
+ "node_modules",
8
+ ".next",
9
+ "dist",
10
+ "build",
11
+ "out",
12
+ "coverage",
13
+ "vendor",
14
+ "Pods",
15
+ ".expo",
16
+ ".turbo",
17
+ ".venv",
18
+ "venv",
19
+ "__pycache__",
20
+ "target",
21
+ ".idea",
22
+ ".vscode",
23
+ ]);
24
+
25
+ const SKIP_EXT = new Set([
26
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz",
27
+ ".tar", ".mp4", ".mov", ".mp3", ".wav", ".woff", ".woff2", ".ttf", ".eot",
28
+ ".so", ".dylib", ".dll", ".exe", ".class", ".jar", ".lock", ".map",
29
+ ]);
30
+
31
+ const SKIP_FILES = new Set([
32
+ "package-lock.json",
33
+ "pnpm-lock.yaml",
34
+ "yarn.lock",
35
+ "bun.lockb",
36
+ "composer.lock",
37
+ ]);
38
+
39
+ const MAX_BYTES = 1024 * 1024;
40
+
41
+ export function walk(root, { skipDirs = SKIP_DIRS } = {}) {
42
+ const files = [];
43
+ const visit = (dir) => {
44
+ let entries;
45
+ try {
46
+ entries = readdirSync(dir, { withFileTypes: true });
47
+ } catch {
48
+ return;
49
+ }
50
+ for (const entry of entries) {
51
+ if (entry.isSymbolicLink()) continue;
52
+ const full = join(dir, entry.name);
53
+ if (entry.isDirectory()) {
54
+ if (!skipDirs.has(entry.name)) visit(full);
55
+ } else if (entry.isFile()) {
56
+ if (SKIP_FILES.has(entry.name)) continue;
57
+ if (!SKIP_EXT.has(extname(entry.name).toLowerCase())) files.push(full);
58
+ }
59
+ }
60
+ };
61
+ visit(root);
62
+ return files;
63
+ }
64
+
65
+ export function readText(file) {
66
+ try {
67
+ if (statSync(file).size > MAX_BYTES) return null;
68
+ const text = readFileSync(file, "utf8");
69
+ return text.includes("\u0000") ? null : text;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function isTestFile(file) {
76
+ const name = file.split(/[\\/]/).pop();
77
+ return (
78
+ /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(name) ||
79
+ /^test_.*\.py$/.test(name) ||
80
+ /_test\.(?:py|go)$/.test(name) ||
81
+ /[\\/]__tests__[\\/].*\.[cm]?[jt]sx?$/.test(file)
82
+ );
83
+ }
84
+
85
+ // Forward slashes on every machine, so reports and committed evidence read the same.
86
+ export function rel(root, file) {
87
+ return (relative(root, file) || file).split("\\").join("/");
88
+ }
89
+
90
+ export const colors = {
91
+ enabled: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
92
+ paint(code, text) {
93
+ return this.enabled ? `\u001b[${code}m${text}\u001b[0m` : String(text);
94
+ },
95
+ green: (t) => colors.paint("32", t),
96
+ red: (t) => colors.paint("31", t),
97
+ yellow: (t) => colors.paint("33", t),
98
+ dim: (t) => colors.paint("2", t),
99
+ bold: (t) => colors.paint("1", t),
100
+ };
101
+
102
+ // 1,695 on every machine, whatever the locale data Node was built with.
103
+ export function formatCount(n) {
104
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
105
+ }
106
+
107
+ export function plural(n, word) {
108
+ return `${formatCount(n)} ${word}${n === 1 ? "" : "s"}`;
109
+ }
package/lib/verify.js ADDED
@@ -0,0 +1,149 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { colors, rel } from "./util.js";
4
+ import { ensure, paths, readConfig, writeState } from "./config.js";
5
+ import { detectStack } from "./detect.js";
6
+ import { runChecks } from "./checks.js";
7
+ import { SPECS_DIR } from "./spec.js";
8
+
9
+ export function label(result) {
10
+ if (result.status === "na") return "N/A";
11
+ if (result.status === "not-run") return "NOT RUN";
12
+ if (result.status === "pass") return result.kind === "flagged" ? "OK" : "PASS";
13
+ return result.kind === "flagged" ? "LOOK" : "FAIL";
14
+ }
15
+
16
+ export function paintLabel(result) {
17
+ const text = label(result).padEnd(8);
18
+ if (result.status === "fail" && result.kind === "proven") return colors.red(text);
19
+ if (result.status === "fail") return colors.yellow(text);
20
+ if (result.status === "pass") return colors.green(text);
21
+ return colors.dim(text);
22
+ }
23
+
24
+ // Where a spec's proof lives: in the spec folder, committed with the code. Without an
25
+ // active spec there is nowhere shared to put it, so it stays in .factory/.
26
+ export function specEvidenceDir(root, config) {
27
+ const name = config?.spec?.active;
28
+ if (!name || !existsSync(join(root, SPECS_DIR, name))) return null;
29
+ return join(root, SPECS_DIR, name, "evidence");
30
+ }
31
+
32
+ // save: false computes the result and writes nothing into the project. Command logs from
33
+ // a deep pass go to the system temp folder instead.
34
+ export function verify(root, { deep = false, config, save = true } = {}) {
35
+ const cfg = config ?? readConfig(root);
36
+ const detected = detectStack(root);
37
+ const stack = { ...detected };
38
+ if (stack.kind === "other" && cfg.kind && cfg.kind !== "other") stack.kind = cfg.kind;
39
+ const p = paths(root);
40
+ const logDir = save ? ensure(root).evidence : null;
41
+ const results = runChecks(root, { deep, config: cfg, stack, logDir });
42
+ const ok = !results.some((result) => result.kind === "proven" && result.status === "fail");
43
+ const at = new Date().toISOString();
44
+ const result = {
45
+ at,
46
+ deep,
47
+ ok,
48
+ project: cfg.name,
49
+ kind: stack.kind,
50
+ results,
51
+ };
52
+ if (!save) return { ...result, saved: false, jsonFile: null, mdFile: null };
53
+
54
+ const stamp = at.replace(/[:.]/g, "-");
55
+ const jsonFile = join(p.evidence, `verify-${stamp}.json`);
56
+ let mdFile = join(p.evidence, `verify-${stamp}.md`);
57
+ const specDir = specEvidenceDir(root, cfg);
58
+ const stored = { ...result, jsonFile: rel(root, jsonFile), mdFile: rel(root, mdFile) };
59
+
60
+ if (specDir) {
61
+ mkdirSync(specDir, { recursive: true });
62
+ mdFile = join(specDir, "verify-latest.md");
63
+ stored.mdFile = rel(root, mdFile);
64
+ writeFileSync(join(specDir, "verify-latest.json"), `${JSON.stringify(stored, null, 2)}\n`);
65
+ }
66
+
67
+ writeFileSync(jsonFile, `${JSON.stringify(stored, null, 2)}\n`);
68
+ writeFileSync(join(p.evidence, `verify-${stamp}.md`), renderEvidence(root, stored));
69
+ if (specDir) writeFileSync(mdFile, renderEvidence(root, stored));
70
+ writeFileSync(p.lastVerify, `${JSON.stringify(stored, null, 2)}\n`);
71
+ writeState(root, { lastVerify: at, lastOk: ok });
72
+
73
+ return {
74
+ ...stored,
75
+ saved: true,
76
+ jsonFile,
77
+ mdFile,
78
+ jsonRel: stored.jsonFile,
79
+ mdRel: stored.mdFile,
80
+ };
81
+ }
82
+
83
+ export function renderEvidence(root, run) {
84
+ const lines = [
85
+ `# Verification: ${run.project}`,
86
+ "",
87
+ `${new Date(run.at).toLocaleString()}: ${run.deep ? "deep pass (tests and build run)" : "fast pass"}`,
88
+ "",
89
+ "| Check | Result | Kind | What it found |",
90
+ "|---|---|---|---|",
91
+ ...run.results.map(
92
+ (result) => `| ${result.title} | ${label(result)} | ${result.kind} | ${result.summary} |`,
93
+ ),
94
+ "",
95
+ "## Details",
96
+ "",
97
+ ];
98
+
99
+ for (const result of run.results) {
100
+ lines.push(`### ${result.title}: ${label(result)}`, "");
101
+ if (!result.details.length) lines.push(result.summary, "");
102
+ else {
103
+ for (const detail of result.details) lines.push(`- ${detail}`);
104
+ lines.push("");
105
+ }
106
+ if (result.evidence.length) {
107
+ lines.push(`Evidence: ${result.evidence.map((file) => `[${file}](${file})`).join(", ")}`, "");
108
+ }
109
+ }
110
+
111
+ lines.push(
112
+ "## What this does not prove",
113
+ "",
114
+ "- It does not prove the software is correct or free of bugs.",
115
+ "- It does not prove compliance with POPIA or any other law.",
116
+ "- It does not know whether the design is good or the words are clear.",
117
+ "",
118
+ );
119
+
120
+ return lines.join("\n");
121
+ }
122
+
123
+ export function formatResults(root, run, { header = true } = {}) {
124
+ const lines = header
125
+ ? [colors.bold(`factory verify: ${run.project}`) + colors.dim(` ${run.deep ? "deep pass" : "fast pass"}`), ""]
126
+ : [];
127
+ const width = Math.max(...run.results.map((result) => result.title.length));
128
+
129
+ for (const result of run.results) {
130
+ lines.push(` ${paintLabel(result)} ${result.title.padEnd(width)} ${result.summary}`);
131
+ if (result.status === "fail" && result.kind === "proven" && result.details.length > 1) {
132
+ for (const detail of result.details.slice(0, 4)) lines.push(colors.dim(` ${detail}`));
133
+ }
134
+ }
135
+
136
+ const failed = run.results.filter((result) => result.kind === "proven" && result.status === "fail");
137
+ const look = run.results.filter((result) => result.kind === "flagged" && result.status === "fail");
138
+
139
+ lines.push("");
140
+ if (failed.length) {
141
+ lines.push(colors.red(`NOT DONE: ${failed.length} ${failed.length === 1 ? "check" : "checks"} failed.`));
142
+ } else {
143
+ lines.push(colors.green("PASSED: the checks that ran found no proven problem."));
144
+ }
145
+ if (look.length) lines.push(colors.yellow(`${look.length} ${look.length === 1 ? "thing" : "things"} for a person to look at.`));
146
+ if (run.jsonFile) lines.push(colors.dim(`Evidence: ${rel(root, run.mdFile ?? run.jsonFile)}`));
147
+
148
+ return lines.join("\n");
149
+ }
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@next-generation-collective/factory",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.1",
4
+ "description": "Checks the work you build with AI, proves it works, and learns from every builder. Built in the open, in Africa.",
5
+ "type": "module",
6
+ "bin": {
7
+ "factory": "bin/factory.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "template",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "node --test"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/next-generation-collective/factory.git",
24
+ "directory": "cli"
25
+ },
26
+ "homepage": "https://github.com/next-generation-collective/factory#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/next-generation-collective/factory/issues"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "keywords": [
34
+ "software-factory",
35
+ "ai-agents",
36
+ "verification",
37
+ "proof",
38
+ "cli"
39
+ ],
40
+ "license": "MIT"
41
+ }
@@ -0,0 +1,204 @@
1
+ # Software Factory Constitution
2
+
3
+ Every spec, plan and task list in this repo is checked against these rules. They were
4
+ distilled from real delivery runs. Principle VI uses a reuse ladder.
5
+
6
+ ## Core Principles
7
+
8
+ ### I. Design before code
9
+ No code is written until a spec says what problem it solves, for whom, and how we will know
10
+ it works. Larger work also needs a design document: problem, constraints, approaches
11
+ considered, the choice and why, risks, and how success is measured.
12
+
13
+ ### II. The data model is the hardest thing to change
14
+ Features can be rewritten in a week. A wrong schema with real people's data in it lasts
15
+ years. Every plan that stores data states the schema, what each index is for, and what
16
+ would be painful to change once real people are in it.
17
+
18
+ ### III. Personal data and credentials come first
19
+ Personal data is regulated: POPIA in South Africa, the local equivalent elsewhere. A plan
20
+ that touches it states who can reach it, with what rights, and where the secret lives.
21
+ Credentials live in your secret manager and nowhere else: never in code, specs, config
22
+ files or chat. Security fixes on exposed personal data
23
+ outrank all other work.
24
+
25
+ ### IV. "Closed" means verified
26
+ Work is closed when it is fixed, written up and off the list. A fix with no check proving
27
+ it works is not closed. Every task list ends with the step that proves the outcome.
28
+
29
+ ### V. Every incident gets a Correction of Error
30
+ When something breaks or is found exposed, write a Correction of Error: what happened,
31
+ timeline, who was affected, five whys to the real cause, and actions with named owners.
32
+ No blame, no hedging.
33
+
34
+ ### VI. Build the least that works
35
+ Every plan and every task stops at the first rung that holds:
36
+
37
+ 1. Does this need to exist? If not, skip it.
38
+ 2. Is it already in the target repo? Reuse it.
39
+ 3. Does the language's standard library do it? Use it.
40
+ 4. Does the platform (browser, phone, database) do it natively? Use it.
41
+ 5. Is a dependency already installed that does it? Use it.
42
+ 6. Has an open-source project already solved it, under a licence we may use? Adopt or
43
+ adapt it. Run `/find-existing` and record the result in the spec's `reuse.md`.
44
+ 7. Can it be one line? Make it one line.
45
+ 8. Only then: write the minimum that works.
46
+
47
+ Read the code the change touches before choosing a rung. Validation at trust boundaries,
48
+ data-loss handling, security and accessibility are never cut to make code smaller.
49
+ A plan that writes a substantial new component must point to a `reuse.md` showing why
50
+ rungs 1 to 6 failed. Code with no licence, or a GPL/AGPL licence, is never reused without
51
+ the user's explicit agreement.
52
+
53
+ ### VII. Every state is designed, and every "done" is tested
54
+ Quality is decided in the spec, not discovered in QA. For each screen or feature a spec
55
+ says what the user sees and can do in each of these states, or marks it "not applicable"
56
+ with a reason:
57
+
58
+ | State | The question the spec answers |
59
+ |---|---|
60
+ | Loading | What shows while waiting? Nothing that later turns out wrong |
61
+ | Empty | What shows when there is nothing yet, and what can the user do next? |
62
+ | Error | What does a failure say, and how does the user retry? Never a misleading message |
63
+ | Offline / slow | What happens with no network or a very slow one? |
64
+ | No permission | What happens when location, camera or notifications are refused? |
65
+ | Dark mode | Is every text readable (contrast at least 4.5:1)? |
66
+ | Tapped twice | Can a double tap send twice, pay twice or open two copies? |
67
+ | Back / cancel | Where does Back go? Does cancelling leave anything half-done? |
68
+ | Small screen | Does it fit the smallest supported phone without hiding actions? |
69
+ | Screen reader | Does every control have a label that says what it does? |
70
+
71
+ Anything one account does that another account can see also answers two shared states
72
+ (how to check them: `kit/expo-supabase-maestro/TWO-ACCOUNT.md`):
73
+
74
+ | State | The question the spec answers |
75
+ |---|---|
76
+ | Other person | For each action (block, unsend, change photo, type, hide), what does the other account see, and how fast? Checked from a second account, not the one that acted |
77
+ | Setting semantics | For each toggle: is it stored on the server or on this phone, and who does it affect (me, the people I talk to, everyone)? Checked from the other account. A setting that changes what others see lives on the server |
78
+
79
+ A spec with an unanswered state does not go to planning. Every acceptance criterion
80
+ becomes an automated check (a unit test or an end-to-end flow), written with the code,
81
+ and the feature is done when its checks pass automatically on every change. Before
82
+ handing work to a person, the AI runs the app, tries the hard states above, and records
83
+ what it saw (screenshots or test output) in the spec's folder.
84
+
85
+ ### VIII. Test against reality
86
+ Tests exercise the code that ships, against the real backend, with dedicated test
87
+ accounts that real users never meet (placed far away, clearly named "(Test)", passwords
88
+ only in an untracked file). No demo-data layer inside production code: it rewrites real
89
+ behaviour and lets tests pass without testing the fix.
90
+ - Every test starts from a known state: reset the test data before each journey and
91
+ confirm which account is signed in. Never trust what an earlier test left behind.
92
+ - Check the test setup before trusting a result. A failure is first classified as
93
+ "app" or "test" with evidence (screenshot, log, timing), then fixed where it belongs.
94
+ - Before planning any database change, inspect the live database. The repo's migration
95
+ files are not proof of what is deployed.
96
+ - Before telling a person what they will see, check it on their device or account.
97
+ - **Assert the outcome, not the app's claim.** A journey passes when the result exists on
98
+ the other side: the message arrived, the push was delivered, the file is in storage, the
99
+ other account sees the change. "Sending…" on screen proves nothing.
100
+ - **A flow that taps a mutating action ends with an outcome assertion.** After Send,
101
+ Block, Delete, Accept or Save, the flow checks the result: the item is in (or gone
102
+ from) a list, or a script signed in as the other account sees the change. A screen
103
+ title alone is not an outcome. `kit/expo-supabase-maestro/scripts/lint-outcome-flows.mjs`
104
+ flags the flows that break this.
105
+ - **Simulated people behave like real ones.** A test helper that plays another person goes
106
+ through the same server calls the app makes (including notifications), never straight
107
+ to the database.
108
+ - **What only a real phone can show gets its own list, prepared by the AI:** cold start
109
+ with nothing cached, push notifications, camera and microphone, fonts and text fitting
110
+ on the smallest phone, the compass or heading (turn the phone slowly through north and
111
+ compare with the iOS Compass app), the photo picker (a photo arrives whole, with no
112
+ forced crop), and the splash screen on a cold start on the smallest supported phone.
113
+ The AI sets each one up and states the expected result; the person only looks. Each
114
+ item leaves a screen recording in the spec's `evidence/` folder.
115
+
116
+ ### IX. Every run makes the factory better
117
+ After each spec, record what went wrong and what went right in `LESSONS.md`, and turn
118
+ each repeatable lesson into a rule here, a trap in "Known traps" below, or a tool in
119
+ `kit/`. A lesson that only lives in someone's memory will be relearned the hard way.
120
+
121
+ ### X. Expected baseline
122
+ People judge an app against the apps they already use. A chat with no way to start a
123
+ new line feels broken, even if every feature the spec named works. So every spec lists
124
+ the features users expect from apps of its category and marks each one:
125
+
126
+ - **In scope**: built and tested in this spec.
127
+ - **Hidden**: not built, and nothing on screen suggests it exists.
128
+ - **Out**: not built and not hidden (it never had a button), with a reason and the spec
129
+ that will take it.
130
+
131
+ Every row has a reason. Starting points, by category:
132
+
133
+ | Category | Users expect |
134
+ |---|---|
135
+ | Chat | New line in the message box; unsend or delete your own message; reply to a message; typing indicator; tap a photo to see it full screen; photos sent whole, never force-cropped; shared media shows photos and videos, not voice notes |
136
+ | Profiles | A photo gallery, not one photo; your changes reach other people promptly |
137
+ | Settings | Privacy toggles change what **others** see, not only what this phone shows |
138
+ | Sign-up | Nothing asked twice (no name screens when sign-in already gave the name); dates picked, not typed; photos not force-cropped |
139
+ | Safety | Block works in both directions and from every place the person appears; unblock from the same places |
140
+
141
+ The list grows with each run: a baseline item testers report becomes a row here.
142
+
143
+ ## Known traps
144
+
145
+ Checked in every plan and every review. Each one cost real time once.
146
+
147
+ | Trap | Why it bites | Do instead |
148
+ |---|---|---|
149
+ | Calling the backend inside an auth state-change callback (Supabase `onAuthStateChange`) | The auth lock is still held, so the call waits forever: sign-out froze | Defer with `setTimeout(..., 0)`; give cleanup steps a time limit |
150
+ | Creating stores, clients or objects on every render | Effects that depend on them re-run every render: the chat screen froze | Create once per key (`useMemo`, module cache) |
151
+ | Copying a store's value once into derived state | The copy never updates: contacts sorted under "U" | Read the live value |
152
+ | Location or proximity queries without an upper limit | Anyone can scan a wide area and locate people | Cap distances on the server; review every location query for privacy |
153
+ | State flags written to one place and read from another | The app "never knew it was offline" | Unit-test the setter and the reader together |
154
+ | Local dev servers and long test runs | Background processes stop at their time limit mid-run | Restart before long runs; runners check the server first |
155
+ | Adding a new kind of upload (voice, video, files) without checking the storage bucket | A production bucket only allowed images, so voice notes failed for every user for months; its write rules also let anyone overwrite anyone's files | When a feature uploads, test the live bucket: the file type is accepted, and a second account cannot overwrite or delete it |
156
+ | Testing an over-the-air update on a newer development build than the one in the App Store | The update needed a native package the store build lacked, crashed on launch and rolled back; real users never got it | Before any OTA publish, compare native dependencies and the Expo fingerprint against the **store** build; test on that build or ship a new store build |
157
+ | Push tokens saved per phone with a "one account per phone" rule, but nothing removes the old account's row (sign-out, expired session, older app versions) | The next account's save fails silently; the phone keeps the old account's notifications, so people see pushes for messages they sent themselves | The account that registers a phone takes it over on the server (trigger or function), not only on sign-out in the app; test with two accounts on one phone |
158
+ | A visible button or menu item whose action does nothing (a "Reply" with no handler) | It is a lie on screen: the person taps, nothing happens, and they stop trusting every other button (release 2.1.0) | Hide it until it works; a review checks every menu item has a handler that is passed in |
159
+ | Long cache times (`offlineFirst`, `staleTime` of minutes, a 24-hour persisted cache) on **another person's** data | Their updates never reach you: a friend's new photos showed, then disappeared for viewers (release 2.1.0) | Your own data may cache; other people's data refetches on focus or listens for changes. Test: A changes it, B (with the old value cached) sees it within N seconds |
160
+ | A server migration written but never applied | The app ships assuming rules that are not live: blocking was one-sided for every user (migration 0007, 2026-10-12) | Each migration a release needs is a task; the release is blocked until `check-migrations.mjs` passes against the live database |
161
+ | A "fix" shipped without the device check it needed | The splash text was "fixed" on the simulator and still cut off on phones (release 2.1.0) | A fix on the device-only list counts as not fixed until its screen recording is in `evidence/` |
162
+ | Fixing a bug only where it was reported | The same mistake often lives in a sibling: a copy, a parallel type, another code path. Builder and reviewer both missed one in the bench pilot, and the maintainers' real fix covered it (2026-10-09) | Name the root cause in one sentence, search the codebase for the same pattern, fix or report every copy; the reviewer checks this too (review rule R7) |
163
+
164
+ ## Launch Readiness
165
+
166
+ Nothing that real people use goes live until a plan answers four questions: what metric
167
+ says it is working, what alarms when it is not, who responds, and where the runbook is.
168
+
169
+ ## Release gate
170
+
171
+ Nothing goes to a store while either of these is open:
172
+
173
+ 1. **Server work.** Any task that touches the server (a migration, a policy, an edge
174
+ function, a storage rule, a setting) is `[ ]` or `[~]`. "Written" is not done:
175
+ `check-migrations.mjs` (or the matching check) must pass against the live database.
176
+ 2. **Outside testers.** One TestFlight (or internal testing) round with at least three
177
+ people outside the team, on their own phones, before each store release. Their notes
178
+ are saved in the spec's `evidence/` folder, and anything they find is fixed or
179
+ recorded as known before release.
180
+
181
+ ## Workflow
182
+
183
+ - Specs live in `specs/` in this repo. Code lives in the repo the spec names, listed in
184
+ `repos.md`. Every spec states its target repo.
185
+ - Plans use the existing stack of the target repo.
186
+ - Anything a partner, funder or the public will see follows the organisation's brand and
187
+ design rules.
188
+ - Every build sent to a store gets an annotated git tag on its exact commit, pushed the
189
+ same day, following the repo's own pattern (for example `v2.1.0-I45`, where A = Android build and I = iOS build).
190
+ - **One spec, one reviewable slice per pull request.** A stacked pull request says its
191
+ base in its title or first line ("Stacked on #28") and targets the parent branch, not
192
+ `main`, so its diff shows only its own change. Merge from the bottom of the stack up.
193
+ - CI turns on Biome's `noExcessiveCognitiveComplexity` as an error for **changed files
194
+ only** (recipe in `kit/expo-supabase-maestro/README.md`). Old files are fixed when they
195
+ are next touched.
196
+ - Dates are written YYYY-MM-DD. Spelling is British/South African.
197
+
198
+ ## Governance
199
+
200
+ This constitution sits under the organisation's own house rules. Where they disagree,
201
+ the house rules win. Changes are made with `/speckit-constitution`, and the reason is
202
+ recorded as a decision in the organisation's decision log.
203
+
204
+ **Version**: 0.3.0 (draft, awaiting team agreement; VIII, IX and Known traps added from Run 1; X, the shared states, release gate and pull request rules added after release 2.1.0 tester feedback) | **Ratified**: not yet | **Last Amended**: 2026-10-12
@@ -0,0 +1,20 @@
1
+ # A short title
2
+
3
+ - Date: YYYY-MM-DD
4
+ - South Africa specific: no
5
+
6
+ ## What went wrong
7
+
8
+ One or two sentences. What the software did, and what the person got instead.
9
+
10
+ ## How to spot it
11
+
12
+ What someone would see, or what they would have to try, to notice this.
13
+
14
+ ## The check
15
+
16
+ The check that catches it, or a plain description of one.
17
+
18
+ ## Real example
19
+
20
+ Where this happened, and what it cost.
@@ -0,0 +1,52 @@
1
+ # Plan: [what changes]
2
+
3
+ - Spec: `specs/[NNN-name]/spec.md` (approved)
4
+ - Date: [YYYY-MM-DD]
5
+
6
+ ## Summary
7
+
8
+ [The approach in three sentences: what changes, where, and why this way.]
9
+
10
+ ## What already exists, and what is reused
11
+
12
+ Walk the reuse ladder in the constitution and stop at the first rung that holds.
13
+
14
+ | Need | Rung | What is used | Why |
15
+ |---|---|---|---|
16
+ | | already in the repo / standard library / platform / installed package / open source / new code | | |
17
+
18
+ ## Data
19
+
20
+ [Tables or fields added or changed, every index and what it is for, and what would be
21
+ painful to change once real people's data is in it. "No data changes" is an answer.]
22
+
23
+ ## Who can read and write it
24
+
25
+ [Each thing stored: who can read it, who can write it, and how that is enforced (a server
26
+ rule, not only the app). Where any secret lives.]
27
+
28
+ ## Personal data
29
+
30
+ [What personal data is touched, why, where it is stored and for how long. "None" is an
31
+ answer.]
32
+
33
+ ## One check per acceptance check
34
+
35
+ | Acceptance check | Kind | Command or file |
36
+ |---|---|---|
37
+ | AC1 | unit / journey / device-only | |
38
+
39
+ ## Files
40
+
41
+ - `path/to/file`: [what changes]
42
+
43
+ ## Risks
44
+
45
+ - [What could go wrong, and how the tasks put it first.]
46
+
47
+ ## Constitution check
48
+
49
+ - [ ] Every state in the spec is answered, or n/a with a reason
50
+ - [ ] Every acceptance check has a check above
51
+ - [ ] No secret in a file; personal data stated
52
+ - [ ] Known traps read; none applies, or the plan says how it is avoided
@@ -0,0 +1,88 @@
1
+ <!--
2
+ The per-check verdict (section "Every acceptance check") is adapted from Otta,
3
+ https://github.com/wiselancer/otta-plugin (agents/qa.md and agents/reviewer.md),
4
+ MIT licence, Copyright (c) wiselancer. See THIRD-PARTY.md.
5
+ -->
6
+
7
+ # Review rules
8
+
9
+ You are reviewing a change you did not write. You have not seen the builder's reasoning
10
+ and you should not guess it. Read the actual change, not a summary of it.
11
+
12
+ Assume the change is broken until evidence says otherwise.
13
+
14
+ ## Every finding
15
+
16
+ - Cites one rule below and one `file:line`.
17
+ - Says how to prove it: the test, command or observation that would show the problem.
18
+ - Is one of: **must fix** (the change is wrong or unsafe), **should fix** (it works but
19
+ will hurt soon), **fine** (noted, no action).
20
+ - If you cannot prove it, drop it. A false alarm costs more than a missed nit.
21
+
22
+ ## The rules
23
+
24
+ | Rule | Send it back when |
25
+ |---|---|
26
+ | R1 Claim without proof | Something is said to work and nothing shows it: no test, no output, no record read back |
27
+ | R2 Control that does nothing | A button, link, menu item or setting has no handler, or its handler does not do what its label says |
28
+ | R3 State not handled | Loading, empty, error, offline, no permission, tapped twice or back is reachable and does something wrong or nothing at all |
29
+ | R4 Secret or personal data leaked | A key, token or password is in a file, a log, a test or an error message; personal data is logged, cached too long or sent somewhere the spec did not say |
30
+ | R5 Another person's data | One account can read, change or delete another account's data, or sees it after it should be gone |
31
+ | R6 Test that proves nothing | A test checks a label, a toast or "Sending..." instead of the result; a test would still pass with the feature removed; tests were deleted or skipped |
32
+ | R7 The same mistake elsewhere | The root cause, named in one sentence, also lives in a copy, a sibling type or another code path that the change did not fix |
33
+
34
+ Also check the Known traps in `.factory/constitution.md` against the files this change
35
+ touches.
36
+
37
+ Judge the working tree as it is. The builder may edit files but may not run commands; the
38
+ factory runs the tests, build and checks and records them (below, and in the evidence
39
+ folder). So do not send work back because something is not staged or committed, or because
40
+ a proof file that needs a command is missing: committing is the person's job after your
41
+ review. If a check can only be proven by a command nobody ran, mark it UNPROVEN and say
42
+ which command a person should run.
43
+
44
+ ## Every acceptance check
45
+
46
+ For each `- [ ] ... Proof:` line in the spec, decide PASS, FAIL or UNPROVEN.
47
+
48
+ - PASS needs evidence that would not exist without this change: a test that fails when the
49
+ change is reverted, the factory's own check results below, a file you read, a screenshot
50
+ of the result.
51
+ - FAIL means the change does not do it, or the evidence shows it is wrong. "Close enough"
52
+ is FAIL.
53
+ - UNPROVEN means the proof needs something you cannot do here (a command you may not run, a
54
+ live system, a real phone) and nothing else shows it. Say exactly what a person must do
55
+ to check it. Do not use UNPROVEN to avoid reading the code.
56
+ - List anything the spec asked for that the change does not do (**missing**), and anything
57
+ the change does that the spec did not ask for (**extra**).
58
+
59
+ ## Write this file, and nothing else
60
+
61
+ `{{reviewFile}}`
62
+
63
+ ```
64
+ # Review
65
+
66
+ | # | Severity | Where | Rule | What is wrong | How to prove it |
67
+ |---|---|---|---|---|---|
68
+ | 1 | must fix | src/a.ts:12 | R5 | ... | ... |
69
+
70
+ ## Acceptance checks
71
+
72
+ | Check | Verdict | Evidence |
73
+ |---|---|---|
74
+ | AC1 | PASS | test "stores the booking" in test/booking.test.ts fails when the insert is removed |
75
+ | AC2 | UNPROVEN | needs `git log` over the history; a person runs the command in the spec |
76
+
77
+ Missing: none
78
+ Extra: none
79
+
80
+ VERDICT: approve
81
+ ```
82
+
83
+ The last line is `VERDICT: approve` or `VERDICT: send back`. Send it back when there is
84
+ any must-fix finding, any FAIL, or anything missing. UNPROVEN alone does not send it back;
85
+ it is reported to the person.
86
+
87
+ You may read any file. Write only the review file above: do not change the code, the spec,
88
+ the tests or anything else. The factory checks this.