pi-lens 4.4.3 → 4.4.4

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 (143) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/README.md +13 -1
  3. package/dist/{chunk-SS36ZX4J.js → chunk-24AKHFGP.js} +6 -6
  4. package/dist/{chunk-JSQFTSC6.js → chunk-2FN63LPV.js} +11 -11
  5. package/dist/{chunk-5UCAH366.js → chunk-2KXTHF3M.js} +20 -20
  6. package/dist/{chunk-EU63SDCG.js → chunk-3DA2JDOL.js} +367 -281
  7. package/dist/{chunk-RWWYJ623.js → chunk-3KAZU3VR.js} +1 -1
  8. package/dist/chunk-3PTHXJXR.js +28 -0
  9. package/dist/chunk-3QED2P4V.js +31 -0
  10. package/dist/{chunk-N2H3ZPZY.js → chunk-3RKP3FMS.js} +1 -0
  11. package/dist/{chunk-XVF2VVXP.js → chunk-5HSRT26B.js} +2 -2
  12. package/dist/{chunk-MXGY6KPT.js → chunk-5IUSNGOC.js} +15 -15
  13. package/dist/{chunk-EJIUXPBX.js → chunk-5OFV52AM.js} +1 -1
  14. package/dist/{chunk-AW7R2GWG.js → chunk-5VSF4UUQ.js} +12 -7
  15. package/dist/{chunk-6STA6Z7I.js → chunk-5WRPDP2A.js} +18 -18
  16. package/dist/{chunk-YIUAVF7E.js → chunk-6DLWKFEX.js} +35 -34
  17. package/dist/{chunk-WOA4LN2R.js → chunk-7D6L4MTX.js} +2 -2
  18. package/dist/{chunk-GAEGEPOC.js → chunk-7HXE67BN.js} +2 -2
  19. package/dist/chunk-AGSS7277.js +84 -0
  20. package/dist/{chunk-R3PCY3YS.js → chunk-AMERCH6V.js} +13 -13
  21. package/dist/{chunk-WU567RSC.js → chunk-ATKJFDQN.js} +14 -10
  22. package/dist/{chunk-VSSBEUFT.js → chunk-AWSCPCN2.js} +16 -16
  23. package/dist/{chunk-VCHXCQFG.js → chunk-BWD5EPKO.js} +3 -3
  24. package/dist/{chunk-H6MF6EVN.js → chunk-CT4MN5IU.js} +1200 -230
  25. package/dist/{chunk-FCSOVF55.js → chunk-CUHSHSB6.js} +6 -6
  26. package/dist/{chunk-CHOW74WE.js → chunk-CYRKQRPM.js} +7 -10
  27. package/dist/{chunk-Q7DUK4LX.js → chunk-DEL2U6CM.js} +5 -5
  28. package/dist/{chunk-6C7JWPCP.js → chunk-DGUKIYSI.js} +26 -23
  29. package/dist/{chunk-PHDMFAKM.js → chunk-DUO2TU77.js} +2 -2
  30. package/dist/{chunk-MRXKYPCS.js → chunk-EBC36LNN.js} +2 -2
  31. package/dist/{chunk-CSHFH5IN.js → chunk-ED36RF6Y.js} +1 -1
  32. package/dist/{chunk-76MWC2K6.js → chunk-EUXZEFTV.js} +90 -97
  33. package/dist/{chunk-QOZXJ4NA.js → chunk-FM4RDMKT.js} +11 -8
  34. package/dist/{chunk-ACXEQZOU.js → chunk-GAQ7OQG6.js} +31 -28
  35. package/dist/{chunk-XAGLWMJ4.js → chunk-GL5LUMZE.js} +23 -23
  36. package/dist/{chunk-BKE7E7TC.js → chunk-GMX5OS7B.js} +9 -9
  37. package/dist/{chunk-ZHFZWTZV.js → chunk-GSLNCYGG.js} +11 -11
  38. package/dist/{chunk-3W7IEVVL.js → chunk-GSM5UU7A.js} +26 -34
  39. package/dist/{chunk-724UL2TJ.js → chunk-GXFV6MYU.js} +3 -3
  40. package/dist/chunk-HAD7QRQI.js +30 -0
  41. package/dist/{chunk-QOBUHWHX.js → chunk-HRNHZA3N.js} +6 -6
  42. package/dist/{chunk-VWCXW2E7.js → chunk-HWCCCQNX.js} +1 -1
  43. package/dist/{chunk-7DYBLXYA.js → chunk-HYGZEMX6.js} +3 -3
  44. package/dist/chunk-IJYGIDF4.js +186 -0
  45. package/dist/{chunk-4W4VLAPJ.js → chunk-IQWN5JCP.js} +11 -11
  46. package/dist/{chunk-SQ4TVJBR.js → chunk-IUE7SSKV.js} +7 -7
  47. package/dist/{chunk-IBGIOBEE.js → chunk-IWDLU6K3.js} +68 -5
  48. package/dist/{chunk-XCJN5IJU.js → chunk-IYNJ6YPZ.js} +4 -4
  49. package/dist/{chunk-JAU72XNY.js → chunk-J64T55EI.js} +3 -3
  50. package/dist/{chunk-2BBLFX75.js → chunk-J6MPGQTS.js} +5 -5
  51. package/dist/chunk-JORED7EX.js +44 -0
  52. package/dist/{chunk-6SDEP3AJ.js → chunk-JUKMK5Y4.js} +8 -8
  53. package/dist/{chunk-FMSO6RDZ.js → chunk-JW3YWBVB.js} +15 -15
  54. package/dist/{chunk-L3BLXZG7.js → chunk-KAPJINKN.js} +30 -69
  55. package/dist/{chunk-2B73ZPOK.js → chunk-KN35K52N.js} +3 -3
  56. package/dist/{chunk-C34ZXZ7O.js → chunk-KUA4OJ5Y.js} +24 -11
  57. package/dist/chunk-KYUB6QDC.js +72 -0
  58. package/dist/{chunk-67WMNWEE.js → chunk-LLJNQKQW.js} +12 -10
  59. package/dist/chunk-LZSEQW6A.js +57 -0
  60. package/dist/{chunk-ZANLILYD.js → chunk-M2IFVFWU.js} +2 -2
  61. package/dist/chunk-M2UD3LPU.js +88 -0
  62. package/dist/{chunk-XIJZVPD5.js → chunk-MBKQHGA7.js} +15 -15
  63. package/dist/{chunk-P7266HLM.js → chunk-MPNJLASA.js} +2 -2
  64. package/dist/{chunk-6GFRNVYR.js → chunk-MZGQNL3Y.js} +10 -10
  65. package/dist/{chunk-ZWFS3OEF.js → chunk-N5FLEO7W.js} +33 -32
  66. package/dist/{chunk-3JNKMP3B.js → chunk-O4NWQT6I.js} +4 -4
  67. package/dist/{chunk-T6CPIZNT.js → chunk-OHKJ7PQF.js} +33 -32
  68. package/dist/{chunk-VRZP76YM.js → chunk-P63GXC37.js} +4 -1
  69. package/dist/{chunk-XTMJ7XDT.js → chunk-POMLXPMY.js} +1 -1
  70. package/dist/{chunk-QQNQ7SJW.js → chunk-QBFBRV2B.js} +10 -4
  71. package/dist/{chunk-CMMHNUZ2.js → chunk-R3T7CLQ2.js} +60 -38
  72. package/dist/{chunk-23WOO7TX.js → chunk-R7YOCSNV.js} +10 -10
  73. package/dist/{chunk-2CD5RS6M.js → chunk-RKBXZN3A.js} +23 -2
  74. package/dist/{chunk-OCAGUNBC.js → chunk-RKKOYPRA.js} +12 -12
  75. package/dist/{chunk-52FUZI6K.js → chunk-RMLC67LF.js} +1 -1
  76. package/dist/{chunk-XMSXXAHW.js → chunk-ROYSX2S5.js} +2 -2
  77. package/dist/{chunk-M6HSLBWY.js → chunk-RVK72CZX.js} +6507 -7514
  78. package/dist/{chunk-FLKWCIZH.js → chunk-S2HEXIWI.js} +5 -5
  79. package/dist/{chunk-UZ3TUTRV.js → chunk-S67KYG37.js} +1 -1
  80. package/dist/{chunk-VCO24T4L.js → chunk-SD4I7ZO4.js} +14 -14
  81. package/dist/{chunk-Z527N6YQ.js → chunk-SKAMFC7A.js} +12 -12
  82. package/dist/{chunk-MQLUAEOC.js → chunk-SRYEJNES.js} +2 -2
  83. package/dist/{chunk-BRGDSBJE.js → chunk-SYE6T2EB.js} +12 -12
  84. package/dist/{chunk-GXW6K4YJ.js → chunk-T57LYLZT.js} +1 -1
  85. package/dist/{chunk-ZXYDQ3TZ.js → chunk-T7FEBKXN.js} +48 -15
  86. package/dist/{chunk-3EFKIGZN.js → chunk-TIQ2PKDW.js} +3 -3
  87. package/dist/{chunk-QILIYSJF.js → chunk-TLX72JTG.js} +1 -1
  88. package/dist/{chunk-TYM6OUIT.js → chunk-TOY6QKOH.js} +12 -12
  89. package/dist/{chunk-IZSHAIJ7.js → chunk-TRETHTQO.js} +12 -12
  90. package/dist/{chunk-3VPVG5SU.js → chunk-U44DJXQP.js} +10 -6
  91. package/dist/{chunk-PNQDPDRC.js → chunk-UA7ZXYDR.js} +1 -1
  92. package/dist/{chunk-OXXKQCSV.js → chunk-UBOQLVT2.js} +4 -4
  93. package/dist/{chunk-QGQ5CCCG.js → chunk-UMS2MFPZ.js} +2 -2
  94. package/dist/{chunk-QHJ6DQ2R.js → chunk-US7ECLD7.js} +55 -13
  95. package/dist/{chunk-AB3YYG6N.js → chunk-UZ2XCLYS.js} +8 -8
  96. package/dist/chunk-V35JG4JB.js +2667 -0
  97. package/dist/{chunk-N3X6Q4YT.js → chunk-VOOAPROD.js} +3 -3
  98. package/dist/{chunk-SAEFPSB6.js → chunk-W66O5E25.js} +21 -11
  99. package/dist/{chunk-KBG47GHP.js → chunk-WMYE35PX.js} +15 -11
  100. package/dist/{chunk-5NGR67U5.js → chunk-XBJVAEEO.js} +32 -5
  101. package/dist/{chunk-YYOIQFRS.js → chunk-XHJBS436.js} +122 -44
  102. package/dist/{chunk-GBPP5P7X.js → chunk-XTBDCR27.js} +4 -4
  103. package/dist/{chunk-4UNXBNE2.js → chunk-XTFDJI3T.js} +11 -11
  104. package/dist/{chunk-UXGETRP4.js → chunk-XYURS6EN.js} +13 -12
  105. package/dist/{chunk-RPLINGVB.js → chunk-Y6GZHWSJ.js} +6 -6
  106. package/dist/{chunk-2VUQMUMT.js → chunk-YCWYO6TQ.js} +41 -38
  107. package/dist/{chunk-77M37OKR.js → chunk-YH5MVY2U.js} +7 -7
  108. package/dist/{chunk-AOHZU2BM.js → chunk-YNE3RP52.js} +1 -1
  109. package/dist/{chunk-YSZNHOBY.js → chunk-ZHH37KZL.js} +12 -12
  110. package/dist/{chunk-ZGDGW2V3.js → chunk-ZIVJBOO3.js} +2 -2
  111. package/dist/{chunk-RXCOMYPY.js → chunk-ZVUL7QYY.js} +29 -28
  112. package/dist/index.js +11052 -5852
  113. package/dist/mcp/analyze-cli.js +10 -9
  114. package/dist/mcp/cli.js +41 -38
  115. package/dist/mcp/server.js +1120 -474
  116. package/dist/mcp/worker.js +61 -58
  117. package/dist/probes/installer.js +19 -19
  118. package/dist/workers/project-snapshot-persist-worker.js +28 -9
  119. package/dist/workers/review-graph-persist-worker.js +1 -1
  120. package/docs/configuration.md +11 -0
  121. package/docs/engineering-principles.md +32 -0
  122. package/docs/lsp-capability-matrix.md +1 -1
  123. package/docs/lsp-idle-eviction.md +15 -26
  124. package/docs/pi-lens-fixer.md +117 -13
  125. package/docs/pi-lens-merge-policy.md +97 -7
  126. package/docs/pi-lens-monitor.md +8 -2
  127. package/docs/pi-lens-retro.md +4 -1
  128. package/docs/pi-lens-reviewer.md +112 -4
  129. package/docs/pi-lens-subagent.md +22 -14
  130. package/docs/release-qa-baseline.md +3 -1
  131. package/docs/servercapabilities.md +9 -19
  132. package/package.json +7 -5
  133. package/scripts/download-grammars.js +40 -7
  134. package/scripts/lib/latency-log-phases.mjs +95 -0
  135. package/scripts/rpc-load-check.mjs +570 -81
  136. package/dist/chunk-3YN6V5TV.js +0 -85
  137. package/dist/chunk-466Z26MG.js +0 -30
  138. package/dist/chunk-EC47DWFA.js +0 -56
  139. package/dist/chunk-KDERNTCQ.js +0 -28
  140. package/dist/chunk-KSX3TXSZ.js +0 -69
  141. package/dist/chunk-LOP6GRPH.js +0 -31
  142. package/dist/chunk-UOPVJQ4N.js +0 -103
  143. /package/dist/{chunk-FC4RP4I2.js → chunk-6HWVI6MI.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "4.4.3",
3
+ "version": "4.4.4",
4
4
  "type": "module",
5
5
  "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
@@ -20,10 +20,11 @@
20
20
  "prepare": "npm run build:dist && node scripts/download-grammars.js --core --dest grammars && node scripts/setup-git-hooks.mjs && node scripts/stamp-package-lock.mjs && node scripts/warm-loader-cache.mjs",
21
21
  "prepack": "node scripts/strip-dev-deps-for-pack.mjs --strip",
22
22
  "postpack": "node scripts/strip-dev-deps-for-pack.mjs --restore",
23
- "lint": "tsc --project tsconfig.json && npm run lint:js && npm run lint:js:tests",
23
+ "lint": "tsc --project tsconfig.json && npm run lint:js && npm run lint:js:tests && npm run lint:js:promises",
24
24
  "lint:js": "oxlint --deny-warnings --import-plugin --promise-plugin -D block-scoped-var -D import/default -D import/namespace -D import/no-absolute-path -D import/no-empty-named-blocks -D import/no-named-as-default -D import/no-self-import -D no-extend-native -D no-extra-bind -D no-implied-eval -D no-new -D no-unexpected-multiline -D no-useless-constructor -D oxc/approx-constant -D oxc/misrefactored-assign-op -D oxc/no-accumulating-spread -D oxc/no-async-endpoint-handlers -D oxc/no-this-in-exported-function -D promise/no-callback-in-promise -D promise/no-multiple-resolved -D promise/no-new-statics -D promise/valid-params -D typescript/no-confusing-non-null-assertion -D typescript/no-extraneous-class -D typescript/no-unnecessary-type-constraint -D typescript/no-unsafe-enum-comparison -D unicorn/no-accessor-recursion -D unicorn/no-array-fill-with-reference-type -D unicorn/no-confusing-array-with -D unicorn/no-instanceof-builtins -D unicorn/prefer-array-flat-map -D unicorn/require-module-specifiers --ignore-pattern \"tests/**/*.ts\" --ignore-pattern \"tests/**/*.tsx\" --ignore-pattern \"**/*.d.mts\" --ignore-pattern \"tests/fixtures/autofix-smoke/**\" --ignore-pattern \"tests/fixtures/format-smoke/**\" --ignore-pattern \"tests/fixtures/tool-smoke/**\" --ignore-pattern \"tests/fixtures/call-graph/javascript/**\" --ignore-pattern \"tests/fixtures/function-facts/javascript-parameter.js\" .",
25
25
  "lint:js:advisory": "node scripts/lint-js-advisory.mjs --deny-warnings --format unix -D correctness -D suspicious -D perf --import-plugin --promise-plugin --type-aware -A no-underscore-dangle -A no-await-in-loop -A unicorn/no-array-sort -A unicorn/consistent-function-scoping -A promise/no-promise-in-callback -A no-useless-call -A import/no-unassigned-import -A no-unmodified-loop-condition --ignore-pattern \"**/*.d.mts\" --ignore-pattern \"tests/fixtures/autofix-smoke/**\" --ignore-pattern \"tests/fixtures/format-smoke/**\" --ignore-pattern \"tests/fixtures/tool-smoke/**\" --ignore-pattern \"tests/fixtures/call-graph/javascript/**\" --ignore-pattern \"tests/fixtures/function-facts/javascript-parameter.js\" .",
26
26
  "lint:js:tests": "node scripts/lint-js-advisory.mjs --deny-warnings --format unix --type-aware -A correctness -A suspicious -A perf --ignore-pattern \"tests/fixtures/**\" tests",
27
+ "lint:js:promises": "node scripts/lint-js-advisory.mjs --deny-warnings --format unix --type-aware -A all -D typescript/await-thenable -D typescript/no-floating-promises -D typescript/no-misused-promises clients mcp tools index.ts",
27
28
  "depcruise:check": "depcruise --validate --config .dependency-cruiser.cjs --ignore-known .dependency-cruiser-known-violations.json --output-type err index.ts \"clients/**/*.ts\"",
28
29
  "fmt": "oxfmt",
29
30
  "fmt:check": "oxfmt --check",
@@ -113,6 +114,7 @@
113
114
  "scripts/analyze-pi-lens-logs.mjs",
114
115
  "scripts/install-selftest.mjs",
115
116
  "scripts/lib/host-provided-deps.mjs",
117
+ "scripts/lib/latency-log-phases.mjs",
116
118
  "scripts/lib/skills-predicate.mjs",
117
119
  "scripts/lib/web-tree-sitter-dir.mjs",
118
120
  "scripts/lib/warm-loader-cache.mjs",
@@ -124,8 +126,8 @@
124
126
  "banner.png"
125
127
  ],
126
128
  "dependencies": {
127
- "@ast-grep/cli": "0.45.3",
128
- "@ast-grep/napi": "^0.45.0",
129
+ "@ast-grep/cli": "0.50.0",
130
+ "@ast-grep/napi": "^0.50.0",
129
131
  "js-yaml": "^5.2.2",
130
132
  "minimatch": "^10.2.6",
131
133
  "pidusage": "^4.0.1",
@@ -160,7 +162,7 @@
160
162
  ]
161
163
  },
162
164
  "allowScripts": {
163
- "@ast-grep/cli@0.45.3": true,
165
+ "@ast-grep/cli@0.50.0": true,
164
166
  "@google/genai@2.21.0": true,
165
167
  "esbuild@0.28.2": true,
166
168
  "fsevents@2.3.3": false,
@@ -172,11 +172,44 @@ function baseUrl(version) {
172
172
  function grammarUrl(version, filename) {
173
173
  return SOURCE_OVERRIDES[filename]?.url ?? `${baseUrl(version)}/${filename}`;
174
174
  }
175
- async function fetchGrammar(version, filename) {
176
- const res = await fetch(grammarUrl(version, filename));
177
- if (!res.ok)
178
- throw new Error(`HTTP ${res.status} fetching ${filename}`);
179
- return Buffer.from(await res.arrayBuffer());
175
+ /**
176
+ * Abort a fetch that receives no bytes for this long, headers included; each
177
+ * chunk restarts the count, so a slow link that keeps delivering completes.
178
+ * Mirrors `GRAMMAR_STALL_MS` in clients/grammar-source.ts, which this script
179
+ * cannot import before the TS build; `tests/clients/grammar-source.test.ts`
180
+ * pins the two together (#4425).
181
+ */
182
+ export const GRAMMAR_STALL_MS = 15_000;
183
+ async function fetchGrammar(version, filename, stallMs = GRAMMAR_STALL_MS) {
184
+ const controller = new AbortController();
185
+ const timer = setTimeout(() => controller.abort(), stallMs);
186
+ try {
187
+ const res = await fetch(grammarUrl(version, filename), {
188
+ signal: controller.signal,
189
+ });
190
+ if (!res.ok)
191
+ throw new Error(`HTTP ${res.status} fetching ${filename}`);
192
+ const reader = res.body?.getReader();
193
+ if (!reader)
194
+ return Buffer.from(await res.arrayBuffer());
195
+ const chunks = [];
196
+ for (;;) {
197
+ const { done, value } = await reader.read();
198
+ if (done)
199
+ return Buffer.concat(chunks);
200
+ timer.refresh();
201
+ chunks.push(value);
202
+ }
203
+ }
204
+ catch (err) {
205
+ if (controller.signal.aborted) {
206
+ throw new Error(`no bytes arrived for ${stallMs} ms fetching ${filename}`);
207
+ }
208
+ throw err;
209
+ }
210
+ finally {
211
+ clearTimeout(timer);
212
+ }
180
213
  }
181
214
  /**
182
215
  * Download `filename` into `destDir` (if it isn't already verified), checking its
@@ -184,12 +217,12 @@ async function fetchGrammar(version, filename) {
184
217
  * integrity mismatch — a grammar whose bytes don't match the pinned manifest is
185
218
  * never written (guards CDN corruption / tampering / ABI drift).
186
219
  */
187
- async function downloadGrammar(destDir, filename, manifest) {
220
+ export async function downloadGrammar(destDir, filename, manifest, stallMs = GRAMMAR_STALL_MS) {
188
221
  if (!needsDownload(destDir, filename, manifest)) {
189
222
  console.error(` skip ${filename} (verified)`);
190
223
  return;
191
224
  }
192
- const buf = await fetchGrammar(manifest.version, filename);
225
+ const buf = await fetchGrammar(manifest.version, filename, stallMs);
193
226
  const actual = sha256(buf);
194
227
  const expected = manifest.grammars[filename];
195
228
  if (expected && actual !== expected) {
@@ -0,0 +1,95 @@
1
+ // Pure NDJSON latency-log phase parsing for scripts/compat-smoke-behavioral.mjs
2
+ // (#476, Layer B).
3
+ //
4
+ // `clients/latency-logger.ts` writes one JSON object per line to
5
+ // `~/.pi-lens/latency.log` (see `createNdjsonLogger`). Layer B launches real
6
+ // `pi` processes and needs to assert (or assert the ABSENCE of) specific
7
+ // `type: "phase"` entries that landed during a bounded time window — this
8
+ // module is the pure parsing/filtering half, kept separate from the
9
+ // file-reading/process-spawning orchestration so it is unit-testable without
10
+ // touching the filesystem or spawning anything.
11
+
12
+ /**
13
+ * Parse NDJSON text into an array of entries. Malformed lines (partial write,
14
+ * truncation mid-append) are skipped rather than throwing — the log is
15
+ * best-effort by construction (ndjson-logger.ts swallows write errors), so a
16
+ * single bad line must never abort the whole read.
17
+ *
18
+ * @param {string} text raw latency.log contents
19
+ * @returns {Array<Record<string, unknown>>}
20
+ */
21
+ export function parseNdjsonEntries(text) {
22
+ const entries = [];
23
+ for (const line of text.split(/\r?\n/)) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed) continue;
26
+ try {
27
+ const parsed = JSON.parse(trimmed);
28
+ if (parsed && typeof parsed === "object") entries.push(parsed);
29
+ } catch {
30
+ // partial/corrupt line — skip
31
+ }
32
+ }
33
+ return entries;
34
+ }
35
+
36
+ /**
37
+ * Filter entries to `type: "phase"` records with the given `phase` name,
38
+ * optionally restricted to entries whose `ts` (ISO timestamp) falls at or
39
+ * after `sinceIso`. Used to scope a check to "phases logged by THIS run" when
40
+ * the log file is long-lived and shared across sessions.
41
+ *
42
+ * @param {Array<Record<string, unknown>>} entries
43
+ * @param {string} phase
44
+ * @param {string} [sinceIso] ISO timestamp lower bound (inclusive)
45
+ */
46
+ export function findPhaseEntries(entries, phase, sinceIso) {
47
+ const sinceMs = sinceIso ? Date.parse(sinceIso) : undefined;
48
+ return entries.filter((e) => {
49
+ if (e.type !== "phase" || e.phase !== phase) return false;
50
+ if (sinceMs === undefined) return true;
51
+ const ts = typeof e.ts === "string" ? Date.parse(e.ts) : Number.NaN;
52
+ return Number.isFinite(ts) && ts >= sinceMs;
53
+ });
54
+ }
55
+
56
+ /**
57
+ * True iff at least one `findPhaseEntries` match exists.
58
+ *
59
+ * @param {Array<Record<string, unknown>>} entries
60
+ * @param {string} phase
61
+ * @param {string} [sinceIso]
62
+ */
63
+ export function phaseWasLogged(entries, phase, sinceIso) {
64
+ return findPhaseEntries(entries, phase, sinceIso).length > 0;
65
+ }
66
+
67
+ /**
68
+ * True iff NO phase in `phases` was logged since `sinceIso` — used to assert
69
+ * a degraded/skip path did NOT engage (e.g. heavyweight scans must be absent
70
+ * under subagent light mode).
71
+ *
72
+ * @param {Array<Record<string, unknown>>} entries
73
+ * @param {string[]} phases
74
+ * @param {string} [sinceIso]
75
+ */
76
+ export function noPhasesLogged(entries, phases, sinceIso) {
77
+ return phases.every((phase) => !phaseWasLogged(entries, phase, sinceIso));
78
+ }
79
+
80
+ /**
81
+ * Number of entries (any type) whose `ts` falls at or after `sinceIso`.
82
+ * Positive control for the absence assertions: "no heavyweight phase logged"
83
+ * is only evidence when the run wrote SOMETHING to the log the smoke is
84
+ * reading — an empty or mis-located log made those pass vacuously (#2570).
85
+ *
86
+ * @param {Array<Record<string, unknown>>} entries
87
+ * @param {string} sinceIso ISO timestamp lower bound (inclusive)
88
+ */
89
+ export function countEntriesSince(entries, sinceIso) {
90
+ const sinceMs = Date.parse(sinceIso);
91
+ return entries.filter((e) => {
92
+ const ts = typeof e.ts === "string" ? Date.parse(e.ts) : Number.NaN;
93
+ return Number.isFinite(ts) && ts >= sinceMs;
94
+ }).length;
95
+ }