@cspeach/cli 1.1.31 → 1.1.32
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/agent/loop.js +16 -6
- package/dist/cli.js +13 -4
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-gate.js +10 -1
- package/dist/commands/plan-resume.js +4 -0
- package/dist/commands/team-rules.js +187 -0
- package/dist/commands/team.js +632 -90
- package/dist/journey/chain.js +265 -15
- package/dist/journey/clean-core.js +25 -3
- package/dist/journey/flow.js +51 -1
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +213 -20
- package/dist/journey/session-deps.js +5 -1
- package/dist/lane/app-chain.js +12 -2
- package/dist/one-shot.js +24 -17
- package/dist/projects/clean-core-confirm.js +38 -5
- package/dist/projects/save-command.js +20 -3
- package/dist/register/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +49 -19
- package/dist/register/export-files.js +58 -0
- package/dist/register/measure.js +191 -9
- package/dist/register/plan-import.js +6 -3
- package/dist/register/records.js +15 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -0
- package/dist/register/status-file/register-status-file.js +106 -108
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +1 -0
- package/dist/register/status-html.js +25 -6
- package/dist/register/store.js +4 -3
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +22 -56
- package/dist/register/write-whole.js +30 -2
- package/dist/renderer/tool-labels.js +2 -1
- package/dist/repl/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +4 -2
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +145 -86
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/queued-step.js +45 -0
- package/dist/skills/bundled-skills.js +53 -53
- package/dist/tools/dispatch-skill.js +8 -4
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_findings.js +13 -2
- package/dist/upgrade-check.js +49 -4
- package/package.json +120 -120
- package/.cspeach/project.json +0 -4
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// GENERATED COPY — do not edit here. Edit viewer/src/team/team.css,
|
|
3
3
|
// then run `npx tsx scripts/sync-status-file.ts` (prebuild runs it too).
|
|
4
4
|
// src/register/__tests__/status-file-pin.test.ts fails when this copy drifts.
|
|
5
|
-
export const TEAM_CSS = "/**\n * /team/ — one stylesheet, shared by the page and by the status file that\n * the page saves. That is why it pulls in no other stylesheet, no font file and\n * no image by address, and carries no selector that depends on anything outside\n * itself: the status file is one document on somebody's desktop, with no\n * network behind it.\n *\n * The nine state colours are the only colours on this page that mean anything.\n * Everything else is paper, ink and one edge, so nothing competes with the map.\n * The peach accent is kept for two things and no others: the button you are\n * meant to press, and where the keyboard currently is.\n */\n\n:root {\n --peach: oklch(0.74 0.13 35); --peach-deep: oklch(0.58 0.16 35);\n --paper: oklch(0.97 0.006 80); --paper-2: oklch(0.94 0.008 80);\n --ink: oklch(0.20 0.012 40); --ink-2: oklch(0.34 0.012 40); --ink-3: oklch(0.48 0.010 40);\n --edge: oklch(0.88 0.010 80);\n /* State colours: red = the system disagrees, grey = nobody has started, amber = words only, green = proven. */\n --s-regressed: oklch(0.52 0.20 27); --s-did-not-hold: oklch(0.64 0.19 40);\n --s-untouched: oklch(0.84 0.006 80); --s-decided: oklch(0.80 0.06 250); --s-working: oklch(0.70 0.10 250);\n --s-claimed: oklch(0.82 0.13 90); --s-verified: oklch(0.60 0.15 145); --s-clean: oklch(0.80 0.09 145);\n --s-retired: oklch(0.93 0.004 80);\n /* The tenth colour is not a state. It marks data holding a state this build\n does not know, and it is meant to look wrong: a cell nobody can explain\n must never pass for a quiet one. */\n --s-unknown: oklch(0.62 0.30 330);\n --font: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n --mono: ui-monospace, \"Cascadia Mono\", \"SF Mono\", Menlo, monospace;\n /* The drawings on the empty screen are themed with the site's own names, so\n the page has to answer to all of them or a drawing falls back to a fixed\n light hex and goes wrong in the dark. Fonts included: font-src is 'none'\n here, so the display face names system serifs and never a downloaded one. */\n --paper-3: oklch(0.91 0.010 80); --ink-4: oklch(0.62 0.008 40); --edge-2: oklch(0.82 0.012 80);\n --peach-soft: oklch(0.92 0.05 35); --peach-wash: oklch(0.97 0.02 35);\n --ok: oklch(0.60 0.15 145); --warn: oklch(0.70 0.15 75); --bad: oklch(0.56 0.19 27); --slate: oklch(0.55 0.02 250);\n --dgm-node: #ffffff;\n --font-sans: var(--font); --font-mono: var(--mono); --font-display: Georgia, \"Times New Roman\", serif;\n color-scheme: light dark;\n font-family: var(--font);\n color: var(--ink); background: var(--paper);\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --paper: oklch(0.19 0.008 60); --paper-2: oklch(0.24 0.010 60); --ink: oklch(0.94 0.006 80);\n --ink-2: oklch(0.82 0.008 80); --ink-3: oklch(0.68 0.008 80); --edge: oklch(0.32 0.010 60);\n --s-untouched: oklch(0.36 0.006 80); --s-retired: oklch(0.27 0.004 80);\n --paper-3: oklch(0.29 0.012 60); --ink-4: oklch(0.56 0.008 80); --edge-2: oklch(0.40 0.012 60);\n --peach-soft: oklch(0.36 0.06 35); --peach-wash: oklch(0.27 0.03 35); --dgm-node: var(--paper-2);\n }\n}\n\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--paper); color: var(--ink); line-height: 1.55; -webkit-text-size-adjust: 100%; }\n#app { max-width: 1180px; margin: 0 auto; padding: 32px 16px 64px; }\n[hidden] { display: none !important; }\n:focus-visible { outline: 2px solid var(--peach-deep); outline-offset: 2px; border-radius: 4px; }\n\n/* ── The answer, before anything else ─────────────────────────────────── */\n.masthead { margin: 0 0 40px; }\n/* SAP names run long and never break on their own (WAVE4_CLEANCORE_FINAL_V2),\n so anything that can hold one is allowed to break inside a word. */\n.provenance { margin: 0 0 10px; font-size: 14px; color: var(--ink-3); max-width: 70ch; overflow-wrap: anywhere; }\nh1 { margin: 0 0 12px; font-size: clamp(28px, 4.6vw, 44px); font-weight: 640; letter-spacing: -0.022em; line-height: 1.05; max-width: 22ch; overflow-wrap: anywhere; }\n.summary { margin: 0 0 22px; font-size: clamp(17px, 2.2vw, 21px); font-weight: 480; line-height: 1.4; color: var(--ink-2); max-width: 58ch; }\n.notice { margin: 0 0 20px; padding: 12px 14px; border-left: 3px solid var(--s-regressed); background: var(--paper-2); color: var(--ink); max-width: 70ch; }\n\n.actions { display: flex; flex-wrap: wrap; gap: 8px; }\nbutton { font: inherit; padding: 9px 15px; border-radius: 8px; cursor: pointer; max-width: 100%; overflow-wrap: anywhere; }\nbutton.primary { border: 1px solid var(--peach-deep); background: var(--peach); color: oklch(0.20 0.012 40); }\nbutton.quiet { border: 1px solid var(--edge); background: transparent; color: var(--ink-2); }\nbutton.quiet:hover { border-color: var(--ink-3); color: var(--ink); }\n/* The colour switch: the chosen mode reads as chosen, from the same tokens. */\nbutton.quiet[aria-pressed=\"true\"] { border-color: var(--ink); color: var(--ink); font-weight: 600; }\n.map > .actions { margin: 0 0 12px; }\n\n/* ── The map: the hero. No borders, no card, nothing to look at but colour ── */\n.map { margin: 0 0 48px; }\n.legend { list-style: none; display: flex; flex-wrap: wrap; gap: 6px 18px; padding: 0; margin: 0 0 20px; font-size: 13px; color: var(--ink-2); }\n.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: baseline; }\n.blocks { display: flex; flex-wrap: wrap; gap: 28px 32px; align-items: flex-start; }\n.blocks figure { margin: 0; max-width: 100%; }\n.blocks h3 { margin: 0 0 8px; font-family: var(--mono); font-size: 11.5px; font-weight: 600; color: var(--ink-2); letter-spacing: 0; }\n.blocks figcaption { margin-top: 6px; font-size: 11.5px; color: var(--ink-3); }\n.block-map { display: block; max-width: 100%; height: auto; }\n.cell { cursor: pointer; }\n.cell.selected { stroke: var(--ink); stroke-width: 2; }\n/* The map has one tab stop and the arrow keys move it, so where that stop\n currently sits has to be unmistakable — an outline alone is too thin at 10px. */\n.cell:focus-visible { outline: 2px solid var(--peach-deep); outline-offset: 2px; stroke: var(--ink); stroke-width: 2; }\n\n.s-regressed { fill: var(--s-regressed); background: var(--s-regressed); }\n.s-did-not-hold { fill: var(--s-did-not-hold); background: var(--s-did-not-hold); }\n.s-untouched { fill: var(--s-untouched); background: var(--s-untouched); }\n.s-decided { fill: var(--s-decided); background: var(--s-decided); }\n.s-working { fill: var(--s-working); background: var(--s-working); }\n.s-claimed { fill: var(--s-claimed); background: var(--s-claimed); }\n.s-verified { fill: var(--s-verified); background: var(--s-verified); }\n.s-clean { fill: var(--s-clean); background: var(--s-clean); }\n.s-retired { fill: var(--s-retired); background: var(--s-retired); }\n.s-unknown { fill: var(--s-unknown); background: var(--s-unknown); }\n\n/* The one bordered surface on the page, because it is a thing that opened. */\n.detail { margin-top: 28px; padding: 18px 20px; border: 1px solid var(--edge); border-radius: 12px; background: var(--paper-2); scroll-margin-top: 16px; }\n.detail h3 { margin: 0 0 10px; font-size: 17px; font-weight: 620; }\n.detail h3 small { font-family: var(--font); font-size: 13px; font-weight: 400; color: var(--ink-3); }\n.detail p { margin: 0 0 8px; font-size: 14px; }\n.detail .state { font-size: 14px; color: var(--ink-2); }\n.detail table { margin: 12px 0; }\n.ident { font-family: var(--mono); overflow-wrap: anywhere; }\n.warn { color: var(--s-regressed); }\n\n/* ── What needs a person, and who owns what ───────────────────────────── */\n.pair { display: grid; gap: 40px 48px; }\n@media (min-width: 900px) { .pair { grid-template-columns: 1.3fr 1fr; } }\n.needs, .lanes { min-width: 0; }\nh2 { margin: 0 0 14px; font-size: 15px; font-weight: 620; color: var(--ink-2); }\n.needs ul { list-style: none; margin: 0; padding: 0; }\n/* The bar carries the state's own colour where the item is a state, and a\n neutral one where it is not — so the colour never means a second thing. */\n.need { display: grid; gap: 3px; margin-bottom: 12px; padding: 2px 0 2px 12px; border-left: 3px solid var(--ink-3); }\n.need b { font-size: 14.5px; font-weight: 620; }\n.need span { font-size: 14px; color: var(--ink-2); overflow-wrap: anywhere; }\n.k-regressed { border-left-color: var(--s-regressed); }\n.k-did-not-hold { border-left-color: var(--s-did-not-hold); }\n\n.scroller { overflow-x: auto; }\ntable { width: 100%; border-collapse: collapse; font-size: 14px; }\nth, td { text-align: left; padding: 8px 12px 8px 0; border-bottom: 1px solid var(--edge); white-space: nowrap; }\nth { font-weight: 600; color: var(--ink-3); font-size: 12.5px; }\n\n/* ── How it works (the empty screen) ──────────────────────────────────── */\n.how { margin-top: 48px; }\n.how p { font-size: 14px; color: var(--ink-2); max-width: 70ch; }\n/* The drawings follow the page's own tokens, so the card is the page's paper in\n both themes rather than a fixed light slab. Each drawing ships twice: a wide\n composition and a narrow one that stacks the same nodes and says the same\n words. This is the only thing that chooses between them — no script, and no\n style attribute, because the page's policy forbids one. */\n.diagram { margin-bottom: 16px; padding: 20px 16px; border: 1px solid var(--edge); border-radius: 12px; background: var(--paper); }\n.diagram svg { display: block; width: 100%; height: auto; margin: 0 auto; }\n.diagram .narrow { display: none; }\n@media (max-width: 640px) {\n .diagram { padding: 16px 8px; }\n .diagram .wide { display: none; }\n .diagram .narrow { display: block; max-width: 420px; margin: 0 auto; }\n}\n.how + .how { margin-top: 40px; }\n\nfooter { margin-top: 56px; padding-top: 20px; border-top: 1px solid var(--edge); font-size: 13px; color: var(--ink-3); }\nfooter p { margin: 0 0 8px; max-width: 78ch; }\n\n/* ── The way off the page ─────────────────────────────────────────────── */\n/* It sits outside the app, so it lines itself up with the app's own column\n instead of running the full width of the window. No rule above it: the\n page's own footer has already drawn one, and two lines in a row read as a\n mistake. It does not belong on paper — a printed record is not clickable. */\n.site-foot { max-width: 1180px; margin: 24px auto 0; padding: 0 16px 48px; border-top: 0; }\n.site-foot a { color: var(--ink-2); }\n.site-foot a:hover { color: var(--ink); }\n@media print { .site-foot { display: none; } }\n\n/* ── The status file ──────────────────────────────────────────────────── */\n/* The saved copy is this same page as one document, so it wears this same\n stylesheet and nearly all of the rules above. It has no buttons to press, so\n the peach accent never appears in it — the accent marks the thing you act on,\n and in a document there is nothing to act on. These are the only rules it\n adds.\n\n The stamp is the one mark that says this is a document and not a page: a\n moment, boxed and set in the face this page already uses for machine facts.\n It is bordered rather than filled so it survives a printer that drops\n colour. */\n.stamp {\n display: inline-block; margin: 0 0 22px; padding: 7px 11px; max-width: 100%;\n border: 1px solid var(--edge); border-radius: 6px;\n font-family: var(--mono); font-size: 12.5px; line-height: 1.5; color: var(--ink-2); overflow-wrap: anywhere;\n}\n\n/* Thousands of rows folded into one line per package: the count is in the line\n so the size of a package reads without opening it. Ruled, not carded — these\n are rows of a list, not a set of tiles. */\n.objects { margin-top: 48px; }\n.objects details { border-top: 1px solid var(--edge); }\n.objects details:last-of-type { border-bottom: 1px solid var(--edge); }\n.objects summary { padding: 11px 0; font-family: var(--mono); font-size: 12.5px; font-weight: 600; color: var(--ink-2); cursor: pointer; }\n.objects summary:hover, .objects details[open] > summary { color: var(--ink); }\n.objects .scroller { padding-bottom: 16px; }\n\n@media print {\n /* A printed record that quietly leaves out the folded half is not the record,\n so every package opens on paper. Two rules for the two ways a browser hides\n a closed fold; a browser that knows neither selector ignores its rule. */\n .objects details:not([open]) > :not(summary) { display: block; }\n .objects details::details-content { content-visibility: visible; }\n .objects summary { break-after: avoid; }\n .stamp { border-color: var(--ink-3); }\n}\n\n/* ── What was measured, and what was not ──────────────────────────────── */\n/* Two marks, and deliberately no new colour. The nine state colours are the\n only colours on this page that mean anything, and how a number was arrived at\n is not a tenth state — so the file tag is an outline, and the line about what\n nobody has measured borrows the amber the key already reads as \"words only\".\n Both are bars and pills the page has elsewhere; nothing new is introduced. */\n.unmeasured { margin: 0 0 20px; padding: 10px 14px; border-left: 3px solid var(--s-claimed); background: var(--paper-2); font-size: 15px; color: var(--ink-2); max-width: 70ch; }\n.detail .unmeasured { margin: 12px 0 0; }\n.runs { margin-top: 48px; min-width: 0; }\n.tag { display: inline-block; padding: 1px 7px; border: 1px solid var(--edge); border-radius: 999px; font-size: 12px; color: var(--ink-2); background: var(--paper); white-space: nowrap; }\n.quiet-note { font-size: 13px; color: var(--ink-3); }\n/* A note that belongs to the run above it. The run keeps no rule under it, so\n the two read as one thing rather than as a row and a stray sentence. */\ntr.has-note > td { border-bottom: 0; }\ntd.run-note { white-space: normal; padding-top: 0; }\n/* The runs nobody folded in. Indented off the table above and set quieter, so\n it reads as a footnote to the measurements rather than as more of them. */\n.aside { margin-top: 18px; padding-left: 12px; border-left: 1px solid var(--edge); font-size: 13px; color: var(--ink-3); max-width: 70ch; }\n.aside p { margin: 0 0 6px; }\n.aside ul { margin: 0; padding-left: 18px; }\n.aside li { overflow-wrap: anywhere; }\n\n/* ── Where it stands, and what was decided ────────────────────────────── */\n/* Five numbers a manager reads before the map. Not cards: each is a number on\n the page's paper with the same left bar the \"Needs you\" items carry, so the\n page keeps one way of marking a thing out. The bar is the edge colour until a\n number means trouble — then it borrows the red the key already reads as \"the\n system disagrees\", and nothing else on this strip is coloured. */\n.stands, .dispositions { margin: 0 0 40px; }\n.tiles { list-style: none; margin: 0 0 24px; padding: 0; display: grid; gap: 12px 20px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }\n.tile { padding: 2px 0 2px 12px; border-left: 3px solid var(--edge); display: grid; gap: 1px; align-content: start; }\n.tile-n { font-size: clamp(26px, 3.4vw, 32px); font-weight: 640; letter-spacing: -0.02em; line-height: 1.1; color: var(--ink); font-variant-numeric: tabular-nums; }\n.tile-label { font-size: 14.5px; font-weight: 620; color: var(--ink); }\n.tile-note { font-size: 13px; color: var(--ink-3); }\n.tile.bad { border-left-color: var(--s-regressed); }\n.tile.bad .tile-n { color: var(--s-regressed); }\n.empty-note { margin: 0 0 20px; color: var(--ink-2); max-width: 70ch; }\n\n/* The two level bars. A and B lean green, C amber, D red, \"not decided\" the\n grey of an object nobody has started — the key's own reading of each colour,\n so a level never asks the eye to learn a new palette. The words under each\n bar carry the same numbers: colour is never the only way to read it. */\n.levels { display: grid; gap: 14px; max-width: 640px; }\n.levelbar { margin: 0; }\n.levelbar figcaption { font-size: 13px; font-weight: 600; color: var(--ink-2); margin-bottom: 4px; }\n.levelbar svg { display: block; width: 100%; height: 12px; border-radius: 2px; }\n.levelwords, .legend-line { margin: 4px 0 0; font-size: 13px; color: var(--ink-3); }\n.l-A { fill: var(--s-verified); background: var(--s-verified); }\n.l-B { fill: var(--s-clean); background: var(--s-clean); }\n.l-C { fill: var(--s-claimed); background: var(--s-claimed); }\n.l-D { fill: var(--s-regressed); background: var(--s-regressed); }\n.l-none { fill: var(--s-untouched); background: var(--s-untouched); }\n\n/* The five dispositions, one line. Outlined pills — the same mark as the\n \"measured from a file\" tag — because a disposition is a person's choice,\n not a state, and only states wear colour here. */\n.chips { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.chip { padding: 3px 11px; border: 1px solid var(--edge); border-radius: 999px; font-size: 14px; color: var(--ink-2); background: var(--paper); }\n.chip b { font-weight: 640; color: var(--ink); font-variant-numeric: tabular-nums; }\n.exceptions { max-width: 78ch; font-size: 14px; }\n.exceptions summary { cursor: pointer; color: var(--ink-2); padding: 4px 0; }\n.exceptions summary:hover, .exceptions[open] > summary { color: var(--ink); }\n.exceptions ul { margin: 6px 0 0; padding-left: 18px; }\n.exceptions li { margin-bottom: 4px; overflow-wrap: anywhere; }\n.lanes .quiet-note { margin: 8px 0 0; }\n.k-exception-no-reason { border-left-color: var(--warn); }\n\n/* ── The focus, the estate, and what the plan adds (spec 2026-10-06) ──── */\n/* The focus sits under the one line of truth as a quieter second line: the\n same left bar as a tile, in the edge colour, because it is context for the\n numbers, not a number that means trouble. */\n.focus-line { margin: -10px 0 22px; padding: 2px 0 2px 12px; border-left: 3px solid var(--edge); font-size: 15px; color: var(--ink-2); max-width: 70ch; overflow-wrap: anywhere; }\n/* The estate reads before the team project's own numbers. Same tiles, same\n table, same quiet notes as the rest of the page — nothing new to learn. */\n.estate { margin: 0 0 40px; min-width: 0; }\n.estate h3 { margin: 0 0 8px; font-size: 14px; font-weight: 620; color: var(--ink-2); }\n/* The one tile whose value is words, not a count: set smaller so a sentence\n does not shout as loudly as a number. */\n.tile-text { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--ink); overflow-wrap: anywhere; }\n.estate-scope { margin: 0 0 20px; font-size: 15px; color: var(--ink-2); }\n.estate-unknowns { margin: 0 0 24px; max-width: 78ch; font-size: 14px; }\n.estate-unknowns ul { margin: 0; padding: 0; list-style: none; }\n.estate-unknowns li { margin-bottom: 6px; padding-left: 12px; border-left: 3px solid var(--warn); overflow-wrap: anywhere; }\n.estate-unknowns p { margin: 0; }\n/* A retire nobody has confirmed yet is words only — the amber the key already\n reads that way. The other plan items are housekeeping for the lead. */\n.tile.retire-candidates { border-left-color: var(--s-claimed); }\n.k-retire-candidate { border-left-color: var(--s-claimed); }\n.k-out-of-scope-no-reason, .k-excel-clashes { border-left-color: var(--warn); }\n/* Next names up to three objects; let them wrap rather than widen the table. */\ntd.next { white-space: normal; min-width: 14ch; }\n\n/* ── Progress over time, and the plan (spec 2026-10-06 §9 items 5, 8, 9) ── */\n/* The chart borrows the key's own colours: decided blue, claimed amber,\n proven green. Level A is not a state, so it is the quiet ink, dotted. Each\n line also has its own dash, and the legend says the numbers in words, so a\n printer that drops colour or an eye that cannot tell them apart still reads\n it. Ruled like the level bars, not carded. */\n.progress, .plan { margin: 0 0 40px; min-width: 0; }\n.progress h3, .plan h3 { margin: 20px 0 8px; font-size: 14px; font-weight: 620; color: var(--ink-2); }\n.progress-chart { margin: 0; max-width: 720px; }\n.progress-chart svg { display: block; width: 100%; height: auto; }\n.progress-chart .grid { stroke: var(--edge); stroke-width: 1; }\n.progress-chart .axis { fill: var(--ink-3); font-size: 11px; font-family: var(--font); }\n.pl { fill: none; stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; }\n.pl-decided, .pd-decided { stroke: var(--s-decided); }\n.pl-claimed, .pd-claimed { stroke: var(--s-claimed); }\n.pl-proven, .pd-proven { stroke: var(--s-verified); }\n.pl-a, .pd-a { stroke: var(--ink-3); }\n.pl-decided { stroke-dasharray: 6 4; }\n.pl-claimed { stroke-dasharray: 2 3; }\n.pl-a { stroke-dasharray: 1 4; stroke-width: 2.5; }\n.pd { fill: var(--paper); stroke-width: 2; }\n.chart-legend { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 20px; font-size: 13px; color: var(--ink-2); }\n.key { display: inline-block; width: 18px; height: 0; margin: 0 6px 0 0; vertical-align: middle; border-top: 3px solid var(--ink-3); }\n.key-decided { border-top: 3px dashed var(--s-decided); }\n.key-claimed { border-top: 3px dotted var(--s-claimed); }\n.key-proven { border-top-color: var(--s-verified); }\n.key-a { border-top: 3px dotted var(--ink-3); }\n/* Done % per developer and per focus: a track, a fill, the number in words. */\n.pct-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; max-width: 720px; }\n.pct-bars li { display: grid; grid-template-columns: minmax(10ch, 16rem) minmax(80px, 1fr) auto; gap: 4px 12px; align-items: center; font-size: 14px; }\n.pct-label { overflow-wrap: anywhere; color: var(--ink); }\n.pct-bar { display: block; height: 10px; border-radius: 2px; background: var(--s-untouched); overflow: hidden; }\n.pct-fill { display: block; height: 100%; background: var(--s-verified); }\n.pct-fill.f-focus { background: var(--s-clean); }\n.pct-n { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }\n@media (max-width: 560px) { .pct-bars li { grid-template-columns: 1fr auto; } .pct-bar { grid-column: 1 / -1; grid-row: 2; } }\n.w-0{width:0%} .w-5{width:5%} .w-10{width:10%} .w-15{width:15%} .w-20{width:20%} .w-25{width:25%} .w-30{width:30%} .w-35{width:35%} .w-40{width:40%} .w-45{width:45%} .w-50{width:50%} .w-55{width:55%} .w-60{width:60%} .w-65{width:65%} .w-70{width:70%} .w-75{width:75%} .w-80{width:80%} .w-85{width:85%} .w-90{width:90%} .w-95{width:95%} .w-100{width:100%}\n.plan-weights { margin: 0 0 12px; font-size: 14.5px; color: var(--ink-2); max-width: 78ch; }\n.focus-stack { margin: 0; padding-left: 22px; max-width: 78ch; font-size: 14px; }\n.focus-stack li { margin-bottom: 6px; overflow-wrap: anywhere; }\ntd.what { white-space: normal; min-width: 18ch; overflow-wrap: anywhere; }\n/* The detail panel's longer evidence: two small lists, the replaced decisions quieter. */\n.detail h4 { margin: 14px 0 6px; font-size: 13.5px; font-weight: 620; color: var(--ink-2); }\n.detail .history { margin: 0 0 8px; padding-left: 18px; font-size: 14px; }\n.detail .history li { margin-bottom: 4px; overflow-wrap: anywhere; }\n.detail .history li.superseded { color: var(--ink-3); }\n\n/* ── Paper ────────────────────────────────────────────────────────────── */\n/* Paper is white, whatever the machine is set to. A manager on a dark-mode\n laptop who prints this — or prints the status file they were mailed, which\n carries this stylesheet inside it — would otherwise get white ink on white\n paper, and the map is the whole message. Every token the dark theme\n overrides is put back here to its light value; a test reads both lists and\n fails if one of them gains a token the other does not. */\n@media print {\n :root {\n --paper: oklch(0.97 0.006 80); --paper-2: oklch(0.94 0.008 80); --ink: oklch(0.20 0.012 40);\n --ink-2: oklch(0.34 0.012 40); --ink-3: oklch(0.48 0.010 40); --edge: oklch(0.88 0.010 80);\n --s-untouched: oklch(0.84 0.006 80); --s-retired: oklch(0.93 0.004 80);\n --paper-3: oklch(0.91 0.010 80); --ink-4: oklch(0.62 0.008 40); --edge-2: oklch(0.82 0.012 80);\n --peach-soft: oklch(0.92 0.05 35); --peach-wash: oklch(0.97 0.02 35); --dgm-node: #ffffff;\n }\n}\n\n@media print {\n .actions, .detail { display: none; }\n #app { max-width: none; padding: 0; }\n .pair { grid-template-columns: 1fr; }\n .cell { cursor: default; }\n /* The map is the message. A printer that drops the colour prints nothing.\n `.notice` is on this list for the status file: on paper it is the line that\n says the picture may be incomplete, and a caveat with its mark stripped off\n is a caveat somebody skims past. */\n .cell, .swatch, .need, .notice, .unmeasured, .tag, .diagram, .tile, .lvl, .chip, .focus-line, .estate-unknowns li, .pct-bar, .pct-fill, .pl, .key { print-color-adjust: exact; -webkit-print-color-adjust: exact; }\n footer, .needs, .lanes, .runs, .stands, .dispositions, .estate-unknowns, .progress-chart, .pct-bars { break-inside: avoid; }\n /* A printed list of exceptions that stays folded is a list nobody can read. */\n .exceptions:not([open]) > :not(summary) { display: block; }\n .exceptions::details-content { content-visibility: visible; }\n}\n";
|
|
5
|
+
export const TEAM_CSS = "/**\n * /team/ — one stylesheet, shared by the page and by the status file that\n * the page saves. That is why it pulls in no other stylesheet, no font file and\n * no image by address, and carries no selector that depends on anything outside\n * itself: the status file is one document on somebody's desktop, with no\n * network behind it.\n *\n * The nine state colours are the only colours on this page that mean anything.\n * Everything else is paper, ink and one edge, so nothing competes with the map.\n * The peach accent is kept for two things and no others: the button you are\n * meant to press, and where the keyboard currently is.\n */\n\n:root {\n --peach: oklch(0.74 0.13 35); --peach-deep: oklch(0.58 0.16 35);\n --paper: oklch(0.97 0.006 80); --paper-2: oklch(0.94 0.008 80);\n --ink: oklch(0.20 0.012 40); --ink-2: oklch(0.34 0.012 40); --ink-3: oklch(0.48 0.010 40);\n --edge: oklch(0.88 0.010 80);\n /* State colours: red = the system disagrees, grey = nobody has started, amber = words only, green = proven. */\n --s-regressed: oklch(0.52 0.20 27); --s-did-not-hold: oklch(0.64 0.19 40);\n --s-untouched: oklch(0.84 0.006 80); --s-decided: oklch(0.80 0.06 250); --s-working: oklch(0.70 0.10 250);\n --s-claimed: oklch(0.82 0.13 90); --s-verified: oklch(0.60 0.15 145); --s-clean: oklch(0.80 0.09 145);\n --s-retired: oklch(0.93 0.004 80);\n /* The tenth colour is not a state. It marks data holding a state this build\n does not know, and it is meant to look wrong: a cell nobody can explain\n must never pass for a quiet one. */\n --s-unknown: oklch(0.62 0.30 330);\n --font: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n --mono: ui-monospace, \"Cascadia Mono\", \"SF Mono\", Menlo, monospace;\n /* The drawings on the empty screen are themed with the site's own names, so\n the page has to answer to all of them or a drawing falls back to a fixed\n light hex and goes wrong in the dark. Fonts included: font-src is 'none'\n here, so the display face names system serifs and never a downloaded one. */\n --paper-3: oklch(0.91 0.010 80); --ink-4: oklch(0.62 0.008 40); --edge-2: oklch(0.82 0.012 80);\n --peach-soft: oklch(0.92 0.05 35); --peach-wash: oklch(0.97 0.02 35);\n --ok: oklch(0.60 0.15 145); --warn: oklch(0.70 0.15 75); --bad: oklch(0.56 0.19 27); --slate: oklch(0.55 0.02 250);\n --dgm-node: #ffffff;\n --font-sans: var(--font); --font-mono: var(--mono); --font-display: Georgia, \"Times New Roman\", serif;\n color-scheme: light dark;\n font-family: var(--font);\n color: var(--ink); background: var(--paper);\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --paper: oklch(0.19 0.008 60); --paper-2: oklch(0.24 0.010 60); --ink: oklch(0.94 0.006 80);\n --ink-2: oklch(0.82 0.008 80); --ink-3: oklch(0.68 0.008 80); --edge: oklch(0.32 0.010 60);\n --s-untouched: oklch(0.36 0.006 80); --s-retired: oklch(0.27 0.004 80);\n --paper-3: oklch(0.29 0.012 60); --ink-4: oklch(0.56 0.008 80); --edge-2: oklch(0.40 0.012 60);\n --peach-soft: oklch(0.36 0.06 35); --peach-wash: oklch(0.27 0.03 35); --dgm-node: var(--paper-2);\n }\n}\n\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--paper); color: var(--ink); line-height: 1.55; -webkit-text-size-adjust: 100%; }\n#app { max-width: 1180px; margin: 0 auto; padding: 32px 16px 64px; }\n[hidden] { display: none !important; }\n:focus-visible { outline: 2px solid var(--peach-deep); outline-offset: 2px; border-radius: 4px; }\n\n/* ── The answer, before anything else ─────────────────────────────────── */\n.masthead { margin: 0 0 40px; }\n/* SAP names run long and never break on their own (WAVE4_CLEANCORE_FINAL_V2),\n so anything that can hold one is allowed to break inside a word. */\n.provenance { margin: 0 0 10px; font-size: 14px; color: var(--ink-3); max-width: 70ch; overflow-wrap: anywhere; }\nh1 { margin: 0 0 12px; font-size: clamp(28px, 4.6vw, 44px); font-weight: 640; letter-spacing: -0.022em; line-height: 1.05; max-width: 22ch; overflow-wrap: anywhere; }\n.summary { margin: 0 0 22px; font-size: clamp(17px, 2.2vw, 21px); font-weight: 480; line-height: 1.4; color: var(--ink-2); max-width: 58ch; }\n.notice { margin: 0 0 20px; padding: 12px 14px; border-left: 3px solid var(--s-regressed); background: var(--paper-2); color: var(--ink); max-width: 70ch; }\n\n.actions { display: flex; flex-wrap: wrap; gap: 8px; }\nbutton { font: inherit; padding: 9px 15px; border-radius: 8px; cursor: pointer; max-width: 100%; overflow-wrap: anywhere; }\nbutton.primary { border: 1px solid var(--peach-deep); background: var(--peach); color: oklch(0.20 0.012 40); }\nbutton.quiet { border: 1px solid var(--edge); background: transparent; color: var(--ink-2); }\nbutton.quiet:hover { border-color: var(--ink-3); color: var(--ink); }\n/* The colour switch: the chosen mode reads as chosen, from the same tokens. */\nbutton.quiet[aria-pressed=\"true\"] { border-color: var(--ink); color: var(--ink); font-weight: 600; }\n.map > .actions { margin: 0 0 12px; }\n\n/* ── The map: the hero. No borders, no card, nothing to look at but colour ── */\n.map { margin: 0 0 48px; }\n.legend { list-style: none; display: flex; flex-wrap: wrap; gap: 6px 18px; padding: 0; margin: 0 0 20px; font-size: 13px; color: var(--ink-2); }\n.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: baseline; }\n.blocks { display: flex; flex-wrap: wrap; gap: 28px 32px; align-items: flex-start; }\n.blocks figure { margin: 0; max-width: 100%; }\n.blocks h3 { margin: 0 0 8px; font-family: var(--mono); font-size: 11.5px; font-weight: 600; color: var(--ink-2); letter-spacing: 0; }\n.blocks figcaption { margin-top: 6px; font-size: 11.5px; color: var(--ink-3); }\n.block-map { display: block; max-width: 100%; height: auto; }\n.cell { cursor: pointer; }\n.cell.selected { stroke: var(--ink); stroke-width: 2; }\n/* The map has one tab stop and the arrow keys move it, so where that stop\n currently sits has to be unmistakable — an outline alone is too thin at 10px. */\n.cell:focus-visible { outline: 2px solid var(--peach-deep); outline-offset: 2px; stroke: var(--ink); stroke-width: 2; }\n\n.s-regressed { fill: var(--s-regressed); background: var(--s-regressed); }\n.s-did-not-hold { fill: var(--s-did-not-hold); background: var(--s-did-not-hold); }\n.s-untouched { fill: var(--s-untouched); background: var(--s-untouched); }\n.s-decided { fill: var(--s-decided); background: var(--s-decided); }\n.s-working { fill: var(--s-working); background: var(--s-working); }\n.s-claimed { fill: var(--s-claimed); background: var(--s-claimed); }\n.s-verified { fill: var(--s-verified); background: var(--s-verified); }\n.s-clean { fill: var(--s-clean); background: var(--s-clean); }\n.s-retired { fill: var(--s-retired); background: var(--s-retired); }\n.s-unknown { fill: var(--s-unknown); background: var(--s-unknown); }\n\n/* The one bordered surface on the page, because it is a thing that opened. */\n.detail { margin-top: 28px; padding: 18px 20px; border: 1px solid var(--edge); border-radius: 12px; background: var(--paper-2); scroll-margin-top: 16px; }\n.detail h3 { margin: 0 0 10px; font-size: 17px; font-weight: 620; }\n.detail h3 small { font-family: var(--font); font-size: 13px; font-weight: 400; color: var(--ink-3); }\n.detail p { margin: 0 0 8px; font-size: 14px; }\n.detail .state { font-size: 14px; color: var(--ink-2); }\n.detail table { margin: 12px 0; }\n.ident { font-family: var(--mono); overflow-wrap: anywhere; }\n.warn { color: var(--s-regressed); }\n\n/* ── What needs a person, and who owns what ───────────────────────────── */\n.pair { display: grid; gap: 40px 48px; }\n@media (min-width: 900px) { .pair { grid-template-columns: 1.3fr 1fr; } }\n.needs, .lanes { min-width: 0; }\nh2 { margin: 0 0 14px; font-size: 15px; font-weight: 620; color: var(--ink-2); }\n.needs ul { list-style: none; margin: 0; padding: 0; }\n/* The bar carries the state's own colour where the item is a state, and a\n neutral one where it is not — so the colour never means a second thing. */\n.need { display: grid; gap: 3px; margin-bottom: 12px; padding: 2px 0 2px 12px; border-left: 3px solid var(--ink-3); }\n.need b { font-size: 14.5px; font-weight: 620; }\n.need span { font-size: 14px; color: var(--ink-2); overflow-wrap: anywhere; }\n.k-regressed { border-left-color: var(--s-regressed); }\n.k-did-not-hold { border-left-color: var(--s-did-not-hold); }\n\n.scroller { overflow-x: auto; }\ntable { width: 100%; border-collapse: collapse; font-size: 14px; }\nth, td { text-align: left; padding: 8px 12px 8px 0; border-bottom: 1px solid var(--edge); white-space: nowrap; }\nth { font-weight: 600; color: var(--ink-3); font-size: 12.5px; }\n\n/* ── How it works (the empty screen) ──────────────────────────────────── */\n.how { margin-top: 48px; }\n.how p { font-size: 14px; color: var(--ink-2); max-width: 70ch; }\n/* The drawings follow the page's own tokens, so the card is the page's paper in\n both themes rather than a fixed light slab. Each drawing ships twice: a wide\n composition and a narrow one that stacks the same nodes and says the same\n words. This is the only thing that chooses between them — no script, and no\n style attribute, because the page's policy forbids one. */\n.diagram { margin-bottom: 16px; padding: 20px 16px; border: 1px solid var(--edge); border-radius: 12px; background: var(--paper); }\n.diagram svg { display: block; width: 100%; height: auto; margin: 0 auto; }\n.diagram .narrow { display: none; }\n@media (max-width: 640px) {\n .diagram { padding: 16px 8px; }\n .diagram .wide { display: none; }\n .diagram .narrow { display: block; max-width: 420px; margin: 0 auto; }\n}\n.how + .how { margin-top: 40px; }\n\nfooter { margin-top: 56px; padding-top: 20px; border-top: 1px solid var(--edge); font-size: 13px; color: var(--ink-3); }\nfooter p { margin: 0 0 8px; max-width: 78ch; }\n\n/* ── The way off the page ─────────────────────────────────────────────── */\n/* It sits outside the app, so it lines itself up with the app's own column\n instead of running the full width of the window. No rule above it: the\n page's own footer has already drawn one, and two lines in a row read as a\n mistake. It does not belong on paper — a printed record is not clickable. */\n.site-foot { max-width: 1180px; margin: 24px auto 0; padding: 0 16px 48px; border-top: 0; }\n.site-foot a { color: var(--ink-2); }\n.site-foot a:hover { color: var(--ink); }\n@media print { .site-foot { display: none; } }\n\n/* ── The status file ──────────────────────────────────────────────────── */\n/* The saved copy is this same page as one document, so it wears this same\n stylesheet and nearly all of the rules above. It has no buttons to press, so\n the peach accent never appears in it — the accent marks the thing you act on,\n and in a document there is nothing to act on. These are the only rules it\n adds.\n\n The stamp is the one mark that says this is a document and not a page: a\n moment, boxed and set in the face this page already uses for machine facts.\n It is bordered rather than filled so it survives a printer that drops\n colour. */\n.stamp {\n display: inline-block; margin: 0 0 22px; padding: 7px 11px; max-width: 100%;\n border: 1px solid var(--edge); border-radius: 6px;\n font-family: var(--mono); font-size: 12.5px; line-height: 1.5; color: var(--ink-2); overflow-wrap: anywhere;\n}\n\n/* Thousands of rows folded into one line per package: the count is in the line\n so the size of a package reads without opening it. Ruled, not carded — these\n are rows of a list, not a set of tiles. */\n.objects { margin-top: 48px; }\n.objects details { border-top: 1px solid var(--edge); }\n.objects details:last-of-type { border-bottom: 1px solid var(--edge); }\n.objects summary { padding: 11px 0; font-family: var(--mono); font-size: 12.5px; font-weight: 600; color: var(--ink-2); cursor: pointer; }\n.objects summary:hover, .objects details[open] > summary { color: var(--ink); }\n.objects .scroller { padding-bottom: 16px; }\n\n@media print {\n /* A printed record that quietly leaves out the folded half is not the record,\n so every package opens on paper. Two rules for the two ways a browser hides\n a closed fold; a browser that knows neither selector ignores its rule. */\n .objects details:not([open]) > :not(summary) { display: block; }\n .objects details::details-content { content-visibility: visible; }\n .objects summary { break-after: avoid; }\n .stamp { border-color: var(--ink-3); }\n}\n\n/* ── What was measured, and what was not ──────────────────────────────── */\n/* Two marks, and deliberately no new colour. The nine state colours are the\n only colours on this page that mean anything, and how a number was arrived at\n is not a tenth state — so the file tag is an outline, and the line about what\n nobody has measured borrows the amber the key already reads as \"words only\".\n Both are bars and pills the page has elsewhere; nothing new is introduced. */\n.unmeasured { margin: 0 0 20px; padding: 10px 14px; border-left: 3px solid var(--s-claimed); background: var(--paper-2); font-size: 15px; color: var(--ink-2); max-width: 70ch; }\n.detail .unmeasured { margin: 12px 0 0; }\n.runs { margin-top: 48px; min-width: 0; }\n.tag { display: inline-block; padding: 1px 7px; border: 1px solid var(--edge); border-radius: 999px; font-size: 12px; color: var(--ink-2); background: var(--paper); white-space: nowrap; }\n.quiet-note { font-size: 13px; color: var(--ink-3); }\n/* A note that belongs to the run above it. The run keeps no rule under it, so\n the two read as one thing rather than as a row and a stray sentence. */\ntr.has-note > td { border-bottom: 0; }\ntd.run-note { white-space: normal; padding-top: 0; }\n/* The runs nobody folded in. Indented off the table above and set quieter, so\n it reads as a footnote to the measurements rather than as more of them. */\n.aside { margin-top: 18px; padding-left: 12px; border-left: 1px solid var(--edge); font-size: 13px; color: var(--ink-3); max-width: 70ch; }\n.aside p { margin: 0 0 6px; }\n.aside ul { margin: 0; padding-left: 18px; }\n.aside li { overflow-wrap: anywhere; }\n\n/* ── Where it stands, and what was decided ────────────────────────────── */\n/* Five numbers a manager reads before the map. Not cards: each is a number on\n the page's paper with the same left bar the \"Needs you\" items carry, so the\n page keeps one way of marking a thing out. The bar is the edge colour until a\n number means trouble — then it borrows the red the key already reads as \"the\n system disagrees\", and nothing else on this strip is coloured. */\n.stands, .dispositions { margin: 0 0 40px; }\n.tiles { list-style: none; margin: 0 0 24px; padding: 0; display: grid; gap: 12px 20px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }\n.tile { padding: 2px 0 2px 12px; border-left: 3px solid var(--edge); display: grid; gap: 1px; align-content: start; }\n.tile-n { font-size: clamp(26px, 3.4vw, 32px); font-weight: 640; letter-spacing: -0.02em; line-height: 1.1; color: var(--ink); font-variant-numeric: tabular-nums; }\n.tile-label { font-size: 14.5px; font-weight: 620; color: var(--ink); }\n.tile-note { font-size: 13px; color: var(--ink-3); }\n.tile.bad { border-left-color: var(--s-regressed); }\n.tile.bad .tile-n { color: var(--s-regressed); }\n.empty-note { margin: 0 0 20px; color: var(--ink-2); max-width: 70ch; }\n\n/* The two level bars. A and B lean green, C amber, D red, \"not decided\" the\n grey of an object nobody has started — the key's own reading of each colour,\n so a level never asks the eye to learn a new palette. The words under each\n bar carry the same numbers: colour is never the only way to read it. */\n.levels { display: grid; gap: 14px; max-width: 640px; }\n.levelbar { margin: 0; }\n.levelbar figcaption { font-size: 13px; font-weight: 600; color: var(--ink-2); margin-bottom: 4px; }\n.levelbar svg { display: block; width: 100%; height: 12px; border-radius: 2px; }\n.levelwords, .legend-line { margin: 4px 0 0; font-size: 13px; color: var(--ink-3); }\n.l-A { fill: var(--s-verified); background: var(--s-verified); }\n.l-B { fill: var(--s-clean); background: var(--s-clean); }\n.l-C { fill: var(--s-claimed); background: var(--s-claimed); }\n.l-D { fill: var(--s-regressed); background: var(--s-regressed); }\n.l-none { fill: var(--s-untouched); background: var(--s-untouched); }\n\n/* The five dispositions, one line. Outlined pills — the same mark as the\n \"measured from a file\" tag — because a disposition is a person's choice,\n not a state, and only states wear colour here. */\n.chips { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }\n.chip { padding: 3px 11px; border: 1px solid var(--edge); border-radius: 999px; font-size: 14px; color: var(--ink-2); background: var(--paper); }\n.chip b { font-weight: 640; color: var(--ink); font-variant-numeric: tabular-nums; }\n.exceptions { max-width: 78ch; font-size: 14px; }\n.exceptions summary { cursor: pointer; color: var(--ink-2); padding: 4px 0; }\n.exceptions summary:hover, .exceptions[open] > summary { color: var(--ink); }\n.exceptions ul { margin: 6px 0 0; padding-left: 18px; }\n.exceptions li { margin-bottom: 4px; overflow-wrap: anywhere; }\n.lanes .quiet-note { margin: 8px 0 0; }\n.k-exception-no-reason { border-left-color: var(--warn); }\n\n/* ── The focus, the estate, and what the plan adds (spec 2026-10-06) ──── */\n/* The focus sits under the one line of truth as a quieter second line: the\n same left bar as a tile, in the edge colour, because it is context for the\n numbers, not a number that means trouble. */\n.focus-line { margin: -10px 0 22px; padding: 2px 0 2px 12px; border-left: 3px solid var(--edge); font-size: 15px; color: var(--ink-2); max-width: 70ch; overflow-wrap: anywhere; }\n/* The estate reads before the team project's own numbers. Same tiles, same\n table, same quiet notes as the rest of the page — nothing new to learn. */\n.estate { margin: 0 0 40px; min-width: 0; }\n.estate h3 { margin: 0 0 8px; font-size: 14px; font-weight: 620; color: var(--ink-2); }\n/* The one tile whose value is words, not a count: set smaller so a sentence\n does not shout as loudly as a number. */\n.tile-text { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--ink); overflow-wrap: anywhere; }\n.estate-scope { margin: 0 0 20px; font-size: 15px; color: var(--ink-2); }\n.estate-unknowns { margin: 0 0 24px; max-width: 78ch; font-size: 14px; }\n.estate-unknowns ul { margin: 0; padding: 0; list-style: none; }\n.estate-unknowns li { margin-bottom: 6px; padding-left: 12px; border-left: 3px solid var(--warn); overflow-wrap: anywhere; }\n.estate-unknowns p { margin: 0; }\n/* A retire nobody has confirmed yet is words only — the amber the key already\n reads that way. The other plan items are housekeeping for the lead. */\n.tile.retire-candidates { border-left-color: var(--s-claimed); }\n.k-retire-candidate { border-left-color: var(--s-claimed); }\n.k-out-of-scope-no-reason, .k-excel-clashes { border-left-color: var(--warn); }\n/* Next names up to three objects; let them wrap rather than widen the table. */\ntd.next { white-space: normal; min-width: 14ch; }\n\n/* ── Progress over time, and the plan (spec 2026-10-06 §9 items 5, 8, 9) ── */\n/* The chart borrows the key's own colours: decided blue, claimed amber,\n proven green. Level A is not a state, so it is the quiet ink, dotted. Each\n line also has its own dash, and the legend says the numbers in words, so a\n printer that drops colour or an eye that cannot tell them apart still reads\n it. Ruled like the level bars, not carded. */\n.progress, .plan { margin: 0 0 40px; min-width: 0; }\n.progress h3, .plan h3 { margin: 20px 0 8px; font-size: 14px; font-weight: 620; color: var(--ink-2); }\n.progress-chart { margin: 0; max-width: 720px; }\n.progress-chart svg { display: block; width: 100%; height: auto; }\n.progress-chart .grid { stroke: var(--edge); stroke-width: 1; }\n.progress-chart .axis { fill: var(--ink-3); font-size: 11px; font-family: var(--font); }\n.pl { fill: none; stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; }\n.pl-decided, .pd-decided { stroke: var(--s-decided); }\n.pl-claimed, .pd-claimed { stroke: var(--s-claimed); }\n.pl-proven, .pd-proven { stroke: var(--s-verified); }\n.pl-a, .pd-a { stroke: var(--ink-3); }\n.pl-decided { stroke-dasharray: 6 4; }\n.pl-claimed { stroke-dasharray: 2 3; }\n.pl-a { stroke-dasharray: 1 4; stroke-width: 2.5; }\n.pd { fill: var(--paper); stroke-width: 2; }\n.chart-legend { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 20px; font-size: 13px; color: var(--ink-2); }\n.key { display: inline-block; width: 18px; height: 0; margin: 0 6px 0 0; vertical-align: middle; border-top: 3px solid var(--ink-3); }\n.key-decided { border-top: 3px dashed var(--s-decided); }\n.key-claimed { border-top: 3px dotted var(--s-claimed); }\n.key-proven { border-top-color: var(--s-verified); }\n.key-a { border-top: 3px dotted var(--ink-3); }\n/* Done % per developer and per focus: a track, a fill, the number in words. */\n.pct-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; max-width: 720px; }\n.pct-bars li { display: grid; grid-template-columns: minmax(10ch, 16rem) minmax(80px, 1fr) auto; gap: 4px 12px; align-items: center; font-size: 14px; }\n.pct-label { overflow-wrap: anywhere; color: var(--ink); }\n.pct-bar { display: block; height: 10px; border-radius: 2px; background: var(--s-untouched); overflow: hidden; }\n.pct-fill { display: block; height: 100%; background: var(--s-verified); }\n.pct-fill.f-focus { background: var(--s-clean); }\n.pct-n { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }\n@media (max-width: 560px) { .pct-bars li { grid-template-columns: 1fr auto; } .pct-bar { grid-column: 1 / -1; grid-row: 2; } }\n.w-0{width:0%} .w-5{width:5%} .w-10{width:10%} .w-15{width:15%} .w-20{width:20%} .w-25{width:25%} .w-30{width:30%} .w-35{width:35%} .w-40{width:40%} .w-45{width:45%} .w-50{width:50%} .w-55{width:55%} .w-60{width:60%} .w-65{width:65%} .w-70{width:70%} .w-75{width:75%} .w-80{width:80%} .w-85{width:85%} .w-90{width:90%} .w-95{width:95%} .w-100{width:100%}\n.plan-weights { margin: 0 0 12px; font-size: 14.5px; color: var(--ink-2); max-width: 78ch; }\n.focus-stack { margin: 0; padding-left: 22px; max-width: 78ch; font-size: 14px; }\n.focus-stack li { margin-bottom: 6px; overflow-wrap: anywhere; }\ntd.what { white-space: normal; min-width: 18ch; overflow-wrap: anywhere; }\n/* The detail panel's longer evidence: two small lists, the replaced decisions quieter. */\n.detail h4 { margin: 14px 0 6px; font-size: 13.5px; font-weight: 620; color: var(--ink-2); }\n.detail .history { margin: 0 0 8px; padding-left: 18px; font-size: 14px; }\n.detail .history li { margin-bottom: 4px; overflow-wrap: anywhere; }\n.detail .history li.superseded { color: var(--ink-3); }\n\n/* ── The one report (status file, step 2) ─────────────────────────────── */\n/* One page a manager reads in thirty seconds, every lane on it. The coverage\n line is the one ruled band on the page: it says how much of the project the\n check has seen, and it comes before any number, so no count can be read\n without it. Everything else is ink on paper; the levels share one colour,\n because a level is a fact, not a grade. No peach: nothing here is a button. */\n.rp-coverage { margin: 0 0 22px; padding: 10px 0 10px; border-top: 2px solid var(--ink); border-bottom: 1px solid var(--edge); max-width: 78ch; }\n.rp-coverage-line { margin: 0; font-size: 15.5px; font-weight: 600; line-height: 1.45; color: var(--ink); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }\n.rp-trend { margin: 4px 0 0; font-size: 13.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }\n.rp-headline { margin: 0 0 8px; font-size: clamp(21px, 3vw, 28px); font-weight: 600; letter-spacing: -0.015em; line-height: 1.2; max-width: 34ch; }\n.rp-scope { margin: 0 0 24px; font-size: 15px; color: var(--ink-2); max-width: 70ch; }\n.rp-numbers { list-style: none; margin: 0 0 32px; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); border-top: 1px solid var(--edge); border-bottom: 1px solid var(--edge); }\n.rp-numbers li { display: grid; align-content: start; gap: 2px; padding: 14px 14px 14px 0; }\n.rp-numbers li + li { padding-left: 14px; border-left: 1px solid var(--edge); }\n.rp-n { font-size: clamp(28px, 3.6vw, 36px); font-weight: 640; letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums; }\n.rp-label { margin-top: 6px; font-size: 14.5px; font-weight: 620; line-height: 1.25; }\n.rp-sub { font-size: 12.5px; color: var(--ink-3); line-height: 1.35; }\n.rp-band { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px 40px; margin: 0 0 32px; align-items: start; }\n.rp-lvs { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--edge); }\n.rp-lvs li { display: grid; grid-template-columns: minmax(7ch, 9.5rem) 4ch minmax(0, 1fr); gap: 8px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--edge); }\n.rp-lv { font-weight: 620; }\n.rp-lv-n { text-align: right; font-weight: 620; font-variant-numeric: tabular-nums; }\n.rp-lv-key { font-size: 13px; color: var(--ink-3); line-height: 1.35; }\n.rp-target { margin: 12px 0 0; padding-left: 12px; border-left: 3px solid var(--ink); font-size: 14.5px; max-width: 60ch; }\n.rp-note { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-3); max-width: 60ch; }\n.rp-decide { padding: 14px 18px; border: 1px solid var(--edge); border-left: 3px solid var(--ink); border-radius: 4px; background: var(--paper-2); min-width: 0; }\n.rp-decide ol, .rp-changed ul, .rp-kept-list, .rp-rebuilds ul { list-style: none; margin: 0; padding: 0; }\n.rp-decide li, .rp-changed li, .rp-kept-list li, .rp-rebuilds li { padding: 7px 0; border-top: 1px solid var(--edge); overflow-wrap: anywhere; }\n.rp-decide li:first-child { border-top: 0; padding-top: 0; }\n.rp-decide p, .rp-kept-list p, .rp-changed p, .rp-rebuilds p { margin: 0; font-size: 14.5px; }\n.rp-who, .rp-why { color: var(--ink-3); font-size: 13.5px !important; }\n.rp-tail { margin: 10px 0 0; font-size: 13px; color: var(--ink-3); max-width: 78ch; }\n.rp-changed { margin: 0 0 40px; max-width: 78ch; }\n.rp-detail { margin: 0 0 32px; padding-top: 28px; border-top: 1px solid var(--ink-3); min-width: 0; }\n.rp-devs tbody th { font-weight: 620; color: var(--ink); font-size: 14px; }\n.rp-devs td, .rp-devs th { font-variant-numeric: tabular-nums; }\n.rp-devs td.rp-pk { white-space: normal; min-width: 14ch; font-family: var(--mono); font-size: 12.5px; }\n.rp-kept, .rp-rebuilds { margin: 0 0 32px; max-width: 78ch; }\n.rp-key { color: var(--ink-3); }\n@media (max-width: 720px) {\n .rp-band { grid-template-columns: minmax(0, 1fr); }\n .rp-numbers { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .rp-numbers li, .rp-numbers li + li { padding: 12px 12px 12px 0; border-left: 0; border-top: 1px solid var(--edge); }\n .rp-numbers li:nth-child(even) { padding-left: 12px; border-left: 1px solid var(--edge); }\n .rp-numbers li:nth-child(-n+2) { border-top: 0; }\n .rp-lvs li { grid-template-columns: minmax(7ch, 8rem) 4ch minmax(0, 1fr); }\n}\n@media print {\n .rp-coverage, .rp-decide, .rp-target { print-color-adjust: exact; -webkit-print-color-adjust: exact; }\n .rp-first { break-after: page; }\n .rp-detail { border-top: 0; padding-top: 0; }\n .rp-numbers, .rp-band, .rp-decide, .rp-devs tr, .rp-kept-list li { break-inside: avoid; }\n}\n\n/* ── Paper ────────────────────────────────────────────────────────────── */\n/* Paper is white, whatever the machine is set to. A manager on a dark-mode\n laptop who prints this — or prints the status file they were mailed, which\n carries this stylesheet inside it — would otherwise get white ink on white\n paper, and the map is the whole message. Every token the dark theme\n overrides is put back here to its light value; a test reads both lists and\n fails if one of them gains a token the other does not. */\n@media print {\n :root {\n --paper: oklch(0.97 0.006 80); --paper-2: oklch(0.94 0.008 80); --ink: oklch(0.20 0.012 40);\n --ink-2: oklch(0.34 0.012 40); --ink-3: oklch(0.48 0.010 40); --edge: oklch(0.88 0.010 80);\n --s-untouched: oklch(0.84 0.006 80); --s-retired: oklch(0.93 0.004 80);\n --paper-3: oklch(0.91 0.010 80); --ink-4: oklch(0.62 0.008 40); --edge-2: oklch(0.82 0.012 80);\n --peach-soft: oklch(0.92 0.05 35); --peach-wash: oklch(0.97 0.02 35); --dgm-node: #ffffff;\n }\n}\n\n@media print {\n .actions, .detail { display: none; }\n #app { max-width: none; padding: 0; }\n .pair { grid-template-columns: 1fr; }\n .cell { cursor: default; }\n /* The map is the message. A printer that drops the colour prints nothing.\n `.notice` is on this list for the status file: on paper it is the line that\n says the picture may be incomplete, and a caveat with its mark stripped off\n is a caveat somebody skims past. */\n .cell, .swatch, .need, .notice, .unmeasured, .tag, .diagram, .tile, .lvl, .chip, .focus-line, .estate-unknowns li, .pct-bar, .pct-fill, .pl, .key { print-color-adjust: exact; -webkit-print-color-adjust: exact; }\n footer, .needs, .lanes, .runs, .stands, .dispositions, .estate-unknowns, .progress-chart, .pct-bars { break-inside: avoid; }\n /* A printed list of exceptions that stays folded is a list nobody can read. */\n .exceptions:not([open]) > :not(summary) { display: block; }\n .exceptions::details-content { content-visibility: visible; }\n}\n";
|
|
@@ -10,6 +10,7 @@ export const COPIES = [
|
|
|
10
10
|
['../viewer/src/lib/estate-parts.js', 'src/register/status-file/estate-parts.ts'],
|
|
11
11
|
['../viewer/src/lib/progress-chart.js', 'src/register/status-file/progress-chart.ts'],
|
|
12
12
|
['../viewer/src/lib/plan-parts.js', 'src/register/status-file/plan-parts.ts'],
|
|
13
|
+
['../viewer/src/lib/report-model.js', 'src/register/status-file/report-model.ts'],
|
|
13
14
|
['../viewer/src/team/team.css', 'src/register/status-file/team-css.ts'],
|
|
14
15
|
];
|
|
15
16
|
export const HEADER = (from) => [
|
|
@@ -1,19 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status file, from Node — the same document the team page saves, built by
|
|
3
|
+
* the viewer's own code (copied into ./status-file/ and pinned by a test).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
1
6
|
import { buildStatusHtml as buildStatusHtmlUntyped } from './status-file/register-status-file.js';
|
|
2
|
-
import { needsYou as needsYouUntyped
|
|
7
|
+
import { needsYou as needsYouUntyped } from './status-file/register-model.js';
|
|
3
8
|
import { TEAM_CSS } from './status-file/team-css.js';
|
|
9
|
+
import { asOfFromStatusHtml, reportView as reportViewUntyped } from './status-file/report-model.js';
|
|
4
10
|
// The copy is plain JavaScript under `// @ts-nocheck`, so TypeScript infers
|
|
5
11
|
// `needsYou`'s options from its `= {}` default and loses `now`. Say the shape
|
|
6
12
|
// here rather than edit the generated copy.
|
|
7
13
|
const needsYou = needsYouUntyped;
|
|
8
|
-
// The same for `
|
|
14
|
+
// The same for the page's own options: their `= []` / `= null` defaults read as "always empty".
|
|
9
15
|
const buildStatusHtml = buildStatusHtmlUntyped;
|
|
16
|
+
/** The one report's model, typed for the Node callers (team status, the workbook). */
|
|
17
|
+
export const reportViewFor = reportViewUntyped;
|
|
10
18
|
export function statusHtmlFor(view) {
|
|
11
|
-
const { register, unreadable, asOf } = view;
|
|
19
|
+
const { register, unreadable, asOf, ...rest } = view;
|
|
12
20
|
return buildStatusHtml({
|
|
13
|
-
...
|
|
14
|
-
summary: summarize(register, { unreadable }),
|
|
21
|
+
...rest, register, unreadable, asOf,
|
|
15
22
|
needs: needsYou(register, { now: asOf, unreadable, surface: 'file' }),
|
|
16
|
-
blocks: packageBlocks(register),
|
|
17
23
|
css: TEAM_CSS,
|
|
18
24
|
});
|
|
19
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The as-of stamp of a status page already on disk, read BEFORE it is
|
|
28
|
+
* overwritten: the "changed since the last report" block starts from it. Null
|
|
29
|
+
* when there is no page, or one from 1.1.31 without the tag (review F10).
|
|
30
|
+
*/
|
|
31
|
+
export function previousAsOfIn(file) {
|
|
32
|
+
try {
|
|
33
|
+
return asOfFromStatusHtml(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/register/store.js
CHANGED
|
@@ -17,6 +17,7 @@ import fs from 'node:fs';
|
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { cspeachRoot } from '../config/paths.js';
|
|
20
|
+
import { writeWhole } from './write-whole.js';
|
|
20
21
|
export const TEAM_FILE = 'cspeach-team.json';
|
|
21
22
|
export const BASELINE_FILE = path.join('baseline', 'objects.json');
|
|
22
23
|
export const LANES_FILE = 'lanes.json';
|
|
@@ -103,8 +104,7 @@ export function writeProjectVariant(projectDir, variant) {
|
|
|
103
104
|
throw new RegisterError(`"${variant}" is not an ATC check variant name.`);
|
|
104
105
|
const file = path.join(projectDir, TEAM_FILE);
|
|
105
106
|
const next = { ...readProjectFile(projectDir), atcVariant: variant };
|
|
106
|
-
|
|
107
|
-
fs.renameSync(`${file}.tmp`, file);
|
|
107
|
+
writeWhole(file, `${JSON.stringify(next, null, 2)}\n`);
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
110
|
* One read, one sentence. `existsSync` then `readFileSync` answers only the
|
|
@@ -156,8 +156,9 @@ export function readLanes(projectDir) {
|
|
|
156
156
|
const l = readJson(file);
|
|
157
157
|
return Array.isArray(l?.lanes) ? l.lanes : [];
|
|
158
158
|
}
|
|
159
|
+
/** Whole, then renamed (spec §3.7): a crash mid-write leaves the old lanes.json as it was. */
|
|
159
160
|
export function writeLanes(projectDir, projectId, lanes) {
|
|
160
|
-
|
|
161
|
+
writeWhole(path.join(projectDir, LANES_FILE), `${JSON.stringify({ kind: 'lanes', formatVersion: 1, projectId, lanes }, null, 2)}\n`);
|
|
161
162
|
}
|
|
162
163
|
/**
|
|
163
164
|
* A record is a short list of object names. Anything this big is a dump, a log
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { VARIANT_NAME } from './store.js';
|
|
2
|
+
import { shownUpTo } from './atc-file.js';
|
|
3
|
+
/*
|
|
4
|
+
* SCICHKV_HD — the SCI check-variant header. COLUMNS VERIFIED live on S4H,
|
|
5
|
+
* 2026-10-08 (`__tests__/fixtures/step2/scichkv-hd-columns.json`):
|
|
6
|
+
* CHECKVNAME holds the variant name; CIUSER is the owner — blank for a global
|
|
7
|
+
* variant, a user name for that user's private one. CHECKVID is NOT the owner:
|
|
8
|
+
* it is the variant's own number (00465 for ABAP_CLOUD_READINESS), never
|
|
9
|
+
* blank, so reading it as the owner would make every variant private.
|
|
10
|
+
*/
|
|
11
|
+
const NAME_COLUMN = 'CHECKVNAME';
|
|
12
|
+
const OWNER_COLUMN = 'CIUSER';
|
|
13
|
+
/**
|
|
14
|
+
* True: the owner column (CIUSER) was seen on a live system (2026-10-08), so
|
|
15
|
+
* CSPeach says "global" or "<USER>'s private variant", not only "found".
|
|
16
|
+
*/
|
|
17
|
+
export const PRIVATE_VARIANT_COLUMN_VERIFIED = true;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the variant exists on the system: global, private to one user, not
|
|
20
|
+
* found, or 'unreadable' when the table could not be read. A name that is not
|
|
21
|
+
* a variant name is never put into a statement: it is not found.
|
|
22
|
+
*/
|
|
23
|
+
export async function lookupVariant(sql, name,
|
|
24
|
+
/** Tests only: read (or not) the owner column. */
|
|
25
|
+
opts = {}) {
|
|
26
|
+
const wanted = name.trim().toUpperCase();
|
|
27
|
+
if (!VARIANT_NAME.test(wanted))
|
|
28
|
+
return { found: false };
|
|
29
|
+
let rows;
|
|
30
|
+
try {
|
|
31
|
+
({ rows } = await sql(`SELECT ${NAME_COLUMN}, ${OWNER_COLUMN} FROM SCICHKV_HD WHERE ${NAME_COLUMN} = '${wanted}'`, 50));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 'unreadable';
|
|
35
|
+
}
|
|
36
|
+
const mine = (Array.isArray(rows) ? rows : []).filter((r) => String(r?.[NAME_COLUMN] ?? '').trim().toUpperCase() === wanted);
|
|
37
|
+
if (!mine.length)
|
|
38
|
+
return { found: false };
|
|
39
|
+
if (!(opts.ownerColumnVerified ?? PRIVATE_VARIANT_COLUMN_VERIFIED))
|
|
40
|
+
return { found: true };
|
|
41
|
+
if (mine.some((r) => !String(r?.[OWNER_COLUMN] ?? '').trim()))
|
|
42
|
+
return { found: true, global: true };
|
|
43
|
+
// Read from the system, shown on a card: cleaned for the screen, at most 40 characters.
|
|
44
|
+
return { found: true, privateTo: shownUpTo(String(mine[0][OWNER_COLUMN]).trim().toUpperCase(), 40) };
|
|
45
|
+
}
|
|
46
|
+
/** The card's title. */
|
|
47
|
+
export const VARIANT_TITLE = (name) => `Checked with: ${name}`;
|
|
48
|
+
/** What the variant is, in one sentence. Never "SAP-recommended". */
|
|
49
|
+
export function variantWhat(name) {
|
|
50
|
+
if (name === 'ABAP_CLOUD_READINESS')
|
|
51
|
+
return "SAP's ABAP Cloud readiness check.";
|
|
52
|
+
if (name === 'S4HANA_READINESS')
|
|
53
|
+
return "SAP's S/4HANA readiness check.";
|
|
54
|
+
return 'A check variant on your system.';
|
|
55
|
+
}
|
|
56
|
+
/** What the lookup found, in one sentence (the card's second fact). */
|
|
57
|
+
export function variantLookupLine(name, sid, look) {
|
|
58
|
+
if (look === 'unreadable')
|
|
59
|
+
return `CSPeach could not confirm the variant exists on ${sid}.`;
|
|
60
|
+
if (!look.found)
|
|
61
|
+
return `CSPeach could not find the variant ${name} on ${sid}. Use it anyway?`;
|
|
62
|
+
if (look.privateTo)
|
|
63
|
+
return `This is ${look.privateTo}'s private variant; a global one is safer for a team.`;
|
|
64
|
+
return look.global ? `Found on ${sid} as a global variant.` : `Found on ${sid}.`;
|
|
65
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The two workbooks (spec §6.2, §6.3), built from the sheet model in
|
|
3
|
-
* register-core — the same functions the team page prints — so a number in
|
|
2
|
+
* The two workbooks (spec §6.2, §6.3; step 2 §3.3), built from the sheet model in
|
|
3
|
+
* register-core and the report model — the same functions the team page prints — so a number in
|
|
4
4
|
* Excel and the same number on the page cannot differ.
|
|
5
5
|
*
|
|
6
6
|
* Regenerated, never read back: an edit typed into a workbook changes nothing.
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { utcWords, workingListRows, WORKING_LIST_COLUMNS } from '@cspeach/register-core';
|
|
12
12
|
import { registerFor } from './picture.js';
|
|
13
|
+
import { BUSINESS_COLUMNS, HISTORY_COLUMNS, OBJECTS_COLUMNS, businessSheetRows, historySheetRows, objectsSheetRows, summarySheetRows, } from './status-file/report-model.js';
|
|
14
|
+
import { reportViewFor } from './status-html.js';
|
|
13
15
|
import { safeFileStem, writeXlsxFile } from './xlsx.js';
|
|
14
16
|
const LIST_WIDTHS = [28, 6, 16, 6, 8, 22, 48, 26, 10, 10, 20, 18, 20, 22];
|
|
15
17
|
const PLAN_WIDTHS = [28, 6, 16, 22, 9, 9, 12, 40, 3, 10, 18, 20, 7, 22, 20, 10];
|
|
16
|
-
/** The Read me line `plan import` reads the export stamp from. */
|
|
18
|
+
/** The Summary (or, in a 1.1.31 workbook, Read me) line `plan import` reads the export stamp from. */
|
|
17
19
|
export const EXPORT_STAMP = 'Export stamp: ';
|
|
18
20
|
const LEVEL_LINE = 'Levels: A uses only released APIs. B calls public classic APIs. C uses SAP-internal objects. D modifies SAP standard.';
|
|
19
21
|
export const workingListFileName = (name) => `${safeFileStem(name)} - working list.xlsx`;
|
|
@@ -35,59 +37,23 @@ export function workingListSheets(register, laneIds, who, asOf, folderName) {
|
|
|
35
37
|
]),
|
|
36
38
|
];
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const summary = [
|
|
51
|
-
...summaryRows(register),
|
|
52
|
-
[], ...table(levelTable(register)),
|
|
53
|
-
[], ...table(packageTable(register)),
|
|
54
|
-
[], ...table(developerTable(register)),
|
|
55
|
-
...(note ? [[note]] : []),
|
|
56
|
-
];
|
|
57
|
-
const lanes = register.lanes.map((l) => ({
|
|
58
|
-
name: l.owner, header: [...WORKING_LIST_COLUMNS], rows: workingListRows(register, [l.id]), widths: LIST_WIDTHS,
|
|
59
|
-
}));
|
|
60
|
-
const runs = measurementTable(register);
|
|
61
|
-
const progress = progressRows(register);
|
|
62
|
-
const capped = register.progressCapped;
|
|
63
|
-
const rules = priorityRulesRows(register);
|
|
64
|
-
const estate = register.estate ? estateSheets(register.estate).find((s) => s.name === 'Packages')?.table : undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The combined workbook: four sheets (step 2, the one report) — Summary ·
|
|
42
|
+
* Objects · For the business · History — built from the same report model the
|
|
43
|
+
* status page prints, so a number in Excel and on the page cannot differ.
|
|
44
|
+
* Objects carries the Plan sheet's columns, so `plan import` reads it; the
|
|
45
|
+
* export stamp sits on the Summary sheet.
|
|
46
|
+
*/
|
|
47
|
+
export function combinedSheets(register, asOf, folderName, project) {
|
|
48
|
+
const view = reportViewFor({ project, register, asOf });
|
|
49
|
+
const summary = summarySheetRows(view, { madeFor: `Made by CSPeach on ${utcWords(asOf)}, from the team project folder "${folderName}".` });
|
|
50
|
+
const stampAt = summary.findIndex((r) => r[0] === 'As of') + 1;
|
|
51
|
+
summary.splice(stampAt, 0, [`${EXPORT_STAMP}${asOf}`, '']);
|
|
65
52
|
return [
|
|
66
|
-
{ name: 'Summary', header: ['Summary', 'Count'], rows: summary, widths: [
|
|
67
|
-
|
|
68
|
-
{ name: '
|
|
69
|
-
{ name: '
|
|
70
|
-
...lanes,
|
|
71
|
-
{ name: 'Measurements', header: runs.header, rows: runs.rows, widths: [20, 20, 26, 22, 10, 14, 24] },
|
|
72
|
-
{
|
|
73
|
-
name: 'Progress', header: progress.header, widths: [20, 9, 9, 9, 9, 9, 9, 10, 13], filter: !capped,
|
|
74
|
-
rows: [...progress.rows, ...(capped ? [[], [`Showing the last ${capped.shown} of ${capped.total} check runs.`]] : [])],
|
|
75
|
-
},
|
|
76
|
-
{ name: 'Priority rules', header: rules.header, rows: rules.rows, widths: [24, 18, 30], filter: false },
|
|
77
|
-
...(estate ? [{ name: 'Estate inventory', header: estate.header, rows: estate.rows, widths: [22, 34, 18, 18, 16, 9, 12, 15, 10, 20, 16, 9] }] : []),
|
|
78
|
-
readMe([
|
|
79
|
-
`Made by CSPeach on ${utcWords(asOf)}, from the team project folder "${folderName}".`,
|
|
80
|
-
`${EXPORT_STAMP}${asOf}`,
|
|
81
|
-
`Edit only the five columns before the dash: ${PLAN_EDITABLE.join(', ')}.`,
|
|
82
|
-
'Bring the sheet back with: cspeach team plan import <file>',
|
|
83
|
-
'Changes made by someone else after this export are shown and skipped, never overwritten.',
|
|
84
|
-
'The numbers are the same ones the team page shows for this folder.',
|
|
85
|
-
'This file is made again by cspeach team report. Only the Plan sheet is read back, and only through plan import.',
|
|
86
|
-
'Proven means a check run said so. A claimed fix is not counted as proven.',
|
|
87
|
-
'A level moves only when a check proves the fix.',
|
|
88
|
-
LEVEL_LINE,
|
|
89
|
-
dSentence(register),
|
|
90
|
-
]),
|
|
53
|
+
{ name: 'Summary', header: ['Summary', 'Count'], rows: summary, widths: [64, 18, 10, 10, 12, 18, 8, 10, 10, 12], filter: false },
|
|
54
|
+
{ name: 'Objects', header: [...OBJECTS_COLUMNS], rows: objectsSheetRows(register, view.coverage.variant), widths: [...PLAN_WIDTHS, 10, 40, 20, 24] },
|
|
55
|
+
{ name: 'For the business', header: [...BUSINESS_COLUMNS], rows: businessSheetRows(register), widths: [28, 6, 16, 20, 24, 40, 30, 12, 9, 22, 12] },
|
|
56
|
+
{ name: 'History', header: [...HISTORY_COLUMNS], rows: historySheetRows(register), widths: [20, 20, 40, 28, 40] },
|
|
91
57
|
];
|
|
92
58
|
}
|
|
93
59
|
export const HELD = new Set(['EBUSY', 'EPERM', 'EACCES', 'EISDIR', 'ENOTEMPTY']);
|
|
@@ -5,12 +5,40 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
/** OneDrive or a virus scanner holding the file for a moment: EPERM / EBUSY, gone within milliseconds. */
|
|
9
|
+
const HELD = new Set(['EPERM', 'EBUSY']);
|
|
10
|
+
const RENAME_TRIES = 3;
|
|
11
|
+
const RENAME_WAIT_MS = 200;
|
|
12
|
+
/** Rename, tried up to 3 times 200 ms apart when the file is held; any other error at once. */
|
|
13
|
+
function renameHeld(from, to) {
|
|
14
|
+
for (let attempt = 1;; attempt++) {
|
|
15
|
+
try {
|
|
16
|
+
fs.renameSync(from, to);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (attempt >= RENAME_TRIES || !HELD.has(e?.code ?? ''))
|
|
21
|
+
throw e;
|
|
22
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, RENAME_WAIT_MS);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
8
26
|
export function writeWhole(file, data) {
|
|
9
27
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
10
28
|
const tmp = `${file}.tmp`;
|
|
11
|
-
fs.writeFileSync(tmp, data);
|
|
12
29
|
try {
|
|
13
|
-
fs.
|
|
30
|
+
fs.writeFileSync(tmp, data);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
// Half a .tmp is no use to anyone and a sync tool would carry it to the whole team.
|
|
34
|
+
try {
|
|
35
|
+
fs.unlinkSync(tmp);
|
|
36
|
+
}
|
|
37
|
+
catch { /* nothing was created */ }
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
renameHeld(tmp, file);
|
|
14
42
|
}
|
|
15
43
|
catch (e) {
|
|
16
44
|
try {
|
|
@@ -161,6 +161,7 @@ const RULES = {
|
|
|
161
161
|
cap_link_smoke: (a) => ({ verb: 'S/4 link check', object: str(a['s4']?.['entitySet']) ?? '' }),
|
|
162
162
|
app_lane_resolve: (a) => ({ verb: 'Chose lane', object: shortLeft(str(a['specPath']) ?? '') }),
|
|
163
163
|
team_findings: (a) => ({ verb: 'Read team findings', object: str(a['lane']) ?? str(a['packages']) ?? 'all objects' }),
|
|
164
|
+
rules_propose: () => ({ verb: 'Check a proposed decision rule', object: 'nothing is saved' }),
|
|
164
165
|
team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
|
|
165
166
|
sap_badi_list: (a) => ({ verb: 'Searched BAdIs', object: shortRight(`"${str(a['query']) ?? ''}"`) }),
|
|
166
167
|
sap_snapshot_list: (a) => ({ verb: 'Listed snapshots', object: shortRight(str(a['name']) ?? '') }),
|
|
@@ -267,7 +268,7 @@ const PRESENT = {
|
|
|
267
268
|
Preview: 'Preview', 'Searched samples': 'Searching samples', 'Read sample': 'Reading sample',
|
|
268
269
|
'Audited metadata': 'Auditing metadata', 'Extended model': 'Extending model',
|
|
269
270
|
'Checked capability': 'Checking capability',
|
|
270
|
-
'Read texts of': 'Reading texts of', 'Read team findings': 'Reading team findings', 'Read estate inventory': 'Reading estate inventory', 'Wrote texts': 'Writing texts',
|
|
271
|
+
'Read texts of': 'Reading texts of', 'Read team findings': 'Reading team findings', 'Check a proposed decision rule': 'Checking a proposed decision rule', 'Read estate inventory': 'Reading estate inventory', 'Wrote texts': 'Writing texts',
|
|
271
272
|
};
|
|
272
273
|
export function toolActivityLabel(name, args) {
|
|
273
274
|
const l = toolLabel(name, args);
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* inquirer, so the router can import it.
|
|
10
10
|
*/
|
|
11
11
|
import { creditsActive } from '../cost/credits.js';
|
|
12
|
+
// Pure generated data (type-only import inside), so this file stays light.
|
|
13
|
+
import { BUNDLED_RUNS } from '../runs/bundled-runs.js';
|
|
12
14
|
export const COST_DESCRIPTION_USD = 'Show this session\'s spend so far ($ and tokens)';
|
|
13
15
|
export const COST_DESCRIPTION_CREDITS = 'Show the credits this session has used so far';
|
|
14
16
|
/** Piece 2 §10: managed customers see credits, never "$" (cost/display-mode.ts). */
|
|
@@ -34,6 +36,8 @@ export const BUILTIN_ENTRIES = [
|
|
|
34
36
|
{ name: '/ui', description: 'View or change rendering mode (auto / ink), or the look: /ui look panels|legacy' },
|
|
35
37
|
{ name: '/reroute', description: 'Re-dispatch the previous prompt to a different skill' },
|
|
36
38
|
{ name: '/transport', description: 'View or set the active transport for this session' },
|
|
39
|
+
{ name: '/rules', description: 'View or change the decision rules of the joined team project (the lead changes them)' },
|
|
40
|
+
{ name: '/run', description: `Start a guided run — ${BUNDLED_RUNS.map((r) => r.entry).join(' or ')}` },
|
|
37
41
|
{ name: '/safety-mode', description: 'Toggle structured Forge Rule confirmation cards (on / off / status)' },
|
|
38
42
|
];
|
|
39
43
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { enterJourney } from '../journey/chain.js';
|
|
1
|
+
import { enterJourney, journeyStoppedOnError } from '../journey/chain.js';
|
|
2
2
|
import { openStatusPage } from '../util/open-in-browser.js';
|
|
3
3
|
export async function runJourneyEntry(trigger, a) {
|
|
4
4
|
const hooks = {
|
|
@@ -20,6 +20,8 @@ export async function runJourneyEntry(trigger, a) {
|
|
|
20
20
|
catch (err) {
|
|
21
21
|
if (process.env['CSPEACH_DEBUG'] === '1')
|
|
22
22
|
throw err;
|
|
23
|
-
|
|
23
|
+
// Audit M-1: paused (not ended), so the line's "carry on" works; the error
|
|
24
|
+
// is logged, and a second stop at the same step says so (follow-up a).
|
|
25
|
+
a.emit(journeyStoppedOnError(a.session, a.cwd, err));
|
|
24
26
|
}
|
|
25
27
|
}
|
package/dist/repl/plan-hop.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Task 13 fix round (2026-10-04) — an "in-run hop" is a plan-resume the plan
|
|
4
|
-
// chain ITSELF queued (runPostPhaseChain / runReauditPass /
|
|
5
|
-
// runContinuationChain: the next step, the re-run after a failed check).
|
|
6
|
-
// Marked at the source, never inferred from the command shape: the first step
|
|
7
|
-
// of an /abap-app run (lane/app-chain.ts) and a typed resume are NOT hops, so
|
|
8
|
-
// they show the board and the guarded notice once.
|
|
1
|
+
const PLAN_ORIGIN = { by: 'plan' };
|
|
9
2
|
export function createPlanHopTracker(slot) {
|
|
10
3
|
let chainQueued = null;
|
|
11
4
|
return {
|
|
12
5
|
queue(cmd) {
|
|
13
6
|
chainQueued = cmd;
|
|
14
|
-
slot.set(cmd);
|
|
7
|
+
slot.set(cmd, PLAN_ORIGIN);
|
|
15
8
|
},
|
|
16
9
|
take(cmd) {
|
|
17
10
|
const hop = chainQueued !== null && chainQueued === cmd;
|