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