@prestack/thread 0.1.20 → 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.
- package/CHANGELOG.md +193 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.d.ts +2 -0
- package/dist/core/descend.js +18 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +68 -0
- package/dist/core/rows.d.ts +5 -0
- package/dist/core/rows.js +45 -0
- package/dist/core/tree.d.ts +1 -0
- package/dist/core/tree.js +7 -1
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +103 -22
- package/dist/derivation/generators/release.js +74 -26
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.d.ts +3 -9
- package/dist/integrations/cli.js +96 -36
- package/dist/integrations/declared.d.ts +8 -0
- package/dist/integrations/declared.js +22 -0
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +4 -1
- package/dist/integrations/target.js +20 -6
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/generators/publish.js +2 -1
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +234 -24
- package/dist/operations/generators/compile.d.ts +21 -0
- package/dist/operations/generators/compile.js +151 -51
- package/dist/operations/generators/permits.d.ts +2 -1
- package/dist/operations/generators/permits.js +52 -47
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1173
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -459
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -615
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -656
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -232
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -224
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
|
@@ -12,16 +12,17 @@ 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 { declaresUi, 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';
|
|
18
19
|
import { secrecyOf } from '../../integrations/secret.js';
|
|
19
20
|
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';
|
|
21
|
+
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
22
|
import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.js';
|
|
22
23
|
import { DISPATCH_AT } from '../dispatch.js';
|
|
23
24
|
import { STREAMED, TOLD } from '../../integrations/https.js';
|
|
24
|
-
import { holdsRow, listBounds,
|
|
25
|
+
import { holdsRow, listBounds, nestedBounds, rowsIn, unread, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.js';
|
|
25
26
|
import { said } from '../attach.js';
|
|
26
27
|
import { MEMBERS_LIST } from '../operations.js';
|
|
27
28
|
import { beside, operations, quoted, runtime, types } from './seed.js';
|
|
@@ -322,7 +323,7 @@ function client(found) {
|
|
|
322
323
|
function route(one) {
|
|
323
324
|
const at = routeAt(one.operation);
|
|
324
325
|
return [
|
|
325
|
-
|
|
326
|
+
"import { existsSync } from 'node:fs'",
|
|
326
327
|
"import { join } from 'node:path'",
|
|
327
328
|
'',
|
|
328
329
|
'import { counting, DECLARED, opening } from ' + quoted(beside(at, REACHING_AT)),
|
|
@@ -378,7 +379,7 @@ function inbound(one) {
|
|
|
378
379
|
", because: " + quoted(why) + " }, { status: 403 })";
|
|
379
380
|
return [
|
|
380
381
|
...(signed ? ["import { createHmac, timingSafeEqual } from " + quoted("node:crypto")] : []),
|
|
381
|
-
"import { existsSync } from " + quoted(
|
|
382
|
+
"import { existsSync } from " + quoted("node:fs"),
|
|
382
383
|
"import { join } from " + quoted("node:path"),
|
|
383
384
|
"",
|
|
384
385
|
"import { counting, DECLARED, opening } from " + quoted(beside(at, REACHING_AT)),
|
|
@@ -692,22 +693,220 @@ function caller(keeping, kept) {
|
|
|
692
693
|
}
|
|
693
694
|
const carries = () => existsSync(resolve(ROOT, 'node_modules', packageOf('operations')));
|
|
694
695
|
const emitted = () => existsSync(resolve(ROOT, CHECKED_AT));
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
696
|
+
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
697
|
+
function routeSegments(at) {
|
|
698
|
+
const parts = at.split('/');
|
|
699
|
+
if (parts.length < 2 || parts[0] !== 'app' || parts[parts.length - 1] !== 'route.ts')
|
|
700
|
+
return undefined;
|
|
701
|
+
return parts.slice(1, -1);
|
|
702
|
+
}
|
|
703
|
+
function methodsIn(source) {
|
|
704
|
+
const file = ts.createSourceFile(SERVER_AT, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
705
|
+
const found = [];
|
|
706
|
+
for (const statement of file.statements) {
|
|
707
|
+
const shown = ((ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined) ?? []).some((one) => one.kind === ts.SyntaxKind.ExportKeyword);
|
|
708
|
+
if (!shown)
|
|
709
|
+
continue;
|
|
710
|
+
if (ts.isFunctionDeclaration(statement) && statement.name !== undefined && METHODS.includes(statement.name.text)) {
|
|
711
|
+
found.push(statement.name.text);
|
|
712
|
+
}
|
|
713
|
+
else if (ts.isVariableStatement(statement)) {
|
|
714
|
+
for (const one of statement.declarationList.declarations) {
|
|
715
|
+
if (ts.isIdentifier(one.name) && METHODS.includes(one.name.text))
|
|
716
|
+
found.push(one.name.text);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return found;
|
|
721
|
+
}
|
|
722
|
+
function servedByNode() {
|
|
723
|
+
const pass = descend();
|
|
724
|
+
const routes = [];
|
|
725
|
+
for (const at of pass.threaded.keys()) {
|
|
726
|
+
const segments = routeSegments(at);
|
|
727
|
+
if (segments === undefined)
|
|
728
|
+
continue;
|
|
729
|
+
const path = resolve(ROOT, at);
|
|
730
|
+
if (!existsSync(path))
|
|
731
|
+
continue;
|
|
732
|
+
const methods = methodsIn(readFileSync(path, 'utf8'));
|
|
733
|
+
if (methods.length === 0)
|
|
734
|
+
continue;
|
|
735
|
+
routes.push({ at, segments, methods });
|
|
736
|
+
}
|
|
737
|
+
const dynamism = (one) => one.segments.filter((word) => word.startsWith('[')).length;
|
|
738
|
+
routes.sort((a, b) => dynamism(a) - dynamism(b) || compare(a.at, b.at));
|
|
739
|
+
return { hasUi: declaresUi(findTree()), routes };
|
|
740
|
+
}
|
|
741
|
+
function identifier(segments, method) {
|
|
742
|
+
const plain = segments.map((one) => one.split('[').join('').split(']').join('')).join('_');
|
|
743
|
+
const base = plain.replace(/[^A-Za-z0-9_$]/g, '_') || 'root';
|
|
744
|
+
return `${/^[0-9]/.test(base) ? '_' : ''}${base}_${method}`;
|
|
745
|
+
}
|
|
746
|
+
function entry(routes) {
|
|
747
|
+
const table = routes.map((one) => {
|
|
748
|
+
const segments = one.segments.map((word) => quoted(word)).join(', ');
|
|
749
|
+
const methods = one.methods.map((method) => `${method}: ${identifier(one.segments, method)}`).join(', ');
|
|
750
|
+
return ` { segments: [${segments}], methods: { ${methods} } },`;
|
|
751
|
+
});
|
|
752
|
+
return [
|
|
753
|
+
"import { createServer } from 'node:http'",
|
|
754
|
+
"import { Readable } from 'node:stream'",
|
|
755
|
+
'',
|
|
756
|
+
...routes.map((one) => `import { ${one.methods.map((method) => `${method} as ${identifier(one.segments, method)}`).join(', ')} } from ${quoted(beside(SERVER_AT, one.at))}`),
|
|
757
|
+
'',
|
|
758
|
+
'interface Route {',
|
|
759
|
+
' readonly segments: readonly string[]',
|
|
760
|
+
' readonly methods: Readonly<Record<string, (request: Request, context: { readonly params: Readonly<Record<string, string>> }) => Response | Promise<Response>>>',
|
|
761
|
+
'}',
|
|
762
|
+
'',
|
|
763
|
+
'const ROUTES: readonly Route[] = [',
|
|
764
|
+
...table,
|
|
765
|
+
']',
|
|
766
|
+
'',
|
|
767
|
+
'function matched(pathname: string): (Route & { readonly params: Readonly<Record<string, string>> }) | undefined {',
|
|
768
|
+
" const wanted = pathname.split('/').filter((one) => one.length > 0)",
|
|
769
|
+
' for (const route of ROUTES) {',
|
|
770
|
+
' if (route.segments.length !== wanted.length) continue',
|
|
771
|
+
' const params: Record<string, string> = {}',
|
|
772
|
+
' const ok = route.segments.every((segment, at) => {',
|
|
773
|
+
" if (segment.startsWith('[') && segment.endsWith(']')) {",
|
|
774
|
+
' params[segment.slice(1, -1)] = decodeURIComponent(wanted[at] as string)',
|
|
775
|
+
' return true',
|
|
776
|
+
' }',
|
|
777
|
+
' return segment === wanted[at]',
|
|
778
|
+
' })',
|
|
779
|
+
' if (ok) return { ...route, params }',
|
|
780
|
+
' }',
|
|
781
|
+
' return undefined',
|
|
782
|
+
'}',
|
|
783
|
+
'',
|
|
784
|
+
'const PORT = Number(process.env.PORT ?? 3000)',
|
|
785
|
+
"const HOSTNAME = process.env.HOSTNAME ?? '0.0.0.0'",
|
|
786
|
+
'',
|
|
787
|
+
'createServer((req, res) => {',
|
|
788
|
+
' const host = req.headers.host ?? `${HOSTNAME}:${String(PORT)}`',
|
|
789
|
+
" const url = new URL(req.url ?? '/', `http://${host}`)",
|
|
790
|
+
' const route = matched(url.pathname)',
|
|
791
|
+
' if (route === undefined) {',
|
|
792
|
+
' res.writeHead(404)',
|
|
793
|
+
' res.end()',
|
|
794
|
+
' return',
|
|
795
|
+
' }',
|
|
796
|
+
" const method = (req.method ?? 'GET').toUpperCase()",
|
|
797
|
+
' const handler = route.methods[method]',
|
|
798
|
+
' if (handler === undefined) {',
|
|
799
|
+
" res.writeHead(405, { allow: Object.keys(route.methods).join(', ') })",
|
|
800
|
+
' res.end()',
|
|
801
|
+
' return',
|
|
802
|
+
' }',
|
|
803
|
+
' const controller = new AbortController()',
|
|
804
|
+
" res.on('close', () => {",
|
|
805
|
+
' if (!res.writableEnded) controller.abort()',
|
|
806
|
+
' })',
|
|
807
|
+
' const headers = new Headers()',
|
|
808
|
+
' for (const [key, value] of Object.entries(req.headers)) {',
|
|
809
|
+
' if (value === undefined) continue',
|
|
810
|
+
' for (const one of Array.isArray(value) ? value : [value]) headers.append(key, one)',
|
|
811
|
+
' }',
|
|
812
|
+
' const request = new Request(url.href, {',
|
|
813
|
+
' method,',
|
|
814
|
+
' headers,',
|
|
815
|
+
" body: method === 'GET' || method === 'HEAD' ? undefined : Readable.toWeb(req),",
|
|
816
|
+
' signal: controller.signal,',
|
|
817
|
+
" duplex: 'half',",
|
|
818
|
+
' } as never)',
|
|
819
|
+
' Promise.resolve(handler(request, { params: route.params }))',
|
|
820
|
+
' .then((answered) => {',
|
|
821
|
+
' const said: Record<string, string> = {}',
|
|
822
|
+
' answered.headers.forEach((value, key) => {',
|
|
823
|
+
' said[key] = value',
|
|
824
|
+
' })',
|
|
825
|
+
' res.writeHead(answered.status, said)',
|
|
826
|
+
' if (answered.body === null) {',
|
|
827
|
+
' res.end()',
|
|
828
|
+
' return',
|
|
829
|
+
' }',
|
|
830
|
+
' Readable.fromWeb(answered.body as never).pipe(res)',
|
|
831
|
+
' })',
|
|
832
|
+
' .catch((thrown: unknown) => {',
|
|
833
|
+
' console.error(thrown instanceof Error ? thrown.stack ?? thrown.message : thrown)',
|
|
834
|
+
' if (!res.headersSent) res.writeHead(500)',
|
|
835
|
+
' res.end()',
|
|
836
|
+
' })',
|
|
837
|
+
'}).listen(PORT, HOSTNAME)',
|
|
838
|
+
'',
|
|
839
|
+
].join('\n');
|
|
840
|
+
}
|
|
841
|
+
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.';
|
|
842
|
+
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.';
|
|
843
|
+
function entered(hasUi, writing) {
|
|
844
|
+
const sheet = resolve(ROOT, OPERATIONS_AT);
|
|
845
|
+
if (!existsSync(sheet))
|
|
846
|
+
return [];
|
|
847
|
+
const text = readFileSync(sheet, 'utf8');
|
|
848
|
+
const where = listBounds(text, 'derives');
|
|
849
|
+
if (where === undefined)
|
|
850
|
+
return [];
|
|
851
|
+
const holds = holdsRow(text, where, 'server-entry');
|
|
852
|
+
const wants = !hasUi;
|
|
853
|
+
if (wants === holds)
|
|
854
|
+
return [];
|
|
855
|
+
const wrong = [
|
|
856
|
+
wants
|
|
857
|
+
? `this app declares no ui, and its row does not derive ${SERVER_AT}`
|
|
858
|
+
: `this app declares ui, and its row still derives ${SERVER_AT}`,
|
|
859
|
+
];
|
|
860
|
+
if (!writing)
|
|
861
|
+
return wrong;
|
|
862
|
+
const next = wants
|
|
863
|
+
? withRow(text, where, said({
|
|
864
|
+
id: 'server-entry',
|
|
865
|
+
at: SERVER_AT,
|
|
866
|
+
describes: SERVER_ENTRY_DESCRIBES,
|
|
867
|
+
version: 1,
|
|
868
|
+
by: 'thread capabilities',
|
|
869
|
+
runs: 'thread capabilities --check',
|
|
870
|
+
derives: [
|
|
871
|
+
{
|
|
872
|
+
id: 'server',
|
|
873
|
+
at: SERVER_OUT,
|
|
874
|
+
describes: SERVER_DESCRIBES,
|
|
875
|
+
version: 1,
|
|
876
|
+
by: 'thread build',
|
|
877
|
+
runs: 'thread build --check',
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
}, ' '))
|
|
881
|
+
: withoutRow(text, where, 'server-entry');
|
|
882
|
+
writeFileSync(sheet, next, 'utf8');
|
|
883
|
+
return wrong;
|
|
884
|
+
}
|
|
885
|
+
function unentered(hasUi) {
|
|
886
|
+
if (!hasUi)
|
|
887
|
+
return [];
|
|
888
|
+
return [SERVER_AT, SERVER_OUT].filter((at) => existsSync(resolve(ROOT, at)));
|
|
889
|
+
}
|
|
890
|
+
const files = (found, targets, calls, keeps, webhooks = []) => {
|
|
891
|
+
const { hasUi, routes } = servedByNode();
|
|
892
|
+
return [
|
|
893
|
+
{ at: CAPABILITIES_AT, text: printed(found) },
|
|
894
|
+
{ at: OPENAPI_AT, text: printed(openapi(found)) },
|
|
895
|
+
{ at: MCP_AT, text: printed(mcp(found)) },
|
|
896
|
+
{ at: CLIENT_AT, text: client(found) },
|
|
897
|
+
{ at: REACHING_AT, text: reaching(targets, calls, kept(), existsSync(resolve(ROOT, VAULT_AT)), carries()) },
|
|
898
|
+
{ at: ACTOR_AT, text: caller(existsSync(resolve(ROOT, ENTITIES_AT)), keeps) },
|
|
899
|
+
...(emitted() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
|
|
900
|
+
...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
|
|
901
|
+
...webhooks
|
|
902
|
+
.filter((one) => found.some((each) => each.operation === one.invokes && each.live.includes('webhook') && each.webhooks.includes(one.id) && compiled(one.invokes)))
|
|
903
|
+
.flatMap((one) => {
|
|
904
|
+
const at = (one.derives ?? []).map((each) => each.at).find((each) => each === inboundAt(one.id));
|
|
905
|
+
return at === undefined ? [] : [{ at, text: inbound(one) }];
|
|
906
|
+
}),
|
|
907
|
+
...(hasUi ? [] : [{ at: SERVER_AT, text: entry(routes) }]),
|
|
908
|
+
];
|
|
909
|
+
};
|
|
711
910
|
function unmoved() {
|
|
712
911
|
const tree = defaulted();
|
|
713
912
|
if (tree === undefined || tree.stated)
|
|
@@ -789,7 +988,7 @@ if (invoked(import.meta.url)) {
|
|
|
789
988
|
const words = argv.slice(2).filter((one) => !one.startsWith('-'));
|
|
790
989
|
if (words[0] !== undefined && words[0] !== 'read') {
|
|
791
990
|
console.error('capabilities: `' + words[0] + '` is not something this command does. `thread capabilities` writes what can be reached, and ' +
|
|
792
|
-
'`thread capabilities read <
|
|
991
|
+
'`thread capabilities read <operation>` prints one. Nothing was written.');
|
|
793
992
|
exit(1);
|
|
794
993
|
}
|
|
795
994
|
if (!argv.includes('--check')) {
|
|
@@ -808,13 +1007,12 @@ if (invoked(import.meta.url)) {
|
|
|
808
1007
|
const { found, refusals, live } = await derive();
|
|
809
1008
|
for (const one of refusals)
|
|
810
1009
|
console.error(`capabilities: ${one}`);
|
|
811
|
-
const mark = (one) => marked([one.operation, one.describes, one.requires.join(','), one.nodes.join(','), one.live.join(',')]);
|
|
812
1010
|
const verb = words[0];
|
|
813
1011
|
if (verb === 'read') {
|
|
814
1012
|
const which = words[1];
|
|
815
|
-
const one = found.find((each) =>
|
|
1013
|
+
const one = found.find((each) => each.operation === which);
|
|
816
1014
|
if (one === undefined) {
|
|
817
|
-
console.error(`capabilities: \`${which ?? '(nothing)'}\` is not
|
|
1015
|
+
console.error(`capabilities: \`${which ?? '(nothing)'}\` is not an operation this app declares. Name one of ${found.map((each) => each.operation).join(', ') || '(none yet)'}.`);
|
|
818
1016
|
exit(1);
|
|
819
1017
|
}
|
|
820
1018
|
stdout.write(`${one.operation} — ${one.describes}\n`);
|
|
@@ -840,6 +1038,7 @@ if (invoked(import.meta.url)) {
|
|
|
840
1038
|
const people = sheet('entities.ts', 'ENTITIES');
|
|
841
1039
|
const one = people.find((each) => each.actor !== undefined);
|
|
842
1040
|
const where = one === undefined ? undefined : targetOf(one.id, all);
|
|
1041
|
+
const { hasUi } = servedByNode();
|
|
843
1042
|
const written = files(found, all, sheet('targets.ts', 'CALLS'), where === undefined || typeof where === 'string' ? undefined : where.id, sheet('targets.ts', 'WEBHOOKS'));
|
|
844
1043
|
if (checking) {
|
|
845
1044
|
const drifted = written.filter((one) => {
|
|
@@ -853,6 +1052,8 @@ if (invoked(import.meta.url)) {
|
|
|
853
1052
|
...(unmoved() === undefined ? [] : [`${CAPABILITIES_AT} switches surfaces that no row states yet`]),
|
|
854
1053
|
...routed(found, false),
|
|
855
1054
|
...unserved(found).map((at) => `${at} is still on disk to be served, and its operation does not answer on api`),
|
|
1055
|
+
...entered(hasUi, false),
|
|
1056
|
+
...unentered(hasUi).map((at) => `${at} is still on disk, and this app declares ui now`),
|
|
856
1057
|
];
|
|
857
1058
|
for (const one of unsettled)
|
|
858
1059
|
console.error(`capabilities: ${one}. Run \`thread capabilities\` and commit the result.`);
|
|
@@ -873,6 +1074,15 @@ if (invoked(import.meta.url)) {
|
|
|
873
1074
|
}
|
|
874
1075
|
for (const one of routed(found, true))
|
|
875
1076
|
console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`);
|
|
1077
|
+
for (const one of entered(hasUi, true))
|
|
1078
|
+
console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`);
|
|
1079
|
+
for (const at of unentered(hasUi)) {
|
|
1080
|
+
rmSync(resolve(ROOT, at));
|
|
1081
|
+
const folder = dirname(resolve(ROOT, at));
|
|
1082
|
+
if (readdirSync(folder).length === 0)
|
|
1083
|
+
rmSync(folder, { recursive: true, force: true });
|
|
1084
|
+
console.log(`capabilities: deleted ${at}, because this app declares ui now and Next serves it instead`);
|
|
1085
|
+
}
|
|
876
1086
|
for (const at of unserved(found)) {
|
|
877
1087
|
rmSync(resolve(ROOT, at));
|
|
878
1088
|
const folder = dirname(resolve(ROOT, at));
|
|
@@ -1,2 +1,23 @@
|
|
|
1
|
+
import { type Call, type Target } from '../../integrations/target.ts';
|
|
2
|
+
import type { Secret } from '../../integrations/secret.ts';
|
|
3
|
+
export declare function reached(targets: readonly Target[], calls: readonly Call[], secrets: readonly Secret[]): Readonly<Record<string, {
|
|
4
|
+
readonly reach: string;
|
|
5
|
+
readonly address?: string;
|
|
6
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
7
|
+
readonly sends: readonly {
|
|
8
|
+
readonly secret: string;
|
|
9
|
+
readonly header: string;
|
|
10
|
+
readonly prefix: string;
|
|
11
|
+
}[];
|
|
12
|
+
readonly signs: readonly {
|
|
13
|
+
readonly secret: string;
|
|
14
|
+
readonly header: string;
|
|
15
|
+
readonly prefix: string;
|
|
16
|
+
}[];
|
|
17
|
+
readonly calls: Readonly<Record<string, {
|
|
18
|
+
readonly method: string | null;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
}>>;
|
|
21
|
+
}>>;
|
|
1
22
|
export declare function parts(text: string): readonly string[];
|
|
2
23
|
export declare function compile(say?: boolean): Promise<number>;
|
|
@@ -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}`);
|
|
@@ -27,7 +27,7 @@ async function reaching() {
|
|
|
27
27
|
const secrets = await held('secrets.ts', 'SECRETS');
|
|
28
28
|
return reached(targets, calls, secrets);
|
|
29
29
|
}
|
|
30
|
-
function reached(targets, calls, secrets) {
|
|
30
|
+
export function reached(targets, calls, secrets) {
|
|
31
31
|
const out = {};
|
|
32
32
|
for (const one of targets) {
|
|
33
33
|
const mine = calls.filter((each) => each.of === one.id && one.takes.includes(each.id));
|
|
@@ -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
|
-
|
|
194
|
-
|
|
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.
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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);
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Grant } from '../access.ts';
|
|
2
|
+
export declare function permitting(grants: readonly Grant[]): string;
|