@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.
- package/.env.example +136 -0
- package/CHANGELOG.md +296 -0
- package/CHARTER.md +125 -0
- package/LICENSE +22 -0
- package/README.md +783 -2
- package/docs/AUTHORSHIP.md +26 -0
- package/docs/BENCHMARKS.md +261 -0
- package/docs/BOOTSTRAP.md +127 -0
- package/docs/BOTS.md +79 -0
- package/docs/COVERAGE.md +64 -0
- package/docs/DEPLOY.md +246 -0
- package/docs/DISCIPLINES.md +29 -0
- package/docs/GUARDRAILS-AND-MONORAILS.md +107 -0
- package/docs/GUIDED-SETUP.md +296 -0
- package/docs/PERMISSIONS.md +118 -0
- package/docs/ROADMAP.md +59 -0
- package/docs/SETUP.md +259 -0
- package/guides/NEURODIVERGENT_SUPPORT.md +278 -0
- package/package.json +70 -4
- package/server.json +39 -0
- package/src/aleph.js +395 -0
- package/src/annotations.js +135 -0
- package/src/approve-throttle.js +54 -0
- package/src/atlas-role.js +58 -0
- package/src/conformance.js +688 -0
- package/src/content_analyzer.js +450 -0
- package/src/database.js +811 -0
- package/src/disciplines.js +26 -0
- package/src/dispatch.js +279 -0
- package/src/env.js +52 -0
- package/src/floor.js +187 -0
- package/src/gaps.js +476 -0
- package/src/guides.js +48 -0
- package/src/http-remote.js +269 -0
- package/src/index.js +390 -0
- package/src/invites.js +167 -0
- package/src/layers.js +449 -0
- package/src/lessons-to-checks.js +366 -0
- package/src/logger.js +299 -0
- package/src/metadata.js +61 -0
- package/src/module-path.js +32 -0
- package/src/mongo-connect.js +48 -0
- package/src/named-items.js +208 -0
- package/src/oauth-provider.js +838 -0
- package/src/persons.js +116 -0
- package/src/provenance.js +257 -0
- package/src/redact.js +231 -0
- package/src/request-log.js +124 -0
- package/src/scopes.js +97 -0
- package/src/security-headers.js +26 -0
- package/src/session-graph.js +945 -0
- package/src/session.js +1097 -0
- package/src/setup.js +1313 -0
- package/src/startup-pages.js +354 -0
- package/src/startup-payload.js +126 -0
- package/src/tools.js +4271 -0
- package/src/transcript-session.js +106 -0
- package/src/validation.js +1163 -0
- package/src/version.js +2 -0
- package/templates/BOARD.md +72 -0
- package/templates/BOTHANDOFF.md +57 -0
- package/templates/CHARACTER.md +30 -0
- package/templates/HANDOFF.md +63 -0
- package/templates/IDENTITY.md +29 -0
- package/templates/INDEX.md +82 -0
- package/templates/INSTALL.md +29 -0
- package/templates/PROCEDURE.md +39 -0
- package/templates/ROSTER.md +43 -0
- package/templates/SEED.md +65 -0
- package/templates/STAGING.md +37 -0
- package/templates/STANDARD.md +99 -0
- package/templates/THREAD.md +43 -0
- package/templates/USER.md +29 -0
package/src/metadata.js
ADDED
|
@@ -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
|
+
}
|