redmine-context 1.0.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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `getOrCompute` — padrão de acesso ao cache dos extratores (M3-04, ADR-004).
3
+ *
4
+ * Encapsula o fluxo canônico "leia; se faltar, extraia sob lock; grave" que TODOS
5
+ * os extratores do M3/M4 usarão, garantindo que uma extração CARA rode UMA única
6
+ * vez mesmo sob concorrência na mesma chave. O helper depende apenas do contrato
7
+ * {@link CacheStore} (lock por chave + get/put), logo funciona com QUALQUER
8
+ * implementação — memória ou disco — sem alteração.
9
+ *
10
+ * Duas checagens complementares evitam recomputar:
11
+ * - fast-path: um `get` ANTES do lock resolve o caso comum (hit) sem serializar;
12
+ * - double-check: após ADQUIRIR o lock, um novo `get` cobre a corrida em que outro
13
+ * waiter da mesma chave já gravou o valor enquanto esperávamos — nesse caso
14
+ * retornamos sem recomputar.
15
+ *
16
+ * POLÍTICA DE ERRO (decisão desta issue): se `compute` lançar, o erro propaga ao
17
+ * DONO do lock (quem chamou `compute`); nada é gravado. Os waiters, ao adquirirem
18
+ * o lock em seguida, veem o miss no double-check e RECOMPUTAM — não herdam a falha
19
+ * do dono. Isso mantém a falha localizada e permite recuperação transparente.
20
+ */
21
+ import type { CacheKey, CacheStore } from './contract.js';
22
+ /**
23
+ * Retorna o valor cacheado de `key`, computando-o sob lock caso ausente.
24
+ *
25
+ * @typeParam V - Tipo do valor cacheado.
26
+ * @param store - Backend de cache (memória ou disco) — ver {@link CacheStore}.
27
+ * @param key - Chave de qualquer camada (ADR-004).
28
+ * @param compute - Produz o valor quando há cache-miss; roda no máximo uma vez por
29
+ * vencedor do lock. Sob concorrência na mesma chave, apenas o dono a executa.
30
+ * @returns O valor cacheado (existente ou recém-computado).
31
+ * @throws Propaga qualquer erro lançado por `compute` ao chamador que a executou;
32
+ * nesse caso nada é gravado e os demais waiters recomputam.
33
+ * @example
34
+ * const extracao = await getOrCompute(store, key, () => extrairAudio(anexo));
35
+ */
36
+ export declare function getOrCompute<V>(store: CacheStore<V>, key: CacheKey, compute: () => Promise<V>): Promise<V>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `getOrCompute` — padrão de acesso ao cache dos extratores (M3-04, ADR-004).
3
+ *
4
+ * Encapsula o fluxo canônico "leia; se faltar, extraia sob lock; grave" que TODOS
5
+ * os extratores do M3/M4 usarão, garantindo que uma extração CARA rode UMA única
6
+ * vez mesmo sob concorrência na mesma chave. O helper depende apenas do contrato
7
+ * {@link CacheStore} (lock por chave + get/put), logo funciona com QUALQUER
8
+ * implementação — memória ou disco — sem alteração.
9
+ *
10
+ * Duas checagens complementares evitam recomputar:
11
+ * - fast-path: um `get` ANTES do lock resolve o caso comum (hit) sem serializar;
12
+ * - double-check: após ADQUIRIR o lock, um novo `get` cobre a corrida em que outro
13
+ * waiter da mesma chave já gravou o valor enquanto esperávamos — nesse caso
14
+ * retornamos sem recomputar.
15
+ *
16
+ * POLÍTICA DE ERRO (decisão desta issue): se `compute` lançar, o erro propaga ao
17
+ * DONO do lock (quem chamou `compute`); nada é gravado. Os waiters, ao adquirirem
18
+ * o lock em seguida, veem o miss no double-check e RECOMPUTAM — não herdam a falha
19
+ * do dono. Isso mantém a falha localizada e permite recuperação transparente.
20
+ */
21
+ /**
22
+ * Retorna o valor cacheado de `key`, computando-o sob lock caso ausente.
23
+ *
24
+ * @typeParam V - Tipo do valor cacheado.
25
+ * @param store - Backend de cache (memória ou disco) — ver {@link CacheStore}.
26
+ * @param key - Chave de qualquer camada (ADR-004).
27
+ * @param compute - Produz o valor quando há cache-miss; roda no máximo uma vez por
28
+ * vencedor do lock. Sob concorrência na mesma chave, apenas o dono a executa.
29
+ * @returns O valor cacheado (existente ou recém-computado).
30
+ * @throws Propaga qualquer erro lançado por `compute` ao chamador que a executou;
31
+ * nesse caso nada é gravado e os demais waiters recomputam.
32
+ * @example
33
+ * const extracao = await getOrCompute(store, key, () => extrairAudio(anexo));
34
+ */
35
+ export async function getOrCompute(store, key, compute) {
36
+ // Fast-path: hit comum não precisa serializar no lock.
37
+ const cached = await store.get(key);
38
+ if (cached !== undefined) {
39
+ return cached;
40
+ }
41
+ return store.lock(key, async () => {
42
+ // Double-check: outro waiter da mesma chave pode ter gravado enquanto
43
+ // esperávamos o lock — reusa o resultado em vez de recomputar.
44
+ const existing = await store.get(key);
45
+ if (existing !== undefined) {
46
+ return existing;
47
+ }
48
+ const value = await compute();
49
+ await store.put(key, value);
50
+ return value;
51
+ });
52
+ }
@@ -0,0 +1,9 @@
1
+ export declare const MODULE_NAME: "cache";
2
+ export { DEFAULT_STALE_LOCK_TTL_MS, instanceHash, serializeCacheKey, type AttachmentCacheKey, type CacheKey, type CacheStore, type CacheStoreOptions, type ExtractorParams, type GcContext, type GcHook, type IssueCacheKey, } from './contract.js';
3
+ export { getOrCompute } from './get-or-compute.js';
4
+ export { InMemoryCacheStore } from './memory.js';
5
+ export { DiskCacheStore, defaultCacheDir, type DiskCacheStoreOptions } from './disk.js';
6
+ export { buildAttachmentKey, deriveAttachmentDigest, type BuildAttachmentKeyInput, type ExtractorConfig, } from './keys.js';
7
+ export { DiskCacheIndex, INDEX_FILE_NAME } from './disk-index.js';
8
+ export type { CacheIndexEntry, CacheEntryType } from './disk-index.js';
9
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_ORIGINAL_FRACTION, planGc, resolveEvictionTarget, type GcCandidate, type GcDecision, type GcPolicyOptions, } from './gc.js';
@@ -0,0 +1,8 @@
1
+ export const MODULE_NAME = 'cache';
2
+ export { DEFAULT_STALE_LOCK_TTL_MS, instanceHash, serializeCacheKey, } from './contract.js';
3
+ export { getOrCompute } from './get-or-compute.js';
4
+ export { InMemoryCacheStore } from './memory.js';
5
+ export { DiskCacheStore, defaultCacheDir } from './disk.js';
6
+ export { buildAttachmentKey, deriveAttachmentDigest, } from './keys.js';
7
+ export { DiskCacheIndex, INDEX_FILE_NAME } from './disk-index.js';
8
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_ORIGINAL_FRACTION, planGc, resolveEvictionTarget, } from './gc.js';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Builder de alto nível da chave attachment-level (M3-03, ADR-004).
3
+ *
4
+ * Monta a {@link AttachmentCacheKey} correta a partir do modelo `Attachment` do
5
+ * contrato ({@link Attachment}) mais a configuração do extrator
6
+ * ({@link ExtractorConfig}), escondendo das camadas superiores dois detalhes:
7
+ *
8
+ * 1. o cálculo do `instance_hash` a partir da URL da instância
9
+ * ({@link instanceHash}); e
10
+ * 2. a origem do `digest` — usa `attachment.digest` quando presente e, como
11
+ * FALLBACK para Redmine < 4.x (que não expõe `digest`), deriva um digest
12
+ * DETERMINÍSTICO de `(id, filesize, created_on)`.
13
+ *
14
+ * A chave attachment-level NÃO depende de `updated_on` da issue: o builder sequer
15
+ * o recebe. Assim, editar a issue (novo `updated_on`, comentários) invalida só a
16
+ * camada de issue e reaproveita 100% das extrações caras dos anexos imutáveis.
17
+ *
18
+ * Sanitização hex "por construção": o digest de fallback é o SHA-256 hex do
19
+ * material derivado — 64 chars `[0-9a-f]`. Ele satisfaz o padrão hex que o
20
+ * {@link DiskCacheStore} usa ao montar `<id>-<digest8>/`, atravessando o caminho
21
+ * hex direto (sem o re-hash de escape reservado a valores não-hex/maliciosos).
22
+ */
23
+ import type { Attachment } from '../contract.js';
24
+ import { type AttachmentCacheKey, type ExtractorParams } from './contract.js';
25
+ /**
26
+ * Configuração do extrator que participa da identidade da extração (ADR-004).
27
+ * Trocar qualquer campo invalida corretamente a entrada de cache do anexo.
28
+ */
29
+ export interface ExtractorConfig {
30
+ /** Versão do extrator (mapeada para `extractorVersion` na chave). */
31
+ version: string;
32
+ /** Modelo usado (ex.: `whisper-large-v3`). */
33
+ model: string;
34
+ /** Parâmetros escalares do extrator — ver {@link ExtractorParams}. */
35
+ params: ExtractorParams;
36
+ }
37
+ /** Entrada de {@link buildAttachmentKey}. */
38
+ export interface BuildAttachmentKeyInput {
39
+ /** URL base da instância Redmine — vira `instanceHash` na chave. */
40
+ instanceUrl: string;
41
+ /** Anexo do contrato; `digest` pode faltar em Redmine < 4.x. */
42
+ attachment: Attachment;
43
+ /** Configuração do extrator — ver {@link ExtractorConfig}. */
44
+ extractor: ExtractorConfig;
45
+ }
46
+ /**
47
+ * Deriva um digest DETERMINÍSTICO de um anexo sem `digest` (fallback Redmine
48
+ * < 4.x): SHA-256 hex de `(id, filesize, created_on)` separados por NUL.
49
+ *
50
+ * O resultado é hex puro (64 chars), estável entre execuções e distinto sempre
51
+ * que qualquer um dos três campos muda — servindo como identidade de conteúdo
52
+ * "boa o suficiente" quando o hash real do anexo não está disponível.
53
+ *
54
+ * @param attachment - Anexo do contrato.
55
+ * @returns Digest hex (SHA-256, 64 chars).
56
+ * @example
57
+ * deriveAttachmentDigest({ id: 7, filesize: 12345, created_on: '2026-07-20T00:00:00Z', ... });
58
+ */
59
+ export declare function deriveAttachmentDigest(attachment: Attachment): string;
60
+ /**
61
+ * Monta a {@link AttachmentCacheKey} de um anexo para uma configuração de
62
+ * extrator (ADR-004). Usa `attachment.digest` quando presente; caso ausente
63
+ * (`undefined` ou `""`, típico de Redmine < 4.x), cai no fallback determinístico
64
+ * de {@link deriveAttachmentDigest}.
65
+ *
66
+ * @param input - Ver {@link BuildAttachmentKeyInput}.
67
+ * @returns Chave attachment-level pronta para o {@link CacheStore}.
68
+ * @throws {TypeError} Se `instanceUrl` não for uma URL válida (via {@link instanceHash}).
69
+ * @example
70
+ * const key = buildAttachmentKey({
71
+ * instanceUrl: 'https://redmine.example',
72
+ * attachment,
73
+ * extractor: { version: '1.0.0', model: 'whisper-large-v3', params: { language: 'pt' } },
74
+ * });
75
+ */
76
+ export declare function buildAttachmentKey({ instanceUrl, attachment, extractor, }: BuildAttachmentKeyInput): AttachmentCacheKey;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Builder de alto nível da chave attachment-level (M3-03, ADR-004).
3
+ *
4
+ * Monta a {@link AttachmentCacheKey} correta a partir do modelo `Attachment` do
5
+ * contrato ({@link Attachment}) mais a configuração do extrator
6
+ * ({@link ExtractorConfig}), escondendo das camadas superiores dois detalhes:
7
+ *
8
+ * 1. o cálculo do `instance_hash` a partir da URL da instância
9
+ * ({@link instanceHash}); e
10
+ * 2. a origem do `digest` — usa `attachment.digest` quando presente e, como
11
+ * FALLBACK para Redmine < 4.x (que não expõe `digest`), deriva um digest
12
+ * DETERMINÍSTICO de `(id, filesize, created_on)`.
13
+ *
14
+ * A chave attachment-level NÃO depende de `updated_on` da issue: o builder sequer
15
+ * o recebe. Assim, editar a issue (novo `updated_on`, comentários) invalida só a
16
+ * camada de issue e reaproveita 100% das extrações caras dos anexos imutáveis.
17
+ *
18
+ * Sanitização hex "por construção": o digest de fallback é o SHA-256 hex do
19
+ * material derivado — 64 chars `[0-9a-f]`. Ele satisfaz o padrão hex que o
20
+ * {@link DiskCacheStore} usa ao montar `<id>-<digest8>/`, atravessando o caminho
21
+ * hex direto (sem o re-hash de escape reservado a valores não-hex/maliciosos).
22
+ */
23
+ import { createHash } from 'node:crypto';
24
+ import { instanceHash } from './contract.js';
25
+ /**
26
+ * Separador NUL entre os campos do material de derivação do digest de fallback.
27
+ * ` ` (NUL) nunca ocorre em inteiros nem em timestamps ISO, evitando que a
28
+ * concatenação de campos vizinhos colida (ex.: `(1, 23)` vs `(12, 3)`).
29
+ */
30
+ const FALLBACK_FIELD_SEPARATOR = '\0';
31
+ /**
32
+ * Deriva um digest DETERMINÍSTICO de um anexo sem `digest` (fallback Redmine
33
+ * < 4.x): SHA-256 hex de `(id, filesize, created_on)` separados por NUL.
34
+ *
35
+ * O resultado é hex puro (64 chars), estável entre execuções e distinto sempre
36
+ * que qualquer um dos três campos muda — servindo como identidade de conteúdo
37
+ * "boa o suficiente" quando o hash real do anexo não está disponível.
38
+ *
39
+ * @param attachment - Anexo do contrato.
40
+ * @returns Digest hex (SHA-256, 64 chars).
41
+ * @example
42
+ * deriveAttachmentDigest({ id: 7, filesize: 12345, created_on: '2026-07-20T00:00:00Z', ... });
43
+ */
44
+ export function deriveAttachmentDigest(attachment) {
45
+ const material = [attachment.id, attachment.filesize, attachment.created_on].join(FALLBACK_FIELD_SEPARATOR);
46
+ return createHash('sha256').update(material).digest('hex');
47
+ }
48
+ /**
49
+ * Monta a {@link AttachmentCacheKey} de um anexo para uma configuração de
50
+ * extrator (ADR-004). Usa `attachment.digest` quando presente; caso ausente
51
+ * (`undefined` ou `""`, típico de Redmine < 4.x), cai no fallback determinístico
52
+ * de {@link deriveAttachmentDigest}.
53
+ *
54
+ * @param input - Ver {@link BuildAttachmentKeyInput}.
55
+ * @returns Chave attachment-level pronta para o {@link CacheStore}.
56
+ * @throws {TypeError} Se `instanceUrl` não for uma URL válida (via {@link instanceHash}).
57
+ * @example
58
+ * const key = buildAttachmentKey({
59
+ * instanceUrl: 'https://redmine.example',
60
+ * attachment,
61
+ * extractor: { version: '1.0.0', model: 'whisper-large-v3', params: { language: 'pt' } },
62
+ * });
63
+ */
64
+ export function buildAttachmentKey({ instanceUrl, attachment, extractor, }) {
65
+ // Presente = definido e não-vazio; qualquer outra forma cai no fallback.
66
+ const digest = attachment.digest !== undefined && attachment.digest !== ''
67
+ ? attachment.digest
68
+ : deriveAttachmentDigest(attachment);
69
+ return {
70
+ kind: 'attachment',
71
+ instanceHash: instanceHash(instanceUrl),
72
+ attachmentId: attachment.id,
73
+ digest,
74
+ extractorVersion: extractor.version,
75
+ model: extractor.model,
76
+ params: extractor.params,
77
+ };
78
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Implementação de REFERÊNCIA em memória do {@link CacheStore} (M3-01, ADR-004).
3
+ *
4
+ * Serve a dois propósitos: (1) fixar o comportamento esperado que a suíte de
5
+ * contrato valida e (2) ser um backend real usável em testes de camadas
6
+ * superiores. A implementação em disco (issues seguintes) satisfará o mesmo
7
+ * contrato e passará na mesma suíte.
8
+ *
9
+ * O lock por chave é promise-based: cada chave mantém a cauda de uma fila
10
+ * encadeada de promises; um novo pretendente espera a cauda atual antes de
11
+ * executar sua seção crítica. Um teto de "stale lock" evita deadlock permanente
12
+ * caso um detentor nunca libere.
13
+ */
14
+ import { type CacheKey, type CacheStore, type CacheStoreOptions } from './contract.js';
15
+ /**
16
+ * {@link CacheStore} em memória (referência do contrato ADR-004). Não persiste
17
+ * entre processos; o isolamento por instância se dá pela própria chave.
18
+ *
19
+ * @typeParam V - Tipo do valor cacheado.
20
+ */
21
+ export declare class InMemoryCacheStore<V = unknown> implements CacheStore<V> {
22
+ private readonly entries;
23
+ /** Cauda da fila de locks por chave serializada. */
24
+ private readonly lockTails;
25
+ private readonly logger;
26
+ private readonly onGc;
27
+ private readonly staleLockTtlMs;
28
+ /** @param options - Ver {@link CacheStoreOptions}. */
29
+ constructor(options?: CacheStoreOptions);
30
+ /** @inheritdoc */
31
+ get(key: CacheKey): Promise<V | undefined>;
32
+ /** @inheritdoc */
33
+ put(key: CacheKey, value: V): Promise<void>;
34
+ /** @inheritdoc */
35
+ invalidate(key: CacheKey): Promise<void>;
36
+ /** @inheritdoc */
37
+ lock<T>(key: CacheKey, critical: () => Promise<T>): Promise<T>;
38
+ /** @inheritdoc */
39
+ gc(): Promise<void>;
40
+ /**
41
+ * Espera o detentor anterior liberar, com teto de stale lock: se o detentor
42
+ * exceder `staleLockTtlMs`, assume-se que ele morreu e a aquisição prossegue
43
+ * (com aviso). `staleLockTtlMs <= 0` desabilita o teto (espera indefinida).
44
+ */
45
+ private awaitHolder;
46
+ /** Aciona o hook de GC, se configurado, com a origem informada. */
47
+ private runGc;
48
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Implementação de REFERÊNCIA em memória do {@link CacheStore} (M3-01, ADR-004).
3
+ *
4
+ * Serve a dois propósitos: (1) fixar o comportamento esperado que a suíte de
5
+ * contrato valida e (2) ser um backend real usável em testes de camadas
6
+ * superiores. A implementação em disco (issues seguintes) satisfará o mesmo
7
+ * contrato e passará na mesma suíte.
8
+ *
9
+ * O lock por chave é promise-based: cada chave mantém a cauda de uma fila
10
+ * encadeada de promises; um novo pretendente espera a cauda atual antes de
11
+ * executar sua seção crítica. Um teto de "stale lock" evita deadlock permanente
12
+ * caso um detentor nunca libere.
13
+ */
14
+ import { DEFAULT_STALE_LOCK_TTL_MS, serializeCacheKey, } from './contract.js';
15
+ /** Logger no-op: usado quando nenhum logger é injetado. Nunca `console.*`. */
16
+ const noopLogger = { warn: () => undefined };
17
+ /**
18
+ * {@link CacheStore} em memória (referência do contrato ADR-004). Não persiste
19
+ * entre processos; o isolamento por instância se dá pela própria chave.
20
+ *
21
+ * @typeParam V - Tipo do valor cacheado.
22
+ */
23
+ export class InMemoryCacheStore {
24
+ entries = new Map();
25
+ /** Cauda da fila de locks por chave serializada. */
26
+ lockTails = new Map();
27
+ logger;
28
+ onGc;
29
+ staleLockTtlMs;
30
+ /** @param options - Ver {@link CacheStoreOptions}. */
31
+ constructor(options = {}) {
32
+ this.logger = options.logger ?? noopLogger;
33
+ this.onGc = options.onGc;
34
+ this.staleLockTtlMs = options.staleLockTtlMs ?? DEFAULT_STALE_LOCK_TTL_MS;
35
+ }
36
+ /** @inheritdoc */
37
+ async get(key) {
38
+ return this.entries.get(serializeCacheKey(key));
39
+ }
40
+ /** @inheritdoc */
41
+ async put(key, value) {
42
+ this.entries.set(serializeCacheKey(key), value);
43
+ await this.runGc('put');
44
+ }
45
+ /** @inheritdoc */
46
+ async invalidate(key) {
47
+ this.entries.delete(serializeCacheKey(key));
48
+ }
49
+ /** @inheritdoc */
50
+ async lock(key, critical) {
51
+ const serial = serializeCacheKey(key);
52
+ const previous = this.lockTails.get(serial);
53
+ // Encadeia esta aquisição na cauda: o próximo pretendente esperará `current`.
54
+ let release;
55
+ const current = new Promise((resolve) => {
56
+ release = resolve;
57
+ });
58
+ this.lockTails.set(serial, current);
59
+ if (previous !== undefined) {
60
+ await this.awaitHolder(previous, serial);
61
+ }
62
+ try {
63
+ return await critical();
64
+ }
65
+ finally {
66
+ release();
67
+ // Só limpa o mapa se ninguém entrou na fila depois de nós (evita vazar
68
+ // chaves) — caso contrário a cauda ainda é necessária para o próximo.
69
+ if (this.lockTails.get(serial) === current) {
70
+ this.lockTails.delete(serial);
71
+ }
72
+ }
73
+ }
74
+ /** @inheritdoc */
75
+ async gc() {
76
+ await this.runGc('manual');
77
+ }
78
+ /**
79
+ * Espera o detentor anterior liberar, com teto de stale lock: se o detentor
80
+ * exceder `staleLockTtlMs`, assume-se que ele morreu e a aquisição prossegue
81
+ * (com aviso). `staleLockTtlMs <= 0` desabilita o teto (espera indefinida).
82
+ */
83
+ async awaitHolder(previous, serial) {
84
+ if (this.staleLockTtlMs <= 0) {
85
+ await previous;
86
+ return;
87
+ }
88
+ let timer;
89
+ const staleTimeout = new Promise((resolve) => {
90
+ timer = setTimeout(() => resolve('stale'), this.staleLockTtlMs);
91
+ });
92
+ const outcome = await Promise.race([
93
+ previous.then(() => 'released'),
94
+ staleTimeout,
95
+ ]);
96
+ if (timer !== undefined) {
97
+ clearTimeout(timer);
98
+ }
99
+ if (outcome === 'stale') {
100
+ this.logger.warn(`Lock estagnado para a chave ${serial} após ${this.staleLockTtlMs}ms; prosseguindo.`);
101
+ }
102
+ }
103
+ /** Aciona o hook de GC, se configurado, com a origem informada. */
104
+ async runGc(reason) {
105
+ if (this.onGc === undefined) {
106
+ return;
107
+ }
108
+ await this.onGc({ reason, entryCount: this.entries.size });
109
+ }
110
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Leitura CACHE-FIRST não-bloqueante das extrações de anexos (M4-11, ADR-004).
3
+ *
4
+ * O núcleo do modo cache-first do MCP (#70): dado uma issue JÁ normalizada, para
5
+ * cada anexo com extrator registrado LÊ o cache attachment-level e devolve
6
+ * IMEDIATAMENTE o que já está pronto (`done`/`text`). Quando a extração de um
7
+ * anexo pesado (vídeo/áudio) ainda NÃO está no cache, NÃO espera a extração
8
+ * terminar: marca o anexo como {@link ExtractionStatus} `processing` com
9
+ * metadados e, opcionalmente, dispara o job de extração em SEGUNDO PLANO via a
10
+ * fila (`runQueue`, #67) — fire-and-forget. Assim nenhuma chamada MCP bloqueia
11
+ * aguardando mídia (resposta imediata; a continuação que completa na 2ª chamada
12
+ * é a #71).
13
+ *
14
+ * A leitura de cache (índice/`extraction.json`) é barata (ms); só a COMPUTAÇÃO da
15
+ * extração é cara — e é justamente ela que este módulo evita no caminho síncrono.
16
+ * A chave attachment-level é montada EXATAMENTE como no caminho bloqueante
17
+ * ({@link extractIssueAttachments}) — mesmos `probableMime`/`toExtractorConfig` —
18
+ * garantindo cache-hit coerente com o que o job de background grava.
19
+ *
20
+ * Fronteira ADR-005: módulo de core (pipeline de extração). Não importa de
21
+ * `src/surfaces/**` e nunca usa `console.*` — avisos vão pelo {@link Logger}.
22
+ */
23
+ import { type AttachmentCacheKey, type CacheStore } from './cache/index.js';
24
+ import type { Logger } from './client/index.js';
25
+ import type { Attachment, ExtractionResult, Issue } from './contract.js';
26
+ import { type Extractor, type ExtractorRegistry } from './extract/index.js';
27
+ /**
28
+ * Constrói um {@link ExtractionResult} `processing` — o anexo existe e TEM
29
+ * extrator, mas a extração (cara) ainda não está no cache. Sinaliza à superfície
30
+ * que o texto virá em uma consulta futura (#71), sem bloquear a atual.
31
+ *
32
+ * @returns Resultado `processing` com motivo/dica legíveis (sem texto).
33
+ */
34
+ export declare function processingResult(): ExtractionResult;
35
+ /** Alvo de uma extração pendente descoberta no caminho cache-first (cache-miss). */
36
+ export interface BackgroundExtractionTarget {
37
+ /** Anexo cujo texto ainda não está no cache. */
38
+ readonly attachment: Attachment;
39
+ /** Extrator resolvido pelo MIME provável do anexo. */
40
+ readonly extractor: Extractor;
41
+ /** Chave attachment-level já montada (idêntica à do caminho bloqueante). */
42
+ readonly key: AttachmentCacheKey;
43
+ }
44
+ /**
45
+ * Dispatcher de extração em SEGUNDO PLANO (#70/#71). Recebe um anexo com
46
+ * cache-miss e DEVE retornar imediatamente (fire-and-forget): jamais aguarda a
47
+ * extração. Injetável para testes (um fake determinístico que não espera de fato).
48
+ */
49
+ export type BackgroundExtractor = (target: BackgroundExtractionTarget) => void;
50
+ /** Opções de {@link extractIssueAttachmentsCacheFirst}. */
51
+ export interface CacheFirstExtractionOptions {
52
+ /** URL base da instância — compõe o `instance_hash` da chave (ADR-004). */
53
+ instanceUrl: string;
54
+ /** Registry consultado para achar o extrator do MIME provável de cada anexo. */
55
+ registry: ExtractorRegistry;
56
+ /** Store de cache attachment-level (memória ou disco) — apenas LIDO aqui. */
57
+ store: CacheStore<ExtractionResult>;
58
+ /**
59
+ * Dispatcher opcional do job em background (#71). Chamado UMA vez por anexo com
60
+ * cache-miss. Ausente = apenas sinaliza `processing` (o #70 não exige disparar).
61
+ */
62
+ background?: BackgroundExtractor;
63
+ /** Logger para avisos (ex.: falha ao ler o cache); default no-op. Nunca `console.*`. */
64
+ logger?: Logger;
65
+ }
66
+ /**
67
+ * Lê o cache das extrações de TODOS os anexos com extrator registrado de uma
68
+ * issue, SEM computar nenhuma extração (não-bloqueante).
69
+ *
70
+ * Para cada anexo:
71
+ * - sem extrator para o MIME provável → nem entra no mapa (igual ao bloqueante);
72
+ * - cache-hit → o {@link ExtractionResult} pronto (texto imediato);
73
+ * - cache-miss → `processing` + (opcional) dispara o job de background.
74
+ *
75
+ * @param issue - Issue normalizada cujos anexos serão consultados no cache.
76
+ * @param options - Ver {@link CacheFirstExtractionOptions}.
77
+ * @returns `Map<attachmentId, ExtractionResult>` só com anexos que têm extrator.
78
+ * @example
79
+ * const map = await extractIssueAttachmentsCacheFirst(issue, {
80
+ * instanceUrl, registry, store, background,
81
+ * });
82
+ */
83
+ export declare function extractIssueAttachmentsCacheFirst(issue: Issue, options: CacheFirstExtractionOptions): Promise<Map<number, ExtractionResult>>;
84
+ /**
85
+ * Computa a extração de UM anexo em background. Recebe o alvo do cache-miss e o
86
+ * `AbortSignal` da fila (#69); deve resolver com o {@link ExtractionResult}
87
+ * gravado no cache (ou reportar falha por rejeição, isolada pela fila).
88
+ */
89
+ export type BackgroundCompute = (target: BackgroundExtractionTarget, signal: AbortSignal | undefined) => Promise<ExtractionResult>;
90
+ /** Opções de {@link makeQueueBackgroundExtractor}. */
91
+ export interface QueueBackgroundOptions {
92
+ /** Logger para avisos de jobs que falharam/abortaram (ADR-005; sem `console.*`). */
93
+ logger?: Logger;
94
+ /** Sinal de cancelamento propagado à fila e aos jobs (#69). */
95
+ signal?: AbortSignal;
96
+ /**
97
+ * Store attachment-level onde o resultado do job de background é PERSISTIDO ao
98
+ * concluir (#71), sob a MESMA chave que a leitura cache-first busca. Fecha o
99
+ * loop do #70: a 2ª chamada (pós-conclusão) acerta o cache e devolve o resultado
100
+ * completo — e uma FALHA vira `failed` PERSISTIDO, nunca `processing` eterno.
101
+ * Ausente = comportamento #70 (fire-and-forget sem persistência de fechamento).
102
+ */
103
+ store?: CacheStore<ExtractionResult>;
104
+ }
105
+ /**
106
+ * Cria um {@link BackgroundExtractor} respaldado pela fila de jobs (#67): cada
107
+ * anexo com cache-miss vira UM {@link QueueJob} rodado por `runQueue`, cujas
108
+ * transições são consumidas de forma DESACOPLADA (fire-and-forget). O dispatcher
109
+ * retorna imediatamente — o trabalho caro acontece na fila, fora do caminho da
110
+ * resposta MCP. O `AbortSignal` da fila é propagado ao `compute` via
111
+ * {@link JobContext.signal} (#69), pronto para o kill de subprocesso do #68.
112
+ *
113
+ * DEDUP DE IN-FLIGHT (#71): o dispatcher mantém um registro dos jobs em andamento
114
+ * POR CHAVE attachment-level. Uma 2ª chamada para um anexo cujo job ainda processa
115
+ * NÃO dispara um job duplicado — reusa o que já corre. O registro é limpo quando o
116
+ * job conclui (via `finally`), liberando futuras re-extrações (ex.: cache invalidado).
117
+ *
118
+ * @param compute - Executa a extração real do anexo — ver {@link BackgroundCompute}.
119
+ * @param options - Logger/signal/store opcionais — ver {@link QueueBackgroundOptions}.
120
+ * @returns Um dispatcher fire-and-forget para injetar em {@link CacheFirstExtractionOptions.background}.
121
+ * @example
122
+ * const background = makeQueueBackgroundExtractor(async (target) => {
123
+ * const map = await extractIssueAttachments(http, single, { store, registry, instanceUrl });
124
+ * return map.get(target.attachment.id) ?? processingResult();
125
+ * }, { store });
126
+ */
127
+ export declare function makeQueueBackgroundExtractor(compute: BackgroundCompute, options?: QueueBackgroundOptions): BackgroundExtractor;