@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

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 (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -8,7 +8,8 @@ import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
10
  import { classifyBundledResourceDir, reportBundledResourceDirHealth, } from "./bundled-resource-health.js";
11
- import { getDegradationLedgerGeneration } from "./degradation-ledger.js";
11
+ import { getDegradationLedgerGeneration, recordDegradationOnce, } from "./degradation-ledger.js";
12
+ import yaml from "./deps/js-yaml.js";
12
13
  import { resolvePackagePath } from "./package-root.js";
13
14
  /**
14
15
  * The bundled `rules/tree-sitter-queries` root — the ONE spelling of this
@@ -165,14 +166,20 @@ export function queriesForLanguage(queries, languageId) {
165
166
  return ruleSourceLanguages(languageId).flatMap((langId) => enabled(langId));
166
167
  }
167
168
  /**
168
- * Drop a trailing ` # comment` from an unquoted YAML scalar. Quoted values keep
169
- * their `#` (a message may legitimately contain one), matching YAML's rule that
170
- * a comment only starts after whitespace outside quotes.
169
+ * Coerce a parsed YAML scalar to a string, refusing a mapping or array.
170
+ * `String({})`/`String([])` silently produce the literal text
171
+ * `"[object Object]"`/`"a,b"` — SonarCloud typescript:S6551 flagged this at
172
+ * every scalar field in `parseQueryFile` once `parseYaml` widened to
173
+ * `Record<string, unknown>` (#3054 review F2). Returns `undefined` for
174
+ * `null`/`undefined`/a non-scalar so call sites keep using `||`/`??` for
175
+ * defaults exactly as the old `String(x || fallback)` calls did.
171
176
  */
172
- function stripInlineComment(value) {
173
- if (value.startsWith('"') || value.startsWith("'"))
174
- return value;
175
- return value.replace(/\s+#.*$/, "").trim();
177
+ function str(value) {
178
+ return typeof value === "string" ||
179
+ typeof value === "number" ||
180
+ typeof value === "boolean"
181
+ ? String(value)
182
+ : undefined;
176
183
  }
177
184
  export function isDisabledQueryFilePath(filePath) {
178
185
  const normalized = filePath.replaceAll("\\", "/");
@@ -185,6 +192,20 @@ export class TreeSitterQueryLoader {
185
192
  loaded = false;
186
193
  loadedRoot = null;
187
194
  verbose;
195
+ /**
196
+ * This root's per-file parse failures, remembered across the memoized
197
+ * (no-`force`) `loadQueries` path so a later session's ledger can carry
198
+ * the same row without re-parsing every file (#3070 N1).
199
+ */
200
+ parseFailures = new Map();
201
+ /**
202
+ * Generation (`getDegradationLedgerGeneration()`) at which `parseFailures`
203
+ * was last replayed into the ledger — the same generation-keyed memo
204
+ * idiom `getBundledQueriesRootHealth` above uses, applied here so a
205
+ * memoized `loadQueries` return still re-arms `recordDegradationOnce` once
206
+ * per session rather than only in the session that actually parsed.
207
+ */
208
+ parseFailuresReplayedGeneration;
188
209
  constructor(verbose = false) {
189
210
  this.verbose = verbose;
190
211
  }
@@ -199,6 +220,52 @@ export class TreeSitterQueryLoader {
199
220
  });
200
221
  }
201
222
  }
223
+ /**
224
+ * One degradation-ledger record per malformed rule file per session
225
+ * (#3054 review F1). The old hand-rolled scanner tolerated almost any
226
+ * line-level mistake and still produced SOME parsed shape; `yaml.load`
227
+ * correctly throws on realistic authoring mistakes the scanner shrugged
228
+ * off (a colon in an unquoted scalar, a tab in list indentation, a
229
+ * duplicate key, an unclosed quote, a bare `@` value — fuzzed over 800
230
+ * corruptions of one shipped query: 139 that loaded before now skip, 0
231
+ * the other way). Before this, the only sink on that skip path was
232
+ * `dbg()`, gated behind `verbose` — both production instantiations
233
+ * (this file's `queryLoader` singleton and `tree-sitter-client.ts`'s
234
+ * `new TreeSitterQueryLoader()`) construct with the `verbose = false`
235
+ * default — so a silently-dropped custom rule had no surviving signal.
236
+ */
237
+ recordQueryParseFailure(filePath, reason) {
238
+ this.parseFailures.set(filePath, reason);
239
+ recordDegradationOnce({
240
+ kind: "tree-sitter-query-parse-failed",
241
+ subject: filePath,
242
+ reason,
243
+ });
244
+ }
245
+ /**
246
+ * Replay every remembered per-file parse failure into the CURRENT
247
+ * session's ledger, at most once per ledger generation (#3070 N1). A
248
+ * memoized `loadQueries` return skips `parseQueryFile` entirely, so
249
+ * without this replay the `tree-sitter-query-parse-failed` record only
250
+ * ever reached the FIRST session that actually parsed — `resetDegradationLedger`
251
+ * (wired into `handleSessionStart`) clears the once-keys every session,
252
+ * but the loader instance and its `parseFailures` memo are kept across
253
+ * sessions (`clients/tree-sitter-shared.ts:39`), the same shape
254
+ * `getBundledQueriesRootHealth` above already re-probes per generation.
255
+ */
256
+ replayQueryParseFailures() {
257
+ const generation = getDegradationLedgerGeneration();
258
+ if (this.parseFailuresReplayedGeneration === generation)
259
+ return;
260
+ this.parseFailuresReplayedGeneration = generation;
261
+ for (const [filePath, reason] of this.parseFailures) {
262
+ recordDegradationOnce({
263
+ kind: "tree-sitter-query-parse-failed",
264
+ subject: filePath,
265
+ reason,
266
+ });
267
+ }
268
+ }
202
269
  /**
203
270
  * Load all queries from the rules/tree-sitter-queries directory.
204
271
  *
@@ -212,9 +279,11 @@ export class TreeSitterQueryLoader {
212
279
  async loadQueries(rootDir = process.cwd(), options = {}) {
213
280
  const resolvedRoot = path.resolve(rootDir);
214
281
  if (!options.force && this.loaded && this.loadedRoot === resolvedRoot) {
282
+ this.replayQueryParseFailures();
215
283
  return this.queries;
216
284
  }
217
285
  this.queries.clear();
286
+ this.parseFailures.clear();
218
287
  this.loaded = false;
219
288
  // Load from user's project rules AND package built-in rules (coexist)
220
289
  const queryDirs = [
@@ -254,6 +323,11 @@ export class TreeSitterQueryLoader {
254
323
  }
255
324
  this.loaded = true;
256
325
  this.loadedRoot = resolvedRoot;
326
+ // Every failure hit above was recorded into THIS generation's ledger
327
+ // directly (via recordQueryParseFailure); mark it replayed so a
328
+ // same-generation memoized call right after this one doesn't redo the
329
+ // (harmless but pointless) replay loop.
330
+ this.parseFailuresReplayedGeneration = getDegradationLedgerGeneration();
257
331
  return this.queries;
258
332
  }
259
333
  /**
@@ -262,37 +336,42 @@ export class TreeSitterQueryLoader {
262
336
  parseQueryFile(filePath, language) {
263
337
  try {
264
338
  const content = fs.readFileSync(filePath, "utf-8");
265
- // Simple YAML parsing (extract key: value pairs)
266
339
  const parsed = this.parseYaml(content);
267
- if (!parsed.id || !parsed.query) {
340
+ // #3054 review F2: type-checked, not just truthy. `yaml.load` widened
341
+ // `parsed` to `Record<string, unknown>`, so a mapping- or
342
+ // array-valued `id`/`query` (a plausible authoring slip: forgetting
343
+ // the `|` on `query:` turns the block into a nested mapping) is
344
+ // truthy and used to sail through the old `!parsed.id || !parsed.query`
345
+ // check, then `String(...)` turned it into the literal text
346
+ // `"[object Object]"` — which for `query` reached the Query
347
+ // constructor (SonarCloud typescript:S6551 flagged every such
348
+ // stringification in this function; fixed below via `str()`).
349
+ const id = str(parsed.id);
350
+ const query = typeof parsed.query === "string" ? parsed.query : undefined;
351
+ if (!id || !query) {
268
352
  this.dbg(`Invalid query file: ${filePath}`);
353
+ this.recordQueryParseFailure(filePath, !id
354
+ ? `'id' is missing or not a scalar (got ${typeof parsed.id})`
355
+ : `'query' is missing or not a string (got ${typeof parsed.query})`);
269
356
  return null;
270
357
  }
271
358
  return {
272
- id: String(parsed.id),
273
- name: String(parsed.name || parsed.id),
359
+ id,
360
+ name: str(parsed.name) || id,
274
361
  severity: this.parseSeverity(parsed.severity),
275
- category: String(parsed.category || "general"),
276
- language: String(parsed.language || language),
277
- message: String(parsed.message || `Pattern: ${parsed.id}`),
278
- description: parsed.description
279
- ? String(parsed.description)
280
- : undefined,
281
- query: this.extractMultilineValue(content, "query") || String(parsed.query),
362
+ category: str(parsed.category) || "general",
363
+ language: str(parsed.language) || language,
364
+ message: str(parsed.message) || `Pattern: ${id}`,
365
+ description: str(parsed.description) || undefined,
366
+ query,
282
367
  metavars: Array.isArray(parsed.metavars)
283
368
  ? parsed.metavars.map(String)
284
- : this.extractMetavars(String(parsed.query)),
285
- post_filter: parsed.post_filter
286
- ? String(parsed.post_filter)
287
- : undefined,
369
+ : this.extractMetavars(query),
370
+ post_filter: str(parsed.post_filter) || undefined,
288
371
  // biome-ignore lint/suspicious/noExplicitAny: Post filter params
289
372
  post_filter_params: parsed.post_filter_params,
290
- defect_class: parsed.defect_class
291
- ? String(parsed.defect_class)
292
- : undefined,
293
- inline_tier: parsed.inline_tier
294
- ? String(parsed.inline_tier)
295
- : undefined,
373
+ defect_class: str(parsed.defect_class) || undefined,
374
+ inline_tier: (str(parsed.inline_tier) || undefined),
296
375
  skip_test_files: parsed.skip_test_files === true,
297
376
  ignore_paths: Array.isArray(parsed.ignore_paths)
298
377
  ? parsed.ignore_paths.map(String)
@@ -310,178 +389,37 @@ export class TreeSitterQueryLoader {
310
389
  owasp: Array.isArray(parsed.owasp)
311
390
  ? parsed.owasp.map(String)
312
391
  : undefined,
313
- confidence: parsed.confidence
314
- ? String(parsed.confidence)
315
- : undefined,
392
+ confidence: (str(parsed.confidence) || undefined),
316
393
  has_fix: parsed.has_fix === true || parsed.has_fix === "true",
317
- fix_action: parsed.fix_action ? String(parsed.fix_action) : undefined,
394
+ fix_action: str(parsed.fix_action) || undefined,
318
395
  filePath,
319
396
  };
320
397
  }
321
398
  catch (err) {
322
399
  this.dbg(`Failed to parse ${filePath}: ${err}`);
400
+ this.recordQueryParseFailure(filePath, err instanceof Error ? err.message : String(err));
323
401
  return null;
324
402
  }
325
403
  }
326
404
  /**
327
- * Simple YAML parser for our query files
405
+ * Parse a query file's YAML with `js-yaml` — the same real parser
406
+ * `clients/dispatch/runners/yaml-rule-parser.ts` uses for ast-grep rules
407
+ * (#206: a hand-rolled line scanner flattened nested structures there; the
408
+ * hand-rolled scanner this loader carried made the identical mistake,
409
+ * twice over — its inline `[a, b]` array branch unquoted list items but
410
+ * its multi-line `- item` branch did not, so `console-statement.yml`'s
411
+ * quoted `ignore_paths` glob parsed with the quote marks attached and the
412
+ * #965 carve-out never matched a path, #3041/#3046). A genuine syntax
413
+ * error throws, caught by `parseQueryFile`'s `try`/`catch`; a
414
+ * syntactically valid but wrong-shaped document (a bare scalar, a list,
415
+ * `null`) is cast here and skipped by `parseQueryFile`'s `id`/`query`
416
+ * type check below — property access on a non-object primitive never
417
+ * throws in JS, so no separate `typeof parsed !== "object"` guard is
418
+ * needed here (#3054 review F3: that guard was vacuous — deleting it
419
+ * reds nothing, every case it caught was already caught one frame up).
328
420
  */
329
421
  parseYaml(content) {
330
- const result = {};
331
- const lines = content.split("\n");
332
- for (let i = 0; i < lines.length; i++) {
333
- const line = lines[i];
334
- const match = line.match(/^([a-z_]+):\s*(.*)$/);
335
- if (match) {
336
- const key = match[1];
337
- // Strip a trailing YAML comment (` # …`) on unquoted scalars, as the
338
- // array-item branch below already does. Without this, a rule written
339
- // `post_filter: not_in_test_block # skip test blocks` carried the
340
- // whole comment as the filter NAME, so the filter never resolved and
341
- // the rule reported unfiltered matches.
342
- let value = stripInlineComment(match[2].trim());
343
- // Handle arrays inline: metavars: [A, B, C]
344
- if (value.startsWith("[") && value.endsWith("]")) {
345
- value = value
346
- .slice(1, -1)
347
- .split(",")
348
- .map((s) => s.trim().replace(/^["']|["']$/g, ""));
349
- }
350
- // Handle multi-line arrays: metavars:\n - A\n - B
351
- // and nested objects: post_filter_params:\n KEY: "value"
352
- else if (value === "") {
353
- const arrayItems = [];
354
- const nestedObj = {};
355
- const baseIndent = line.match(/^(\s*)/)?.[0].length || 0;
356
- for (let j = i + 1; j < lines.length; j++) {
357
- const nextLine = lines[j];
358
- const nextIndent = nextLine.match(/^(\s*)/)?.[0].length || 0;
359
- // Stop if we hit a line with same or less indent (new key)
360
- if (nextIndent <= baseIndent &&
361
- nextLine.trim() !== "" &&
362
- nextLine.match(/^\S/)) {
363
- break;
364
- }
365
- // Check if it's an array item
366
- const itemMatch = nextLine.match(/^\s+-\s*(.+)$/);
367
- if (itemMatch) {
368
- const item = itemMatch[1].trim().replace(/\s*#.*$/, "");
369
- if (item)
370
- arrayItems.push(item);
371
- continue;
372
- }
373
- // Check if it's a nested key: value pair
374
- const nestedMatch = nextLine.match(/^\s+(\w+):\s*(.+)$/);
375
- if (nestedMatch) {
376
- let nv = nestedMatch[2].trim();
377
- if ((nv.startsWith('"') && nv.endsWith('"')) ||
378
- (nv.startsWith("'") && nv.endsWith("'"))) {
379
- nv = nv.slice(1, -1);
380
- }
381
- nestedObj[nestedMatch[1]] = nv;
382
- }
383
- }
384
- if (arrayItems.length > 0) {
385
- value = arrayItems;
386
- }
387
- else if (Object.keys(nestedObj).length > 0) {
388
- // biome-ignore lint/suspicious/noExplicitAny: nested object from YAML
389
- result[key] = nestedObj;
390
- continue;
391
- }
392
- }
393
- // Handle booleans
394
- else if (value === "true")
395
- value = true;
396
- else if (value === "false")
397
- value = false;
398
- // Strip quotes from strings
399
- else if (value.startsWith('"') && value.endsWith('"')) {
400
- value = value.slice(1, -1);
401
- }
402
- result[key] = value;
403
- }
404
- }
405
- return result;
406
- }
407
- /**
408
- * Extract a multiline value (like query) from YAML
409
- */
410
- extractMultilineValue(content, key) {
411
- const lines = content.split("\n");
412
- let startLine = -1;
413
- let startIndent = 0;
414
- const keyPrefix = `${key}:`;
415
- // Find the key line
416
- for (let i = 0; i < lines.length; i++) {
417
- const trimmed = lines[i].trimStart();
418
- if (trimmed.startsWith(keyPrefix)) {
419
- startLine = i;
420
- startIndent = lines[i].length - trimmed.length;
421
- const afterKey = trimmed.slice(keyPrefix.length).trim();
422
- // If there's content on the same line (not just |), return it
423
- if (afterKey && afterKey !== "|")
424
- return afterKey;
425
- break;
426
- }
427
- }
428
- if (startLine === -1)
429
- return null;
430
- // Collect all lines until we hit a new key with same or less indent
431
- const valueLines = [];
432
- for (let i = startLine + 1; i < lines.length; i++) {
433
- const line = lines[i];
434
- // Track empty lines
435
- if (!line.trim()) {
436
- valueLines.push("");
437
- continue;
438
- }
439
- // Check indent
440
- const indentMatch = line.match(/^(\s*)/);
441
- const indent = indentMatch ? indentMatch[1].length : 0;
442
- const trimmed = line.trim();
443
- // Stop at a new top-level key (same or less indent than the key).
444
- if (indent <= startIndent && trimmed.match(/^[a-z_]+:/)) {
445
- break;
446
- }
447
- // A comment at or below the key's indent is a document-level comment
448
- // that follows the block, not part of it — stop. Block-scalar content
449
- // (including native tree-sitter predicate lines `#eq?`/`#match?`) is
450
- // always MORE indented than the key, so those are preserved. Without
451
- // this, a stray `# …` line between a `query: |` block and the next key
452
- // was appended to the query and made it fail to compile (mixed-async).
453
- if (trimmed.startsWith("#") && indent <= startIndent) {
454
- break;
455
- }
456
- // Skip YAML comment lines for most keys, but preserve native
457
- // tree-sitter predicate lines in query blocks (#eq?, #match?, ...).
458
- if (trimmed.startsWith("#") && key !== "query")
459
- continue;
460
- // This is part of the multiline value
461
- valueLines.push(line);
462
- }
463
- // Strip the common minimum indent (relative to startIndent).
464
- // YAML's `|` block scalar preserves content with consistent
465
- // indentation; we want to remove the leading whitespace that
466
- // was used for YAML formatting.
467
- // biome-ignore lint/suspicious/noExplicitAny: line iteration
468
- const nonEmpty = valueLines.filter((l) => l.trim().length > 0);
469
- if (nonEmpty.length > 0) {
470
- const minExtraIndent = Math.min(...nonEmpty.map((l) => {
471
- const m = l.match(/^(\s*)/);
472
- return (m?.[1].length ?? 0) - startIndent;
473
- }));
474
- for (let i = 0; i < valueLines.length; i++) {
475
- if (valueLines[i].trim().length === 0)
476
- continue; // leave blank lines alone
477
- valueLines[i] = valueLines[i].slice(startIndent + Math.max(0, minExtraIndent));
478
- }
479
- }
480
- // Clean up - remove trailing empty lines
481
- while (valueLines.length > 0 && !valueLines[valueLines.length - 1].trim()) {
482
- valueLines.pop();
483
- }
484
- return valueLines.length > 0 ? valueLines.join("\n") : null;
422
+ return yaml.load(content);
485
423
  }
486
424
  /**
487
425
  * Parse severity string to valid type
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { stat } from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
@@ -11,6 +12,8 @@ import { collectForwardImportMtimes } from "./blocker-freshness.js";
11
12
  import { freshnessFromMtime } from "./freshness.js";
12
13
  import { PAST_EOF_STALE_MARKER } from "./diagnostic-line-freshness.js";
13
14
  import { STALE_LINE_MARKER } from "./stale-marker.js";
15
+ import { anchorsForDiagnostic, applyDispositions, getDisposition, registerWidgetDispositionReconciler, } from "./diagnostic-dispositions.js";
16
+ import { recordDegradationOnce } from "./degradation-ledger.js";
14
17
  /**
15
18
  * Canonical key for the `files` map (and `diagnosticsWriteGuard`) — #1020.
16
19
  *
@@ -111,6 +114,85 @@ function maybePruneInactiveFileRecords() {
111
114
  export function setRenderCallback(fn) {
112
115
  requestRenderFn = fn;
113
116
  }
117
+ /** Reconcile a mark through the same apply/count/commit path as dispatch. */
118
+ export function reconcileWidgetDisposition(cwd, target, _disposition, content) {
119
+ const current = getFileDiagnostics(target.filePath);
120
+ if (!current) {
121
+ recordDegradationOnce({
122
+ kind: "widget-disposition-reconcile-fallback",
123
+ subject: target.filePath,
124
+ reason: "the marked finding's widget record is absent",
125
+ });
126
+ return;
127
+ }
128
+ let source = content ?? target.content;
129
+ if (source === undefined) {
130
+ try {
131
+ source = readFileSync(target.filePath, "utf8");
132
+ }
133
+ catch {
134
+ recordDegradationOnce({
135
+ kind: "widget-disposition-reconcile-fallback",
136
+ subject: target.filePath,
137
+ reason: "the disposition anchor content could not be read",
138
+ });
139
+ source = "";
140
+ }
141
+ }
142
+ const active = new Set(applyDispositions(current, cwd, target.filePath, source));
143
+ const normalized = current.map((diagnostic) => {
144
+ const { strict, weak } = anchorsForDiagnostic(cwd, target.filePath, diagnostic, source);
145
+ const entry = getDisposition(cwd, strict) ?? getDisposition(cwd, weak);
146
+ if (!active.has(diagnostic) &&
147
+ (entry?.disposition === "false-positive" ||
148
+ entry?.disposition === "suppress")) {
149
+ return { ...diagnostic, disposition: entry.disposition, flagged: false };
150
+ }
151
+ const { disposition: _disposition, flagged: _flagged, ...baseDiagnostic } = diagnostic;
152
+ return entry?.disposition === "flagged"
153
+ ? { ...baseDiagnostic, flagged: true }
154
+ : baseDiagnostic;
155
+ });
156
+ const rec = getOrCreate(target.filePath);
157
+ const writeIndex = admitWidgetDiagnosticsWrite(target.filePath);
158
+ if (!diagnosticsWriteGuard.shouldWrite(fileMapKey(target.filePath), writeIndex))
159
+ return;
160
+ commitDiagnostics(rec, target.filePath, normalized, Date.now());
161
+ }
162
+ /** Reserve a widget write for a producer that has no runtime write index. */
163
+ export function admitWidgetDiagnosticsWrite(filePath, writeIndex) {
164
+ const key = fileMapKey(filePath);
165
+ const token = writeIndex ?? diagnosticsWriteGuard.nextToken(key);
166
+ diagnosticsWriteGuard.shouldWrite(key, token);
167
+ runnerWriteGuard.shouldWrite(key, token);
168
+ return token;
169
+ }
170
+ registerWidgetDispositionReconciler((cwd, target, disposition) => reconcileWidgetDisposition(cwd, target, disposition, target.content));
171
+ /** Consume the host-side disposition event and refresh the real widget store. */
172
+ export function wireWidgetDispositionSubscriber(args) {
173
+ args.events?.on?.("pilens:diagnostic:disposition", (data) => {
174
+ if (!data || typeof data !== "object")
175
+ return;
176
+ const payload = data;
177
+ if (payload.source !== "pi-lens" || typeof payload.cwd !== "string")
178
+ return;
179
+ if (typeof payload.filePath !== "string" ||
180
+ typeof payload.disposition !== "string")
181
+ return;
182
+ if (typeof payload.message !== "string")
183
+ return;
184
+ if (!["false-positive", "suppress", "defer", "flagged"].includes(payload.disposition))
185
+ return;
186
+ reconcileWidgetDisposition(payload.cwd, {
187
+ cwd: payload.cwd,
188
+ filePath: payload.filePath,
189
+ message: typeof payload.message === "string" ? payload.message : "",
190
+ ...(typeof payload.tool === "string" ? { tool: payload.tool } : {}),
191
+ ...(typeof payload.rule === "string" ? { rule: payload.rule } : {}),
192
+ ...(typeof payload.line === "number" ? { line: payload.line } : {}),
193
+ }, payload.disposition);
194
+ });
195
+ }
114
196
  /**
115
197
  * #2275 review F1: files whose dependency-drift-demoted rows the footer has
116
198
  * ACTUALLY DRAWN since the last turn end — the delivery population
@@ -428,6 +510,8 @@ function countDiagnostics(diags) {
428
510
  let errors = 0;
429
511
  let warnings = 0;
430
512
  for (const diagnostic of diags) {
513
+ if (diagnostic.disposition)
514
+ continue;
431
515
  if (isBlocking(diagnostic))
432
516
  blocking++;
433
517
  // A past-EOF stale entry keeps its severity for display purposes but is
@@ -1065,6 +1149,7 @@ export function renderWidget(width, theme) {
1065
1149
  const totalBlocking = countBlockingIn(deduped);
1066
1150
  const totalErrors = countTotalIn("error", deduped);
1067
1151
  const totalWarnings = countTotalIn("warning", deduped);
1152
+ const totalSuppressed = countSuppressedIn(deduped);
1068
1153
  const hasPendingAnalysis = deduped.some(isPendingAnalysis);
1069
1154
  const errorChunk = totalErrors > 0
1070
1155
  ? (totalBlocking > 0 ? red : yellow)(`●${totalErrors}E`)
@@ -1082,6 +1167,9 @@ export function renderWidget(width, theme) {
1082
1167
  const lspChip = useHorizontal && spawning.length > 0 ? " " + dim("LSP↑") : "";
1083
1168
  const header = ` ${cyan("pi-lens")}${langStr ? " " + dim(langStr) : ""}${lspChip}${summary ? " " + summary : ""}`;
1084
1169
  lines.push(fitLine(header, w));
1170
+ if (totalSuppressed > 0) {
1171
+ lines.push(fitLine(` ${dim(`suppressed: ${totalSuppressed}`)}`, w));
1172
+ }
1085
1173
  // File list — display order varies by mode
1086
1174
  if (useHorizontal) {
1087
1175
  const displayOrder = sortByTierThenRecency(recencySorted);
@@ -1407,6 +1495,16 @@ function countTotalIn(severity, recs) {
1407
1495
  }
1408
1496
  return n;
1409
1497
  }
1498
+ function countSuppressedIn(recs) {
1499
+ let n = 0;
1500
+ for (const rec of recs) {
1501
+ for (const diagnostic of rec.allDiagnostics) {
1502
+ if (diagnostic.disposition)
1503
+ n++;
1504
+ }
1505
+ }
1506
+ return n;
1507
+ }
1410
1508
  function countBlockingIn(recs) {
1411
1509
  let n = 0;
1412
1510
  for (const rec of recs)
@@ -18,6 +18,7 @@ import * as path from "node:path";
18
18
  import { createDeadline, forEachCooperatively, yieldIfOverBudget, } from "./cooperative-budget.js";
19
19
  import { incrementDegradationCount } from "./degradation-ledger.js";
20
20
  import { KIND_EXTENSIONS } from "./file-kinds.js";
21
+ import { isTestFileName } from "./file-role.js";
21
22
  import { PathKeyedMap } from "./path-keyed-map.js";
22
23
  import { isAtOrAboveHomeDir, normalizeEphemeralMapKey } from "./path-utils.js";
23
24
  import { createSingleFlight } from "./single-flight.js";
@@ -87,14 +88,16 @@ const STOPWORDS = new Set([
87
88
  "not",
88
89
  "with",
89
90
  ]);
90
- const TEST_VENDOR_RE = /(?:(^|[\\/])(?:tests?|__tests__|spec|specs|__mocks__|vendor|node_modules|examples?|fixtures?|\.git|dist|build|coverage)([\\/]|$))|(?:\.(?:test|spec)\.[a-z]+$)/i;
91
+ // Retain the old directory alternatives and anchors; filename classification
92
+ // belongs to file-role (#2928). The relevance multiplier remains 0.3.
93
+ const VENDOR_DIR_RE = /(^|[\\/])(?:tests?|__tests__|spec|specs|__mocks__|vendor|node_modules|examples?|fixtures?|\.git|dist|build|coverage)([\\/]|$)/i;
91
94
  const DOC_FILE_RE = /\.(?:md|mdx|markdown|json|json5|jsonc|txt|rst|lock|ya?ml|toml|csv)$/i;
92
95
  const TEST_VENDOR_PENALTY = 0.3;
93
96
  const DOC_FILE_PENALTY = 0.5;
94
97
  const BM25_K1 = 1.2;
95
98
  const BM25_B = 0.75;
96
99
  function isTestOrVendor(file) {
97
- return TEST_VENDOR_RE.test(file);
100
+ return isTestFileName(file) || VENDOR_DIR_RE.test(file);
98
101
  }
99
102
  function isDocFile(file) {
100
103
  return DOC_FILE_RE.test(file);
@@ -47,6 +47,13 @@ export class WriteOrderingGuard {
47
47
  this.lastSeen.set(key, token);
48
48
  return true;
49
49
  }
50
+ /** Allocate the next ordered position for a writer admitted without a token. */
51
+ nextToken(key) {
52
+ const last = this.lastSeen.get(key);
53
+ const next = (last ?? 0) + 1;
54
+ this.lastSeen.set(key, next);
55
+ return next;
56
+ }
50
57
  /** Drop tracked ordering state for `key` (e.g. on cache eviction). */
51
58
  delete(key) {
52
59
  this.lastSeen.delete(key);