@jterrazz/test 15.3.0 → 16.1.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 (222) hide show
  1. package/README.md +120 -427
  2. package/dist/appium.adapter.js +71 -15
  3. package/dist/artifacts.js +25 -1735
  4. package/dist/browser/index.js +304 -233
  5. package/dist/checker.d.ts +1 -1
  6. package/dist/checker.js +1103 -97
  7. package/dist/fixtures.js +110 -0
  8. package/dist/index.d.ts +956 -971
  9. package/dist/index.js +2159 -2281
  10. package/dist/literate.js +1747 -0
  11. package/dist/match.js +625 -84
  12. package/dist/oxlint.cjs +3585 -1480
  13. package/dist/oxlint.d.cts +60 -48
  14. package/dist/oxlint.d.ts +60 -48
  15. package/dist/oxlint.js +3585 -1480
  16. package/dist/playwright.adapter.js +51 -8
  17. package/dist/{intercept.js → server.js} +2 -2
  18. package/dist/stream.js +6 -6
  19. package/dist/{ambiguity.js → substring-warning.js} +106 -7
  20. package/dist/vitest.d.ts +67 -48
  21. package/dist/vitest.js +229 -93
  22. package/docs/01-architecture.md +95 -0
  23. package/docs/02-developing.md +351 -0
  24. package/docs/03-testing.md +495 -0
  25. package/docs/04-operating.md +101 -0
  26. package/docs/05-module-tests.md +152 -0
  27. package/docs/06-integration.md +180 -0
  28. package/docs/07-component.md +246 -0
  29. package/docs/08-website.md +404 -0
  30. package/docs/09-mobile.md +270 -0
  31. package/docs/10-api.md +264 -0
  32. package/docs/11-jobs.md +157 -0
  33. package/docs/12-cli.md +677 -0
  34. package/docs/13-elements.md +242 -0
  35. package/docs/14-assertions.md +314 -0
  36. package/docs/15-tokens.md +253 -0
  37. package/docs/16-contracts.md +438 -0
  38. package/docs/17-services.md +188 -0
  39. package/docs/18-conventions.md +305 -0
  40. package/docs/19-linting.md +510 -0
  41. package/docs/README.md +36 -0
  42. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  43. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  44. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  45. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  46. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  47. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  48. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  49. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  50. package/docs/decisions/_template.md +22 -0
  51. package/docs/reference/classes/Matcher.md +120 -0
  52. package/docs/reference/classes/ProcessHandle.md +368 -0
  53. package/docs/reference/functions/defineContract.md +39 -0
  54. package/docs/reference/functions/defineContracts.md +36 -0
  55. package/docs/reference/functions/disabled.md +28 -0
  56. package/docs/reference/functions/enabled.md +20 -0
  57. package/docs/reference/functions/focused.md +26 -0
  58. package/docs/reference/functions/mockOf.md +79 -0
  59. package/docs/reference/functions/postgres.md +31 -0
  60. package/docs/reference/functions/process.md +33 -0
  61. package/docs/reference/functions/redis.md +28 -0
  62. package/docs/reference/functions/required.md +41 -0
  63. package/docs/reference/functions/selected.md +26 -0
  64. package/docs/reference/functions/sqlite.md +33 -0
  65. package/docs/reference/functions/testId.md +20 -0
  66. package/docs/reference/functions/text.md +40 -0
  67. package/docs/reference/functions/valued.md +28 -0
  68. package/docs/reference/functions/waitUntil.md +46 -0
  69. package/docs/reference/functions/within.md +29 -0
  70. package/docs/reference/index.md +196 -0
  71. package/docs/reference/interfaces/BaseResult.md +87 -0
  72. package/docs/reference/interfaces/CallResult.md +160 -0
  73. package/docs/reference/interfaces/CaptureScope.md +82 -0
  74. package/docs/reference/interfaces/CliResult.md +253 -0
  75. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  76. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  77. package/docs/reference/interfaces/FetchResult.md +180 -0
  78. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  79. package/docs/reference/interfaces/HttpResult.md +126 -0
  80. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  81. package/docs/reference/interfaces/PageResult.md +356 -0
  82. package/docs/reference/interfaces/RenderResult.md +72 -0
  83. package/docs/reference/interfaces/RenderedText.md +110 -0
  84. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  85. package/docs/reference/interfaces/ScreenResult.md +128 -0
  86. package/docs/reference/interfaces/TableAccessor.md +26 -0
  87. package/docs/reference/interfaces/TextAccessor.md +96 -0
  88. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  89. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  90. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  91. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  92. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  93. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  94. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  95. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  96. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  97. package/docs/reference/type-aliases/CliEnv.md +11 -0
  98. package/docs/reference/type-aliases/CliHandle.md +80 -0
  99. package/docs/reference/type-aliases/CliInput.md +36 -0
  100. package/docs/reference/type-aliases/CliOutput.md +46 -0
  101. package/docs/reference/type-aliases/CliPort.md +58 -0
  102. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  103. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  104. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  105. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  106. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  107. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  108. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  109. package/docs/reference/type-aliases/Contract.md +68 -0
  110. package/docs/reference/type-aliases/ContractInput.md +13 -0
  111. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  112. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  113. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  114. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  115. package/docs/reference/type-aliases/Contracts.md +48 -0
  116. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  117. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  118. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  119. package/docs/reference/type-aliases/DevicePort.md +51 -0
  120. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  121. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  122. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  123. package/docs/reference/type-aliases/DomMount.md +21 -0
  124. package/docs/reference/type-aliases/ElementKind.md +27 -0
  125. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  126. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  127. package/docs/reference/type-aliases/ElementRef.md +128 -0
  128. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  129. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  130. package/docs/reference/type-aliases/HonoApp.md +31 -0
  131. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  132. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  133. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  134. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  135. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  136. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  137. package/docs/reference/type-aliases/Intercept.md +19 -0
  138. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  139. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  140. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  141. package/docs/reference/type-aliases/JobHandle.md +34 -0
  142. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  143. package/docs/reference/type-aliases/JobsResult.md +16 -0
  144. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  145. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  146. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  147. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  148. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  149. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  150. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  151. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  152. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  153. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  154. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  155. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  156. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  157. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  158. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  159. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  160. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  161. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  162. package/docs/reference/type-aliases/MockPort.md +10 -0
  163. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  164. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  165. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  166. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  167. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  168. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  169. package/docs/reference/type-aliases/ServerPort.md +36 -0
  170. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  171. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  172. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  173. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  174. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  175. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  176. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  177. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  178. package/docs/reference/type-aliases/SpecKind.md +8 -0
  179. package/docs/reference/type-aliases/SpecRun.md +108 -0
  180. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  181. package/docs/reference/type-aliases/SpecStream.md +32 -0
  182. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  183. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  184. package/docs/reference/type-aliases/SseEvent.md +58 -0
  185. package/docs/reference/type-aliases/StreamBody.md +61 -0
  186. package/docs/reference/type-aliases/TextFilter.md +22 -0
  187. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  188. package/docs/reference/type-aliases/Visitor.md +215 -0
  189. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  190. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  191. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  192. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  193. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  194. package/docs/reference/variables/anthropic.md +19 -0
  195. package/docs/reference/variables/banner.md +10 -0
  196. package/docs/reference/variables/button.md +10 -0
  197. package/docs/reference/variables/clock.md +16 -0
  198. package/docs/reference/variables/complementary.md +10 -0
  199. package/docs/reference/variables/component.md +10 -0
  200. package/docs/reference/variables/content.md +10 -0
  201. package/docs/reference/variables/contentinfo.md +10 -0
  202. package/docs/reference/variables/dialog.md +10 -0
  203. package/docs/reference/variables/field.md +10 -0
  204. package/docs/reference/variables/form.md +10 -0
  205. package/docs/reference/variables/heading.md +10 -0
  206. package/docs/reference/variables/http.md +40 -0
  207. package/docs/reference/variables/intercept.md +11 -0
  208. package/docs/reference/variables/link.md +10 -0
  209. package/docs/reference/variables/listitem.md +10 -0
  210. package/docs/reference/variables/main.md +10 -0
  211. package/docs/reference/variables/match.md +8 -0
  212. package/docs/reference/variables/navigation.md +10 -0
  213. package/docs/reference/variables/openai.md +21 -0
  214. package/docs/reference/variables/option.md +17 -0
  215. package/docs/reference/variables/region.md +10 -0
  216. package/docs/reference/variables/row.md +10 -0
  217. package/docs/reference/variables/search.md +10 -0
  218. package/docs/reference/variables/specification.md +8 -0
  219. package/docs/reference/variables/status.md +10 -0
  220. package/docs/reference/variables/table.md +10 -0
  221. package/package.json +32 -11
  222. package/dist/spec-document.js +0 -651
package/dist/vitest.js CHANGED
@@ -1,11 +1,12 @@
1
- import { c as GROUND_EXPECTED } from "./match.js";
2
- import { a as specDescription, r as VITEST_ARTIFACTS_DIR, t as COVERAGE_DIR } from "./artifacts.js";
1
+ import { i as GROUND_EXPECTED } from "./fixtures.js";
2
+ import { i as SCREENSHOTS_DIR, n as COVERAGE_DIR, o as VITEST_ARTIFACTS_DIR, r as REPORTER_OUTPUT_FILES, t as ATTACHMENTS_DIR } from "./artifacts.js";
3
+ import { n as specDescription } from "./literate.js";
3
4
  import { createRequire } from "node:module";
4
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { basename, dirname, isAbsolute, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { configDefaults, mergeConfig } from "vitest/config";
8
- //#region src/vitest/literate-plugin.ts
9
+ //#region src/runner/literate-plugin.ts
9
10
  /**
10
11
  * The `literate()` vite plugin — door one of the spec-document format.
11
12
  *
@@ -70,7 +71,7 @@ function literate(options) {
70
71
  };
71
72
  }
72
73
  //#endregion
73
- //#region src/vitest/preset.ts
74
+ //#region src/runner/preset.ts
74
75
  /**
75
76
  * `defineSpecConfig()` — the shared vitest config every repository of the
76
77
  * ecosystem starts from.
@@ -116,6 +117,27 @@ const HOOK_TIMEOUT_MS = 3e4;
116
117
  */
117
118
  const EXCLUDE = [...configDefaults.exclude, "**/_fixtures/**"];
118
119
  /**
120
+ * What a test may never carry into the next one, and the one policy the
121
+ * framework states rather than lints.
122
+ *
123
+ * `retry: 0` is the policy: a flaky test is fixed or deleted, never re-rolled
124
+ * until it passes. A retry turns a real defect into a slow one and hides the
125
+ * day it becomes permanent.
126
+ *
127
+ * The three restores are the other half of the same idea. `vi.spyOn`,
128
+ * `vi.stubGlobal` and `vi.stubEnv` each own a teardown, and a test that forgets
129
+ * one leaks into whatever runs next — a failure that appears in a FILE that did
130
+ * nothing wrong and disappears when that file is run alone. Turning them on
131
+ * here is what lets `clock.at()` and `vi.stubEnv()` be written without an
132
+ * `afterEach`, which is the shape rule J6w asks for.
133
+ */
134
+ const HYGIENE = {
135
+ restoreMocks: true,
136
+ retry: 0,
137
+ unstubEnvs: true,
138
+ unstubGlobals: true
139
+ };
140
+ /**
119
141
  * Defaults a vitest PROJECT accepts. `coverage` and `reporters` are root-only
120
142
  * options (vitest omits them from a project's config), so they are absent here
121
143
  * on purpose — setting them would be silently dropped.
@@ -130,24 +152,122 @@ function projectDefaults() {
130
152
  return {
131
153
  cacheDir: VITEST_ARTIFACTS_DIR,
132
154
  test: {
155
+ attachmentsDir: ATTACHMENTS_DIR,
133
156
  exclude: EXCLUDE,
134
157
  hookTimeout: HOOK_TIMEOUT_MS,
158
+ ...HYGIENE,
135
159
  testTimeout: TEST_TIMEOUT_MS
136
160
  }
137
161
  };
138
162
  }
163
+ /**
164
+ * Coverage, when `--coverage` asks for it.
165
+ *
166
+ * `v8` because it is the runtime's own counter: no instrumentation pass, no
167
+ * second transform of every file, and the numbers do not move when the bundler
168
+ * does. The provider is an OPTIONAL peer (`@vitest/coverage-v8`), so a project
169
+ * that never asks for coverage never installs it.
170
+ *
171
+ * `json-summary` is not a taste: it is the machine-readable report the ratchet
172
+ * reads. `text` is for the human running the command, and `html` for the one
173
+ * chasing a line. All three land under `.artifacts/vitest/coverage/`, the one
174
+ * directory the toolchain's page owns.
175
+ *
176
+ * What is EXCLUDED is the shape of the answer, not a way to raise the number:
177
+ * a type-only module has nothing to execute (v8 reports it as 0/0 or, worse,
178
+ * as uncovered lines that cannot be covered), and the CLI entries are proven
179
+ * by running the binary, not by importing it.
180
+ */
181
+ const COVERAGE = {
182
+ exclude: [
183
+ "**/node_modules/**",
184
+ "specs/**",
185
+ "**/_fixtures/**",
186
+ "**/*.d.ts",
187
+ "**/*.test-d.ts",
188
+ "**/*.fixtures.ts",
189
+ "dist/**",
190
+ ".artifacts/**",
191
+ "**/*.config.ts",
192
+ "**/*.config.mts"
193
+ ],
194
+ provider: "v8",
195
+ reporter: [
196
+ "text",
197
+ "html",
198
+ "json-summary"
199
+ ],
200
+ reportsDirectory: COVERAGE_DIR
201
+ };
139
202
  /** Defaults for the root config — the project ones, plus what only a root carries. */
140
203
  function rootDefaults() {
141
204
  return {
142
205
  cacheDir: VITEST_ARTIFACTS_DIR,
143
206
  test: {
144
- coverage: { reportsDirectory: COVERAGE_DIR },
207
+ attachmentsDir: ATTACHMENTS_DIR,
208
+ coverage: COVERAGE,
145
209
  exclude: EXCLUDE,
146
210
  hookTimeout: HOOK_TIMEOUT_MS,
211
+ ...HYGIENE,
212
+ outputFile: REPORTER_OUTPUT_FILES,
147
213
  testTimeout: TEST_TIMEOUT_MS
148
214
  }
149
215
  };
150
216
  }
217
+ /** A glob list with its repeats taken out, or nothing where there is no list. */
218
+ function eachOnce(globs) {
219
+ return globs === void 0 ? void 0 : [...new Set(globs)];
220
+ }
221
+ /**
222
+ * The glob lists vite CONCATENATES rather than overrides, each glob kept once.
223
+ *
224
+ * A project built ON the defaults and then merged INTO them by
225
+ * `defineSpecConfig` carried every default twice. Glob matching is idempotent,
226
+ * so nothing collected differently — but the no-tests banner is the one place a
227
+ * consumer ever reads that list, and it read as three merges of the same three
228
+ * globs.
229
+ */
230
+ function statedOnce(test) {
231
+ const exclude = eachOnce(test.exclude);
232
+ const include = eachOnce(test.include);
233
+ return {
234
+ ...test,
235
+ ...exclude === void 0 ? {} : { exclude },
236
+ ...include === void 0 ? {} : { include }
237
+ };
238
+ }
239
+ /**
240
+ * Build a project ON the defaults — the ONE way a project is built here.
241
+ *
242
+ * @internal
243
+ */
244
+ function onProjectDefaults(stated) {
245
+ const merged = mergeConfig(projectDefaults(), stated);
246
+ return merged.test === void 0 ? merged : {
247
+ ...merged,
248
+ test: statedOnce(merged.test)
249
+ };
250
+ }
251
+ /**
252
+ * Is every project an inline object — one this preset merged its defaults into?
253
+ * A project named by a glob or handed as a promise carries a config this call
254
+ * never saw, and the root's exclusions are the only ones it gets from here.
255
+ */
256
+ function allInline(projects) {
257
+ return projects.every((project) => typeof project === "object" && project !== null && !("then" in project));
258
+ }
259
+ /**
260
+ * The root's exclusions, beside projects that already carry them.
261
+ *
262
+ * With `projects`, the root collects nothing itself: its `exclude` only travels
263
+ * INTO each project, where vitest CONCATENATES it with the project's own — so
264
+ * every preset glob reached the banner twice. The preset's own are dropped
265
+ * here, since each inline project states them in full; what the CONSUMER stated
266
+ * at the root stays, because no project holds a copy of that.
267
+ */
268
+ function rootExcludeBeside(projects, exclude) {
269
+ return allInline(projects) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
270
+ }
151
271
  /**
152
272
  * A project inherits NOTHING from the root `test` block — vitest resolves each
153
273
  * project as its own config — so the defaults are merged into every inline one.
@@ -156,24 +276,71 @@ function rootDefaults() {
156
276
  */
157
277
  function withProjectDefaults(project) {
158
278
  if (typeof project !== "object" || project === null || "then" in project) return project;
159
- return mergeConfig(projectDefaults(), project);
279
+ return onProjectDefaults(project);
160
280
  }
161
281
  function defineSpecConfig(config = {}) {
162
282
  const { literate: literateOptions, ...userConfig } = config;
163
283
  const stated = literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig;
164
284
  const merged = mergeConfig(rootDefaults(), stated);
165
- const projects = merged.test?.projects;
166
- if (!projects) return merged;
285
+ const root = merged.test === void 0 ? void 0 : statedOnce(merged.test);
286
+ if (root === void 0) return merged;
287
+ const { projects } = root;
288
+ if (projects === void 0) return {
289
+ ...merged,
290
+ test: root
291
+ };
292
+ const inline = projects.map(withProjectDefaults);
167
293
  return {
168
294
  ...merged,
169
295
  test: {
170
- ...merged.test,
171
- projects: projects.map(withProjectDefaults)
296
+ ...root,
297
+ ...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
298
+ projects: inline
172
299
  }
173
300
  };
174
301
  }
175
302
  //#endregion
176
- //#region src/integrations/vitest-browser/commands.ts
303
+ //#region src/runner/facet-project.ts
304
+ /** The globs a node facet collects, and the ones a caller stated instead. */
305
+ function facetProject(facet, options) {
306
+ return onProjectDefaults({ test: {
307
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude },
308
+ include: options.include ?? [`specs/${facet}/**/*.spec.ts`],
309
+ ...options.serial === true ? { fileParallelism: false } : {},
310
+ name: facet,
311
+ sequence: { groupOrder: 0 },
312
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
313
+ } });
314
+ }
315
+ //#endregion
316
+ //#region src/facets/api/api.project.ts
317
+ /** `api()` — the app met through HTTP. */
318
+ function api(options = {}) {
319
+ return facetProject("api", options);
320
+ }
321
+ //#endregion
322
+ //#region src/facets/cli/cli.project.ts
323
+ /** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
324
+ const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
325
+ const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
326
+ /**
327
+ * A cli project collects the facet's `.test.ts` files AND its documents: a
328
+ * `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
329
+ * so the plugin's glob has to join the include of the project that collects
330
+ * them — which is this one, never the root config.
331
+ */
332
+ function cli(options = {}) {
333
+ const { literate: literateOptions, ...common } = options;
334
+ const project = facetProject("cli", common);
335
+ if (literateOptions === false) return project;
336
+ const documents = literateOptions?.include ?? CLI_DOCUMENTS;
337
+ return mergeConfig(project, { plugins: [literate({
338
+ include: documents,
339
+ specification: literateOptions?.specification ?? CLI_SPECIFICATION
340
+ })] });
341
+ }
342
+ //#endregion
343
+ //#region src/seams/vitest-browser/commands.ts
177
344
  /** Where a golden sits: `_expected/<name>` beside the test that asked for it. */
178
345
  function goldenPath(testPath, name) {
179
346
  if (testPath === void 0) throw new Error("golden: the command carries no test path — a golden is read from a test file.");
@@ -205,7 +372,7 @@ const COMPONENT_COMMANDS = {
205
372
  goldenWrite
206
373
  };
207
374
  //#endregion
208
- //#region src/vitest/projects.ts
375
+ //#region src/facets/component/component.project.ts
209
376
  /** The pipeline keys a consumer's Vite config contributes, and nothing else. */
210
377
  const PIPELINE_KEYS = [
211
378
  "assetsInclude",
@@ -459,39 +626,6 @@ function componentGlobs(options) {
459
626
  };
460
627
  }
461
628
  /**
462
- * A module test runs beside the module it covers: no DOM, no browser, no
463
- * pipeline. `.test.tsx` is excluded because a rendered thing is a component
464
- * test — a project's dependency scan reads every file its globs match, and a
465
- * `.tsx` collected here would be optimised for a page that never opens.
466
- */
467
- function unit(options = {}) {
468
- const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
469
- return mergeConfig(projectDefaults(), { test: {
470
- exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
471
- include: options.include ?? fromRoots ?? ["**/*.test.ts"],
472
- ...options.serial === true ? { fileParallelism: false } : {},
473
- name: "unit",
474
- sequence: { groupOrder: 0 },
475
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
476
- } });
477
- }
478
- /**
479
- * A website spec meets the assembled product through a served page, in the
480
- * Chromium playwright drives. It opens a browser, so it is scheduled before the
481
- * component project and never beside it: two Chromiums must not share a slot on
482
- * a 2-vCPU runner.
483
- */
484
- function website(options = {}) {
485
- return mergeConfig(projectDefaults(), { test: {
486
- ...options.exclude === void 0 ? {} : { exclude: options.exclude },
487
- ...options.serial === true ? { fileParallelism: false } : {},
488
- include: options.include ?? ["specs/website/**/*.test.ts"],
489
- name: "website",
490
- sequence: { groupOrder: 1 },
491
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
492
- } });
493
- }
494
- /**
495
629
  * A component test renders in a real Chromium — the browser the website facet
496
630
  * already drives through the same `playwright` peer.
497
631
  *
@@ -505,23 +639,23 @@ async function component(options = {}) {
505
639
  const { playwright } = await import("@vitest/browser-playwright");
506
640
  const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
507
641
  const consumerPlugins = Array.isArray(pipeline.plugins) ? pipeline.plugins : [];
508
- const project = {
642
+ return onProjectDefaults({
509
643
  ...pipeline,
510
644
  ...jsxPipeline(pipeline),
511
645
  optimizeDeps: { include: SEAM_DEPENDENCIES.filter(resolves) },
512
646
  plugins: [mswWorkerPlugin(), ...consumerPlugins],
513
647
  test: {
514
- attachmentsDir: `${VITEST_ARTIFACTS_DIR}/attachments`,
515
648
  browser: {
516
649
  commands: COMPONENT_COMMANDS,
517
650
  enabled: true,
651
+ expect: { toMatchScreenshot: { screenshotDirectory: SCREENSHOTS_DIR } },
518
652
  headless: true,
519
653
  instances: [{ browser: "chromium" }],
520
654
  provider: playwright({ contextOptions: {
521
655
  locale: options.locale ?? "en-US",
522
656
  timezoneId: options.timezone ?? "UTC"
523
657
  } }),
524
- screenshotDirectory: `${VITEST_ARTIFACTS_DIR}/screenshots`,
658
+ screenshotDirectory: SCREENSHOTS_DIR,
525
659
  viewport: viewportOf(options)
526
660
  },
527
661
  ...componentGlobs(options),
@@ -532,42 +666,22 @@ async function component(options = {}) {
532
666
  sequence: { groupOrder: 2 },
533
667
  ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
534
668
  }
535
- };
536
- return mergeConfig(projectDefaults(), project);
537
- }
538
- /**
539
- * The node facets' canonical projects.
540
- *
541
- * Each one collects the facet's own tree, carries the preset's budgets and
542
- * artefact directory, and runs in group 0 — the browser facets follow, so two
543
- * Chromiums never share a slot on a two-vCPU runner. `{ include, exclude,
544
- * timeout, serial }` is what a repository states when its tree, its budget or
545
- * its parallelism differs;
546
- * everything else is the framework's.
547
- */
548
- /** The globs a node facet collects, and the ones a caller stated instead. */
549
- function facetProject(facet, options) {
550
- return mergeConfig(projectDefaults(), { test: {
551
- ...options.exclude === void 0 ? {} : { exclude: options.exclude },
552
- include: options.include ?? [`specs/${facet}/**/*.test.ts`],
553
- ...options.serial === true ? { fileParallelism: false } : {},
554
- name: facet,
555
- sequence: { groupOrder: 0 },
556
- ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
557
- } });
669
+ });
558
670
  }
559
- /** `api()` — the app met through HTTP. */
560
- function api(options = {}) {
561
- return facetProject("api", options);
671
+ //#endregion
672
+ //#region src/facets/integration/integration.project.ts
673
+ /** `integration()` — a module against real services, or against a golden. */
674
+ function integration(options = {}) {
675
+ return facetProject("integration", options);
562
676
  }
677
+ //#endregion
678
+ //#region src/facets/jobs/jobs.project.ts
563
679
  /** `jobs()` — what a name triggers in-process. */
564
680
  function jobs(options = {}) {
565
681
  return facetProject("jobs", options);
566
682
  }
567
- /** `integration()` — a module against real services, or against a golden. */
568
- function integration(options = {}) {
569
- return facetProject("integration", options);
570
- }
683
+ //#endregion
684
+ //#region src/facets/mobile/mobile.project.ts
571
685
  /**
572
686
  * `mobile()` — the app on a simulator. A simulator is the one thing that
573
687
  * cannot share a machine with itself, so it runs last and alone (group 3).
@@ -575,23 +689,45 @@ function integration(options = {}) {
575
689
  function mobile(options = {}) {
576
690
  return mergeConfig(facetProject("mobile", options), { test: { sequence: { groupOrder: 3 } } });
577
691
  }
578
- /** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
579
- const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
580
- const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
692
+ //#endregion
693
+ //#region src/facets/website/website.project.ts
581
694
  /**
582
- * A cli project collects the facet's `.test.ts` files AND its documents: a
583
- * `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
584
- * so the plugin's glob has to join the include of the project that collects
585
- * them — which is this one, never the root config.
695
+ * A website spec meets the assembled product through a served page, in the
696
+ * Chromium playwright drives. It opens a browser, so it is scheduled before the
697
+ * component project and never beside it: two Chromiums must not share a slot on
698
+ * a 2-vCPU runner.
586
699
  */
587
- function cli(options = {}) {
588
- const { literate: literateOptions, ...common } = options;
589
- const project = facetProject("cli", common);
590
- if (literateOptions === false) return project;
591
- return mergeConfig(project, { plugins: [literate({
592
- include: literateOptions?.include ?? CLI_DOCUMENTS,
593
- specification: literateOptions?.specification ?? CLI_SPECIFICATION
594
- })] });
700
+ function website(options = {}) {
701
+ return onProjectDefaults({ test: {
702
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude },
703
+ ...options.serial === true ? { fileParallelism: false } : {},
704
+ include: options.include ?? ["specs/website/**/*.spec.ts"],
705
+ name: "website",
706
+ sequence: { groupOrder: 1 },
707
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
708
+ } });
709
+ }
710
+ //#endregion
711
+ //#region src/runner/projects.ts
712
+ /**
713
+ * A module test runs beside the module it covers: no DOM, no browser, no
714
+ * pipeline. Its suffix is `.test.ts` and its place is OUTSIDE `specs/`, where
715
+ * the assembled product is specified in `.spec.ts` — the two exclusions say
716
+ * exactly that. `.test.tsx` is the third exclusion because a rendered thing is
717
+ * a component test: a project's dependency scan reads every file its globs
718
+ * match, and a `.tsx` collected here would be optimised for a page that never
719
+ * opens.
720
+ */
721
+ function unit(options = {}) {
722
+ const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
723
+ return onProjectDefaults({ test: {
724
+ exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
725
+ include: options.include ?? fromRoots ?? ["**/*.test.ts"],
726
+ ...options.serial === true ? { fileParallelism: false } : {},
727
+ name: "unit",
728
+ sequence: { groupOrder: 0 },
729
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
730
+ } });
595
731
  }
596
732
  //#endregion
597
733
  export { api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
@@ -0,0 +1,95 @@
1
+ # 01 — Architecture
2
+
3
+ What this package IS: one runner model behind six constructors, one chain that needs none, five source trees with declared edges, and seven channels through which its conventions are enforced — each row of the catalogue naming exactly one. The chapters that follow explain how to USE each facet; this one draws the lines they sit inside.
4
+
5
+ | The shape | Held below |
6
+ | -------------------------- | -------------------------------------------------------------------------------- |
7
+ | The runner model | [The runner model](#the-runner-model) — constructor, handle, chain, result |
8
+ | The source trees | [The five trees](#the-five-trees) — `model`, `facets`, `seams`, `runner`, `lint` |
9
+ | How a convention is held | [The seven enforcement channels](#the-seven-enforcement-channels) |
10
+ | What ships out of the tree | [What the tree publishes](#what-the-tree-publishes) |
11
+
12
+ The rules themselves — what each family says, what a reviewer must judge — are the constitution, [18 — Conventions](18-conventions.md); their normative sentences are the generated catalogue, [19 — Linting](19-linting.md).
13
+
14
+ ## The runner model
15
+
16
+ Every facet is the same four steps. A **constructor** takes options and returns a **handle**; the handle opens a **chain** of zero or more setups closed by exactly one terminal action; the action executes and resolves to a **typed result**; the result is asserted through vitest's `expect()`. Nothing else is public — there is no imperative escape hatch, because a spec that can do anything proves nothing in particular.
17
+
18
+ Six constructors exist and the list is closed (`src/facets/specification.ts`):
19
+
20
+ | Constructor | Handle destructures to | Subject under test |
21
+ | ----------------------------- | ---------------------------------- | ------------------------------------------------ |
22
+ | `specification.api()` | `{ api, cleanup, docker }` | An HTTP API, built and served in this process |
23
+ | `specification.jobs()` | `{ jobs, cleanup }` | A background pipeline, triggered by name |
24
+ | `specification.cli()` | `{ cli, cleanup, docker }` | A command binary in a fresh temp directory |
25
+ | `specification.integration()` | `{ integration, cleanup, docker }` | A module against real services, or a golden |
26
+ | `specification.website()` | `{ website, cleanup, url }` | A rendered page — fetched, or driven in chromium |
27
+ | `specification.mobile()` | `{ mobile, cleanup, udid }` | A native screen on an iOS simulator |
28
+
29
+ The asymmetry in that column is the model, not an oversight: `jobs` never spawns a container, so it is handed no `docker`; `website` and `mobile` drive a browser and a simulator rather than an orchestrated stack, so they carry neither.
30
+
31
+ **One facet has no constructor**, and the same reason explains it: a rendered component starts nothing. `component` ([07 — Component specs](07-component.md)) is a chain the package exports directly — `component.intercept(…).render(<X />)` — so there is no handle to destructure and no `cleanup` to pass to `afterAll`. What every render of a project shares (the providers, the Vite pipeline, the viewport) is the PROJECT's, stated once by `component()` in `vitest.config.ts`.
32
+
33
+ A runner is created **once per suite**, in a `*.specification.ts` file, and imported by the test files beside it. That split is what makes the container lifecycle affordable — one Postgres per suite, not one per test — and it is why `afterAll(cleanup)` belongs in the specification file and nowhere else.
34
+
35
+ ### The seam under the chain
36
+
37
+ `SpecificationBuilder` (`src/model/chain/builder.ts`) holds the chain for every facet; each facet contributes its own setups and its own terminal actions on top. The infrastructure a chain needs is reached through **ports** — `src/model/ports/` declares eight of them (`browser`, `cli`, `container`, `database`, `device`, `isolation`, `server`, `service`) — and an integration implements one. So the chain knows "a database exists"; it never knows Postgres.
38
+
39
+ Every seam is opened lazily rather than imported, and each one is an optional peer loaded by the one module that owns it: `playwright` for `.visit()`, `appium`/`webdriverio` for `.open()`, `@vitest/browser-playwright` + `vitest-browser-react` + `react` for `component.render()`, and — since 16.0 — `better-sqlite3`, `pg`, `redis` and `testcontainers` for the services a record declares. A project that tests no page, no component and no database installs none of them; `src/seams/peer.ts` owns the one message a missing peer produces, which names the facet that asked, the command, and under pnpm the `onlyBuiltDependencies` line a native binding needs.
40
+
41
+ ## The five trees
42
+
43
+ The source tree is five trees with declared, one-directional edges. The map is not the linter's to assume — `i1-layer-boundaries` ships inert — so this package declares its own as `FRAMEWORK_LAYERS` in `oxlint.config.ts`, and that declaration is the enforced statement of what follows.
44
+
45
+ | Tree | May import | Holds |
46
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `model/` | itself, the docker/process/yaml seams, the msw seam through two named lazy imports, `runner/update`, and each facet's `*.result` module | What every facet is made of: the chain and the orchestrator, the results and their accessors, the element vocabulary, the contracts and `intercept()`, the `{{token}}` engine, the goldens, the clock, the doubles, the ports |
48
+ | `facets/` | `model/`, itself, the runner's project shape, and the one seam each constructor opens | Seven folders, the SAME four files in each: `<facet>.specification.ts` (the constructor), `<facet>.chain.ts`, `<facet>.result.ts`, `<facet>.project.ts` (the vitest helper), plus what is unique to the facet |
49
+ | `seams/` | its OWN external dependency, plus `model/` and the facet vocabulary an adapter projects into | One folder per dependency: `postgres`, `redis`, `sqlite`, `testcontainers`, `docker`, `hono`, `playwright`, `vitest-browser`, `appium`, `msw`, `openai`, `anthropic`, `yaml`, `process` |
50
+ | `runner/` | `vitest`, `vite`, `@vitest/browser-playwright`, `model/`, `facets/` | The CONFIG side of the runner coupling: the preset, update mode, the `literate()` plugin, the shape a node facet's project takes, and the project index |
51
+ | `lint/` | itself, plus a short list of PURE `model/` modules | The tool-facing channel: the oxlint plugin, the conventions checker, the catalogue manifest and generator |
52
+
53
+ **Every facet folder carries the same four files, whether or not it owns all four.** The setups and terminal actions of the six node facets are ONE builder, because a second copy of "declare a contract, seed a table, run the thing" is how two facets drift — so `api.chain.ts` and its five siblings are a named re-export of that builder rather than an implementation, and `jobs.result.ts` re-exports the shape the builder states. The shape is uniform so that a reader who has opened one facet has opened all seven; the re-export says, in the folder itself, which of the four a facet does not own.
54
+
55
+ Three of those edges carry their reason in the declaration itself. `model/` reaches `seams/docker`, `seams/process` and the yaml parser because none of the three has a dependency of its own to leak, and it reaches each facet's `*.result` module because the ONE builder constructs all seven results — a value edge, stated rather than hidden. `lint/` reaches exactly the pure modules the runner also uses — the token list, the ground names, the root walk, the `<case>.spec.yaml` parser — so that the file the lint accepts is the file the runner runs, from ONE parser. And the runner is named in exactly TWO places outside a test, one per side of it: `runner/` for the config side — the preset, the project shape, the plugin — and `seams/vitest-browser/` for the page side, where the locators, the visitor, the golden commands and the page's own `vitest` primitives live. Swapping the runner would be a rewrite of those two folders and of nothing else; `model/` states what a render IS and never which runner performs it, which is why the layer map exempts that seam from the prod-import ban (F2) by name.
56
+
57
+ The table's May-import column is a reading of `FRAMEWORK_LAYERS`, never a second statement of it: the declaration in `oxlint.config.ts` is what the linter enforces and what a change edits.
58
+
59
+ There are TWO composition roots and neither names a layer. `src/index.ts` is node's: it wires the container integrations into the registry seam and re-exports the public surface. `src/browser/index.ts` is the page's, and `src/surface.ts` is what they agree on — see [What the tree publishes](#what-the-tree-publishes).
60
+
61
+ ## The seven enforcement channels
62
+
63
+ A convention this package states is held by a **channel**, and every row of the catalogue names exactly one of seven. The guiding aim is that most enforcement is programmatic, not review-borne.
64
+
65
+ - **statique** — the `jterrazz/*` oxlint plugin: one file per rule under `src/lint/rules/model/` or `src/lint/rules/<facet>/` — the tree the rule reaches, which is also the `facet` field of its manifest row — AST analysis, one diagnostic per file.
66
+ - **upstream** — a convention an oxlint vitest-plugin rule already holds, set as an OPTION from the `testing` fragment rather than duplicated as a rule of ours ([ADR-005](decisions/005-the-doubles-ladder-is-closed.md)).
67
+ - **checker** — passes of the `jterrazz-test-check` binary reading what an AST cannot: the `{{token}}` grammar of `_requests/` and `_expected/` fixtures, the `<case>.spec.yaml` document family, cross-file analyses that cross a `*.specification.ts` with its tests, and the member pass that judges a package rather than a tree.
68
+ - **runtime** — the framework refuses incorrect usage as it executes, where static analysis abstains (a non-literal argument) or cannot reach (the network, a container lifecycle).
69
+ - **type** — what the compiler refuses with no rule at all, proven by `src/type-channel.test-d.ts`.
70
+ - **meta** — what the framework's own suite holds about itself: the catalogue's freshness, its completeness, the reach of its rules — [03 — Testing](03-testing.md) owns the channel.
71
+ - **process** — the judgement no single channel settles: asset-driven grouping, golden-file discipline, retro-propagation.
72
+
73
+ The channel a rule sits on is not prose here: it is a field of `src/lint/manifest.ts`, the single source of truth from which the catalogue in [19 — Linting](19-linting.md) is generated. A rule and its normative sentence live together in the code, so the two cannot drift.
74
+
75
+ ## What the tree publishes
76
+
77
+ The bundle is built by `tsdown` into `dist/` and its shape follows how each entry is consumed. `index` and `vitest`, plus the `checker`/`catalog` CLIs, are ESM-only — vitest is ESM-only and the CLIs are invoked as `node dist/*.js`. The `oxlint` plugin ships dual, because oxlint loads it from a consumer project that may itself be CommonJS.
78
+
79
+ **There are FOUR entries and no more**: `@jterrazz/test` for everything a spec uses, `@jterrazz/test/vitest` for what `vitest.config.ts` needs, `@jterrazz/test/oxlint` for the lint plugin, `@jterrazz/test/schema` for an editor validating a `<case>.spec.yaml`. That is rule F1 rather than a convention of taste, and the exemption is derived from the manifest's own `exports` map (`src/lint/package-exports.ts`), so a subpath is exempt the moment it is published and stops being exempt the moment it is withdrawn.
80
+
81
+ **The root entry has two runtimes.** A component test renders in a real browser, and the node entry cannot load there: it wires `pg`, `better-sqlite3`, `testcontainers` and `node:fs` at import. So `exports["."]` carries a `browser` condition resolving to `dist/browser/index.js` — the same public surface, built for a page, where every node-only name (the six constructors, the services, the docker accessors, the results that walk a disk) is a stub that throws where it was called, naming the facet and saying it runs under node. The split is the RUNTIME's and never the facets': one specifier, so no spec has to remember which entry it may import.
82
+
83
+ `types` is stated once, and it is the node build's — so the two runtimes are ONE type surface and a name cannot exist on one side only. What both agree on lives in `src/surface.ts`, and the `package-exports` meta-test compares the two builds' exported names on every run.
84
+
85
+ Two committed projections leave the code and land in the corpus: the API reference under `docs/reference/` (typedoc, through `typescript docs`) and the rule catalogue spliced into [19 — Linting](19-linting.md) and `skills/jterrazz-test/references/rules.md`. A third, `schema/spec.schema.json`, is generated from the document grammar's own constants. Both `docs/` and `schema/` ship in the tarball — the reference included, since the corpus links into it — so a consumer reads the manual where the messages send them, inside the install ([04 — Operating](04-operating.md#what-leaves-the-tree)). All three are regenerated by one gesture and sync-checked — see [02 — Developing](02-developing.md).
86
+
87
+ ## Pitfalls
88
+
89
+ - **Reaching for a dependency from `model/`.** A new external package belongs in a folder of `seams/` that imports it and nothing else; `model/` importing it directly is the one boundary this package cannot afford to blur, and `i1-layer-boundaries` refuses it.
90
+ - **Adding a seventh constructor.** The six are the closed vocabulary the conventions, the linter and the documentation are all shaped around. A new SUBJECT to specify is a design decision, and it earns a record in [`decisions/`](decisions/) before it earns a constructor — as `integration` did in [ADR-004](decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md).
91
+ - **Writing a rule's normative sentence into a chapter.** It belongs in `src/lint/manifest.ts` beside the implementation; a chapter that restates it is the second copy the whole design exists to prevent.
92
+
93
+ ## Related
94
+
95
+ [02 — Developing](02-developing.md) · [03 — Testing](03-testing.md) · [18 — Conventions](18-conventions.md) · [19 — Linting](19-linting.md)