@ijfw/memory-server 1.4.3 → 1.5.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 (233) hide show
  1. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  2. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  3. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  4. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  5. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  6. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  7. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  8. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  9. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  23. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  24. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  26. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  27. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  28. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  29. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  47. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  48. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  49. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  50. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  52. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  53. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  54. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  68. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  69. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  71. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  72. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  73. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  74. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  92. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  93. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  94. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  95. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  96. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  97. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  98. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  99. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  115. package/package.json +1 -1
  116. package/src/active-extension-writer.js +144 -64
  117. package/src/api-client.js +43 -5
  118. package/src/audit-roster.js +80 -5
  119. package/src/blackboard.js +298 -6
  120. package/src/cli-run.js +33 -5
  121. package/src/codex-agents.js +96 -5
  122. package/src/cost/aggregator.js +39 -9
  123. package/src/cost/pricing.js +57 -0
  124. package/src/cost/readers/gemini.js +1 -1
  125. package/src/cross-audit-chunker.js +189 -0
  126. package/src/cross-dispatcher.js +124 -21
  127. package/src/cross-orchestrator-cli.js +550 -14
  128. package/src/cross-orchestrator.js +1171 -10
  129. package/src/cross-project-search.js +195 -9
  130. package/src/dashboard-client-planning.html +273 -0
  131. package/src/dashboard-client-waves.html +304 -0
  132. package/src/dashboard-client.html +17 -2
  133. package/src/dashboard-server.js +152 -0
  134. package/src/deploy-alerts.js +150 -0
  135. package/src/design/iframe-bridge.js +242 -0
  136. package/src/design-companion.js +144 -0
  137. package/src/dispatch/checkpoint-cli.js +97 -0
  138. package/src/dispatch/colon-syntax.js +81 -1
  139. package/src/dispatch/extension.js +27 -1
  140. package/src/dispatch/registry-cli.js +4 -1
  141. package/src/dispatch/wave-cli.js +323 -0
  142. package/src/dispatch/worktree-cli.js +40 -0
  143. package/src/dispatch-planner.js +97 -2
  144. package/src/dream/runner.mjs +47 -11
  145. package/src/dream/stage-runner.js +40 -0
  146. package/src/dream/state-file.js +102 -0
  147. package/src/extension-installer.js +70 -24
  148. package/src/extension-quota-tracker.js +4 -2
  149. package/src/extension-registry.js +289 -35
  150. package/src/feedback-detector.js +26 -0
  151. package/src/fs-lock.js +259 -7
  152. package/src/gate-result.js +95 -1
  153. package/src/hero-line.js +86 -5
  154. package/src/intent-router.js +35 -0
  155. package/src/lib/a11y-contract.js +117 -0
  156. package/src/lib/atomic-io.js +29 -8
  157. package/src/lib/cache-keepalive.js +150 -0
  158. package/src/lib/jsonl-rotation.js +104 -0
  159. package/src/lib/lighthouse-pillar.js +121 -0
  160. package/src/lib/llm-call.js +121 -0
  161. package/src/lib/playwright-baseline.js +205 -0
  162. package/src/lib/rekor-bridge.js +221 -0
  163. package/src/lib/repo-map.js +392 -0
  164. package/src/lib/shasum-verify.js +164 -0
  165. package/src/lib/sketches-gc.js +132 -0
  166. package/src/lib/tmp-suffix.js +62 -0
  167. package/src/lib/ui-review-runner.js +554 -0
  168. package/src/lib/uispec-drift.js +301 -0
  169. package/src/lib/uispec-intake.js +381 -0
  170. package/src/lib/worktree-guards.js +118 -0
  171. package/src/lib/worktree-recovery.js +100 -0
  172. package/src/memory/auto-linker.js +152 -0
  173. package/src/memory/benchmark.js +498 -0
  174. package/src/memory/dedup.js +126 -0
  175. package/src/memory/embedding-cache.js +136 -0
  176. package/src/memory/fact-extractor.js +168 -0
  177. package/src/memory/fts5.js +65 -1
  178. package/src/memory/migrations/004-bitemporal.js +91 -0
  179. package/src/memory/migrations/005-vector-cache.js +61 -0
  180. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  181. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  182. package/src/memory/migrations/008-write-provenance.js +41 -0
  183. package/src/memory/obsidian-parser.js +91 -0
  184. package/src/memory/query-dataview.js +86 -0
  185. package/src/memory/search.js +10 -0
  186. package/src/memory/temporal.js +529 -0
  187. package/src/memory/tokenize.js +10 -0
  188. package/src/memory-facts-handler.js +37 -0
  189. package/src/memory-feedback.js +260 -2
  190. package/src/model-refresh.js +292 -0
  191. package/src/observability/cost-anomaly.js +166 -0
  192. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  193. package/src/observability/trace-id.js +163 -0
  194. package/src/orchestrator/agents-md-blackboard.js +152 -0
  195. package/src/orchestrator/checkpoint-contract.md +140 -0
  196. package/src/orchestrator/debug-trident.js +570 -0
  197. package/src/orchestrator/merge-block-aware.js +350 -0
  198. package/src/orchestrator/plan-checker.js +475 -0
  199. package/src/orchestrator/post-done-runner.js +249 -0
  200. package/src/orchestrator/review.js +136 -0
  201. package/src/orchestrator/runtime-loop.js +430 -0
  202. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  203. package/src/orchestrator/skill-telemetry.js +37 -0
  204. package/src/orchestrator/state-events.js +459 -0
  205. package/src/orchestrator/state-sdk.js +1764 -0
  206. package/src/orchestrator/status-protocol.js +235 -0
  207. package/src/orchestrator/subagent-telemetry.js +452 -0
  208. package/src/orchestrator/termination.js +160 -0
  209. package/src/orchestrator/verification-gate.js +281 -0
  210. package/src/orchestrator/wave-state.js +564 -0
  211. package/src/orchestrator/worktree-provision.js +77 -0
  212. package/src/override-use-registry.js +111 -5
  213. package/src/receipts.js +36 -4
  214. package/src/recovery/checkpoint.js +56 -3
  215. package/src/recovery/code-fixer.js +656 -0
  216. package/src/recovery/truncation.js +317 -0
  217. package/src/redactor.js +75 -6
  218. package/src/runtime-mediator.js +15 -0
  219. package/src/sanitizer.js +10 -0
  220. package/src/search-hybrid.js +139 -0
  221. package/src/server.js +603 -59
  222. package/src/swarm/worktree.js +27 -4
  223. package/src/swarm-config.js +113 -12
  224. package/src/team/domain-templates/book.json +51 -0
  225. package/src/team/domain-templates/business.json +41 -0
  226. package/src/team/domain-templates/content.json +50 -0
  227. package/src/team/domain-templates/design.json +44 -0
  228. package/src/team/domain-templates/research.json +41 -0
  229. package/src/team/domain-templates/software.json +40 -0
  230. package/src/team/generator.js +278 -3
  231. package/src/team/modify.js +203 -0
  232. package/src/team/schemas.js +48 -0
  233. package/src/update-apply.js +19 -3
@@ -116,6 +116,63 @@ export function computeCost(modelId, usage) {
116
116
  );
117
117
  }
118
118
 
119
+ /**
120
+ * Per-tier dashboard view ($/M tokens).
121
+ * Single source of truth for any UI that wants to show Anthropic family
122
+ * rates without hardcoding numbers. Derived from the canonical model_prices
123
+ * table via the same getPricing() resolver everything else uses, so it
124
+ * cannot drift from receipt cost computation.
125
+ *
126
+ * Tiers are keyed by lowercase family name -- callers tier-match on
127
+ * model id substring (opus | sonnet | haiku). Values are USD per million
128
+ * tokens for input/output/cache_read/cache_creation.
129
+ */
130
+ export function getTierRatesPerMillion() {
131
+ const M = 1_000_000;
132
+ const tiers = {
133
+ opus: 'claude-opus-4-1',
134
+ sonnet: 'claude-sonnet-4-5',
135
+ haiku: 'claude-haiku-4-5',
136
+ };
137
+ const out = {};
138
+ for (const [tier, modelId] of Object.entries(tiers)) {
139
+ const p = getPricing(modelId);
140
+ out[tier] = {
141
+ in: p.in * M,
142
+ out: p.out * M,
143
+ cache_read: p.cache_read * M,
144
+ cache_creation: p.cache_create_5m * M,
145
+ };
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /**
151
+ * Per-provider USD/token estimate for budget pre-flight (cross-dispatcher).
152
+ * One representative model per provider, resolved through the canonical
153
+ * pricing table so this cannot drift from receipt billing.
154
+ *
155
+ * Returns USD per *input* token (output is typically priced separately;
156
+ * pre-flight uses the conservative input-only estimate).
157
+ */
158
+ const PROVIDER_DEFAULT_MODEL = {
159
+ claude: 'claude-sonnet-4-5',
160
+ anthropic: 'claude-sonnet-4-5',
161
+ codex: 'gpt-5',
162
+ opencode: 'gpt-5',
163
+ aider: 'gpt-5',
164
+ copilot: 'gpt-4o',
165
+ gemini: 'gemini/gemini-1.5-flash',
166
+ };
167
+
168
+ export function getProviderInputRate(providerId) {
169
+ const id = (providerId || '').toLowerCase().trim();
170
+ const modelId = PROVIDER_DEFAULT_MODEL[id];
171
+ if (!modelId) return null;
172
+ const p = getPricing(modelId);
173
+ return p.in;
174
+ }
175
+
119
176
  /** Return the raw prices table for /api/prices transparency endpoint. */
120
177
  export function getPricesTable() {
121
178
  const prices = loadPrices();
@@ -90,7 +90,7 @@ function processGeminiChat(chat, projectHash) {
90
90
  session_id: sessionId,
91
91
  project: projectHash,
92
92
  timestamp: lastTimestamp,
93
- model: 'gemini-2.0-flash', // Gemini CLI default; no model id in chat file
93
+ model: 'gemini-3.1-pro', // Gemini CLI default; no model id in chat file
94
94
  input_tokens: inputTokens,
95
95
  output_tokens: outputTokens,
96
96
  cache_create_tokens_5m: 0,
@@ -0,0 +1,189 @@
1
+ // r17.1 (item 5) — Chunking strategy for cross-audit on large targets.
2
+ //
3
+ // Cross-audit's auditors (codex / gemini / claude / etc) all have practical
4
+ // input limits well below the 64 KB target cap — and even at the cap, audit
5
+ // quality degrades because the model is asked to grade ~20K tokens at once.
6
+ // This module splits a large target into overlapping chunks, lets the caller
7
+ // audit each chunk independently, then merges + dedupes the findings.
8
+ //
9
+ // Design choices:
10
+ // - Boundary-aware split: prefers blank-line, then sentence, then word, then
11
+ // character. Audit quality drops sharply when a chunk ends mid-thought.
12
+ // - Overlapping chunks: ~10% overlap so findings near a boundary appear in
13
+ // both neighbors and dedup catches them as a single finding.
14
+ // - Finding shape match: hybrid with the existing finding structure from
15
+ // `printFindings`: { severity, target, finding, action } (severity is
16
+ // 'high' | 'medium' | 'low'). Unknown keys preserved.
17
+ // - Dedupe: case-insensitive jaccard over the (target + finding) text. A
18
+ // duplicate threshold of 0.85 is the default; configurable via opts.
19
+
20
+ const DEFAULT_CHUNK_SIZE = Math.floor(64 * 1024 * 0.9); // 90% of TARGET_FILE_SIZE_CAP
21
+ const DEFAULT_OVERLAP = Math.floor(64 * 1024 * 0.10); // 10%
22
+ const SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
23
+
24
+ /**
25
+ * Split text into overlapping chunks at the best boundary available.
26
+ *
27
+ * @param {string} text - the text to chunk
28
+ * @param {object} [opts]
29
+ * @param {number} [opts.chunkSize] - max chars per chunk (default 90% of cap)
30
+ * @param {number} [opts.overlap] - chars of overlap (default 10% of cap)
31
+ * @returns {Array<{ index: number, start: number, end: number, text: string }>}
32
+ */
33
+ export function chunkText(text, opts = {}) {
34
+ if (typeof text !== 'string' || text.length === 0) return [];
35
+
36
+ const chunkSize = Math.max(1024, opts.chunkSize ?? DEFAULT_CHUNK_SIZE);
37
+ // r17-M1: clamp overlap to <= chunkSize/2 so the advance step is always
38
+ // strictly forward. Without this, opts.chunkSize=500 + default overlap
39
+ // (~6553) would set start to max(start+1, cutEnd - 6553) and the loop
40
+ // would advance one char per iteration on small chunks.
41
+ const overlap = Math.min(Math.floor(chunkSize / 2), Math.max(0, opts.overlap ?? DEFAULT_OVERLAP));
42
+
43
+ // Tiny text: one chunk.
44
+ if (text.length <= chunkSize) {
45
+ return [{ index: 0, start: 0, end: text.length, text }];
46
+ }
47
+
48
+ const chunks = [];
49
+ let start = 0;
50
+ let index = 0;
51
+ while (start < text.length) {
52
+ const remaining = text.length - start;
53
+ if (remaining <= chunkSize) {
54
+ chunks.push({ index, start, end: text.length, text: text.slice(start) });
55
+ break;
56
+ }
57
+
58
+ // Find the best boundary within [start + chunkSize - 1024, start + chunkSize].
59
+ // We slide LEFT from the nominal end looking for a clean break point.
60
+ const nominalEnd = start + chunkSize;
61
+ const searchFloor = Math.max(start + 1, nominalEnd - 1024);
62
+ let cutEnd = nominalEnd;
63
+
64
+ // Prefer blank-line boundary.
65
+ let p = text.lastIndexOf('\n\n', nominalEnd);
66
+ if (p >= searchFloor) {
67
+ cutEnd = p + 2;
68
+ } else {
69
+ // Try sentence-ending punctuation.
70
+ p = Math.max(
71
+ text.lastIndexOf('. ', nominalEnd),
72
+ text.lastIndexOf('! ', nominalEnd),
73
+ text.lastIndexOf('? ', nominalEnd),
74
+ text.lastIndexOf('.\n', nominalEnd),
75
+ );
76
+ if (p >= searchFloor) {
77
+ cutEnd = p + 2;
78
+ } else {
79
+ // Try a single newline.
80
+ p = text.lastIndexOf('\n', nominalEnd);
81
+ if (p >= searchFloor) cutEnd = p + 1;
82
+ // Else fall through to nominal char boundary.
83
+ }
84
+ }
85
+
86
+ chunks.push({ index, start, end: cutEnd, text: text.slice(start, cutEnd) });
87
+ index++;
88
+ // Advance with overlap.
89
+ start = Math.max(start + 1, cutEnd - overlap);
90
+ }
91
+ return chunks;
92
+ }
93
+
94
+ // Normalize finding shape — accept {severity, finding, target, action} OR
95
+ // {severity, message} OR raw strings; coerce to a common shape.
96
+ // v1.5.0 wire-W4: widened the field-name fallback chain to cover the field
97
+ // names auditors actually emit. Trident r19 dropped 100% of finding text to
98
+ // "(no detail)" because the lens responses used `description`/`issue`/`detail`
99
+ // rather than `finding`/`message`/`text`. r20 captures them properly.
100
+ function normaliseFinding(raw) {
101
+ if (typeof raw === 'string') {
102
+ return { severity: 'medium', finding: raw, target: '', action: '' };
103
+ }
104
+ if (!raw || typeof raw !== 'object') return null;
105
+ return {
106
+ severity: String(raw.severity || 'medium').toLowerCase(),
107
+ finding: String(
108
+ raw.finding || raw.message || raw.text || raw.description ||
109
+ raw.issue || raw.detail || raw.details || raw.note || raw.summary || ''
110
+ ),
111
+ target: String(
112
+ raw.target || raw.location || raw.path || raw.file ||
113
+ raw.where || raw.line || ''
114
+ ),
115
+ action: String(
116
+ raw.action || raw.fix || raw.recommendation ||
117
+ raw.suggestion || raw.remediation || ''
118
+ ),
119
+ // preserve unknown keys for printers that want them
120
+ _extra: raw,
121
+ };
122
+ }
123
+
124
+ // Jaccard similarity on lowercased word sets. Cheap, deterministic, gives
125
+ // "are these two findings essentially the same?" for free.
126
+ function jaccard(a, b) {
127
+ const tokA = new Set(String(a).toLowerCase().split(/\W+/).filter(t => t.length > 2));
128
+ const tokB = new Set(String(b).toLowerCase().split(/\W+/).filter(t => t.length > 2));
129
+ if (tokA.size === 0 && tokB.size === 0) return 1;
130
+ let inter = 0;
131
+ for (const t of tokA) if (tokB.has(t)) inter++;
132
+ const uni = tokA.size + tokB.size - inter;
133
+ return uni === 0 ? 0 : inter / uni;
134
+ }
135
+
136
+ /**
137
+ * Merge findings from per-chunk audits. Dedupes near-duplicates (Jaccard ≥
138
+ * threshold) and keeps the max severity from each cluster.
139
+ *
140
+ * @param {Array<{ chunkIndex: number, findings: Array }>} perChunkResults
141
+ * @param {object} [opts]
142
+ * @param {number} [opts.dedupeThreshold] - Jaccard threshold (default 0.85)
143
+ * @returns {Array} - merged + deduped findings, sorted high→low severity
144
+ */
145
+ export function mergeFindings(perChunkResults, opts = {}) {
146
+ const threshold = opts.dedupeThreshold ?? 0.85;
147
+ const flat = [];
148
+ for (const r of perChunkResults || []) {
149
+ const chunkIndex = typeof r.chunkIndex === 'number' ? r.chunkIndex : null;
150
+ const findings = Array.isArray(r.findings) ? r.findings : [];
151
+ for (const raw of findings) {
152
+ const n = normaliseFinding(raw);
153
+ if (n) flat.push({ ...n, chunkIndex });
154
+ }
155
+ }
156
+ if (flat.length === 0) return [];
157
+
158
+ // Cluster by jaccard. Each cluster's representative is the first member;
159
+ // we keep the MAX severity across the cluster and record `clusterSize`.
160
+ const clusters = [];
161
+ for (const f of flat) {
162
+ const key = `${f.target} ${f.finding}`;
163
+ let placed = false;
164
+ for (const c of clusters) {
165
+ const repKey = `${c.target} ${c.finding}`;
166
+ if (jaccard(key, repKey) >= threshold) {
167
+ c._members.push(f);
168
+ if ((SEVERITY_RANK[f.severity] || 0) > (SEVERITY_RANK[c.severity] || 0)) {
169
+ c.severity = f.severity;
170
+ }
171
+ placed = true;
172
+ break;
173
+ }
174
+ }
175
+ if (!placed) clusters.push({ ...f, _members: [f] });
176
+ }
177
+
178
+ // Stamp clusterSize + clusterChunks for printers + sort by severity desc.
179
+ for (const c of clusters) {
180
+ c.clusterSize = c._members.length;
181
+ c.clusterChunks = [...new Set(c._members.map(m => m.chunkIndex).filter(i => i !== null))];
182
+ delete c._members;
183
+ }
184
+ clusters.sort((a, b) => (SEVERITY_RANK[b.severity] || 0) - (SEVERITY_RANK[a.severity] || 0));
185
+ return clusters;
186
+ }
187
+
188
+ // Constants exported so consumers + tests can reference the same defaults.
189
+ export const CHUNKER_DEFAULTS = { chunkSize: DEFAULT_CHUNK_SIZE, overlap: DEFAULT_OVERLAP, dedupeThreshold: 0.85 };
@@ -10,8 +10,45 @@
10
10
  // Internal templates
11
11
  // ---------------------------------------------------------------------------
12
12
 
13
+ // v1.5.0 audit-MED-trident-M8 — single canonical severity taxonomy.
14
+ // Findings use CRITICAL/HIGH/MEDIUM/LOW (security-style). Audit DISPOSITIONS
15
+ // (PASS/CONDITIONAL/WARN/FLAG/FAIL) are a separate axis — they describe the
16
+ // audit's *status*, not a finding's *severity*. mergeAudit normalizes any
17
+ // disposition values that slip in as `severity` so they no longer sink to
18
+ // the "unrecognized" bottom of the list.
13
19
  const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
14
20
 
21
+ // M8: map trident dispatch dispositions → finding severities. Used by
22
+ // mergeAudit to coerce stray disposition-valued severities into the canonical
23
+ // taxonomy before sorting. A finding tagged `severity:'warn'` is treated as
24
+ // medium; `flag` as high; `fail` as critical; `pass` as low; `conditional`
25
+ // as medium. Callers that legitimately want disposition values should put
26
+ // them on a separate `disposition` field.
27
+ const DISPOSITION_TO_SEVERITY = {
28
+ pass: 'low',
29
+ conditional: 'medium',
30
+ warn: 'medium',
31
+ flag: 'high',
32
+ fail: 'critical',
33
+ };
34
+
35
+ // Public exports so downstream callers (status renderers, dashboard) share
36
+ // the same vocabulary. M8 closure.
37
+ export const FINDING_SEVERITIES = Object.freeze(['critical', 'high', 'medium', 'low']);
38
+ export const AUDIT_DISPOSITIONS = Object.freeze(['pass', 'conditional', 'warn', 'flag', 'fail']);
39
+
40
+ // normaliseSeverity -- coerce any stray value (disposition or unknown) into
41
+ // a canonical finding severity. Returns the original lower-cased string when
42
+ // already canonical; maps dispositions per DISPOSITION_TO_SEVERITY; returns
43
+ // null on unknown input.
44
+ export function normaliseSeverity(raw) {
45
+ if (raw == null) return null;
46
+ const v = String(raw).toLowerCase().trim();
47
+ if (SEVERITY_ORDER[v] !== undefined) return v;
48
+ if (DISPOSITION_TO_SEVERITY[v]) return DISPOSITION_TO_SEVERITY[v];
49
+ return null;
50
+ }
51
+
15
52
  // Format-contract footer shared by all templates -- tells auditors the exact
16
53
  // fenced block schema to use so parseResponse can extract it reliably.
17
54
  function formatContract(schema) {
@@ -306,12 +343,83 @@ export function mergeResponses(mode, responses) {
306
343
  throw new Error(`Unknown mode: ${mode}`);
307
344
  }
308
345
 
346
+ // v1.5.0 audit-MED-trident-M3 — consensus / contested clustering.
347
+ // Before: findings from N lenses appeared N times in the user-visible list
348
+ // because mergeAudit just flattened + sorted. With Trident running 3 lenses
349
+ // on the same target, a real bug got reported 3 times and looked like 3 bugs.
350
+ //
351
+ // After: lexical-bucket-cluster on a normalized signature (issue + location
352
+ // when available; falls back to text/description). A cluster touched by ≥2
353
+ // lenses is tagged `consensus: true`, `consensusCount: N`, and `consensusLenses`
354
+ // (array of lens ids when discoverable). Single-lens findings carry
355
+ // `consensus: false`. Order: CONSENSUS group first (by severity), single-lens
356
+ // group second (by severity). Within each group, sort by canonical severity
357
+ // (M8 — disposition values are coerced before sort).
358
+ //
359
+ // The clustering is intentionally LEXICAL only — same heuristic as
360
+ // mergeResearch's normaliseClaim. Semantic clustering (paraphrases that mean
361
+ // the same thing) is delegated to the optional synthesis pass.
362
+ function _findingSignature(item) {
363
+ // Prefer (issue + location) when present. Fallback chain: counterArg, text,
364
+ // description, then a stringified blob so two identical objects still match.
365
+ const loc = String(item.location || item.file || '').toLowerCase().trim();
366
+ const issue = String(item.issue || item.counterArg || item.text || item.description || '').toLowerCase().trim();
367
+ if (issue) return `${issue}::${loc}`.replace(/\s+/g, ' ');
368
+ // Last-resort signature: JSON of the item, sorted-keys.
369
+ try {
370
+ return JSON.stringify(item, Object.keys(item || {}).sort()).toLowerCase();
371
+ } catch {
372
+ return String(item).toLowerCase();
373
+ }
374
+ }
375
+
376
+ function _coercedSeverityRank(item) {
377
+ // M8: try canonical severity first, then disposition mapping, then 99.
378
+ const canon = normaliseSeverity(item.severity ?? item.level);
379
+ if (canon == null) return 99;
380
+ return SEVERITY_ORDER[canon] ?? 99;
381
+ }
382
+
309
383
  function mergeAudit(responses) {
310
- const all = responses.flatMap(r => (r && Array.isArray(r.items) ? r.items : []));
311
- return all.slice().sort((a, b) => {
312
- const sa = SEVERITY_ORDER[String(a.severity).toLowerCase()] ?? 99;
313
- const sb = SEVERITY_ORDER[String(b.severity).toLowerCase()] ?? 99;
314
- return sa - sb;
384
+ // Flatten with an auditor index so we can attribute consensus to lenses.
385
+ // responses may include an optional `_lens` / `auditorId` marker on each
386
+ // item (the orchestrator already stamps `_lens` in defaultConvergeDispatch
387
+ // merges). We honor it when present.
388
+ const buckets = new Map();
389
+ responses.forEach((r, auditorIdx) => {
390
+ const items = r && Array.isArray(r.items) ? r.items : [];
391
+ for (const item of items) {
392
+ const sig = _findingSignature(item);
393
+ if (!sig) continue;
394
+ if (!buckets.has(sig)) buckets.set(sig, []);
395
+ const lensId = item._lens || item.auditorId || String(auditorIdx);
396
+ buckets.get(sig).push({ item, lensId, auditorIdx });
397
+ }
398
+ });
399
+
400
+ const out = [];
401
+ for (const [, entries] of buckets) {
402
+ // Distinct lens count: a single lens can only contribute one vote to
403
+ // consensus (it may emit the same finding twice in its own list).
404
+ const distinctLenses = [...new Set(entries.map(e => e.lensId))];
405
+ const consensus = distinctLenses.length >= 2;
406
+ // Use the first entry as the representative finding (oldest wins);
407
+ // attach consensus metadata so consumers can group / highlight.
408
+ const rep = { ...entries[0].item };
409
+ if (consensus) {
410
+ rep.consensus = true;
411
+ rep.consensusCount = distinctLenses.length;
412
+ rep.consensusLenses = distinctLenses;
413
+ } else {
414
+ rep.consensus = false;
415
+ }
416
+ out.push(rep);
417
+ }
418
+
419
+ // Sort: consensus first, then by canonical severity (M8 disposition-coerce).
420
+ return out.sort((a, b) => {
421
+ if (a.consensus !== b.consensus) return a.consensus ? -1 : 1;
422
+ return _coercedSeverityRank(a) - _coercedSeverityRank(b);
315
423
  });
316
424
  }
317
425
 
@@ -371,9 +479,11 @@ function mergeCritique(responses) {
371
479
  const sa = scoreRebuttalSurvival(a);
372
480
  const sb = scoreRebuttalSurvival(b);
373
481
  if (sb !== sa) return sb - sa; // DESC survival
374
- const ra = SEVERITY_ORDER[String(a.severity).toLowerCase()] ?? 99;
375
- const rb = SEVERITY_ORDER[String(b.severity).toLowerCase()] ?? 99;
376
- return ra - rb; // DESC severity (lower index = higher sev)
482
+ // M8: coerce dispositions (warn/flag/fail) to canonical severities so a
483
+ // counter-arg tagged `severity:'warn'` doesn't sink to bottom unranked.
484
+ const ra = _coercedSeverityRank(a);
485
+ const rb = _coercedSeverityRank(b);
486
+ return ra - rb; // ASC by canonical rank = DESC severity
377
487
  });
378
488
  }
379
489
 
@@ -381,18 +491,11 @@ function mergeCritique(responses) {
381
491
  // Budget guard (Step 10B.6)
382
492
  // ---------------------------------------------------------------------------
383
493
 
384
- // Rough per-token list prices (USD) for each provider family.
385
- // Used only for pre-flight estimation -- not for billing.
386
- // Prices are input-side costs at standard rates as of 2026.
387
- const PROVIDER_PRICE_PER_TOKEN = {
388
- codex: 0.000_015, // OpenAI o4-mini input ~$15/M
389
- opencode: 0.000_015,
390
- aider: 0.000_015,
391
- gemini: 0.000_000_5, // Gemini 1.5 Flash input ~$0.50/M
392
- copilot: 0.000_010, // GPT-4o input ~$10/M (conservative)
393
- claude: 0.000_003, // Sonnet input ~$3/M
394
- anthropic: 0.000_003,
395
- };
494
+ // Per-provider input-token price (USD/token), sourced from the canonical
495
+ // pricing module. Used only for pre-flight estimation -- not for billing.
496
+ // Single source of truth = mcp-server/src/cost/pricing.js. H4.8 audit fix.
497
+ import { getProviderInputRate } from './cost/pricing.js';
498
+
396
499
  const DEFAULT_PRICE_PER_TOKEN = 0.000_010; // fallback for unknown providers
397
500
 
398
501
  // estimateCost(target, picks) -- rough cost in USD for one runCrossOp call.
@@ -402,7 +505,7 @@ export function estimateCost(target, picks) {
402
505
  const tokens = charCount / 4;
403
506
  let total = 0;
404
507
  for (const pick of picks) {
405
- const price = PROVIDER_PRICE_PER_TOKEN[pick.id] ?? DEFAULT_PRICE_PER_TOKEN;
508
+ const price = getProviderInputRate(pick.id) ?? DEFAULT_PRICE_PER_TOKEN;
406
509
  total += tokens * price;
407
510
  }
408
511
  return total;