@payconductor/houndsman 0.13.0 → 0.16.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/README.md +6 -1
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/golden-command.ts +92 -0
- package/src/cli/commands/hunt-command.ts +105 -4
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +6 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/progress.ts +7 -2
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +8 -0
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +278 -16
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +266 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/golden.ts +176 -0
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +29 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +310 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +41 -3
- package/src/core/defaults.ts +50 -4
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +371 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +14 -1
- package/src/core/engine/review-session.ts +11 -13
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/findings-state.ts +22 -2
- package/src/core/github-review.ts +5 -2
- package/src/core/golden.ts +346 -0
- package/src/core/hunt/hunt-runner.ts +621 -34
- package/src/core/hunt/index.ts +40 -1
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +298 -5
- package/src/core/hunt/maki-runner.ts +93 -16
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/hunt/review-checks.ts +460 -0
- package/src/core/logging/index.ts +6 -1
- package/src/core/logging/progress.ts +85 -1
- package/src/core/orchestrator.ts +33 -1289
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +18 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +90 -27
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +116 -278
- package/src/core/severity.ts +245 -0
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +17 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +51 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +55 -10
- package/src/skill/README.md +19 -14
- package/src/skill/SETUP.md +1 -0
- package/src/skill/SKILL.md +13 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +205 -58
- package/src/skill/train.md +3 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +208 -184
- package/src/tests/ai-core.test.ts +46 -0
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/compare.test.ts +256 -0
- package/src/tests/findings-state.test.ts +82 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/golden.test.ts +195 -0
- package/src/tests/hunt-maki.test.ts +710 -2
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +29 -1
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +38 -29
- package/src/tests/review-scripts.test.ts +125 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/severity.test.ts +235 -0
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo de intenção da PR (`business-summary.json`).
|
|
3
|
+
*
|
|
4
|
+
* Descreve o que a mudança **promete** entregar — fluxos, invariantes, contratos
|
|
5
|
+
* e estados — antes de qualquer watchdog julgar o código. É insumo obrigatório
|
|
6
|
+
* do revisor `general`: sem ele a revisão volta a caçar só violações de regra.
|
|
7
|
+
*
|
|
8
|
+
* Produtores, em ordem de precedência:
|
|
9
|
+
* 1. `ai` — `AiBusinessSummarizer` no prepare (quando há chave).
|
|
10
|
+
* 2. `reviewer` — subagente (skill) ou pré-passe maki (`houndsman hunt`).
|
|
11
|
+
* 3. `deterministic` — fallback montado a partir de AST/churn/contexto.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AstAnalysis, AstImpact, AstSymbolKind } from "./ast/types";
|
|
15
|
+
import { GENERAL_WATCHDOG_ID } from "./builtins";
|
|
16
|
+
import type { ChangedScope } from "./changed-scope";
|
|
17
|
+
import type { ClassificationResult } from "./file-classifier";
|
|
18
|
+
|
|
19
|
+
/** Fluxo de negócio tocado pela mudança. */
|
|
20
|
+
export type BusinessFlow = {
|
|
21
|
+
name: string;
|
|
22
|
+
/** O que dispara o fluxo (endpoint, evento, job, ação do usuário). */
|
|
23
|
+
trigger?: string;
|
|
24
|
+
/** Comportamento esperado ao final do fluxo. */
|
|
25
|
+
expected?: string;
|
|
26
|
+
/** Arquivos envolvidos (para o revisor rastrear). */
|
|
27
|
+
files?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Contrato/paridade entre lados (provider × orquestrador, adapter × tipo). */
|
|
31
|
+
export type BusinessContract = {
|
|
32
|
+
name: string;
|
|
33
|
+
/** Lados que precisam concordar (ex.: `asaas`, `charge-processor`). */
|
|
34
|
+
sides?: string[];
|
|
35
|
+
/** O que precisa ser verdadeiro em todos os lados. */
|
|
36
|
+
expectation?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Estados e transições relevantes do domínio. */
|
|
40
|
+
export type BusinessState = {
|
|
41
|
+
name: string;
|
|
42
|
+
transitions?: string[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Fonte do modelo de intenção. */
|
|
46
|
+
export type BusinessSummarySource = "ai" | "reviewer" | "deterministic";
|
|
47
|
+
|
|
48
|
+
/** Modelo de intenção de uma revisão. */
|
|
49
|
+
export type BusinessSummary = {
|
|
50
|
+
source: BusinessSummarySource;
|
|
51
|
+
/** Commit coberto — permite saber a que revisão o modelo se refere. */
|
|
52
|
+
headSha?: string;
|
|
53
|
+
/** O que a PR promete entregar (1 parágrafo curto). */
|
|
54
|
+
intent?: string;
|
|
55
|
+
flows: BusinessFlow[];
|
|
56
|
+
invariants: string[];
|
|
57
|
+
contracts: BusinessContract[];
|
|
58
|
+
states: BusinessState[];
|
|
59
|
+
/** O que a PR explicitamente não se propõe a fazer. */
|
|
60
|
+
nonGoals?: string[];
|
|
61
|
+
/** Pontos que o revisor deve confirmar com o autor. */
|
|
62
|
+
openQuestions?: string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Entrada do builder determinístico (não lê disco). */
|
|
66
|
+
export type BusinessInput = {
|
|
67
|
+
classification: Pick<ClassificationResult, "files" | "churn" | "watchdogs">;
|
|
68
|
+
/** Domínios ativos (id + nome legível). */
|
|
69
|
+
domains?: readonly { id: string; name: string }[];
|
|
70
|
+
ast?: ReadonlyMap<string, AstAnalysis>;
|
|
71
|
+
impact?: AstImpact;
|
|
72
|
+
changedScope?: ChangedScope;
|
|
73
|
+
headSha?: string;
|
|
74
|
+
context?: {
|
|
75
|
+
objective?: string;
|
|
76
|
+
motivation?: string;
|
|
77
|
+
changeSummary?: string[];
|
|
78
|
+
} | null;
|
|
79
|
+
/** Patch por arquivo (`diff.byFile`) — usado no payload da IA. */
|
|
80
|
+
snippets?: ReadonlyMap<string, string>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const MAX_FLOWS = 12;
|
|
84
|
+
const MAX_INVARIANTS = 12;
|
|
85
|
+
const MAX_CONTRACTS = 12;
|
|
86
|
+
const MAX_STATES = 8;
|
|
87
|
+
const MAX_FILES_PER_FLOW = 20;
|
|
88
|
+
const MAX_SYMBOLS_PER_FLOW = 6;
|
|
89
|
+
const MAX_PROMPT_FILES = 40;
|
|
90
|
+
const MAX_SNIPPET_CHARS = 1_200;
|
|
91
|
+
|
|
92
|
+
/** Corta o texto em `max` caracteres, sinalizando com reticências. */
|
|
93
|
+
function truncate(value: string, max: number): string {
|
|
94
|
+
const text = value.trim().replace(/\s+/g, " ");
|
|
95
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `true` para símbolos que representam contratos. */
|
|
99
|
+
function isContract(kind: AstSymbolKind): boolean {
|
|
100
|
+
return kind === "interface" || kind === "type" || kind === "enum";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Símbolos alterados de um arquivo (adicionados/alterados), sem duplicatas. */
|
|
104
|
+
function changedSymbolNames(analysis: AstAnalysis | undefined): string[] {
|
|
105
|
+
const delta = analysis?.delta;
|
|
106
|
+
if (!delta) return [];
|
|
107
|
+
const seen = new Set<string>();
|
|
108
|
+
const names: string[] = [];
|
|
109
|
+
for (const symbol of [...delta.addedSymbols, ...delta.modifiedSymbols]) {
|
|
110
|
+
if (seen.has(symbol.name)) continue;
|
|
111
|
+
seen.add(symbol.name);
|
|
112
|
+
names.push(symbol.name);
|
|
113
|
+
}
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Indica se o arquivo está entre os alterados com conteúdo revisável. */
|
|
118
|
+
function orderByChurn(
|
|
119
|
+
files: readonly string[],
|
|
120
|
+
churn: ClassificationResult["churn"],
|
|
121
|
+
): string[] {
|
|
122
|
+
return [...files].sort((a, b) => {
|
|
123
|
+
const ca = (churn[a]?.additions ?? 0) + (churn[a]?.deletions ?? 0);
|
|
124
|
+
const cb = (churn[b]?.additions ?? 0) + (churn[b]?.deletions ?? 0);
|
|
125
|
+
return cb - ca || a.localeCompare(b);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Monta o modelo determinístico: agrupa por domínio, lista símbolos alterados e
|
|
131
|
+
* extrai contratos declarados. Nunca inventa invariantes nem fluxos de negócio
|
|
132
|
+
* (isso exige leitura do diff — papel da IA/subagente).
|
|
133
|
+
*/
|
|
134
|
+
export function buildBusinessSummary(input: BusinessInput): BusinessSummary {
|
|
135
|
+
const flowDomains = (input.domains ?? []).filter(
|
|
136
|
+
(domain) => domain.id !== GENERAL_WATCHDOG_ID,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const flows: BusinessFlow[] = [];
|
|
140
|
+
for (const domain of flowDomains) {
|
|
141
|
+
const files = orderByChurn(
|
|
142
|
+
input.classification.watchdogs[domain.id] ?? [],
|
|
143
|
+
input.classification.churn,
|
|
144
|
+
).slice(0, MAX_FILES_PER_FLOW);
|
|
145
|
+
if (files.length === 0) continue;
|
|
146
|
+
const symbols = new Set<string>();
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
for (const name of changedSymbolNames(input.ast?.get(file))) {
|
|
149
|
+
if (symbols.size >= MAX_SYMBOLS_PER_FLOW) break;
|
|
150
|
+
symbols.add(name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
flows.push({
|
|
154
|
+
name: domain.name,
|
|
155
|
+
files,
|
|
156
|
+
expected:
|
|
157
|
+
symbols.size > 0
|
|
158
|
+
? `Símbolos alterados: ${[...symbols].join(", ")}`
|
|
159
|
+
: undefined,
|
|
160
|
+
});
|
|
161
|
+
if (flows.length >= MAX_FLOWS) break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const contracts: BusinessContract[] = [];
|
|
165
|
+
const seenContracts = new Set<string>();
|
|
166
|
+
if (input.ast) {
|
|
167
|
+
for (const [file, analysis] of input.ast) {
|
|
168
|
+
const added = analysis.delta?.addedSymbols ?? [];
|
|
169
|
+
for (const symbol of added) {
|
|
170
|
+
if (!isContract(symbol.kind)) continue;
|
|
171
|
+
const key = `${symbol.name}|${file}`;
|
|
172
|
+
if (seenContracts.has(key)) continue;
|
|
173
|
+
seenContracts.add(key);
|
|
174
|
+
contracts.push({
|
|
175
|
+
name: symbol.name,
|
|
176
|
+
sides: [file],
|
|
177
|
+
expectation: `Declaração \`${symbol.kind}\` nova — verificar se todos os lados do contrato a honram.`,
|
|
178
|
+
});
|
|
179
|
+
if (contracts.length >= MAX_CONTRACTS) break;
|
|
180
|
+
}
|
|
181
|
+
if (contracts.length >= MAX_CONTRACTS) break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
source: "deterministic",
|
|
187
|
+
headSha: input.headSha,
|
|
188
|
+
intent:
|
|
189
|
+
input.context?.objective?.trim() ||
|
|
190
|
+
input.context?.motivation?.trim() ||
|
|
191
|
+
undefined,
|
|
192
|
+
flows,
|
|
193
|
+
invariants: [],
|
|
194
|
+
contracts,
|
|
195
|
+
states: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Texto curto usado no brief e no manifesto. */
|
|
200
|
+
export function summarizeBusiness(business: BusinessSummary): {
|
|
201
|
+
flows: number;
|
|
202
|
+
invariants: number;
|
|
203
|
+
contracts: number;
|
|
204
|
+
states: number;
|
|
205
|
+
} {
|
|
206
|
+
return {
|
|
207
|
+
flows: business.flows.length,
|
|
208
|
+
invariants: business.invariants.length,
|
|
209
|
+
contracts: business.contracts.length,
|
|
210
|
+
states: business.states.length,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Payload compacto enviado ao modelo (nunca arquivos inteiros). */
|
|
215
|
+
export function buildBusinessPromptPayload(
|
|
216
|
+
input: BusinessInput,
|
|
217
|
+
): Record<string, unknown> {
|
|
218
|
+
const files = Object.entries(input.classification.files)
|
|
219
|
+
.map(([path, file]) => {
|
|
220
|
+
const churn = input.classification.churn[path];
|
|
221
|
+
const scope = input.changedScope?.[path];
|
|
222
|
+
return {
|
|
223
|
+
path,
|
|
224
|
+
status: file.status,
|
|
225
|
+
additions: churn?.additions ?? 0,
|
|
226
|
+
deletions: churn?.deletions ?? 0,
|
|
227
|
+
symbols: changedSymbolNames(input.ast?.get(path)).slice(
|
|
228
|
+
0,
|
|
229
|
+
MAX_SYMBOLS_PER_FLOW,
|
|
230
|
+
),
|
|
231
|
+
ranges:
|
|
232
|
+
scope?.ranges.map((range) =>
|
|
233
|
+
range.start === range.end
|
|
234
|
+
? `${range.start}`
|
|
235
|
+
: `${range.start}-${range.end}`,
|
|
236
|
+
) ?? [],
|
|
237
|
+
importedBy: input.impact?.importers[path]?.slice(0, 8) ?? [],
|
|
238
|
+
snippet: input.snippets
|
|
239
|
+
? truncate(input.snippets.get(path) ?? "", MAX_SNIPPET_CHARS)
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
})
|
|
243
|
+
.sort(
|
|
244
|
+
(a, b) =>
|
|
245
|
+
b.additions + b.deletions - (a.additions + a.deletions) ||
|
|
246
|
+
a.path.localeCompare(b.path),
|
|
247
|
+
)
|
|
248
|
+
.slice(0, MAX_PROMPT_FILES);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
headSha: input.headSha,
|
|
252
|
+
context: input.context
|
|
253
|
+
? {
|
|
254
|
+
objective: input.context.objective,
|
|
255
|
+
motivation: input.context.motivation,
|
|
256
|
+
changeSummary: input.context.changeSummary,
|
|
257
|
+
}
|
|
258
|
+
: undefined,
|
|
259
|
+
domains: (input.domains ?? []).map((domain) => domain.name),
|
|
260
|
+
files,
|
|
261
|
+
deterministic: {
|
|
262
|
+
intent:
|
|
263
|
+
input.context?.objective?.trim() ||
|
|
264
|
+
input.context?.motivation?.trim() ||
|
|
265
|
+
undefined,
|
|
266
|
+
contracts: buildBusinessSummary(input).contracts,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Normaliza uma lista de strings com limite de tamanho/quantidade. */
|
|
272
|
+
function normalizeList(
|
|
273
|
+
value: unknown,
|
|
274
|
+
max: number,
|
|
275
|
+
maxLength: number,
|
|
276
|
+
): string[] | undefined {
|
|
277
|
+
if (!Array.isArray(value)) return undefined;
|
|
278
|
+
const items = value
|
|
279
|
+
.map((item) => (typeof item === "string" ? truncate(item, maxLength) : ""))
|
|
280
|
+
.filter((item) => item.length > 0)
|
|
281
|
+
.slice(0, max);
|
|
282
|
+
return items.length > 0 ? items : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Valida/normaliza o `business-summary.json`. `null` quando não há conteúdo
|
|
287
|
+
* útil (nem intenção, nem fluxo, nem contrato).
|
|
288
|
+
*/
|
|
289
|
+
export function parseBusinessSummary(
|
|
290
|
+
raw: unknown,
|
|
291
|
+
source: BusinessSummarySource = "deterministic",
|
|
292
|
+
headSha?: string,
|
|
293
|
+
): BusinessSummary | null {
|
|
294
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
295
|
+
const obj = raw as Record<string, unknown>;
|
|
296
|
+
|
|
297
|
+
const intent =
|
|
298
|
+
typeof obj.intent === "string" && obj.intent.trim().length > 0
|
|
299
|
+
? truncate(obj.intent, 800)
|
|
300
|
+
: undefined;
|
|
301
|
+
|
|
302
|
+
const flows: BusinessFlow[] = [];
|
|
303
|
+
if (Array.isArray(obj.flows)) {
|
|
304
|
+
for (const item of obj.flows) {
|
|
305
|
+
if (typeof item !== "object" || item === null) continue;
|
|
306
|
+
const entry = item as Record<string, unknown>;
|
|
307
|
+
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
|
308
|
+
if (!name) continue;
|
|
309
|
+
flows.push({
|
|
310
|
+
name: truncate(name, 120),
|
|
311
|
+
trigger:
|
|
312
|
+
typeof entry.trigger === "string"
|
|
313
|
+
? truncate(entry.trigger, 240)
|
|
314
|
+
: undefined,
|
|
315
|
+
expected:
|
|
316
|
+
typeof entry.expected === "string"
|
|
317
|
+
? truncate(entry.expected, 400)
|
|
318
|
+
: undefined,
|
|
319
|
+
files: normalizeList(entry.files, MAX_FILES_PER_FLOW, 200),
|
|
320
|
+
});
|
|
321
|
+
if (flows.length >= MAX_FLOWS) break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const contracts: BusinessContract[] = [];
|
|
326
|
+
if (Array.isArray(obj.contracts)) {
|
|
327
|
+
for (const item of obj.contracts) {
|
|
328
|
+
if (typeof item !== "object" || item === null) continue;
|
|
329
|
+
const entry = item as Record<string, unknown>;
|
|
330
|
+
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
|
331
|
+
if (!name) continue;
|
|
332
|
+
contracts.push({
|
|
333
|
+
name: truncate(name, 120),
|
|
334
|
+
sides: normalizeList(entry.sides, 8, 160),
|
|
335
|
+
expectation:
|
|
336
|
+
typeof entry.expectation === "string"
|
|
337
|
+
? truncate(entry.expectation, 400)
|
|
338
|
+
: undefined,
|
|
339
|
+
});
|
|
340
|
+
if (contracts.length >= MAX_CONTRACTS) break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const states: BusinessState[] = [];
|
|
345
|
+
if (Array.isArray(obj.states)) {
|
|
346
|
+
for (const item of obj.states) {
|
|
347
|
+
if (typeof item !== "object" || item === null) continue;
|
|
348
|
+
const entry = item as Record<string, unknown>;
|
|
349
|
+
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
|
350
|
+
if (!name) continue;
|
|
351
|
+
states.push({
|
|
352
|
+
name: truncate(name, 120),
|
|
353
|
+
transitions: normalizeList(entry.transitions, 12, 200),
|
|
354
|
+
});
|
|
355
|
+
if (states.length >= MAX_STATES) break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const invariants = normalizeList(obj.invariants, MAX_INVARIANTS, 300) ?? [];
|
|
360
|
+
const nonGoals = normalizeList(obj.nonGoals, 8, 240);
|
|
361
|
+
const openQuestions = normalizeList(obj.openQuestions, 8, 300);
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
!intent &&
|
|
365
|
+
flows.length === 0 &&
|
|
366
|
+
contracts.length === 0 &&
|
|
367
|
+
invariants.length === 0
|
|
368
|
+
) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
source: obj.source === "ai" || obj.source === "reviewer" ? obj.source : source,
|
|
374
|
+
headSha: typeof obj.headSha === "string" ? obj.headSha : headSha,
|
|
375
|
+
intent,
|
|
376
|
+
flows,
|
|
377
|
+
invariants,
|
|
378
|
+
contracts,
|
|
379
|
+
states,
|
|
380
|
+
nonGoals,
|
|
381
|
+
openQuestions,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Linhas compactas do modelo de intenção (brief e resumo). */
|
|
386
|
+
export function renderBusinessLines(
|
|
387
|
+
business: BusinessSummary,
|
|
388
|
+
limits = { flows: 6, invariants: 6, contracts: 6, states: 4 },
|
|
389
|
+
): string[] {
|
|
390
|
+
const lines: string[] = [];
|
|
391
|
+
if (business.intent) lines.push(`**Intenção:** ${truncate(business.intent, 400)}`);
|
|
392
|
+
if (business.invariants.length > 0) {
|
|
393
|
+
lines.push("", "**Invariantes:**");
|
|
394
|
+
for (const item of business.invariants.slice(0, limits.invariants)) {
|
|
395
|
+
lines.push(`- ${truncate(item, 240)}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (business.flows.length > 0) {
|
|
399
|
+
lines.push("", `**Fluxos (${business.flows.length}):**`);
|
|
400
|
+
for (const flow of business.flows.slice(0, limits.flows)) {
|
|
401
|
+
const parts = [`- **${truncate(flow.name, 100)}**`];
|
|
402
|
+
if (flow.trigger) parts.push(`disparo: ${truncate(flow.trigger, 160)}`);
|
|
403
|
+
if (flow.expected) parts.push(`esperado: ${truncate(flow.expected, 240)}`);
|
|
404
|
+
lines.push(parts.join(" — "));
|
|
405
|
+
if (flow.files && flow.files.length > 0) {
|
|
406
|
+
lines.push(` arquivos: ${flow.files.slice(0, 6).join(", ")}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (business.contracts.length > 0) {
|
|
411
|
+
lines.push("", `**Contratos (${business.contracts.length}):**`);
|
|
412
|
+
for (const contract of business.contracts.slice(0, limits.contracts)) {
|
|
413
|
+
const sides = contract.sides?.slice(0, 4).join(" × ") ?? "";
|
|
414
|
+
const expectation = contract.expectation
|
|
415
|
+
? ` — ${truncate(contract.expectation, 240)}`
|
|
416
|
+
: "";
|
|
417
|
+
lines.push(
|
|
418
|
+
`- **${truncate(contract.name, 100)}**${sides ? ` (${sides})` : ""}${expectation}`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (business.states.length > 0) {
|
|
423
|
+
lines.push("", "**Estados:**");
|
|
424
|
+
for (const state of business.states.slice(0, limits.states)) {
|
|
425
|
+
const transitions = state.transitions?.slice(0, 6).join(" → ") ?? "";
|
|
426
|
+
lines.push(
|
|
427
|
+
`- ${truncate(state.name, 100)}${transitions ? `: ${truncate(transitions, 240)}` : ""}`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (business.openQuestions && business.openQuestions.length > 0) {
|
|
432
|
+
lines.push("", "**Pontos a confirmar:**");
|
|
433
|
+
for (const item of business.openQuestions.slice(0, 4)) {
|
|
434
|
+
lines.push(`- ${truncate(item, 240)}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return lines;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Schema textual do output (repetido no brief e no harness maki). */
|
|
441
|
+
export const BUSINESS_SUMMARY_SCHEMA = `{
|
|
442
|
+
"intent": "o que a PR promete entregar (1 parágrafo)",
|
|
443
|
+
"flows": [
|
|
444
|
+
{ "name": "fluxo", "trigger": "o que dispara", "expected": "resultado esperado", "files": ["caminho/relativo.ts"] }
|
|
445
|
+
],
|
|
446
|
+
"invariants": ["o que precisa ser verdadeiro sempre"],
|
|
447
|
+
"contracts": [
|
|
448
|
+
{ "name": "contrato/capacidade", "sides": ["lado A", "lado B"], "expectation": "o que os lados precisam honrar" }
|
|
449
|
+
],
|
|
450
|
+
"states": [{ "name": "estado", "transitions": ["A → B"] }],
|
|
451
|
+
"nonGoals": ["fora do escopo"],
|
|
452
|
+
"openQuestions": ["o que precisa ser confirmado"]
|
|
453
|
+
}`;
|
|
454
|
+
|
|
455
|
+
export type BusinessBriefInput = {
|
|
456
|
+
projectName: string;
|
|
457
|
+
branch: string;
|
|
458
|
+
headSha?: string;
|
|
459
|
+
outputPath: string;
|
|
460
|
+
/** Modelo determinístico já montado (ponto de partida). */
|
|
461
|
+
business: BusinessSummary;
|
|
462
|
+
/** Arquivos + escopo + churn para orientar a leitura. */
|
|
463
|
+
files: readonly {
|
|
464
|
+
path: string;
|
|
465
|
+
status: string;
|
|
466
|
+
ranges?: readonly { start: number; end: number }[];
|
|
467
|
+
symbols?: readonly string[];
|
|
468
|
+
}[];
|
|
469
|
+
docs?: readonly string[];
|
|
470
|
+
context?: {
|
|
471
|
+
objective?: string;
|
|
472
|
+
motivation?: string;
|
|
473
|
+
changeSummary?: string[];
|
|
474
|
+
} | null;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Brief do passo de intenção. Executado por subagente (skill) ou por pré-passe
|
|
479
|
+
* maki (`houndsman hunt`) — o output é o mesmo `BusinessSummary`.
|
|
480
|
+
*/
|
|
481
|
+
export function buildBusinessBrief(input: BusinessBriefInput): string {
|
|
482
|
+
const lines: string[] = [
|
|
483
|
+
`# Brief — Intenção da PR (\`business\`)`,
|
|
484
|
+
"",
|
|
485
|
+
`**Projeto:** ${input.projectName}`,
|
|
486
|
+
`**Branch:** ${input.branch}`,
|
|
487
|
+
...(input.headSha ? [`**Commit:** \`${input.headSha.slice(0, 7)}\``] : []),
|
|
488
|
+
`**Output obrigatório:** \`${input.outputPath}\``,
|
|
489
|
+
"",
|
|
490
|
+
"## Papel",
|
|
491
|
+
"",
|
|
492
|
+
"Você reconstrói **o que esta PR promete entregar** antes de qualquer watchdog",
|
|
493
|
+
"julgar o código. Leia o diff real (não só a descrição da PR) e produza um",
|
|
494
|
+
"modelo de intenção verificável. **Não produza achados** — este passo não revisa.",
|
|
495
|
+
"",
|
|
496
|
+
"## Schema do output",
|
|
497
|
+
"",
|
|
498
|
+
"```json",
|
|
499
|
+
BUSINESS_SUMMARY_SCHEMA,
|
|
500
|
+
"```",
|
|
501
|
+
"",
|
|
502
|
+
"## Procedimento",
|
|
503
|
+
"",
|
|
504
|
+
"1. **INTENT** — em 1 parágrafo, o comportamento que a PR deve entregar.",
|
|
505
|
+
"2. **FLOWS** — cada fluxo tocado (criação, renovação, cancelamento, webhook,",
|
|
506
|
+
" retry, transferência…) com disparo, resultado esperado e arquivos.",
|
|
507
|
+
"3. **INVARIANTS** — o que precisa valer sempre (paridade de capacidade,",
|
|
508
|
+
" credencial/conta correta, idempotência, estado consistente).",
|
|
509
|
+
"4. **CONTRACTS** — capacidades/interfaces e os lados que precisam concordar.",
|
|
510
|
+
"5. **STATES** — estados e transições afetados.",
|
|
511
|
+
"",
|
|
512
|
+
"Use apenas o que estiver no diff/arquivos do escopo. Se algo não estiver claro,",
|
|
513
|
+
"registre em `openQuestions` em vez de inventar.",
|
|
514
|
+
"",
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
if (input.context) {
|
|
518
|
+
lines.push("## Contexto da PR", "");
|
|
519
|
+
if (input.context.objective)
|
|
520
|
+
lines.push(`**Objetivo:** ${input.context.objective}`);
|
|
521
|
+
if (input.context.motivation)
|
|
522
|
+
lines.push(`**Motivação:** ${input.context.motivation}`);
|
|
523
|
+
for (const item of input.context.changeSummary ?? []) lines.push(`- ${item}`);
|
|
524
|
+
lines.push("");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const starting = renderBusinessLines(input.business, {
|
|
528
|
+
flows: 12,
|
|
529
|
+
invariants: 12,
|
|
530
|
+
contracts: 12,
|
|
531
|
+
states: 8,
|
|
532
|
+
});
|
|
533
|
+
if (starting.length > 0) {
|
|
534
|
+
lines.push(
|
|
535
|
+
"## Modelo determinístico (ponto de partida)",
|
|
536
|
+
"",
|
|
537
|
+
"> Derivado de AST/churn/contexto. Confirme, corrija e complete com o diff real.",
|
|
538
|
+
"",
|
|
539
|
+
...starting,
|
|
540
|
+
"",
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
lines.push(`## Arquivos do escopo (${input.files.length})`, "");
|
|
545
|
+
for (const file of input.files.slice(0, 60)) {
|
|
546
|
+
const ranges =
|
|
547
|
+
file.ranges && file.ranges.length > 0
|
|
548
|
+
? ` — linhas ${file.ranges
|
|
549
|
+
.map((range) =>
|
|
550
|
+
range.start === range.end
|
|
551
|
+
? `${range.start}`
|
|
552
|
+
: `${range.start}-${range.end}`,
|
|
553
|
+
)
|
|
554
|
+
.join(", ")}`
|
|
555
|
+
: "";
|
|
556
|
+
lines.push(`- \`${file.path}\` (${file.status})${ranges}`);
|
|
557
|
+
if (file.symbols && file.symbols.length > 0) {
|
|
558
|
+
lines.push(` símbolos: ${file.symbols.slice(0, 8).join(", ")}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (input.files.length > 60) lines.push(`- … +${input.files.length - 60}`);
|
|
562
|
+
lines.push("");
|
|
563
|
+
|
|
564
|
+
if (input.docs && input.docs.length > 0) {
|
|
565
|
+
lines.push(
|
|
566
|
+
"## Docs de contexto",
|
|
567
|
+
"",
|
|
568
|
+
...input.docs.map((doc) => `- \`${doc}\``),
|
|
569
|
+
"",
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
lines.push(
|
|
574
|
+
"## Regra de escopo",
|
|
575
|
+
"",
|
|
576
|
+
"> Só considere o que o diff alterou. Código fora das faixas é contexto,",
|
|
577
|
+
"> nunca um fluxo novo desta PR.",
|
|
578
|
+
"",
|
|
579
|
+
`Grave o JSON em \`${input.outputPath}\` com \`source: "reviewer"\`.`,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
return `${lines.join("\n")}\n`;
|
|
583
|
+
}
|