wendkeep 0.66.0 → 0.66.3
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.
- package/CHANGELOG.md +36 -0
- package/README.en.md +13 -2
- package/README.md +13 -2
- package/docs/en/commands/memory-migration.md +2 -1
- package/docs/en/commands/memory.md +28 -3
- package/docs/pt-BR/commands/memory-migration.md +2 -1
- package/docs/pt-BR/commands/memory.md +27 -3
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -1
- package/packages/vault/src/memory-schema.mjs +25 -0
- package/packages/vault/src/memory-store.mjs +93 -6
- package/src/memory.mjs +335 -22
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.66.3] — 2026-07-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **O replay reavalia candidates transitórios contra a fonte moderna final.** Um Stop da mesma
|
|
12
|
+
sessão/activation/epoch e turno maior agora avança depois que a correção causal já presente se
|
|
13
|
+
torna ativa; turno menor fica superseded e divergências reais continuam para curadoria.
|
|
14
|
+
- **`memory repair` migra o checkpoint antigo somente com prova e CAS.** O repair compara a
|
|
15
|
+
semântica anterior e a atual, faz backup, atualiza attempt e espelho e registra auditoria sem
|
|
16
|
+
reordenar, reescrever ou acrescentar evento ao ledger. Prova incompleta continua bloqueada. A
|
|
17
|
+
0.66.2 não deve ser publicada no npm; publique e instale a 0.66.3.
|
|
18
|
+
|
|
19
|
+
## [0.66.2] — 2026-07-29
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Promoção preserva valor e identidade causal do evento escolhido.** `memory promote` não
|
|
24
|
+
converte mais objetos/arrays JSON em texto e mantém sessão, activation/epoch, turno de origem e
|
|
25
|
+
sequência; o próximo `SessionStop` da mesma cadeia avança o handoff sem recriar candidate.
|
|
26
|
+
- **A recuperação 0.66.1 cobre inversão física/temporal sem ampliar autoridade.** Quando uma
|
|
27
|
+
promoção legada projetada fica fora do candidate por `observed_at`, `memory promote` só a inclui
|
|
28
|
+
em `supersedes` sob prova de ancestralidade e mesma linhagem; fonte moderna ou alheia falha antes
|
|
29
|
+
de anexar. O E2E percorre o dispatcher público e prova Stop posterior e duplicado idempotentes.
|
|
30
|
+
|
|
31
|
+
## [0.66.1] — 2026-07-29
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **A curadoria de candidates agora é durável e idempotente.** `memory promote` e
|
|
36
|
+
`memory reject` registram decisões causais no ledger; repair/replay não recriam o conflito
|
|
37
|
+
resolvido, retries não duplicam eventos, candidates sobrepostos permanecem isolados e a
|
|
38
|
+
promoção recupera o checkpoint divergente do attempt correspondente sem tocar attempt novo.
|
|
39
|
+
- **Promoção de conflito exige escolha explícita.** `memory promote <candidate> --event
|
|
40
|
+
<event-id>` publica exatamente o evento escolhido, rejeita IDs externos ao candidate e não
|
|
41
|
+
permite que `blocked_by_core` sobrescreva a autoridade canônica de CORE.
|
|
42
|
+
|
|
7
43
|
## [0.66.0] — 2026-07-29
|
|
8
44
|
|
|
9
45
|
### Added
|
package/README.en.md
CHANGED
|
@@ -256,7 +256,7 @@ Hot memory now separates human authorship, operational state, and evidence:
|
|
|
256
256
|
- **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
|
|
257
257
|
- **`SHARED_MEMORY.md` is generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
258
258
|
- **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
|
|
259
|
-
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event.
|
|
259
|
+
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
|
|
260
260
|
|
|
261
261
|
Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
|
|
262
262
|
|
|
@@ -294,7 +294,18 @@ backup/audit; divergent mirrors fail closed. A demonstrably superseded
|
|
|
294
294
|
ambiguity uses `memory reconcile <session> --by-session <successor>
|
|
295
295
|
--reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
|
|
296
296
|
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
|
|
297
|
-
explicit curation
|
|
297
|
+
explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
|
|
298
|
+
candidate, while `memory reject <id>` keeps the current value. The decision is idempotent, and a
|
|
299
|
+
new promotion accepts a later Stop from the same session/activation without recreating a conflict.
|
|
300
|
+
A promotion written by 0.66.1 remains historical: if the next Stop forms a new candidate, update
|
|
301
|
+
to 0.66.3 and run `memory repair`. During replay, a transient candidate is re-evaluated against the
|
|
302
|
+
final modern source: the same session/activation/epoch and a higher turn advances; a lower turn is
|
|
303
|
+
superseded. Repair migrates the checkpoint and mirror only when it proves the exact previous
|
|
304
|
+
replay, attempt identity, and absence of a real conflict; it creates a backup and audit without
|
|
305
|
+
appending or rewriting events. Ambiguity stays queued for explicit curation. Do not publish or
|
|
306
|
+
install 0.66.2; use 0.66.3. Decisions survive repair/replay;
|
|
307
|
+
`blocked_by_core` cannot override CORE. Doctor only
|
|
308
|
+
diagnoses. See [memory and curation](docs/en/commands/memory.md).
|
|
298
309
|
|
|
299
310
|
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
300
311
|
|
package/README.md
CHANGED
|
@@ -256,7 +256,7 @@ Hot memory now separates human authorship, operational state, and evidence:
|
|
|
256
256
|
- **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
|
|
257
257
|
- **`SHARED_MEMORY.md` is generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
258
258
|
- **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
|
|
259
|
-
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event.
|
|
259
|
+
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
|
|
260
260
|
|
|
261
261
|
Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
|
|
262
262
|
|
|
@@ -294,7 +294,18 @@ backup/audit; divergent mirrors fail closed. A demonstrably superseded
|
|
|
294
294
|
ambiguity uses `memory reconcile <session> --by-session <successor>
|
|
295
295
|
--reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
|
|
296
296
|
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
|
|
297
|
-
explicit curation
|
|
297
|
+
explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
|
|
298
|
+
candidate, while `memory reject <id>` keeps the current value. The decision is idempotent, and a
|
|
299
|
+
new promotion accepts a later Stop from the same session/activation without recreating a conflict.
|
|
300
|
+
A promotion written by 0.66.1 remains historical: if the next Stop forms a new candidate, update
|
|
301
|
+
to 0.66.3 and run `memory repair`. During replay, a transient candidate is re-evaluated against the
|
|
302
|
+
final modern source: the same session/activation/epoch and a higher turn advances; a lower turn is
|
|
303
|
+
superseded. Repair migrates the checkpoint and mirror only when it proves the exact previous
|
|
304
|
+
replay, attempt identity, and absence of a real conflict; it creates a backup and audit without
|
|
305
|
+
appending or rewriting events. Ambiguity stays queued for explicit curation. Do not publish or
|
|
306
|
+
install 0.66.2; use 0.66.3. Decisions survive repair/replay;
|
|
307
|
+
`blocked_by_core` cannot override CORE. Doctor only
|
|
308
|
+
diagnoses. See [memory and curation](docs/en/commands/memory.md).
|
|
298
309
|
|
|
299
310
|
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
300
311
|
|
|
@@ -64,7 +64,8 @@ to revision 1. Replaying that prompt or Stop does not duplicate the event/revisi
|
|
|
64
64
|
- Partial/corrupt v2 bundle: use status and repair; migration is not a corruption tool.
|
|
65
65
|
- First post-migration Stop is `ambiguous`: verify that its `turn_id` belongs to the transcript and
|
|
66
66
|
that `UserPromptSubmit` opened/advanced the recovery activation.
|
|
67
|
-
- Many candidates: curate gradually with `memory promote`/`memory reject
|
|
67
|
+
- Many candidates: curate gradually with `memory promote`/`memory reject`; conflicts require
|
|
68
|
+
`memory promote <candidate> --event <event-id>` to choose the winner explicitly.
|
|
68
69
|
- Legacy warning remains after apply: verify the selected vault and project binding.
|
|
69
70
|
|
|
70
71
|
## Next steps
|
|
@@ -26,7 +26,7 @@ Pass the vault explicitly in automation. Preserve backups and evidence before re
|
|
|
26
26
|
npx wendkeep memory status [--gate] --vault <vault>
|
|
27
27
|
npx wendkeep memory repair --vault <vault>
|
|
28
28
|
npx wendkeep memory reconcile <ambiguous-session> --by-session <successor-session> --reason <reason> [--apply] --vault <vault>
|
|
29
|
-
npx wendkeep memory promote <candidate> --vault <vault>
|
|
29
|
+
npx wendkeep memory promote <candidate> [--event <event-id>] --vault <vault>
|
|
30
30
|
npx wendkeep memory reject <candidate> --vault <vault>
|
|
31
31
|
npx wendkeep validate-memory [CORE-path]
|
|
32
32
|
npx wendkeep validate-memory --vault <v2-vault>
|
|
@@ -63,7 +63,22 @@ npx wendkeep validate-memory --vault <v2-vault>
|
|
|
63
63
|
Junctions, symlinks, reparse points, or hardlinks fail closed without touching external bytes.
|
|
64
64
|
Locks publish owner and lease atomically, never reap a live PID by age alone, and release only
|
|
65
65
|
the lease they acquired.
|
|
66
|
-
- `promote`/`reject` append auditable
|
|
66
|
+
- `promote`/`reject` append an auditable, idempotent decision to the ledger. Replay and repair
|
|
67
|
+
preserve that decision and do not recreate the resolved candidate. For a `conflict` candidate,
|
|
68
|
+
`promote` requires an `--event <event-id>` that belongs to the candidate; date or random ID
|
|
69
|
+
never picks an implicit winner. `reject` preserves the current operational value. A
|
|
70
|
+
`blocked_by_core` candidate can only be rejected: promotion first requires canonical CORE
|
|
71
|
+
curation. If the selected event still belongs to the matching latest `projected` attempt,
|
|
72
|
+
promotion also refreshes its checkpoint and mirror causally; JSON reports
|
|
73
|
+
`checkpointRefreshed`, and a newer concurrent attempt remains untouched. The decision keeps
|
|
74
|
+
the already validated JSON value without string coercion and copies the selected event's
|
|
75
|
+
`canonical_session_id`, activation/epoch, `source_turn_id`, and `turn_sequence`. A later Stop
|
|
76
|
+
from the same session/activation therefore advances the value instead of opening another candidate.
|
|
77
|
+
During replay, a transient candidate is re-evaluated against the final modern source. The same
|
|
78
|
+
session/activation/epoch and a higher turn applies the Stop; a lower turn is superseded. A
|
|
79
|
+
different, incomplete, or ambiguous identity keeps the candidate queued for curation. `memory
|
|
80
|
+
repair` compares the old and current replay and migrates checkpoint+mirror only with exact
|
|
81
|
+
identity, backup, audit, and CAS; it does not reorder, rewrite, or append a ledger event.
|
|
67
82
|
- `validate-memory <CORE.md>` checks the 25-line cap, required sections, and secrets.
|
|
68
83
|
- `validate-memory --vault` requires a complete v2 bundle and is not the legacy-vault gate.
|
|
69
84
|
|
|
@@ -74,7 +89,8 @@ npx wendkeep memory status --gate --vault .MyApp-vault
|
|
|
74
89
|
npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --vault .MyApp-vault
|
|
75
90
|
npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --apply --vault .MyApp-vault
|
|
76
91
|
npx wendkeep validate-memory .MyApp-vault/.brain/CORE.md
|
|
77
|
-
npx wendkeep memory promote candidate-123 --vault .MyApp-vault
|
|
92
|
+
npx wendkeep memory promote candidate-123 --event mem-selected --vault .MyApp-vault
|
|
93
|
+
npx wendkeep memory reject candidate-456 --vault .MyApp-vault
|
|
78
94
|
```
|
|
79
95
|
|
|
80
96
|
## Expected result
|
|
@@ -97,6 +113,15 @@ of a global projection that has already advanced with concurrent events.
|
|
|
97
113
|
`memory reconcile` dry run before authorizing `--apply`; the command fails when the ambiguous
|
|
98
114
|
attempt already contains event IDs.
|
|
99
115
|
- Ordinary pending candidate: recoverable warning, requiring human choice when appropriate.
|
|
116
|
+
- `promote` reports that `--event` is required: inspect the candidate `event_ids`, compare their
|
|
117
|
+
provenance/value, and name the winner explicitly. An ID outside the candidate fails without
|
|
118
|
+
mutating the ledger or projections.
|
|
119
|
+
- A promotion made by 0.66.1 followed by a transient candidate: update to 0.66.3, preserve a backup,
|
|
120
|
+
and run `memory repair`. Do not publish or install 0.66.2. Repair migrates the checkpoint only
|
|
121
|
+
when the old replay, attempt identity, and new event match exactly; a real conflict stays blocked
|
|
122
|
+
for a human `promote`/`reject` choice.
|
|
123
|
+
- `promote` says that the candidate no longer matches the causal projection: no event was appended.
|
|
124
|
+
Run `memory status`, inspect the current candidate again, and do not force a different lineage.
|
|
100
125
|
- Missing `event_cursor` or mismatched v2 hash: preserve the bundle and assess `memory repair`.
|
|
101
126
|
- `validate-memory --vault` fails on legacy: validate CORE only or migrate first.
|
|
102
127
|
|
|
@@ -64,7 +64,8 @@ revision 1. Repetir esse prompt ou Stop não duplica evento/revision.
|
|
|
64
64
|
- Bundle v2 parcial/corrompido: use status e repair; migração não é ferramenta de corrupção.
|
|
65
65
|
- Primeiro Stop pós-migração fica `ambiguous`: confirme que o `turn_id` pertence ao transcript e
|
|
66
66
|
que `UserPromptSubmit` abriu/avançou a activation de recuperação.
|
|
67
|
-
- Candidates numerosos: curate gradualmente com `memory promote`/`memory reject
|
|
67
|
+
- Candidates numerosos: curate gradualmente com `memory promote`/`memory reject`; conflitos
|
|
68
|
+
exigem `memory promote <candidate> --event <event-id>` para escolher o vencedor explicitamente.
|
|
68
69
|
- Warning legado após apply: confirme o vault efetivamente selecionado e o vínculo do projeto.
|
|
69
70
|
|
|
70
71
|
## Próximos passos
|
|
@@ -26,7 +26,7 @@ Informe o vault explicitamente em automações. Preserve backups e evidências a
|
|
|
26
26
|
npx wendkeep memory status [--gate] --vault <cofre>
|
|
27
27
|
npx wendkeep memory repair --vault <cofre>
|
|
28
28
|
npx wendkeep memory reconcile <sessão-ambígua> --by-session <sessão-sucessora> --reason <motivo> [--apply] --vault <cofre>
|
|
29
|
-
npx wendkeep memory promote <candidate> --vault <cofre>
|
|
29
|
+
npx wendkeep memory promote <candidate> [--event <event-id>] --vault <cofre>
|
|
30
30
|
npx wendkeep memory reject <candidate> --vault <cofre>
|
|
31
31
|
npx wendkeep validate-memory [caminho-do-CORE]
|
|
32
32
|
npx wendkeep validate-memory --vault <cofre-v2>
|
|
@@ -61,7 +61,21 @@ npx wendkeep validate-memory --vault <cofre-v2>
|
|
|
61
61
|
candidates, registry, notas, backups, temporários e sidecars antes de ler ou escrever. Junction,
|
|
62
62
|
symlink, reparse point ou hardlink falham fechados sem tocar bytes externos. Locks publicam owner
|
|
63
63
|
e lease atomicamente, não colhem PID vivo apenas por idade e só liberam a lease adquirida.
|
|
64
|
-
- `promote`/`reject` acrescentam decisão auditável
|
|
64
|
+
- `promote`/`reject` acrescentam uma decisão auditável e idempotente ao ledger. Replay e repair
|
|
65
|
+
preservam a decisão e não recriam o candidate resolvido. Para candidate `conflict`, `promote`
|
|
66
|
+
exige `--event <event-id>` pertencente ao candidate; não há vencedor implícito por data ou ID.
|
|
67
|
+
`reject` preserva o valor operacional atual. Candidate `blocked_by_core` só pode ser rejeitado:
|
|
68
|
+
promover exige antes alterar CORE pela curadoria canônica. Se o evento escolhido ainda pertence
|
|
69
|
+
ao último attempt `projected` correspondente, a promoção também atualiza causalmente checkpoint
|
|
70
|
+
e espelho; o JSON retorna `checkpointRefreshed`, e um attempt concorrente mais novo não é tocado.
|
|
71
|
+
A decisão conserva, sem coerção para string, o valor JSON já validado e copia do evento escolhido
|
|
72
|
+
`canonical_session_id`, activation/epoch, `source_turn_id` e `turn_sequence`. Por isso, um Stop
|
|
73
|
+
posterior da mesma sessão/activation avança o valor em vez de abrir outro candidate. Durante o
|
|
74
|
+
replay, um candidate transitório é reavaliado contra a fonte moderna final. Mesma
|
|
75
|
+
sessão/activation/epoch e turno maior aplica o Stop; turno menor fica superseded. Identidade
|
|
76
|
+
divergente, incompleta ou ambígua mantém o candidate para curadoria. `memory repair` compara o
|
|
77
|
+
replay anterior e o atual e só migra checkpoint+espelho com identidade exata, backup, audit e
|
|
78
|
+
CAS; ele não reordena, reescreve nem acrescenta evento ao ledger.
|
|
65
79
|
- `validate-memory <CORE.md>` valida cap de 25 linhas, seções e segredos.
|
|
66
80
|
- `validate-memory --vault` exige bundle v2 completo; não é o gate correto para vault legado.
|
|
67
81
|
|
|
@@ -72,7 +86,8 @@ npx wendkeep memory status --gate --vault .MeuApp-vault
|
|
|
72
86
|
npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --vault .MeuApp-vault
|
|
73
87
|
npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --apply --vault .MeuApp-vault
|
|
74
88
|
npx wendkeep validate-memory .MeuApp-vault/.brain/CORE.md
|
|
75
|
-
npx wendkeep memory promote candidate-123 --vault .MeuApp-vault
|
|
89
|
+
npx wendkeep memory promote candidate-123 --event mem-escolhido --vault .MeuApp-vault
|
|
90
|
+
npx wendkeep memory reject candidate-456 --vault .MeuApp-vault
|
|
76
91
|
```
|
|
77
92
|
|
|
78
93
|
## Resultado esperado
|
|
@@ -94,6 +109,15 @@ prefixo válido de uma projeção global que já avançou com eventos concorrent
|
|
|
94
109
|
ambiguidade for comprovadamente substituída por uma sessão sucessora, revise o dry-run de
|
|
95
110
|
`memory reconcile` antes de autorizar `--apply`; o comando falha se o attempt ambíguo tiver IDs.
|
|
96
111
|
- Candidate pendente comum: warning recuperável, exige decisão humana quando apropriado.
|
|
112
|
+
- `promote` informa que `--event` é obrigatório: leia os `event_ids` do candidate, compare a
|
|
113
|
+
proveniência/valor e indique explicitamente o vencedor. ID que não pertence ao candidate falha
|
|
114
|
+
sem mutar ledger ou projeções.
|
|
115
|
+
- Promoção feita pela 0.66.1 seguida de candidate transitório: atualize para a 0.66.3, preserve um
|
|
116
|
+
backup e rode `memory repair`. Não publique nem instale a 0.66.2. O repair só migra o checkpoint
|
|
117
|
+
quando o replay anterior, a identidade do attempt e o evento novo coincidem exatamente; conflito
|
|
118
|
+
real continua bloqueado para escolha humana com `promote`/`reject`.
|
|
119
|
+
- `promote` informa que o candidate não corresponde mais à projeção causal: nenhum evento foi
|
|
120
|
+
anexado. Rode `memory status`, releia o candidate atual e não force uma linhagem diferente.
|
|
97
121
|
- `event_cursor` ausente ou hash divergente em v2: preserve o bundle e avalie `memory repair`.
|
|
98
122
|
- `validate-memory --vault` falha no legado: valide apenas CORE ou migre primeiro.
|
|
99
123
|
|
package/hooks/brain-core.mjs
CHANGED
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
// .agent/hooks/brain-core.mjs
|
|
2
|
-
// Camada fria do brain: indexa o frontmatter das notas de sessão (0 token LLM).
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { basename, join } from 'node:path';
|
|
5
|
-
import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
|
|
6
|
-
import { getLocale } from './locale.mjs';
|
|
7
|
-
|
|
8
|
-
export function brainDir(vaultBase) {
|
|
9
|
-
return join(vaultBase, '.brain');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Frontmatter YAML simples: escalares `k: v` + listas `k:` seguido de ` - item`.
|
|
13
|
-
export function parseFrontmatter(content) {
|
|
14
|
-
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
15
|
-
if (!m) return {};
|
|
16
|
-
const data = {};
|
|
17
|
-
const lines = m[1].split('\n');
|
|
18
|
-
for (let i = 0; i < lines.length; i++) {
|
|
19
|
-
const kv = lines[i].match(/^([\w-]+):\s*(.*)$/);
|
|
20
|
-
if (!kv) continue;
|
|
21
|
-
const key = kv[1];
|
|
22
|
-
const val = kv[2];
|
|
23
|
-
if (val === '') {
|
|
24
|
-
const list = [];
|
|
25
|
-
while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
|
|
26
|
-
list.push(stripYamlQuotes(lines[++i].replace(/^\s+-\s+/, '').trim()));
|
|
27
|
-
}
|
|
28
|
-
data[key] = list.length ? list : '';
|
|
29
|
-
} else {
|
|
30
|
-
data[key] = stripYamlQuotes(val.trim());
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return data;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function walkMd(dir) {
|
|
37
|
-
const out = [];
|
|
38
|
-
let entries;
|
|
39
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
40
|
-
for (const e of entries) {
|
|
41
|
-
const fp = join(dir, e.name);
|
|
42
|
-
if (e.isDirectory()) out.push(...walkMd(fp));
|
|
43
|
-
else if (e.name.endsWith('.md')) out.push(fp);
|
|
44
|
-
}
|
|
45
|
-
return out;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const DERIVED_RE = /\[\[(0[456]-[^\]|]+?)(?:\|[^\]]*)?\]\]/g;
|
|
49
|
-
function derivedLinks(content) {
|
|
50
|
-
const dec = new Set(), bug = new Set(), lea = new Set();
|
|
51
|
-
let m;
|
|
52
|
-
while ((m = DERIVED_RE.exec(content))) {
|
|
53
|
-
const t = m[1];
|
|
54
|
-
if (t.startsWith('04-')) dec.add(t);
|
|
55
|
-
else if (t.startsWith('05-')) bug.add(t);
|
|
56
|
-
else if (t.startsWith('06-')) lea.add(t);
|
|
57
|
-
}
|
|
58
|
-
return { decisions: [...dec], bugs: [...bug], learnings: [...lea] };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
|
|
62
|
-
export function buildBrainIndex(vaultBase) {
|
|
63
|
-
const rows = [];
|
|
64
|
-
for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
|
|
65
|
-
let content;
|
|
66
|
-
try { content = readFileSync(fp, 'utf8'); } catch { continue; }
|
|
67
|
-
const fm = parseFrontmatter(content);
|
|
68
|
-
if (fm.type && fm.type !== 'session') continue;
|
|
69
|
-
const der = derivedLinks(content);
|
|
70
|
-
rows.push({
|
|
71
|
-
session_id: fm.session_id || '',
|
|
72
|
-
date: fm.date || '',
|
|
73
|
-
provider: fm.provider || '',
|
|
74
|
-
status: fm.status || '',
|
|
75
|
-
summary: fm.summary || '',
|
|
76
|
-
file: toVaultRelative(vaultBase, fp),
|
|
77
|
-
tags: Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []),
|
|
78
|
-
decisions: der.decisions,
|
|
79
|
-
bugs: der.bugs,
|
|
80
|
-
learnings: der.learnings,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
rows.sort((a, b) => (a.date + a.file).localeCompare(b.date + b.file));
|
|
84
|
-
ensureDir(brainDir(vaultBase));
|
|
85
|
-
const out = rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
|
|
86
|
-
writeFileSync(join(brainDir(vaultBase), 'index.jsonl'), out, 'utf8');
|
|
87
|
-
return rows;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Lê o índice gravado (linhas JSONL). Usado pelo recall e pelo digest.
|
|
91
|
-
export function loadIndex(vaultBase) {
|
|
92
|
-
try {
|
|
93
|
-
return readFileSync(join(brainDir(vaultBase), 'index.jsonl'), 'utf8')
|
|
94
|
-
.split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
95
|
-
} catch {
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const DIGEST_CAPS = { decisions: 5, sessions: 4, bugs: 2, learnings: 2 };
|
|
101
|
-
|
|
102
|
-
function adrNumber(path) {
|
|
103
|
-
const m = path.match(/ADR-(\d+)/);
|
|
104
|
-
return m ? Number(m[1]) : -1;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Destila index.jsonl em .brain/DIGEST.md (camada quente, determinístico, 0 token LLM).
|
|
108
|
-
// Cap por construção: 1 header + 13 itens (5/4/2/2) + 1 pointer = máx 15 linhas.
|
|
109
|
-
export function buildBrainDigest(vaultBase, rows = null) {
|
|
110
|
-
const data = rows ?? loadIndex(vaultBase);
|
|
111
|
-
const byDateDesc = [...data].sort((a, b) =>
|
|
112
|
-
String(b.date || '').localeCompare(String(a.date || '')) || String(b.file || '').localeCompare(String(a.file || '')));
|
|
113
|
-
|
|
114
|
-
const seen = new Set();
|
|
115
|
-
const pick = (kind, max) => {
|
|
116
|
-
const out = [];
|
|
117
|
-
for (const r of byDateDesc) {
|
|
118
|
-
for (const p of r[kind] || []) {
|
|
119
|
-
if (out.length >= max) return out;
|
|
120
|
-
if (!seen.has(p)) { seen.add(p); out.push(p); }
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// The digest is INJECTED into every session, so a dead wikilink is dead weight in the model's
|
|
127
|
-
// context. Keep only targets that resolve to a real note (by vault-relative path or basename)
|
|
128
|
-
// and drop placeholder paths (a truncated `…` from a summary line). `pick` collects extra so
|
|
129
|
-
// caps still fill after filtering.
|
|
130
|
-
const known = new Set();
|
|
131
|
-
for (const r of data) {
|
|
132
|
-
const rel = String(r.file || '').replace(/\.md$/i, '');
|
|
133
|
-
if (rel) { known.add(rel); known.add(basename(rel)); }
|
|
134
|
-
}
|
|
135
|
-
const resolves = (p) => {
|
|
136
|
-
const t = String(p || '').replace(/\.md$/i, '').trim();
|
|
137
|
-
if (!t || t.includes('...') || t.includes('…')) return false;
|
|
138
|
-
return known.has(t) || known.has(basename(t)) || existsSync(join(vaultBase, `${t}.md`));
|
|
139
|
-
};
|
|
140
|
-
const pickLive = (kind, max) => pick(kind, max * 4).filter(resolves).slice(0, max);
|
|
141
|
-
|
|
142
|
-
const decisions = pickLive('decisions', DIGEST_CAPS.decisions).sort((a, b) => adrNumber(b) - adrNumber(a));
|
|
143
|
-
const sessions = byDateDesc.slice(0, DIGEST_CAPS.sessions);
|
|
144
|
-
const bugs = pickLive('bugs', DIGEST_CAPS.bugs);
|
|
145
|
-
const learnings = pickLive('learnings', DIGEST_CAPS.learnings);
|
|
146
|
-
|
|
147
|
-
const lines = ['<!-- AUTO-GERADO por brain-core.mjs (0 token LLM). NÃO editar. Rebuild: node .agent/hooks/brain-reindex.mjs -->'];
|
|
148
|
-
for (const d of decisions) lines.push(`- Decisão: [[${d}]]`);
|
|
149
|
-
for (const s of sessions) lines.push(`- Sessão ${s.date} (${s.provider || '?'}): ${s.summary || s.file} → [[${String(s.file || '').replace(/\.md$/, '')}]]`);
|
|
150
|
-
for (const b of bugs) lines.push(`- Bug: [[${b}]]`);
|
|
151
|
-
for (const l of learnings) lines.push(`- Aprendizado: [[${l}]]`);
|
|
152
|
-
|
|
153
|
-
const shown = sessions.length;
|
|
154
|
-
if (data.length > shown) lines.push(`- +${data.length - shown} mais no índice — use /brain-recall <tópico>`);
|
|
155
|
-
|
|
156
|
-
ensureDir(brainDir(vaultBase));
|
|
157
|
-
writeFileSync(join(brainDir(vaultBase), 'DIGEST.md'), lines.join('\n') + '\n', 'utf8');
|
|
158
|
-
return lines;
|
|
159
|
-
}
|
|
1
|
+
// .agent/hooks/brain-core.mjs
|
|
2
|
+
// Camada fria do brain: indexa o frontmatter das notas de sessão (0 token LLM).
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
|
|
6
|
+
import { getLocale } from './locale.mjs';
|
|
7
|
+
|
|
8
|
+
export function brainDir(vaultBase) {
|
|
9
|
+
return join(vaultBase, '.brain');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Frontmatter YAML simples: escalares `k: v` + listas `k:` seguido de ` - item`.
|
|
13
|
+
export function parseFrontmatter(content) {
|
|
14
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
15
|
+
if (!m) return {};
|
|
16
|
+
const data = {};
|
|
17
|
+
const lines = m[1].split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const kv = lines[i].match(/^([\w-]+):\s*(.*)$/);
|
|
20
|
+
if (!kv) continue;
|
|
21
|
+
const key = kv[1];
|
|
22
|
+
const val = kv[2];
|
|
23
|
+
if (val === '') {
|
|
24
|
+
const list = [];
|
|
25
|
+
while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
|
|
26
|
+
list.push(stripYamlQuotes(lines[++i].replace(/^\s+-\s+/, '').trim()));
|
|
27
|
+
}
|
|
28
|
+
data[key] = list.length ? list : '';
|
|
29
|
+
} else {
|
|
30
|
+
data[key] = stripYamlQuotes(val.trim());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function walkMd(dir) {
|
|
37
|
+
const out = [];
|
|
38
|
+
let entries;
|
|
39
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
const fp = join(dir, e.name);
|
|
42
|
+
if (e.isDirectory()) out.push(...walkMd(fp));
|
|
43
|
+
else if (e.name.endsWith('.md')) out.push(fp);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DERIVED_RE = /\[\[(0[456]-[^\]|]+?)(?:\|[^\]]*)?\]\]/g;
|
|
49
|
+
function derivedLinks(content) {
|
|
50
|
+
const dec = new Set(), bug = new Set(), lea = new Set();
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = DERIVED_RE.exec(content))) {
|
|
53
|
+
const t = m[1];
|
|
54
|
+
if (t.startsWith('04-')) dec.add(t);
|
|
55
|
+
else if (t.startsWith('05-')) bug.add(t);
|
|
56
|
+
else if (t.startsWith('06-')) lea.add(t);
|
|
57
|
+
}
|
|
58
|
+
return { decisions: [...dec], bugs: [...bug], learnings: [...lea] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
|
|
62
|
+
export function buildBrainIndex(vaultBase) {
|
|
63
|
+
const rows = [];
|
|
64
|
+
for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
|
|
65
|
+
let content;
|
|
66
|
+
try { content = readFileSync(fp, 'utf8'); } catch { continue; }
|
|
67
|
+
const fm = parseFrontmatter(content);
|
|
68
|
+
if (fm.type && fm.type !== 'session') continue;
|
|
69
|
+
const der = derivedLinks(content);
|
|
70
|
+
rows.push({
|
|
71
|
+
session_id: fm.session_id || '',
|
|
72
|
+
date: fm.date || '',
|
|
73
|
+
provider: fm.provider || '',
|
|
74
|
+
status: fm.status || '',
|
|
75
|
+
summary: fm.summary || '',
|
|
76
|
+
file: toVaultRelative(vaultBase, fp),
|
|
77
|
+
tags: Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []),
|
|
78
|
+
decisions: der.decisions,
|
|
79
|
+
bugs: der.bugs,
|
|
80
|
+
learnings: der.learnings,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
rows.sort((a, b) => (a.date + a.file).localeCompare(b.date + b.file));
|
|
84
|
+
ensureDir(brainDir(vaultBase));
|
|
85
|
+
const out = rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
|
|
86
|
+
writeFileSync(join(brainDir(vaultBase), 'index.jsonl'), out, 'utf8');
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Lê o índice gravado (linhas JSONL). Usado pelo recall e pelo digest.
|
|
91
|
+
export function loadIndex(vaultBase) {
|
|
92
|
+
try {
|
|
93
|
+
return readFileSync(join(brainDir(vaultBase), 'index.jsonl'), 'utf8')
|
|
94
|
+
.split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DIGEST_CAPS = { decisions: 5, sessions: 4, bugs: 2, learnings: 2 };
|
|
101
|
+
|
|
102
|
+
function adrNumber(path) {
|
|
103
|
+
const m = path.match(/ADR-(\d+)/);
|
|
104
|
+
return m ? Number(m[1]) : -1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Destila index.jsonl em .brain/DIGEST.md (camada quente, determinístico, 0 token LLM).
|
|
108
|
+
// Cap por construção: 1 header + 13 itens (5/4/2/2) + 1 pointer = máx 15 linhas.
|
|
109
|
+
export function buildBrainDigest(vaultBase, rows = null) {
|
|
110
|
+
const data = rows ?? loadIndex(vaultBase);
|
|
111
|
+
const byDateDesc = [...data].sort((a, b) =>
|
|
112
|
+
String(b.date || '').localeCompare(String(a.date || '')) || String(b.file || '').localeCompare(String(a.file || '')));
|
|
113
|
+
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
const pick = (kind, max) => {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const r of byDateDesc) {
|
|
118
|
+
for (const p of r[kind] || []) {
|
|
119
|
+
if (out.length >= max) return out;
|
|
120
|
+
if (!seen.has(p)) { seen.add(p); out.push(p); }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// The digest is INJECTED into every session, so a dead wikilink is dead weight in the model's
|
|
127
|
+
// context. Keep only targets that resolve to a real note (by vault-relative path or basename)
|
|
128
|
+
// and drop placeholder paths (a truncated `…` from a summary line). `pick` collects extra so
|
|
129
|
+
// caps still fill after filtering.
|
|
130
|
+
const known = new Set();
|
|
131
|
+
for (const r of data) {
|
|
132
|
+
const rel = String(r.file || '').replace(/\.md$/i, '');
|
|
133
|
+
if (rel) { known.add(rel); known.add(basename(rel)); }
|
|
134
|
+
}
|
|
135
|
+
const resolves = (p) => {
|
|
136
|
+
const t = String(p || '').replace(/\.md$/i, '').trim();
|
|
137
|
+
if (!t || t.includes('...') || t.includes('…')) return false;
|
|
138
|
+
return known.has(t) || known.has(basename(t)) || existsSync(join(vaultBase, `${t}.md`));
|
|
139
|
+
};
|
|
140
|
+
const pickLive = (kind, max) => pick(kind, max * 4).filter(resolves).slice(0, max);
|
|
141
|
+
|
|
142
|
+
const decisions = pickLive('decisions', DIGEST_CAPS.decisions).sort((a, b) => adrNumber(b) - adrNumber(a));
|
|
143
|
+
const sessions = byDateDesc.slice(0, DIGEST_CAPS.sessions);
|
|
144
|
+
const bugs = pickLive('bugs', DIGEST_CAPS.bugs);
|
|
145
|
+
const learnings = pickLive('learnings', DIGEST_CAPS.learnings);
|
|
146
|
+
|
|
147
|
+
const lines = ['<!-- AUTO-GERADO por brain-core.mjs (0 token LLM). NÃO editar. Rebuild: node .agent/hooks/brain-reindex.mjs -->'];
|
|
148
|
+
for (const d of decisions) lines.push(`- Decisão: [[${d}]]`);
|
|
149
|
+
for (const s of sessions) lines.push(`- Sessão ${s.date} (${s.provider || '?'}): ${s.summary || s.file} → [[${String(s.file || '').replace(/\.md$/, '')}]]`);
|
|
150
|
+
for (const b of bugs) lines.push(`- Bug: [[${b}]]`);
|
|
151
|
+
for (const l of learnings) lines.push(`- Aprendizado: [[${l}]]`);
|
|
152
|
+
|
|
153
|
+
const shown = sessions.length;
|
|
154
|
+
if (data.length > shown) lines.push(`- +${data.length - shown} mais no índice — use /brain-recall <tópico>`);
|
|
155
|
+
|
|
156
|
+
ensureDir(brainDir(vaultBase));
|
|
157
|
+
writeFileSync(join(brainDir(vaultBase), 'DIGEST.md'), lines.join('\n') + '\n', 'utf8');
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
package/hooks/brain-recall.mjs
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
// .agent/hooks/brain-recall.mjs
|
|
2
|
-
// Query engine read-only: pontua o índice por tópico. Token só no resultado.
|
|
3
|
-
// Uso: node .agent/hooks/brain-recall.mjs <termos da busca>
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { getVaultBase } from './obsidian-common.mjs';
|
|
6
|
-
import { loadIndex } from './brain-core.mjs';
|
|
7
|
-
|
|
8
|
-
export { loadIndex };
|
|
9
|
-
|
|
10
|
-
export function scoreRows(rows, query, topK = 5) {
|
|
11
|
-
const terms = String(query).toLowerCase().split(/\s+/).filter(Boolean);
|
|
12
|
-
if (!terms.length) return [];
|
|
13
|
-
return rows
|
|
14
|
-
.map((r) => {
|
|
15
|
-
// Inclui slug do file + paths das derivadas (ADR/bug/aprendizado) no haystack:
|
|
16
|
-
// os títulos das sessões e tags são genéricos; o sinal tópico vem dos slugs.
|
|
17
|
-
const hay = `${r.summary || ''} ${(r.tags || []).join(' ')} ${r.file || ''} ${(r.decisions || []).join(' ')} ${(r.bugs || []).join(' ')} ${(r.learnings || []).join(' ')}`.toLowerCase();
|
|
18
|
-
let score = 0;
|
|
19
|
-
for (const t of terms) if (hay.includes(t)) score++;
|
|
20
|
-
return { row: r, score };
|
|
21
|
-
})
|
|
22
|
-
.filter((s) => s.score > 0)
|
|
23
|
-
.sort((a, b) => b.score - a.score || String(b.row.date || '').localeCompare(String(a.row.date || '')))
|
|
24
|
-
.slice(0, topK)
|
|
25
|
-
.map((s) => s.row);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
29
|
-
const vaultBase = getVaultBase();
|
|
30
|
-
const hits = scoreRows(loadIndex(vaultBase), process.argv.slice(2).join(' '));
|
|
31
|
-
process.stdout.write(JSON.stringify(hits, null, 2) + '\n');
|
|
32
|
-
}
|
|
1
|
+
// .agent/hooks/brain-recall.mjs
|
|
2
|
+
// Query engine read-only: pontua o índice por tópico. Token só no resultado.
|
|
3
|
+
// Uso: node .agent/hooks/brain-recall.mjs <termos da busca>
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { getVaultBase } from './obsidian-common.mjs';
|
|
6
|
+
import { loadIndex } from './brain-core.mjs';
|
|
7
|
+
|
|
8
|
+
export { loadIndex };
|
|
9
|
+
|
|
10
|
+
export function scoreRows(rows, query, topK = 5) {
|
|
11
|
+
const terms = String(query).toLowerCase().split(/\s+/).filter(Boolean);
|
|
12
|
+
if (!terms.length) return [];
|
|
13
|
+
return rows
|
|
14
|
+
.map((r) => {
|
|
15
|
+
// Inclui slug do file + paths das derivadas (ADR/bug/aprendizado) no haystack:
|
|
16
|
+
// os títulos das sessões e tags são genéricos; o sinal tópico vem dos slugs.
|
|
17
|
+
const hay = `${r.summary || ''} ${(r.tags || []).join(' ')} ${r.file || ''} ${(r.decisions || []).join(' ')} ${(r.bugs || []).join(' ')} ${(r.learnings || []).join(' ')}`.toLowerCase();
|
|
18
|
+
let score = 0;
|
|
19
|
+
for (const t of terms) if (hay.includes(t)) score++;
|
|
20
|
+
return { row: r, score };
|
|
21
|
+
})
|
|
22
|
+
.filter((s) => s.score > 0)
|
|
23
|
+
.sort((a, b) => b.score - a.score || String(b.row.date || '').localeCompare(String(a.row.date || '')))
|
|
24
|
+
.slice(0, topK)
|
|
25
|
+
.map((s) => s.row);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
29
|
+
const vaultBase = getVaultBase();
|
|
30
|
+
const hits = scoreRows(loadIndex(vaultBase), process.argv.slice(2).join(' '));
|
|
31
|
+
process.stdout.write(JSON.stringify(hits, null, 2) + '\n');
|
|
32
|
+
}
|