dd-harness 0.1.3 → 0.3.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.
- package/dist/api.d.ts +21 -0
- package/dist/api.js +69 -0
- package/dist/argv.d.ts +14 -0
- package/dist/argv.js +27 -0
- package/dist/artefato.d.ts +30 -0
- package/dist/artefato.js +42 -0
- package/dist/buscar.d.ts +20 -0
- package/dist/buscar.js +5 -11
- package/dist/check.d.ts +68 -0
- package/dist/check.js +14 -2
- package/dist/config.d.ts +33 -0
- package/dist/curar.d.ts +12 -0
- package/dist/curar.js +28 -17
- package/dist/diff.d.ts +30 -0
- package/dist/diff.js +7 -1
- package/dist/gravar.d.ts +30 -0
- package/dist/gravar.js +17 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +105 -11
- package/dist/init.d.ts +18 -0
- package/dist/init.js +27 -1
- package/dist/materializa.d.ts +70 -0
- package/dist/materializa.js +5 -0
- package/dist/medir.d.ts +1 -0
- package/dist/medir.js +41 -0
- package/dist/pasta.d.ts +11 -0
- package/dist/pasta.js +6 -12
- package/dist/politica.d.ts +27 -0
- package/dist/politica.js +35 -0
- package/dist/projeto.d.ts +19 -0
- package/dist/projeto.js +39 -0
- package/dist/sync.d.ts +59 -0
- package/dist/sync.js +2 -1
- package/package.json +44 -35
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ConfigDoRepo } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* A fronteira com o servico: ler credencial e falar HTTP.
|
|
4
|
+
*
|
|
5
|
+
* Cada comando reimplementava as mesmas tres linhas — le config, le token, monta `fetch`
|
|
6
|
+
* com `Authorization` — e cada copia era uma chance de divergir na mensagem de erro.
|
|
7
|
+
* Aqui elas existem uma vez, e o servidor MCP consome as mesmas funcoes que o CLI: as
|
|
8
|
+
* duas portas falam com a API pelo mesmo caminho, entao corrigir uma corrige a outra.
|
|
9
|
+
*/
|
|
10
|
+
export type Credencial = {
|
|
11
|
+
config: ConfigDoRepo;
|
|
12
|
+
token: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function credencial(raiz: string): Promise<Credencial>;
|
|
15
|
+
/**
|
|
16
|
+
* O `erro` que a API devolve no corpo e mais util que o status, e e o que o agente le.
|
|
17
|
+
* Sem corpo interpretavel, o status ao menos diz que houve resposta.
|
|
18
|
+
*/
|
|
19
|
+
export declare function recusa(resposta: Response): Promise<never>;
|
|
20
|
+
export declare function cabecalhos(token: string, comCorpo?: boolean): Record<string, string>;
|
|
21
|
+
export declare function pede(url: string | URL, init?: RequestInit, tentativas?: number): Promise<Response>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { leConfigDoRepo, leToken } from "./config.js";
|
|
2
|
+
export async function credencial(raiz) {
|
|
3
|
+
const config = await leConfigDoRepo(raiz);
|
|
4
|
+
const token = await leToken(config.api);
|
|
5
|
+
if (!token) {
|
|
6
|
+
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
7
|
+
}
|
|
8
|
+
return { config, token };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* O `erro` que a API devolve no corpo e mais util que o status, e e o que o agente le.
|
|
12
|
+
* Sem corpo interpretavel, o status ao menos diz que houve resposta.
|
|
13
|
+
*/
|
|
14
|
+
export async function recusa(resposta) {
|
|
15
|
+
const { erro } = (await resposta.json().catch(() => ({})));
|
|
16
|
+
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
17
|
+
}
|
|
18
|
+
export function cabecalhos(token, comCorpo = false) {
|
|
19
|
+
return comCorpo
|
|
20
|
+
? { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
|
|
21
|
+
: { Authorization: `Bearer ${token}` };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `fetch` com nova tentativa para falha transitoria.
|
|
25
|
+
*
|
|
26
|
+
* Medido em uso real: um `ECONNRESET` de conexao ociosa derrubou um `arquivar` que
|
|
27
|
+
* funcionou na segunda tentativa manual. Sem retry, falha de rede de um segundo vira erro
|
|
28
|
+
* final — e para um agente isso e a diferenca entre seguir sozinho e parar para pedir
|
|
29
|
+
* ajuda. Com varios agentes concorrendo, transitorio deixa de ser raro.
|
|
30
|
+
*
|
|
31
|
+
* O que NAO e repetido, e o cuidado que importa:
|
|
32
|
+
*
|
|
33
|
+
* - Erro de aplicacao (4xx). Token invalido ou memoria fora dos filtros nao melhora na
|
|
34
|
+
* segunda tentativa; repetir so atrasa a mensagem que o agente precisa ler.
|
|
35
|
+
* - 5xx em requisicao que ESCREVE. `POST /memorias` pode ter gravado antes de a resposta
|
|
36
|
+
* se perder, e repetir criaria duas. Perder a resposta de uma escrita que funcionou e
|
|
37
|
+
* ruim; gravar duas vezes e pior.
|
|
38
|
+
*
|
|
39
|
+
* Entao: repete falha de REDE (o `fetch` nem chegou a receber resposta) sempre, e 5xx
|
|
40
|
+
* apenas quando o metodo e seguro de repetir.
|
|
41
|
+
*/
|
|
42
|
+
const IDEMPOTENTES = new Set(["GET", "HEAD", "PUT", "DELETE"]);
|
|
43
|
+
const espera = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
44
|
+
export async function pede(url, init = {}, tentativas = 3) {
|
|
45
|
+
const metodo = (init.method ?? "GET").toUpperCase();
|
|
46
|
+
const podeRepetirErroDoServidor = IDEMPOTENTES.has(metodo);
|
|
47
|
+
let ultimoErro;
|
|
48
|
+
for (let tentativa = 1; tentativa <= tentativas; tentativa += 1) {
|
|
49
|
+
try {
|
|
50
|
+
const resposta = await fetch(url, init);
|
|
51
|
+
if (resposta.status >= 500 && podeRepetirErroDoServidor && tentativa < tentativas) {
|
|
52
|
+
await espera(tentativa * 400);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
return resposta;
|
|
56
|
+
}
|
|
57
|
+
catch (erro) {
|
|
58
|
+
// Aqui o `fetch` falhou antes de qualquer resposta: DNS, conexao recusada,
|
|
59
|
+
// ECONNRESET. Nada foi processado do outro lado, entao repetir e seguro mesmo
|
|
60
|
+
// para POST.
|
|
61
|
+
ultimoErro = erro;
|
|
62
|
+
if (tentativa === tentativas)
|
|
63
|
+
break;
|
|
64
|
+
await espera(tentativa * 400);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const detalhe = ultimoErro instanceof Error ? ultimoErro.message : String(ultimoErro);
|
|
68
|
+
throw new Error(`não consegui falar com o serviço depois de ${tentativas} tentativas: ${detalhe}`);
|
|
69
|
+
}
|
package/dist/argv.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leitura de argumentos da linha de comando.
|
|
3
|
+
*
|
|
4
|
+
* Vive fora do `index.ts` porque aquele modulo executa o CLI ao ser importado
|
|
5
|
+
* (`principal()` no topo) — teste que o importasse rodaria o programa inteiro.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Os termos livres de uma consulta, sem os pares `--flag valor`.
|
|
9
|
+
*
|
|
10
|
+
* Filtrar so o que comeca com `--` deixava o VALOR da flag para tras, e ele entrava na
|
|
11
|
+
* consulta: `buscar "efeito" --limite 2` virava a pergunta `"efeito 2"`. A busca roda, a
|
|
12
|
+
* resposta parece valida e responde a outra coisa — quem chamou nao tem como notar.
|
|
13
|
+
*/
|
|
14
|
+
export declare function termosDaConsulta(argv: string[]): string;
|
package/dist/argv.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leitura de argumentos da linha de comando.
|
|
3
|
+
*
|
|
4
|
+
* Vive fora do `index.ts` porque aquele modulo executa o CLI ao ser importado
|
|
5
|
+
* (`principal()` no topo) — teste que o importasse rodaria o programa inteiro.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Os termos livres de uma consulta, sem os pares `--flag valor`.
|
|
9
|
+
*
|
|
10
|
+
* Filtrar so o que comeca com `--` deixava o VALOR da flag para tras, e ele entrava na
|
|
11
|
+
* consulta: `buscar "efeito" --limite 2` virava a pergunta `"efeito 2"`. A busca roda, a
|
|
12
|
+
* resposta parece valida e responde a outra coisa — quem chamou nao tem como notar.
|
|
13
|
+
*/
|
|
14
|
+
export function termosDaConsulta(argv) {
|
|
15
|
+
const termos = [];
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const atual = argv[i];
|
|
18
|
+
if (atual.startsWith("--")) {
|
|
19
|
+
// `--flag=valor` carrega o valor no proprio token; `--flag valor` consome o proximo.
|
|
20
|
+
if (!atual.includes("="))
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
termos.push(atual);
|
|
25
|
+
}
|
|
26
|
+
return termos.join(" ").trim();
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ler e escrever a politica e o briefing do projeto.
|
|
3
|
+
*
|
|
4
|
+
* Ate aqui artefato so se editava pela interface, e isso deixava o agente sem saida para a
|
|
5
|
+
* unica coisa que ele mais precisa saber: a propria politica. O CLI exige que alguem ja
|
|
6
|
+
* tenha escrito nela que o CLI existe — e era justamente ela que estava fora de alcance.
|
|
7
|
+
*/
|
|
8
|
+
export type TipoDeArtefato = "politica" | "briefing";
|
|
9
|
+
/**
|
|
10
|
+
* O `GET` devolve o payload inteiro (politica, briefing, pastas e memorias); aqui so o
|
|
11
|
+
* artefato pedido interessa.
|
|
12
|
+
*
|
|
13
|
+
* `null` e `""` significam a mesma coisa para quem le — "ainda nao existe" —, e a diferenca
|
|
14
|
+
* entre elas e detalhe de como a linha foi parar no banco. Quem chama recebe `existe`, que
|
|
15
|
+
* e a pergunta real.
|
|
16
|
+
*/
|
|
17
|
+
export declare function leArtefato(raiz: string, tipo: TipoDeArtefato): Promise<{
|
|
18
|
+
conteudo: string;
|
|
19
|
+
existe: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Substituicao total, nao append: quem quer acrescentar um paragrafo le, concatena e
|
|
23
|
+
* reenvia. Conteudo vazio e valido e significa "apagar" — a linha fica no banco, guardando
|
|
24
|
+
* quem mexeu por ultimo.
|
|
25
|
+
*/
|
|
26
|
+
export declare function escreveArtefato(raiz: string, tipo: TipoDeArtefato, conteudo: string): Promise<{
|
|
27
|
+
artefato: string;
|
|
28
|
+
criou: boolean;
|
|
29
|
+
tamanho: number;
|
|
30
|
+
}>;
|
package/dist/artefato.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
2
|
+
/**
|
|
3
|
+
* O `GET` devolve o payload inteiro (politica, briefing, pastas e memorias); aqui so o
|
|
4
|
+
* artefato pedido interessa.
|
|
5
|
+
*
|
|
6
|
+
* `null` e `""` significam a mesma coisa para quem le — "ainda nao existe" —, e a diferenca
|
|
7
|
+
* entre elas e detalhe de como a linha foi parar no banco. Quem chama recebe `existe`, que
|
|
8
|
+
* e a pergunta real.
|
|
9
|
+
*/
|
|
10
|
+
export async function leArtefato(raiz, tipo) {
|
|
11
|
+
const { config, token } = await credencial(raiz);
|
|
12
|
+
const url = new URL(`${config.api}/api/v1/artefatos`);
|
|
13
|
+
url.searchParams.set("tenant", config.tenant);
|
|
14
|
+
url.searchParams.set("projeto", config.projeto);
|
|
15
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
16
|
+
if (!resposta.ok)
|
|
17
|
+
await recusa(resposta);
|
|
18
|
+
const payload = (await resposta.json());
|
|
19
|
+
const conteudo = typeof payload[tipo] === "string" ? payload[tipo] : "";
|
|
20
|
+
return { conteudo, existe: conteudo.length > 0 };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Substituicao total, nao append: quem quer acrescentar um paragrafo le, concatena e
|
|
24
|
+
* reenvia. Conteudo vazio e valido e significa "apagar" — a linha fica no banco, guardando
|
|
25
|
+
* quem mexeu por ultimo.
|
|
26
|
+
*/
|
|
27
|
+
export async function escreveArtefato(raiz, tipo, conteudo) {
|
|
28
|
+
const { config, token } = await credencial(raiz);
|
|
29
|
+
const resposta = await pede(`${config.api}/api/v1/artefatos`, {
|
|
30
|
+
method: "PUT",
|
|
31
|
+
headers: cabecalhos(token, true),
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
tenant: config.tenant,
|
|
34
|
+
projeto: config.projeto,
|
|
35
|
+
tipo,
|
|
36
|
+
conteudo,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
if (!resposta.ok)
|
|
40
|
+
await recusa(resposta);
|
|
41
|
+
return (await resposta.json());
|
|
42
|
+
}
|
package/dist/buscar.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dd-harness buscar "<pergunta>"` — recuperacao por relevancia.
|
|
3
|
+
*
|
|
4
|
+
* O contraponto da ancora: ela e exata e responde "que memorias falam deste arquivo?";
|
|
5
|
+
* esta responde "o que ja aprendemos sobre isto?", que e a pergunta de quem ainda esta
|
|
6
|
+
* entendendo o problema e nem sabe qual arquivo abrir.
|
|
7
|
+
*
|
|
8
|
+
* Devolve endereco, nao conteudo: quem quiser o corpo abre o arquivo que o `sync` ja
|
|
9
|
+
* materializou. Assim a busca continua barata e o Brain em disco continua sendo a fonte.
|
|
10
|
+
*/
|
|
11
|
+
export type Achado = {
|
|
12
|
+
endereco: string;
|
|
13
|
+
titulo: string;
|
|
14
|
+
resumo: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function busca(raiz: string, consulta: string, limite?: number): Promise<{
|
|
17
|
+
semantica: boolean;
|
|
18
|
+
provedor: string | null;
|
|
19
|
+
achados: Achado[];
|
|
20
|
+
}>;
|
package/dist/buscar.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
2
2
|
export async function busca(raiz, consulta, limite) {
|
|
3
|
-
const config = await
|
|
4
|
-
const token = await leToken(config.api);
|
|
5
|
-
if (!token) {
|
|
6
|
-
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
7
|
-
}
|
|
3
|
+
const { config, token } = await credencial(raiz);
|
|
8
4
|
const url = new URL(`${config.api}/api/v1/busca`);
|
|
9
5
|
url.searchParams.set("tenant", config.tenant);
|
|
10
6
|
url.searchParams.set("projeto", config.projeto);
|
|
11
7
|
url.searchParams.set("q", consulta);
|
|
12
8
|
if (limite)
|
|
13
9
|
url.searchParams.set("limite", String(limite));
|
|
14
|
-
const resposta = await
|
|
15
|
-
if (!resposta.ok)
|
|
16
|
-
|
|
17
|
-
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
18
|
-
}
|
|
10
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
11
|
+
if (!resposta.ok)
|
|
12
|
+
await recusa(resposta);
|
|
19
13
|
const lido = (await resposta.json());
|
|
20
14
|
return { semantica: lido.semantica, provedor: lido.provedor, achados: lido.resultados };
|
|
21
15
|
}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Tocada } from "./diff.js";
|
|
2
|
+
import type { Brain } from "./materializa.js";
|
|
3
|
+
/**
|
|
4
|
+
* `dd-harness check` — resolve as âncoras contra a árvore de trabalho.
|
|
5
|
+
*
|
|
6
|
+
* Mede cada alvo e manda o resultado ao serviço, que julga. Nunca falha o processo por
|
|
7
|
+
* encontrar deriva: sai com 0 e imprime. Deriva avisa, não bloqueia — bloqueio antes de
|
|
8
|
+
* o mecanismo ter reputação ensina o reflexo do `--no-verify`.
|
|
9
|
+
*/
|
|
10
|
+
export type Medicao = {
|
|
11
|
+
pasta: string;
|
|
12
|
+
memoria: string;
|
|
13
|
+
valor: string;
|
|
14
|
+
sha: string | null;
|
|
15
|
+
detalhe?: string;
|
|
16
|
+
};
|
|
17
|
+
export type ResultadoDoCheck = {
|
|
18
|
+
medidas: number;
|
|
19
|
+
ausentes: string[];
|
|
20
|
+
novas: number;
|
|
21
|
+
base: number;
|
|
22
|
+
jaAbertas: number;
|
|
23
|
+
/** Observacoes que fecharam sozinhas: o alvo voltou a bater com a linha de base. */
|
|
24
|
+
fechadas: number;
|
|
25
|
+
/** Memorias que falam do que o commit mudou. Vazio quando nao ha `--commit`. */
|
|
26
|
+
tocadas: Tocada[];
|
|
27
|
+
};
|
|
28
|
+
export type ResultadoDoStatus = {
|
|
29
|
+
/**
|
|
30
|
+
* O tamanho do acervo, por pasta. Sem isto, um agente que pergunta "o que ha no Brain
|
|
31
|
+
* deste projeto?" nao tem comando que responda — e a saida que resta e contar arquivo
|
|
32
|
+
* em disco, que acerta por acidente: memoria gravada e nao sincronizada nao esta la, e
|
|
33
|
+
* memoria arquivada sai do disco mas continua no indice, em `## Historico`.
|
|
34
|
+
*/
|
|
35
|
+
acervo: {
|
|
36
|
+
ativas: number;
|
|
37
|
+
arquivadas: number;
|
|
38
|
+
porPasta: {
|
|
39
|
+
pasta: string;
|
|
40
|
+
quantas: number;
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
43
|
+
comDeriva: {
|
|
44
|
+
pasta: string;
|
|
45
|
+
memoria: string;
|
|
46
|
+
titulo: string;
|
|
47
|
+
abertas: number;
|
|
48
|
+
}[];
|
|
49
|
+
vencidas: {
|
|
50
|
+
pasta: string;
|
|
51
|
+
memoria: string;
|
|
52
|
+
titulo: string;
|
|
53
|
+
}[];
|
|
54
|
+
/**
|
|
55
|
+
* Memorias que o indexador desistiu de processar. Sem embedding elas somem da busca
|
|
56
|
+
* semantica e continuam aparecendo no acervo — o unico sintoma e procurar e nao achar.
|
|
57
|
+
*/
|
|
58
|
+
travadasNaFila: number;
|
|
59
|
+
};
|
|
60
|
+
/** Separado do IO para poder ser testado sem rede: dado um Brain, o que se mede. */
|
|
61
|
+
export declare function medeAsAncoras(raiz: string, brain: Brain): Promise<Medicao[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Somente leitura, de proposito: e o que roda na ABERTURA da sessao. Medir e reportar
|
|
64
|
+
* ali dentro faria toda sessao escrever no servico antes de a pessoa digitar qualquer
|
|
65
|
+
* coisa — caro, e o tipo de efeito colateral que ninguem espera de "abrir o editor".
|
|
66
|
+
*/
|
|
67
|
+
export declare function status(raiz: string): Promise<ResultadoDoStatus>;
|
|
68
|
+
export declare function check(raiz: string, commit?: string): Promise<ResultadoDoCheck>;
|
package/dist/check.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pede } from "./api.js";
|
|
1
2
|
import { leConfigDoRepo, leToken } from "./config.js";
|
|
2
3
|
import { caminhosDoCommit, memoriasTocadas } from "./diff.js";
|
|
3
4
|
import { mede } from "./medir.js";
|
|
@@ -29,7 +30,7 @@ async function buscaOBrain(raiz) {
|
|
|
29
30
|
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
30
31
|
}
|
|
31
32
|
const parametros = `tenant=${encodeURIComponent(config.tenant)}&projeto=${encodeURIComponent(config.projeto)}`;
|
|
32
|
-
const resposta = await
|
|
33
|
+
const resposta = await pede(`${config.api}/api/v1/artefatos?${parametros}`, {
|
|
33
34
|
headers: { Authorization: `Bearer ${token}` },
|
|
34
35
|
});
|
|
35
36
|
if (resposta.status === 401) {
|
|
@@ -49,7 +50,17 @@ export async function status(raiz) {
|
|
|
49
50
|
const { brain } = await buscaOBrain(raiz);
|
|
50
51
|
const agora = Date.now();
|
|
51
52
|
const ativas = brain.memorias.filter((m) => m.status === "ativa");
|
|
53
|
+
const porPasta = new Map();
|
|
54
|
+
for (const m of ativas)
|
|
55
|
+
porPasta.set(m.pasta, (porPasta.get(m.pasta) ?? 0) + 1);
|
|
52
56
|
return {
|
|
57
|
+
acervo: {
|
|
58
|
+
ativas: ativas.length,
|
|
59
|
+
arquivadas: brain.memorias.length - ativas.length,
|
|
60
|
+
porPasta: [...porPasta]
|
|
61
|
+
.map(([pasta, quantas]) => ({ pasta, quantas }))
|
|
62
|
+
.sort((a, b) => a.pasta.localeCompare(b.pasta)),
|
|
63
|
+
},
|
|
53
64
|
comDeriva: ativas
|
|
54
65
|
.filter((m) => (m.deriva_aberta ?? 0) > 0)
|
|
55
66
|
.map((m) => ({
|
|
@@ -61,6 +72,7 @@ export async function status(raiz) {
|
|
|
61
72
|
vencidas: ativas
|
|
62
73
|
.filter((m) => m.revisar_ate && new Date(m.revisar_ate).getTime() < agora)
|
|
63
74
|
.map((m) => ({ pasta: m.pasta, memoria: m.slug, titulo: m.titulo })),
|
|
75
|
+
travadasNaFila: brain.travadas_na_fila ?? 0,
|
|
64
76
|
};
|
|
65
77
|
}
|
|
66
78
|
export async function check(raiz, commit) {
|
|
@@ -74,7 +86,7 @@ export async function check(raiz, commit) {
|
|
|
74
86
|
if (medicoes.length === 0) {
|
|
75
87
|
return { medidas: 0, ausentes: [], novas: 0, base: 0, jaAbertas: 0, fechadas: 0, tocadas };
|
|
76
88
|
}
|
|
77
|
-
const envio = await
|
|
89
|
+
const envio = await pede(`${config.api}/api/v1/deriva`, {
|
|
78
90
|
method: "POST",
|
|
79
91
|
headers: { ...cabecalhos, "Content-Type": "application/json" },
|
|
80
92
|
body: JSON.stringify({
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duas configuracoes, em lugares diferentes de proposito.
|
|
3
|
+
*
|
|
4
|
+
* `.dd-harness.json` fica **no repositorio** e e versionado: diz qual projeto aquele
|
|
5
|
+
* repo consome, e isso e informacao do repo, nao da pessoa.
|
|
6
|
+
*
|
|
7
|
+
* A credencial fica **na maquina** (`~/.dd-harness/credentials.json`, modo 0600) e nunca
|
|
8
|
+
* no repo: token e da pessoa e da maquina, e commitado seria segredo publicado.
|
|
9
|
+
*/
|
|
10
|
+
export type ConfigDoRepo = {
|
|
11
|
+
api: string;
|
|
12
|
+
tenant: string;
|
|
13
|
+
projeto: string;
|
|
14
|
+
/** Onde o gerado vive. Muda-la move tudo de uma vez, menos o CLAUDE.md da raiz. */
|
|
15
|
+
pasta: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const CAMINHO_CONFIG = ".dd-harness.json";
|
|
18
|
+
export declare const CAMINHO_MANIFESTO = "dd-harness/manifest.json";
|
|
19
|
+
export declare function leConfigDoRepo(raiz: string): Promise<ConfigDoRepo>;
|
|
20
|
+
/**
|
|
21
|
+
* Credencial por origem da API: a mesma maquina pode falar com uma instalacao local e
|
|
22
|
+
* com a de producao, e misturar token entre as duas daria 401 confuso.
|
|
23
|
+
*/
|
|
24
|
+
export declare function leToken(api: string): Promise<string | null>;
|
|
25
|
+
export declare function guardaToken(api: string, token: string): Promise<string>;
|
|
26
|
+
export type Manifesto = {
|
|
27
|
+
etag: string | null;
|
|
28
|
+
arquivos: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
export declare const manifestoVazio: () => Manifesto;
|
|
31
|
+
export declare function leManifesto(raiz: string): Promise<Manifesto>;
|
|
32
|
+
export declare function gravaManifesto(raiz: string, manifesto: Manifesto): Promise<void>;
|
|
33
|
+
export declare const hashDe: (conteudo: string) => string;
|
package/dist/curar.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function edita(raiz: string, caminho: string): Promise<{
|
|
2
|
+
endereco: string;
|
|
3
|
+
ancoras: number;
|
|
4
|
+
}>;
|
|
5
|
+
export type Arquivamento = {
|
|
6
|
+
motivo: "obsoleta" | "incorreta" | "fora_dos_filtros";
|
|
7
|
+
substituidaPor?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function arquiva(raiz: string, endereco: string, opcoes: Arquivamento): Promise<{
|
|
10
|
+
endereco: string;
|
|
11
|
+
motivo: string;
|
|
12
|
+
}>;
|
package/dist/curar.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
4
|
+
import { gravaManifesto, hashDe, leManifesto } from "./config.js";
|
|
3
5
|
import { interpreta } from "./gravar.js";
|
|
4
6
|
/**
|
|
5
7
|
* Curadoria pelo agente: editar e arquivar.
|
|
@@ -12,25 +14,33 @@ import { interpreta } from "./gravar.js";
|
|
|
12
14
|
* que o `sync` materializou e manda de volta. Um formato so para as duas operacoes, e o
|
|
13
15
|
* que ele ja sabe ler.
|
|
14
16
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Depois que o servico aceita, o arquivo em disco deixa de ser "edicao nao enviada" — e o
|
|
19
|
+
* manifesto tem que saber, senao o `sync` seguinte para com "editado a mao" e a unica saida
|
|
20
|
+
* que resta e descartar o arquivo. O ciclo materializa-corrige-envia travava justamente
|
|
21
|
+
* aqui, com o servico ja atualizado.
|
|
22
|
+
*
|
|
23
|
+
* Guarda o hash do que foi enviado, nao do que o servico devolveria: o `sync` seguinte
|
|
24
|
+
* reescreve o arquivo na forma canonica quando o ETag mudar.
|
|
25
|
+
*/
|
|
26
|
+
async function marcaComoEnviado(raiz, caminho, conteudo) {
|
|
27
|
+
const chave = relative(raiz, caminho).split("\\").join("/");
|
|
28
|
+
const manifesto = await leManifesto(raiz);
|
|
29
|
+
// Arquivo de fora do repositorio (um rascunho em /tmp, por exemplo) nao esta no
|
|
30
|
+
// manifesto e nao deve entrar: o manifesto descreve o que o `sync` materializou.
|
|
31
|
+
if (!manifesto.arquivos[chave])
|
|
32
|
+
return;
|
|
33
|
+
manifesto.arquivos[chave] = hashDe(conteudo);
|
|
34
|
+
await gravaManifesto(raiz, manifesto);
|
|
26
35
|
}
|
|
27
36
|
export async function edita(raiz, caminho) {
|
|
28
37
|
const { config, token } = await credencial(raiz);
|
|
29
|
-
const
|
|
38
|
+
const cru = await readFile(caminho, "utf8");
|
|
39
|
+
const memoria = interpreta(cru);
|
|
30
40
|
const endereco = `${memoria.pasta}/${memoria.slug}`;
|
|
31
|
-
const resposta = await
|
|
41
|
+
const resposta = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
|
|
32
42
|
method: "PATCH",
|
|
33
|
-
headers:
|
|
43
|
+
headers: cabecalhos(token, true),
|
|
34
44
|
body: JSON.stringify({
|
|
35
45
|
tenant: config.tenant,
|
|
36
46
|
projeto: config.projeto,
|
|
@@ -45,13 +55,14 @@ export async function edita(raiz, caminho) {
|
|
|
45
55
|
});
|
|
46
56
|
if (!resposta.ok)
|
|
47
57
|
await recusa(resposta);
|
|
58
|
+
await marcaComoEnviado(raiz, caminho, cru);
|
|
48
59
|
return { endereco, ancoras: memoria.ancoras.length };
|
|
49
60
|
}
|
|
50
61
|
export async function arquiva(raiz, endereco, opcoes) {
|
|
51
62
|
const { config, token } = await credencial(raiz);
|
|
52
|
-
const resposta = await
|
|
63
|
+
const resposta = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
|
|
53
64
|
method: "DELETE",
|
|
54
|
-
headers:
|
|
65
|
+
headers: cabecalhos(token, true),
|
|
55
66
|
body: JSON.stringify({
|
|
56
67
|
tenant: config.tenant,
|
|
57
68
|
projeto: config.projeto,
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Brain } from "./materializa.js";
|
|
2
|
+
/**
|
|
3
|
+
* Quais memorias falam do que este commit mudou.
|
|
4
|
+
*
|
|
5
|
+
* E a peca que devolve a memoria ao code review. Tirar o Brain do repositorio tirou o
|
|
6
|
+
* revisor que o diff dava de graca — no modelo file-based a memoria estava ali, no
|
|
7
|
+
* caminho de quem mexia. Aqui ela volta pelo cruzamento: caminho do diff contra ancora.
|
|
8
|
+
*
|
|
9
|
+
* Repare que isto NAO e deriva. Deriva e "o alvo mudou desde a linha de base"; isto e
|
|
10
|
+
* "voce acabou de mexer no que esta memoria guarda" — util mesmo quando a base ainda
|
|
11
|
+
* nem foi medida, e util mesmo que a memoria continue valendo.
|
|
12
|
+
*/
|
|
13
|
+
/** Caminhos alterados num commit. Vazio quando o git nao responde — nunca quebra. */
|
|
14
|
+
export declare function caminhosDoCommit(raiz: string, commit: string): Promise<string[]>;
|
|
15
|
+
export type Tocada = {
|
|
16
|
+
pasta: string;
|
|
17
|
+
memoria: string;
|
|
18
|
+
titulo: string;
|
|
19
|
+
ancora: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
|
|
23
|
+
* diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
|
|
24
|
+
*
|
|
25
|
+
* Ancora de trecho (`arquivo.js#alvo`) casa pelo ARQUIVO: o `git diff-tree` devolve
|
|
26
|
+
* caminho puro, nunca com `#`, entao comparar o valor inteiro nao casava nunca — e em
|
|
27
|
+
* silencio, porque nao casar e resultado valido. O alvo continua no `Tocada.ancora`, que
|
|
28
|
+
* e o que a mensagem mostra: quem le precisa saber qual trecho a memoria guarda.
|
|
29
|
+
*/
|
|
30
|
+
export declare function memoriasTocadas(brain: Brain, caminhos: string[]): Tocada[];
|
package/dist/diff.js
CHANGED
|
@@ -30,6 +30,11 @@ export async function caminhosDoCommit(raiz, commit) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
|
|
32
32
|
* diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
|
|
33
|
+
*
|
|
34
|
+
* Ancora de trecho (`arquivo.js#alvo`) casa pelo ARQUIVO: o `git diff-tree` devolve
|
|
35
|
+
* caminho puro, nunca com `#`, entao comparar o valor inteiro nao casava nunca — e em
|
|
36
|
+
* silencio, porque nao casar e resultado valido. O alvo continua no `Tocada.ancora`, que
|
|
37
|
+
* e o que a mensagem mostra: quem le precisa saber qual trecho a memoria guarda.
|
|
33
38
|
*/
|
|
34
39
|
export function memoriasTocadas(brain, caminhos) {
|
|
35
40
|
const tocadas = [];
|
|
@@ -37,7 +42,8 @@ export function memoriasTocadas(brain, caminhos) {
|
|
|
37
42
|
if (memoria.status !== "ativa")
|
|
38
43
|
continue;
|
|
39
44
|
for (const ancora of memoria.ancoras) {
|
|
40
|
-
const
|
|
45
|
+
const alvo = ancora.valor.split("#")[0];
|
|
46
|
+
const casa = caminhos.some((c) => c === alvo || c.startsWith(`${alvo}/`));
|
|
41
47
|
if (casa) {
|
|
42
48
|
tocadas.push({
|
|
43
49
|
pasta: memoria.pasta,
|
package/dist/gravar.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dd-harness gravar <arquivo.md>` — o caminho pelo qual o agente registra memoria.
|
|
3
|
+
*
|
|
4
|
+
* A entrada e um markdown no mesmo formato que o `sync` materializa, porque e o formato
|
|
5
|
+
* que o agente ja sabe ler: frontmatter, corpo, os tres filtros, ancoras. Escrever um
|
|
6
|
+
* arquivo e o que ele fazia no modelo file-based; aqui o arquivo vira requisicao em vez
|
|
7
|
+
* de virar commit.
|
|
8
|
+
*
|
|
9
|
+
* O arquivo nao e deixado no repositorio: quem materializa e o `sync`, a partir do
|
|
10
|
+
* servico. Gravar e depois sincronizar traz a memoria de volta ja com o endereco real —
|
|
11
|
+
* e evita a divergencia de ter uma copia escrita a mao ao lado da copia gerada.
|
|
12
|
+
*/
|
|
13
|
+
export type MemoriaLida = {
|
|
14
|
+
slug: string;
|
|
15
|
+
titulo: string;
|
|
16
|
+
resumo: string;
|
|
17
|
+
pasta: string;
|
|
18
|
+
corpo: string;
|
|
19
|
+
dano: string;
|
|
20
|
+
invisibilidade: string;
|
|
21
|
+
externalidade: string;
|
|
22
|
+
ancoras: string[];
|
|
23
|
+
tambemEm: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function interpreta(texto: string): MemoriaLida;
|
|
26
|
+
export declare function grava(raiz: string, caminho: string): Promise<{
|
|
27
|
+
endereco: string;
|
|
28
|
+
ancoras: number;
|
|
29
|
+
projetos: number;
|
|
30
|
+
}>;
|
package/dist/gravar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
3
3
|
const OBRIGATORIOS = ["name", "titulo", "description", "pasta"];
|
|
4
4
|
/** Frontmatter simples: `chave: valor` por linha. Sem lib de YAML — nao ha aninhamento. */
|
|
5
5
|
function leFrontmatter(texto) {
|
|
@@ -46,9 +46,18 @@ export function interpreta(texto) {
|
|
|
46
46
|
throw new Error(`frontmatter sem \`${chave}\`.`);
|
|
47
47
|
}
|
|
48
48
|
// O corpo vai ate a secao dos filtros; dali para baixo e metadado, nao conteudo.
|
|
49
|
-
const
|
|
50
|
-
|
|
49
|
+
const cabecalhoDosFiltros = /\n##\s+Os tr[êe]s filtros\s*\n/gi;
|
|
50
|
+
const ocorrencias = [...resto.matchAll(cabecalhoDosFiltros)];
|
|
51
|
+
if (ocorrencias.length === 0)
|
|
51
52
|
throw new Error("falta a seção `## Os três filtros`.");
|
|
53
|
+
// Duas ocorrencias tornam o arquivo ambiguo: cortar na primeira trunca o corpo (e os
|
|
54
|
+
// filtros ainda saem certos da segunda, entao grava e perde dado sem avisar), e cortar
|
|
55
|
+
// na ultima escolheria em silencio qual secao e a de verdade. Recusar diz o que houve.
|
|
56
|
+
if (ocorrencias.length > 1) {
|
|
57
|
+
throw new Error("a seção `## Os três filtros` aparece mais de uma vez — deixe só a real, " +
|
|
58
|
+
"ou reescreva a citação no corpo (por exemplo, entre crases).");
|
|
59
|
+
}
|
|
60
|
+
const corte = ocorrencias[0].index;
|
|
52
61
|
const corpo = resto
|
|
53
62
|
.slice(0, corte)
|
|
54
63
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
@@ -74,15 +83,11 @@ export function interpreta(texto) {
|
|
|
74
83
|
};
|
|
75
84
|
}
|
|
76
85
|
export async function grava(raiz, caminho) {
|
|
77
|
-
const config = await
|
|
78
|
-
const token = await leToken(config.api);
|
|
79
|
-
if (!token) {
|
|
80
|
-
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
81
|
-
}
|
|
86
|
+
const { config, token } = await credencial(raiz);
|
|
82
87
|
const memoria = interpreta(await readFile(caminho, "utf8"));
|
|
83
|
-
const resposta = await
|
|
88
|
+
const resposta = await pede(`${config.api}/api/v1/memorias`, {
|
|
84
89
|
method: "POST",
|
|
85
|
-
headers:
|
|
90
|
+
headers: cabecalhos(token, true),
|
|
86
91
|
body: JSON.stringify({
|
|
87
92
|
tenant: config.tenant,
|
|
88
93
|
projeto: config.projeto,
|
|
@@ -98,10 +103,8 @@ export async function grava(raiz, caminho) {
|
|
|
98
103
|
tambem_em: memoria.tambemEm,
|
|
99
104
|
}),
|
|
100
105
|
});
|
|
101
|
-
if (!resposta.ok)
|
|
102
|
-
|
|
103
|
-
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
104
|
-
}
|
|
106
|
+
if (!resposta.ok)
|
|
107
|
+
await recusa(resposta);
|
|
105
108
|
const { endereco } = (await resposta.json());
|
|
106
109
|
return {
|
|
107
110
|
endereco,
|
package/dist/index.d.ts
ADDED