@prestack/thread 0.0.1 → 0.0.3

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,263 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { dirname, join, relative } from 'node:path'
4
+ import { argv, cwd, env, execPath, stdout } from 'node:process'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import { SKILLS_AT, declared, nouns, skillOf } from '../../core/asking.ts'
8
+ import { generators } from '../../core/chain.ts'
9
+ import { TREE } from '../../core/descend.ts'
10
+ import { LOADER, exit, invoked } from '../../core/invoked.ts'
11
+ import { ORGANISATION, named } from '../../core/scope.ts'
12
+ import type { Tree } from '../../core/tree.ts'
13
+ import { findTree } from '../../core/tree.ts'
14
+
15
+ const WHERE = SKILLS_AT
16
+
17
+ const RECEIPT_LINE = /^[a-z]+: receipt .*$/gm
18
+
19
+ const NONCE = /--receipt [0-9a-f]{6,}/g
20
+
21
+ const CRLF = /\r\n/g
22
+
23
+ const META = fileURLToPath(new URL('../../core/generators/meta.ts', import.meta.url))
24
+
25
+ const fileFor = (noun: string): string => skillOf(noun)
26
+
27
+ async function offering(tree: Tree): Promise<ReadonlyMap<string, string>> {
28
+ const answers = relative(tree.at, META).split('\\').join('/')
29
+ const found = new Map(nouns(await generators(tree)))
30
+ found.set('meta', answers)
31
+ for (const noun of (await declared(tree)).keys()) if (!found.has(noun)) found.set(noun, answers)
32
+ return found
33
+ }
34
+
35
+ function printed(root: string, at: string, noun: string): { readonly refused: string; readonly body: string } {
36
+ if (at === '') return { refused: `nothing in this tree is written by \`thread create ${noun}\``, body: '' }
37
+
38
+ const ran = spawnSync(execPath, [...LOADER, join(root, at)], {
39
+ cwd: root,
40
+ encoding: 'utf8',
41
+ env: { ...env, THREAD_NOUN: noun },
42
+
43
+ stdio: ['ignore', 'pipe', 'pipe'],
44
+ })
45
+ const body = String(ran.stdout ?? '').replace(CRLF, '\n').replace(RECEIPT_LINE, '').trimEnd()
46
+ const said = String(ran.stderr ?? '').replace(CRLF, '\n').trim().split('\n')[0] ?? ''
47
+ return { refused: body === '' ? said || 'it printed nothing' : '', body }
48
+ }
49
+
50
+ function skill(noun: string, body: string): string {
51
+ return [
52
+ `# ${noun}`,
53
+ '',
54
+ `Everything \`npx thread create ${noun}\` knows, written out by \`npx thread skills\`.`,
55
+ '',
56
+ '**Do not edit this file.** It is derived from the tree, so an edit here is reverted the next',
57
+ 'time the command runs, and `npx thread skills --check` fails until it is. If something below is',
58
+ 'wrong or missing, the command that printed it is what to change.',
59
+ '',
60
+ `**Read this before you declare a ${noun}.** What follows is what already exists. The reason it`,
61
+ 'exists is that things get built twice by people who did not look — thirteen elements were once',
62
+ 'declared in one app that every one of them already shipped, and the cost was two days of fixes',
63
+ 'landing on a page nobody was looking at. Open what it names before you add to it.',
64
+ '',
65
+ 'When you are ready to write, run the command yourself for a live receipt. The one printed here',
66
+ 'has been removed on purpose: a receipt is spent by one run and goes stale the moment its sheet',
67
+ 'changes, so a copy kept in a file is always the wrong one.',
68
+ '',
69
+ '```',
70
+ body.replace(NONCE, '--receipt <receipt>'),
71
+ '```',
72
+ '',
73
+ ].join('\n')
74
+ }
75
+
76
+ const SHEET = `${TREE}_generated/skills.ts`
77
+
78
+ function sheet(files: readonly string[]): string {
79
+ const rows = files.map((at) => {
80
+ const noun = at.slice(WHERE.length + 1, -'.md'.length)
81
+ return [
82
+ ' {',
83
+ ` id: 'skill-${noun}',`,
84
+ ` at: '${at}',`,
85
+ ` describes: 'What \`thread create ${noun}\` teaches: the ${noun}s this tree already has, with the sentence each one states, and the rules for declaring another. Written from what that command prints about itself, so the skill and the command cannot disagree.',`,
86
+ ' version: 1,',
87
+ " by: 'thread skills',",
88
+ " runs: 'thread skills --check',",
89
+ ' },',
90
+ ].join('\n')
91
+ })
92
+ return [
93
+ "import type { Threaded } from '../../thread/core/thread.ts'",
94
+ '',
95
+ 'export const SKILLS: Threaded = {',
96
+ " id: 'skills',",
97
+ ` at: '${SHEET}',`,
98
+ " describes: 'One row per skill `thread skills` writes, so every `skills/<noun>.md` is declared by the command that wrote it instead of being refused as a file nothing named. It sits beside the files it names because the nouns are not known until the tree is read, and a list somebody keeps by hand would go stale the first time a package brings a new maker.',",
99
+ ' version: 1,',
100
+ ' derives: [',
101
+ ...rows,
102
+ ' ],',
103
+ '}',
104
+ '',
105
+ ].join('\n')
106
+ }
107
+
108
+ const HERE = (): string => cwd()
109
+
110
+ const COINS = /by:\s*'thread create ([a-z][a-z0-9-]*)'/g
111
+
112
+ const ANSWERS_AT = new RegExp(`at:\\s*'node_modules/${ORGANISATION}/[^/]+/(dist/[^']+)'`, 'g')
113
+
114
+ function ownMakers(root: string): ReadonlyMap<string, string> {
115
+ const found = new Map<string, string>()
116
+
117
+ let names: readonly string[]
118
+ try {
119
+ names = readdirSync(join(root, 'src'), { recursive: true }) as readonly string[]
120
+ } catch {
121
+ return found
122
+ }
123
+
124
+ for (const one of names) {
125
+ const path = String(one).split('\\').join('/')
126
+ if (!path.endsWith('attach.ts')) continue
127
+
128
+ let text: string
129
+ try {
130
+ text = readFileSync(join(root, 'src', String(one)), 'utf8')
131
+ } catch {
132
+ continue
133
+ }
134
+
135
+ for (const row of text.split(/\{\s*$/m)) {
136
+ const noun = [...row.matchAll(COINS)].at(0)?.[1]
137
+ const at = [...row.matchAll(ANSWERS_AT)].at(-1)?.[1]
138
+ if (noun === undefined || at === undefined || found.has(noun)) continue
139
+ found.set(noun, join(root, at))
140
+ }
141
+ }
142
+
143
+ return found
144
+ }
145
+
146
+ function printedOwn(at: string): { readonly refused: string; readonly body: string } {
147
+ const ran = spawnSync(execPath, ['--experimental-strip-types', at, '--own'], {
148
+ encoding: 'utf8',
149
+ stdio: ['ignore', 'pipe', 'pipe'],
150
+ })
151
+ const body = String(ran.stdout ?? '').replace(CRLF, '\n').replace(RECEIPT_LINE, '').trimEnd()
152
+ const said = String(ran.stderr ?? '').replace(CRLF, '\n').trim().split('\n')[0] ?? ''
153
+ return { refused: ran.status === 0 && body !== '' ? '' : said || 'it printed nothing', body }
154
+ }
155
+
156
+ async function run(): Promise<number> {
157
+ const own = argv.includes('--own')
158
+ const checking = argv.includes('--check')
159
+
160
+ const root = own ? HERE() : (findTree() as Tree).at
161
+
162
+ const mine = own ? ownMakers(root) : new Map<string, string>()
163
+ const theirs = own ? new Map<string, string>() : await offering(findTree() as Tree)
164
+ const known = [...(own ? mine : theirs).keys()].sort()
165
+
166
+ if (known.length === 0) {
167
+ stdout.write(
168
+ own
169
+ ? 'skills: this package ships no maker, so it coins no noun and has no skill to write.\n'
170
+ : 'skills: this tree declares no `create` command yet, so there is no method to write down. ' +
171
+ `Attach a package that brings one — \`npx thread attach ${named('ui')}\` — and run this again.\n`,
172
+ )
173
+ return 0
174
+ }
175
+
176
+ const wanted = new Map<string, string>()
177
+ const skipped: string[] = []
178
+ for (const noun of known) {
179
+ const { refused, body } = own ? printedOwn(mine.get(noun) ?? '') : printed(root, theirs.get(noun) ?? '', noun)
180
+ if (refused !== '') {
181
+ skipped.push(` ${noun} — ${refused}`)
182
+ continue
183
+ }
184
+ wanted.set(fileFor(noun), skill(noun, body))
185
+ }
186
+
187
+ if (wanted.size === 0) {
188
+ stdout.write(
189
+ `skills: every \`create\` this tree declares refused, so there is no method to write down yet:\n${skipped.join('\n')}\n`,
190
+ )
191
+ return 1
192
+ }
193
+
194
+ const made = wanted.size
195
+ const sheetAt = own ? '' : SHEET
196
+ if (sheetAt !== '') wanted.set(sheetAt, sheet([...wanted.keys()].sort()))
197
+
198
+ const held = new Map<string, string>()
199
+ try {
200
+ for (const name of readdirSync(join(root,WHERE))) {
201
+ if (!name.endsWith('.md')) continue
202
+ held.set(join(WHERE, name), readFileSync(join(root,WHERE, name), 'utf8').replace(CRLF, '\n'))
203
+ }
204
+ } catch {
205
+
206
+ }
207
+
208
+ if (sheetAt !== '') {
209
+ try {
210
+ held.set(sheetAt, readFileSync(join(root, sheetAt), 'utf8').replace(CRLF, '\n'))
211
+ } catch {
212
+
213
+ }
214
+ }
215
+
216
+ const stale = [...held.keys()].filter((one) => !wanted.has(one)).sort()
217
+ const differs = [...wanted.entries()].filter(([one, text]) => held.get(one) !== text).map(([one]) => one).sort()
218
+
219
+ if (checking) {
220
+ if (stale.length === 0 && differs.length === 0) {
221
+ stdout.write(
222
+ `skills: ${String(made)} skill(s), each matching what its own command prints` +
223
+ (sheetAt === '' ? '' : `, and each declared by a row in ${sheetAt}`) +
224
+ '.\n',
225
+ )
226
+ return 0
227
+ }
228
+ for (const one of differs) {
229
+ stdout.write(
230
+ `skills: ${one} is not what \`npx thread skills\` writes. A skill is derived from the command ` +
231
+ 'it describes, so an edit here is a second statement of the method and will drift from the ' +
232
+ 'first. Change the command, then run `npx thread skills`.\n',
233
+ )
234
+ }
235
+ for (const one of stale) {
236
+ stdout.write(
237
+ `skills: ${one} names a \`create\` this tree no longer declares, so it teaches a command that ` +
238
+ 'will refuse. Run `npx thread skills`.\n',
239
+ )
240
+ }
241
+ return 1
242
+ }
243
+
244
+ for (const [one, text] of wanted) {
245
+ mkdirSync(dirname(join(root,one)), { recursive: true })
246
+ writeFileSync(join(root,one), text, 'utf8')
247
+ }
248
+ for (const one of stale) rmSync(join(root,one), { force: true })
249
+
250
+ stdout.write(
251
+ `skills: wrote ${String(made)} skill(s) into ${WHERE}/` +
252
+ (sheetAt === '' ? '' : `, and the ${String(made)} row(s) declaring them into ${sheetAt}`) +
253
+ (stale.length === 0
254
+ ? ''
255
+ : `, and removed ${String(stale.length)} naming a command this tree no longer has`) +
256
+ '.\n' +
257
+
258
+ (skipped.length === 0 ? '' : `skills: no method to keep yet for:\n${skipped.join('\n')}\n`),
259
+ )
260
+ return 0
261
+ }
262
+
263
+ if (invoked(import.meta.url)) exit(await run())
@@ -0,0 +1,6 @@
1
+ export interface Glossary {
2
+ readonly id: string
3
+ readonly under: string
4
+ readonly from: readonly string[]
5
+ readonly means: string
6
+ }
@@ -0,0 +1,88 @@
1
+ import type { Actor } from '../core/actor.ts'
2
+ import { mentionsActor, walkOperands, type Condition } from '../core/condition.ts'
3
+ import type { Member } from '../core/thread.ts'
4
+
5
+ export const PERMITS_AT = 'src/_generated/permits.ts'
6
+
7
+ export function declaredWords(
8
+ entities: readonly {
9
+ readonly holds: readonly { readonly id: string; readonly options?: readonly string[] }[]
10
+ readonly actor?: { readonly roles: string; readonly entitlements?: string }
11
+ }[],
12
+ which: 'roles' | 'entitlements',
13
+ everybody: string,
14
+ ): readonly string[] {
15
+ const found = entities.flatMap((one) => {
16
+ const at = one.actor?.[which]
17
+ return at === undefined ? [] : (one.holds.find((each) => each.id === at)?.options ?? [])
18
+ })
19
+ return [...new Set(which === 'roles' ? [everybody, ...found] : found)]
20
+ }
21
+
22
+ export function undeclared(answered: string, offers: readonly string[], which: 'roles' | 'entitlements'): string | undefined {
23
+ const stray = answered
24
+ .split(',')
25
+ .map((one) => one.trim())
26
+ .filter((one) => one !== '' && !offers.includes(one))
27
+ if (stray.length === 0) return undefined
28
+
29
+ return (
30
+ `this app declares no ${which === 'roles' ? 'role' : 'entitlement'} called ${stray.map((one) => `\`${one}\``).join(', ')} — ` +
31
+ (offers.length === 0
32
+ ? `it declares none at all. A ${which === 'roles' ? 'role' : 'entitlement'} is one of the words the value an ` +
33
+ 'entity names as saying what a caller may do is held to. Declare them with `thread create entity edit` — ' +
34
+ 'name that value, give it the `choice` format, and state its options'
35
+ : `it declares ${offers.map((one) => `\`${one}\``).join(', ')}`)
36
+ )
37
+ }
38
+
39
+ export interface Grant extends Member {
40
+
41
+ readonly may: readonly string[]
42
+
43
+ readonly on?: string
44
+
45
+ readonly to?: {
46
+ readonly roles?: Actor['roles']
47
+ readonly entitlements?: Actor['entitlements']
48
+ }
49
+
50
+ readonly when?: Condition
51
+ }
52
+
53
+ export function opensToEverybody(grants: readonly Grant[]): readonly Grant[] {
54
+ return grants.filter((one) => one.to === undefined && (one.when === undefined || !mentionsActor(one.when)))
55
+ }
56
+
57
+ export function grantedTwice(grants: readonly Grant[]): readonly string[] {
58
+ const seen = new Set<string>()
59
+ const twice = new Set<string>()
60
+ for (const one of grants) {
61
+ if (seen.has(one.id)) twice.add(one.id)
62
+ seen.add(one.id)
63
+ }
64
+ return [...twice].sort()
65
+ }
66
+
67
+ function operandsOf(one: Grant): readonly string[] {
68
+ if (one.when === undefined) return []
69
+ const found: string[] = []
70
+ walkOperands(one.when, (each) => {
71
+ if ('agg' in each) found.push('agg')
72
+ if ('was' in each) found.push('was')
73
+ if ('outer' in each) found.push('outer')
74
+ })
75
+ return found
76
+ }
77
+
78
+ export function unanswerable(grants: readonly Grant[]): readonly string[] {
79
+ return grants.flatMap((one) => {
80
+ const held = [...new Set(operandsOf(one))]
81
+ if (held.length === 0) return []
82
+ const why = held.includes('agg')
83
+ ? 'it counts other rows, and a gate is asked about the one in front of it. Put the aggregate in an invariant on the entity, where it runs against the rows themselves'
84
+ : 'it reads a value as it was before the write, and at the moment this is asked the write has not happened. Only an invariant, which runs on the write itself, can see a before'
85
+ return [`\`${one.id}\` states a condition this gate cannot answer — ${why}`]
86
+ })
87
+ }
88
+