@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
|
@@ -0,0 +1,688 @@
|
|
|
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
|
+
* conformance - template validation for controlled documents.
|
|
8
|
+
*
|
|
9
|
+
* A template is a control: it puts the answer in front of whoever fills it
|
|
10
|
+
* before the moment a field could be skipped. That only holds if the filled
|
|
11
|
+
* record is actually checked against it, so this module does the checking and
|
|
12
|
+
* `writeDoc()` calls it on every document write. There is no way to ask it not
|
|
13
|
+
* to (STANDARD 9a L2): an optional control is a reminder, and an instruction to
|
|
14
|
+
* be careful is not a control (charter 3, commitment 5).
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT DOES NOT DO. A nonconforming write is never rejected and never
|
|
17
|
+
* altered - the user's decision, 2026-09-10: "Mark for audit and review."
|
|
18
|
+
* The document lands, carries a visible marker naming what failed, and stays
|
|
19
|
+
* counted at session start until it is corrected by a later revision or
|
|
20
|
+
* accepted with a written reason. A validator that could block a write would be
|
|
21
|
+
* a validator that could lose one.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IT CANNOT DO, stated so no one assumes otherwise: it checks FORM and
|
|
24
|
+
* CONTINUITY, never truth. Whether a "none" states its scope honestly, and
|
|
25
|
+
* whether any answer is true, remain the author's and the reader's.
|
|
26
|
+
*
|
|
27
|
+
* The templates are their own specification: required sections are read from
|
|
28
|
+
* the template document's own headings, so the rule lives once, in the
|
|
29
|
+
* template, and never in a second copy here (charter 3, commitment 4).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
import path from 'path';
|
|
34
|
+
import { resolveModulePath } from './module-path.js';
|
|
35
|
+
import { setting } from './env.js';
|
|
36
|
+
|
|
37
|
+
const HERE = resolveModulePath(import.meta.url);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* L1: the SHIPPED copy governs. Required sections and status are read from the
|
|
41
|
+
* copy that ships with the release, never from the store copy - otherwise a
|
|
42
|
+
* template can be loosened by editing the store, or validation switched off by
|
|
43
|
+
* flipping a template to DRAFT. The store copy is still compared against this
|
|
44
|
+
* one, and a difference is itself a nonconformance.
|
|
45
|
+
*/
|
|
46
|
+
export const SHIPPED_TEMPLATES_DIR = path.resolve(HERE, '..', 'templates');
|
|
47
|
+
|
|
48
|
+
export const STATUS = Object.freeze({ DRAFT: 'DRAFT', IN_FORCE: 'IN FORCE', RETIRED: 'RETIRED' });
|
|
49
|
+
|
|
50
|
+
export const VERDICT = Object.freeze({
|
|
51
|
+
CONFORMS: 'conforms',
|
|
52
|
+
NONCONFORMING: 'nonconforming',
|
|
53
|
+
UNTEMPLATED: 'untemplated', // no template maps to this doc_id
|
|
54
|
+
UNGATED: 'ungated' // a template maps, but it is not IN FORCE yet
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ============================================
|
|
58
|
+
// PARSING - a template is its own specification
|
|
59
|
+
// ============================================
|
|
60
|
+
|
|
61
|
+
/** `## §3 OPEN BOARD` -> { num: '3', title: 'OPEN BOARD' }. The section number
|
|
62
|
+
* may carry a letter suffix (9a), which the standard itself uses. */
|
|
63
|
+
const SECTION_RE = /^##[ \t]*§([0-9]+[a-z]?)[ \t]*(.*)$/gm;
|
|
64
|
+
|
|
65
|
+
/** The control line, template or record. Both open with a bolded label. */
|
|
66
|
+
const CONTROL_RE = /^\*\*(?:Template|Record) control:\*\*(.*)$/m;
|
|
67
|
+
|
|
68
|
+
/** Field inside a control line: `status: IN FORCE`, up to the next separator. */
|
|
69
|
+
function controlField(controlLine, name) {
|
|
70
|
+
if (!controlLine) return null;
|
|
71
|
+
const re = new RegExp(`${name}\\s*:\\s*([^·|\\n]+)`, 'i');
|
|
72
|
+
const m = re.exec(controlLine);
|
|
73
|
+
return m ? m[1].trim() : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A doc id written as `templates/BOARD.md`, in backticks or bare.
|
|
78
|
+
*
|
|
79
|
+
* ⚠ THE `id` TOKEN MUST BE BOUNDED, and it was not. Written bare, the pattern
|
|
80
|
+
* matched the letters inside "val-id-ation-v1-pricing-2026-09.md" (without the
|
|
81
|
+
* hyphens) and captured the tail, reporting a declared template of
|
|
82
|
+
* "ation-v1-pricing-2026-09.md" - a filename mentioned in passing turning into
|
|
83
|
+
* a template the record never declared. Found by running the validator against
|
|
84
|
+
* the reference install's own board, 2026-09-11.
|
|
85
|
+
*
|
|
86
|
+
* The boundary is written as lookaround rather than \b on purpose: `_` is a
|
|
87
|
+
* word character, so \bid\b would refuse a control line that writes `doc_id:`.
|
|
88
|
+
* Lookaround on [A-Za-z0-9] rejects "validation" and "identifier" while still
|
|
89
|
+
* accepting `id`, `doc_id` and `Record id`.
|
|
90
|
+
*/
|
|
91
|
+
const CONTROL_ID_RE = /(?<![A-Za-z0-9])id(?![A-Za-z0-9])\s*:?\s*[`']?([A-Za-z0-9_./-]+\.md)[`']?/i;
|
|
92
|
+
const CONTROL_TEMPLATE_RE = /Template\s*:\s*[`']?([A-Za-z0-9_./-]+\.md)[`']?/i;
|
|
93
|
+
|
|
94
|
+
function controlDocId(controlLine) {
|
|
95
|
+
if (!controlLine) return null;
|
|
96
|
+
const m = CONTROL_ID_RE.exec(controlLine) || CONTROL_TEMPLATE_RE.exec(controlLine);
|
|
97
|
+
return m ? m[1] : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ⚠ ORDER IS LOAD-BEARING, and it is not obvious. The conventional way to write
|
|
102
|
+
* an unadopted template's status is `status: DRAFT, not in force` - and
|
|
103
|
+
* "NOT IN FORCE" CONTAINS "IN FORCE". Testing for IN FORCE first therefore reads
|
|
104
|
+
* every draft as adopted, which is the precise failure the gating exists to
|
|
105
|
+
* prevent: validation firing against templates nobody approved. Caught by
|
|
106
|
+
* tests/conformance.test.js on the first run, 2026-09-11.
|
|
107
|
+
*
|
|
108
|
+
* RETIRED and DRAFT are checked first because they are unambiguous, and an
|
|
109
|
+
* explicit negation is honoured before anything else.
|
|
110
|
+
*/
|
|
111
|
+
function normaliseStatus(raw) {
|
|
112
|
+
if (!raw) return null;
|
|
113
|
+
const s = raw.toUpperCase();
|
|
114
|
+
if (s.includes('RETIRED')) return STATUS.RETIRED;
|
|
115
|
+
if (s.includes('DRAFT')) return STATUS.DRAFT;
|
|
116
|
+
if (/\bNOT IN FORCE\b/.test(s)) return STATUS.DRAFT;
|
|
117
|
+
if (s.includes('IN FORCE')) return STATUS.IN_FORCE;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Sections in document order, with the body that follows each heading. */
|
|
122
|
+
export function parseSections(text) {
|
|
123
|
+
const src = String(text || '');
|
|
124
|
+
const found = [];
|
|
125
|
+
SECTION_RE.lastIndex = 0;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = SECTION_RE.exec(src)) !== null) {
|
|
128
|
+
found.push({ num: m[1], title: m[2].trim(), start: m.index, bodyStart: m.index + m[0].length });
|
|
129
|
+
}
|
|
130
|
+
return found.map((s, i) => ({
|
|
131
|
+
num: s.num,
|
|
132
|
+
title: s.title,
|
|
133
|
+
first_word: (s.title.split(/[\s—-]+/)[0] || '').toUpperCase(),
|
|
134
|
+
body: src.slice(s.bodyStart, i + 1 < found.length ? found[i + 1].start : src.length).trim()
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse a template document into the specification it is.
|
|
140
|
+
* `source` is 'shipped' or 'store' - which copy this came from, so a caller
|
|
141
|
+
* can never confuse the authority (L1) with the copy being audited.
|
|
142
|
+
*/
|
|
143
|
+
export function parseTemplate(text, { doc_id = null, source = 'shipped' } = {}) {
|
|
144
|
+
const src = String(text || '');
|
|
145
|
+
const control = (CONTROL_RE.exec(src) || [])[1] || null;
|
|
146
|
+
const sections = parseSections(src);
|
|
147
|
+
return {
|
|
148
|
+
template_id: controlDocId(control) || doc_id,
|
|
149
|
+
status: normaliseStatus(controlField(control, 'status')),
|
|
150
|
+
approved: controlField(control, 'approved'),
|
|
151
|
+
revision: controlField(control, 'revision'),
|
|
152
|
+
owner: controlField(control, 'owner'),
|
|
153
|
+
source,
|
|
154
|
+
required_sections: sections.map((s) => ({ num: s.num, title: s.title, first_word: s.first_word })),
|
|
155
|
+
placeholder_count: countPlaceholders(src),
|
|
156
|
+
body: src
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Front matter is the record control block for templates that declare it that way
|
|
161
|
+
// - a seed is the case in hand. It must open the file: anything indented, or sitting
|
|
162
|
+
// below prose, is body text and is not read. This is deliberately strict, because a
|
|
163
|
+
// trigger buried in a fenced block inside the body is exactly what an automated scan
|
|
164
|
+
// cannot see, and four seeds in the reference store are invisible for that reason.
|
|
165
|
+
const FRONT_MATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
166
|
+
|
|
167
|
+
export function parseFrontMatter(text) {
|
|
168
|
+
const m = FRONT_MATTER_RE.exec(String(text || ''));
|
|
169
|
+
return m ? m[1] : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const SEED_KINDS = new Set(['DATE', 'ROW', 'SIGNAL', 'TOPIC']);
|
|
173
|
+
const SEED_STATUSES = new Set(['DORMANT', 'MET', 'SHELVED', 'ACTED ON']);
|
|
174
|
+
|
|
175
|
+
/** Field checks for a seed whose record control is YAML front matter.
|
|
176
|
+
* Presence of the block is not enough: trigger kind and status have to be
|
|
177
|
+
* values the template names, or a seed can pass while remaining invisible
|
|
178
|
+
* to a startup scan. */
|
|
179
|
+
export function checkSeedFrontMatter(block) {
|
|
180
|
+
const failures = [];
|
|
181
|
+
const src = String(block || '');
|
|
182
|
+
const kinds = [];
|
|
183
|
+
const tests = [];
|
|
184
|
+
let status = null, resumes = null, planted = null, plantedBy = null, session = null;
|
|
185
|
+
for (const raw of src.split(/\r?\n/)) {
|
|
186
|
+
const line = raw.trim().replace(/^-\s+/, '');
|
|
187
|
+
const kv = line.match(/^([A-Za-z_]+):\s*(.*)$/);
|
|
188
|
+
if (!kv) continue;
|
|
189
|
+
const key = kv[1].toLowerCase();
|
|
190
|
+
const val = kv[2].trim();
|
|
191
|
+
if (key === 'kind') kinds.push(val.split(/\s+/)[0].toUpperCase());
|
|
192
|
+
else if (key === 'test') tests.push(val);
|
|
193
|
+
else if (key === 'status') status = val.toUpperCase();
|
|
194
|
+
else if (key === 'resumes_when') resumes = val;
|
|
195
|
+
else if (key === 'planted') planted = val;
|
|
196
|
+
else if (key === 'planted_by') plantedBy = val;
|
|
197
|
+
else if (key === 'session') session = val;
|
|
198
|
+
}
|
|
199
|
+
if (kinds.length === 0) failures.push('seed front matter names no trigger kind');
|
|
200
|
+
for (const k of kinds) {
|
|
201
|
+
if (!SEED_KINDS.has(k)) failures.push(`seed trigger kind "${k}" is not DATE, ROW, SIGNAL or TOPIC`);
|
|
202
|
+
}
|
|
203
|
+
if (tests.length === 0 || tests.every((t) => !t || /^_+$/.test(t))) {
|
|
204
|
+
failures.push('seed front matter trigger has no test');
|
|
205
|
+
}
|
|
206
|
+
if (!status) failures.push('seed front matter names no status');
|
|
207
|
+
else if (!SEED_STATUSES.has(status)) failures.push(`seed status "${status}" is not DORMANT, MET, SHELVED or ACTED ON`);
|
|
208
|
+
if (status === 'SHELVED' && (!resumes || /^_+$/.test(resumes))) {
|
|
209
|
+
failures.push('shelved seed names no resumes_when');
|
|
210
|
+
}
|
|
211
|
+
if (!planted || /^_+$/.test(planted)) failures.push('seed front matter names no planted date');
|
|
212
|
+
if (!plantedBy || /^_+$/.test(plantedBy)) failures.push('seed front matter names no planted_by');
|
|
213
|
+
if (!session || /^_+$/.test(session)) failures.push('seed front matter names no session');
|
|
214
|
+
return failures;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* An unfilled placeholder is a run of two or more underscores that is not part
|
|
219
|
+
* of a longer word (snake__case is not a placeholder). Limit stated plainly:
|
|
220
|
+
* markdown's `__bold__` would also match, so templates and records use `**` for
|
|
221
|
+
* emphasis. Flagged rather than silently tolerated - a half-filled record
|
|
222
|
+
* reported as done is the failure this check exists for.
|
|
223
|
+
*/
|
|
224
|
+
const PLACEHOLDER_RE = /(?<![A-Za-z0-9])_{2,}(?![A-Za-z0-9])/g;
|
|
225
|
+
|
|
226
|
+
export function countPlaceholders(text) {
|
|
227
|
+
const m = String(text || '').match(PLACEHOLDER_RE);
|
|
228
|
+
return m ? m.length : 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================
|
|
232
|
+
// THE MAP - which documents are templated
|
|
233
|
+
// ============================================
|
|
234
|
+
//
|
|
235
|
+
// L3: settings may ADD a mapping; they may never remove or redirect a base one.
|
|
236
|
+
// The board and handoff ids come from the same settings emet_initialize and
|
|
237
|
+
// emet_session_close use, so the document the startup reads is always the
|
|
238
|
+
// document that is validated - it cannot be unmapped by renaming it.
|
|
239
|
+
//
|
|
240
|
+
// Leaving the record control block out does not evade the check: the map is
|
|
241
|
+
// consulted by doc_id, independently of what the record declares.
|
|
242
|
+
|
|
243
|
+
/** The base map. Exact ids first, then prefixes. Never removable. */
|
|
244
|
+
export function baseMap() {
|
|
245
|
+
return {
|
|
246
|
+
exact: {
|
|
247
|
+
[setting('HANDOFF_DOC', 'HANDOFF.md')]: 'templates/HANDOFF.md',
|
|
248
|
+
[setting('STATE_DOC', 'STATE.md')]: 'templates/BOARD.md',
|
|
249
|
+
'bootstrap/IDENTITY.md': 'templates/IDENTITY.md',
|
|
250
|
+
'bootstrap/CHARACTER.md': 'templates/CHARACTER.md',
|
|
251
|
+
'bootstrap/SOUL.md': 'templates/CHARACTER.md',
|
|
252
|
+
'bootstrap/PERSONALITY.md': 'templates/CHARACTER.md',
|
|
253
|
+
'bootstrap/USER.md': 'templates/USER.md',
|
|
254
|
+
[setting('INSTALL_DOC', 'bootstrap/INSTALL.md')]: 'templates/INSTALL.md'
|
|
255
|
+
},
|
|
256
|
+
prefix: {
|
|
257
|
+
'seeds/': 'templates/SEED.md',
|
|
258
|
+
'threads/': 'templates/THREAD.md',
|
|
259
|
+
'staging/': 'templates/STAGING.md',
|
|
260
|
+
'procedures/': 'templates/PROCEDURE.md'
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Additional mappings from EMET_TEMPLATE_MAP, formatted `id=template,...`.
|
|
267
|
+
* Additive only: an entry that would redirect or remove a base mapping is
|
|
268
|
+
* IGNORED and reported, never applied (L3).
|
|
269
|
+
*/
|
|
270
|
+
export function extraMappings(raw = setting('TEMPLATE_MAP')) {
|
|
271
|
+
const added = {}, refused = [];
|
|
272
|
+
const base = baseMap();
|
|
273
|
+
for (const pair of String(raw ?? '').split(',')) {
|
|
274
|
+
const [k, v] = pair.split('=').map((s) => (s || '').trim());
|
|
275
|
+
if (!k || !v) continue;
|
|
276
|
+
const collides = Object.prototype.hasOwnProperty.call(base.exact, k)
|
|
277
|
+
|| Object.keys(base.prefix).some((p) => k === p);
|
|
278
|
+
if (collides) { refused.push(`${k} is a base mapping and cannot be redirected (L3)`); continue; }
|
|
279
|
+
added[k] = v;
|
|
280
|
+
}
|
|
281
|
+
return { added, refused };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Which template governs this doc_id, or null. */
|
|
285
|
+
export function templateFor(doc_id) {
|
|
286
|
+
if (!doc_id) return null;
|
|
287
|
+
const base = baseMap();
|
|
288
|
+
if (base.exact[doc_id]) return base.exact[doc_id];
|
|
289
|
+
const { added } = extraMappings();
|
|
290
|
+
if (added[doc_id]) return added[doc_id];
|
|
291
|
+
for (const [prefix, tpl] of Object.entries(base.prefix)) {
|
|
292
|
+
if (doc_id.startsWith(prefix)) return tpl;
|
|
293
|
+
}
|
|
294
|
+
for (const [k, tpl] of Object.entries(added)) {
|
|
295
|
+
if (k.endsWith('/') && doc_id.startsWith(k)) return tpl;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Base templates and the charter - the documents L1 and L5 protect. */
|
|
301
|
+
export function isGovernedByShippedCopy(doc_id) {
|
|
302
|
+
return doc_id === 'CHARTER.md' || /^templates\/[A-Z]+\.md$/.test(String(doc_id || ''));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A drop id. Retired drops still count. */
|
|
306
|
+
export function isDropDoc(doc_id) {
|
|
307
|
+
return String(doc_id || '').startsWith('drops/');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================
|
|
311
|
+
// THE SHIPPED COPIES (L1)
|
|
312
|
+
// ============================================
|
|
313
|
+
|
|
314
|
+
const shippedCache = new Map();
|
|
315
|
+
|
|
316
|
+
/** Read a shipped template from the release. Absence is reported, never thrown:
|
|
317
|
+
* a release built without its templates must still serve writes. */
|
|
318
|
+
export function loadShipped(template_id, { dir = SHIPPED_TEMPLATES_DIR, cache = true } = {}) {
|
|
319
|
+
// The directory is part of the key. Caching by template id alone would let a
|
|
320
|
+
// lookup against one directory answer a lookup against another, which is
|
|
321
|
+
// wrong in exactly the case that matters: two copies of the same template id
|
|
322
|
+
// that differ.
|
|
323
|
+
const key = `${dir}|${template_id}`;
|
|
324
|
+
if (cache && shippedCache.has(key)) return shippedCache.get(key);
|
|
325
|
+
const file = path.join(dir, path.basename(template_id));
|
|
326
|
+
let parsed = null;
|
|
327
|
+
try {
|
|
328
|
+
parsed = parseTemplate(fs.readFileSync(file, 'utf8'), { doc_id: template_id, source: 'shipped' });
|
|
329
|
+
} catch {
|
|
330
|
+
parsed = null;
|
|
331
|
+
}
|
|
332
|
+
if (cache) shippedCache.set(key, parsed);
|
|
333
|
+
return parsed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Test seam: forget what was read from disk. */
|
|
337
|
+
export function clearShippedCache() { shippedCache.clear(); }
|
|
338
|
+
|
|
339
|
+
// ============================================
|
|
340
|
+
// THE CHECKS (STANDARD 9)
|
|
341
|
+
// ============================================
|
|
342
|
+
|
|
343
|
+
const ID_RE = /\bB-\d{3,}\b/g;
|
|
344
|
+
|
|
345
|
+
/** The cells of a markdown table row. `| a | b |` splits to ['', ' a ', ' b ', ''];
|
|
346
|
+
* the outer empties are dropped so cells[i] is the i-th column. */
|
|
347
|
+
function cells(row) {
|
|
348
|
+
const parts = String(row || '').split('|');
|
|
349
|
+
if (parts.length > 2) return parts.slice(1, -1).map((c) => c.trim());
|
|
350
|
+
return [String(row || '').trim()];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The id cell of a row. The id column is located by its HEADER ("Id"), not by
|
|
354
|
+
* position: the user asked (2026-09-15) that the id, which means nothing to
|
|
355
|
+
* him in conversation, sit at the END of each row rather than lead it. A
|
|
356
|
+
* template that still puts it first is read the same way. No "Id" header at
|
|
357
|
+
* all falls back to the first cell, which is what every board before this
|
|
358
|
+
* revision had. */
|
|
359
|
+
function idCell(row, idIndex) {
|
|
360
|
+
const c = cells(row);
|
|
361
|
+
return (idIndex !== null && idIndex < c.length ? c[idIndex] : c[0]) || '';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Row ids taken from the ID COLUMN of each section's table.
|
|
366
|
+
*
|
|
367
|
+
* ⚠ NOT SCRAPED FROM THE SECTION BODY, and that is the whole point. Reading
|
|
368
|
+
* every B-### in the body made an id MENTIONED IN PROSE inside another row -
|
|
369
|
+
* a closed row named in a note, an item cross-referenced in a next action -
|
|
370
|
+
* read as a row of its own, so the carry-forward check reported it missing
|
|
371
|
+
* from the next revision. Found by running the validator against the reference
|
|
372
|
+
* install's own board, 2026-09-11: two rows were both closed on the prior
|
|
373
|
+
* revision and appeared only inside another row's text.
|
|
374
|
+
*
|
|
375
|
+
* The failure direction matters and is recorded so no one "fixes" this by
|
|
376
|
+
* loosening it: the bug made the check NOISY, never permissive. Nothing passed
|
|
377
|
+
* that should have failed. A row that genuinely leaves the board undispositioned
|
|
378
|
+
* is still caught, because a real row has a cell in the id column.
|
|
379
|
+
*/
|
|
380
|
+
function rowIds(sections, nums) {
|
|
381
|
+
const wanted = new Set(nums);
|
|
382
|
+
const ids = new Set();
|
|
383
|
+
for (const s of sections) {
|
|
384
|
+
if (!wanted.has(s.num)) continue;
|
|
385
|
+
const idIndex = idColumnIndex(s.body);
|
|
386
|
+
for (const row of tableRows(s.body)) {
|
|
387
|
+
for (const id of idCell(row, idIndex).match(ID_RE) || []) ids.add(id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return ids;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Index of the column headed "Id" in the first table of a section body, or
|
|
394
|
+
* null when no such header exists. */
|
|
395
|
+
function idColumnIndex(body) {
|
|
396
|
+
const header = String(body || '').split('\n').map((l) => l.trim()).find((l) => l.startsWith('|'));
|
|
397
|
+
if (!header) return null;
|
|
398
|
+
const i = cells(header).findIndex((h) => /^id$/i.test(h));
|
|
399
|
+
return i === -1 ? null : i;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Data rows of the first markdown table in a section body. */
|
|
403
|
+
function tableRows(body) {
|
|
404
|
+
const lines = String(body || '').split('\n').map((l) => l.trim());
|
|
405
|
+
const rows = [];
|
|
406
|
+
let seenHeader = false, seenRule = false;
|
|
407
|
+
for (const line of lines) {
|
|
408
|
+
if (!line.startsWith('|')) { if (seenRule && rows.length) break; continue; }
|
|
409
|
+
if (!seenHeader) { seenHeader = true; continue; }
|
|
410
|
+
if (!seenRule) { seenRule = /^\|[\s:|-]+\|$/.test(line); continue; }
|
|
411
|
+
rows.push(line);
|
|
412
|
+
}
|
|
413
|
+
return rows;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Check one record against one parsed template.
|
|
418
|
+
*
|
|
419
|
+
* Returns failures (nonconformances) and warnings (reported, not counted).
|
|
420
|
+
* Pure: no database, no disk, no clock beyond `checked_at` supplied by the
|
|
421
|
+
* caller - so every check is assertable in a test without a live store.
|
|
422
|
+
*/
|
|
423
|
+
export function checkRecord(content, template, { previous = null, doc_id = null } = {}) {
|
|
424
|
+
const failures = [], warnings = [];
|
|
425
|
+
const text = String(content || '');
|
|
426
|
+
const sections = parseSections(text);
|
|
427
|
+
const byNum = new Map(sections.map((s) => [s.num, s]));
|
|
428
|
+
|
|
429
|
+
// 1. Record control: a §0 section that names the template it was filled from -
|
|
430
|
+
// or, where the record control IS the front matter, the front matter itself.
|
|
431
|
+
//
|
|
432
|
+
// This demanded a `## §0` heading unconditionally until 2026-09-13. A seed's
|
|
433
|
+
// record control is YAML front matter by design, and front matter cannot sit
|
|
434
|
+
// underneath a heading and remain front matter, so no correctly-written seed
|
|
435
|
+
// could satisfy it. Every seed in the reference store was marked nonconforming
|
|
436
|
+
// for obeying its own template, and a check that cannot be passed is worse than
|
|
437
|
+
// no check: it marks everything, and a marker that is always on is one nobody
|
|
438
|
+
// reads. Nothing is relaxed here - a record carrying neither still fails.
|
|
439
|
+
const zero = byNum.get('0');
|
|
440
|
+
const control = zero ? zero.body : parseFrontMatter(text);
|
|
441
|
+
if (!control) {
|
|
442
|
+
failures.push('record control block missing: no `## §0` section and no front matter');
|
|
443
|
+
} else if (!controlDocId(control) && !/Template\s*:/i.test(control)) {
|
|
444
|
+
failures.push('record control block names no template id');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Seed record control is front matter by design (templates/SEED.md product
|
|
448
|
+
// note). Presence of the block was already required above; kind and status
|
|
449
|
+
// are checked here so a seed can fail for a bad trigger rather than only
|
|
450
|
+
// for a missing heading.
|
|
451
|
+
if (template.template_id === 'templates/SEED.md') {
|
|
452
|
+
const fm = parseFrontMatter(text) || (!zero ? control : null);
|
|
453
|
+
if (fm) failures.push(...checkSeedFrontMatter(fm));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 2. Every required section present, in order.
|
|
457
|
+
const present = sections.map((s) => s.num);
|
|
458
|
+
let cursor = -1;
|
|
459
|
+
for (const req of template.required_sections) {
|
|
460
|
+
const at = present.indexOf(req.num);
|
|
461
|
+
if (at === -1) { failures.push(`required section §${req.num} ${req.title} is missing`); continue; }
|
|
462
|
+
if (at < cursor) failures.push(`section §${req.num} appears out of order`);
|
|
463
|
+
cursor = Math.max(cursor, at);
|
|
464
|
+
const got = byNum.get(req.num);
|
|
465
|
+
// Heading drift is a warning, never a failure: matching on the § number and
|
|
466
|
+
// the first word keeps a retitled section from reading as an absent one.
|
|
467
|
+
if (req.first_word && got.first_word && got.first_word !== req.first_word) {
|
|
468
|
+
warnings.push(`§${req.num} is titled "${got.title}"; the template says "${req.title}"`);
|
|
469
|
+
}
|
|
470
|
+
// 3. No empty required section.
|
|
471
|
+
if (!got.body || got.body.replace(/[\s|:-]/g, '') === '') {
|
|
472
|
+
failures.push(`required section §${req.num} ${req.title} is empty`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 4. No unfilled placeholder survived from the template.
|
|
477
|
+
const left = countPlaceholders(text);
|
|
478
|
+
if (left > 0) failures.push(`${left} unfilled template placeholder${left === 1 ? '' : 's'} left in the record`);
|
|
479
|
+
|
|
480
|
+
// 5. NOT COMPLETED carries a reason.
|
|
481
|
+
for (const m of text.match(/NOT (?:COMPLETED|CHECKED)[^\n]*/g) || []) {
|
|
482
|
+
if (!/NOT (?:COMPLETED|CHECKED)\s*[—–-]\s*\S/.test(m)) {
|
|
483
|
+
failures.push(`"${m.trim().slice(0, 60)}" states no reason`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 6. The board's own check: the §0 counts must match the rows they describe.
|
|
488
|
+
if (template.template_id === 'templates/BOARD.md' && zero) {
|
|
489
|
+
const counted = {
|
|
490
|
+
'Half-state': tableRows((byNum.get('1') || {}).body).length,
|
|
491
|
+
'Closed this revision': tableRows((byNum.get('4') || {}).body).length
|
|
492
|
+
};
|
|
493
|
+
const declared = tableRows(zero.body)[0];
|
|
494
|
+
if (declared) {
|
|
495
|
+
const cells = declared.split('|').map((c) => c.trim()).filter((c) => c !== '');
|
|
496
|
+
const headerCells = (zero.body.split('\n').find((l) => l.includes('Half-state')) || '')
|
|
497
|
+
.split('|').map((c) => c.trim()).filter((c) => c !== '');
|
|
498
|
+
for (const [label, actual] of Object.entries(counted)) {
|
|
499
|
+
const i = headerCells.findIndex((h) => h.toLowerCase() === label.toLowerCase());
|
|
500
|
+
if (i === -1) continue;
|
|
501
|
+
const stated = Number(cells[i]);
|
|
502
|
+
if (Number.isFinite(stated) && stated !== actual) {
|
|
503
|
+
failures.push(`§0 states ${label} ${stated}; §${label === 'Half-state' ? '1' : '4'} holds ${actual} row${actual === 1 ? '' : 's'}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 7. CARRY-FORWARD. Every row id on the previous revision's open sections is
|
|
510
|
+
// still present in this one, somewhere - open, or dispositioned in §4.
|
|
511
|
+
// The prior revision is already in hand at the door, so this is a
|
|
512
|
+
// comparison and not a judgment. This check alone would have caught the
|
|
513
|
+
// two job-application rows that left the board with nobody deciding they
|
|
514
|
+
// should (reference install, September 2026).
|
|
515
|
+
if (previous && typeof previous === 'string') {
|
|
516
|
+
const before = rowIds(parseSections(previous), ['1', '2', '3']);
|
|
517
|
+
const after = rowIds(sections, ['1', '2', '3', '4']);
|
|
518
|
+
const dropped = [...before].filter((id) => !after.has(id));
|
|
519
|
+
if (dropped.length) {
|
|
520
|
+
failures.push(
|
|
521
|
+
`${dropped.length} row${dropped.length === 1 ? '' : 's'} on the previous revision ` +
|
|
522
|
+
`left this one with no disposition: ${dropped.sort().join(', ')}`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return { failures, warnings };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ============================================
|
|
531
|
+
// THE DOOR'S ENTRY POINT
|
|
532
|
+
// ============================================
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Validate one document write. Called from writeDoc() on every write, with no
|
|
536
|
+
* argument that can turn it off (L2).
|
|
537
|
+
*
|
|
538
|
+
* NEVER THROWS for nonconformance and never alters the content. A validator
|
|
539
|
+
* fault is itself reported as a warning rather than failing the write: the
|
|
540
|
+
* write path's job is to store the document, and a check that can block one is
|
|
541
|
+
* a check that can lose one.
|
|
542
|
+
*
|
|
543
|
+
* @param {string} doc_id
|
|
544
|
+
* @param {string} content the content about to be written, post-redaction
|
|
545
|
+
* @param {string|null} previous the prior revision's content, already in hand
|
|
546
|
+
* @returns {object} the conformance record stored on the row and returned in
|
|
547
|
+
* the receipt.
|
|
548
|
+
*/
|
|
549
|
+
export function validateDocument(doc_id, content, previous = null, { dir } = {}) {
|
|
550
|
+
const checked_at = new Date().toISOString();
|
|
551
|
+
try {
|
|
552
|
+
const failures = [], warnings = [];
|
|
553
|
+
|
|
554
|
+
// L1/L5: a base template or the charter, written into the store. The
|
|
555
|
+
// shipped copy is the authority; the store copy is for reading. Drift is a
|
|
556
|
+
// nonconformance on the STORE copy and changes nothing about the shipped one.
|
|
557
|
+
if (isGovernedByShippedCopy(doc_id)) {
|
|
558
|
+
// The charter ships at the repository ROOT, beside the templates
|
|
559
|
+
// directory, not inside it. Until 2026-09-15 this lookup joined the
|
|
560
|
+
// templates directory with the charter's basename, found nothing, and
|
|
561
|
+
// reported "no shipped copy" as a warning - so the L1 lock the charter's
|
|
562
|
+
// own section 9 promises had never once fired. Found by the store write
|
|
563
|
+
// of the revised charter returning that warning.
|
|
564
|
+
const shippedDir = dir ? dir : (doc_id === 'CHARTER.md' ? path.dirname(SHIPPED_TEMPLATES_DIR) : SHIPPED_TEMPLATES_DIR);
|
|
565
|
+
const shipped = loadShipped(doc_id, { dir: shippedDir });
|
|
566
|
+
if (!shipped) {
|
|
567
|
+
warnings.push(`no shipped copy of ${doc_id} found in this release; the store copy cannot be checked against it (L1)`);
|
|
568
|
+
} else if (String(shipped.body).replace(/\r\n/g, '\n').trim() !== String(content).replace(/\r\n/g, '\n').trim()) {
|
|
569
|
+
failures.push(`store copy of ${doc_id} differs from the copy shipped with this release; the shipped copy governs (L1)`);
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
template: null, template_status: null, verdict: failures.length ? VERDICT.NONCONFORMING : VERDICT.CONFORMS,
|
|
573
|
+
governed_by: 'shipped release copy', failures, warnings, checked_at
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const mapped_id = templateFor(doc_id);
|
|
578
|
+
const { refused } = extraMappings();
|
|
579
|
+
for (const r of refused) warnings.push(r);
|
|
580
|
+
|
|
581
|
+
if (!mapped_id) {
|
|
582
|
+
return { template: null, template_status: null, verdict: VERDICT.UNTEMPLATED, failures: [], warnings, checked_at };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const template = loadShipped(mapped_id, dir ? { dir } : {});
|
|
586
|
+
if (!template) {
|
|
587
|
+
return {
|
|
588
|
+
template: mapped_id, template_status: null, verdict: VERDICT.UNGATED,
|
|
589
|
+
failures: [], checked_at,
|
|
590
|
+
warnings: [...warnings, `${mapped_id} is not present in this release, so ${doc_id} could not be validated`]
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Gating: nothing is marked until the user adopts a template. Before
|
|
595
|
+
// that, the build is silent - which is what let it ship ahead of adoption.
|
|
596
|
+
if (template.status !== STATUS.IN_FORCE) {
|
|
597
|
+
return {
|
|
598
|
+
template: mapped_id, template_status: template.status, verdict: VERDICT.UNGATED,
|
|
599
|
+
failures: [], warnings, checked_at,
|
|
600
|
+
note: `${mapped_id} is ${template.status || 'unstated'}; records are checked but not marked until it is IN FORCE.`
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
// A template cannot be IN FORCE without its approval recorded (STANDARD 6;
|
|
604
|
+
// ISO 13485 4.2.4 - review and approval before issue).
|
|
605
|
+
if (!template.approved) {
|
|
606
|
+
warnings.push(`${mapped_id} is IN FORCE with no approval recorded (who · date · decision record)`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const result = checkRecord(content, template, { previous, doc_id });
|
|
610
|
+
failures.push(...result.failures);
|
|
611
|
+
warnings.push(...result.warnings);
|
|
612
|
+
|
|
613
|
+
// L4: a record that declares a DIFFERENT template is checked against BOTH.
|
|
614
|
+
// Declaring a weaker local template adds a check; it never replaces one.
|
|
615
|
+
const declared = controlDocId((parseSections(content).find((s) => s.num === '0') || {}).body || '');
|
|
616
|
+
if (declared && declared !== mapped_id) {
|
|
617
|
+
warnings.push(`record declares ${declared}; the map says ${mapped_id}. Validated against both (L4).`);
|
|
618
|
+
const alt = loadShipped(declared, dir ? { dir } : {});
|
|
619
|
+
if (alt && alt.status === STATUS.IN_FORCE) {
|
|
620
|
+
const second = checkRecord(content, alt, { previous, doc_id });
|
|
621
|
+
for (const f of second.failures) if (!failures.includes(f)) failures.push(`against ${declared}: ${f}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
template: mapped_id, template_status: template.status,
|
|
627
|
+
verdict: failures.length ? VERDICT.NONCONFORMING : VERDICT.CONFORMS,
|
|
628
|
+
failures, warnings, checked_at
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
// The check failed, not the write. Say so and store the document.
|
|
632
|
+
return {
|
|
633
|
+
template: null, template_status: null, verdict: VERDICT.UNTEMPLATED, failures: [], checked_at,
|
|
634
|
+
warnings: [`conformance check could not run: ${error && error.message ? error.message : String(error)}`]
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ============================================
|
|
640
|
+
// ACCEPTANCE (L6)
|
|
641
|
+
// ============================================
|
|
642
|
+
//
|
|
643
|
+
// A marker is never cleared silently. It leaves in one of two ways: the next
|
|
644
|
+
// revision passes, or a person accepts it in writing. An acceptance binds to
|
|
645
|
+
// the revision it names and to nothing after it - the next revision is
|
|
646
|
+
// validated fresh, and accepted markers stay counted at session start,
|
|
647
|
+
// separately from open ones, so an acceptance is a disposition and not a
|
|
648
|
+
// disappearance.
|
|
649
|
+
|
|
650
|
+
export const MIN_ACCEPTANCE_REASON = 10;
|
|
651
|
+
|
|
652
|
+
export function buildAcceptance({ reason, by = null, version = null, person_id = null, client_id = null, session_id = null }) {
|
|
653
|
+
const text = String(reason || '').trim();
|
|
654
|
+
if (text.length < MIN_ACCEPTANCE_REASON) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`An acceptance needs a written reason of at least ${MIN_ACCEPTANCE_REASON} characters. ` +
|
|
657
|
+
`It is the disposition of record for a nonconformance and is read by whoever audits it later.`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
reason: text, accepted_by: by, accepted_at: new Date().toISOString(), covers_version: version,
|
|
662
|
+
person_id, client_id: client_id || 'stdio', session_id
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** True when this row's marker is open: nonconforming, and not accepted FOR
|
|
667
|
+
* THIS revision. An acceptance from an earlier version does not carry. */
|
|
668
|
+
export function isOpenNonconformance(conformance, version = null) {
|
|
669
|
+
if (!conformance || conformance.verdict !== VERDICT.NONCONFORMING) return false;
|
|
670
|
+
const a = conformance.accepted;
|
|
671
|
+
if (!a) return true;
|
|
672
|
+
if (a.covers_version === null || a.covers_version === undefined) return false;
|
|
673
|
+
return Number(a.covers_version) !== Number(version);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** True when the marker stands but has been dispositioned for this revision. */
|
|
677
|
+
export function isAcceptedNonconformance(conformance, version = null) {
|
|
678
|
+
if (!conformance || conformance.verdict !== VERDICT.NONCONFORMING) return false;
|
|
679
|
+
return Boolean(conformance.accepted) && !isOpenNonconformance(conformance, version);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export default {
|
|
683
|
+
SHIPPED_TEMPLATES_DIR, STATUS, VERDICT, MIN_ACCEPTANCE_REASON,
|
|
684
|
+
parseSections, parseTemplate, countPlaceholders,
|
|
685
|
+
baseMap, extraMappings, templateFor, isGovernedByShippedCopy,
|
|
686
|
+
loadShipped, clearShippedCache, checkRecord, validateDocument,
|
|
687
|
+
buildAcceptance, isOpenNonconformance, isAcceptedNonconformance
|
|
688
|
+
};
|