@prestack/thread 0.1.21 → 0.1.23

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 (138) hide show
  1. package/CHANGELOG.md +227 -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 +69 -0
  7. package/dist/derivation/generators/build.d.ts +3 -1
  8. package/dist/derivation/generators/build.js +104 -22
  9. package/dist/derivation/generators/release.js +74 -25
  10. package/dist/derivation/release.js +3 -3
  11. package/dist/integrations/cli.d.ts +0 -1
  12. package/dist/integrations/cli.js +69 -11
  13. package/dist/integrations/declared.d.ts +2 -0
  14. package/dist/integrations/declared.js +6 -1
  15. package/dist/integrations/pglite.js +13 -3
  16. package/dist/integrations/target.d.ts +3 -0
  17. package/dist/integrity/attach.js +1 -5
  18. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  19. package/dist/operations/access.js +3 -2
  20. package/dist/operations/capabilities.d.ts +2 -0
  21. package/dist/operations/capabilities.js +2 -0
  22. package/dist/operations/generators/capability.js +237 -24
  23. package/dist/operations/generators/compile.js +150 -50
  24. package/dist/operations/generators/seed.d.ts +3 -0
  25. package/dist/operations/generators/seed.js +25 -3
  26. package/dist/operations/nodes.d.ts +2 -2
  27. package/package.json +1 -1
  28. package/dist/core/actor.ts +0 -21
  29. package/dist/core/attach.ts +0 -613
  30. package/dist/core/chain.ts +0 -145
  31. package/dist/core/condition.ts +0 -227
  32. package/dist/core/descend.ts +0 -1185
  33. package/dist/core/errors/catalogue.ts +0 -314
  34. package/dist/core/errors/classes.ts +0 -51
  35. package/dist/core/errors/error.ts +0 -152
  36. package/dist/core/errors/namespaces.ts +0 -19
  37. package/dist/core/finding.ts +0 -37
  38. package/dist/core/generators/commit.ts +0 -157
  39. package/dist/core/init.ts +0 -222
  40. package/dist/core/invoked.ts +0 -52
  41. package/dist/core/round.ts +0 -74
  42. package/dist/core/rows.ts +0 -506
  43. package/dist/core/scope.ts +0 -20
  44. package/dist/core/thread.ts +0 -29
  45. package/dist/core/tree.ts +0 -90
  46. package/dist/core/types.ts +0 -351
  47. package/dist/derivation/attach.ts +0 -57
  48. package/dist/derivation/generators/build.ts +0 -371
  49. package/dist/derivation/generators/check.ts +0 -118
  50. package/dist/derivation/generators/determinism.ts +0 -145
  51. package/dist/derivation/generators/release.ts +0 -236
  52. package/dist/derivation/release.ts +0 -43
  53. package/dist/integrations/attach.ts +0 -187
  54. package/dist/integrations/cli.ts +0 -606
  55. package/dist/integrations/declared.ts +0 -34
  56. package/dist/integrations/disk.ts +0 -466
  57. package/dist/integrations/generators/read.ts +0 -127
  58. package/dist/integrations/generators/vault.ts +0 -210
  59. package/dist/integrations/https.ts +0 -216
  60. package/dist/integrations/pglite.ts +0 -206
  61. package/dist/integrations/secret.ts +0 -54
  62. package/dist/integrations/target.ts +0 -672
  63. package/dist/integrations/vault.ts +0 -148
  64. package/dist/integrity/attach.ts +0 -150
  65. package/dist/integrity/check-threads.ts +0 -166
  66. package/dist/integrity/constraint.ts +0 -1246
  67. package/dist/integrity/generators/compiler.ts +0 -138
  68. package/dist/integrity/generators/gate.ts +0 -227
  69. package/dist/integrity/generators/issues.ts +0 -160
  70. package/dist/integrity/generators/publish.ts +0 -233
  71. package/dist/integrity/generators/trial.ts +0 -359
  72. package/dist/integrity/lint.ts +0 -214
  73. package/dist/integrity/lsp/bin.ts +0 -44
  74. package/dist/integrity/lsp/diagnostics.ts +0 -92
  75. package/dist/integrity/lsp/findings.ts +0 -63
  76. package/dist/integrity/lsp/lint.ts +0 -124
  77. package/dist/integrity/lsp/protocol.ts +0 -78
  78. package/dist/integrity/lsp/server.ts +0 -224
  79. package/dist/integrity/slopnator.ts +0 -324
  80. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  81. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  82. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  83. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  84. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  85. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  86. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  87. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  88. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  89. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  90. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  91. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  92. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  93. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  94. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  95. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  96. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  97. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  98. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  99. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  100. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  101. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  102. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  103. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  104. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  105. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  106. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  107. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  108. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  109. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  110. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  111. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  112. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  113. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  114. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  115. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  116. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  117. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  118. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  119. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  120. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  121. package/dist/integrity/sweep.ts +0 -395
  122. package/dist/knowledge/attach.ts +0 -327
  123. package/dist/knowledge/generators/docsgen.ts +0 -186
  124. package/dist/knowledge/generators/glossary.ts +0 -73
  125. package/dist/knowledge/glossary.ts +0 -6
  126. package/dist/operations/access.ts +0 -131
  127. package/dist/operations/attach.ts +0 -408
  128. package/dist/operations/capabilities.ts +0 -88
  129. package/dist/operations/dispatch.ts +0 -294
  130. package/dist/operations/error-results.ts +0 -31
  131. package/dist/operations/generators/capability.ts +0 -1054
  132. package/dist/operations/generators/compile.ts +0 -701
  133. package/dist/operations/generators/permits.ts +0 -229
  134. package/dist/operations/generators/seed.ts +0 -359
  135. package/dist/operations/generators/trial.ts +0 -666
  136. package/dist/operations/nodes.ts +0 -53
  137. package/dist/operations/operations.ts +0 -91
  138. package/dist/operations/result.ts +0 -28
@@ -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';
@@ -12,16 +12,18 @@ import { dirname, resolve } from 'node:path';
12
12
  import { argv, stdout } from 'node:process';
13
13
  import { pathToFileURL } from 'node:url';
14
14
  import { ROOT, TREE, descend, forget } from '../../core/descend.js';
15
+ import { findTree } from '../../core/tree.js';
15
16
  import { exit, invoked } from '../../core/invoked.js';
16
17
  import { named as packageOf } from '../../core/scope.js';
17
18
  import { PREFIXES } from '../../core/thread.js';
19
+ import { declaresUi } from '../../integrations/declared.js';
18
20
  import { secrecyOf } from '../../integrations/secret.js';
19
21
  import { reachOf, servesBrowser, targetOf } from '../../integrations/target.js';
20
- import { CAPABILITIES_AT, CLIENT_AT, MCP_AT, OPENAPI_AT, REACHING_AT, ACTOR_AT, DEFAULTS_AT, DISPATCHING_AT, CHECKED_AT, inboundAt, isSurface, ROUTE, routeAt, SURFACES, switched, UNSTATED, } from '../capabilities.js';
22
+ import { CAPABILITIES_AT, SERVER_AT, SERVER_OUT, CLIENT_AT, MCP_AT, OPENAPI_AT, REACHING_AT, ACTOR_AT, DEFAULTS_AT, DISPATCHING_AT, CHECKED_AT, inboundAt, isSurface, ROUTE, routeAt, SURFACES, switched, UNSTATED, } from '../capabilities.js';
21
23
  import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.js';
22
24
  import { DISPATCH_AT } from '../dispatch.js';
23
25
  import { STREAMED, TOLD } from '../../integrations/https.js';
24
- import { holdsRow, listBounds, marked, nestedBounds, rowsIn, unread, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.js';
26
+ import { holdsRow, listBounds, nestedBounds, rowsIn, unread, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.js';
25
27
  import { said } from '../attach.js';
26
28
  import { MEMBERS_LIST } from '../operations.js';
27
29
  import { beside, operations, quoted, runtime, types } from './seed.js';
@@ -322,7 +324,7 @@ function client(found) {
322
324
  function route(one) {
323
325
  const at = routeAt(one.operation);
324
326
  return [
325
- `import { existsSync } from ${quoted(runtime(at, '../../integrations/disk.ts'))}`,
327
+ "import { existsSync } from 'node:fs'",
326
328
  "import { join } from 'node:path'",
327
329
  '',
328
330
  'import { counting, DECLARED, opening } from ' + quoted(beside(at, REACHING_AT)),
@@ -378,7 +380,7 @@ function inbound(one) {
378
380
  ", because: " + quoted(why) + " }, { status: 403 })";
379
381
  return [
380
382
  ...(signed ? ["import { createHmac, timingSafeEqual } from " + quoted("node:crypto")] : []),
381
- "import { existsSync } from " + quoted(runtime(at, "../../integrations/disk.ts")),
383
+ "import { existsSync } from " + quoted("node:fs"),
382
384
  "import { join } from " + quoted("node:path"),
383
385
  "",
384
386
  "import { counting, DECLARED, opening } from " + quoted(beside(at, REACHING_AT)),
@@ -692,22 +694,222 @@ function caller(keeping, kept) {
692
694
  }
693
695
  const carries = () => existsSync(resolve(ROOT, 'node_modules', packageOf('operations')));
694
696
  const emitted = () => existsSync(resolve(ROOT, CHECKED_AT));
695
- const files = (found, targets, calls, keeps, webhooks = []) => [
696
- { at: CAPABILITIES_AT, text: printed(found) },
697
- { at: OPENAPI_AT, text: printed(openapi(found)) },
698
- { at: MCP_AT, text: printed(mcp(found)) },
699
- { at: CLIENT_AT, text: client(found) },
700
- { at: REACHING_AT, text: reaching(targets, calls, kept(), existsSync(resolve(ROOT, VAULT_AT)), carries()) },
701
- { at: ACTOR_AT, text: caller(existsSync(resolve(ROOT, ENTITIES_AT)), keeps) },
702
- ...(emitted() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
703
- ...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
704
- ...webhooks
705
- .filter((one) => found.some((each) => each.operation === one.invokes && each.live.includes('webhook') && each.webhooks.includes(one.id) && compiled(one.invokes)))
706
- .flatMap((one) => {
707
- const at = (one.derives ?? []).map((each) => each.at).find((each) => each === inboundAt(one.id));
708
- return at === undefined ? [] : [{ at, text: inbound(one) }];
709
- }),
710
- ];
697
+ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
698
+ function routeSegments(at) {
699
+ const parts = at.split('/');
700
+ if (parts.length < 2 || parts[0] !== 'app' || parts[parts.length - 1] !== 'route.ts')
701
+ return undefined;
702
+ return parts.slice(1, -1);
703
+ }
704
+ function methodsIn(source) {
705
+ const file = ts.createSourceFile(SERVER_AT, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
706
+ const found = [];
707
+ for (const statement of file.statements) {
708
+ const shown = ((ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined) ?? []).some((one) => one.kind === ts.SyntaxKind.ExportKeyword);
709
+ if (!shown)
710
+ continue;
711
+ if (ts.isFunctionDeclaration(statement) && statement.name !== undefined && METHODS.includes(statement.name.text)) {
712
+ found.push(statement.name.text);
713
+ }
714
+ else if (ts.isVariableStatement(statement)) {
715
+ for (const one of statement.declarationList.declarations) {
716
+ if (ts.isIdentifier(one.name) && METHODS.includes(one.name.text))
717
+ found.push(one.name.text);
718
+ }
719
+ }
720
+ }
721
+ return found;
722
+ }
723
+ function servedByNode() {
724
+ const pass = descend();
725
+ const routes = [];
726
+ for (const at of pass.threaded.keys()) {
727
+ const segments = routeSegments(at);
728
+ if (segments === undefined)
729
+ continue;
730
+ const path = resolve(ROOT, at);
731
+ if (!existsSync(path))
732
+ continue;
733
+ const methods = methodsIn(readFileSync(path, 'utf8'));
734
+ if (methods.length === 0)
735
+ continue;
736
+ routes.push({ at, segments, methods });
737
+ }
738
+ const dynamism = (one) => one.segments.filter((word) => word.startsWith('[')).length;
739
+ routes.sort((a, b) => dynamism(a) - dynamism(b) || compare(a.at, b.at));
740
+ return { hasUi: declaresUi(findTree()), routes };
741
+ }
742
+ function identifier(segments, method) {
743
+ const plain = segments.map((one) => one.split('[').join('').split(']').join('')).join('_');
744
+ const base = plain.replace(/[^A-Za-z0-9_$]/g, '_') || 'root';
745
+ return `${/^[0-9]/.test(base) ? '_' : ''}${base}_${method}`;
746
+ }
747
+ function entry(routes) {
748
+ const table = routes.map((one) => {
749
+ const segments = one.segments.map((word) => quoted(word)).join(', ');
750
+ const methods = one.methods.map((method) => `${method}: ${identifier(one.segments, method)}`).join(', ');
751
+ return ` { segments: [${segments}], methods: { ${methods} } },`;
752
+ });
753
+ return [
754
+ "import { createServer } from 'node:http'",
755
+ "import { Readable } from 'node:stream'",
756
+ '',
757
+ ...routes.map((one) => `import { ${one.methods.map((method) => `${method} as ${identifier(one.segments, method)}`).join(', ')} } from ${quoted(beside(SERVER_AT, one.at))}`),
758
+ '',
759
+ 'interface Route {',
760
+ ' readonly segments: readonly string[]',
761
+ ' readonly methods: Readonly<Record<string, (request: Request, context: { readonly params: Readonly<Record<string, string>> }) => Response | Promise<Response>>>',
762
+ '}',
763
+ '',
764
+ 'const ROUTES: readonly Route[] = [',
765
+ ...table,
766
+ ']',
767
+ '',
768
+ 'function matched(pathname: string): (Route & { readonly params: Readonly<Record<string, string>> }) | undefined {',
769
+ " const wanted = pathname.split('/').filter((one) => one.length > 0)",
770
+ ' for (const route of ROUTES) {',
771
+ ' if (route.segments.length !== wanted.length) continue',
772
+ ' const params: Record<string, string> = {}',
773
+ ' const ok = route.segments.every((segment, at) => {',
774
+ " if (segment.startsWith('[') && segment.endsWith(']')) {",
775
+ ' params[segment.slice(1, -1)] = decodeURIComponent(wanted[at] as string)',
776
+ ' return true',
777
+ ' }',
778
+ ' return segment === wanted[at]',
779
+ ' })',
780
+ ' if (ok) return { ...route, params }',
781
+ ' }',
782
+ ' return undefined',
783
+ '}',
784
+ '',
785
+ 'const argued = process.argv.slice(2)',
786
+ "const flagged = argued.includes('--port') ? argued[argued.indexOf('--port') + 1] : undefined",
787
+ 'const PORT = Number(flagged ?? process.env.PORT ?? 3000)',
788
+ "const HOSTNAME = process.env.HOSTNAME ?? '127.0.0.1'",
789
+ '',
790
+ 'createServer((req, res) => {',
791
+ ' const host = req.headers.host ?? `${HOSTNAME}:${String(PORT)}`',
792
+ " const url = new URL(req.url ?? '/', `http://${host}`)",
793
+ ' const route = matched(url.pathname)',
794
+ ' if (route === undefined) {',
795
+ ' res.writeHead(404)',
796
+ ' res.end()',
797
+ ' return',
798
+ ' }',
799
+ " const method = (req.method ?? 'GET').toUpperCase()",
800
+ ' const handler = route.methods[method]',
801
+ ' if (handler === undefined) {',
802
+ " res.writeHead(405, { allow: Object.keys(route.methods).join(', ') })",
803
+ ' res.end()',
804
+ ' return',
805
+ ' }',
806
+ ' const controller = new AbortController()',
807
+ " res.on('close', () => {",
808
+ ' if (!res.writableEnded) controller.abort()',
809
+ ' })',
810
+ ' const headers = new Headers()',
811
+ ' for (const [key, value] of Object.entries(req.headers)) {',
812
+ ' if (value === undefined) continue',
813
+ ' for (const one of Array.isArray(value) ? value : [value]) headers.append(key, one)',
814
+ ' }',
815
+ ' const request = new Request(url.href, {',
816
+ ' method,',
817
+ ' headers,',
818
+ " body: method === 'GET' || method === 'HEAD' ? undefined : Readable.toWeb(req),",
819
+ ' signal: controller.signal,',
820
+ " duplex: 'half',",
821
+ ' } as never)',
822
+ ' Promise.resolve(handler(request, { params: route.params }))',
823
+ ' .then((answered) => {',
824
+ ' const said: Record<string, string> = {}',
825
+ ' answered.headers.forEach((value, key) => {',
826
+ ' said[key] = value',
827
+ ' })',
828
+ ' res.writeHead(answered.status, said)',
829
+ ' if (answered.body === null) {',
830
+ ' res.end()',
831
+ ' return',
832
+ ' }',
833
+ ' Readable.fromWeb(answered.body as never).pipe(res)',
834
+ ' })',
835
+ ' .catch((thrown: unknown) => {',
836
+ ' console.error(thrown instanceof Error ? thrown.stack ?? thrown.message : thrown)',
837
+ ' if (!res.headersSent) res.writeHead(500)',
838
+ ' res.end()',
839
+ ' })',
840
+ '}).listen(PORT, HOSTNAME)',
841
+ '',
842
+ ].join('\n');
843
+ }
844
+ const SERVER_ENTRY_DESCRIBES = 'The server this app runs when it declares no `ui`. It imports every route this app declares, reads the path and method off each request, and calls the one that answers: 404 for a path nothing declares, 405 naming the methods that do answer when the path matches and the method does not, 500 with nothing of the error in the body when the route it calls throws. `thread capabilities` writes it only while this app has no `ui`, because an app that has one is served by Next instead, and takes both it and its row out the moment that changes.';
845
+ const SERVER_DESCRIBES = 'The server entry bundled into the one file this app is actually started from. `thread build` writes it the same way it writes every other declared artifact, so what a released app starts is compiled code rather than the source of this framework.';
846
+ function entered(hasUi, writing) {
847
+ const sheet = resolve(ROOT, OPERATIONS_AT);
848
+ if (!existsSync(sheet))
849
+ return [];
850
+ const text = readFileSync(sheet, 'utf8');
851
+ const where = listBounds(text, 'derives');
852
+ if (where === undefined)
853
+ return [];
854
+ const holds = holdsRow(text, where, 'server-entry');
855
+ const wants = !hasUi;
856
+ if (wants === holds)
857
+ return [];
858
+ const wrong = [
859
+ wants
860
+ ? `this app declares no ui, and its row does not derive ${SERVER_AT}`
861
+ : `this app declares ui, and its row still derives ${SERVER_AT}`,
862
+ ];
863
+ if (!writing)
864
+ return wrong;
865
+ const next = wants
866
+ ? withRow(text, where, said({
867
+ id: 'server-entry',
868
+ at: SERVER_AT,
869
+ describes: SERVER_ENTRY_DESCRIBES,
870
+ version: 1,
871
+ by: 'thread capabilities',
872
+ runs: 'thread capabilities --check',
873
+ derives: [
874
+ {
875
+ id: 'server',
876
+ at: SERVER_OUT,
877
+ describes: SERVER_DESCRIBES,
878
+ version: 1,
879
+ by: 'thread build',
880
+ runs: 'thread build --check',
881
+ },
882
+ ],
883
+ }, ' '))
884
+ : withoutRow(text, where, 'server-entry');
885
+ writeFileSync(sheet, next, 'utf8');
886
+ return wrong;
887
+ }
888
+ function unentered(hasUi) {
889
+ if (!hasUi)
890
+ return [];
891
+ return [SERVER_AT, SERVER_OUT].filter((at) => existsSync(resolve(ROOT, at)));
892
+ }
893
+ const files = (found, targets, calls, keeps, webhooks = []) => {
894
+ const { hasUi, routes } = servedByNode();
895
+ return [
896
+ { at: CAPABILITIES_AT, text: printed(found) },
897
+ { at: OPENAPI_AT, text: printed(openapi(found)) },
898
+ { at: MCP_AT, text: printed(mcp(found)) },
899
+ { at: CLIENT_AT, text: client(found) },
900
+ { at: REACHING_AT, text: reaching(targets, calls, kept(), existsSync(resolve(ROOT, VAULT_AT)), carries()) },
901
+ { at: ACTOR_AT, text: caller(existsSync(resolve(ROOT, ENTITIES_AT)), keeps) },
902
+ ...(emitted() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
903
+ ...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
904
+ ...webhooks
905
+ .filter((one) => found.some((each) => each.operation === one.invokes && each.live.includes('webhook') && each.webhooks.includes(one.id) && compiled(one.invokes)))
906
+ .flatMap((one) => {
907
+ const at = (one.derives ?? []).map((each) => each.at).find((each) => each === inboundAt(one.id));
908
+ return at === undefined ? [] : [{ at, text: inbound(one) }];
909
+ }),
910
+ ...(hasUi ? [] : [{ at: SERVER_AT, text: entry(routes) }]),
911
+ ];
912
+ };
711
913
  function unmoved() {
712
914
  const tree = defaulted();
713
915
  if (tree === undefined || tree.stated)
@@ -789,7 +991,7 @@ if (invoked(import.meta.url)) {
789
991
  const words = argv.slice(2).filter((one) => !one.startsWith('-'));
790
992
  if (words[0] !== undefined && words[0] !== 'read') {
791
993
  console.error('capabilities: `' + words[0] + '` is not something this command does. `thread capabilities` writes what can be reached, and ' +
792
- '`thread capabilities read <mark>` prints one. Nothing was written.');
994
+ '`thread capabilities read <operation>` prints one. Nothing was written.');
793
995
  exit(1);
794
996
  }
795
997
  if (!argv.includes('--check')) {
@@ -808,13 +1010,12 @@ if (invoked(import.meta.url)) {
808
1010
  const { found, refusals, live } = await derive();
809
1011
  for (const one of refusals)
810
1012
  console.error(`capabilities: ${one}`);
811
- const mark = (one) => marked([one.operation, one.describes, one.requires.join(','), one.nodes.join(','), one.live.join(',')]);
812
1013
  const verb = words[0];
813
1014
  if (verb === 'read') {
814
1015
  const which = words[1];
815
- const one = found.find((each) => mark(each) === which);
1016
+ const one = found.find((each) => each.operation === which);
816
1017
  if (one === undefined) {
817
- console.error(`capabilities: \`${which ?? '(nothing)'}\` is not a mark this tree issued. Run \`thread capabilities\` to see them — each is printed in brackets beside the capability it belongs to.`);
1018
+ console.error(`capabilities: \`${which ?? '(nothing)'}\` is not an operation this app declares. Name one of ${found.map((each) => each.operation).join(', ') || '(none yet)'}.`);
818
1019
  exit(1);
819
1020
  }
820
1021
  stdout.write(`${one.operation} — ${one.describes}\n`);
@@ -840,6 +1041,7 @@ if (invoked(import.meta.url)) {
840
1041
  const people = sheet('entities.ts', 'ENTITIES');
841
1042
  const one = people.find((each) => each.actor !== undefined);
842
1043
  const where = one === undefined ? undefined : targetOf(one.id, all);
1044
+ const { hasUi } = servedByNode();
843
1045
  const written = files(found, all, sheet('targets.ts', 'CALLS'), where === undefined || typeof where === 'string' ? undefined : where.id, sheet('targets.ts', 'WEBHOOKS'));
844
1046
  if (checking) {
845
1047
  const drifted = written.filter((one) => {
@@ -853,6 +1055,8 @@ if (invoked(import.meta.url)) {
853
1055
  ...(unmoved() === undefined ? [] : [`${CAPABILITIES_AT} switches surfaces that no row states yet`]),
854
1056
  ...routed(found, false),
855
1057
  ...unserved(found).map((at) => `${at} is still on disk to be served, and its operation does not answer on api`),
1058
+ ...entered(hasUi, false),
1059
+ ...unentered(hasUi).map((at) => `${at} is still on disk, and this app declares ui now`),
856
1060
  ];
857
1061
  for (const one of unsettled)
858
1062
  console.error(`capabilities: ${one}. Run \`thread capabilities\` and commit the result.`);
@@ -873,6 +1077,15 @@ if (invoked(import.meta.url)) {
873
1077
  }
874
1078
  for (const one of routed(found, true))
875
1079
  console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`);
1080
+ for (const one of entered(hasUi, true))
1081
+ console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`);
1082
+ for (const at of unentered(hasUi)) {
1083
+ rmSync(resolve(ROOT, at));
1084
+ const folder = dirname(resolve(ROOT, at));
1085
+ if (readdirSync(folder).length === 0)
1086
+ rmSync(folder, { recursive: true, force: true });
1087
+ console.log(`capabilities: deleted ${at}, because this app declares ui now and Next serves it instead`);
1088
+ }
876
1089
  for (const at of unserved(found)) {
877
1090
  rmSync(resolve(ROOT, at));
878
1091
  const folder = dirname(resolve(ROOT, at));
@@ -8,7 +8,7 @@ import { exit, invoked } from '../../core/invoked.js';
8
8
  import { inCode, valuesIn } from '../../core/rows.js';
9
9
  import { credentialsOf, holdsOf, reachOf } from '../../integrations/target.js';
10
10
  import { DISPATCH_AT, dispatched } from '../dispatch.js';
11
- import { beside, blockIn, builtFrom, operations, problem, quoted, runtime, types } from './seed.js';
11
+ import { beside, blockIn, builtFrom, operations, problem, quoted, runtime, stepsOf, types } from './seed.js';
12
12
  async function reaching() {
13
13
  const held = async (file, list) => {
14
14
  const path = resolve(ROOT, `_threads/${file}`);
@@ -181,17 +181,21 @@ function spliced(text, shape, roles) {
181
181
  const up = node.parent;
182
182
  if (up !== undefined && ts.isPropertyAccessExpression(up) && up.expression === node) {
183
183
  const said = up.name.text;
184
- if (said !== 'input' && !names.has(said)) {
184
+ if (said !== 'input' && !names.has(said) && !shape.aliases.has(said)) {
185
185
  because =
186
186
  `reads \`${read.held}.${said}\`, and no node before it answers to that name. ` +
187
187
  `\`${read.held}\` holds \`input\`, which is what this operation was called with, ` +
188
- (names.size === 0
188
+ (names.size + shape.aliases.size === 0
189
189
  ? 'and nothing else — this is the first node, so `input` is all there is'
190
- : `and one name per node that has already answered: ${[...names].map((one) => `\`${one}\``).join(', ')}`);
190
+ : `and one name per node that has already answered: ${[...names, ...shape.aliases.keys()].map((one) => `\`${one}\``).join(', ')}`);
191
191
  return;
192
192
  }
193
- edits.push({ from: up.getStart(), to: up.getEnd(), text: said });
194
- shape.heard.add(said);
193
+ const alias = shape.aliases.get(said);
194
+ edits.push({ from: up.getStart(), to: up.getEnd(), text: alias ?? said });
195
+ if (alias === undefined)
196
+ shape.heard.add(said);
197
+ else
198
+ shape.now.add(said);
195
199
  return;
196
200
  }
197
201
  because = `reaches for \`${read.held}\` itself rather than one thing on it, and once every node is a local there is no such thing to hand it`;
@@ -257,13 +261,7 @@ function forked(name, said, shape) {
257
261
  return 'names its ways in a way this cannot read — write each one as `name: block`';
258
262
  const unread = shape.unread.has(name);
259
263
  const picked = `${OWN}${name}_said`;
260
- const runs = [
261
- ...(unread ? [] : [`let ${name}: unknown`]),
262
- chose.form === 'expression'
263
- ? `const ${picked}: string = await (${chose.text})`
264
- : `const ${picked}: string = await ((${said[0] ?? ''}) as (opened: ${shape.reads}) => Promise<string>)(opened)`,
265
- `switch (${picked}) {`,
266
- ];
264
+ const runs = [...(unread ? [] : [`let ${name}: unknown`]), choosing(chose, shape, picked), `switch (${picked}) {`];
267
265
  for (const way of named) {
268
266
  const each = spliced(way?.held ?? '', shape, ROLES.block);
269
267
  if (typeof each === 'string')
@@ -398,44 +396,25 @@ function compiled(at, id, one, known, unread) {
398
396
  };
399
397
  }
400
398
  const only = one.requires.length === 0 ? 'never' : one.requires.map((each) => quoted(each)).join(' | ');
401
- const mine = new Set(['input', ...one.nodes.map((row) => String(row.id))]);
402
- const runs = [];
403
- const names = new Set();
399
+ const mine = new Set(['input', ...stepsOf(one).map((row) => String(row.id))]);
404
400
  const heard = new Set();
405
- let last;
406
- for (const [order, row] of one.nodes.entries()) {
407
- const name = row.id;
408
- const from = builtFrom(row, known);
409
- if (from === 'nd_stop' && order !== one.nodes.length - 1) {
410
- const after = one.nodes.slice(order + 1).map((each) => `\`${String(each.id)}\``);
411
- return {
412
- because: `the node \`${String(name)}\` ends \`${id}\`, and ${after.join(', ')} ${after.length === 1 ? 'is' : 'are'} declared after it. ` +
413
- 'Nothing runs after the node that ends an operation — move it last, or take off what follows it',
414
- };
415
- }
416
- if (name === undefined)
417
- return { because: `\`${id}\` declares a node with no id of its own` };
418
- if (row.at === undefined)
419
- return { because: `the node \`${name}\` names no file of its own` };
420
- if (from === undefined)
421
- return { because: `the node \`${name}\` names no type this app declares` };
422
- const path = resolve(ROOT, row.at);
423
- if (!existsSync(path))
424
- return { because: `the node \`${name}\` seeds ${row.at} and nothing is there — run \`thread seed\`` };
425
- const held = blockIn(readFileSync(path, 'utf8'));
426
- if (held === undefined) {
427
- return { because: `the node \`${name}\` states no block this can read in ${row.at} — run \`thread seed\` and write it again` };
428
- }
429
- const shape = { all: `Opened<${only}>`, reads: `Reads<Opened<${only}>>`, names, mine, heard, unread: unread ?? new Set() };
430
- const made = (SHAPES[from] ?? ownType)(name, parts(held), shape);
431
- if (typeof made === 'string')
432
- return { because: `the node \`${name}\` ${made}` };
433
- runs.push(` ${ENTERED} = ${quoted(name)}`);
434
- for (const line of made.runs)
435
- runs.push(` ${line}`);
436
- names.add(name);
437
- last = from === 'nd_stop' ? undefined : name;
438
- }
401
+ const made = sequence(one.nodes, {
402
+ of: id,
403
+ all: `Opened<${only}>`,
404
+ reads: `Reads<Opened<${only}>>`,
405
+ names: new Set(),
406
+ mine,
407
+ heard,
408
+ unread: unread ?? new Set(),
409
+ aliases: new Map(),
410
+ now: new Set(),
411
+ within: one.within,
412
+ known,
413
+ });
414
+ if (typeof made === 'string')
415
+ return { because: made };
416
+ const runs = made.runs.map((line) => ` ${line}`);
417
+ const { last } = made;
439
418
  if (unread === undefined) {
440
419
  const final = one.nodes.at(-1)?.id;
441
420
  return compiled(at, id, one, known, new Set([...mine].filter((name) => name !== final && !heard.has(name))));
@@ -477,6 +456,127 @@ function compiled(at, id, one, known, unread) {
477
456
  };
478
457
  }
479
458
  const ownType = (name, said, shape) => plainly(name, said, shape, shape.all, false);
459
+ const choosing = (one, shape, picked) => one.form === 'expression'
460
+ ? `const ${picked}: string = await (${one.text})`
461
+ : `const ${picked}: string = await ((${one.whole}) as (opened: ${shape.reads}) => Promise<string>)(opened)`;
462
+ function sequence(steps, shape) {
463
+ const runs = [];
464
+ let last;
465
+ for (const [order, row] of steps.entries()) {
466
+ const name = row.id;
467
+ const from = builtFrom(row, shape.known);
468
+ if (from === 'nd_stop' && order !== steps.length - 1) {
469
+ const after = steps.slice(order + 1).map((each) => `\`${String(each.id)}\``);
470
+ return (`the node \`${String(name)}\` ends \`${shape.of}\`, and ${after.join(', ')} ${after.length === 1 ? 'is' : 'are'} declared after it. ` +
471
+ 'Nothing runs after the node that ends an operation — move it last, or take off what follows it');
472
+ }
473
+ if (name === undefined)
474
+ return `\`${shape.of}\` declares a node with no id of its own`;
475
+ if (row.at === undefined)
476
+ return `the node \`${name}\` names no file of its own`;
477
+ if (from === undefined)
478
+ return `the node \`${name}\` names no type this app declares`;
479
+ const path = resolve(ROOT, row.at);
480
+ if (!existsSync(path))
481
+ return `the node \`${name}\` seeds ${row.at} and nothing is there — run \`thread seed\``;
482
+ const held = blockIn(readFileSync(path, 'utf8'));
483
+ if (held === undefined)
484
+ return `the node \`${name}\` states no block this can read in ${row.at} — run \`thread seed\` and write it again`;
485
+ const kids = shape.within.get(name) ?? [];
486
+ const holds = HELD[from];
487
+ let made;
488
+ if (kids.length === 0) {
489
+ const own = (SHAPES[from] ?? ownType)(name, parts(held), shape);
490
+ made = typeof own === 'string' ? `the node \`${name}\` ${own}` : own;
491
+ }
492
+ else if (holds === undefined) {
493
+ return (`the node \`${name}\` holds ${kids.map((each) => `\`${String(each.id)}\``).join(', ')}, and it is built from \`${from}\`. ` +
494
+ 'Only a loop built from `nd_repeat` or a branch built from `nd_branch` holds other nodes. Take each one out with ' +
495
+ `${kids.map((each) => `\`thread remove ${String(each.id)}\``).join(', ')}, then add it where it belongs with \`thread create operation edit\``);
496
+ }
497
+ else
498
+ made = holds(name, parts(held), kids, shape);
499
+ if (typeof made === 'string')
500
+ return made;
501
+ runs.push(`${ENTERED} = ${quoted(name)}`, ...made.runs);
502
+ shape.names.add(name);
503
+ last = from === 'nd_stop' ? undefined : name;
504
+ }
505
+ return { runs, last };
506
+ }
507
+ function looped(name, said, kids, shape) {
508
+ if (said.length !== 1) {
509
+ return (`the node \`${name}\` holds other nodes, so its file states how many passes it may make and nothing else, and it states ${String(said.length)} thing(s). ` +
510
+ 'The nodes it holds are what each pass runs');
511
+ }
512
+ const tail = kids.at(-1);
513
+ if (tail !== undefined && builtFrom(tail, shape.known) === 'nd_stop') {
514
+ return (`the node \`${name}\` ends each pass with \`${String(tail.id)}\`, which ends the whole operation, so no pass could say whether the loop is done. ` +
515
+ 'Hold last a node that answers `{ done, value }`');
516
+ }
517
+ const now = `${OWN}${name}_now`;
518
+ const body = sequence(kids, { ...shape, names: new Set(shape.names), aliases: new Map([...shape.aliases, [name, now]]) });
519
+ if (typeof body === 'string')
520
+ return body;
521
+ const { last } = body;
522
+ if (last === undefined)
523
+ return `the node \`${name}\` holds no node that answers, so no pass could say whether it is done`;
524
+ shape.heard.add(last);
525
+ const ceiling = `${OWN}${name}_ceiling`;
526
+ const carried = `${OWN}${name}_carried`;
527
+ const done = `${OWN}${name}_done`;
528
+ const pass = `${OWN}${name}_pass`;
529
+ const answer = `${OWN}${name}_said`;
530
+ const unread = shape.unread.has(name);
531
+ const reads = shape.now.has(name);
532
+ return {
533
+ runs: [
534
+ ...(unread ? [] : [`let ${name}: unknown`]),
535
+ `const ${ceiling}: number = ${said[0] ?? ''}`,
536
+ ...(reads ? [`let ${carried}: unknown`] : []),
537
+ `let ${done} = false`,
538
+ `for (let ${pass} = 0; ${pass} < ${ceiling}; ${pass} += 1) {`,
539
+ ...(reads ? [` const ${now} = { pass: ${pass}, carried: ${carried} }`] : []),
540
+ ...body.runs.map((line) => ` ${line}`),
541
+ ` const ${answer} = ${last} as { readonly done: boolean; readonly value: unknown }`,
542
+ ` if (${answer}.done) { ${unread ? '' : `${name} = ${answer}.value; `}${done} = true; break }`,
543
+ ...(reads ? [` ${carried} = ${answer}.value`] : []),
544
+ '}',
545
+ `${ENTERED} = ${quoted(name)}`,
546
+ `if (!${done}) throw new Error(\`${name} reached its ceiling of \${String(${ceiling})} passes without answering done\`)`,
547
+ ],
548
+ };
549
+ }
550
+ function chosen(name, said, kids, shape) {
551
+ if (said.length !== 1) {
552
+ return (`the node \`${name}\` holds other nodes, so its file states the block that chooses between them and nothing else, and it states ${String(said.length)} thing(s). ` +
553
+ 'Each node it holds is one way it can go');
554
+ }
555
+ const chose = spliced(said[0] ?? '', shape, ROLES.block);
556
+ if (typeof chose === 'string')
557
+ return `the node \`${name}\` chooses with a block that ${chose}`;
558
+ const unread = shape.unread.has(name);
559
+ const picked = `${OWN}${name}_said`;
560
+ const runs = [...(unread ? [] : [`let ${name}: unknown`]), choosing(chose, shape, picked), `switch (${picked}) {`];
561
+ for (const kid of kids) {
562
+ const way = String(kid.id);
563
+ const body = sequence([kid], { ...shape, names: new Set(shape.names) });
564
+ if (typeof body === 'string')
565
+ return body;
566
+ runs.push(` case ${quoted(way)}: {`, ...body.runs.map((line) => ` ${line}`));
567
+ if (body.last !== undefined) {
568
+ shape.heard.add(way);
569
+ runs.push(unread ? ` void ${way}` : ` ${name} = ${way}`, ' break');
570
+ }
571
+ runs.push(' }');
572
+ }
573
+ runs.push(` default: throw new Error(\`${name} chose \${${picked}}, which names no node it holds\`)`, '}');
574
+ return { runs };
575
+ }
576
+ const HELD = {
577
+ nd_repeat: looped,
578
+ nd_branch: chosen,
579
+ };
480
580
  async function planned() {
481
581
  const pass = descend();
482
582
  const known = types(pass);
@@ -9,10 +9,13 @@ interface Woven {
9
9
  readonly nodes: readonly Row[];
10
10
  readonly takes: readonly Row[];
11
11
  readonly body?: string;
12
+ readonly within: ReadonlyMap<string, readonly Row[]>;
12
13
  readonly requires: readonly string[];
13
14
  readonly stray: readonly string[];
14
15
  readonly timeout?: number;
15
16
  }
17
+ export declare const HOLDING: Readonly<Record<string, (first: string) => string>>;
18
+ export declare const stepsOf: (one: Woven) => readonly Row[];
16
19
  export declare function operations(pass: Pass, known: ReadonlySet<string>): ReadonlyMap<string, Woven>;
17
20
  export declare const problem: (id: string, one: Woven) => string | undefined;
18
21
  export declare function blockIn(text: string): string | undefined;