@golden-frijoles/kit 0.21.0 → 0.23.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.
@@ -137,6 +137,7 @@ function readEpic(root, slug) {
137
137
  risk: readme.data.risk ?? null,
138
138
  phase: readme.data.phase ?? null,
139
139
  lifecycle: readme.data.status ?? null,
140
+ quote: quoteOf(readme.data),
140
141
  contract: readme.hasFrontmatter && !readme.error && 'phase' in readme.data,
141
142
  sprints,
142
143
  };
@@ -165,9 +166,10 @@ const TYPES_KNOWN = ['feature', 'spike', 'bug', 'chore'];
165
166
 
166
167
  /**
167
168
  * What the branch names, longest reading first: an epic, else a seed (a seed that carries `epic:` is that
168
- * epic — once scaffolded the seed is funnel-only), else null.
169
+ * epic — once scaffolded the seed is funnel-only), else null. Exported for epic-actuals.mjs (finops D16): spend is
170
+ * attributed to an epic by exactly this reading of a branch, never a second one.
169
171
  */
170
- function resolveTarget(root, branch) {
172
+ export function resolveTarget(root, branch) {
171
173
  for (const c of branchCandidates(branch)) {
172
174
  const epic = readEpic(root, c.slug);
173
175
  if (epic) return { kind: 'epic', epic, sprint: c.sprint, match: c.exact ? 'exact' : 'prefix' };
@@ -258,6 +260,94 @@ export function prFromFacts(facts, branch) {
258
260
 
259
261
  const notInFlight = (reason, extra = {}) => ({ in_flight: false, reason, ...extra });
260
262
 
263
+ // ── Spend (finops S1.3, D5) ─────────────────────────────────────────────────────────────────────────────
264
+ // The band only READS: `<main checkout>/.golden-frijoles/usage-summary.json`, written by epic-actuals.mjs (refreshed
265
+ // by the mod off the hot path, and by any CLI run). Nothing here scans a transcript. No summary, or no row for this
266
+ // epic, means no Spend line — never a zero.
267
+ export const USAGE_SUMMARY = ['.golden-frijoles', 'usage-summary.json'];
268
+
269
+ /** The main checkout of `root` (the parent of git's common dir) — where the usage summary lives (finops D19). */
270
+ function mainCheckoutOf(git, root) {
271
+ const common = tryGit(git, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
272
+ if (!common) return root;
273
+ return /[\\/]\.git$/.test(common) ? dirname(common) : root;
274
+ }
275
+
276
+ /** This epic's row of the usage summary, or null. Never throws. */
277
+ export function spendFor(root, git, slug) {
278
+ try {
279
+ const path = join(mainCheckoutOf(git, root), ...USAGE_SUMMARY);
280
+ if (!existsSync(path)) return null;
281
+ const summary = JSON.parse(readFileSync(path, 'utf8'));
282
+ // A first scan that ran out of its time budget is not a total yet: no row until it completes, never a low number.
283
+ if (summary?.complete === false) return null;
284
+ const row = summary?.epics?.[slug];
285
+ if (!row || typeof row.usd !== 'number') return null;
286
+ return {
287
+ usd: row.usd,
288
+ usd_known: row.usd_known !== false,
289
+ mtok: typeof row.mtok === 'number' ? row.mtok : null,
290
+ sessions: Number.isInteger(row.sessions) ? row.sessions : null,
291
+ basis: typeof summary.basis === 'string' ? summary.basis : 'this machine',
292
+ generated_at: summary.generated_at ?? null,
293
+ };
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+
299
+ /** `≈$38` (a lower bound reads `≥$38`: some turns could not be priced — finops D4). */
300
+ export function dollars(usd, known = true) {
301
+ const sign = known ? '≈' : '≥';
302
+ return usd > 0 && usd < 1 ? `${sign}$${usd.toFixed(2)}` : `${sign}$${Math.round(usd)}`;
303
+ }
304
+
305
+ /**
306
+ * The quote's label (finops S2.4): `(M)` for a calibrated quote, `(M · 2 past epics, wide)` while history is thin. Read
307
+ * from the `quote_basis` quote.mjs writes (`M, n=2, wide` / `M, n=5, p25–p75`); an unreadable basis shows as written.
308
+ */
309
+ export function quoteLabel(basis, appetite = null) {
310
+ const m = /^\s*([SML])\s*,\s*n=(\d+)\s*,\s*(wide|p25–p75)\s*$/.exec(String(basis ?? ''));
311
+ if (m)
312
+ return m[3] === 'wide' ? `(${m[1]} · ${m[2]} past epic${m[2] === '1' ? '' : 's'}, wide)` : `(${m[1]})`;
313
+ if (basis) return `(${basis})`;
314
+ return appetite ? `(${appetite})` : '';
315
+ }
316
+
317
+ /**
318
+ * The Spend line's value — the four states of the approved mockup (finops S2.4; the resolver owns the words, D3):
319
+ * inside ≈$38 of quote $30–55 (M) · 1.9M tok · 4 sessions
320
+ * over ≈$71 · 29% over quote $30–55 (M) · 3.4M tok (D8: alert-only)
321
+ * no quote ≈$22 · no quote · 1.1M tok · 2 sessions (never an invented quote)
322
+ * thin ≈$9 of quote $25–90 (M · 2 past epics, wide) · …
323
+ * With no quote object at all (a seed, an old caller) it is Sprint 1's line, which names where it was measured.
324
+ */
325
+ export function spendValue(spend, quote) {
326
+ const tok = spend.mtok !== null ? `${spend.mtok}M tok` : null;
327
+ const sessions =
328
+ spend.sessions !== null ? `${spend.sessions} session${spend.sessions === 1 ? '' : 's'}` : null;
329
+ const usd = dollars(spend.usd, spend.usd_known);
330
+ if (quote === undefined) return [usd, tok, sessions, spend.basis].filter(Boolean).join(' · ');
331
+ if (!quote) return [usd, 'no quote', tok, sessions].filter(Boolean).join(' · ');
332
+ const label = quoteLabel(quote.basis, quote.appetite);
333
+ const range = `$${quote.low}–${quote.high}${label ? ` ${label}` : ''}`;
334
+ if (spend.usd > quote.high) {
335
+ const pct = quote.high > 0 ? Math.round(((spend.usd - quote.high) / quote.high) * 100) : null;
336
+ // `<1% over`, never `0% over`: the comparison is on the raw dollars, so a rounded 0 would read as "not over".
337
+ const over = pct === null ? 'over' : pct < 1 ? '<1% over' : `${pct}% over`;
338
+ return [usd, `${over} quote ${range}`, tok].filter(Boolean).join(' · ');
339
+ }
340
+ return [`${usd} of quote ${range}`, tok, sessions].filter(Boolean).join(' · ');
341
+ }
342
+
343
+ /** The epic's quote off its README frontmatter, or null (an unquoted epic — never a $0 quote, D4). */
344
+ export function quoteOf(readme) {
345
+ const lo = readme.quote_low_usd;
346
+ const hi = readme.quote_high_usd;
347
+ if (typeof lo !== 'number' || typeof hi !== 'number' || lo > hi) return null;
348
+ return { low: lo, high: hi, basis: readme.quote_basis ?? null, appetite: readme.appetite ?? null };
349
+ }
350
+
261
351
  /** Does `text` name `slug` as a whole slug — `aws` in "aws S1.1", but not inside "aws-s3 S1.1"? */
262
352
  export function namesSlug(text, slug) {
263
353
  const esc = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -596,6 +686,8 @@ function resolve_({ root, offline = false, git = makeGit(root), facts = null, gh
596
686
  kind: 'epic',
597
687
  branch,
598
688
  branch_match: target.match,
689
+ spend: spendFor(root, git, epic.slug),
690
+ quote: epic.quote,
599
691
  lifecycle: epic.lifecycle,
600
692
  epic: {
601
693
  slug: epic.slug,
@@ -744,6 +836,7 @@ export function renderLines(state) {
744
836
  const storyPart = `Story ${progress.story ?? '?'} of ${progress.stories}`;
745
837
  const sprintPart = `Sprint ${progress.sprint ?? '?'} of ${progress.sprints}`;
746
838
  lines.push(`${pad('Progress')}${storyPart} · ${sprintPart}`);
839
+ if (state.spend) lines.push(`${pad('Spend')}${spendValue(state.spend, state.quote ?? null)}`);
747
840
  lines.push(`${pad('Status')}${statusValue(state)}`);
748
841
  return [...lines, ...also, ...boardLines(state, pad)];
749
842
  }