@prestack/thread 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +68 -0
  7. package/dist/core/tree.d.ts +1 -0
  8. package/dist/core/tree.js +7 -1
  9. package/dist/derivation/generators/build.d.ts +3 -1
  10. package/dist/derivation/generators/build.js +103 -22
  11. package/dist/derivation/generators/release.js +74 -26
  12. package/dist/derivation/release.js +3 -3
  13. package/dist/integrations/cli.js +69 -10
  14. package/dist/integrations/pglite.js +13 -3
  15. package/dist/integrations/target.d.ts +3 -0
  16. package/dist/integrity/attach.js +1 -5
  17. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  18. package/dist/operations/access.js +3 -2
  19. package/dist/operations/capabilities.d.ts +2 -0
  20. package/dist/operations/capabilities.js +2 -0
  21. package/dist/operations/generators/capability.js +234 -24
  22. package/dist/operations/generators/compile.js +150 -50
  23. package/dist/operations/generators/seed.d.ts +3 -0
  24. package/dist/operations/generators/seed.js +25 -3
  25. package/dist/operations/nodes.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/dist/core/actor.ts +0 -21
  28. package/dist/core/attach.ts +0 -613
  29. package/dist/core/chain.ts +0 -145
  30. package/dist/core/condition.ts +0 -227
  31. package/dist/core/descend.ts +0 -1185
  32. package/dist/core/errors/catalogue.ts +0 -314
  33. package/dist/core/errors/classes.ts +0 -51
  34. package/dist/core/errors/error.ts +0 -152
  35. package/dist/core/errors/namespaces.ts +0 -19
  36. package/dist/core/finding.ts +0 -37
  37. package/dist/core/generators/commit.ts +0 -157
  38. package/dist/core/init.ts +0 -222
  39. package/dist/core/invoked.ts +0 -52
  40. package/dist/core/round.ts +0 -74
  41. package/dist/core/rows.ts +0 -506
  42. package/dist/core/scope.ts +0 -20
  43. package/dist/core/thread.ts +0 -29
  44. package/dist/core/tree.ts +0 -90
  45. package/dist/core/types.ts +0 -351
  46. package/dist/derivation/attach.ts +0 -57
  47. package/dist/derivation/generators/build.ts +0 -371
  48. package/dist/derivation/generators/check.ts +0 -118
  49. package/dist/derivation/generators/determinism.ts +0 -145
  50. package/dist/derivation/generators/release.ts +0 -236
  51. package/dist/derivation/release.ts +0 -43
  52. package/dist/integrations/attach.ts +0 -187
  53. package/dist/integrations/cli.ts +0 -606
  54. package/dist/integrations/declared.ts +0 -34
  55. package/dist/integrations/disk.ts +0 -466
  56. package/dist/integrations/generators/read.ts +0 -127
  57. package/dist/integrations/generators/vault.ts +0 -210
  58. package/dist/integrations/https.ts +0 -216
  59. package/dist/integrations/pglite.ts +0 -206
  60. package/dist/integrations/secret.ts +0 -54
  61. package/dist/integrations/target.ts +0 -672
  62. package/dist/integrations/vault.ts +0 -148
  63. package/dist/integrity/attach.ts +0 -150
  64. package/dist/integrity/check-threads.ts +0 -166
  65. package/dist/integrity/constraint.ts +0 -1246
  66. package/dist/integrity/generators/compiler.ts +0 -138
  67. package/dist/integrity/generators/gate.ts +0 -227
  68. package/dist/integrity/generators/issues.ts +0 -160
  69. package/dist/integrity/generators/publish.ts +0 -233
  70. package/dist/integrity/generators/trial.ts +0 -359
  71. package/dist/integrity/lint.ts +0 -214
  72. package/dist/integrity/lsp/bin.ts +0 -44
  73. package/dist/integrity/lsp/diagnostics.ts +0 -92
  74. package/dist/integrity/lsp/findings.ts +0 -63
  75. package/dist/integrity/lsp/lint.ts +0 -124
  76. package/dist/integrity/lsp/protocol.ts +0 -78
  77. package/dist/integrity/lsp/server.ts +0 -224
  78. package/dist/integrity/slopnator.ts +0 -324
  79. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  80. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  81. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  82. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  83. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  84. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  85. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  86. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  87. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  88. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  89. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  90. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  91. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  92. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  93. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  94. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  95. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  96. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  97. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  98. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  99. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  100. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  101. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  102. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  103. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  104. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  105. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  106. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  107. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  108. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  109. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  110. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  111. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  112. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  113. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  114. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  115. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  116. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  117. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  118. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  119. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  120. package/dist/integrity/sweep.ts +0 -395
  121. package/dist/knowledge/attach.ts +0 -327
  122. package/dist/knowledge/generators/docsgen.ts +0 -186
  123. package/dist/knowledge/generators/glossary.ts +0 -73
  124. package/dist/knowledge/glossary.ts +0 -6
  125. package/dist/operations/access.ts +0 -131
  126. package/dist/operations/attach.ts +0 -408
  127. package/dist/operations/capabilities.ts +0 -88
  128. package/dist/operations/dispatch.ts +0 -294
  129. package/dist/operations/error-results.ts +0 -31
  130. package/dist/operations/generators/capability.ts +0 -1054
  131. package/dist/operations/generators/compile.ts +0 -701
  132. package/dist/operations/generators/permits.ts +0 -229
  133. package/dist/operations/generators/seed.ts +0 -359
  134. package/dist/operations/generators/trial.ts +0 -666
  135. package/dist/operations/nodes.ts +0 -53
  136. package/dist/operations/operations.ts +0 -91
  137. package/dist/operations/result.ts +0 -28
@@ -12,7 +12,7 @@ import { argv, cwd, stderr, stdout } from 'node:process';
12
12
  import { pathToFileURL } from 'node:url';
13
13
  import { build, initialize, stop } from 'esbuild-wasm/esm/browser.js';
14
14
  import { descend, reached } from '../../core/descend.js';
15
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.js';
15
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.js';
16
16
  import { exit, invoked } from '../../core/invoked.js';
17
17
  import { valuesIn } from '../../core/rows.js';
18
18
  import { findTree } from '../../core/tree.js';
@@ -83,35 +83,110 @@ function exported(held, sub, conditions) {
83
83
  }
84
84
  return undefined;
85
85
  }
86
- function packaged(specifier, from, conditions, top) {
87
- const name = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
- const sub = `.${specifier.slice(name.length)}`;
86
+ function named(specifier) {
87
+ return specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
+ }
89
+ function rooted(name, from, top) {
89
90
  for (let at = from; inside(top, at); at = dirname(at)) {
90
91
  const root = join(at, 'node_modules', name);
91
- const manifest = join(root, 'package.json');
92
- if (filed(manifest)) {
93
- const held = JSON.parse(readFileSync(manifest, 'utf8'));
94
- if (held.exports !== undefined) {
95
- const found = exported(held.exports, sub, conditions);
96
- return found === undefined ? undefined : located(join(root, found));
97
- }
98
- const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js';
99
- return located(join(root, sub === '.' ? main : sub));
100
- }
92
+ if (filed(join(root, 'package.json')))
93
+ return root;
101
94
  if (at === top)
102
95
  return undefined;
103
96
  }
104
97
  return undefined;
105
98
  }
106
- const folder = (outside, top) => ({
99
+ function packaged(specifier, from, conditions, top) {
100
+ const name = named(specifier);
101
+ const sub = `.${specifier.slice(name.length)}`;
102
+ const root = rooted(name, from, top);
103
+ if (root === undefined)
104
+ return undefined;
105
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
106
+ if (held.exports !== undefined) {
107
+ const found = exported(held.exports, sub, conditions);
108
+ return found === undefined ? undefined : located(join(root, found));
109
+ }
110
+ const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js';
111
+ return located(join(root, sub === '.' ? main : sub));
112
+ }
113
+ function binary(at) {
114
+ const bytes = readFileSync(at, 'latin1');
115
+ if (bytes.length < 4)
116
+ return false;
117
+ const byte = (i) => bytes.charCodeAt(i);
118
+ if (byte(0) === 0x7f && bytes.slice(1, 4) === 'ELF')
119
+ return true;
120
+ if (byte(0) === 0x4d && byte(1) === 0x5a)
121
+ return true;
122
+ if (bytes.slice(0, 4) === '\0asm')
123
+ return true;
124
+ if (byte(1) === 0xfa && byte(2) === 0xed && byte(3) === 0xfe && (byte(0) === 0xce || byte(0) === 0xcf))
125
+ return true;
126
+ if (byte(0) === 0xfe && byte(1) === 0xed && byte(2) === 0xfa && (byte(3) === 0xce || byte(3) === 0xcf))
127
+ return true;
128
+ if (byte(0) === 0xca && byte(1) === 0xfe && byte(2) === 0xba && (byte(3) === 0xbe || byte(3) === 0xbf))
129
+ return true;
130
+ return false;
131
+ }
132
+ function owned(root) {
133
+ const found = [];
134
+ const seen = new Set();
135
+ const walk = (at) => {
136
+ const real = realpathSync(at);
137
+ if (seen.has(real))
138
+ return;
139
+ seen.add(real);
140
+ for (const one of readdirSync(at)) {
141
+ if (one === 'node_modules')
142
+ continue;
143
+ const path = join(at, one);
144
+ const held = statSync(path, { throwIfNoEntry: false });
145
+ if (held?.isDirectory() === true)
146
+ walk(path);
147
+ else if (held?.isFile() === true)
148
+ found.push(path);
149
+ }
150
+ };
151
+ walk(root);
152
+ return found;
153
+ }
154
+ const staying = new Map();
155
+ function installed(root, top, visiting = new Set()) {
156
+ const cached = staying.get(root);
157
+ if (cached !== undefined)
158
+ return cached;
159
+ if (visiting.has(root))
160
+ return false;
161
+ visiting.add(root);
162
+ let decided = owned(root).some(binary);
163
+ if (!decided) {
164
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
165
+ const names = Object.keys(held.optionalDependencies ?? {});
166
+ decided = names.some((one) => {
167
+ const found = rooted(one, root, top);
168
+ return found !== undefined && installed(found, top, visiting);
169
+ });
170
+ }
171
+ visiting.delete(root);
172
+ staying.set(root, decided);
173
+ return decided;
174
+ }
175
+ const folder = (outside, top, keeping) => ({
107
176
  name: 'thread-folder',
108
177
  setup(held) {
109
178
  held.onResolve({ filter: /.*/ }, (args) => {
110
179
  const bare = !args.path.startsWith('.') && !isAbsolute(args.path);
111
180
  if (args.path.startsWith('node:') || RUNTIME.has(args.path))
112
181
  return { path: args.path, external: true, sideEffects: false };
113
- if (bare && outside)
114
- return { path: args.path, external: true };
182
+ if (bare && outside) {
183
+ const name = named(args.path);
184
+ const root = rooted(name, args.resolveDir, top);
185
+ if (root !== undefined && installed(root, top)) {
186
+ keeping?.add(name);
187
+ return { path: args.path, external: true };
188
+ }
189
+ }
115
190
  const conditions = args.kind === 'require-call' || args.kind === 'require-resolve' ? REQUIRED : IMPORTED;
116
191
  const path = resolve(args.resolveDir, args.path);
117
192
  const found = bare ? packaged(args.path, args.resolveDir, conditions, top) : inside(top, path) ? located(path) : undefined;
@@ -141,13 +216,13 @@ export async function bundle(options) {
141
216
  hosted.self ??= globalThis;
142
217
  await initialize({ wasmModule: compiled, worker: false });
143
218
  try {
144
- const { packages, plugins, absWorkingDir = cwd(), ...rest } = options;
219
+ const { packages, plugins, keeping, absWorkingDir = cwd(), ...rest } = options;
145
220
  const made = await build({
146
221
  absWorkingDir,
147
222
  ...rest,
148
223
  bundle: true,
149
224
  write: false,
150
- plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir)],
225
+ plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir, keeping)],
151
226
  });
152
227
  const text = made.outputFiles?.[0]?.text;
153
228
  if (text === undefined)
@@ -323,13 +398,14 @@ async function main() {
323
398
  }
324
399
  const forBrowser = browsing(entry);
325
400
  const refusals = forBrowser ? withheld(pass) : new Map();
401
+ const keeping = forBrowser ? undefined : new Set();
326
402
  let held;
327
403
  try {
328
404
  held = await bundle({
329
405
  entryPoints: [resolve(ROOT, entry)],
330
406
  format: 'esm',
331
407
  platform: forBrowser ? 'browser' : 'node',
332
- ...(forBrowser ? {} : { packages: 'external' }),
408
+ ...(keeping === undefined ? {} : { packages: 'external', keeping }),
333
409
  outfile: at,
334
410
  plugins: [declared(), ...(refusals.size === 0 ? [] : [withholding(refusals)]), ...compiling],
335
411
  logLevel: 'silent',
@@ -339,16 +415,21 @@ async function main() {
339
415
  stderr.write(`build: ${out} was not written — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
340
416
  exit(1);
341
417
  }
418
+ const installs = keeping === undefined
419
+ ? ''
420
+ : keeping.size === 0
421
+ ? ' Every package it imports is compiled in, so npm installs nothing to run it.'
422
+ : ` It still needs npm to install ${String(keeping.size)} package${keeping.size === 1 ? '' : 's'}: ${[...keeping].sort().join(', ')}. Each one loads a native file or starts a program that only exists once npm installs it.`;
342
423
  if (checking) {
343
424
  if (held !== before) {
344
425
  stderr.write(`build: ${out} is not what ${BUILDS} writes. A built artifact has no author — run \`${BUILDS}\` and commit the result.\n`);
345
426
  exit(1);
346
427
  }
347
- stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.\n`);
428
+ stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.${installs}\n`);
348
429
  continue;
349
430
  }
350
431
  writeFileSync(at, held, 'utf8');
351
- stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.\n`);
432
+ stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.${installs}\n`);
352
433
  }
353
434
  }
354
435
  if (invoked(import.meta.url))
@@ -1,15 +1,20 @@
1
1
  import { join } from 'node:path';
2
2
  import { argv, stderr, stdout } from 'node:process';
3
- import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.js';
3
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from '../../integrations/disk.js';
4
+ import { COPY_AT, PROGRAM } from '../../integrations/cli.js';
5
+ import { VAULT_DIR } from '../../integrations/vault.js';
4
6
  import { exit, invoked } from '../../core/invoked.js';
5
- import { marked, valuesIn } from '../../core/rows.js';
7
+ import { markOf, valuesIn } from '../../core/rows.js';
6
8
  import { ours } from '../../core/scope.js';
7
- import { findTree, inTree } from '../../core/tree.js';
9
+ import { declaresUi, findTree, inTree } from '../../core/tree.js';
10
+ import { SERVER_OUT } from '../../operations/capabilities.js';
8
11
  import { CHECKED_BY, KINDS, LISTENS, SAID, WRITES, WRITTEN_BY, kindOf } from '../release.js';
9
12
  const TREE = findTree();
10
13
  const MANIFEST = 'package.json';
14
+ const RECEIPT = join(VAULT_DIR, 'gate.json');
15
+ const ROUTE = /^app\/(?:.+\/)?route\.ts$/;
11
16
  const addressed = (rows) => rows.flatMap((one) => [...(one.at === undefined ? [] : [one.at]), ...addressed(one.derives ?? [])]);
12
- const mark = (one) => marked([one.id ?? '', one.at ?? '']);
17
+ const mark = (one) => markOf(one);
13
18
  function releases() {
14
19
  const at = inTree(TREE, 'derivation.ts');
15
20
  if (!existsSync(at))
@@ -32,35 +37,73 @@ function manager() {
32
37
  return 'npm';
33
38
  }
34
39
  }
35
- function scripted() {
36
- const tool = manager();
37
- return { build: `${tool} run build`, start: tool === 'npm' ? 'npm start' : `${tool} start` };
40
+ function served() {
41
+ const found = new Set();
42
+ const walk = (held) => {
43
+ if (held === null || typeof held !== 'object')
44
+ return;
45
+ const row = held;
46
+ const routes = Array.isArray(row.derives) ? row.derives.filter((one) => ROUTE.test(one.at ?? '')) : [];
47
+ if (typeof row.at === 'string' && !row.at.startsWith(`${TREE.name}/`) && routes.length > 0)
48
+ found.add(row.at);
49
+ for (const one of Object.values(held))
50
+ walk(one);
51
+ };
52
+ for (const name of readdirSync(TREE.dir).filter((one) => one.endsWith('.ts')).sort()) {
53
+ walk(valuesIn(readFileSync(join(TREE.dir, name), 'utf8')));
54
+ }
55
+ return [...found].sort();
56
+ }
57
+ function runsThread() {
58
+ const at = inTree(TREE, 'targets.ts');
59
+ if (!existsSync(at))
60
+ return false;
61
+ const held = valuesIn(readFileSync(at, 'utf8'));
62
+ return (held.TARGETS ?? []).some((one) => one.id === PROGRAM);
38
63
  }
39
- function dockerfile(build, start) {
64
+ function dockerfile() {
40
65
  const tool = manager();
41
66
  const corepack = tool === 'npm' ? [] : ['RUN corepack enable'];
42
67
  const lock = tool === 'npm' ? 'package-lock.json*' : `${tool}-lock.yaml*`;
43
- const first = tool === 'npm'
44
- ? 'RUN npm ci --omit=dev --ignore-scripts || npm install --omit=dev --ignore-scripts'
45
- : `RUN ${tool} install --prod --frozen-lockfile --ignore-scripts || ${tool} install --prod --ignore-scripts`;
46
- const all = tool === 'npm' ? 'RUN npm install --ignore-scripts' : `RUN ${tool} install --ignore-scripts`;
68
+ const install = tool === 'npm'
69
+ ? 'RUN npm ci --ignore-scripts || npm install --ignore-scripts'
70
+ : `RUN ${tool} install --frozen-lockfile --ignore-scripts || ${tool} install --ignore-scripts`;
71
+ const thread = runsThread();
72
+ const carried = [RECEIPT, ...served(), ...(thread ? [COPY_AT] : [])];
73
+ const running = ['WORKDIR /app', 'ENV NODE_ENV=production', 'ENV HOSTNAME=0.0.0.0', `ENV PORT=${String(LISTENS)}`];
74
+ const ending = [...(thread ? [`RUN mkdir -p ${TREE.name}`] : []), `EXPOSE ${String(LISTENS)}`];
75
+ if (!declaresUi(TREE)) {
76
+ const production = tool === 'npm'
77
+ ? 'RUN npm ci --omit=dev --ignore-scripts || npm install --omit=dev --ignore-scripts'
78
+ : `RUN ${tool} install --prod --frozen-lockfile --ignore-scripts || ${tool} install --prod --ignore-scripts`;
79
+ return [
80
+ 'FROM node:22-slim',
81
+ ...running,
82
+ ...corepack,
83
+ `COPY package.json ${lock} ./`,
84
+ production,
85
+ ...[SERVER_OUT, ...carried].map((one) => `COPY ${one} ./${one}`),
86
+ ...ending,
87
+ `CMD ${JSON.stringify(['node', SERVER_OUT])}`,
88
+ '',
89
+ ].join('\n');
90
+ }
47
91
  return [
48
92
  'FROM node:22-slim AS build',
49
93
  'WORKDIR /app',
50
94
  ...corepack,
51
95
  `COPY package.json ${lock} ./`,
52
- first,
96
+ install,
53
97
  'COPY . .',
54
- all,
55
- `RUN ${build}`,
98
+ `RUN ${tool} run build`,
56
99
  '',
57
100
  'FROM node:22-slim',
58
- 'WORKDIR /app',
59
- ...corepack,
60
- 'ENV NODE_ENV=production',
61
- 'COPY --from=build /app ./',
62
- `EXPOSE ${String(LISTENS)}`,
63
- `CMD ${JSON.stringify(start.split(' '))}`,
101
+ ...running,
102
+ 'COPY --from=build /app/.next/standalone ./',
103
+ 'COPY --from=build /app/.next/static ./.next/static',
104
+ ...carried.map((one) => `COPY --from=build /app/${one} ./${one}`),
105
+ ...ending,
106
+ `CMD ${JSON.stringify(['node', 'server.js'])}`,
64
107
  '',
65
108
  ].join('\n');
66
109
  }
@@ -76,7 +119,10 @@ function compose(id) {
76
119
  }
77
120
  function withheld() {
78
121
  return [
79
- '.thread',
122
+ VAULT_DIR,
123
+ `!${RECEIPT}`,
124
+ ...(runsThread() ? [`!${COPY_AT}`] : []),
125
+ '.next',
80
126
  'node_modules',
81
127
  '.git',
82
128
  '',
@@ -85,7 +131,6 @@ function withheld() {
85
131
  function made() {
86
132
  const found = new Map();
87
133
  const rows = releases();
88
- const { build, start } = scripted();
89
134
  for (const one of rows) {
90
135
  const id = one.id;
91
136
  if (id === undefined)
@@ -94,7 +139,7 @@ function made() {
94
139
  if (kind === undefined)
95
140
  continue;
96
141
  if (kind === 'docker') {
97
- found.set('Dockerfile', dockerfile(build, start));
142
+ found.set('Dockerfile', dockerfile());
98
143
  found.set('compose.yaml', compose(id));
99
144
  found.set('.dockerignore', withheld());
100
145
  }
@@ -187,8 +232,11 @@ function run() {
187
232
  }
188
233
  for (const [at, text] of files)
189
234
  writeFileSync(join(TREE.at, at), text, 'utf8');
190
- stdout.write('release: wrote ' + [...files.keys()].join(', ') + ' — built and started by what this app\'s own manifest ' +
191
- 'declares, listening on ' + String(LISTENS) + '.\n' +
235
+ stdout.write('release: wrote ' + [...files.keys()].join(', ') + ' — ' +
236
+ (declaresUi(TREE)
237
+ ? 'built by the build this app\'s own manifest declares, and started by `node server.js` from what that build compiled'
238
+ : 'started by `node ' + SERVER_OUT + '`, the server `thread build` compiled into this tree, beside only the packages installed for production') +
239
+ ', listening on ' + String(LISTENS) + '.\n' +
192
240
  'release: no secret is named there. A secret is read from the sealed store at the moment it is used, and ' +
193
241
  'naming it here would be telling whoever deploys this to put a credential in the environment instead.\n');
194
242
  return 0;
@@ -4,7 +4,7 @@ export const WRITES = {
4
4
  docker: [
5
5
  {
6
6
  at: 'Dockerfile',
7
- describes: 'How this app becomes an image and what starts it inside one — the build and start commands its own manifest declares, run with the package manager that manifest names, and the port its server listens on.',
7
+ describes: 'How this app becomes an image, and what starts it inside one. An app that declares `ui` is built inside the image by the build command its own manifest declares, with the package manager that manifest names, and started with `node server.js` from what Next compiled. One that does not builds nothing there: it is started with Node on the server `thread build` compiled into this tree, beside only the packages its manifest installs for production. Either way the image keeps the files its routes read from disk, the receipt of the gate that passed, and the compiled thread command when this app runs one. Nothing of the framework\'s source goes in either way.',
8
8
  },
9
9
  {
10
10
  at: 'compose.yaml',
@@ -12,13 +12,13 @@ export const WRITES = {
12
12
  },
13
13
  {
14
14
  at: '.dockerignore',
15
- describes: 'What a build of this image must never carry out of this directory: the sealed store and the key that opens it, the installed packages, and the history. Without it the build copies everything beside it, and every sealed value goes into the image and into whoever pulls it.',
15
+ describes: 'What a build of this image must never carry out of this directory: the sealed store and the key that opens it, an earlier build, the installed packages, and the history. From the sealed store\'s folder it lets through only the receipt of the gate that passed and, when this app runs the thread command, the compiled copy of that command. Neither holds a secret. Without it the build copies everything beside it, and every sealed value goes into the image and into whoever pulls it.',
16
16
  },
17
17
  ],
18
18
  };
19
19
  export const LISTENS = 3000;
20
20
  export const SAID = {
21
- docker: 'A container image built from this app and the compose file that runs it. It is the one that needs no account and no network to be proven, so it is what an app can be taken end to end with before anybody has chosen where it really lives. What it holds is derived: the build and start commands this app already declares, the port its own server listens on, and the name of every secret it says it must be given — the names only, because a value is typed by a person and a row has never held one.',
21
+ docker: 'A container image built from this app and the compose file that runs it. It needs no account and no network to be proven, so an app can be taken end to end with it before anybody has chosen where it really lives. What it holds is derived: the server, on the port it listens on, with the files its routes read from disk, the receipt of the gate that passed, and the compiled thread command when this app runs one. An app that declares `ui` is served by what Next compiles, started with `node server.js`; one that does not is served by the server `thread build` compiled into this tree, started with Node beside only the packages its manifest installs for production. It names no secret, because a value is typed by a person and read from the sealed store where it is used.',
22
22
  };
23
23
  export const kindOf = (id) => KINDS.find((one) => id.startsWith(PREFIXES[one]));
24
24
  export const WRITTEN_BY = 'thread release';
@@ -263,6 +263,46 @@ function forgets(one, asked) {
263
263
  one.known.delete(key);
264
264
  }
265
265
  }
266
+ const AHEAD = 32;
267
+ const upOf = (at) => (at.includes('/') ? at.slice(0, at.lastIndexOf('/')) : '');
268
+ const nameOf = (at) => at.slice(at.lastIndexOf('/') + 1);
269
+ const inside = (at, name) => (at === '' ? name : at + '/' + name);
270
+ function listedAt(one, at) {
271
+ const held = one.known.get(told({ order: 'list', at }))?.found;
272
+ return Array.isArray(held) ? held : [];
273
+ }
274
+ function realOf(one, at) {
275
+ const held = one.known.get(told({ order: 'real', at }));
276
+ if (held !== undefined || at === '')
277
+ return held;
278
+ const entry = listedAt(one, upOf(at)).find((each) => each.name === nameOf(at));
279
+ if (entry === undefined || entry.kind === 'other')
280
+ return undefined;
281
+ const above = realOf(one, upOf(at));
282
+ return typeof above?.at === 'string' ? { answered: 'real', at: above.at + '/' + nameOf(at) } : undefined;
283
+ }
284
+ function aheadOf(one, asked) {
285
+ if (asked.order === 'real')
286
+ return asked.at === '' || one.known.has(told({ order: 'real', at: '' })) ? [] : [{ order: 'real', at: '', ahead: true }];
287
+ const name = nameOf(asked.at);
288
+ const dot = name.indexOf('.');
289
+ const reading = asked.order === 'read' && dot > 0 && asked.at.split('/').includes(INSTALLED);
290
+ if (!reading && asked.order !== 'stat')
291
+ return [];
292
+ const suffix = reading ? name.slice(dot) : '';
293
+ const up = upOf(asked.at);
294
+ const found = [];
295
+ for (const each of listedAt(one, up)) {
296
+ if (found.length === AHEAD)
297
+ break;
298
+ if (each.kind !== 'file' || each.name === name || !each.name.endsWith(suffix))
299
+ continue;
300
+ const order = { order: asked.order, at: inside(up, each.name), ahead: true };
301
+ if (!one.known.has(told(order)))
302
+ found.push(order);
303
+ }
304
+ return found;
305
+ }
266
306
  const plainly = (one) => one.order + ' ' + (one.at === '' ? '.' : one.at);
267
307
  const outside = (root, at) => {
268
308
  const step = relative(root, at);
@@ -315,11 +355,22 @@ function watching(one) {
315
355
  reply(one.dir, short, known);
316
356
  continue;
317
357
  }
358
+ const derived = asked.order === 'real' ? realOf(one, asked.at) : undefined;
359
+ if (derived !== undefined) {
360
+ reply(one.dir, short, derived);
361
+ kept(one, asked, derived);
362
+ continue;
363
+ }
318
364
  if (!READS.has(asked.order))
319
365
  forgets(one, asked);
320
366
  const due = one.replaying.shift();
321
367
  if (due !== undefined) {
322
368
  if (told(due.order) === told(asked)) {
369
+ while (one.replaying[0]?.order.ahead === true) {
370
+ const extra = one.replaying.shift();
371
+ if (extra !== undefined)
372
+ kept(one, extra.order, extra.answered);
373
+ }
323
374
  reply(one.dir, short, due.answered);
324
375
  kept(one, asked, due.answered);
325
376
  continue;
@@ -334,7 +385,8 @@ function watching(one) {
334
385
  }
335
386
  one.pending = short;
336
387
  one.asking = asked;
337
- hand(one, still(one, [asked]));
388
+ one.ahead = aheadOf(one, asked);
389
+ hand(one, still(one, [asked, ...one.ahead]));
338
390
  }
339
391
  }, TICK);
340
392
  }
@@ -362,11 +414,11 @@ function next(one, within) {
362
414
  }, Math.max(1_000, within - MARGIN));
363
415
  });
364
416
  }
365
- function begun(from, word, args, root, hears, called, recording = []) {
417
+ function begun(from, word, args, root, hears, called, recording = [], stash) {
366
418
  const of = called ?? randomBytes(6).toString('hex');
367
419
  const dir = join(tmpdir(), SERVED, randomBytes(6).toString('hex'));
368
420
  writeFileSync(join(dir, 'channel'), '', 'utf8');
369
- const one = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map() };
421
+ const one = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map(), ahead: [], ...(stash === undefined ? {} : { stash }) };
370
422
  const child = spawn(execPath, [...from.words, word, ...args], {
371
423
  cwd: from.own,
372
424
  shell: false,
@@ -397,6 +449,7 @@ function counted(one, held) {
397
449
  one.last = held;
398
450
  return held;
399
451
  }
452
+ const stashing = (one, held) => (one.stash === undefined ? held : { ...held, stashed: one.stash });
400
453
  async function served(word, args, within, how) {
401
454
  const of = how?.of;
402
455
  if (of !== undefined) {
@@ -411,20 +464,26 @@ async function served(word, args, within, how) {
411
464
  const from = startable();
412
465
  if (typeof from === 'string')
413
466
  return nowhere(from);
414
- const again = begun(from, word, args, root, how?.hears, of, recording);
415
- return counted(again, await next(again, within));
467
+ const again = begun(from, word, args, root, how?.hears, of, recording, how?.stash);
468
+ return stashing(again, counted(again, await next(again, within)));
416
469
  }
417
470
  const answering = how?.answering;
418
471
  if (answering !== undefined && answering < one.handed && one.last !== undefined)
419
- return one.last;
420
- const given = (how?.answered ?? [])[0];
472
+ return stashing(one, one.last);
473
+ const [given, ...rest] = how?.answered ?? [];
421
474
  if (one.pending !== '' && given !== undefined) {
475
+ one.ahead.forEach((order, index) => {
476
+ const held = rest[index];
477
+ if (held !== undefined)
478
+ kept(one, order, held);
479
+ });
480
+ one.ahead = [];
422
481
  reply(one.dir, one.pending, given);
423
482
  if (one.asking !== undefined)
424
483
  kept(one, one.asking, given);
425
484
  one.pending = '';
426
485
  }
427
- return counted(one, await next(one, within));
486
+ return stashing(one, counted(one, await next(one, within)));
428
487
  }
429
488
  const root = how?.at ?? '';
430
489
  if (root === '')
@@ -432,8 +491,8 @@ async function served(word, args, within, how) {
432
491
  const from = startable();
433
492
  if (typeof from === 'string')
434
493
  return nowhere(from);
435
- const fresh = begun(from, word, args, root, how?.hears);
436
- return counted(fresh, await next(fresh, within));
494
+ const fresh = begun(from, word, args, root, how?.hears, undefined, [], how?.stash);
495
+ return stashing(fresh, counted(fresh, await next(fresh, within)));
437
496
  }
438
497
  export function running(name, subcommands, at) {
439
498
  const out = {};
@@ -52,14 +52,24 @@ export async function shapeOf(sql) {
52
52
  }
53
53
  }
54
54
  export function standing(held) {
55
- const store = storing({ ...held, at: 'memory://', migrations: [] });
55
+ const store = storing({ ...held, at: 'memory://' });
56
56
  const keeps = Object.keys(held.tables);
57
57
  return {
58
58
  keeps,
59
59
  store,
60
60
  async seed(actor, rows) {
61
- for (const one of rows)
61
+ for (const one of rows) {
62
+ const by = held.address[one.entity];
63
+ const key = by === undefined ? undefined : one.row[by];
64
+ if (by !== undefined && (typeof key === 'string' || typeof key === 'number')) {
65
+ const there = await store.list(actor, one.entity, { where: { [by]: key }, limit: 1 });
66
+ if (there.length > 0) {
67
+ await store.update(actor, one.entity, key, one.row);
68
+ continue;
69
+ }
70
+ }
62
71
  await store.insert(actor, one.entity, one.row);
72
+ }
63
73
  },
64
74
  async taken(actor) {
65
75
  const found = {};
@@ -83,7 +93,7 @@ export function storing(held) {
83
93
  const found = held.address[entity];
84
94
  if (found === undefined) {
85
95
  throw new Error(entity + ' declares nothing that identifies one of its rows, so a row of it cannot be addressed here. ' +
86
- 'Give it a value and say so with `thread create entity edit`.');
96
+ 'Name the value that does: give `thread edit ' + entity + ' --must` its invariants again with one more, unique over that value alone.');
87
97
  }
88
98
  return found;
89
99
  };
@@ -68,6 +68,7 @@ export interface Order {
68
68
  readonly said?: string;
69
69
  readonly mode?: number;
70
70
  readonly words?: readonly string[];
71
+ readonly ahead?: boolean;
71
72
  }
72
73
  export interface Answer {
73
74
  readonly answered: string;
@@ -87,6 +88,7 @@ export interface How {
87
88
  readonly answered?: readonly Answer[];
88
89
  readonly recording?: readonly Recorded[];
89
90
  readonly answering?: number;
91
+ readonly stash?: unknown;
90
92
  }
91
93
  export interface Ran {
92
94
  readonly there: boolean;
@@ -99,6 +101,7 @@ export interface Ran {
99
101
  readonly of?: string;
100
102
  readonly asks?: readonly Order[];
101
103
  readonly lost?: string;
104
+ readonly stashed?: unknown;
102
105
  }
103
106
  export type Running<C extends string = string> = Readonly<Record<C, (args: readonly string[], shows?: boolean | 'errors' | 'tee', within?: number, how?: How) => Promise<Ran>>>;
104
107
  export type Calling<C extends string = string> = {
@@ -116,11 +116,7 @@ export const ATTACH = {
116
116
  { file: 'rules.ts', source: RULES_SOURCE },
117
117
  { file: 'gate.ts', source: GATE_SOURCE },
118
118
  ],
119
- ignores: ['.thread/', '.next/', 'node_modules/', 'tsconfig.tsbuildinfo'],
120
- scripts: {
121
- build: 'next build',
122
- start: 'next start',
123
- },
119
+ ignores: ['.thread/', 'node_modules/', 'tsconfig.tsbuildinfo'],
124
120
  needs: {
125
121
  '@types/node': '^24.0.0',
126
122
  typescript: '^6.0.0',
@@ -76,6 +76,9 @@ export const slopnator = {
76
76
  return false;
77
77
  if (derived().has(path))
78
78
  return false;
79
+ const folder = path.slice(0, path.lastIndexOf('/'));
80
+ if (statSync(resolve(ROOT, folder), { throwIfNoEntry: false })?.isDirectory() !== true)
81
+ return false;
79
82
  return `\`${value}\` is not on disk`;
80
83
  },
81
84
  proves: {
@@ -152,6 +155,13 @@ export const slopnator = {
152
155
  ],
153
156
  at: 'probe.tsx',
154
157
  },
158
+ {
159
+ files: [
160
+ { at: 'docs/_probe_there.md', source: 'there\n' },
161
+ { at: 'probe.tsx', source: "const at = 'docs/elsewhere/entry.ts'\n" },
162
+ ],
163
+ at: 'probe.tsx',
164
+ },
155
165
  "const at = 'threads/'",
156
166
  "const said = 'a sentence about packages/core/integrity/src/constraint.ts and nothing else'",
157
167
  {
@@ -17,8 +17,9 @@ export function undeclared(answered, offers, which) {
17
17
  return (`this app declares no ${which === 'roles' ? 'role' : 'entitlement'} called ${stray.map((one) => `\`${one}\``).join(', ')} — ` +
18
18
  (offers.length === 0
19
19
  ? `it declares none at all. A ${which === 'roles' ? 'role' : 'entitlement'} is one of the words the value an ` +
20
- 'entity names as saying what a caller may do is held to. Declare them with `thread create entity edit` — ' +
21
- 'name that value, give it the `choice` format, and state its options'
20
+ 'entity names as saying what a caller may do is held to. Declare them on that entity: give ' +
21
+ '`thread edit <entity> --holds` its values again with that one in the `choice` format and its options stated, ' +
22
+ `and name it as \`${which}\` in \`thread edit <entity> --actor\``
22
23
  : `it declares ${offers.map((one) => `\`${one}\``).join(', ')}`));
23
24
  }
24
25
  export const keyIn = (one) => (one.must ?? []).flatMap((each) => (each.unique?.length === 1 ? [each.unique[0]] : []))[0];
@@ -9,6 +9,8 @@ export interface Defaults extends Threaded {
9
9
  readonly live?: readonly Surface[];
10
10
  }
11
11
  export declare const CAPABILITIES_AT = "operations/_capabilities.json";
12
+ export declare const SERVER_AT = "_generated/server/entry.ts";
13
+ export declare const SERVER_OUT = "_generated/server/server.mjs";
12
14
  export declare const CLIENT_AT = "operations/_client.ts";
13
15
  export declare const DISPATCHING_AT = "app/_generated/dispatching.ts";
14
16
  export declare const COMPILED = "app/_generated/compiled";
@@ -9,6 +9,8 @@ export const MEANS = {
9
9
  export const DEFAULTS_AT = '_threads/defaults.ts';
10
10
  export const UNSTATED = ['cli'];
11
11
  export const CAPABILITIES_AT = 'operations/_capabilities.json';
12
+ export const SERVER_AT = '_generated/server/entry.ts';
13
+ export const SERVER_OUT = '_generated/server/server.mjs';
12
14
  export const CLIENT_AT = 'operations/_client.ts';
13
15
  export const DISPATCHING_AT = 'app/_generated/dispatching.ts';
14
16
  export const COMPILED = 'app/_generated/compiled';