@dreamforgestudiollc/emet 0.0.0-stage → 0.2.6

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 (114) hide show
  1. package/.env.example +189 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CHARTER.md +137 -0
  4. package/LICENSE +22 -0
  5. package/README.md +868 -2
  6. package/docs/AUTHORSHIP.md +26 -0
  7. package/docs/BENCHMARKS.md +261 -0
  8. package/docs/BOOTSTRAP.md +127 -0
  9. package/docs/BOTS.md +83 -0
  10. package/docs/COVERAGE.md +64 -0
  11. package/docs/DEPLOY.md +251 -0
  12. package/docs/DISCIPLINES.md +29 -0
  13. package/docs/GUARDRAILS-AND-MONORAILS.md +108 -0
  14. package/docs/GUIDED-SETUP.md +301 -0
  15. package/docs/PERMISSIONS.md +121 -0
  16. package/docs/ROADMAP.md +62 -0
  17. package/docs/SETUP.md +265 -0
  18. package/docs/SUBKEYS.md +59 -0
  19. package/gems/artifact-index.json +12 -0
  20. package/gems/bot-team-backup/floor.json +15 -0
  21. package/gems/bot-team-backup/gem.json +43 -0
  22. package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
  23. package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
  24. package/gems/fieldnote/floor.json +14 -0
  25. package/gems/fieldnote/gem.json +70 -0
  26. package/gems/fieldnote/runes/fieldnote.js +4 -0
  27. package/gems/lean-six-sigma/CHANGELOG.md +10 -0
  28. package/gems/lean-six-sigma/README.md +92 -0
  29. package/gems/lean-six-sigma/floor.json +31 -0
  30. package/gems/lean-six-sigma/gem.json +1296 -0
  31. package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
  32. package/gems/lean-six-sigma/guides/BELTS.md +52 -0
  33. package/gems/lean-six-sigma/guides/DMADV.md +56 -0
  34. package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
  35. package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
  36. package/gems/lean-six-sigma/guides/LEAN.md +97 -0
  37. package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
  38. package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
  39. package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
  40. package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
  41. package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
  42. package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
  43. package/gems/neurendipity/floor.json +65 -0
  44. package/gems/neurendipity/gem.json +42 -0
  45. package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
  46. package/gems/neurendipity/guides/NEURODIVERGENT_SUPPORT.md +287 -0
  47. package/guides/README.md +3 -0
  48. package/package.json +76 -4
  49. package/server.json +79 -0
  50. package/src/aleph.js +395 -0
  51. package/src/annotations.js +170 -0
  52. package/src/approve-throttle.js +54 -0
  53. package/src/atlas-role.js +58 -0
  54. package/src/attribution.js +555 -0
  55. package/src/conformance.js +688 -0
  56. package/src/content_analyzer.js +450 -0
  57. package/src/database.js +812 -0
  58. package/src/disciplines.js +26 -0
  59. package/src/dispatch.js +382 -0
  60. package/src/env.js +97 -0
  61. package/src/floor.js +228 -0
  62. package/src/gaps.js +509 -0
  63. package/src/gem-check.js +1359 -0
  64. package/src/guides.js +75 -0
  65. package/src/host-reach.js +23 -0
  66. package/src/http-remote.js +313 -0
  67. package/src/index.js +397 -0
  68. package/src/invites.js +167 -0
  69. package/src/layers.js +449 -0
  70. package/src/lessons-to-checks.js +366 -0
  71. package/src/logger.js +300 -0
  72. package/src/metadata.js +61 -0
  73. package/src/module-path.js +32 -0
  74. package/src/mongo-connect.js +91 -0
  75. package/src/named-items.js +208 -0
  76. package/src/oauth-provider.js +838 -0
  77. package/src/owner-approvals.js +281 -0
  78. package/src/persons.js +136 -0
  79. package/src/provenance.js +257 -0
  80. package/src/redact.js +237 -0
  81. package/src/request-log.js +124 -0
  82. package/src/rune-dispatch.js +360 -0
  83. package/src/rune-registry.js +71 -0
  84. package/src/rune-scope.js +42 -0
  85. package/src/scopes.js +100 -0
  86. package/src/security-headers.js +26 -0
  87. package/src/session-graph.js +947 -0
  88. package/src/session.js +1097 -0
  89. package/src/setup.js +1580 -0
  90. package/src/socket.js +545 -0
  91. package/src/startup-pages.js +371 -0
  92. package/src/startup-payload.js +129 -0
  93. package/src/subkey-store.js +357 -0
  94. package/src/subkeys.js +947 -0
  95. package/src/tools.js +4508 -0
  96. package/src/transcript-session.js +106 -0
  97. package/src/validation.js +1187 -0
  98. package/src/version.js +2 -0
  99. package/templates/BOARD.md +72 -0
  100. package/templates/BOTHANDOFF.md +57 -0
  101. package/templates/CHARACTER.md +30 -0
  102. package/templates/HANDOFF.md +63 -0
  103. package/templates/IDENTITY.md +29 -0
  104. package/templates/INDEX.md +82 -0
  105. package/templates/INSTALL.md +29 -0
  106. package/templates/PROCEDURE.md +39 -0
  107. package/templates/ROSTER.md +43 -0
  108. package/templates/SEED.md +65 -0
  109. package/templates/STAGING.md +37 -0
  110. package/templates/STANDARD.md +99 -0
  111. package/templates/THREAD.md +43 -0
  112. package/templates/USER.md +32 -0
  113. package/verification/load-gate/corpus.json +1765 -0
  114. package/verification/load-gate/load-gate.mjs +1246 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Paged startup (ref 358 amendment, paging; Alex approved 2026-10-03, design
3
+ * paper question 1). The connector gateway cuts a tool result at 20,000 UTF-8
4
+ * bytes, and the full startup is about 64K, so on those hosts the floor,
5
+ * install notes, source tags, seeds_due, record gaps and lessons never
6
+ * arrived. Paging delivers the SAME full payload in pages, binding text first.
7
+ *
8
+ * The rules this file keeps:
9
+ * - Every page, envelope included, is at most the page budget
10
+ * (EMET_INIT_PAGE_BYTES, default 18,000 bytes: 2,000 under the cut;
11
+ * clamped to 12,000-19,000).
12
+ * - Keys go out in one fixed order, binding first (PAGE_ORDER), never
13
+ * reordered by size. A key splits only when it cannot fit a page on its
14
+ * own; its chunks run consecutively and join back exactly.
15
+ * - Page 1 carries the plan: which keys are on which page, and which of them
16
+ * are binding. Nothing binding can drop silently: a host that stops early
17
+ * has been told, in page 1, what it has not yet read.
18
+ * - Every page but the last says, first, to call again with the next page
19
+ * before replying. The pages of one load come from one build (load_id);
20
+ * a page asked for after the startup changed says to start over.
21
+ * - The pages joined back are the full payload byte for byte (assemblePages).
22
+ * - Full payload only. This file never builds or serves the abridged form.
23
+ * - `page: "all"` returns the whole payload in one result, unchanged, for a
24
+ * host with no 20,000-byte limit; a payload that fits one page is returned
25
+ * unchanged with no paging fields at all.
26
+ */
27
+
28
+ import crypto from 'crypto';
29
+ import { resultSize } from './startup-payload.js';
30
+
31
+ export const DEFAULT_PAGE_BYTES = 18000;
32
+ export const MIN_PAGE_BYTES = 12000;
33
+ export const MAX_PAGE_BYTES = 19000;
34
+
35
+ /** The page budget in UTF-8 bytes of the whole result, from EMET_INIT_PAGE_BYTES, clamped. */
36
+ export function pageBudget(raw = process.env.EMET_INIT_PAGE_BYTES) {
37
+ const n = Number(raw);
38
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_BYTES;
39
+ return Math.min(MAX_PAGE_BYTES, Math.max(MIN_PAGE_BYTES, Math.floor(n)));
40
+ }
41
+
42
+ /** Binding first: the order keys are paged in. Keys not listed follow, in payload order. */
43
+ export const PAGE_ORDER = Object.freeze([
44
+ 'state', 'database', 'source_tag', 'source_basis', 'source_login', 'source_remembered', 'approval_state', 'person_id', 'client_approvals', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops', 'emet_line', 'emet_line_if_unanswered',
45
+ 'tools_in_force', 'instructions_short', 'session_close_protocol', 'operating_discipline', 'charter',
46
+ 'floor', 'floor_precedence', 'floor_reassertion', 'gems_available', 'gems_active',
47
+ 'seeds_due', 'seeds_evaluated_on', 'install_notes_doc_id',
48
+ 'identity', 'character', 'character_doc_id', 'character_sha256', 'user',
49
+ 'record_gaps', 'named_items', 'lessons_pending', 'layer_health',
50
+ 'install_notes', 'instructions', 'layer_charter',
51
+ 'seeds', 'threads', 'recent_context', 'layer_counts', 'layer_counts_last_30d'
52
+ ]);
53
+
54
+ /** Keys that bind the session (ref 358): never optional, never dropped. */
55
+ export const BINDING_KEYS = Object.freeze([
56
+ 'tools_in_force', 'instructions_short', 'session_close_protocol',
57
+ 'floor', 'floor_precedence', 'floor_reassertion', 'gems_available', 'gems_active',
58
+ 'identity', 'character', 'user'
59
+ ]);
60
+
61
+ /** Page fields that are never payload keys. */
62
+ export const PAGE_FIELDS = Object.freeze(['startup_page', 'page_notice', 'page_plan']);
63
+
64
+ export const loadIdOf = (full) => crypto.createHash('sha256').update(JSON.stringify(full), 'utf8').digest('hex').slice(0, 16);
65
+
66
+ function orderedKeys(full) {
67
+ const keys = PAGE_ORDER.filter((k) => k in full);
68
+ for (const k of Object.keys(full)) if (!keys.includes(k)) keys.push(k);
69
+ return keys;
70
+ }
71
+
72
+ const bytes = (s) => Buffer.byteLength(s, 'utf8');
73
+ const entryBytes = (key, value) => bytes(JSON.stringify({ [key]: value })) - 1; // `"key":value` plus a comma
74
+
75
+ /**
76
+ * GEM lists travel with the floor. They are never split into chunks and never
77
+ * replaced by a pointer (startup-payload.js keeps them too). A list that does
78
+ * not fit the bytes left on the current page moves whole onto the next page.
79
+ * A list that cannot fit even an empty page is still one entry: this packer
80
+ * does not slice it. The caps in the socket keep a real list small enough to fit.
81
+ */
82
+ const WHOLE_KEYS = new Set(['gems_available', 'gems_active']);
83
+ export { WHOLE_KEYS };
84
+
85
+ /**
86
+ * Pack the payload into pages of entries. budgets(k) = content bytes allowed
87
+ * on page k (0-based). An entry is {key, value} or {key, value, chunk, of, json}.
88
+ */
89
+ function pack(full, budgets) {
90
+ const pages = [[]]; const used = [0];
91
+ const room = () => budgets(pages.length - 1) - used[pages.length - 1];
92
+ const newPage = () => { pages.push([]); used.push(0); };
93
+ for (const key of orderedKeys(full)) {
94
+ const value = full[key];
95
+ const size = entryBytes(key, value);
96
+ if (WHOLE_KEYS.has(key)) {
97
+ if (size > room() && pages[pages.length - 1].length) newPage();
98
+ pages[pages.length - 1].push({ key, value });
99
+ used[pages.length - 1] += size;
100
+ continue;
101
+ }
102
+ if (size <= room()) { pages[pages.length - 1].push({ key, value }); used[pages.length - 1] += size; continue; }
103
+ if (size <= budgets(pages.length)) { newPage(); pages[pages.length - 1].push({ key, value }); used[pages.length - 1] += size; continue; }
104
+ // Too big for any one page: chunk it. Strings split as text; anything else
105
+ // as its JSON text. Code points are never split; chunks run consecutively.
106
+ const json = typeof value !== 'string';
107
+ const cps = Array.from(json ? JSON.stringify(value) : value);
108
+ const mine = [];
109
+ let i = 0;
110
+ while (i < cps.length) {
111
+ if (room() < 1000 && pages[pages.length - 1].length) newPage();
112
+ const r = room() - 60; // the chunk marker in startup_page
113
+ let lo = 1, hi = cps.length - i, best = 0;
114
+ while (lo <= hi) {
115
+ const mid = (lo + hi) >> 1;
116
+ if (entryBytes(key, cps.slice(i, i + mid).join('')) <= r) { best = mid; lo = mid + 1; } else hi = mid - 1;
117
+ }
118
+ if (best === 0) best = 1;
119
+ const piece = cps.slice(i, i + best).join('');
120
+ const e = { key, value: piece, chunk: mine.length + 1, of: 0, json };
121
+ mine.push(e);
122
+ pages[pages.length - 1].push(e);
123
+ used[pages.length - 1] += entryBytes(key, piece) + 60;
124
+ i += best;
125
+ if (i < cps.length) newPage();
126
+ }
127
+ for (const e of mine) e.of = mine.length;
128
+ }
129
+ return pages.filter((p, i) => p.length || i === 0);
130
+ }
131
+
132
+ function planOf(pages) {
133
+ return pages.map((entries, i) => {
134
+ const keys = entries.map((e) => (e.chunk ? `${e.key} (part ${e.chunk} of ${e.of})` : e.key));
135
+ const binding = [...new Set(entries.filter((e) => BINDING_KEYS.includes(e.key)).map((e) => e.key))];
136
+ return { page: i + 1, keys, ...(binding.length ? { binding } : {}) };
137
+ });
138
+ }
139
+
140
+ function nextCall(k, loadId, recentLimit) {
141
+ const args = { page: k, load_id: loadId };
142
+ if (recentLimit !== undefined && recentLimit !== null && recentLimit !== '') args.recent_limit = Number(recentLimit);
143
+ return `emet_initialize ${JSON.stringify(args)}`;
144
+ }
145
+
146
+ /** The last page's line (2026-10-03): the open is tied to this startup, so it names the load_id to pass. */
147
+ export function sessionOpenLine(loadId) {
148
+ return `Pass "load_id": "${loadId}" to emet_session_open; the startup page check runs on that load.`;
149
+ }
150
+
151
+ /**
152
+ * A startup served whole - page "all", a startup that fits one result, or a
153
+ * store that is not ready (setup_required, unreachable, misconfigured) -
154
+ * carries its load_id too (2026-10-03), in the same `startup_page` block a
155
+ * page carries, with a notice to pass it to emet_session_open. The payload
156
+ * follows unchanged, and the load_id is the payload's own (loadIdOf), the
157
+ * same id its pages carry. The caller records it with every page fetched.
158
+ */
159
+ export function wholeStartup(full, index = 1) {
160
+ const loadId = loadIdOf(full);
161
+ return {
162
+ startup_page: { index, of: 1, load_id: loadId, next: null },
163
+ page_notice: `The whole startup in one result (load ${loadId}). ${sessionOpenLine(loadId)}`,
164
+ ...full
165
+ };
166
+ }
167
+
168
+ /** The line page 1 and every next-call notice carry (2026-10-03): open checks, so fetch first. */
169
+ export function fetchAllLine(n) {
170
+ return `Fetch all ${n} pages before emet_session_open; an open with any page not fetched is refused.`;
171
+ }
172
+
173
+ function noticeFor(k, n, plan, loadId, recentLimit) {
174
+ if (k === n) {
175
+ return `Startup page ${k} of ${n}, the last. You now hold the whole startup (load ${loadId}). ` +
176
+ 'Follow `instructions_short` from page 1 and `instructions`, now, in that order. ' +
177
+ `${sessionOpenLine(loadId)}`;
178
+ }
179
+ const later = plan.slice(k).filter((p) => p.binding).map((p) => `page ${p.page}: ${p.binding.join(', ')}`);
180
+ // "Fetch all N pages" says how far to keep calling, so it replaced "keep
181
+ // calling until a page says it is the last" (2026-10-03): the notice did not
182
+ // grow, and the binding text keeps its place on the page it was on.
183
+ return `Startup page ${k} of ${n}. Do not reply to the user yet. Call ${nextCall(k + 1, loadId, recentLimit)} now. ` +
184
+ `${fetchAllLine(n)} None is optional` +
185
+ (later.length ? `; binding text is still to come (${later.join('; ')}).` : '.') +
186
+ (k === 1 ? ' `page_plan` lists what each page carries.' : '');
187
+ }
188
+
189
+ function pageObject(pages, plan, k, loadId, recentLimit, payloadOrder) {
190
+ const entries = pages[k - 1];
191
+ const n = pages.length;
192
+ const chunks = {};
193
+ const content = {};
194
+ for (const e of entries) {
195
+ content[e.key] = e.value;
196
+ if (e.chunk) chunks[e.key] = { part: e.chunk, of: e.of, ...(e.json ? { json_text: true } : {}) };
197
+ }
198
+ const startup_page = {
199
+ index: k, of: n, load_id: loadId,
200
+ keys: plan[k - 1].keys,
201
+ ...(Object.keys(chunks).length ? { chunks } : {}),
202
+ next: k < n ? nextCall(k + 1, loadId, recentLimit) : null,
203
+ // The last page names the payload's own key order, so the pages join back
204
+ // to the exact bytes of the one-result payload (assemblePages).
205
+ ...(k === n ? { payload_order: payloadOrder } : {})
206
+ };
207
+ return {
208
+ startup_page,
209
+ page_notice: noticeFor(k, n, plan, loadId, recentLimit),
210
+ ...(k === 1 ? { page_plan: plan } : {}),
211
+ ...content
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Lay the full payload out in pages that each fit the budget. Returns
217
+ * {pages: [pageObject...], plan, load_id}. Pure: the same payload and budget
218
+ * always give the same pages.
219
+ */
220
+ export function paginate(full, { budget = pageBudget(), recentLimit } = {}) {
221
+ const loadId = loadIdOf(full);
222
+ const order = Object.keys(full);
223
+ // The page fields (notice, plan, next call) cost bytes that depend on the
224
+ // layout, so the layout is found by iterating: pack with the overheads last
225
+ // measured, build, measure again. Stops at the first layout that fits and
226
+ // has stopped changing.
227
+ let overhead = [2400]; let other = 900;
228
+ let best = null; let lastShape = null;
229
+ for (let attempt = 0; attempt < 16; attempt++) {
230
+ const pages = pack(full, (i) => budget - (overhead[i] ?? other) - 48);
231
+ const plan = planOf(pages);
232
+ const objs = pages.map((_, i) => pageObject(pages, plan, i + 1, loadId, recentLimit, order));
233
+ const sizes = objs.map((o) => resultSize(o).bytes);
234
+ const fits = sizes.every((sz) => sz <= budget);
235
+ const shape = JSON.stringify(plan);
236
+ if (fits) {
237
+ best = { pages: objs, plan, load_id: loadId, sizes };
238
+ if (shape === lastShape) break;
239
+ }
240
+ lastShape = shape;
241
+ const measured = pages.map((entries, i) => sizes[i] - entries.reduce((n, e) => n + entryBytes(e.key, e.value) + (e.chunk ? 60 : 0), 0));
242
+ overhead = measured.map((m, i) => (fits ? m : Math.max(m, overhead[i] ?? other)));
243
+ other = Math.max(...measured.slice(1), 600);
244
+ if (!fits) sizes.forEach((sz, i) => { if (sz > budget) overhead[i] += sz - budget + 64; });
245
+ }
246
+ if (!best) throw new Error('startup paging: could not fit the pages to the budget');
247
+ return best;
248
+ }
249
+
250
+ /** Join pages back into the payload, exactly, in the payload's own key order (for hosts and tests). */
251
+ export function assemblePages(pages) {
252
+ const got = {}; const text = {}; const done = {};
253
+ const ordered = [...pages].sort((a, b) => a.startup_page.index - b.startup_page.index);
254
+ for (const p of ordered) {
255
+ const chunks = p.startup_page.chunks || {};
256
+ for (const [k, v] of Object.entries(p)) {
257
+ if (PAGE_FIELDS.includes(k)) continue;
258
+ if (!chunks[k]) { got[k] = v; continue; }
259
+ text[k] = (text[k] || '') + v;
260
+ if (chunks[k].part === chunks[k].of) { got[k] = chunks[k].json_text ? JSON.parse(text[k]) : text[k]; done[k] = true; }
261
+ }
262
+ }
263
+ const last = ordered[ordered.length - 1];
264
+ const order = (last && last.startup_page.payload_order) || Object.keys(got);
265
+ const out = {};
266
+ for (const k of order) if (k in got) out[k] = got[k];
267
+ for (const k of Object.keys(got)) if (!(k in out)) out[k] = got[k];
268
+ return out;
269
+ }
270
+
271
+ // One load's pages are served from the build that made page 1, so a write
272
+ // between two calls never moves a key across pages. In memory, per process.
273
+ const CACHE_MS = 10 * 60 * 1000;
274
+ const CACHE_MAX = 32;
275
+ // The fetched-page record outlives the payload cache: the startup_pages gate
276
+ // refuses an open whose load has no record, so a host that reads its startup
277
+ // and opens a while later (up to 60 minutes after its last page) still opens.
278
+ // Its own lifetime, never the payload cache's.
279
+ export const SERVED_MS = 60 * 60 * 1000;
280
+ // The fetched-page record is a few numbers per load, so it keeps far more
281
+ // loads than the payload cache: an open with an evicted load is refused (the
282
+ // startup_pages gate cannot check it), so many hosts starting at once must not
283
+ // push a load out before its host opens.
284
+ export const SERVED_MAX = 2000;
285
+ const cache = new Map();
286
+ export function cacheLoad(full, now = Date.now()) {
287
+ const id = loadIdOf(full);
288
+ cache.set(id, { full, at: now });
289
+ for (const [k, v] of cache) if (now - v.at > CACHE_MS) cache.delete(k);
290
+ while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
291
+ return id;
292
+ }
293
+ export function cachedLoad(id, now = Date.now()) {
294
+ const hit = id && cache.get(String(id));
295
+ if (!hit) return null;
296
+ if (now - hit.at > CACHE_MS) { cache.delete(String(id)); return null; }
297
+ return hit.full;
298
+ }
299
+ export function clearPageCache() { cache.clear(); served.clear(); }
300
+
301
+ // Which pages of each load a host was given (2026-10-03): read by the
302
+ // startup_pages gate, which refuses an open with any page never fetched.
303
+ // In memory, per process, same lifetime as the cache. Keyed by load_id, a hash
304
+ // of the content, so two hosts that load the same startup share one record.
305
+ const served = new Map();
306
+ /**
307
+ * Note that page `index` of `of` was served for a load; index 'all' = the
308
+ * whole startup at once. Returns the LATE pages this serve settles
309
+ * (2026-10-03): for each session opened while this page was missing,
310
+ * {load_id, session, page, of}. A page fetched after the open is late, not
311
+ * still missing, so the lab does not count it as missing. [] when none.
312
+ */
313
+ export function notePageServed(loadId, index, of, now = Date.now()) {
314
+ if (!loadId) return [];
315
+ const id = String(loadId);
316
+ const rec = served.get(id) || { of: null, pages: new Set(), all: false, at: now, opens: new Map() };
317
+ if (index === 'all') rec.all = true;
318
+ else { rec.pages.add(index); rec.of = of; }
319
+ rec.at = now;
320
+ const late = [];
321
+ for (const [session, missing] of rec.opens || []) {
322
+ for (const p of [...missing]) {
323
+ if (index === 'all' || p === index) { missing.delete(p); late.push({ load_id: id, session, page: p, of: rec.of }); }
324
+ }
325
+ if (!missing.size) rec.opens.delete(session);
326
+ }
327
+ served.delete(id); served.set(id, rec); // keep insertion order = recency
328
+ for (const [k, v] of served) if (now - v.at > SERVED_MS) served.delete(k);
329
+ while (served.size > SERVED_MAX) served.delete(served.keys().next().value);
330
+ return late;
331
+ }
332
+ /**
333
+ * Note that a session opened while pages of a load were missing (report
334
+ * mode), so a page fetched afterwards is recorded as late. `session` is the
335
+ * label the open's lab row carries. Same lifetime as the served record.
336
+ */
337
+ export function noteOpenMissing(loadId, session, missing = []) {
338
+ const rec = loadId ? served.get(String(loadId)) : null;
339
+ if (!rec || !session || !missing.length) return;
340
+ if (!rec.opens) rec.opens = new Map();
341
+ const set = rec.opens.get(session) || new Set();
342
+ for (const p of missing) set.add(p);
343
+ rec.opens.set(session, set);
344
+ }
345
+ /**
346
+ * Which pages of the named load were never served. null when no load is
347
+ * named, or there is no record of it (no page of it served in the last 60
348
+ * minutes, SERVED_MS, or the server restarted). Never another load in its place. Never throws, never blocks.
349
+ */
350
+ export function pageCompletion(loadId = null, now = Date.now()) {
351
+ const id = loadId ? String(loadId) : null;
352
+ const rec = id ? served.get(id) : null;
353
+ if (!rec || now - rec.at > SERVED_MS) return null;
354
+ if (rec.all || !rec.of) return { load_id: id, complete: true, missing: [] };
355
+ const missing = [];
356
+ for (let i = 1; i <= rec.of; i++) if (!rec.pages.has(i)) missing.push(i);
357
+ return { load_id: id, of: rec.of, served: [...rec.pages].sort((a, b) => a - b), missing, complete: missing.length === 0 };
358
+ }
359
+
360
+ /** The reply to a page asked for after the startup changed (or the server restarted). */
361
+ export function restartPage(page, loadId) {
362
+ return {
363
+ state: 'ready',
364
+ startup_page: { index: null, requested: page, load_id: loadId || null, restart: true, next: 'emet_initialize {}' },
365
+ page_notice: 'The startup changed after page 1 was built (new memory was written, or the server restarted), so ' +
366
+ `page ${page} of that load can no longer be served. Call emet_initialize {} now to start again from page 1, and ` +
367
+ 'use the new pages in place of the earlier ones. Do not reply to the user until you have every page.'
368
+ };
369
+ }
370
+
371
+ export default { fetchAllLine, sessionOpenLine, wholeStartup, pageBudget, PAGE_ORDER, BINDING_KEYS, PAGE_FIELDS, paginate, assemblePages, loadIdOf, cacheLoad, cachedLoad, clearPageCache, restartPage, notePageServed, noteOpenMissing, pageCompletion };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The startup payload's size and its abridged form (ref 358 amendment, the
3
+ * host-aware startup; design: research paper emet-startup-amendment rev 3,
4
+ * sections 4a-4c, conservative variant chosen by Alex 2026-10-02, procedural
5
+ * ref 405 item 8 and ref 407).
6
+ *
7
+ * WHAT THIS FILE DOES AND DOES NOT DO (2026-10-03):
8
+ * - payloadForLayerCharter: the layer charter as the payload serves it, with
9
+ * the one exact duplicate removed. `importance_range` is the machine twin
10
+ * of the `importance` band and says the same thing (tests/layers.test.js
11
+ * pins them consistent), so the payload carries the band once. Every word
12
+ * of the charter prose stays; LAYER_CHARTER itself is unchanged.
13
+ * - abridgeReadyPayload: builds the conservative abridged payload from a
14
+ * full ready payload. It is NOT reachable from any host. No tool argument
15
+ * selects it: the amendment says the server decides the mode and the model
16
+ * does not, and a host can never ask for less than the server allows. The
17
+ * server can only prove a returning host with the startup receipts of
18
+ * stage 2 (review note 3b, 5), which do not exist yet, so emet_initialize serves
19
+ * the full payload to every caller. This function is the tested builder
20
+ * stage 2's mode check will call, and the measure of what abridging saves.
21
+ * - resultSize: the size of a payload as the host receives it, in characters
22
+ * and UTF-8 bytes (the connector gateway cuts at 20,000 bytes).
23
+ *
24
+ * FIDELITY RULE (Alex, ref 358): the floor, identity, character, user, the close
25
+ * protocol and tools in force are never pointers and never shortened here.
26
+ * Only install notes, the layer charter and the long instructions become
27
+ * hash-pinned pointers in the abridged form, and the seed and thread indexes
28
+ * and the layer counts become compact indexes.
29
+ */
30
+
31
+ import crypto from 'crypto';
32
+ import { LAYER_CHARTER } from './layers.js';
33
+
34
+ const sha256 = (s) => crypto.createHash('sha256').update(String(s), 'utf8').digest('hex');
35
+
36
+ /** The layer charter as the startup payload serves it: every field except the duplicate importance_range. */
37
+ export function payloadForLayerCharter(charter = LAYER_CHARTER) {
38
+ const out = {};
39
+ for (const [layer, entry] of Object.entries(charter)) {
40
+ const { importance_range, ...rest } = entry; // eslint-disable-line no-unused-vars
41
+ out[layer] = rest;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /** Characters and UTF-8 bytes of the result text a host receives (createSuccessResponse's compact envelope). */
47
+ export function resultSize(data, tool = 'emet_initialize') {
48
+ const text = JSON.stringify({ success: true, tool, timestamp: 1790000000000, data });
49
+ return { chars: text.length, bytes: Buffer.byteLength(text, 'utf8') };
50
+ }
51
+
52
+ /** Per-key JSON size of a payload, largest first. */
53
+ export function keySizes(payload) {
54
+ return Object.entries(payload)
55
+ .map(([key, v]) => ({ key, chars: JSON.stringify(v === undefined ? null : v).length }))
56
+ .sort((a, b) => b.chars - a.chars);
57
+ }
58
+
59
+ /** The keys every abridged load embeds exactly as the full load has them (review note 4b; never dropped). */
60
+ export const ABRIDGED_KEEP = Object.freeze([
61
+ 'state', 'database', 'source_tag', 'person_id', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops',
62
+ 'emet_line', 'emet_line_if_unanswered',
63
+ 'tools_in_force', 'instructions_short', 'session_close_protocol', 'operating_discipline', 'charter',
64
+ 'identity', 'character', 'character_doc_id', 'character_sha256', 'user',
65
+ 'install_notes_doc_id',
66
+ 'seeds_due', 'seeds_evaluated_on',
67
+ 'record_gaps', 'named_items', 'lessons_pending',
68
+ 'floor', 'floor_precedence', 'floor_reassertion',
69
+ 'gems_available', 'gems_active'
70
+ ]);
71
+
72
+ /** Keys that become hash-pinned pointers: text the host already echoed at a full load, unchanged. */
73
+ export const ABRIDGED_PINNED = Object.freeze(['install_notes', 'layer_charter', 'instructions']);
74
+
75
+ /** Keys that become compact indexes (not hash-pinned; the seed index is watermarked in stage 2). */
76
+ export const ABRIDGED_INDEXED = Object.freeze(['seeds', 'threads', 'layer_counts', 'layer_counts_last_30d']);
77
+
78
+ function layerHealthy(h) {
79
+ return !!(h && h.assessed === true && Array.isArray(h.underused) && h.underused.length === 0);
80
+ }
81
+
82
+ /**
83
+ * The conservative abridged payload, built from a full ready payload.
84
+ * @param {object} full the object initialize() returns in the ready state
85
+ * @param {object} [opts]
86
+ * @param {string} [opts.since] ISO time of the host's last full-load receipt;
87
+ * recent_context then keeps only entries newer than it (at most 5). Without
88
+ * it recent_context is kept as served (the safe direction).
89
+ * @param {string} [opts.reason] startup_reason to report
90
+ */
91
+ export function abridgeReadyPayload(full, opts = {}) {
92
+ if (!full || full.state !== 'ready') throw new Error('abridgeReadyPayload: only a ready payload can be abridged');
93
+ const out = { startup_mode: 'abridged', startup_reason: opts.reason || 'returning configured bot (proven by receipt)' };
94
+ for (const k of ABRIDGED_KEEP) if (k in full) out[k] = full[k];
95
+
96
+ const pointers = [];
97
+ if (full.install_notes) {
98
+ pointers.push({
99
+ key: 'install_notes', kind: 'hash_pinned', doc_id: full.install_notes.doc_id,
100
+ version: full.install_notes.version ?? null, sha256: full.install_notes.sha256 || sha256(full.install_notes.content),
101
+ read_via: `read_doc ${full.install_notes.doc_id}`
102
+ });
103
+ }
104
+ for (const k of ['layer_charter', 'instructions']) {
105
+ if (full[k] != null) {
106
+ pointers.push({ key: k, kind: 'hash_pinned', sha256: sha256(JSON.stringify(full[k])), read_via: 'emet_initialize (a full load; the server serves the whole text again whenever this sha256 differs from the copy you hold)' });
107
+ }
108
+ }
109
+ const seeds = Array.isArray(full.seeds) ? full.seeds : [];
110
+ pointers.push({ key: 'seeds', kind: 'index', count: seeds.length, read_via: 'list_docs {prefix: "seeds/"}, then read_doc' });
111
+ const threads = Array.isArray(full.threads) ? full.threads : [];
112
+ pointers.push({
113
+ key: 'threads', kind: 'index', count: threads.length,
114
+ latest_updated_at: threads.map((t) => t.updated_at).filter(Boolean).sort().pop() || null,
115
+ read_via: 'list_docs {prefix: "threads/"}'
116
+ });
117
+ pointers.push({ key: 'layer_counts', kind: 'index', read_via: 'emet_status' });
118
+
119
+ // layer_health only when it reports something; recent_context as a delta.
120
+ if (!layerHealthy(full.layer_health)) out.layer_health = full.layer_health;
121
+ else pointers.push({ key: 'layer_health', kind: 'index', status: 'healthy', read_via: 'emet_status' });
122
+ const recent = Array.isArray(full.recent_context) ? full.recent_context : [];
123
+ out.recent_context = (opts.since ? recent.filter((r) => r.when && r.when > opts.since) : recent).slice(0, 5);
124
+
125
+ out.pointers = pointers;
126
+ return out;
127
+ }
128
+
129
+ export default { payloadForLayerCharter, resultSize, keySizes, abridgeReadyPayload, ABRIDGED_KEEP, ABRIDGED_PINNED, ABRIDGED_INDEXED };