@golden-frijoles/kit 0.39.0 → 0.40.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 +117 -10
- package/package.json +1 -1
package/dist/build-state.mjs
CHANGED
|
@@ -56,12 +56,12 @@ function repoEnv() {
|
|
|
56
56
|
import { readFileSync, readdirSync, existsSync, statSync, realpathSync } from 'node:fs';
|
|
57
57
|
import { fileURLToPath } from 'node:url';
|
|
58
58
|
import { dirname, join, resolve } from 'node:path';
|
|
59
|
-
import { PHASES, parseDocFrontmatter } from './lib/roadmap-contract.mjs';
|
|
59
|
+
import { PHASES, isDay, parseDocFrontmatter } from './lib/roadmap-contract.mjs';
|
|
60
60
|
import { parseJournal, JOURNAL_BRANCH, JOURNAL_PATH } from './lib/session-journal.mjs';
|
|
61
61
|
import { branchCandidates, parseBranch } from './lib/work-branch.mjs';
|
|
62
62
|
import { buildRows } from './roadmap-extract.mjs';
|
|
63
63
|
import { gatherFacts } from './lib/stage-facts.mjs';
|
|
64
|
-
import { groupByStage } from './lib/stage.mjs';
|
|
64
|
+
import { STAGES, groupByStage } from './lib/stage.mjs';
|
|
65
65
|
import { getKey } from './lib/config.mjs';
|
|
66
66
|
|
|
67
67
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -222,6 +222,7 @@ function readEpic(root, slug) {
|
|
|
222
222
|
locked_at: readme.data.locked_at ?? null, // live-build-view D10 — stamped by scripts/epic-phase.mjs lock
|
|
223
223
|
lifecycle: readme.data.status ?? null,
|
|
224
224
|
quote: quoteOf(readme.data),
|
|
225
|
+
target: targetOf(readme.data),
|
|
225
226
|
contract: readme.hasFrontmatter && !readme.error && 'phase' in readme.data,
|
|
226
227
|
sprints,
|
|
227
228
|
};
|
|
@@ -435,6 +436,25 @@ function spendLine(spend, quote) {
|
|
|
435
436
|
return [`${usd} of quote ${range}`, tok, sessions].filter(Boolean).join(' · ');
|
|
436
437
|
}
|
|
437
438
|
|
|
439
|
+
/**
|
|
440
|
+
* The result record's target off the README frontmatter (build-view-upgrade D1; the fields are result-record's
|
|
441
|
+
* TARGET_FIELDS). A target is the metric with both numbers; a hypothesis on its own is kept, and is not a target.
|
|
442
|
+
*/
|
|
443
|
+
export function targetOf(readme) {
|
|
444
|
+
const text = (v) => (typeof v === 'string' && v.trim() && v.trim() !== 'null' ? v.trim() : null);
|
|
445
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
446
|
+
const metric = text(readme.target_metric);
|
|
447
|
+
const from = num(readme.target_from);
|
|
448
|
+
const to = num(readme.target_to);
|
|
449
|
+
return {
|
|
450
|
+
hypothesis: text(readme.hypothesis),
|
|
451
|
+
metric: metric !== null && from !== null && to !== null ? metric : null,
|
|
452
|
+
from,
|
|
453
|
+
to,
|
|
454
|
+
read_date: isDay(readme.read_date) ? readme.read_date : null,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
438
458
|
/** The epic's quote off its README frontmatter, or null (an unquoted epic — never a $0 quote, D4). */
|
|
439
459
|
export function quoteOf(readme) {
|
|
440
460
|
const lo = readme.quote_low_usd;
|
|
@@ -486,7 +506,8 @@ export function boardState({ root, facts, state, now = new Date() }) {
|
|
|
486
506
|
qa: cols.QA.length,
|
|
487
507
|
ready: cols['Ready to build'].length,
|
|
488
508
|
next: next ? { name: next.name, slug: next.slug, build_order: next.build_order_num ?? null } : null,
|
|
489
|
-
|
|
509
|
+
// build-view-upgrade D4 — the work's own page when the board has a row for it, else the board.
|
|
510
|
+
url: hubUrl ? (slug && row ? `${hubUrl}/epic/${encodeURIComponent(slug)}` : `${hubUrl}/board`) : null,
|
|
490
511
|
},
|
|
491
512
|
};
|
|
492
513
|
}
|
|
@@ -811,8 +832,15 @@ function resolve_({ root, offline = false, git = makeGit(root), facts = null, gh
|
|
|
811
832
|
story_source: storySource,
|
|
812
833
|
story_note: unreadableSprint ? `${unreadableSprint}${storyNote ? ` — ${storyNote}` : ''}` : storyNote,
|
|
813
834
|
warning: unreadableSprint,
|
|
835
|
+
target: epic.target,
|
|
814
836
|
progress: {
|
|
815
837
|
stories_with_commits: withCommits.size,
|
|
838
|
+
// build-view-upgrade D2 — the same measure per sprint, in build order: [done, total] for each.
|
|
839
|
+
by_sprint: epic.sprints.map((sp) => ({
|
|
840
|
+
n: sp.n,
|
|
841
|
+
done: sp.stories.filter((st) => withCommits.has(st.id)).length,
|
|
842
|
+
total: sp.stories.length,
|
|
843
|
+
})),
|
|
816
844
|
story: story ? story.ordinal : null,
|
|
817
845
|
stories: allStories.length,
|
|
818
846
|
sprint: sprint ? epic.sprints.indexOf(sprint) + 1 : null,
|
|
@@ -868,6 +896,12 @@ function elsewhereLines(state, pad) {
|
|
|
868
896
|
*/
|
|
869
897
|
export function statusValue(state) {
|
|
870
898
|
if (!state.stage) return state.status || `unknown${state.warning ? ` — ${state.warning}` : ''}`;
|
|
899
|
+
const { stage, source, age, phase } = stageParts(state);
|
|
900
|
+
return `${stage} · from ${source} (${age})${phase}`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** The stage in flight, where it came from, how old that is and a differing written phase — statusValue's parts. */
|
|
904
|
+
function stageParts(state) {
|
|
871
905
|
const source = String(state.stage_source || '').replace(/ · snapshot@\S+$/, '');
|
|
872
906
|
// live-build-view D11 — the band's refinement of Building, not a new stage (the Hub and the board keep Building): an
|
|
873
907
|
// epic whose branch is live but whose README carries no `locked_at` is still Locking architecture. The lock is a
|
|
@@ -889,7 +923,74 @@ export function statusValue(state) {
|
|
|
889
923
|
: 'docs only, no snapshot yet';
|
|
890
924
|
const phase =
|
|
891
925
|
state.phase_written && state.phase_written !== stage ? ` · phase ${state.phase_written}` : '';
|
|
892
|
-
return
|
|
926
|
+
return { stage, locking, source, age, phase };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// build-view-upgrade D3 — the stage as a track, in lib/stage.mjs's words. `To groom` shows only when it is the stage.
|
|
930
|
+
const TRACK_WORDS = { 'To groom': 'Backlog', 'Ready to build': 'Ready' };
|
|
931
|
+
export const TRACK_MARK = '◉';
|
|
932
|
+
|
|
933
|
+
/** `Grooming ─ Ready ─ ◉ Building ─ QA ─ Shipped` for a stage; `◉ Locking` in Building's place while locking. */
|
|
934
|
+
export function stageTrack(stage, locking = false) {
|
|
935
|
+
return STAGES.filter((s) => s !== 'To groom' || stage === 'To groom')
|
|
936
|
+
.map((s) => {
|
|
937
|
+
const word = s === 'Building' && locking ? 'Locking' : (TRACK_WORDS[s] ?? s);
|
|
938
|
+
return s === stage ? `${TRACK_MARK} ${word}` : word;
|
|
939
|
+
})
|
|
940
|
+
.join(' ─ ');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** The Status line and its continuation (D3): the track, then where the stage came from. No stage → one plain line. */
|
|
944
|
+
function statusLines(state, pad, cont) {
|
|
945
|
+
if (!state.stage || !STAGES.includes(state.stage)) return [`${pad('Status')}${statusValue(state)}`];
|
|
946
|
+
const { locking, source, age, phase } = stageParts(state);
|
|
947
|
+
return [`${pad('Status')}${stageTrack(state.stage, locking)}`, `${cont}from ${source} (${age})${phase}`];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// build-view-upgrade D1 — why we are building it: the result record's target, or "no target set".
|
|
951
|
+
const WIDTH = 80;
|
|
952
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
953
|
+
|
|
954
|
+
/** `2026-12-04` → `4 Dec` (with the year when it is not `now`'s), read in UTC like every result-record day. */
|
|
955
|
+
export function shortDay(day, now = new Date()) {
|
|
956
|
+
if (!isDay(day)) return null;
|
|
957
|
+
const [y, m, d] = day.split('-').map(Number);
|
|
958
|
+
return `${d} ${MONTHS[m - 1]}${y !== now.getUTCFullYear() ? ` ${y}` : ''}`;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Four significant digits, so a small target (0.004 → 0.006) never rounds to 0 (#312 review).
|
|
962
|
+
const fmtNum = (n) => (Number.isInteger(n) ? String(n) : String(Number(n.toPrecision(4))));
|
|
963
|
+
|
|
964
|
+
/** The Why line and, with a target, its continuation — each clipped to fit 80 columns. */
|
|
965
|
+
export function whyLines(target, pad, cont, now = new Date()) {
|
|
966
|
+
const room = WIDTH - cont.length;
|
|
967
|
+
const t = target || {};
|
|
968
|
+
if (!t.metric) {
|
|
969
|
+
if (!t.hypothesis) return [`${pad('Why')}no target set`];
|
|
970
|
+
return [`${pad('Why')}${clip(t.hypothesis, room)}`, `${cont}no target set`];
|
|
971
|
+
}
|
|
972
|
+
const read = t.read_date ? `read ${shortDay(t.read_date, now)}` : 'read 30 days after shipping';
|
|
973
|
+
const numbers = ` ${fmtNum(t.from)} ━━▸ ${fmtNum(t.to)} · ${read}`;
|
|
974
|
+
const metric = `${clip(t.metric, Math.max(8, room - numbers.length))}${numbers}`;
|
|
975
|
+
return t.hypothesis
|
|
976
|
+
? [`${pad('Why')}${clip(t.hypothesis, room)}`, `${cont}${clip(metric, room)}`]
|
|
977
|
+
: [`${pad('Why')}${clip(metric, room)}`];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// build-view-upgrade D2 — one cell per story, sprint by sprint: ▰ has a commit, ▱ not yet, │ between sprints.
|
|
981
|
+
export const SPRINT_CELLS_MAX = 8;
|
|
982
|
+
|
|
983
|
+
/** `[{done:2,total:3},{done:0,total:4}]` → `▰▰▱│▱▱▱▱`. A sprint wider than SPRINT_CELLS_MAX is scaled to it. */
|
|
984
|
+
export function sprintBars(bySprint) {
|
|
985
|
+
return (bySprint || [])
|
|
986
|
+
.filter((sp) => sp.total > 0)
|
|
987
|
+
.map((sp) => {
|
|
988
|
+
const width = Math.min(sp.total, SPRINT_CELLS_MAX);
|
|
989
|
+
// Rounded down, so a scaled sprint is never drawn full before every story has a commit (#312 review).
|
|
990
|
+
const filled = sp.done >= sp.total ? width : Math.max(0, Math.min(width - 1, Math.floor((sp.done / sp.total) * width)));
|
|
991
|
+
return '▰'.repeat(filled) + '▱'.repeat(width - filled);
|
|
992
|
+
})
|
|
993
|
+
.join('│');
|
|
893
994
|
}
|
|
894
995
|
|
|
895
996
|
/** The board around this work in one line, and the link to it when `board.hubUrl` is set (S3.1). */
|
|
@@ -903,7 +1004,7 @@ function boardLines(state, pad) {
|
|
|
903
1004
|
return [`${pad('Board')}${counts}`, ...(b.url ? [`${pad('')}↗ ${b.url}`] : [])];
|
|
904
1005
|
}
|
|
905
1006
|
|
|
906
|
-
export function renderLines(state) {
|
|
1007
|
+
export function renderLines(state, now = new Date()) {
|
|
907
1008
|
const pad = (label) => ` ${label.padEnd(9)}`;
|
|
908
1009
|
if (!state.in_flight)
|
|
909
1010
|
return [`No epic in flight — ${state.reason}`, ...elsewhereLines(state, pad), ...boardLines(state, pad)];
|
|
@@ -926,7 +1027,7 @@ export function renderLines(state) {
|
|
|
926
1027
|
HEADINGS[seed.type] || 'Currently working on',
|
|
927
1028
|
`${pad(label)}${seed.title}${meta ? ` ${meta}` : ''}`,
|
|
928
1029
|
`${pad('Seed')}${seed.path}`,
|
|
929
|
-
|
|
1030
|
+
...statusLines(state, pad, ' '.repeat(11)),
|
|
930
1031
|
...also,
|
|
931
1032
|
...boardLines(state, pad),
|
|
932
1033
|
];
|
|
@@ -934,7 +1035,11 @@ export function renderLines(state) {
|
|
|
934
1035
|
const { epic, story, progress } = state;
|
|
935
1036
|
const cont = ' '.repeat(11);
|
|
936
1037
|
const risk = epic.risk ? ` · risk ${epic.risk.toUpperCase()}` : '';
|
|
937
|
-
const lines = [
|
|
1038
|
+
const lines = [
|
|
1039
|
+
'Currently building',
|
|
1040
|
+
`${pad('Epic')}${epic.title} ${epic.area}${risk}`,
|
|
1041
|
+
...whyLines(state.target, pad, cont, now),
|
|
1042
|
+
];
|
|
938
1043
|
if (story) {
|
|
939
1044
|
lines.push(`${pad('Story')}${story.id}${story.title ? ` — ${story.title}` : ''}`);
|
|
940
1045
|
lines.push(
|
|
@@ -948,12 +1053,14 @@ export function renderLines(state) {
|
|
|
948
1053
|
}
|
|
949
1054
|
// S2.2 — stories DONE (with commits), not the in-flight story's position: "Story 1 of 7" at the end of a sprint
|
|
950
1055
|
// was a position, and read as progress.
|
|
951
|
-
|
|
1056
|
+
// build-view-upgrade D2: "done" is the same measure, a story with a commit — the bars draw it per sprint.
|
|
1057
|
+
const bars = sprintBars(progress.by_sprint);
|
|
1058
|
+
const done = `${progress.stories_with_commits ?? 0} of ${progress.stories} stories done`;
|
|
952
1059
|
const inFlight = story ? ` · in flight ${story.id}` : '';
|
|
953
1060
|
const sprintPart = `Sprint ${progress.sprint ?? '?'} of ${progress.sprints}`;
|
|
954
|
-
lines.push(`${pad('Progress')}${done}${inFlight} · ${sprintPart}`);
|
|
1061
|
+
lines.push(`${pad('Progress')}${bars ? `${bars} ` : ''}${done}${inFlight} · ${sprintPart}`);
|
|
955
1062
|
if (state.spend) lines.push(`${pad('Spend')}${spendValue(state.spend, state.quote ?? null)}`);
|
|
956
|
-
lines.push(
|
|
1063
|
+
lines.push(...statusLines(state, pad, cont));
|
|
957
1064
|
return [...lines, ...also, ...boardLines(state, pad)];
|
|
958
1065
|
}
|
|
959
1066
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "The scripts the Golden Frijoles skills run, packaged so they work in any repo: planning, build-order, reporting and verification rails. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|