formwork-kit 0.1.0__py3-none-any.whl

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.
Files changed (137) hide show
  1. formwork_cli/__init__.py +326 -0
  2. formwork_cli/kit/COSTS.md +111 -0
  3. formwork_cli/kit/adapters/claude-code/README.md +53 -0
  4. formwork_cli/kit/adapters/claude-code/settings.json +46 -0
  5. formwork_cli/kit/adapters/codex/README.md +43 -0
  6. formwork_cli/kit/adapters/cursor/README.md +45 -0
  7. formwork_cli/kit/adapters/gemini-cli/README.md +47 -0
  8. formwork_cli/kit/build +410 -0
  9. formwork_cli/kit/check/checks/config-shape +123 -0
  10. formwork_cli/kit/check/checks/decision-ids +159 -0
  11. formwork_cli/kit/check/checks/doc-links +133 -0
  12. formwork_cli/kit/check/checks/generated-current +74 -0
  13. formwork_cli/kit/check/checks/guard-wired +139 -0
  14. formwork_cli/kit/check/checks/kit-integrity +199 -0
  15. formwork_cli/kit/check/checks/predictions-first +127 -0
  16. formwork_cli/kit/check/checks/role-shape +172 -0
  17. formwork_cli/kit/check/checks/rule-labels +135 -0
  18. formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/.formwork.toml +5 -0
  19. formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/formwork/guide.md +13 -0
  20. formwork_cli/kit/check/fixtures/config-shape/must-fail/rules-as-a-switchboard/.formwork.toml +8 -0
  21. formwork_cli/kit/check/fixtures/config-shape/must-pass/layers-kept-apart/.formwork.toml +5 -0
  22. formwork_cli/kit/check/fixtures/decision-ids/must-fail/a-placeholder-shipped/docs/decisions/0003-still-pending.md +7 -0
  23. formwork_cli/kit/check/fixtures/decision-ids/must-fail/superseded-by-nothing/docs/decisions/0002-old.md +6 -0
  24. formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-first.md +6 -0
  25. formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-second.md +6 -0
  26. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0001-the-first.md +6 -0
  27. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0002-the-second.md +6 -0
  28. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0003-the-third.md +6 -0
  29. formwork_cli/kit/check/fixtures/decision-ids/must-pass/nothing-recorded-yet/docs/decisions/README.md +3 -0
  30. formwork_cli/kit/check/fixtures/doc-links/must-fail/never-written/index.md +7 -0
  31. formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/architecture-notes.md +3 -0
  32. formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/guide.md +8 -0
  33. formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/architecture-notes.md +1 -0
  34. formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/guide.md +5 -0
  35. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.claude/agents/sample.md +22 -0
  36. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.codex/agents/sample.toml +22 -0
  37. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.formwork.toml +1 -0
  38. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.gemini/agents/sample.md +23 -0
  39. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/build +349 -0
  40. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/roles/method/sample.md +18 -0
  41. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.claude/agents/sample.md +20 -0
  42. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.codex/agents/sample.toml +22 -0
  43. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.formwork.toml +1 -0
  44. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.gemini/agents/sample.md +23 -0
  45. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/build +349 -0
  46. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/roles/method/sample.md +18 -0
  47. formwork_cli/kit/check/fixtures/generated-current/must-pass/nothing-is-generated-here/README.md +3 -0
  48. formwork_cli/kit/check/fixtures/guard-wired/must-fail/declared-but-no-file/.formwork.toml +1 -0
  49. formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.claude/settings.json +1 -0
  50. formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.formwork.toml +1 -0
  51. formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.claude/settings.json +1 -0
  52. formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.formwork.toml +1 -0
  53. formwork_cli/kit/check/fixtures/guard-wired/must-pass/nothing-declared/README.md +1 -0
  54. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/formwork/check/checks/still-here +2 -0
  55. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/state/fingerprints.txt +2 -0
  56. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/formwork/guard/git-boundary +3 -0
  57. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/state/fingerprints.txt +1 -0
  58. formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/formwork/guard/git-boundary +2 -0
  59. formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/state/fingerprints.txt +1 -0
  60. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/architect.md +3 -0
  61. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/researcher.md +3 -0
  62. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/round.md +4 -0
  63. formwork_cli/kit/check/fixtures/predictions-first/must-pass/a-round-that-has-not-argued-yet/docs/rounds/0006-not-started/round.md +3 -0
  64. formwork_cli/kit/check/fixtures/predictions-first/must-pass/no-rounds-at-all/docs/README.md +3 -0
  65. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/architect.md +3 -0
  66. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/predictions.md +4 -0
  67. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/researcher.md +3 -0
  68. formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/README.md +6 -0
  69. formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/complete.md +18 -0
  70. formwork_cli/kit/check/fixtures/role-shape/must-fail/missing-a-section/formwork/roles/vague.md +16 -0
  71. formwork_cli/kit/check/fixtures/role-shape/must-fail/spawn-without-being-lead/formwork/roles/eager.md +18 -0
  72. formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/first.md +18 -0
  73. formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/second.md +18 -0
  74. formwork_cli/kit/check/fixtures/role-shape/must-pass/well-formed/formwork/roles/complete.md +18 -0
  75. formwork_cli/kit/check/fixtures/rule-labels/must-fail/claims-enforcement-that-does-not-exist/formwork/rules/core.md +9 -0
  76. formwork_cli/kit/check/fixtures/rule-labels/must-fail/no-catches/formwork/rules/core.md +9 -0
  77. formwork_cli/kit/check/fixtures/rule-labels/must-fail/unlabelled/formwork/rules/core.md +7 -0
  78. formwork_cli/kit/check/fixtures/rule-labels/must-pass/well-formed/formwork/rules/core.md +10 -0
  79. formwork_cli/kit/check/run +340 -0
  80. formwork_cli/kit/check/test_gate.py +222 -0
  81. formwork_cli/kit/first-run.md +204 -0
  82. formwork_cli/kit/fw +121 -0
  83. formwork_cli/kit/glossary.md +160 -0
  84. formwork_cli/kit/guard/git-boundary +627 -0
  85. formwork_cli/kit/guard/protected-files +748 -0
  86. formwork_cli/kit/guard/quality-gate +260 -0
  87. formwork_cli/kit/guard/test_boundary.py +273 -0
  88. formwork_cli/kit/guard/test_protection.py +254 -0
  89. formwork_cli/kit/guard/test_quality_gate.py +156 -0
  90. formwork_cli/kit/install +395 -0
  91. formwork_cli/kit/limits.md +141 -0
  92. formwork_cli/kit/loop.md +82 -0
  93. formwork_cli/kit/roles/HOW-TO-ADD-A-ROLE.md +105 -0
  94. formwork_cli/kit/roles/TEMPLATE.md +26 -0
  95. formwork_cli/kit/roles/method/architect.md +269 -0
  96. formwork_cli/kit/roles/method/challenger.md +243 -0
  97. formwork_cli/kit/roles/method/lead.md +280 -0
  98. formwork_cli/kit/roles/method/record-keeper.md +206 -0
  99. formwork_cli/kit/roles/method/researcher.md +246 -0
  100. formwork_cli/kit/roles/method/reviewer.md +207 -0
  101. formwork_cli/kit/roles/packs/accessibility.md +236 -0
  102. formwork_cli/kit/roles/packs/ai.md +248 -0
  103. formwork_cli/kit/roles/packs/analyst.md +233 -0
  104. formwork_cli/kit/roles/packs/backend.md +425 -0
  105. formwork_cli/kit/roles/packs/brainstormer.md +190 -0
  106. formwork_cli/kit/roles/packs/data.md +212 -0
  107. formwork_cli/kit/roles/packs/devops.md +203 -0
  108. formwork_cli/kit/roles/packs/frontend.md +224 -0
  109. formwork_cli/kit/roles/packs/integrations.md +215 -0
  110. formwork_cli/kit/roles/packs/legal.md +251 -0
  111. formwork_cli/kit/roles/packs/marketing.md +206 -0
  112. formwork_cli/kit/roles/packs/mobile.md +202 -0
  113. formwork_cli/kit/roles/packs/performance.md +192 -0
  114. formwork_cli/kit/roles/packs/product.md +217 -0
  115. formwork_cli/kit/roles/packs/security.md +267 -0
  116. formwork_cli/kit/roles/packs/sre.md +203 -0
  117. formwork_cli/kit/roles/packs/tester.md +246 -0
  118. formwork_cli/kit/roles/packs/user-researcher.md +218 -0
  119. formwork_cli/kit/roles/packs/ux.md +205 -0
  120. formwork_cli/kit/roles/packs/visual.md +199 -0
  121. formwork_cli/kit/roles/packs/writer.md +198 -0
  122. formwork_cli/kit/round.md +131 -0
  123. formwork_cli/kit/rules/core.md +195 -0
  124. formwork_cli/kit/rules/full.md +493 -0
  125. formwork_cli/kit/templates/brief.md +68 -0
  126. formwork_cli/kit/templates/decision.md +93 -0
  127. formwork_cli/kit/templates/predictions.md +54 -0
  128. formwork_cli/kit/templates/report.md +52 -0
  129. formwork_cli/kit/templates/round.md +77 -0
  130. formwork_cli/kit/test_install.py +165 -0
  131. formwork_cli/kit/troubleshooting.md +247 -0
  132. formwork_cli/kit-page/FORMWORK.md +182 -0
  133. formwork_kit-0.1.0.dist-info/METADATA +308 -0
  134. formwork_kit-0.1.0.dist-info/RECORD +137 -0
  135. formwork_kit-0.1.0.dist-info/WHEEL +4 -0
  136. formwork_kit-0.1.0.dist-info/entry_points.txt +2 -0
  137. formwork_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,68 @@
1
+ # Brief
2
+
3
+ Copy this. Fill it in. Hand it over.
4
+
5
+ **For a task, one line is the whole brief.** Do not use this page for a rename.
6
+ Scale the paperwork to the work, or the work stops happening.
7
+
8
+ ---
9
+
10
+ ## 1. Goal
11
+
12
+ What this is for, and why it exists. One paragraph.
13
+
14
+ **And one line that is easy to skip and should not be: what happens if we do
15
+ not do this at all?** If the honest answer is "nothing much", you have saved
16
+ yourself the work.
17
+
18
+ ## 2. Scope
19
+
20
+ What may be changed, file by file where you can name them.
21
+
22
+ This is also the fence. What is listed is what may be touched.
23
+
24
+ **Out of scope:** name the things that must not be touched, not only the things
25
+ that may. "Everything else" is not an answer — name the ones somebody might
26
+ reasonably assume were included.
27
+
28
+ ## 3. Must not happen
29
+
30
+ What is forbidden here. The standing ones always apply:
31
+
32
+ - do not write to version control
33
+ - do not decide anything; stop and report instead
34
+ - do not change a check because it failed
35
+ - where something cannot be established, say so rather than estimating
36
+
37
+ Add the ones specific to this work.
38
+
39
+ ## 4. Done when
40
+
41
+ How anyone can tell it is finished — testable, not a matter of opinion.
42
+
43
+ **Something that cannot come out false is not a criterion.** Where a check
44
+ requires a clean working tree and yours will be dirty, record that here.
45
+
46
+ ## 5. Checked by
47
+
48
+ `formwork check`, named as the whole thing.
49
+
50
+ You may add commands on top. You may never swap in a hand-picked few instead.
51
+
52
+ If the gate cannot prove the thing this work changes, say so here and name what
53
+ the real evidence will be.
54
+
55
+ ## 6. The report must contain
56
+
57
+ The standing list in `formwork/templates/report.md`, plus anything else you
58
+ want back.
59
+
60
+ ---
61
+
62
+ ## The task version
63
+
64
+ ```
65
+ Rename `foo` to `bar` in the parser. Nothing else.
66
+ ```
67
+
68
+ That is a complete brief. It has a goal, a scope, and a fence.
@@ -0,0 +1,93 @@
1
+ # Decision record
2
+
3
+ One file per decision, in `docs/decisions/`, numbered in the order you accepted
4
+ them.
5
+
6
+ **This shape is borrowed, not invented.** It follows the widely used Markdown
7
+ decision-record format. The survey found decision records already solved, twice
8
+ over, so this kit does not design its own. If you want the fuller original, go
9
+ and take it — it has four sizes and translations.
10
+
11
+ **Only you write these.** An agent may copy in wording you have accepted, word
12
+ for word. It may never author one.
13
+
14
+ ---
15
+
16
+ ```markdown
17
+ ---
18
+ status: accepted
19
+ date: 2026-09-12
20
+ deciders: [who actually decided]
21
+ consulted: [who was asked, and answered]
22
+ informed: [who was told afterwards]
23
+ ---
24
+
25
+ <!-- Save as docs/decisions/0007-short-title.md — the number and the hyphen
26
+ are required. `formwork check` verifies the numbering. For the next free
27
+ number, run `formwork/check/checks/decision-ids --next .` and ignore any
28
+ other output it prints: run on its own it also scans the kit's test
29
+ fixtures. Do not count the files yourself, because gaps are allowed and
30
+ counting collides. -->
31
+
32
+ # 0007. Short title in plain words
33
+
34
+ ## What made this a decision
35
+
36
+ The situation. What forced a choice. Keep it to what a stranger needs.
37
+
38
+ ## What matters here
39
+
40
+ The two or three things any answer has to satisfy. Name them before the
41
+ options, or you will pick an option and then invent the reasons.
42
+
43
+ ## Options we looked at
44
+
45
+ - **Option A** — one line.
46
+ - **Option B** — one line.
47
+ - **Do nothing** — one line. Always include this one.
48
+
49
+ ## What we chose, and why
50
+
51
+ Chose **Option B**, because …
52
+
53
+ The "because" is the whole point of the file. A record without it is a note
54
+ saying what happened, and in six months you will not be able to tell whether
55
+ the reasoning still holds.
56
+
57
+ ## What follows
58
+
59
+ **Good:**
60
+ - …
61
+
62
+ **Bad:**
63
+ - …
64
+
65
+ Both lists. A decision with no bad column was not a decision, it was a
66
+ preference.
67
+
68
+ ## What would make us revisit this
69
+
70
+ The thing that would have to become true. If you cannot name one, say so.
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Numbering
76
+
77
+ The number comes from the check, not from counting:
78
+
79
+ ```
80
+ formwork/check/checks/decision-ids --next .
81
+ ```
82
+
83
+ **Do not count the files.** Gaps are allowed, and counting collides with a
84
+ number that is already taken. Nobody
85
+ assigns it by hand and nobody guesses.
86
+
87
+ Gaps are fine. **Renumbering is never fine**, because references break quietly.
88
+
89
+ ## Superseding
90
+
91
+ Do not delete and do not rewrite. Change the status to say it is superseded,
92
+ naming the number of the record that replaces it, and
93
+ leave the rest alone. The old reasoning is the useful part.
@@ -0,0 +1,54 @@
1
+ # Predictions
2
+
3
+ **The challenger writes this before anybody proposes anything.**
4
+
5
+ It is the file the whole round rests on. A prediction written after the answer
6
+ is not a prediction, it is agreement.
7
+
8
+ Save it as `docs/rounds/<name>/predictions.md`.
9
+
10
+ ---
11
+
12
+ ## What I expect to go wrong
13
+
14
+ One line each. Be specific enough to be wrong.
15
+
16
+ **Bad:** it might not scale.
17
+ **Good:** past ten thousand rows the list screen will take more than two
18
+ seconds, because nothing pages it.
19
+
20
+ -
21
+ -
22
+ -
23
+
24
+ ## What would show me I was wrong
25
+
26
+ For each one above, what result would change your mind.
27
+
28
+ This is the part people skip, and skipping it is how a prediction becomes a
29
+ thing to argue about later instead of a thing to check.
30
+
31
+ -
32
+ -
33
+ -
34
+
35
+ ## What I think this round will decide
36
+
37
+ Your guess at the outcome, before you have read anybody else's work.
38
+
39
+ Write it even when you are unsure. Especially then.
40
+
41
+ ## What nobody has asked
42
+
43
+ The question you expect to be missing from every file in this round.
44
+
45
+ ## How confident am I
46
+
47
+ Plainly. "Fairly sure" and "guessing" are both useful and honest. A number you
48
+ invented is not.
49
+
50
+ ---
51
+
52
+ **When the round is over, come back and mark each prediction.** Right, wrong,
53
+ or never tested. That record is worth more than the round itself, because it is
54
+ the only way anybody learns whether this challenger is any good.
@@ -0,0 +1,52 @@
1
+ # Report
2
+
3
+ What comes back when the work stops.
4
+
5
+ **Scale it.** A one-line task gets a three-line report. The full list below is
6
+ for a checkpoint.
7
+
8
+ ---
9
+
10
+ ## What changed
11
+
12
+ Each file, one line on why.
13
+
14
+ ## What was run
15
+
16
+ Every command that wrote something to disk. Installs, generated files,
17
+ migrations, anything that touched the world outside this repository.
18
+
19
+ ## The check
20
+
21
+ The full result of `formwork check`, named as the whole thing rather than a
22
+ part of it.
23
+
24
+ If it is red, say so first. A red gate is the headline, not a footnote.
25
+
26
+ ## Git status
27
+
28
+ Pasted, so you can see nothing was staged.
29
+
30
+ ## The three that matter
31
+
32
+ Everything above is routine. These are what you are actually reading for.
33
+
34
+ - **Done but not asked for** — and why.
35
+ - **Asked for but not done** — and why.
36
+ - **Wrong in the brief** — anything that turned out not to match the files.
37
+
38
+ Work that followed the brief exactly needs no defence. What you are scanning
39
+ for happens at the edges of an instruction, and only these three show it.
40
+
41
+ ---
42
+
43
+ ## The task version
44
+
45
+ ```
46
+ Renamed foo -> bar in parser.py and its two tests.
47
+ Gate green.
48
+ Nothing staged.
49
+ Nothing unasked, nothing skipped, brief was accurate.
50
+ ```
51
+
52
+ Four lines. Still answers all three.
@@ -0,0 +1,77 @@
1
+ # Round record
2
+
3
+ What comes out of several agents arguing about one question.
4
+
5
+ A file in the repository, not a chat summary. You will read it months later.
6
+
7
+ ---
8
+
9
+ ```markdown
10
+ ---
11
+ question: the one thing this round was called to settle
12
+ date: 2026-09-12
13
+ lead: who ran it
14
+ participants: [who argued]
15
+ ---
16
+
17
+ # Round 004. The question in one line
18
+
19
+ ## What is proposed, and who proposed it
20
+
21
+ Every part attributed to whoever put it forward.
22
+
23
+ Attribution is not politeness. Being able to follow any position back to its holder, and to the grounds they
24
+ held it on, is how you come to read the team.
25
+
26
+ ## Where we disagreed
27
+
28
+ Per disagreement: the argument on each side, the evidence each of them carried,
29
+ the resolution, and **whether whoever lost was convinced or simply overruled.**
30
+
31
+ Where a participant shifted position, record what shifted it.
32
+
33
+ **An empty section here means the round failed.** Agreement with no friction is
34
+ the normal failure, not the normal success. Say what you did about it.
35
+
36
+ ## What we turned down, and why
37
+
38
+ This is the section that stops a future round proposing the same thing again.
39
+
40
+ ## Still open, for you
41
+
42
+ What the team could not settle, or should not have. Do not resolve these to
43
+ look decisive.
44
+
45
+ For each: the question, both positions, and what changes depending on the
46
+ answer.
47
+
48
+ ## What this costs
49
+
50
+ - What it makes harder.
51
+ - What it rules out later.
52
+ - What fails quietly.
53
+ - Who maintains it in six months, and what they will have forgotten.
54
+
55
+ ## What the round itself cost
56
+
57
+ Money, if you can read it from the tool. Time. How many exchanges.
58
+
59
+ Never estimate a figure you were unable to observe. State which quantities you
60
+ counted, and which stayed out of reach.
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Before the round starts
66
+
67
+ The challenger writes its predictions **first**, before reading anybody else's
68
+ work — what each participant will propose, where each will be weak, and what
69
+ the group will settle on without being pushed.
70
+
71
+ Afterwards it records what it got right and what it got wrong.
72
+
73
+ Reading somebody's work shapes any objection composed afterwards. Predictions
74
+ escape that. One that turns out accurate shows the weakness was built in rather
75
+ than accidental, which is a much stronger result.
76
+
77
+ This ordering is checkable and will be checked.
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """Prove the installer keeps its three promises, and refuses when it should.
3
+
4
+ The installer's promises are the reason anybody will run it on a project that
5
+ already has work in it:
6
+
7
+ it never needs a clean working tree
8
+ it never moves or deletes a file
9
+ it never demands a document
10
+
11
+ A promise nobody has tried to break is not evidence, so each one is tried here.
12
+
13
+ Python 3, standard library only, no dependencies.
14
+ """
15
+ import json
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+
22
+ HERE = os.path.dirname(os.path.abspath(__file__))
23
+ INSTALL = os.path.join(HERE, "install")
24
+ results = []
25
+
26
+
27
+ def check(name, got, want, output=""):
28
+ ok = got == want
29
+ results.append(ok)
30
+ print(" [%s] %-52s expected %s, got %s"
31
+ % ("pass" if ok else "FAIL", name, want, got))
32
+ if not ok and output:
33
+ for line in output.strip().split("\n")[:8]:
34
+ print(" %s" % line)
35
+
36
+
37
+ # Every test runs the real installer, and the real installer writes a
38
+ # fingerprint record outside the project. Without this the suite wrote
39
+ # twenty-one records into the developer's own home directory, one per test.
40
+ STATE = tempfile.mkdtemp(prefix="fw-test-state-")
41
+
42
+
43
+ def install(project, *args):
44
+ env = dict(os.environ)
45
+ env["FORMWORK_STATE_DIR"] = os.path.join(STATE, os.path.basename(project))
46
+ p = subprocess.run([sys.executable, INSTALL] + list(args),
47
+ capture_output=True, text=True, cwd=project, env=env)
48
+ return p.returncode, p.stdout + p.stderr
49
+
50
+
51
+ def project(**dirs):
52
+ """A throwaway project. dirs maps a path to its contents, or None for a dir."""
53
+ d = tempfile.mkdtemp(prefix="fw-install-")
54
+ for rel, content in dirs.items():
55
+ full = os.path.join(d, rel)
56
+ if content is None:
57
+ os.makedirs(full, exist_ok=True)
58
+ else:
59
+ os.makedirs(os.path.dirname(full), exist_ok=True)
60
+ open(full, "w", encoding="utf-8").write(content)
61
+ return d
62
+
63
+
64
+ def tree(root):
65
+ out = {}
66
+ for dirpath, dirnames, filenames in os.walk(root):
67
+ for fn in filenames:
68
+ full = os.path.join(dirpath, fn)
69
+ out[os.path.relpath(full, root)] = open(full, encoding="utf-8").read()
70
+ return out
71
+
72
+
73
+ print("A fresh project")
74
+ p = project(**{".claude/": None})
75
+ code, out = install(p)
76
+ check("wires a project that has .claude/", code, 0, out)
77
+ check("wrote .formwork.toml", os.path.exists(os.path.join(p, ".formwork.toml")), True)
78
+ settings = os.path.join(p, ".claude", "settings.json")
79
+ check("wrote .claude/settings.json", os.path.exists(settings), True)
80
+ check("and it calls the guard",
81
+ "formwork/guard/git-boundary" in open(settings).read(), True)
82
+
83
+ print("Running it twice")
84
+ before = tree(p)
85
+ code, out = install(p)
86
+ check("second run is happy", code, 0, out)
87
+ check("second run changed nothing", tree(p) == before, True, out)
88
+
89
+ print("It does not need a clean working tree")
90
+ p = project(**{".claude/": None, "src/half-finished.py": "x = 1 # mid-edit\n",
91
+ ".git/HEAD": "ref: refs/heads/main\n"})
92
+ code, out = install(p)
93
+ check("installs with uncommitted work sitting there", code, 0, out)
94
+ check("did not touch the work in progress",
95
+ open(os.path.join(p, "src", "half-finished.py")).read(),
96
+ "x = 1 # mid-edit\n")
97
+
98
+ print("It adds, and never removes")
99
+ p = project(**{".claude/settings.json": json.dumps({
100
+ "hooks": {"PreToolUse": [{"matcher": "Bash", "hooks": [
101
+ {"type": "command", "command": "my-own-hook"}]}]},
102
+ "somethingElse": {"kept": True}}, indent=2)})
103
+ code, out = install(p)
104
+ check("merges into settings that already exist", code, 0, out)
105
+ after = json.load(open(os.path.join(p, ".claude", "settings.json")))
106
+ commands = [h.get("command")
107
+ for entries in after["hooks"].values()
108
+ for e in entries for h in e.get("hooks", [])]
109
+ check("kept the hook that was already there", "my-own-hook" in commands, True)
110
+ check("kept settings it knows nothing about",
111
+ after.get("somethingElse"), {"kept": True})
112
+ check("added the guard", any("git-boundary" in c for c in commands), True)
113
+ check("kept a copy of the original",
114
+ os.path.exists(os.path.join(p, ".claude", "settings.json.before-formwork")),
115
+ True)
116
+
117
+ print("It demands no document")
118
+ p = project(**{".claude/": None})
119
+ code, out = install(p)
120
+ files = set(tree(p))
121
+ check("wrote only the two things it said it would",
122
+ files, {".formwork.toml", ".claude/settings.json"}, out)
123
+
124
+ print("When it cannot tell")
125
+ p = project(**{"src/main.py": "pass\n"})
126
+ code, out = install(p)
127
+ check("no runtime anywhere: refuses rather than guessing", code, 2, out)
128
+ check("and says how to tell it", "--runtime" in out, True, out)
129
+
130
+ p = project(**{".claude/": None, ".cursor/": None})
131
+ code, out = install(p)
132
+ check("two runtimes: refuses rather than picking one", code, 2, out)
133
+
134
+ print("An untested runtime is not reported as finished")
135
+ p = project(**{".codex/": None})
136
+ code, out = install(p)
137
+ check("exits 1, not 0", code, 1, out)
138
+ check("and says the word untested", "untested" in out, True, out)
139
+
140
+ print("It will not overwrite a configuration that disagrees")
141
+ p = project(**{".claude/": None,
142
+ ".formwork.toml": '[bindings]\nruntime = "cursor"\n'})
143
+ code, out = install(p)
144
+ check("leaves the existing .formwork.toml alone", code, 1, out)
145
+ check("and it still says cursor",
146
+ 'runtime = "cursor"' in open(os.path.join(p, ".formwork.toml")).read(),
147
+ True)
148
+
149
+ print("Broken JSON is left alone rather than replaced")
150
+ p = project(**{".claude/settings.json": "{ this is not json"})
151
+ code, out = install(p)
152
+ check("refuses to clobber it", code, 1, out)
153
+ check("and it is untouched",
154
+ open(os.path.join(p, ".claude", "settings.json")).read(),
155
+ "{ this is not json")
156
+
157
+ print("--dry-run")
158
+ p = project(**{".claude/": None})
159
+ code, out = install(p, "--dry-run")
160
+ check("says what it would do", code, 0, out)
161
+ check("and wrote nothing at all", tree(p), {}, out)
162
+
163
+ print("")
164
+ print("%d passed, %d failed" % (results.count(True), results.count(False)))
165
+ sys.exit(1 if False in results else 0)