@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
@@ -0,0 +1,1359 @@
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
+ * gem-check, revision 2026-10-12. The gate in gem-library/CONFORMANCE.md.
8
+ * The socket runs these same functions. Nothing here imports the store,
9
+ * a GEM's code, or a JSON-schema library. A throw inside a check is a
10
+ * failure of that check. The process exits 1 on any failure.
11
+ *
12
+ * CLI: node src/gem-check.js <gem-directory> [--emet-version x.y.z]
13
+ * stdout: one line per check, `GC-01 pass` or `GC-01 fail: <reason>`
14
+ * exit 0 all required checks passed or skipped as specified
15
+ * exit 1 a failure, or an unexpected throw
16
+ * exit 2 the argument is missing or not a directory
17
+ * stderr is empty on exit 0
18
+ */
19
+
20
+ import crypto from 'node:crypto';
21
+ import fs from 'node:fs';
22
+ import os from 'node:os';
23
+ import path from 'node:path';
24
+ import { spawnSync } from 'node:child_process';
25
+ import { pathToFileURL } from 'node:url';
26
+ import { resolveModulePath } from './module-path.js';
27
+ import { refuseHostAccess, MAX_SOURCE_BYTES, runeSpecifierReason, runeExtensionReason } from './host-reach.js';
28
+
29
+ export const SPEC_REVISION = '2026-10-12';
30
+ export const CHECK_IDS = Object.freeze([
31
+ 'GC-01', 'GC-02', 'GC-03', 'GC-04', 'GC-05', 'GC-06', 'GC-07', 'GC-08', 'GC-09', 'GC-10',
32
+ 'GC-11', 'GC-12', 'GC-13', 'GC-14', 'GC-15', 'GC-16', 'GC-17', 'GC-18', 'GC-19', 'GC-20',
33
+ 'GC-21', 'GC-22'
34
+ ]);
35
+
36
+ const CODE_EXT = Object.freeze(['.js', '.mjs', '.cjs', '.wasm', '.sh', '.py', '.exe', '.so', '.dylib', '.dll']);
37
+ const JS_EXT = Object.freeze(['.js', '.mjs']);
38
+ const ACCESS_RE = /^(identity|semantic|episodic|procedural|meta|working|doc:[A-Za-z0-9][A-Za-z0-9_./-]{0,80})$/;
39
+ const RANGE_RE = /^(>=|\^|~)?(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})$/;
40
+ const VERSION_RE = /^(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})$/;
41
+
42
+ /** Core paths a GEM must not override. An entry fails when it equals one or starts with one plus a dot. */
43
+ export const PROTECTED_OVERRIDES = Object.freeze([
44
+ 'voice.not_modifiable',
45
+ 'voice.honesty',
46
+ 'voice.measured',
47
+ 'voice.epistemic_honesty',
48
+ 'voice.discipline',
49
+ 'voice.no_moralising',
50
+ 'engagement.answer_and_stop',
51
+ 'engagement.no_borrowed_hazards',
52
+ 'engagement.not_retired',
53
+ 'engagement.their_own_plan',
54
+ 'engagement.no_rapport_mirroring',
55
+ 'persistence.named_items',
56
+ 'corpus_discipline.judgment_gated',
57
+ 'corpus_discipline.name_the_weight',
58
+ 'corpus_discipline.equips_not_diagnoses',
59
+ 'corpus_discipline.lived_data_wins'
60
+ ]);
61
+
62
+ /** The only override paths this revision allows. Anything else fails until a GEP adds it. */
63
+ export const MODIFIABLE_OVERRIDES = Object.freeze([
64
+ 'voice.composure', 'voice.directness', 'voice.concrete', 'voice.restraint', 'voice.priorities',
65
+ 'engagement.lead', 'engagement.one_question', 'engagement.path_forward', 'engagement.mentorship',
66
+ 'engagement.adapt', 'engagement.acknowledge', 'engagement.modifiable', 'engagement.goal_check'
67
+ ]);
68
+
69
+ /**
70
+ * Floor-negation lint. The list is the one in CONFORMANCE.md. Add a pattern
71
+ * when a bypass is in front of you, not before.
72
+ */
73
+ export const FLOOR_NEGATION = Object.freeze([
74
+ /ignore the (core )?floor/i,
75
+ /disregard the (core )?floor/i,
76
+ /override the (core )?floor/i,
77
+ /you may flatter/i,
78
+ /flattery is (fine|welcome|encouraged)/i,
79
+ /soften the truth/i,
80
+ /do not verify/i,
81
+ /skip verification/i,
82
+ /verification is optional/i,
83
+ /invent a cause/i,
84
+ /do not name (their |the )?(items|named)/i,
85
+ /skip named[_ ]items/i,
86
+ /named[_ ]items (are|is) optional/i,
87
+ /out of sight[\s\S]{0,40}(skip|ignore|do not name)/i,
88
+ /the user does not need the truth/i
89
+ ]);
90
+
91
+ const SECRET_PATTERNS = Object.freeze([
92
+ /-----BEGIN (?:RSA |OPENSSH |EC |PRIVATE )?KEY-----/,
93
+ /AKIA[0-9A-Z]{16}/,
94
+ /mongodb(?:\+srv)?:\/\/[^\s/]+:[^\s/]+@/,
95
+ /(api[_-]?key|secret|token|password)\s*[:=]\s*['"][^\s'"]{12,}/i
96
+ ]);
97
+
98
+ const SECTION_KEY = /^[a-z][a-z0-9_]{0,40}$/;
99
+ const FORBIDDEN_SECTIONS = new Set(['voice', 'engagement', 'persistence', 'corpus_discipline', 'operating_discipline']);
100
+
101
+ export function schemaPath() {
102
+ return resolveModulePath(import.meta.url, ['..', 'gem-library', 'gem.schema.json']);
103
+ }
104
+
105
+ export function trustStorePath() {
106
+ return resolveModulePath(import.meta.url, ['..', 'gem-library', 'keys.json']);
107
+ }
108
+
109
+ let schemaCache = null;
110
+
111
+ function loadSchema(explicit) {
112
+ if (explicit) return explicit;
113
+ if (schemaCache) return schemaCache;
114
+ const text = fs.readFileSync(schemaPath(), 'utf8');
115
+ const parsed = JSON.parse(text);
116
+ if (!parsed || parsed.$id !== 'https://github.com/dreamforgestudiollc/EMET-GEMs/raw/main/gem.schema.json') {
117
+ throw new Error('schema id');
118
+ }
119
+ schemaCache = parsed;
120
+ return schemaCache;
121
+ }
122
+
123
+ /**
124
+ * One JSON value, with duplicate keys rejected. JSON.parse keeps the last
125
+ * duplicate and would let a manifest hide a field under another. Comments
126
+ * and trailing commas throw, because they are not JSON.
127
+ */
128
+ export function parseStrictJson(text) {
129
+ const s = String(text);
130
+ const n = s.length;
131
+ let i = 0;
132
+ function skip() {
133
+ while (i < n && (s[i] === ' ' || s[i] === '\n' || s[i] === '\r' || s[i] === '\t')) i++;
134
+ }
135
+ function parse() {
136
+ skip();
137
+ if (i >= n) throw new Error('unexpected end');
138
+ const c = s[i];
139
+ if (c === '{') return parseObject();
140
+ if (c === '[') return parseArray();
141
+ if (c === '"') return parseString();
142
+ if (c === 't') return atom('true', true);
143
+ if (c === 'f') return atom('false', false);
144
+ if (c === 'n') return atom('null', null);
145
+ if (c === '-' || (c >= '0' && c <= '9')) return parseNumber();
146
+ throw new Error('unexpected character');
147
+ }
148
+ function atom(word, value) {
149
+ if (s.slice(i, i + word.length) !== word) throw new Error('unexpected token');
150
+ i += word.length;
151
+ return value;
152
+ }
153
+ function parseNumber() {
154
+ const start = i;
155
+ if (s[i] === '-') i++;
156
+ if (s[i] === '0') i++;
157
+ else if (s[i] >= '1' && s[i] <= '9') { while (i < n && s[i] >= '0' && s[i] <= '9') i++; }
158
+ else throw new Error('bad number');
159
+ if (i < n && s[i] === '.') {
160
+ i++;
161
+ if (i >= n || s[i] < '0' || s[i] > '9') throw new Error('bad number');
162
+ while (i < n && s[i] >= '0' && s[i] <= '9') i++;
163
+ }
164
+ if (i < n && (s[i] === 'e' || s[i] === 'E')) {
165
+ i++;
166
+ if (s[i] === '+' || s[i] === '-') i++;
167
+ if (i >= n || s[i] < '0' || s[i] > '9') throw new Error('bad number');
168
+ while (i < n && s[i] >= '0' && s[i] <= '9') i++;
169
+ }
170
+ const lit = s.slice(start, i);
171
+ if (/^0\d/.test(lit) || /^-0\d/.test(lit)) throw new Error('bad number');
172
+ const num = JSON.parse(lit);
173
+ if (typeof num !== 'number' || !Number.isFinite(num)) throw new Error('bad number');
174
+ return num;
175
+ }
176
+ function parseString() {
177
+ i++;
178
+ let out = '';
179
+ while (i < n) {
180
+ const c = s[i];
181
+ if (c === '\\') {
182
+ const e = s[i + 1];
183
+ const map = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
184
+ if (map[e] !== undefined) { out += map[e]; i += 2; continue; }
185
+ if (e === 'u' && /^[0-9a-fA-F]{4}$/.test(s.slice(i + 2, i + 6))) {
186
+ out += String.fromCharCode(parseInt(s.slice(i + 2, i + 6), 16));
187
+ i += 6;
188
+ continue;
189
+ }
190
+ throw new Error('bad escape');
191
+ }
192
+ if (c === '"') { i++; return out; }
193
+ if (c < ' ') throw new Error('raw control in string');
194
+ out += c;
195
+ i++;
196
+ }
197
+ throw new Error('unterminated string');
198
+ }
199
+ function parseArray() {
200
+ i++;
201
+ skip();
202
+ const arr = [];
203
+ if (s[i] === ']') { i++; return arr; }
204
+ while (i < n) {
205
+ arr.push(parse());
206
+ skip();
207
+ if (s[i] === ',') { i++; skip(); if (s[i] === ']') throw new Error('trailing comma'); continue; }
208
+ if (s[i] === ']') { i++; return arr; }
209
+ throw new Error('expected comma');
210
+ }
211
+ throw new Error('unterminated array');
212
+ }
213
+ function parseObject() {
214
+ i++;
215
+ skip();
216
+ const obj = {};
217
+ const seen = new Set();
218
+ if (s[i] === '}') { i++; return obj; }
219
+ while (i < n) {
220
+ skip();
221
+ if (s[i] !== '"') throw new Error('expected key');
222
+ const key = parseString();
223
+ if (seen.has(key)) throw new Error('duplicate key');
224
+ seen.add(key);
225
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') throw new Error('forbidden key');
226
+ skip();
227
+ if (s[i] !== ':') throw new Error('expected colon');
228
+ i++;
229
+ const value = parse();
230
+ Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
231
+ skip();
232
+ if (s[i] === ',') { i++; skip(); if (s[i] === '}') throw new Error('trailing comma'); continue; }
233
+ if (s[i] === '}') { i++; return obj; }
234
+ throw new Error('expected comma');
235
+ }
236
+ throw new Error('unterminated object');
237
+ }
238
+ if (s.charCodeAt(0) === 0xfeff) throw new Error('bom');
239
+ const value = parse();
240
+ skip();
241
+ if (i !== n) throw new Error('trailing data');
242
+ return value;
243
+ }
244
+
245
+ /** RFC 8785 for the values a manifest can hold. Non-integers fail closed. */
246
+ export function canonicalize(value) {
247
+ return Buffer.from(jcs(value), 'utf8');
248
+ }
249
+
250
+ function jcs(value) {
251
+ if (value === null) return 'null';
252
+ const t = typeof value;
253
+ if (t === 'boolean') return value ? 'true' : 'false';
254
+ if (t === 'number') {
255
+ if (!Number.isSafeInteger(value)) throw new Error('number is not a safe integer');
256
+ return JSON.stringify(value);
257
+ }
258
+ if (t === 'string') return JSON.stringify(value);
259
+ if (Array.isArray(value)) return '[' + value.map(jcs).join(',') + ']';
260
+ if (t === 'object') {
261
+ const keys = Object.keys(value).sort();
262
+ return '{' + keys.map((k) => JSON.stringify(k) + ':' + jcs(value[k])).join(',') + '}';
263
+ }
264
+ throw new Error('unsupported value');
265
+ }
266
+
267
+ function cmpVer(a, b) {
268
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
269
+ return 0;
270
+ }
271
+
272
+ function parseVer(text) {
273
+ const m = VERSION_RE.exec(text);
274
+ if (!m) return null;
275
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
276
+ }
277
+
278
+ /** Grammar only. Returns null when the text is not a range this revision evaluates. */
279
+ export function parseEmetRange(text) {
280
+ const m = RANGE_RE.exec(String(text));
281
+ if (!m) return null;
282
+ return { op: m[1] || '=', base: [Number(m[2]), Number(m[3]), Number(m[4])] };
283
+ }
284
+
285
+ /**
286
+ * exact, >=, ^, ~ on MAJOR.MINOR.PATCH.
287
+ * ^1.2.3 is >=1.2.3 <2.0.0. ~1.2.3 is >=1.2.3 <1.3.0.
288
+ * A leading 0 uses the same shape as the usual caret rule: ^0.3.0 is >=0.3.0 <0.4.0,
289
+ * and ^0.0.3 is >=0.0.3 <0.0.4.
290
+ */
291
+ export function satisfiesEmetRange(version, rangeText) {
292
+ const range = parseEmetRange(rangeText);
293
+ const cur = parseVer(version);
294
+ if (!range || !cur) return false;
295
+ const { op, base } = range;
296
+ if (op === '=') return cmpVer(cur, base) === 0;
297
+ if (op === '>=') return cmpVer(cur, base) >= 0;
298
+ let upper;
299
+ if (op === '~') upper = [base[0], base[1] + 1, 0];
300
+ else if (base[0] === 0 && base[1] === 0) upper = [0, 0, base[2] + 1];
301
+ else if (base[0] === 0) upper = [0, base[1] + 1, 0];
302
+ else upper = [base[0] + 1, 0, 0];
303
+ return cmpVer(cur, base) >= 0 && cmpVer(cur, upper) < 0;
304
+ }
305
+
306
+ function pointer(base, key) {
307
+ return base + '/' + String(key).replace(/~/g, '~0').replace(/\//g, '~1');
308
+ }
309
+
310
+ function stable(value) {
311
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
312
+ if (Array.isArray(value)) return '[' + value.map(stable).join(',') + ']';
313
+ return '{' + Object.keys(value).sort().map((k) => JSON.stringify(k) + ':' + stable(value[k])).join(',') + '}';
314
+ }
315
+
316
+ function resolveRef(root, ref) {
317
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
318
+ let node = root;
319
+ for (const part of ref.slice(2).split('/')) {
320
+ if (!node || typeof node !== 'object') return null;
321
+ node = node[part];
322
+ }
323
+ return node || null;
324
+ }
325
+
326
+ function schemaError(schema, data, at, root) {
327
+ if (!schema || typeof schema !== 'object') return { path: at || '/', message: 'schema missing' };
328
+ if (schema.$ref) {
329
+ const next = resolveRef(root, schema.$ref);
330
+ if (!next) return { path: at || '/', message: 'unresolved $ref' };
331
+ return schemaError(next, data, at, root);
332
+ }
333
+ if (schema.allOf) {
334
+ for (const sub of schema.allOf) {
335
+ const err = schemaError(sub, data, at, root);
336
+ if (err) return err;
337
+ }
338
+ }
339
+ if (schema.anyOf) {
340
+ let matched = false;
341
+ for (const sub of schema.anyOf) {
342
+ if (!schemaError(sub, data, at, root)) { matched = true; break; }
343
+ }
344
+ if (!matched) return { path: at || '/', message: 'does not match' };
345
+ }
346
+ if (schema.if) {
347
+ if (!schemaError(schema.if, data, at, root) && schema.then) {
348
+ const err = schemaError(schema.then, data, at, root);
349
+ if (err) return err;
350
+ }
351
+ }
352
+ if (Object.prototype.hasOwnProperty.call(schema, 'const') && !sameJson(data, schema.const)) {
353
+ return { path: at || '/', message: 'const mismatch' };
354
+ }
355
+ if (schema.enum && !schema.enum.some((item) => sameJson(item, data))) {
356
+ return { path: at || '/', message: 'not in enum' };
357
+ }
358
+ if (schema.type) {
359
+ const err = typeError(schema.type, data, at);
360
+ if (err) return err;
361
+ }
362
+ if (typeof data === 'string') {
363
+ if (schema.minLength !== undefined && [...data].length < schema.minLength) return { path: at || '/', message: 'shorter than minLength' };
364
+ if (schema.maxLength !== undefined && [...data].length > schema.maxLength) return { path: at || '/', message: 'longer than maxLength' };
365
+ if (schema.pattern && !new RegExp(schema.pattern).test(data)) return { path: at || '/', message: 'does not match pattern' };
366
+ }
367
+ if (typeof data === 'number' && schema.type === 'integer') {
368
+ if (schema.minimum !== undefined && data < schema.minimum) return { path: at || '/', message: 'below minimum' };
369
+ if (schema.maximum !== undefined && data > schema.maximum) return { path: at || '/', message: 'above maximum' };
370
+ }
371
+ if (Array.isArray(data)) {
372
+ if (schema.minItems !== undefined && data.length < schema.minItems) return { path: at || '/', message: 'fewer than minItems' };
373
+ if (schema.maxItems !== undefined && data.length > schema.maxItems) return { path: at || '/', message: 'more than maxItems' };
374
+ if (schema.uniqueItems) {
375
+ const seen = new Set();
376
+ for (const item of data) {
377
+ const key = stable(item);
378
+ if (seen.has(key)) return { path: at || '/', message: 'duplicate item' };
379
+ seen.add(key);
380
+ }
381
+ }
382
+ if (schema.items) {
383
+ for (let n = 0; n < data.length; n++) {
384
+ const err = schemaError(schema.items, data[n], pointer(at, n), root);
385
+ if (err) return err;
386
+ }
387
+ }
388
+ }
389
+ if (isObject(data)) {
390
+ const keys = Object.keys(data);
391
+ if (schema.required) {
392
+ for (const key of schema.required) {
393
+ if (!Object.prototype.hasOwnProperty.call(data, key)) return { path: pointer(at, key), message: 'required property missing' };
394
+ }
395
+ }
396
+ if (schema.additionalProperties === false && schema.properties) {
397
+ for (const key of keys) {
398
+ if (!Object.prototype.hasOwnProperty.call(schema.properties, key)) return { path: pointer(at, key), message: 'unexpected property' };
399
+ }
400
+ }
401
+ if (schema.properties) {
402
+ for (const key of keys) {
403
+ if (!schema.properties[key]) continue;
404
+ const err = schemaError(schema.properties[key], data[key], pointer(at, key), root);
405
+ if (err) return err;
406
+ }
407
+ }
408
+ }
409
+ return null;
410
+ }
411
+
412
+ function sameJson(a, b) {
413
+ if (a === b) return true;
414
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
415
+ return stable(a) === stable(b);
416
+ }
417
+
418
+ function isObject(value) {
419
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
420
+ }
421
+
422
+ function typeError(type, data, at) {
423
+ const where = at || '/';
424
+ if (type === 'object' && !isObject(data)) return { path: where, message: 'must be object' };
425
+ if (type === 'array' && !Array.isArray(data)) return { path: where, message: 'must be array' };
426
+ if (type === 'string' && typeof data !== 'string') return { path: where, message: 'must be string' };
427
+ if (type === 'boolean' && typeof data !== 'boolean') return { path: where, message: 'must be boolean' };
428
+ if (type === 'integer' && !Number.isInteger(data)) return { path: where, message: 'must be integer' };
429
+ if (type === 'number' && typeof data !== 'number') return { path: where, message: 'must be number' };
430
+ return null;
431
+ }
432
+
433
+ function extname(rel) {
434
+ const base = rel.split('/').pop() || '';
435
+ const dot = base.lastIndexOf('.');
436
+ return dot <= 0 ? '' : base.slice(dot).toLowerCase();
437
+ }
438
+
439
+ function inside(rootReal, candidateReal) {
440
+ const rel = path.relative(rootReal, candidateReal);
441
+ return rel === '' || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
442
+ }
443
+
444
+ function sha256File(buf) {
445
+ return 'sha256:' + crypto.createHash('sha256').update(buf).digest('hex');
446
+ }
447
+
448
+ /** Equal-length digests compared without an early exit. Unequal lengths fail closed. */
449
+ export function digestEqual(declared, actual) {
450
+ const a = Buffer.from(String(declared));
451
+ const b = Buffer.from(String(actual));
452
+ if (a.length !== b.length) return false;
453
+ return crypto.timingSafeEqual(a, b);
454
+ }
455
+
456
+ async function readRaw(abs) {
457
+ return fs.promises.readFile(abs);
458
+ }
459
+
460
+ function utf8Fatal(buf) {
461
+ try {
462
+ new TextDecoder('utf-8', { fatal: true }).decode(buf);
463
+ return true;
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
468
+
469
+ async function walkFiles(rootReal) {
470
+ const found = [];
471
+ async function walk(dir) {
472
+ const ents = await fs.promises.readdir(dir, { withFileTypes: true });
473
+ for (const ent of ents) {
474
+ const abs = path.join(dir, ent.name);
475
+ if (ent.isSymbolicLink()) {
476
+ const rel = path.relative(rootReal, abs).split(path.sep).join('/');
477
+ if (rel === 'runes' || rel.startsWith('runes/')) {
478
+ const err = new Error('symlink');
479
+ err.code = 'SYMLINK';
480
+ err.rel = rel;
481
+ throw err;
482
+ }
483
+ const real = await fs.promises.realpath(abs);
484
+ if (!inside(rootReal, real)) {
485
+ const err = new Error('symlink escapes');
486
+ err.code = 'ESCAPE';
487
+ err.rel = path.relative(rootReal, abs).split(path.sep).join('/');
488
+ throw err;
489
+ }
490
+ const st = await fs.promises.stat(real);
491
+ if (st.isDirectory()) await walk(abs);
492
+ else if (st.isFile()) found.push(path.relative(rootReal, abs).split(path.sep).join('/'));
493
+ else {
494
+ const err = new Error('not a regular file');
495
+ err.rel = path.relative(rootReal, abs).split(path.sep).join('/');
496
+ throw err;
497
+ }
498
+ } else if (ent.isDirectory()) {
499
+ await walk(abs);
500
+ } else if (ent.isFile()) {
501
+ found.push(path.relative(rootReal, abs).split(path.sep).join('/'));
502
+ } else {
503
+ const err = new Error('unsupported file');
504
+ err.rel = path.relative(rootReal, abs).split(path.sep).join('/');
505
+ throw err;
506
+ }
507
+ }
508
+ }
509
+ await walk(rootReal);
510
+ return found;
511
+ }
512
+
513
+ function pathSyntax(rel) {
514
+ if (typeof rel !== 'string' || rel.length === 0) return 'empty path';
515
+ if (rel.startsWith('/') || rel.startsWith('\\')) return 'absolute path';
516
+ if (rel.includes('\\')) return 'backslash';
517
+ if (rel.split('/').some((part) => part === '..' || part === '.' || part === '')) return 'path segment';
518
+ return null;
519
+ }
520
+
521
+ function stringsIn(value, out) {
522
+ if (typeof value === 'string') out.push(value);
523
+ else if (Array.isArray(value)) for (const item of value) stringsIn(item, out);
524
+ else if (isObject(value)) for (const item of Object.values(value)) stringsIn(item, out);
525
+ return out;
526
+ }
527
+
528
+ function negationHit(text) {
529
+ for (const re of FLOOR_NEGATION) if (re.test(text)) return true;
530
+ return false;
531
+ }
532
+
533
+ function secretHit(text) {
534
+ for (const re of SECRET_PATTERNS) if (re.test(text)) return true;
535
+ return false;
536
+ }
537
+
538
+ class ScanRefuse extends Error {
539
+ constructor(reason) { super(reason); this.reason = reason; }
540
+ }
541
+
542
+ /**
543
+ * Walk source as text for imports, literals, and the calls this revision
544
+ * already named. Comments are not code. String literals are collected and
545
+ * not executed. A template expression is scanned as its own span.
546
+ * node --check is the syntax gate. refuseHostAccess then parses the same
547
+ * source with acorn and refuses a host reach. A parse error fails closed.
548
+ * Input over MAX_SOURCE_BYTES is refused before the walk.
549
+ */
550
+ export function scanModuleSource(source, fromRel) {
551
+ const specifiers = [];
552
+ const literals = [];
553
+ let dynamicImport = false;
554
+ let network = false;
555
+ const s = String(source);
556
+ if (Buffer.byteLength(s) > MAX_SOURCE_BYTES) throw new ScanRefuse('too large');
557
+ const n = s.length;
558
+ let i = 0;
559
+
560
+ function skipSpace() {
561
+ while (i < n && /[ \t\r\n]/.test(s[i])) i++;
562
+ }
563
+ function skipGapIndex(from) {
564
+ let j = from;
565
+ for (;;) {
566
+ while (j < n && /[ \t\r\n]/.test(s[j])) j++;
567
+ if (s[j] === '/' && s[j + 1] === '/') { j += 2; while (j < n && s[j] !== '\n') j++; continue; }
568
+ if (s[j] === '/' && s[j + 1] === '*') {
569
+ j += 2;
570
+ while (j < n && !(s[j] === '*' && s[j + 1] === '/')) j++;
571
+ j = Math.min(n, j + 2);
572
+ continue;
573
+ }
574
+ return j;
575
+ }
576
+ }
577
+ function word(name) {
578
+ if (!s.startsWith(name, i)) return false;
579
+ const before = i > 0 ? s[i - 1] : '';
580
+ const after = s[i + name.length] || '';
581
+ if (before && /[A-Za-z0-9_$]/.test(before)) return false;
582
+ if (after && /[A-Za-z0-9_$]/.test(after)) return false;
583
+ return true;
584
+ }
585
+ function readQuoted() {
586
+ const q = s[i];
587
+ i++;
588
+ let out = '';
589
+ while (i < n) {
590
+ const c = s[i];
591
+ if (c === '\\') {
592
+ const e = s[i + 1];
593
+ const map = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', '\\': '\\', "'": "'", '"': '"', '`': '`' };
594
+ if (map[e] !== undefined) { out += map[e]; i += 2; continue; }
595
+ if (e === '\n') { i += 2; continue; }
596
+ if (e === 'u' && /^[0-9a-fA-F]{4}/.test(s.slice(i + 2, i + 6))) {
597
+ out += String.fromCharCode(parseInt(s.slice(i + 2, i + 6), 16));
598
+ i += 6;
599
+ continue;
600
+ }
601
+ if (e === 'x' && /^[0-9a-fA-F]{2}/.test(s.slice(i + 2, i + 4))) {
602
+ out += String.fromCharCode(parseInt(s.slice(i + 2, i + 4), 16));
603
+ i += 4;
604
+ continue;
605
+ }
606
+ if (e === undefined) throw new ScanRefuse('bad escape');
607
+ out += e;
608
+ i += 2;
609
+ continue;
610
+ }
611
+ if (c === q) { i++; literals.push(out); return out; }
612
+ if (c === '\n' && q !== '`') throw new ScanRefuse('unterminated string');
613
+ out += c;
614
+ i++;
615
+ }
616
+ throw new ScanRefuse('unterminated string');
617
+ }
618
+ function readTemplate() {
619
+ i++;
620
+ let out = '';
621
+ while (i < n) {
622
+ if (s[i] === '\\') {
623
+ out += s[i + 1] === '\n' ? '' : (s[i + 1] || '');
624
+ i += 2;
625
+ continue;
626
+ }
627
+ if (s[i] === '`') { i++; literals.push(out); return; }
628
+ if (s[i] === '$' && s[i + 1] === '{') {
629
+ literals.push(out);
630
+ out = '';
631
+ i += 2;
632
+ const start = i;
633
+ let depth = 1;
634
+ while (i < n && depth > 0) {
635
+ if (s[i] === "'" || s[i] === '"' || s[i] === '`') {
636
+ const inner = scanSlice(i);
637
+ i = inner;
638
+ continue;
639
+ }
640
+ if (s[i] === '{') depth++;
641
+ else if (s[i] === '}') depth--;
642
+ if (depth > 0) i++;
643
+ }
644
+ if (s[i - 1] !== '}' && s[i] !== '}') throw new ScanRefuse('bad template');
645
+ if (s[i] === '}') i++;
646
+ const expr = s.slice(start, i - 1);
647
+ const nested = scanModuleSource(expr, fromRel);
648
+ specifiers.push(...nested.specifiers);
649
+ literals.push(...nested.literals);
650
+ continue;
651
+ }
652
+ out += s[i];
653
+ i++;
654
+ }
655
+ throw new ScanRefuse('unterminated template');
656
+ }
657
+ function scanSlice(from) {
658
+ const saved = i;
659
+ i = from;
660
+ if (s[i] === '`') readTemplate();
661
+ else readQuoted();
662
+ const end = i;
663
+ i = saved;
664
+ return end;
665
+ }
666
+ function noteSpecifier(value) {
667
+ if (value === 'vm' || value === 'node:vm') throw new ScanRefuse('vm');
668
+ specifiers.push(value);
669
+ }
670
+ function expectSpecifier(kind) {
671
+ skipSpace();
672
+ if (s[i] !== "'" && s[i] !== '"' && s[i] !== '`') throw new ScanRefuse('non-literal ' + kind);
673
+ const value = s[i] === '`' ? (readTemplate(), literals[literals.length - 1]) : readQuoted();
674
+ noteSpecifier(value);
675
+ }
676
+ function skipGap() {
677
+ for (;;) {
678
+ skipSpace();
679
+ if (s[i] === '/' && s[i + 1] === '/') { i += 2; while (i < n && s[i] !== '\n') i++; continue; }
680
+ if (s[i] === '/' && s[i + 1] === '*') { i += 2; while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++; i = Math.min(n, i + 2); continue; }
681
+ return;
682
+ }
683
+ }
684
+ function parseImport() {
685
+ i += 'import'.length;
686
+ skipGap();
687
+ if (s[i] === '(') {
688
+ i++;
689
+ skipGap();
690
+ if (s[i] !== "'" && s[i] !== '"') throw new ScanRefuse('computed import');
691
+ const value = readQuoted();
692
+ noteSpecifier(value);
693
+ skipGap();
694
+ if (s[i] !== ')') throw new ScanRefuse('computed import');
695
+ i++;
696
+ dynamicImport = true;
697
+ return;
698
+ }
699
+ if (s[i] === "'" || s[i] === '"' || s[i] === '`') { expectSpecifier('import'); return; }
700
+ let depth = 0;
701
+ while (i < n) {
702
+ const c = s[i];
703
+ if (c === '/' && s[i + 1] === '/') { i += 2; while (i < n && s[i] !== '\n') i++; continue; }
704
+ if (c === '/' && s[i + 1] === '*') { i += 2; while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++; i += 2; continue; }
705
+ if (c === "'" || c === '"') { readQuoted(); continue; }
706
+ if (c === '`') { readTemplate(); continue; }
707
+ if (c === '{') depth++;
708
+ if (c === '}') depth--;
709
+ if (depth === 0 && word('from')) {
710
+ i += 'from'.length;
711
+ expectSpecifier('import');
712
+ return;
713
+ }
714
+ if (depth <= 0 && (c === ';' || c === '\n')) { i++; return; }
715
+ i++;
716
+ }
717
+ }
718
+
719
+ while (i < n) {
720
+ const c = s[i];
721
+ if (c === '/' && s[i + 1] === '/') { i += 2; while (i < n && s[i] !== '\n') i++; continue; }
722
+ if (c === '/' && s[i + 1] === '*') { i += 2; while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++; i = Math.min(n, i + 2); continue; }
723
+ if (c === "'" || c === '"') { readQuoted(); continue; }
724
+ if (c === '`') { readTemplate(); continue; }
725
+ if (word('import')) { parseImport(); continue; }
726
+ if (word('export')) {
727
+ const look = s.slice(i, Math.min(n, i + 400));
728
+ if (/\bfrom\b/.test(look)) {
729
+ i += 'export'.length;
730
+ let depth = 0;
731
+ while (i < n) {
732
+ if (s[i] === "'" || s[i] === '"') { readQuoted(); continue; }
733
+ if (s[i] === '`') { readTemplate(); continue; }
734
+ if (s[i] === '{') depth++;
735
+ if (s[i] === '}') depth--;
736
+ if (depth === 0 && word('from')) { i += 'from'.length; expectSpecifier('export'); break; }
737
+ if (depth <= 0 && s[i] === ';') { i++; break; }
738
+ i++;
739
+ }
740
+ continue;
741
+ }
742
+ }
743
+ if (word('require')) {
744
+ i += 'require'.length;
745
+ skipSpace();
746
+ if (s[i] === '(') { i++; expectSpecifier('require'); continue; }
747
+ }
748
+ if (word('eval')) {
749
+ const at = i + 'eval'.length;
750
+ let j = at;
751
+ while (j < n && /[ \t]/.test(s[j])) j++;
752
+ if (s[j] === '(') throw new ScanRefuse('eval');
753
+ }
754
+ if (word('Function') && s[skipGapIndex(i + 'Function'.length)] === '(') throw new ScanRefuse('Function');
755
+ if (word('fetch') || word('WebSocket') || word('XMLHttpRequest')) network = true;
756
+ if (word('process')) {
757
+ let j = i + 'process'.length;
758
+ while (j < n && /[ \t]/.test(s[j])) j++;
759
+ if (s[j] === '.') {
760
+ j++;
761
+ while (j < n && /[ \t]/.test(s[j])) j++;
762
+ if (s.startsWith('binding', j) || s.startsWith('dlopen', j)) throw new ScanRefuse('process binding');
763
+ }
764
+ }
765
+ i++;
766
+ }
767
+ const gateOptions = { importMode: 'gc16' };
768
+ if (typeof fromRel === 'string' && fromRel.length > 0) gateOptions.from = fromRel;
769
+ refuseHostAccess(s, undefined, gateOptions);
770
+ return { specifiers, literals, dynamicImport, network };
771
+ }
772
+
773
+ function specifierAllowed(spec, fileRel, listed, resolved) {
774
+ return runeSpecifierReason(spec, fileRel, listed, 'gc16', resolved) == null;
775
+ }
776
+
777
+ /**
778
+ * Real path of each relative import that lexically lands under runes/.
779
+ * GC-04 refuses a symlink under runes/ before the scan, so checkGem does
780
+ * not reach this with a link. This is the backstop: a real path outside
781
+ * runes/ is recorded here, and runeSpecifierReason refuses it.
782
+ */
783
+ export async function resolvedUnderRunes(rootReal, fileRel, specs) {
784
+ const resolved = Object.create(null);
785
+ for (const spec of specs) {
786
+ if (typeof spec !== 'string' || (!spec.startsWith('./') && !spec.startsWith('../'))) continue;
787
+ const norm = path.posix.normalize(path.posix.join(path.posix.dirname(fileRel), spec));
788
+ if (!norm.startsWith('runes/') || norm.split('/').includes('..')) continue;
789
+ let real;
790
+ try { real = await fs.promises.realpath(path.resolve(rootReal, ...norm.split('/'))); }
791
+ catch { resolved[norm] = ''; continue; }
792
+ if (!inside(rootReal, real)) { resolved[norm] = ''; continue; }
793
+ resolved[norm] = path.relative(rootReal, real).split(path.sep).join('/');
794
+ }
795
+ return resolved;
796
+ }
797
+
798
+ async function moduleSyntaxOk(buf) {
799
+ const tmp = path.join(os.tmpdir(), `emet-gem-check-${crypto.randomBytes(8).toString('hex')}.mjs`);
800
+ try {
801
+ await fs.promises.writeFile(tmp, buf, { mode: 0o600 });
802
+ const checked = spawnSync(process.execPath, ['--check', tmp], {
803
+ timeout: 5000,
804
+ encoding: 'utf8',
805
+ stdio: ['ignore', 'pipe', 'pipe'],
806
+ env: { PATH: process.env.PATH || '' },
807
+ windowsHide: true
808
+ });
809
+ return Boolean(checked && checked.status === 0);
810
+ } catch {
811
+ return false;
812
+ } finally {
813
+ await fs.promises.rm(tmp, { force: true }).catch(() => {});
814
+ }
815
+ }
816
+
817
+ function lineOf(check) {
818
+ if (check.status === 'pass') return `${check.id} pass`;
819
+ if (check.status === 'skip') return `${check.id} skip: ${check.reason}`;
820
+ return `${check.id} fail: ${check.reason}`;
821
+ }
822
+
823
+ function pass(id) { return { id, status: 'pass', reason: null }; }
824
+ function fail(id, reason) { return { id, status: 'fail', reason }; }
825
+ function skip(id, reason) { return { id, status: 'skip', reason }; }
826
+
827
+ async function loadTrust(options) {
828
+ if (options.trustStore) return options.trustStore;
829
+ if (options.trustStore === null) return null;
830
+ try {
831
+ const text = await fs.promises.readFile(trustStorePath(), 'utf8');
832
+ return JSON.parse(text);
833
+ } catch (e) {
834
+ if (e && e.code === 'ENOENT') return null;
835
+ return { invalid: true };
836
+ }
837
+ }
838
+
839
+ function verifySignature(manifest, store) {
840
+ if (!store || store.invalid || !Array.isArray(store.keys) || store.keys.length === 0) return 'no trust store';
841
+ const signature = manifest.signature;
842
+ if (!signature || typeof signature !== 'object') return 'signature required';
843
+ if (!/^[A-Za-z0-9+/]{86}==$/.test(signature.value || '')) return 'signature encoding';
844
+ const sig = Buffer.from(signature.value, 'base64');
845
+ if (sig.length !== 64) return 'signature length';
846
+ const key = store.keys.find((k) => k && k.keyid === signature.keyid && k.alg === 'Ed25519');
847
+ if (!key || typeof key.publicKey !== 'string') return 'unknown keyid';
848
+ let raw;
849
+ try { raw = Buffer.from(key.publicKey, 'base64'); } catch { return 'trust store invalid'; }
850
+ if (raw.length !== 32) return 'trust store invalid';
851
+ let message;
852
+ try {
853
+ const body = { ...manifest };
854
+ delete body.signature;
855
+ message = canonicalize(body);
856
+ } catch {
857
+ return 'cannot canonicalize';
858
+ }
859
+ let publicKey;
860
+ try {
861
+ publicKey = crypto.createPublicKey({
862
+ key: { kty: 'OKP', crv: 'Ed25519', x: raw.toString('base64url') },
863
+ format: 'jwk'
864
+ });
865
+ } catch {
866
+ return 'trust store invalid';
867
+ }
868
+ const ok = crypto.verify(null, message, publicKey, sig);
869
+ return ok ? null : 'signature did not verify';
870
+ }
871
+
872
+ /**
873
+ * Run every GC check on one GEM directory. Does not apply server refusals.
874
+ * @param {string} dir
875
+ * @param {{ emetVersion?: string, trustStore?: object|null, schema?: object }} [options]
876
+ */
877
+ const gemCheckCache = new Map();
878
+
879
+ async function gemCacheKey(dir, options) {
880
+ let st;
881
+ try { st = await fs.promises.lstat(dir); }
882
+ catch { return null; }
883
+ // A symlink must not share the real directory's cache key. lstat of a
884
+ // link is not a directory, so the symlink clause is what keeps the key empty.
885
+ if (st.isSymbolicLink() || !st.isDirectory()) return null;
886
+ let root;
887
+ try { root = await fs.promises.realpath(dir); }
888
+ catch { return null; }
889
+ const gemPath = path.join(root, 'gem.json');
890
+ let gemBuf;
891
+ let manifest;
892
+ try {
893
+ gemBuf = await fs.promises.readFile(gemPath);
894
+ manifest = JSON.parse(gemBuf.toString('utf8'));
895
+ } catch { return null; }
896
+ const hash = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
897
+ const schemaHash = hash(Buffer.from(JSON.stringify(options.schema || null)));
898
+ // Content, not mtime: a same-size rewrite in the same millisecond must miss.
899
+ const parts = [root, hash(gemBuf), options.emetVersion || '', schemaHash, JSON.stringify(options.trustStore || null)];
900
+ const files = manifest && Array.isArray(manifest.files) ? manifest.files : [];
901
+ for (const file of files) {
902
+ if (!file || typeof file.path !== 'string') { parts.push('bad'); continue; }
903
+ try {
904
+ parts.push(file.path, hash(await fs.promises.readFile(path.join(root, file.path))));
905
+ } catch { parts.push(file.path, 'missing'); }
906
+ }
907
+ return parts.join('\0');
908
+ }
909
+
910
+ export async function checkGem(dir, options = {}) {
911
+ const gemKey = await gemCacheKey(dir, options);
912
+ if (gemKey && gemCheckCache.has(gemKey)) return structuredClone(gemCheckCache.get(gemKey));
913
+ const result = await checkGemUncached(dir, options);
914
+ if (gemKey) gemCheckCache.set(gemKey, result);
915
+ return result;
916
+ }
917
+
918
+ async function checkGemUncached(dir, options = {}) {
919
+ const ctx = {
920
+ dir,
921
+ rootReal: null,
922
+ manifest: null,
923
+ gemBuf: null,
924
+ files: [],
925
+ walked: [],
926
+ bytes: new Map(),
927
+ floor: null,
928
+ floorBuf: null,
929
+ floorTrusted: false,
930
+ floorDigest: null,
931
+ schema: null
932
+ };
933
+ const checks = [];
934
+ async function run(id, fn) {
935
+ try {
936
+ checks.push(await fn());
937
+ } catch (e) {
938
+ const reason = e && e.reason ? e.reason : 'checker threw';
939
+ checks.push(fail(id, reason));
940
+ }
941
+ }
942
+
943
+ await run('GC-01', async () => {
944
+ let st;
945
+ try { st = await fs.promises.lstat(dir); } catch { return fail('GC-01', 'not a directory'); }
946
+ if (st.isSymbolicLink()) return fail('GC-01', 'symlink');
947
+ if (!st.isDirectory()) return fail('GC-01', 'not a directory');
948
+ try { ctx.rootReal = await fs.promises.realpath(dir); } catch { return fail('GC-01', 'directory cannot be resolved'); }
949
+ const gemPath = path.join(ctx.rootReal, 'gem.json');
950
+ try { ctx.gemBuf = await readRaw(gemPath); } catch { return fail('GC-01', 'gem.json missing'); }
951
+ if (!utf8Fatal(ctx.gemBuf)) return fail('GC-01', 'gem.json is not utf-8');
952
+ try { ctx.manifest = parseStrictJson(ctx.gemBuf.toString('utf8')); }
953
+ catch { return fail('GC-01', 'gem.json did not parse'); }
954
+ if (!isObject(ctx.manifest)) return fail('GC-01', 'gem.json is not an object');
955
+ return pass('GC-01');
956
+ });
957
+
958
+ await run('GC-02', async () => {
959
+ if (!ctx.manifest) return fail('GC-02', 'gem.json did not parse');
960
+ try { ctx.schema = loadSchema(options.schema); }
961
+ catch { return fail('GC-02', 'schema missing from checker package'); }
962
+ const err = schemaError(ctx.schema, ctx.manifest, '', ctx.schema);
963
+ if (err) return fail('GC-02', `${err.path} ${err.message}`);
964
+ return pass('GC-02');
965
+ });
966
+
967
+ await run('GC-03', async () => {
968
+ if (!ctx.manifest) return fail('GC-03', 'gem.json did not parse');
969
+ if (ctx.manifest.spec !== SPEC_REVISION) return fail('GC-03', 'spec is not ' + SPEC_REVISION);
970
+ return pass('GC-03');
971
+ });
972
+
973
+ await run('GC-04', async () => {
974
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.files)) return fail('GC-04', 'files missing');
975
+ const rels = ctx.manifest.files.map((f) => f && f.path);
976
+ if (new Set(rels).size !== rels.length) return fail('GC-04', 'duplicate path');
977
+ for (const rel of rels) {
978
+ const why = pathSyntax(rel);
979
+ if (why) return fail('GC-04', why);
980
+ }
981
+ try { ctx.walked = await walkFiles(ctx.rootReal); }
982
+ catch (e) {
983
+ if (e && e.code === 'ESCAPE') return fail('GC-04', 'symlink escapes');
984
+ if (e && e.code === 'SYMLINK') return fail('GC-04', 'symlink');
985
+ return fail('GC-04', 'cannot list files');
986
+ }
987
+ for (const rel of rels) {
988
+ const abs = path.resolve(ctx.rootReal, rel);
989
+ let real;
990
+ try { real = await fs.promises.realpath(abs); } catch { return fail('GC-04', 'file missing'); }
991
+ if (!inside(ctx.rootReal, real)) return fail('GC-04', 'path escapes');
992
+ const st = await fs.promises.stat(real);
993
+ if (!st.isFile()) return fail('GC-04', 'not a regular file');
994
+ }
995
+ if (!rels.includes(ctx.manifest.floor)) return fail('GC-04', 'floor is not listed');
996
+ if (Array.isArray(ctx.manifest.guides)) {
997
+ for (const g of ctx.manifest.guides) if (!rels.includes(g)) return fail('GC-04', 'guide is not listed');
998
+ }
999
+ const expected = new Set(rels);
1000
+ for (const rel of ctx.walked) {
1001
+ if (rel === 'gem.json') continue;
1002
+ if (!expected.has(rel)) return fail('GC-04', 'unlisted file');
1003
+ }
1004
+ if (ctx.walked.length - (ctx.walked.includes('gem.json') ? 1 : 0) !== rels.length) {
1005
+ return fail('GC-04', 'file list does not match the directory');
1006
+ }
1007
+ if (ctx.manifest.kind === 'gem') {
1008
+ for (const rel of rels) if (CODE_EXT.includes(extname(rel))) return fail('GC-04', 'code file on a prompt GEM');
1009
+ }
1010
+ ctx.files = rels;
1011
+ return pass('GC-04');
1012
+ });
1013
+
1014
+ await run('GC-05', async () => {
1015
+ if (!ctx.files.length) return fail('GC-05', 'files missing');
1016
+ let floorTrusted = false;
1017
+ for (const entry of ctx.manifest.files) {
1018
+ const abs = path.resolve(ctx.rootReal, entry.path);
1019
+ const buf = await readRaw(abs);
1020
+ ctx.bytes.set(entry.path, buf);
1021
+ if (buf.length !== entry.size) return fail('GC-05', 'size mismatch');
1022
+ const digest = sha256File(buf);
1023
+ if (!digestEqual(entry.digest, digest)) return fail('GC-05', 'digest mismatch');
1024
+ if (entry.path === ctx.manifest.floor) {
1025
+ floorTrusted = true;
1026
+ ctx.floorBuf = buf;
1027
+ ctx.floorDigest = digest;
1028
+ }
1029
+ }
1030
+ ctx.floorTrusted = floorTrusted;
1031
+ return pass('GC-05');
1032
+ });
1033
+
1034
+ await run('GC-06', async () => {
1035
+ if (!ctx.floorBuf) return fail('GC-06', 'floor missing');
1036
+ if (!utf8Fatal(ctx.floorBuf)) return fail('GC-06', 'floor is not utf-8');
1037
+ let floor;
1038
+ try { floor = parseStrictJson(ctx.floorBuf.toString('utf8')); }
1039
+ catch { return fail('GC-06', 'floor did not parse'); }
1040
+ if (!isObject(floor)) return fail('GC-06', 'floor is not an object');
1041
+ const keys = Object.keys(floor);
1042
+ if (keys.some((k) => k !== 'sections' && k !== 'reassertion')) return fail('GC-06', 'unexpected floor key');
1043
+ if (!isObject(floor.sections)) return fail('GC-06', 'sections missing');
1044
+ for (const [key, section] of Object.entries(floor.sections)) {
1045
+ if (!SECTION_KEY.test(key)) return fail('GC-06', 'section key');
1046
+ if (FORBIDDEN_SECTIONS.has(key)) return fail('GC-06', 'forbidden section');
1047
+ if (!isObject(section)) return fail('GC-06', 'section shape');
1048
+ const sk = Object.keys(section);
1049
+ if (sk.length !== 3 || !sk.includes('title') || !sk.includes('note') || !sk.includes('lines')) return fail('GC-06', 'section shape');
1050
+ if (typeof section.title !== 'string' || typeof section.note !== 'string' || !isObject(section.lines)) return fail('GC-06', 'section shape');
1051
+ for (const line of Object.values(section.lines)) if (typeof line !== 'string') return fail('GC-06', 'nested line');
1052
+ }
1053
+ if (!Array.isArray(floor.reassertion) || floor.reassertion.length < 1 || floor.reassertion.length > 24) return fail('GC-06', 'reassertion length');
1054
+ if (floor.reassertion.some((line) => typeof line !== 'string')) return fail('GC-06', 'reassertion shape');
1055
+ ctx.floor = floor;
1056
+ return pass('GC-06');
1057
+ });
1058
+
1059
+ await run('GC-07', async () => {
1060
+ const guides = ctx.manifest && Array.isArray(ctx.manifest.guides) ? ctx.manifest.guides : null;
1061
+ if (!guides) return fail('GC-07', 'guides missing');
1062
+ if (guides.length > 16) return fail('GC-07', 'more than 16 guides');
1063
+ for (const rel of guides) {
1064
+ const buf = ctx.bytes.get(rel);
1065
+ if (!buf) return fail('GC-07', 'guide missing');
1066
+ if (buf.length > 65536) return fail('GC-07', 'guide exceeds 65536 bytes');
1067
+ if (buf.includes(0)) return fail('GC-07', 'guide contains NUL');
1068
+ if (!utf8Fatal(buf)) return fail('GC-07', 'guide is not utf-8');
1069
+ }
1070
+ return pass('GC-07');
1071
+ });
1072
+
1073
+ await run('GC-08', async () => {
1074
+ if (!ctx.floorBuf) return fail('GC-08', 'floor missing');
1075
+ if (ctx.floorBuf.length > 8192) return fail('GC-08', 'floor exceeds 8192 bytes');
1076
+ return pass('GC-08');
1077
+ });
1078
+
1079
+ await run('GC-09', async () => {
1080
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.files)) return fail('GC-09', 'files missing');
1081
+ const sum = ctx.manifest.files.reduce((n, f) => n + (f && Number.isInteger(f.size) ? f.size : 0), 0);
1082
+ if (sum > 262144) return fail('GC-09', 'files exceed 262144 bytes');
1083
+ return pass('GC-09');
1084
+ });
1085
+
1086
+ await run('GC-10', async () => {
1087
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.runes)) return fail('GC-10', 'runes missing');
1088
+ const n = ctx.manifest.runes.length;
1089
+ if (ctx.manifest.kind === 'gem' && n !== 0) return fail('GC-10', 'prompt GEM has runes');
1090
+ if (ctx.manifest.kind === 'runed-gem' && (n < 1 || n > 16)) return fail('GC-10', 'rune count');
1091
+ const names = ctx.manifest.runes.map((r) => r && r.name);
1092
+ if (new Set(names).size !== names.length) return fail('GC-10', 'duplicate rune name');
1093
+ return pass('GC-10');
1094
+ });
1095
+
1096
+ await run('GC-11', async () => {
1097
+ if (!ctx.manifest) return fail('GC-11', 'gem.json did not parse');
1098
+ if (ctx.walked.includes('keys.json') || ctx.files.includes('keys.json')) return fail('GC-11', 'keys.json inside GEM');
1099
+ const signature = ctx.manifest.signature;
1100
+ if (ctx.manifest.distribution === 'bundled' && (signature === undefined || signature === null)) return pass('GC-11');
1101
+ if (ctx.manifest.distribution === 'library' && (signature === undefined || signature === null)) return fail('GC-11', 'signature required');
1102
+ const store = await loadTrust(options);
1103
+ if (!store) return fail('GC-11', 'no trust store');
1104
+ const why = verifySignature(ctx.manifest, store);
1105
+ if (why) return fail('GC-11', why);
1106
+ return pass('GC-11');
1107
+ });
1108
+
1109
+ await run('GC-12', async () => {
1110
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.overrides)) return fail('GC-12', 'overrides missing');
1111
+ const allowed = new Set(MODIFIABLE_OVERRIDES);
1112
+ for (const entry of ctx.manifest.overrides) {
1113
+ if (typeof entry !== 'string') return fail('GC-12', 'override not modifiable');
1114
+ for (const blocked of PROTECTED_OVERRIDES) {
1115
+ if (entry === blocked || entry.startsWith(blocked + '.')) return fail('GC-12', 'override not modifiable');
1116
+ }
1117
+ if (!allowed.has(entry)) return fail('GC-12', 'override not modifiable');
1118
+ }
1119
+ return pass('GC-12');
1120
+ });
1121
+
1122
+ await run('GC-13', async () => {
1123
+ if (!ctx.floor) return fail('GC-13', 'floor missing');
1124
+ const texts = stringsIn(ctx.floor, []);
1125
+ if (typeof ctx.manifest.description === 'string') texts.push(ctx.manifest.description);
1126
+ if (Array.isArray(ctx.manifest.runes)) {
1127
+ for (const rune of ctx.manifest.runes) if (rune && typeof rune.description === 'string') texts.push(rune.description);
1128
+ }
1129
+ for (const text of texts) if (negationHit(text)) return fail('GC-13', 'floor-negation pattern');
1130
+ return pass('GC-13');
1131
+ });
1132
+
1133
+ await run('GC-14', async () => {
1134
+ const blobs = [];
1135
+ if (ctx.gemBuf) blobs.push(ctx.gemBuf.toString('latin1'));
1136
+ for (const rel of ctx.files) {
1137
+ const buf = ctx.bytes.get(rel);
1138
+ if (buf) blobs.push(buf.toString('latin1'));
1139
+ }
1140
+ if (!blobs.length) return fail('GC-14', 'nothing to scan');
1141
+ for (const text of blobs) if (secretHit(text)) return fail('GC-14', 'secret pattern');
1142
+ return pass('GC-14');
1143
+ });
1144
+
1145
+ await run('GC-15', async () => {
1146
+ if (!ctx.rootReal) return fail('GC-15', 'directory missing');
1147
+ for (const name of ['node_modules', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']) {
1148
+ try {
1149
+ await fs.promises.lstat(path.join(ctx.rootReal, name));
1150
+ return fail('GC-15', name);
1151
+ } catch (e) {
1152
+ if (e && e.code !== 'ENOENT') return fail('GC-15', name);
1153
+ }
1154
+ }
1155
+ let pkg = null;
1156
+ try { pkg = await fs.promises.lstat(path.join(ctx.rootReal, 'package.json')); }
1157
+ catch (e) { if (e && e.code !== 'ENOENT') return fail('GC-15', 'package.json'); }
1158
+ if (pkg && ctx.manifest && ctx.manifest.kind === 'gem') return fail('GC-15', 'package.json');
1159
+ if (pkg) {
1160
+ const buf = ctx.bytes.get('package.json') || await readRaw(path.join(ctx.rootReal, 'package.json'));
1161
+ let parsed;
1162
+ try { parsed = parseStrictJson(buf.toString('utf8')); }
1163
+ catch { return fail('GC-15', 'package.json'); }
1164
+ for (const key of ['dependencies', 'devDependencies', 'optionalDependencies', 'bundledDependencies']) {
1165
+ if (parsed && parsed[key] && typeof parsed[key] === 'object' && Object.keys(parsed[key]).length > 0) {
1166
+ return fail('GC-15', 'dependencies');
1167
+ }
1168
+ }
1169
+ }
1170
+ return pass('GC-15');
1171
+ });
1172
+
1173
+ await run('GC-16', async () => {
1174
+ if (!ctx.rootReal || !ctx.manifest) return fail('GC-16', 'files missing');
1175
+ for (const rel of ctx.files) {
1176
+ const why = runeExtensionReason(rel);
1177
+ if (why) return fail('GC-16', why);
1178
+ }
1179
+ const code = ctx.files.filter((rel) => CODE_EXT.includes(extname(rel)));
1180
+ const js = code.filter((rel) => JS_EXT.includes(extname(rel)) && rel.startsWith('runes/'));
1181
+ const foreign = code.filter((rel) => !JS_EXT.includes(extname(rel)));
1182
+ if (foreign.length) return fail('GC-16', 'unsupported code file');
1183
+ if (!js.length) return skip('GC-16', 'no code');
1184
+ for (const rel of js) {
1185
+ const buf = ctx.bytes.get(rel);
1186
+ if (!buf || buf.length > MAX_SOURCE_BYTES) return fail('GC-16', 'code file size');
1187
+ // Syntax-check a copy named .mjs. The GEM file is not the process
1188
+ // argument, and --check does not run it. A .js file outside this
1189
+ // package would otherwise be parsed as a script.
1190
+ if (!(await moduleSyntaxOk(buf))) return fail('GC-16', 'syntax');
1191
+ let scanned;
1192
+ try { scanned = scanModuleSource(buf.toString('utf8'), rel); }
1193
+ catch (e) { return fail('GC-16', e && e.reason ? e.reason : 'could not scan'); }
1194
+ const resolved = await resolvedUnderRunes(ctx.rootReal, rel, scanned.specifiers);
1195
+ for (const spec of scanned.specifiers) {
1196
+ if (!specifierAllowed(spec, rel, ctx.files, resolved)) return fail('GC-16', 'specifier not allowed');
1197
+ }
1198
+ for (const lit of scanned.literals) {
1199
+ if (/https?:\/\//.test(lit) || /mongodb(?:\+srv)?:\/\//.test(lit)) return fail('GC-16', 'url in code');
1200
+ }
1201
+ }
1202
+ return pass('GC-16');
1203
+ });
1204
+
1205
+ await run('GC-17', async () => {
1206
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.runes)) return fail('GC-17', 'runes missing');
1207
+ const hints = new Set(['title', 'readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint']);
1208
+ for (const rune of ctx.manifest.runes) {
1209
+ if (!rune || typeof rune !== 'object') return fail('GC-17', 'rune shape');
1210
+ if (rune.network !== 'none') return fail('GC-17', 'network');
1211
+ const ann = rune.annotations;
1212
+ if (!isObject(ann)) return fail('GC-17', 'annotations');
1213
+ if (ann.openWorldHint !== false || ann.destructiveHint !== false) return fail('GC-17', 'annotations');
1214
+ for (const key of Object.keys(ann)) if (!hints.has(key)) return fail('GC-17', 'unknown annotation');
1215
+ if (!rune.inputSchema || rune.inputSchema.type !== 'object') return fail('GC-17', 'inputSchema');
1216
+ if (!Array.isArray(rune.reads) || !Array.isArray(rune.writes)) return fail('GC-17', 'access');
1217
+ for (const entry of [...rune.reads, ...rune.writes]) if (typeof entry !== 'string' || !ACCESS_RE.test(entry)) return fail('GC-17', 'access');
1218
+ if (ann.readOnlyHint === true && rune.writes.length !== 0) return fail('GC-17', 'read-only writes');
1219
+ }
1220
+ return pass('GC-17');
1221
+ });
1222
+
1223
+ await run('GC-18', async () => {
1224
+ const js = ctx.files.filter((rel) => JS_EXT.includes(extname(rel)));
1225
+ const banned = ['MongoClient', 'mongoose', 'collection(', '.db(', 'getDb', 'databaseUrl'];
1226
+ for (const rel of js) {
1227
+ const buf = ctx.bytes.get(rel);
1228
+ if (!buf) return fail('GC-18', 'code file missing');
1229
+ const text = buf.toString('latin1');
1230
+ for (const token of banned) if (text.includes(token)) return fail('GC-18', 'raw database access');
1231
+ }
1232
+ return pass('GC-18');
1233
+ });
1234
+
1235
+ await run('GC-19', async () => {
1236
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.runes)) return fail('GC-19', 'runes missing');
1237
+ if (ctx.manifest.runes.length === 0 && ctx.manifest.kind !== 'runed-gem') return skip('GC-19', 'no runes');
1238
+ if (ctx.manifest.runes.length === 0) return fail('GC-19', 'no runes');
1239
+ const prior = checks.find((c) => c.id === 'GC-16');
1240
+ if (prior && prior.status === 'fail') return fail('GC-19', 'static analysis failed');
1241
+ const listed = new Map((ctx.manifest.files || []).filter((f) => f && f.path).map((f) => [f.path, f]));
1242
+ for (const rune of ctx.manifest.runes) {
1243
+ if (!rune || rune.execution !== 'client' || rune.version !== ctx.manifest.version) return fail('GC-19', 'manifest');
1244
+ if (!Array.isArray(rune.reads) || rune.reads.length || !Array.isArray(rune.writes) || rune.writes.length) return fail('GC-19', 'reads and writes empty');
1245
+ if (!rune.annotations || rune.annotations.readOnlyHint !== true || rune.network !== 'none') return fail('GC-19', 'permissions');
1246
+ const pin = listed.get(rune.artifact);
1247
+ const buf = typeof rune.artifact === 'string' ? ctx.bytes.get(rune.artifact) : null;
1248
+ if (!pin || !buf || !String(rune.artifact).startsWith('runes/')) return fail('GC-19', 'artifact');
1249
+ const actual = sha256File(buf);
1250
+ if (!digestEqual(rune.sha256, actual) || !digestEqual(pin.digest, actual)) return fail('GC-19', 'digest');
1251
+ let scanned;
1252
+ try { scanned = scanModuleSource(buf.toString('utf8'), rune.artifact); }
1253
+ catch (e) { return fail('GC-19', e && e.reason ? e.reason : 'load gate'); }
1254
+ if (scanned.specifiers.includes('emet/rune')) return fail('GC-19', 'emet/rune');
1255
+ if (scanned.dynamicImport) return fail('GC-19', 'dynamic import');
1256
+ if (scanned.network) return fail('GC-19', 'network');
1257
+ }
1258
+ return pass('GC-19');
1259
+ });
1260
+
1261
+ await run('GC-20', async () => {
1262
+ const text = ctx.manifest && ctx.manifest.requires && ctx.manifest.requires.emet;
1263
+ if (typeof text !== 'string' || !parseEmetRange(text)) return fail('GC-20', 'requires.emet grammar');
1264
+ if (options.emetVersion && !satisfiesEmetRange(options.emetVersion, text)) {
1265
+ return fail('GC-20', 'requires.emet excludes this version');
1266
+ }
1267
+ return pass('GC-20');
1268
+ });
1269
+
1270
+ await run('GC-21', async () => {
1271
+ if (!ctx.manifest || !Array.isArray(ctx.manifest.runes)) return fail('GC-21', 'runes missing');
1272
+ const hints = new Set(['title', 'readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint']);
1273
+ for (const rune of ctx.manifest.runes) {
1274
+ if (!rune || !rune.inputSchema || rune.inputSchema.type !== 'object' || typeof rune.inputSchema.type !== 'string') {
1275
+ return fail('GC-21', 'inputSchema');
1276
+ }
1277
+ if (Object.prototype.hasOwnProperty.call(rune, 'outputSchema') && !isObject(rune.outputSchema)) {
1278
+ return fail('GC-21', 'outputSchema');
1279
+ }
1280
+ if (!isObject(rune.annotations)) return fail('GC-21', 'annotations');
1281
+ for (const key of Object.keys(rune.annotations)) if (!hints.has(key)) return fail('GC-21', 'unknown annotation');
1282
+ }
1283
+ return pass('GC-21');
1284
+ });
1285
+
1286
+ await run('GC-22', async () => {
1287
+ if (!ctx.manifest) return fail('GC-22', 'gem.json did not parse');
1288
+ const listed = new Set(ctx.files);
1289
+ for (const rel of ctx.walked) {
1290
+ if (!CODE_EXT.includes(extname(rel))) continue;
1291
+ if (ctx.manifest.kind !== 'runed-gem') return fail('GC-22', 'code file on a prompt GEM');
1292
+ if (!rel.startsWith('runes/')) return fail('GC-22', 'code file outside runes/');
1293
+ if (!listed.has(rel)) return fail('GC-22', 'code file is not listed');
1294
+ }
1295
+ return pass('GC-22');
1296
+ });
1297
+
1298
+ const lines = checks.map(lineOf);
1299
+ const first = checks.find((c) => c.status === 'fail') || null;
1300
+ return {
1301
+ ok: !first,
1302
+ exitCode: first ? 1 : 0,
1303
+ lines,
1304
+ checks,
1305
+ firstFailure: first ? first.id : null,
1306
+ firstDetail: first ? first.reason : null,
1307
+ manifest: ctx.manifest,
1308
+ floor: ctx.floor,
1309
+ floorBytes: ctx.floorTrusted ? ctx.floorBuf.length : 0,
1310
+ floorTrusted: ctx.floorTrusted,
1311
+ floorDigest: ctx.floorDigest
1312
+ };
1313
+ }
1314
+
1315
+ function invokedDirectly() {
1316
+ const arg = process.argv[1];
1317
+ if (!arg) return false;
1318
+ try { return import.meta.url === pathToFileURL(path.resolve(arg)).href; }
1319
+ catch { return false; }
1320
+ }
1321
+
1322
+ async function cli() {
1323
+ const argv = process.argv.slice(2);
1324
+ let emetVersion = null;
1325
+ const position = [];
1326
+ for (let n = 0; n < argv.length; n++) {
1327
+ if (argv[n] === '--emet-version') {
1328
+ emetVersion = argv[++n];
1329
+ if (!emetVersion) {
1330
+ process.stderr.write('gem-check: --emet-version needs x.y.z\n');
1331
+ process.exit(2);
1332
+ }
1333
+ } else position.push(argv[n]);
1334
+ }
1335
+ if (position.length !== 1) {
1336
+ process.stderr.write('gem-check: one directory is required\n');
1337
+ process.exit(2);
1338
+ }
1339
+ let st;
1340
+ try { st = await fs.promises.stat(position[0]); }
1341
+ catch {
1342
+ process.stderr.write('gem-check: not a directory\n');
1343
+ process.exit(2);
1344
+ }
1345
+ if (!st.isDirectory()) {
1346
+ process.stderr.write('gem-check: not a directory\n');
1347
+ process.exit(2);
1348
+ }
1349
+ try {
1350
+ const result = await checkGem(path.resolve(position[0]), { emetVersion });
1351
+ process.stdout.write(result.lines.join('\n') + '\n');
1352
+ process.exit(result.exitCode);
1353
+ } catch {
1354
+ process.stdout.write('GC-01 fail: checker threw\n');
1355
+ process.exit(1);
1356
+ }
1357
+ }
1358
+
1359
+ if (invokedDirectly()) cli();