@precisa-saude/cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +640 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/index.d.ts +86 -0
  7. package/dist/index.js +630 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/templates/README.md +51 -0
  10. package/dist/templates/dotfiles/editorconfig +12 -0
  11. package/dist/templates/dotfiles/gitignore +10 -0
  12. package/dist/templates/dotfiles/npmrc +1 -0
  13. package/dist/templates/dotfiles/nvmrc +1 -0
  14. package/dist/templates/dotfiles/prettierignore +7 -0
  15. package/dist/templates/dotfiles/prettierrc +1 -0
  16. package/dist/templates/dotfiles/renovate.json +29 -0
  17. package/dist/templates/github/ISSUE_TEMPLATE/bug.md +33 -0
  18. package/dist/templates/github/ISSUE_TEMPLATE/config.yml +8 -0
  19. package/dist/templates/github/ISSUE_TEMPLATE/feature.md +18 -0
  20. package/dist/templates/github/PULL_REQUEST_TEMPLATE.md +18 -0
  21. package/dist/templates/github/workflows/ci.yml +44 -0
  22. package/dist/templates/github/workflows/release.yml +62 -0
  23. package/dist/templates/github/workflows/review.yml +435 -0
  24. package/dist/templates/governance/CITATION.cff +8 -0
  25. package/dist/templates/governance/CODE_OF_CONDUCT.md +43 -0
  26. package/dist/templates/governance/CONTRIBUTING.md +32 -0
  27. package/dist/templates/governance/CONVENTIONS.md +37 -0
  28. package/dist/templates/governance/SECURITY.md +22 -0
  29. package/dist/templates/governance/SUPPORT.md +14 -0
  30. package/dist/templates/husky/commit-msg +8 -0
  31. package/dist/templates/husky/pre-commit +1 -0
  32. package/dist/templates/husky/pre-push +21 -0
  33. package/dist/templates/templates.manifest.yml +125 -0
  34. package/package.json +62 -0
@@ -0,0 +1,435 @@
1
+ name: Code Review
2
+
3
+ # Tries providers in order (Anthropic → OpenAI) and uses the first one that
4
+ # returns a 200. Any non-200 from a provider triggers fallback to the next.
5
+ # If all configured providers fail (or none are configured), the workflow
6
+ # exits without posting a review and without emitting a round marker, so
7
+ # the next PR activity retries fresh.
8
+
9
+ on:
10
+ pull_request:
11
+ types: [opened, synchronize, reopened]
12
+
13
+ concurrency:
14
+ group: review-${{ github.event.pull_request.number }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ check-review-status:
19
+ name: Check review status
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 2
22
+ permissions:
23
+ pull-requests: read
24
+ issues: read
25
+ outputs:
26
+ should_run: ${{ steps.check.outputs.should_run }}
27
+ review_round: ${{ steps.check.outputs.review_round }}
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Decide whether to run
32
+ id: check
33
+ uses: actions/github-script@v8
34
+ with:
35
+ github-token: ${{ secrets.GITHUB_TOKEN }}
36
+ script: |
37
+ const prNumber = context.issue.number;
38
+
39
+ // Skip docs/config-only PRs
40
+ const { data: files } = await github.rest.pulls.listFiles({
41
+ owner: context.repo.owner,
42
+ repo: context.repo.repo,
43
+ pull_number: prNumber,
44
+ per_page: 100,
45
+ });
46
+ const ignore = [/\.md$/, /^docs\//, /^\.github\//, /\.json$/, /\.ya?ml$/, /\.cff$/];
47
+ const hasCode = files.some(f => !ignore.some(p => p.test(f.filename)));
48
+ if (!hasCode) {
49
+ core.setOutput('should_run', 'false');
50
+ core.setOutput('review_round', 'skip');
51
+ return;
52
+ }
53
+
54
+ const { data: comments } = await github.rest.issues.listComments({
55
+ owner: context.repo.owner,
56
+ repo: context.repo.repo,
57
+ issue_number: prNumber,
58
+ per_page: 100,
59
+ });
60
+ const round1 = comments.some(c => c.body?.includes('<!-- AUTOMATED_REVIEW_ROUND_1 -->'));
61
+ const round2 = comments.some(c => c.body?.includes('<!-- AUTOMATED_REVIEW_ROUND_2_COMPLETE -->'));
62
+
63
+ if (round2) {
64
+ core.setOutput('should_run', 'false');
65
+ core.setOutput('review_round', 'complete');
66
+ return;
67
+ }
68
+ core.setOutput('should_run', 'true');
69
+ core.setOutput('review_round', round1 ? '2' : '1');
70
+
71
+ review:
72
+ name: Automated review
73
+ needs: check-review-status
74
+ if: needs.check-review-status.outputs.should_run == 'true'
75
+ runs-on: ubuntu-latest
76
+ timeout-minutes: 10
77
+ continue-on-error: true
78
+ permissions:
79
+ contents: read
80
+ pull-requests: write
81
+ env:
82
+ ANTHROPIC_MODEL: claude-haiku-4-5-20251001
83
+ OPENAI_MODEL: gpt-4o-mini
84
+ MAX_DIFF_SIZE: '150000'
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+ with:
88
+ fetch-depth: 0
89
+
90
+ - name: Build review input
91
+ id: pr
92
+ env:
93
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94
+ REVIEW_ROUND: ${{ needs.check-review-status.outputs.review_round }}
95
+ run: |
96
+ PR_NUMBER=${{ github.event.pull_request.number }}
97
+ HEAD_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid -q .headRefOid)
98
+ echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT
99
+ echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT
100
+
101
+ if [ "$REVIEW_ROUND" = "2" ]; then
102
+ FIRST=$(gh pr view "$PR_NUMBER" --json commits -q '.commits[0].oid')
103
+ git diff "$FIRST"..HEAD > pr.diff
104
+ else
105
+ gh pr diff "$PR_NUMBER" > pr.diff
106
+ fi
107
+
108
+ gh pr view "$PR_NUMBER" --json files -q '.files[].path' > changed_files.txt
109
+
110
+ DIFF_SIZE=$(wc -c < pr.diff)
111
+ if [ "$DIFF_SIZE" -gt "$MAX_DIFF_SIZE" ]; then
112
+ head -c "$MAX_DIFF_SIZE" pr.diff > pr.diff.tmp && mv pr.diff.tmp pr.diff
113
+ echo "diff_truncated=true" >> $GITHUB_OUTPUT
114
+ else
115
+ echo "diff_truncated=false" >> $GITHUB_OUTPUT
116
+ fi
117
+
118
+ - name: Call review API (with provider failover)
119
+ id: call
120
+ env:
121
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
122
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
123
+ DIFF_TRUNCATED: ${{ steps.pr.outputs.diff_truncated }}
124
+ REVIEW_ROUND: ${{ needs.check-review-status.outputs.review_round }}
125
+ run: |
126
+ node << 'SCRIPT'
127
+ const fs = require('fs');
128
+
129
+ const diff = fs.readFileSync('pr.diff', 'utf8');
130
+ const files = fs.readFileSync('changed_files.txt', 'utf8').trim();
131
+ const round = process.env.REVIEW_ROUND;
132
+ const truncated = process.env.DIFF_TRUNCATED === 'true';
133
+
134
+ const system = [
135
+ 'You are a senior software engineer performing a code review.',
136
+ 'Analyze the PR diff carefully and provide actionable, specific feedback.',
137
+ 'Focus on bugs, security issues, performance problems, and significant code-quality concerns.',
138
+ 'Do not include nitpicks, style preferences, or suggestions to add comments.',
139
+ ].join(' ');
140
+
141
+ const user = [
142
+ `Review Round: ${round}`,
143
+ '',
144
+ '## Files changed in this PR',
145
+ 'These are the ONLY files modified. Do NOT reference any other files in feedback:',
146
+ files,
147
+ '',
148
+ truncated ? '> **Note**: Diff truncated due to size. Review only what is shown below.\n' : '',
149
+ '## Diff',
150
+ '',
151
+ '```diff',
152
+ diff,
153
+ '```',
154
+ '',
155
+ 'Respond with ONLY a valid JSON object (no markdown fences). Use this exact structure:',
156
+ '{"summary": "...", "changes": ["..."], "feedback": [{"file": "path", "line": 42, "comment": "..."}]}',
157
+ '',
158
+ 'Rules:',
159
+ '- The "file" field MUST be one of the files listed above.',
160
+ '- The "line" field MUST reference a line number from the NEW version (right side of diff, + lines). Cross-check against @@ hunk headers.',
161
+ '- Include feedback only for real issues; skip nits.',
162
+ '- Do NOT hallucinate code not visible in the diff.',
163
+ '- If no issues, return an empty feedback array.',
164
+ ].join('\n');
165
+
166
+ // Per-1M-token USD pricing. Update when providers change rates.
167
+ // Missing entries silently skip cost display (tokens still shown).
168
+ const PRICING = {
169
+ 'claude-haiku-4-5-20251001': { in: 1.00, out: 5.00 },
170
+ 'claude-sonnet-4-6': { in: 3.00, out: 15.00 },
171
+ 'claude-opus-4-7': { in: 15.00, out: 75.00 },
172
+ 'gpt-4o-mini': { in: 0.15, out: 0.60 },
173
+ 'gpt-4o': { in: 2.50, out: 10.00 },
174
+ 'gpt-5-mini': { in: 0.25, out: 2.00 },
175
+ };
176
+
177
+ function computeCost(model, promptTokens, completionTokens) {
178
+ const rate = PRICING[model];
179
+ if (!rate) return null;
180
+ return (promptTokens / 1_000_000) * rate.in + (completionTokens / 1_000_000) * rate.out;
181
+ }
182
+
183
+ const providers = {
184
+ anthropic: {
185
+ label: 'Anthropic',
186
+ envKey: 'ANTHROPIC_API_KEY',
187
+ modelEnv: 'ANTHROPIC_MODEL',
188
+ defaultModel: 'claude-haiku-4-5-20251001',
189
+ build: (model, key) => ({
190
+ url: 'https://api.anthropic.com/v1/messages',
191
+ headers: {
192
+ 'content-type': 'application/json',
193
+ 'x-api-key': key,
194
+ 'anthropic-version': '2023-06-01',
195
+ },
196
+ body: JSON.stringify({
197
+ model,
198
+ max_tokens: 8192,
199
+ system,
200
+ messages: [{ role: 'user', content: user }],
201
+ }),
202
+ }),
203
+ extractText: (r) => r?.content?.[0]?.text ?? '',
204
+ extractUsage: (r) => ({
205
+ prompt_tokens: r?.usage?.input_tokens ?? 0,
206
+ completion_tokens: r?.usage?.output_tokens ?? 0,
207
+ }),
208
+ extractError: (r) => r?.error?.message ?? r?.message ?? 'see workflow logs',
209
+ },
210
+ openai: {
211
+ label: 'OpenAI',
212
+ envKey: 'OPENAI_API_KEY',
213
+ modelEnv: 'OPENAI_MODEL',
214
+ defaultModel: 'gpt-4o-mini',
215
+ build: (model, key) => ({
216
+ url: 'https://api.openai.com/v1/chat/completions',
217
+ headers: {
218
+ 'content-type': 'application/json',
219
+ Authorization: `Bearer ${key}`,
220
+ },
221
+ body: JSON.stringify({
222
+ model,
223
+ max_tokens: 8192,
224
+ messages: [
225
+ { role: 'system', content: system },
226
+ { role: 'user', content: user },
227
+ ],
228
+ response_format: { type: 'json_object' },
229
+ }),
230
+ }),
231
+ extractText: (r) => r?.choices?.[0]?.message?.content ?? '',
232
+ extractUsage: (r) => ({
233
+ prompt_tokens: r?.usage?.prompt_tokens ?? 0,
234
+ completion_tokens: r?.usage?.completion_tokens ?? 0,
235
+ }),
236
+ extractError: (r) => r?.error?.message ?? 'see workflow logs',
237
+ },
238
+ };
239
+
240
+ // Provider order: try Anthropic, fall back to OpenAI.
241
+ const order = ['anthropic', 'openai'];
242
+
243
+ const setOutput = (k, v) => {
244
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `${k}<<EOF\n${String(v)}\nEOF\n`);
245
+ };
246
+
247
+ async function tryProvider(name) {
248
+ const p = providers[name];
249
+ const key = process.env[p.envKey];
250
+ if (!key) return { ok: false, reason: 'no key configured', skip: true };
251
+
252
+ const model = process.env[p.modelEnv] || p.defaultModel;
253
+ const req = p.build(model, key);
254
+
255
+ try {
256
+ const resp = await fetch(req.url, {
257
+ method: 'POST',
258
+ headers: req.headers,
259
+ body: req.body,
260
+ });
261
+ const text = await resp.text();
262
+ let parsed;
263
+ try {
264
+ parsed = JSON.parse(text);
265
+ } catch {
266
+ return { ok: false, reason: `HTTP ${resp.status}: non-JSON response`, provider: name, model };
267
+ }
268
+ if (!resp.ok) {
269
+ return {
270
+ ok: false,
271
+ reason: `HTTP ${resp.status}: ${p.extractError(parsed)}`,
272
+ provider: name,
273
+ model,
274
+ };
275
+ }
276
+ const reviewText = p.extractText(parsed);
277
+ const usage = p.extractUsage(parsed);
278
+ return { ok: true, provider: name, model, reviewText, usage };
279
+ } catch (e) {
280
+ return { ok: false, reason: `network error: ${e.message}`, provider: name, model };
281
+ }
282
+ }
283
+
284
+ (async () => {
285
+ const attempts = [];
286
+ let winner = null;
287
+
288
+ for (const name of order) {
289
+ const attempt = await tryProvider(name);
290
+ attempts.push(attempt);
291
+ if (attempt.ok) {
292
+ winner = attempt;
293
+ break;
294
+ }
295
+ if (attempt.skip) continue;
296
+ console.log(`::warning::${providers[name].label} failed: ${attempt.reason}`);
297
+ }
298
+
299
+ if (!winner) {
300
+ const configured = attempts.filter((a) => !a.skip);
301
+ if (configured.length === 0) {
302
+ console.log('::warning::No review providers configured. Skipping.');
303
+ setOutput('status', 'skipped');
304
+ setOutput('error_reason', 'no providers configured');
305
+ } else {
306
+ const reasons = configured
307
+ .map((a) => `${providers[a.provider].label}: ${a.reason}`)
308
+ .join('; ');
309
+ console.log(`::warning::All providers failed. ${reasons}`);
310
+ setOutput('status', 'error');
311
+ setOutput('error_reason', reasons);
312
+ }
313
+ return;
314
+ }
315
+
316
+ fs.writeFileSync('review.json', winner.reviewText);
317
+ setOutput('status', 'ok');
318
+ setOutput('provider_label', providers[winner.provider].label);
319
+ setOutput('provider_model', winner.model);
320
+
321
+ const promptTokens = winner.usage.prompt_tokens;
322
+ const completionTokens = winner.usage.completion_tokens;
323
+ const cost = computeCost(winner.model, promptTokens, completionTokens);
324
+ setOutput('prompt_tokens', String(promptTokens));
325
+ setOutput('completion_tokens', String(completionTokens));
326
+ setOutput('cost_usd', cost == null ? '' : cost.toFixed(4));
327
+
328
+ // Emit fallback details to the workflow log (not the PR comment).
329
+ const failed = attempts.filter((a) => !a.ok && !a.skip);
330
+ const fellBack = failed.length > 0;
331
+ setOutput('fell_back', fellBack ? 'true' : 'false');
332
+ if (fellBack) {
333
+ const notes = failed
334
+ .map((a) => `${providers[a.provider].label} failed: ${a.reason}`)
335
+ .join('; ');
336
+ console.log(`::notice::Fell back to ${providers[winner.provider].label}. ${notes}`);
337
+ }
338
+ })();
339
+ SCRIPT
340
+
341
+ - name: Post review
342
+ uses: actions/github-script@v8
343
+ with:
344
+ github-token: ${{ secrets.GITHUB_TOKEN }}
345
+ script: |
346
+ const fs = require('fs');
347
+ const prNumber = ${{ steps.pr.outputs.number }};
348
+ const headSha = '${{ steps.pr.outputs.head_sha }}';
349
+ const round = '${{ needs.check-review-status.outputs.review_round }}';
350
+ const status = `${{ steps.call.outputs.status }}`;
351
+ const errorReason = `${{ steps.call.outputs.error_reason }}`;
352
+ const providerLabel = `${{ steps.call.outputs.provider_label }}`;
353
+ const providerModel = `${{ steps.call.outputs.provider_model }}`;
354
+ const promptTokens = Number(`${{ steps.call.outputs.prompt_tokens }}` || '0');
355
+ const completionTokens = Number(`${{ steps.call.outputs.completion_tokens }}` || '0');
356
+ const costUsd = `${{ steps.call.outputs.cost_usd }}`;
357
+ const fellBack = `${{ steps.call.outputs.fell_back }}` === 'true';
358
+
359
+ const fmt = (n) => n.toLocaleString('en-US');
360
+ const costPart = costUsd ? ` | $${costUsd}` : '';
361
+ const usagePart = promptTokens + completionTokens > 0
362
+ ? ` | ${fmt(promptTokens)} in / ${fmt(completionTokens)} out${costPart}`
363
+ : '';
364
+ const fallbackBadge = fellBack ? ' (fallback)' : '';
365
+
366
+ // Failure paths — no round marker, so the next push retries fresh.
367
+ if (status !== 'ok') {
368
+ const note = status === 'skipped'
369
+ ? `Review skipped — ${errorReason}. No round marker emitted; the next PR activity will retry as Round 1.`
370
+ : `Review failed — ${errorReason}. No round marker emitted; the next PR activity will retry as Round 1.`;
371
+ await github.rest.issues.createComment({
372
+ owner: context.repo.owner,
373
+ repo: context.repo.repo,
374
+ issue_number: prNumber,
375
+ body: `### Automated Review — not posted\n\n${note}`,
376
+ });
377
+ return;
378
+ }
379
+
380
+ const raw = fs.readFileSync('review.json', 'utf8').trim();
381
+ let review;
382
+ try {
383
+ let s = raw;
384
+ if (s.startsWith('```')) s = s.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
385
+ review = JSON.parse(s);
386
+ } catch (e) {
387
+ await github.rest.issues.createComment({
388
+ owner: context.repo.owner,
389
+ repo: context.repo.repo,
390
+ issue_number: prNumber,
391
+ body: `## Automated Review (Round ${round})\n\n${raw}\n\n---\n*Reviewed by ${providerLabel} ${providerModel}${fallbackBadge}${usagePart}*`,
392
+ });
393
+ return;
394
+ }
395
+
396
+ const summary = review.summary || 'No summary provided';
397
+ const changes = (review.changes || []).map(c => `- ${c}`).join('\n') || '_none_';
398
+ const feedback = review.feedback || [];
399
+
400
+ const isRound1 = round === '1';
401
+ const marker = isRound1 ? '<!-- AUTOMATED_REVIEW_ROUND_1 -->' : '<!-- AUTOMATED_REVIEW_ROUND_2_COMPLETE -->';
402
+ const label = isRound1 ? 'Round 1' : 'Round 2 (Final)';
403
+
404
+ let body = `## Automated Review — ${label}\n\n${marker}\n\n`;
405
+ body += `### Summary\n${summary}\n\n`;
406
+ body += `### Changes\n${changes}\n\n---\n`;
407
+ body += feedback.length === 0
408
+ ? `:white_check_mark: No issues found\n\n`
409
+ : `:mag: Found ${feedback.length} suggestion${feedback.length === 1 ? '' : 's'} (see inline comments)\n\n`;
410
+ body += `*Reviewed by ${providerLabel} ${providerModel}${fallbackBadge}${usagePart}`;
411
+ body += isRound1 ? ` — Round 1 of 2*` : ` — Round 2 (Final); no further reviews will be performed*`;
412
+
413
+ await github.rest.issues.createComment({
414
+ owner: context.repo.owner,
415
+ repo: context.repo.repo,
416
+ issue_number: prNumber,
417
+ body,
418
+ });
419
+
420
+ for (const item of feedback) {
421
+ try {
422
+ await github.rest.pulls.createReviewComment({
423
+ owner: context.repo.owner,
424
+ repo: context.repo.repo,
425
+ pull_number: prNumber,
426
+ body: item.comment,
427
+ path: item.file,
428
+ line: item.line,
429
+ commit_id: headSha,
430
+ side: 'RIGHT',
431
+ });
432
+ } catch (e) {
433
+ console.log(`Failed to post comment on ${item.file}:${item.line} — ${e.message}`);
434
+ }
435
+ }
@@ -0,0 +1,8 @@
1
+ cff-version: 1.2.0
2
+ title: '{{REPO_NAME}}'
3
+ message: 'If you use this software, please cite it as below.'
4
+ type: software
5
+ authors:
6
+ - name: '{{OWNER_ORG}}'
7
+ repository-code: 'https://github.com/{{REPO_SLUG}}'
8
+ license: Apache-2.0
@@ -0,0 +1,43 @@
1
+ # Código de Conduta — Contributor Covenant
2
+
3
+ ## Nosso compromisso
4
+
5
+ Nós, como membros, contribuidores e líderes, nos comprometemos a tornar a participação em nossa comunidade uma experiência livre de assédio para todos, independentemente de idade, tamanho corporal, deficiência visível ou invisível, etnia, características sexuais, identidade e expressão de gênero, nível de experiência, educação, status socioeconômico, nacionalidade, aparência pessoal, raça, religião ou identidade e orientação sexual.
6
+
7
+ Comprometemo-nos a agir e interagir de maneiras que contribuam para uma comunidade aberta, acolhedora, diversa, inclusiva e saudável.
8
+
9
+ ## Nossos padrões
10
+
11
+ Exemplos de comportamento que contribuem para um ambiente positivo:
12
+
13
+ - Demonstrar empatia e gentileza com outras pessoas
14
+ - Respeitar opiniões, pontos de vista e experiências diferentes
15
+ - Dar e aceitar graciosamente feedback construtivo
16
+ - Assumir responsabilidade e pedir desculpas aos afetados por nossos erros, aprendendo com a experiência
17
+ - Focar no que é melhor não apenas para nós individualmente, mas para a comunidade como um todo
18
+
19
+ Exemplos de comportamento inaceitável:
20
+
21
+ - Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais de qualquer tipo
22
+ - Trolling, comentários insultuosos ou depreciativos, e ataques pessoais ou políticos
23
+ - Assédio público ou privado
24
+ - Publicar informações privadas de outros, como endereço físico ou de e-mail, sem permissão explícita
25
+ - Outra conduta que poderia razoavelmente ser considerada inapropriada em um ambiente profissional
26
+
27
+ ## Responsabilidades de aplicação
28
+
29
+ Os líderes da comunidade são responsáveis por esclarecer e fazer cumprir nossos padrões de comportamento aceitável e tomarão ação corretiva apropriada e justa em resposta a qualquer comportamento que considerem inapropriado, ameaçador, ofensivo ou prejudicial.
30
+
31
+ ## Escopo
32
+
33
+ Este Código de Conduta aplica-se em todos os espaços da comunidade e também quando um indivíduo está representando oficialmente a comunidade em espaços públicos.
34
+
35
+ ## Aplicação
36
+
37
+ Instâncias de comportamento abusivo, de assédio ou de outra forma inaceitável podem ser reportadas aos líderes da comunidade responsáveis pela aplicação em **{{CONDUCT_EMAIL}}**.
38
+
39
+ Todas as reclamações serão revisadas e investigadas de forma rápida e justa.
40
+
41
+ ## Atribuição
42
+
43
+ Este Código de Conduta é adaptado do [Contributor Covenant](https://www.contributor-covenant.org), versão 2.1.
@@ -0,0 +1,32 @@
1
+ # Contributing
2
+
3
+ Thanks for considering a contribution to `{{REPO_NAME}}`.
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm -r build
10
+ pnpm -r typecheck
11
+ pnpm -r test
12
+ pnpm -r lint
13
+ ```
14
+
15
+ Node {{NODE_VERSION}}+ and pnpm {{PNPM_VERSION}}+ are required (see `.nvmrc` and the `packageManager` field).
16
+
17
+ ## Workflow
18
+
19
+ 1. Fork (or branch) from `main`
20
+ 2. Open a PR — direct pushes to `main` are not allowed
21
+ 3. CI must be green
22
+ 4. A reviewer approves and merges via GitHub
23
+
24
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org). Allowed scopes for this repo are configured in `.commitlintrc.cjs`.
25
+
26
+ ## Dependency changes
27
+
28
+ Ask before adding runtime or peer dependencies. In the PR description explain: what the package does, why it's needed, and whether an existing dependency could serve the purpose.
29
+
30
+ ## Signing
31
+
32
+ All commits should be GPG-signed. The `commit-msg` hook blocks AI-attribution footers (`Co-Authored-By: Claude` and similar).
@@ -0,0 +1,37 @@
1
+ # Conventions
2
+
3
+ ## Commit format
4
+
5
+ [Conventional Commits](https://www.conventionalcommits.org):
6
+
7
+ ```
8
+ type(scope): short description
9
+
10
+ optional longer body
11
+
12
+ BREAKING CHANGE: notes (optional)
13
+ ```
14
+
15
+ **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `build`, `ci`, `revert`
16
+
17
+ **Scopes**: declared in `.commitlintrc.cjs`.
18
+
19
+ **AI attribution**: Do not include `Co-Authored-By: Claude`, `Generated with Claude`, or similar lines. The `commit-msg` hook enforces this.
20
+
21
+ ## Code style
22
+
23
+ - TypeScript strict mode, `noUncheckedIndexedAccess`
24
+ - ESM modules
25
+ - Prettier-enforced formatting (100 char width, single quotes, semicolons, trailing commas — via `@precisa-saude/prettier-config`)
26
+ - ESLint rules via `@precisa-saude/eslint-config`
27
+ - Imports sorted by `eslint-plugin-simple-import-sort`
28
+ - Interfaces, objects, and JSX props sorted by `eslint-plugin-perfectionist` where applicable
29
+
30
+ ## Tests
31
+
32
+ Vitest with an 80% coverage floor (configured per-repo in `vitest.config.ts`).
33
+
34
+ ## Dependency rules
35
+
36
+ - Core/SDK packages avoid runtime dependencies when feasible
37
+ - External dependencies require explicit approval in the PR
@@ -0,0 +1,22 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security vulnerability in this project, please report it responsibly.
6
+
7
+ **Do not open a public GitHub issue.**
8
+
9
+ Email: **{{SECURITY_EMAIL}}**
10
+
11
+ Include:
12
+
13
+ - Description of the vulnerability
14
+ - Steps to reproduce
15
+ - Potential impact
16
+ - Suggested fix (if any)
17
+
18
+ We will acknowledge receipt within 48 hours and provide a timeline for resolution.
19
+
20
+ ## Disclosure
21
+
22
+ Disclosure is coordinated privately. Fixes are released in the affected package(s) and noted in the CHANGELOG.
@@ -0,0 +1,14 @@
1
+ # Support
2
+
3
+ ## Getting help
4
+
5
+ - **Report bugs**: [GitHub Issues](https://github.com/{{REPO_SLUG}}/issues)
6
+ - **Request features**: [GitHub Issues](https://github.com/{{REPO_SLUG}}/issues)
7
+ - **Questions and discussions**: [GitHub Discussions](https://github.com/{{REPO_SLUG}}/discussions)
8
+
9
+ ## Before opening an issue
10
+
11
+ 1. Search existing issues to avoid duplicates
12
+ 2. Include a minimal reproducible example
13
+ 3. Specify the package and version
14
+ 4. Include Node/pnpm versions and the OS you're on
@@ -0,0 +1,8 @@
1
+ pnpm exec commitlint --edit "$1"
2
+
3
+ # Block AI attribution lines in commit messages
4
+ if grep -q -i -E "co-authored-by:.*claude|generated with.*claude|co-authored-by:.*anthropic|co-authored-by:.*gpt|co-authored-by:.*copilot" "$1"; then
5
+ echo "❌ Commit message contains AI attribution. Remove these lines:"
6
+ grep -i -E "co-authored-by:.*claude|generated with.*claude|co-authored-by:.*anthropic|co-authored-by:.*gpt|co-authored-by:.*copilot" "$1"
7
+ exit 1
8
+ fi
@@ -0,0 +1 @@
1
+ pnpm exec lint-staged
@@ -0,0 +1,21 @@
1
+ # Full-checks hook. Runs the same gates as CI so a failing push is caught locally.
2
+ # Per-repo coverage thresholds live in vitest config, not here.
3
+
4
+ set -e
5
+
6
+ echo "→ verifying lockfile is in sync"
7
+ pnpm install --frozen-lockfile --prefer-offline
8
+
9
+ echo "→ prettier"
10
+ pnpm format:check
11
+
12
+ echo "→ eslint"
13
+ pnpm -r lint
14
+
15
+ echo "→ typescript"
16
+ pnpm -r typecheck
17
+
18
+ echo "→ tests"
19
+ pnpm -r test
20
+
21
+ echo "✓ pre-push checks passed"