@dreamforgestudiollc/emet 0.0.0-stage → 0.2.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.
Files changed (73) hide show
  1. package/.env.example +136 -0
  2. package/CHANGELOG.md +296 -0
  3. package/CHARTER.md +125 -0
  4. package/LICENSE +22 -0
  5. package/README.md +783 -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 +79 -0
  10. package/docs/COVERAGE.md +64 -0
  11. package/docs/DEPLOY.md +246 -0
  12. package/docs/DISCIPLINES.md +29 -0
  13. package/docs/GUARDRAILS-AND-MONORAILS.md +107 -0
  14. package/docs/GUIDED-SETUP.md +296 -0
  15. package/docs/PERMISSIONS.md +118 -0
  16. package/docs/ROADMAP.md +59 -0
  17. package/docs/SETUP.md +259 -0
  18. package/guides/NEURODIVERGENT_SUPPORT.md +278 -0
  19. package/package.json +70 -4
  20. package/server.json +39 -0
  21. package/src/aleph.js +395 -0
  22. package/src/annotations.js +135 -0
  23. package/src/approve-throttle.js +54 -0
  24. package/src/atlas-role.js +58 -0
  25. package/src/conformance.js +688 -0
  26. package/src/content_analyzer.js +450 -0
  27. package/src/database.js +811 -0
  28. package/src/disciplines.js +26 -0
  29. package/src/dispatch.js +279 -0
  30. package/src/env.js +52 -0
  31. package/src/floor.js +187 -0
  32. package/src/gaps.js +476 -0
  33. package/src/guides.js +48 -0
  34. package/src/http-remote.js +269 -0
  35. package/src/index.js +390 -0
  36. package/src/invites.js +167 -0
  37. package/src/layers.js +449 -0
  38. package/src/lessons-to-checks.js +366 -0
  39. package/src/logger.js +299 -0
  40. package/src/metadata.js +61 -0
  41. package/src/module-path.js +32 -0
  42. package/src/mongo-connect.js +48 -0
  43. package/src/named-items.js +208 -0
  44. package/src/oauth-provider.js +838 -0
  45. package/src/persons.js +116 -0
  46. package/src/provenance.js +257 -0
  47. package/src/redact.js +231 -0
  48. package/src/request-log.js +124 -0
  49. package/src/scopes.js +97 -0
  50. package/src/security-headers.js +26 -0
  51. package/src/session-graph.js +945 -0
  52. package/src/session.js +1097 -0
  53. package/src/setup.js +1313 -0
  54. package/src/startup-pages.js +354 -0
  55. package/src/startup-payload.js +126 -0
  56. package/src/tools.js +4271 -0
  57. package/src/transcript-session.js +106 -0
  58. package/src/validation.js +1163 -0
  59. package/src/version.js +2 -0
  60. package/templates/BOARD.md +72 -0
  61. package/templates/BOTHANDOFF.md +57 -0
  62. package/templates/CHARACTER.md +30 -0
  63. package/templates/HANDOFF.md +63 -0
  64. package/templates/IDENTITY.md +29 -0
  65. package/templates/INDEX.md +82 -0
  66. package/templates/INSTALL.md +29 -0
  67. package/templates/PROCEDURE.md +39 -0
  68. package/templates/ROSTER.md +43 -0
  69. package/templates/SEED.md +65 -0
  70. package/templates/STAGING.md +37 -0
  71. package/templates/STANDARD.md +99 -0
  72. package/templates/THREAD.md +43 -0
  73. package/templates/USER.md +29 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ *
5
+ * Licensed under the MIT License - see LICENSE
6
+ *
7
+ * metadata: ONE place that knows the field has had two shapes.
8
+ *
9
+ * Until 2026-09-05 every layer entry stored `metadata` as a JSON STRING. Mongo
10
+ * cannot see inside a string, so nothing in it was queryable or indexable -
11
+ * `metadata.superseded_by` matched zero documents (probe, 2026-08-24). Two
12
+ * fields that bit in practice, superseded_by and valid_until, were promoted to
13
+ * top-level columns as a targeted fix; the provenance pointer was written
14
+ * top-level from the start for the same reason. That fixed instances. This
15
+ * fixes the class: metadata is now stored as a real subdocument, and the
16
+ * corpus was migrated (scripts/migrate_metadata_to_subdocument.mjs).
17
+ *
18
+ * Readers still go through parseMetadata() rather than trusting the shape,
19
+ * because a store that was never migrated - or one document a migration
20
+ * skipped - must read correctly rather than throw. Tolerance on read is what
21
+ * makes the write-side change safe to ship ahead of the migration.
22
+ */
23
+
24
+ /**
25
+ * Return metadata as an object whatever shape it was stored in.
26
+ * - object -> returned as-is
27
+ * - string -> JSON.parse; an unparseable string yields {} (the read must not
28
+ * throw over one bad row, and nothing is lost - the row is untouched)
29
+ * - null / undefined / anything else -> {}
30
+ */
31
+ export function parseMetadata(value) {
32
+ if (value === null || value === undefined) return {};
33
+ if (typeof value === 'string') {
34
+ try {
35
+ const parsed = JSON.parse(value);
36
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+ if (typeof value === 'object' && !Array.isArray(value)) return value;
42
+ return {};
43
+ }
44
+
45
+ /** True when a stored metadata value is the pre-2026-09-05 JSON-string shape. */
46
+ export function isLegacyMetadataString(value) {
47
+ return typeof value === 'string';
48
+ }
49
+
50
+ /**
51
+ * Is this a column the SQL dialect may translate? Top-level columns come from
52
+ * the caller's whitelist; metadata sub-paths are allowed because the field is a
53
+ * subdocument now, provided the path is plain identifier segments - no operators,
54
+ * no `$`, nothing that could be read by Mongo as anything but a field path.
55
+ */
56
+ const METADATA_PATH = /^metadata(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
57
+ export function isMetadataPath(col) {
58
+ return typeof col === 'string' && METADATA_PATH.test(col);
59
+ }
60
+
61
+ export default { parseMetadata, isLegacyMetadataString, isMetadataPath };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ *
5
+ * Licensed under the MIT License - see LICENSE
6
+ *
7
+ * A module URL is not a filesystem path. On Windows, `new URL(href).pathname`
8
+ * is `/C:/...`. `path.resolve` against a working directory on `C:` then joins
9
+ * that into `C:\C:\...`, and opening the file fails. `fileURLToPath` decodes
10
+ * the URL first; `path.resolve` walks from that path's directory.
11
+ */
12
+
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ /**
17
+ * Absolute path of a file URL, then any relative segments. A leading `..`
18
+ * leaves the module's own directory.
19
+ *
20
+ * `opts.path` and `opts.toPath` exist so a test can pass `path.win32` and a
21
+ * Windows `fileURLToPath` without running on Windows. Production callers omit
22
+ * them and get the host path module.
23
+ *
24
+ * @param {string|URL} fileUrl
25
+ * @param {string[]} [segments]
26
+ * @param {{ path?: typeof path, toPath?: (url: string|URL) => string }} [opts]
27
+ */
28
+ export function resolveModulePath(fileUrl, segments = [], opts = {}) {
29
+ const pathImpl = opts.path || path;
30
+ const toPath = opts.toPath || fileURLToPath;
31
+ return pathImpl.resolve(pathImpl.dirname(toPath(fileUrl)), ...segments);
32
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * EMET - the one way to open a MongoDB connection (2026-09-29).
3
+ *
4
+ * Some local resolvers (home routers, VPN DNS) refuse
5
+ * the SRV queries a mongodb+srv:// URI needs, and the driver fails with
6
+ * "querySrv ECONNREFUSED". Until today the public-DNS fallback lived only in
7
+ * EmetDatabase.ensureClient; nine other connections in src/ and
8
+ * scripts/chain-verify.mjs opened their own client without it, so every tool
9
+ * behind them failed on a machine with such a resolver while the layer tools
10
+ * worked. One helper, used everywhere, so the fallback cannot be forgotten in
11
+ * a tenth place.
12
+ *
13
+ * On a querySrv failure: switch this process to public DNS (8.8.8.8, 1.1.1.1)
14
+ * and retry once. dns.setServers is process-wide, so every later connection
15
+ * in the same process resolves first time. Any other failure is rethrown as
16
+ * it came.
17
+ */
18
+
19
+ import dns from 'dns';
20
+ import { MongoClient } from 'mongodb';
21
+
22
+ export const PUBLIC_DNS = Object.freeze(['8.8.8.8', '1.1.1.1']);
23
+
24
+ export function isSrvRefusal(error) {
25
+ return /querySrv/i.test(String(error && error.message));
26
+ }
27
+
28
+ /**
29
+ * Test seam (2026-10-01): the suites install an in-memory client so a tool
30
+ * that connects for itself (emet_transcript_append) can be run end to end
31
+ * with no database. Never set outside tests; null means the real driver.
32
+ */
33
+ let testClient = null;
34
+ export function useTestClient(Client) { testClient = Client || null; }
35
+ export function testClientActive() { return testClient != null; }
36
+
37
+ /** Connect and return the client. `onFallback(message)` is told when the retry happens. */
38
+ export async function connectMongo(uri, options = {}, { onFallback = null, _Client = testClient || MongoClient, _dns = dns } = {}) {
39
+ const attempt = async () => { const c = new _Client(uri, options); await c.connect(); return c; };
40
+ try {
41
+ return await attempt();
42
+ } catch (error) {
43
+ if (!isSrvRefusal(error)) throw error;
44
+ if (onFallback) onFallback(`SRV DNS lookup refused by the local resolver - retrying with public DNS (${PUBLIC_DNS.join(', ')})`);
45
+ _dns.setServers([...PUBLIC_DNS]);
46
+ return await attempt();
47
+ }
48
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Names the board and handoff already hold, so a session start can say them.
3
+ *
4
+ * The board template keeps half-finished work in §1, dates in §2, and open,
5
+ * waiting, and parked rows in §3. The handoff keeps what was left undecided.
6
+ * A section that is not a table is marked unread. An empty list is not a
7
+ * claim that the document has nothing in it.
8
+ */
9
+
10
+ function isPlaceholder(s) {
11
+ const t = String(s || '').trim();
12
+ if (!t || t === '__' || /^_+$/.test(t)) return true;
13
+ if (/^none\b/i.test(t)) return true;
14
+ return false;
15
+ }
16
+
17
+ function tableCells(line) {
18
+ return String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
19
+ }
20
+
21
+ function isSeparatorRow(cells) {
22
+ return cells.length > 0 && cells.every((c) => /^:?-{3,}:?$/.test(c));
23
+ }
24
+
25
+ function sectionBodies(text) {
26
+ const sections = new Map();
27
+ let key = null;
28
+ const buf = [];
29
+ const flush = () => {
30
+ if (key) sections.set(key, buf.join('\n'));
31
+ buf.length = 0;
32
+ };
33
+ for (const line of String(text || '').split(/\r?\n/)) {
34
+ const m = line.match(/^##\s+§(\d+)\b/);
35
+ if (m) { flush(); key = m[1]; }
36
+ else if (key) buf.push(line);
37
+ }
38
+ flush();
39
+ return sections;
40
+ }
41
+
42
+ function firstTable(sectionText) {
43
+ const lines = String(sectionText || '').split(/\r?\n/);
44
+ const start = lines.findIndex((l) => l.trim().startsWith('|'));
45
+ if (start < 0) return null;
46
+ const raw = [];
47
+ for (let i = start; i < lines.length; i++) {
48
+ if (!lines[i].trim().startsWith('|')) break;
49
+ raw.push(lines[i]);
50
+ }
51
+ if (!raw.length) return null;
52
+ const header = tableCells(raw[0]).map((h) => h.toLowerCase());
53
+ const data = [];
54
+ for (const line of raw.slice(1)) {
55
+ const cells = tableCells(line);
56
+ if (isSeparatorRow(cells)) continue;
57
+ data.push(cells);
58
+ }
59
+ return { header, data };
60
+ }
61
+
62
+ function colIndex(header, names) {
63
+ return header.findIndex((h) => names.some((n) => h === n || h.startsWith(n)));
64
+ }
65
+
66
+ function cell(row, header, names) {
67
+ const i = colIndex(header, names);
68
+ return i >= 0 ? (row[i] || '').trim() : '';
69
+ }
70
+
71
+ function nameOf(header, row) {
72
+ const named = cell(row, header, ['what', 'item']);
73
+ if (named) return named;
74
+ const i = header.findIndex((h, idx) => h !== 'id' && !isPlaceholder(row[idx]));
75
+ return i >= 0 ? (row[i] || '').trim() : '';
76
+ }
77
+
78
+ function addDaysIso(iso, n) {
79
+ const [y, m, d] = String(iso).split('-').map(Number);
80
+ if (!y || !m || !d) return null;
81
+ const dt = new Date(Date.UTC(y, m - 1, d));
82
+ dt.setUTCDate(dt.getUTCDate() + n);
83
+ return dt.toISOString().slice(0, 10);
84
+ }
85
+
86
+ function isoDateIn(text) {
87
+ const m = String(text || '').match(/\d{4}-\d{2}-\d{2}/);
88
+ return m ? m[0] : null;
89
+ }
90
+
91
+ function resumesWhen(statusRaw) {
92
+ const m = String(statusRaw || '').match(/resumes when:\s*(.+)$/i);
93
+ return m ? m[1].trim() : null;
94
+ }
95
+
96
+ function takeSection(board, sections, num, label, out) {
97
+ if (typeof board !== 'string') {
98
+ out.sections[label] = 'unread';
99
+ return null;
100
+ }
101
+ if (!sections.has(num)) {
102
+ out.sections[label] = 'unread';
103
+ return null;
104
+ }
105
+ const table = firstTable(sections.get(num));
106
+ // Prose is not a table the server could read. unread is not an empty list:
107
+ // the session still has to read the document.
108
+ out.sections[label] = table ? 'read' : 'unread';
109
+ return table;
110
+ }
111
+
112
+ export function undecidedFrom(text) {
113
+ if (typeof text !== 'string') return { section: 'unread', lines: [] };
114
+ const m = text.match(/\*\*Left undecided this session:\*\*([^\n]*)((?:\n(?!-\s+\*\*|##\s).*)*)/i);
115
+ if (!m) return { section: 'unread', lines: [] };
116
+ const lines = [];
117
+ const push = (s) => {
118
+ const t = String(s || '').trim().replace(/^[-*]\s+/, '').replace(/^:\s*/, '').trim();
119
+ if (!t || isPlaceholder(t)) return;
120
+ if (/one line each/i.test(t) || /^drafts, proposals/i.test(t)) return;
121
+ lines.push(t);
122
+ };
123
+ push(m[1]);
124
+ for (const line of m[2].split('\n')) push(line);
125
+ return { section: 'read', lines };
126
+ }
127
+
128
+ /**
129
+ * @param {string|null|undefined} board
130
+ * @param {string|null|undefined} handoff
131
+ * @param {string} today YYYY-MM-DD
132
+ * @param {{ board_doc?: string, handoff_doc?: string }} [ids]
133
+ */
134
+ export function namedItemsFrom(board, handoff, today, ids = {}) {
135
+ const day = typeof today === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(today)
136
+ ? today
137
+ : new Date().toISOString().slice(0, 10);
138
+ const end = addDaysIso(day, 7);
139
+ const out = {
140
+ board_doc: ids.board_doc || 'STATE.md',
141
+ handoff_doc: ids.handoff_doc || 'HANDOFF.md',
142
+ board_read: typeof board === 'string',
143
+ handoff_read: typeof handoff === 'string',
144
+ as_of: day,
145
+ half_finished: [],
146
+ dated: [],
147
+ open: [],
148
+ waiting: [],
149
+ parked: [],
150
+ undecided: [],
151
+ sections: {},
152
+ note: 'Say every name in this list. Parked names are their own list: say what brings each one back, and ask nothing of it. A section marked unread was not a table the server could read, so read that document and name what it holds. An empty list is not a reason to skip the document. Then name lessons_pending. A count never replaces the names.'
153
+ };
154
+ const sections = sectionBodies(board);
155
+ const half = takeSection(board, sections, '1', 'half_finished', out);
156
+ if (half) {
157
+ for (const row of half.data) {
158
+ const name = nameOf(half.header, row);
159
+ if (isPlaceholder(name)) continue;
160
+ out.half_finished.push({
161
+ name,
162
+ state: cell(row, half.header, ['state']) || null,
163
+ next_action: cell(row, half.header, ['next action']) || null,
164
+ id: cell(row, half.header, ['id']) || null
165
+ });
166
+ }
167
+ }
168
+ const dated = takeSection(board, sections, '2', 'dated', out);
169
+ if (dated) {
170
+ for (const row of dated.data) {
171
+ const item = nameOf(dated.header, row);
172
+ const dateRaw = cell(row, dated.header, ['date']);
173
+ if (isPlaceholder(item) && isPlaceholder(dateRaw)) continue;
174
+ const date = isoDateIn(dateRaw);
175
+ out.dated.push({
176
+ date: date || (isPlaceholder(dateRaw) ? null : dateRaw) || null,
177
+ name: isPlaceholder(item) ? null : item,
178
+ owner: cell(row, dated.header, ['owner']) || null,
179
+ id: cell(row, dated.header, ['id']) || null,
180
+ within_seven_days: date && end ? date >= day && date <= end : null
181
+ });
182
+ }
183
+ }
184
+ const open = takeSection(board, sections, '3', 'open', out);
185
+ if (open) {
186
+ for (const row of open.data) {
187
+ const name = nameOf(open.header, row);
188
+ if (isPlaceholder(name)) continue;
189
+ const statusRaw = cell(row, open.header, ['status']);
190
+ const status = (statusRaw || 'OPEN').toUpperCase();
191
+ const entry = {
192
+ name,
193
+ status: status.split(/\s+/)[0],
194
+ next_action: cell(row, open.header, ['next action']) || null,
195
+ owner: cell(row, open.header, ['owner']) || null,
196
+ id: cell(row, open.header, ['id']) || null,
197
+ resumes_when: cell(row, open.header, ['resumes when']) || resumesWhen(statusRaw) || null
198
+ };
199
+ if (status.startsWith('PARKED')) out.parked.push(entry);
200
+ else if (status.startsWith('WAITING')) out.waiting.push(entry);
201
+ else out.open.push(entry);
202
+ }
203
+ }
204
+ const und = undecidedFrom(handoff);
205
+ out.sections.undecided = und.section;
206
+ out.undecided = und.lines;
207
+ return out;
208
+ }