@trohde/earos 1.2.0 → 1.3.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 (69) hide show
  1. package/assets/init/docs/getting-started.md +1 -1
  2. package/assets/init/docs/onboarding/agent-assisted.md +19 -19
  3. package/assets/init/docs/onboarding/first-assessment.md +18 -18
  4. package/assets/init/docs/onboarding/governed-review.md +10 -10
  5. package/assets/init/docs/onboarding/overview.md +15 -15
  6. package/assets/init/docs/onboarding/scaling-optimization.md +13 -13
  7. package/assets/init/docs/plans/2026-03-23-001-refactor-site-review-findings-plan.md +195 -0
  8. package/assets/init/docs/plans/2026-03-23-002-refactor-cli-review-findings-plan.md +736 -0
  9. package/assets/init/docs/terminology.md +1 -1
  10. package/bin.js +156 -36
  11. package/dist/assets/{_basePickBy-PmSUrUsK.js → _basePickBy-BlC_TeV6.js} +1 -1
  12. package/dist/assets/{_baseUniq-HuZouVIz.js → _baseUniq-CVy7rcC1.js} +1 -1
  13. package/dist/assets/{arc-CJFxtF3d.js → arc-Cd8wvd7z.js} +1 -1
  14. package/dist/assets/{architectureDiagram-2XIMDMQ5-XA-oU2UG.js → architectureDiagram-2XIMDMQ5-D_f4_aMp.js} +1 -1
  15. package/dist/assets/{blockDiagram-WCTKOSBZ-Oxp-wAST.js → blockDiagram-WCTKOSBZ-B-y6N5--.js} +1 -1
  16. package/dist/assets/{c4Diagram-IC4MRINW-D8m5hQH9.js → c4Diagram-IC4MRINW-C3-v3oNT.js} +1 -1
  17. package/dist/assets/channel-BSC0F15G.js +1 -0
  18. package/dist/assets/{chunk-4BX2VUAB-D2kBTn2O.js → chunk-4BX2VUAB-CMPwQN83.js} +1 -1
  19. package/dist/assets/{chunk-55IACEB6-Dxqrf5oZ.js → chunk-55IACEB6-Bdkfhvrr.js} +1 -1
  20. package/dist/assets/{chunk-FMBD7UC4-DoOEFFQC.js → chunk-FMBD7UC4-ptKQX5uF.js} +1 -1
  21. package/dist/assets/{chunk-JSJVCQXG-BerphV2K.js → chunk-JSJVCQXG-DO0UU_OX.js} +1 -1
  22. package/dist/assets/{chunk-KX2RTZJC-CxUAqT05.js → chunk-KX2RTZJC-DRj2OZnD.js} +1 -1
  23. package/dist/assets/{chunk-NQ4KR5QH-fCqZgFkU.js → chunk-NQ4KR5QH-C4Nsf7ww.js} +1 -1
  24. package/dist/assets/{chunk-QZHKN3VN-HlpHnJEy.js → chunk-QZHKN3VN-B1GO0Nwy.js} +1 -1
  25. package/dist/assets/{chunk-WL4C6EOR-D9yxAHyd.js → chunk-WL4C6EOR-lFR6fjR8.js} +1 -1
  26. package/dist/assets/classDiagram-VBA2DB6C-BHDWMOEz.js +1 -0
  27. package/dist/assets/classDiagram-v2-RAHNMMFH-BHDWMOEz.js +1 -0
  28. package/dist/assets/clone-BdN-3iAD.js +1 -0
  29. package/dist/assets/{cose-bilkent-S5V4N54A-F5xOBvqW.js → cose-bilkent-S5V4N54A-IpR9mVIO.js} +1 -1
  30. package/dist/assets/{dagre-KLK3FWXG-CD3BTpHv.js → dagre-KLK3FWXG-B4YA6T7N.js} +1 -1
  31. package/dist/assets/{diagram-E7M64L7V-C3D9MCay.js → diagram-E7M64L7V-Do5l6es_.js} +1 -1
  32. package/dist/assets/{diagram-IFDJBPK2-zJBVM-GK.js → diagram-IFDJBPK2-D5MxfKVv.js} +1 -1
  33. package/dist/assets/{diagram-P4PSJMXO-BrmFZOLB.js → diagram-P4PSJMXO-Djr28EgW.js} +1 -1
  34. package/dist/assets/{erDiagram-INFDFZHY-aSMhKiV2.js → erDiagram-INFDFZHY-BuM-rbCL.js} +1 -1
  35. package/dist/assets/{flowDiagram-PKNHOUZH-DwgX7l8F.js → flowDiagram-PKNHOUZH-By3WGI7Q.js} +1 -1
  36. package/dist/assets/{ganttDiagram-A5KZAMGK-C57Hz6QW.js → ganttDiagram-A5KZAMGK-GLmBfK72.js} +1 -1
  37. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-CuchqqGh.js → gitGraphDiagram-K3NZZRJ6-BN0iXeIv.js} +1 -1
  38. package/dist/assets/{graph-CPFGBV5J.js → graph-CDzuMtjV.js} +1 -1
  39. package/dist/assets/{index-DMt1cpG6.js → index-DoeSN_Oe.js} +130 -130
  40. package/dist/assets/{infoDiagram-LFFYTUFH-Dd_5tfX7.js → infoDiagram-LFFYTUFH-C888gaFw.js} +1 -1
  41. package/dist/assets/{ishikawaDiagram-PHBUUO56-DwosSEvT.js → ishikawaDiagram-PHBUUO56-ChIO9DG-.js} +1 -1
  42. package/dist/assets/{journeyDiagram-4ABVD52K-BuCxcsX0.js → journeyDiagram-4ABVD52K-CufMUDcs.js} +1 -1
  43. package/dist/assets/{kanban-definition-K7BYSVSG-DF_1UCkW.js → kanban-definition-K7BYSVSG-BpsSVpX8.js} +1 -1
  44. package/dist/assets/{layout-DIcS6m1g.js → layout-B8RWVBSF.js} +1 -1
  45. package/dist/assets/{linear-BXkwBhoJ.js → linear-BJwxtq9r.js} +1 -1
  46. package/dist/assets/{mindmap-definition-YRQLILUH-DcDvYagd.js → mindmap-definition-YRQLILUH-C6WPimbf.js} +1 -1
  47. package/dist/assets/{pieDiagram-SKSYHLDU-BmeDeWDM.js → pieDiagram-SKSYHLDU-DeCGMWf8.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-337W2JSQ-3zfjULUM.js → quadrantDiagram-337W2JSQ-D9TWaS83.js} +1 -1
  49. package/dist/assets/{requirementDiagram-Z7DCOOCP-B2wQMJpq.js → requirementDiagram-Z7DCOOCP-DTnuXlAq.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-WA2Y5GQK-__kKlCTq.js → sankeyDiagram-WA2Y5GQK-B2dplCgD.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-2WXFIKYE-B7O81Vih.js → sequenceDiagram-2WXFIKYE-cBvgSSju.js} +1 -1
  52. package/dist/assets/{stateDiagram-RAJIS63D-CcJaDrAK.js → stateDiagram-RAJIS63D-Cwr7VtSX.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-FVOUBMTO-B59h7VTZ.js +1 -0
  54. package/dist/assets/{timeline-definition-YZTLITO2-DSaQQqIU.js → timeline-definition-YZTLITO2-Dkp163fK.js} +1 -1
  55. package/dist/assets/{treemap-KZPCXAKY-9Hcrd8XD.js → treemap-KZPCXAKY-BUWHa5xU.js} +1 -1
  56. package/dist/assets/{vennDiagram-LZ73GAT5-BqHNyca2.js → vennDiagram-LZ73GAT5-BihD66ma.js} +1 -1
  57. package/dist/assets/{xychartDiagram-JWTSCODW-BqeYf6Fk.js → xychartDiagram-JWTSCODW-Cw4lPbuZ.js} +1 -1
  58. package/dist/index.html +1 -1
  59. package/export-docx.js +12 -4
  60. package/init.js +19 -14
  61. package/manifest-cli.mjs +32 -3
  62. package/package.json +3 -2
  63. package/serve.js +44 -19
  64. package/utils/export-markdown.js +486 -0
  65. package/dist/assets/channel-SoktpVBQ.js +0 -1
  66. package/dist/assets/classDiagram-VBA2DB6C-BT2AdZTe.js +0 -1
  67. package/dist/assets/classDiagram-v2-RAHNMMFH-BT2AdZTe.js +0 -1
  68. package/dist/assets/clone-DOjIfi5r.js +0 -1
  69. package/dist/assets/stateDiagram-v2-FVOUBMTO-B2goOPt-.js +0 -1
@@ -0,0 +1,736 @@
1
+ ---
2
+ title: "refactor: Address 28 EaROS CLI code review findings"
3
+ type: refactor
4
+ status: completed
5
+ date: 2026-03-23
6
+ ---
7
+
8
+ # refactor: Address 28 EaROS CLI Code Review Findings
9
+
10
+ ## Overview
11
+
12
+ A comprehensive 6-agent code review of the EaROS CLI (`@trohde/earos` v1.2.0 at `tools/editor/`) identified 28 findings across security, performance, architecture, type safety, agent parity, and simplification. This plan organizes them into 5 phases ordered by risk and dependency, with each phase independently shippable as a version bump.
13
+
14
+ **Review agents used:** Security Sentinel, Performance Oracle, Architecture Strategist, Kieran TypeScript Reviewer, Agent-Native Reviewer, Code Simplicity Reviewer.
15
+
16
+ ## Problem Statement
17
+
18
+ The CLI works correctly for its current user base but has:
19
+ - **Security gaps** (path traversal bypass, network-exposed API, no security headers) that could allow file read/write from adjacent machines or directories
20
+ - **Dead code** (`cli.ts`, `manifest-cli.ts`) creating contributor confusion
21
+ - **Agent parity gaps** — the CLI cannot export rubrics/evaluations to DOCX, has no `--json` output, and lacks evaluation listing without the server
22
+ - **Performance inefficiencies** — sequential icon downloads, uncached filesystem scans
23
+ - **Type safety debt** — `strict: false`, 49+ `any` usages, silent validation bypass
24
+
25
+ ## Proposed Solution
26
+
27
+ Five phases, each independently committable and publishable:
28
+
29
+ 1. **Security Hardening** — fix path traversal, bind to localhost, add headers (patch bump)
30
+ 2. **Dead Code & Simplification** — delete dead files, extract shared utils (patch bump)
31
+ 3. **CLI Agent Parity** — extend export, add `--json` flags, new list commands (minor bump)
32
+ 4. **Performance** — parallelize downloads, lazy-load schema (patch bump)
33
+ 5. **Code Quality** — enable strict mode, de-duplicate, move mermaid dep (patch bump)
34
+
35
+ ---
36
+
37
+ ## Technical Approach
38
+
39
+ ### Phase 1: Security Hardening
40
+
41
+ **Estimated effort:** Small (1-2 hours). All changes are in `bin.js` and `serve.ts`/`serve.js`.
42
+
43
+ #### 1.1 Fix `safeRepoPath()` path traversal bypass
44
+
45
+ **File:** `tools/editor/src/serve.ts:28-33`, compiled to `tools/editor/serve.js:24-30`
46
+
47
+ The current `abs.startsWith(repoRoot)` check allows access to sibling directories whose names share the same prefix (e.g., `EAROS-private/` when repo root is `EAROS`).
48
+
49
+ ```typescript
50
+ // serve.ts — BEFORE
51
+ function safeRepoPath(repoRoot: string, rawPath: string): string | null {
52
+ const decoded = decodeURIComponent(rawPath)
53
+ const abs = resolve(repoRoot, decoded)
54
+ if (!abs.startsWith(repoRoot)) return null
55
+ return abs
56
+ }
57
+
58
+ // serve.ts — AFTER
59
+ import { sep } from 'path'
60
+
61
+ function safeRepoPath(repoRoot: string, rawPath: string): string | null {
62
+ const decoded = decodeURIComponent(rawPath)
63
+ const abs = resolve(repoRoot, decoded)
64
+ // Append separator to prevent prefix collisions (e.g., EAROS vs EAROS-private)
65
+ if (abs !== repoRoot && !abs.startsWith(repoRoot + sep)) return null
66
+ return abs
67
+ }
68
+ ```
69
+
70
+ Additionally, restrict `POST /api/file/*` writes to YAML files only:
71
+
72
+ ```typescript
73
+ // serve.ts — POST handler, add after safeRepoPath check
74
+ if (!absPath.endsWith('.yaml') && !absPath.endsWith('.yml')) {
75
+ res.status(400).json({ error: 'Only YAML files can be written' })
76
+ return
77
+ }
78
+ ```
79
+
80
+ - [ ] Fix `safeRepoPath()` with trailing separator — `serve.ts:28-33`
81
+ - [ ] Restrict POST writes to `.yaml`/`.yml` extensions — `serve.ts:140`
82
+ - [ ] Apply the same fixes to compiled `serve.js`
83
+
84
+ #### 1.2 Bind Express to localhost
85
+
86
+ **File:** `tools/editor/src/serve.ts:239`, compiled to `tools/editor/serve.js:231`
87
+
88
+ ```typescript
89
+ // BEFORE
90
+ app.listen(port, () => { ... })
91
+
92
+ // AFTER — default to localhost, allow override for Docker/WSL2
93
+ const host = process.env.EAROS_HOST ?? '127.0.0.1'
94
+ app.listen(port, host, () => { ... })
95
+ ```
96
+
97
+ - [ ] Bind to `127.0.0.1` with `EAROS_HOST` env var override — `serve.ts:239`
98
+ - [ ] Update help text in `bin.js` to document `EAROS_HOST` — `bin.js:81-92`
99
+
100
+ #### 1.3 Fix Mermaid image path traversal
101
+
102
+ **File:** `tools/editor/src/export-docx.ts` (search for `resolveLocalMermaidImage`), compiled to `export-docx.js:48-56`
103
+
104
+ ```typescript
105
+ // AFTER — add containment check
106
+ function resolveLocalMermaidImage(assetPath: string): string | null {
107
+ const relativePath = assetPath.replace(/^\/+/, '')
108
+ for (const baseDir of LOCAL_MERMAID_IMAGE_DIRS) {
109
+ const candidate = resolve(baseDir, relativePath)
110
+ if (!candidate.startsWith(baseDir + sep)) continue // containment check
111
+ if (existsSync(candidate)) return candidate
112
+ }
113
+ return null
114
+ }
115
+ ```
116
+
117
+ - [ ] Add containment check to `resolveLocalMermaidImage` — `export-docx.ts`
118
+
119
+ #### 1.4 Add security headers
120
+
121
+ **File:** `tools/editor/src/serve.ts`, after `app.use(express.json(...))`
122
+
123
+ ```typescript
124
+ // Security headers (no dependency needed — manual is fine for a local tool)
125
+ app.use((_req, res, next) => {
126
+ res.setHeader('X-Content-Type-Options', 'nosniff')
127
+ res.setHeader('X-Frame-Options', 'DENY')
128
+ next()
129
+ })
130
+ ```
131
+
132
+ - [ ] Add security headers middleware — `serve.ts`, after line 63
133
+
134
+ #### 1.5 Sanitize error messages
135
+
136
+ Replace `String(e)` in API error responses with generic messages:
137
+
138
+ ```typescript
139
+ // BEFORE
140
+ res.status(500).json({ error: String(e) })
141
+
142
+ // AFTER
143
+ console.error('[API error]', e)
144
+ res.status(500).json({ error: 'Internal server error' })
145
+ ```
146
+
147
+ Apply to all 5 error handlers in `serve.ts` (lines 134, 149, 179, 197, 215). Keep the 400-level errors as-is (they return controlled messages, not exception details).
148
+
149
+ - [ ] Sanitize 500-level error responses in all API handlers — `serve.ts`
150
+
151
+ #### 1.6 Scope JSON body limit
152
+
153
+ The 25MB limit is only needed by the DOCX export routes. `POST /api/file/*` could receive large artifacts, so use 5MB there.
154
+
155
+ ```typescript
156
+ // Global default
157
+ app.use(express.json({ limit: '1mb' }))
158
+
159
+ // Per-route overrides
160
+ const largeBody = express.json({ limit: '25mb' })
161
+ app.post('/api/export/docx', largeBody, async (req, res) => { ... })
162
+ app.post('/api/export/docx/rubric', largeBody, async (req, res) => { ... })
163
+ app.post('/api/export/docx/evaluation', largeBody, async (req, res) => { ... })
164
+
165
+ const mediumBody = express.json({ limit: '5mb' })
166
+ app.post('/api/file/*', mediumBody, (req, res) => { ... })
167
+ ```
168
+
169
+ - [ ] Set global body limit to 1MB — `serve.ts:63`
170
+ - [ ] Set 25MB limit on export routes — `serve.ts:154,183,201`
171
+ - [ ] Set 5MB limit on file write route — `serve.ts:140`
172
+
173
+ #### 1.7 Rebuild server files
174
+
175
+ After all Phase 1 changes to `.ts` source files:
176
+
177
+ - [ ] Run `tsc -p tsconfig.server.json` to recompile `serve.js` and `export-docx.js`
178
+
179
+ ---
180
+
181
+ ### Phase 2: Dead Code & Simplification
182
+
183
+ **Estimated effort:** Small (30 min). Low-risk deletions and extractions.
184
+
185
+ #### 2.1 Delete `src/cli.ts`
186
+
187
+ This 78-line file is dead code. It is not in `tsconfig.server.json`'s `files` array, not imported by anything, and has diverged from `bin.js` (missing `init`, `export`, `manifest`, `dev` commands). The header comment says "The compiled output is NOT used."
188
+
189
+ - [ ] Delete `tools/editor/src/cli.ts`
190
+ - [ ] Verify it is not referenced in any tsconfig `include` or `files` array
191
+
192
+ #### 2.2 Delete `manifest-cli.ts`
193
+
194
+ This file is not compiled (not in `tsconfig.server.json`). The actual runtime file is `manifest-cli.mjs` (hand-maintained). The `.ts` file has diverged — it hardcodes `REPO_ROOT` as `resolve(__dir, '../..')` while the `.mjs` reads from `process.env.EAROS_REPO_ROOT`.
195
+
196
+ - [ ] Delete `tools/editor/manifest-cli.ts`
197
+ - [ ] Remove the stale header comment in `manifest-cli.mjs` that says "source is manifest-cli.ts"
198
+
199
+ #### 2.3 Import manifest-cli directly (optional, defer if risky)
200
+
201
+ Currently `bin.js` spawns `manifest-cli.mjs` as a child process via `spawnSync`. This works but adds process overhead and requires env-var IPC. The simplest improvement is to export a function from `manifest-cli.mjs` and import it:
202
+
203
+ **Decision:** Defer to a later iteration. The current `spawnSync` approach works, and refactoring `manifest-cli.mjs` to remove its 7 `process.exit()` calls is a medium-effort change with marginal benefit. Keep as a future cleanup.
204
+
205
+ - [ ] Update `manifest-cli.mjs` header comment to remove false "compiled" claim
206
+
207
+ #### 2.4 De-duplicate `findRepoRoot()`
208
+
209
+ This function is duplicated in `bin.js:14-20` and `serve.ts:19-26`. Since `bin.js` is hand-written JS and `serve.ts` is compiled, the cleanest approach is to keep both but ensure they stay identical. Document this in a comment.
210
+
211
+ **Decision:** Low priority given Phase 2.3 is deferred. Leave as-is with a comment.
212
+
213
+ ---
214
+
215
+ ### Phase 3: CLI Agent Parity
216
+
217
+ **Estimated effort:** Medium (2-3 hours). Extends `bin.js` with new commands and flags.
218
+
219
+ #### 3.1 Extend `earos export` to all YAML kinds
220
+
221
+ **File:** `tools/editor/bin.js:94-130`
222
+
223
+ Currently rejects non-artifact files. Detect `kind` field and route to the appropriate export function:
224
+
225
+ ```javascript
226
+ // bin.js — replace the artifact-only check
227
+ const kind = artifactData?.kind
228
+ if (kind === 'artifact' || !kind) {
229
+ const { exportToDocx } = await import('./export-docx.js')
230
+ const buf = await exportToDocx(artifactData)
231
+ // ... write file
232
+ } else if (kind === 'evaluation') {
233
+ const { exportEvaluationToDocx } = await import('./export-docx.js')
234
+ const buf = await exportEvaluationToDocx(artifactData)
235
+ const outputPath = inputPath.replace(/\.(yaml|yml)$/i, '') + '-assessment.docx'
236
+ // ... write file
237
+ } else if (kind === 'core_rubric' || kind === 'profile' || kind === 'overlay') {
238
+ const { exportRubricToDocx } = await import('./export-docx.js')
239
+ const buf = await exportRubricToDocx(artifactData)
240
+ // ... write file
241
+ } else {
242
+ console.error(`Unsupported kind for export: ${kind}`)
243
+ process.exit(1)
244
+ }
245
+ ```
246
+
247
+ - [ ] Extend `earos export` to detect `kind` and route to appropriate exporter — `bin.js:94-130`
248
+ - [ ] Update help text to reflect all supported kinds — `bin.js:87`
249
+
250
+ #### 3.2 Add `--json` flag to `earos validate`
251
+
252
+ **Convention:** `--json` mode outputs only JSON to stdout; all status/error messages go to stderr.
253
+
254
+ ```javascript
255
+ // bin.js — in validateFile()
256
+ const jsonMode = args.includes('--json')
257
+
258
+ if (valid) {
259
+ if (jsonMode) {
260
+ process.stdout.write(JSON.stringify({ valid: true, kind: kind ?? 'unknown', file: filePath }) + '\n')
261
+ } else {
262
+ console.log(`✓ ${filePath} is valid (kind: ${kind ?? 'unknown'})`)
263
+ }
264
+ } else {
265
+ const result = {
266
+ valid: false,
267
+ file: filePath,
268
+ kind: kind ?? 'unknown',
269
+ errors: validate.errors.map(err => ({
270
+ path: err.instancePath || '(root)',
271
+ message: err.message
272
+ }))
273
+ }
274
+ if (jsonMode) {
275
+ process.stdout.write(JSON.stringify(result) + '\n')
276
+ } else {
277
+ console.error(`✗ ${filePath} — ${validate.errors.length} error(s):`)
278
+ for (const err of validate.errors) {
279
+ console.error(` ${err.instancePath || '(root)'} ${err.message}`)
280
+ }
281
+ }
282
+ process.exit(1)
283
+ }
284
+ ```
285
+
286
+ - [ ] Add `--json` flag to `earos validate` — `bin.js:31-76`
287
+ - [ ] Update help text — `bin.js:87`
288
+
289
+ #### 3.3 Add `--json` flag to `earos manifest check`
290
+
291
+ ```javascript
292
+ // manifest-cli.mjs — in the check block
293
+ const jsonMode = subArgs.includes('--json')
294
+
295
+ // At the end of check:
296
+ if (jsonMode) {
297
+ process.stdout.write(JSON.stringify({ consistent: errors.length === 0, errors, warnings }) + '\n')
298
+ process.exit(errors.length > 0 ? 1 : 0)
299
+ }
300
+ ```
301
+
302
+ - [ ] Add `--json` flag to `earos manifest check` — `manifest-cli.mjs:151-202`
303
+
304
+ #### 3.4 Add `earos manifest list [--json]`
305
+
306
+ New subcommand that outputs the manifest contents.
307
+
308
+ ```javascript
309
+ // manifest-cli.mjs — add before the "Unknown subcommand" block
310
+ if (subCmd === 'list') {
311
+ const manifest = loadManifest()
312
+ if (!manifest) {
313
+ console.error('No earos.manifest.yaml found. Run `earos manifest` first.')
314
+ process.exit(1)
315
+ }
316
+ if (subArgs.includes('--json')) {
317
+ process.stdout.write(JSON.stringify(manifest, null, 2) + '\n')
318
+ } else {
319
+ const sections = ['core', 'profiles', 'overlays']
320
+ for (const section of sections) {
321
+ const entries = manifest[section] ?? []
322
+ if (entries.length === 0) continue
323
+ console.log(`\n${section}:`)
324
+ for (const e of entries) {
325
+ console.log(` ${e.path} ${e.rubric_id ?? ''} ${e.title ?? ''}`)
326
+ }
327
+ }
328
+ }
329
+ process.exit(0)
330
+ }
331
+ ```
332
+
333
+ - [ ] Add `earos manifest list [--json]` — `manifest-cli.mjs`
334
+ - [ ] Update help text in `bin.js:91`
335
+
336
+ #### 3.5 Add `earos list evaluations [--json]`
337
+
338
+ Extract `findEvaluationFiles` from `serve.ts`'s `startServer()` closure into a standalone function, then reuse in `bin.js`.
339
+
340
+ The simplest approach: duplicate the 15-line scan function in `bin.js` (it's pure `fs` operations). This avoids coupling `bin.js` to `serve.js`.
341
+
342
+ ```javascript
343
+ // bin.js — new command block
344
+ } else if (args[0] === 'list' && args[1] === 'evaluations') {
345
+ const REPO_ROOT = findRepoRoot()
346
+ const jsonMode = args.includes('--json')
347
+ // ... inline findEvaluationFiles + summary logic
348
+ }
349
+ ```
350
+
351
+ - [ ] Add `earos list evaluations [--json]` command — `bin.js`
352
+ - [ ] Include summary metadata (status, score, date, title) in output
353
+ - [ ] Update help text — `bin.js:81-92`
354
+
355
+ #### 3.6 Add Markdown export (`earos export <file> --format md`)
356
+
357
+ The `exportArtifactToMarkdown`, `exportRubricToMarkdown` functions in `src/utils/export-markdown.ts` are pure functions (no DOM dependency) except for `downloadAsFile()` which uses `document.createElement`. To make them available in Node.js:
358
+
359
+ 1. Add `src/utils/export-markdown.ts` to `tsconfig.server.json`'s `files` array
360
+ 2. Guard `downloadAsFile` with `typeof document !== 'undefined'`
361
+ 3. Import in `bin.js` when `--format md` is passed
362
+
363
+ Note: `exportEvaluationToMarkdown` has a different signature (takes 4 args instead of 1). The CLI would need to handle that or skip it initially.
364
+
365
+ **Decision:** Implement for artifact and rubric only in this phase. Evaluation Markdown export requires assembling dimension/result data that the CLI does not currently load.
366
+
367
+ - [ ] Guard `downloadAsFile` with environment check — `export-markdown.ts:39`
368
+ - [ ] Add `export-markdown.ts` to `tsconfig.server.json` `files` array
369
+ - [ ] Add `--format` flag to `earos export` (`docx` default, `md` alternative) — `bin.js`
370
+
371
+ ---
372
+
373
+ ### Phase 4: Performance
374
+
375
+ **Estimated effort:** Small (30-60 min). Isolated changes with clear impact.
376
+
377
+ #### 4.1 Parallelize icon downloads
378
+
379
+ **File:** `tools/editor/src/init.ts:396-406`
380
+
381
+ ```typescript
382
+ // BEFORE
383
+ for (const config of ICON_PACKAGES) {
384
+ const result = await downloadIconPackage(target, config)
385
+ results.push(result)
386
+ }
387
+
388
+ // AFTER
389
+ const settled = await Promise.allSettled(
390
+ ICON_PACKAGES.map(config => downloadIconPackage(target, config))
391
+ )
392
+ for (const outcome of settled) {
393
+ if (outcome.status === 'fulfilled') {
394
+ results.push(outcome.value)
395
+ if (outcome.value.missingAliases.length) {
396
+ console.warn(` Missing ${outcome.value.name} icon aliases: ${outcome.value.missingAliases.join(', ')}`)
397
+ }
398
+ } else {
399
+ console.error(` Failed to download icons: ${outcome.reason?.message ?? outcome.reason}`)
400
+ }
401
+ }
402
+ ```
403
+
404
+ **Note:** Parallel downloads may interleave console output. This is acceptable.
405
+
406
+ - [ ] Replace sequential icon download loop with `Promise.allSettled` — `init.ts:396-406`
407
+
408
+ #### 4.2 Replace sort-for-max with linear scan
409
+
410
+ **File:** `tools/editor/src/init.ts:312-316`
411
+
412
+ ```typescript
413
+ // BEFORE
414
+ const bestCandidate = extractedEntries
415
+ .map(entry => ({ entry, score: scoreAliasCandidate(entry, spec, config) }))
416
+ .filter(c => Number.isFinite(c.score))
417
+ .sort((a, b) => b.score - a.score)[0]
418
+
419
+ // AFTER
420
+ let bestScore = Number.NEGATIVE_INFINITY
421
+ let bestEntry: ExtractedIconEntry | null = null
422
+ for (const entry of extractedEntries) {
423
+ const score = scoreAliasCandidate(entry, spec, config)
424
+ if (score > bestScore) {
425
+ bestScore = score
426
+ bestEntry = entry
427
+ }
428
+ }
429
+ ```
430
+
431
+ - [ ] Replace `sort()[0]` with linear max-scan in `createIconAliases` — `init.ts:312-316`
432
+
433
+ #### 4.3 Lazy-load artifact schema
434
+
435
+ **File:** `tools/editor/src/export-docx.ts` (search for `ARTIFACT_SCHEMA`)
436
+
437
+ ```typescript
438
+ // BEFORE
439
+ const ARTIFACT_SCHEMA = loadArtifactSchema()
440
+
441
+ // AFTER
442
+ let _artifactSchema: Record<string, any> | null | undefined
443
+ function getArtifactSchema() {
444
+ if (_artifactSchema === undefined) {
445
+ _artifactSchema = loadArtifactSchema()
446
+ }
447
+ return _artifactSchema
448
+ }
449
+ ```
450
+
451
+ Replace all references to `ARTIFACT_SCHEMA` with `getArtifactSchema()`.
452
+
453
+ - [ ] Lazy-load `ARTIFACT_SCHEMA` on first use — `export-docx.ts`
454
+
455
+ #### 4.4 Rebuild server files
456
+
457
+ - [ ] Run `tsc -p tsconfig.server.json` to recompile `init.js` and `export-docx.js`
458
+
459
+ ---
460
+
461
+ ### Phase 5: Code Quality & Architecture
462
+
463
+ **Estimated effort:** Medium-Large (3-5 hours). The `strict: true` migration is the largest item.
464
+
465
+ #### 5.1 Enable strict mode incrementally
466
+
467
+ **File:** `tools/editor/tsconfig.server.json`
468
+
469
+ **Step 1:** Run `tsc -p tsconfig.server.json --strict --noEmit 2>&1 | wc -l` to assess error count.
470
+
471
+ **Step 2:** Enable `strictNullChecks` and `noImplicitAny` first (the highest-value flags):
472
+
473
+ ```json
474
+ {
475
+ "compilerOptions": {
476
+ "strict": false,
477
+ "strictNullChecks": true,
478
+ "noImplicitAny": true
479
+ }
480
+ }
481
+ ```
482
+
483
+ **Step 3:** Fix resulting errors in `serve.ts`, `init.ts`, and `export-docx.ts`. For `export-docx.ts`, the 42+ `any` usages can be addressed by:
484
+ - Typing section renderer parameters with interfaces derived from `artifact.schema.json`
485
+ - Replacing `as any` casts with narrowing checks
486
+ - Adding `unknown` instead of `any` for truly dynamic data
487
+
488
+ **Step 4:** Once those two flags pass, enable full `strict: true`.
489
+
490
+ - [ ] Assess strict mode error count — `tsconfig.server.json`
491
+ - [ ] Enable `strictNullChecks` + `noImplicitAny` and fix errors
492
+ - [ ] Enable full `strict: true` and fix remaining errors
493
+
494
+ #### 5.2 De-duplicate `getGateSeverity`
495
+
496
+ **File:** `tools/editor/src/components/AssessmentSummary.tsx:40`
497
+
498
+ The function is duplicated in `AssessmentSummary.tsx` and `score-helpers.tsx`. Gate logic must have a single source of truth.
499
+
500
+ ```typescript
501
+ // AssessmentSummary.tsx — BEFORE
502
+ function getGateSeverity(gate: any): string { ... }
503
+
504
+ // AssessmentSummary.tsx — AFTER
505
+ import { getGateSeverity } from '../utils/score-helpers'
506
+ ```
507
+
508
+ - [ ] Remove duplicate `getGateSeverity` from `AssessmentSummary.tsx`, import from `score-helpers.tsx`
509
+
510
+ #### 5.3 Move `mermaid` to devDependencies
511
+
512
+ **File:** `tools/editor/package.json:61`
513
+
514
+ **Prerequisite:** Verify the Vite build fully bundles mermaid into `dist/`. Steps:
515
+ 1. Move `mermaid` from `dependencies` to `devDependencies`
516
+ 2. Run `npm run build`
517
+ 3. Run `npm pack` to create tarball
518
+ 4. Install tarball in a temp directory: `npm install /path/to/trohde-earos-*.tgz`
519
+ 5. Run `npx earos --help` — if it works, mermaid is fully bundled
520
+ 6. Start the server and load the editor — verify Mermaid diagrams render
521
+
522
+ - [ ] Move `mermaid` to `devDependencies` — `package.json`
523
+ - [ ] Verify Vite bundle is self-contained after the move
524
+
525
+ #### 5.4 Extract shared helpers
526
+
527
+ Create `tools/editor/src/utils/format-helpers.ts`:
528
+
529
+ ```typescript
530
+ export function str(v: unknown): string {
531
+ if (v == null) return ''
532
+ if (typeof v === 'string') return v.trim()
533
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v)
534
+ return ''
535
+ }
536
+
537
+ export function humanize(key: string): string {
538
+ return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
539
+ }
540
+
541
+ export function isPlainObject(v: unknown): v is Record<string, unknown> {
542
+ return !!v && typeof v === 'object' && !Array.isArray(v)
543
+ }
544
+
545
+ export const SECTION_ORDER = [ /* ... shared constant ... */ ]
546
+ ```
547
+
548
+ Then update imports in `export-docx.ts` (rename `prettyLabel` to `humanize`) and `export-markdown.ts`.
549
+
550
+ Also extract `extractDiagrams` and `DIAGRAM_FIELDS` into `utils/diagrams.ts` for sharing between `export-docx.ts` and `mermaid.ts`.
551
+
552
+ - [ ] Create `src/utils/format-helpers.ts` with shared `str`, `humanize`, `isPlainObject`, `SECTION_ORDER`
553
+ - [ ] Create `src/utils/diagrams.ts` with shared `extractDiagrams` and `DIAGRAM_FIELDS`
554
+ - [ ] Update `export-docx.ts` to import from shared modules (rename `prettyLabel` to `humanize`)
555
+ - [ ] Update `export-markdown.ts` to import from shared modules
556
+ - [ ] Update `mermaid.ts` to import `DIAGRAM_FIELDS` from shared module
557
+
558
+ #### 5.5 Extract document shell boilerplate
559
+
560
+ The `Document` construction with Header, Footer, margins, packing is copy-pasted across 3 export functions. Extract a `buildDocShell` factory.
561
+
562
+ ```typescript
563
+ // export-docx.ts — new shared function
564
+ function buildDocShell(opts: {
565
+ title: string
566
+ creator: string
567
+ headerLabel: string
568
+ children: Paragraph[]
569
+ }): Buffer {
570
+ const doc = new Document({
571
+ creator: opts.creator,
572
+ title: opts.title,
573
+ // ... shared margins, header, footer, ToC pattern
574
+ sections: [{ children: opts.children }]
575
+ })
576
+ return Packer.toBuffer(doc)
577
+ }
578
+ ```
579
+
580
+ - [ ] Extract `buildDocShell` from the 3 export functions — `export-docx.ts`
581
+
582
+ #### 5.6 Fix silent validation bypass
583
+
584
+ **File:** `tools/editor/src/utils/validate.ts:27-28`
585
+
586
+ ```typescript
587
+ // BEFORE
588
+ try {
589
+ cache.set(key, ajv.compile(rest))
590
+ } catch {
591
+ return null // Caller treats null as "valid"!
592
+ }
593
+
594
+ // AFTER
595
+ try {
596
+ cache.set(key, ajv.compile(rest))
597
+ } catch (e) {
598
+ console.warn('[validate] Schema compilation failed:', e)
599
+ // Return a validator that always fails with a clear message
600
+ const failValidator = (() => false) as any
601
+ failValidator.errors = [{ instancePath: '', message: 'Schema compilation failed' }]
602
+ cache.set(key, failValidator)
603
+ return failValidator
604
+ }
605
+ ```
606
+
607
+ - [ ] Fix schema compilation failure to report invalid instead of valid — `validate.ts:27`
608
+
609
+ #### 5.7 Add Express server caching for evaluation endpoints
610
+
611
+ **File:** `tools/editor/src/serve.ts:87-123`
612
+
613
+ Add a simple TTL cache for evaluation file scans:
614
+
615
+ ```typescript
616
+ let evalCache: { files: Array<{ path: string; name: string }>; ts: number } | null = null
617
+ const EVAL_CACHE_TTL = 5000 // 5 seconds
618
+
619
+ function getCachedEvaluationFiles(repoRoot: string): Array<{ path: string; name: string }> {
620
+ if (evalCache && Date.now() - evalCache.ts < EVAL_CACHE_TTL) return evalCache.files
621
+ const files: Array<{ path: string; name: string }> = []
622
+ for (const dir of ['examples', 'evaluations']) {
623
+ files.push(...findEvaluationFiles(resolve(repoRoot, dir), `${dir}/`))
624
+ }
625
+ evalCache = { files, ts: Date.now() }
626
+ return files
627
+ }
628
+ ```
629
+
630
+ - [ ] Add TTL cache for evaluation file scans — `serve.ts`
631
+ - [ ] Invalidate cache on POST /api/file/* writes to evaluation paths
632
+
633
+ #### 5.8 Rebuild all server files
634
+
635
+ - [ ] Run `tsc -p tsconfig.server.json` to recompile all server files
636
+ - [ ] Run `npm run build` for full rebuild
637
+
638
+ ---
639
+
640
+ ## System-Wide Impact
641
+
642
+ - **Build pipeline:** Phases 3 and 5 modify `tsconfig.server.json` (adding `export-markdown.ts` to files, enabling strict flags). The 4-stage build (`tsc server → tsc check → vite build → build:assets`) is unchanged.
643
+ - **Published package:** The `files` array in `package.json` does not change. Compiled `.js` files are updated in place. No new files ship to npm (shared utils are compiled into their consumers by `tsc`).
644
+ - **Breaking changes:** Phase 1 changes the default bind address from `0.0.0.0` to `127.0.0.1`. Users running the server in Docker/WSL2 must set `EAROS_HOST=0.0.0.0`. Document in the changelog.
645
+ - **Backward compatibility:** All existing CLI commands continue to work unchanged. New flags (`--json`, `--format`) are additive. The `earos export` extension is backward-compatible (existing artifact exports work the same).
646
+
647
+ ## Acceptance Criteria
648
+
649
+ ### Phase 1 — Security
650
+ - [ ] `safeRepoPath` rejects paths to sibling directories (e.g., `../EAROS-evil/`)
651
+ - [ ] Server only listens on `127.0.0.1` by default
652
+ - [ ] `resolveLocalMermaidImage` rejects `..` traversal beyond icon directories
653
+ - [ ] API 500 responses do not leak filesystem paths
654
+ - [ ] POST `/api/file/*` rejects non-YAML file extensions
655
+ - [ ] JSON body >1MB rejected on non-export routes
656
+
657
+ ### Phase 2 — Dead Code
658
+ - [ ] `src/cli.ts` deleted
659
+ - [ ] `manifest-cli.ts` deleted
660
+ - [ ] `manifest-cli.mjs` header comment updated
661
+
662
+ ### Phase 3 — Agent Parity
663
+ - [ ] `earos export evaluation.yaml` produces a DOCX file
664
+ - [ ] `earos export rubric.yaml` produces a DOCX file
665
+ - [ ] `earos validate --json` outputs valid JSON to stdout
666
+ - [ ] `earos manifest check --json` outputs valid JSON to stdout
667
+ - [ ] `earos manifest list` outputs manifest contents
668
+ - [ ] `earos list evaluations --json` discovers and lists evaluation files
669
+ - [ ] `earos export artifact.yaml --format md` produces a Markdown file
670
+ - [ ] Help text (`earos --help`) documents all new commands and flags
671
+
672
+ ### Phase 4 — Performance
673
+ - [ ] `earos init --icons` downloads all 3 icon packages in parallel
674
+ - [ ] Alias matching uses linear scan instead of sort
675
+ - [ ] `ARTIFACT_SCHEMA` is not loaded until first export
676
+
677
+ ### Phase 5 — Code Quality
678
+ - [ ] `tsconfig.server.json` has `strict: true`
679
+ - [ ] `getGateSeverity` exists in one place only (`score-helpers.tsx`)
680
+ - [ ] `mermaid` is in `devDependencies`, editor still renders diagrams
681
+ - [ ] Shared helpers extracted to `format-helpers.ts` and `diagrams.ts`
682
+ - [ ] Document shell boilerplate exists once as `buildDocShell`
683
+ - [ ] Schema compilation failure reports validation error (not silent pass)
684
+ - [ ] Evaluation endpoints use cached file scans
685
+
686
+ ## Dependencies & Risks
687
+
688
+ | Risk | Mitigation |
689
+ |------|------------|
690
+ | `strict: true` surfaces hundreds of errors in `export-docx.ts` | Incremental approach: `strictNullChecks` + `noImplicitAny` first |
691
+ | Moving `mermaid` to devDeps breaks runtime | Verify with `npm pack` + clean install before publishing |
692
+ | Localhost binding breaks Docker users | `EAROS_HOST` env var override documented in help text |
693
+ | Body limit reduction breaks large artifact saves | 5MB limit for `/api/file/*` (well above typical artifact size) |
694
+ | `export-markdown.ts` has browser-only `downloadAsFile` | Guard with `typeof document` check before adding to server build |
695
+
696
+ ## Implementation Order
697
+
698
+ ```
699
+ Phase 1 (Security) ──→ Phase 2 (Dead Code) ──→ Phase 3 (Agent Parity)
700
+
701
+ Phase 4 (Performance) ─────────────────────────────────┤
702
+
703
+ Phase 5 (Code Quality)
704
+ ```
705
+
706
+ Phases 1 and 4 are independent and can run in parallel. Phase 3 depends on Phase 2 (dead code removal first). Phase 5 depends on Phase 3 (shared helper extraction benefits from the new `export-markdown.ts` server build target).
707
+
708
+ ## Version Bump Strategy
709
+
710
+ | Phase | Bump | Version |
711
+ |-------|------|---------|
712
+ | Phase 1: Security | patch | 1.2.1 |
713
+ | Phase 2: Dead code | patch | 1.2.2 |
714
+ | Phase 3: Agent parity | minor | 1.3.0 |
715
+ | Phase 4: Performance | patch | 1.3.1 |
716
+ | Phase 5: Code quality | patch | 1.3.2 |
717
+
718
+ Or combine Phases 1+2 into one patch and 4+5 into another, yielding: `1.2.1 → 1.3.0 → 1.3.1`.
719
+
720
+ ## Sources & References
721
+
722
+ ### Internal References
723
+ - Prior art: `docs/plans/2026-03-23-001-refactor-site-review-findings-plan.md` — same review-then-fix pattern applied to the `site/` frontend
724
+ - Path traversal guard: `tools/editor/src/serve.ts:28-33`
725
+ - CLI dispatch: `tools/editor/bin.js:80-188`
726
+ - Export pipeline: `tools/editor/src/export-docx.ts` (~1770 lines)
727
+ - Server build config: `tools/editor/tsconfig.server.json`
728
+ - Package config: `tools/editor/package.json`
729
+
730
+ ### Review Agents
731
+ - Security Sentinel: 2 HIGH, 4 MEDIUM, 3 LOW findings
732
+ - Performance Oracle: 3 CRITICAL, 8 optimization opportunities
733
+ - Architecture Strategist: 8 findings on patterns, drift, dead code
734
+ - Kieran TypeScript Reviewer: strict mode, 42+ `any` usages, duplication
735
+ - Agent-Native Reviewer: 16/20 capabilities agent-accessible, 4 gaps
736
+ - Code Simplicity Reviewer: ~175 LOC removable (5.3%), 5 simplification opportunities