@prestack/thread 0.0.1 → 0.0.2

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 (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,170 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join, relative, resolve } from 'node:path'
3
+
4
+ import { quoted } from './rows.ts'
5
+
6
+ export const TREE = '_threads'
7
+ export const ROOT = 'thread.ts'
8
+
9
+ export class AlreadyThreadedError extends Error {
10
+ readonly at: string
11
+ constructor(at: string) {
12
+ super(
13
+
14
+ `${at} already exists, so this repository already has a root. A tree has one root and it is written once. ` +
15
+ 'Declare something under it with `thread create` — run it bare to see what this tree makes — or take on ' +
16
+ 'a package with `thread attach`, which brings its rows with it. Neither is written by hand.',
17
+ )
18
+ this.name = 'AlreadyThreadedError'
19
+ this.at = at
20
+ }
21
+ }
22
+
23
+ export const COMPILER = 'tsconfig.json'
24
+
25
+ export const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb'] as const
26
+
27
+ export const MANIFEST = 'package.json'
28
+
29
+ const MODULE = 'module'
30
+
31
+ export const REGISTRY = '.npm' + 'rc'
32
+
33
+ export const IGNORED = '.git' + 'ignore'
34
+
35
+ export function ignored(): string {
36
+ return [
37
+ 'node_' + 'modules/',
38
+ '.next/',
39
+ 'dist/',
40
+
41
+ '*.tsbuildinfo',
42
+
43
+ 'next-env.d.ts',
44
+ '.DS_Store',
45
+ '',
46
+ ].join('\n')
47
+ }
48
+
49
+ export function compiler(): string {
50
+ return `${JSON.stringify(
51
+ {
52
+ compilerOptions: {
53
+ target: 'ES2022',
54
+ module: 'ESNext',
55
+ moduleResolution: 'bundler',
56
+ strict: true,
57
+
58
+ allowImportingTsExtensions: true,
59
+
60
+ skipLibCheck: true,
61
+
62
+ types: ['node'],
63
+ jsx: 'react-jsx',
64
+ noEmit: true,
65
+ },
66
+ },
67
+ null,
68
+ 2,
69
+ )}\n`
70
+ }
71
+
72
+ function owned(into: string): readonly { readonly id: string; readonly at: string; readonly describes: string; readonly runs?: string }[] {
73
+ const held = LOCKFILES.filter((one) => existsSync(join(into, one)))
74
+ return [
75
+ {
76
+ id: 'manifest',
77
+ at: 'package.json',
78
+ describes:
79
+ 'What this app is made of and what may be run against it — the packages it installs and the commands that build it, serve it and refuse it.',
80
+ },
81
+
82
+ ...(existsSync(join(into, REGISTRY))
83
+ ? [
84
+ {
85
+ id: 'registry',
86
+ at: REGISTRY,
87
+ describes:
88
+ 'Which registry this app’s packages come from, and any credential scoped to it. Declared beside the lockfile because an install is not reproducible without it: the lockfile says which versions answered, and this says who answered.',
89
+ },
90
+ ]
91
+ : []),
92
+ {
93
+ id: 'ignored',
94
+ at: IGNORED,
95
+ describes:
96
+ 'What is built rather than written, so the checks that read this repository can tell the two apart. Every surface here asks which files are tracked, and without it an app that has been served once answers with its whole build cache.',
97
+ },
98
+ ...held.map((one) => ({
99
+ id: 'lockfile',
100
+ at: one,
101
+ describes:
102
+ 'The exact version of every package this app resolved to, so an install here and an install anywhere else are the same install.',
103
+ })),
104
+ ]
105
+ }
106
+
107
+ export function root(into: string): string {
108
+ const rows = owned(into).flatMap((one) => [
109
+ ' {',
110
+ ` id: ${quoted(one.id)},`,
111
+ ` at: ${quoted(one.at)},`,
112
+ ` describes: ${quoted(one.describes)},`,
113
+ ' version: 1,',
114
+ ...(one.runs === undefined ? [] : [` runs: ${quoted(one.runs)},`]),
115
+ ' },',
116
+ ])
117
+ return [
118
+ "import type { Threaded } from './thread.ts'",
119
+ '',
120
+ 'export const THREAD: Threaded = {',
121
+ " id: 'thread',",
122
+ ` at: '${TREE}/${ROOT}',`,
123
+ " describes: 'The framework that declares what must exist, forbids what is not declared, and derives what can be derived.',",
124
+ ' version: 1,',
125
+ ' declares: [',
126
+ ...rows,
127
+ ' ],',
128
+ '}',
129
+ '',
130
+ ].join('\n')
131
+ }
132
+
133
+ export function init(into: string): readonly string[] {
134
+ const dir = resolve(into, TREE)
135
+ const at = join(dir, ROOT)
136
+ if (existsSync(at)) throw new AlreadyThreadedError(relative(into, at))
137
+ mkdirSync(dir, { recursive: true })
138
+ const written: string[] = []
139
+
140
+ const held = join(into, COMPILER)
141
+ if (!existsSync(held)) {
142
+ writeFileSync(held, compiler(), 'utf8')
143
+ written.push(COMPILER)
144
+ }
145
+
146
+ const manifest = join(into, MANIFEST)
147
+ if (!existsSync(manifest)) {
148
+ throw new Error(
149
+ `there is no ${MANIFEST} here, and a thread tree is source this package has to be able to load. ` +
150
+ 'Run `npm init` first, then `thread init` — the tree is written beside a manifest, not instead of one.',
151
+ )
152
+ }
153
+ const stated = JSON.parse(readFileSync(manifest, 'utf8')) as { type?: string }
154
+ if (stated.type !== MODULE) {
155
+ writeFileSync(manifest, `${JSON.stringify({ ...stated, type: MODULE }, null, 2)}\n`, 'utf8')
156
+ written.push(MANIFEST)
157
+ }
158
+
159
+ const ignore = join(into, IGNORED)
160
+ if (!existsSync(ignore)) {
161
+ writeFileSync(ignore, ignored(), 'utf8')
162
+ written.push(IGNORED)
163
+ }
164
+
165
+ for (const [name, text] of [[ROOT, root(into)]] as const) {
166
+ writeFileSync(join(dir, name), text, 'utf8')
167
+ written.push(relative(into, join(dir, name)))
168
+ }
169
+ return written
170
+ }
@@ -0,0 +1,47 @@
1
+ import { realpathSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { argv, exit as ending } from 'node:process'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ export function invoked(url: string): boolean {
7
+ const named = argv[1]
8
+ if (named === undefined) return false
9
+ try {
10
+ const held = new URL(url)
11
+ held.search = ''
12
+ held.hash = ''
13
+ return realpathSync(named) === realpathSync(fileURLToPath(held))
14
+ } catch {
15
+ return false
16
+ }
17
+ }
18
+
19
+ export const SOURCE = import.meta.url.endsWith('.ts')
20
+
21
+ export const BESIDE = (name: string): string => `${name}${SOURCE ? '.ts' : '.js'}`
22
+
23
+ export const LOADER: readonly string[] = SOURCE ? ['--import', 'tsx'] : []
24
+
25
+ export const CLI: readonly string[] = [
26
+ ...LOADER,
27
+ join(dirname(fileURLToPath(import.meta.url)), BESIDE('cli')),
28
+ ]
29
+
30
+ export const REFUSED = Symbol.for('thread.refused')
31
+
32
+ let sharing = false
33
+
34
+ export const shared = (on: boolean): void => {
35
+ sharing = on
36
+ }
37
+
38
+ export function exit(code?: number): never {
39
+ if (!sharing) ending(code)
40
+ throw Object.assign(new Error(`refused with ${String(code ?? 0)}`), { [REFUSED]: code ?? 0 })
41
+ }
42
+
43
+ export const refusal = (thrown: unknown): number | undefined => {
44
+ if (typeof thrown !== 'object' || thrown === null) return undefined
45
+ const held = (thrown as Record<symbol, unknown>)[REFUSED]
46
+ return typeof held === 'number' ? held : undefined
47
+ }
@@ -0,0 +1,57 @@
1
+ import { resolve } from 'node:path'
2
+ import { argv, exitCode, stdout } from 'node:process'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ import { generators, RAN, REFUSED_BY } from './chain.ts'
6
+ import { forget } from './descend.ts'
7
+ import { refusal, shared } from './invoked.ts'
8
+ import { findTree } from './tree.ts'
9
+
10
+ let spun = 0
11
+
12
+ const words = (command: string): readonly string[] => command.trim().split(/\s+/).slice(1)
13
+
14
+ shared(true)
15
+
16
+ const tree = findTree()
17
+ const rows = await generators(tree)
18
+
19
+ for (const command of argv.slice(2).filter((one) => one !== '')) {
20
+ const said = words(command)
21
+ const name = said[0]
22
+ if (name === undefined) continue
23
+
24
+ const held = rows.find((one) => one.by === `thread ${name}`)
25
+ if (held?.at === undefined) {
26
+ stdout.write(`${REFUSED_BY} ${command}\n`)
27
+ stdout.write(`round: no command named "${name}", so \`${command}\` was not run\n`)
28
+ continue
29
+ }
30
+
31
+ const found = resolve(tree.at, held.at)
32
+
33
+ forget()
34
+
35
+ argv.length = 1
36
+ argv.push(found, ...said.slice(1))
37
+
38
+ spun += 1
39
+
40
+ process.exitCode = 0
41
+
42
+ try {
43
+ await import(`${pathToFileURL(found).href}?round=${String(spun)}`)
44
+ if (exitCode !== undefined && exitCode !== 0) stdout.write(`${REFUSED_BY} ${command}\n`)
45
+ else stdout.write(`${RAN} ${command}\n`)
46
+ } catch (thrown) {
47
+ const code = refusal(thrown)
48
+ if (code === undefined) {
49
+
50
+ stdout.write(`${REFUSED_BY} ${command}\n`)
51
+ stdout.write(`round: \`${command}\` threw: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`)
52
+ } else if (code === 0) stdout.write(`${RAN} ${command}\n`)
53
+ else stdout.write(`${REFUSED_BY} ${command}\n`)
54
+ }
55
+ }
56
+
57
+ process.exitCode = 0
@@ -0,0 +1,369 @@
1
+ import ts from 'typescript'
2
+
3
+ export interface Bounds {
4
+ readonly open: number
5
+ readonly close: number
6
+ }
7
+
8
+ const PARSED = new Map<string, ts.SourceFile>()
9
+
10
+ function tree(text: string): ts.SourceFile {
11
+ const held = PARSED.get(text)
12
+ if (held !== undefined) return held
13
+ const made = ts.createSourceFile('rows.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
14
+ if (PARSED.size > 8) PARSED.clear()
15
+ PARSED.set(text, made)
16
+ return made
17
+ }
18
+
19
+ function walk(node: ts.Node, seen: (one: ts.Node) => void): void {
20
+ seen(node)
21
+ ts.forEachChild(node, (one) => {
22
+ walk(one, seen)
23
+ })
24
+ }
25
+
26
+ const keyed = (one: ts.PropertyAssignment): string | undefined =>
27
+ ts.isIdentifier(one.name) || ts.isStringLiteral(one.name) ? one.name.text : undefined
28
+
29
+ function stringy(node: ts.ObjectLiteralExpression, name: string): string | undefined {
30
+ for (const one of node.properties) {
31
+ if (!ts.isPropertyAssignment(one) || keyed(one) !== name) continue
32
+ return ts.isStringLiteral(one.initializer) ? one.initializer.text : undefined
33
+ }
34
+ return undefined
35
+ }
36
+
37
+ function listy(node: ts.ObjectLiteralExpression, name: string): ts.ArrayLiteralExpression | undefined {
38
+ for (const one of node.properties) {
39
+ if (!ts.isPropertyAssignment(one) || keyed(one) !== name) continue
40
+ return ts.isArrayLiteralExpression(one.initializer) ? one.initializer : undefined
41
+ }
42
+ return undefined
43
+ }
44
+
45
+ const spanned = (node: ts.Node): { readonly open: number; readonly close: number } => ({
46
+ open: node.getStart(),
47
+ close: node.getEnd() - 1,
48
+ })
49
+
50
+ export function inCode(text: string): readonly boolean[] {
51
+ const held = new Array<boolean>(text.length).fill(true)
52
+ let cursor = 0
53
+ while (cursor < text.length) {
54
+ const one = text[cursor]
55
+ const next = text[cursor + 1]
56
+ if (one === '/' && next === '/') {
57
+ while (cursor < text.length && text[cursor] !== '\n') held[cursor++] = false
58
+ continue
59
+ }
60
+ if (one === '/' && next === '*') {
61
+ held[cursor++] = false
62
+ held[cursor++] = false
63
+ while (cursor < text.length && !(text[cursor] === '*' && text[cursor + 1] === '/')) held[cursor++] = false
64
+ held[cursor++] = false
65
+ held[cursor++] = false
66
+ continue
67
+ }
68
+ if (one === "'" || one === '"' || one === '`') {
69
+ held[cursor++] = false
70
+ while (cursor < text.length && text[cursor] !== one) {
71
+ if (text[cursor] === '\\') held[cursor++] = false
72
+ held[cursor++] = false
73
+ }
74
+ held[cursor++] = false
75
+ continue
76
+ }
77
+ cursor += 1
78
+ }
79
+ return held
80
+ }
81
+
82
+ export function matched(text: string, code: readonly boolean[], from: number, open: string, close: string): number {
83
+ let depth = 0
84
+ for (let cursor = from; cursor < text.length; cursor += 1) {
85
+ if (code[cursor] !== true) continue
86
+ if (text[cursor] === open) depth += 1
87
+ if (text[cursor] === close) {
88
+ depth -= 1
89
+ if (depth === 0) return cursor
90
+ }
91
+ }
92
+ return -1
93
+ }
94
+
95
+ export function listBounds(text: string, list: string): Bounds | undefined {
96
+ const file = tree(text)
97
+
98
+ let found: ts.ArrayLiteralExpression | undefined
99
+ let held: ts.ObjectLiteralExpression | undefined
100
+
101
+ walk(file, (node) => {
102
+ if (found !== undefined || !ts.isVariableDeclaration(node) || node.initializer === undefined) return
103
+
104
+ const unwrapped = (node2: ts.Expression): ts.Expression =>
105
+ ts.isAsExpression(node2) || ts.isSatisfiesExpression(node2) ? unwrapped(node2.expression) : node2
106
+ const one = unwrapped(node.initializer)
107
+ if (ts.isIdentifier(node.name) && node.name.text === list && ts.isArrayLiteralExpression(one)) {
108
+ found = one
109
+ return
110
+ }
111
+ if (held === undefined && ts.isObjectLiteralExpression(one)) held = one
112
+ })
113
+
114
+ if (found !== undefined) return spanned(found)
115
+ if (held === undefined) return undefined
116
+ const inside = listy(held, list)
117
+ return inside === undefined ? undefined : spanned(inside)
118
+ }
119
+
120
+ function elements(text: string, where: Bounds): readonly ts.ObjectLiteralExpression[] {
121
+ let found: ts.ArrayLiteralExpression | undefined
122
+ walk(tree(text), (node) => {
123
+ if (found !== undefined) return
124
+ if (ts.isArrayLiteralExpression(node) && node.getStart() === where.open && node.getEnd() - 1 === where.close) {
125
+ found = node
126
+ }
127
+ })
128
+ if (found === undefined) return []
129
+ return found.elements.filter((one): one is ts.ObjectLiteralExpression => ts.isObjectLiteralExpression(one))
130
+ }
131
+
132
+ function rowAt(text: string, where: Bounds, id: string): ts.ObjectLiteralExpression | undefined {
133
+ return elements(text, where).find((one) => stringy(one, 'id') === id)
134
+ }
135
+
136
+ export function holdsRow(text: string, where: Bounds, id: string): boolean {
137
+ return rowAt(text, where, id) !== undefined
138
+ }
139
+
140
+ export function seededAt(text: string, where: Bounds, id: string): string | undefined {
141
+ const held = rowAt(text, where, id)
142
+ if (held === undefined) return undefined
143
+ const seeds = listy(held, 'seeds')
144
+ const derives = listy(held, 'derives')
145
+ if (seeds !== undefined && derives !== undefined) return undefined
146
+ const first = (seeds ?? derives)?.elements[0]
147
+ if (first === undefined || !ts.isObjectLiteralExpression(first)) return undefined
148
+ return stringy(first, 'at')
149
+ }
150
+
151
+ export function rowsIn(
152
+ text: string,
153
+ where: Bounds,
154
+ ): readonly { readonly id: string; readonly at: string | undefined; readonly source: string }[] {
155
+ const found: { id: string; at: string | undefined; source: string }[] = []
156
+ for (const one of elements(text, where)) {
157
+ const id = stringy(one, 'id')
158
+ if (id === undefined) continue
159
+
160
+ const at = stringy(one, 'at')
161
+ const from = one.getStart()
162
+ const line = text.lastIndexOf('\n', from) + 1
163
+ const pad = text.slice(line, from)
164
+ found.push({ id, at, source: `${/^[ \t]*$/.test(pad) ? pad : ''}${text.slice(from, one.getEnd())},` })
165
+ }
166
+ return found
167
+ }
168
+
169
+ export function withRow(text: string, where: Bounds, row: string): string {
170
+ const held = /[ \t]*$/.exec(text.slice(0, where.close))?.[0] ?? ''
171
+ const before = text.slice(0, where.close - held.length)
172
+ const empty = before.trimEnd().endsWith('[')
173
+ const line = text.lastIndexOf('\n', where.open)
174
+ const pad = empty ? (/^[ \t]*/.exec(text.slice(line + 1))?.[0] ?? '') : held
175
+ return `${before}${empty ? '\n' : ''}${row}\n${pad}${text.slice(where.close)}`
176
+ }
177
+
178
+ export function withSeed(text: string, where: Bounds, id: string, seed: string): string {
179
+ const held = rowAt(text, where, id)
180
+ if (held === undefined) return text
181
+ if (listy(held, 'seeds') !== undefined) return text
182
+
183
+ const to = held.getEnd() - 1
184
+ const line = text.lastIndexOf('\n', held.getStart())
185
+ const pad = /^[ \t]*/.exec(text.slice(line + 1))?.[0] ?? ' '
186
+ const before = text.slice(0, to).trimEnd()
187
+ const comma = before.endsWith(',') ? '' : ','
188
+ return `${before}${comma}\n${pad} seeds: [\n${seed}\n${pad} ],\n${pad}${text.slice(to)}`
189
+ }
190
+
191
+ export function withRequires(text: string, where: Bounds, id: string, requires: readonly string[]): string | undefined {
192
+ const held = rowAt(text, where, id)
193
+ if (held === undefined) return undefined
194
+
195
+ const line = text.lastIndexOf('\n', held.getStart()) + 1
196
+ const pad = `${/^[ \t]*/.exec(text.slice(line))?.[0] ?? ' '} `
197
+ const written = requires.length === 0 ? '' : `${pad}requires: [${requires.map((one) => `'${one}'`).join(', ')}],\n`
198
+
199
+ const propertyNamed = (name: string): ts.PropertyAssignment | undefined => {
200
+ for (const one of held.properties) {
201
+ if (ts.isPropertyAssignment(one) && keyed(one) === name) return one
202
+ }
203
+ return undefined
204
+ }
205
+
206
+ const already = propertyNamed('requires')
207
+ if (already !== undefined) {
208
+ const opens = text.lastIndexOf('\n', already.getStart()) + 1
209
+ const shut = text.indexOf('\n', already.getEnd())
210
+ return `${text.slice(0, opens)}${written}${text.slice(shut === -1 ? text.length : shut + 1)}`
211
+ }
212
+
213
+ const version = propertyNamed('version')
214
+ if (version === undefined) return undefined
215
+ const after = text.indexOf('\n', version.getEnd())
216
+ const at = after === -1 ? text.length : after + 1
217
+ return `${text.slice(0, at)}${written}${text.slice(at)}`
218
+ }
219
+
220
+ export function seedsIn(text: string, where: Bounds, id: string): Bounds | undefined {
221
+ const held = rowAt(text, where, id)
222
+ if (held === undefined) return undefined
223
+ const list = listy(held, 'seeds')
224
+ return list === undefined ? undefined : spanned(list)
225
+ }
226
+
227
+ export function afterRow(text: string, where: Bounds, follows: string, row: string): string | undefined {
228
+ const held = rowAt(text, where, follows)
229
+ if (held === undefined) return undefined
230
+ let to = held.getEnd()
231
+ if (text[to] === ',') to += 1
232
+ return `${text.slice(0, to)}\n${row}${text.slice(to)}`
233
+ }
234
+
235
+ function headed(text: string): ts.ObjectLiteralExpression | undefined {
236
+ let found: ts.ObjectLiteralExpression | undefined
237
+ walk(tree(text), (node) => {
238
+ if (found !== undefined || !ts.isVariableDeclaration(node) || node.initializer === undefined) return
239
+ const one = ts.isAsExpression(node.initializer) ? node.initializer.expression : node.initializer
240
+ if (ts.isObjectLiteralExpression(one)) found = one
241
+ })
242
+ return found
243
+ }
244
+
245
+ function stated(node: ts.ObjectLiteralExpression, field: string): Bounds | undefined {
246
+ for (const one of node.properties) {
247
+ if (!ts.isPropertyAssignment(one) || keyed(one) !== field) continue
248
+ return { open: one.initializer.getStart(), close: one.initializer.getEnd() }
249
+ }
250
+ return undefined
251
+ }
252
+
253
+ export const quoted = (one: string): string => `'${one.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`
254
+
255
+ export const STATED = ['at', 'describes', 'version']
256
+
257
+ export function reconciled(held: string, source: string, fields: readonly string[]): string {
258
+ const ships = headed(source)
259
+ if (ships === undefined) return held
260
+ const named = stringy(ships, 'id')
261
+
262
+ let next = held
263
+ for (const field of fields) {
264
+ const want = stated(ships, field)
265
+ if (want === undefined) continue
266
+
267
+ const mine = headed(next)
268
+ if (mine === undefined || stringy(mine, 'id') !== named) return held
269
+ const have = stated(mine, field)
270
+ if (have === undefined) continue
271
+ const said = source.slice(want.open, want.close)
272
+ if (next.slice(have.open, have.close) === said) continue
273
+ next = next.slice(0, have.open) + said + next.slice(have.close)
274
+ }
275
+ return next
276
+ }
277
+
278
+ export function fieldIn(text: string, where: Bounds, id: string, field: string): string | undefined {
279
+ const held = rowAt(text, where, id)
280
+ if (held === undefined) return undefined
281
+ for (const one of held.properties) {
282
+ if (!ts.isPropertyAssignment(one) || keyed(one) !== field) continue
283
+ return text.slice(one.initializer.getStart(), one.initializer.getEnd())
284
+ }
285
+ return undefined
286
+ }
287
+
288
+ export function withField(
289
+ text: string,
290
+ where: Bounds,
291
+ id: string,
292
+ field: string,
293
+ value: string,
294
+ ): string | undefined {
295
+ const held = rowAt(text, where, id)
296
+ if (held === undefined) return undefined
297
+ for (const one of held.properties) {
298
+ if (!ts.isPropertyAssignment(one) || keyed(one) !== field) continue
299
+ return `${text.slice(0, one.initializer.getStart())}${value}${text.slice(one.initializer.getEnd())}`
300
+ }
301
+
302
+ const last = held.properties.at(-1)
303
+ if (last === undefined) return undefined
304
+
305
+ let ends = last.getEnd()
306
+ if (text[ends] === ',') ends += 1
307
+ const already = text[ends - 1] === ',' ? '' : ','
308
+
309
+ if (!text.slice(held.getStart(), held.getEnd()).includes('\n')) {
310
+ return `${text.slice(0, ends)}${already} ${field}: ${value}${text.slice(ends)}`
311
+ }
312
+
313
+ const line = text.lastIndexOf('\n', last.getStart()) + 1
314
+ const pad = /^[ \t]*/.exec(text.slice(line))?.[0] ?? ' '
315
+ return `${text.slice(0, ends)}${already}\n${pad}${field}: ${value},${text.slice(ends)}`
316
+ }
317
+
318
+ export function withoutRow(text: string, where: Bounds, id: string): string {
319
+ const held = rowAt(text, where, id)
320
+ if (held === undefined) return text
321
+ let ends = held.getEnd()
322
+ if (text[ends] === ',') ends += 1
323
+ const pad = /[ \t]*$/.exec(text.slice(0, held.getStart()))?.[0] ?? ''
324
+ const from = held.getStart() - pad.length
325
+ if (text[ends] === '\n') ends += 1
326
+ const left = text.slice(0, from)
327
+ const right = text.slice(ends)
328
+ return left.trimEnd().endsWith('[') && right.trimStart().startsWith(']')
329
+ ? `${left.replace(/\s*$/, '')}${right.replace(/^\s*/, '')}`
330
+ : left + right
331
+ }
332
+
333
+ const lines = (held: unknown, pad: string): readonly string[] => {
334
+ if (held === null || held === undefined) return [String(held)]
335
+ if (Array.isArray(held)) {
336
+ if (held.length === 0) return ['[]']
337
+ if (held.every((one) => typeof one !== 'object' || one === null)) return [held.map((one) => String(one)).join(', ')]
338
+ return held.flatMap((one) => {
339
+ const [first = '', ...rest] = lines(one, `${pad} `)
340
+ return [`${pad}- ${first.trimStart()}`, ...rest]
341
+ })
342
+ }
343
+ if (typeof held === 'object') {
344
+ const parts = Object.entries(held as Record<string, unknown>)
345
+ if (parts.length === 0) return ['{}']
346
+ return parts.flatMap(([key, one]) => {
347
+ const [first = '', ...rest] = lines(one, `${pad} `)
348
+ return rest.length === 0 && !first.includes('\n') ? [`${pad}${key}: ${first}`] : [`${pad}${key}:`, first, ...rest]
349
+ })
350
+ }
351
+ return [String(held)]
352
+ }
353
+
354
+ export function showing(row: object, only: readonly string[] = []): string {
355
+ const held = row as Record<string, unknown>
356
+ const keys = Object.keys(held)
357
+
358
+ const wanted = only.filter((one) => one !== '')
359
+ const stray = wanted.filter((one) => !keys.includes(one))
360
+ if (stray.length > 0) {
361
+ return `${stray.map((one) => `\`${one}\``).join(', ')} ${stray.length === 1 ? 'is not a field' : 'are not fields'} of this row. It states ${keys.join(', ')}\n`
362
+ }
363
+
364
+ const showing = wanted.length === 0 ? keys : wanted
365
+ return `${showing.flatMap((key) => {
366
+ const [first = '', ...rest] = lines(held[key], ' ')
367
+ return rest.length === 0 ? [`${key}: ${first.trimStart()}`] : [`${key}:`, first, ...rest]
368
+ }).join('\n')}\n`
369
+ }
@@ -0,0 +1,18 @@
1
+ export const ORGANISATION = '@prestack'
2
+
3
+ export const THING = 'thread'
4
+
5
+ export const NAMED = `${ORGANISATION}/${THING}`
6
+
7
+ const CORE = 'core'
8
+
9
+ const HEAD = `${NAMED}-`
10
+
11
+ export const named = (id: string): string => (id === CORE ? NAMED : `${HEAD}${id}`)
12
+
13
+ export const idOf = (name: string): string | undefined => {
14
+ if (name === NAMED) return CORE
15
+ return name.startsWith(HEAD) ? name.slice(HEAD.length) : undefined
16
+ }
17
+
18
+ export const ours = (name: string): boolean => idOf(name) !== undefined
@@ -0,0 +1,29 @@
1
+ export interface Quote {
2
+ readonly on: string
3
+ readonly said: string
4
+ }
5
+
6
+ export interface Threaded {
7
+ readonly id: string
8
+ readonly at: string
9
+ readonly describes: string
10
+ readonly version?: number
11
+ readonly cites?: readonly Quote[]
12
+ readonly requires?: readonly string[]
13
+ readonly takes?: readonly string[]
14
+ readonly by?: string
15
+ readonly runs?: string
16
+ readonly declares?: readonly Threaded[]
17
+ readonly derives?: readonly Threaded[]
18
+ readonly seeds?: readonly Threaded[]
19
+ }
20
+
21
+ export interface Member extends Threaded {
22
+ readonly version: number
23
+ }
24
+
25
+ export const PREFIXES = {
26
+ workflow: 'wf_',
27
+ view: 'vw_',
28
+ agent: 'agt_',
29
+ } as const satisfies Readonly<Record<string, string>>