runcastle 1.2.0 → 1.2.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.
package/bin/runcastle.js CHANGED
@@ -246,7 +246,7 @@ var init_ids = () => {};
246
246
 
247
247
  // ../core/src/schemas.ts
248
248
  import * as z2 from "zod";
249
- var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
249
+ var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
250
250
  var init_schemas = __esm(() => {
251
251
  Phase = z2.enum([
252
252
  "ideation",
@@ -308,6 +308,17 @@ var init_schemas = __esm(() => {
308
308
  lastSessionId: z2.string().optional(),
309
309
  summary: z2.string().optional()
310
310
  });
311
+ TestNoteStatus = z2.enum(["open", "done", "promoted"]);
312
+ TestNote = z2.object({
313
+ id: z2.string(),
314
+ featureId: z2.string(),
315
+ lap: z2.number(),
316
+ text: z2.string(),
317
+ status: TestNoteStatus,
318
+ ticketId: z2.string().optional(),
319
+ createdAt: z2.number(),
320
+ updatedAt: z2.number()
321
+ });
311
322
  PREPARED_KEYS = [
312
323
  "setupCommand",
313
324
  "verifyCommands",
@@ -560,7 +571,7 @@ var init_pipeline = __esm(() => {
560
571
 
561
572
  // ../core/src/db-schema.ts
562
573
  import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
563
- var projects, projectFindings, features, sessions, tickets, waypoints, runs, events, gateOverrides;
574
+ var projects, projectFindings, features, sessions, tickets, testNotes, waypoints, runs, events, gateOverrides;
564
575
  var init_db_schema = __esm(() => {
565
576
  projects = sqliteTable("projects", {
566
577
  id: text("id").primaryKey(),
@@ -632,6 +643,16 @@ var init_db_schema = __esm(() => {
632
643
  attemptBranch: text("attempt_branch"),
633
644
  conflictFiles: text("conflict_files", { mode: "json" }).$type()
634
645
  });
646
+ testNotes = sqliteTable("test_notes", {
647
+ id: text("id").primaryKey(),
648
+ featureId: text("feature_id").notNull(),
649
+ lap: integer("lap").notNull(),
650
+ text: text("text").notNull(),
651
+ status: text("status").notNull().$type(),
652
+ ticketId: text("ticket_id"),
653
+ createdAt: integer("created_at").notNull(),
654
+ updatedAt: integer("updated_at").notNull()
655
+ });
635
656
  waypoints = sqliteTable("waypoints", {
636
657
  id: text("id").primaryKey(),
637
658
  featureId: text("feature_id").notNull(),
@@ -694,6 +715,7 @@ var init_schema = __esm(() => {
694
715
  features,
695
716
  sessions,
696
717
  tickets,
718
+ testNotes,
697
719
  waypoints,
698
720
  runs,
699
721
  events,
@@ -9346,8 +9368,188 @@ var init_system = __esm(() => {
9346
9368
  });
9347
9369
  });
9348
9370
 
9349
- // src/trpc/routers/ticket.ts
9371
+ // src/services/test-notes.ts
9372
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9373
+ import { dirname as dirname10 } from "path";
9374
+ import { asc as asc4, eq as eq14 } from "drizzle-orm";
9375
+ function rowToNote(row) {
9376
+ return {
9377
+ id: row.id,
9378
+ featureId: row.featureId,
9379
+ lap: row.lap,
9380
+ text: row.text,
9381
+ status: row.status,
9382
+ ticketId: row.ticketId ?? undefined,
9383
+ createdAt: row.createdAt,
9384
+ updatedAt: row.updatedAt
9385
+ };
9386
+ }
9387
+ function getNote(ctx, id) {
9388
+ const row = ctx.db.select().from(testNotes).where(eq14(testNotes.id, id)).get();
9389
+ if (!row)
9390
+ throw new NotFoundError(`test note ${id} not found`);
9391
+ return rowToNote(row);
9392
+ }
9393
+ function cleanText(text2) {
9394
+ const trimmed = text2.trim();
9395
+ if (!trimmed)
9396
+ throw new InvalidInputError("note text cannot be empty");
9397
+ return trimmed;
9398
+ }
9399
+ function assertOpen(note, action) {
9400
+ if (note.status !== "open") {
9401
+ throw new InvalidInputError(`cannot ${action} note ${note.id} \u2014 it is ${note.status}; only open notes can be changed`);
9402
+ }
9403
+ }
9404
+ function listByFeature3(ctx, featureId) {
9405
+ return ctx.db.select().from(testNotes).where(eq14(testNotes.featureId, featureId)).orderBy(asc4(testNotes.createdAt), asc4(testNotes.id)).all().map(rowToNote);
9406
+ }
9407
+ function addNote(ctx, featureId, text2) {
9408
+ const body = cleanText(text2);
9409
+ const feature = getFeatureRow(ctx, featureId);
9410
+ const now = Date.now();
9411
+ const row = ctx.db.insert(testNotes).values({
9412
+ id: newId("note"),
9413
+ featureId,
9414
+ lap: feature.lap,
9415
+ text: body,
9416
+ status: "open",
9417
+ ticketId: null,
9418
+ createdAt: now,
9419
+ updatedAt: now
9420
+ }).returning().get();
9421
+ const note = rowToNote(row);
9422
+ emit(ctx, featureId, {
9423
+ type: "note.added",
9424
+ message: `note captured on lap ${note.lap}`,
9425
+ data: { noteId: note.id, lap: note.lap }
9426
+ });
9427
+ renderTestNotes(ctx, feature);
9428
+ return note;
9429
+ }
9430
+ function editNote(ctx, noteId, text2) {
9431
+ const current2 = getNote(ctx, noteId);
9432
+ assertOpen(current2, "edit");
9433
+ const body = cleanText(text2);
9434
+ ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9435
+ emit(ctx, current2.featureId, {
9436
+ type: "note.edited",
9437
+ message: "note edited",
9438
+ data: { noteId }
9439
+ });
9440
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9441
+ return getNote(ctx, noteId);
9442
+ }
9443
+ function deleteNote(ctx, noteId) {
9444
+ const current2 = getNote(ctx, noteId);
9445
+ assertOpen(current2, "delete");
9446
+ ctx.db.delete(testNotes).where(eq14(testNotes.id, noteId)).run();
9447
+ emit(ctx, current2.featureId, {
9448
+ type: "note.deleted",
9449
+ message: "note deleted",
9450
+ data: { noteId }
9451
+ });
9452
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9453
+ }
9454
+ function toggleNote(ctx, noteId) {
9455
+ const current2 = getNote(ctx, noteId);
9456
+ if (current2.status === "promoted") {
9457
+ throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
9458
+ }
9459
+ const status = current2.status === "open" ? "done" : "open";
9460
+ ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9461
+ emit(ctx, current2.featureId, {
9462
+ type: "note.toggled",
9463
+ message: `note ${current2.status} \u2192 ${status}`,
9464
+ data: { noteId, from: current2.status, to: status }
9465
+ });
9466
+ renderTestNotes(ctx, getFeatureRow(ctx, current2.featureId));
9467
+ return getNote(ctx, noteId);
9468
+ }
9469
+ function promotionTicket(feature, note) {
9470
+ const firstLine = note.text.split(`
9471
+ `)[0].trim();
9472
+ const docs = featureDocsRel(feature.slug);
9473
+ return {
9474
+ title: firstLine.length <= TITLE_MAX ? firstLine : `${firstLine.slice(0, TITLE_MAX - 1).trimEnd()}\u2026`,
9475
+ goal: note.text,
9476
+ context: [
9477
+ `Found during lap ${note.lap} test drive of ${feature.slug}.`,
9478
+ `Read ${docs}/spec.md and ${docs}/decisions.md for what this feature is meant to do.`
9479
+ ].join(`
9480
+
9481
+ `),
9482
+ acceptanceCriteria: [`The noted behavior no longer reproduces: ${note.text}`],
9483
+ seams: [],
9484
+ blockedBy: []
9485
+ };
9486
+ }
9487
+ function promoteNote(ctx, noteId) {
9488
+ const current2 = getNote(ctx, noteId);
9489
+ assertOpen(current2, "promote");
9490
+ const feature = getFeatureRow(ctx, current2.featureId);
9491
+ const [ticket] = storeTickets(ctx, current2.featureId, [promotionTicket(feature, current2)]);
9492
+ ctx.db.update(testNotes).set({ status: "promoted", ticketId: ticket.id, updatedAt: Date.now() }).where(eq14(testNotes.id, noteId)).run();
9493
+ emit(ctx, current2.featureId, {
9494
+ type: "note.promoted",
9495
+ message: `note promoted to ticket ${ticket.seq}`,
9496
+ ticketId: ticket.id,
9497
+ data: { noteId, seq: ticket.seq }
9498
+ });
9499
+ renderTestNotes(ctx, feature);
9500
+ return { note: getNote(ctx, noteId), ticket };
9501
+ }
9502
+ function noteLine(ctx, note) {
9503
+ if (note.status === "open")
9504
+ return `- [ ] ${note.text}`;
9505
+ const ticket = note.ticketId ? ` (\u2192 ticket ${getTicket(ctx, note.ticketId).seq})` : "";
9506
+ return `- [x] ${note.text}${ticket}`;
9507
+ }
9508
+ function renderTestNotes(ctx, feature) {
9509
+ const notes = listByFeature3(ctx, feature.id);
9510
+ const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
9511
+ const lines = ["# Test notes"];
9512
+ for (const lap of laps) {
9513
+ lines.push("", `## Lap ${lap}`, "");
9514
+ for (const note of notes.filter((n) => n.lap === lap))
9515
+ lines.push(noteLine(ctx, note));
9516
+ }
9517
+ const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9518
+ mkdirSync9(dirname10(path), { recursive: true });
9519
+ writeFileSync5(path, `${lines.join(`
9520
+ `)}
9521
+ `, "utf8");
9522
+ }
9523
+ var TITLE_MAX = 60;
9524
+ var init_test_notes = __esm(() => {
9525
+ init_src();
9526
+ init_paths();
9527
+ init_schema();
9528
+ init_errors();
9529
+ init_events();
9530
+ init_feature_docs();
9531
+ init_repo();
9532
+ init_tickets();
9533
+ });
9534
+
9535
+ // src/trpc/routers/test-notes.ts
9350
9536
  import * as z12 from "zod";
9537
+ var testNotesRouter;
9538
+ var init_test_notes2 = __esm(() => {
9539
+ init_test_notes();
9540
+ init_context();
9541
+ testNotesRouter = router({
9542
+ list: publicProcedure.input(z12.object({ featureId: z12.string() })).query(({ ctx, input }) => listByFeature3(ctx, input.featureId)),
9543
+ add: publicProcedure.input(z12.object({ featureId: z12.string(), text: z12.string().min(1) })).mutation(({ ctx, input }) => addNote(ctx, input.featureId, input.text)),
9544
+ edit: publicProcedure.input(z12.object({ noteId: z12.string(), text: z12.string().min(1) })).mutation(({ ctx, input }) => editNote(ctx, input.noteId, input.text)),
9545
+ remove: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => deleteNote(ctx, input.noteId)),
9546
+ toggle: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => toggleNote(ctx, input.noteId)),
9547
+ promote: publicProcedure.input(z12.object({ noteId: z12.string() })).mutation(({ ctx, input }) => promoteNote(ctx, input.noteId))
9548
+ });
9549
+ });
9550
+
9551
+ // src/trpc/routers/ticket.ts
9552
+ import * as z13 from "zod";
9351
9553
  var ticketRouter;
9352
9554
  var init_ticket = __esm(() => {
9353
9555
  init_features();
@@ -9356,8 +9558,8 @@ var init_ticket = __esm(() => {
9356
9558
  init_ticket_burner();
9357
9559
  init_context();
9358
9560
  ticketRouter = router({
9359
- retry: publicProcedure.input(z12.object({ ticketId: z12.string(), fresh: z12.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
9360
- stop: publicProcedure.input(z12.object({ ticketId: z12.string() })).mutation(({ ctx, input }) => {
9561
+ retry: publicProcedure.input(z13.object({ ticketId: z13.string(), fresh: z13.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
9562
+ stop: publicProcedure.input(z13.object({ ticketId: z13.string() })).mutation(({ ctx, input }) => {
9361
9563
  if (stopTicketRun(input.ticketId))
9362
9564
  return { stopped: true, swept: false };
9363
9565
  const ticket = getTicket(ctx, input.ticketId);
@@ -9367,13 +9569,13 @@ var init_ticket = __esm(() => {
9367
9569
  }
9368
9570
  return { stopped: false, swept: false };
9369
9571
  }),
9370
- cancel: publicProcedure.input(z12.object({ ticketId: z12.string(), reason: z12.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
9371
- edit: publicProcedure.input(z12.object({
9372
- ticketId: z12.string(),
9373
- title: z12.string().min(1).optional(),
9374
- goal: z12.string().min(1).optional(),
9375
- context: z12.string().optional(),
9376
- acceptanceCriteria: z12.array(z12.string().min(1)).optional()
9572
+ cancel: publicProcedure.input(z13.object({ ticketId: z13.string(), reason: z13.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
9573
+ edit: publicProcedure.input(z13.object({
9574
+ ticketId: z13.string(),
9575
+ title: z13.string().min(1).optional(),
9576
+ goal: z13.string().min(1).optional(),
9577
+ context: z13.string().optional(),
9578
+ acceptanceCriteria: z13.array(z13.string().min(1)).optional()
9377
9579
  })).mutation(({ ctx, input }) => {
9378
9580
  const { ticketId, ...patch } = input;
9379
9581
  return editTicket(ctx, ticketId, patch);
@@ -9393,12 +9595,14 @@ var init_router = __esm(() => {
9393
9595
  init_settings2();
9394
9596
  init_setup2();
9395
9597
  init_system();
9598
+ init_test_notes2();
9396
9599
  init_ticket();
9397
9600
  appRouter = router({
9398
9601
  project: projectRouter,
9399
9602
  feature: featureRouter,
9400
9603
  run: runRouter,
9401
9604
  ticket: ticketRouter,
9605
+ notes: testNotesRouter,
9402
9606
  events: eventsRouter,
9403
9607
  docs: docsRouter,
9404
9608
  settings: settingsRouter,
@@ -9408,14 +9612,14 @@ var init_router = __esm(() => {
9408
9612
  });
9409
9613
 
9410
9614
  // src/workflows/reconcile-runs.ts
9411
- import { eq as eq14 } from "drizzle-orm";
9615
+ import { eq as eq15 } from "drizzle-orm";
9412
9616
  async function reconcileStaleRuns(ctx) {
9413
- const stale = ctx.db.select().from(runs).where(eq14(runs.status, "running")).all().map(rowToRun);
9617
+ const stale = ctx.db.select().from(runs).where(eq15(runs.status, "running")).all().map(rowToRun);
9414
9618
  const reconciled = [];
9415
9619
  for (const run3 of stale) {
9416
9620
  if (isRunActive(run3.id))
9417
9621
  continue;
9418
- ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq14(runs.id, run3.id)).run();
9622
+ ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq15(runs.id, run3.id)).run();
9419
9623
  const released = releaseForSession(ctx, run3.id);
9420
9624
  const swept = workflowClaimsFeatureBranch(run3.workflow) ? sweepOrphanedBurning(ctx, run3.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
9421
9625
  if (workflowClaimsFeatureBranch(run3.workflow)) {
@@ -9645,7 +9849,7 @@ var init_cli = __esm(() => {
9645
9849
  // src/bin/runcastle.ts
9646
9850
  init_asset_paths();
9647
9851
  init_version();
9648
- import { dirname as dirname10, resolve as resolve11 } from "path";
9852
+ import { dirname as dirname11, resolve as resolve11 } from "path";
9649
9853
  import { fileURLToPath as fileURLToPath8 } from "url";
9650
9854
  function parseCommand(argv) {
9651
9855
  const [first, ...rest] = argv;
@@ -9670,7 +9874,7 @@ Usage:
9670
9874
  runcastle --help Show this help
9671
9875
  `;
9672
9876
  async function main(argv) {
9673
- applyInstalledAssetEnv(resolve11(dirname10(fileURLToPath8(import.meta.url)), ".."));
9877
+ applyInstalledAssetEnv(resolve11(dirname11(fileURLToPath8(import.meta.url)), ".."));
9674
9878
  const { command, args } = parseCommand(argv);
9675
9879
  switch (command) {
9676
9880
  case "version":
@@ -0,0 +1,10 @@
1
+ CREATE TABLE `test_notes` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `feature_id` text NOT NULL,
4
+ `lap` integer NOT NULL,
5
+ `text` text NOT NULL,
6
+ `status` text NOT NULL,
7
+ `ticket_id` text,
8
+ `created_at` integer NOT NULL,
9
+ `updated_at` integer NOT NULL
10
+ );