@payconductor/houndsman 0.4.0 → 0.6.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 +35 -41
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +77 -13
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +454 -16
- package/src/core/paths.ts +3 -0
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +15 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
package/src/core/brief.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* específico do domínio vive nas regras do watchdog (`rules`).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { AstDelta } from "./ast/types";
|
|
8
9
|
import {
|
|
9
10
|
type Rule,
|
|
10
11
|
SEVERITY_EMOJI,
|
|
@@ -117,6 +118,141 @@ function deletedSection(args: WatchdogRenderArgs, def: WatchdogDefinition): stri
|
|
|
117
118
|
];
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/** Escapa um valor para uso em code-span (evita quebrar o markdown). */
|
|
122
|
+
function inlineCode(value: string): string {
|
|
123
|
+
return `\`${value.replace(/`/g, "'")}\``;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Trunca um texto longo (assinatura) para o brief. */
|
|
127
|
+
function shorten(value: string, max = 120): string {
|
|
128
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
129
|
+
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Linhas de um delta estrutural (compactas e determinísticas). */
|
|
133
|
+
function deltaLines(delta: AstDelta): string[] {
|
|
134
|
+
const lines: string[] = [];
|
|
135
|
+
for (const symbol of delta.addedSymbols.slice(0, 4)) {
|
|
136
|
+
lines.push(`+ ${inlineCode(symbol.name)} (${symbol.kind})`);
|
|
137
|
+
}
|
|
138
|
+
for (const symbol of delta.removedSymbols.slice(0, 4)) {
|
|
139
|
+
lines.push(`− ${inlineCode(symbol.name)} (${symbol.kind})`);
|
|
140
|
+
}
|
|
141
|
+
for (const symbol of delta.modifiedSymbols.slice(0, 4)) {
|
|
142
|
+
lines.push(`~ ${inlineCode(symbol.name)} (${symbol.kind})`);
|
|
143
|
+
}
|
|
144
|
+
for (const change of delta.changedSignatures.slice(0, 3)) {
|
|
145
|
+
lines.push(
|
|
146
|
+
`assinatura ${inlineCode(change.name)}: ${inlineCode(shorten(change.before) || "—")} → ${inlineCode(shorten(change.after) || "—")}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (delta.addedImports.length > 0) {
|
|
150
|
+
lines.push(
|
|
151
|
+
`imports + ${delta.addedImports.slice(0, 4).map(inlineCode).join(", ")}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (delta.removedImports.length > 0) {
|
|
155
|
+
lines.push(
|
|
156
|
+
`imports − ${delta.removedImports.slice(0, 4).map(inlineCode).join(", ")}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Plano de revisão sugerido por IA — complementa (nunca substitui) as regras.
|
|
164
|
+
*/
|
|
165
|
+
function aiPlanSection(args: WatchdogRenderArgs): string[] {
|
|
166
|
+
const plan = args.aiPlan;
|
|
167
|
+
if (!plan) return [];
|
|
168
|
+
const focus = plan.focus ?? [];
|
|
169
|
+
const questions = plan.questions ?? [];
|
|
170
|
+
const risks = plan.risks ?? [];
|
|
171
|
+
const logic = plan.logic ?? [];
|
|
172
|
+
const domain = plan.domain ?? [];
|
|
173
|
+
if (
|
|
174
|
+
focus.length === 0 &&
|
|
175
|
+
questions.length === 0 &&
|
|
176
|
+
risks.length === 0 &&
|
|
177
|
+
logic.length === 0 &&
|
|
178
|
+
domain.length === 0
|
|
179
|
+
) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const lines = [
|
|
183
|
+
"## Plano de revisão (IA) — sugestão",
|
|
184
|
+
"",
|
|
185
|
+
"> Sugestão automática para orientar a leitura. **Não** substitui as regras",
|
|
186
|
+
"> do watchdog nem dispensa confirmar cada violação no código.",
|
|
187
|
+
"",
|
|
188
|
+
];
|
|
189
|
+
if (focus.length > 0) {
|
|
190
|
+
lines.push("**Foco de leitura:**", "");
|
|
191
|
+
for (const item of focus.slice(0, 5)) lines.push(`- ${item}`);
|
|
192
|
+
lines.push("");
|
|
193
|
+
}
|
|
194
|
+
if (questions.length > 0) {
|
|
195
|
+
lines.push("**Perguntas de verificação:**", "");
|
|
196
|
+
for (const item of questions.slice(0, 3)) lines.push(`- ${item}`);
|
|
197
|
+
lines.push("");
|
|
198
|
+
}
|
|
199
|
+
if (risks.length > 0) {
|
|
200
|
+
lines.push("**Riscos prováveis:**", "");
|
|
201
|
+
for (const item of risks.slice(0, 3)) lines.push(`- ${item}`);
|
|
202
|
+
lines.push("");
|
|
203
|
+
}
|
|
204
|
+
if (logic.length > 0) {
|
|
205
|
+
lines.push("**Consistência lógica:**", "");
|
|
206
|
+
for (const item of logic.slice(0, 3)) lines.push(`- ${item}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
if (domain.length > 0) {
|
|
210
|
+
lines.push("**Domínio de negócio:**", "");
|
|
211
|
+
for (const item of domain.slice(0, 3)) lines.push(`- ${item}`);
|
|
212
|
+
lines.push("");
|
|
213
|
+
}
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Estrutura/delta dos arquivos do watchdog (AST). É um mapa para orientar a
|
|
219
|
+
* leitura — o watchdog confirma no código e não trata como achado.
|
|
220
|
+
*/
|
|
221
|
+
function astSection(args: WatchdogRenderArgs): string[] {
|
|
222
|
+
const entries = args.ast ?? [];
|
|
223
|
+
if (entries.length === 0) return [];
|
|
224
|
+
const lines: string[] = [
|
|
225
|
+
"## Estrutura (AST)",
|
|
226
|
+
"",
|
|
227
|
+
"> Delta estrutural dos arquivos do seu domínio. Use como mapa para",
|
|
228
|
+
"> priorizar a leitura; **não** é achado por si só.",
|
|
229
|
+
"",
|
|
230
|
+
];
|
|
231
|
+
let rendered = 0;
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
const parts = deltaLines(entry.delta);
|
|
234
|
+
if (entry.importers && entry.importers.length > 0) {
|
|
235
|
+
parts.push(
|
|
236
|
+
`impacto: ${entry.importers.slice(0, 5).map(inlineCode).join(", ")}` +
|
|
237
|
+
(entry.importers.length > 5
|
|
238
|
+
? ` (+${entry.importers.length - 5})`
|
|
239
|
+
: ""),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (parts.length === 0) continue;
|
|
243
|
+
lines.push(
|
|
244
|
+
`### ${inlineCode(entry.file)}`,
|
|
245
|
+
"",
|
|
246
|
+
...parts.map((part) => `- ${part}`),
|
|
247
|
+
"",
|
|
248
|
+
);
|
|
249
|
+
rendered++;
|
|
250
|
+
if (rendered >= 12) break;
|
|
251
|
+
}
|
|
252
|
+
if (rendered === 0) return [];
|
|
253
|
+
return lines;
|
|
254
|
+
}
|
|
255
|
+
|
|
120
256
|
/**
|
|
121
257
|
* Render padrão do brief de um watchdog (ordem estável).
|
|
122
258
|
* `def` é a definição do próprio watchdog (evita ciclo com o registry).
|
|
@@ -139,6 +275,8 @@ export function defaultRenderBrief(
|
|
|
139
275
|
|
|
140
276
|
if (args.context) lines.push(...renderBriefContext(args.context));
|
|
141
277
|
|
|
278
|
+
lines.push(...aiPlanSection(args));
|
|
279
|
+
lines.push(...astSection(args));
|
|
142
280
|
lines.push(...optionalList("Prioridade de leitura", def.reviewFocus));
|
|
143
281
|
lines.push(
|
|
144
282
|
"## Regras (por severidade)",
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* com o UUID interno no banco.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { AiClient } from "../ai/ai-client";
|
|
14
|
+
import { AiVerifier } from "../ai/features/verifier";
|
|
13
15
|
import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
|
|
16
|
+
import { ENV_NAMES } from "../env";
|
|
14
17
|
import {
|
|
15
18
|
applyFindingUpdate,
|
|
16
19
|
FINDING_DECISIONS,
|
|
@@ -36,9 +39,11 @@ import {
|
|
|
36
39
|
type SubmitPreview,
|
|
37
40
|
type SubmitSelection,
|
|
38
41
|
submitFindings,
|
|
42
|
+
syncResolvedThreads,
|
|
39
43
|
} from "../github-review";
|
|
40
44
|
import { VERIFY_FILE } from "../paths";
|
|
41
45
|
import { currentBranch } from "../pr-context";
|
|
46
|
+
import { ProjectConfig } from "../project-config";
|
|
42
47
|
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
43
48
|
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
44
49
|
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
@@ -214,6 +219,25 @@ function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
|
|
|
214
219
|
pingFindingsStatus(ctx.storage, ctx.review.id, state);
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Resolve no GitHub as threads dos findings recém-resolvidos localmente.
|
|
224
|
+
* Fail-open (`gh` ausente/sem PR ⇒ só a mensagem). Devolve a linha para o
|
|
225
|
+
* output ou `null` quando não há vínculo remoto pendente.
|
|
226
|
+
*/
|
|
227
|
+
function syncThreadsOutput(ctx: CommandContext, ids: readonly string[]): string | null {
|
|
228
|
+
if (ids.length === 0) return null;
|
|
229
|
+
const state = loadState(ctx);
|
|
230
|
+
const outcome = syncResolvedThreads({ state, ids });
|
|
231
|
+
if (outcome.checked === 0) return null;
|
|
232
|
+
if (outcome.updated.length > 0) ctx.storage.writeFindings(ctx.review.id, state);
|
|
233
|
+
if (!outcome.available) {
|
|
234
|
+
return `GitHub: threads não sincronizadas (${outcome.reason}).`;
|
|
235
|
+
}
|
|
236
|
+
const failures =
|
|
237
|
+
outcome.failed.length > 0 ? `, ${outcome.failed.length} falha(s)` : "";
|
|
238
|
+
return `GitHub: ${outcome.resolved.length} thread(s) resolvida(s), ${outcome.skipped.length} já resolvida(s)/pulada(s)${failures}.`;
|
|
239
|
+
}
|
|
240
|
+
|
|
217
241
|
/** Aplica uma atualização a um finding e persiste. */
|
|
218
242
|
function updateOne(
|
|
219
243
|
ctx: CommandContext,
|
|
@@ -309,10 +333,17 @@ function execute(
|
|
|
309
333
|
}
|
|
310
334
|
const finding = updateOne(ctx, input, update);
|
|
311
335
|
pingStatus(ctx, loadState(ctx));
|
|
336
|
+
const threads =
|
|
337
|
+
update.execState === "done" ? syncThreadsOutput(ctx, [finding.id]) : null;
|
|
312
338
|
return {
|
|
313
339
|
output: parsed.bools.has("--json")
|
|
314
340
|
? JSON.stringify(finding, null, 2)
|
|
315
|
-
:
|
|
341
|
+
: [
|
|
342
|
+
`Atualizado: \`${finding.id}\` → ${finding.decision}/${finding.execState}.`,
|
|
343
|
+
threads,
|
|
344
|
+
]
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
.join("\n"),
|
|
316
347
|
exitCode: 0,
|
|
317
348
|
};
|
|
318
349
|
}
|
|
@@ -320,8 +351,17 @@ function execute(
|
|
|
320
351
|
const ids = requireIds(parsed, "done");
|
|
321
352
|
const updated = updateMany(ctx, ids, { execState: "done" });
|
|
322
353
|
pingStatus(ctx, loadState(ctx));
|
|
354
|
+
const threads = syncThreadsOutput(
|
|
355
|
+
ctx,
|
|
356
|
+
updated.map((finding) => finding.id),
|
|
357
|
+
);
|
|
323
358
|
return {
|
|
324
|
-
output:
|
|
359
|
+
output: [
|
|
360
|
+
`Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
|
|
361
|
+
threads,
|
|
362
|
+
]
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
.join("\n"),
|
|
325
365
|
exitCode: 0,
|
|
326
366
|
};
|
|
327
367
|
}
|
|
@@ -449,6 +489,20 @@ export async function runVerifyCommand(
|
|
|
449
489
|
}
|
|
450
490
|
}
|
|
451
491
|
|
|
492
|
+
/** Trecho de ~20 linhas ao redor do finding (entrada da verificação por IA). */
|
|
493
|
+
function snippetsAround(content: string | null, line: number | null): string {
|
|
494
|
+
if (!content) return "(arquivo indisponível)";
|
|
495
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
496
|
+
const center = line && line > 0 ? line : 1;
|
|
497
|
+
const start = Math.max(1, center - 10);
|
|
498
|
+
const end = Math.min(lines.length, center + 10);
|
|
499
|
+
return lines
|
|
500
|
+
.slice(start - 1, end)
|
|
501
|
+
.map((text, index) => `${start + index}: ${text}`)
|
|
502
|
+
.join("\n")
|
|
503
|
+
.slice(0, 4_000);
|
|
504
|
+
}
|
|
505
|
+
|
|
452
506
|
async function executeVerify(
|
|
453
507
|
config: HoundsmanConfig,
|
|
454
508
|
storage: StorageProvider,
|
|
@@ -474,7 +528,7 @@ async function executeVerify(
|
|
|
474
528
|
if (parsed.bools.has("--no-jev")) {
|
|
475
529
|
jevReason = "desativado (--no-jev)";
|
|
476
530
|
} else if (!jevVerifier.isConfigured()) {
|
|
477
|
-
jevReason =
|
|
531
|
+
jevReason = `${ENV_NAMES.typeSafeApiKey} não configurada`;
|
|
478
532
|
} else {
|
|
479
533
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
480
534
|
const outcome = await jevVerifier.verify(
|
|
@@ -495,6 +549,44 @@ async function executeVerify(
|
|
|
495
549
|
}
|
|
496
550
|
}
|
|
497
551
|
|
|
552
|
+
// Tier 2 — IA opcional para os ambíguos que o Jev não respondeu.
|
|
553
|
+
let aiChecked = 0;
|
|
554
|
+
let aiReason: string | undefined;
|
|
555
|
+
const stillAmbiguous = entries.filter(
|
|
556
|
+
(entry) => entry.ambiguous && entry.basis === "anchor",
|
|
557
|
+
);
|
|
558
|
+
if (stillAmbiguous.length > 0) {
|
|
559
|
+
const client = new AiClient({ project: new ProjectConfig(config) });
|
|
560
|
+
const aiVerifier = new AiVerifier(client);
|
|
561
|
+
if (parsed.bools.has("--no-ai")) {
|
|
562
|
+
aiReason = "desativado (--no-ai)";
|
|
563
|
+
} else if (!client.available) {
|
|
564
|
+
aiReason = "IA não configurada";
|
|
565
|
+
} else if (!aiVerifier.enabled) {
|
|
566
|
+
aiReason = "feature ai.features.verify desligada";
|
|
567
|
+
} else {
|
|
568
|
+
const readFile = fileReader(process.cwd());
|
|
569
|
+
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
570
|
+
const confidence = await aiVerifier.verify({
|
|
571
|
+
findings: stillAmbiguous.map((entry) => ({
|
|
572
|
+
id: entry.id,
|
|
573
|
+
rule: entry.rule,
|
|
574
|
+
file: entry.file,
|
|
575
|
+
line: entry.line,
|
|
576
|
+
message: byId.get(entry.id)?.message ?? entry.message,
|
|
577
|
+
snippets: snippetsAround(readFile(entry.file), entry.line),
|
|
578
|
+
})),
|
|
579
|
+
});
|
|
580
|
+
aiChecked = verifier.applyConfidence(
|
|
581
|
+
entries,
|
|
582
|
+
confidence,
|
|
583
|
+
JevCandidatesStore.THRESHOLD,
|
|
584
|
+
"ai",
|
|
585
|
+
);
|
|
586
|
+
aiReason = `${aiChecked} caso(s) confirmado(s) pela IA`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
498
590
|
const summary = verifier.summarize(entries);
|
|
499
591
|
const location = ctx.storage.location(ctx.review.id);
|
|
500
592
|
ctx.storage.writeArtifact(ctx.review.id, {
|
|
@@ -511,6 +603,9 @@ async function executeVerify(
|
|
|
511
603
|
pingStatus(ctx, current);
|
|
512
604
|
}
|
|
513
605
|
|
|
606
|
+
// Resolvidos localmente ⇒ resolve as conversas correspondentes no PR.
|
|
607
|
+
const threads = syncThreadsOutput(ctx, applied);
|
|
608
|
+
|
|
514
609
|
const output = parsed.bools.has("--json")
|
|
515
610
|
? JSON.stringify(
|
|
516
611
|
{
|
|
@@ -518,6 +613,8 @@ async function executeVerify(
|
|
|
518
613
|
summary,
|
|
519
614
|
jevChecked,
|
|
520
615
|
jevReason,
|
|
616
|
+
aiChecked,
|
|
617
|
+
aiReason,
|
|
521
618
|
applied,
|
|
522
619
|
entries,
|
|
523
620
|
},
|
|
@@ -534,6 +631,7 @@ async function executeVerify(
|
|
|
534
631
|
parsed.bools.has("--apply")
|
|
535
632
|
? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
|
|
536
633
|
: "",
|
|
634
|
+
threads,
|
|
537
635
|
]
|
|
538
636
|
.filter(Boolean)
|
|
539
637
|
.join("\n");
|
|
@@ -603,6 +701,21 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
603
701
|
"",
|
|
604
702
|
`Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
|
|
605
703
|
];
|
|
704
|
+
const review = outcome.review;
|
|
705
|
+
if (review?.id !== undefined) {
|
|
706
|
+
const target = review.url ? ` — ${review.url}` : "";
|
|
707
|
+
if (review.created) {
|
|
708
|
+
const status = review.finalized
|
|
709
|
+
? `finalizada (${review.event})`
|
|
710
|
+
: "criada, mas não finalizada";
|
|
711
|
+
lines.push(`Revisão #${review.id} ${status}${target}`);
|
|
712
|
+
} else {
|
|
713
|
+
lines.push(
|
|
714
|
+
`Comentários adicionados à revisão pendente #${review.id}${target}`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (review?.error) lines.push(`Revisão: ❌ ${review.error}`);
|
|
606
719
|
if (outcome.general) {
|
|
607
720
|
lines.push(
|
|
608
721
|
`Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
|
|
@@ -615,8 +728,9 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
615
728
|
}
|
|
616
729
|
|
|
617
730
|
/**
|
|
618
|
-
* Subcomando `findings submit` — envia os findings selecionados
|
|
619
|
-
*
|
|
731
|
+
* Subcomando `findings submit` — envia os findings selecionados numa revisão da
|
|
732
|
+
* PR (via `gh`): reaproveita a revisão pendente ou cria/finaliza uma nova.
|
|
733
|
+
* `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
|
|
620
734
|
*/
|
|
621
735
|
export async function runSubmitCommand(
|
|
622
736
|
config: HoundsmanConfig,
|
|
@@ -664,6 +778,6 @@ async function executeSubmit(
|
|
|
664
778
|
|
|
665
779
|
return {
|
|
666
780
|
output: renderSubmitOutcome(outcome, json),
|
|
667
|
-
exitCode: outcome.failed.length > 0 ? 1 : 0,
|
|
781
|
+
exitCode: outcome.failed.length > 0 || Boolean(outcome.review?.error) ? 1 : 0,
|
|
668
782
|
};
|
|
669
783
|
}
|
|
@@ -28,7 +28,9 @@ export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
|
28
28
|
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
29
29
|
if (parser.has("--clean")) opts.clean = true;
|
|
30
30
|
if (parser.has("--no-jev")) opts.noJev = true;
|
|
31
|
+
if (parser.has("--no-ast")) opts.noAst = true;
|
|
31
32
|
if (parser.has("--full")) opts.full = true;
|
|
33
|
+
if (parser.has("--no-ai")) opts.noAi = true;
|
|
32
34
|
|
|
33
35
|
return opts;
|
|
34
36
|
}
|
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
* ├── .gitignore AUTO — ignora core/ (gerado)
|
|
9
9
|
* ├── core/types.ts AUTO — snapshot dos tipos + helpers
|
|
10
10
|
* ├── core/skill/*.md AUTO — docs do skill (com {{projectName}} resolvido)
|
|
11
|
+
* ├── core/grammars/*.wasm AUTO — runtime + grammars do AST
|
|
11
12
|
* ├── watchdogs/*.ts USUÁRIO
|
|
12
13
|
* ├── doc-mappings.ts USUÁRIO
|
|
13
14
|
* └── exclusions.ts USUÁRIO
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
copyFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
17
25
|
import { basename, join } from "node:path";
|
|
18
|
-
import { SKILL_DIR, SKILL_FILE } from "../assets";
|
|
26
|
+
import { GRAMMAR_FILES, grammarPath, SKILL_DIR, SKILL_FILE } from "../assets";
|
|
19
27
|
import { CORE_DIR, CORE_SKILL_DIR, OUTPUT_FORMAT_PATH, PROJECT_DIR } from "../paths";
|
|
20
28
|
|
|
21
29
|
export type Harness = "agents" | "claude";
|
|
@@ -123,11 +131,21 @@ export type Rule = {
|
|
|
123
131
|
severity: Severity;
|
|
124
132
|
text: string;
|
|
125
133
|
jev?: JevRuleSpec;
|
|
134
|
+
hints?: RuleHints;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Sinais opcionais de uma regra (símbolos, imports, decorators, paths, checks). */
|
|
138
|
+
export type RuleHints = {
|
|
139
|
+
symbols?: string[];
|
|
140
|
+
imports?: string[];
|
|
141
|
+
decorators?: string[];
|
|
142
|
+
paths?: string[];
|
|
143
|
+
checks?: string[];
|
|
126
144
|
};
|
|
127
145
|
|
|
128
146
|
/** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
|
|
129
147
|
export type ClassifyRule = {
|
|
130
|
-
type?: "filename" | "content";
|
|
148
|
+
type?: "filename" | "content" | "symbol" | "import" | "export" | "decorator";
|
|
131
149
|
value: string;
|
|
132
150
|
};
|
|
133
151
|
|
|
@@ -144,6 +162,10 @@ export type WatchdogDefinition = WatchdogMeta & {
|
|
|
144
162
|
id: string;
|
|
145
163
|
description: string;
|
|
146
164
|
breakingChange?: boolean;
|
|
165
|
+
/** Só assume arquivos que nenhum outro watchdog casou. */
|
|
166
|
+
fallback?: boolean;
|
|
167
|
+
/** Permite findings emergentes (Lógica:/Domínio:) fora das regras. */
|
|
168
|
+
allowEmergentFindings?: boolean;
|
|
147
169
|
reviewFocus?: readonly string[];
|
|
148
170
|
exclude?: readonly string[];
|
|
149
171
|
};
|
|
@@ -182,6 +204,54 @@ export type HoundsmanStorage = {
|
|
|
182
204
|
path?: string;
|
|
183
205
|
};
|
|
184
206
|
|
|
207
|
+
/** Linguagens suportadas pelo AST na primeira fase. */
|
|
208
|
+
export type AstLanguage = "typescript" | "tsx" | "javascript";
|
|
209
|
+
|
|
210
|
+
/** Configuração do AST determinístico (classificação estrutural, brief e impacto). */
|
|
211
|
+
export type HoundsmanAst = {
|
|
212
|
+
enabled?: boolean;
|
|
213
|
+
maxFileBytes?: number;
|
|
214
|
+
maxImpactFiles?: number;
|
|
215
|
+
languages?: Record<string, AstLanguage>;
|
|
216
|
+
aliases?: Record<string, string>;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Providers suportados pela camada de IA. */
|
|
220
|
+
export type AiProviderKind = "deepseek" | "openai" | "anthropic";
|
|
221
|
+
|
|
222
|
+
/** Features de IA do fluxo normal (todas fail-open). */
|
|
223
|
+
export type HoundsmanAiFeatures = {
|
|
224
|
+
context?: boolean;
|
|
225
|
+
brief?: boolean;
|
|
226
|
+
classification?: boolean;
|
|
227
|
+
preAnalysis?: boolean;
|
|
228
|
+
verify?: boolean;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** Overrides de IA por watchdog. */
|
|
232
|
+
export type HoundsmanAiWatchdog = {
|
|
233
|
+
model?: string;
|
|
234
|
+
maxSteps?: number;
|
|
235
|
+
maxOutputTokens?: number;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/** Configuração da camada de IA (briefs, contexto, classificação e hunt nativo). */
|
|
239
|
+
export type HoundsmanAi = {
|
|
240
|
+
enabled?: boolean;
|
|
241
|
+
provider?: AiProviderKind;
|
|
242
|
+
model?: string;
|
|
243
|
+
baseURL?: string;
|
|
244
|
+
concurrency?: number;
|
|
245
|
+
maxSteps?: number;
|
|
246
|
+
maxOutputTokens?: number;
|
|
247
|
+
temperature?: number;
|
|
248
|
+
timeoutMs?: number;
|
|
249
|
+
maxCallsPerReview?: number;
|
|
250
|
+
maxTokensPerReview?: number;
|
|
251
|
+
features?: HoundsmanAiFeatures;
|
|
252
|
+
watchdogs?: Record<string, HoundsmanAiWatchdog>;
|
|
253
|
+
};
|
|
254
|
+
|
|
185
255
|
/** Contrato de configuração do motor. */
|
|
186
256
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
187
257
|
projectName: string;
|
|
@@ -193,6 +263,8 @@ export type HoundsmanConfig<TId extends string = string> = {
|
|
|
193
263
|
alwaysIncludeDocs?: readonly string[];
|
|
194
264
|
budgets?: HoundsmanBudgets;
|
|
195
265
|
concurrency?: HoundsmanConcurrency;
|
|
266
|
+
ast?: HoundsmanAst;
|
|
267
|
+
ai?: HoundsmanAi;
|
|
196
268
|
};
|
|
197
269
|
|
|
198
270
|
/** Helper de autoria (identidade) — apenas tipagem. */
|
|
@@ -212,7 +284,8 @@ const WATCHDOG_EXAMPLE = `import { defineWatchdog } from "../core/types";
|
|
|
212
284
|
|
|
213
285
|
/**
|
|
214
286
|
* Watchdog de exemplo — substitua pelas regras reais do projeto.
|
|
215
|
-
* Um watchdog declara seus arquivos via \`classify\` (regex de path ou
|
|
287
|
+
* Um watchdog declara seus arquivos via \`classify\` (regex de path, conteúdo ou
|
|
288
|
+
* fatos AST: \`symbol\`/\`import\`/\`export\`/\`decorator\`).
|
|
216
289
|
* O texto de cada regra começa com a polaridade: \`Promover:\` (DEVE acontecer)
|
|
217
290
|
* ou \`Evitar:\`/\`Não\`/\`Nunca\` (NÃO DEVE acontecer).
|
|
218
291
|
*/
|
|
@@ -220,8 +293,8 @@ export const exampleWatchdog = defineWatchdog({
|
|
|
220
293
|
id: "example",
|
|
221
294
|
name: "Exemplo",
|
|
222
295
|
description: "Watchdog de exemplo — descreva o propósito em uma frase.",
|
|
223
|
-
scope: "
|
|
224
|
-
classify: [{ value: "^src/" }],
|
|
296
|
+
scope: "src/example/** (ajuste o escopo)",
|
|
297
|
+
classify: [{ value: "^src/example/" }],
|
|
225
298
|
rules: [
|
|
226
299
|
{
|
|
227
300
|
severity: "attention",
|
|
@@ -237,13 +310,65 @@ export const exampleWatchdog = defineWatchdog({
|
|
|
237
310
|
});
|
|
238
311
|
`;
|
|
239
312
|
|
|
240
|
-
const
|
|
313
|
+
const DEFAULT_WATCHDOG = `import { defineWatchdog } from "../core/types";
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Watchdog de padrões gerais e lógica (fallback).
|
|
317
|
+
*
|
|
318
|
+
* Só assume arquivos que nenhum outro watchdog casou. Cobre consistência
|
|
319
|
+
* lógica, invariantes de domínio e defeitos genéricos que não pertencem a um
|
|
320
|
+
* domínio específico. Findings fora das regras abaixo podem ser reportados com
|
|
321
|
+
* os prefixos reservados "Lógica:" e "Domínio:" (opt-in abaixo).
|
|
322
|
+
*/
|
|
323
|
+
export const defaultWatchdog = defineWatchdog({
|
|
324
|
+
id: "default",
|
|
325
|
+
name: "Padrões gerais e lógica",
|
|
326
|
+
description:
|
|
327
|
+
"Cobre arquivos sem domínio específico: consistência lógica, domínio e defeitos genéricos.",
|
|
328
|
+
scope: "Arquivos não classificados por outros watchdogs",
|
|
329
|
+
fallback: true,
|
|
330
|
+
allowEmergentFindings: true,
|
|
331
|
+
rules: [
|
|
332
|
+
{
|
|
333
|
+
severity: "attention",
|
|
334
|
+
text: "Promover: nome, contrato e documentação do símbolo correspondem ao comportamento real.",
|
|
335
|
+
hints: { checks: ["symbol_context"] },
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
severity: "attention",
|
|
339
|
+
text: "Evitar: engolir erros/exceções sem propagar, logar ou tratar.",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
severity: "attention",
|
|
343
|
+
text: "Promover: transições de estado e efeitos colaterais do domínio são explícitos, ordenados e idempotentes.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
severity: "suggestion",
|
|
347
|
+
text: "Evitar: lógica de negócio duplicada ou divergente entre camadas.",
|
|
348
|
+
hints: { checks: ["find_references"] },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
severity: "suggestion",
|
|
352
|
+
text: "Promover: casos de borda do domínio (vazio, zero, negativo, timezone, arredondamento) são tratados.",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
severity: "suggestion",
|
|
356
|
+
text: "Promover: a lógica do domínio respeita as invariantes e contratos documentados.",
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
contextFiles: ["AGENTS.md"],
|
|
360
|
+
});
|
|
361
|
+
`;
|
|
362
|
+
|
|
363
|
+
const WATCHDOGS_INDEX = `import { defaultWatchdog } from "./default";
|
|
364
|
+
import { exampleWatchdog } from "./example";
|
|
241
365
|
|
|
242
366
|
/**
|
|
243
367
|
* Definições dos watchdogs — as chaves definem o union \`WatchdogId\`.
|
|
244
368
|
* Adicionar um watchdog = criar \`<id>.ts\` e registrá-lo aqui.
|
|
245
369
|
*/
|
|
246
370
|
export const WATCHDOG_DEFINITIONS = {
|
|
371
|
+
default: defaultWatchdog,
|
|
247
372
|
example: exampleWatchdog,
|
|
248
373
|
};
|
|
249
374
|
|
|
@@ -267,9 +392,10 @@ export function isExcluded(filePath: string): boolean {
|
|
|
267
392
|
const DOC_MAPPINGS_TEMPLATE = `/**
|
|
268
393
|
* Mapeamento de padrões nos paths → documentação relevante.
|
|
269
394
|
* Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
395
|
+
*
|
|
396
|
+
* Exemplo: [/^src\\//, ["README.md"]]
|
|
397
|
+
*/
|
|
398
|
+
export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [];
|
|
273
399
|
`;
|
|
274
400
|
|
|
275
401
|
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`
|
|
@@ -307,6 +433,14 @@ class WatchdogExampleTemplate implements ScaffoldTemplate {
|
|
|
307
433
|
}
|
|
308
434
|
}
|
|
309
435
|
|
|
436
|
+
class DefaultWatchdogTemplate implements ScaffoldTemplate {
|
|
437
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "default.ts");
|
|
438
|
+
|
|
439
|
+
render(): string {
|
|
440
|
+
return DEFAULT_WATCHDOG;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
310
444
|
class WatchdogsIndexTemplate implements ScaffoldTemplate {
|
|
311
445
|
readonly path = join(PROJECT_DIR, "watchdogs", "index.ts");
|
|
312
446
|
|
|
@@ -402,6 +536,7 @@ export class ProjectScaffolder {
|
|
|
402
536
|
const context = this.context();
|
|
403
537
|
const templates: ScaffoldTemplate[] = [
|
|
404
538
|
new ConfigTemplate(),
|
|
539
|
+
new DefaultWatchdogTemplate(),
|
|
405
540
|
new WatchdogExampleTemplate(),
|
|
406
541
|
new WatchdogsIndexTemplate(),
|
|
407
542
|
new ExclusionsTemplate(),
|
|
@@ -415,6 +550,7 @@ export class ProjectScaffolder {
|
|
|
415
550
|
const context = this.context();
|
|
416
551
|
this.writeGeneratedFile(new TypesTemplate(), context);
|
|
417
552
|
this.writeGeneratedFile(new GitignoreTemplate(), context);
|
|
553
|
+
this.syncGrammars();
|
|
418
554
|
|
|
419
555
|
// Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry
|
|
420
556
|
// point dos harnesses, não um doc da camada.
|
|
@@ -428,6 +564,19 @@ export class ProjectScaffolder {
|
|
|
428
564
|
}
|
|
429
565
|
}
|
|
430
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Copia as grammars wasm para a camada. O consumidor não precisa rodar nada:
|
|
569
|
+
* `setup` e `update` sincronizam os binários junto dos demais auto-gerados.
|
|
570
|
+
*/
|
|
571
|
+
private syncGrammars(): void {
|
|
572
|
+
const directory = join(this.ctx.cwd, CORE_DIR, "grammars");
|
|
573
|
+
mkdirSync(directory, { recursive: true });
|
|
574
|
+
for (const name of GRAMMAR_FILES) {
|
|
575
|
+
copyFileSync(grammarPath(name), join(directory, name));
|
|
576
|
+
}
|
|
577
|
+
this.ctx.generated.push(join(CORE_DIR, "grammars", "*.wasm"));
|
|
578
|
+
}
|
|
579
|
+
|
|
431
580
|
private context(): TemplateContext {
|
|
432
581
|
return { projectName: this.ctx.projectName };
|
|
433
582
|
}
|