@apptoolkit/harness 1.0.0 → 1.0.2

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 (242) hide show
  1. package/README.md +20 -45
  2. package/harness.js +104 -0
  3. package/package.json +9 -23
  4. package/payload/harness/runtime/harness.mjs +104 -0
  5. package/dist/apptoolkit-harness-1.0.0.tgz +0 -0
  6. package/dist/bun/apptoolkit-harness-1.0.0.tgz +0 -0
  7. package/dist/bun/native/README.md +0 -33
  8. package/dist/bun/native/harness +0 -4
  9. package/dist/bun/native/payload/harness/runtime/harness.mjs +0 -104
  10. package/dist/bun/npm/LICENSE +0 -21
  11. package/dist/bun/npm/README.md +0 -33
  12. package/dist/bun/npm/harness.js +0 -104
  13. package/dist/bun/npm/package.json +0 -23
  14. package/dist/bun/npm/payload/HARNESS.md +0 -21
  15. package/dist/bun/npm/payload/author-style/README.md +0 -56
  16. package/dist/bun/npm/payload/author-style/biome-core.json +0 -57
  17. package/dist/bun/npm/payload/author-style/biome-react.json +0 -9
  18. package/dist/bun/npm/payload/author-style/biome-sql.json +0 -9
  19. package/dist/bun/npm/payload/author-style/bun.lock +0 -37
  20. package/dist/bun/npm/payload/author-style/check.llm.md +0 -17
  21. package/dist/bun/npm/payload/author-style/check.test.llm.md +0 -17
  22. package/dist/bun/npm/payload/author-style/check.test.ts +0 -238
  23. package/dist/bun/npm/payload/author-style/check.ts +0 -181
  24. package/dist/bun/npm/payload/author-style/member-spacing.test.ts +0 -172
  25. package/dist/bun/npm/payload/author-style/member-spacing.ts +0 -106
  26. package/dist/bun/npm/payload/author-style/package.json +0 -18
  27. package/dist/bun/npm/payload/author-style/plugins/class-member-order.grit +0 -133
  28. package/dist/bun/npm/payload/author-style/plugins/event-handler-names.grit +0 -110
  29. package/dist/bun/npm/payload/author-style/plugins/explicit-arrow-body.grit +0 -15
  30. package/dist/bun/npm/payload/author-style/plugins/explicit-source-constructs.grit +0 -10
  31. package/dist/bun/npm/payload/author-style/plugins/react-arrow-body.grit +0 -85
  32. package/dist/bun/npm/payload/author-style/plugins/sql-style.grit +0 -125
  33. package/dist/bun/npm/payload/author-style/react-module.llm.md +0 -17
  34. package/dist/bun/npm/payload/author-style/react-module.test.ts +0 -150
  35. package/dist/bun/npm/payload/author-style/react-module.ts +0 -255
  36. package/dist/bun/npm/payload/author-style/source-policy.llm.md +0 -17
  37. package/dist/bun/npm/payload/author-style/source-policy.test.ts +0 -70
  38. package/dist/bun/npm/payload/author-style/source-policy.ts +0 -215
  39. package/dist/bun/npm/payload/author-style/spacing.llm.md +0 -17
  40. package/dist/bun/npm/payload/author-style/spacing.test.ts +0 -174
  41. package/dist/bun/npm/payload/author-style/spacing.ts +0 -211
  42. package/dist/bun/npm/payload/harness/POLICY.md +0 -33
  43. package/dist/bun/npm/payload/harness/PROJECT.md +0 -26
  44. package/dist/bun/npm/payload/harness/REVIEW.md +0 -17
  45. package/dist/bun/npm/payload/harness/TEMPLATES.md +0 -20
  46. package/dist/bun/npm/payload/harness/WORKFLOWS.md +0 -65
  47. package/dist/bun/npm/payload/harness/runtime/ZOD-LICENSE +0 -21
  48. package/dist/bun/npm/payload/harness/runtime/harness.mjs +0 -104
  49. package/dist/bun/npm/payload/harness/services/README.md +0 -26
  50. package/dist/bun/npm/payload/harness/services/penpot.json +0 -7
  51. package/dist/bun/npm/payload/harness/services/plane.json +0 -9
  52. package/dist/bun/npm/payload/harness/workflows/W01.md +0 -13
  53. package/dist/bun/npm/payload/harness/workflows/W02.md +0 -13
  54. package/dist/bun/npm/payload/harness/workflows/W03.md +0 -13
  55. package/dist/bun/npm/payload/harness/workflows/W04.md +0 -13
  56. package/dist/bun/npm/payload/harness/workflows/W05.md +0 -13
  57. package/dist/bun/npm/payload/harness/workflows/W06.md +0 -13
  58. package/dist/bun/npm/payload/harness/workflows/W07.md +0 -13
  59. package/dist/bun/npm/payload/harness/workflows/W08.md +0 -13
  60. package/dist/bun/npm/payload/harness/workflows/W09.md +0 -13
  61. package/dist/bun/npm/payload/harness/workflows/W10.md +0 -13
  62. package/dist/bun/npm/payload/harness/workflows/W11.md +0 -13
  63. package/dist/bun/npm/payload/harness/workflows/W12.md +0 -13
  64. package/dist/bun/npm/payload/harness/workflows/W13.md +0 -13
  65. package/dist/bun/npm/payload/harness/workflows/W14.md +0 -13
  66. package/dist/bun/npm/payload/harness/workflows/W15.md +0 -13
  67. package/dist/bun/npm/payload/harness/workflows/W16.md +0 -13
  68. package/dist/bun/npm/payload/harness/workflows/W17.md +0 -13
  69. package/dist/bun/npm/payload/harness/workflows/W18.md +0 -13
  70. package/dist/bun/npm/payload/harness/workflows/W19.md +0 -13
  71. package/dist/bun/npm/payload/harness/workflows/W20.md +0 -13
  72. package/dist/bun/npm/payload/harness/workflows/W21.md +0 -15
  73. package/dist/bun/npm/payload/harness/workflows/W22.md +0 -13
  74. package/dist/bun/npm/payload/harness/workflows/W23.md +0 -21
  75. package/dist/bun/npm/payload/harness/workflows/W24.md +0 -13
  76. package/dist/bun/npm/payload/harness/workflows/W25.md +0 -13
  77. package/dist/bun/npm/payload/harness/workflows/W26.md +0 -13
  78. package/dist/bun/npm/payload/harness/workflows/W27.md +0 -13
  79. package/dist/bun/npm/payload/harness/workflows/W28.md +0 -13
  80. package/dist/bun/npm/payload/harness/workflows/W29.md +0 -13
  81. package/dist/bun/npm/payload/harness/workflows/W30.md +0 -13
  82. package/dist/bun/npm/payload/harness/workflows/W31.md +0 -13
  83. package/dist/bun/npm/payload/harness/workflows/W32.md +0 -13
  84. package/dist/bun/npm/payload/harness/workflows/W33.md +0 -13
  85. package/dist/bun/npm/payload/harness/workflows/W34.md +0 -13
  86. package/dist/bun/npm/payload/harness/workflows/W35.md +0 -13
  87. package/dist/bun/npm/payload/harness/workflows/W36.md +0 -13
  88. package/dist/bun/npm/payload/harness/workflows/W37.md +0 -13
  89. package/dist/npm/LICENSE +0 -21
  90. package/dist/npm/README.md +0 -30
  91. package/dist/npm/harness.js +0 -104
  92. package/dist/npm/package.json +0 -25
  93. package/dist/npm/payload/HARNESS.md +0 -21
  94. package/dist/npm/payload/author-style/README.md +0 -56
  95. package/dist/npm/payload/author-style/biome-core.json +0 -57
  96. package/dist/npm/payload/author-style/biome-react.json +0 -9
  97. package/dist/npm/payload/author-style/biome-sql.json +0 -9
  98. package/dist/npm/payload/author-style/check.llm.md +0 -17
  99. package/dist/npm/payload/author-style/check.test.llm.md +0 -17
  100. package/dist/npm/payload/author-style/check.test.ts +0 -224
  101. package/dist/npm/payload/author-style/check.ts +0 -179
  102. package/dist/npm/payload/author-style/member-spacing.test.ts +0 -172
  103. package/dist/npm/payload/author-style/member-spacing.ts +0 -106
  104. package/dist/npm/payload/author-style/package-lock.json +0 -180
  105. package/dist/npm/payload/author-style/package.json +0 -17
  106. package/dist/npm/payload/author-style/plugins/class-member-order.grit +0 -133
  107. package/dist/npm/payload/author-style/plugins/event-handler-names.grit +0 -110
  108. package/dist/npm/payload/author-style/plugins/explicit-arrow-body.grit +0 -15
  109. package/dist/npm/payload/author-style/plugins/explicit-source-constructs.grit +0 -10
  110. package/dist/npm/payload/author-style/plugins/react-arrow-body.grit +0 -85
  111. package/dist/npm/payload/author-style/plugins/sql-style.grit +0 -125
  112. package/dist/npm/payload/author-style/react-module.llm.md +0 -17
  113. package/dist/npm/payload/author-style/react-module.test.ts +0 -150
  114. package/dist/npm/payload/author-style/react-module.ts +0 -255
  115. package/dist/npm/payload/author-style/source-policy.llm.md +0 -17
  116. package/dist/npm/payload/author-style/source-policy.test.ts +0 -70
  117. package/dist/npm/payload/author-style/source-policy.ts +0 -215
  118. package/dist/npm/payload/author-style/spacing.llm.md +0 -17
  119. package/dist/npm/payload/author-style/spacing.test.ts +0 -174
  120. package/dist/npm/payload/author-style/spacing.ts +0 -211
  121. package/dist/npm/payload/harness/POLICY.md +0 -33
  122. package/dist/npm/payload/harness/PROJECT.md +0 -26
  123. package/dist/npm/payload/harness/REVIEW.md +0 -17
  124. package/dist/npm/payload/harness/TEMPLATES.md +0 -20
  125. package/dist/npm/payload/harness/WORKFLOWS.md +0 -65
  126. package/dist/npm/payload/harness/runtime/ZOD-LICENSE +0 -21
  127. package/dist/npm/payload/harness/runtime/harness.mjs +0 -104
  128. package/dist/npm/payload/harness/services/README.md +0 -26
  129. package/dist/npm/payload/harness/services/penpot.json +0 -7
  130. package/dist/npm/payload/harness/services/plane.json +0 -9
  131. package/dist/npm/payload/harness/workflows/W01.md +0 -13
  132. package/dist/npm/payload/harness/workflows/W02.md +0 -13
  133. package/dist/npm/payload/harness/workflows/W03.md +0 -13
  134. package/dist/npm/payload/harness/workflows/W04.md +0 -13
  135. package/dist/npm/payload/harness/workflows/W05.md +0 -13
  136. package/dist/npm/payload/harness/workflows/W06.md +0 -13
  137. package/dist/npm/payload/harness/workflows/W07.md +0 -13
  138. package/dist/npm/payload/harness/workflows/W08.md +0 -13
  139. package/dist/npm/payload/harness/workflows/W09.md +0 -13
  140. package/dist/npm/payload/harness/workflows/W10.md +0 -13
  141. package/dist/npm/payload/harness/workflows/W11.md +0 -13
  142. package/dist/npm/payload/harness/workflows/W12.md +0 -13
  143. package/dist/npm/payload/harness/workflows/W13.md +0 -13
  144. package/dist/npm/payload/harness/workflows/W14.md +0 -13
  145. package/dist/npm/payload/harness/workflows/W15.md +0 -13
  146. package/dist/npm/payload/harness/workflows/W16.md +0 -13
  147. package/dist/npm/payload/harness/workflows/W17.md +0 -13
  148. package/dist/npm/payload/harness/workflows/W18.md +0 -13
  149. package/dist/npm/payload/harness/workflows/W19.md +0 -13
  150. package/dist/npm/payload/harness/workflows/W20.md +0 -13
  151. package/dist/npm/payload/harness/workflows/W21.md +0 -15
  152. package/dist/npm/payload/harness/workflows/W22.md +0 -13
  153. package/dist/npm/payload/harness/workflows/W23.md +0 -21
  154. package/dist/npm/payload/harness/workflows/W24.md +0 -13
  155. package/dist/npm/payload/harness/workflows/W25.md +0 -13
  156. package/dist/npm/payload/harness/workflows/W26.md +0 -13
  157. package/dist/npm/payload/harness/workflows/W27.md +0 -13
  158. package/dist/npm/payload/harness/workflows/W28.md +0 -13
  159. package/dist/npm/payload/harness/workflows/W29.md +0 -13
  160. package/dist/npm/payload/harness/workflows/W30.md +0 -13
  161. package/dist/npm/payload/harness/workflows/W31.md +0 -13
  162. package/dist/npm/payload/harness/workflows/W32.md +0 -13
  163. package/dist/npm/payload/harness/workflows/W33.md +0 -13
  164. package/dist/npm/payload/harness/workflows/W34.md +0 -13
  165. package/dist/npm/payload/harness/workflows/W35.md +0 -13
  166. package/dist/npm/payload/harness/workflows/W36.md +0 -13
  167. package/dist/npm/payload/harness/workflows/W37.md +0 -13
  168. /package/{dist/bun/native/LICENSE → LICENSE} +0 -0
  169. /package/{dist/bun/native/payload → payload}/HARNESS.md +0 -0
  170. /package/{dist/bun/native/payload → payload}/author-style/README.md +0 -0
  171. /package/{dist/bun/native/payload → payload}/author-style/biome-core.json +0 -0
  172. /package/{dist/bun/native/payload → payload}/author-style/biome-react.json +0 -0
  173. /package/{dist/bun/native/payload → payload}/author-style/biome-sql.json +0 -0
  174. /package/{dist/bun/native/payload → payload}/author-style/bun.lock +0 -0
  175. /package/{dist/bun/native/payload → payload}/author-style/check.llm.md +0 -0
  176. /package/{dist/bun/native/payload → payload}/author-style/check.test.llm.md +0 -0
  177. /package/{dist/bun/native/payload → payload}/author-style/check.test.ts +0 -0
  178. /package/{dist/bun/native/payload → payload}/author-style/check.ts +0 -0
  179. /package/{dist/bun/native/payload → payload}/author-style/member-spacing.test.ts +0 -0
  180. /package/{dist/bun/native/payload → payload}/author-style/member-spacing.ts +0 -0
  181. /package/{dist/bun/native/payload → payload}/author-style/package.json +0 -0
  182. /package/{dist/bun/native/payload → payload}/author-style/plugins/class-member-order.grit +0 -0
  183. /package/{dist/bun/native/payload → payload}/author-style/plugins/event-handler-names.grit +0 -0
  184. /package/{dist/bun/native/payload → payload}/author-style/plugins/explicit-arrow-body.grit +0 -0
  185. /package/{dist/bun/native/payload → payload}/author-style/plugins/explicit-source-constructs.grit +0 -0
  186. /package/{dist/bun/native/payload → payload}/author-style/plugins/react-arrow-body.grit +0 -0
  187. /package/{dist/bun/native/payload → payload}/author-style/plugins/sql-style.grit +0 -0
  188. /package/{dist/bun/native/payload → payload}/author-style/react-module.llm.md +0 -0
  189. /package/{dist/bun/native/payload → payload}/author-style/react-module.test.ts +0 -0
  190. /package/{dist/bun/native/payload → payload}/author-style/react-module.ts +0 -0
  191. /package/{dist/bun/native/payload → payload}/author-style/source-policy.llm.md +0 -0
  192. /package/{dist/bun/native/payload → payload}/author-style/source-policy.test.ts +0 -0
  193. /package/{dist/bun/native/payload → payload}/author-style/source-policy.ts +0 -0
  194. /package/{dist/bun/native/payload → payload}/author-style/spacing.llm.md +0 -0
  195. /package/{dist/bun/native/payload → payload}/author-style/spacing.test.ts +0 -0
  196. /package/{dist/bun/native/payload → payload}/author-style/spacing.ts +0 -0
  197. /package/{dist/bun/native/payload → payload}/harness/POLICY.md +0 -0
  198. /package/{dist/bun/native/payload → payload}/harness/PROJECT.md +0 -0
  199. /package/{dist/bun/native/payload → payload}/harness/REVIEW.md +0 -0
  200. /package/{dist/bun/native/payload → payload}/harness/TEMPLATES.md +0 -0
  201. /package/{dist/bun/native/payload → payload}/harness/WORKFLOWS.md +0 -0
  202. /package/{dist/bun/native/payload → payload}/harness/runtime/ZOD-LICENSE +0 -0
  203. /package/{dist/bun/native/payload → payload}/harness/services/README.md +0 -0
  204. /package/{dist/bun/native/payload → payload}/harness/services/penpot.json +0 -0
  205. /package/{dist/bun/native/payload → payload}/harness/services/plane.json +0 -0
  206. /package/{dist/bun/native/payload → payload}/harness/workflows/W01.md +0 -0
  207. /package/{dist/bun/native/payload → payload}/harness/workflows/W02.md +0 -0
  208. /package/{dist/bun/native/payload → payload}/harness/workflows/W03.md +0 -0
  209. /package/{dist/bun/native/payload → payload}/harness/workflows/W04.md +0 -0
  210. /package/{dist/bun/native/payload → payload}/harness/workflows/W05.md +0 -0
  211. /package/{dist/bun/native/payload → payload}/harness/workflows/W06.md +0 -0
  212. /package/{dist/bun/native/payload → payload}/harness/workflows/W07.md +0 -0
  213. /package/{dist/bun/native/payload → payload}/harness/workflows/W08.md +0 -0
  214. /package/{dist/bun/native/payload → payload}/harness/workflows/W09.md +0 -0
  215. /package/{dist/bun/native/payload → payload}/harness/workflows/W10.md +0 -0
  216. /package/{dist/bun/native/payload → payload}/harness/workflows/W11.md +0 -0
  217. /package/{dist/bun/native/payload → payload}/harness/workflows/W12.md +0 -0
  218. /package/{dist/bun/native/payload → payload}/harness/workflows/W13.md +0 -0
  219. /package/{dist/bun/native/payload → payload}/harness/workflows/W14.md +0 -0
  220. /package/{dist/bun/native/payload → payload}/harness/workflows/W15.md +0 -0
  221. /package/{dist/bun/native/payload → payload}/harness/workflows/W16.md +0 -0
  222. /package/{dist/bun/native/payload → payload}/harness/workflows/W17.md +0 -0
  223. /package/{dist/bun/native/payload → payload}/harness/workflows/W18.md +0 -0
  224. /package/{dist/bun/native/payload → payload}/harness/workflows/W19.md +0 -0
  225. /package/{dist/bun/native/payload → payload}/harness/workflows/W20.md +0 -0
  226. /package/{dist/bun/native/payload → payload}/harness/workflows/W21.md +0 -0
  227. /package/{dist/bun/native/payload → payload}/harness/workflows/W22.md +0 -0
  228. /package/{dist/bun/native/payload → payload}/harness/workflows/W23.md +0 -0
  229. /package/{dist/bun/native/payload → payload}/harness/workflows/W24.md +0 -0
  230. /package/{dist/bun/native/payload → payload}/harness/workflows/W25.md +0 -0
  231. /package/{dist/bun/native/payload → payload}/harness/workflows/W26.md +0 -0
  232. /package/{dist/bun/native/payload → payload}/harness/workflows/W27.md +0 -0
  233. /package/{dist/bun/native/payload → payload}/harness/workflows/W28.md +0 -0
  234. /package/{dist/bun/native/payload → payload}/harness/workflows/W29.md +0 -0
  235. /package/{dist/bun/native/payload → payload}/harness/workflows/W30.md +0 -0
  236. /package/{dist/bun/native/payload → payload}/harness/workflows/W31.md +0 -0
  237. /package/{dist/bun/native/payload → payload}/harness/workflows/W32.md +0 -0
  238. /package/{dist/bun/native/payload → payload}/harness/workflows/W33.md +0 -0
  239. /package/{dist/bun/native/payload → payload}/harness/workflows/W34.md +0 -0
  240. /package/{dist/bun/native/payload → payload}/harness/workflows/W35.md +0 -0
  241. /package/{dist/bun/native/payload → payload}/harness/workflows/W36.md +0 -0
  242. /package/{dist/bun/native/payload → payload}/harness/workflows/W37.md +0 -0
@@ -1,70 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { test } from 'node:test';
3
- import { findSourcePolicyIssues } from './source-policy.ts';
4
-
5
- test('separates real comments from strings, regular expressions and JSX text', () => {
6
- const source = [
7
- '/// <reference types="node" />',
8
- "export const url = 'https://example.test';",
9
- 'export const pattern = /https?:\\/\\//;',
10
- 'export const label = <p>// literal {/* explanatory text */}</p>;',
11
- 'export const query = sql /* sql */`SELECT 1`;',
12
- 'export const number = 1; // explanatory suffix',
13
- '/** explanatory docs */',
14
- 'export const end = 2;',
15
- 'export function empty(/* parameters */) { /* body */ }',
16
- 'export const options = [/* element */];',
17
- 'export const outside = /* sql */`SELECT 1`;',
18
- '/// <reference explanatory text',
19
- ].join('\n');
20
-
21
- assert.deepEqual(
22
- findSourcePolicyIssues(source, 'example.tsx').map(({ line }) => line),
23
- [4, 6, 7, 9, 9, 10, 11, 12],
24
- );
25
- });
26
-
27
- test('checks local schema aliases together and ignores aliases for imported schemas', () => {
28
- const source = [
29
- "import { z as contract } from 'zod';",
30
- "import { shared } from './shared.ts';",
31
- 'const schema = contract.string();',
32
- 'type Input = contract.input<typeof schema>;',
33
- 'type Output = contract.output<typeof schema>;',
34
- 'const late = contract.number();',
35
- 'type Shared = contract.infer<typeof shared>;',
36
- 'type Late = contract.infer<typeof late>;',
37
- ].join('\n');
38
-
39
- assert.deepEqual(
40
- findSourcePolicyIssues(source, 'example.ts').map(({ line }) => line),
41
- [8],
42
- );
43
- });
44
-
45
- test('recognizes a default or namespace binding from a versioned Zod entry point', () => {
46
- for (const importText of ["import schema from 'zod/v4';", "import * as schema from 'zod/v3';"]) {
47
- const source = [
48
- importText,
49
- 'const profile = schema.string();',
50
- 'export const separator = 1;',
51
- 'export type Profile = schema.infer<typeof profile>;',
52
- ].join('\n');
53
-
54
- assert.match(findSourcePolicyIssues(source, 'example.ts')[0]?.message ?? '', /Place Profile alongside/);
55
- }
56
- });
57
-
58
- test('rejects chained and parenthesized double assertions without rejecting satisfies', () => {
59
- const source = [
60
- 'export const first = value as never as string;',
61
- 'export const second = (value as never) as string;',
62
- 'export const third = <string>(<never>value);',
63
- 'export const allowed = { id: 1 } as const satisfies { id: number };',
64
- ].join('\n');
65
-
66
- assert.deepEqual(
67
- findSourcePolicyIssues(source, 'example.ts').map(({ line }) => line),
68
- [1, 2, 3],
69
- );
70
- });
@@ -1,215 +0,0 @@
1
- import ts from 'typescript';
2
-
3
- export type SourceIssue = {
4
- line: number;
5
- column: number;
6
- message: string;
7
- };
8
-
9
- export function hasGeneratedHeader(source: string): boolean {
10
- const leading = ts.getLeadingCommentRanges(source, ts.getShebang(source)?.length ?? 0);
11
-
12
- return (
13
- leading?.some(({ pos, end }) => {
14
- const comment = source.slice(pos, end);
15
-
16
- return /(?:^|\r?\n)\s*(?:\/\/|\/\*+|\*)\s*(?:@generated\b|Code generated\b)/i.test(comment);
17
- }) ?? false
18
- );
19
- }
20
-
21
- function collectComments(file: ts.SourceFile): Map<number, number> {
22
- const comments = new Map<number, number>();
23
-
24
- function collect(start: number, end: number): void {
25
- comments.set(start, end);
26
- }
27
-
28
- function visit(node: ts.Node): void {
29
- if (ts.isJsxText(node)) {
30
- return;
31
- }
32
-
33
- const children = node.getChildren(file);
34
-
35
- if (children.length === 0 && node.kind >= ts.SyntaxKind.FirstToken && node.kind <= ts.SyntaxKind.LastToken) {
36
- const from = node.getFullStart();
37
-
38
- ts.forEachLeadingCommentRange(file.text, from, collect);
39
- ts.forEachTrailingCommentRange(file.text, from, collect);
40
- }
41
-
42
- for (const child of children) {
43
- visit(child);
44
- }
45
- }
46
-
47
- visit(file);
48
-
49
- return comments;
50
- }
51
-
52
- function sqlMarkers(file: ts.SourceFile): Set<number> {
53
- const markers = new Set<number>();
54
-
55
- function visit(node: ts.Node): void {
56
- if (
57
- ts.isTaggedTemplateExpression(node) &&
58
- ts.isIdentifier(node.tag) &&
59
- ['sql', 'database', 'client', 'transaction'].includes(node.tag.text)
60
- ) {
61
- const start = node.tag.end;
62
- const end = node.template.getStart(file);
63
- const trivia = file.text.slice(start, end);
64
-
65
- if (/^\s*\/\* sql \*\/\s*$/.test(trivia)) {
66
- markers.add(start + trivia.indexOf('/* sql */'));
67
- }
68
- }
69
-
70
- ts.forEachChild(node, visit);
71
- }
72
-
73
- visit(file);
74
-
75
- return markers;
76
- }
77
-
78
- function zodBindings(file: ts.SourceFile): Set<string> {
79
- const names = new Set<string>();
80
-
81
- for (const statement of file.statements) {
82
- if (
83
- !ts.isImportDeclaration(statement) ||
84
- !ts.isStringLiteral(statement.moduleSpecifier) ||
85
- !['zod', 'zod/v3', 'zod/v4'].includes(statement.moduleSpecifier.text)
86
- ) {
87
- continue;
88
- }
89
-
90
- const clause = statement.importClause;
91
- const bindings = clause?.namedBindings;
92
-
93
- if (clause?.name) {
94
- names.add(clause.name.text);
95
- }
96
-
97
- if (bindings && ts.isNamespaceImport(bindings)) {
98
- names.add(bindings.name.text);
99
- } else if (bindings) {
100
- for (const binding of bindings.elements) {
101
- if ((binding.propertyName ?? binding.name).text === 'z') {
102
- names.add(binding.name.text);
103
- }
104
- }
105
- }
106
- }
107
-
108
- return names;
109
- }
110
-
111
- function inferredLocalSchema(type: ts.TypeNode, bindings: ReadonlySet<string>): string | undefined {
112
- if (!ts.isTypeReferenceNode(type) || !ts.isQualifiedName(type.typeName)) {
113
- return undefined;
114
- }
115
-
116
- const { left, right } = type.typeName;
117
- const query = type.typeArguments?.[0];
118
-
119
- if (
120
- !ts.isIdentifier(left) ||
121
- !bindings.has(left.text) ||
122
- !['infer', 'input', 'output'].includes(right.text) ||
123
- !query ||
124
- !ts.isTypeQueryNode(query) ||
125
- !ts.isIdentifier(query.exprName)
126
- ) {
127
- return undefined;
128
- }
129
-
130
- return query.exprName.text;
131
- }
132
-
133
- function isDoubleAssertion(node: ts.Node): boolean {
134
- if (!ts.isAsExpression(node) && !ts.isTypeAssertionExpression(node)) {
135
- return false;
136
- }
137
-
138
- let value = node.expression;
139
-
140
- while (ts.isParenthesizedExpression(value)) {
141
- value = value.expression;
142
- }
143
-
144
- return ts.isAsExpression(value) || ts.isTypeAssertionExpression(value);
145
- }
146
-
147
- export function findSourcePolicyIssues(source: string, fileName: string): SourceIssue[] {
148
- const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
149
- const issues: SourceIssue[] = [];
150
-
151
- function report(position: number, message: string): void {
152
- const location = file.getLineAndCharacterOfPosition(position);
153
-
154
- issues.push({ line: location.line + 1, column: location.character + 1, message });
155
- }
156
-
157
- function visit(node: ts.Node): void {
158
- if (isDoubleAssertion(node)) {
159
- report(node.getStart(file), 'Avoid double assertions. Validate or narrow the value before using it.');
160
- }
161
-
162
- ts.forEachChild(node, visit);
163
- }
164
-
165
- visit(file);
166
-
167
- const references = [...file.referencedFiles, ...file.typeReferenceDirectives, ...file.libReferenceDirectives];
168
- const markers = sqlMarkers(file);
169
-
170
- for (const [start, end] of collectComments(file)) {
171
- const recognizedReference =
172
- source.startsWith('///', start) && references.some((reference) => reference.pos >= start && reference.end <= end);
173
-
174
- if (!markers.has(start) && !recognizedReference) {
175
- report(start, 'Use names and code structure to explain behavior. Move explanatory comments out of source.');
176
- }
177
- }
178
-
179
- const localDeclarations = new Map<string, ts.VariableStatement>();
180
-
181
- for (const statement of file.statements) {
182
- if (ts.isVariableStatement(statement)) {
183
- for (const declaration of statement.declarationList.declarations) {
184
- if (ts.isIdentifier(declaration.name)) {
185
- localDeclarations.set(declaration.name.text, statement);
186
- }
187
- }
188
- }
189
- }
190
-
191
- const preceding = new Map<string, ts.Statement>();
192
- const bindings = zodBindings(file);
193
-
194
- for (const [index, statement] of file.statements.entries()) {
195
- if (!ts.isTypeAliasDeclaration(statement)) {
196
- continue;
197
- }
198
-
199
- const schema = inferredLocalSchema(statement.type, bindings);
200
-
201
- if (!schema) {
202
- continue;
203
- }
204
-
205
- const declaration = localDeclarations.get(schema);
206
-
207
- if (declaration && file.statements[index - 1] !== (preceding.get(schema) ?? declaration)) {
208
- report(statement.getStart(file), `Place ${statement.name.text} alongside its source schema ${schema}.`);
209
- }
210
-
211
- preceding.set(schema, statement);
212
- }
213
-
214
- return issues.sort((left, right) => left.line - right.line || left.column - right.column);
215
- }
@@ -1,17 +0,0 @@
1
- # Statement spacing map
2
-
3
- - [Binding discovery](#binding-discovery)
4
- - [Phase detection](#phase-detection)
5
- - [Diagnostics](#diagnostics)
6
-
7
- ## Binding discovery
8
-
9
- `findBindings` identifies supported hook and assertion names from syntax. `isSeparatedHookCall` and `isAssertion` recognize the supported expression shapes. They do not resolve arbitrary runtime aliases or lexical shadowing.
10
-
11
- ## Phase detection
12
-
13
- `statementPhase` groups declarations, operations and assertions and identifies independent control flow. `hasBlankLine` measures the boundary outside comment ownership. Class member spacing has a separate check in `member-spacing.ts`.
14
-
15
- ## Diagnostics
16
-
17
- `findSpacingIssues` checks parse errors and visits statement lists without changing source. `spacing.test.ts` covers independent phases, grouped declarations and assertions, imports, hooks, comments and parse failures. See the [check matrix](README.md#deterministic-checks) for scope.
@@ -1,174 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
-
4
- import { findSpacingIssues } from './spacing.ts';
5
-
6
- function issueLines(source: string, fileName = 'example.ts'): number[] {
7
- return findSpacingIssues(source, fileName).map((issue) => issue.line);
8
- }
9
-
10
- test('reports transitions while preserving declaration, operation, and assertion groups', () => {
11
- const source = [
12
- "import { expect as verify } from '@playwright/test';",
13
- "import { build } from './fixtures';",
14
- '',
15
- 'async function scenario() {',
16
- ' const first = build();',
17
- ' const second = build();',
18
- ' await first.save();',
19
- ' await second.save();',
20
- ' await verify.poll(read).toBe(1);',
21
- ' (expect(first)).toBeDefined();',
22
- ' run("verify(first)");',
23
- ' return first;',
24
- '}',
25
- ].join('\n');
26
-
27
- assert.deepEqual(issueLines(source), [7, 9, 11, 12]);
28
- assert.deepEqual(findSpacingIssues(source, 'example.spec.ts')[0], {
29
- line: 7,
30
- column: 3,
31
- message: 'Separate statement phases with a blank line.',
32
- });
33
- });
34
-
35
- test('preserves adjacent imports and schema type pairs', () => {
36
- const source = [
37
- "import { z } from 'zod';",
38
- "import { read } from './input';",
39
- 'const schema = z.string();',
40
- 'type Value = z.infer<typeof schema>;',
41
- 'const value: Value = schema.parse(read());',
42
- ].join('\n');
43
-
44
- assert.deepEqual(issueLines(source), []);
45
- });
46
-
47
- test('recognizes TypeScript and TSX hooks while preserving ordinary declarations', () => {
48
- const source = [
49
- "import * as ReactAlias from 'react';",
50
- "import { useForm as makeForm } from 'react-hook-form';",
51
- "import { useState as state } from 'react';",
52
- '',
53
- 'export function Component() {',
54
- ' const id = 1;',
55
- ' const [value] = state(id);',
56
- ' const title = (ReactAlias.useMemo(() => value, [value]) satisfies number);',
57
- ' const form = makeForm({});',
58
- ' ReactAlias.useEffect(() => {}, []);',
59
- ' return <button id={form.id}>{title}</button>;',
60
- '}',
61
- ].join('\n');
62
-
63
- assert.deepEqual(issueLines(source, 'Component.tsx'), [8, 9, 10, 11]);
64
- });
65
-
66
- test('preserves type-only imports, unsupported hook aliases and namespace assertions', () => {
67
- const source = [
68
- "import { type useMemo as memo } from 'react';",
69
- "import { useForm as form } from 'other';",
70
- "import type { expect as verify } from '@playwright/test';",
71
- "import * as checks from '@playwright/test';",
72
- '',
73
- 'function example() {',
74
- ' const first = memo(() => value, []);',
75
- ' const second = form();',
76
- ' const third = other.useEffect(run);',
77
- ' const fourth = wrap(useMemo(run, []));',
78
- '',
79
- ' inspect();',
80
- ' checks.expect(first);',
81
- ' verify(second);',
82
- '}',
83
- ].join('\n');
84
-
85
- assert.deepEqual(issueLines(source), []);
86
- });
87
-
88
- test('recognizes default React bindings and direct hooks inside typed wrappers', () => {
89
- const source = [
90
- "import ReactAlias from 'react';",
91
- '',
92
- 'function example() {',
93
- ' let before;',
94
- ' const first = <number>ReactAlias.useMemo(() => 1, []);',
95
- ' const second = useCallback(run, [])!;',
96
- ' const third = useReducer(reducer, 0) as State;',
97
- ' const fourth = useForm({});',
98
- ' const after = 1;',
99
- '}',
100
- ].join('\n');
101
-
102
- assert.deepEqual(issueLines(source), [5, 6, 7, 8, 9]);
103
- });
104
-
105
- test('uses the full control-flow statement as one phase and leaves block edges alone', () => {
106
- const phases = [
107
- 'if (ready) { act(); } else if (pending) { wait(); } else { reset(); }',
108
- 'for (;;) {}',
109
- 'for (const item of items) {}',
110
- 'for (const key in record) {}',
111
- 'while (ready) {}',
112
- 'do {} while (ready);',
113
- 'switch (value) { default: break; }',
114
- 'try { act(); } catch (cause) { fail(cause); }',
115
- 'class Fixture {}',
116
- 'function nested() {}',
117
- 'throw new Error("Synthetic failure.");',
118
- ];
119
-
120
- for (const phase of phases) {
121
- const source = ['function run() {', ' prepare();', ` ${phase}`, ' finish();', '}'].join('\n');
122
-
123
- assert.deepEqual(issueLines(source), [3, 4], phase);
124
- }
125
- });
126
-
127
- test('scans namespaces and switch cases independently and sorts findings by source position', () => {
128
- const source = [
129
- 'namespace Example {',
130
- ' const start = read();',
131
- ' run(start);',
132
- ' switch (start) {',
133
- ' case 1:',
134
- ' const item = read();',
135
- ' run(item);',
136
- ' break;',
137
- ' default:',
138
- ' const other = read();',
139
- ' run(other);',
140
- ' }',
141
- ' finish();',
142
- '}',
143
- 'function after() {}',
144
- ].join('\n');
145
-
146
- assert.deepEqual(issueLines(source), [3, 4, 7, 11, 13, 15]);
147
- });
148
-
149
- test('handles CRLF and ignores blank lines inside leading or trailing comments', () => {
150
- const source =
151
- 'function run() {\r\n prepare(); /* trailing\r\n\r\n detail */\r\n /* leading\r\n\r\n detail */\r\n return value;\r\n}\r\n';
152
- const spaced = source.replace('detail */\r\n /*', 'detail */\r\n \t\r\n /*');
153
-
154
- assert.deepEqual(issueLines(source), [8]);
155
- assert.deepEqual(issueLines(spaced), []);
156
- });
157
-
158
- test('reports same-line phases without modifying the supplied source', () => {
159
- const source = 'function run() { inspect(); return value; }';
160
-
161
- assert.deepEqual(findSpacingIssues(source, 'example.ts'), [
162
- { line: 1, column: source.indexOf('return') + 1, message: 'Separate statement phases with a blank line.' },
163
- ]);
164
- });
165
-
166
- test('skips generated source and surfaces syntax and invalid input errors', () => {
167
- assert.deepEqual(issueLines('// @generated\nconst = invalid;'), []);
168
- assert.deepEqual(issueLines(''), []);
169
- assert.throws(() => findSpacingIssues('const = invalid;', 'broken.ts'), {
170
- name: 'SyntaxError',
171
- message: /Cannot check statement spacing in broken\.ts:/,
172
- });
173
- assert.throws(() => findSpacingIssues('const value = 1;', ''), /fileName/);
174
- });
@@ -1,211 +0,0 @@
1
- import ts from 'typescript';
2
- import { z } from 'zod';
3
- import { hasGeneratedHeader } from './source-policy.ts';
4
-
5
- type SpacingIssue = { line: number; column: number; message: string };
6
- type StatementPhase = 'declaration' | 'operation' | 'assertion' | 'independent' | 'other';
7
-
8
- type SpacingBindings = {
9
- assertions: Set<string>;
10
- hooks: Set<string>;
11
- hookNamespaces: Map<string, ReadonlySet<string>>;
12
- };
13
-
14
- const spacingInput = z.object({ source: z.string(), fileName: z.string().min(1) });
15
- const spacingMessage = 'Separate statement phases with a blank line.';
16
- const reactHooks = new Set(['useEffect', 'useMemo', 'useCallback', 'useReducer']);
17
- const formHooks = new Set(['useForm']);
18
- const supportedHooks = new Map([
19
- ['react', reactHooks],
20
- ['@tanstack/react-form', formHooks],
21
- ['react-hook-form', formHooks],
22
- ]);
23
-
24
- function findBindings(file: ts.SourceFile): SpacingBindings {
25
- const bindings: SpacingBindings = {
26
- assertions: new Set(['expect']),
27
- hooks: new Set([...reactHooks, ...formHooks]),
28
- hookNamespaces: new Map([['React', reactHooks]]),
29
- };
30
-
31
- for (const statement of file.statements) {
32
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
33
- continue;
34
- }
35
-
36
- const clause = statement.importClause;
37
-
38
- if (!clause || clause.isTypeOnly) {
39
- continue;
40
- }
41
-
42
- const hookNames = supportedHooks.get(statement.moduleSpecifier.text);
43
- const namedBindings = clause.namedBindings;
44
-
45
- if (hookNames && clause.name) {
46
- bindings.hookNamespaces.set(clause.name.text, hookNames);
47
- }
48
-
49
- if (hookNames && namedBindings && ts.isNamespaceImport(namedBindings)) {
50
- bindings.hookNamespaces.set(namedBindings.name.text, hookNames);
51
- }
52
-
53
- if (!namedBindings || !ts.isNamedImports(namedBindings)) {
54
- continue;
55
- }
56
-
57
- for (const element of namedBindings.elements) {
58
- if (element.isTypeOnly) {
59
- continue;
60
- }
61
-
62
- const importedName = element.propertyName?.text ?? element.name.text;
63
-
64
- if (importedName === 'expect') {
65
- bindings.assertions.add(element.name.text);
66
- }
67
-
68
- if (hookNames?.has(importedName)) {
69
- bindings.hooks.add(element.name.text);
70
- }
71
- }
72
- }
73
-
74
- return bindings;
75
- }
76
-
77
- function isSeparatedHookCall(expression: ts.Expression, bindings: SpacingBindings): boolean {
78
- if (
79
- ts.isParenthesizedExpression(expression) ||
80
- ts.isAssertionExpression(expression) ||
81
- ts.isSatisfiesExpression(expression) ||
82
- ts.isNonNullExpression(expression)
83
- ) {
84
- return isSeparatedHookCall(expression.expression, bindings);
85
- }
86
-
87
- if (!ts.isCallExpression(expression)) {
88
- return false;
89
- }
90
-
91
- const callee = expression.expression;
92
-
93
- if (ts.isIdentifier(callee)) {
94
- return bindings.hooks.has(callee.text);
95
- }
96
-
97
- return (
98
- ts.isPropertyAccessExpression(callee) &&
99
- ts.isIdentifier(callee.expression) &&
100
- (bindings.hookNamespaces.get(callee.expression.text)?.has(callee.name.text) ?? false)
101
- );
102
- }
103
-
104
- function isAssertion(expression: ts.Expression, names: ReadonlySet<string>): boolean {
105
- if (
106
- ts.isCallExpression(expression) ||
107
- ts.isPropertyAccessExpression(expression) ||
108
- ts.isAwaitExpression(expression) ||
109
- ts.isParenthesizedExpression(expression)
110
- ) {
111
- return isAssertion(expression.expression, names);
112
- }
113
-
114
- return ts.isIdentifier(expression) && names.has(expression.text);
115
- }
116
-
117
- function statementPhase(statement: ts.Statement, bindings: SpacingBindings): StatementPhase {
118
- if (ts.isVariableStatement(statement)) {
119
- const hasHook = statement.declarationList.declarations.some((declaration) => {
120
- return declaration.initializer !== undefined && isSeparatedHookCall(declaration.initializer, bindings);
121
- });
122
-
123
- return hasHook ? 'independent' : 'declaration';
124
- }
125
-
126
- if (ts.isExpressionStatement(statement)) {
127
- const expression = statement.expression;
128
-
129
- if (isSeparatedHookCall(expression, bindings)) {
130
- return 'independent';
131
- }
132
-
133
- return isAssertion(expression, bindings.assertions) ? 'assertion' : 'operation';
134
- }
135
-
136
- if (
137
- ts.isIfStatement(statement) ||
138
- ts.isFunctionDeclaration(statement) ||
139
- ts.isClassDeclaration(statement) ||
140
- ts.isIterationStatement(statement, false) ||
141
- ts.isSwitchStatement(statement) ||
142
- ts.isTryStatement(statement) ||
143
- ts.isThrowStatement(statement)
144
- ) {
145
- return 'independent';
146
- }
147
-
148
- return 'other';
149
- }
150
-
151
- function hasBlankLine(previous: ts.Statement, current: ts.Statement, file: ts.SourceFile): boolean {
152
- const trailingComments = ts.getTrailingCommentRanges(file.text, previous.end);
153
- const previousEnd = trailingComments?.at(-1)?.end ?? previous.end;
154
- const leadingComments = ts.getLeadingCommentRanges(file.text, previousEnd);
155
- const currentStart = leadingComments?.[0]?.pos ?? current.getStart(file);
156
-
157
- return /\r?\n[^\S\r\n]*\r?\n/.test(file.text.slice(previousEnd, currentStart));
158
- }
159
-
160
- export function findSpacingIssues(source: string, fileName: string): readonly SpacingIssue[] {
161
- spacingInput.parse({ source, fileName });
162
-
163
- if (hasGeneratedHeader(source)) {
164
- return [];
165
- }
166
-
167
- const parsed = ts.transpileModule(source, {
168
- fileName: fileName.replace(/\.d\.(c|m)?ts$/, '.$1ts'),
169
- reportDiagnostics: true,
170
- });
171
- const errors = parsed.diagnostics?.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
172
-
173
- if (errors && errors.length > 0) {
174
- const details = errors.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
175
-
176
- throw new SyntaxError(`Cannot check statement spacing in ${fileName}: ${details.join(' ')}`);
177
- }
178
-
179
- const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
180
- const bindings = findBindings(file);
181
- const issues: SpacingIssue[] = [];
182
-
183
- function visit(node: ts.Node): void {
184
- if (ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node) || ts.isCaseOrDefaultClause(node)) {
185
- let previous: ts.Statement | undefined;
186
- let previousPhase: StatementPhase = 'other';
187
-
188
- for (const current of node.statements) {
189
- const currentPhase = statementPhase(current, bindings);
190
- const changesPhase = previousPhase !== 'other' && currentPhase !== 'other' && previousPhase !== currentPhase;
191
- const independent = previousPhase === 'independent' || currentPhase === 'independent';
192
- const needsSeparation = independent || changesPhase || ts.isReturnStatement(current);
193
-
194
- if (previous && needsSeparation && !hasBlankLine(previous, current, file)) {
195
- const { line, character } = file.getLineAndCharacterOfPosition(current.getStart(file));
196
-
197
- issues.push({ line: line + 1, column: character + 1, message: spacingMessage });
198
- }
199
-
200
- previous = current;
201
- previousPhase = currentPhase;
202
- }
203
- }
204
-
205
- ts.forEachChild(node, visit);
206
- }
207
-
208
- visit(file);
209
-
210
- return issues.sort((left, right) => left.line - right.line || left.column - right.column);
211
- }