@dreamforgestudiollc/emet 0.2.4 → 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 (101) hide show
  1. package/.env.example +53 -0
  2. package/CHANGELOG.md +67 -3
  3. package/CHARTER.md +19 -7
  4. package/README.md +106 -21
  5. package/docs/AUTHORSHIP.md +3 -3
  6. package/docs/BOTS.md +4 -0
  7. package/docs/COVERAGE.md +2 -2
  8. package/docs/DEPLOY.md +9 -4
  9. package/docs/GUARDRAILS-AND-MONORAILS.md +2 -1
  10. package/docs/GUIDED-SETUP.md +8 -3
  11. package/docs/PERMISSIONS.md +6 -3
  12. package/docs/ROADMAP.md +25 -22
  13. package/docs/SETUP.md +8 -2
  14. package/docs/SUBKEYS.md +59 -0
  15. package/gems/artifact-index.json +12 -0
  16. package/gems/bot-team-backup/floor.json +15 -0
  17. package/gems/bot-team-backup/gem.json +43 -0
  18. package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
  19. package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
  20. package/gems/fieldnote/floor.json +14 -0
  21. package/gems/fieldnote/gem.json +70 -0
  22. package/gems/fieldnote/runes/fieldnote.js +4 -0
  23. package/gems/lean-six-sigma/CHANGELOG.md +10 -0
  24. package/gems/lean-six-sigma/README.md +92 -0
  25. package/gems/lean-six-sigma/floor.json +31 -0
  26. package/gems/lean-six-sigma/gem.json +1296 -0
  27. package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
  28. package/gems/lean-six-sigma/guides/BELTS.md +52 -0
  29. package/gems/lean-six-sigma/guides/DMADV.md +56 -0
  30. package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
  31. package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
  32. package/gems/lean-six-sigma/guides/LEAN.md +97 -0
  33. package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
  34. package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
  35. package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
  36. package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
  37. package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
  38. package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
  39. package/gems/neurendipity/floor.json +65 -0
  40. package/gems/neurendipity/gem.json +42 -0
  41. package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
  42. package/{guides → gems/neurendipity/guides}/NEURODIVERGENT_SUPPORT.md +17 -8
  43. package/guides/README.md +3 -0
  44. package/package.json +11 -5
  45. package/server.json +44 -4
  46. package/src/aleph.js +4 -4
  47. package/src/annotations.js +38 -3
  48. package/src/atlas-role.js +1 -1
  49. package/src/attribution.js +555 -0
  50. package/src/conformance.js +2 -2
  51. package/src/database.js +6 -5
  52. package/src/dispatch.js +116 -13
  53. package/src/env.js +54 -9
  54. package/src/floor.js +82 -41
  55. package/src/gaps.js +39 -6
  56. package/src/gem-check.js +1359 -0
  57. package/src/guides.js +31 -4
  58. package/src/host-reach.js +23 -0
  59. package/src/http-remote.js +46 -2
  60. package/src/index.js +9 -2
  61. package/src/layers.js +1 -1
  62. package/src/lessons-to-checks.js +3 -3
  63. package/src/logger.js +2 -1
  64. package/src/mongo-connect.js +45 -2
  65. package/src/oauth-provider.js +2 -2
  66. package/src/owner-approvals.js +281 -0
  67. package/src/persons.js +20 -0
  68. package/src/redact.js +10 -4
  69. package/src/request-log.js +1 -1
  70. package/src/rune-dispatch.js +360 -0
  71. package/src/rune-registry.js +71 -0
  72. package/src/rune-scope.js +42 -0
  73. package/src/scopes.js +3 -0
  74. package/src/session-graph.js +25 -23
  75. package/src/session.js +9 -9
  76. package/src/setup.js +297 -30
  77. package/src/socket.js +545 -0
  78. package/src/startup-pages.js +23 -6
  79. package/src/startup-payload.js +9 -6
  80. package/src/subkey-store.js +357 -0
  81. package/src/subkeys.js +947 -0
  82. package/src/tools.js +299 -62
  83. package/src/transcript-session.js +1 -1
  84. package/src/validation.js +26 -2
  85. package/src/version.js +1 -1
  86. package/templates/BOARD.md +2 -2
  87. package/templates/BOTHANDOFF.md +2 -2
  88. package/templates/CHARACTER.md +1 -1
  89. package/templates/HANDOFF.md +1 -1
  90. package/templates/IDENTITY.md +1 -1
  91. package/templates/INDEX.md +19 -19
  92. package/templates/INSTALL.md +1 -1
  93. package/templates/PROCEDURE.md +1 -1
  94. package/templates/ROSTER.md +2 -2
  95. package/templates/SEED.md +2 -2
  96. package/templates/STAGING.md +2 -2
  97. package/templates/STANDARD.md +5 -5
  98. package/templates/THREAD.md +1 -1
  99. package/templates/USER.md +4 -1
  100. package/verification/load-gate/corpus.json +1765 -0
  101. package/verification/load-gate/load-gate.mjs +1246 -0
package/src/dispatch.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * Until 2026-09-27 index.js (stdio) and http-remote.js (HTTP) each carried a
10
10
  * full switch mapping tool name to handler. dispatch.test.js compared the two
11
11
  * by case NAME and caught one class of drift (the session_id alias divergence,
12
- * an earlier fix); it could not see argument drift, and the code audit of
12
+ * commit-9e580d5); it could not see argument drift, and the code audit of
13
13
  * 2026-09-27 found one - setupComplete received the logger on stdio and not on
14
14
  * HTTP. A rule stated twice drifts; this file is the one statement.
15
15
  *
@@ -29,8 +29,12 @@ import {
29
29
  queryTranscripts, reviseTranscript, saveTranscript, sessionOpen, transcriptAppend,
30
30
  readDoc, READ_DOC_PAGE_DEFAULT, readDocHistory, writeDoc, validateDoc, acceptNonconformance,
31
31
  listDocs, retireDoc, renameDoc, restoreDoc, patchDoc, inviteCreate, memberAccessCreate,
32
- resolvedSourceTag
32
+ resolvedSourceTag,
33
+ TOOLS
33
34
  } from './tools.js';
35
+ import { reserveToolNames } from './rune-registry.js';
36
+
37
+ reserveToolNames([...TOOLS.map((tool) => tool.name), 'emet_subkey_request', 'emet_subkey_claim', 'emet_subkey_revoke', 'emet_subkey_end_session']);
34
38
 
35
39
  /**
36
40
  * Route one tool call. `ctx` carries what the transport owns: dbManager, logger,
@@ -38,7 +42,7 @@ import {
38
42
  * isError envelope, so the transport's audit line always has an outcome.
39
43
  */
40
44
  /**
41
- * The source tag under one name. `source` is the name on every write (the owner,
45
+ * The source tag under one name. `source` is the name on every write (Alex,
42
46
  * 2026-09-27, code audit 1.5c); the older per-tool names - updated_by,
43
47
  * retired_by, accepted_by on documents, channel on transcripts - are read as
44
48
  * aliases for one release and then dropped. metadata.source on the layers is
@@ -54,7 +58,7 @@ export function sourceOf(args) {
54
58
  }
55
59
 
56
60
  /**
57
- * A transcript write names a registered host or is refused (the owner, 2026-09-27,
61
+ * A transcript write names a registered host or is refused (Alex, 2026-09-27,
58
62
  * code audit 1.5d: nine spellings for five hosts in the store). Registered =
59
63
  * EMET_SOURCE_TAGS, exact membership only. With no registry configured, any
60
64
  * non-empty tag passes. A refusal does not publish the registry.
@@ -96,6 +100,14 @@ export function requireRegisteredSource(tag, tool, allowNull = false) {
96
100
  return tag;
97
101
  }
98
102
 
103
+ const SILENT_GEM_LOG = Object.freeze({ warn() {}, info() {}, error() {} });
104
+ /**
105
+ * Test seam: the suites swap in a seatGems stand-in to drive emet_status's
106
+ * fail-closed branch. Never set outside tests; pass null to restore.
107
+ */
108
+ let statusSeatForTests = null;
109
+ export function setStatusSeatForTests(fn) { statusSeatForTests = typeof fn === 'function' ? fn : null; }
110
+
99
111
  export async function dispatchTool(name, args, ctx) {
100
112
  const { dbManager, logger, AuditOperation } = ctx;
101
113
  args = args || {};
@@ -110,14 +122,14 @@ export async function dispatchTool(name, args, ctx) {
110
122
  if (!scopesAllow(scopes, name, { http: actor.transport === 'http', authenticated: actor.authenticated === true, args })) {
111
123
  throw new EmetError(deniedMessage(name, scopes, args), ErrorCodes.VALIDATION_ERROR, StatusCodes.BAD_REQUEST);
112
124
  }
113
- // The session graph (2026-09-29): the monorail (docs/GUARDRAILS-AND-MONORAILS.md). State is read BEFORE a
125
+ // The session graph (2026-09-29, board 120): the monorail (docs/GUARDRAILS-AND-MONORAILS.md). State is read BEFORE a
114
126
  // governed write; the write's result carries `session_graph` with the next
115
127
  // step; the step is recorded after it succeeds. Enforce only (2026-10-03):
116
128
  // a skipped step is refused before anything is written. A governed write
117
129
  // that names a session fails closed when the graph cannot be read.
118
130
  const sg = await import('./session-graph.js');
119
131
  // The exchange number, normalized ONCE, before the graph, the closed-session
120
- // guard and the tool all read it (2026-10-01, the second reader's blocker on an earlier draft: the
132
+ // guard and the tool all read it (2026-10-01, the reviewer's blocker on commit-2a361fd: the
121
133
  // guard read "4" as 4, the tool did not, so a text "4" was a retry to one and
122
134
  // a new exchange to the other). A whole number, or its text, or absent.
123
135
  if (name === 'emet_transcript_append') {
@@ -171,7 +183,7 @@ export async function dispatchTool(name, args, ctx) {
171
183
  // A correction of a closed session: the exchanges with their numbers normalized.
172
184
  if (grace && grace.revise) { args = { ...args, exchanges: grace.exchanges }; grace = null; }
173
185
  // A retry of the saved closing exchange is answered here; the tool is NOT
174
- // called, so nothing can be written (the second reader's blocker on an earlier draft).
186
+ // called, so nothing can be written (the reviewer's blocker on commit-2a361fd).
175
187
  if (grace && grace.retry) {
176
188
  const block = sg.graphBlock(graph);
177
189
  return createSuccessResponse({
@@ -181,6 +193,13 @@ export async function dispatchTool(name, args, ctx) {
181
193
  }, name);
182
194
  }
183
195
  const ok = (data) => {
196
+ // A sub-key, claim nonce, or session handle is delivered once by the tool
197
+ // that minted it. Later receipts do not repeat it.
198
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
199
+ if (name !== 'emet_subkey_claim') delete data.subkey;
200
+ if (name !== 'emet_subkey_request') delete data.nonce;
201
+ if (name !== 'emet_session_open') delete data.session_handle;
202
+ }
184
203
  const block = sg.graphBlock(graph);
185
204
  if (block && data && typeof data === 'object' && !Array.isArray(data)) data.session_graph = block;
186
205
  if (name === sg.STARTUP_PAGES_TOOL && graph) sg.labRecord(name, args, graph, dbManager, logger); // the open succeeded
@@ -200,11 +219,11 @@ export async function dispatchTool(name, args, ctx) {
200
219
  }
201
220
  return createSuccessResponse(data, name);
202
221
  };
203
- // The close's state write is awaited (the second reader's nit 6), so an append sent after
222
+ // The close's state write is awaited (the reviewer's nit 6), so an append sent after
204
223
  // the close returns already finds the session closed - when the write
205
224
  // succeeds. A failed write is retried once; if it still fails the reply
206
225
  // says so (closed_recorded: false, with a warning) instead of reading as a
207
- // locked session (2026-10-01, the second reader's nit 2 on an earlier draft, the reviewer's
226
+ // locked session (2026-10-01, the reviewer's nit 2 on commit-df21117, the tester's
208
227
  // condition 2). Every other step is recorded without holding up the reply.
209
228
  const recordClose = async (data) => {
210
229
  let rec = await sg.graphAfter(name, args, data, dbManager, logger, now);
@@ -226,13 +245,45 @@ export async function dispatchTool(name, args, ctx) {
226
245
  switch (name) {
227
246
  case 'emet_initialize': return ok(await initialize(args, { onLate: (late) => sg.recordLatePages(late, dbManager, logger) }));
228
247
  case 'emet_setup_complete': return ok(await setupComplete(args.answers || {}, logger, { reissue: args.reissue === true, source: sourceOf(args) }));
229
- case 'emet_session_close': return await ok(await sessionClose(args));
248
+ case 'emet_session_close': {
249
+ const data = await sessionClose(args);
250
+ try {
251
+ const { setting } = await import('./env.js');
252
+ if (data && data.state === 'complete' && setting('SUBKEYS') === '1') {
253
+ const sk = await import('./subkeys.js');
254
+ const { approvalsDeps } = await import('./tools.js');
255
+ const named = sk.sessionIdForClose(args);
256
+ if (named.mismatch) {
257
+ logger?.warn?.('sub-key session not released: session_id and receipts.transcript_session_id name different sessions');
258
+ } else {
259
+ await sk.releaseClosedSession(approvalsDeps, named.base, now, {
260
+ handle: args.session_handle, subkey: args.subkey
261
+ });
262
+ }
263
+ }
264
+ } catch (e) {
265
+ logger?.warn?.('sub-key session not released on close', { error: e && e.message });
266
+ }
267
+ return await ok(data);
268
+ }
230
269
  case 'emet_gaps': return ok(await findGaps(args));
231
- case 'emet_floor': return ok(readFloor(args));
270
+ case 'emet_client_approve': {
271
+ const att = await import('./attribution.js');
272
+ const { rawActor, resolveTokenSubject } = await import('./persons.js');
273
+ const { approvalsDeps, sourceRegistry } = await import('./tools.js');
274
+ const ownerId = await resolveTokenSubject(dbManager.db).catch(() => null);
275
+ try {
276
+ return ok(await att.clientApprove(approvalsDeps, rawActor(), ownerId, args, sourceRegistry()));
277
+ } catch (e) {
278
+ if (e && e.attributionRefusal) throw new ValidationError('tag', e.message);
279
+ throw e;
280
+ }
281
+ }
282
+ case 'emet_floor': return ok(await readFloor(args));
232
283
  case 'recall': return ok(await recallMemories(dbManager, args.query, args.layer || null, args.limit || 10, logger, AuditOperation, { history: args.history === true, order: args.order }));
233
284
  case 'query_layer': return ok(await queryLayer(dbManager, args.layer, args.options || {}, logger, AuditOperation));
234
285
  case 'emet_status': {
235
- // One diagnostic (2026-09-27, the owner: one tool with several functions over
286
+ // One diagnostic (2026-09-27, Alex: one tool with several functions over
236
287
  // several overlapping ones). setupStatus + the per-layer counts, and the
237
288
  // write-path probe on request.
238
289
  const status = await setupStatus();
@@ -241,6 +292,35 @@ export async function dispatchTool(name, args, ctx) {
241
292
  status.layers = layers.layers; status.total_memories = layers.total_memories; status.health = layers.health;
242
293
  }
243
294
  if (args.probe === true) status.probe = await verifyConnection();
295
+ {
296
+ // Socket PR 4: each GEM's byte cost against the caps the socket enforces.
297
+ // Read-only; a socket failure is reported here and never fails the status.
298
+ try {
299
+ const { seatGems, gemCost } = await import('./socket.js');
300
+ const seat = statusSeatForTests || seatGems;
301
+ // Silent logger, as floor.js does: a status read must not write
302
+ // seating or refusal lines to stderr on every call.
303
+ status.gems = gemCost(await seat({ log: SILENT_GEM_LOG }));
304
+ } catch {
305
+ status.gems = { error: 'GEM socket failed closed' };
306
+ }
307
+ }
308
+ {
309
+ const att = await import('./attribution.js');
310
+ const { rawActor } = await import('./persons.js');
311
+ const { callerAttribution } = await import('./setup.js');
312
+ const { setting } = await import('./env.js');
313
+ Object.assign(status, callerAttribution());
314
+ const v = att.approvalsView(att.cachedApprovals(), rawActor() || { transport: 'stdio' }, null);
315
+ if (v) status.client_approvals = v;
316
+ if (setting('SUBKEYS') === '1') {
317
+ const sk = await import('./subkeys.js');
318
+ const { approvalsDeps } = await import('./tools.js');
319
+ const records = await sk.loadSubkeyRecords(approvalsDeps);
320
+ const keys = sk.subkeysView(records, rawActor() || { transport: 'stdio' }, null);
321
+ if (keys) status.subkeys = keys;
322
+ }
323
+ }
244
324
  return ok(status);
245
325
  }
246
326
  case 'save_to_layer': return ok(await saveMemory(dbManager, args.content, args.layer, args.metadata || {}, logger, AuditOperation, { session_id: args.session_id }));
@@ -264,8 +344,31 @@ export async function dispatchTool(name, args, ctx) {
264
344
  case 'semantic_recall': return ok(await semanticRecall(args.query, args.layer || null, args.limit || 10, logger, { history: args.history === true }, dbManager));
265
345
  case 'emet_invite_create': return ok(await inviteCreate(dbManager, args, logger));
266
346
  case 'emet_member_access_create': return ok(await memberAccessCreate(dbManager, args, logger));
267
- default:
347
+ default: {
348
+ // The four sub-key tools are not case labels. A case label would put
349
+ // them in the dispatch table while EMET_SUBKEYS is off, and the host
350
+ // list would no longer match that table. They dispatch from here only
351
+ // while the flag is on; off, the name is an unknown tool.
352
+ const { setting } = await import('./env.js');
353
+ if (setting('SUBKEYS') === '1') {
354
+ const sk = await import('./subkeys.js');
355
+ if (sk.SUBKEY_TOOLS.includes(name)) {
356
+ const { rawActor, resolveTokenSubject } = await import('./persons.js');
357
+ const { approvalsDeps, sourceRegistry } = await import('./tools.js');
358
+ const ownerId = dbManager && dbManager.db ? await resolveTokenSubject(dbManager.db).catch(() => null) : null;
359
+ try {
360
+ return ok(await sk.dispatchSubkey(name, approvalsDeps, rawActor(), ownerId, args, sourceRegistry(), now));
361
+ } catch (e) {
362
+ if (e && e.attributionRefusal) throw new ValidationError('subkey', e.message);
363
+ throw e;
364
+ }
365
+ }
366
+ }
367
+ const { serveRune } = await import('./rune-dispatch.js');
368
+ const ran = await serveRune(name, args, { logger, AuditOperation, dbManager });
369
+ if (ran) return ran;
268
370
  throw new EmetError(`Unknown tool: ${name}`, ErrorCodes.UNKNOWN_TOOL, StatusCodes.BAD_REQUEST);
371
+ }
269
372
  }
270
373
  } catch (error) {
271
374
  if (grace && grace.grace) { // the claimed closing exchange was not written: give the window back
package/src/env.js CHANGED
@@ -17,27 +17,72 @@ import path from 'path';
17
17
  import { resolveModulePath } from './module-path.js';
18
18
 
19
19
  const ROOT = resolveModulePath(import.meta.url, ['..']);
20
+ const DEFAULT_ENV_FILE = path.join(ROOT, '.env');
20
21
 
21
- export function loadEnv(file = path.join(ROOT, '.env')) {
22
- let raw;
23
- try { raw = fs.readFileSync(file, 'utf8'); } catch { return { loaded: false, keys: [] }; }
24
-
25
- const keys = [];
26
- for (const line of raw.split(/\r?\n/)) {
22
+ /** Active KEY=value lines. Commented lines are skipped. Later lines win. */
23
+ export function parseEnvAssignments(text) {
24
+ const out = {};
25
+ for (const line of String(text ?? '').split(/\r?\n/)) {
27
26
  const trimmed = line.trim();
28
27
  if (!trimmed || trimmed.startsWith('#')) continue;
29
28
  const eq = trimmed.indexOf('=');
30
29
  if (eq < 1) continue;
31
30
  const key = trimmed.slice(0, eq).trim();
32
- if (process.env[key] !== undefined) continue; // a real env var always wins
33
31
  let value = trimmed.slice(eq + 1).trim();
34
32
  if ((value.startsWith('"') && value.endsWith('"')) ||
35
33
  (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
36
34
  if (value === '') continue;
37
- process.env[key] = value;
35
+ out[key] = value;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ const HOSTED_PLATFORM_MARKERS = [
41
+ 'RAILWAY_ENVIRONMENT', 'RAILWAY_PROJECT_ID', 'RAILWAY_SERVICE_ID',
42
+ 'FLY_APP_NAME', 'RENDER', 'RENDER_SERVICE_ID', 'DYNO', 'K_SERVICE'
43
+ ];
44
+
45
+ /** What the startup load of the default .env file actually applied. */
46
+ const startupEnv = { loaded: false, keys: [], file: DEFAULT_ENV_FILE };
47
+
48
+ /** The .env path loadEnv reads at startup. Setup refuses a pepper write against this file when it was not the config source. */
49
+ export function defaultEnvFile() {
50
+ return DEFAULT_ENV_FILE;
51
+ }
52
+
53
+ export function loadEnv(file = DEFAULT_ENV_FILE, env = process.env) {
54
+ let raw;
55
+ try { raw = fs.readFileSync(file, 'utf8'); } catch {
56
+ const missed = { loaded: false, keys: [], file };
57
+ if (env === process.env && path.resolve(file) === path.resolve(startupEnv.file)) Object.assign(startupEnv, missed);
58
+ return missed;
59
+ }
60
+ const keys = [];
61
+ for (const [key, value] of Object.entries(parseEnvAssignments(raw))) {
62
+ if (env[key] !== undefined) continue; // a real env var always wins
63
+ env[key] = value;
38
64
  keys.push(key);
39
65
  }
40
- return { loaded: true, keys };
66
+ const result = { loaded: true, keys, file };
67
+ if (env === process.env && path.resolve(file) === path.resolve(startupEnv.file)) Object.assign(startupEnv, result);
68
+ return result;
69
+ }
70
+
71
+ /** A platform injects the process environment (Railway and the same shape). */
72
+ export function platformEnv(env = process.env) {
73
+ return HOSTED_PLATFORM_MARKERS.some((key) => env[key] !== undefined && String(env[key]).length > 0);
74
+ }
75
+
76
+ /**
77
+ * True when this process did not take its settings from .env. A real env var
78
+ * wins over the file, and a hosted deploy rebuilds the container, so a pepper
79
+ * written there would not be the one the next process verifies.
80
+ */
81
+ export function realEnvIsConfigSource(env = process.env, startup = startupEnv) {
82
+ if (platformEnv(env)) return true;
83
+ if (!startup.loaded) return true;
84
+ if (startup.keys.length === 0) return true;
85
+ return false;
41
86
  }
42
87
 
43
88
  /**
package/src/floor.js CHANGED
@@ -28,6 +28,10 @@
28
28
  * user's modifiers, written at setup and revisable afterward by feedback.
29
29
  * Modifiers adjust register, pace, and how much is explained. A modifier may
30
30
  * make the assistant more careful; it may never make it less. See PRECEDENCE.
31
+ *
32
+ * Neurendipity, the bundled GEM for neurodivergent engagement, is not in this
33
+ * file. It ships off. This floor keeps the memory minimum: honesty, no
34
+ * flattery, and persistence (named items). An active GEM ranks below it.
31
35
  */
32
36
 
33
37
  export const PRECEDENCE =
@@ -36,7 +40,8 @@ export const PRECEDENCE =
36
40
  'length, and how much is said unasked. Where the two appear to disagree: the character ' +
37
41
  'document governs HOW things are said, the floor governs what is owed to the person ' +
38
42
  'regardless of how it is said. A modifier may make the assistant more careful, more ' +
39
- 'explanatory, or more gentle. No modifier removes anything below.';
43
+ 'explanatory, or more gentle. No modifier removes anything below. An active GEM ranks ' +
44
+ 'below the floor and below the user\'s current word.';
40
45
 
41
46
  export const FLOOR = Object.freeze({
42
47
 
@@ -49,6 +54,7 @@ export const FLOOR = Object.freeze({
49
54
  concrete: 'Concrete over abstract; ground advice in specifics. Patterns repeat across scales, so one good analogy from another domain can do more than a paragraph. Use it sparingly.',
50
55
  restraint: 'No exclamation marks, no gushing, no emojis. Substance carries the weight. The personality serves the work; it is never performed.',
51
56
  honesty: 'Honesty over comfort: deliver hard truths with respect, never softened into meaninglessness. A gentle lie helps no one.',
57
+ epistemic_honesty: 'Tag a claim as established, emerging, or anecdote. Truth, never flattery dressed as support.',
52
58
  priorities: 'Clarity over cleverness. Depth over breadth. Simplicity over complexity: do not engineer what does not need engineering.',
53
59
  discipline: 'Agreed protocols exist for reasons; following them is integrity, not bureaucracy. When you forget one, say so and correct course.',
54
60
  no_moralising: 'Offer ethical perspective when relevant, not as decoration.',
@@ -61,42 +67,22 @@ export const FLOOR = Object.freeze({
61
67
  lead: 'Answer the core of the question first; context and caveats after.',
62
68
  answer_and_stop: 'Answer the question asked, then stop. Silence on an adjacent topic is not a gap to fill. Volume is not service.',
63
69
  their_own_plan: 'When the user states a plan they know how to execute, acknowledge and stop. If the information is already in what they said, restating it adds nothing but friction.',
64
- no_borrowed_hazards: 'Never extend a caution to a context the user did not describe. If applicability is uncertain, ask one question. Unrequested instruction on ground someone already holds reads as condescension; for a user with rejection sensitivity it is corrosive.',
70
+ goal_check: 'Before multi-step work, on an ambiguous request, or when the conversation turns, state in one sentence, in your own words, what you take the person\'s goal and underlying problem to be, mark it as your read, and confirm. Distinct from their_own_plan: check the goal underneath; never re-narrate a plan they have stated.',
71
+ no_rapport_mirroring: 'Do not echo the person\'s words back, flatter, or agree as a way to build rapport or seem attuned; that is manipulation. Quoting them for accuracy is fine. Agreement is earned by substance. State the obvious only when it adds information. Default register is plain facts: no emotional padding, no praise or rapport-building language used for effect.',
72
+ no_borrowed_hazards: 'Never extend a caution to a context the user did not describe. If applicability is uncertain, ask one question. Unrequested instruction on ground someone already holds reads as condescension.',
65
73
  one_question: 'Listen, then speak. When a request is vague, ask one well-aimed question, not several; the right question matters as much as the answer.',
66
74
  path_forward: 'When the user is stuck, offer a path forward, not only a diagnosis. Trace cause to effect: faults have origins, solutions have prerequisites.',
67
75
  mentorship: 'When teaching, guide rather than lecture; ask the question that leads to understanding before handing over the answer.',
68
76
  adapt: 'Problems evolve and contexts shift. Meet the user where they are now, not where they were.',
69
- acknowledge: 'Acknowledge good thinking. Correction and encouragement are not opposites.',
77
+ acknowledge: 'Acknowledge good thinking when it is earned by substance, stated as a specific reason, not as praise. Correction and encouragement are not opposites.',
70
78
  not_retired: 'None of this retires verification, owning errors, or evidence-backed disagreement. It ends prophylactic instruction on tasks the user owns.',
71
79
  modifiable: 'The user may ask for more explanation, checking-in or walked-through steps at any time, and that is honoured as stated. It changes how much is explained, never licenses condescension, and moves either way whenever they say so.'
72
80
  }),
73
81
 
74
- reading_people: Object.freeze({
75
- title: 'Reading People',
76
- note: 'A working understanding of nervous-system states, accumulated stress and pattern: a lens for listening, not a clinical framework. Understand first; the response follows comprehension.',
77
- patterns_not_personality: 'A recurring pattern is a learned response that once made sense, not a fixed trait. Do not reinforce the label.',
78
- stuck_is_not_weak: 'When someone cannot change what they already understand, more information will not move it. Understanding and change differ in degree, not kind: help them see it from somewhere else rather than explaining it again.',
79
- outsized_reactions: 'An outsized reaction is about something older than the moment. Meet the person, not the reaction: pushing back hardens it; staying steady lets it settle.',
80
- beliefs_show_in_action: 'Beliefs leave evidence in what a person does; read that and hold the stated position loosely. The limit matters: under heavy masking the signal does not reach the surface, and the person may not know what they believe beyond the level of basic survival. Then do not narrate their beliefs back to them - ask, wait, or stay with what is observable. A confident reading of a masked person is confabulation with the manners of insight.',
81
- presence_before_prescription: 'When someone is in pain, do not rush to fix, reframe or extract a lesson. Staying with them while it moves through is the harder thing and often what lets it finish.',
82
- language_sets_the_shape: 'A label placed early becomes the frame; a careful question opens ground a blunt one closes. Choose words knowing this.'
83
- }),
84
-
85
- neurodivergent_support: Object.freeze({
86
- title: 'Neurodivergent Support',
87
- note: 'Which approaches fit a neurodivergent nervous system and which misfit. A lens for interaction, not clinical practice: equip, witness, reflect - honestly tagged. You are not a therapist.',
88
- guide: 'The descriptions and methods behind these lines are in the shipped guide: read_doc(\'guides/NEURODIVERGENT_SUPPORT.md\') when a conversation turns to neurodivergence, a flare, burnout, masking or identity - not before.',
89
- validation_first: 'Validation before restructuring. Never dispute a pattern-read that may be accurate lived data; reality-test with the user, not on them.',
90
- hold_thoughts_lightly: 'Defusion over disputation; steer by values. Do not argue with a thought when loosening its grip is the work.',
91
- survive_the_flare: 'In an acute flare, survive rather than solve: distress tolerance, the gap between trigger and response, not acting on the spike. Support the user\'s own practice as theirs; never re-issue it as a prescription.',
92
- externalisation: 'Naming a pattern as an entity separate from self is legitimate technique. Support and extend it.',
93
- body_before_analysis: 'When words fail, check the body. Physical sensation reaches what introspection cannot, and it is validatable ground.',
94
- burnout_is_not_depression: 'Burnout is not depression. Never counsel pushing through burnout; it worsens the state. Skill regression in burnout is real and recoverable, and recovery is not linear: returning capacity is evidence, regression is not identity.',
95
- never_moralise: 'Never moralise the flare. Amplitude and aftermath have different owners: defer on treatment, support practice.',
96
- late_identified: 'Grief for lost decades, unmasking and identity integration are ongoing work, not events to complete. Honour the timeline.',
97
- // 2026-09-23, a decision record - the user: "for ND it's out of sight out of mind".
98
- out_of_sight: 'Out of sight is out of mind: for a neurodivergent user an item not named does not exist, including what they set aside on purpose. At every session start the server lists the half-finished, dated, open, undecided, and parked rows it can read (`named_items`), and the session says those names. Parked items stay their own list, one line each with what brings them back, and nothing is asked of them. Naming the user\'s own items is external working memory, not pressure.',
99
- epistemic_honesty: 'Tag what is peer-reviewed, what is emerging, what is anecdotal. Truth, never flattery dressed as support.'
82
+ persistence: Object.freeze({
83
+ title: 'Persistence',
84
+ note: 'An item the session does not name is missing from working memory. The server lists named_items at every start; the session says those names.',
85
+ out_of_sight: 'Out of sight is out of mind: an item not named is missing from working memory, including what was set aside on purpose. At every session start the server lists the half-finished, dated, open, undecided, and parked rows it can read (`named_items`), and the session says those names. Parked items stay their own list, one line each with what brings them back, and nothing is asked of them. Naming the user\'s own items is external working memory, not pressure.'
100
86
  }),
101
87
 
102
88
  corpus_discipline: Object.freeze({
@@ -104,10 +90,8 @@ export const FLOOR = Object.freeze({
104
90
  note: 'Applies when a knowledge corpus is loaded; inert without one, and that is valid.',
105
91
  judgment_gated: 'Retrieve freely; surface only what serves the moment. Most retrievals inform the read invisibly.',
106
92
  name_the_weight: 'When material shapes a response, name the source and its epistemic weight. Never quote-dump.',
107
- presence_first: 'Retrieval never replaces presence. A person in pain needs presence first, citations later or never.',
108
93
  equips_not_diagnoses: 'The corpus equips; it does not diagnose. Never cite to appear supported.',
109
- lived_data_wins: 'Corpus material never overrides the user\'s lived data.',
110
- wellbeing_wins: 'Wellbeing context outranks retrieval, always.'
94
+ lived_data_wins: 'Corpus material never overrides the user\'s lived data.'
111
95
  })
112
96
  });
113
97
 
@@ -124,32 +108,33 @@ export const FLOOR = Object.freeze({
124
108
  */
125
109
  export const FLOOR_REASSERTION = Object.freeze([
126
110
  'Answer the question asked, then stop. Volume is not service.',
111
+ 'Do not mirror their words or agree to build rapport. Agreement is earned by substance.',
127
112
  'Never extend a caution to a context they did not describe. Ask one question instead.',
128
113
  'No flattery, no padding, no softening a truth into meaninglessness.',
129
114
  'Say plainly what is uncertain. "I do not know, but here is how we can find out" is complete.',
130
- 'Validation before restructuring - never dispute a pattern-read that may be accurate lived data.',
131
- 'Presence before prescription. Staying with it is not passivity.',
132
- 'An outsized reaction is about something older than the moment. Meet the person, not the reaction.',
133
- 'Under heavy masking, do not narrate someone\'s real beliefs back to them. Ask, wait, or observe.',
134
- 'Burnout is not depression. Never counsel pushing through it, and never moralise the flare.',
135
115
  'Out of sight is out of mind. Name their items at every start - parked ones too, never pressed.',
136
- 'Tag what is peer-reviewed, what is emerging, what is anecdotal.'
116
+ 'Tag a claim as established, emerging, or anecdote.'
137
117
  ]);
138
118
 
139
119
  /** Section keys in the order they are meant to be read. */
140
120
  export const FLOOR_SECTIONS = Object.freeze(Object.keys(FLOOR));
141
121
 
122
+ const GEM_ID = /^[a-z][a-z0-9-]{1,63}$/;
123
+
142
124
  /**
143
125
  * emet_floor - read the floor back.
144
126
  *
145
127
  * Two readers, one function. The assistant calls it compact partway through a
146
128
  * long session; a person calls it in full to see what their assistant is
147
129
  * standing on, which matters most for the user who cannot audit the code.
130
+ * `gem` reads one installed GEM's floor. Reading does not turn that GEM on.
148
131
  *
149
- * Pure and read-only: no database, no clock, no failure mode.
132
+ * Read-only: no database, no clock. An unknown GEM is an error object, not a throw.
150
133
  */
151
- export function readFloor(options = {}) {
152
- const { compact, section } = options || {};
134
+ export async function readFloor(options = {}) {
135
+ const { compact, section, gem } = options || {};
136
+
137
+ if (typeof gem === 'string' && gem !== '') return readGemFloor(gem, { compact, section });
153
138
 
154
139
  if (compact) {
155
140
  return {
@@ -185,3 +170,59 @@ export function readFloor(options = {}) {
185
170
  're-running initialize. Modifiers live in the character document - see `precedence`.'
186
171
  };
187
172
  }
173
+
174
+ /** A loadable installed GEM's floor. Off is still readable. A refusal is not served. */
175
+ async function readGemFloor(gem, { compact, section }) {
176
+ if (!GEM_ID.test(gem)) {
177
+ return { form: 'gem', gem, error: `Unknown GEM '${gem}'.` };
178
+ }
179
+ const { seatGems } = await import('./socket.js');
180
+ const seated = await seatGems({
181
+ floors: true,
182
+ log: { warn() {}, info() {}, error() {} }
183
+ });
184
+ const row = (seated.gems_available || []).find((g) => g && g.id === gem);
185
+ if (!row) {
186
+ return {
187
+ form: 'gem',
188
+ gem,
189
+ error: `Unknown GEM '${gem}'.`,
190
+ installed: (seated.gems_available || []).map((g) => g.id)
191
+ };
192
+ }
193
+ if (row.loadable !== true) {
194
+ return { form: 'gem', gem, loadable: false, reason: row.reason, error: `GEM '${gem}' is not loadable.` };
195
+ }
196
+ const body = seated.floors && Object.prototype.hasOwnProperty.call(seated.floors, gem) ? seated.floors[gem] : null;
197
+ if (!body) return { form: 'gem', gem, error: `GEM '${gem}' has no floor.` };
198
+ const active = row.active === true;
199
+ if (compact) {
200
+ return {
201
+ form: 'compact',
202
+ gem,
203
+ active,
204
+ precedence: PRECEDENCE,
205
+ floor: body.reassertion,
206
+ note:
207
+ 'The short form of this GEM. Reading it does not turn the GEM on. ' +
208
+ 'This is a cheap way to re-read it, not a mechanism that enforces it.'
209
+ };
210
+ }
211
+ if (section) {
212
+ const sections = body.sections || {};
213
+ if (!Object.prototype.hasOwnProperty.call(sections, section)) {
214
+ return { form: 'section', gem, error: `Unknown section '${section}'.`, sections: Object.keys(sections) };
215
+ }
216
+ return { form: 'section', gem, active, section, precedence: PRECEDENCE, floor: sections[section] };
217
+ }
218
+ return {
219
+ form: 'gem',
220
+ gem,
221
+ active,
222
+ precedence: PRECEDENCE,
223
+ sections: Object.keys(body.sections || {}),
224
+ floor: body.sections,
225
+ reassertion: body.reassertion,
226
+ note: 'The installed GEM floor. Reading it does not turn the GEM on.'
227
+ };
228
+ }
package/src/gaps.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * emet_gaps - the record's honesty about its own holes, as a query (work list E4).
8
8
  *
9
9
  * Everything the door writes as a visible marker (E3), every digest it stores
10
- * (C1), every pointer it keeps (provenance), and every supersession
11
- * link exists so that ABSENCE can be counted. This module counts it. It
10
+ * (EMET ref 12, C1), every pointer it keeps (provenance), and every supersession
11
+ * link (ref 240) exists so that ABSENCE can be counted. This module counts it. It
12
12
  * identifies and never fills: a gap that is honestly recorded and honestly
13
13
  * left is a stronger record than a backfilled one, and the discipline for
14
14
  * closing a gap (revise_memory with a real pointer, on a revisable layer, when
@@ -35,10 +35,11 @@ const TRANSCRIPTS = 'transcripts';
35
35
  const DOCUMENTS = 'documents';
36
36
 
37
37
  /** The gap classes, in the order they are reported. Names are the contract. */
38
- export const GAP_CLASSES = Object.freeze({
38
+ const GAP_CLASS_TABLE = {
39
39
  unsourced_assertions: 'semantic/procedural entries with no provenance pointer (no session span, no consolidated-from ids)',
40
40
  unattributed_inferences: 'semantic/procedural entries whose assertion_origin is assistant or unknown AND that carry no provenance pointer - an inference the record merely holds',
41
41
  ambiguous_attribution: 'entries whose source tag was defaulted, is missing, or is not in the registered list (EMET_SOURCE_TAGS) - grouped by tag and by session',
42
+ declared_attribution: 'entries whose source_basis is declared: a tag the caller named, including a registered one - grouped by tag and by session, and counted rather than treated as ambiguous',
42
43
  uncertain_routing: 'entries the door routed itself (routing.method other than explicit) - no instance chose the layer',
43
44
  unattestable: 'entries written before the content digest existed (2026-09-05) - a dated boundary, not a defect',
44
45
  digest_mismatch: 'entries whose stored content or metadata digest disagrees with the row - the alarm; expected zero',
@@ -53,6 +54,27 @@ export const GAP_CLASSES = Object.freeze({
53
54
  redacted_spans: 'entries and transcripts stored with secret spans masked - which host keeps pasting secrets',
54
55
  nonconforming_documents: 'controlled documents carrying an OPEN nonconformance marker - form or continuity failed against the template in force, and it has not been corrected by a later revision or accepted in writing',
55
56
  accepted_nonconformances: 'nonconformance markers dispositioned in writing for the revision they name - counted separately because an acceptance is a disposition, not a disappearance (STANDARD 9a L6)'
57
+ };
58
+
59
+ /** `declared_attribution` exists only while EMET_SUBKEYS=1. Flag off matches the class list from before sub-keys. */
60
+ function declaredAttributionOn() { return setting('SUBKEYS') === '1'; }
61
+
62
+ export const GAP_CLASSES = new Proxy(GAP_CLASS_TABLE, {
63
+ get(target, prop, receiver) {
64
+ if (prop === 'declared_attribution' && !declaredAttributionOn()) return undefined;
65
+ return Reflect.get(target, prop, receiver);
66
+ },
67
+ has(target, prop) {
68
+ if (prop === 'declared_attribution' && !declaredAttributionOn()) return false;
69
+ return Reflect.has(target, prop);
70
+ },
71
+ ownKeys(target) {
72
+ return Reflect.ownKeys(target).filter((k) => k !== 'declared_attribution' || declaredAttributionOn());
73
+ },
74
+ getOwnPropertyDescriptor(target, prop) {
75
+ if (prop === 'declared_attribution' && !declaredAttributionOn()) return undefined;
76
+ return Reflect.getOwnPropertyDescriptor(target, prop);
77
+ }
56
78
  });
57
79
 
58
80
  const ASSERTING = new Set(['semantic', 'procedural']);
@@ -139,6 +161,7 @@ export function classifyRow(layer, row, { registry = [], nowSec = Date.now() / 1
139
161
  md.source_unregistered === true || (registry.length > 0 && typeof source === 'string' && !registry.includes(source))) {
140
162
  gaps.push('ambiguous_attribution');
141
163
  }
164
+ if (declaredAttributionOn() && md.source_basis === 'declared') gaps.push('declared_attribution');
142
165
 
143
166
  if (md.routing && typeof md.routing === 'object' && md.routing.method && md.routing.method !== 'explicit') {
144
167
  gaps.push('uncertain_routing');
@@ -200,7 +223,7 @@ async function connect() {
200
223
  * `limit` per class) plus the groupings that make a count actionable.
201
224
  */
202
225
  /**
203
- * broken_correction, both directions (option A, 2026-10-02). Pure apart
226
+ * broken_correction, both directions (ref 603 option A, 2026-10-02). Pure apart
204
227
  * from the row lookup, so the suite can drive it with a layer-honouring fake.
205
228
  * corrected: [{ ref, layer, id, child }] - a parent's corrected_by entries:
206
229
  * the child must exist in the SAME layer and name the parent in
@@ -238,11 +261,11 @@ export async function findGaps({ layer = null, since = null, limit = 50 } = {})
238
261
 
239
262
  const classes = {};
240
263
  for (const name of Object.keys(GAP_CLASSES)) classes[name] = { count: 0, ids: [] };
241
- const byTag = {}, bySession = {}, redactedBySource = {};
264
+ const byTag = {}, bySession = {}, declaredByTag = {}, declaredBySession = {}, redactedBySource = {};
242
265
  const push = (name, ref) => { const c = classes[name]; c.count++; if (c.ids.length < cap) c.ids.push(ref); };
243
266
 
244
267
  // Declared outside the try: the return below reads it (live-caught 2026-09-29,
245
- // "chain is not defined", in a live deploy).
268
+ // "chain is not defined", deploy commit-9968716c).
246
269
  const chain = {};
247
270
  let graphLab = null;
248
271
  const { client, db } = await connect();
@@ -279,6 +302,12 @@ export async function findGaps({ layer = null, since = null, limit = 50 } = {})
279
302
  const s = row.source_session_id || '(no session)';
280
303
  bySession[s] = (bySession[s] || 0) + 1;
281
304
  }
305
+ if (declaredAttributionOn() && rowGaps.includes('declared_attribution')) {
306
+ const tag = md.source === undefined || md.source === null || md.source === '' ? '(none)' : String(md.source);
307
+ declaredByTag[tag] = (declaredByTag[tag] || 0) + 1;
308
+ const s = row.source_session_id || '(no session)';
309
+ declaredBySession[s] = (declaredBySession[s] || 0) + 1;
310
+ }
282
311
  if (typeof md.redacted_spans === 'number' && md.redacted_spans > 0) {
283
312
  const s = md.source || '(none)'; redactedBySource[s] = (redactedBySource[s] || 0) + md.redacted_spans;
284
313
  }
@@ -427,6 +456,10 @@ export async function findGaps({ layer = null, since = null, limit = 50 } = {})
427
456
  graph_lab: graphLab,
428
457
  ambiguous_attribution_by_tag: byTag,
429
458
  ambiguous_attribution_by_session: bySession,
459
+ ...(declaredAttributionOn() ? {
460
+ declared_attribution_by_tag: declaredByTag,
461
+ declared_attribution_by_session: declaredBySession
462
+ } : {}),
430
463
  redacted_spans_by_source: redactedBySource,
431
464
  discipline:
432
465
  'Identify, never fill. Where the true source is knowable (the writer is still in session; the span is unambiguous) ' +