staysfixed 0.9.0 → 0.9.1
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/CHANGELOG.md +44 -0
- package/docs/mcp.md +13 -0
- package/docs/running-it-in-ci.md +123 -1
- package/package.json +1 -1
- package/src/guard/name.js +41 -1
- package/src/v2/check.js +12 -0
- package/src/v2/init.js +8 -1
- package/src/v2/mcp/tools.js +8 -1
- package/src/v2/sealed.js +7 -1
- package/src/v2/ship.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,50 @@ numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
Nothing yet.
|
|
10
10
|
|
|
11
|
+
## [0.9.1] — 2026-08-30
|
|
12
|
+
|
|
13
|
+
Eleven more, from the same hunt that produced 0.9.0 — the findings it had already paid for.
|
|
14
|
+
Every one of these was decided by reading the code rather than by building another product to
|
|
15
|
+
reproduce it; the ones that genuinely need a Next.js app or a monorepo are written down and
|
|
16
|
+
left, because guessing at them would be worse.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Every API shape change on every project was sealed as LOSING DATA**, permanently, and no
|
|
21
|
+
agent could wave any of it through. The classifier reads the words in a finding — and it
|
|
22
|
+
was reading the difference's *description*, which is the TOOL's sentence, identical on every
|
|
23
|
+
finding that channel makes: "a renamed or dropped field shows up on its own". `dropped` is
|
|
24
|
+
a data-loss word. A seal is decided by what the PRODUCT said now — its addresses, its
|
|
25
|
+
values, its journey names — never by vocabulary no user wrote and nobody can change. A real
|
|
26
|
+
deletion still seals.
|
|
27
|
+
- **`ship` printed no coverage caveat at all**, in text or `--json`. It is the command that
|
|
28
|
+
decides what "working" MEANS from that moment on, which makes it the last place that should
|
|
29
|
+
stay quiet about what the check never walked. It says it now, in the same breath as the
|
|
30
|
+
good news, exactly as every other surface does.
|
|
31
|
+
- **`init` said "Nothing is being left out."** from a survey of which SURFACES this machine
|
|
32
|
+
can drive — on a project the same run reported as five of six doors never opened. And
|
|
33
|
+
`getting-started` tells the agent to repeat that sentence to the person.
|
|
34
|
+
- **The MCP server echoed an agent's own strings back unbounded and kept them for ever.** A
|
|
35
|
+
megabyte of summary came back as a megabyte of tool result. Capped, and the cut says so
|
|
36
|
+
rather than quietly keeping the first part.
|
|
37
|
+
- **`check --journeys recorded` reported a missing FILE called "recorded".** `--help` offers
|
|
38
|
+
the word and the MCP surface answers it honestly — written, not wired into a run yet. The
|
|
39
|
+
same question gets the same answer on both surfaces now.
|
|
40
|
+
- **Guard names that said nothing were accepted**: `a b c`, `1 2 3`, `TODO fix later` and
|
|
41
|
+
`AC-101 regression check` all passed every rule. That name is the sentence somebody reads
|
|
42
|
+
when the guard fails, six months from now. Refused now — and every name that actually says
|
|
43
|
+
something still passes.
|
|
44
|
+
- **Every command-line check wrote two rows** to the log `ship` reads, so any count of how
|
|
45
|
+
many checks had run was doubled.
|
|
46
|
+
- **`--only` printed "everything that worked still works"** about a slice, with five of six
|
|
47
|
+
guards filtered away and one of the five failing.
|
|
48
|
+
- **`ship` blessed a build nothing had ever looked at** — it matched by git commit, so an
|
|
49
|
+
uncommitted edit resolved to an earlier build that had been checked and was clean.
|
|
50
|
+
- `docs/running-it-in-ci.md` told you to copy `.github/workflows/staysfixed.yml` "as it is".
|
|
51
|
+
That file is not in the npm package. It is printed in full in the page now.
|
|
52
|
+
- `docs/mcp.md` still told you to fill the project path in by hand; `doctor --json` has
|
|
53
|
+
filled it in since 0.8.0, and a path filled in wrongly fails silently.
|
|
54
|
+
|
|
11
55
|
## [0.9.0] — 2026-08-30
|
|
12
56
|
|
|
13
57
|
Found the same way as 0.8.0 and one better: the published build was installed as a stranger
|
package/docs/mcp.md
CHANGED
|
@@ -293,3 +293,16 @@ Nobody who wired that up has to change anything. Everything version 1's picture
|
|
|
293
293
|
check did is also still reachable from the command line — `staysfixed check
|
|
294
294
|
--pictures`, `staysfixed approve` — and pictures still require a person to
|
|
295
295
|
approve them there, for the same reason they always did.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## You do not have to fill the path in yourself
|
|
300
|
+
|
|
301
|
+
The blocks above show `/absolute/path/to/your/project` because a page cannot know where you
|
|
302
|
+
are. `staysfixed doctor --json` can: its `wiring.mcp` carries the same block with `cwd`
|
|
303
|
+
already set to this project, ready to paste. Ask for that rather than editing by hand — a
|
|
304
|
+
path filled in wrongly fails silently, which is the worst way for this to go wrong.
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
npx staysfixed doctor --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.stringify(JSON.parse(s).wiring.mcp,null,2)))"
|
|
308
|
+
```
|
package/docs/running-it-in-ci.md
CHANGED
|
@@ -17,7 +17,7 @@ Three files make that work:
|
|
|
17
17
|
|
|
18
18
|
| | |
|
|
19
19
|
| --- | --- |
|
|
20
|
-
| `.github/workflows/staysfixed.yml` | The job.
|
|
20
|
+
| `.github/workflows/staysfixed.yml` | The job. It is printed in full at the bottom of this page, because it is not in the npm package. |
|
|
21
21
|
| `src/v2/ci.js` | Works out what to compare against, runs the check, writes the report, exits with the code that decides the job. |
|
|
22
22
|
| This file | What it can and cannot do up there, and why. |
|
|
23
23
|
|
|
@@ -304,3 +304,125 @@ What is **not** proved, and would only be settled by a real run:
|
|
|
304
304
|
- How long a paired run takes on a real product. On a two-command scratch project it was
|
|
305
305
|
about a second; a real project with a browser in it will be minutes, and the 30-minute
|
|
306
306
|
timeout in the workflow is a guess.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## The workflow itself
|
|
311
|
+
|
|
312
|
+
This file lives in the repository, not in the npm package — so if you installed Stays Fixed
|
|
313
|
+
from npm you cannot copy it off disk, and the line above used to send you looking for it.
|
|
314
|
+
Here it is in full. Save it as `.github/workflows/staysfixed.yml` in your own project.
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
# Prove that nothing which already worked has changed — on every pull request.
|
|
318
|
+
#
|
|
319
|
+
# WHY A BUILD SERVER IS A GOOD PLACE FOR THIS, and not a compromise. Everything Stays Fixed
|
|
320
|
+
# concludes rests on one claim: the difference was caused by the change and nothing else. A
|
|
321
|
+
# fresh runner has the same fonts every time, the same operating system, nothing else
|
|
322
|
+
# competing for a port, and no half-finished experiment left over from yesterday. It is a
|
|
323
|
+
# better machine for this job than anybody's laptop.
|
|
324
|
+
#
|
|
325
|
+
# WHAT IT COMPARES AGAINST. On a pull request: the commit your branch forked from — put back
|
|
326
|
+
# on this same runner with `git archive` and walked again, minutes apart. That is a full
|
|
327
|
+
# paired run, and it needs nothing stored from a previous job. On a push it works down to the
|
|
328
|
+
# last release, the last tag, or the commit before. Whichever it lands on is named in the job
|
|
329
|
+
# summary, because they are not equally strong.
|
|
330
|
+
#
|
|
331
|
+
# THE ONE LINE YOU MUST NOT DELETE is `fetch-depth: 0`. A shallow checkout answers "what did
|
|
332
|
+
# this branch fork from" with "this commit" — so the check would compare the build against
|
|
333
|
+
# itself, find nothing, and go green. Stays Fixed catches that and refuses, but the run is
|
|
334
|
+
# then worth nothing. Full history is what makes this work.
|
|
335
|
+
|
|
336
|
+
name: Stays Fixed
|
|
337
|
+
|
|
338
|
+
on:
|
|
339
|
+
pull_request:
|
|
340
|
+
push:
|
|
341
|
+
branches: [main, master]
|
|
342
|
+
|
|
343
|
+
# Nothing is published, released or tagged here, and no token is used for anything.
|
|
344
|
+
permissions:
|
|
345
|
+
contents: read
|
|
346
|
+
|
|
347
|
+
# Two runs of the same branch would be two builds on one machine fighting over ports and
|
|
348
|
+
# user data directories. Sequential, never simultaneous, is a rule of the whole tool.
|
|
349
|
+
concurrency:
|
|
350
|
+
group: staysfixed-${{ github.workflow }}-${{ github.ref }}
|
|
351
|
+
cancel-in-progress: true
|
|
352
|
+
|
|
353
|
+
jobs:
|
|
354
|
+
check:
|
|
355
|
+
name: Has anything that worked changed?
|
|
356
|
+
runs-on: ubuntu-latest
|
|
357
|
+
timeout-minutes: 30
|
|
358
|
+
|
|
359
|
+
steps:
|
|
360
|
+
- uses: actions/checkout@v4
|
|
361
|
+
with:
|
|
362
|
+
# Read the paragraph above before changing this.
|
|
363
|
+
fetch-depth: 0
|
|
364
|
+
|
|
365
|
+
- uses: actions/setup-node@v4
|
|
366
|
+
with:
|
|
367
|
+
node-version: '22'
|
|
368
|
+
|
|
369
|
+
- name: Install
|
|
370
|
+
run: npm ci || npm install
|
|
371
|
+
|
|
372
|
+
# Only needed if anything you check is a web page. Delete these three lines for a
|
|
373
|
+
# command line tool or a library — the check will say what it could not look at either way.
|
|
374
|
+
- name: A browser to look at web pages with
|
|
375
|
+
run: npx playwright install chromium
|
|
376
|
+
|
|
377
|
+
- name: Is Stays Fixed set up in this project?
|
|
378
|
+
id: setup
|
|
379
|
+
run: |
|
|
380
|
+
# Two separate looks on purpose. One `ls` over both patterns fails whenever
|
|
381
|
+
# either of them matches nothing, which would report a configured project as
|
|
382
|
+
# unconfigured and quietly skip the whole check.
|
|
383
|
+
if ls staysfixed.config.* >/dev/null 2>&1 || ls .staysfixed/config.* >/dev/null 2>&1; then
|
|
384
|
+
echo "configured=yes" >> "$GITHUB_OUTPUT"
|
|
385
|
+
else
|
|
386
|
+
echo "configured=no" >> "$GITHUB_OUTPUT"
|
|
387
|
+
{
|
|
388
|
+
echo "## Stays Fixed"
|
|
389
|
+
echo
|
|
390
|
+
echo "This project has no Stays Fixed settings file, so **nothing was checked**."
|
|
391
|
+
echo "This job is green because nothing was claimed — not because anything was proved."
|
|
392
|
+
echo
|
|
393
|
+
echo 'Run `npx staysfixed init` in your project to set it up.'
|
|
394
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
# Works out what to compare against, runs the check, writes the plain-English report
|
|
398
|
+
# into the job summary, and exits with the code that decides this job:
|
|
399
|
+
#
|
|
400
|
+
# 0 nothing that already worked has changed
|
|
401
|
+
# 1 something changed that nobody accounted for
|
|
402
|
+
# 2 the check could not run, or there was nothing to compare against — which is
|
|
403
|
+
# NOT a pass, and is why this step has no `continue-on-error`
|
|
404
|
+
- name: Check
|
|
405
|
+
if: steps.setup.outputs.configured == 'yes'
|
|
406
|
+
run: |
|
|
407
|
+
# In your own project Stays Fixed is a dependency. In the Stays Fixed repository
|
|
408
|
+
# itself it is the source you are looking at. Both paths are tried so this file
|
|
409
|
+
# can be copied straight out of one into the other.
|
|
410
|
+
if [ -f node_modules/staysfixed/src/v2/ci.js ]; then
|
|
411
|
+
node node_modules/staysfixed/src/v2/ci.js
|
|
412
|
+
else
|
|
413
|
+
node src/v2/ci.js
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# Every observation both builds produced, the verdict, and what it compared against.
|
|
417
|
+
# Kept even when the job fails — especially then.
|
|
418
|
+
- name: Keep the evidence
|
|
419
|
+
if: always() && steps.setup.outputs.configured == 'yes'
|
|
420
|
+
uses: actions/upload-artifact@v4
|
|
421
|
+
with:
|
|
422
|
+
name: staysfixed-${{ github.run_id }}
|
|
423
|
+
path: |
|
|
424
|
+
.staysfixed/ci
|
|
425
|
+
.staysfixed/v2
|
|
426
|
+
if-no-files-found: ignore
|
|
427
|
+
retention-days: 14
|
|
428
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "staysfixed",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Prove that what already worked still works after an agent changed the code. Picture checks, guards for fixed bugs, a pre-release walkthrough, and known-good markers — as a CLI and as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/guard/name.js
CHANGED
|
@@ -26,6 +26,9 @@ export const NAME_RULE_EXPLAINER =
|
|
|
26
26
|
const MAX_LENGTH = 120;
|
|
27
27
|
|
|
28
28
|
/** Words that start a test name rather than describe the app. */
|
|
29
|
+
/** Words that begin a note to yourself rather than a promise about the product. */
|
|
30
|
+
const PLACEHOLDERS = new Set(['todo', 'fixme', 'wip', 'xxx', 'tbd', 'temp', 'placeholder', 'hack']);
|
|
31
|
+
|
|
29
32
|
const TEST_SPEAK = new Set([
|
|
30
33
|
'test',
|
|
31
34
|
'tests',
|
|
@@ -100,7 +103,7 @@ const VERBS = new Set([
|
|
|
100
103
|
]);
|
|
101
104
|
|
|
102
105
|
/**
|
|
103
|
-
* @typedef {'empty'|'long'|'path'|'id'|'symbols'|'caps'|'identifier'|'testspeak'|'short'} RefusalKind
|
|
106
|
+
* @typedef {'empty'|'long'|'path'|'id'|'symbols'|'caps'|'identifier'|'testspeak'|'short'|'placeholder'|'not-a-sentence'|'mostly-numbers'|'ticket'} RefusalKind
|
|
104
107
|
*/
|
|
105
108
|
|
|
106
109
|
/**
|
|
@@ -194,6 +197,43 @@ function refuse(name) {
|
|
|
194
197
|
};
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
// Three words is a shape, not a sentence. `a b c`, `1 2 3`, `TODO fix later` and
|
|
201
|
+
// `AC-101 regression check` all cleared every rule above, and each of them is a guard
|
|
202
|
+
// nobody will understand in six months — which is the entire job of this name.
|
|
203
|
+
if (PLACEHOLDERS.has(first)) {
|
|
204
|
+
return {
|
|
205
|
+
kind: 'placeholder',
|
|
206
|
+
why: `Starting with "${words[0]}" is a note to yourself, not a promise about the product. Say what should still be true, so the failure reads as that sentence.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A ticket reference is not a behaviour, and this file already says so about "#" and "::".
|
|
211
|
+
// `AC-101 regression check` cleared every other rule and names nothing a person could act on.
|
|
212
|
+
const ticket = words.find((w) => /^[A-Za-z]{2,}[-_]\d+$/.test(w));
|
|
213
|
+
if (ticket) {
|
|
214
|
+
return {
|
|
215
|
+
kind: 'ticket',
|
|
216
|
+
why: `"${ticket}" is a ticket reference, not a behaviour. Put it in "link" and say here what should still be true.`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const realWords = words.filter((w) => /[A-Za-z]{4,}/.test(w));
|
|
221
|
+
if (realWords.length < 2) {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'not-a-sentence',
|
|
224
|
+
why: 'That does not read as a sentence about the product — there are almost no words in it. Write what should still be true, in the words you would say out loud, like "the sidebar still collapses".',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const letters = (text.match(/[A-Za-z]/g) ?? []).length;
|
|
229
|
+
const digits = (text.match(/\d/g) ?? []).length;
|
|
230
|
+
if (digits > letters) {
|
|
231
|
+
return {
|
|
232
|
+
kind: 'mostly-numbers',
|
|
233
|
+
why: 'That is mostly numbers. A guard name is the sentence somebody reads when it fails, so put the ticket number in "link" and say the behaviour here.',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
197
237
|
return null;
|
|
198
238
|
}
|
|
199
239
|
|
package/src/v2/check.js
CHANGED
|
@@ -1916,6 +1916,18 @@ async function gatherJourneys({ root, config, options }) {
|
|
|
1916
1916
|
|
|
1917
1917
|
const named =
|
|
1918
1918
|
options.journeys && !['code', 'config', 'suite'].includes(options.journeys) ? options.journeys : null;
|
|
1919
|
+
// `recorded` is a word this tool knows and `check --help` offers it — it is simply not
|
|
1920
|
+
// wired into a run yet. The MCP surface says exactly that; the command line fell through to
|
|
1921
|
+
// the branch above, treated the word as a FILE PATH, and answered that a file called
|
|
1922
|
+
// "recorded" was missing. The same question has to get the same answer on both.
|
|
1923
|
+
if (options.journeys === 'recorded') {
|
|
1924
|
+
throw new StaysFixedError(
|
|
1925
|
+
'Replaying a recorded session is written and not wired into a run yet, so nothing was checked.',
|
|
1926
|
+
{
|
|
1927
|
+
hint: 'Leave --journeys out to use the steps each adapter reads from your source, pass `suite` to walk your own test suite, or pass the path to a journeys file.',
|
|
1928
|
+
},
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1919
1931
|
if (named) journeys.push(...(await readJourneyFile(path.resolve(root, named))));
|
|
1920
1932
|
|
|
1921
1933
|
// The project's own test suite, when somebody asked for it in those words and never
|
package/src/v2/init.js
CHANGED
|
@@ -1579,7 +1579,14 @@ function whatItCovers(readiness) {
|
|
|
1579
1579
|
else parts.push('Right now a check here covers nothing in full.');
|
|
1580
1580
|
if (waiting.length > 0) parts.push(`${plainList(waiting, true)} ${waiting.length === 1 ? 'is' : 'are'} not covered yet, and the list below says exactly what is in the way and who has to do it.`);
|
|
1581
1581
|
if (notCovered.length > 0) parts.push(`${plainList(notCovered, true)} ${notCovered.length === 1 ? 'is' : 'are'} not checked at all, so a clean result says nothing whatever about ${notCovered.length === 1 ? 'it' : 'them'}.`);
|
|
1582
|
-
|
|
1582
|
+
// "on this machine", because that is the only thing this sentence knows. It is built from
|
|
1583
|
+
// SURFACES — which kinds of product can be watched here — and says nothing whatever about
|
|
1584
|
+
// how much of this project a run actually walks. Unqualified, it read as a coverage
|
|
1585
|
+
// promise, and `getting-started` tells the agent to repeat it to the person: measured
|
|
1586
|
+
// 2026-08-30 on a project the same run reported as five of six doors never opened.
|
|
1587
|
+
if (partly.length === 0 && notCovered.length === 0 && covered.length > 0) {
|
|
1588
|
+
parts.push('Nothing is being left out on this machine — though how much of the project a run actually walks is a separate question, and every check answers it.');
|
|
1589
|
+
}
|
|
1583
1590
|
|
|
1584
1591
|
// The hole that never closes, named with the language that causes it. "Not covered yet"
|
|
1585
1592
|
// reads as a job somebody will get to; this one is nobody's job and saying so is the
|
package/src/v2/mcp/tools.js
CHANGED
|
@@ -1661,7 +1661,14 @@ function problem(message) {
|
|
|
1661
1661
|
function text(v) {
|
|
1662
1662
|
if (typeof v !== 'string') return null;
|
|
1663
1663
|
const s = v.trim();
|
|
1664
|
-
|
|
1664
|
+
if (s === '') return null;
|
|
1665
|
+
// Capped, because every one of these is a string an AGENT chose and several of them are
|
|
1666
|
+
// echoed straight back in the reply and then written into the store for ever. A megabyte
|
|
1667
|
+
// of summary came back as a megabyte of tool result and stayed there. Nothing legitimate
|
|
1668
|
+
// here is long: a reason, a finding id, a surface name. Cutting says so out loud rather
|
|
1669
|
+
// than quietly keeping the first part.
|
|
1670
|
+
const MOST = 4000;
|
|
1671
|
+
return s.length <= MOST ? s : `${s.slice(0, MOST)} … (cut here: this was ${s.length} characters, and nothing this tool asks for is that long)`;
|
|
1665
1672
|
}
|
|
1666
1673
|
|
|
1667
1674
|
/**
|
package/src/v2/sealed.js
CHANGED
|
@@ -358,7 +358,13 @@ function readFinding(finding) {
|
|
|
358
358
|
const differences = finding.differences ?? [];
|
|
359
359
|
for (const d of differences) {
|
|
360
360
|
add(d.path, d.path);
|
|
361
|
-
|
|
361
|
+
// NOT `d.describe`. That sentence is the TOOL's, not the product's — it explains what a
|
|
362
|
+
// channel watches and is the same on every finding that channel ever produces. The API
|
|
363
|
+
// shape channel's says "a renamed or dropped field shows up on its own", and `dropped`
|
|
364
|
+
// is a data-loss word, so every API shape change on every project was sealed as losing
|
|
365
|
+
// data, permanently, and no agent could wave any of it through. A seal has to be decided
|
|
366
|
+
// by what the PRODUCT said — its addresses, its values, its journey names — never by the
|
|
367
|
+
// tool's own vocabulary, which no user wrote and nobody can change.
|
|
362
368
|
add(faceOf(d.reference), d.path);
|
|
363
369
|
add(faceOf(d.candidate), d.path);
|
|
364
370
|
add(d.journey, `the ${d.journey} journey`);
|
package/src/v2/ship.js
CHANGED
|
@@ -211,10 +211,13 @@ export async function onShip(opts = {}) {
|
|
|
211
211
|
result.cut = cut.unchanged !== true;
|
|
212
212
|
result.unchanged = cut.unchanged === true;
|
|
213
213
|
|
|
214
|
+
const missed = await whatTheCheckMissed(store);
|
|
215
|
+
|
|
214
216
|
if (cut.unchanged) {
|
|
215
217
|
result.lines = [
|
|
216
218
|
`${product} ${release.describe}`,
|
|
217
219
|
`That build was already what ${product} calls working, so nothing moved and no waivers were retired. Recording a release twice is safe.`,
|
|
220
|
+
...(missed ? [missed] : []),
|
|
218
221
|
];
|
|
219
222
|
result.summary = `${product} ${release.what} was already the reference — nothing changed.`;
|
|
220
223
|
return result;
|
|
@@ -228,6 +231,8 @@ export async function onShip(opts = {}) {
|
|
|
228
231
|
// ran only once, so part of this reference has no steadiness record behind it.
|
|
229
232
|
...(cut.stability.measuredJourneys < cut.stability.journeys ? [cut.stability.note] : []),
|
|
230
233
|
'Nobody has to approve anything. The next check compares against this.',
|
|
234
|
+
// Said in the same breath as the good news, exactly as every other surface says it.
|
|
235
|
+
...(missed ? [missed] : []),
|
|
231
236
|
];
|
|
232
237
|
result.summary = cut.summary;
|
|
233
238
|
return result;
|
|
@@ -247,6 +252,33 @@ export async function onShip(opts = {}) {
|
|
|
247
252
|
|
|
248
253
|
// ---------------------------------------------------------------------------
|
|
249
254
|
// What just shipped?
|
|
255
|
+
/**
|
|
256
|
+
* What the last check did NOT look at, said here too.
|
|
257
|
+
*
|
|
258
|
+
* `ship` is the one command that decides what "working" MEANS from now on, and it printed no
|
|
259
|
+
* coverage caveat at all — not in the text, not in `--json`. Every other surface says it, in
|
|
260
|
+
* the same breath as the good news, because a green result on a product with doors nobody has
|
|
261
|
+
* ever opened is true and is not what it looks like. The command that turns that result into
|
|
262
|
+
* the standard is the last place that should stay quiet about it.
|
|
263
|
+
*
|
|
264
|
+
* @param {Store} store
|
|
265
|
+
* @returns {Promise<string|null>}
|
|
266
|
+
*/
|
|
267
|
+
async function whatTheCheckMissed(store) {
|
|
268
|
+
try {
|
|
269
|
+
const raw = JSON.parse(await fsp.readFile(path.join(store.dir, 'last-check.json'), 'utf8'));
|
|
270
|
+
const coverage = raw?.result?.coverage ?? null;
|
|
271
|
+
if (!coverage) return null;
|
|
272
|
+
const { whatWasNotChecked } = await import('./check.js');
|
|
273
|
+
const said = whatWasNotChecked(coverage);
|
|
274
|
+
return typeof said === 'string' && said.trim() ? said.trim() : null;
|
|
275
|
+
} catch {
|
|
276
|
+
// No record, or unreadable. Saying nothing is right here — inventing a caveat would be
|
|
277
|
+
// its own kind of lie.
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
250
282
|
// ---------------------------------------------------------------------------
|
|
251
283
|
|
|
252
284
|
/**
|