create-pmos 0.15.0 → 0.16.0

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 (36) hide show
  1. package/README.md +38 -0
  2. package/package.json +1 -1
  3. package/payload/KIT-VERSION +1 -1
  4. package/workspace/README.md +44 -12
  5. package/workspace/design-parity.js +358 -0
  6. package/workspace/lib/find-browser.js +60 -0
  7. package/workspace/lib/four-panel.js +345 -0
  8. package/workspace/lib/git-info.js +28 -1
  9. package/workspace/lib/mobile-nav.js +32 -2
  10. package/workspace/package-lock.json +29 -2
  11. package/workspace/package.json +13 -5
  12. package/workspace/public/index.html +79 -409
  13. package/workspace/public/pm/app.js +242 -0
  14. package/workspace/public/pm/data.js +877 -0
  15. package/workspace/public/pm/dom.js +116 -0
  16. package/workspace/public/pm/fixture.js +979 -0
  17. package/workspace/public/pm/mobile.css +200 -0
  18. package/workspace/public/pm/mobile.js +403 -0
  19. package/workspace/public/pm/modes.js +1076 -0
  20. package/workspace/public/pm/panel-detail.js +648 -0
  21. package/workspace/public/pm/panel-initiatives.js +426 -0
  22. package/workspace/public/pm/panel-repos.js +460 -0
  23. package/workspace/public/pm/panel-tabs.js +487 -0
  24. package/workspace/public/pm/state.js +164 -0
  25. package/workspace/public/pm/tab-doc.js +279 -0
  26. package/workspace/public/pm/tab-task.js +83 -0
  27. package/workspace/public/pm/tab-term.js +560 -0
  28. package/workspace/public/pm/tokens.css +73 -0
  29. package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
  30. package/workspace/server.js +158 -8
  31. package/workspace/focus.test.js +0 -650
  32. package/workspace/handoff.test.js +0 -443
  33. package/workspace/profiles.test.js +0 -362
  34. package/workspace/public/app.js +0 -5560
  35. package/workspace/public/styles.css +0 -2258
  36. package/workspace/repo-scan.test.js +0 -327
@@ -1,327 +0,0 @@
1
- #!/usr/bin/env node
2
- /* Unit suite for lib/repo-scan.js — the product-ingest scanner.
3
- *
4
- * No framework, no mocks: every case builds a REAL temporary repository on disk and scans it, the
5
- * same way workspace/test.js builds real repos for the server suite. The filesystem behaviours that
6
- * matter here (a symlink, an oversized file, a directory that is not there) cannot be exercised
7
- * honestly against a stub.
8
- *
9
- * The suite is organised around the one invariant the module exists to hold:
10
- *
11
- * A · proposal shape + the declared/guessed separation (a guess must never read as a declaration)
12
- * B · the guess itself — single candidate, ambiguity, nothing, frontmatter-is-not-body
13
- * C · absent inputs are CALM and STATED (missing planning/, missing okf/, empty directories)
14
- * D · failed reads are RED and carry a reason — never silently skipped, never fatal to the scan
15
- * E · ADRs
16
- * F · purity + determinism (the scan writes nothing and repeats itself)
17
- * G · the real repository, as a corpus check
18
- *
19
- * Exits non-zero if any check fails.
20
- */
21
- 'use strict';
22
- const fs = require('fs');
23
- const os = require('os');
24
- const path = require('path');
25
- const assert = require('assert');
26
-
27
- const scan = require('./lib/repo-scan');
28
-
29
- const results = [];
30
- const ok = (name, cond, extra = '') => { results.push({ name, cond }); console.log(`${cond ? '✓' : '✗'} ${name}${extra ? ' — ' + extra : ''}`); };
31
-
32
- const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'pmos-repo-scan-'));
33
- const made = [];
34
- function makeRepo(name) {
35
- const root = path.join(TMP, name);
36
- fs.mkdirSync(root, { recursive: true });
37
- made.push(root);
38
- return root;
39
- }
40
- function write(root, rel, text) {
41
- const full = path.join(root, rel);
42
- fs.mkdirSync(path.dirname(full), { recursive: true });
43
- fs.writeFileSync(full, text);
44
- return full;
45
- }
46
- /** An initiative record with whatever frontmatter the case needs. */
47
- function record(fm, body = 'Body text with nothing anchor-shaped in it.\n') {
48
- const keys = Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join('\n');
49
- return `---\n${keys}\n---\n\n${body}`;
50
- }
51
- const logHas = (r, needle) => r.log.some((l) => (l.message + ' ' + (l.path || '')).includes(needle));
52
- const byPath = (r, p) => r.initiatives.find((i) => i.path === p);
53
-
54
- try {
55
- // ── A · proposal shape + the declared/guessed separation ────────────────────────────────────
56
- const A = makeRepo('full');
57
- write(A, 'planning/initiatives/declared.md', record({
58
- type: 'Initiative', title: '"declared"', timestamp: '2026-07-29',
59
- id: 'declared', lane: 'feature', parent_kr: 'q3-2026:o1:kr1',
60
- }, 'This record also mentions health:tech in its prose, which must change nothing.\n'));
61
- write(A, 'planning/initiatives/guessable.md', record({
62
- type: 'Initiative', title: '"guessable"', timestamp: '2026-07-29', id: 'guessable', lane: 'chore',
63
- }, 'Carved out of the work anchored to q3-2026:o1:kr2 last quarter.\n'));
64
- write(A, 'planning/initiatives/ambiguous.md', record({
65
- type: 'Initiative', title: '"ambiguous"', timestamp: '2026-07-29', id: 'ambiguous', lane: 'chore',
66
- }, 'Touches q3-2026:o1:kr1 and also health:tech, so it is not obvious which one owns it.\n'));
67
- write(A, 'planning/initiatives/bare.md', record({
68
- type: 'Initiative', title: '"bare"', timestamp: '2026-07-29', id: 'bare', lane: 'chore',
69
- }));
70
- write(A, 'planning/initiatives/empty-anchor.md', record({
71
- type: 'Initiative', title: '"empty anchor"', timestamp: '2026-07-29', id: 'empty-anchor',
72
- lane: 'chore', parent_kr: '',
73
- }));
74
- write(A, 'planning/initiatives/README.md', record({ type: 'Reference', title: '"readme"', timestamp: '2026-07-29' }));
75
- const a = scan.scanRepo(A);
76
-
77
- ok('A1: a readable repo scans ok with the documented shape',
78
- a.ok === true && a.error === null && Array.isArray(a.initiatives) && Array.isArray(a.adrs)
79
- && Array.isArray(a.log) && Array.isArray(a.unreadable),
80
- JSON.stringify({ ok: a.ok, n: a.initiatives.length }));
81
-
82
- const dec = byPath(a, 'planning/initiatives/declared.md');
83
- ok('A2: a declared parent_kr lands in declaredAnchor and NOTHING lands in guessedAnchor',
84
- !!dec && dec.declaredAnchor === 'q3-2026:o1:kr1' && dec.guessedAnchor === null,
85
- JSON.stringify({ declared: dec && dec.declaredAnchor, guessed: dec && dec.guessedAnchor }));
86
-
87
- ok('A3: a declaration is never second-guessed — a body mention of a DIFFERENT anchor is ignored',
88
- !!dec && dec.guessedAnchor === null && dec.why.includes('not an inference'),
89
- dec ? dec.why.slice(0, 60) : 'missing');
90
-
91
- ok('A4: the separation invariant — no row EVER carries both a declaration and a guess',
92
- a.initiatives.every((r) => !(r.declaredAnchor && r.guessedAnchor)),
93
- JSON.stringify(a.initiatives.map((r) => [r.id, !!r.declaredAnchor, !!r.guessedAnchor])));
94
-
95
- ok('A5: every proposed row states its anchor reasoning in `why` (a field a PM can check)',
96
- a.initiatives.every((r) => typeof r.why === 'string' && r.why.length > 40),
97
- JSON.stringify(a.initiatives.map((r) => (r.why || '').length)));
98
-
99
- ok('A6: a non-Initiative markdown file is NOT proposed and its absence is stated in the log',
100
- !byPath(a, 'planning/initiatives/README.md') && logHas(a, 'declares `type: Reference`, not Initiative'),
101
- JSON.stringify(a.initiatives.map((r) => r.id)));
102
-
103
- // ── B · the guess ───────────────────────────────────────────────────────────────────────────
104
- const g = byPath(a, 'planning/initiatives/guessable.md');
105
- ok('B1: exactly one anchor-shaped token in the body becomes a GUESS, with its line number',
106
- !!g && g.declaredAnchor === null && g.guessedAnchor === 'q3-2026:o1:kr2' && /line \d+/.test(g.why),
107
- g ? g.guessedAnchor + ' / ' + g.why.slice(0, 70) : 'missing');
108
-
109
- ok('B2: the guess `why` says out loud that a mention is not a declaration and D59 still bites',
110
- !!g && g.why.includes('not a declaration') && g.why.includes('D59'),
111
- g ? g.why.slice(-90) : 'missing');
112
-
113
- const amb = byPath(a, 'planning/initiatives/ambiguous.md');
114
- ok('B3: two candidates yields NO guess and names both — the scan does not pick a winner',
115
- !!amb && amb.guessedAnchor === null && amb.why.includes('q3-2026:o1:kr1') && amb.why.includes('health:tech'),
116
- amb ? amb.why.slice(0, 100) : 'missing');
117
-
118
- const bare = byPath(a, 'planning/initiatives/bare.md');
119
- ok('B4: nothing anchor-shaped anywhere → both fields null and the record is called unanchored',
120
- !!bare && bare.declaredAnchor === null && bare.guessedAnchor === null && bare.why.includes('unanchored'),
121
- bare ? bare.why.slice(0, 80) : 'missing');
122
-
123
- const emptyA = byPath(a, 'planning/initiatives/empty-anchor.md');
124
- ok('B5: `parent_kr:` present but empty is reported as present-and-empty, not as absent',
125
- !!emptyA && emptyA.declaredAnchor === null && emptyA.why.includes('present but empty'),
126
- emptyA ? emptyA.why.slice(0, 70) : 'missing');
127
-
128
- // The frontmatter/body boundary. A status line quoting an anchor is prose about an anchor, not a
129
- // second declaration site — if this leaked, every record with a chatty `status:` would acquire a
130
- // guess nobody wrote.
131
- const B = makeRepo('fm-boundary');
132
- write(B, 'planning/initiatives/chatty.md', record({
133
- type: 'Initiative', title: '"chatty"', timestamp: '2026-07-29', id: 'chatty', lane: 'chore',
134
- status: '"BLOCKED — was going to hang off q3-2026:o1:kr1 until that KR was retired"',
135
- }));
136
- const b = scan.scanRepo(B);
137
- ok('B6: an anchor quoted in ANOTHER frontmatter key is not mined — only `parent_kr` declares',
138
- b.initiatives.length === 1 && b.initiatives[0].guessedAnchor === null && b.initiatives[0].declaredAnchor === null,
139
- JSON.stringify(b.initiatives.map((r) => [r.declaredAnchor, r.guessedAnchor])));
140
-
141
- ok('B7: the guesser is narrow by design — a bare legacy `O1.KR3` in prose is NOT guessed at',
142
- scan.guessAnchorFromBody('body mentions O1.KR3 and **O1** only\n', 1).length === 0,
143
- JSON.stringify(scan.guessAnchorFromBody('body mentions O1.KR3 and **O1**\n', 1)));
144
-
145
- // ── C · absence is calm and stated ──────────────────────────────────────────────────────────
146
- const C = makeRepo('bare-folder');
147
- const c = scan.scanRepo(C);
148
- ok('C1: a folder with no planning/ and no okf/ is a SUCCESSFUL read of nothing, not an error',
149
- c.ok === true && c.error === null && c.initiatives.length === 0 && c.adrs.length === 0 && c.unreadable.length === 0,
150
- JSON.stringify({ ok: c.ok, err: c.error }));
151
-
152
- ok('C2: …and both absences are STATED in the log rather than left to be inferred from emptiness',
153
- logHas(c, 'planning/initiatives') && logHas(c, 'does not exist') && logHas(c, 'okf'),
154
- JSON.stringify(c.log.map((l) => l.path)));
155
-
156
- const C2 = makeRepo('empty-dirs');
157
- fs.mkdirSync(path.join(C2, 'planning', 'initiatives'), { recursive: true });
158
- fs.mkdirSync(path.join(C2, 'okf', 'products', 'x', 'adr'), { recursive: true });
159
- const c2 = scan.scanRepo(C2);
160
- ok('C3: directories that exist but hold nothing are called empty reads, not failures',
161
- c2.ok === true && c2.unreadable.length === 0 && logHas(c2, 'holds no markdown files')
162
- && logHas(c2, 'exists but holds no markdown files'),
163
- JSON.stringify(c2.log.filter((l) => l.level !== 'info').map((l) => l.message)));
164
-
165
- // ── D · failed reads are red, carry a reason, and never end the scan ────────────────────────
166
- const D = makeRepo('broken');
167
- write(D, 'planning/initiatives/good.md', record({
168
- type: 'Initiative', title: '"good"', timestamp: '2026-07-29', id: 'good', lane: 'chore', parent_kr: 'health:tech',
169
- }));
170
- write(D, 'planning/initiatives/no-type.md', '---\ntitle: "no type"\ntimestamp: 2026-07-29\nid: no-type\n---\n\nbody\n');
171
- write(D, 'planning/initiatives/unterminated.md', '---\ntype: Initiative\ntitle: "oops"\nid: unterminated\n\nbody that was meant to be below a closing fence\n');
172
- write(D, 'planning/initiatives/huge.md', '---\ntype: Initiative\ntitle: "huge"\nid: huge\n---\n' + 'x'.repeat(scan.MAX_BYTES + 10));
173
- fs.symlinkSync(path.join(D, 'planning', 'initiatives', 'good.md'), path.join(D, 'planning', 'initiatives', 'linked.md'));
174
- const d = scan.scanRepo(D);
175
- const un = (p) => d.unreadable.find((u) => u.path === p);
176
-
177
- ok('D1: a broken file does not end the scan — the readable record beside it is still proposed',
178
- d.ok === true && d.initiatives.length === 1 && d.initiatives[0].id === 'good',
179
- JSON.stringify({ ok: d.ok, ids: d.initiatives.map((r) => r.id) }));
180
-
181
- ok('D2: a file with no `type:` is unreadable WITH ITS REASON, never silently skipped',
182
- !!un('planning/initiatives/no-type.md') && un('planning/initiatives/no-type.md').error.includes('D63'),
183
- un('planning/initiatives/no-type.md') ? un('planning/initiatives/no-type.md').error.slice(0, 70) : 'absent');
184
-
185
- ok('D3: an unterminated frontmatter fence is reported as malformed, not parsed leniently',
186
- !!un('planning/initiatives/unterminated.md') && un('planning/initiatives/unterminated.md').error.includes('never closes'),
187
- un('planning/initiatives/unterminated.md') ? un('planning/initiatives/unterminated.md').error.slice(0, 70) : 'absent');
188
-
189
- ok('D4: a file past the size cap is refused with its size — not read, nothing claimed about it',
190
- !!un('planning/initiatives/huge.md') && /\d+ bytes/.test(un('planning/initiatives/huge.md').error),
191
- un('planning/initiatives/huge.md') ? un('planning/initiatives/huge.md').error.slice(0, 80) : 'absent');
192
-
193
- ok('D5: a symlinked record is refused rather than followed (provenance, not paranoia)',
194
- !!un('planning/initiatives/linked.md') && un('planning/initiatives/linked.md').error.includes('symbolic link'),
195
- un('planning/initiatives/linked.md') ? un('planning/initiatives/linked.md').error.slice(0, 60) : 'absent');
196
-
197
- ok('D6: every unreadable file also appears in the human log as an error — neither list is partial',
198
- d.unreadable.every((u) => d.log.some((l) => l.level === 'error' && l.path === u.path && l.message === u.error)),
199
- JSON.stringify({ unreadable: d.unreadable.length, errorLines: d.log.filter((l) => l.level === 'error').length }));
200
-
201
- ok('D7: the log warns that unreadable files are NOT represented in the proposal above',
202
- logHas(d, 'NOT represented in the proposal'),
203
- JSON.stringify(d.log.filter((l) => l.level === 'warn').map((l) => l.message.slice(0, 40))));
204
-
205
- // A root that is not there, and a root that is a file. Both are "the scan could not run" — the one
206
- // case where ok is false — and both must still return the full shape so no consumer crashes.
207
- const missing = scan.scanRepo(path.join(TMP, 'no-such-repo'));
208
- ok('D8: a missing root is ok:false with a reason, and still returns every array',
209
- missing.ok === false && typeof missing.error === 'string' && missing.error.length > 20
210
- && Array.isArray(missing.initiatives) && Array.isArray(missing.adrs)
211
- && Array.isArray(missing.log) && Array.isArray(missing.unreadable),
212
- (missing.error || '').slice(0, 60));
213
-
214
- const filePath = write(makeRepo('is-a-file'), 'thing.md', 'x');
215
- const notDir = scan.scanRepo(filePath);
216
- ok('D9: a root that is a file is ok:false, not an empty proposal',
217
- notDir.ok === false && notDir.error.includes('not a directory'), (notDir.error || '').slice(0, 50));
218
-
219
- const noArg = scan.scanRepo(undefined);
220
- ok('D10: no path at all is refused without throwing', noArg.ok === false && noArg.initiatives.length === 0,
221
- (noArg.error || '').slice(0, 50));
222
-
223
- // ── D11/D12 · identity edges ────────────────────────────────────────────────────────────────
224
- const E0 = makeRepo('identity');
225
- write(E0, 'planning/initiatives/stem-id.md', record({
226
- type: 'Initiative', title: '"no id"', timestamp: '2026-07-29', lane: 'chore', parent_kr: 'health:tech',
227
- }));
228
- write(E0, 'planning/initiatives/dup-a.md', record({
229
- type: 'Initiative', title: '"dup a"', timestamp: '2026-07-29', id: 'same', lane: 'chore', parent_kr: 'health:tech',
230
- }));
231
- write(E0, 'planning/initiatives/dup-b.md', record({
232
- type: 'Initiative', title: '"dup b"', timestamp: '2026-07-29', id: 'same', lane: 'chore', parent_kr: 'health:tech',
233
- }));
234
- write(E0, 'planning/initiatives/index.md', '# initiatives\n\n- a listing, no frontmatter\n');
235
- const e0 = scan.scanRepo(E0);
236
- ok('D11: a record with no `id:` falls back to the filename stem and the fallback is logged as a guess',
237
- !!byPath(e0, 'planning/initiatives/stem-id.md') && byPath(e0, 'planning/initiatives/stem-id.md').id === 'stem-id'
238
- && logHas(e0, 'A filename is a guess at an identity'),
239
- JSON.stringify(e0.initiatives.map((r) => r.id)));
240
-
241
- ok('D12: two records claiming one id are BOTH kept and the collision is flagged for a human',
242
- e0.initiatives.filter((r) => r.id === 'same').length === 2 && logHas(e0, 'propose the same id'),
243
- JSON.stringify(e0.initiatives.map((r) => r.id)));
244
-
245
- ok('D13: OKF-reserved index.md is not treated as a record, and the log says why',
246
- !byPath(e0, 'planning/initiatives/index.md') && logHas(e0, 'OKF-reserved filename'),
247
- JSON.stringify(e0.initiatives.map((r) => r.path)));
248
-
249
- // ── E · ADRs ────────────────────────────────────────────────────────────────────────────────
250
- const E = makeRepo('adrs');
251
- write(E, 'okf/products/thing/adr/d01-first.md', '---\ntype: ADR\ntitle: D01 — the first decision\ntimestamp: 2026-07-29\n---\n\nbody\n');
252
- write(E, 'okf/products/thing/adr/d02-second.md', '---\ntype: ADR\ntimestamp: 2026-07-29\n---\n\nno title here\n');
253
- write(E, 'okf/products/thing/adr/notes.md', '---\ntype: Reference\ntitle: notes\ntimestamp: 2026-07-29\n---\n\nbody\n');
254
- write(E, 'okf/core/concepts/thing.md', '---\ntype: Concept\ntitle: c\ntimestamp: 2026-07-29\n---\n\nbody\n');
255
- write(E, 'okf/products/other/adr/d09-nested.md', '---\ntype: ADR\ntitle: D09 — nested elsewhere\ntimestamp: 2026-07-29\n---\n\nbody\n');
256
- const e = scan.scanRepo(E);
257
- ok('E1: ADRs are found in every okf/**/adr/ directory, id from the filename, title from frontmatter',
258
- e.adrs.length === 3
259
- && e.adrs.some((x) => x.id === 'd01-first' && x.title === 'D01 — the first decision' && x.path === 'okf/products/thing/adr/d01-first.md')
260
- && e.adrs.some((x) => x.id === 'd09-nested'),
261
- JSON.stringify(e.adrs.map((x) => x.id)));
262
-
263
- ok('E2: an ADR with no title gets a NULL title — a filename is never printed as a title',
264
- (e.adrs.find((x) => x.id === 'd02-second') || {}).title === null && logHas(e, 'null title rather than a filename'),
265
- JSON.stringify(e.adrs.find((x) => x.id === 'd02-second')));
266
-
267
- ok('E3: a non-ADR file inside adr/ is not imported on the strength of its location, and is logged',
268
- !e.adrs.some((x) => x.id === 'notes') && logHas(e, 'declares `type: Reference`, not ADR'),
269
- JSON.stringify(e.adrs.map((x) => x.id)));
270
-
271
- ok('E4: a concept outside adr/ is not swept in',
272
- !e.adrs.some((x) => x.path.includes('concepts')), JSON.stringify(e.adrs.map((x) => x.path)));
273
-
274
- ok('E5: the ADR log line says the human still has to accept or supersede each one',
275
- logHas(e, 'accepted or superseded by a human'),
276
- JSON.stringify(e.log.filter((l) => (l.message || '').includes('ADR(s)')).map((l) => l.message)));
277
-
278
- // ── F · purity + determinism ────────────────────────────────────────────────────────────────
279
- function snapshot(dir) {
280
- const out = [];
281
- (function walk(d) {
282
- for (const ent of fs.readdirSync(d, { withFileTypes: true }).sort((x, y) => (x.name < y.name ? -1 : 1))) {
283
- const full = path.join(d, ent.name);
284
- const st = fs.lstatSync(full);
285
- out.push(`${path.relative(dir, full)}|${st.size}|${st.mtimeMs}|${st.isDirectory() ? 'd' : 'f'}`);
286
- if (st.isDirectory()) walk(full);
287
- }
288
- })(dir);
289
- return out.join('\n');
290
- }
291
- const before = snapshot(A);
292
- const a2 = scan.scanRepo(A);
293
- const after = snapshot(A);
294
- ok('F1: the scan writes NOTHING — no file, size or mtime in the tree changes', before === after,
295
- before === after ? '' : 'tree changed');
296
-
297
- let same = true;
298
- try { assert.deepStrictEqual(a2, a); } catch { same = false; }
299
- ok('F2: two scans of the same tree return an identical proposal (stable order, no clock in it)', same);
300
-
301
- ok('F3: proposals are path-sorted, so an import diff is signal rather than reordering noise',
302
- a.initiatives.map((r) => r.path).join() === [...a.initiatives.map((r) => r.path)].sort().join(),
303
- a.initiatives.map((r) => r.path).join(', '));
304
-
305
- // ── G · the real corpus ─────────────────────────────────────────────────────────────────────
306
- // The fixtures above are written by this file and therefore agree with it by construction. This
307
- // one check runs against ~100 records nobody wrote for the scanner.
308
- const self = scan.scanRepo(path.join(__dirname, '..'));
309
- ok('G1: this repository scans ok and proposes its real initiative records',
310
- self.ok === true && self.initiatives.length > 50,
311
- `${self.initiatives.length} initiatives, ${self.adrs.length} ADRs, ${self.unreadable.length} unreadable`);
312
-
313
- ok('G2: on a real corpus the declared/guessed separation still holds row for row',
314
- self.initiatives.every((r) => !(r.declaredAnchor && r.guessedAnchor)),
315
- `${self.initiatives.filter((r) => r.declaredAnchor).length} declared, ${self.initiatives.filter((r) => r.guessedAnchor).length} guessed`);
316
-
317
- ok('G3: the real ADR set is found with titles',
318
- self.adrs.length > 40 && self.adrs.every((x) => x.id && x.path) && self.adrs.filter((x) => x.title).length > 40,
319
- `${self.adrs.length} ADRs, ${self.adrs.filter((x) => !x.title).length} untitled`);
320
- } catch (e) {
321
- ok('no exceptions during suite', false, e && e.stack ? e.stack.split('\n').slice(0, 2).join(' @ ') : String(e));
322
- } finally {
323
- try { fs.rmSync(TMP, { recursive: true, force: true }); } catch {}
324
- const failed = results.filter((r) => !r.cond);
325
- console.log(`\n${results.length - failed.length}/${results.length} passed${failed.length ? ' — FAILURES: ' + failed.map((f) => f.name).join('; ') : ''}`);
326
- process.exit(failed.length ? 1 : 0);
327
- }