@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,14 +1,28 @@
1
1
  import type { Threaded } from '../core/thread.ts'
2
2
  import { named } from '../core/scope.ts'
3
3
 
4
+ export interface Sourced {
5
+ readonly at: string
6
+ readonly source: string
7
+ }
8
+
9
+ export type Proof = string | { readonly files: readonly Sourced[]; readonly at: string }
10
+
11
+ export function shown(proof: Proof): string {
12
+ if (typeof proof === 'string') return proof
13
+ const target = proof.files.find((one) => one.at === proof.at) ?? proof.files[0]
14
+ const beside = proof.files.filter((one) => one.at !== proof.at).map((one) => one.at)
15
+ return `${target?.source ?? ''}${beside.length > 0 ? ` (in a tree holding ${beside.join(', ')})` : ''}`
16
+ }
17
+
4
18
  export interface Constraint extends Threaded {
5
19
  readonly version: number
6
20
  readonly where: { readonly paths: readonly string[]; readonly except?: readonly string[] }
7
21
  readonly bans: string
8
22
  readonly because: string
9
23
  readonly instead: string
10
- readonly wrong: string
11
- readonly right: string
24
+ readonly wrong: Proof
25
+ readonly right: Proof
12
26
  readonly governs?: string
13
27
  readonly severity?: 'forbidden' | 'warning'
14
28
  }
@@ -214,7 +228,14 @@ export const CONSTRAINTS: readonly Constraint[] = [
214
228
  '`declares` is exactly one parent ever, and with two the child\'s owner, its shape and its origin each have two answers while the tree gives none. Measured 2026-08-14: the two rows may sit in different files, so no slopnator that sees one file can find this — check-threads found it only by comparing parents across the whole descent, which is why it was a check rather than a rule. Measured 2026-08-15: this rule read `threads/` and nothing else, so a file outside the tree that declared a child the tree already declares was invisible to it — and a file outside the tree may name children, which is the whole premise `broken-lineage` rests on. It now counts parents from every file the tree reaches, not from one directory',
215
229
  instead:
216
230
  'keep the row that owns it and delete the other. If both are true, one of them is a `derives` or a `seeds`, which may have many parents because neither of them owns the child',
217
- wrong: "const M: Threaded = {\n declares: [\n {\n at: 'threads/one.ts',\n },\n {\n at: 'threads/one.ts',\n },\n ],\n}",
231
+ wrong: {
232
+ files: [
233
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
234
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_parented_row.ts',\n },\n ],\n}\n" },
235
+ { at: 'threads/_probe_parented_row.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n {\n at: 'threads/one.ts',\n },\n {\n at: 'threads/one.ts',\n },\n ],\n}\n" },
236
+ ],
237
+ at: 'threads/_probe_parented_row.ts',
238
+ },
218
239
  right: "const M: Threaded = {\n declares: [\n {\n at: 'threads/one.ts',\n },\n {\n at: 'threads/two.ts',\n },\n ],\n}",
219
240
  cites: [{ on: '2026-08-14', said: 'Cause we need unthreaded, redthread, broken lineage, and the request/produce errors....' }],
220
241
  seeds: [
@@ -275,13 +296,19 @@ export const CONSTRAINTS: readonly Constraint[] = [
275
296
  at: './constraint.ts',
276
297
  describes: 'The refusal of a written address that is not on disk — a file moves and every sentence naming it becomes a confident lie that still reads as correct, and counting those without judging them is worse than never scanning, because the number reads as a result.',
277
298
  version: 1,
278
- where: { paths: ['.'], except: ['slopnators/', '_generated/', 'thread/integrity/sealed.ts'] },
299
+ where: { paths: ['.'], except: ['slopnators/', '_generated/'] },
279
300
  bans: 'a string naming a path in this repository that is not on disk',
280
301
  because:
281
302
  'measured 2026-08-10: 91 distinct paths across 240 mentions named files that had moved under `packages/core/`, and every one of them read as correct. Measured 2026-08-14: paths:check scanned 505 files, found 59 more and judged none of them, printing "no findings" while holding the count — a class counted and unjudged is worse than one never scanned, because the number reads as a result',
282
303
  instead:
283
304
  'fix the path, or delete the sentence naming it. A path that carries a `{variable}` states the directory it writes into, and that directory must exist',
284
- wrong: "const at = 'threads/nope.ts'",
305
+ wrong: {
306
+ files: [
307
+ { at: 'threads/_probe_there.ts', source: 'export const THERE = 1\n' },
308
+ { at: 'probe.tsx', source: "const at = 'threads/nope.ts'\n" },
309
+ ],
310
+ at: 'probe.tsx',
311
+ },
285
312
  right: "const at = 'packages/core/integrity/src/constraint.ts'",
286
313
  cites: [{ on: '2026-08-14', said: 'I want you to transform every single thing that currently is a check into a law, so we can retire checks and we can have SSOT for laws, and slopnators only for execution.' }],
287
314
  seeds: [
@@ -347,7 +374,13 @@ export const CONSTRAINTS: readonly Constraint[] = [
347
374
  because:
348
375
  'the gate is the commands these name, so one that package.json lacks fails as `Command "nope:check" not found` at the moment it is reached — after every step before it has already run, and only when somebody runs the gate. Measured 2026-08-14: sixteen rows name sixteen commands and nothing has ever compared them against package.json; they are correct today by nobody having mistyped one',
349
376
  instead: 'add the script to package.json, or fix the name. A row naming a command is a promise that the command exists',
350
- wrong: "const M = {\n runs: 'pnpm nope:check',\n}",
377
+ wrong: {
378
+ files: [
379
+ { at: 'package.json', source: '{\n "scripts": {\n "typecheck": "tsc"\n }\n}\n' },
380
+ { at: 'threads/_probe_command.ts', source: "const M = {\n runs: 'pnpm nope:check',\n}\n" },
381
+ ],
382
+ at: 'threads/_probe_command.ts',
383
+ },
351
384
  right: "const M = {\n runs: 'pnpm typecheck',\n}",
352
385
  cites: [{ on: '2026-08-14', said: 'I think you need to study how a file gets parented and make proper error handling for everything. No at is just one.' }],
353
386
  seeds: [
@@ -379,7 +412,13 @@ export const CONSTRAINTS: readonly Constraint[] = [
379
412
  'a rule is a row and a slopnator is a function, and nothing but their names holds the two together — so either half can go missing and the other reads as working. A rule whose slopnator is absent registers, loads, reports nothing, and is indistinguishable from a rule being obeyed. A slopnator no rule names is code that runs in no gate and governs nothing, while looking like enforcement to anybody reading the directory. Measured 2026-08-15: this rule was real and enforced, but it lived as a gate step in ci.ts rather than as a rule — one more place where a guarantee could be turned off by editing a list, which is the thing the rules exist to stop. Moving it here deletes the step rather than adding a check',
380
413
  instead: 'write the rule that needs the slopnator, or delete the slopnator. `pnpm slopnator:seed` writes the scaffold for a rule that has none, and a rename is a `git mv` rather than a second file',
381
414
  wrong: "const CONSTRAINTS = [\n {\n id: 'nothing-names-this-slopnator',\n },\n]",
382
- right: "const CONSTRAINTS = [\n {\n id: 'no-comment',\n },\n]",
415
+ right: {
416
+ files: [
417
+ { at: 'threads/_probe_paired.ts', source: "const CONSTRAINTS = [\n {\n id: 'no-comment',\n },\n]\n" },
418
+ { at: 'threads/slopnators/sl_no_comment.ts', source: 'export const slopnator = { on: [], holds: () => false }\n' },
419
+ ],
420
+ at: 'threads/_probe_paired.ts',
421
+ },
383
422
  cites: [
384
423
  { on: '2026-08-14', said: 'A law is written, names a slopnator. Slopnator is seeded magically. Someone write the function. An enforcer picks it up and it just works.' },
385
424
  { on: '2026-08-15', said: 'Does a slopnator without a law or a law without a slopnator exist? This should be an error....' },
@@ -413,7 +452,14 @@ export const CONSTRAINTS: readonly Constraint[] = [
413
452
  'the glossary is derived: a word\'s meaning is the `describes` on the row that coins it, so a word coined twice with two descriptions has two meanings and the file can hold one. Repetition is not the defect — measured 2026-08-15, the tree coins `slopnator` on twelve rows and `trial` on twelve, deliberately, because the founder asked for the category in the glossary rather than each instance under its own name. Disagreement is the defect: those same twelve rows each stated a different sentence, so the tree asserted twelve meanings for one word and nothing looked. Five words were split this way — `slopnator`, `trial`, `ci`, `build` and `glossary` — and three of them were the same sentence written twice, which is the SSOT failure wearing a second coat',
414
453
  instead:
415
454
  'state the category meaning on every row that coins the word, because the word is what the row names. Where two rows describe the same file from different heights, the row that declares it wins and the node repeats it. Where a row needs to say something only true of its own instance, it needs its own id — and if you do not want that word in the glossary, the sentence does not belong on the row',
416
- wrong: "const M: Threaded = {\n declares: [\n {\n id: 'one',\n describes: 'The first thing.',\n },\n {\n id: 'one',\n describes: 'A different thing entirely.',\n },\n ],\n}",
455
+ wrong: {
456
+ files: [
457
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
458
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_meant_row.ts',\n },\n ],\n}\n" },
459
+ { at: 'threads/_probe_meant_row.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n {\n id: 'one',\n describes: 'The first thing.',\n },\n {\n id: 'one',\n describes: 'A different thing entirely.',\n },\n ],\n}\n" },
460
+ ],
461
+ at: 'threads/_probe_meant_row.ts',
462
+ },
417
463
  right: "const M: Threaded = {\n declares: [\n {\n id: 'one',\n describes: 'The first thing.',\n },\n {\n id: 'one',\n describes: 'The first thing.',\n },\n ],\n}",
418
464
  cites: [
419
465
  { on: '2026-08-15', said: 'the ids thing. We can\'t have that, the id\'s being the same.' },
@@ -592,8 +638,24 @@ export const CONSTRAINTS: readonly Constraint[] = [
592
638
  'measured 2026-08-25: nothing anywhere holds a dispatched name against a declaration. `packages/ui/src/bindings.tsx` exposes `run(workflow: string, input?)`, and that workflow is a bare string passed to a context the UI package never sees — so a name that matches no operation reaches the browser and fails at whatever the host does with an unknown id. The second half is measured from the other side: `dispatch` in packages/core/operations/src/dispatch.ts takes an Operation object rather than a name, so no shipped call site names an operation as a string at all, and the only place a name was written down was that `run`. Measured again the same day, after `packages/ui/src/bindings.tsx` was deleted: `dispatch` is now a seam on the UI config, named once, handed a name — so from here on a dispatched name is always written where it can be read, and this rule is what holds it to a declaration. A name assembled at runtime is the same hole with a lid on it — it reads from every check exactly like a name that was checked and found',
593
639
  instead:
594
640
  'write the name where it can be read, and declare the operation it names — `dispatch(\'wf_send_invoice\')` against a row whose `at` is `operations/wf_send_invoice.ts`. Where the name genuinely varies, the choosing belongs in the declaration, not in a template string',
595
- wrong: "dispatch('wf_not_declared')",
596
- right: "dispatch('wf_send_invoice')",
641
+ wrong: {
642
+ files: [
643
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
644
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_operations.ts',\n },\n ],\n}\n" },
645
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n at: 'operations/wf_send_invoice.ts',\n },\n ],\n}\n" },
646
+ { at: 'app/_probe_wrong.tsx', source: "dispatch('wf_not_declared')" },
647
+ ],
648
+ at: 'app/_probe_wrong.tsx',
649
+ },
650
+ right: {
651
+ files: [
652
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
653
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_operations.ts',\n },\n ],\n}\n" },
654
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n at: 'operations/wf_send_invoice.ts',\n },\n ],\n}\n" },
655
+ { at: 'app/_probe_right.tsx', source: "dispatch('wf_send_invoice')" },
656
+ ],
657
+ at: 'app/_probe_right.tsx',
658
+ },
597
659
  cites: [
598
660
  { on: '2026-08-25', said: 'And make sure the composition can only dispatch existing operations.' },
599
661
  { on: '2026-08-25', said: 'Yes — reading a view is dispatching too' },
@@ -629,8 +691,24 @@ export const CONSTRAINTS: readonly Constraint[] = [
629
691
  'measured 2026-08-25: the founder wrote `<Button.primary text="translation.key" />` as the shape a composition takes, and the fourteen variants in packages/ui/src/variants.ts hold no row `primary` — the one that fits is `button-primary`. `element()` in packages/ui/src/wrappers.tsx resolves any member through a proxy, so that call renders a button carrying the class `thread-primary`, which the stylesheet generator never writes because no variant of that name exists to write it from. It renders, it is unstyled, and nothing in the repository says a word. The pairing half is the same hole from the other side: `card` declares fifteen elements that wear it and `Button` is not among them, and nothing stops `<Button.card />` either. Both are the silent-pass shape — a check reporting success because it measured nothing',
630
692
  instead:
631
693
  'declare the variant, and name the element in that row\'s `wears` — the row is what makes the pairing legal, which is why `<Button.buttonPrimary />` holds and `<Button.primary />` does not',
632
- wrong: '<Button.primary />',
633
- right: '<Button.buttonPrimary />',
694
+ wrong: {
695
+ files: [
696
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
697
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_worn.ts',\n },\n ],\n}\n" },
698
+ { at: 'threads/_probe_worn.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const ELEMENTS: Threaded = {\n id: 'element',\n declares: [\n {\n id: 'button',\n at: 'app/_probe_button.tsx',\n },\n ],\n}\n\nexport const VARIANTS: Threaded = {\n id: 'variant',\n declares: [\n {\n id: 'button-primary',\n at: 'app/_probe_variant.ts',\n wears: ['Button'],\n },\n ],\n}\n" },
699
+ { at: 'app/_probe_wrong.tsx', source: '<Button.primary />' },
700
+ ],
701
+ at: 'app/_probe_wrong.tsx',
702
+ },
703
+ right: {
704
+ files: [
705
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
706
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_worn.ts',\n },\n ],\n}\n" },
707
+ { at: 'threads/_probe_worn.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const ELEMENTS: Threaded = {\n id: 'element',\n declares: [\n {\n id: 'button',\n at: 'app/_probe_button.tsx',\n },\n ],\n}\n\nexport const VARIANTS: Threaded = {\n id: 'variant',\n declares: [\n {\n id: 'button-primary',\n at: 'app/_probe_variant.ts',\n wears: ['Button'],\n },\n ],\n}\n" },
708
+ { at: 'app/_probe_right.tsx', source: '<Button.buttonPrimary />' },
709
+ ],
710
+ at: 'app/_probe_right.tsx',
711
+ },
634
712
  cites: [
635
713
  { on: '2026-08-25', said: 'Variants themselves declare what elements they relate to, in their declarations. Then the composition says button.primary and if that matches the combination of variant and element it\'s acceptable.' },
636
714
  { on: '2026-08-25', said: 'If there\'s no primary variant.....that\' an error moron. Didn\'t I said that it can\'t name a variant that doesn\'t exist?' },
@@ -702,8 +780,22 @@ export const CONSTRAINTS: readonly Constraint[] = [
702
780
  'a member is added by declaring it and then written, never the other way round — the same order a rule follows to its slopnator, and for the same reason: the row is what everything downstream reads, so a file that arrived without one is a thing the runtime, the API, the agent tools and the trials have all never heard of, while it sits there able to act. The two halves are held together by nothing but a name, so either can go missing and the other reads as working. Absence is the finding in both directions because there is no third place to look: the rows are what exists, the folder is what was written, and any gap between them is one somebody has to close on purpose. The rule reaches by shape and not by name — it reads every island that seeds its members from the tree, so an island added tomorrow is governed without editing this row',
703
781
  instead:
704
782
  'add the row to the island\'s tree file first and run `pnpm member:seed`, which writes the file that row brings into existence — then write the member into the template it left',
705
- wrong: "export const OPERATIONS: readonly Member[] = [\n {\n id: 'wf_nothing_seeded_this',\n },\n]",
706
- right: "export const OPERATIONS: readonly Member[] = []",
783
+ wrong: {
784
+ files: [
785
+ { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'operations', at: 'src/islands/operations.ts', governs: ['operations/'] },\n { id: 'operation', at: 'threads/operations.ts' },\n ],\n}\n" },
786
+ { at: 'src/islands/operations.ts', source: 'export const OperationsIslandSchema = {}\n' },
787
+ { at: 'threads/operations.ts', source: "export const OPERATIONS: readonly Threaded[] = [\n {\n id: 'wf_nothing_seeded_this',\n },\n]\n" },
788
+ ],
789
+ at: 'threads/operations.ts',
790
+ },
791
+ right: {
792
+ files: [
793
+ { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'operations', at: 'src/islands/operations.ts', governs: ['operations/'] },\n { id: 'operation', at: 'threads/operations.ts' },\n ],\n}\n" },
794
+ { at: 'src/islands/operations.ts', source: 'export const OperationsIslandSchema = {}\n' },
795
+ { at: 'threads/operations.ts', source: 'export const OPERATIONS: readonly Threaded[] = []\n' },
796
+ ],
797
+ at: 'threads/operations.ts',
798
+ },
707
799
  cites: [
708
800
  { on: '2026-08-17', said: 'FIRST we add the new row in workflows.ts, which seeds a new wf_{id} file in a workflows folder.' },
709
801
  { on: '2026-08-17', said: 'The file is not authored but seeded. We can write the operation after, but it must have a template with instructions maybe of how to build. Like slopnators.' },
@@ -771,7 +863,14 @@ export const CONSTRAINTS: readonly Constraint[] = [
771
863
  because:
772
864
  'the loader opens the paths an island governs and nothing else, so a declaration written beside the code that needs it is not a smaller declaration — it is no declaration, and every surface downstream reports success over it: it typechecks, the editor completes it, and the generator emits nothing. This is one rule rather than one per island because the rule never differed — measured 2026-08-17: the same sentence was written eleven times, and the island-to-path fact it depends on appeared 130 times across 56 authored files, so eleven copies of a rule drifted against a fact stated everywhere. Here the island states what it governs once, and the slopnator reads it. The call it looks for is read out of the island itself rather than derived from its name, because no convention survives contact with the real set: `sql` exposes `defineSqlFunctions` and `navigation` exposes `defineMap`. An island that governs only folders is one whose kind is derived and authored nowhere, so the call is refused everywhere — which is what `AUTHORED_MAP` did for the map at build.ts:150, now stated on the island instead of in the CLI',
773
865
  instead: 'declare it in the file the island governs, and import it where it is needed',
774
- wrong: "export default definePermissions({ specVersion: 1, entities: [{ entity: 'ent_order' }] })",
866
+ wrong: {
867
+ files: [
868
+ { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'permissions', at: 'threads/_island_permissions.ts', governs: ['app/_declare/permissions.ts'] },\n ],\n}\n" },
869
+ { at: 'threads/_island_permissions.ts', source: 'export const PermissionsIslandSchema = {}\nexport function definePermissions() {}\n' },
870
+ { at: 'probe.tsx', source: "export default definePermissions({ specVersion: 1, entities: [{ entity: 'ent_order' }] })\n" },
871
+ ],
872
+ at: 'probe.tsx',
873
+ },
775
874
  right: "import permissions from 'app/_declare/permissions'",
776
875
  cites: [
777
876
  { on: '2026-08-17', said: 'If there\'s something that is govern by an island but it\'s propelry done through island, error.' },
@@ -805,8 +904,20 @@ export const CONSTRAINTS: readonly Constraint[] = [
805
904
  because:
806
905
  'what an island governs is the one fact about it that nothing else can supply. It is not derivable from the island\'s name — measured 2026-08-17: `navigation` governs `app/screens/` and its schema lives in `map.ts`, and `sql` exposes `defineSqlFunctions`, so two of thirteen break any convention a generator could apply. Stated as nothing, the rule that refuses a declaration elsewhere has no elsewhere to refuse: no file is the place its kind belongs, which makes every file the place. The island still parses, still typechecks, still appears in the tree, and governs nobody — inert rather than absent, which is the harder kind to notice',
807
906
  instead: 'state what the island governs — a file where its kind is declared, or a folder whose implementation it governs',
808
- wrong: "const M: Islanding = { declares: [{ id: 'events', at: 'packages/core/spec/src/islands/events.ts' }] }",
809
- right: "const M: Islanding = { declares: [{ id: 'events', at: 'packages/core/spec/src/islands/events.ts', governs: ['app/_declare/events.ts'] }] }",
907
+ wrong: {
908
+ files: [
909
+ { at: 'threads/_inert_probe.ts', source: "export const M: Threaded = {\n declares: [\n { id: 'events', at: 'packages/core/spec/src/islands/events.ts' },\n ],\n}\n" },
910
+ { at: 'packages/core/spec/src/islands/events.ts', source: 'export const EventsIslandSchema = {}\n' },
911
+ ],
912
+ at: 'threads/_inert_probe.ts',
913
+ },
914
+ right: {
915
+ files: [
916
+ { at: 'threads/_inert_probe.ts', source: "export const M: Threaded = {\n declares: [\n { id: 'events', at: 'packages/core/spec/src/islands/events.ts', governs: ['app/_declare/events.ts'] },\n ],\n}\n" },
917
+ { at: 'packages/core/spec/src/islands/events.ts', source: 'export const EventsIslandSchema = {}\n' },
918
+ ],
919
+ at: 'threads/_inert_probe.ts',
920
+ },
810
921
  cites: [
811
922
  { on: '2026-08-17', said: 'An island extends threader. It declares what it governs.' },
812
923
  { on: '2026-08-17', said: 'If there\'s an island that doesn\'t state what it governs, error.' },
@@ -875,7 +986,14 @@ export const CONSTRAINTS: readonly Constraint[] = [
875
986
  instead:
876
987
  'name an island the tree declares, or drop the `governs` — a rule about a kind nobody can declare is a rule about nothing',
877
988
  wrong: "export const L: Constraint = {\n id: 'a-rule',\n governs: 'no-island-by-this-name',\n}",
878
- right: "export const L: Constraint = {\n id: 'a-rule',\n governs: 'entities',\n}",
989
+ right: {
990
+ files: [
991
+ { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'entities', at: 'threads/_island_entities.ts', governs: ['app/_declare/entities.ts'] },\n ],\n}\n" },
992
+ { at: 'threads/_island_entities.ts', source: 'export const EntitiesIslandSchema = {}\nexport function defineEntities() {}\n' },
993
+ { at: 'threads/_probe_governed.ts', source: "export const L: Constraint = {\n id: 'a-rule',\n governs: 'entities',\n}\n" },
994
+ ],
995
+ at: 'threads/_probe_governed.ts',
996
+ },
879
997
  cites: [
880
998
  { on: '2026-08-16', said: 'Here\'s a new law I need you to add: No island without law.' },
881
999
  { on: '2026-08-16', said: 'So also we need a "law doesn\'t match island" if this is to be done entirely by hand so we catch drifts.' },
@@ -1045,8 +1163,24 @@ export const CONSTRAINTS: readonly Constraint[] = [
1045
1163
  because:
1046
1164
  'measured 2026-08-22: `OperationNode` stated `node` as a bare string, so a body naming `nd_teleport` compiled, loaded and dispatched, and nothing anywhere compared that name against a declared node. The first reading of this rule matched that string. Measured again 2026-08-26, after a node began building its own span: an operation writes no such string any more — it imports a node and calls it — so the reading matched nothing on any real file, and its own proofs were written in the same retired shape, so it passed its trial while measuring nothing. Both halves drifted together, which is the only way a rule goes quiet without anybody noticing. Proved by planting a node body no row declares and running a real app: it ran, answered `ok`, and the sweep named the file for being unthreaded and said nothing about the operation built from it. What an operation says today is the import it reaches its node through, so that is what is read — resolved to a path and checked against the descent, which also catches an import that resolves to nothing at all, where the only other answer was a module-not-found at run time. Measured a third time the same day, an hour after that reading shipped: it matched `import … from` and nothing else, so `await import(...)`, `require(...)` and `export … from` each walked through it, each tried against a real tree and each unremarked. Twice now this rule has enumerated a form instead of stating its rule, which is how a refusal ends up being a list of whatever somebody happened to try. The rule is about the PATH: a string that resolves under `nodes/` names a node, and a node the descent never reaches is a step nobody agreed to — whether it arrives by import, by require, by re-export, or sitting in a const waiting to be used',
1047
1165
  instead: 'declare the node in the tree and then build from it. What an app can be asked to do grows by declaring a node, never by importing a file nothing brought into existence',
1048
- wrong: "import { node as ghost } from '../nodes/nd_ghost.ts'",
1049
- right: "import { node as read } from '../nodes/nd_read.ts'",
1166
+ wrong: {
1167
+ files: [
1168
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
1169
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_nodes.ts',\n },\n ],\n}\n" },
1170
+ { at: 'threads/_probe_nodes.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const NODES: Threaded = {\n id: 'nodes',\n declares: [\n {\n id: 'read',\n at: 'nodes/nd_read.ts',\n },\n ],\n}\n" },
1171
+ { at: 'operations/_probe_wrong.ts', source: "import { node as ghost } from '../nodes/nd_ghost.ts'" },
1172
+ ],
1173
+ at: 'operations/_probe_wrong.ts',
1174
+ },
1175
+ right: {
1176
+ files: [
1177
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
1178
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_nodes.ts',\n },\n ],\n}\n" },
1179
+ { at: 'threads/_probe_nodes.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const NODES: Threaded = {\n id: 'nodes',\n declares: [\n {\n id: 'read',\n at: 'nodes/nd_read.ts',\n },\n ],\n}\n" },
1180
+ { at: 'operations/_probe_right.ts', source: "import { node as read } from '../nodes/nd_read.ts'" },
1181
+ ],
1182
+ at: 'operations/_probe_right.ts',
1183
+ },
1050
1184
  cites: [
1051
1185
  { on: '2026-08-22', said: 'Operations are bilt of nodes, only nodes that are declared.' },
1052
1186
  { on: '2026-08-22', said: 'That\'s why it grows. We declare, operations can do more.' },
@@ -1080,8 +1214,26 @@ export const CONSTRAINTS: readonly Constraint[] = [
1080
1214
  because:
1081
1215
  'measured 2026-08-24: a prefix is not bound to a file. Both tree files export a list called `MEMBERS`, both are arrays of rows carrying a string id, and nothing in TypeScript or in any check separated them — a node id placed in the operations list typechecked, attached, seeded a body and read as agreed, and an operation id placed in the nodes list did the same. The prefix is the only thing that says which of the two a row is, and it was decoration. Measured again 2026-08-25, after the first reading of this was deleted for a different reason: the same gap was still open, and the founder named it as one of five mismatches against their stated requirements. Depth is what the reading turns on — a member row states its own id, and the `seeds` and `derives` rows beneath it state theirs, which are bodies rather than operations or nodes, so `wf_send_invoice_body` carries no prefix and must not be refused for it. Only rows sitting directly in the list are read',
1082
1216
  instead: 'name the row for what it is. An operation is `wf_`, `vw_` or `agt_` and belongs in the operations tree; a node is `nd_` and belongs in the nodes tree. A row in the wrong list is not a naming slip, it is a declaration of the wrong thing',
1083
- wrong: "export const MEMBERS: readonly Member[] = [\n {\n id: 'nd_teleport',\n at: '_threads/operations.ts',\n describes: 'a node wearing an operation list',\n version: 1,\n },\n]",
1084
- right: "export const MEMBERS: readonly Member[] = [\n {\n id: 'wf_send_invoice',\n at: '_threads/operations.ts',\n describes: 'sends an invoice',\n version: 1,\n },\n]",
1217
+ wrong: {
1218
+ files: [
1219
+ {
1220
+ at: 'threads/operations.ts',
1221
+ source:
1222
+ "export const MEMBERS: readonly Member[] = [\n {\n id: 'nd_teleport',\n at: '_threads/operations.ts',\n describes: 'a node wearing an operation list',\n version: 1,\n },\n]\n",
1223
+ },
1224
+ ],
1225
+ at: 'threads/operations.ts',
1226
+ },
1227
+ right: {
1228
+ files: [
1229
+ {
1230
+ at: 'threads/operations.ts',
1231
+ source:
1232
+ "export const MEMBERS: readonly Member[] = [\n {\n id: 'wf_send_invoice',\n at: '_threads/operations.ts',\n describes: 'sends an invoice',\n version: 1,\n },\n]\n",
1233
+ },
1234
+ ],
1235
+ at: 'threads/operations.ts',
1236
+ },
1085
1237
  cites: [
1086
1238
  { on: '2026-08-24', said: 'operations.ts, where it declares nodes.ts and dispatch.ts, and member rows in operations are wf_,vw_ and agt_ files.' },
1087
1239
  { on: '2026-08-25', said: '3 fix it please.' },
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { attached, readdirSync, readFileSync, writeFileSync } from "../../core/files.js";
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { argv, execPath, stderr, stdout } from 'node:process';
@@ -114,6 +114,12 @@ if (argv.includes('--check')) {
114
114
  catch (cause) {
115
115
  refuse(`the compiler is not installed — ${String(cause)}. \`typescript\` is a dependency this app declares, so \`npm install\` is what puts it there.`);
116
116
  }
117
+ if (attached()) {
118
+ stderr.write('compiler: NOT RUN. The typecheck is a separate compiler process, so it reads the disk of the machine this is\n' +
119
+ 'running on rather than the tree being answered for. It would report on the wrong files, and a check that\n' +
120
+ 'passed against the wrong machine says nothing about this one.\n');
121
+ exit(1);
122
+ }
117
123
  const built = spawnSync(execPath, [cli, '--noEmit'], { cwd: ROOT, stdio: 'inherit' });
118
124
  if (built.error !== undefined)
119
125
  refuse(`the compiler could not be started — ${built.error.message}.`);
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process'
2
- import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { attached, readdirSync, readFileSync, writeFileSync } from '../../core/files.ts'
3
3
  import { createRequire } from 'node:module'
4
4
  import { dirname, join, resolve } from 'node:path'
5
5
  import { argv, execPath, stderr, stdout } from 'node:process'
@@ -142,6 +142,15 @@ if (argv.includes('--check')) {
142
142
  )
143
143
  }
144
144
 
145
+ if (attached()) {
146
+ stderr.write(
147
+ 'compiler: NOT RUN. The typecheck is a separate compiler process, so it reads the disk of the machine this is\n' +
148
+ 'running on rather than the tree being answered for. It would report on the wrong files, and a check that\n' +
149
+ 'passed against the wrong machine says nothing about this one.\n',
150
+ )
151
+ exit(1)
152
+ }
153
+
145
154
  const built = spawnSync(execPath, [cli, '--noEmit'], { cwd: ROOT, stdio: 'inherit' })
146
155
  if (built.error !== undefined) refuse(`the compiler could not be started — ${built.error.message}.`)
147
156
  exit(built.status ?? 1)
@@ -1,26 +1,19 @@
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
2
  import { createHash } from 'node:crypto';
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
- import { dirname, join } from 'node:path';
3
+ import { readFileSync as bytesAt } from 'node:fs';
4
+ import { join } from 'node:path';
13
5
  import { argv, execPath, stderr, stdout } from 'node:process';
14
- import { pathToFileURL } from 'node:url';
15
6
  import { descend } from "../../core/descend.js";
7
+ import { attached, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "../../core/files.js";
16
8
  import { LOADER, exit } from "../../core/invoked.js";
9
+ import { valuesIn } from "../../core/rows.js";
17
10
  import { ORGANISATION, ours } from "../../core/scope.js";
18
11
  import { findTree, inTree } from "../../core/tree.js";
19
12
  const TREE = findTree();
20
13
  const ROOT = TREE.at;
21
14
  const STEP_TIMEOUT = 600_000;
22
- const { GATE } = (await import(__rewriteRelativeImportExtension(pathToFileURL(inTree(TREE, 'gate.ts')).href)));
23
- const { GENERATORS } = (await import(__rewriteRelativeImportExtension(pathToFileURL(inTree(TREE, 'derivation.ts')).href)));
15
+ const { GATE } = valuesIn(readFileSync(inTree(TREE, 'gate.ts')));
16
+ const { GENERATORS } = valuesIn(readFileSync(inTree(TREE, 'derivation.ts')));
24
17
  function refuse(why) {
25
18
  stderr.write(`gate: ${why}\n`);
26
19
  exit(1);
@@ -51,7 +44,7 @@ function steps() {
51
44
  const writers = [
52
45
  ...rowsOf(GATE.declares),
53
46
  ...rowsOf(GENERATORS),
54
- ...[...walked.threaded.values()].flat().filter((one) => one.list === 'declares').map((one) => one.row),
47
+ ...[...walked.threaded.values()].flat().filter((one) => one.list === 'declares' && one.row.by === undefined).map((one) => one.row),
55
48
  ...[...walked.stated.values()].map((one) => one),
56
49
  ];
57
50
  const written = new Map();
@@ -106,10 +99,18 @@ if (chosen.length === 0) {
106
99
  exit(1);
107
100
  }
108
101
  const failed = [];
102
+ const unmeasured = [];
109
103
  let passed = 0;
110
104
  for (const [index, step] of chosen.entries()) {
111
105
  const at = `step ${String(index + 1)} of ${String(chosen.length)}`;
112
106
  stdout.write(`\ngate: ${at} — ${step.runs} (${step.id})\n`);
107
+ if (attached() && step.runs.trim().split(/\s+/)[0] !== 'thread') {
108
+ unmeasured.push(step);
109
+ stdout.write(`gate: ${at} — NOT RUN. \`${step.runs}\` is not a thread command, so it reads the disk of the machine this is\n` +
110
+ `running on rather than the tree being answered for. It would report on the wrong files, and a step that\n` +
111
+ `passed against the wrong machine says nothing about this one.\n`);
112
+ continue;
113
+ }
113
114
  const code = ran(step.runs);
114
115
  stdout.write(`gate: ${at} — exited ${String(code)}\n`);
115
116
  if (code === 0)
@@ -127,7 +128,7 @@ function fingerprint() {
127
128
  const hash = (at) => {
128
129
  if (!existsSync(join(ROOT, at)))
129
130
  return;
130
- of[at] = createHash('sha256').update(readFileSync(join(ROOT, at))).digest('hex').slice(0, 16);
131
+ of[at] = createHash('sha256').update(bytesAt(join(ROOT, at))).digest('hex').slice(0, 16);
131
132
  };
132
133
  const sheets = join(ROOT, '_threads');
133
134
  if (existsSync(sheets)) {
@@ -154,8 +155,12 @@ function fingerprint() {
154
155
  }
155
156
  const RECEIPT_AT = join('.thread', 'gate.json');
156
157
  const RECEIPT = join(ROOT, RECEIPT_AT);
157
- if (failed.length === 0 && dropped.length === 0) {
158
- mkdirSync(dirname(RECEIPT), { recursive: true });
158
+ if (unmeasured.length > 0) {
159
+ stdout.write(`gate: ${String(unmeasured.length)} step(s) could not be measured here — ${unmeasured.map((step) => step.id).join(', ')}. Each one\n` +
160
+ `runs a tool that is not part of this framework, so it reads whatever disk it was started on. This is not a\n` +
161
+ `gate pass, and no receipt is written for a run that left them unanswered.\n`);
162
+ }
163
+ if (failed.length === 0 && dropped.length === 0 && unmeasured.length === 0) {
159
164
  writeFileSync(RECEIPT, `${JSON.stringify({ passed: chosen.length, ...fingerprint() }, null, 2)}\n`, 'utf8');
160
165
  stdout.write(`gate: wrote ${RECEIPT_AT} — what compiles is checked against this, so a tree that skipped the gate does not build.\n`);
161
166
  }
@@ -164,7 +169,7 @@ else if (existsSync(RECEIPT)) {
164
169
  stdout.write(`gate: removed ${RECEIPT_AT} — the tree no longer passes, so nothing may compile against an older run.\n`);
165
170
  }
166
171
  if (failed.length === 0)
167
- exit(0);
172
+ exit(unmeasured.length === 0 ? 0 : 1);
168
173
  stderr.write(`gate: FAILED — ${String(failed.length)} of ${String(chosen.length)} step(s). The other ${String(passed)} ran and passed; a failing step stops nothing.\n`);
169
174
  for (const step of failed)
170
175
  stderr.write(` - ${step.id} — run it alone with \`${step.runs}\`\n`);
@@ -1,11 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process'
2
2
  import { createHash } from 'node:crypto'
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { readFileSync as bytesAt } from 'node:fs'
4
4
  import { dirname, join } from 'node:path'
5
5
  import { argv, execPath, stderr, stdout } from 'node:process'
6
- import { fileURLToPath, pathToFileURL } from 'node:url'
6
+ import { fileURLToPath } from 'node:url'
7
7
  import { descend } from '../../core/descend.ts'
8
+ import { attached, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from '../../core/files.ts'
8
9
  import { LOADER, exit } from '../../core/invoked.ts'
10
+ import { valuesIn } from '../../core/rows.ts'
9
11
  import { ORGANISATION, ours } from '../../core/scope.ts'
10
12
  import { findTree, inTree } from '../../core/tree.ts'
11
13
 
@@ -14,8 +16,8 @@ const ROOT = TREE.at
14
16
 
15
17
  const STEP_TIMEOUT = 600_000
16
18
 
17
- const { GATE } = (await import(pathToFileURL(inTree(TREE, 'gate.ts')).href)) as { GATE: { derives?: unknown; declares?: unknown } }
18
- const { GENERATORS } = (await import(pathToFileURL(inTree(TREE, 'derivation.ts')).href)) as { GENERATORS: readonly Record<string, unknown>[] }
19
+ const { GATE } = valuesIn(readFileSync(inTree(TREE, 'gate.ts'))) as { GATE: { derives?: unknown; declares?: unknown } }
20
+ const { GENERATORS } = valuesIn(readFileSync(inTree(TREE, 'derivation.ts'))) as { GENERATORS: readonly Record<string, unknown>[] }
19
21
 
20
22
  function refuse(why: string): never {
21
23
  stderr.write(`gate: ${why}\n`)
@@ -66,7 +68,7 @@ function steps(): readonly Step[] {
66
68
  const writers: readonly Row[] = [
67
69
  ...rowsOf(GATE.declares),
68
70
  ...rowsOf(GENERATORS),
69
- ...[...walked.threaded.values()].flat().filter((one) => one.list === 'declares').map((one) => one.row as Row),
71
+ ...[...walked.threaded.values()].flat().filter((one) => one.list === 'declares' && (one.row as Row).by === undefined).map((one) => one.row as Row),
70
72
  ...[...walked.stated.values()].map((one) => one as Row),
71
73
  ]
72
74
 
@@ -132,12 +134,23 @@ if (chosen.length === 0) {
132
134
  }
133
135
 
134
136
  const failed: Step[] = []
137
+ const unmeasured: Step[] = []
135
138
  let passed = 0
136
139
 
137
140
  for (const [index, step] of chosen.entries()) {
138
141
  const at = `step ${String(index + 1)} of ${String(chosen.length)}`
139
142
  stdout.write(`\ngate: ${at} — ${step.runs} (${step.id})\n`)
140
143
 
144
+ if (attached() && step.runs.trim().split(/\s+/)[0] !== 'thread') {
145
+ unmeasured.push(step)
146
+ stdout.write(
147
+ `gate: ${at} — NOT RUN. \`${step.runs}\` is not a thread command, so it reads the disk of the machine this is\n` +
148
+ `running on rather than the tree being answered for. It would report on the wrong files, and a step that\n` +
149
+ `passed against the wrong machine says nothing about this one.\n`,
150
+ )
151
+ continue
152
+ }
153
+
141
154
  const code = ran(step.runs)
142
155
 
143
156
  stdout.write(`gate: ${at} — exited ${String(code)}\n`)
@@ -156,7 +169,7 @@ function fingerprint(): { readonly of: Record<string, string>; readonly versions
156
169
  const of: Record<string, string> = {}
157
170
  const hash = (at: string): void => {
158
171
  if (!existsSync(join(ROOT, at))) return
159
- of[at] = createHash('sha256').update(readFileSync(join(ROOT, at))).digest('hex').slice(0, 16)
172
+ of[at] = createHash('sha256').update(bytesAt(join(ROOT, at))).digest('hex').slice(0, 16)
160
173
  }
161
174
  const sheets = join(ROOT, '_threads')
162
175
  if (existsSync(sheets)) {
@@ -181,8 +194,15 @@ function fingerprint(): { readonly of: Record<string, string>; readonly versions
181
194
  const RECEIPT_AT = join('.thread', 'gate.json')
182
195
  const RECEIPT = join(ROOT, RECEIPT_AT)
183
196
 
184
- if (failed.length === 0 && dropped.length === 0) {
185
- mkdirSync(dirname(RECEIPT), { recursive: true })
197
+ if (unmeasured.length > 0) {
198
+ stdout.write(
199
+ `gate: ${String(unmeasured.length)} step(s) could not be measured here — ${unmeasured.map((step) => step.id).join(', ')}. Each one\n` +
200
+ `runs a tool that is not part of this framework, so it reads whatever disk it was started on. This is not a\n` +
201
+ `gate pass, and no receipt is written for a run that left them unanswered.\n`,
202
+ )
203
+ }
204
+
205
+ if (failed.length === 0 && dropped.length === 0 && unmeasured.length === 0) {
186
206
  writeFileSync(RECEIPT, `${JSON.stringify({ passed: chosen.length, ...fingerprint() }, null, 2)}\n`, 'utf8')
187
207
  stdout.write(`gate: wrote ${RECEIPT_AT} — what compiles is checked against this, so a tree that skipped the gate does not build.\n`)
188
208
  } else if (existsSync(RECEIPT)) {
@@ -191,7 +211,7 @@ if (failed.length === 0 && dropped.length === 0) {
191
211
  stdout.write(`gate: removed ${RECEIPT_AT} — the tree no longer passes, so nothing may compile against an older run.\n`)
192
212
  }
193
213
 
194
- if (failed.length === 0) exit(0)
214
+ if (failed.length === 0) exit(unmeasured.length === 0 ? 0 : 1)
195
215
 
196
216
  stderr.write(
197
217
  `gate: FAILED — ${String(failed.length)} of ${String(chosen.length)} step(s). The other ${String(passed)} ran and passed; a failing step stops nothing.\n`,