staysfixed 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +342 -0
- package/README.md +191 -55
- package/docs/design-v2.md +24 -4
- package/docs/getting-started.md +18 -5
- package/docs/guards.md +2 -2
- package/docs/how-v2-works.md +12 -11
- package/docs/mcp.md +17 -8
- package/docs/settings.md +549 -0
- package/docs/watching.md +10 -4
- package/examples/staysfixed.config.electron.js +17 -6
- package/examples/staysfixed.config.web.js +22 -5
- package/package.json +2 -1
- package/src/cli/index.js +55 -46
- package/src/cli/watch-flags.js +54 -0
- package/src/core/config.js +23 -3
- package/src/guard/run.js +49 -1
- package/src/report/console.js +15 -2
- package/src/v2/adapters/android-driver.js +6 -1
- package/src/v2/adapters/android.js +97 -2
- package/src/v2/adapters/contract.js +42 -5
- package/src/v2/adapters/electron.js +72 -6
- package/src/v2/adapters/http.js +11 -2
- package/src/v2/adapters/ios-driver.js +64 -14
- package/src/v2/adapters/ios.js +247 -25
- package/src/v2/adapters/process.js +728 -66
- package/src/v2/adapters/python.js +495 -0
- package/src/v2/adapters/source.js +373 -18
- package/src/v2/adapters/web-driver.js +94 -24
- package/src/v2/adapters/web.js +142 -9
- package/src/v2/adapters/windows.js +18 -1
- package/src/v2/browsers.js +9 -1
- package/src/v2/cause.js +61 -17
- package/src/v2/check.js +530 -66
- package/src/v2/ci.js +130 -35
- package/src/v2/cli.js +42 -24
- package/src/v2/cluster.js +164 -13
- package/src/v2/coverage.js +43 -176
- package/src/v2/detect.js +308 -60
- package/src/v2/doctor.js +285 -45
- package/src/v2/init.js +162 -61
- package/src/v2/intent.js +9 -23
- package/src/v2/journeys/from-suite.js +336 -30
- package/src/v2/journeys/index.js +99 -6
- package/src/v2/mcp/tools.js +10 -11
- package/src/v2/normalise.js +169 -23
- package/src/v2/observation.js +19 -33
- package/src/v2/rank.js +216 -23
- package/src/v2/reference.js +40 -10
- package/src/v2/remote.js +113 -18
- package/src/v2/run.js +103 -14
- package/src/v2/sealed.js +0 -20
- package/src/v2/selfcheck.js +190 -13
- package/src/v2/ship.js +29 -5
- package/src/v2/store.js +67 -1
- package/src/v2/types.js +12 -2
- package/src/v2/waiver.js +64 -54
- package/src/v2/watch/events.js +60 -215
- package/src/v2/watch/focus.js +14 -4
- package/src/v2/watch/panel.js +167 -17
package/src/v2/coverage.js
CHANGED
|
@@ -98,7 +98,14 @@ import { listBuilds, listCaptures, loadCapture, referencePointer } from './store
|
|
|
98
98
|
* @property {string} [buildId]
|
|
99
99
|
* @property {string[]} paths Non-contract observation addresses, and every
|
|
100
100
|
* prefix of each, so a lookup is one set hit.
|
|
101
|
-
* @property {string[]} [doors] Door keys the journey's steps name
|
|
101
|
+
* @property {string[]} [doors] Door keys the journey's steps name, for steps that
|
|
102
|
+
* named a door with nothing to tell apart from
|
|
103
|
+
* another of the same name. See doorKey.
|
|
104
|
+
* @property {string[]} [doorAddresses] Full door addresses, for steps that were specific
|
|
105
|
+
* enough to build one. A route step knows its verb,
|
|
106
|
+
* and GET /x and POST /x are two doors that share a
|
|
107
|
+
* doorKey — so a step that knows which one it knocked
|
|
108
|
+
* on lands here instead, and the other stays shut.
|
|
102
109
|
* @property {string[]} [touchedFiles]
|
|
103
110
|
* @property {string[]} [touchedFunctions] 'file:name', from the suite's own coverage.
|
|
104
111
|
* @property {number} [functionsNotListed] Functions that ran and were cut from the list to
|
|
@@ -412,9 +419,21 @@ export function walkFromCapture(capture, journey) {
|
|
|
412
419
|
paths: touched.paths,
|
|
413
420
|
};
|
|
414
421
|
if (journey?.steps) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
422
|
+
// Two lists, because a doorKey is kind and name only. That is right for an IPC channel or
|
|
423
|
+
// an exported name, where the name IS the door; it is wrong for a route, where GET /basket
|
|
424
|
+
// and POST /basket share a key and are two different doors. A step that knows which one it
|
|
425
|
+
// knocked on says so with `doorDetail`, and goes in the exact list — otherwise walking GET
|
|
426
|
+
// would report POST as walked too, which is the coverage ledger lying in the one direction
|
|
427
|
+
// it must never lie in.
|
|
428
|
+
const named = journey.steps.filter((s) => typeof s.door === 'string' && typeof s.kind === 'string');
|
|
429
|
+
const exact = named.filter((s) => typeof s.doorDetail === 'string' && s.doorDetail !== '');
|
|
430
|
+
const byName = named.filter((s) => typeof s.doorDetail !== 'string' || s.doorDetail === '');
|
|
431
|
+
if (byName.length > 0) walk.doors = byName.map((s) => doorKey({ kind: String(s.kind), name: String(s.door) }));
|
|
432
|
+
if (exact.length > 0) {
|
|
433
|
+
walk.doorAddresses = exact.map((s) =>
|
|
434
|
+
doorAddress({ kind: String(s.kind), name: String(s.door), detail: String(s.doorDetail), file: typeof s.doorFile === 'string' ? s.doorFile : undefined }),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
418
437
|
}
|
|
419
438
|
if (journey?.touched?.files) walk.touchedFiles = journey.touched.files;
|
|
420
439
|
if (journey?.touched?.functions) walk.touchedFunctions = journey.touched.functions;
|
|
@@ -442,7 +461,7 @@ const ADDRESS_RULE = new Set(['ipc', 'route', 'export', 'env']);
|
|
|
442
461
|
* @returns {{state: 'opened'|'reached', how: string}|null}
|
|
443
462
|
*/
|
|
444
463
|
export function whatTheWalkDid(door, walk, paths) {
|
|
445
|
-
if (walk.doors?.includes(doorKey(door))) {
|
|
464
|
+
if (walk.doorAddresses?.includes(door.address) || walk.doors?.includes(doorKey(door))) {
|
|
446
465
|
return { state: 'opened', how: `"${walk.journey}" has a step that knocks on it directly.` };
|
|
447
466
|
}
|
|
448
467
|
if (ADDRESS_RULE.has(door.kind)) {
|
|
@@ -783,172 +802,6 @@ export async function ledger(store, product, opts = {}) {
|
|
|
783
802
|
});
|
|
784
803
|
}
|
|
785
804
|
|
|
786
|
-
/**
|
|
787
|
-
* The same picture, for one run.
|
|
788
|
-
*
|
|
789
|
-
* A verdict does not carry its observations, so on its own this can only report totals — and
|
|
790
|
-
* it says `knows: 'counts only'` rather than pretending to a per-door answer it does not
|
|
791
|
-
* have. Hand it the doors and the walks from that run and it upgrades to the full ledger.
|
|
792
|
-
*
|
|
793
|
-
* @param {Verdict} verdict
|
|
794
|
-
* @param {{doors?: (Door|DoorFact)[], walks?: Walk[]}} [opts]
|
|
795
|
-
* @returns {Ledger}
|
|
796
|
-
*/
|
|
797
|
-
export function coverageOf(verdict, opts = {}) {
|
|
798
|
-
/** @type {Coverage} */
|
|
799
|
-
const coverage = verdict.coverage ?? { paths: 0, journeys: 0, byChannel: {}, gaps: [] };
|
|
800
|
-
if (opts.doors && opts.walks) {
|
|
801
|
-
const doors = opts.doors.map((d) => ('address' in d ? d : doorFact(d)));
|
|
802
|
-
return buildLedger({
|
|
803
|
-
product: verdict.product,
|
|
804
|
-
doors,
|
|
805
|
-
walks: opts.walks,
|
|
806
|
-
byChannel: coverage.byChannel,
|
|
807
|
-
captures: opts.walks.length,
|
|
808
|
-
builds: 1,
|
|
809
|
-
at: verdict.startedAt,
|
|
810
|
-
caveats: [`This is one run — ${nameRun(verdict)} — not everything this tool has ever walked.`],
|
|
811
|
-
gaps: coverage.gaps ?? [],
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const doors = coverage.doorsKnown ?? 0;
|
|
816
|
-
const opened = coverage.doorsWalked ?? 0;
|
|
817
|
-
/** @type {string[]} */
|
|
818
|
-
const caveats = [
|
|
819
|
-
`This is one run — ${nameRun(verdict)} — not everything this tool has ever walked.`,
|
|
820
|
-
'A verdict carries totals rather than addresses, so this cannot name which doors were left shut. Call ledger(store, product) for that.',
|
|
821
|
-
];
|
|
822
|
-
if (verdict.mode === 'stored-record') {
|
|
823
|
-
caveats.push(
|
|
824
|
-
verdict.modeWarning
|
|
825
|
-
?? 'The old build was not booted. This run was compared against observations stored the last time it ran, which lets back in every difference that comes from the day being different.',
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
if (doors === 0) {
|
|
829
|
-
caveats.push('Nothing counted the doors on this run, so there is no denominator, and "nothing changed" here means only "nothing I looked at changed".');
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
return {
|
|
833
|
-
product: verdict.product,
|
|
834
|
-
at: verdict.startedAt,
|
|
835
|
-
knows: 'counts only',
|
|
836
|
-
doors,
|
|
837
|
-
opened,
|
|
838
|
-
reached: 0,
|
|
839
|
-
never: Math.max(0, doors - opened),
|
|
840
|
-
unwalkable: 0,
|
|
841
|
-
work: Math.max(0, doors - opened),
|
|
842
|
-
irreversible: 0,
|
|
843
|
-
entries: [],
|
|
844
|
-
byKind: {},
|
|
845
|
-
journeys: coverage.journeys ?? 0,
|
|
846
|
-
byJourneySource: {},
|
|
847
|
-
byChannel: coverage.byChannel ?? {},
|
|
848
|
-
captures: coverage.journeys ?? 0,
|
|
849
|
-
builds: 1,
|
|
850
|
-
caveats,
|
|
851
|
-
gaps: coverage.gaps ?? [],
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* @param {Verdict} verdict
|
|
857
|
-
* @returns {string}
|
|
858
|
-
*/
|
|
859
|
-
function nameRun(verdict) {
|
|
860
|
-
const candidate = verdict.candidate?.version || verdict.candidate?.id || 'this build';
|
|
861
|
-
return `${candidate}, ${verdict.mode === 'paired' ? 'against the old build booted live' : 'against the stored record'}`;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// ---------------------------------------------------------------------------
|
|
865
|
-
// Saying it out loud
|
|
866
|
-
// ---------------------------------------------------------------------------
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* The ledger in plain English, honest and specific, one line each.
|
|
870
|
-
*
|
|
871
|
-
* The headline is the number nobody wants to publish, and it goes first on purpose:
|
|
872
|
-
* "452 doors, 61 opened, 391 never opened. A clean result says nothing about those 391."
|
|
873
|
-
*
|
|
874
|
-
* There is no percentage anywhere in here, and that is not an oversight. A percentage
|
|
875
|
-
* invites a target, a target invites gaming, and a gamed coverage number is worse than none
|
|
876
|
-
* because somebody believes it.
|
|
877
|
-
*
|
|
878
|
-
* @param {Ledger} led
|
|
879
|
-
* @returns {string[]} join with a space for a paragraph, or a newline for a list
|
|
880
|
-
*/
|
|
881
|
-
export function describeCoverage(led) {
|
|
882
|
-
/** @type {string[]} */
|
|
883
|
-
const lines = [];
|
|
884
|
-
|
|
885
|
-
if (led.doors === 0) {
|
|
886
|
-
lines.push('Nothing here knows how many doors this product has, so there is no honest way to say how much of it was checked.');
|
|
887
|
-
} else {
|
|
888
|
-
const parts = [count(led.doors, 'door'), `${led.opened} opened`];
|
|
889
|
-
if (led.reached > 0) parts.push(`${led.reached} in code that ran but never addressed`);
|
|
890
|
-
parts.push(`${led.never} never opened`);
|
|
891
|
-
lines.push(`${parts.join(', ')}.`);
|
|
892
|
-
if (led.never > 0) lines.push(`A clean result says nothing about those ${led.never}.`);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (led.unwalkable > 0) {
|
|
896
|
-
const permanent = led.irreversible > 0
|
|
897
|
-
? `${led.irreversible} that would spend money, send a message or destroy something and are stopped at the call on purpose`
|
|
898
|
-
: 'doors there is nothing to knock on';
|
|
899
|
-
lines.push(
|
|
900
|
-
`Of those ${led.never}, ${led.unwalkable} can never be opened from here — settings that are read rather than called, names built while the program runs, and ${permanent}. That leaves ${led.work} that could be covered and are not.`,
|
|
901
|
-
);
|
|
902
|
-
} else if (led.work > 0 && led.doors > 0) {
|
|
903
|
-
lines.push(`All ${led.work} of the unopened ones could be covered.`);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Every kind, not the worst four. There are five kinds of door in the whole tool, so
|
|
907
|
-
// cutting the list saved one line and dropped a whole category of the product out of the
|
|
908
|
-
// only sentence that says how much of it is covered.
|
|
909
|
-
const kinds = Object.entries(led.byKind)
|
|
910
|
-
.filter(([, k]) => k.doors > 0)
|
|
911
|
-
.sort((a, b) => b[1].never - a[1].never)
|
|
912
|
-
.map(([kind, k]) => `${k.opened} of ${k.doors} ${k.doors === 1 ? KIND_ONE[kind] ?? kind : KIND_MANY[kind] ?? kind}`);
|
|
913
|
-
if (kinds.length > 0) lines.push(`By kind: ${kinds.join(', ')}.`);
|
|
914
|
-
|
|
915
|
-
if (led.journeys === 0) {
|
|
916
|
-
lines.push('No journey has ever been walked against this product, so nothing here rests on anything.');
|
|
917
|
-
} else {
|
|
918
|
-
const sources = Object.entries(led.byJourneySource)
|
|
919
|
-
.filter(([, n]) => n > 0)
|
|
920
|
-
.sort((a, b) => b[1] - a[1])
|
|
921
|
-
.map(([source, n]) => `${n} ${SOURCE_PHRASE[source] ?? source}`);
|
|
922
|
-
lines.push(
|
|
923
|
-
`${count(led.journeys, 'journey')} produced ${count(led.captures, 'capture')}${sources.length > 0 ? ` — ${sources.join(', ')}` : ''}.`,
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
for (const caveat of led.caveats) lines.push(caveat);
|
|
928
|
-
if (led.gaps.length > 0) {
|
|
929
|
-
lines.push(`${count(led.gaps.length, 'other thing')} could not be looked at, and each one says what would fix it.`);
|
|
930
|
-
}
|
|
931
|
-
return lines;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/** @type {Record<string, string>} */
|
|
935
|
-
const SOURCE_PHRASE = {
|
|
936
|
-
code: 'read out of the code',
|
|
937
|
-
suite: "harvested from the project's own tests",
|
|
938
|
-
recorded: 'recorded from a real session',
|
|
939
|
-
explored: 'found by an agent exploring',
|
|
940
|
-
unknown: 'of unrecorded origin',
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* @param {number} n
|
|
945
|
-
* @param {string} noun
|
|
946
|
-
* @returns {string}
|
|
947
|
-
*/
|
|
948
|
-
function count(n, noun) {
|
|
949
|
-
return `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
805
|
// ---------------------------------------------------------------------------
|
|
953
806
|
// The work queue
|
|
954
807
|
// ---------------------------------------------------------------------------
|
|
@@ -958,7 +811,7 @@ function count(n, noun) {
|
|
|
958
811
|
* @property {number} [worst] How many jobs to hand back. Default 12.
|
|
959
812
|
* @property {boolean} [includeUnwalkable] Include doors nothing here could ever open. Off:
|
|
960
813
|
* they belong in the honest total, not in a queue,
|
|
961
|
-
* and
|
|
814
|
+
* and the ledger counts them either way.
|
|
962
815
|
* @property {number} [minDoors] Ignore families smaller than this. Default 1.
|
|
963
816
|
*/
|
|
964
817
|
|
|
@@ -1108,11 +961,25 @@ export function toCoverage(led, opts = {}) {
|
|
|
1108
961
|
const out = [...led.gaps];
|
|
1109
962
|
if (led.doors > 0 && led.never > 0) {
|
|
1110
963
|
out.push({
|
|
1111
|
-
|
|
1112
|
-
|
|
964
|
+
// A product with one door read "1 of this product's 1 doors have never been opened",
|
|
965
|
+
// which is the sentence a reader stops believing the rest of the report over. The count
|
|
966
|
+
// is the whole point of the line, so it is worth the four words it costs to say it in
|
|
967
|
+
// English.
|
|
968
|
+
what: led.doors === 1
|
|
969
|
+
? "This product's only door has never been opened by this tool."
|
|
970
|
+
: led.never === 1
|
|
971
|
+
? `1 of this product's ${led.doors} doors has never been opened by this tool.`
|
|
972
|
+
: `${led.never} of this product's ${led.doors} doors have never been opened by this tool.`,
|
|
973
|
+
why: led.never === 1
|
|
974
|
+
? 'No journey reaches it, so a break behind it would not show up in any run — clean or otherwise.'
|
|
975
|
+
: 'No journey reaches them, so a break behind one of them would not show up in any run — clean or otherwise.',
|
|
1113
976
|
unlockedBy: led.work > 0
|
|
1114
|
-
?
|
|
1115
|
-
|
|
977
|
+
? led.work === 1
|
|
978
|
+
? 'It could be covered by a journey that reaches it — read out of your source, or named by hand in a journeys file.'
|
|
979
|
+
: `${led.work} of them could be covered by journeys that reach them — read out of your source, or named by hand in a journeys file.`
|
|
980
|
+
: led.never === 1
|
|
981
|
+
? 'Nothing. It is a door this tool cannot open from here, and it says why.'
|
|
982
|
+
: 'Nothing. Every one of them is a door this tool cannot open from here, and each says why.',
|
|
1116
983
|
channel: 'contract',
|
|
1117
984
|
doors: led.never,
|
|
1118
985
|
});
|