@cfbender/cesium 0.6.2 → 0.7.1

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/CHANGELOG.md +82 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/index.ts +2 -0
  5. package/src/prompt/system-fragment.md +68 -8
  6. package/src/render/annotate-frozen.ts +90 -0
  7. package/src/render/blocks/render.ts +20 -0
  8. package/src/render/blocks/renderers/callout.ts +3 -2
  9. package/src/render/blocks/renderers/code.ts +17 -2
  10. package/src/render/blocks/renderers/compare-table.ts +3 -2
  11. package/src/render/blocks/renderers/diagram.ts +3 -2
  12. package/src/render/blocks/renderers/diff.ts +23 -9
  13. package/src/render/blocks/renderers/hero.ts +3 -2
  14. package/src/render/blocks/renderers/kv.ts +3 -2
  15. package/src/render/blocks/renderers/list.ts +5 -4
  16. package/src/render/blocks/renderers/pill-row.ts +3 -2
  17. package/src/render/blocks/renderers/prose.ts +8 -2
  18. package/src/render/blocks/renderers/raw-html.ts +8 -2
  19. package/src/render/blocks/renderers/risk-table.ts +3 -2
  20. package/src/render/blocks/renderers/section.ts +4 -2
  21. package/src/render/blocks/renderers/timeline.ts +3 -2
  22. package/src/render/blocks/renderers/tldr.ts +3 -2
  23. package/src/render/client-js.ts +803 -6
  24. package/src/render/critique.ts +5 -335
  25. package/src/render/theme.ts +455 -6
  26. package/src/render/validate.ts +353 -97
  27. package/src/render/wrap.ts +67 -9
  28. package/src/server/api.ts +162 -3
  29. package/src/storage/index-gen.ts +4 -2
  30. package/src/storage/mutate.ts +433 -27
  31. package/src/tools/annotate.ts +336 -0
  32. package/src/tools/ask.ts +2 -6
  33. package/src/tools/critique.ts +15 -45
  34. package/src/tools/publish.ts +16 -56
  35. package/src/tools/styleguide.ts +7 -1
  36. package/src/tools/wait.ts +77 -24
package/src/server/api.ts CHANGED
@@ -4,13 +4,22 @@
4
4
  // Routes:
5
5
  // POST /api/sessions/:projectSlug/:filename/answers/:questionId
6
6
  // GET /api/sessions/:projectSlug/:filename/state
7
+ // POST /api/sessions/:projectSlug/:filename/comments
8
+ // DELETE /api/sessions/:projectSlug/:filename/comments/:commentId
9
+ // POST /api/sessions/:projectSlug/:filename/verdict
7
10
  //
8
11
  // Any other /api/* path returns a JSON 404 (rather than falling through to the
9
12
  // static file handler, which would return the HTML 404 page).
10
13
 
11
14
  import { join, resolve, relative } from "node:path";
12
15
  import { Hono } from "hono";
13
- import { submitAnswer, getState } from "../storage/mutate.ts";
16
+ import {
17
+ submitAnswer,
18
+ getState,
19
+ addComment,
20
+ removeComment,
21
+ setVerdict,
22
+ } from "../storage/mutate.ts";
14
23
  import type { AnswerValue } from "../render/validate.ts";
15
24
 
16
25
  export interface ApiHandlerOptions {
@@ -136,16 +145,166 @@ export function createApiApp(options: ApiHandlerOptions): Hono {
136
145
  return c.json({ ok: false, reason: outcome.reason }, 404);
137
146
  }
138
147
 
148
+ if (outcome.kind === "ask") {
149
+ return c.json(
150
+ {
151
+ kind: "ask",
152
+ status: outcome.status,
153
+ answers: outcome.answers,
154
+ remaining: outcome.remaining,
155
+ },
156
+ 200,
157
+ );
158
+ }
159
+
160
+ // kind === "annotate"
139
161
  return c.json(
140
162
  {
163
+ kind: "annotate",
141
164
  status: outcome.status,
142
- answers: outcome.answers,
143
- remaining: outcome.remaining,
165
+ comments: outcome.comments,
166
+ verdict: outcome.verdict,
167
+ verdictMode: outcome.verdictMode,
144
168
  },
145
169
  200,
146
170
  );
147
171
  });
148
172
 
173
+ // POST /api/sessions/:projectSlug/:filename/comments
174
+ app.post("/api/sessions/:projectSlug/:filename/comments", async (c) => {
175
+ const { projectSlug, filename } = c.req.param();
176
+
177
+ const resolved = resolveArtifact(stateDir, projectSlug, filename);
178
+ if (resolved instanceof Response) return resolved;
179
+
180
+ let body: unknown;
181
+ try {
182
+ body = await c.req.json();
183
+ } catch {
184
+ return c.json({ ok: false, error: "invalid JSON body" }, 400);
185
+ }
186
+
187
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
188
+ return c.json({ ok: false, error: "body must be an object" }, 400);
189
+ }
190
+
191
+ const raw = body as Record<string, unknown>;
192
+ if (
193
+ typeof raw["anchor"] !== "string" ||
194
+ typeof raw["selectedText"] !== "string" ||
195
+ typeof raw["comment"] !== "string"
196
+ ) {
197
+ return c.json(
198
+ { ok: false, error: 'body must contain "anchor", "selectedText", and "comment" fields' },
199
+ 400,
200
+ );
201
+ }
202
+
203
+ const outcome = await addComment({
204
+ artifactPath: resolved.artifactPath,
205
+ anchor: raw["anchor"],
206
+ selectedText: raw["selectedText"],
207
+ comment: raw["comment"],
208
+ });
209
+
210
+ if (outcome.ok) {
211
+ return c.json({ ok: true, comment: outcome.comment }, 200);
212
+ }
213
+
214
+ switch (outcome.reason) {
215
+ case "not-found":
216
+ case "not-interactive":
217
+ return c.json({ ok: false, reason: outcome.reason }, 404);
218
+ case "session-ended":
219
+ return c.json({ ok: false, status: outcome.status }, 410);
220
+ case "expired":
221
+ return c.json({ ok: false, status: "expired" }, 410);
222
+ case "invalid-value":
223
+ return c.json({ ok: false, message: outcome.message }, 422);
224
+ }
225
+
226
+ return c.json({ ok: false, error: "internal error" }, 500);
227
+ });
228
+
229
+ // DELETE /api/sessions/:projectSlug/:filename/comments/:commentId
230
+ app.delete("/api/sessions/:projectSlug/:filename/comments/:commentId", async (c) => {
231
+ const { projectSlug, filename, commentId } = c.req.param();
232
+
233
+ const resolved = resolveArtifact(stateDir, projectSlug, filename);
234
+ if (resolved instanceof Response) return resolved;
235
+
236
+ const outcome = await removeComment({
237
+ artifactPath: resolved.artifactPath,
238
+ commentId,
239
+ });
240
+
241
+ if (outcome.ok) {
242
+ return c.json({ ok: true }, 200);
243
+ }
244
+
245
+ switch (outcome.reason) {
246
+ case "not-found":
247
+ case "not-interactive":
248
+ return c.json({ ok: false, reason: outcome.reason }, 404);
249
+ case "comment-not-found":
250
+ return c.json({ ok: false, reason: "comment-not-found" }, 404);
251
+ case "session-ended":
252
+ return c.json({ ok: false, status: outcome.status }, 410);
253
+ case "expired":
254
+ return c.json({ ok: false, status: "expired" }, 410);
255
+ }
256
+
257
+ return c.json({ ok: false, error: "internal error" }, 500);
258
+ });
259
+
260
+ // POST /api/sessions/:projectSlug/:filename/verdict
261
+ app.post("/api/sessions/:projectSlug/:filename/verdict", async (c) => {
262
+ const { projectSlug, filename } = c.req.param();
263
+
264
+ const resolved = resolveArtifact(stateDir, projectSlug, filename);
265
+ if (resolved instanceof Response) return resolved;
266
+
267
+ let body: unknown;
268
+ try {
269
+ body = await c.req.json();
270
+ } catch {
271
+ return c.json({ ok: false, error: "invalid JSON body" }, 400);
272
+ }
273
+
274
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
275
+ return c.json({ ok: false, error: "body must be an object" }, 400);
276
+ }
277
+
278
+ const raw = body as Record<string, unknown>;
279
+ if (typeof raw["verdict"] !== "string") {
280
+ return c.json({ ok: false, error: 'body must contain a string "verdict" field' }, 400);
281
+ }
282
+
283
+ // Pass verdict as-is to setVerdict; mode-checking happens internally
284
+ const outcome = await setVerdict({
285
+ artifactPath: resolved.artifactPath,
286
+ verdict: raw["verdict"] as import("../render/validate.ts").Verdict,
287
+ });
288
+
289
+ if (outcome.ok) {
290
+ return c.json({ ok: true, status: outcome.status, verdict: outcome.verdict }, 200);
291
+ }
292
+
293
+ switch (outcome.reason) {
294
+ case "not-found":
295
+ case "not-interactive":
296
+ return c.json({ ok: false, reason: outcome.reason }, 404);
297
+ case "session-ended":
298
+ return c.json({ ok: false, status: outcome.status }, 410);
299
+ case "expired":
300
+ return c.json({ ok: false, status: "expired" }, 410);
301
+ case "invalid-value":
302
+ return c.json({ ok: false, message: outcome.message }, 422);
303
+ }
304
+
305
+ return c.json({ ok: false, error: "internal error" }, 500);
306
+ });
307
+
149
308
  // Catch-all under /api/* — keeps unmatched API paths as JSON 404 instead of
150
309
  // falling through to the static file handler.
151
310
  app.all("/api/*", (c) => c.json({ ok: false, error: "not found" }, 404));
@@ -94,6 +94,8 @@ function indexCss(): string {
94
94
  /* the eyebrow text is uppercased + tracked; the emblem sits flush left */
95
95
  }
96
96
  .cesium-eyebrow svg { display: block; }
97
+ a.cesium-eyebrow { color: inherit; text-decoration: none; transition: opacity 0.15s; }
98
+ a.cesium-eyebrow:hover { opacity: 0.7; }
97
99
  .filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; align-items: center; }
98
100
  .filter-chip {
99
101
  display: inline-block; font-family: var(--sans); font-size: 0.8em; font-weight: 500;
@@ -365,7 +367,7 @@ ${cardsHtml}
365
367
  </head>
366
368
  <body>
367
369
  <div class="page">
368
- <p class="eyebrow cesium-eyebrow">${faviconEmblemSvg(18)}<span>cesium · project</span></p>
370
+ <a class="eyebrow cesium-eyebrow" href="../../index.html">${faviconEmblemSvg(18)}<span>cesium · project</span></a>
369
371
  <h1 class="h-display">${esc(projectName)}</h1>
370
372
  ${subhead}
371
373
  ${filterRow}
@@ -454,7 +456,7 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
454
456
  </head>
455
457
  <body>
456
458
  <div class="page">
457
- <p class="eyebrow cesium-eyebrow">${faviconEmblemSvg(18)}<span>cesium</span></p>
459
+ <a class="eyebrow cesium-eyebrow" href="index.html">${faviconEmblemSvg(18)}<span>cesium</span></a>
458
460
  <h1 class="h-display">All projects</h1>
459
461
  ${subhead}
460
462
  ${bodyContent}