@ceccec/millennium-solutions 0.1.1 → 9.0.4
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/.zenodo.json +33 -9
- package/ACCOUNTING.md +7 -7
- package/AXIOMS.md +559 -0
- package/CHALLENGES.md +2181 -2479
- package/CITATION.cff +1 -1
- package/DEPLOY.md +2 -2
- package/DEVELOP.md +1 -1
- package/LICENSE +6 -5
- package/PRIOR-ART.md +149 -0
- package/README.md +221 -123
- package/SIGNATURE.md +2 -2
- package/TRIAL.md +69 -0
- package/WHITEPAPER.md +5 -5
- package/captain.md +2 -2
- package/compare.md +4 -4
- package/compute.md +9 -0
- package/dashboard.md +1 -1
- package/examples.md +5 -5
- package/forensic.md +67 -0
- package/guide.md +5 -5
- package/index.md +209 -139
- package/package.json +88 -9
- package/paper.md +7183 -0
- package/quantum.md +42 -0
- package/rights.md +39 -0
- package/solutions.md +7 -7
- package/speedup.md +4 -4
- package/src/0/index.ts +12 -2
- package/src/1/index.ts +1 -1
- package/src/2/frequency-scales.ts +13 -1
- package/src/2/index.ts +1 -1
- package/src/3/index.ts +1 -1
- package/src/4/index.ts +1 -1
- package/src/5/index.ts +1 -1
- package/src/6/index.ts +1 -1
- package/src/7/index.ts +1 -1
- package/src/9/funding.ts +2 -2
- package/src/api/gates.ts +117 -0
- package/src/api/index.ts +488 -0
- package/src/api/lanes.ts +81 -0
- package/src/demand/queries.json +1764 -0
- package/src/face/index.ts +100 -0
- package/src/html/index.ts +10 -0
- package/src/latex/index.ts +474 -0
- package/src/millennium/index.ts +53 -0
- package/src/proof/README.md +1 -1
- package/src/proof/address.lean +117 -0
- package/src/proof/coin.lean +148 -0
- package/src/proof/covered.json +9 -0
- package/src/proof/demand.lean +112 -0
- package/src/proof/demand2.lean +171 -0
- package/src/proof/demand3.lean +117 -0
- package/src/proof/discovered.json +10965 -1864
- package/src/proof/elementary.lean +393 -0
- package/src/proof/energy.lean +215 -0
- package/src/proof/families.lean +435 -0
- package/src/proof/fixtures/axiom-control.lean +39 -0
- package/src/proof/fnv.lean +99 -0
- package/src/proof/generated-theorems.json +207 -0
- package/src/proof/generated.lean +101 -0
- package/src/proof/imagined.lean +492 -0
- package/src/proof/index.lean +98 -18
- package/src/proof/index.ts +2 -2
- package/src/proof/involution.lean +90 -0
- package/src/proof/ledgerclaims.lean +84 -0
- package/src/proof/light.lean +135 -0
- package/src/proof/mechanical.lean +421 -0
- package/src/proof/merkaba.lean +89 -0
- package/src/proof/merkle.lean +132 -0
- package/src/proof/nim.lean +105 -0
- package/src/proof/phenomena.lean +90 -0
- package/src/proof/priorart.lean +189 -0
- package/src/proof/quantum.lean +161 -0
- package/src/proof/reach.lean +84 -0
- package/src/proof/recovered.lean +56 -0
- package/src/proof/reversal.lean +73 -0
- package/src/proof/rights.lean +157 -0
- package/src/proof/sequences.lean +105 -0
- package/src/proof/speed.lean +129 -0
- package/src/proof/split.lean +180 -0
- package/src/proof/theorems.lean +48 -1
- package/src/proof/trial-all.json +15260 -0
- package/src/proof/z9.lean +92 -0
- package/src/proof/z9plus.lean +208 -0
- package/src/proofs.lisp +3 -3
- package/src/prove/emit.ts +115 -0
- package/src/prove/index.ts +143 -0
- package/src/prove/translate.ts +638 -0
- package/src/publication/index.ts +363 -0
- package/src/quantum/field.ts +73 -0
- package/src/quantum/tree.ts +79 -0
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// THE REUSABLE API — one place that knows how to read this deposit.
|
|
2
|
+
//
|
|
3
|
+
// Thirty-two scripts loaded src/proof/discovered.json themselves and nine parsed Lean theorem names with
|
|
4
|
+
// their own regex. That is not a style problem: every one of those copies is a place where the deposit can
|
|
5
|
+
// start disagreeing with itself, and this session found several that had. forensics counted key families by
|
|
6
|
+
// splitting key TEXT and reported 377 development leads that did not exist; seal-lean matched whole keys and
|
|
7
|
+
// called 25 living theorems orphans; verify read a foreign ledger and reported every entry unproven. Each was
|
|
8
|
+
// a private answer to a question this module now answers once.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here decides anything. It reads artefacts and returns them typed — the ledger, the Lean sources,
|
|
11
|
+
// and the relation between a sealed key and the theorem on disk that carries it. Judgement stays in the gates
|
|
12
|
+
// that own it.
|
|
13
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
|
|
14
|
+
|
|
15
|
+
export const LEDGER_PATH = 'src/proof/discovered.json'
|
|
16
|
+
export const PROOF_DIR = 'src/proof'
|
|
17
|
+
|
|
18
|
+
/** THE RECORD HAS FOUR STATES, NOT TWO. `revoked` was carrying two different meanings: a claim that stopped
|
|
19
|
+
* holding, and a claim that was withdrawn for want of a proof and has since been given one. Reporting the
|
|
20
|
+
* second as merely withdrawn understates the record — 113 entries whose statement the kernel now checks were
|
|
21
|
+
* being counted with the 1778 that nobody proved.
|
|
22
|
+
*
|
|
23
|
+
* Nothing is un-revoked. The original entry's own evidence is still a TypeScript test, and rewriting its
|
|
24
|
+
* status would erase the fact that it did not hold on what it had. What changes is that the record can now
|
|
25
|
+
* say both things at once: withdrawn on its own evidence, and STANDING through the theorem that carries it.
|
|
26
|
+
*
|
|
27
|
+
* standing — live, kernel-checked, nothing withdrawn
|
|
28
|
+
* carried — withdrawn on its own evidence, and its statement is proved by a live theorem
|
|
29
|
+
* withdrawn — withdrawn, and nothing proves it
|
|
30
|
+
* (revoked) — the raw flag, kept because the chain and every receipt were written against it
|
|
31
|
+
*/
|
|
32
|
+
export type Status = 'standing' | 'carried' | 'withdrawn'
|
|
33
|
+
|
|
34
|
+
export interface Entry {
|
|
35
|
+
key: string
|
|
36
|
+
name: string
|
|
37
|
+
receipt: string
|
|
38
|
+
revoked?: boolean
|
|
39
|
+
reason?: string
|
|
40
|
+
supersededBy?: string
|
|
41
|
+
portable?: boolean
|
|
42
|
+
statement?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The whole record, in order. Append-only: withdrawn entries are present and marked, never removed.
|
|
46
|
+
*
|
|
47
|
+
* CACHED ON THE FILE'S IDENTITY, because it was read 4252 times in a single `pages.ts` run. Every helper
|
|
48
|
+
* below defaults its parameter to `ledger()`, so `live()`, `liveKeys()`, `byKey()` and `statusOf()` each
|
|
49
|
+
* re-read and re-parsed the whole 2418-entry file when called without one — and called inside a loop, that
|
|
50
|
+
* is megabytes of JSON parsed thousands of times. A CPU profile put 7.1s of pages.ts's 16s in this function
|
|
51
|
+
* and 7.5s in readFileSync beneath it; the site build is 80% of a deploy and this was 45% of its pre-build.
|
|
52
|
+
*
|
|
53
|
+
* The key is mtime+size, NOT a plain memo. seal-lean.ts and carry.ts WRITE this file and then read it back,
|
|
54
|
+
* and a cache that ignored that would serve them the record as it was before their own append — which in an
|
|
55
|
+
* append-only ledger is the one stale read that could cause real damage. A statSync per call is the price,
|
|
56
|
+
* and it is a rounding error against a parse. */
|
|
57
|
+
let _lcache: { key: string; rows: Entry[] } | null = null
|
|
58
|
+
export const ledger = (): Entry[] => {
|
|
59
|
+
if (!existsSync(LEDGER_PATH)) return []
|
|
60
|
+
const st = statSync(LEDGER_PATH)
|
|
61
|
+
const key = `${st.mtimeMs}:${st.size}`
|
|
62
|
+
if (_lcache && _lcache.key === key) return _lcache.rows
|
|
63
|
+
const rows = JSON.parse(readFileSync(LEDGER_PATH, 'utf8')) as Entry[]
|
|
64
|
+
_lcache = { key, rows }
|
|
65
|
+
return rows
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The entries that STAND. Everything that judges what the deposit currently claims wants this one. */
|
|
69
|
+
export const live = (l: Entry[] = ledger()): Entry[] => l.filter((e) => !e.revoked)
|
|
70
|
+
|
|
71
|
+
/** Withdrawn but kept — still receipted, still in the chain, no longer claimed. */
|
|
72
|
+
export const withdrawn = (l: Entry[] = ledger()): Entry[] => l.filter((e) => e.revoked === true)
|
|
73
|
+
|
|
74
|
+
/** Withdrawn AND since re-established by a Lean theorem, which the record links rather than un-revoking. */
|
|
75
|
+
export const superseded = (l: Entry[] = ledger()): Entry[] => l.filter((e) => e.supersededBy)
|
|
76
|
+
|
|
77
|
+
export const liveKeys = (l: Entry[] = ledger()): Set<string> => new Set(live(l).map((e) => e.key))
|
|
78
|
+
export const byKey = (l: Entry[] = ledger()): Map<string, Entry> => new Map(l.map((e) => [e.key, e]))
|
|
79
|
+
|
|
80
|
+
/** The octave reading — the deposit counts in eights. A target the theorems earn, never a quota. */
|
|
81
|
+
export const octave = (l: Entry[] = ledger()) => ({
|
|
82
|
+
total: l.length, octaves: Math.floor(l.length / 8), remainder: l.length % 8, exact: l.length % 8 === 0,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
export interface LeanTheorem { name: string; file: string; tactic: string; statement: string; namespace: string }
|
|
86
|
+
|
|
87
|
+
export const leanFiles = (): string[] =>
|
|
88
|
+
existsSync(PROOF_DIR) ? readdirSync(PROOF_DIR).filter((f) => f.endsWith('.lean')).sort() : []
|
|
89
|
+
|
|
90
|
+
export const leanSource = (file: string): string => readFileSync(`${PROOF_DIR}/${file}`, 'utf8')
|
|
91
|
+
|
|
92
|
+
/** Every theorem on disk, with the file that carries it and the tactic that closed it. ONE parse of the
|
|
93
|
+
* `theorem` syntax, so a change to how proofs are written is a change in one place. */
|
|
94
|
+
/** A theorem's statement text, with comments removed and whitespace flattened.
|
|
95
|
+
*
|
|
96
|
+
* A TRAILING COMMENT IS NOT PART OF THE FORMULA. Stripping only whole-line comments (`^\s*--`) left a
|
|
97
|
+
* comment that trailed a CONTINUED line embedded in the statement, and the page then printed
|
|
98
|
+
* `... == d) -- σ ∘ σ = id everywhere — the shared involution ∧ ((List.range 10)...` — English spliced
|
|
99
|
+
* between two conjuncts, published as mathematics. A comment runs to end of line wherever it starts. */
|
|
100
|
+
export const normalizeStatement = (raw: string): string =>
|
|
101
|
+
raw.split('\n').map((l) => l.replace(/(^|\s)--.*$/, '')).join(' ').replace(/\s+/g, ' ').trim()
|
|
102
|
+
|
|
103
|
+
export const leanTheorems = (): LeanTheorem[] => {
|
|
104
|
+
const out: LeanTheorem[] = []
|
|
105
|
+
for (const file of leanFiles()) {
|
|
106
|
+
const src = leanSource(file)
|
|
107
|
+
const ns = src.match(/^namespace\s+([A-Za-z_0-9.]+)/m)?.[1] ?? file.replace('.lean', '')
|
|
108
|
+
for (const m of src.matchAll(/^theorem\s+([A-Za-z_0-9]+)\s*:([\s\S]*?):=\s*(by decide|rfl|by\s+\w+)/gm))
|
|
109
|
+
out.push({ name: m[1], file, namespace: ns, tactic: m[3], statement: normalizeStatement(m[2]) })
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Which file carries the theorem a sealed key was minted from.
|
|
115
|
+
*
|
|
116
|
+
* Keys carry their naming HISTORY: older entries are `lean_<theorem>` and current ones are
|
|
117
|
+
* `lean_<namespace>_<theorem>`. Comparing whole keys called 25 living theorems orphans, and splitting key
|
|
118
|
+
* text invented a family per theorem. Matching on the theorem IDENTIFIER is what survives both conventions,
|
|
119
|
+
* and it is the only comparison any caller should be making. */
|
|
120
|
+
/** The domain `by decide` actually walked — the count of cases the kernel exhausted, read off the
|
|
121
|
+
* statement. One definition: scripts/leandoc.ts and scripts/pages.ts each had their own copy of this,
|
|
122
|
+
* byte-identical, so the number a theorem page prints and the number the front pages rank by could
|
|
123
|
+
* drift apart on the next edit to either. */
|
|
124
|
+
export const domainOf = (statement: string): number => {
|
|
125
|
+
let n = 1
|
|
126
|
+
for (const m of statement.matchAll(/List\.range'\s+\d+\s+(\d+)/g)) n *= Number(m[1])
|
|
127
|
+
for (const m of statement.matchAll(/List\.range\s+(\d+)/g)) n *= Number(m[1])
|
|
128
|
+
for (const m of statement.matchAll(/\[([0-9,\s]+)\]/g)) n *= Math.max(1, m[1].split(',').filter((x) => x.trim()).length)
|
|
129
|
+
return n
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** How many theorems there are, and how many ledger keys name them — which are NOT the same number.
|
|
133
|
+
*
|
|
134
|
+
* The deposit published its live-key count as though it were a count of theorems. It is not: src/proof holds
|
|
135
|
+
* the theorems; the ledger once held more live keys than that, because 24 carried two — one minted
|
|
136
|
+
* before keys had a namespace (`lean_units_are_six`) and one after (`lean_z9_units_are_six`). Both were live
|
|
137
|
+
* and both are legitimate history, but counting a key as a theorem overstates the deposit. Those 24 second
|
|
138
|
+
* addresses were retired in favour of their namespaced form, so no theorem now carries two. The 8 `rfl`
|
|
139
|
+
* DECLARATIONS have no key at all, because seal-lean.ts seals `by decide` only — and they are declarations,
|
|
140
|
+
* never theorems, by the definition this file states.
|
|
141
|
+
*
|
|
142
|
+
* Lean decides what a theorem is. Every count of theorems is taken from src/proof; a count of keys is
|
|
143
|
+
* called a count of keys. */
|
|
144
|
+
export interface Census {
|
|
145
|
+
theorems: number; byDecide: number; rfl: number
|
|
146
|
+
liveKeys: number; sealedTheorems: number; surplusKeys: number; unresolvableKeys: number; unsealed: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** THE CLAY FLOOR, MEASURED OVER THE TREE — not certified by a constant.
|
|
150
|
+
*
|
|
151
|
+
* index.lean used to carry `def provenHere : Nat := 0` with `the_floor_is_zero_of_seven := rfl` beside it,
|
|
152
|
+
* and that literal glued as a conjunct onto every theorem. The repo's own trial already said what was wrong
|
|
153
|
+
* with it (finding seventeen): "closed by reflexivity on a constant this file declares … written as seven it
|
|
154
|
+
* would be equally green, and therefore it supports neither number." A counter the author maintains is an
|
|
155
|
+
* arbiter the author writes.
|
|
156
|
+
*
|
|
157
|
+
* An absence is not established by a certificate. It is established by there being no proof that reaches —
|
|
158
|
+
* a property of the tree, checkable over the tree, and REFUTABLE: add one theorem quantifying over ℝ, or
|
|
159
|
+
* naming the ζ-zeros, and this stops holding. That is what makes it worth running.
|
|
160
|
+
*
|
|
161
|
+
* It measures three things about the Clay-named theorems: that all seven are present and closed by `decide`;
|
|
162
|
+
* the largest finite domain any of them walks; and whether any statement reaches for an object the
|
|
163
|
+
* conjectures actually concern. */
|
|
164
|
+
export interface ClayFloor {
|
|
165
|
+
seven: number; inFile: number; allByDecide: boolean; largestDomain: number; reaches: string[]; holds: boolean
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The objects the seven conjectures are about. A statement mentioning one of these is reaching past the
|
|
169
|
+
* finite algebra this deposit decides, which is exactly what must not happen silently. */
|
|
170
|
+
export const CONJECTURE_OBJECTS = [
|
|
171
|
+
'ℝ', 'ℂ', 'Real', 'Complex', 'zeta', 'critical line', 'polynomial time', 'viscosity', 'gauge',
|
|
172
|
+
'mass gap', 'quantum field', 'cohomolog', 'algebraic cycle', 'elliptic curve', 'l-function',
|
|
173
|
+
'manifold', 'homeomorph', '3-sphere',
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
const CLAY_NAMED = /riemann|p_vs_np|navier|yang|hodge|birch|poincare/
|
|
177
|
+
|
|
178
|
+
/** THE VERIFICATION ADVANTAGE, read off src/proof/speed.lean rather than retyped.
|
|
179
|
+
*
|
|
180
|
+
* The deposit proves this and does not say it where anyone reads: measured over the front pages, four of
|
|
181
|
+
* eight proved properties were stated, and the four missing ones were the whole advantage. A boundary that
|
|
182
|
+
* is stated loudly while the result is left in a source file is not modesty, it is a page that misinforms
|
|
183
|
+
* in the other direction.
|
|
184
|
+
*
|
|
185
|
+
* Everything here comes from the `def`s the theorems in that file decide over, so the page and the kernel
|
|
186
|
+
* cannot disagree, and none of it is a number typed into prose. */
|
|
187
|
+
export interface Advantage {
|
|
188
|
+
recomputeUs: number; verifyUs: number; nsPerVerify: number; leaves: number; rounds: number; ratio: number
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const advantage = (): Advantage => {
|
|
192
|
+
const src = leanSource('speed.lean')
|
|
193
|
+
const num = (name: string): number => {
|
|
194
|
+
const m = src.match(new RegExp('def\\s+' + name + '\\s*:\\s*Nat\\s*:=\\s*(\\d+)'))
|
|
195
|
+
if (!m) throw new Error('speed.lean no longer defines ' + name + ' — the advantage cannot be reported without it')
|
|
196
|
+
return Number(m[1])
|
|
197
|
+
}
|
|
198
|
+
const recomputeUs = num('recomputeUs'), verifyUs = num('verifyUs'), nsPerVerify = num('nsPerVerify')
|
|
199
|
+
const leaves = 2 ** 20
|
|
200
|
+
return { recomputeUs, verifyUs, nsPerVerify, leaves, rounds: 20, ratio: Math.floor(recomputeUs / verifyUs) }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const clayFloor = (): ClayFloor => {
|
|
204
|
+
// EVERY THEOREM IN THE CLAY-NAMED FILE, not only the seven that carry a problem's name. index.lean holds
|
|
205
|
+
// eight: the seven, and `the_seven_rest_on_one_finite_structure`, the involution they share. Scanning only
|
|
206
|
+
// the name-matched seven left that eighth unmeasured — it could have reached for a conjecture object and
|
|
207
|
+
// this check would not have seen it, which is the hole the narrow filter opened.
|
|
208
|
+
const inFile = leanTheorems().filter((t) => t.file === 'index.lean')
|
|
209
|
+
const seven = inFile.filter((t) => CLAY_NAMED.test(t.name))
|
|
210
|
+
const text = inFile.map((t) => t.statement).join(' ')
|
|
211
|
+
const reaches = CONJECTURE_OBJECTS.filter((o) => new RegExp(o.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(text))
|
|
212
|
+
let largest = 0
|
|
213
|
+
for (const t of inFile) largest = Math.max(largest, domainOf(t.statement))
|
|
214
|
+
const allByDecide = inFile.length > 0 && inFile.every((t) => t.tactic === 'by decide')
|
|
215
|
+
return {
|
|
216
|
+
seven: seven.length,
|
|
217
|
+
inFile: inFile.length,
|
|
218
|
+
allByDecide,
|
|
219
|
+
largestDomain: largest,
|
|
220
|
+
reaches,
|
|
221
|
+
holds: seven.length === 7 && allByDecide && reaches.length === 0,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** WHAT A THEOREM OF THIS DEPOSIT IS — one definition, because the word was carrying four.
|
|
226
|
+
*
|
|
227
|
+
* Asked for the exact definition, the deposit could produce four different numbers, and all four were in
|
|
228
|
+
* use in its own pages:
|
|
229
|
+
*
|
|
230
|
+
* a `theorem` declaration the kernel accepts — sorry-free, axiom-free, in src/proof
|
|
231
|
+
* of those, closed by EXHAUSTION (`by decide`) — the kernel evaluated the proposition at every point
|
|
232
|
+
* a live key in the ledger — an ADDRESS; 0 theorems carry two, one resolves to none
|
|
233
|
+
* an entry in the append-only ledger — a RECEIPT, and most of them are withdrawn
|
|
234
|
+
*
|
|
235
|
+
* The repo's own rule already chose, in seal-lean.ts: "`by decide` is algebra — the kernel evaluates the
|
|
236
|
+
* proposition over its whole finite domain and the result is a computation, not a convention. `rfl` on a
|
|
237
|
+
* declared constant proves the declaration and nothing else — it is not evidence." That is why seal-lean
|
|
238
|
+
* seals `by decide` and refuses `rfl`, and it is the line taken here.
|
|
239
|
+
*
|
|
240
|
+
* A THEOREM OF THIS DEPOSIT is a declaration in src/proof that the Lean kernel accepts, free of `sorry`
|
|
241
|
+
* and of axioms, and CLOSED BY EXHAUSTION over a stated finite domain. A declaration closed by `rfl` is a
|
|
242
|
+
* definitional unfolding and is counted separately, never as a theorem. A key is an address and an entry
|
|
243
|
+
* is a receipt; neither is a theorem, and scripts/contradictions.ts fails the build when either count is
|
|
244
|
+
* printed next to the word. */
|
|
245
|
+
export const THEOREM_DEFINITION =
|
|
246
|
+
'a declaration in src/proof that the Lean kernel accepts — sorry-free, axiom-free — and closes by exhaustion over a stated finite domain'
|
|
247
|
+
|
|
248
|
+
/** The count that definition yields. Everything reporting "theorems" should read this. */
|
|
249
|
+
export const theoremCount = (): number => census().byDecide
|
|
250
|
+
|
|
251
|
+
/** THE DIGIT SPLIT, read off src/proof/split.lean rather than retyped.
|
|
252
|
+
*
|
|
253
|
+
* Measured after the theorems were written: not one of them reached the front pages. They were in the
|
|
254
|
+
* collected paper — a hundred pages — and on their own theorem pages, and a reader of the homepage saw
|
|
255
|
+
* none of it, including the answer to what paying the two coins buys. Proved and unsaid is the underclaim
|
|
256
|
+
* this deposit had no guard against until the inverse ratchet.
|
|
257
|
+
*
|
|
258
|
+
* Every value comes from the `def`s the kernel decides over in that file, so the page cannot drift from
|
|
259
|
+
* the theorems and nothing here is a number typed into prose. */
|
|
260
|
+
export interface Split {
|
|
261
|
+
tokens: number[]; singles: number[]; coins: number; coinStep: number; sealBits: number
|
|
262
|
+
exhaustible: number[]; halting: number[]; payments: number; orbitPeriod: number
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const split = (): Split => {
|
|
266
|
+
const src = leanSource('split.lean')
|
|
267
|
+
const list = (name: string): number[] => {
|
|
268
|
+
const m = src.match(new RegExp('def\\s+' + name + '\\s*:\\s*List Nat\\s*:=\\s*\\[([^\\]]*)\\]'))
|
|
269
|
+
if (!m) throw new Error('split.lean no longer defines ' + name)
|
|
270
|
+
return m[1].split(',').map((x) => Number(x.trim()))
|
|
271
|
+
}
|
|
272
|
+
const num = (name: string): number => {
|
|
273
|
+
// A plain numeral only. `eval` on a source file is how a build starts executing whatever the file
|
|
274
|
+
// happens to contain, and reading a constant does not need it.
|
|
275
|
+
const m = src.match(new RegExp('def\\s+' + name + '\\s*:\\s*Nat\\s*:=\\s*(\\d+)'))
|
|
276
|
+
if (!m) throw new Error('split.lean no longer defines ' + name + ' as a plain numeral')
|
|
277
|
+
return Number(m[1])
|
|
278
|
+
}
|
|
279
|
+
const tokens = list('tokens'), singles = list('singles')
|
|
280
|
+
const coins = num('coins'), coinStep = 3 * coins, sealBits = num('sealBits')
|
|
281
|
+
return {
|
|
282
|
+
tokens, singles, coins, coinStep, sealBits,
|
|
283
|
+
exhaustible: tokens.filter((t) => t % coinStep === 0),
|
|
284
|
+
halting: tokens.filter((t) => t % coinStep !== 0),
|
|
285
|
+
payments: sealBits / coins,
|
|
286
|
+
orbitPeriod: 6,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const census = (): Census => {
|
|
291
|
+
const T = leanTheorems()
|
|
292
|
+
const keys = (live() as { key: string }[]).filter((e) => e.key.startsWith('lean_')).map((e) => e.key)
|
|
293
|
+
const named = new Set<string>()
|
|
294
|
+
let matched = 0
|
|
295
|
+
for (const k of keys) {
|
|
296
|
+
const t = theoremOfKey(k, T)
|
|
297
|
+
if (t) { named.add(t.file + '::' + t.name); matched++ }
|
|
298
|
+
}
|
|
299
|
+
// The arithmetic closes exactly, and is asserted in scripts/contradictions.ts:
|
|
300
|
+
// liveKeys = sealedTheorems + surplusKeys + unresolvableKeys
|
|
301
|
+
// The unresolvable one is `lean_add_group`, whose name is declared in two files and whose key predates
|
|
302
|
+
// namespaces, so no single theorem can be said to be the one it was minted from.
|
|
303
|
+
return {
|
|
304
|
+
theorems: T.length,
|
|
305
|
+
byDecide: T.filter((t) => t.tactic === 'by decide').length,
|
|
306
|
+
rfl: T.filter((t) => t.tactic === 'rfl').length,
|
|
307
|
+
liveKeys: keys.length,
|
|
308
|
+
sealedTheorems: named.size,
|
|
309
|
+
surplusKeys: matched - named.size,
|
|
310
|
+
unresolvableKeys: keys.length - matched,
|
|
311
|
+
unsealed: T.length - named.size,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export const theoremOfKey = (key: string, thms: LeanTheorem[] = leanTheorems()): LeanTheorem | null => {
|
|
316
|
+
const rest = key.replace(/^lean_/, '')
|
|
317
|
+
const flat = (x: string) => x.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
318
|
+
|
|
319
|
+
// A NAME IS NOT AN IDENTIFIER WHEN IT IS NOT UNIQUE. `add_group` is declared in both mechanical.lean and
|
|
320
|
+
// z9.lean with DIFFERENT statements, so matching on the trailing name alone sent lean_z9_add_group to
|
|
321
|
+
// mechanical.lean — the page then printed a formula that was not the one its key was minted from. The
|
|
322
|
+
// namespace the key carries is what separates them, so it is tried first, exactly.
|
|
323
|
+
const named = thms.filter((t) => rest === t.name || rest.endsWith('_' + t.name) || rest.endsWith('.' + t.name))
|
|
324
|
+
if (named.length <= 1) return named[0] ?? null
|
|
325
|
+
for (const t of named) {
|
|
326
|
+
const prefix = rest.slice(0, rest.length - t.name.length).replace(/[_.]$/, '')
|
|
327
|
+
if (prefix && (flat(prefix) === flat(t.namespace) || flat(prefix) === flat(t.file.replace('.lean', '')))) return t
|
|
328
|
+
}
|
|
329
|
+
// Ambiguous: the name is shared and the key does not say which. Returning one at random is how the wrong
|
|
330
|
+
// statement got onto a page, so this returns nothing and the caller shows nothing.
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The file alone, for callers that only need to know where a key was minted from. Delegates to
|
|
335
|
+
* theoremOfKey so the two-convention matching above is written once and cannot drift between them. */
|
|
336
|
+
export const fileOfKey = (key: string, thms: LeanTheorem[] = leanTheorems()): string | null =>
|
|
337
|
+
theoremOfKey(key, thms)?.file ?? null
|
|
338
|
+
|
|
339
|
+
/** Frontmatter written as `-- key: value` at the head of a Lean file, before its prose. */
|
|
340
|
+
export const frontmatter = (file: string): Record<string, string> => {
|
|
341
|
+
const fm: Record<string, string> = {}
|
|
342
|
+
for (const line of leanSource(file).split('\n')) {
|
|
343
|
+
const m = line.match(/^\s*--\s*([a-z][a-z0-9_]*):\s*(.+?)\s*$/)
|
|
344
|
+
if (!m) { if (/^\s*--/.test(line)) continue; if (line.trim() === '' || /^(import|set_option)/.test(line)) continue; break }
|
|
345
|
+
fm[m[1]] = m[2]
|
|
346
|
+
}
|
|
347
|
+
return fm
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** THE TWO QUESTIONS, ASKED ONE WAY. Eight scripts spelled "does this entry stand" as their own inline
|
|
351
|
+
* predicate — `!e.revoked` here, `filter((e) => e.revoked)` there, `e.revoked === true` elsewhere. The
|
|
352
|
+
* predicates happened to agree, but agreement by coincidence is what a shared definition removes: a
|
|
353
|
+
* withdrawn entry that later grows a third state (superseded is already one) would need finding in eight
|
|
354
|
+
* places, and this session has twice watched a private copy answer differently from its siblings. */
|
|
355
|
+
export const isLive = (e: Entry): boolean => !e.revoked
|
|
356
|
+
|
|
357
|
+
/** The state of one entry, as the record can actually justify it.
|
|
358
|
+
*
|
|
359
|
+
* THE SUCCESSOR MUST ITSELF STAND. The first version of this read `e.supersededBy ? 'carried' : 'withdrawn'`
|
|
360
|
+
* — the mere PRESENCE of a forwarding key was taken as proof that the statement survives. Twenty-five entries
|
|
361
|
+
* forward to a key that is itself withdrawn, and every one of them was being reported as carried: the record
|
|
362
|
+
* claimed a live proof at the far end of a link that leads nowhere. CHALLENGES.md published those links, and
|
|
363
|
+
* the seal gate caught a page citing a withdrawn theorem, which is how this was found rather than shipped.
|
|
364
|
+
* A carry is a claim about where the proof is now, so it is only a carry when there is a proof there.
|
|
365
|
+
*
|
|
366
|
+
* AND THE LINK CAN BE MORE THAN ONE HOP. That fix stopped at the first heir, and three claims were reading
|
|
367
|
+
* as withdrawn while their proof was two links away: `add_group` forwards to `lean_add_group`, which was
|
|
368
|
+
* itself retired when the file-prefixed address format arrived and forwards to `lean_z9_add_group`, which
|
|
369
|
+
* is live and decides exactly the claim. The record said where the proof went, twice, and the reader
|
|
370
|
+
* stopped after the first sentence. Walking the chain does NOT weaken the check above — a chain ending at
|
|
371
|
+
* a revoked entry with nowhere further to go is still withdrawn, which is the whole content of "the
|
|
372
|
+
* successor must itself stand". A cycle or an implausibly long chain is withdrawn too: the record has
|
|
373
|
+
* stopped making sense at that point, and reporting a carry on it would be trusting a loop. */
|
|
374
|
+
export const CARRY_HOPS = 16
|
|
375
|
+
|
|
376
|
+
/** The key that actually holds the proof for a carried entry — the LIVE end of the forwarding chain, not
|
|
377
|
+
* the first link. CHALLENGES.md published the first link and the seal gate refused the page: `euler_units_pow6`
|
|
378
|
+
* forwards to `lean_euler_units_pow_six`, which is retired and forwards on to `lean_z9_euler_units_pow_six`.
|
|
379
|
+
* Citing the middle of a chain sends a reader to a page that says the theorem moved. One walk, used by both
|
|
380
|
+
* `statusOf` and every surface that prints where the proof went, so the status and the citation cannot
|
|
381
|
+
* disagree about which key that is. */
|
|
382
|
+
export const carrierOf = (e: Entry, l: Entry[] = ledger()): string | null => {
|
|
383
|
+
if (!e.revoked) return null
|
|
384
|
+
const seen = new Set<string>([e.key])
|
|
385
|
+
let cur = e
|
|
386
|
+
for (let hop = 0; hop < CARRY_HOPS; hop++) {
|
|
387
|
+
if (!cur.supersededBy || seen.has(cur.supersededBy)) return null
|
|
388
|
+
seen.add(cur.supersededBy)
|
|
389
|
+
const heir = l.find((x) => x.key === cur.supersededBy)
|
|
390
|
+
if (!heir) return null
|
|
391
|
+
if (!heir.revoked) return heir.key
|
|
392
|
+
cur = heir
|
|
393
|
+
}
|
|
394
|
+
return null
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const statusOf = (e: Entry, l: Entry[] = ledger()): Status => {
|
|
398
|
+
if (!e.revoked) return 'standing'
|
|
399
|
+
return carrierOf(e, l) ? 'carried' : 'withdrawn'
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Entries whose statement stands, whether by their own seal or through the theorem that carries it. This is
|
|
403
|
+
* the honest answer to "how much of this deposit is proved" — it is NOT the same as `live`, which counts
|
|
404
|
+
* only the entries that carry their own proof, and both numbers are worth reporting separately. */
|
|
405
|
+
export const carried = (l: Entry[] = ledger()): Entry[] => l.filter((e) => statusOf(e, l) === 'carried')
|
|
406
|
+
export const proved = (l: Entry[] = ledger()): Entry[] => l.filter((e) => statusOf(e, l) !== 'withdrawn')
|
|
407
|
+
export const isWithdrawn = (e: Entry): boolean => e.revoked === true
|
|
408
|
+
|
|
409
|
+
// ── THE ℤ/9 SETS, COMPUTED AND CHECKED AGAINST THE THEOREM THAT PROVES THEM ────────────────────────────────
|
|
410
|
+
//
|
|
411
|
+
// The tooling had these written out as literals — `[1,2,4,5,7,8]` for the units, `[3,6,0]` for the triad,
|
|
412
|
+
// `[1,2,4,8,7,5]` for the orbit — in imagine.ts, fold.ts and pages.ts. Every one of them is COMPUTED in
|
|
413
|
+
// src/0 and PROVED in src/proof, so the literal was a third copy that no gate was checking: change the
|
|
414
|
+
// modulus and the runtime and the kernel would both follow while the tooling silently kept describing ℤ/9.
|
|
415
|
+
//
|
|
416
|
+
// These compute the set and then refuse to return it unless the sealed theorem that proves it is live in the
|
|
417
|
+
// ledger. That is the whole point — not that the numbers are right today, but that the tooling cannot go on
|
|
418
|
+
// using them after the proof behind them stops standing.
|
|
419
|
+
import { units as runtimeUnits, triad as runtimeTriad, vortexOrbit as runtimeOrbit } from '../0/index.ts'
|
|
420
|
+
|
|
421
|
+
/** Is a live key addressing this THEOREM, whatever key it is filed under?
|
|
422
|
+
*
|
|
423
|
+
* The check used to require one exact key string, and retiring 24 duplicate addresses broke it instantly:
|
|
424
|
+
* `lean_units_are_six` was withdrawn in favour of `lean_z9_units_are_six`, the theorem never stopped being
|
|
425
|
+
* proved, and the API refused to serve the units. The guarantee is meant to be "a live theorem proves
|
|
426
|
+
* this", and a key is an ADDRESS — the same distinction that put 330 dead URLs in the deposition records
|
|
427
|
+
* this morning, here in the one place that decides whether the tooling may use a value at all.
|
|
428
|
+
*
|
|
429
|
+
* Resolving through theoremOfKey means the guarantee survives a rename or a re-address and still fails
|
|
430
|
+
* when the proof itself goes — which is what it was for. */
|
|
431
|
+
// MEMOISED, because the first version was O(live keys × theorems) PER CALL and units() is called from
|
|
432
|
+
// everywhere: it resolved every live key against every kernel-accepted declaration on each invocation
|
|
433
|
+
// and took `npm run ci:local` past ten minutes. The set of names reachable from a live key is built once,
|
|
434
|
+
// on the same mtime+size key the ledger cache uses, so a script that writes the ledger and reads it back
|
|
435
|
+
// still sees its own append.
|
|
436
|
+
let _provenCache: { key: string; names: Set<string> } | null = null
|
|
437
|
+
const provenNames = (): Set<string> => {
|
|
438
|
+
const st = existsSync(LEDGER_PATH) ? statSync(LEDGER_PATH) : null
|
|
439
|
+
const ck = st ? `${st.mtimeMs}:${st.size}` : 'none'
|
|
440
|
+
if (_provenCache && _provenCache.key === ck) return _provenCache.names
|
|
441
|
+
const thms = leanTheorems()
|
|
442
|
+
const names = new Set<string>()
|
|
443
|
+
for (const k of liveKeys()) { const t = theoremOfKey(k, thms); if (t) names.add(t.name) }
|
|
444
|
+
_provenCache = { key: ck, names }
|
|
445
|
+
return names
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const provenLive = (key: string): boolean => {
|
|
449
|
+
if (liveKeys().has(key)) return true
|
|
450
|
+
const want = theoremOfKey(key, leanTheorems())
|
|
451
|
+
return want ? provenNames().has(want.name) : false
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const backedBy = (key: string, value: number[], what: string): number[] => {
|
|
455
|
+
if (!provenLive(key)) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`api: refusing to serve ${what} — the theorem that proves it (${key}) is not live in the ledger. ` +
|
|
458
|
+
`The value would still compute; what is missing is the reason to trust it. Prove it, or stop using it.`)
|
|
459
|
+
}
|
|
460
|
+
return value
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** The six units of ℤ/9 — computed, and served only while lean_units_are_six stands. */
|
|
464
|
+
export const units = (): number[] => backedBy('lean_units_are_six', runtimeUnits(), 'the units')
|
|
465
|
+
|
|
466
|
+
/** The triad {3,6,9} — the non-units, the merkaba's axis. */
|
|
467
|
+
export const triad = (): number[] => backedBy('lean_units_are_six', runtimeTriad(), 'the triad')
|
|
468
|
+
|
|
469
|
+
/** The doubling orbit 1,2,4,8,7,5 — six turns before it returns to where it began. */
|
|
470
|
+
export const orbit = (): number[] =>
|
|
471
|
+
backedBy('lean_millenniumfloor_riemann_reflection_and_heart', runtimeOrbit(), 'the doubling orbit')
|
|
472
|
+
|
|
473
|
+
/** THE MOD-3 CLASSES — the merkaba's axis and its two tetrahedra, computed from the partition rather than
|
|
474
|
+
* typed. These were the literals I missed when I claimed the tooling held no hardcoded ℤ/9 sets: the claim
|
|
475
|
+
* was scoped to the three patterns I had grepped for, which is how a check confirms what it was told to look
|
|
476
|
+
* for and nothing else. Backed by the theorem that proves the classes partition the ring 3+3+3. */
|
|
477
|
+
export const tetA = (): number[] =>
|
|
478
|
+
backedBy('lean_merkaba_the_three_classes_partition_z9',
|
|
479
|
+
Array.from({ length: 9 }, (_, d) => d).filter((d) => d % 3 === 1), 'the first tetrahedron')
|
|
480
|
+
|
|
481
|
+
export const tetB = (): number[] =>
|
|
482
|
+
backedBy('lean_merkaba_the_three_classes_partition_z9',
|
|
483
|
+
Array.from({ length: 9 }, (_, d) => d).filter((d) => d % 3 === 2), 'the second tetrahedron')
|
|
484
|
+
|
|
485
|
+
/** The axis {3,6,0} — the same partition's third class, and the triad by another name. */
|
|
486
|
+
export const axis = (): number[] =>
|
|
487
|
+
backedBy('lean_merkaba_the_three_classes_partition_z9',
|
|
488
|
+
Array.from({ length: 9 }, (_, d) => d).filter((d) => d % 3 === 0), 'the axis')
|
package/src/api/lanes.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** ── HOW MANY JOBS THIS MACHINE CAN ACTUALLY HOLD, WHICH IS NOT HOW MANY CORES IT HAS ─────────────────────
|
|
2
|
+
*
|
|
3
|
+
* `availableParallelism()` answers "how many things can run at once" and says nothing about whether they
|
|
4
|
+
* FIT. On this host that gap is not academic — measured, one `lean` process elaborating families.lean
|
|
5
|
+
* peaks at about 2.9 GB, so ten lanes is 29 GB of demand on a 32 GB machine from a single session. The
|
|
6
|
+
* machine does not refuse; it swaps, and the run gets slower while every core reads as busy.
|
|
7
|
+
*
|
|
8
|
+
* A peer session on uuidna reported a window where this host sat at 88% system time against 9% user with
|
|
9
|
+
* swap climbing — ten times more kernel than work, a machine moving pages rather than computing — and
|
|
10
|
+
* named the cause as a capacity function that is honest per process and blind to its neighbours. I could
|
|
11
|
+
* not verify that recording: the host has rebooted since and currently shows no swap in use at all. What I
|
|
12
|
+
* could verify is the arithmetic, and the arithmetic is worse than the framing, because cores were never
|
|
13
|
+
* the binding constraint. Memory is.
|
|
14
|
+
*
|
|
15
|
+
* So a lane budget is the smallest of three numbers, and the third is the neighbour term:
|
|
16
|
+
*
|
|
17
|
+
* cores what can run at once
|
|
18
|
+
* memory / perJob what fits, from memory that is actually reclaimable
|
|
19
|
+
* minus neighbours jobs of this kind already running, whoever started them
|
|
20
|
+
*
|
|
21
|
+
* THE NEIGHBOUR TERM NEEDS NO AGREEMENT BETWEEN SESSIONS. A shared budget would have to be negotiated,
|
|
22
|
+
* which means a protocol, which means every repo adopting it before any repo benefits. Yielding to jobs
|
|
23
|
+
* that are already running is unilateral: if each session does it alone, the sum is bounded anyway.
|
|
24
|
+
*/
|
|
25
|
+
import { availableParallelism, totalmem } from 'node:os'
|
|
26
|
+
import { execFileSync } from 'node:child_process'
|
|
27
|
+
|
|
28
|
+
/** Memory a checker may actually take: free plus the pages the OS can reclaim without swapping. `freemem()`
|
|
29
|
+
* counts only the free list and reports a few gigabytes on a machine with twenty reclaimable — budgeting
|
|
30
|
+
* from it would refuse lanes the machine can easily afford. */
|
|
31
|
+
export const reclaimableMB = (): number => {
|
|
32
|
+
try {
|
|
33
|
+
const out = String(execFileSync('vm_stat', [], { stdio: 'pipe' }))
|
|
34
|
+
const page = Number(out.match(/page size of (\d+)/)?.[1] ?? 4096)
|
|
35
|
+
const pages = (k: string) => Number(out.match(new RegExp(`Pages ${k}:\\s+(\\d+)`))?.[1] ?? 0)
|
|
36
|
+
const usable = pages('free') + pages('inactive') + pages('speculative') + pages('purgeable')
|
|
37
|
+
if (usable > 0) return (usable * page) / (1024 * 1024)
|
|
38
|
+
} catch { /* not macOS, or vm_stat unavailable — fall through */ }
|
|
39
|
+
return (totalmem() / (1024 * 1024)) / 2 // half of physical: a guess, and named as one
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Jobs of this kind already running, whoever started them — or NULL when it could not be measured.
|
|
43
|
+
*
|
|
44
|
+
* The first version ran `pgrep -c`, counted the output, and caught every failure as zero. macOS pgrep has
|
|
45
|
+
* no `-c` flag — that is Linux — so it printed a usage error to stderr, threw, and the catch reported "no
|
|
46
|
+
* neighbours" on every call. The term never fired once, and nothing said so: a check that cannot run,
|
|
47
|
+
* answering all-clear. Three answers, not two, is the rule that catches this — none, some, and unknown are
|
|
48
|
+
* different, and only the first two are a measurement. */
|
|
49
|
+
export const neighbours = (procName: string): number | null => {
|
|
50
|
+
try {
|
|
51
|
+
const out = String(execFileSync('pgrep', ['-x', procName], { stdio: ['ignore', 'pipe', 'ignore'] }))
|
|
52
|
+
return out.split('\n').filter((l) => l.trim().length).length
|
|
53
|
+
} catch (e: any) {
|
|
54
|
+
// pgrep exits 1 with NO output when nothing matched: that is a real measurement of zero.
|
|
55
|
+
if (e?.status === 1 && !String(e?.stdout ?? '').trim()) return 0
|
|
56
|
+
return null // anything else — missing pgrep, wrong flag, no permission — is UNKNOWN, not zero
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type Budget = { lanes: number; cores: number; byMemory: number; running: number | null; perJobMB: number; why: string }
|
|
61
|
+
|
|
62
|
+
/** The lane count, with every term it was derived from, so a caller can print WHY rather than a bare number. */
|
|
63
|
+
export const laneBudget = (opts: { perJobMB: number; procName: string; envLanes?: string }): Budget => {
|
|
64
|
+
const cores = availableParallelism?.() ?? 4
|
|
65
|
+
const forced = Number(opts.envLanes ?? '')
|
|
66
|
+
const perJobMB = Math.max(1, opts.perJobMB)
|
|
67
|
+
const byMemory = Math.max(1, Math.floor(reclaimableMB() / perJobMB))
|
|
68
|
+
const running = neighbours(opts.procName)
|
|
69
|
+
// UNKNOWN IS NOT ZERO. When the neighbour count cannot be measured the budget halves rather than assuming
|
|
70
|
+
// the machine is empty — the failure that matters here is claiming capacity somebody else is already using.
|
|
71
|
+
const base = Math.min(cores, byMemory)
|
|
72
|
+
const lanes = forced > 0 ? forced
|
|
73
|
+
: running === null ? Math.max(1, Math.floor(base / 2))
|
|
74
|
+
: Math.max(1, base - running)
|
|
75
|
+
const why = forced > 0
|
|
76
|
+
? `forced to ${forced} by the environment`
|
|
77
|
+
: running === null
|
|
78
|
+
? `min(cores ${cores}, memory ${byMemory} at ~${perJobMB}MB each) halved — the neighbour count could NOT be measured on this host`
|
|
79
|
+
: `min(cores ${cores}, memory ${byMemory} at ~${perJobMB}MB each) minus ${running} already running`
|
|
80
|
+
return { lanes, cores, byMemory, running, perJobMB, why }
|
|
81
|
+
}
|