@prestack/thread 0.1.3 → 0.1.5
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.
- package/CHANGELOG.md +563 -0
- package/dist/core/actor.d.ts +1 -1
- package/dist/core/actor.js +5 -4
- package/dist/core/actor.ts +5 -4
- package/dist/core/attach.d.ts +0 -7
- package/dist/core/attach.js +38 -67
- package/dist/core/attach.ts +34 -80
- package/dist/core/chain.js +13 -17
- package/dist/core/chain.ts +11 -15
- package/dist/core/condition.d.ts +4 -4
- package/dist/core/condition.js +2 -2
- package/dist/core/descend.d.ts +4 -2
- package/dist/core/descend.js +91 -23
- package/dist/core/descend.ts +90 -21
- package/dist/core/errors/catalogue.d.ts +52 -0
- package/dist/{operations/errors/errors.js → core/errors/catalogue.js} +1 -1
- package/dist/{operations/errors/errors.ts → core/errors/catalogue.ts} +1 -1
- package/dist/{operations/errors/catalog.d.ts → core/errors/error.d.ts} +2 -3
- package/dist/{operations/errors/catalog.js → core/errors/error.js} +1 -16
- package/dist/{operations/errors/catalog.ts → core/errors/error.ts} +3 -19
- package/dist/core/errors/namespaces.d.ts +3 -0
- package/dist/core/errors/namespaces.js +15 -0
- package/dist/core/errors/namespaces.ts +19 -0
- package/dist/core/finding.js +2 -2
- package/dist/core/finding.ts +1 -2
- package/dist/core/init.d.ts +1 -1
- package/dist/core/init.js +51 -5
- package/dist/core/init.ts +58 -5
- package/dist/core/invoked.d.ts +1 -1
- package/dist/core/invoked.js +3 -1
- package/dist/core/invoked.ts +5 -2
- package/dist/core/round.js +36 -22
- package/dist/core/round.ts +26 -9
- package/dist/core/rows.d.ts +4 -0
- package/dist/core/rows.js +31 -2
- package/dist/core/rows.ts +33 -1
- package/dist/core/scope.d.ts +1 -0
- package/dist/core/scope.js +1 -0
- package/dist/core/scope.ts +2 -0
- package/dist/core/thread.d.ts +3 -3
- package/dist/core/tree.d.ts +4 -1
- package/dist/core/tree.js +26 -6
- package/dist/core/tree.ts +29 -6
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +3 -1
- package/dist/core/types.ts +2 -1
- package/dist/derivation/attach.js +3 -2
- package/dist/derivation/attach.ts +2 -1
- package/dist/derivation/generators/build.js +6 -6
- package/dist/derivation/generators/build.ts +1 -1
- package/dist/derivation/generators/check.js +1 -1
- package/dist/derivation/generators/determinism.js +1 -1
- package/dist/derivation/generators/release.js +60 -47
- package/dist/derivation/generators/release.ts +66 -43
- package/dist/derivation/release.d.ts +7 -3
- package/dist/derivation/release.js +14 -1
- package/dist/derivation/release.ts +22 -2
- package/dist/integrations/attach.d.ts +4 -0
- package/dist/integrations/attach.js +37 -56
- package/dist/integrations/attach.ts +54 -60
- package/dist/integrations/cli.d.ts +37 -0
- package/dist/integrations/cli.js +440 -0
- package/dist/integrations/cli.ts +543 -0
- package/dist/{core/files.d.ts → integrations/disk.d.ts} +16 -24
- package/dist/integrations/disk.js +435 -0
- package/dist/integrations/disk.ts +465 -0
- package/dist/integrations/generators/read.js +6 -6
- package/dist/integrations/generators/read.ts +4 -4
- package/dist/integrations/generators/vault.js +10 -11
- package/dist/integrations/generators/vault.ts +4 -5
- package/dist/{operations/http.d.ts → integrations/https.d.ts} +10 -6
- package/dist/integrations/https.js +170 -0
- package/dist/integrations/https.ts +216 -0
- package/dist/integrations/pglite.d.ts +26 -0
- package/dist/integrations/pglite.js +143 -0
- package/dist/integrations/pglite.ts +206 -0
- package/dist/integrations/secret.d.ts +8 -2
- package/dist/integrations/secret.js +3 -5
- package/dist/integrations/secret.ts +10 -6
- package/dist/integrations/target.d.ts +155 -14
- package/dist/integrations/target.js +171 -50
- package/dist/integrations/target.ts +377 -61
- package/dist/integrations/vault.d.ts +1 -0
- package/dist/integrations/vault.js +20 -5
- package/dist/integrations/vault.ts +17 -5
- package/dist/integrity/attach.js +8 -77
- package/dist/integrity/attach.ts +7 -76
- package/dist/integrity/check-threads.js +5 -5
- package/dist/integrity/check-threads.ts +2 -2
- package/dist/integrity/constraint.d.ts +0 -1
- package/dist/integrity/constraint.js +267 -366
- package/dist/integrity/constraint.ts +267 -375
- package/dist/integrity/generators/compiler.js +14 -17
- package/dist/integrity/generators/compiler.ts +10 -15
- package/dist/integrity/generators/gate.js +15 -15
- package/dist/integrity/generators/gate.ts +10 -10
- package/dist/integrity/generators/issues.js +7 -7
- package/dist/integrity/generators/publish.js +39 -57
- package/dist/integrity/generators/publish.ts +44 -55
- package/dist/integrity/generators/trial.d.ts +3 -3
- package/dist/integrity/generators/trial.js +133 -165
- package/dist/integrity/generators/trial.ts +145 -169
- package/dist/integrity/lint.js +9 -13
- package/dist/integrity/lint.ts +7 -10
- package/dist/integrity/lsp/bin.js +5 -4
- package/dist/integrity/lsp/bin.ts +3 -1
- package/dist/integrity/lsp/diagnostics.js +3 -3
- package/dist/integrity/lsp/diagnostics.ts +1 -1
- package/dist/integrity/lsp/lint.js +19 -30
- package/dist/integrity/lsp/lint.ts +16 -32
- package/dist/integrity/lsp/server.js +3 -3
- package/dist/integrity/slopnator.d.ts +3 -13
- package/dist/integrity/slopnator.js +59 -38
- package/dist/integrity/slopnator.ts +54 -38
- package/dist/integrity/slopnators/sl_broken_lineage.js +2 -2
- package/dist/integrity/slopnators/sl_declared_elsewhere.js +1 -1
- package/dist/integrity/slopnators/sl_derived_drift.js +4 -4
- package/dist/integrity/slopnators/sl_derived_drift.ts +1 -1
- package/dist/integrity/slopnators/sl_door_in_a_node.js +220 -0
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +250 -0
- package/dist/integrity/slopnators/sl_inert_island.js +10 -8
- package/dist/integrity/slopnators/sl_inert_island.ts +8 -6
- package/dist/integrity/slopnators/sl_missing_row.js +6 -51
- package/dist/integrity/slopnators/sl_missing_row.ts +4 -50
- package/dist/integrity/slopnators/sl_no_barrel.js +1 -1
- package/dist/integrity/slopnators/sl_no_child_process.js +36 -18
- package/dist/integrity/slopnators/sl_no_child_process.ts +44 -18
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +80 -149
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +95 -147
- package/dist/integrity/slopnators/sl_no_import_cycle.js +15 -4
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +20 -4
- package/dist/integrity/slopnators/sl_no_such_command.js +2 -2
- package/dist/integrity/slopnators/sl_no_such_command.ts +1 -1
- package/dist/integrity/slopnators/sl_outside_an_operation.js +20 -0
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +25 -0
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.d.ts +2 -0
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.js +61 -0
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +70 -0
- package/dist/integrity/slopnators/sl_secret_literal.js +1 -33
- package/dist/integrity/slopnators/sl_secret_literal.ts +1 -33
- package/dist/integrity/slopnators/sl_stale_path.js +5 -22
- package/dist/integrity/slopnators/sl_stale_path.ts +4 -20
- package/dist/integrity/slopnators/sl_twice_meant.js +1 -1
- package/dist/integrity/slopnators/sl_twice_parented.js +29 -5
- package/dist/integrity/slopnators/sl_twice_parented.ts +28 -4
- package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +4 -1
- package/dist/integrity/slopnators/sl_undeclared_crossing.js +426 -62
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +454 -70
- package/dist/integrity/slopnators/sl_undeclared_destination.js +59 -25
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +69 -21
- package/dist/integrity/slopnators/sl_undeclared_list.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_list.js +363 -0
- package/dist/integrity/slopnators/sl_undeclared_list.ts +358 -0
- package/dist/integrity/slopnators/sl_undeclared_node.js +1 -11
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -10
- package/dist/integrity/slopnators/sl_undeclared_operation.js +111 -29
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +134 -26
- package/dist/integrity/slopnators/sl_undeclared_variant.js +2 -2
- package/dist/integrity/slopnators/sl_undeclared_webhook.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_webhook.js +90 -0
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +108 -0
- package/dist/integrity/slopnators/sl_underived_route.d.ts +2 -0
- package/dist/integrity/slopnators/sl_underived_route.js +61 -0
- package/dist/integrity/slopnators/sl_underived_route.ts +72 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -3
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +1 -1
- package/dist/integrity/slopnators/sl_unlisted_program.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unlisted_program.js +39 -0
- package/dist/integrity/slopnators/sl_unlisted_program.ts +40 -0
- package/dist/integrity/slopnators/sl_unnamed_island.js +1 -1
- package/dist/integrity/slopnators/sl_unpaired_member.js +3 -3
- package/dist/integrity/slopnators/sl_unpaired_member.ts +1 -1
- package/dist/integrity/slopnators/sl_unpaired_slopnator.js +4 -4
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +1 -1
- package/dist/integrity/slopnators/sl_unprefixed_member.js +5 -31
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +2 -28
- package/dist/integrity/slopnators/sl_unthreaded.js +11 -7
- package/dist/integrity/slopnators/sl_unthreaded.ts +6 -7
- package/dist/integrity/slopnators/sl_untranslated_key.js +2 -25
- package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -25
- package/dist/integrity/slopnators/sl_unvaulted_secret.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unvaulted_secret.js +107 -0
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +132 -0
- package/dist/integrity/slopnators/sl_unversioned.js +6 -4
- package/dist/integrity/slopnators/sl_unversioned.ts +5 -3
- package/dist/integrity/sweep.d.ts +1 -0
- package/dist/integrity/sweep.js +105 -42
- package/dist/integrity/sweep.ts +115 -42
- package/dist/knowledge/attach.js +4 -3
- package/dist/knowledge/attach.ts +3 -2
- package/dist/knowledge/generators/docsgen.js +8 -6
- package/dist/knowledge/generators/docsgen.ts +4 -2
- package/dist/knowledge/generators/glossary.js +4 -4
- package/dist/knowledge/generators/glossary.ts +1 -1
- package/dist/operations/access.d.ts +23 -0
- package/dist/operations/access.js +28 -1
- package/dist/operations/access.ts +43 -0
- package/dist/operations/attach.js +33 -113
- package/dist/operations/attach.ts +51 -117
- package/dist/operations/capabilities.d.ts +14 -5
- package/dist/operations/capabilities.js +13 -4
- package/dist/operations/capabilities.ts +27 -8
- package/dist/operations/dispatch.d.ts +8 -2
- package/dist/operations/dispatch.js +128 -185
- package/dist/operations/dispatch.ts +138 -191
- package/dist/operations/error-results.d.ts +1 -1
- package/dist/operations/error-results.js +2 -2
- package/dist/operations/error-results.ts +2 -2
- package/dist/operations/generators/capability.d.ts +11 -1
- package/dist/operations/generators/capability.js +481 -224
- package/dist/operations/generators/capability.ts +562 -233
- package/dist/operations/generators/compile.js +66 -39
- package/dist/operations/generators/compile.ts +78 -39
- package/dist/operations/generators/permits.js +11 -12
- package/dist/operations/generators/permits.ts +3 -4
- package/dist/operations/generators/seed.d.ts +1 -0
- package/dist/operations/generators/seed.js +19 -17
- package/dist/operations/generators/seed.ts +22 -14
- package/dist/operations/generators/trial.d.ts +6 -8
- package/dist/operations/generators/trial.js +200 -70
- package/dist/operations/generators/trial.ts +230 -93
- package/dist/operations/nodes.d.ts +15 -16
- package/dist/operations/nodes.ts +16 -17
- package/dist/operations/operations.d.ts +40 -4
- package/dist/operations/operations.js +1 -0
- package/dist/operations/operations.ts +61 -4
- package/dist/operations/result.d.ts +2 -2
- package/dist/operations/result.ts +2 -2
- package/package.json +1 -4
- package/dist/core/answers.d.ts +0 -171
- package/dist/core/answers.js +0 -789
- package/dist/core/answers.ts +0 -999
- package/dist/core/client.d.ts +0 -63
- package/dist/core/client.js +0 -249
- package/dist/core/client.ts +0 -289
- package/dist/core/files.js +0 -171
- package/dist/core/files.ts +0 -222
- package/dist/integrations/module.d.ts +0 -6
- package/dist/integrations/module.js +0 -153
- package/dist/integrations/module.ts +0 -191
- package/dist/integrity/slopnators/sl_unkept_rows.js +0 -104
- package/dist/integrity/slopnators/sl_unkept_rows.ts +0 -111
- package/dist/integrity/slopnators/sl_unseamed_reach.js +0 -73
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +0 -79
- package/dist/operations/cost.d.ts +0 -14
- package/dist/operations/cost.js +0 -37
- package/dist/operations/cost.ts +0 -58
- package/dist/operations/errors/errors.d.ts +0 -52
- package/dist/operations/http.js +0 -124
- package/dist/operations/http.ts +0 -169
- package/dist/operations/memory.d.ts +0 -104
- package/dist/operations/memory.js +0 -365
- package/dist/operations/memory.ts +0 -470
- package/dist/operations/metering.d.ts +0 -55
- package/dist/operations/metering.js +0 -247
- package/dist/operations/metering.ts +0 -328
- package/dist/operations/queue-policy.d.ts +0 -3
- package/dist/operations/queue-policy.js +0 -8
- package/dist/operations/queue-policy.ts +0 -10
- package/dist/operations/seams.d.ts +0 -239
- package/dist/operations/seams.js +0 -45
- package/dist/operations/seams.ts +0 -364
- package/dist/operations/store.d.ts +0 -14
- package/dist/operations/store.js +0 -1
- package/dist/operations/store.ts +0 -29
- /package/dist/{operations → core}/errors/classes.d.ts +0 -0
- /package/dist/{operations → core}/errors/classes.js +0 -0
- /package/dist/{operations → core}/errors/classes.ts +0 -0
- /package/dist/integrity/slopnators/{sl_unkept_rows.d.ts → sl_door_in_a_node.d.ts} +0 -0
- /package/dist/integrity/slopnators/{sl_unseamed_reach.d.ts → sl_outside_an_operation.d.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { named } from
|
|
1
|
+
import { named } from '../core/scope.js';
|
|
2
2
|
export function shown(proof) {
|
|
3
3
|
if (typeof proof === 'string')
|
|
4
4
|
return proof;
|
|
@@ -12,7 +12,7 @@ export const CONSTRAINTS = [
|
|
|
12
12
|
at: './constraint.ts',
|
|
13
13
|
describes: 'The refusal that keeps meaning in one place — an agent reads prose beside the code, believes it over the code, and writes more of it, so explanation accumulates where nothing checks it while the glossary declared to hold it stays empty.',
|
|
14
14
|
version: 1,
|
|
15
|
-
where: { paths: ['.']
|
|
15
|
+
where: { paths: ['.'] },
|
|
16
16
|
bans: 'a comment of any kind — block, line or JSDoc',
|
|
17
17
|
because: 'measured 2026-08-13: 52,187 of this repository\'s 124,964 lines are comment, and the five thread files carry 739 of them against a glossary holding 27 words with 27 empty meanings — so meaning lives in prose that nothing checks while the one place declared to hold it is empty. A single read of those five files found 17 statements contradicting the code beside them: three teach the `parent:` field `no-child-parent` refuses, one reports a 61-row book that holds 18, and one names `pnpm ci` as the gate when the script is `pnpm gate`',
|
|
18
18
|
instead: 'write the meaning in `threads/glossary.ts`, and whatever a file must say about itself in the row that parents it — its `describes` and its `cites`',
|
|
@@ -27,31 +27,18 @@ export const CONSTRAINTS = [
|
|
|
27
27
|
cites: [{ 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.' }],
|
|
28
28
|
},
|
|
29
29
|
],
|
|
30
|
-
derives: [
|
|
31
|
-
{
|
|
32
|
-
id: 'trial',
|
|
33
|
-
at: '_threads/_generated/trials.ts',
|
|
34
|
-
by: 'thread trials',
|
|
35
|
-
runs: 'thread trials',
|
|
36
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
37
|
-
cites: [
|
|
38
|
-
{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' },
|
|
39
|
-
{ on: '2026-08-14', said: 'maybe laws + slopnators produce a trial through law instead? That also works for me.' },
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
30
|
},
|
|
44
31
|
{
|
|
45
32
|
id: 'secret-literal',
|
|
46
33
|
at: './constraint.ts',
|
|
47
34
|
describes: 'The refusal of a credential written into a file — the one leak this framework can actually prevent, because a value in a tree is in every copy of it, in its history for ever, and in whatever bundle reaches it.',
|
|
48
35
|
version: 1,
|
|
49
|
-
where: { paths: ['.']
|
|
36
|
+
where: { paths: ['.'] },
|
|
50
37
|
bans: 'a credential written as a literal — a vendor key by its own prefix, a private key block, a password inside a connection string, or a long opaque value assigned to something called a secret, a password, a token or a key',
|
|
51
|
-
because: 'a secret is the one thing here that cannot be taken back. A wrong schema is regenerated and a wrong row is edited, but a credential committed once is in the history of every clone of that repository and has to be rotated at the vendor — and the framework itself never ships, so the only place a value can leak from is an app somebody built with it. Nothing else refuses this: the descent asks who declared a file, `
|
|
38
|
+
because: 'a secret is the one thing here that cannot be taken back. A wrong schema is regenerated and a wrong row is edited, but a credential committed once is in the history of every clone of that repository and has to be rotated at the vendor — and the framework itself never ships, so the only place a value can leak from is an app somebody built with it. Nothing else refuses this: the descent asks who declared a file, `undeclared-crossing` asks what a node reaches, and neither reads what a string contains. A vendor prefix is matched on its own because it is certain rather than probable: nothing else in a source file begins `sk_live_` or `AKIA` followed by twenty characters. A value that does not announce itself needs BOTH halves — a name that means credential AND something long and mixed enough to be one — because either alone is noise, a short assignment being a map key and a long opaque string being as likely a hash, a path or a fixture. Four things are deliberately not findings: a secret this app declares, which is a name and is the whole remedy this law recommends; an environment variable, whose contents this law cannot see and which `unvaulted-secret` refuses outright — a value arriving from the environment never passed the vault, which makes it a second defect rather than the remedy for this one; the obvious placeholders somebody writes while wiring something up; and the sealed store, which is base64 by construction, so the entropy half would find every value in it and recommend the thing the file already is — a law whose refusal has no honest remedy is one people learn to ignore. Measured 2026-09-17, against the rule that a check may hold no list of its own: this one holds twelve and they cannot come from the tree. A tree declares the NAMES of the secrets an app keeps, never the SHAPES of the keys other companies issue, so there is nothing here to read. Every scanner that exists spells the formats, and the one alternative — refusing whatever merely looks random — is the entropy half this row already rejects above. The list stands knowingly, and this is the one place in this framework where a check reaches exactly as far as somebody typed.',
|
|
52
39
|
instead: 'declare it with `thread create secret`, which asks a person for the value at a terminal with the echo off and seals it into the store beside the tree. The row holds the name and the sentence and has no field for a value at all; a target requires the row by name, and dispatch resolves it at the moment of the call',
|
|
53
40
|
wrong: "export const stripe = { apiKey: 'sk_live_51H8xQvL2eZvKYlo2C0Nx9K4mQwErTyUiOp' }",
|
|
54
|
-
right: "export const node = nd_action(async (
|
|
41
|
+
right: "export const node = nd_action(async (opened) => await opened.ht_stripe.charge({ json: { amount: 100 } }))",
|
|
55
42
|
cites: [
|
|
56
43
|
{ on: '2026-08-29', said: 'some of the secrets are server and extremely important, others are public....so we need a way to make this extremely robust to attacks or leaks. Mostly leaks since the framework doesn\'t ship' },
|
|
57
44
|
{ on: '2026-08-29', said: 'we need constraints.ts to have ways to protect us' },
|
|
@@ -65,37 +52,16 @@ export const CONSTRAINTS = [
|
|
|
65
52
|
},
|
|
66
53
|
],
|
|
67
54
|
},
|
|
68
|
-
{
|
|
69
|
-
id: 'unseamed-reach',
|
|
70
|
-
at: './constraint.ts',
|
|
71
|
-
describes: 'The refusal of a node taking a capability from the runtime instead of from the seams it is handed — everything through the seams is recorded under the node\'s own id, counted against the run\'s quota and permitted by its grants, and everything around them is none of those things.',
|
|
72
|
-
version: 1,
|
|
73
|
-
where: { paths: ['operations/', 'nodes/'], except: ['thread/operations/', '_generated/', 'node_modules/'] },
|
|
74
|
-
bans: 'a file under an operation or a node type importing a runtime built-in — anything `node:`, by that name or the bare one',
|
|
75
|
-
because: 'measured 2026-08-27 in an app installed from a packed build: a node whose row says `nd_read` was given `import { readFileSync } from \'node:fs\'` and a body that read /etc/hosts, and `thread gate` passed 5 of 5 — the compiler, the descent, the sweep, the build and the seed check all agreed, and the run\'s record said the node ran and read nothing. Nothing else refuses this, because every other rule reads what a file declares rather than what it reaches: the descent asks who named the file, `no-dispatch-bypass` asks who called the operation, and neither asks what the body touches. An operation whose nodes can reach the disk directly is an operation whose log describes a different program, and the log is the whole reason a node has an id',
|
|
76
|
-
instead: 'take it from the seams the node is handed — `store` and `search` on a read node, the whole of `seams` on an action node. If what you need is not there, it is a seam the framework has not declared yet, and that is a question for the founder rather than an import',
|
|
77
|
-
wrong: "import { readFileSync } from 'node:fs'",
|
|
78
|
-
right: 'export const node = nd_read<In>(async (hands) => await hands.store.get(\'customer\', held.input.id))',
|
|
79
|
-
cites: [{ on: '2026-08-27', said: 'node:fs law' }],
|
|
80
|
-
seeds: [
|
|
81
|
-
{
|
|
82
|
-
id: 'slopnator',
|
|
83
|
-
at: './slopnators/sl_unseamed_reach.ts',
|
|
84
|
-
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
85
|
-
cites: [{ 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.' }],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
55
|
{
|
|
90
56
|
id: 'no-import-cycle',
|
|
91
57
|
at: './constraint.ts',
|
|
92
58
|
describes: 'The refusal that keeps the tree readable from the top — a ring of files has no first one, so there is no order to descend in, and it loads at all only while every back edge happens to be type-only.',
|
|
93
59
|
version: 1,
|
|
94
|
-
where: { paths: ['
|
|
60
|
+
where: { paths: ['.'] },
|
|
95
61
|
bans: 'a file importing a file that imports it back',
|
|
96
62
|
because: 'measured 2026-08-14: threads/ holds six files and three of them — thread.ts, types.ts and meta.ts — import each other in a ring. It compiled only because every back edge happened to be type-only; make one of them a value import and the package stops loading. A ring has no first file, so there is no order to read the tree in, and reading it top down is the whole premise. Measured 2026-08-15, after the shapes became one interface: the ring is gone and every import points down — thread.ts imports nothing, meta.ts and ci.ts take from it, law.ts and glossary.ts take from meta.ts. A guard that has not fired since is a guard doing its job, and the ring took one refactor to reappear once already',
|
|
97
63
|
instead: 'move the thing that is depended upon into the file that is depended upon, so every import points one way. Where that fights the parent that declares it, the depended-upon thing wins — the alternative is code that does not compile',
|
|
98
|
-
wrong: "import
|
|
64
|
+
wrong: "import { Threaded } from './probe.tsx'",
|
|
99
65
|
right: "import type { Threaded } from '../core/thread.ts'",
|
|
100
66
|
cites: [{ on: '2026-08-14', said: 'does any file import a file that imports it back? Three do right now. That is the whole check, and it is the kind of thing a law could hold. Do this as a law first.' }],
|
|
101
67
|
seeds: [
|
|
@@ -106,24 +72,13 @@ export const CONSTRAINTS = [
|
|
|
106
72
|
cites: [{ 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.' }],
|
|
107
73
|
},
|
|
108
74
|
],
|
|
109
|
-
derives: [
|
|
110
|
-
{
|
|
111
|
-
id: 'trial',
|
|
112
|
-
at: '_threads/_generated/trials.ts',
|
|
113
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
114
|
-
cites: [
|
|
115
|
-
{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' },
|
|
116
|
-
{ on: '2026-08-14', said: 'maybe laws + slopnators produce a trial through law instead? That also works for me.' },
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
75
|
},
|
|
121
76
|
{
|
|
122
77
|
id: 'undescribed',
|
|
123
78
|
at: './constraint.ts',
|
|
124
79
|
describes: 'The one refusal a type cannot make — a required string is satisfied by an empty one and by a restatement of the name beside it, so presence is all a compiler ever proves and content has to be checked by something else.',
|
|
125
80
|
version: 1,
|
|
126
|
-
where: { paths: ['threads/']
|
|
81
|
+
where: { paths: ['threads/'] },
|
|
127
82
|
bans: 'a `describes` or `means` that is empty, or whose every word beyond the common ones already appears in the id beside it',
|
|
128
83
|
because: 'measured 2026-08-14: threads/glossary.ts holds 27 words and 27 empty meanings, and every other check passed them because presence was all anything asked for — so the tree read as described and said nothing. Measured 2026-08-15: 35 words, 35 empty meanings, and this is the only rule that sees them, because a type can require a field and never require it to say anything. A description that restates its own name is the same hole with prose in it: it adds nothing a reader could not already read off the id, and it is what a machine writes when a person was supposed to. Measured 2026-08-15, once the glossary became derived: every `means` in threads/glossary.ts is a copy of the `describes` this rule already reads on the row that coins the word, so scanning the derived file read the same 52 strings a second time and could not report anything the first pass had missed. That file is excepted — a rule reading a copy of what it already read is a second opinion from the same source',
|
|
129
84
|
instead: 'say what the thing is and what it solves, in a sentence somebody who has not seen the code can act on. If the honest answer is the id again, the thing does not need a description — it needs a reason to exist',
|
|
@@ -138,26 +93,26 @@ export const CONSTRAINTS = [
|
|
|
138
93
|
cites: [{ on: '2026-08-14', said: 'I for sure need all errors to be defined, easy to understand, well explained, with fix suggestions.' }],
|
|
139
94
|
},
|
|
140
95
|
],
|
|
141
|
-
derives: [
|
|
142
|
-
{
|
|
143
|
-
id: 'trial',
|
|
144
|
-
at: '_threads/_generated/trials.ts',
|
|
145
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
146
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
96
|
},
|
|
150
97
|
{
|
|
151
98
|
id: 'unthreaded',
|
|
152
99
|
at: './constraint.ts',
|
|
153
100
|
describes: 'The refusal of a file no parent names through the interface, which is how this repository decides what is wired: whatever the descent never reaches is not part of the declared surface, and a reach count printed beside the gap is not a refusal of it.',
|
|
154
101
|
version: 1,
|
|
155
|
-
where: { paths: ['threads/', '.']
|
|
102
|
+
where: { paths: ['threads/', '.'] },
|
|
156
103
|
bans: 'a file that no `at:` names from inside a node annotated `Threaded`, `threads/thread.ts` excepted because the root exists by fiat',
|
|
157
104
|
because: 'measured 2026-08-14: the descent from threads/thread.ts names 28 of 641 tracked files, and nothing refused the gap — check-threads printed it as a reach number and exited 0, so twelve files in the mechanism\'s own directories read as wired while nothing declared them. Whatever the descent never reaches is not wired, and a count is not a refusal. This rule reads the mentions and `broken-lineage` reads the roots, deliberately: a file in `threads/` that nothing names still threads what it points at here, and it is that file — not the subtree under it — that gets reported, which is one finding at the cause instead of a cascade at the symptoms. Measured 2026-08-15: an `at:` was a mention, not a thread. Eight lines appended to scripts/finding.ts — a bare object with no id, no at, no describes, no cites and no type — threaded packages/core/engine/src/cache.ts on the next run, and nothing refused it. A file must have a parent; a parent must use the `Threaded` interface to parent it; if it does not, the file is unthreaded. So an `at:` counts only inside a node annotated `Threaded` or an interface extending it, and the bare object threads nothing',
|
|
158
105
|
instead: 'declare it from the row that owns it, or delete it. A file that nothing names is either somebody\'s child nobody wrote down, or it is nobody\'s',
|
|
159
106
|
wrong: 'export const ORPHAN = 1',
|
|
160
|
-
right:
|
|
107
|
+
right: {
|
|
108
|
+
files: [
|
|
109
|
+
{ 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" },
|
|
110
|
+
{ at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_names_it.ts',\n },\n ],\n}\n" },
|
|
111
|
+
{ at: 'threads/_names_it.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const HELD: Threaded = {\n declares: [\n {\n at: 'threads/core.ts',\n },\n ],\n}\n" },
|
|
112
|
+
{ at: 'threads/core.ts', source: 'export const CORE = 1\n' },
|
|
113
|
+
],
|
|
114
|
+
at: 'threads/_names_it.ts',
|
|
115
|
+
},
|
|
161
116
|
cites: [
|
|
162
117
|
{ on: '2026-08-12', said: 'Not untraced, unthreaded. Let\'s enforce the naming.' },
|
|
163
118
|
{ on: '2026-08-14', said: 'Cause we need unthreaded, redthread, broken lineage, and the request/produce errors....' },
|
|
@@ -170,21 +125,13 @@ export const CONSTRAINTS = [
|
|
|
170
125
|
cites: [{ on: '2026-08-14', said: 'I want you to transform every single thing that currently is a check into a law.' }],
|
|
171
126
|
},
|
|
172
127
|
],
|
|
173
|
-
derives: [
|
|
174
|
-
{
|
|
175
|
-
id: 'trial',
|
|
176
|
-
at: '_threads/_generated/trials.ts',
|
|
177
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
178
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
128
|
},
|
|
182
129
|
{
|
|
183
130
|
id: 'twice-parented',
|
|
184
131
|
at: './constraint.ts',
|
|
185
132
|
describes: 'The refusal of one address claimed by two owners, because a child declared twice has two answers for who owns it and where it came from, and the tree can give neither — the two rows may sit in different files, so only a slopnator that compares them all can see it.',
|
|
186
133
|
version: 1,
|
|
187
|
-
where: { paths: ['.']
|
|
134
|
+
where: { paths: ['.'] },
|
|
188
135
|
bans: 'one `at` claimed by two `declares` rows, wherever the two rows live',
|
|
189
136
|
because: '`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',
|
|
190
137
|
instead: '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',
|
|
@@ -196,7 +143,16 @@ export const CONSTRAINTS = [
|
|
|
196
143
|
],
|
|
197
144
|
at: 'threads/_probe_parented_row.ts',
|
|
198
145
|
},
|
|
199
|
-
right:
|
|
146
|
+
right: {
|
|
147
|
+
files: [
|
|
148
|
+
{ 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" },
|
|
149
|
+
{ 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_once.ts',\n },\n ],\n}\n" },
|
|
150
|
+
{ at: 'threads/_probe_once.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n {\n at: 'threads/core.ts',\n },\n {\n at: 'threads/data.ts',\n },\n ],\n}\n" },
|
|
151
|
+
{ at: 'threads/core.ts', source: 'export const CORE = 1\n' },
|
|
152
|
+
{ at: 'threads/data.ts', source: 'export const DATA = 1\n' },
|
|
153
|
+
],
|
|
154
|
+
at: 'threads/_probe_once.ts',
|
|
155
|
+
},
|
|
200
156
|
cites: [{ on: '2026-08-14', said: 'Cause we need unthreaded, redthread, broken lineage, and the request/produce errors....' }],
|
|
201
157
|
seeds: [
|
|
202
158
|
{
|
|
@@ -206,21 +162,13 @@ export const CONSTRAINTS = [
|
|
|
206
162
|
cites: [{ on: '2026-08-14', said: 'I want you to transform every single thing that currently is a check into a law.' }],
|
|
207
163
|
},
|
|
208
164
|
],
|
|
209
|
-
derives: [
|
|
210
|
-
{
|
|
211
|
-
id: 'trial',
|
|
212
|
-
at: '_threads/_generated/trials.ts',
|
|
213
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
214
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
165
|
},
|
|
218
166
|
{
|
|
219
167
|
id: 'broken-lineage',
|
|
220
168
|
at: './constraint.ts',
|
|
221
169
|
describes: 'The refusal of a file that names children while nothing names it — the break is above them, so every child reads as merely unnamed and repairing them one by one fixes nothing, which is the difference between one finding and a subtree of them.',
|
|
222
170
|
version: 1,
|
|
223
|
-
where: { paths: ['threads/', '.']
|
|
171
|
+
where: { paths: ['threads/', '.'] },
|
|
224
172
|
bans: 'a file that names children while its own origin does not trace back to `threads/`',
|
|
225
173
|
because: 'a file outside `threads/` may name children — that is how `guard.mjs`, once threaded, parents `try-guard.ts` — but the chain has to start somewhere, and the only somewhere is `threads/`. A file that parents while nothing parents IT strands every child it names, and each of those reads as merely unthreaded rather than as the consequence of one break higher up. Measured 2026-08-14: the descent from threads/thread.ts reaches 33 of 646 tracked files, and a single unrooted parent is the difference between one finding and a subtree of them',
|
|
226
174
|
instead: 'thread the file itself — name it from whatever already traces back to `threads/` — or delete it and the children it threaded. Fixing the children first repairs nothing, because the break is above them',
|
|
@@ -238,23 +186,15 @@ export const CONSTRAINTS = [
|
|
|
238
186
|
cites: [{ on: '2026-08-14', said: 'Red thread is broken lineage.' }],
|
|
239
187
|
},
|
|
240
188
|
],
|
|
241
|
-
derives: [
|
|
242
|
-
{
|
|
243
|
-
id: 'trial',
|
|
244
|
-
at: '_threads/_generated/trials.ts',
|
|
245
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
246
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
189
|
},
|
|
250
190
|
{
|
|
251
191
|
id: 'stale-path',
|
|
252
192
|
at: './constraint.ts',
|
|
253
193
|
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.',
|
|
254
194
|
version: 1,
|
|
255
|
-
where: { paths: ['.']
|
|
195
|
+
where: { paths: ['.'] },
|
|
256
196
|
bans: 'a string naming a path in this repository that is not on disk',
|
|
257
|
-
because: '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',
|
|
197
|
+
because: '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. Measured 2026-08-29 on a real install: this refused the glossary file on an app first gate run — a file the glossary command writes and had not written yet — with advice that would have deleted a correct declaration, so a path a row derives is now read off the tree and left alone until its command has run',
|
|
258
198
|
instead: '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',
|
|
259
199
|
wrong: {
|
|
260
200
|
files: [
|
|
@@ -263,7 +203,7 @@ export const CONSTRAINTS = [
|
|
|
263
203
|
],
|
|
264
204
|
at: 'probe.tsx',
|
|
265
205
|
},
|
|
266
|
-
right: "const at = '
|
|
206
|
+
right: "const at = 'thread/integrity/constraint.ts'",
|
|
267
207
|
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.' }],
|
|
268
208
|
seeds: [
|
|
269
209
|
{
|
|
@@ -273,23 +213,15 @@ export const CONSTRAINTS = [
|
|
|
273
213
|
cites: [{ on: '2026-08-14', said: 'I want you to transform every single thing that currently is a check into a law.' }],
|
|
274
214
|
},
|
|
275
215
|
],
|
|
276
|
-
derives: [
|
|
277
|
-
{
|
|
278
|
-
id: 'trial',
|
|
279
|
-
at: '_threads/_generated/trials.ts',
|
|
280
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
281
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
216
|
},
|
|
285
217
|
{
|
|
286
218
|
id: 'missing-row',
|
|
287
219
|
at: './constraint.ts',
|
|
288
220
|
describes: 'The refusal of a tree that has dropped a row the package it attached from still ships — what a framework offers is not an app\'s to withdraw, and until this existed an app could delete any shipped row and every check still passed.',
|
|
289
221
|
version: 1,
|
|
290
|
-
where: { paths: ['.']
|
|
222
|
+
where: { paths: ['.'] },
|
|
291
223
|
bans: 'a tree holding fewer rows than the package it attached from declares',
|
|
292
|
-
because: 'measured 2026-08-22: the `ease` row was deleted from an app\'s `_threads/tokens.ts` and check-threads reported NOTHING — not one finding, and the exit was unchanged. The descent sweeps an app for files nothing declares; it never asks the opposite question, is anything the PACKAGE declares missing from this list, and a shipped file lives under `node_modules/` where nothing sweeps for orphans. So 84 shipped rows could be deleted one at a time in silence, and every check would report success having measured nothing',
|
|
224
|
+
because: 'measured 2026-08-22: the `ease` row was deleted from an app\'s `_threads/tokens.ts` and check-threads reported NOTHING — not one finding, and the exit was unchanged. The descent sweeps an app for files nothing declares; it never asks the opposite question, is anything the PACKAGE declares missing from this list, and a shipped file lives under `node_modules/` where nothing sweeps for orphans. So 84 shipped rows could be deleted one at a time in silence, and every check would report success having measured nothing. It reads those package files as TEXT and imports nothing but node builtins, because eight laws were once added whose walks imported a workspace package, plain Node could not resolve it, the loader swallowed the failure, and all eight then read from every caller exactly like laws finding nothing. What counts as a row was widened three times by measurement: a node may open its list inline or name a declares array beside it, and reading only the first made 107 of the 178 rows one package ships invisible, every one deletable in silence; a row may be an object literal or a call to a factory that takes the id first, which was the other half of the same blindness; and what a PACKAGE ships is only the ids inside a declares list, because reading every id in the file counted a row the package asks an app to append into another list as one it declares. What a TREE holds is the opposite — every id, whichever list it sits in — since gating that on declares made every row in the derivation sheet read as missing while it sat plainly on disk, and running the command this law recommends would not have cleared it. Only packages the tree ATTACHED are asked for: a package nothing points at is a dependency, and a dependency ships nothing this tree promised to hold',
|
|
293
225
|
instead: 'run `thread attach <package>` — it appends any row that has gone and skips every row already there, so one command restores the list without touching what the app added',
|
|
294
226
|
wrong: "const M: Threaded = {\n declares: [],\n}",
|
|
295
227
|
right: `const M: Threaded = {\n declares: [\n {\n at: 'node_modules/${named('ui')}/dist/tokens/colour.js',\n },\n ],\n}`,
|
|
@@ -305,14 +237,6 @@ export const CONSTRAINTS = [
|
|
|
305
237
|
cites: [{ on: '2026-08-14', said: 'I want you to transform every single thing that currently is a check into a law.' }],
|
|
306
238
|
},
|
|
307
239
|
],
|
|
308
|
-
derives: [
|
|
309
|
-
{
|
|
310
|
-
id: 'trial',
|
|
311
|
-
at: '_threads/_generated/trials.ts',
|
|
312
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
313
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
240
|
},
|
|
317
241
|
{
|
|
318
242
|
id: 'no-such-command',
|
|
@@ -340,21 +264,13 @@ export const CONSTRAINTS = [
|
|
|
340
264
|
cites: [{ on: '2026-08-14', said: 'I think you need to study how a file gets parented and make proper error handling for everything.' }],
|
|
341
265
|
},
|
|
342
266
|
],
|
|
343
|
-
derives: [
|
|
344
|
-
{
|
|
345
|
-
id: 'trial',
|
|
346
|
-
at: '_threads/_generated/trials.ts',
|
|
347
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
348
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
349
|
-
},
|
|
350
|
-
],
|
|
351
267
|
},
|
|
352
268
|
{
|
|
353
269
|
id: 'unpaired-slopnator',
|
|
354
270
|
at: './constraint.ts',
|
|
355
271
|
describes: 'The refusal of half a rule — a row without the function that enforces it, or a function no row threaded — because nothing but their names holds the two together, so either half can go missing while the other reads as working.',
|
|
356
272
|
version: 1,
|
|
357
|
-
where: { paths: ['threads/', 'thread/integrity/slopnators/']
|
|
273
|
+
where: { paths: ['threads/', 'thread/integrity/slopnators/'] },
|
|
358
274
|
bans: 'a rule naming a slopnator that is not on disk, and a slopnator in the slopnators directory that no rule names',
|
|
359
275
|
because: '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',
|
|
360
276
|
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',
|
|
@@ -378,21 +294,13 @@ export const CONSTRAINTS = [
|
|
|
378
294
|
cites: [{ on: '2026-08-15', said: 'Does a slopnator without a law or a law without a slopnator exist? This should be an error....' }],
|
|
379
295
|
},
|
|
380
296
|
],
|
|
381
|
-
derives: [
|
|
382
|
-
{
|
|
383
|
-
id: 'trial',
|
|
384
|
-
at: '_threads/_generated/trials.ts',
|
|
385
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
386
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
297
|
},
|
|
390
298
|
{
|
|
391
299
|
id: 'twice-meant',
|
|
392
300
|
at: './constraint.ts',
|
|
393
301
|
describes: 'The refusal of one word carrying two meanings — the glossary holds a single row per word and takes it from the `describes` beside the id, so rows that disagree leave the meaning decided by whichever the slopnator reached first.',
|
|
394
302
|
version: 1,
|
|
395
|
-
where: { paths: ['.']
|
|
303
|
+
where: { paths: ['.'] },
|
|
396
304
|
bans: 'two rows coining the same id while stating different `describes`',
|
|
397
305
|
because: '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',
|
|
398
306
|
instead: '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',
|
|
@@ -418,14 +326,6 @@ export const CONSTRAINTS = [
|
|
|
418
326
|
cites: [{ on: '2026-08-15', said: 'Yes, rows carry category meaning. No individual ids.' }],
|
|
419
327
|
},
|
|
420
328
|
],
|
|
421
|
-
derives: [
|
|
422
|
-
{
|
|
423
|
-
id: 'trial',
|
|
424
|
-
at: '_threads/_generated/trials.ts',
|
|
425
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
426
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
329
|
},
|
|
430
330
|
{
|
|
431
331
|
id: 'derived-drift',
|
|
@@ -437,7 +337,7 @@ export const CONSTRAINTS = [
|
|
|
437
337
|
because: 'a derivation is only worth the name while running it again produces what is on disk — the moment the two differ, the file is a hand edit wearing a generated file\'s name, and every reader downstream believes the wrong one. Measured 2026-08-15: five derivations each carried their own `:check` command as a separate gate step, so the rule was stated five times and nowhere. Measured again 2026-08-15, after this rule existed: the tree holds twenty `derives` rows and this slopnator read five — a row missing `by` or `runs` was dropped at the `if` that collected it, so fifteen declared derivations were never compared to anything and the rule reported zero findings over them. The file they all name survived only because a sixteenth row elsewhere happened to carry the command; delete that one row and fifteen go dark with nothing saying so. `permissions.deny` stops an agent editing a derived file; nothing stopped a shell, a merge, or a row that never named a generator at all',
|
|
438
338
|
instead: 'run the generator the row names in `by` and commit what it writes. Never edit the file — a derived file has no author, and an edit to one is a change to the generator that somebody skipped. Where many rows name one derived file, at least one of them must state the `by` that writes it and the `runs` that proves it',
|
|
439
339
|
wrong: "export const M: Threaded = {\n derives: [\n {\n at: 'scripts/_generated/gate.mjs',\n by: 'pnpm gate:generate',\n runs: 'pnpm nope:check',\n },\n ],\n}",
|
|
440
|
-
right: "export const M: Threaded = {\n derives: [\n {\n at: '
|
|
340
|
+
right: "export const M: Threaded = {\n derives: [\n {\n at: 'next.config.js',\n by: 'thread routes',\n runs: 'thread routes --check',\n },\n ],\n}",
|
|
441
341
|
cites: [
|
|
442
342
|
{ on: '2026-08-15', said: 'You\'ll take the checks that exist, make them into laws that derive the exact same checks. The laws create ALL things that check stuff. I do not want multiple sources of truth for anything.' },
|
|
443
343
|
{ on: '2026-08-15', said: 'So slopnators should have kinds. walk, check, whatever else it needs so it works.' },
|
|
@@ -450,26 +350,18 @@ export const CONSTRAINTS = [
|
|
|
450
350
|
cites: [{ on: '2026-08-15', said: 'make them into laws that derive the exact same checks' }],
|
|
451
351
|
},
|
|
452
352
|
],
|
|
453
|
-
derives: [
|
|
454
|
-
{
|
|
455
|
-
id: 'trial',
|
|
456
|
-
at: '_threads/_generated/trials.ts',
|
|
457
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
458
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
459
|
-
},
|
|
460
|
-
],
|
|
461
353
|
},
|
|
462
354
|
{
|
|
463
355
|
id: 'undeclared-crossing',
|
|
464
356
|
at: './constraint.ts',
|
|
465
|
-
describes: 'The refusal that makes
|
|
357
|
+
describes: 'The refusal that makes an operation the only thing in this app that does anything. Work is not written where it happens to be wanted; it is declared as an operation, which states what it takes, what it hands back and what it has to reach, and the operation is what runs. Anything that acts outside one is work this app never said it would do, and nothing can replay it, retry it, price it or unwind it afterwards.',
|
|
466
358
|
version: 1,
|
|
467
|
-
where: { paths: ['.']
|
|
468
|
-
bans: '
|
|
469
|
-
because: 'the trial, the replay, the trace, the cost record and the compensation
|
|
470
|
-
instead: '
|
|
359
|
+
where: { paths: ['.'] },
|
|
360
|
+
bans: 'acting outside an operation — anything that reaches past this process, whatever it is called',
|
|
361
|
+
because: 'An operation is how this framework does anything at all: it states what it takes, what it hands back and what it has to reach, and the trial, the replay, the trace, the cost record and the compensation every one hang off that statement. Work written beside one has none of them: nothing recorded it, so it cannot be replayed; nothing owns it, so it cannot be retried; it left no record, so it cannot be priced; no compensation names it, so it cannot be unwound. A `fetch` in a screen is not a smaller operation than a workflow — it is an operation the app cannot see, and the page renders fine either way, which is why nothing catches it without a rule. The clock is not one of these. `Date.now()`, a timer and `new Date()` are each accepted here and have been for as long as this check has run — they sit in its own list of forms it stays silent on. A paragraph stood here saying the opposite, on the grounds that a run reading the clock cannot be replayed, and it described a refusal this check never made; an agent read it, reported the clock as a gap, and was told twice. Founder, 2026-09-17: date is environment, and environment is free. A duration or easing curve typed into an animate call is a separate matter, and nothing refuses it today — hardcoded-style reads a CSS declaration written inside a string, and an object argument is not one. Measured 2026-09-16: the check behind this carried ten ways of acting written out, so starting a process — which nobody had typed — was not a crossing at all, and 48 places that spawn one were green. The law was never about those ten. Measured 2026-09-17, against the rule that a check may hold no list of its own: the twenty-one spellings this reads cannot come from the tree either, and two ways out were tried. Reading the declared doors instead — the rows naming the files that open a disk, a process and a network — refuses every file that legitimately goes through one, which is most of this framework. Reading what those door files themselves import brings node:path in with them, and a path join is not a crossing. What is left is the spelling of the capability, which Node names and this tree never does. It stands, and it is the second place where a check reaches exactly as far as somebody typed.',
|
|
362
|
+
instead: 'Declare an operation and put the work in it. `thread create operation` asks what it takes, what it hands back and which targets it reaches, then writes it; `thread run trial <operation>` runs it against an input you supply, so you see it work before anything else does. An operation reaches outside only through a target it named — that is not a restriction on the work, it is where the work goes.',
|
|
471
363
|
wrong: "const res = await fetch('https://api.stripe.com/v1/charges')",
|
|
472
|
-
right: "{ id: '
|
|
364
|
+
right: "{ id: 'wf_charge_card', describes: 'Takes payment for an order through the stripe target this app declares.', requires: ['ht_stripe'] }",
|
|
473
365
|
cites: [
|
|
474
366
|
{ on: '2026-08-16', said: 'The ones most important are the ones that enforce UI through elements and screens, and the ones that enforce that the only things that can operate are workflows and views.' },
|
|
475
367
|
{ on: '2026-08-16', said: 'No operation without a workflow or view.' },
|
|
@@ -483,21 +375,13 @@ export const CONSTRAINTS = [
|
|
|
483
375
|
cites: [{ on: '2026-08-16', said: 'No operation without a workflow or view.' }],
|
|
484
376
|
},
|
|
485
377
|
],
|
|
486
|
-
derives: [
|
|
487
|
-
{
|
|
488
|
-
id: 'trial',
|
|
489
|
-
at: '_threads/_generated/trials.ts',
|
|
490
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
491
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
378
|
},
|
|
495
379
|
{
|
|
496
380
|
id: 'hardcoded-style',
|
|
497
381
|
at: './constraint.ts',
|
|
498
382
|
describes: 'The refusal that keeps the compiler a translator. The style guide, the fonts, the animations and the icons hold the values; CSS is what one of those means said in another language, and a generator is the sentence that carries it across. So a generator may know that a thing becomes `background` and may not know what colour that is. Nothing else in this repository looks here — a row is checked, a variant is checked, an element is checked, and the strings a generator itself emits are checked by nobody. This reads them, and answers on any value typed in rather than taken from the sheet that owns it.',
|
|
499
383
|
version: 1,
|
|
500
|
-
where: { paths: ['.']
|
|
384
|
+
where: { paths: ['.'] },
|
|
501
385
|
bans: 'a value typed into the compiler where a sheet should have supplied it — a CSS declaration whose value is neither the name of a row nor derived from one',
|
|
502
386
|
because: 'a value lives in exactly one sheet and everything reads it from there, so a value typed into the compiler is outside that system while sitting at its centre: it survives a rebrand, ignores every mode and theme, answers to no command, and no sheet knows it exists. It is also the one hardcoding no other law can see — the raw-value refusal reads what a ROW states, and this is written where nothing states anything.',
|
|
503
387
|
instead: 'take it from the sheet that owns it: name the row, `var(--colour-page)` rather than `#f8fbfa`, or derive it the way `color-scheme` is derived from the modes an app declares. The property and the selector are the translation and are the compiler\'s; the value never is.',
|
|
@@ -516,14 +400,6 @@ export const CONSTRAINTS = [
|
|
|
516
400
|
cites: [{ on: '2026-09-03', said: 'nothing in the compiler can EVER hardcode a value' }],
|
|
517
401
|
},
|
|
518
402
|
],
|
|
519
|
-
derives: [
|
|
520
|
-
{
|
|
521
|
-
id: 'trial',
|
|
522
|
-
at: '_threads/_generated/trials.ts',
|
|
523
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
524
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
525
|
-
},
|
|
526
|
-
],
|
|
527
403
|
},
|
|
528
404
|
{
|
|
529
405
|
id: 'undeclared-destination',
|
|
@@ -531,12 +407,12 @@ export const CONSTRAINTS = [
|
|
|
531
407
|
governs: 'navigation',
|
|
532
408
|
describes: 'The refusal that makes the map the whole of where an app can go — a screen is reached by naming it, so every destination is one the map declares and a link nobody declared cannot exist.',
|
|
533
409
|
version: 1,
|
|
534
|
-
where: { paths: ['.']
|
|
535
|
-
bans: 'a destination reached without naming a declared screen — an `href`,
|
|
536
|
-
because: 'the map island holds the screens and the links between them, and everything that reasons about where an app can go reads `island.links`: `checkMapIsland` refuses a link naming a screen nothing declares, two screens answering one path, a link from a screen to itself and an app with no `entry`, and `orphanScreens` finds every screen no link reaches. Five checks, all of them blind to a destination reached another way — an `href` is not a link the island holds, so the screen it points at reads as unreachable, the app reads as having no path to it, and the page renders and navigates correctly. That is the whole failure: the map is not what the app does, it is what the app declared it does, and the two only stay the same thing if there is no second way to move',
|
|
410
|
+
where: { paths: ['.'] },
|
|
411
|
+
bans: 'a destination reached without naming a declared screen — an `href`, or the address of a declared screen written out as a path',
|
|
412
|
+
because: 'the map island holds the screens and the links between them, and everything that reasons about where an app can go reads `island.links`: `checkMapIsland` refuses a link naming a screen nothing declares, two screens answering one path, a link from a screen to itself and an app with no `entry`, and `orphanScreens` finds every screen no link reaches. Five checks, all of them blind to a destination reached another way — an `href` is not a link the island holds, so the screen it points at reads as unreachable, the app reads as having no path to it, and the page renders and navigates correctly. That is the whole failure: the map is not what the app does, it is what the app declared it does, and the two only stay the same thing if there is no second way to move. Measured 2026-09-17: what this read was a list of nine names — window, globalThis, self, top, parent, document, router, navigation, history — so a mover nobody had typed was not a mover at all, and the rule reached exactly as far as those nine. It reads the tree now: every screen an app declares has an address, and writing that address out rather than naming the screen is the finding, whatever does the writing. What is given up is a destination held in a variable, which was only ever caught when it passed through one of the nine.',
|
|
537
413
|
instead: 'name the screen — `navigateTo="Order"` — and let the map carry the destination, so a link exists in the island that reasons about links',
|
|
538
414
|
wrong: '<a href="/orders/42">{label}</a>',
|
|
539
|
-
right: '<Link.
|
|
415
|
+
right: '<Link.linkInline navigateTo="Order" labelKey="order.title" />',
|
|
540
416
|
cites: [
|
|
541
417
|
{ on: '2026-08-16', said: 'The ones most important are the ones that enforce UI through elements and screens, and the ones that enforce that the only things that can operate are workflows and views.' },
|
|
542
418
|
{ on: '2026-08-16', said: 'But they must be app wide.' },
|
|
@@ -551,23 +427,15 @@ export const CONSTRAINTS = [
|
|
|
551
427
|
cites: [{ on: '2026-08-16', said: 'The ones most important are the ones that enforce UI through elements and screens' }],
|
|
552
428
|
},
|
|
553
429
|
],
|
|
554
|
-
derives: [
|
|
555
|
-
{
|
|
556
|
-
id: 'trial',
|
|
557
|
-
at: '_threads/_generated/trials.ts',
|
|
558
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
559
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
430
|
},
|
|
563
431
|
{
|
|
564
432
|
id: 'undeclared-operation',
|
|
565
433
|
at: './constraint.ts',
|
|
566
434
|
describes: 'The refusal that keeps a dispatched name answerable to a declaration — a name handed to dispatch is a string like any other, so one that names nothing, or one assembled while the program runs, reaches for an operation that does not exist and fails in front of a person rather than where it was written.',
|
|
567
435
|
version: 1,
|
|
568
|
-
where: { paths: ['.']
|
|
436
|
+
where: { paths: ['.'] },
|
|
569
437
|
bans: 'dispatch handed a name nothing declares, or a name this file does not write down',
|
|
570
|
-
because: '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',
|
|
438
|
+
because: '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. Measured 2026-08-31 on a registry install: an operations-only app, everything made by the CLI, failed its own sweep on a file its own generator had just written, naming an operation the same tree declares — and a law that refuses a generated file is an app that cannot ship. So an identifier is accepted now, but only when it IS one of the declared operations; a variable holding a computed name is still refused, because it matches no declaration, and that is the hole this law was written for',
|
|
571
439
|
instead: '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',
|
|
572
440
|
wrong: {
|
|
573
441
|
files: [
|
|
@@ -600,21 +468,13 @@ export const CONSTRAINTS = [
|
|
|
600
468
|
cites: [{ on: '2026-08-25', said: 'And make sure the composition can only dispatch existing operations.' }],
|
|
601
469
|
},
|
|
602
470
|
],
|
|
603
|
-
derives: [
|
|
604
|
-
{
|
|
605
|
-
id: 'trial',
|
|
606
|
-
at: '_threads/_generated/trials.ts',
|
|
607
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
608
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
609
|
-
},
|
|
610
|
-
],
|
|
611
471
|
},
|
|
612
472
|
{
|
|
613
473
|
id: 'undeclared-variant',
|
|
614
474
|
at: './constraint.ts',
|
|
615
475
|
describes: 'The refusal that leaves a variant row as the only thing deciding what an element may wear — a member written on an element is a name nothing checks against anything, so one that was never declared, or one declared for a different element, renders carrying a class the stylesheet never writes and reads on screen as lost styling rather than a missing declaration.',
|
|
616
476
|
version: 1,
|
|
617
|
-
where: { paths: ['.']
|
|
477
|
+
where: { paths: ['.'] },
|
|
618
478
|
bans: 'an element wearing a member that no variant row declares, or that a variant row declares for a different element',
|
|
619
479
|
because: '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',
|
|
620
480
|
instead: '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',
|
|
@@ -648,14 +508,6 @@ export const CONSTRAINTS = [
|
|
|
648
508
|
cites: [{ on: '2026-08-25', said: 'Variants themselves declare what elements they relate to, in their declarations.' }],
|
|
649
509
|
},
|
|
650
510
|
],
|
|
651
|
-
derives: [
|
|
652
|
-
{
|
|
653
|
-
id: 'trial',
|
|
654
|
-
at: '_threads/_generated/trials.ts',
|
|
655
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
656
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
657
|
-
},
|
|
658
|
-
],
|
|
659
511
|
},
|
|
660
512
|
{
|
|
661
513
|
id: 'undeclared-element',
|
|
@@ -663,7 +515,7 @@ export const CONSTRAINTS = [
|
|
|
663
515
|
governs: 'elements',
|
|
664
516
|
describes: 'The refusal that makes the element catalogue the only thing an interface is built from — a lowercase tag is HTML written in place: it wears no declared style, sits in no catalogue, and is the one shape that renders like an element while being invisible to everything that reads what an app is made of.',
|
|
665
517
|
version: 1,
|
|
666
|
-
where: { paths: ['.']
|
|
518
|
+
where: { paths: ['.'] },
|
|
667
519
|
bans: 'a host element — any lowercase-initial tag, not only `<div>` — written anywhere in an app',
|
|
668
520
|
because: 'a host element carries neither `category` nor `style`, which are the two fields `ElementSchema` offers and the only places a look is declared, so the style guide cannot show it, a rebrand cannot move it and an overlay cannot vary it — it renders correctly and is absent from every derivation that reads the catalogue, which is why nothing catches it without a rule. Measured 2026-08-16: the rule that refused this was already written, 113 rows naming every HTML tag down to `<wbr>`, held in a book of app rules that nothing in this repository imported — its only mention anywhere was a tree row stating its address, and the book was deleted on 2026-08-17. Enumerated to the last tag, consumed by nothing, refusing nothing: the same shape a check takes when it measured zero files. This one governs the whole app rather than the screens directory, because a host element written in a composition ships to a screen either way',
|
|
669
521
|
instead: 'declare the element in the elements island and write the member that wears its look — `<Box.card>` — using the declared groupings `Column`, `Stack` and `Box` wherever a host element would have gone',
|
|
@@ -683,21 +535,13 @@ export const CONSTRAINTS = [
|
|
|
683
535
|
cites: [{ on: '2026-08-16', said: 'The ones most important are the ones that enforce UI through elements and screens' }],
|
|
684
536
|
},
|
|
685
537
|
],
|
|
686
|
-
derives: [
|
|
687
|
-
{
|
|
688
|
-
id: 'trial',
|
|
689
|
-
at: '_threads/_generated/trials.ts',
|
|
690
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
691
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
692
|
-
},
|
|
693
|
-
],
|
|
694
538
|
},
|
|
695
539
|
{
|
|
696
540
|
id: 'unpaired-member',
|
|
697
541
|
at: './constraint.ts',
|
|
698
542
|
describes: 'The refusal of half a member — a row that seeded no file, or a file no row brought into existence, so nothing an island governs can exist that was not declared first.',
|
|
699
543
|
version: 1,
|
|
700
|
-
where: { paths: ['threads/', 'operations/', 'entities/']
|
|
544
|
+
where: { paths: ['threads/', 'operations/', 'entities/'] },
|
|
701
545
|
bans: 'a member declared in an island\'s tree file whose file is not on disk, and a file in an island\'s folder that no row declares',
|
|
702
546
|
because: '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
547
|
instead: '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',
|
|
@@ -731,21 +575,13 @@ export const CONSTRAINTS = [
|
|
|
731
575
|
cites: [{ on: '2026-08-17', said: 'No wf_ without a row in workflows.ts' }],
|
|
732
576
|
},
|
|
733
577
|
],
|
|
734
|
-
derives: [
|
|
735
|
-
{
|
|
736
|
-
id: 'trial',
|
|
737
|
-
at: '_threads/_generated/trials.ts',
|
|
738
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
739
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
740
|
-
},
|
|
741
|
-
],
|
|
742
578
|
},
|
|
743
579
|
{
|
|
744
580
|
id: 'unversioned',
|
|
745
581
|
at: './constraint.ts',
|
|
746
582
|
describes: 'The refusal that makes a governing declaration say which one it is — anything other things must conform to states a version, so a change to it is a diff a consumer can see and a gap something can be carried across.',
|
|
747
583
|
version: 1,
|
|
748
|
-
where: { paths: ['.']
|
|
584
|
+
where: { paths: ['.'] },
|
|
749
585
|
bans: 'a declaration that governs and states no version — a node annotated `Threaded` or an interface extending it, and an island schema pinning no `specVersion`',
|
|
750
586
|
because: 'a version is not bookkeeping, it is the only thing that makes an update mechanical rather than archaeological: without it a consumer built against an older shape is indistinguishable from one built against this shape, so nothing can decide whether to migrate and nothing can tell what changed. The mechanism to carry a declaration forward already exists and has never been usable — `packages/core/spec/src/codemods.ts` holds the chain from one grammar version to the next and its own row says the chain is empty, because there was never a second version to carry anything to. Measured 2026-08-17: twelve of the thirteen island schemas pin a `specVersion` and `RegistryIslandSchema` pins none, so one kind of declaration was already unversioned and nothing said so; and the whole of `threads/` — the root, four metas, and every rule that governs a path — stated no version at all. This is the one rule that governs the governors, which is why it reaches by annotation rather than by naming them: a meta added tomorrow is covered without editing this row',
|
|
751
587
|
instead: 'state a `version` on the node, and a `specVersion` on the island schema — one integer, raised when what it governs changes shape',
|
|
@@ -764,21 +600,13 @@ export const CONSTRAINTS = [
|
|
|
764
600
|
cites: [{ on: '2026-08-17', said: 'The things that govern other things, we need those to be versioned.' }],
|
|
765
601
|
},
|
|
766
602
|
],
|
|
767
|
-
derives: [
|
|
768
|
-
{
|
|
769
|
-
id: 'trial',
|
|
770
|
-
at: '_threads/_generated/trials.ts',
|
|
771
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
772
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
773
|
-
},
|
|
774
|
-
],
|
|
775
603
|
},
|
|
776
604
|
{
|
|
777
605
|
id: 'declared-elsewhere',
|
|
778
606
|
at: './constraint.ts',
|
|
779
607
|
describes: 'The refusal that makes an island the only place its kind is declared — one rule for every island, reading what each one governs, so a kind added tomorrow is enforced the moment its row exists.',
|
|
780
608
|
version: 1,
|
|
781
|
-
where: { paths: ['.']
|
|
609
|
+
where: { paths: ['.'] },
|
|
782
610
|
bans: 'a declaration of a kind an island governs, written anywhere but where that island governs',
|
|
783
611
|
because: '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',
|
|
784
612
|
instead: 'declare it in the file the island governs, and import it where it is needed',
|
|
@@ -804,14 +632,6 @@ export const CONSTRAINTS = [
|
|
|
804
632
|
cites: [{ on: '2026-08-17', said: 'Each island governs it\'s thing.' }],
|
|
805
633
|
},
|
|
806
634
|
],
|
|
807
|
-
derives: [
|
|
808
|
-
{
|
|
809
|
-
id: 'trial',
|
|
810
|
-
at: '_threads/_generated/trials.ts',
|
|
811
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
812
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
813
|
-
},
|
|
814
|
-
],
|
|
815
635
|
},
|
|
816
636
|
{
|
|
817
637
|
id: 'inert-island',
|
|
@@ -831,7 +651,9 @@ export const CONSTRAINTS = [
|
|
|
831
651
|
},
|
|
832
652
|
right: {
|
|
833
653
|
files: [
|
|
834
|
-
{ at: '
|
|
654
|
+
{ at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly governs?: readonly string[]\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
|
|
655
|
+
{ at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_inert_probe.ts',\n },\n ],\n}\n" },
|
|
656
|
+
{ at: 'threads/_inert_probe.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n { id: 'events', at: 'packages/core/spec/src/islands/events.ts', governs: ['app/_declare/events.ts'] },\n ],\n}\n" },
|
|
835
657
|
{ at: 'packages/core/spec/src/islands/events.ts', source: 'export const EventsIslandSchema = {}\n' },
|
|
836
658
|
],
|
|
837
659
|
at: 'threads/_inert_probe.ts',
|
|
@@ -849,21 +671,13 @@ export const CONSTRAINTS = [
|
|
|
849
671
|
cites: [{ on: '2026-08-17', said: 'no island is inert' }],
|
|
850
672
|
},
|
|
851
673
|
],
|
|
852
|
-
derives: [
|
|
853
|
-
{
|
|
854
|
-
id: 'trial',
|
|
855
|
-
at: '_threads/_generated/trials.ts',
|
|
856
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
857
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
858
|
-
},
|
|
859
|
-
],
|
|
860
674
|
},
|
|
861
675
|
{
|
|
862
676
|
id: 'unnamed-island',
|
|
863
677
|
at: './constraint.ts',
|
|
864
678
|
describes: 'The refusal that stops an island arriving as a file — a schema the tree does not name states what an app may say while nothing states where it may say it, so the kind is declarable and every rule about it is absent.',
|
|
865
679
|
version: 1,
|
|
866
|
-
where: { paths: ['.']
|
|
680
|
+
where: { paths: ['.'] },
|
|
867
681
|
bans: 'a module exporting an island schema that no row in the tree names',
|
|
868
682
|
because: 'an island is a kind an app may declare, and adding one by writing a file is how a kind arrives with no rule attached — the schema parses, the loader can be pointed at it, and nothing anywhere says which file its declarations belong in. This is the guarantee that has to hold when a package contributes an island rather than the framework: a file alone cannot create a kind, because the row is what carries what it governs, and what it governs is what every rule about it reads. Absence is the finding here for the same reason it is in `unthreaded` — whatever the descent never reaches is not wired, and a schema nobody named is a kind nobody governs',
|
|
869
683
|
instead: 'name it from the row that owns it, stating what it governs — or delete the schema, because a kind with no rule is not a kind',
|
|
@@ -882,14 +696,6 @@ export const CONSTRAINTS = [
|
|
|
882
696
|
cites: [{ on: '2026-08-17', said: 'If there\'s an island that doesn\'t extends threader, error.' }],
|
|
883
697
|
},
|
|
884
698
|
],
|
|
885
|
-
derives: [
|
|
886
|
-
{
|
|
887
|
-
id: 'trial',
|
|
888
|
-
at: '_threads/_generated/trials.ts',
|
|
889
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
890
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
891
|
-
},
|
|
892
|
-
],
|
|
893
699
|
},
|
|
894
700
|
{
|
|
895
701
|
id: 'ungoverned-island',
|
|
@@ -922,21 +728,13 @@ export const CONSTRAINTS = [
|
|
|
922
728
|
cites: [{ on: '2026-08-16', said: 'Here\'s a new law I need you to add: No island without law.' }],
|
|
923
729
|
},
|
|
924
730
|
],
|
|
925
|
-
derives: [
|
|
926
|
-
{
|
|
927
|
-
id: 'trial',
|
|
928
|
-
at: '_threads/_generated/trials.ts',
|
|
929
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
930
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
931
|
-
},
|
|
932
|
-
],
|
|
933
731
|
},
|
|
934
732
|
{
|
|
935
733
|
id: 'no-barrel',
|
|
936
734
|
at: './constraint.ts',
|
|
937
735
|
version: 1,
|
|
938
736
|
describes: 'The refusal that stops a file existing to list what exists — a barrel says a name is there while the tree already says what it is, so the two drift apart and no import names the file anything came from.',
|
|
939
|
-
where: { paths: ['.']
|
|
737
|
+
where: { paths: ['.'] },
|
|
940
738
|
bans: 'a barrel — a file named index.ts, and any file whose whole content is re-exports',
|
|
941
739
|
because: 'the tree already answers what exists and what each thing is for, so a barrel is a second answer that goes stale on its own. Measured 2026-08-18: packages/core/spec/src/islands/index.ts held seventeen `export *` lines and was hand-edited four times in one session to drop islands that had been deleted, and derived-drift caught it only once the glossary stopped masking it. That same file made `islands.X` the import at 454 sites across 11 packages, so not one file said which islands it used and the dependency graph could not be read from the imports at all. every package here already ships with no barrel: one line of package.json exports maps every file to its own path, measured 2026-08-27 across all eight',
|
|
942
740
|
instead: 'export from the file that holds the thing, and let package.json carry a wildcard exports map so every file is importable at its own path. The tree is where you look to see what exists',
|
|
@@ -955,14 +753,6 @@ export const CONSTRAINTS = [
|
|
|
955
753
|
cites: [{ on: '2026-08-18', said: 'no barrels allowed' }],
|
|
956
754
|
},
|
|
957
755
|
],
|
|
958
|
-
derives: [
|
|
959
|
-
{
|
|
960
|
-
id: 'trial',
|
|
961
|
-
at: '_threads/_generated/trials.ts',
|
|
962
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
963
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
964
|
-
},
|
|
965
|
-
],
|
|
966
756
|
},
|
|
967
757
|
{
|
|
968
758
|
id: 'inert-slopnator',
|
|
@@ -987,14 +777,6 @@ export const CONSTRAINTS = [
|
|
|
987
777
|
cites: [{ on: '2026-08-19', said: 'No inert slopnator.' }],
|
|
988
778
|
},
|
|
989
779
|
],
|
|
990
|
-
derives: [
|
|
991
|
-
{
|
|
992
|
-
id: 'trial',
|
|
993
|
-
at: '_threads/_generated/trials.ts',
|
|
994
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
995
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
996
|
-
},
|
|
997
|
-
],
|
|
998
780
|
},
|
|
999
781
|
{
|
|
1000
782
|
id: 'no-child-process',
|
|
@@ -1002,11 +784,19 @@ export const CONSTRAINTS = [
|
|
|
1002
784
|
version: 1,
|
|
1003
785
|
describes: 'The refusal of a slopnator that can start a process — enforcement is one process reading files, and a slopnator that shells out can be re-entered by whatever it starts.',
|
|
1004
786
|
where: { paths: ['threads/slopnators/', 'thread/integrity/slopnators/'] },
|
|
1005
|
-
bans: 'a slopnator importing
|
|
787
|
+
bans: 'a slopnator importing the one place this framework starts a process',
|
|
1006
788
|
because: 'measured 2026-08-19, twice, on the founder\'s machine: `derived-drift` spawned every command a `derives` row named, one of which was `pnpm trial:try`, which runs the trial for `derived-drift`, which spawned `pnpm trial:try` again. Six-way branching, unbounded depth, each level a fresh process holding the whole engine. Killing the parent did nothing because the children were already orphaned to launchd, and the machine ran out of memory and had to be restarted — twice, an hour apart, from one inverted filter. No comparable tool does this: eslint, semgrep, clippy and ruff are each a single process. The recursion was possible only because a slopnator was allowed to start something, so the rule is about the capability rather than about any one slopnator that misused it',
|
|
1007
789
|
instead: 'read what you need in this process. If a slopnator cannot see something without running a command, that command is a gate step, declared once and run once in the chain — not a slopnator that fires on every sweep and every file written',
|
|
1008
|
-
wrong:
|
|
1009
|
-
|
|
790
|
+
wrong: {
|
|
791
|
+
files: [
|
|
792
|
+
{ 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" },
|
|
793
|
+
{ 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_programs.ts',\n },\n ],\n}\n" },
|
|
794
|
+
{ at: 'threads/_probe_programs.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const INTEGRATIONS: Threaded = {\n id: 'integrations',\n declares: [\n {\n id: 'programs',\n at: 'thread/integrations/cli.ts',\n },\n ],\n}\n" },
|
|
795
|
+
{ at: 'thread/integrity/slopnators/sl_probe_law.ts', source: "import { starts } from '../../integrations/cli.ts'\n" },
|
|
796
|
+
],
|
|
797
|
+
at: 'thread/integrity/slopnators/sl_probe_law.ts',
|
|
798
|
+
},
|
|
799
|
+
right: "import { readFileSync } from '../../integrations/disk.ts'",
|
|
1010
800
|
cites: [
|
|
1011
801
|
{ on: '2026-08-19', said: 'this last thing you did killed my mac. It spawned infinite number of processes called node which each took around 75mb, and they killed my memory, my ram, my disk. DO NOT DO THIS AGAIN.' },
|
|
1012
802
|
{ on: '2026-08-19', said: 'You can put a the no node child thing in place already. Make it real.' },
|
|
@@ -1019,26 +809,26 @@ export const CONSTRAINTS = [
|
|
|
1019
809
|
cites: [{ on: '2026-08-19', said: 'You can put a the no node child thing in place already.' }],
|
|
1020
810
|
},
|
|
1021
811
|
],
|
|
1022
|
-
derives: [
|
|
1023
|
-
{
|
|
1024
|
-
id: 'trial',
|
|
1025
|
-
at: '_threads/_generated/trials.ts',
|
|
1026
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
1027
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
1028
|
-
},
|
|
1029
|
-
],
|
|
1030
812
|
},
|
|
1031
813
|
{
|
|
1032
814
|
id: 'no-dispatch-bypass',
|
|
1033
815
|
at: './constraint.ts',
|
|
1034
816
|
describes: 'The refusal of a second way in — anything done with an imported operation other than handing it to `dispatch`, so identity, rate, replay, input, quota and rules end up decided by whoever happened to hold the object.',
|
|
1035
817
|
version: 1,
|
|
1036
|
-
where: { paths: ['.']
|
|
818
|
+
where: { paths: ['.'] },
|
|
1037
819
|
bans: 'reaching into an imported operation, calling it, or passing it anywhere but `dispatch`',
|
|
1038
|
-
because: 'measured 2026-08-22: nothing stopped it, and the tree said otherwise. The `dispatch` row this framework writes into every app states "a way to run that did not come through here does not exist", and the glossary publishes that sentence as the meaning of the word — while any file could `import { operation }` from a body and call `run` directly, skipping six gates in one line. A promise a tree makes about itself and cannot keep is worse than one it never made, because everything downstream reads the promise. Measured again 2026-08-24: the refusal read one form only, `… from \'<relative path>\'`, so three ways in were invisible — `await import(...)`, `createRequire(...)(...)`, and a path written from the tree root rather than from the file. Each was tried against a real tree and each passed unremarked. Measured a third time 2026-08-25: reading the BODY\'S ADDRESS still left a way in, because a body\'s address does not have to appear anywhere — `await import(\'../operations/\' + name + \'.ts\')` writes only a fragment, and a sweep over a real tree reported that file once, for being unthreaded, and not at all for this. So what is read is the DIRECTORY, not the file in it: an operation is seeded under `operations/`, dispatch reaches it by building the path at run time from the row and never writes it down, and everything else that gets there has to write a path pointing in — whole, or in pieces. Two things are not reached, and neither is an exception to the rule: dispatch, because it writes no such path, and the `at` of a row, read off the syntax tree rather than off the text, because naming a child is what an `at` is for and it is the one place the address belongs. The directory has to be read with its separator. Reading a bare `operations` as well fired on `id: \'operations\'` — the word this whole system is named after, sitting in the row that declares it — in both tree files at once, and the first reading of that misfire blamed the `at` exclusion and replaced it with a weaker one that matched `at:` as text. Nothing was wrong with the `at` exclusion; a word is not a path until it has a slash in it. Measured 2026-08-25, after dispatch was changed to take the operation itself instead of its name: this reading is now behind the rule it states. A caller has to import the body to hand it over, so the one sanctioned call site writes a path into `operations/` and is refused, while the deliberate bypass beside it writes the identical string — the reading cannot tell them apart, and a check that refuses the right answer and the wrong one alike is deciding nothing. What has to be read is what a file DOES with the operation, not whether it named the file: handing it to dispatch is the whole of a caller, and touching what it holds is the finding. Rewritten the same day: the reading is now over USE, not over paths. A file may import an operation — there is no other way to hand one over in this language — and the only thing it may then do with the binding is pass it to `dispatch`. Reaching into it, calling it, or passing it anywhere else is the finding, read off the syntax tree: a member access on the binding, a call whose callee is the binding, or the binding as an argument to anything that is not dispatch. Type-only imports bind nothing that can run and are skipped. Measured against a real tree the same day: the sanctioned caller, which imports the body and hands it to dispatch, is quiet, and the probe beside it, which imports the identical body and reaches into it, fires — the same two files the path reading refused alike. Measured 2026-08-28, after the founder said "I have no interest in shipping the framework. This is just scaffolding": dispatch moved, and its name did not. It was a value imported from `@thread/operations`, which is how ~900 lines of this framework ended up inside every artifact an app shipped; it is now the app\'s own `operations/_dispatch.ts`, written by `thread compile` — the same nine checks in the same order, emitted rather than imported. The first attempt at this renamed it to `gate`, and a real app refused that in two ways within one run of `thread gate`: `gate` was already coined by the command of that name, so the glossary held one word with two meanings, and the emitted file carried explanatory comments, which `no-comment` refuses in an app. Both were the same mistake — changing more than the ask needed. What this rule refuses did not change, the reading did not change, and the word a caller writes did not change. Only the import specifier did',
|
|
820
|
+
because: 'measured 2026-08-22: nothing stopped it, and the tree said otherwise. The `dispatch` row this framework writes into every app states "a way to run that did not come through here does not exist", and the glossary publishes that sentence as the meaning of the word — while any file could `import { operation }` from a body and call `run` directly, skipping six gates in one line. A promise a tree makes about itself and cannot keep is worse than one it never made, because everything downstream reads the promise. Measured again 2026-08-24: the refusal read one form only, `… from \'<relative path>\'`, so three ways in were invisible — `await import(...)`, `createRequire(...)(...)`, and a path written from the tree root rather than from the file. Each was tried against a real tree and each passed unremarked. Measured a third time 2026-08-25: reading the BODY\'S ADDRESS still left a way in, because a body\'s address does not have to appear anywhere — `await import(\'../operations/\' + name + \'.ts\')` writes only a fragment, and a sweep over a real tree reported that file once, for being unthreaded, and not at all for this. So what is read is the DIRECTORY, not the file in it: an operation is seeded under `operations/`, dispatch reaches it by building the path at run time from the row and never writes it down, and everything else that gets there has to write a path pointing in — whole, or in pieces. Two things are not reached, and neither is an exception to the rule: dispatch, because it writes no such path, and the `at` of a row, read off the syntax tree rather than off the text, because naming a child is what an `at` is for and it is the one place the address belongs. The directory has to be read with its separator. Reading a bare `operations` as well fired on `id: \'operations\'` — the word this whole system is named after, sitting in the row that declares it — in both tree files at once, and the first reading of that misfire blamed the `at` exclusion and replaced it with a weaker one that matched `at:` as text. Nothing was wrong with the `at` exclusion; a word is not a path until it has a slash in it. Measured 2026-08-25, after dispatch was changed to take the operation itself instead of its name: this reading is now behind the rule it states. A caller has to import the body to hand it over, so the one sanctioned call site writes a path into `operations/` and is refused, while the deliberate bypass beside it writes the identical string — the reading cannot tell them apart, and a check that refuses the right answer and the wrong one alike is deciding nothing. What has to be read is what a file DOES with the operation, not whether it named the file: handing it to dispatch is the whole of a caller, and touching what it holds is the finding. Rewritten the same day: the reading is now over USE, not over paths. A file may import an operation — there is no other way to hand one over in this language — and the only thing it may then do with the binding is pass it to `dispatch`. Reaching into it, calling it, or passing it anywhere else is the finding, read off the syntax tree: a member access on the binding, a call whose callee is the binding, or the binding as an argument to anything that is not dispatch. Type-only imports bind nothing that can run and are skipped. Measured against a real tree the same day: the sanctioned caller, which imports the body and hands it to dispatch, is quiet, and the probe beside it, which imports the identical body and reaches into it, fires — the same two files the path reading refused alike. Measured 2026-08-28, after the founder said "I have no interest in shipping the framework. This is just scaffolding": dispatch moved, and its name did not. It was a value imported from `@thread/operations`, which is how ~900 lines of this framework ended up inside every artifact an app shipped; it is now the app\'s own `operations/_dispatch.ts`, written by `thread compile` — the same nine checks in the same order, emitted rather than imported. The first attempt at this renamed it to `gate`, and a real app refused that in two ways within one run of `thread gate`: `gate` was already coined by the command of that name, so the glossary held one word with two meanings, and the emitted file carried explanatory comments, which `no-comment` refuses in an app. Both were the same mistake — changing more than the ask needed. What this rule refuses did not change, the reading did not change, and the word a caller writes did not change. Only the import specifier did. Which files are bodies is asked of the tree through two clauses from two authorities, because each alone was measured wrong: reading the directory refused the woven body for importing its own steps, and asking the owning word called every byproduct of that meta a body — measured 2026-09-14, the four-line file that hands the seams on was refused for spreading a seam table, because this had decided the file it came from was an operation. So a body is a file some row derives with the compile command AND whose word wears an operation prefix, and neither clause is enough alone. Three things bind no operation and are passed: a type-only import, which names a shape nothing can dispatch; a type query or reference, which is erased before anything runs and whose refusal was a false positive that would have refused the probe written to test the reassignment form; and dispatch itself, which lives beside the operations because an app writes its own, so a file importing it from there had the handler added to the set and was then refused for mishandling the handler. The file is parsed here rather than taken from the visitor, because the sweep calls this once per file with no node at all, and what a file DOES with a binding is a question about the whole file rather than about one node in it',
|
|
1039
821
|
instead: 'hand `dispatch` the operation and let it run. Handing it over is the whole of what a caller does; reaching into what it holds — calling the function it compiled to, reading what it requires — is reaching past every gate the operation sits behind',
|
|
1040
|
-
wrong: "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation.run(
|
|
1041
|
-
right:
|
|
822
|
+
wrong: "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation.run(opened, held)",
|
|
823
|
+
right: {
|
|
824
|
+
files: [
|
|
825
|
+
{ 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" },
|
|
826
|
+
{ 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" },
|
|
827
|
+
{ 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 describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
|
|
828
|
+
{ at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nawait dispatch(operation, input, actor, opened)\n" },
|
|
829
|
+
],
|
|
830
|
+
at: 'probe.tsx',
|
|
831
|
+
},
|
|
1042
832
|
cites: [
|
|
1043
833
|
{ on: '2026-08-17', said: 'That way we can enforce how things run, and they can only run through dispatch.' },
|
|
1044
834
|
{ on: '2026-08-17', said: 'when something wants to run an operation (meaning an invoke), it\'s a simple, direct, dispatch(wf_) or dispatch(agt_) and it just works, it just runs, and everything is as is.' },
|
|
@@ -1053,21 +843,13 @@ export const CONSTRAINTS = [
|
|
|
1053
843
|
cites: [{ on: '2026-08-22', said: 'I guess you should do both then.' }],
|
|
1054
844
|
},
|
|
1055
845
|
],
|
|
1056
|
-
derives: [
|
|
1057
|
-
{
|
|
1058
|
-
id: 'trial',
|
|
1059
|
-
at: '_threads/_generated/trials.ts',
|
|
1060
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
1061
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
1062
|
-
},
|
|
1063
|
-
],
|
|
1064
846
|
},
|
|
1065
847
|
{
|
|
1066
848
|
id: 'undeclared-node',
|
|
1067
849
|
at: './constraint.ts',
|
|
1068
850
|
describes: 'The refusal of a step nobody declared — an operation built from a node the descent never reaches, so the flow reads as agreed while one of its spans is a step this app never said it could take.',
|
|
1069
851
|
version: 1,
|
|
1070
|
-
where: { paths: ['operations/']
|
|
852
|
+
where: { paths: ['operations/'] },
|
|
1071
853
|
bans: 'an operation reaching a node under `nodes/` that the tree does not declare',
|
|
1072
854
|
because: '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',
|
|
1073
855
|
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',
|
|
@@ -1102,23 +884,15 @@ export const CONSTRAINTS = [
|
|
|
1102
884
|
cites: [{ on: '2026-08-22', said: 'Operations are bilt of nodes, only nodes that are declared.' }],
|
|
1103
885
|
},
|
|
1104
886
|
],
|
|
1105
|
-
derives: [
|
|
1106
|
-
{
|
|
1107
|
-
id: 'trial',
|
|
1108
|
-
at: '_threads/_generated/trials.ts',
|
|
1109
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
1110
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
1111
|
-
},
|
|
1112
|
-
],
|
|
1113
887
|
},
|
|
1114
888
|
{
|
|
1115
889
|
id: 'unprefixed-member',
|
|
1116
890
|
at: './constraint.ts',
|
|
1117
891
|
describes: 'The refusal of a row wearing the wrong prefix — an operation declared among the nodes or a node among the operations, so what a row IS stops being readable from its name and everything downstream believes the list it happens to sit in.',
|
|
1118
892
|
version: 1,
|
|
1119
|
-
where: { paths: ['.']
|
|
893
|
+
where: { paths: ['.'] },
|
|
1120
894
|
bans: 'a row in a tree file\'s `MEMBERS` whose id does not carry a prefix that list takes — `wf_`, `vw_` or `agt_` in the operations tree, `nd_` in the nodes tree',
|
|
1121
|
-
because: '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',
|
|
895
|
+
because: '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. What a row may wear is not written out here either — the prefixes come from the one list that `thread create` and this law both read, so a kind added there is never a kind this refuses. The braces are read off the code mask this package already ships rather than counted in the raw text, because counting them directly desynced on a describes holding a brace, and a second weaker walker beside the one that exists is the drift it exists to prevent',
|
|
1122
896
|
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',
|
|
1123
897
|
wrong: {
|
|
1124
898
|
files: [
|
|
@@ -1150,58 +924,15 @@ export const CONSTRAINTS = [
|
|
|
1150
924
|
cites: [{ on: '2026-08-22', said: 'I guess you should do both then.' }],
|
|
1151
925
|
},
|
|
1152
926
|
],
|
|
1153
|
-
derives: [
|
|
1154
|
-
{
|
|
1155
|
-
id: 'trial',
|
|
1156
|
-
at: '_threads/_generated/trials.ts',
|
|
1157
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
1158
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
1159
|
-
},
|
|
1160
|
-
],
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
id: 'unkept-rows',
|
|
1164
|
-
at: './constraint.ts',
|
|
1165
|
-
describes: 'The refusal of an app that declares somewhere to keep its rows and then keeps them somewhere else. A target that holds entities derives a store of its own; if what says where rows go names none of them, the tables are written, the migrations are written, the gate is green, and the database is never opened.',
|
|
1166
|
-
version: 1,
|
|
1167
|
-
where: { paths: ['.'], except: ['_generated/', 'node_modules/'] },
|
|
1168
|
-
bans: 'a store derived for a declared target while what says where rows go names none of them',
|
|
1169
|
-
because: 'until 2026-09-01 there was exactly one implementation of the store this framework ships, and it held rows in a Map. A target derived a schema and its migrations and nothing that could talk to the database those describe — so an app declared postgres, had its tables written, passed every one of its checks, and then kept every row in memory, losing them all on restart with nothing anywhere saying so. The store exists now; this is what stops the same silence coming back, because every surface that could have noticed the old state reported success',
|
|
1170
|
-
instead: 'run `thread capabilities`, which writes where rows go from what this app declares it keeps. If what it wrote is the in-memory stand-in, then this app declares no target that keeps entities — declare one with `thread create target` and put an entity in it with `thread create entity`',
|
|
1171
|
-
wrong: 'a store is derived for a declared target and where rows go still names the in-memory stand-in',
|
|
1172
|
-
right: 'a store is derived for a declared target and where rows go names that store',
|
|
1173
|
-
cites: [
|
|
1174
|
-
{
|
|
1175
|
-
on: '2026-09-01',
|
|
1176
|
-
said: 'What the fuck does that mean? Cause this seems to be the thing that NEVER IS READY. WHY IS THIS NOT READY? WHAT NEEDS TO CHANGE???????',
|
|
1177
|
-
},
|
|
1178
|
-
{ on: '2026-09-01', said: 'Yes — all three' },
|
|
1179
|
-
],
|
|
1180
|
-
seeds: [
|
|
1181
|
-
{
|
|
1182
|
-
id: 'slopnator',
|
|
1183
|
-
at: './slopnators/sl_unkept_rows.ts',
|
|
1184
|
-
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1185
|
-
cites: [{ on: '2026-08-22', said: 'I guess you should do both then.' }],
|
|
1186
|
-
},
|
|
1187
|
-
],
|
|
1188
|
-
derives: [
|
|
1189
|
-
{
|
|
1190
|
-
id: 'trial',
|
|
1191
|
-
at: '_threads/_generated/trials.ts',
|
|
1192
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
1193
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
1194
|
-
},
|
|
1195
|
-
],
|
|
1196
927
|
},
|
|
1197
928
|
{
|
|
1198
929
|
id: 'untranslated-key',
|
|
1199
930
|
at: './constraint.ts',
|
|
1200
931
|
describes: 'The refusal of wording an app asks for and cannot say — a key named in the interface that a declared language answers with nothing, which renders as a blank where a sentence belongs and looks, to whoever sees it, like a broken component rather than an unfinished translation.',
|
|
1201
932
|
version: 1,
|
|
1202
|
-
where: { paths: ['.']
|
|
933
|
+
where: { paths: ['.'] },
|
|
1203
934
|
bans: 'a message key named by an element and left empty, or absent, in any language the app declares',
|
|
1204
|
-
because: 'until 2026-08-29 an app could not declare a single word of its own: the translator was built over the catalogue compiled into `@thread/ui`, and a key that catalogue did not hold threw at render. So every app showed the framework\'s English and nothing else, and there was no file anywhere for a second language to go in. With the wording generated, the remaining hole is the one that cannot be generated — nobody can invent the sentence, so a key arrives holding an empty string, and an empty string is indistinguishable at every other surface from a translated one. The compiler catches a key a language has no LINE for; only this catches a line with nothing in it',
|
|
935
|
+
because: 'until 2026-08-29 an app could not declare a single word of its own: the translator was built over the catalogue compiled into `@thread/ui`, and a key that catalogue did not hold threw at render. So every app showed the framework\'s English and nothing else, and there was no file anywhere for a second language to go in. With the wording generated, the remaining hole is the one that cannot be generated — nobody can invent the sentence, so a key arrives holding an empty string, and an empty string is indistinguishable at every other surface from a translated one. The compiler catches a key a language has no LINE for; only this catches a line with nothing in it. It reads the files that are on disk and the markup that is actually there rather than calling the generator, because a check that runs the thing it is checking passes whenever that thing passes, which is no check at all. And which props carry a key is read off the declarations of the installed package itself: a list written into this law would go stale the first time an element gained a prop — silently, by finding fewer keys than there are, which is the failure this law exists to prevent',
|
|
1205
936
|
instead: 'write it with `thread create message`, which asks for every declared language at the moment the key is made and refuses a blank answer, or fill the line `thread i18n` added to each `i18n/<language>.json`',
|
|
1206
937
|
wrong: '<Button label="checkout.confirm" /> with { "checkout.confirm": "" } in i18n/pt-BR.json',
|
|
1207
938
|
right: '<Button label="checkout.confirm" /> with { "checkout.confirm": "Confirmar" } in i18n/pt-BR.json',
|
|
@@ -1217,14 +948,6 @@ export const CONSTRAINTS = [
|
|
|
1217
948
|
cites: [{ on: '2026-08-22', said: 'I guess you should do both then.' }],
|
|
1218
949
|
},
|
|
1219
950
|
],
|
|
1220
|
-
derives: [
|
|
1221
|
-
{
|
|
1222
|
-
id: 'trial',
|
|
1223
|
-
at: '_threads/_generated/trials.ts',
|
|
1224
|
-
describes: 'The proof that a rule does what its row claims — the rule fired at the form it says it refuses and at the fix it recommends, derived from the row and the slopnator it names rather than written by hand.',
|
|
1225
|
-
cites: [{ on: '2026-08-14', said: 'Slopnators though also need to run. So they derive their own tests.' }],
|
|
1226
|
-
},
|
|
1227
|
-
],
|
|
1228
951
|
},
|
|
1229
952
|
{
|
|
1230
953
|
id: 'only-takes-a-literal',
|
|
@@ -1245,4 +968,182 @@ export const CONSTRAINTS = [
|
|
|
1245
968
|
},
|
|
1246
969
|
],
|
|
1247
970
|
},
|
|
971
|
+
{
|
|
972
|
+
id: 'unvaulted-secret',
|
|
973
|
+
at: 'thread/integrity/constraint.ts',
|
|
974
|
+
describes: 'The refusal of a secret taken from the environment instead of the vault. The vault refuses a name no row declares and a value nothing sealed; the environment refuses nothing, so whoever reads it is handed whatever the machine happens to hold under that name.',
|
|
975
|
+
version: 1,
|
|
976
|
+
where: { paths: ['.'] },
|
|
977
|
+
bans: 'reading process.env under a name the file does not state, or under a name wearing a secret prefix',
|
|
978
|
+
because: 'measured 2026-09-15: this repository shipped a resolver that read process.env under the uppercased name of whatever was asked for, and installed it as the vault seam whenever the environment said production. Every gate the vault states — that the name is one a row declares, that a value was sealed for this environment, that the key opens it — was skipped for every secret, and a server credential and a browser value read identically. Nothing refused it, because secret-literal reads what a string contains and a member expression contains nothing, and because that law named an environment read among the things it deliberately does not find. The two halves are refused for different reasons: a name the file states can be compared against the secrets, while a name nobody reading the file can see can be any of them. Measured 2026-09-17, against the rule that a check may hold no list of its own: the two prefixes this read were a list typed into thread/integrations/secret.ts, and I first recorded here that they were where the tree coins them. That was wrong and the founder caught it: `sec_` and `pub_` are coined in the sentence on the secrets row and in the glossary, which is prose the descent cannot read. They come from the declared secrets now — every secret row wears one, and the prefixes are what those ids begin with — so a new kind of secret widens this without anybody typing it.',
|
|
979
|
+
instead: 'ask the vault. A declared secret is opened by the file `thread vault` writes, which refuses a name no row states and a value nothing sealed. Nothing inside an operation ever holds the value: the operation names a target on its `requires`, dispatch opens what that target is wired to send, and it goes straight into the call. If what you are reading is not a secret, give it a name wearing no secret prefix and write that name in the file rather than computing it',
|
|
980
|
+
wrong: 'const key = process.env[named.toUpperCase()]',
|
|
981
|
+
right: 'const mode = process.env[\'NODE_ENV\']',
|
|
982
|
+
seeds: [
|
|
983
|
+
{
|
|
984
|
+
id: 'slopnator',
|
|
985
|
+
at: 'thread/integrity/slopnators/sl_unvaulted_secret.ts',
|
|
986
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
id: 'undeclared-list',
|
|
992
|
+
at: 'thread/integrity/constraint.ts',
|
|
993
|
+
describes: 'The refusal of a check that decides from a list of words somebody typed. A rule reaches exactly as far as those words, so the first thing built under a name nobody typed walks straight past and the rule reports green over it. The list is followed wherever it was put — wrapped in as const, nested under a key, spread out of a helper, turned into a set of its own keys, or moved into another file and imported back.',
|
|
994
|
+
version: 1,
|
|
995
|
+
where: { paths: ['thread/integrity/slopnators/', 'threads/slopnators/'] },
|
|
996
|
+
bans: 'a check holding its own list of words — a name set to a written-out list, or to something built from one',
|
|
997
|
+
because: 'Measured 2026-09-16: `undeclared-crossing` says only a declared operation may act, and its check carried ten ways of acting written out. Starting a process was not one of them, so 48 places that start one reported green for as long as that list stood. Founder the same day: it must not be a hand written list. Measured 2026-09-17: this reading called every `!X.has(y)` a list the check only skips, so a catch list written as a negated guard that leaves the check — `if (!STARTS_A_PROCESS.has(from)) return false` — read as exempt, and `no-child-process` carried two spellings and reported green over every other way to start a process. What decides it is what the branch does: leaving the check on a membership test is the list deciding the refusal, while a `continue` in a loop is the list stepping over one candidate.',
|
|
998
|
+
instead: 'Ask the tree for the words. Every one this framework can refuse is coined by a row, so `thread inspect <word>` names where any of them is declared, and a check that reads those rows widens when the tree does rather than when somebody remembers to type.',
|
|
999
|
+
wrong: 'const CROSSINGS = [\'fetch\', \'setTimeout\', \'setInterval\']',
|
|
1000
|
+
right: 'const CROSSINGS = declared()',
|
|
1001
|
+
seeds: [
|
|
1002
|
+
{
|
|
1003
|
+
id: 'slopnator',
|
|
1004
|
+
at: 'thread/integrity/slopnators/sl_undeclared_list.ts',
|
|
1005
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1006
|
+
},
|
|
1007
|
+
],
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
id: 'screen-holds-only-declared',
|
|
1011
|
+
at: 'thread/integrity/constraint.ts',
|
|
1012
|
+
describes: 'The refusal of anything in a screen that is not one of the three things a screen is made of. A screen arranges declared elements and compositions and may hand an operation to dispatch; it reaches nothing and computes nothing itself, so a fetch or a clock written into one has nowhere to be rather than a rule standing against it.',
|
|
1013
|
+
version: 1,
|
|
1014
|
+
where: { paths: ['screens/', 'compositions/'] },
|
|
1015
|
+
bans: 'a call in a screen that is not an element, a composition, or an operation handed to dispatch',
|
|
1016
|
+
because: 'Measured 2026-09-17: undeclared-crossing carried twenty-one host spellings to catch exactly this, and reported nothing across all 320 files of this repository, because the framework itself always goes through a declared door. A screen is the one place those spellings were for: the screens command seeds an empty screen file and a person fills the body in, so a raw fetch can be typed straight into one. Founder the same day: Fetch just should not have a way to exist. Naming the ways to reach outside is a list that goes stale; naming the three things a screen may hold is not, because those three come from rows.',
|
|
1017
|
+
instead: 'put the work in an operation and hand it to dispatch, and put what is drawn in an element or a composition. thread create operation declares the first, thread create element the second, and a screen then names them.',
|
|
1018
|
+
wrong: 'export const Home = screen(\'home\', {}, () => fetch(address))',
|
|
1019
|
+
right: {
|
|
1020
|
+
files: [
|
|
1021
|
+
{ 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" },
|
|
1022
|
+
{ 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" },
|
|
1023
|
+
{ 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_place_order',\n describes: 'places an order',\n derives: [\n {\n id: 'wf_place_order_body',\n at: 'operations/wf_place_order.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
|
|
1024
|
+
{ at: 'screens/probe.tsx', source: "import { dispatch } from '../operations/_dispatch.ts'\n\nimport { operation } from '../operations/wf_place_order.ts'\n\nexport const Home = screen('home', {}, () => dispatch(operation, input, actor, opened))\n" },
|
|
1025
|
+
],
|
|
1026
|
+
at: 'screens/probe.tsx',
|
|
1027
|
+
},
|
|
1028
|
+
seeds: [
|
|
1029
|
+
{
|
|
1030
|
+
id: 'slopnator',
|
|
1031
|
+
at: 'thread/integrity/slopnators/sl_screen_holds_only_declared.ts',
|
|
1032
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
id: 'undeclared-webhook',
|
|
1038
|
+
at: 'thread/integrity/constraint.ts',
|
|
1039
|
+
describes: 'The refusal of a webhook and an operation that do not agree. A webhook names the operation it invokes and that operation names the webhook back, so nothing outside this app reaches an operation that never said it would take it.',
|
|
1040
|
+
version: 1,
|
|
1041
|
+
where: { paths: ['threads/', 'app/inbound/'] },
|
|
1042
|
+
bans: 'a webhook invoking an operation that does not name it back, and a webhook carrying no signature reaching an operation that takes only signed ones',
|
|
1043
|
+
because: 'measured 2026-09-17: a webhook is a url anybody on the internet may post to, so whatever sits behind it is reachable by whoever finds the path. One row saying which operation it runs is half a decision, and the missing half is the one that matters: the operation never agreed to be reached that way, and nothing anywhere recorded that it had.',
|
|
1044
|
+
instead: 'declare the webhook with `thread create webhook new <name>`, which asks the target it is `of`, the `path` it lands on, and the operation it `invokes`. Then name it back on that operation with `thread edit <operation> --webhooks <webhook>` — the same command with no field prints what that row says now, every field it takes with it, `webhooks` and `signed` among them. An operation whose row says `signed` needs the other half as well: `arrives` on the webhook, naming the header its signature comes in on, and a secret declared with `thread create secret new <name>` in what that webhook requires. The route is never written by hand — `thread capabilities` derives it from these rows.',
|
|
1045
|
+
wrong: "const WH = { id: 'wh_stripe', invokes: 'wf_charge' }",
|
|
1046
|
+
right: "const WH = { id: 'wh_stripe', describes: 'Where stripe posts what it has done.' }",
|
|
1047
|
+
seeds: [
|
|
1048
|
+
{
|
|
1049
|
+
id: 'slopnator',
|
|
1050
|
+
at: 'thread/integrity/slopnators/sl_undeclared_webhook.ts',
|
|
1051
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1052
|
+
},
|
|
1053
|
+
],
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
id: 'outside-an-operation',
|
|
1057
|
+
at: 'thread/integrity/constraint.ts',
|
|
1058
|
+
describes: 'The refusal of a call on what this app hands an operation, made anywhere but inside one. An operation is opened with a store, a target or a program and is the only thing that may use them, so a file that gets hold of one and calls on it is doing an operation work where nothing declared it.',
|
|
1059
|
+
version: 1,
|
|
1060
|
+
where: { paths: ['.'] },
|
|
1061
|
+
bans: 'a call on what this app hands an operation, made outside an operation',
|
|
1062
|
+
because: 'Measured 2026-09-17: undeclared-crossing read two different things under one name. One is a reach past this process that goes through no declared integration; the other is a call on what an operation was opened with, made by something that is not an operation. They fire on different forms, they are fixed in different ways, and a single name meant neither could be stated on its own. Founder the same day: Then separate them. One is for things that reach or ways the repo is reached outside the repo and are not integrations, then there is things that reach outside or are reached by an outside thing and are not operations.',
|
|
1063
|
+
instead: 'declare an operation and let dispatch open it. What an operation is handed is handed to it, and nothing else in this app is given a way to use it.',
|
|
1064
|
+
wrong: {
|
|
1065
|
+
files: [
|
|
1066
|
+
{ at: 'thread/operations/operations.ts', source: 'export interface Store {\n insert(row: unknown): Promise<void>\n}\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Store }\n' },
|
|
1067
|
+
{ at: 'threads/_probe_target.ts', source: "import type { Opened } from '../thread/operations/operations.ts'\n\nexport const kept = async (opened: Opened<'pg_main'>): Promise<void> => {\n await opened['pg_main']['insert']({})\n}\n" },
|
|
1068
|
+
],
|
|
1069
|
+
at: 'threads/_probe_target.ts',
|
|
1070
|
+
},
|
|
1071
|
+
right: 'export const carried = (opened: Opened<\'pg_main\'>, run: (given: Opened<\'pg_main\'>) => Promise<void>): Promise<void> => run(opened)',
|
|
1072
|
+
seeds: [
|
|
1073
|
+
{
|
|
1074
|
+
id: 'slopnator',
|
|
1075
|
+
at: 'thread/integrity/slopnators/sl_outside_an_operation.ts',
|
|
1076
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1077
|
+
},
|
|
1078
|
+
],
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
id: 'underived-route',
|
|
1082
|
+
at: 'thread/integrity/constraint.ts',
|
|
1083
|
+
describes: 'The refusal of a route nothing derives. A file that answers a request is written by a generator out of rows, never by hand, so the only shape allowed is a derives row naming it with the command that writes it and the command that catches it drifting.',
|
|
1084
|
+
version: 1,
|
|
1085
|
+
where: { paths: ['threads/'] },
|
|
1086
|
+
bans: 'a route file that is not a derives row carrying both by and runs',
|
|
1087
|
+
because: 'measured 2026-09-17: a generator wrote app/inbound/wh_stripe/route.ts while no row anywhere named that address. Nothing refused it, because unthreaded only asks whether some row names a file and derived-drift only guards addresses a derives row already states. A route is the one file anybody on the internet can reach, so a route nobody declared is a door nobody agreed to.',
|
|
1088
|
+
instead: 'declare the generator with `thread create generator`, which writes its row into GENERATORS with the `by` command it answers, then give the thing that owns the route a `derives` row naming the route file, with `by` set to that command and `runs` set to that command and --check. Both rows or neither: the generator makes the bytes, and the derives row is what says the address was agreed to.',
|
|
1089
|
+
wrong: 'const M = { declares: [{ at: \'app/inbound/wh_stripe/route.ts\' }] }',
|
|
1090
|
+
right: {
|
|
1091
|
+
files: [
|
|
1092
|
+
{ at: 'threads/thread.ts', source: "export const THREAD = {\n declares: [\n { at: 'threads/_probe_route.ts' },\n ],\n}\n" },
|
|
1093
|
+
{ at: 'threads/_probe_route.ts', source: "export const M = {\n derives: [\n { at: 'app/inbound/wh_stripe/route.ts', by: 'thread capabilities', runs: 'thread capabilities --check' },\n ],\n}\n" },
|
|
1094
|
+
],
|
|
1095
|
+
at: 'threads/_probe_route.ts',
|
|
1096
|
+
},
|
|
1097
|
+
seeds: [
|
|
1098
|
+
{
|
|
1099
|
+
id: 'slopnator',
|
|
1100
|
+
at: 'thread/integrity/slopnators/sl_underived_route.ts',
|
|
1101
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1102
|
+
},
|
|
1103
|
+
],
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
id: 'door-in-a-node',
|
|
1107
|
+
at: 'thread/integrity/constraint.ts',
|
|
1108
|
+
describes: 'The refusal of a node that reaches outside by importing the framework\'s own way out — the file that starts programs, reads the disk or sends over https — so what it does out there goes through no declared target and nothing prices it, replays it in a trial or refuses it.',
|
|
1109
|
+
version: 1,
|
|
1110
|
+
where: { paths: ['operations/'] },
|
|
1111
|
+
bans: 'a node importing a file that reaches past this process — by a Node builtin, a global like fetch, or a driver — or importing anything that imports one',
|
|
1112
|
+
because: 'measured 2026-09-22: `undeclared-crossing` refuses the spellings that reach past this process — `node:child_process`, `node:fs`, `fetch` and the rest — and lets a file declared under `integrations` use them, because those files are the doors this framework opens on an operation\'s behalf. A node that imports one of them writes no such spelling, so that check read it as clean, and it required no target, so no target check had anything to read. Nothing read what the imported file opens, and a node could run git or write the disk with every check green',
|
|
1113
|
+
instead: 'require the target on the operation and call what the node is handed. `thread create target` declares the place and `thread create target call` each thing it is asked; the operation\'s `requires` names the target, and the node receives one function per call. A type may be imported from anywhere, because a type opens nothing',
|
|
1114
|
+
wrong: {
|
|
1115
|
+
files: [
|
|
1116
|
+
{ at: 'thread/integrations/cli.ts', source: "import { spawn } from 'node:child_process'\n\nexport const starts = (program: string): void => {\n spawn(program)\n}\n" },
|
|
1117
|
+
{ at: 'operations/_probe_node.ts', source: "import { starts } from '../thread/integrations/cli.ts'\n\nexport const node = (): void => {\n starts('git')\n}\n" },
|
|
1118
|
+
],
|
|
1119
|
+
at: 'operations/_probe_node.ts',
|
|
1120
|
+
},
|
|
1121
|
+
right: 'import { z } from \'zod\'',
|
|
1122
|
+
seeds: [
|
|
1123
|
+
{
|
|
1124
|
+
id: 'slopnator',
|
|
1125
|
+
at: 'thread/integrity/slopnators/sl_door_in_a_node.ts',
|
|
1126
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1127
|
+
},
|
|
1128
|
+
],
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
id: 'unlisted-program',
|
|
1132
|
+
at: 'thread/integrity/constraint.ts',
|
|
1133
|
+
describes: 'The refusal of a cli target naming a program this tree did not install — a program found on the machine instead of under node_modules/.bin is whatever that machine has, so a target on it bounds nothing and a declared call on it can run any line.',
|
|
1134
|
+
version: 1,
|
|
1135
|
+
where: { paths: ['threads/'] },
|
|
1136
|
+
bans: 'a `cli_` target whose program is neither one this tree installed under node_modules/.bin, nor git, node or thread, nor the package manager the lockfile picks',
|
|
1137
|
+
because: 'measured 2026-09-22: the program a cli target starts fell back to the bare name when nothing under node_modules/.bin carried it, so a target `cli_bash` with a call at `-c` would have started a shell with whatever line an operation handed it, and every check stayed green — the prefix was right, the call was declared, the operation required it. Measured the same day: `cli_typescript` was declared naming a program that does not exist, since the installed executable is `tsc`, and nothing said so until a build tried to start it',
|
|
1138
|
+
instead: 'name the target after a program this tree installs, so `cli_` and the file under node_modules/.bin agree — `cli_tsc`, not `cli_typescript` — or after git, node, thread or `packager`, the word for whichever package manager the lockfile picks. A program the tree does not install yet is added as a dependency first, so an install brings it and the lockfile records it',
|
|
1139
|
+
wrong: 'export const TARGETS: readonly Target[] = [{ id: \'cli_typescript\', of: \'in_typescript\', describes: \'The compiler this tree is checked and built with.\', version: 1, takes: [], requires: [] }]',
|
|
1140
|
+
right: 'export const TARGETS: readonly Target[] = [{ id: \'cli_git\', of: \'in_git\', describes: \'The git on the machine a command runs on.\', version: 1, takes: [], requires: [] }]',
|
|
1141
|
+
seeds: [
|
|
1142
|
+
{
|
|
1143
|
+
id: 'slopnator',
|
|
1144
|
+
at: 'thread/integrity/slopnators/sl_unlisted_program.ts',
|
|
1145
|
+
describes: 'The code that finds what a rule refuses — one per rule, resolved from the rule\'s id, in one of three kinds: a pass over a syntax tree, a pass over a file\'s text, or a command whose output is read.',
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
},
|
|
1248
1149
|
];
|