lastlight-shared 0.4.0 → 0.6.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.
@@ -132,13 +132,290 @@ export declare function isReviewTrigger(value: unknown): value is ReviewTrigger;
132
132
  /** Position of a trigger mode on the automation scale. */
133
133
  export declare function reviewTriggerRank(trigger: ReviewTrigger): number;
134
134
  /**
135
- * The `review:` block. Every key is repo-settable, and every one is CLAMPED
136
- * towards less automation: `postsCheck` and `skipDraft` are add-only `true` (a
137
- * repo may ask for the check and may skip drafts; it may not suppress an
138
- * operator's check or force reviews onto drafts), `trigger` takes the lower
139
- * {@link reviewTriggerRank} of repo and operator, `generatedPaths` is
140
- * superset-only (a longer list suppresses MORE re-reviews), and `requestLabel`
141
- * is free — naming a label only ever adds an explicit, human-initiated route.
135
+ * The `review.analysis:` sub-block — the evidence pipeline
136
+ * (`docs/plans/deterministic-pr-levers.md`).
137
+ *
138
+ * **OFF by default, and that is a locked decision** (README locked decision 8):
139
+ * `enabled: false` must reproduce today's two-phase review byte-for-byte, so
140
+ * every projection this block governs is *absent* from the template context
141
+ * rather than present-and-empty. Each stage lands dark and is switched on per
142
+ * deployment once it has been measured.
143
+ *
144
+ * **Operator-only, unlike every other `review:` leaf.** It buys analysis on the
145
+ * operator's budget, which is the same argument that made `review.trigger`
146
+ * clamped rather than free (#256) — except there is no "more conservative"
147
+ * direction here to clamp towards, so a repo asking for it answers
148
+ * `key-not-allowed` exactly as `fix.escalateModelAfterAttempt` does.
149
+ */
150
+ export interface ReviewAnalysisConfig {
151
+ /** `false` ⇒ today's two-phase review, byte-for-byte. */
152
+ enabled: boolean;
153
+ /**
154
+ * How many `spec` obligations one PR may carry.
155
+ *
156
+ * A **safety bound**, not a budget — it should never bind on a real PR. The
157
+ * extractor still ranks, truncates, and records in the rendered block how
158
+ * many it dropped (a silently truncated list is the failure locked decision 6
159
+ * exists to prevent); this number exists only to stop a pathological PR
160
+ * blowing the prompt.
161
+ *
162
+ * It shipped at 6, which was inert while the spec axis produced nothing. The
163
+ * moment the axis started working it bound on FIVE of the six linked cases in
164
+ * the gate set, discarding acceptance criteria a human wrote on the issue —
165
+ * the most direct statement of intent this pipeline ever gets. Capping
166
+ * GENERATION also inverts locked decision 2: we over-generate deliberately and
167
+ * let the probe oracle and WP6b's attention boundary narrow. Truncating
168
+ * obligations truncates DISCOVERY, which is the measured ceiling.
169
+ */
170
+ maxSpecObligations: number;
171
+ /**
172
+ * A TOTAL BACKSTOP over the **facts-derived** obligations one PR may carry,
173
+ * across all five families `lastlight-facts seed` produces (`contract`,
174
+ * `enforcement`, `security`, `state`, `tests`). The `spec` family has its own
175
+ * bound above, because it is built harness-side from the issue text and
176
+ * shares no ranking axis with these.
177
+ *
178
+ * **Truncation is per FAMILY, not here.** The seeder caps each family at its
179
+ * own ceiling — `contract` 12, `enforcement` 12, `state` 8, `security` 8,
180
+ * `tests` 8 (`FAMILY_CAPS` in `packages/code-facts/src/seed.ts`) — because
181
+ * each family's obligations feed exactly ONE survey branch, so the cost is
182
+ * per branch rather than per document, and cross-family ranking prices
183
+ * incommensurable mechanism classes against each other. Measured: `contract`
184
+ * minted 89 across the eight gate cases while `security` minted 3, and the
185
+ * pooled budget went to `contract`.
186
+ *
187
+ * This number is applied AFTER those ceilings and defaults to their sum, so
188
+ * it cannot bind on a shipped configuration — it is there so that raising one
189
+ * ceiling is a bounded act. What it drops is counted in `obligations.json`
190
+ * with the reason (naming the ceiling or the backstop), never silently.
191
+ */
192
+ maxObligations: number;
193
+ /**
194
+ * Which obligation BLOCK the six survey families are handed — the CONTROL for
195
+ * 2026-08-23, and the only key in this block that exists to make a result
196
+ * readable rather than to buy compute.
197
+ *
198
+ * `full` (the default) is that day's block: a mandatory discharge contract
199
+ * with a `discharge` field to record a code in, an un-truncated id checklist,
200
+ * and one worked exemplar. It moved discharge compliance 0/33 → 33/33 on
201
+ * `prreview__skillspro-1587-r2` — and moved the union of matched gold
202
+ * **4-of-5 → 0-of-5**, over three repeats, with half to two thirds of every
203
+ * hypothesis becoming a clean quote (`QUOTE`, `failureScenario: null`).
204
+ *
205
+ * Two variables changed in the same commit, so the run cannot say which:
206
+ * whether the obligations ask the WRONG QUESTION and making a wrong question
207
+ * mandatory turns hunting into checklist-clearing, or whether RELIABLE SEEDING
208
+ * itself suppresses discovery (the same commit stopped ~24% of survey branches
209
+ * losing their seed entirely). `minimal` renders the pre-2026-08-23 block —
210
+ * same obligations, delivered just as reliably, asking the old question — so
211
+ * one arm separates them.
212
+ *
213
+ * It reaches the five facts-derived families as `lastlight-facts seed
214
+ * --contract`, is stamped into `obligations.json`, and the `spec` family reads
215
+ * it directly (`renderSpecObligations`) because it is rendered harness-side.
216
+ * **`lastlight-facts discharge` degrades to its `test -s` floor under
217
+ * `minimal`**: measured compliance under that block was 0/31, 0/34 and 0/40,
218
+ * so a gate demanding a code the block never asked for would fail every family
219
+ * of every run.
220
+ */
221
+ obligationContract: "full" | "minimal";
222
+ /**
223
+ * Which D2 minting arms `lastlight-facts seed` runs, as a comma-list over
224
+ * `all-in-diff` (contract obligations for symbols whose every reference is
225
+ * inside the diff) and `registrations` (security obligations for route/hook
226
+ * registration order).
227
+ *
228
+ * BOTH ON by default — the measured shipped shape (8-case confirm: internal
229
+ * paired +10/−1, p=0.006, the only lever measured to GROW the recall union
230
+ * rather than rotate it; external validation +7/−0, p=0.008). `""` is
231
+ * NEITHER — the pre-D2 baseline set, byte-identical to a run before the
232
+ * toggle existed. Reaches the seeder as `--mint <spec>`
233
+ * on the seed phase's command line, appended ONLY when non-empty, and the
234
+ * seeder stamps what it was asked into `obligations.json` (`minting`) so an
235
+ * artifact answers "which arm produced this". Kept a plain string rather
236
+ * than a validated union because the CLI is the loud gate: any unknown token
237
+ * exits 2 before the document is read — a typo'd arm can never silently run
238
+ * baseline and report a number for an experiment that never happened.
239
+ */
240
+ mint: string;
241
+ /**
242
+ * How many of the six survey families actually run.
243
+ *
244
+ * **Six, and the default is not negotiable down without saying which.** The
245
+ * previous design defaulted this to 3 against six families and never recorded
246
+ * which three ran — so half the families silently never executed, and
247
+ * `enforcement`, the one that produced the only gold match, could have been
248
+ * among them (§D4). A value below 6 takes the families in the seeder's rank
249
+ * order and the run says so in its artifact.
250
+ */
251
+ surveyPasses: number;
252
+ /**
253
+ * How many survey families run CONCURRENTLY (WP11c).
254
+ *
255
+ * A CEILING, not a guarantee: the run clamps it to what the active sandbox
256
+ * backend can actually hold. `none` and `docker` take the declared value;
257
+ * `gondolin` boots a QEMU micro-VM per agent session inside the harness
258
+ * process and pins to 1, as do `smol` and `kubernetes` until measured. So on
259
+ * a stock deployment (gondolin) this key changes nothing at all today.
260
+ *
261
+ * Six by default because six is what the fan-out exists for. The six families
262
+ * write six disjoint append-only files and never read each other's, so there
263
+ * was never an ordering constraint between them — only a scheduler that ran
264
+ * one DAG node at a time. Chained, they were 851s of a 29-minute review (49%
265
+ * of the wall clock); concurrent, they are the slowest single family.
266
+ *
267
+ * Lower it to bound provider rate-limit pressure or memory, not to bound
268
+ * spend: the six passes cost the same in tokens either way.
269
+ */
270
+ surveyConcurrency: number;
271
+ /**
272
+ * WP4 — the `prepare` + `falsify` pair: install dependencies so a probe can be
273
+ * **run**, then write probes and run them.
274
+ *
275
+ * A second switch under an already-gated block, deliberately. `prepare` is the
276
+ * phase that decides whether the review workspace has a `node_modules`, and
277
+ * that one fact changes three things at once: it is what makes a
278
+ * package-extending `tsconfig` resolve (so `contract` can seed at all on a
279
+ * normal monorepo), it is the only route to a coverage artifact (so `tests`
280
+ * can), and it re-arms the memory profile of a *different* phase. Bundling it
281
+ * into `enabled` would have made "run the surveys" and "install the PR
282
+ * author's dependencies" the same decision.
283
+ *
284
+ * `false` reproduces WP3 exactly. Default posture is **off in production, on
285
+ * in the eval overlay** — the ablation rung is what decides whether it ships
286
+ * on, and that is a number, not a judgement call.
287
+ */
288
+ probes: boolean;
289
+ /**
290
+ * Let `prepare`'s install run the tree's own lifecycle scripts.
291
+ *
292
+ * **Off, and it is a security default rather than a performance one.** The
293
+ * install runs against a PULL REQUEST HEAD, so a `postinstall` there is code
294
+ * the PR author wrote executing on the operator's infrastructure — and
295
+ * `pr-review`'s workspace has never installed anything, which makes `prepare`
296
+ * the first thing in the workflow that could. What `prepare` is FOR (making an
297
+ * `extends` resolve, putting library source on disk to be read) needs the
298
+ * files, not their scripts.
299
+ *
300
+ * Turning it on is legitimate for a repo whose install genuinely does not work
301
+ * without them; it is not the default for the same reason `probes` is not.
302
+ */
303
+ probeLifecycleScripts: boolean;
304
+ /**
305
+ * Run the repo's own `tsc --noEmit` in `prepare` and record the diagnostics.
306
+ *
307
+ * Cheap, independent of the other two, and **not** a CI re-run: CI reports a
308
+ * pass/fail summary over a matrix, this reports a per-file, per-line
309
+ * diagnostic that can be attached to a specific hypothesis (locked decision
310
+ * 11 — we never re-derive what `checksState` already said).
311
+ */
312
+ probeTypecheck: boolean;
313
+ /**
314
+ * Run a coverage command in `prepare` so the `tests` obligation family has an
315
+ * input for the first time.
316
+ *
317
+ * **The one step in this pipeline that runs a test suite**, which is the
318
+ * wall-clock item §D13 deleted along with `mutants` and `suite`. It is a
319
+ * separate switch because it is a separate price: everything else in `prepare`
320
+ * is seconds and this is minutes. It never guesses a command — only one the
321
+ * repo itself named (a `coverage` / `test:coverage` script) — because a
322
+ * guessed fifteen-minute run that produced nothing makes "no command" and "no
323
+ * artifact" the same row in the funnel.
324
+ */
325
+ probeCoverage: boolean;
326
+ /** Ceiling on `prepare`'s dependency install, in seconds. */
327
+ prepareTimeoutSeconds: number;
328
+ /** Ceiling on `prepare`'s coverage run, in seconds. Minutes, not seconds. */
329
+ coverageTimeoutSeconds: number;
330
+ /**
331
+ * How many rounds `falsify` gets to write and run probes.
332
+ *
333
+ * Two. v3's lesson 3 is the sizing argument: the loop's exit condition is a
334
+ * five-line existence gate, not a validator — v2's full quote validator was
335
+ * overkill and cost 2.4× for a worse result.
336
+ */
337
+ probeRounds: number;
338
+ /**
339
+ * The inline-comment attention budget (WP6b).
340
+ *
341
+ * Preserving internal recall and spending a human's attention are two
342
+ * different budgets, and conflating them is how a recall-first reviewer
343
+ * becomes unreadable. Everything past this rank goes to the review BODY —
344
+ * still posted, still visible, just not an inline comment. Nothing is dropped.
345
+ *
346
+ * Ten, on the evidence in *"Does AI Code Review Lead to Code Changes?"*
347
+ * (22k+ real review comments): concise, hunk-level, actionable findings are
348
+ * substantially likelier to lead to a change, and the wall the paper warns
349
+ * about is TWENTY — twenty inline comments is not twice the signal of ten,
350
+ * it is a muted bot. Ten is a ceiling, not a budget that bites: measured
351
+ * inline volume is 1–5 per PR, so this has never bound, and anything past it
352
+ * goes to the body rather than away.
353
+ */
354
+ maxInlineComments: number;
355
+ /**
356
+ * Per-obligation-family confidence bar for an INLINE comment. Below the bar a
357
+ * finding goes to the body; it is never deleted.
358
+ *
359
+ * **Per-family, not global, and that is a measured choice.** AutoCommenter
360
+ * (Google Critique) found a global threshold catastrophic — at `t = 0.98`,
361
+ * ~80% of below-threshold predictions were still correct — while per-URL
362
+ * thresholds raised recall without hurting precision.
363
+ *
364
+ * **These numbers are initial guesses to be tuned on the train split, not
365
+ * measurements.** Record each retune in the eval journal.
366
+ */
367
+ thresholds: Record<string, number>;
368
+ /**
369
+ * Below this confidence a finding is recorded but not posted at all.
370
+ *
371
+ * The one tier that costs recall, so it is deliberately low and deliberately
372
+ * auditable. A finding carrying NO confidence is never affected — see
373
+ * `tierFindings`; treating an absent field as zero would silently delete every
374
+ * finding from any prompt that has not been taught to self-score.
375
+ */
376
+ internalFloor: number;
377
+ /**
378
+ * Cap on findings rendered into the review BODY (the "Additional findings"
379
+ * section) — the body-side sibling of `maxInlineComments`, and the one
380
+ * budget that DOES filter: everything past it is recorded `internal` with
381
+ * the machine reason `body-budget` in `disposition.json`, never posted.
382
+ * Nothing is deleted — the demotion stays auditable like every other
383
+ * `internal` entry.
384
+ *
385
+ * - `null` — unlimited: the legacy funnel, where everything demoted from
386
+ * inline lands in the body.
387
+ * - `0` — no overflow at all: nothing tiers to body; anything that would
388
+ * have gone there is recorded `internal` instead.
389
+ * - `N > 0` — at most N body findings, ranked by severity × confidence
390
+ * exactly as the inline overflow ranks (an absent confidence ranks as
391
+ * 1.0, so an unscored document degenerates to severity order).
392
+ *
393
+ * **`5` is the shipped default, and the number it replaced is the reason.**
394
+ * `0` was measured rather than assumed: under the production
395
+ * Sonnet-adjudicator shape no-overflow keeps 29/36 matched gold and lifts
396
+ * precision 0.263 → 0.492 / F1 0.362 → 0.479 on the Martian external set.
397
+ * But the same $0 sweep showed that result is
398
+ * **adjudicator-shape-conditional** — under Haiku-everywhere the body tier
399
+ * carries most of the matched gold and cap 0 costs posted recall 0.42 →
400
+ * 0.12, while MID caps keep nearly all of it (skillspro cap 4: 0.300, cap 8:
401
+ * 0.380 against 0.420 unlimited; Martian cap 4: 0.548, cap 8: 0.581 against
402
+ * 0.581 unlimited, at slightly better precision). `5` is the
403
+ * recall-preserving compromise pending a real boundary tune, not a measured
404
+ * optimum — which is also why eval overlays keep pinning this key
405
+ * explicitly instead of inheriting whatever it currently is.
406
+ */
407
+ maxBodyComments: number | null;
408
+ }
409
+ /**
410
+ * The `review:` block. Every key except `analysis` is repo-settable, and every
411
+ * one of those is CLAMPED towards less automation: `postsCheck` and `skipDraft`
412
+ * are add-only `true` (a repo may ask for the check and may skip drafts; it may
413
+ * not suppress an operator's check or force reviews onto drafts), `trigger`
414
+ * takes the lower {@link reviewTriggerRank} of repo and operator,
415
+ * `generatedPaths` is superset-only (a longer list suppresses MORE
416
+ * re-reviews), and `requestLabel` is free — naming a label only ever adds an
417
+ * explicit, human-initiated route. {@link ReviewAnalysisConfig} is
418
+ * operator-only; see its own doc.
142
419
  */
143
420
  export interface ReviewConfig {
144
421
  /** Post the `last-light/review` Check Run. */
@@ -168,6 +445,8 @@ export interface ReviewConfig {
168
445
  * that also touched a hand-written file — see `resolveReviewTrigger`.
169
446
  */
170
447
  generatedPaths: string[];
448
+ /** The evidence pipeline. Off by default — see {@link ReviewAnalysisConfig}. */
449
+ analysis: ReviewAnalysisConfig;
171
450
  }
172
451
  /**
173
452
  * Where a repo's outbound notifications go — today, the weekly Slack digest.
@@ -141,6 +141,52 @@ export function defaultReviewConfig() {
141
141
  "*.generated.*",
142
142
  "**/__generated__/**",
143
143
  ],
144
+ analysis: {
145
+ enabled: false,
146
+ // A safety bound, not a budget — see config/default.yaml for why this is
147
+ // 40 rather than the 6 it shipped with. It must not bind on a real PR:
148
+ // capping generation truncates discovery, which is the measured ceiling.
149
+ maxSpecObligations: 40,
150
+ // The TOTAL backstop, and it is the per-family ceilings' sum
151
+ // (12 + 12 + 8 + 8 + 8) so it cannot bind unless an operator raises one.
152
+ maxObligations: 48,
153
+ // `minimal` ships, measured in: under `full`, half to two-thirds of
154
+ // survey output arrives as clean-quote verification reports that reached
155
+ // real PRs as posted findings; under `minimal` the same recall union
156
+ // posts with 37–71% better SNR and half the run-to-run variance. `full`
157
+ // remains the opt-in telemetry arm (discharge codes + the
158
+ // clean-discharge demotion at the posting boundary).
159
+ obligationContract: "minimal",
160
+ // Both D2 rules — the measured shipped shape. See the field's doc.
161
+ mint: "all-in-diff,registrations",
162
+ surveyPasses: 6,
163
+ surveyConcurrency: 6,
164
+ probes: false,
165
+ probeLifecycleScripts: false,
166
+ probeTypecheck: false,
167
+ probeCoverage: false,
168
+ prepareTimeoutSeconds: 300,
169
+ coverageTimeoutSeconds: 900,
170
+ probeRounds: 2,
171
+ maxInlineComments: 10,
172
+ thresholds: {
173
+ contract: 0.35,
174
+ enforcement: 0.35,
175
+ security: 0.3,
176
+ state: 0.5,
177
+ tests: 0.6,
178
+ spec: 0.45,
179
+ },
180
+ internalFloor: 0.15,
181
+ // A bounded body overflow. Cap 0 measured better under the production
182
+ // Sonnet adjudicator (precision 0.263→0.492 / F1 0.362→0.479) but that
183
+ // win is adjudicator-shape-conditional — under Haiku-everywhere the body
184
+ // tier carries most of the matched gold (posted recall 0.42→0.12 at 0),
185
+ // and mid caps kept nearly all of it. 5 is the recall-preserving
186
+ // compromise; `null` restores the legacy unlimited funnel. See the
187
+ // field's doc.
188
+ maxBodyComments: 5,
189
+ },
144
190
  };
145
191
  }
146
192
  //# sourceMappingURL=config-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-types.js","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,8EAA8E;AAC9E,qDAAqD;AACrD,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,cAAc;IACd,cAAc;IACd,OAAO;IACP,iBAAiB;IACjB,iBAAiB;CACT,CAAC;AAIX,6DAA6D;AAC7D,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,iBAAuC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/F,CAAC;AAiDD,qFAAqF;AACrF,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,WAAW,EAAE,CAAC;QACd,eAAe,EAAE,CAAC;QAClB,kBAAkB,EAAE,GAAG;QACvB,yBAAyB,EAAE,CAAC;QAC5B,UAAU,EAAE,GAAG;QACf,iBAAiB,EAAE,CAAC;QACpB,gBAAgB,EAAE,CAAC,cAAc,EAAE,cAAc,CAAC;KACnD,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,4DAA4D;AAC5D,8EAA8E;AAE9E,wEAAwE;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AAInF,oEAAoE;AACpE,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,wBAA8C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACtG,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,MAAwB;IAC3D,OAAO,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAClD,CAAC;AAuBD,2FAA2F;AAC3F,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,kBAAkB,EAAE,QAAQ;QAC5B,oBAAoB,EAAE,IAAI;QAC1B,gBAAgB,EAAE,CAAC;QACnB,YAAY,EAAE,IAAI;KACnB,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,YAAY,CAAU,CAAC;AAIhF,2DAA2D;AAC3D,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,eAAqC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC7F,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,yBAAyB,GAAkC;IAC/D,YAAY,EAAE,CAAC;IACf,cAAc,EAAE,CAAC;IACjB,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,0DAA0D;AAC1D,MAAM,UAAU,iBAAiB,CAAC,OAAsB;IACtD,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAmED,+FAA+F;AAC/F,MAAM,UAAU,0BAA0B;IACxC,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;AACtC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,mBAAmB;IACjC,OAAO;QACL,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,cAAc;QACvB,YAAY,EAAE,IAAI;QAClB,SAAS,EAAE,IAAI;QACf,2EAA2E;QAC3E,0EAA0E;QAC1E,yEAAyE;QACzE,qEAAqE;QACrE,cAAc,EAAE;YACd,QAAQ;YACR,mBAAmB;YACnB,qBAAqB;YACrB,gBAAgB;YAChB,WAAW;YACX,WAAW;YACX,QAAQ;YACR,UAAU;YACV,WAAW;YACX,eAAe;YACf,qBAAqB;SACtB;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config-types.js","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * State-database URL vocabulary — the small pure things both `lastlight-core`
3
+ * and the `lastlight` CLI have to agree about.
4
+ *
5
+ * It lives in `shared` for the same reason `repo-config-schema.ts` does: core
6
+ * needs it at runtime (`StateDb.open()` picks a driver, `/config` redacts a
7
+ * credential) and the CLI needs it offline (the setup wizard validates the URL
8
+ * the operator types and reports the driver it implies), and the CLI may never
9
+ * gain an edge to core. Nothing here imports a database driver — a `pg` import
10
+ * anywhere in this file would put node-postgres in the CLI's dependency graph.
11
+ */
12
+ /** Which Postgres driver carries the `"postgres"` dialect. */
13
+ export type PgDriver = "pg" | "neon";
14
+ export declare const PG_DRIVERS: readonly PgDriver[];
15
+ /** Does this DB URL name Postgres (rather than a libsql `file:` / `:memory:`)? */
16
+ export declare function isPostgresUrl(input: string | undefined | null): boolean;
17
+ export declare function isPgDriver(value: unknown): value is PgDriver;
18
+ /**
19
+ * Which driver to run a `postgres://` URL over.
20
+ *
21
+ * `configured` (from `database.driver` / `DATABASE_DRIVER`) always wins — a Neon
22
+ * database fronted by a custom domain, or node-postgres pointed at Neon's TCP
23
+ * endpoint, both need to be expressible. Unset falls back to the host
24
+ * heuristic, which answers `"neon"` only for `*.neon.tech`.
25
+ *
26
+ * An unparseable URL resolves to `"pg"`: the driver builder is about to fail on
27
+ * it anyway, and node-postgres produces the better message.
28
+ */
29
+ export declare function resolvePgDriver(url: string, configured?: PgDriver | null): PgDriver;
30
+ /**
31
+ * A DB URL safe to log or echo from the dashboard's `/config` view.
32
+ *
33
+ * Masks the userinfo and any `password=` query parameter, keeping the host,
34
+ * port and database name — those are not secrets, and they are the whole reason
35
+ * the provenance view exists. `file:` URLs and `:memory:` pass through
36
+ * untouched; there is nothing in them to leak.
37
+ *
38
+ * Anything that carries a `@` but does not parse is masked WHOLESALE rather
39
+ * than returned — a redactor that fails open is not a redactor.
40
+ */
41
+ export declare function redactDbUrl(url: string): string;
42
+ /** The host of a DB URL, or undefined if it has none / does not parse. */
43
+ export declare function dbUrlHost(url: string): string | undefined;
44
+ /**
45
+ * Host + port for a `postgres://` URL, for a reachability probe that must not
46
+ * import a driver (the setup wizard opens a bare TCP socket).
47
+ *
48
+ * Hand-parsed for the same reason {@link redactDbUrl} is: `new URL()` throws on
49
+ * an unencoded `@` in the password, and a wizard that rejects a working URL
50
+ * because of its punctuation is worse than no check at all.
51
+ */
52
+ export declare function parsePgEndpoint(url: string): {
53
+ host: string;
54
+ port: number;
55
+ } | undefined;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * State-database URL vocabulary — the small pure things both `lastlight-core`
3
+ * and the `lastlight` CLI have to agree about.
4
+ *
5
+ * It lives in `shared` for the same reason `repo-config-schema.ts` does: core
6
+ * needs it at runtime (`StateDb.open()` picks a driver, `/config` redacts a
7
+ * credential) and the CLI needs it offline (the setup wizard validates the URL
8
+ * the operator types and reports the driver it implies), and the CLI may never
9
+ * gain an edge to core. Nothing here imports a database driver — a `pg` import
10
+ * anywhere in this file would put node-postgres in the CLI's dependency graph.
11
+ */
12
+ export const PG_DRIVERS = ["pg", "neon"];
13
+ /** Case-insensitive, because URL schemes are and `POSTGRES://` is a real typo. */
14
+ const POSTGRES_URL_RE = /^postgres(ql)?:\/\//i;
15
+ /** Does this DB URL name Postgres (rather than a libsql `file:` / `:memory:`)? */
16
+ export function isPostgresUrl(input) {
17
+ return !!input && POSTGRES_URL_RE.test(input.trim());
18
+ }
19
+ export function isPgDriver(value) {
20
+ return value === "pg" || value === "neon";
21
+ }
22
+ /**
23
+ * Hosts served by Neon's WebSocket pooler. An explicit `database.driver` always
24
+ * beats this, so the list only has to cover the common case.
25
+ */
26
+ const NEON_HOST_RE = /(^|\.)neon\.tech$/i;
27
+ /**
28
+ * Which driver to run a `postgres://` URL over.
29
+ *
30
+ * `configured` (from `database.driver` / `DATABASE_DRIVER`) always wins — a Neon
31
+ * database fronted by a custom domain, or node-postgres pointed at Neon's TCP
32
+ * endpoint, both need to be expressible. Unset falls back to the host
33
+ * heuristic, which answers `"neon"` only for `*.neon.tech`.
34
+ *
35
+ * An unparseable URL resolves to `"pg"`: the driver builder is about to fail on
36
+ * it anyway, and node-postgres produces the better message.
37
+ */
38
+ export function resolvePgDriver(url, configured) {
39
+ if (isPgDriver(configured))
40
+ return configured;
41
+ return NEON_HOST_RE.test(dbUrlHost(url) ?? "") ? "neon" : "pg";
42
+ }
43
+ /**
44
+ * Splits `scheme://` + userinfo + the rest without `new URL()`, which rejects a
45
+ * password containing an unencoded `@` — exactly the case where getting
46
+ * redaction right matters most. The userinfo group is GREEDY so it runs to the
47
+ * LAST `@` in the authority: a lazy match would leave the tail of such a
48
+ * password in the "safe" remainder.
49
+ */
50
+ const USERINFO_RE = /^([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^/?#]*@)(.*)$/s;
51
+ /**
52
+ * A DB URL safe to log or echo from the dashboard's `/config` view.
53
+ *
54
+ * Masks the userinfo and any `password=` query parameter, keeping the host,
55
+ * port and database name — those are not secrets, and they are the whole reason
56
+ * the provenance view exists. `file:` URLs and `:memory:` pass through
57
+ * untouched; there is nothing in them to leak.
58
+ *
59
+ * Anything that carries a `@` but does not parse is masked WHOLESALE rather
60
+ * than returned — a redactor that fails open is not a redactor.
61
+ */
62
+ export function redactDbUrl(url) {
63
+ const trimmed = url.trim();
64
+ if (!trimmed || trimmed === ":memory:" || /^file:/i.test(trimmed))
65
+ return url;
66
+ const match = USERINFO_RE.exec(trimmed);
67
+ if (!match) {
68
+ // No userinfo at all (`postgres://host/db`) — nothing to mask beyond the
69
+ // query string. A string with an `@` that reached here is unparseable, so
70
+ // it gets the blunt treatment.
71
+ if (trimmed.includes("@"))
72
+ return "[redacted]";
73
+ return maskPasswordParam(trimmed);
74
+ }
75
+ const [, scheme, , rest] = match;
76
+ // Fail CLOSED. A surviving `@` means the authority did not end where the
77
+ // regex thought (an unencoded `/` in the password, say), so what looks like
78
+ // the safe remainder may still be part of the credential.
79
+ if (rest.includes("@"))
80
+ return "[redacted]";
81
+ return `${scheme}***:***@${maskPasswordParam(rest)}`;
82
+ }
83
+ function maskPasswordParam(input) {
84
+ return input.replace(/([?&](?:password|pgpassword)=)[^&]*/gi, "$1***");
85
+ }
86
+ /** The host of a DB URL, or undefined if it has none / does not parse. */
87
+ export function dbUrlHost(url) {
88
+ return parsePgEndpoint(url)?.host;
89
+ }
90
+ /**
91
+ * Host + port for a `postgres://` URL, for a reachability probe that must not
92
+ * import a driver (the setup wizard opens a bare TCP socket).
93
+ *
94
+ * Hand-parsed for the same reason {@link redactDbUrl} is: `new URL()` throws on
95
+ * an unencoded `@` in the password, and a wizard that rejects a working URL
96
+ * because of its punctuation is worse than no check at all.
97
+ */
98
+ export function parsePgEndpoint(url) {
99
+ const trimmed = url.trim();
100
+ const afterScheme = trimmed.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "");
101
+ if (afterScheme === trimmed)
102
+ return undefined; // no scheme → not a URL
103
+ // Authority ends at the first `/`, `?` or `#`; userinfo ends at the LAST `@`
104
+ // inside it, so a password containing `@` does not steal the host.
105
+ const authority = afterScheme.split(/[/?#]/, 1)[0] ?? "";
106
+ const hostPort = authority.slice(authority.lastIndexOf("@") + 1);
107
+ if (!hostPort)
108
+ return undefined;
109
+ // IPv6 literal: `[::1]:5432`.
110
+ const v6 = /^\[([^\]]+)\](?::(\d+))?$/.exec(hostPort);
111
+ if (v6)
112
+ return { host: v6[1], port: v6[2] ? Number(v6[2]) : 5432 };
113
+ const [host, port] = hostPort.split(":");
114
+ if (!host)
115
+ return undefined;
116
+ return { host, port: port && /^\d+$/.test(port) ? Number(port) : 5432 };
117
+ }
118
+ //# sourceMappingURL=database-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-url.js","sourceRoot":"","sources":["../src/database-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAE9D,kFAAkF;AAClF,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAE/C,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAAC,KAAgC;IAC5D,OAAO,CAAC,CAAC,KAAK,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,YAAY,GAAG,oBAAoB,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,UAA4B;IACvE,IAAI,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAC9C,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,iDAAiD,CAAC;AAEtE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,CAAC;IAC9E,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,yEAAyE;QACzE,0EAA0E;QAC1E,+BAA+B;QAC/B,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,YAAY,CAAC;QAC/C,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,CAAC,EAAE,MAAM,EAAE,AAAD,EAAG,IAAI,CAAC,GAAG,KAAK,CAAC;IACjC,yEAAyE;IACzE,4EAA4E;IAC5E,0DAA0D;IAC1D,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IAC5C,OAAO,GAAG,MAAM,WAAW,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;AACvD,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,uCAAuC,EAAE,OAAO,CAAC,CAAC;AACzE,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,eAAe,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,+BAA+B,EAAE,EAAE,CAAC,CAAC;IACzE,IAAI,WAAW,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC,CAAC,wBAAwB;IACvE,6EAA6E;IAC7E,mEAAmE;IACnE,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACzD,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACjE,IAAI,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChC,8BAA8B;IAC9B,MAAM,EAAE,GAAG,2BAA2B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtD,IAAI,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACnE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC1E,CAAC"}
package/dist/index.d.ts CHANGED
@@ -19,5 +19,6 @@ export * from "./overlay-assets.js";
19
19
  export * from "./core-pin.js";
20
20
  export * from "./workflow-loader.js";
21
21
  export * from "./config-types.js";
22
+ export * from "./database-url.js";
22
23
  export * from "./repo-config-schema.js";
23
24
  export * from "./sandbox-services.js";
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export * from "./overlay-assets.js";
19
19
  export * from "./core-pin.js";
20
20
  export * from "./workflow-loader.js";
21
21
  export * from "./config-types.js";
22
+ export * from "./database-url.js";
22
23
  export * from "./repo-config-schema.js";
23
24
  export * from "./sandbox-services.js";
24
25
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC"}