@dev-loops/core 0.9.0 → 1.0.0-rc.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.
@@ -2,7 +2,7 @@
2
2
  * Teardown + side-effect ledger orchestrator for the ui_review route (Stage 5).
3
3
  *
4
4
  * Terminal cleanup for a running-app review: stop the app booted in Stage 1,
5
- * drop the dev-DB rows the Stage-2 drive created, and remove the provisioned
5
+ * drop the dev-DB rows the Stage-2 drive tagged, and remove the provisioned
6
6
  * worktree. The core safety property of this stage is that a side-effect ledger
7
7
  * is ALWAYS emitted — enumerating every migration applied, row created/dropped,
8
8
  * the worktree path, and any process left running — so nothing the loop touched
@@ -28,11 +28,12 @@
28
28
  */
29
29
 
30
30
  /**
31
- * The honest row-drop reality: Stage 2 does NOT tag the dev-DB rows it creates
32
- * with a session id or row manifest. So unless an explicit row manifest is
33
- * handed in (and confirmed), this stage CANNOT know which rows to drop and MUST
34
- * NOT guess. When the drive ran mutating flows without a manifest, the ledger
35
- * reports rows "may remain (untagged)" rather than dropping anything.
31
+ * Row-drop model: Stage 2 stamps each mutating step with a drive-session id and
32
+ * emits a session-tagged row manifest. Given that manifest (and confirmation),
33
+ * this stage drops exactly the rows tagged with that session. Only the fallback
34
+ * case — a drive that mutated but handed in no manifest — CANNOT know which rows
35
+ * to drop and MUST NOT guess: the ledger reports rows "may remain (untagged)"
36
+ * rather than dropping anything.
36
37
  */
37
38
 
38
39
  const ROW_STATUS = Object.freeze({
@@ -60,6 +61,13 @@ const PROCESS_STATUS = Object.freeze({
60
61
  SKIPPED: "skipped",
61
62
  });
62
63
 
64
+ const GIST_STATUS = Object.freeze({
65
+ DELETED: "deleted",
66
+ DELETE_FAILED: "delete-failed",
67
+ SKIPPED_UNCONFIRMED: "skipped-unconfirmed",
68
+ NONE: "none",
69
+ });
70
+
63
71
  /**
64
72
  * Did the Stage-2 drive potentially create dev-DB rows? Without row tagging this
65
73
  * is a coarse but honest signal: a drive that actually walked steps exercised
@@ -82,8 +90,12 @@ function driveMayHaveCreatedRows(driveResult) {
82
90
  * signal (whether the drive walked mutating steps). Null when no drive ran.
83
91
  * @param {Array<object>|null} [input.rowManifest] - Explicit rows to drop, when
84
92
  * a session tag/manifest is available. Absent/empty => untagged fallback.
93
+ * @param {{id?:string|null,url?:string|null}|null} [input.gist] - The Stage-4
94
+ * GitHub-native hosting artifact (a secret gist) to prune, when one was
95
+ * published off-Claude. Absent => nothing to prune.
85
96
  * @param {boolean} [input.confirm] - Explicit authorization for the destructive
86
- * steps (row drop, worktree removal). Fail-safe: absent means NOT confirmed.
97
+ * steps (row drop, worktree removal, gist deletion). Fail-safe: absent means
98
+ * NOT confirmed.
87
99
  * @param {boolean} [input.stopApp] - Stop the Stage-1 app (default true). This is
88
100
  * a clean shutdown, NOT gated on confirmation.
89
101
  * @param {object} seams
@@ -92,12 +104,14 @@ function driveMayHaveCreatedRows(driveResult) {
92
104
  * signalling is unsupported) — mapped to MAY_BE_RUNNING (non-fatal), not KILL_FAILED.
93
105
  * @param {(a:{rows:Array<object>})=>Promise<{ok:boolean,dropped:number,detail:string}>} seams.dropRows
94
106
  * @param {(a:{worktreePath:string})=>Promise<{removed:string|null,ok:boolean,detail:string}>} seams.removeWorktree
107
+ * @param {(a:{id:string})=>Promise<{ok:boolean,detail:string}>} [seams.deleteGist] - Prune the
108
+ * Stage-4 hosting gist. Only invoked when a gist id is present AND confirmed.
95
109
  * @param {(msg:string)=>void} [seams.log]
96
110
  * @returns {Promise<{ok:boolean,confirmed:boolean,ledger:object,errors:string[],logs:string[]}>}
97
111
  */
98
112
  export async function teardown(
99
- { provisionResult, driveResult = null, rowManifest = null, confirm = false, stopApp = true },
100
- { killProcess, dropRows, removeWorktree, log = () => {} } = {},
113
+ { provisionResult, driveResult = null, rowManifest = null, gist = null, confirm = false, stopApp = true },
114
+ { killProcess, dropRows, removeWorktree, deleteGist, log = () => {} } = {},
101
115
  ) {
102
116
  const logs = [];
103
117
  const errors = [];
@@ -229,6 +243,33 @@ export async function teardown(
229
243
  }
230
244
  }
231
245
 
246
+ // 4. Prune the Stage-4 hosting gist — DESTRUCTIVE, confirmation-gated. A gist
247
+ // accretes one secret entry per run; deleting it keeps the hosting target
248
+ // from piling up. Only ever acts on an explicit gist id from the report
249
+ // result; a missing id is NONE (nothing published, or Claude-hosted).
250
+ const gistId = typeof gist?.id === "string" && gist.id.trim().length > 0 ? gist.id.trim() : null;
251
+ let gistLedger;
252
+ if (!gistId) {
253
+ gistLedger = { id: null, url: gist?.url ?? null, deleted: false, status: GIST_STATUS.NONE, detail: "no hosting gist to prune" };
254
+ } else if (!confirm) {
255
+ gistLedger = { id: gistId, url: gist?.url ?? null, deleted: false, status: GIST_STATUS.SKIPPED_UNCONFIRMED, detail: "hosting gist retained: teardown not confirmed" };
256
+ record(`gist prune skipped (not confirmed): ${gistId} retained`);
257
+ } else {
258
+ try {
259
+ const del = await deleteGist({ id: gistId });
260
+ if (del.ok) {
261
+ gistLedger = { id: gistId, url: gist?.url ?? null, deleted: true, status: GIST_STATUS.DELETED, detail: del.detail };
262
+ record(`hosting gist deleted: ${gistId} (${del.detail})`);
263
+ } else {
264
+ gistLedger = { id: gistId, url: gist?.url ?? null, deleted: false, status: GIST_STATUS.DELETE_FAILED, detail: del.detail };
265
+ fail(`hosting gist delete FAILED: ${gistId} (${del.detail})`);
266
+ }
267
+ } catch (err) {
268
+ gistLedger = { id: gistId, url: gist?.url ?? null, deleted: false, status: GIST_STATUS.DELETE_FAILED, detail: `deleteGist seam threw: ${err?.message ?? err}` };
269
+ fail(`hosting gist delete FAILED: ${gistId} (deleteGist seam threw: ${err?.message ?? err})`);
270
+ }
271
+ }
272
+
232
273
  // The ledger is ALWAYS emitted (every case), enumerating every known side
233
274
  // effect. Migrations are recorded as applied-not-reverted by design.
234
275
  const ledger = {
@@ -241,10 +282,11 @@ export async function teardown(
241
282
  },
242
283
  rows: rowsLedger,
243
284
  worktree: worktreeLedger,
285
+ gist: gistLedger,
244
286
  process: processLedger,
245
287
  };
246
288
 
247
289
  return { ok: errors.length === 0, confirmed: confirm, ledger, errors, logs };
248
290
  }
249
291
 
250
- export { ROW_STATUS, WORKTREE_STATUS, PROCESS_STATUS };
292
+ export { ROW_STATUS, WORKTREE_STATUS, PROCESS_STATUS, GIST_STATUS };