@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.
- package/dist/build-state.mjs +95 -2
- package/dist/epic-actuals.mjs +798 -0
- package/dist/lib/cross-agent-cli.mjs +2 -2
- package/dist/lib/model-prices.mjs +103 -0
- package/dist/lib/roadmap-contract.mjs +57 -4
- package/dist/quote.mjs +136 -0
- package/dist/roadmap-extract.mjs +22 -2
- package/dist/skeleton/Roadmap/WAYS-OF-WORKING.md +2 -0
- package/package.json +1 -1
package/dist/build-state.mjs
CHANGED
|
@@ -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
|
}
|