@specship/specship-win32-arm64 0.11.8 → 0.12.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 (235) hide show
  1. package/lib/dist/bin/specship.js +141 -66
  2. package/lib/dist/bin/specship.js.map +1 -1
  3. package/lib/dist/commands/specship/check.md +5 -0
  4. package/lib/dist/commands/specship/spec.md +235 -15
  5. package/lib/dist/db/migrations.d.ts.map +1 -1
  6. package/lib/dist/db/migrations.js +1 -0
  7. package/lib/dist/db/migrations.js.map +1 -1
  8. package/lib/dist/enforce/enforce.d.ts +15 -0
  9. package/lib/dist/enforce/enforce.d.ts.map +1 -1
  10. package/lib/dist/enforce/enforce.js +43 -1
  11. package/lib/dist/enforce/enforce.js.map +1 -1
  12. package/lib/dist/extraction/index.d.ts +6 -0
  13. package/lib/dist/extraction/index.d.ts.map +1 -1
  14. package/lib/dist/extraction/index.js.map +1 -1
  15. package/lib/dist/index.d.ts +9 -1
  16. package/lib/dist/index.d.ts.map +1 -1
  17. package/lib/dist/index.js +53 -9
  18. package/lib/dist/index.js.map +1 -1
  19. package/lib/dist/installer/index.d.ts.map +1 -1
  20. package/lib/dist/installer/index.js +35 -0
  21. package/lib/dist/installer/index.js.map +1 -1
  22. package/lib/dist/installer/instructions-template.d.ts +2 -2
  23. package/lib/dist/installer/instructions-template.js +3 -3
  24. package/lib/dist/installer/targets/claude.d.ts +10 -0
  25. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  26. package/lib/dist/installer/targets/claude.js +33 -7
  27. package/lib/dist/installer/targets/claude.js.map +1 -1
  28. package/lib/dist/mcp/designer-tools.js +1 -1
  29. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  30. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  31. package/lib/dist/mcp/server-instructions.js +1 -1
  32. package/lib/dist/mcp/spec-tools.d.ts +10 -0
  33. package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
  34. package/lib/dist/mcp/spec-tools.js +228 -2
  35. package/lib/dist/mcp/spec-tools.js.map +1 -1
  36. package/lib/dist/mcp/tools.d.ts +12 -0
  37. package/lib/dist/mcp/tools.d.ts.map +1 -1
  38. package/lib/dist/mcp/tools.js +34 -1
  39. package/lib/dist/mcp/tools.js.map +1 -1
  40. package/lib/dist/reflect/index.d.ts +1 -1
  41. package/lib/dist/reflect/index.d.ts.map +1 -1
  42. package/lib/dist/reflect/index.js +2 -1
  43. package/lib/dist/reflect/index.js.map +1 -1
  44. package/lib/dist/reflect/miner.d.ts +8 -0
  45. package/lib/dist/reflect/miner.d.ts.map +1 -1
  46. package/lib/dist/reflect/miner.js +64 -36
  47. package/lib/dist/reflect/miner.js.map +1 -1
  48. package/lib/dist/resolution/brief-link-resolver.d.ts +20 -0
  49. package/lib/dist/resolution/brief-link-resolver.d.ts.map +1 -1
  50. package/lib/dist/resolution/brief-link-resolver.js +65 -1
  51. package/lib/dist/resolution/brief-link-resolver.js.map +1 -1
  52. package/lib/dist/resolution/spec-link-resolver.d.ts +16 -2
  53. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  54. package/lib/dist/resolution/spec-link-resolver.js +19 -1
  55. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  56. package/lib/dist/server/cli.js +5 -49
  57. package/lib/dist/server/ingest/ingestor.js +17 -17
  58. package/lib/dist/server/ingest/pricing-backfill.js +55 -0
  59. package/lib/dist/server/ingest/pricing.js +3 -0
  60. package/lib/dist/server/ingest/project-paths.js +152 -0
  61. package/lib/dist/server/project-registry.js +15 -2
  62. package/lib/dist/server/routes/claude.js +47 -18
  63. package/lib/dist/server/routes/events.js +20 -1
  64. package/lib/dist/server/routes/projects.js +6 -3
  65. package/lib/dist/server/routes/spec.js +22 -2
  66. package/lib/dist/server/server.js +38 -4
  67. package/lib/dist/server/ssr/md.mjs +83 -0
  68. package/lib/dist/server/ssr/public/app.css +1 -0
  69. package/lib/dist/server/ssr/render.mjs +564 -0
  70. package/lib/dist/server/ssr/routes.js +73 -0
  71. package/lib/dist/server/static-handler.js +30 -0
  72. package/lib/dist/workflows/defaults/claude-design-implement.yaml +3 -3
  73. package/lib/dist/workflows/defaults/spec-implement.yaml +41 -9
  74. package/lib/node_modules/.package-lock.json +1 -1
  75. package/lib/package.json +3 -3
  76. package/package.json +1 -1
  77. package/lib/dist/commands/specship/design-implement.md +0 -84
  78. package/lib/dist/commands/specship/design-loop.md +0 -125
  79. package/lib/dist/commands/ss-behaviour.md +0 -116
  80. package/lib/dist/web/chunk-2AJCHB7P.js +0 -1
  81. package/lib/dist/web/chunk-2CPLUFCH.js +0 -2
  82. package/lib/dist/web/chunk-2I7L37NS.js +0 -1
  83. package/lib/dist/web/chunk-2IVSOVQZ.js +0 -1
  84. package/lib/dist/web/chunk-2NAWAJB5.js +0 -1
  85. package/lib/dist/web/chunk-2OJBIPE4.js +0 -1
  86. package/lib/dist/web/chunk-3E2WB6D5.js +0 -1
  87. package/lib/dist/web/chunk-3EBFYSCH.js +0 -2
  88. package/lib/dist/web/chunk-3QCQ4BXS.js +0 -1
  89. package/lib/dist/web/chunk-42XVAQ6I.js +0 -1
  90. package/lib/dist/web/chunk-4G7XU55L.js +0 -1
  91. package/lib/dist/web/chunk-4IMMPEYM.js +0 -1
  92. package/lib/dist/web/chunk-4TJQJPCZ.js +0 -1
  93. package/lib/dist/web/chunk-4WZIHTPC.js +0 -1
  94. package/lib/dist/web/chunk-4YVSYOSD.js +0 -1
  95. package/lib/dist/web/chunk-5BQIOYKW.js +0 -1
  96. package/lib/dist/web/chunk-5HGWHUJA.js +0 -1
  97. package/lib/dist/web/chunk-5NTGUSXJ.js +0 -1
  98. package/lib/dist/web/chunk-5Y244R4G.js +0 -1
  99. package/lib/dist/web/chunk-6RRDPT5Z.js +0 -1
  100. package/lib/dist/web/chunk-6VKB2ZWM.js +0 -1
  101. package/lib/dist/web/chunk-7656MA24.js +0 -1
  102. package/lib/dist/web/chunk-7DMFVTU4.js +0 -1
  103. package/lib/dist/web/chunk-7KWBINA7.js +0 -1
  104. package/lib/dist/web/chunk-7MUNFW56.js +0 -1
  105. package/lib/dist/web/chunk-7SMPKVEP.js +0 -1
  106. package/lib/dist/web/chunk-AZJVTPLU.js +0 -1
  107. package/lib/dist/web/chunk-AZUUBUEJ.js +0 -1
  108. package/lib/dist/web/chunk-BLBRMCN2.js +0 -1
  109. package/lib/dist/web/chunk-BMIAXD2V.js +0 -2
  110. package/lib/dist/web/chunk-BPCJLNBS.js +0 -47
  111. package/lib/dist/web/chunk-BUXWEHIY.js +0 -1
  112. package/lib/dist/web/chunk-CYXQILZI.js +0 -1
  113. package/lib/dist/web/chunk-DLQPZWSI.css +0 -1
  114. package/lib/dist/web/chunk-DOKZOFKM.js +0 -1
  115. package/lib/dist/web/chunk-DTWIJ3GJ.js +0 -1
  116. package/lib/dist/web/chunk-DYRFLPJA.js +0 -1
  117. package/lib/dist/web/chunk-E27QAKAB.js +0 -1
  118. package/lib/dist/web/chunk-E3J3CXR5.js +0 -1
  119. package/lib/dist/web/chunk-E73OX2P7.js +0 -1
  120. package/lib/dist/web/chunk-EAXRKDLV.js +0 -1
  121. package/lib/dist/web/chunk-EBKKDHYI.js +0 -1
  122. package/lib/dist/web/chunk-EE7V7Q5P.js +0 -1
  123. package/lib/dist/web/chunk-EKY2FUHU.js +0 -1
  124. package/lib/dist/web/chunk-EP6XOPXH.js +0 -1
  125. package/lib/dist/web/chunk-ESGDLJOJ.js +0 -1
  126. package/lib/dist/web/chunk-ETJG7NCY.js +0 -1
  127. package/lib/dist/web/chunk-EUUEFEDI.js +0 -1
  128. package/lib/dist/web/chunk-EXBUKHH2.js +0 -17
  129. package/lib/dist/web/chunk-F5TBUYDH.js +0 -1
  130. package/lib/dist/web/chunk-FC3R7YHR.js +0 -4
  131. package/lib/dist/web/chunk-FGNZDHTL.js +0 -11
  132. package/lib/dist/web/chunk-FIJW2UNJ.js +0 -1
  133. package/lib/dist/web/chunk-FMV5PXRC.js +0 -5
  134. package/lib/dist/web/chunk-G7RXFHYO.js +0 -1
  135. package/lib/dist/web/chunk-G7VZT5KB.js +0 -3
  136. package/lib/dist/web/chunk-GRZYXPSO.js +0 -7
  137. package/lib/dist/web/chunk-GYGPS3AN.js +0 -1
  138. package/lib/dist/web/chunk-H723TYYD.js +0 -2
  139. package/lib/dist/web/chunk-H7AF7YS4.js +0 -1
  140. package/lib/dist/web/chunk-HDZDQILN.js +0 -1
  141. package/lib/dist/web/chunk-HMK6UO6N.js +0 -1
  142. package/lib/dist/web/chunk-HSJNHNOH.js +0 -6
  143. package/lib/dist/web/chunk-IHEE5NYJ.js +0 -1
  144. package/lib/dist/web/chunk-IOB4JJT4.js +0 -1
  145. package/lib/dist/web/chunk-ISNEBICW.js +0 -1
  146. package/lib/dist/web/chunk-J2GZVLHH.js +0 -1
  147. package/lib/dist/web/chunk-JGDK6YKG.js +0 -1
  148. package/lib/dist/web/chunk-JLOJCDIY.js +0 -1
  149. package/lib/dist/web/chunk-JPSYO24K.js +0 -1
  150. package/lib/dist/web/chunk-JTFXTIPE.js +0 -903
  151. package/lib/dist/web/chunk-K7UD24Z4.js +0 -1
  152. package/lib/dist/web/chunk-KYSTCHAH.js +0 -1
  153. package/lib/dist/web/chunk-LB6JPLX2.js +0 -1
  154. package/lib/dist/web/chunk-LNSVDHCI.js +0 -1
  155. package/lib/dist/web/chunk-LVGIY3SO.js +0 -1
  156. package/lib/dist/web/chunk-LXLHIHEN.js +0 -1
  157. package/lib/dist/web/chunk-MNFTHCSJ.js +0 -1
  158. package/lib/dist/web/chunk-N6SS4G6S.js +0 -1
  159. package/lib/dist/web/chunk-NKPK3BR3.js +0 -1
  160. package/lib/dist/web/chunk-NTBJG6SJ.js +0 -1
  161. package/lib/dist/web/chunk-NUDB3Q2Y.js +0 -3
  162. package/lib/dist/web/chunk-OTQR5VFM.js +0 -1
  163. package/lib/dist/web/chunk-OXEF5E3E.js +0 -1
  164. package/lib/dist/web/chunk-P62EJGSF.js +0 -1
  165. package/lib/dist/web/chunk-PCW6R7OP.js +0 -1
  166. package/lib/dist/web/chunk-PGGJPDJG.js +0 -1
  167. package/lib/dist/web/chunk-POCVU4VK.js +0 -2
  168. package/lib/dist/web/chunk-PS6HRYQQ.js +0 -3
  169. package/lib/dist/web/chunk-PUYSJNJR.js +0 -1
  170. package/lib/dist/web/chunk-PZSU53SS.js +0 -1
  171. package/lib/dist/web/chunk-Q2RVFS45.js +0 -1
  172. package/lib/dist/web/chunk-Q7L6LLAK.js +0 -1
  173. package/lib/dist/web/chunk-QCMKJIWY.js +0 -1
  174. package/lib/dist/web/chunk-QH6CF3M3.js +0 -1
  175. package/lib/dist/web/chunk-QIZ65CGB.js +0 -1
  176. package/lib/dist/web/chunk-QPA2REUE.js +0 -1
  177. package/lib/dist/web/chunk-QQ5LD7PI.js +0 -1
  178. package/lib/dist/web/chunk-QR6L3KAC.js +0 -1
  179. package/lib/dist/web/chunk-R2DLK4HO.js +0 -1
  180. package/lib/dist/web/chunk-RD6TVPOT.js +0 -1
  181. package/lib/dist/web/chunk-RKY4EJYJ.js +0 -1
  182. package/lib/dist/web/chunk-RONYWVY7.js +0 -1
  183. package/lib/dist/web/chunk-RXKXYF2C.js +0 -1
  184. package/lib/dist/web/chunk-SQPYSOV6.js +0 -1
  185. package/lib/dist/web/chunk-SWKJRNYY.js +0 -1
  186. package/lib/dist/web/chunk-SYGIPC2P.js +0 -4
  187. package/lib/dist/web/chunk-T7AZ65JP.js +0 -1
  188. package/lib/dist/web/chunk-TCZDVOHD.js +0 -1
  189. package/lib/dist/web/chunk-TPTITA3V.js +0 -1
  190. package/lib/dist/web/chunk-TR335633.js +0 -1
  191. package/lib/dist/web/chunk-TWVVIWKO.js +0 -1
  192. package/lib/dist/web/chunk-U67LRNER.js +0 -1
  193. package/lib/dist/web/chunk-UR5KDXPX.js +0 -1
  194. package/lib/dist/web/chunk-UR6O2GEH.js +0 -1
  195. package/lib/dist/web/chunk-USVLLFVR.js +0 -1
  196. package/lib/dist/web/chunk-UTNMGWTP.js +0 -1
  197. package/lib/dist/web/chunk-V74ZPP4B.js +0 -1
  198. package/lib/dist/web/chunk-VECWMHJP.js +0 -1
  199. package/lib/dist/web/chunk-VUACT35R.js +0 -3
  200. package/lib/dist/web/chunk-VZI7H4SZ.js +0 -1
  201. package/lib/dist/web/chunk-WB6YHOD4.js +0 -1
  202. package/lib/dist/web/chunk-WBJ5VEB5.js +0 -1
  203. package/lib/dist/web/chunk-WBT64AWV.js +0 -1
  204. package/lib/dist/web/chunk-WFXJIXZE.js +0 -4
  205. package/lib/dist/web/chunk-WTGYRH3Z.js +0 -298
  206. package/lib/dist/web/chunk-WXTCVDTP.js +0 -1
  207. package/lib/dist/web/chunk-XAUQEKZX.js +0 -1
  208. package/lib/dist/web/chunk-XCDHWLVH.js +0 -1
  209. package/lib/dist/web/chunk-Y3H6FFUZ.js +0 -1
  210. package/lib/dist/web/chunk-Y4F5ULGJ.js +0 -1
  211. package/lib/dist/web/chunk-YEGKAAEE.js +0 -1
  212. package/lib/dist/web/chunk-YM2KU57F.js +0 -1
  213. package/lib/dist/web/chunk-YNYVOPWF.js +0 -1
  214. package/lib/dist/web/chunk-YRERBP6T.js +0 -1
  215. package/lib/dist/web/chunk-YV5LB22R.js +0 -1
  216. package/lib/dist/web/chunk-YWIRU745.js +0 -1
  217. package/lib/dist/web/chunk-YXYPTZ6E.js +0 -1
  218. package/lib/dist/web/chunk-ZLV4VCDG.js +0 -3
  219. package/lib/dist/web/chunk-ZTVI5KFF.js +0 -1
  220. package/lib/dist/web/favicon-16.png +0 -0
  221. package/lib/dist/web/favicon-180.png +0 -0
  222. package/lib/dist/web/favicon-32.png +0 -0
  223. package/lib/dist/web/favicon-512.png +0 -0
  224. package/lib/dist/web/favicon-small.svg +0 -15
  225. package/lib/dist/web/favicon.ico +0 -0
  226. package/lib/dist/web/favicon.svg +0 -20
  227. package/lib/dist/web/icon-192.png +0 -0
  228. package/lib/dist/web/icon-512.png +0 -0
  229. package/lib/dist/web/index.html +0 -146
  230. package/lib/dist/web/main-ESADRXN2.css +0 -1
  231. package/lib/dist/web/main-FY3HQ4PX.js +0 -1
  232. package/lib/dist/web/manifest.webmanifest +0 -15
  233. package/lib/dist/web/media/codicon-LN6W7LCM.ttf +0 -0
  234. package/lib/dist/web/styles-KSOPUVDA.css +0 -1
  235. package/lib/dist/web/sw.js +0 -69
@@ -16,10 +16,10 @@ import { writeSseHead } from './sse.js';
16
16
  import { promises as fs, watch } from 'node:fs';
17
17
  import os from 'node:os';
18
18
  import path from 'node:path';
19
- import { decodeProjectSlug } from '../ingest/ingestor.js';
19
+ import { createSlugResolver } from '../ingest/project-paths.js';
20
20
  const projectsBus = new EventEmitter();
21
21
  projectsBus.setMaxListeners(0); // many SSE subscribers + internal
22
- export async function enumerate(claudeRoot) {
22
+ export async function enumerate(claudeRoot, resolverOpts) {
23
23
  let entries;
24
24
  try {
25
25
  entries = await fs.readdir(claudeRoot, { withFileTypes: true });
@@ -27,12 +27,15 @@ export async function enumerate(claudeRoot) {
27
27
  catch {
28
28
  return [];
29
29
  }
30
+ // Slug decoding is lossy ('-'/'.'/'_' → '-'); resolve real paths from
31
+ // ~/.claude.json + transcript cwds instead (REQ-SLUGRES-002).
32
+ const resolveSlug = createSlugResolver(resolverOpts);
30
33
  const out = [];
31
34
  for (const ent of entries) {
32
35
  if (!ent.isDirectory())
33
36
  continue;
34
37
  const slug = ent.name;
35
- const decoded = decodeProjectSlug(slug);
38
+ const decoded = resolveSlug(slug);
36
39
  const projDir = path.join(claudeRoot, slug);
37
40
  let sessionCount = 0;
38
41
  let lastModifiedMs = 0;
@@ -98,8 +98,28 @@ export async function registerSpecRoutes(app) {
98
98
  const cg = await resolveCg(app, req, reply);
99
99
  if (!cg)
100
100
  return;
101
- const docs = cg.getSpecQueries().getAllSpecs();
102
- return { specs: docs };
101
+ const sq = cg.getSpecQueries();
102
+ const docs = sq.getAllSpecs();
103
+ // Rolled-up link state per spec id (REQ-DASHUX-003): worst state wins
104
+ // (broken > orphaned > drifted), else verified only when every link is,
105
+ // else implemented. One bulk pass — the tree colors 500+ rows from this
106
+ // map without per-spec detail calls.
107
+ const statesById = new Map();
108
+ for (const l of sq.getAllLinks()) {
109
+ const arr = statesById.get(l.specId) ?? [];
110
+ arr.push(l.state);
111
+ statesById.set(l.specId, arr);
112
+ }
113
+ const linkStates = {};
114
+ for (const [id, states] of statesById) {
115
+ linkStates[id] =
116
+ states.includes('broken') ? 'broken'
117
+ : states.includes('orphaned') ? 'orphaned'
118
+ : states.includes('drifted') ? 'drifted'
119
+ : states.every((s) => s === 'verified') ? 'verified'
120
+ : 'implemented';
121
+ }
122
+ return { specs: docs, linkStates };
103
123
  });
104
124
  // Spec lifecycle funnel: idea → spec → implemented (REQ-FUNNEL-006). Computed
105
125
  // on the dynamically-loaded SpecShip instance — no runtime import of the
@@ -18,8 +18,9 @@ import Fastify from 'fastify';
18
18
  import cors from '@fastify/cors';
19
19
  import { startWatcher, primaryProjectMatcher } from './ingest/index.js';
20
20
  import { backfillDisplaced } from './ingest/impact-backfill.js';
21
+ import { recostUnpricedPrompts } from './ingest/pricing-backfill.js';
21
22
  import { ProjectRegistry } from './project-registry.js';
22
- import { makeStaticHandler } from './static-handler.js';
23
+ import { makeStaticHandler, isAssetPath } from './static-handler.js';
23
24
  import { registerGraphRoutes } from './routes/graph.js';
24
25
  import { registerSpecRoutes } from './routes/spec.js';
25
26
  import { registerWorkflowRoutes } from './routes/workflow.js';
@@ -86,14 +87,22 @@ export async function createServer(options) {
86
87
  // Lazy-load specship. Used as the open() impl for the registry, and
87
88
  // (only when a primary path is set) to open the primary instance below.
88
89
  const cgMod = await loadSpecShip();
89
- const registry = new ProjectRegistry({ verbose }, (p) => cgMod.SpecShip.open(p));
90
+ // maxOpen must exceed the /api/events cross-project sweep size (10) plus
91
+ // the primary and a user-picked project, or the sweep churns open/close
92
+ // cycles through the LRU on every poll.
93
+ const registry = new ProjectRegistry({ verbose, maxOpen: 16 }, (p) => cgMod.SpecShip.open(p));
90
94
  // Primary project (optional). When set, specship-scoped routes default to
91
95
  // it when no `?project=<slug>` is provided, and the analytics ingest
92
96
  // hooks into its SQLite handle.
93
97
  let primaryCg = null;
94
98
  if (options.projectRoot) {
95
99
  primaryCg = await registry.get(options.projectRoot);
96
- if (!primaryCg && verbose) {
100
+ if (primaryCg) {
101
+ // Routes capture this instance by reference — it must never be LRU
102
+ // evicted (eviction closes the SQLite handle and every request 500s).
103
+ registry.pin(options.projectRoot);
104
+ }
105
+ else if (verbose) {
97
106
  console.error(`[specship-server] primary project ${options.projectRoot} not initialized — booting projectless`);
98
107
  }
99
108
  }
@@ -134,6 +143,16 @@ export async function createServer(options) {
134
143
  catch (err) {
135
144
  console.error('[specship-server] specship-impact backfill failed (non-fatal):', err instanceof Error ? err.message : String(err));
136
145
  }
146
+ // Re-cost prompts ingested while their model family was unpriced,
147
+ // e.g. fable sessions stuck at $0 (REQ-DASHINT-001.A2). Idempotent.
148
+ try {
149
+ const recosted = recostUnpricedPrompts(dbHandle);
150
+ if (verbose && recosted > 0)
151
+ console.error(`[specship-server] re-costed ${recosted} unpriced prompts`);
152
+ }
153
+ catch (err) {
154
+ console.error('[specship-server] pricing backfill failed (non-fatal):', err instanceof Error ? err.message : String(err));
155
+ }
137
156
  }
138
157
  }
139
158
  else if (!primaryCg && options.ingest !== false && verbose) {
@@ -174,6 +193,14 @@ export async function createServer(options) {
174
193
  await registerMaintainabilityRoutes(app);
175
194
  await registerDomainRoutes(app);
176
195
  await registerChatRoutes(app);
196
+ // SSR dashboard (DASH-LEAN-DOC / REQ-DASHLEAN-004): explicit page routes
197
+ // rendered in-process from the /api handlers above (via app.inject), plus the
198
+ // dashboard's static assets. Registered AFTER /api and BEFORE the SPA
199
+ // fallback so the explicit page routes win.
200
+ if (options.ssr) {
201
+ const { registerSsrRoutes } = await import('./ssr/routes.js');
202
+ await registerSsrRoutes(app);
203
+ }
177
204
  // Optional: serve the built Angular UI from `webDir` and fall back to
178
205
  // index.html for client-side routes. Must register AFTER the /api/*
179
206
  // routes so they take precedence over the SPA wildcard.
@@ -198,7 +225,10 @@ export async function createServer(options) {
198
225
  // 2. Fall through to index.html for any other GET so Angular's
199
226
  // client-side router can take over (`/memory`, `/graph`, …).
200
227
  // Non-GET methods and `/api/*` paths still 404 cleanly so the UI
201
- // can surface them.
228
+ // can surface them. A MISSING asset path (e.g. a stale build's old
229
+ // content-hashed bundle) must also 404, never receive the shell —
230
+ // HTML under an asset URL poisons the offline service worker's cache
231
+ // (REQ-OFFLINE-005).
202
232
  app.setNotFoundHandler((request, reply) => {
203
233
  if (request.method !== 'GET') {
204
234
  reply.code(404).send({ error: 'not found' });
@@ -213,6 +243,10 @@ export async function createServer(options) {
213
243
  reply.code(200).type(hit.contentType).send(hit.body);
214
244
  return;
215
245
  }
246
+ if (isAssetPath(request.url)) {
247
+ reply.code(404).send({ error: 'not found' });
248
+ return;
249
+ }
216
250
  reply.code(200).type('text/html').send(cachedIndex);
217
251
  });
218
252
  if (verbose)
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Minimal, dependency-free Markdown → HTML for the SSR dashboard.
3
+ *
4
+ * Scope is deliberately small: the subset spec bodies actually use — headings,
5
+ * bold/italic/inline-code, fenced code, unordered/ordered lists, and paragraphs.
6
+ * Everything is HTML-escaped first, so rendering is safe for untrusted spec
7
+ * text (no raw HTML passthrough). This is the SSR counterpart to the Angular
8
+ * app's `render-md.ts`; keeping it dependency-free is what keeps the web-ssr
9
+ * tree under the REQ-DASHLEAN-002 package ceiling.
10
+ */
11
+
12
+ export function escapeHtml(s) {
13
+ return String(s)
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&#39;');
19
+ }
20
+
21
+ /** Strip embedded `<!-- id: REQ-X -->` markers — structural noise, not prose. */
22
+ export function stripSpecMarkers(s) {
23
+ return String(s).replace(/<!--[\s\S]*?-->/g, '');
24
+ }
25
+
26
+ function inline(s) {
27
+ // Operate on already-escaped text so the markup we add is the only HTML.
28
+ return s
29
+ .replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`)
30
+ .replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`)
31
+ .replace(/(^|[^*])\*([^*]+)\*/g, (_, p, c) => `${p}<em>${c}</em>`);
32
+ }
33
+
34
+ export function renderMd(src) {
35
+ const lines = escapeHtml(stripSpecMarkers(src ?? '')).split('\n');
36
+ const out = [];
37
+ let i = 0;
38
+ let para = [];
39
+ const flushPara = () => {
40
+ if (para.length) { out.push(`<p>${inline(para.join(' ')).trim()}</p>`); para = []; }
41
+ };
42
+
43
+ while (i < lines.length) {
44
+ const line = lines[i];
45
+
46
+ // fenced code block
47
+ const fence = line.match(/^```(.*)$/);
48
+ if (fence) {
49
+ flushPara();
50
+ const body = [];
51
+ i++;
52
+ while (i < lines.length && !/^```/.test(lines[i])) { body.push(lines[i]); i++; }
53
+ i++; // closing fence
54
+ out.push(`<pre><code>${body.join('\n')}</code></pre>`);
55
+ continue;
56
+ }
57
+
58
+ // heading
59
+ const h = line.match(/^(#{1,6})\s+(.*)$/);
60
+ if (h) { flushPara(); const lvl = h[1].length; out.push(`<h${lvl}>${inline(h[2].trim())}</h${lvl}>`); i++; continue; }
61
+
62
+ // list (consume a run of bullet / ordered lines)
63
+ if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
64
+ flushPara();
65
+ const ordered = /^\s*\d+\.\s+/.test(line);
66
+ const items = [];
67
+ while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i])) {
68
+ items.push(`<li>${inline(lines[i].replace(/^\s*([-*]|\d+\.)\s+/, '').trim())}</li>`);
69
+ i++;
70
+ }
71
+ out.push(ordered ? `<ol>${items.join('')}</ol>` : `<ul>${items.join('')}</ul>`);
72
+ continue;
73
+ }
74
+
75
+ // blank line ends a paragraph
76
+ if (/^\s*$/.test(line)) { flushPara(); i++; continue; }
77
+
78
+ para.push(line.trim());
79
+ i++;
80
+ }
81
+ flushPara();
82
+ return out.join('\n');
83
+ }
@@ -0,0 +1 @@
1
+ @charset "UTF-8";@layer properties;@layer theme,base,components,utilities;@layer theme{:root,:host{--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing: .25rem;--ease-out: cubic-bezier(0, 0, .2, 1);--ease-in-out: cubic-bezier(.4, 0, .2, 1);--default-transition-duration: .15s;--default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);--default-font-family: var(--font-sans);--default-mono-font-family: var(--font-mono)}}@layer base{*,:after,:before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings, normal);font-variation-settings:var(--default-font-variation-settings, normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings, normal);font-variation-settings:var(--default-mono-font-variation-settings, normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:transparent;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::placeholder{color:currentcolor}@supports (color: color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]),::file-selector-button{appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.static{position:static}.top-18{top:calc(var(--spacing) * 18)}.top-60{top:calc(var(--spacing) * 60)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.flex-shrink{flex-shrink:1}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.flex-wrap{flex-wrap:wrap}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-7{gap:calc(var(--spacing) * 7)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-12{gap:calc(var(--spacing) * 12)}.gap-16{gap:calc(var(--spacing) * 16)}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.border{border-style:var(--tw-border-style);border-width:1px}.capitalize{text-transform:capitalize}.ring{--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function));transition-duration:var(--tw-duration, var(--default-transition-duration))}.ease-in-out{--tw-ease: var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease: var(--ease-out);transition-timing-function:var(--ease-out)}}:root{color-scheme:dark;--bg-canvas: #0E1014;--bg-canvas-2: #0B0D11;--bg-panel: #161A21;--bg-panel-2: #1A1F28;--bg-elevated: #20262F;--bg-hover: rgba(255,255,255,.04);--bg-active: rgba(255,255,255,.07);--border-subtle: rgba(255,255,255,.07);--border-strong: rgba(255,255,255,.12);--border-focus: #3B82F6;--text-primary: #E8EBF0;--text-secondary: #9AA3B2;--text-muted: #5C6573;--text-faint: #424B59;--accent: #3B82F6;--accent-strong: #2563EB;--accent-soft: rgba(59,130,246,.14);--accent-line: rgba(59,130,246,.35);--node-code: #A586F5;--node-spec: #5B93F2;--node-test: #46C26B;--node-route: #29D2BE;--node-code-soft: rgba(165,134,245,.14);--node-spec-soft: rgba(91,147,242,.14);--node-test-soft: rgba(70,194,107,.14);--node-route-soft: rgba(41,210,190,.14);--node-text: #ffffff;--dot-grid: rgba(255,255,255,.045);--success: #46C26B;--success-soft: rgba(70,194,107,.13);--warn: #E5A50A;--warn-soft: rgba(229,165,10,.14);--error: #F2555A;--error-soft: rgba(242,85,90,.14);--info: #58A6FF;--info-soft: rgba(88,166,255,.13);--font-ui: "Geist", system-ui, -apple-system, sans-serif;--font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;--fs-title: 22px;--fs-base: 13px;--fs-sm: 12px;--fs-xs: 11px;--fs-big: 28px;--r-sm: 5px;--r-md: 8px;--r-lg: 12px;--r-pill: 999px;--shadow-pop: 0 12px 32px -8px rgba(0,0,0,.6), 0 2px 8px rgba(0,0,0,.4);--shadow-panel: 0 1px 0 rgba(255,255,255,.02) inset;--sidebar-w: 232px;--sidebar-w-collapsed: 56px;--status-h: 30px}[data-theme=light]{color-scheme:light;--bg-canvas: #F4F6F8;--bg-canvas-2: #E9ECF1;--bg-panel: #FFFFFF;--bg-panel-2: #F8FAFC;--bg-elevated: #FFFFFF;--bg-hover: rgba(15,23,42,.045);--bg-active: rgba(15,23,42,.07);--border-subtle: rgba(15,23,42,.1);--border-strong: rgba(15,23,42,.18);--border-focus: #2563EB;--text-primary: #161B22;--text-secondary: #4A5566;--text-muted: #7B8696;--text-faint: #A9B2C0;--accent: #2563EB;--accent-strong: #2563EB;--accent-soft: rgba(37,99,235,.1);--accent-line: rgba(37,99,235,.3);--node-code: #7C56E0;--node-spec: #2F6FE0;--node-test: #1E9E4C;--node-route: #0E9C8E;--node-code-soft: rgba(124,86,224,.12);--node-spec-soft: rgba(47,111,224,.12);--node-test-soft: rgba(30,158,76,.12);--node-route-soft: rgba(14,156,142,.12);--node-text: #1A2230;--dot-grid: rgba(15,23,42,.07);--success: #1E9E4C;--success-soft: rgba(30,158,76,.12);--warn: #B27500;--warn-soft: rgba(178,117,0,.13);--error: #DC4A4F;--error-soft: rgba(220,74,79,.12);--info: #2563EB;--info-soft: rgba(37,99,235,.11);--shadow-pop: 0 12px 32px -10px rgba(15,23,42,.22), 0 2px 8px rgba(15,23,42,.1);--shadow-panel: 0 1px 0 rgba(15,23,42,.02) inset}*{box-sizing:border-box}html,body{margin:0;padding:0;height:100%;background:var(--bg-canvas);color:var(--text-primary);font-family:var(--font-ui);font-size:var(--fs-base);line-height:1.45;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-feature-settings:"cv01","cv03","ss03";overflow:hidden}#root{height:100%}::selection{background:var(--accent-soft)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#ffffff14;border-radius:999px;border:2px solid transparent;background-clip:content-box}::-webkit-scrollbar-thumb:hover{background:#ffffff29;background-clip:content-box}.mono{font-family:var(--font-mono);font-feature-settings:"zero"}.tnum{font-variant-numeric:tabular-nums}.btn{font-family:var(--font-ui);font-size:var(--fs-base);font-weight:500;border:1px solid transparent;border-radius:var(--r-sm);padding:6px 12px;display:inline-flex;align-items:center;gap:7px;cursor:pointer;white-space:nowrap;transition:background .1s ease,border-color .1s ease,color .1s ease;-webkit-user-select:none;user-select:none}.btn svg{width:14px;height:14px;flex-shrink:0}.btn-primary{background:var(--accent-strong);color:#fff}.btn-primary:hover{background:var(--accent)}.btn-secondary{background:var(--bg-elevated);color:var(--text-primary);border-color:var(--border-subtle)}.btn-secondary:hover{background:#283039;border-color:var(--border-strong)}.btn-ghost{background:transparent;color:var(--text-secondary)}.btn-ghost:hover{background:var(--bg-hover);color:var(--text-primary)}.btn-destructive{background:var(--error-soft);color:var(--error);border-color:#f2555a4d}.btn-destructive:hover{background:#f2555a38}.btn-sm{padding:4px 9px;font-size:var(--fs-sm)}.btn-xs{padding:2px 7px;font-size:var(--fs-xs);border-radius:4px}.btn:disabled{opacity:.4;cursor:not-allowed}.pill{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);font-weight:600;padding:2px 8px;border-radius:var(--r-pill);letter-spacing:.01em;white-space:nowrap;line-height:1.5}.pill svg{width:11px;height:11px}.pill-dot{width:6px;height:6px;border-radius:50%}.card{background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:var(--r-lg)}.card-pad{padding:14px}.input{background:var(--bg-canvas);border:1px solid var(--border-subtle);border-radius:var(--r-sm);color:var(--text-primary);font-family:var(--font-ui);font-size:var(--fs-base);padding:6px 10px;outline:none;transition:border-color .1s ease,box-shadow .1s ease}.input:focus{border-color:var(--border-focus);box-shadow:0 0 0 3px var(--accent-soft)}.input::placeholder{color:var(--text-muted)}.eyebrow{font-size:var(--fs-xs);font-weight:600;text-transform:uppercase;letter-spacing:.07em;color:var(--text-muted)}:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.row{display:flex;align-items:center}.col{display:flex;flex-direction:column}.gap-2{gap:2px}.gap-4{gap:4px}.gap-6{gap:6px}.gap-8{gap:8px}.gap-10{gap:10px}.gap-12{gap:12px}.gap-16{gap:16px}.grow{flex:1}.muted{color:var(--text-muted)}.secondary{color:var(--text-secondary)}.tabular{font-variant-numeric:tabular-nums}.scroll-y{overflow-y:auto;overflow-x:hidden}@keyframes pulsePill{0%{box-shadow:0 0 0 0 currentColor}to{box-shadow:0 0 0 8px transparent}}@keyframes slideInRight{0%{opacity:0;transform:translate(14px)}to{opacity:1;transform:translate(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes runningDash{to{stroke-dashoffset:-16}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes skeleton{0%{opacity:.5}50%{opacity:.9}to{opacity:.5}}.skel{background:var(--bg-elevated);border-radius:5px;animation:skeleton 1.4s ease-in-out infinite}@media(prefers-reduced-motion:reduce){*{animation-duration:.001ms!important;transition-duration:.001ms!important}}@property --tw-rotate-x{syntax: "*"; inherits: false;}@property --tw-rotate-y{syntax: "*"; inherits: false;}@property --tw-rotate-z{syntax: "*"; inherits: false;}@property --tw-skew-x{syntax: "*"; inherits: false;}@property --tw-skew-y{syntax: "*"; inherits: false;}@property --tw-border-style{syntax: "*"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: "*"; inherits: false;}@property --tw-shadow-alpha{syntax: "<percentage>"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: "*"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: "<percentage>"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: "*"; inherits: false;}@property --tw-ring-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: "*"; inherits: false;}@property --tw-inset-ring-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: "*"; inherits: false;}@property --tw-ring-offset-width{syntax: "<length>"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: "*"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-blur{syntax: "*"; inherits: false;}@property --tw-brightness{syntax: "*"; inherits: false;}@property --tw-contrast{syntax: "*"; inherits: false;}@property --tw-grayscale{syntax: "*"; inherits: false;}@property --tw-hue-rotate{syntax: "*"; inherits: false;}@property --tw-invert{syntax: "*"; inherits: false;}@property --tw-opacity{syntax: "*"; inherits: false;}@property --tw-saturate{syntax: "*"; inherits: false;}@property --tw-sepia{syntax: "*"; inherits: false;}@property --tw-drop-shadow{syntax: "*"; inherits: false;}@property --tw-drop-shadow-color{syntax: "*"; inherits: false;}@property --tw-drop-shadow-alpha{syntax: "<percentage>"; inherits: false; initial-value: 100%;}@property --tw-drop-shadow-size{syntax: "*"; inherits: false;}@property --tw-ease{syntax: "*"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x: initial;--tw-rotate-y: initial;--tw-rotate-z: initial;--tw-skew-x: initial;--tw-skew-y: initial;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-blur: initial;--tw-brightness: initial;--tw-contrast: initial;--tw-grayscale: initial;--tw-hue-rotate: initial;--tw-invert: initial;--tw-opacity: initial;--tw-saturate: initial;--tw-sepia: initial;--tw-drop-shadow: initial;--tw-drop-shadow-color: initial;--tw-drop-shadow-alpha: 100%;--tw-drop-shadow-size: initial;--tw-ease: initial}}}