@prestack/thread 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/dist/core/{asking.d.ts → answers.d.ts} +6 -14
  3. package/dist/core/{asking.js → answers.js} +13 -332
  4. package/dist/core/{asking.ts → answers.ts} +14 -357
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +2 -3
  7. package/dist/core/chain.js +4 -11
  8. package/dist/core/chain.ts +4 -3
  9. package/dist/core/client.d.ts +57 -0
  10. package/dist/core/client.js +243 -0
  11. package/dist/core/client.ts +279 -0
  12. package/dist/core/descend.d.ts +2 -0
  13. package/dist/core/descend.js +109 -16
  14. package/dist/core/descend.ts +120 -16
  15. package/dist/core/files.d.ts +45 -0
  16. package/dist/core/files.js +145 -0
  17. package/dist/core/files.ts +193 -0
  18. package/dist/core/init.d.ts +1 -0
  19. package/dist/core/init.js +10 -2
  20. package/dist/core/init.ts +8 -2
  21. package/dist/core/invoked.js +1 -1
  22. package/dist/core/invoked.ts +1 -1
  23. package/dist/core/rows.d.ts +2 -0
  24. package/dist/core/rows.js +65 -0
  25. package/dist/core/rows.ts +58 -0
  26. package/dist/core/tree.js +1 -1
  27. package/dist/core/tree.ts +1 -1
  28. package/dist/derivation/attach.js +0 -12
  29. package/dist/derivation/attach.ts +0 -12
  30. package/dist/derivation/generators/build.js +8 -7
  31. package/dist/derivation/generators/build.ts +8 -7
  32. package/dist/derivation/generators/release.d.ts +1 -1
  33. package/dist/derivation/generators/release.js +15 -23
  34. package/dist/derivation/generators/release.ts +15 -15
  35. package/dist/integrations/attach.js +1 -19
  36. package/dist/integrations/attach.ts +1 -19
  37. package/dist/integrations/generators/read.js +2 -2
  38. package/dist/integrations/generators/read.ts +4 -4
  39. package/dist/integrations/generators/vault.js +4 -12
  40. package/dist/integrations/generators/vault.ts +4 -4
  41. package/dist/integrations/module.js +5 -3
  42. package/dist/integrations/module.ts +5 -3
  43. package/dist/integrations/vault.js +5 -5
  44. package/dist/integrations/vault.ts +5 -5
  45. package/dist/integrity/attach.js +0 -12
  46. package/dist/integrity/attach.ts +0 -12
  47. package/dist/integrity/check-threads.js +1 -1
  48. package/dist/integrity/check-threads.ts +1 -1
  49. package/dist/integrity/constraint.d.ts +11 -2
  50. package/dist/integrity/constraint.js +163 -20
  51. package/dist/integrity/constraint.ts +174 -22
  52. package/dist/integrity/generators/compiler.js +7 -1
  53. package/dist/integrity/generators/compiler.ts +10 -1
  54. package/dist/integrity/generators/gate.js +23 -18
  55. package/dist/integrity/generators/gate.ts +29 -9
  56. package/dist/integrity/generators/publish.js +14 -26
  57. package/dist/integrity/generators/publish.ts +14 -17
  58. package/dist/integrity/generators/trial.d.ts +2 -2
  59. package/dist/integrity/generators/trial.js +108 -50
  60. package/dist/integrity/generators/trial.ts +107 -48
  61. package/dist/integrity/lint.js +3 -3
  62. package/dist/integrity/lint.ts +3 -3
  63. package/dist/integrity/lsp/diagnostics.js +1 -1
  64. package/dist/integrity/lsp/diagnostics.ts +1 -1
  65. package/dist/integrity/lsp/lint.js +1 -1
  66. package/dist/integrity/lsp/lint.ts +1 -1
  67. package/dist/integrity/slopnator.d.ts +1 -14
  68. package/dist/integrity/slopnator.js +8 -40
  69. package/dist/integrity/slopnator.ts +9 -51
  70. package/dist/integrity/slopnators/sl_broken_lineage.js +4 -2
  71. package/dist/integrity/slopnators/sl_broken_lineage.ts +4 -2
  72. package/dist/integrity/slopnators/sl_declared_elsewhere.js +24 -3
  73. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +24 -3
  74. package/dist/integrity/slopnators/sl_derived_drift.js +10 -4
  75. package/dist/integrity/slopnators/sl_derived_drift.ts +10 -4
  76. package/dist/integrity/slopnators/sl_inert_island.js +7 -13
  77. package/dist/integrity/slopnators/sl_inert_island.ts +7 -16
  78. package/dist/integrity/slopnators/sl_inert_slopnator.js +8 -8
  79. package/dist/integrity/slopnators/sl_inert_slopnator.ts +8 -8
  80. package/dist/integrity/slopnators/sl_missing_row.js +14 -9
  81. package/dist/integrity/slopnators/sl_missing_row.ts +14 -10
  82. package/dist/integrity/slopnators/sl_no_child_process.js +8 -8
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +8 -8
  84. package/dist/integrity/slopnators/sl_no_import_cycle.js +1 -1
  85. package/dist/integrity/slopnators/sl_no_import_cycle.ts +1 -1
  86. package/dist/integrity/slopnators/sl_no_such_command.js +20 -5
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +22 -5
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.js +8 -8
  89. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +8 -8
  90. package/dist/integrity/slopnators/sl_stale_path.js +91 -18
  91. package/dist/integrity/slopnators/sl_stale_path.ts +97 -18
  92. package/dist/integrity/slopnators/sl_twice_meant.js +25 -2
  93. package/dist/integrity/slopnators/sl_twice_meant.ts +28 -2
  94. package/dist/integrity/slopnators/sl_twice_parented.js +26 -5
  95. package/dist/integrity/slopnators/sl_twice_parented.ts +26 -5
  96. package/dist/integrity/slopnators/sl_undeclared_node.js +18 -2
  97. package/dist/integrity/slopnators/sl_undeclared_node.ts +18 -2
  98. package/dist/integrity/slopnators/sl_undeclared_operation.js +3 -1
  99. package/dist/integrity/slopnators/sl_undeclared_operation.ts +3 -1
  100. package/dist/integrity/slopnators/sl_undeclared_variant.js +6 -2
  101. package/dist/integrity/slopnators/sl_undeclared_variant.ts +6 -2
  102. package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -4
  103. package/dist/integrity/slopnators/sl_ungoverned_island.ts +3 -4
  104. package/dist/integrity/slopnators/sl_unkept_rows.js +1 -1
  105. package/dist/integrity/slopnators/sl_unkept_rows.ts +1 -1
  106. package/dist/integrity/slopnators/sl_unpaired_member.js +13 -8
  107. package/dist/integrity/slopnators/sl_unpaired_member.ts +16 -8
  108. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +16 -3
  109. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +16 -3
  110. package/dist/integrity/slopnators/sl_unthreaded.d.ts +1 -0
  111. package/dist/integrity/slopnators/sl_unthreaded.js +18 -10
  112. package/dist/integrity/slopnators/sl_unthreaded.ts +19 -11
  113. package/dist/integrity/slopnators/sl_untranslated_key.js +1 -1
  114. package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -1
  115. package/dist/integrity/slopnators/sl_unversioned.js +16 -2
  116. package/dist/integrity/slopnators/sl_unversioned.ts +16 -2
  117. package/dist/integrity/sweep.js +17 -3
  118. package/dist/integrity/sweep.ts +20 -3
  119. package/dist/knowledge/attach.js +0 -6
  120. package/dist/knowledge/attach.ts +0 -6
  121. package/dist/knowledge/generators/docsgen.js +12 -3
  122. package/dist/knowledge/generators/docsgen.ts +12 -2
  123. package/dist/knowledge/generators/glossary.js +1 -1
  124. package/dist/knowledge/generators/glossary.ts +1 -1
  125. package/dist/operations/attach.js +2 -26
  126. package/dist/operations/attach.ts +2 -27
  127. package/dist/operations/dispatch.js +2 -3
  128. package/dist/operations/dispatch.ts +2 -3
  129. package/dist/operations/generators/capability.js +4 -7
  130. package/dist/operations/generators/capability.ts +3 -6
  131. package/dist/operations/generators/compile.js +8 -13
  132. package/dist/operations/generators/compile.ts +8 -4
  133. package/dist/operations/generators/permits.js +4 -13
  134. package/dist/operations/generators/permits.ts +3 -3
  135. package/dist/operations/generators/seed.js +5 -2
  136. package/dist/operations/generators/seed.ts +6 -2
  137. package/dist/operations/generators/trial.js +10 -10
  138. package/dist/operations/generators/trial.ts +10 -10
  139. package/dist/operations/http.js +4 -1
  140. package/dist/operations/http.ts +6 -1
  141. package/package.json +2 -2
  142. package/dist/core/cli.d.ts +0 -2
  143. package/dist/core/cli.js +0 -1761
  144. package/dist/core/cli.ts +0 -2004
  145. package/dist/core/generators/meta.d.ts +0 -2
  146. package/dist/core/generators/meta.js +0 -829
  147. package/dist/core/generators/meta.ts +0 -1006
  148. package/dist/derivation/generators/derivation.d.ts +0 -3
  149. package/dist/derivation/generators/derivation.js +0 -273
  150. package/dist/derivation/generators/derivation.ts +0 -347
  151. package/dist/derivation/makers/release.d.ts +0 -1
  152. package/dist/derivation/makers/release.js +0 -107
  153. package/dist/derivation/makers/release.ts +0 -135
  154. package/dist/integrations/generators/secret.d.ts +0 -1
  155. package/dist/integrations/generators/secret.js +0 -233
  156. package/dist/integrations/generators/secret.ts +0 -289
  157. package/dist/integrations/generators/target.d.ts +0 -1
  158. package/dist/integrations/generators/target.js +0 -775
  159. package/dist/integrations/generators/target.ts +0 -944
  160. package/dist/integrity/generators/rule.d.ts +0 -2
  161. package/dist/integrity/generators/rule.js +0 -213
  162. package/dist/integrity/generators/rule.ts +0 -271
  163. package/dist/integrity/generators/seal.d.ts +0 -4
  164. package/dist/integrity/generators/seal.js +0 -101
  165. package/dist/integrity/generators/seal.ts +0 -117
  166. package/dist/integrity/sealed.d.ts +0 -1
  167. package/dist/integrity/sealed.js +0 -56
  168. package/dist/integrity/sealed.ts +0 -56
  169. package/dist/knowledge/generators/skills.d.ts +0 -1
  170. package/dist/knowledge/generators/skills.js +0 -222
  171. package/dist/knowledge/generators/skills.ts +0 -263
  172. package/dist/operations/generators/grant.d.ts +0 -1
  173. package/dist/operations/generators/grant.js +0 -267
  174. package/dist/operations/generators/grant.ts +0 -336
  175. package/dist/operations/generators/operation.d.ts +0 -1
  176. package/dist/operations/generators/operation.js +0 -811
  177. package/dist/operations/generators/operation.ts +0 -972
  178. package/dist/operations/makers/capability.d.ts +0 -1
  179. package/dist/operations/makers/capability.js +0 -117
  180. package/dist/operations/makers/capability.ts +0 -166
  181. package/dist/operations/makers/trial.d.ts +0 -1
  182. package/dist/operations/makers/trial.js +0 -413
  183. package/dist/operations/makers/trial.ts +0 -520
@@ -1,16 +1,9 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
1
  import { spawnSync } from 'node:child_process';
10
- import { existsSync, readFileSync } from 'node:fs';
2
+ import { existsSync, readFileSync } from "../../core/files.js";
3
+ import { valuesIn } from "../../core/rows.js";
11
4
  import { join } from 'node:path';
12
5
  import { argv, env, execPath, stderr, stdout } from 'node:process';
13
- import { pathToFileURL } from 'node:url';
6
+ import { TYPED } from "../../core/answers.js";
14
7
  import { CLI, exit } from "../../core/invoked.js";
15
8
  import { named } from "../../core/scope.js";
16
9
  import { findTree, inTree } from "../../core/tree.js";
@@ -31,11 +24,6 @@ const CHECKS = [
31
24
  runs: 'pnpm exec eslint .',
32
25
  because: 'the rules that live on the lint surface, which is the only one carrying a fixer the sweep can report but not repair',
33
26
  },
34
- {
35
- id: 'seal',
36
- runs: 'pnpm seal:check',
37
- because: 'the package must be the bytes it says it is. CHECKED and never regenerated, because a release that rewrites its own seal can never fail it',
38
- },
39
27
  ];
40
28
  const MINE = /^(?:npx |pnpm )?thread /;
41
29
  function run(what, extra) {
@@ -79,7 +67,7 @@ const TOKEN = 'sec_registry_token';
79
67
  const PRODUCTION = 'production';
80
68
  const secrets = inTree(TREE, 'secrets.ts');
81
69
  const declared = existsSync(secrets)
82
- ? ((await import(__rewriteRelativeImportExtension(pathToFileURL(secrets).href))).SECRETS ?? [])
70
+ ? (valuesIn(readFileSync(secrets)).SECRETS ?? [])
83
71
  : [];
84
72
  const vaulted = declared.some((one) => one.id === TOKEN);
85
73
  if (vaulted && !sealedIn(ROOT, PRODUCTION).includes(TOKEN)) {
@@ -108,10 +96,10 @@ stdout.write(vaulted
108
96
  ? `publish: ${TOKEN} is declared and sealed for ${PRODUCTION}. It is opened only after every check passes, and handed to pnpm as its auth setting for ${registry}.\n`
109
97
  : `publish: no row declares ${TOKEN}, so this publish uses ${signedAs}. Declare it with \`thread create secret\` to publish with a token sealed in this repository instead.\n`);
110
98
  stdout.write('publish: deriving and building what the packaging rows state first, so every check below reads the files this release ships rather than the source they were written from.\n');
111
- const made = run('npx thread packaged --build');
99
+ const made = run(`${TYPED} packaged --build`);
112
100
  stdout.write(`publish: packaged exited ${String(made)}\n`);
113
101
  if (made !== 0) {
114
- refuse('packaged', made, 'a package whose manifest and release note are not what its rows state would go out describing itself as something nobody declared, and one that did not compile would go out promising files it does not have', 'npx thread packaged --build');
102
+ refuse('packaged', made, 'a package whose manifest and release note are not what its rows state would go out describing itself as something nobody declared, and one that did not compile would go out promising files it does not have', `${TYPED} packaged --build`);
115
103
  }
116
104
  stdout.write(`\npublish: ${String(CHECKS.length)} check(s) stand between this tree and ${registry}. Any one of them stops it.\n`);
117
105
  for (const check of CHECKS)
@@ -124,32 +112,32 @@ for (const [index, check] of CHECKS.entries()) {
124
112
  if (code !== 0)
125
113
  refuse(check.id, code, check.because, check.runs);
126
114
  }
127
- const numbered = run('npx thread packaged --release');
115
+ const numbered = run(`${TYPED} packaged --release`);
128
116
  stdout.write(`publish: packaged --release exited ${String(numbered)}\n`);
129
117
  if (numbered !== 0) {
130
- refuse('packaged', numbered, 'the next release number is written onto the manifests only once every check has passed, and writing it refused, so nothing here carries a number to go out under', 'npx thread packaged --release');
118
+ refuse('packaged', numbered, 'the next release number is written onto the manifests only once every check has passed, and writing it refused, so nothing here carries a number to go out under', `${TYPED} packaged --release`);
131
119
  }
132
120
  const handed = vaulted ? { [setting()]: await tokened() } : undefined;
133
121
  stdout.write(`\npublish: every check passed. Publishing to ${registry} with ${vaulted ? `${TOKEN} from this repository's vault` : signedAs}.\n`);
134
122
  const sent = run(`pnpm -r publish --registry ${registry} --no-git-checks --access public`, handed);
135
123
  if (sent !== 0) {
136
124
  stderr.write(`\npublish: the checks passed and the publish itself exited ${String(sent)}.\n`);
137
- const back = run('npx thread packaged');
125
+ const back = run(`${TYPED} packaged`);
138
126
  stderr.write(back === 0
139
127
  ? 'publish: the manifests carry the last recorded release again and no note was written, so the next `thread publish` numbers this release once more.\n'
140
- : 'publish: the manifests still carry the number that did not go out. Run `npx thread packaged` to put the last recorded release back.\n');
128
+ : `publish: the manifests still carry the number that did not go out. Run \`${TYPED} packaged\` to put the last recorded release back.\n`);
141
129
  exit(1);
142
130
  }
143
- const recorded = run('npx thread packaged --record');
131
+ const recorded = run(`${TYPED} packaged --record`);
144
132
  stdout.write(`publish: packaged --record exited ${String(recorded)}\n`);
145
133
  if (recorded !== 0) {
146
- stderr.write('\npublish: the packages went out and the note recording them refused to be written, so the next publish would number this release again. Run `npx thread packaged --record` and read what it says.\n');
134
+ stderr.write(`\npublish: the packages went out and the note recording them refused to be written, so the next publish would number this release again. Run \`${TYPED} packaged --record\` and read what it says.\n`);
147
135
  exit(1);
148
136
  }
149
137
  const SHEET = 'packaging.ts';
150
138
  const sheet = inTree(TREE, SHEET);
151
139
  const stated = existsSync(sheet)
152
- ? (await import(__rewriteRelativeImportExtension(pathToFileURL(sheet).href)))
140
+ ? valuesIn(readFileSync(sheet))
153
141
  : {};
154
142
  const shipped = stated.PACKAGES ?? [];
155
143
  function versionAt(folder) {
@@ -191,5 +179,5 @@ else {
191
179
  stdout.write(`publish: committed the derived artifacts to ${branch} — ${going}.\n`);
192
180
  }
193
181
  }
194
- stdout.write(`\npublish: done. What went out was derived, then built, then passed the gate, the lint and the seal, in that order. The compile inside \`thread packaged --build\` is the typecheck.\n`);
182
+ stdout.write(`\npublish: done. What went out was derived, then built, then passed the gate and the lint, in that order. The compile inside \`thread packaged --build\` is the typecheck.\n`);
195
183
  exit(0);
@@ -1,8 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process'
2
- import { existsSync, readFileSync } from 'node:fs'
2
+ import { existsSync, readFileSync } from '../../core/files.ts'
3
+ import { valuesIn } from '../../core/rows.ts'
3
4
  import { join } from 'node:path'
4
5
  import { argv, env, execPath, stderr, stdout } from 'node:process'
5
6
  import { pathToFileURL } from 'node:url'
7
+ import { TYPED } from '../../core/answers.ts'
6
8
  import { CLI, exit } from '../../core/invoked.ts'
7
9
  import { named } from '../../core/scope.ts'
8
10
  import { findTree, inTree } from '../../core/tree.ts'
@@ -36,11 +38,6 @@ const CHECKS: readonly Check[] = [
36
38
  runs: 'pnpm exec eslint .',
37
39
  because: 'the rules that live on the lint surface, which is the only one carrying a fixer the sweep can report but not repair',
38
40
  },
39
- {
40
- id: 'seal',
41
- runs: 'pnpm seal:check',
42
- because: 'the package must be the bytes it says it is. CHECKED and never regenerated, because a release that rewrites its own seal can never fail it',
43
- },
44
41
  ]
45
42
 
46
43
  const MINE = /^(?:npx |pnpm )?thread /
@@ -93,7 +90,7 @@ const PRODUCTION = 'production'
93
90
  const secrets = inTree(TREE, 'secrets.ts')
94
91
 
95
92
  const declared = existsSync(secrets)
96
- ? (((await import(pathToFileURL(secrets).href)) as { SECRETS?: readonly Secret[] }).SECRETS ?? [])
93
+ ? ((valuesIn(readFileSync(secrets)) as { SECRETS?: readonly Secret[] }).SECRETS ?? [])
97
94
  : []
98
95
 
99
96
  const vaulted = declared.some((one) => one.id === TOKEN)
@@ -132,14 +129,14 @@ stdout.write(
132
129
 
133
130
  stdout.write('publish: deriving and building what the packaging rows state first, so every check below reads the files this release ships rather than the source they were written from.\n')
134
131
 
135
- const made = run('npx thread packaged --build')
132
+ const made = run(`${TYPED} packaged --build`)
136
133
  stdout.write(`publish: packaged exited ${String(made)}\n`)
137
134
  if (made !== 0) {
138
135
  refuse(
139
136
  'packaged',
140
137
  made,
141
138
  'a package whose manifest and release note are not what its rows state would go out describing itself as something nobody declared, and one that did not compile would go out promising files it does not have',
142
- 'npx thread packaged --build',
139
+ `${TYPED} packaged --build`,
143
140
  )
144
141
  }
145
142
 
@@ -155,14 +152,14 @@ for (const [index, check] of CHECKS.entries()) {
155
152
  if (code !== 0) refuse(check.id, code, check.because, check.runs)
156
153
  }
157
154
 
158
- const numbered = run('npx thread packaged --release')
155
+ const numbered = run(`${TYPED} packaged --release`)
159
156
  stdout.write(`publish: packaged --release exited ${String(numbered)}\n`)
160
157
  if (numbered !== 0) {
161
158
  refuse(
162
159
  'packaged',
163
160
  numbered,
164
161
  'the next release number is written onto the manifests only once every check has passed, and writing it refused, so nothing here carries a number to go out under',
165
- 'npx thread packaged --release',
162
+ `${TYPED} packaged --release`,
166
163
  )
167
164
  }
168
165
 
@@ -174,19 +171,19 @@ const sent = run(`pnpm -r publish --registry ${registry} --no-git-checks --acces
174
171
 
175
172
  if (sent !== 0) {
176
173
  stderr.write(`\npublish: the checks passed and the publish itself exited ${String(sent)}.\n`)
177
- const back = run('npx thread packaged')
174
+ const back = run(`${TYPED} packaged`)
178
175
  stderr.write(
179
176
  back === 0
180
177
  ? 'publish: the manifests carry the last recorded release again and no note was written, so the next `thread publish` numbers this release once more.\n'
181
- : 'publish: the manifests still carry the number that did not go out. Run `npx thread packaged` to put the last recorded release back.\n',
178
+ : `publish: the manifests still carry the number that did not go out. Run \`${TYPED} packaged\` to put the last recorded release back.\n`,
182
179
  )
183
180
  exit(1)
184
181
  }
185
182
 
186
- const recorded = run('npx thread packaged --record')
183
+ const recorded = run(`${TYPED} packaged --record`)
187
184
  stdout.write(`publish: packaged --record exited ${String(recorded)}\n`)
188
185
  if (recorded !== 0) {
189
- stderr.write('\npublish: the packages went out and the note recording them refused to be written, so the next publish would number this release again. Run `npx thread packaged --record` and read what it says.\n')
186
+ stderr.write(`\npublish: the packages went out and the note recording them refused to be written, so the next publish would number this release again. Run \`${TYPED} packaged --record\` and read what it says.\n`)
190
187
  exit(1)
191
188
  }
192
189
 
@@ -200,7 +197,7 @@ interface Packaged {
200
197
  const sheet = inTree(TREE, SHEET)
201
198
 
202
199
  const stated = existsSync(sheet)
203
- ? ((await import(pathToFileURL(sheet).href)) as { PACKAGES?: readonly Packaged[] })
200
+ ? (valuesIn(readFileSync(sheet)) as { PACKAGES?: readonly Packaged[] })
204
201
  : {}
205
202
 
206
203
  const shipped = stated.PACKAGES ?? []
@@ -243,5 +240,5 @@ if (asked(['rev-parse', '--is-inside-work-tree']) !== 'true') {
243
240
  }
244
241
  }
245
242
 
246
- stdout.write(`\npublish: done. What went out was derived, then built, then passed the gate, the lint and the seal, in that order. The compile inside \`thread packaged --build\` is the typecheck.\n`)
243
+ stdout.write(`\npublish: done. What went out was derived, then built, then passed the gate and the lint, in that order. The compile inside \`thread packaged --build\` is the typecheck.\n`)
247
244
  exit(0)
@@ -1,6 +1,6 @@
1
1
  import type { Rule } from 'eslint';
2
- import type { Constraint } from '../constraint.ts';
3
- import { type Byproduct, type Slopnator, type Sourced } from '../slopnator.ts';
2
+ import type { Constraint, Sourced } from '../constraint.ts';
3
+ import { type Byproduct, type Slopnator } from '../slopnator.ts';
4
4
  export declare const TRIALS_PATH: string;
5
5
  export interface Scenario {
6
6
  readonly files: readonly Sourced[];
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import * as disk from 'node:fs';
2
+ import { answeredBy, existsSync, readFileSync } from "../../core/files.js";
3
+ import { basename, dirname, join, resolve } from 'node:path';
3
4
  import { exit, invoked } from "../../core/invoked.js";
4
5
  import { Linter } from 'eslint';
5
6
  const TREE = findTree();
@@ -16,8 +17,9 @@ const WRAPPINGS = [
16
17
  (source) => `const _probe = () => { ${source} }`,
17
18
  (source) => `const _probe = <Probe>{${source}}</Probe>`,
18
19
  ];
20
+ const canonical = (at) => at.replace(/^threads\//, `${TREE.name}/`);
19
21
  export function probeAt(rule) {
20
- const path = rule.where.paths[0] ?? 'src/';
22
+ const path = canonical(rule.where.paths[0] ?? 'src/');
21
23
  if (path === '.')
22
24
  return PROBE;
23
25
  return path.endsWith('/') ? `${path}${PROBE}` : `${path}/${PROBE}`;
@@ -28,8 +30,9 @@ function requiresOf(rule, reports) {
28
30
  : `${rule.id} stays silent on this source — the fix it recommends is not itself refused`;
29
31
  }
30
32
  function scenarioOf(rule, proof) {
31
- if (typeof proof !== 'string')
32
- return proof;
33
+ if (typeof proof !== 'string') {
34
+ return { at: canonical(proof.at), files: proof.files.map((one) => ({ ...one, at: canonical(one.at) })) };
35
+ }
33
36
  const path = probeAt(rule);
34
37
  return { files: [{ at: path, source: proof }], at: path };
35
38
  }
@@ -74,10 +77,95 @@ function config(rule, module, parser) {
74
77
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
75
78
  },
76
79
  plugins: { rule: { rules: { [rule.id]: module } } },
77
- rules: { [`rule/`]: 'error' },
80
+ rules: { [`rule/${rule.id}`]: 'error' },
78
81
  },
79
82
  ];
80
83
  }
84
+ const flat = (at) => (at.length > 1 && at.endsWith('/') ? at.replace(/\/+$/, '') : at);
85
+ const under = (at) => at.startsWith(ROOT) || `${at}/` === ROOT;
86
+ const own = (at) => under(at) && !at.slice(ROOT.length).split('/').includes('node_modules');
87
+ function beside(at) {
88
+ try {
89
+ return disk.readdirSync(at, { withFileTypes: true });
90
+ }
91
+ catch {
92
+ return [];
93
+ }
94
+ }
95
+ function outside(at) {
96
+ const one = disk.statSync(at, { throwIfNoEntry: false });
97
+ if (one === undefined)
98
+ return 'none';
99
+ return one.isFile() ? 'file' : one.isDirectory() ? 'folder' : 'other';
100
+ }
101
+ const GONE = 'is not in the scenario this trial stands, and while it runs the scenario is the whole tree.';
102
+ function answering(files) {
103
+ const held = new Map(files.map((one) => [resolve(ROOT, one.at), one.source]));
104
+ const folders = new Set();
105
+ const parents = (at) => {
106
+ for (let up = dirname(at); under(`${up}/`); up = dirname(up))
107
+ folders.add(up);
108
+ };
109
+ for (const at of held.keys())
110
+ parents(at);
111
+ const answers = {
112
+ kind: (at) => (held.has(at) ? 'file' : folders.has(at) ? 'folder' : own(at) ? 'none' : outside(at)),
113
+ read: (at) => {
114
+ const said = held.get(at);
115
+ if (said !== undefined)
116
+ return said;
117
+ if (own(at))
118
+ throw new Error(`${at} ${GONE}`);
119
+ return disk.readFileSync(at, 'utf8');
120
+ },
121
+ write: (at, said) => {
122
+ held.set(at, said);
123
+ parents(at);
124
+ },
125
+ list: (at) => {
126
+ const inside = new Map();
127
+ const found = own(at) ? [] : folders.has(at) ? beside(at) : disk.readdirSync(at, { withFileTypes: true });
128
+ for (const one of found)
129
+ inside.set(one.name, { name: one.name, kind: one.isDirectory() ? 'folder' : 'file' });
130
+ for (const path of folders)
131
+ if (dirname(path) === at)
132
+ inside.set(basename(path), { name: basename(path), kind: 'folder' });
133
+ for (const path of held.keys())
134
+ if (dirname(path) === at)
135
+ inside.set(basename(path), { name: basename(path), kind: 'file' });
136
+ return [...inside.values()];
137
+ },
138
+ stat: (at) => {
139
+ const said = held.get(at);
140
+ if (said !== undefined)
141
+ return { size: said.length, modified: 0 };
142
+ if (folders.has(at))
143
+ return { size: 0, modified: 0 };
144
+ if (own(at))
145
+ throw new Error(`${at} ${GONE}`);
146
+ const one = disk.statSync(at);
147
+ return { size: one.size, modified: one.mtimeMs };
148
+ },
149
+ remove: (at) => {
150
+ held.delete(at);
151
+ folders.delete(at);
152
+ },
153
+ real: (at) => (own(at) || held.has(at) || folders.has(at) ? at : disk.realpathSync(at)),
154
+ };
155
+ return {
156
+ kind: (at) => answers.kind(flat(at)),
157
+ read: (at) => answers.read(flat(at)),
158
+ write: (at, said) => {
159
+ answers.write(flat(at), said);
160
+ },
161
+ list: (at) => answers.list(flat(at)),
162
+ stat: (at) => answers.stat(flat(at)),
163
+ remove: (at) => {
164
+ answers.remove(flat(at));
165
+ },
166
+ real: (at) => answers.real(flat(at)),
167
+ };
168
+ }
81
169
  export function runTrial(trial, rule, module, parser, slopnator) {
82
170
  const linter = new Linter();
83
171
  const failures = [];
@@ -91,45 +179,18 @@ export function runTrial(trial, rule, module, parser, slopnator) {
91
179
  };
92
180
  }
93
181
  const target = trial.scenario.files.find((file) => file.at === trial.scenario.at);
94
- if (target === undefined) {
95
- return {
96
- id: trial.id,
97
- rule: rule.id,
98
- passed: false,
99
- parsed: false,
100
- failures: [`the scenario writes no file at "${trial.scenario.at}", the path it lints, so nothing could have been measured`],
101
- };
102
- }
103
- for (const file of trial.scenario.files) {
104
- if (!existsSync(join(ROOT, file.at)))
105
- continue;
106
- return {
107
- id: trial.id,
108
- rule: rule.id,
109
- passed: false,
110
- parsed: false,
111
- failures: [`the scenario would overwrite "${file.at}", which is a real file in this repository — a trial may not stand on a path somebody authored`],
112
- };
113
- }
114
- const written = [];
115
- const created = [];
116
182
  let parsed = false;
117
183
  let satisfied = false;
184
+ answeredBy(answering(trial.scenario.files));
185
+ forget();
118
186
  try {
119
- for (const file of trial.scenario.files) {
120
- const path = join(ROOT, file.at);
121
- const made = mkdirSync(dirname(path), { recursive: true });
122
- if (made !== undefined)
123
- created.push(made);
124
- writeFileSync(path, file.source);
125
- written.push(path);
126
- }
127
- forget();
128
- const path = join(ROOT, trial.scenario.at);
129
187
  if (slopnator !== undefined && !reads(slopnator)) {
130
188
  parsed = true;
131
189
  satisfied = slopnator.checks(ROOT).length > 0 === trial.verdict.reports;
132
190
  }
191
+ else if (target === undefined) {
192
+ failures.push(`the scenario writes no file at "${trial.scenario.at}", the path it lints, so nothing could have been measured`);
193
+ }
133
194
  else if (slopnator !== undefined && sweeps(slopnator)) {
134
195
  const held = slopnator.holds(undefined, {
135
196
  filename: trial.scenario.at,
@@ -143,32 +204,29 @@ export function runTrial(trial, rule, module, parser, slopnator) {
143
204
  else {
144
205
  for (const wrap of WRAPPINGS) {
145
206
  const text = wrap(target.source);
146
- writeFileSync(path, text);
147
207
  const messages = linter.verify(text, config(rule, module, parser), trial.scenario.at);
148
208
  if (messages.some((message) => message.fatal === true))
149
209
  continue;
150
210
  parsed = true;
151
- satisfied = messages.some((message) => message.ruleId === `rule/`) === trial.verdict.reports;
211
+ satisfied = messages.some((message) => message.ruleId === `rule/${rule.id}`) === trial.verdict.reports;
152
212
  break;
153
213
  }
154
214
  }
155
215
  }
216
+ catch (thrown) {
217
+ failures.push(`this trial could not run: ${thrown instanceof Error ? thrown.message : String(thrown)} — its scenario does not describe a world this rule can be measured against`);
218
+ }
156
219
  finally {
157
- for (const path of written)
158
- if (existsSync(path))
159
- rmSync(path);
160
- for (const path of created)
161
- if (existsSync(path))
162
- rmSync(path, { recursive: true });
220
+ answeredBy(undefined);
163
221
  forget();
164
222
  }
165
- if (!parsed) {
223
+ if (failures.length === 0 && !parsed) {
166
224
  failures.push('no wrapping of this source parsed, so nothing was measured — a trial that cannot run is not a trial that passed');
167
225
  }
168
- else if (!satisfied) {
226
+ else if (failures.length === 0 && !satisfied) {
169
227
  failures.push(trial.verdict.reports
170
- ? `expected to report on this source and it stayed silent — the rule documents a form its own slopnator does not refuse`
171
- : `expected to stay silent on this source and it reported — the rule refuses the fix it recommends`);
228
+ ? `expected ${rule.id} to report on this source and it stayed silent — the rule documents a form its own slopnator does not refuse`
229
+ : `expected ${rule.id} to stay silent on this source and it reported — the rule refuses the fix it recommends`);
172
230
  }
173
231
  return { id: trial.id, rule: rule.id, passed: failures.length === 0, parsed, failures };
174
232
  }
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
- import { dirname, join } from 'node:path'
1
+ import * as disk from 'node:fs'
2
+ import { answeredBy, existsSync, readFileSync, type Answering, type Kind, type Listed } from '../../core/files.ts'
3
+ import { basename, dirname, join, resolve } from 'node:path'
3
4
  import { pathToFileURL } from 'node:url'
4
5
 
5
6
  import { exit, invoked } from '../../core/invoked.ts'
@@ -7,11 +8,11 @@ import { exit, invoked } from '../../core/invoked.ts'
7
8
  import type { Linter as LinterType, Rule } from 'eslint'
8
9
  import { Linter } from 'eslint'
9
10
 
10
- import type { Constraint } from '../constraint.ts'
11
+ import type { Constraint, Proof, Sourced } from '../constraint.ts'
11
12
 
12
13
  const TREE = findTree()
13
14
  import { CONSTRAINTS } from '../constraint.ts'
14
- import { NOT_WRITTEN, ROOT, governs, load, proofsOf, reads, rules, slopnatorName, sweeps, write, type Byproduct, type Proof, type Slopnator, type Sourced } from '../slopnator.ts'
15
+ import { NOT_WRITTEN, ROOT, governs, load, proofsOf, reads, rules, slopnatorName, sweeps, write, type Byproduct, type Slopnator } from '../slopnator.ts'
15
16
  import { forget } from '../slopnators/sl_unthreaded.ts'
16
17
  import { findTree, inTree } from '../../core/tree.ts'
17
18
 
@@ -60,8 +61,10 @@ export interface Run {
60
61
  readonly measured: number
61
62
  }
62
63
 
64
+ const canonical = (at: string): string => at.replace(/^threads\//, `${TREE.name}/`)
65
+
63
66
  export function probeAt(rule: Constraint): string {
64
- const path = rule.where.paths[0] ?? 'src/'
67
+ const path = canonical(rule.where.paths[0] ?? 'src/')
65
68
  if (path === '.') return PROBE
66
69
  return path.endsWith('/') ? `${path}${PROBE}` : `${path}/${PROBE}`
67
70
  }
@@ -73,7 +76,9 @@ function requiresOf(rule: Constraint, reports: boolean): string {
73
76
  }
74
77
 
75
78
  function scenarioOf(rule: Constraint, proof: Proof): Scenario {
76
- if (typeof proof !== 'string') return proof
79
+ if (typeof proof !== 'string') {
80
+ return { at: canonical(proof.at), files: proof.files.map((one) => ({ ...one, at: canonical(one.at) })) }
81
+ }
77
82
  const path = probeAt(rule)
78
83
  return { files: [{ at: path, source: proof }], at: path }
79
84
  }
@@ -124,11 +129,91 @@ function config(rule: Constraint, module: Rule.RuleModule, parser: unknown): Par
124
129
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
125
130
  },
126
131
  plugins: { rule: { rules: { [rule.id]: module } } },
127
- rules: { [`rule/`]: 'error' },
132
+ rules: { [`rule/${rule.id}`]: 'error' },
128
133
  },
129
134
  ] as unknown as Parameters<LinterType['verify']>[1]
130
135
  }
131
136
 
137
+ const flat = (at: string): string => (at.length > 1 && at.endsWith('/') ? at.replace(/\/+$/, '') : at)
138
+
139
+ const under = (at: string): boolean => at.startsWith(ROOT) || `${at}/` === ROOT
140
+
141
+ const own = (at: string): boolean => under(at) && !at.slice(ROOT.length).split('/').includes('node_modules')
142
+
143
+ function beside(at: string): readonly disk.Dirent[] {
144
+ try {
145
+ return disk.readdirSync(at, { withFileTypes: true })
146
+ } catch {
147
+ return []
148
+ }
149
+ }
150
+
151
+ function outside(at: string): Kind {
152
+ const one = disk.statSync(at, { throwIfNoEntry: false })
153
+ if (one === undefined) return 'none'
154
+ return one.isFile() ? 'file' : one.isDirectory() ? 'folder' : 'other'
155
+ }
156
+
157
+ const GONE = 'is not in the scenario this trial stands, and while it runs the scenario is the whole tree.'
158
+
159
+ function answering(files: readonly Sourced[]): Answering {
160
+ const held = new Map<string, string>(files.map((one) => [resolve(ROOT, one.at), one.source]))
161
+ const folders = new Set<string>()
162
+ const parents = (at: string): void => {
163
+ for (let up = dirname(at); under(`${up}/`); up = dirname(up)) folders.add(up)
164
+ }
165
+ for (const at of held.keys()) parents(at)
166
+
167
+ const answers: Answering = {
168
+ kind: (at) => (held.has(at) ? 'file' : folders.has(at) ? 'folder' : own(at) ? 'none' : outside(at)),
169
+ read: (at) => {
170
+ const said = held.get(at)
171
+ if (said !== undefined) return said
172
+ if (own(at)) throw new Error(`${at} ${GONE}`)
173
+ return disk.readFileSync(at, 'utf8')
174
+ },
175
+ write: (at, said) => {
176
+ held.set(at, said)
177
+ parents(at)
178
+ },
179
+ list: (at) => {
180
+ const inside = new Map<string, Listed>()
181
+ const found = own(at) ? [] : folders.has(at) ? beside(at) : disk.readdirSync(at, { withFileTypes: true })
182
+ for (const one of found) inside.set(one.name, { name: one.name, kind: one.isDirectory() ? 'folder' : 'file' })
183
+ for (const path of folders) if (dirname(path) === at) inside.set(basename(path), { name: basename(path), kind: 'folder' })
184
+ for (const path of held.keys()) if (dirname(path) === at) inside.set(basename(path), { name: basename(path), kind: 'file' })
185
+ return [...inside.values()]
186
+ },
187
+ stat: (at) => {
188
+ const said = held.get(at)
189
+ if (said !== undefined) return { size: said.length, modified: 0 }
190
+ if (folders.has(at)) return { size: 0, modified: 0 }
191
+ if (own(at)) throw new Error(`${at} ${GONE}`)
192
+ const one = disk.statSync(at)
193
+ return { size: one.size, modified: one.mtimeMs }
194
+ },
195
+ remove: (at) => {
196
+ held.delete(at)
197
+ folders.delete(at)
198
+ },
199
+ real: (at) => (own(at) || held.has(at) || folders.has(at) ? at : disk.realpathSync(at)),
200
+ }
201
+
202
+ return {
203
+ kind: (at) => answers.kind(flat(at)),
204
+ read: (at) => answers.read(flat(at)),
205
+ write: (at, said) => {
206
+ answers.write(flat(at), said)
207
+ },
208
+ list: (at) => answers.list(flat(at)),
209
+ stat: (at) => answers.stat(flat(at)),
210
+ remove: (at) => {
211
+ answers.remove(flat(at))
212
+ },
213
+ real: (at) => answers.real(flat(at)),
214
+ }
215
+ }
216
+
132
217
  export function runTrial(trial: Trial, rule: Constraint, module: Rule.RuleModule, parser: unknown, slopnator?: Slopnator): Outcome {
133
218
  const linter = new Linter()
134
219
  const failures: string[] = []
@@ -144,47 +229,19 @@ export function runTrial(trial: Trial, rule: Constraint, module: Rule.RuleModule
144
229
  }
145
230
 
146
231
  const target = trial.scenario.files.find((file) => file.at === trial.scenario.at)
147
- if (target === undefined) {
148
- return {
149
- id: trial.id,
150
- rule: rule.id,
151
- passed: false,
152
- parsed: false,
153
- failures: [`the scenario writes no file at "${trial.scenario.at}", the path it lints, so nothing could have been measured`],
154
- }
155
- }
156
-
157
- for (const file of trial.scenario.files) {
158
- if (!existsSync(join(ROOT, file.at))) continue
159
- return {
160
- id: trial.id,
161
- rule: rule.id,
162
- passed: false,
163
- parsed: false,
164
- failures: [`the scenario would overwrite "${file.at}", which is a real file in this repository — a trial may not stand on a path somebody authored`],
165
- }
166
- }
167
232
 
168
- const written: string[] = []
169
- const created: string[] = []
170
233
  let parsed = false
171
234
  let satisfied = false
172
235
 
173
- try {
174
- for (const file of trial.scenario.files) {
175
- const path = join(ROOT, file.at)
176
- const made = mkdirSync(dirname(path), { recursive: true })
177
- if (made !== undefined) created.push(made)
178
- writeFileSync(path, file.source)
179
- written.push(path)
180
- }
181
- forget()
182
-
183
- const path = join(ROOT, trial.scenario.at)
236
+ answeredBy(answering(trial.scenario.files))
237
+ forget()
184
238
 
239
+ try {
185
240
  if (slopnator !== undefined && !reads(slopnator)) {
186
241
  parsed = true
187
242
  satisfied = slopnator.checks(ROOT).length > 0 === trial.verdict.reports
243
+ } else if (target === undefined) {
244
+ failures.push(`the scenario writes no file at "${trial.scenario.at}", the path it lints, so nothing could have been measured`)
188
245
  } else if (slopnator !== undefined && sweeps(slopnator)) {
189
246
  const held = slopnator.holds(undefined, {
190
247
  filename: trial.scenario.at,
@@ -197,27 +254,29 @@ export function runTrial(trial: Trial, rule: Constraint, module: Rule.RuleModule
197
254
  } else {
198
255
  for (const wrap of WRAPPINGS) {
199
256
  const text = wrap(target.source)
200
- writeFileSync(path, text)
201
257
  const messages = linter.verify(text, config(rule, module, parser), trial.scenario.at)
202
258
  if (messages.some((message) => message.fatal === true)) continue
203
259
  parsed = true
204
- satisfied = messages.some((message) => message.ruleId === `rule/`) === trial.verdict.reports
260
+ satisfied = messages.some((message) => message.ruleId === `rule/${rule.id}`) === trial.verdict.reports
205
261
  break
206
262
  }
207
263
  }
264
+ } catch (thrown) {
265
+ failures.push(
266
+ `this trial could not run: ${thrown instanceof Error ? thrown.message : String(thrown)} — its scenario does not describe a world this rule can be measured against`,
267
+ )
208
268
  } finally {
209
- for (const path of written) if (existsSync(path)) rmSync(path)
210
- for (const path of created) if (existsSync(path)) rmSync(path, { recursive: true })
269
+ answeredBy(undefined)
211
270
  forget()
212
271
  }
213
272
 
214
- if (!parsed) {
273
+ if (failures.length === 0 && !parsed) {
215
274
  failures.push('no wrapping of this source parsed, so nothing was measured — a trial that cannot run is not a trial that passed')
216
- } else if (!satisfied) {
275
+ } else if (failures.length === 0 && !satisfied) {
217
276
  failures.push(
218
277
  trial.verdict.reports
219
- ? `expected to report on this source and it stayed silent — the rule documents a form its own slopnator does not refuse`
220
- : `expected to stay silent on this source and it reported — the rule refuses the fix it recommends`,
278
+ ? `expected ${rule.id} to report on this source and it stayed silent — the rule documents a form its own slopnator does not refuse`
279
+ : `expected ${rule.id} to stay silent on this source and it reported — the rule refuses the fix it recommends`,
221
280
  )
222
281
  }
223
282