staysfixed 0.7.2 → 0.9.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +429 -0
  2. package/README.md +193 -57
  3. package/docs/design-v2.md +24 -4
  4. package/docs/getting-started.md +19 -6
  5. package/docs/guards.md +2 -2
  6. package/docs/how-v2-works.md +12 -11
  7. package/docs/mcp.md +17 -8
  8. package/docs/settings.md +564 -0
  9. package/docs/watching.md +10 -4
  10. package/examples/staysfixed.config.electron.js +17 -6
  11. package/examples/staysfixed.config.web.js +22 -5
  12. package/package.json +2 -1
  13. package/src/cli/index.js +55 -46
  14. package/src/cli/status.js +45 -1
  15. package/src/cli/watch-flags.js +54 -0
  16. package/src/core/config.js +54 -3
  17. package/src/core/paths.js +15 -0
  18. package/src/guard/run.js +70 -3
  19. package/src/report/console.js +50 -6
  20. package/src/run.js +11 -0
  21. package/src/types.js +3 -0
  22. package/src/v2/adapters/android-driver.js +6 -1
  23. package/src/v2/adapters/android.js +97 -2
  24. package/src/v2/adapters/child.js +101 -0
  25. package/src/v2/adapters/contract.js +42 -5
  26. package/src/v2/adapters/electron.js +72 -6
  27. package/src/v2/adapters/http.js +18 -11
  28. package/src/v2/adapters/ios-driver.js +64 -14
  29. package/src/v2/adapters/ios.js +247 -25
  30. package/src/v2/adapters/process.js +783 -71
  31. package/src/v2/adapters/python.js +495 -0
  32. package/src/v2/adapters/source.js +373 -18
  33. package/src/v2/adapters/web-driver.js +134 -24
  34. package/src/v2/adapters/web.js +149 -18
  35. package/src/v2/adapters/windows.js +18 -1
  36. package/src/v2/browsers.js +66 -3
  37. package/src/v2/cause.js +61 -17
  38. package/src/v2/check.js +653 -69
  39. package/src/v2/ci.js +130 -35
  40. package/src/v2/cli.js +65 -42
  41. package/src/v2/cluster.js +220 -14
  42. package/src/v2/coverage.js +43 -176
  43. package/src/v2/detect.js +308 -60
  44. package/src/v2/doctor.js +353 -54
  45. package/src/v2/escalate.js +5 -1
  46. package/src/v2/init.js +183 -66
  47. package/src/v2/intent.js +9 -23
  48. package/src/v2/journeys/from-suite.js +336 -30
  49. package/src/v2/journeys/index.js +99 -6
  50. package/src/v2/mcp/tools.js +90 -16
  51. package/src/v2/normalise.js +169 -23
  52. package/src/v2/observation.js +19 -33
  53. package/src/v2/rank.js +216 -23
  54. package/src/v2/reference.js +160 -24
  55. package/src/v2/remote.js +113 -18
  56. package/src/v2/run.js +103 -14
  57. package/src/v2/sealed.js +0 -20
  58. package/src/v2/selfcheck.js +190 -13
  59. package/src/v2/ship.js +55 -5
  60. package/src/v2/store.js +67 -1
  61. package/src/v2/types.js +12 -2
  62. package/src/v2/waiver.js +64 -54
  63. package/src/v2/watch/events.js +60 -215
  64. package/src/v2/watch/focus.js +14 -4
  65. package/src/v2/watch/panel.js +167 -17
@@ -49,7 +49,7 @@
49
49
  * the last one.
50
50
  */
51
51
 
52
- import { SURFACE_WORDS, SOURCE_WORDS, surfaceWord } from './events.js';
52
+ import { SURFACE_WORDS, SURFACE_NOTES, SOURCE_WORDS, CLASS_WORDS, surfaceWord } from './events.js';
53
53
 
54
54
  /** @typedef {import('./events.js').PanelPlanShape} PanelPlan */
55
55
  /** @typedef {import('./events.js').PanelJourney} PanelJourney */
@@ -201,6 +201,12 @@ export function panelHtml(plan = {}) {
201
201
  reference,
202
202
  words: SURFACE_WORDS,
203
203
  sources: SOURCE_WORDS,
204
+ // Embedded, never re-typed. This page used to keep its own hand-written copy of the
205
+ // class words and the two had already drifted apart — it said "a bug already reported
206
+ // once" where events.js says "a bug you already reported", and it had no word at all for
207
+ // an ordinary finding. One list, one place, the same rule SURFACE_WORDS has always had.
208
+ classes: CLASS_WORDS,
209
+ surfaceNotes: SURFACE_NOTES,
204
210
  });
205
211
 
206
212
  const subtitle = surfaces.length ? surfaces.join(' · ') : '';
@@ -231,7 +237,8 @@ export function panelHtml(plan = {}) {
231
237
  `<span class="app" id="surfaces"${subtitle ? '' : ' hidden'}>${escapeHtml(subtitle)}</span></p>`,
232
238
  // The one sentence a person reads from four feet away.
233
239
  '<p class="state" id="state">getting ready</p>',
234
- '<p class="what" id="what"></p>',
240
+ '<div class="what" id="what"></div>',
241
+ '<button class="whatmore" id="whatmore" type="button" hidden></button>',
235
242
  '<div class="meter">',
236
243
  '<div class="track" id="track"><div class="fill" id="fill"></div></div>',
237
244
  '<span class="counts mono" id="counts"></span>',
@@ -531,6 +538,12 @@ button { font: inherit; color: inherit; }
531
538
  to { opacity: 1; transform: none; filter: blur(0); }
532
539
  }
533
540
  .what { margin: 7px 0 0; font-size: var(--t-small); color: var(--faint); line-height: 1.5; overflow-wrap: anywhere; }
541
+ .what p { margin: 0 0 5px; }
542
+ .what p:last-child { margin-bottom: 0; }
543
+ .whatmore { margin: 6px 0 0; padding: 0; border: 0; background: none; font: inherit; font-size: var(--t-small); color: var(--dim); cursor: pointer; letter-spacing: .02em; }
544
+ .whatmore:hover { color: var(--text); }
545
+ .whatmore::before { content: '\\25B8'; display: inline-block; margin-right: 5px; transition: transform .18s ease; }
546
+ .whatmore[data-open="1"]::before { transform: rotate(90deg); }
534
547
 
535
548
  /* One hairline, not a row of blocks: each journey adds its own slice, so
536
549
  progress and outcome are the same object. A walk where everything held ends
@@ -615,6 +628,9 @@ button { font: inherit; color: inherit; }
615
628
  font-size: var(--t-meta); color: var(--faintest);
616
629
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
617
630
  }
631
+ /* What "walked" actually MEANT on this surface. "Website" alone does not tell anybody what
632
+ was looked at, and the whole worth of a green run is the reader knowing what it covers. */
633
+ .surfacewhat { padding: 0 3px 7px; font-size: var(--t-meta); color: var(--faintest); }
618
634
  .items { border-radius: var(--radius); background: var(--card); overflow: hidden; box-shadow: inset 0 0 0 1px var(--line); }
619
635
  .item + .item { box-shadow: inset 0 1px 0 var(--line); }
620
636
  .item { transition: background var(--calm) var(--ease); }
@@ -825,7 +841,7 @@ const SCRIPT = `
825
841
  // This page only ever runs in the window Stays Fixed just opened, so there is no
826
842
  // compatibility question to answer and nothing to load.
827
843
 
828
- var plan = { product: '', project: '', journeys: [], surfaces: [], reference: null, words: {}, sources: {} };
844
+ var plan = { product: '', project: '', journeys: [], surfaces: [], reference: null, words: {}, sources: {}, classes: {}, surfaceNotes: {} };
829
845
  try {
830
846
  var blob = document.getElementById('staysfixed-plan');
831
847
  if (blob && blob.textContent) plan = JSON.parse(blob.textContent) || plan;
@@ -838,7 +854,7 @@ const SCRIPT = `
838
854
 
839
855
  var ui = {
840
856
  clock: el('clock'), product: el('product'), surfaces: el('surfaces'), targetsep: el('targetsep'),
841
- state: el('state'), what: el('what'), track: el('track'), fill: el('fill'), counts: el('counts'),
857
+ state: el('state'), what: el('what'), whatMore: el('whatmore'), track: el('track'), fill: el('fill'), counts: el('counts'),
842
858
  refstrip: el('refstrip'), refname: el('refname'), refhow: el('refhow'), refwarn: el('refwarn'),
843
859
  groupWalk: el('groupWalk'), surfaceList: el('surfaceList'), countWalk: el('countWalk'),
844
860
  groupWobble: el('groupWobble'), wSteady: el('wSteady'), wUnstable: el('wUnstable'),
@@ -968,9 +984,60 @@ const SCRIPT = `
968
984
  ui.aura.style.setProperty('--tint', map[tone] || 'var(--accent)');
969
985
  }
970
986
 
987
+ // The summary, one sentence to a line, with the tail folded away.
988
+ //
989
+ // The engine's summary is a single paragraph that carries every fact a run produced, and
990
+ // on a real check it runs to fifteen lines. In a terminal that is a wall of text; in a
991
+ // window three hundred pixels wide it is a grey block nobody reads, sitting directly under
992
+ // the one sentence that matters. The facts are not the problem — dropping any of them
993
+ // would be worse — so nothing is thrown away here. It is split at sentence ends, the
994
+ // first two are shown, and the rest are one click away with the count on the button, so
995
+ // the window opens on something a person can actually take in.
996
+ var whatRest = [];
997
+ var whatOpen = false;
998
+
999
+ // Note the doubled backslashes, and never write a backtick in this file.
1000
+ //
1001
+ // Everything in this function lives inside a template literal in panel.js, and a template
1002
+ // literal eats an unknown escape exactly the way a string literal does: a single
1003
+ // backslash-s arrives in the browser as a plain letter s. Written once, this split ran as
1004
+ // (?<=[.!?])s+ — it matched letters instead of spaces, never split anything, and the
1005
+ // window went on showing the same fifteen-line paragraph while every test passed.
1006
+ function splitSentences(paragraph) {
1007
+ var raw = String(paragraph || '').split(/(?<=[.!?])\\s+(?=[A-Z0-9"'\u201c])/);
1008
+ var out = [];
1009
+ for (var i = 0; i < raw.length; i++) {
1010
+ var one = raw[i].replace(/^\\s+|\\s+$/g, '');
1011
+ if (one) out.push(one);
1012
+ }
1013
+ return out;
1014
+ }
1015
+
1016
+ function paintWhat() {
1017
+ while (ui.what.firstChild) ui.what.removeChild(ui.what.firstChild);
1018
+ var lines = whatShown.concat(whatOpen ? whatRest : []);
1019
+ for (var i = 0; i < lines.length; i++) {
1020
+ var p = document.createElement('p');
1021
+ p.appendChild(document.createTextNode(lines[i]));
1022
+ ui.what.appendChild(p);
1023
+ }
1024
+ show(ui.what, lines.length > 0);
1025
+ if (whatRest.length === 0) {
1026
+ show(ui.whatMore, false);
1027
+ return;
1028
+ }
1029
+ show(ui.whatMore, true);
1030
+ ui.whatMore.setAttribute('data-open', whatOpen ? '1' : '0');
1031
+ text(ui.whatMore, whatOpen ? 'less' : whatRest.length + ' more ' + plural(whatRest.length, 'thing', 'things') + ' this run says');
1032
+ }
1033
+
1034
+ var whatShown = [];
1035
+
971
1036
  function setWhat(sentence) {
972
- text(ui.what, sentence || '');
973
- show(ui.what, !!sentence);
1037
+ var all = splitSentences(sentence);
1038
+ whatShown = all.slice(0, 2);
1039
+ whatRest = all.slice(2);
1040
+ paintWhat();
974
1041
  }
975
1042
 
976
1043
  function updateCounts() {
@@ -1012,7 +1079,7 @@ const SCRIPT = `
1012
1079
  // The walk
1013
1080
  // -------------------------------------------------------------------------
1014
1081
 
1015
- function surfaceBox(word, note) {
1082
+ function surfaceBox(word, surface) {
1016
1083
  if (surfaces[word]) return surfaces[word];
1017
1084
  var box = document.createElement('section');
1018
1085
  box.className = 'surface';
@@ -1023,12 +1090,22 @@ const SCRIPT = `
1023
1090
  name.textContent = word;
1024
1091
  var hint = document.createElement('span');
1025
1092
  hint.className = 'surfacenote';
1026
- hint.textContent = note || '';
1093
+ hint.textContent = '';
1027
1094
  head.appendChild(name);
1028
1095
  head.appendChild(hint);
1029
1096
  var items = document.createElement('div');
1030
1097
  items.className = 'items';
1031
1098
  box.appendChild(head);
1099
+ // How this surface is watched, in one line. A person reading "iPhone · 4 journeys" has
1100
+ // been told nothing about what the tool actually looked at, and on a green run that is
1101
+ // the only thing that says how much the green is worth.
1102
+ var what = surface && plan.surfaceNotes ? plan.surfaceNotes[surface] : '';
1103
+ if (what) {
1104
+ var line = document.createElement('p');
1105
+ line.className = 'surfacewhat';
1106
+ line.textContent = what;
1107
+ box.appendChild(line);
1108
+ }
1032
1109
  box.appendChild(items);
1033
1110
  if (ui.surfaceList) ui.surfaceList.appendChild(box);
1034
1111
  show(ui.groupWalk, true);
@@ -1041,7 +1118,7 @@ const SCRIPT = `
1041
1118
  if (journeys[name]) return journeys[name];
1042
1119
  var info = meta || {};
1043
1120
  var word = surfaceWord(info.surface, info.surfaceWord);
1044
- var group = surfaceBox(word);
1121
+ var group = surfaceBox(word, info.surface);
1045
1122
  group.count++;
1046
1123
  group.hint.textContent = commas(group.count) + ' ' + plural(group.count, 'journey', 'journeys');
1047
1124
 
@@ -1309,7 +1386,10 @@ const SCRIPT = `
1309
1386
  if (f.sealed) sealedFindings.push(f);
1310
1387
  show(ui.groupFindings, true);
1311
1388
  show(ui.nothing, false);
1312
- ui.findingList.appendChild(findingRow(f));
1389
+ // The row is kept ON the finding, because a finding can be taken back — see
1390
+ // dropFindingsNotIn below. Without a handle on its own row there is no way to unsay one.
1391
+ f.node = findingRow(f);
1392
+ ui.findingList.appendChild(f.node);
1313
1393
  text(ui.countFindings, commas(findings.length));
1314
1394
  renderNeeds();
1315
1395
 
@@ -1445,12 +1525,10 @@ const SCRIPT = `
1445
1525
  return item;
1446
1526
  }
1447
1527
 
1528
+ // The one list, embedded from events.js. There used to be a second copy written out here
1529
+ // by hand, and it had already drifted from the first.
1448
1530
  function classWord(name) {
1449
- var words = {
1450
- money: 'money', 'sign-in': 'signing in', 'data-loss': 'losing data',
1451
- crash: 'a crash', guard: 'a bug already reported once'
1452
- };
1453
- return words[name] || name;
1531
+ return (plan.classes && plan.classes[name]) || name;
1454
1532
  }
1455
1533
 
1456
1534
  function onCoverage(ev) {
@@ -1458,6 +1536,13 @@ const SCRIPT = `
1458
1536
  if (!c) return;
1459
1537
  show(ui.groupCoverage, true);
1460
1538
  show(ui.nothing, false);
1539
+ // ONE count of addresses on this page, not two. The header counts up as each journey
1540
+ // reports in; the coverage ledger is the finished tally, worked out over every address
1541
+ // the run touched. Where a journey and its neighbour watch the same address the running
1542
+ // total counts it twice and the ledger counts it once, and the page ended up carrying
1543
+ // two figures with the same words on them and different numbers underneath. The ledger
1544
+ // is the one that was counted properly, so when it lands it is the one that stands.
1545
+ if (typeof c.paths === 'number') { watched = c.paths; updateCounts(); }
1461
1546
  ui.covFigures.textContent = '';
1462
1547
  addFigure(commas(c.paths), plural(c.paths, 'address watched', 'addresses watched'));
1463
1548
  addFigure(commas(c.journeys), plural(c.journeys, 'journey walked', 'journeys walked'));
@@ -1567,12 +1652,37 @@ const SCRIPT = `
1567
1652
  return;
1568
1653
  }
1569
1654
 
1570
- var tone = v.sealed > 0 ? 'wait' : (v.findings > 0 ? 'moved' : 'held');
1655
+ // The waived ones go. A finding only ever arrived here; there was no way to take one
1656
+ // away, and findings ARE taken away — the engine's verdict carries every difference it
1657
+ // found, then the gates remove the ones an agent has recorded as intended and the
1658
+ // settled verdict arrives second. Both reach this window, so a waived finding stayed on
1659
+ // screen, in the "What survived" list, beside a terminal that had already stopped
1660
+ // reporting it. The window was the only place still calling it a problem.
1661
+ // Only ever done when the check actually named the survivors: a verdict that arrives
1662
+ // already trimmed carries a count and no list, and clearing the list on that would wipe
1663
+ // the findings rather than settle them.
1664
+ if (Array.isArray(ev.findingIds)) dropFindingsNotIn(ev.findingIds);
1665
+
1666
+ // WHAT THE RUN SAYS ABOUT ITSELF, not just how many findings came out of it. The
1667
+ // headline was picked from the finding counts alone and never read ok at all, so the run
1668
+ // that compared NOTHING with anything — no findings, because nothing was compared — got
1669
+ // this tool's all-clear sentence. That is the single most dangerous sentence it can produce,
1670
+ // said about the one run that proves least. The engine already exits ok:false with
1671
+ // "NO ANSWER FROM THIS RUN" on it; the window says the same thing.
1672
+ var tone = v.sealed > 0 ? 'wait' : (v.findings > 0 ? 'moved' : (v.ok === false ? 'wait' : 'held'));
1571
1673
  var sentence;
1572
1674
  if (v.sealed > 0) {
1573
1675
  sentence = commas(v.sealed) + ' ' + plural(v.sealed, 'thing needs', 'things need') + ' you.';
1574
1676
  } else if (v.findings > 0) {
1575
1677
  sentence = commas(v.findings) + ' ' + plural(v.findings, 'finding', 'findings') + ' the agent has to deal with.';
1678
+ } else if (v.newlyUnstable > 0) {
1679
+ // No finding, and still not a pass: something that used to give the same answer every
1680
+ // time does not any more. A change that made a product unpredictable has broken
1681
+ // something, even when no single value can be pointed at.
1682
+ sentence = commas(v.newlyUnstable) + ' ' + plural(v.newlyUnstable, 'address that was', 'addresses that were') +
1683
+ ' steady before this change ' + plural(v.newlyUnstable, 'wobbles', 'wobble') + ' now.';
1684
+ } else if (v.ok === false) {
1685
+ sentence = 'No answer from this run — that is not a pass.';
1576
1686
  } else {
1577
1687
  sentence = 'Everything that worked still works.';
1578
1688
  }
@@ -1582,7 +1692,11 @@ const SCRIPT = `
1582
1692
  // hundreds of unopened doors, or measured against a stored record, is a smaller claim
1583
1693
  // than it looks, and the smaller claim is the true one.
1584
1694
  var caveats = [];
1585
- if (v.modeWarning) caveats.push(v.modeWarning);
1695
+ // Said ONCE. The engine already ends the summary with this exact sentence, and pushing
1696
+ // it again here printed the whole "compared against the stored record, that is genuinely
1697
+ // weaker..." paragraph twice in a row, one straight after the other, on every single
1698
+ // stored-record run. A window that repeats itself reads like a window that is broken.
1699
+ if (v.modeWarning && String(v.summary || '').indexOf(v.modeWarning) === -1) caveats.push(v.modeWarning);
1586
1700
  if (v.differencesNoise) {
1587
1701
  caveats.push(commas(v.differencesNoise) + ' ' + plural(v.differencesNoise, 'difference was', 'differences were') + ' this build arguing with itself, and were subtracted.');
1588
1702
  }
@@ -1591,6 +1705,42 @@ const SCRIPT = `
1591
1705
  renderNeeds();
1592
1706
  }
1593
1707
 
1708
+ // Take back every finding the check no longer stands behind.
1709
+ function dropFindingsNotIn(ids) {
1710
+ var keep = {};
1711
+ for (var i = 0; i < ids.length; i++) keep[ids[i]] = true;
1712
+ var left = [];
1713
+ var repaint = {};
1714
+ for (var j = 0; j < findings.length; j++) {
1715
+ var f = findings[j];
1716
+ if (keep[f.id]) { left.push(f); continue; }
1717
+ if (f.node && f.node.parentNode) f.node.parentNode.removeChild(f.node);
1718
+ if (f.journey) repaint[f.journey] = true;
1719
+ }
1720
+ findings = left;
1721
+ // The journey a waived finding was found on goes back to whatever its REMAINING findings
1722
+ // say, not straight to clean. A journey with two findings, one of them waived, is still a
1723
+ // journey with a finding on it, and blanking its mark would hide the other one.
1724
+ for (var name in repaint) {
1725
+ if (!Object.prototype.hasOwnProperty.call(repaint, name) || !journeys[name]) continue;
1726
+ var worst = 'held';
1727
+ for (var m = 0; m < findings.length; m++) {
1728
+ if (findings[m].journey !== name) continue;
1729
+ var t = findings[m].sealed ? 'wait' : (findings[m]['class'] === 'crash' ? 'broke' : 'moved');
1730
+ if (rank(t) > rank(worst)) worst = t;
1731
+ }
1732
+ markJourney(journeys[name], worst, worst !== 'held');
1733
+ }
1734
+ var stillSealed = [];
1735
+ for (var k = 0; k < sealedFindings.length; k++) if (keep[sealedFindings[k].id]) stillSealed.push(sealedFindings[k]);
1736
+ sealedFindings = stillSealed;
1737
+ text(ui.countFindings, commas(findings.length));
1738
+ show(ui.groupFindings, findings.length > 0);
1739
+ // The footer is the one part a person is meant to act on, so it is redrawn from what is
1740
+ // left rather than kept from what there was.
1741
+ renderNeeds();
1742
+ }
1743
+
1594
1744
  // -------------------------------------------------------------------------
1595
1745
  // Evidence, at full size
1596
1746
  // -------------------------------------------------------------------------