module-spec 0.0.0-stage → 0.2.0

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 (79) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +132 -2
  3. package/bin/module-spec.js +193 -0
  4. package/lib/affected.js +54 -0
  5. package/lib/baseline.js +34 -0
  6. package/lib/branch.js +26 -0
  7. package/lib/check.js +53 -0
  8. package/lib/checks/api.js +36 -0
  9. package/lib/checks/clones.js +33 -0
  10. package/lib/checks/imports.js +101 -0
  11. package/lib/checks/manifests.js +52 -0
  12. package/lib/checks/ports.js +32 -0
  13. package/lib/checks/tables.js +37 -0
  14. package/lib/config.js +56 -0
  15. package/lib/files.js +56 -0
  16. package/lib/languages/go/go.mod +5 -0
  17. package/lib/languages/go/index.js +37 -0
  18. package/lib/languages/go/mutate.js +56 -0
  19. package/lib/languages/go/read.js +40 -0
  20. package/lib/languages/go/read_project.go +433 -0
  21. package/lib/languages/go/scaffold.js +28 -0
  22. package/lib/languages/go/templates/module/internal/shell/create.go +14 -0
  23. package/lib/languages/go/templates/module/{{pkg}}.go +15 -0
  24. package/lib/languages/go/templates/module/{{pkg}}_test.go +13 -0
  25. package/lib/languages/go/templates/port/internal/adapters/in_memory_{{portSnake}}.go +10 -0
  26. package/lib/languages/go/templates/port/internal/ports/{{portSnake}}.go +5 -0
  27. package/lib/languages/go/templates/port/{{portSnake}}_contract_test.go +20 -0
  28. package/lib/languages/index.js +66 -0
  29. package/lib/languages/python/environment.js +15 -0
  30. package/lib/languages/python/index.js +35 -0
  31. package/lib/languages/python/mutate.js +72 -0
  32. package/lib/languages/python/package.js +16 -0
  33. package/lib/languages/python/read.js +81 -0
  34. package/lib/languages/python/read_project.py +145 -0
  35. package/lib/languages/python/scaffold.js +19 -0
  36. package/lib/languages/python/templates/module/__init__.py +3 -0
  37. package/lib/languages/python/templates/module/shell/__init__.py +0 -0
  38. package/lib/languages/python/templates/module/shell/create.py +16 -0
  39. package/lib/languages/python/templates/module/tests/__init__.py +0 -0
  40. package/lib/languages/python/templates/module/tests/test_{{snake}}.py +5 -0
  41. package/lib/languages/python/templates/port/adapters/in_memory_{{portSnake}}.py +9 -0
  42. package/lib/languages/python/templates/port/ports/{{portSnake}}.py +5 -0
  43. package/lib/languages/python/templates/port/tests/test_{{portSnake}}_contract.py +11 -0
  44. package/lib/languages/typescript/index.js +37 -0
  45. package/lib/languages/typescript/mutate.js +95 -0
  46. package/lib/languages/typescript/project.js +24 -0
  47. package/lib/languages/typescript/read.js +220 -0
  48. package/lib/languages/typescript/scaffold.js +60 -0
  49. package/lib/languages/typescript/templates/module/index.ts +2 -0
  50. package/lib/languages/typescript/templates/module/shell/create{{Name}}.ts +8 -0
  51. package/lib/languages/typescript/templates/module-test.node.ts +7 -0
  52. package/lib/languages/typescript/templates/module-test.vitest.ts +6 -0
  53. package/lib/languages/typescript/templates/port/adapters/inMemory{{Port}}.ts +5 -0
  54. package/lib/languages/typescript/templates/port/ports/{{Port}}.ts +1 -0
  55. package/lib/languages/typescript/templates/port-test.node.ts +12 -0
  56. package/lib/languages/typescript/templates/port-test.vitest.ts +12 -0
  57. package/lib/languages/typescript/templates/view-module/__tests__/{{name}}.test.tsx +9 -0
  58. package/lib/languages/typescript/templates/view-module/core/model.ts +25 -0
  59. package/lib/languages/typescript/templates/view-module/index.ts +4 -0
  60. package/lib/languages/typescript/templates/view-module/shell/create{{Name}}Model.ts +20 -0
  61. package/lib/languages/typescript/templates/view-module/view/{{Name}}Page.tsx +14 -0
  62. package/lib/modules.js +104 -0
  63. package/lib/mutation.js +124 -0
  64. package/lib/place.js +25 -0
  65. package/lib/report.js +42 -0
  66. package/lib/rules.js +145 -0
  67. package/lib/scaffold.js +57 -0
  68. package/lib/scans.js +62 -0
  69. package/lib/templating.js +28 -0
  70. package/lib/tools.js +17 -0
  71. package/manifest.schema.json +35 -0
  72. package/package.json +38 -4
  73. package/spec/adopting.md +70 -0
  74. package/spec/backend.md +176 -0
  75. package/spec/common.md +163 -0
  76. package/spec/frontend.md +181 -0
  77. package/spec/roles.md +93 -0
  78. package/spec/tests.md +146 -0
  79. package/spec/workflow.md +50 -0
package/spec/tests.md ADDED
@@ -0,0 +1,146 @@
1
+ # Tests
2
+
3
+ A module's shape decides its tests. Tests prove behaviour, never structure,
4
+ so an agent can rebuild a module's inside and every test still holds (Ian
5
+ Cooper, "TDD, Where Did It All Go Wrong", 2017).
6
+
7
+ ```mermaid
8
+ ---
9
+ config:
10
+ look: handDrawn
11
+ ---
12
+ flowchart LR
13
+ e2e["🌐 end-to-end<br/>a few, in the app"]:::test
14
+ behave["🧪 behaviour tests<br/>through the door,<br/>fakes passed in"]:::test
15
+ contract["📜 contract test<br/>one per port"]:::test
16
+ app["🚀 app"]:::part
17
+ subgraph mod["📦 a module"]
18
+ door["🚪 door"]:::part
19
+ shell["🔁 shell"]:::part
20
+ core["🧠 core"]:::part
21
+ adapter["🔧 real adapter"]:::part
22
+ fake["🧸 fake"]:::part
23
+ end
24
+ e2e --> app --> door
25
+ behave --> door
26
+ door --> shell --> core
27
+ shell --> adapter
28
+ contract --> adapter
29
+ contract --> fake
30
+ classDef test fill:#1b4332,stroke:#52b788,color:#d8f3dc
31
+ classDef part fill:#2b2b2b,stroke:#9ca3af,color:#e5e7eb
32
+ ```
33
+
34
+ Green: the three kinds of test. Core and shell have no tests of their own:
35
+ behaviour tests reach them through the door. Each adapter is covered by its
36
+ port's contract test. Tests that span features live in an app, because
37
+ [R1](roles.md#r1-a-feature-imports-only-shared-modules) holds for test files
38
+ too. A frontend module's behaviour tests draw its page through the door, see
39
+ [frontend.md](frontend.md#tests).
40
+
41
+ ## Good design deletes tests
42
+
43
+ Move each fact as high up this ladder as it goes. Each step up deletes the
44
+ tests below it.
45
+
46
+ | Level | Example | Tests needed |
47
+ |---|---|---|
48
+ | Impossible, by a type | an `InvoiceId` cannot be a raw string | none |
49
+ | Blocked, by a check | a core cannot import the database | none |
50
+ | Tested | the fee for 3 hours is 300 | one |
51
+ | Reviewed | a name reads well | none, a person decides |
52
+
53
+ The top rung: make illegal states unrepresentable (Yaron Minsky, "Effective
54
+ ML", 2011), parse, don't validate ([B7](backend.md#b7-parse-inputs-at-the-edge)),
55
+ and design errors out of existence (John Ousterhout, *A Philosophy of Software
56
+ Design*). Code that is hard to test is a design signal, not a testing problem
57
+ (Steve Freeman and Nat Pryce, *Growing Object-Oriented Software, Guided by
58
+ Tests*, 2009).
59
+
60
+ ## T1. Tests reach a module only through its door
61
+
62
+ A module's tests import its door, never the files behind it. The one
63
+ exception is a port's contract test ([T3](#t3-every-port-has-one-contract-test)),
64
+ which imports the port and its adapters.
65
+
66
+ **Why:** a test that imports an inside file breaks when the inside is
67
+ rebuilt, even when the behaviour is unchanged. That false alarm teaches agents
68
+ to edit tests until they pass.
69
+ **Check:** the import checker. In Go, also the package clause: a test in
70
+ `package billing` sees the inside, so tests use `package billing_test`.
71
+
72
+ ## T2. Tests pass fakes in, never patch
73
+
74
+ A test hands the module its fakes through `createX(deps)`. It never patches:
75
+ no `vi.mock`, `jest.mock`, `spyOn`, `mock.method`, `unittest.mock.patch`,
76
+ `monkeypatch`, `mocker`, or a patching library such as Go's `gomonkey`.
77
+
78
+ **Why:** a patch swaps what a module imports behind its back, so the test
79
+ knows the module's inside. Each patch also marks a seam that is missing: a
80
+ dependency the module should have taken as a port.
81
+ **Check:** a scan of every test file for the patching calls of its language.
82
+
83
+ ## T3. Every port has one contract test
84
+
85
+ A contract test is one suite of tests, written once against the port, that
86
+ runs against every adapter of that port: the in-memory fake and the real one
87
+ (J. B. Rainsberger, "Integrated Tests Are a Scam", 2009).
88
+
89
+ ```ts
90
+ // billing/__tests__/InvoiceStore.contract.test.ts
91
+ export function invoiceStoreContract(make: () => Promise<InvoiceStore>): void {
92
+ test("finds the invoice it saved", async () => {
93
+ const store = await make()
94
+ await store.save({ id: "inv-1", cents: 300 })
95
+ deepStrictEqual(await store.find("inv-1"), { id: "inv-1", cents: 300 })
96
+ })
97
+ test("finds nothing for an unknown id", async () => {
98
+ const store = await make()
99
+ deepStrictEqual(await store.find("inv-404"), null)
100
+ })
101
+ }
102
+
103
+ describe("in-memory fake", () => invoiceStoreContract(async () => inMemoryInvoiceStore()))
104
+ describe("Postgres", () => invoiceStoreContract(async () => postgresInvoiceStore(await emptyTestDatabase())))
105
+ ```
106
+
107
+ **Why:** every behaviour test runs against the fake. If the fake drifts from
108
+ the real database, those tests pass against a lie. The contract test keeps the
109
+ two the same, so behaviour tests can stay fast. The real run needs the real
110
+ service, so it runs where one exists, such as CI with a database container.
111
+ **Check:** every file in `ports/` has a contract test named after it, such as
112
+ `InvoiceStore.contract.test.ts`, `test_invoice_store_contract.py` or
113
+ `invoice_store_contract_test.go`. An adapter implements a port by importing
114
+ it, and the contract test must import every adapter that does, so the real one
115
+ cannot be left out. `new-port` writes the suite with the fake already in it.
116
+
117
+ ## Mutation testing
118
+
119
+ A tool changes a module's code in one small way at a time, such as `>` to
120
+ `>=`, and runs the module's tests against each change, a mutant (Richard
121
+ DeMillo, Richard Lipton and Frederick Sayward, "Hints on Test Data
122
+ Selection", 1978). Some test must fail for each one. A mutant every test
123
+ passes with lives.
124
+
125
+ **Why:** T1 and T2 cannot see a test that pins nothing, such as
126
+ `expect(feeFor(3)).toBe(feeFor(3))`. It runs the code and passes, whatever the
127
+ code does. Mutation testing catches it, because every mutant lives.
128
+
129
+ - **What is mutated:** `core/`, `shell/` and the door. Not `ports/`, which
130
+ hold only types, not `adapters/`, whose contract tests need the real
131
+ service, and not `view/`, which holds no rules
132
+ ([F3](frontend.md#f3-a-view-holds-no-rules)).
133
+ - **Which tests count:** only the module's own, and not its adapters' tests
134
+ or contract tests. Those prove an adapter against the outside and are slow.
135
+ The core they reach is proven through the door. A mutant that only another
136
+ module's test, or an adapter's test, catches still lives.
137
+ - **Zero live mutants:** `module-spec mutate` fails on any live mutant, and
138
+ names it, even on a module's first run.
139
+ - **When no test can tell:** some mutants behave exactly like the code, such
140
+ as a change to a log message. `module-spec mutate --accept` lists them in the
141
+ module's `mutation-report.txt`, so the diff shows a reviewer each one. A
142
+ module with no accepted mutant has no report.
143
+ - **Cost:** a run takes seconds to minutes for one module, so it runs only on
144
+ the modules a change touched ([W1](workflow.md#w1-one-module-per-chat)).
145
+ Their dependents run their tests, but not mutation testing: their code and
146
+ tests did not change, so neither did their live mutants.
@@ -0,0 +1,50 @@
1
+ # Working on modules in parallel
2
+
3
+ How agents work on several modules at once without breaking each other.
4
+
5
+ ## W1. One module per chat
6
+
7
+ Each chat works in its own git worktree, on one module. Worktrees stop two
8
+ chats from editing the same lines. They do not stop two chats from changing
9
+ the same decision; the rules in [common.md](common.md) do that.
10
+
11
+ **Check:** `check` fails when a branch changes more than one module besides
12
+ apps. An app only wires, so a feature and the app that wires it may change
13
+ together. Work that spans modules lands one module at a time
14
+ ([W2](#w2-a-door-change-lands-first-alone)).
15
+
16
+ A chat tests what its change can break, not the whole project.
17
+ `module-spec affected` lists the module it changed and every module that
18
+ depends on it, directly or through others. The chat runs those modules' tests.
19
+ Then `module-spec mutate` tests the tests of the modules it changed
20
+ ([tests.md](tests.md#mutation-testing)). A
21
+ change outside every module, such as a new package version, is listed apart,
22
+ because only the full suite covers it. [W3](#w3-test-the-merged-result) still
23
+ runs everything on the merged result.
24
+
25
+ ## W2. A door change lands first, alone
26
+
27
+ When modules need a new or changed door, that change merges as its own small
28
+ pull request before the work that uses it. Each chat then builds against a
29
+ door that is already on `main` (parallel change, Danilo Sato, "ParallelChange",
30
+ martinfowler.com).
31
+
32
+ A change that breaks callers lands in three steps, each one module and so
33
+ within [W1](#w1-one-module-per-chat):
34
+
35
+ 1. **Expand:** the door exports the new shape beside the old one.
36
+ 2. **Migrate:** each calling module moves to the new shape on its own branch.
37
+ 3. **Contract:** once no caller uses the old shape, the door drops it, and
38
+ `module-spec api` shows the line go.
39
+
40
+ Before a project has any `module.yaml`, the branch rules do not run, so doors
41
+ can move with their callers in one branch.
42
+
43
+ ## W3. Test the merged result
44
+
45
+ Two branches can each pass CI, merge with no conflict, and still break `main`,
46
+ because both changed one decision (a semantic conflict). Two habits catch most
47
+ of these:
48
+
49
+ - CI runs on each branch rebased onto the current `main` before it merges.
50
+ - Each user journey has one end-to-end test.