trackfw 3.0.0 → 3.1.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.
Files changed (52) hide show
  1. package/README.md +9 -2
  2. package/package.json +1 -1
  3. package/src/commands/discover.js +18 -0
  4. package/src/commands/index.js +2 -0
  5. package/src/commands/init.js +40 -2
  6. package/src/commands/log.js +2 -1
  7. package/src/commands/note.js +24 -0
  8. package/src/commands/req.js +7 -1
  9. package/src/commands/roadmap.js +4 -3
  10. package/src/commands/ship.js +49 -0
  11. package/src/config/index.js +5 -2
  12. package/src/forge/adapter.js +189 -0
  13. package/src/forge/resolve.js +191 -0
  14. package/src/generators/adr.js +13 -4
  15. package/src/generators/date.js +23 -0
  16. package/src/generators/init.js +105 -3
  17. package/src/generators/note.js +154 -0
  18. package/src/generators/req.js +88 -5
  19. package/src/generators/roadmap.js +96 -20
  20. package/src/i18n/locales/en-US.json +2 -1
  21. package/src/i18n/locales/es-ES.json +2 -1
  22. package/src/i18n/locales/pt-BR.json +2 -1
  23. package/src/identity/preset.js +22 -0
  24. package/src/integrations/assets/agents/architect.md +32 -0
  25. package/src/integrations/assets/agents/backend.md +32 -0
  26. package/src/integrations/assets/agents/code-quality.md +29 -0
  27. package/src/integrations/assets/agents/data.md +32 -0
  28. package/src/integrations/assets/agents/dba.md +32 -0
  29. package/src/integrations/assets/agents/frontend.md +32 -0
  30. package/src/integrations/assets/agents/iac.md +46 -0
  31. package/src/integrations/assets/agents/infra.md +34 -0
  32. package/src/integrations/assets/agents/qa.md +32 -0
  33. package/src/integrations/assets/agents/security.md +29 -0
  34. package/src/integrations/assets/agents/tooling.md +47 -0
  35. package/src/integrations/assets/agents/ux.md +29 -0
  36. package/src/integrations/assets/catalog.json +16 -2
  37. package/src/integrations/assets/skills/architecture.md +66 -0
  38. package/src/integrations/assets/skills/backend.md +67 -0
  39. package/src/integrations/assets/skills/code-quality.md +78 -0
  40. package/src/integrations/assets/skills/data.md +75 -0
  41. package/src/integrations/assets/skills/dba.md +77 -0
  42. package/src/integrations/assets/skills/frontend.md +70 -0
  43. package/src/integrations/assets/skills/iac.md +90 -0
  44. package/src/integrations/assets/skills/infra.md +62 -0
  45. package/src/integrations/assets/skills/qa.md +63 -0
  46. package/src/integrations/assets/skills/security.md +74 -0
  47. package/src/integrations/assets/skills/tooling.md +80 -0
  48. package/src/integrations/assets/skills/ux.md +70 -0
  49. package/src/integrations/render.js +57 -6
  50. package/src/ship/runner.js +454 -0
  51. package/src/validator/index.js +321 -111
  52. package/src/validator/traceid.js +1 -1
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * forge/resolve.js — Forge resolution for trackfw ship.
5
+ *
6
+ * Precedence (highest → lowest):
7
+ * 1. --forge flag (explicit override)
8
+ * 2. forge: field in trackfw.yaml
9
+ * 3. Host extracted from git remote get-url origin
10
+ * 4. CI configuration files in repo root (.gitlab-ci.yml / .github/workflows/)
11
+ * 5. "manual" — never an error
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ /** Accepted values for the --forge flag and forge: config field. */
19
+ const VALID_FORGES = ['github', 'gitlab', 'bitbucket', 'azure'];
20
+
21
+ /**
22
+ * Resolve the forge from the given inputs.
23
+ *
24
+ * @param {object} input
25
+ * @param {string} [input.flagForge] - value from --forge flag
26
+ * @param {string} [input.configForge] - value from trackfw.yaml forge: field
27
+ * @param {string} [input.remoteURL] - output of "git remote get-url origin"
28
+ * @param {string} [input.repoDir] - repo root for CI file detection
29
+ * @returns {{ forge: string, source: string }} Resolution object.
30
+ * forge ∈ {"github", "gitlab", "bitbucket", "azure", "manual"}
31
+ * source ∈ {"flag", "config", "remote", "ci", "none"}
32
+ * @throws {Error} if flagForge or configForge contains an invalid value.
33
+ */
34
+ function resolve(input) {
35
+ const { flagForge = '', configForge = '', remoteURL = '', repoDir = '' } = input || {};
36
+
37
+ // 1. Explicit flag wins everything.
38
+ if (flagForge) {
39
+ validateForge(flagForge);
40
+ return { forge: flagForge, source: 'flag' };
41
+ }
42
+
43
+ // 2. Config field.
44
+ if (configForge) {
45
+ validateForge(configForge);
46
+ return { forge: configForge, source: 'config' };
47
+ }
48
+
49
+ // 3. Remote URL — known hosts only.
50
+ if (remoteURL) {
51
+ const forge = forgeFromRemoteURL(remoteURL);
52
+ if (forge) return { forge, source: 'remote' };
53
+ }
54
+
55
+ // 4. CI files — desempate for self-hosted / unknown host.
56
+ if (repoDir) {
57
+ const forge = forgeFromCI(repoDir);
58
+ if (forge) return { forge, source: 'ci' };
59
+ }
60
+
61
+ // 5. Manual — not an error.
62
+ return { forge: 'manual', source: 'none' };
63
+ }
64
+
65
+ /**
66
+ * Production entry point: runs "git remote get-url origin" in repoDir,
67
+ * then calls resolve().
68
+ */
69
+ function resolveFromRepo(flagForge, configForge, repoDir) {
70
+ const remoteURL = gitRemoteURL(repoDir);
71
+ return resolve({ flagForge, configForge, remoteURL, repoDir });
72
+ }
73
+
74
+ /** @throws {Error} if forge is not in VALID_FORGES */
75
+ function validateForge(forge) {
76
+ if (!VALID_FORGES.includes(forge)) {
77
+ throw new Error(
78
+ `invalid forge "${forge}": accepted values are ${VALID_FORGES.join(', ')}`
79
+ );
80
+ }
81
+ }
82
+
83
+ function forgeFromRemoteURL(rawURL) {
84
+ const host = extractHost(rawURL);
85
+ return hostToForge(host);
86
+ }
87
+
88
+ /**
89
+ * Extracts the lowercase hostname from HTTPS, SSH (git@) or ssh:// URLs.
90
+ */
91
+ function extractHost(rawURL) {
92
+ if (!rawURL) return '';
93
+ rawURL = rawURL.trim();
94
+
95
+ // SSH short form: git@github.com:org/repo.git
96
+ if (rawURL.startsWith('git@')) {
97
+ const rest = rawURL.slice(4); // strip "git@"
98
+ const colonIdx = rest.indexOf(':');
99
+ if (colonIdx >= 0) return rest.slice(0, colonIdx).toLowerCase();
100
+ return '';
101
+ }
102
+
103
+ // SSH long form: ssh://git@github.com/org/repo.git
104
+ if (rawURL.startsWith('ssh://')) {
105
+ let rest = rawURL.slice(6); // strip "ssh://"
106
+ const atIdx = rest.indexOf('@');
107
+ if (atIdx >= 0) rest = rest.slice(atIdx + 1); // strip user@
108
+ const slashIdx = rest.indexOf('/');
109
+ return slashIdx >= 0
110
+ ? rest.slice(0, slashIdx).toLowerCase()
111
+ : rest.toLowerCase();
112
+ }
113
+
114
+ // HTTPS / HTTP
115
+ if (rawURL.startsWith('https://') || rawURL.startsWith('http://')) {
116
+ let rest = rawURL.replace(/^https?:\/\//, '');
117
+ // strip optional user:pass@
118
+ const atIdx = rest.indexOf('@');
119
+ if (atIdx >= 0) rest = rest.slice(atIdx + 1);
120
+ const slashIdx = rest.indexOf('/');
121
+ return slashIdx >= 0
122
+ ? rest.slice(0, slashIdx).toLowerCase()
123
+ : rest.toLowerCase();
124
+ }
125
+
126
+ return '';
127
+ }
128
+
129
+ /**
130
+ * Maps a known hostname to its forge identifier.
131
+ * Azure DevOps uses dev.azure.com (HTTPS) and ssh.dev.azure.com (SSH).
132
+ * Returns '' for unknown / self-hosted hosts.
133
+ */
134
+ function hostToForge(host) {
135
+ if (!host) return '';
136
+ if (host === 'github.com') return 'github';
137
+ if (host === 'gitlab.com') return 'gitlab';
138
+ if (host === 'bitbucket.org') return 'bitbucket';
139
+ if (
140
+ host === 'dev.azure.com' ||
141
+ host.endsWith('.dev.azure.com') ||
142
+ host.endsWith('.visualstudio.com')
143
+ ) {
144
+ return 'azure';
145
+ }
146
+ return '';
147
+ }
148
+
149
+ /**
150
+ * Inspects CI indicator files in repoDir.
151
+ * Priority: .gitlab-ci.yml → gitlab; .github/workflows/ → github.
152
+ */
153
+ function forgeFromCI(repoDir) {
154
+ try {
155
+ fs.statSync(path.join(repoDir, '.gitlab-ci.yml'));
156
+ return 'gitlab';
157
+ } catch (_) {}
158
+
159
+ try {
160
+ const stat = fs.statSync(path.join(repoDir, '.github', 'workflows'));
161
+ if (stat.isDirectory()) return 'github';
162
+ } catch (_) {}
163
+
164
+ return '';
165
+ }
166
+
167
+ /**
168
+ * Runs "git remote get-url origin" in repoDir.
169
+ * Returns '' on any error.
170
+ */
171
+ function gitRemoteURL(repoDir) {
172
+ try {
173
+ return execSync('git remote get-url origin', {
174
+ cwd: repoDir,
175
+ stdio: ['pipe', 'pipe', 'pipe'],
176
+ encoding: 'utf8',
177
+ }).trim();
178
+ } catch (_) {
179
+ return '';
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ VALID_FORGES,
185
+ resolve,
186
+ resolveFromRepo,
187
+ validateForge,
188
+ extractHost,
189
+ hostToForge,
190
+ forgeFromCI,
191
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
+ const { localDateISO } = require('./date')
5
6
 
6
7
  /**
7
8
  * Converte uma string em slug: lowercase + espaços → hifens.
@@ -9,7 +10,13 @@ const path = require('path')
9
10
  * @returns {string}
10
11
  */
11
12
  function toSlug(s) {
12
- return s.toLowerCase().replace(/ /g, '-')
13
+ // NFKD normalization + remove combining marks (diacríticos) + lowercase + non-alphanumeric → hífen
14
+ return s
15
+ .normalize('NFKD')
16
+ .replace(/[̀-ͯ]/g, '')
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, '-')
19
+ .replace(/^-+|-+$/g, '')
13
20
  }
14
21
 
15
22
  /**
@@ -26,11 +33,13 @@ function slugToTitle(slug) {
26
33
  }
27
34
 
28
35
  /**
29
- * Retorna a data atual no formato YYYY-MM-DD.
36
+ * Retorna a data LOCAL atual no formato YYYY-MM-DD.
37
+ * Delega para localDateISO() — usa getDate/getMonth/getFullYear (hora local),
38
+ * não toISOString (UTC).
30
39
  * @returns {string}
31
40
  */
32
41
  function today() {
33
- return new Date().toISOString().slice(0, 10)
42
+ return localDateISO()
34
43
  }
35
44
 
36
45
  /**
@@ -182,4 +191,4 @@ author: ""
182
191
  return filename
183
192
  }
184
193
 
185
- module.exports = { newADR, listADRs, newADRDraft, toSlug }
194
+ module.exports = { newADR, listADRs, newADRDraft, toSlug, today }
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * localDateISO — retorna a data LOCAL atual no formato YYYY-MM-DD.
5
+ *
6
+ * Usa getFullYear/getMonth/getDate (métodos de hora local) em vez de
7
+ * toISOString() (que retorna sempre UTC). Isso garante que uma REQ criada às
8
+ * 22h no Brasil seja datada do dia correto, independentemente do fuso da
9
+ * máquina — comportamento idêntico ao de Go (time.Now().Format("2006-01-02"))
10
+ * e Python (date.today().isoformat()).
11
+ *
12
+ * @returns {string} Data no formato "YYYY-MM-DD"
13
+ */
14
+ function localDateISO() {
15
+ const d = new Date()
16
+ return [
17
+ d.getFullYear(),
18
+ String(d.getMonth() + 1).padStart(2, '0'),
19
+ String(d.getDate()).padStart(2, '0'),
20
+ ].join('-')
21
+ }
22
+
23
+ module.exports = { localDateISO }
@@ -13,6 +13,7 @@ const GOV_DIRS = [
13
13
  'docs/roadmaps/blocked',
14
14
  'docs/roadmaps/done',
15
15
  'docs/roadmaps/abandoned',
16
+ 'vault/notes',
16
17
  ]
17
18
 
18
19
  const GLOBAL_ADRS_DIRECTIVE = 'Obrigatório: Inspecione e respeite todos os ADRs globais nos diretórios listados em adr_dirs (inclusive caminhos ~/...) antes de propor alterações de arquitetura.'
@@ -28,6 +29,7 @@ async function scaffold(cfg, cwd) {
28
29
  console.log(` ✓ ${dir}`)
29
30
  }
30
31
 
32
+ generateVaultIndex(root)
31
33
  writeTrackfwConfig(cfg, root)
32
34
  generateValidateScript(cfg, root)
33
35
  generateAttentionScripts(cfg, root)
@@ -40,6 +42,29 @@ async function scaffold(cfg, cwd) {
40
42
  injectHooksDetected(root)
41
43
  }
42
44
 
45
+ /**
46
+ * generateVaultIndex — cria vault/notes/index.md se ainda não existir.
47
+ * @param {string} root
48
+ */
49
+ function generateVaultIndex(root) {
50
+ const indexPath = path.join(root || process.cwd(), 'vault', 'notes', 'index.md')
51
+ if (fs.existsSync(indexPath)) return
52
+ const content = `# Vault de Conhecimento
53
+
54
+ > Ponto de entrada de conhecimento do projeto para agentes e pessoas.
55
+ > Cada nota documenta uma causa-raiz, decisão técnica ou restrição não óbvia.
56
+ > Crie notas com: trackfw note new "<título>"
57
+
58
+ ## Índice
59
+
60
+ <!-- As notas serão listadas abaixo. Exemplo:
61
+ - [nome-da-nota-YYYY-MM-DD](nome-da-nota-YYYY-MM-DD.md)
62
+ -->
63
+ `
64
+ fs.writeFileSync(indexPath, content, 'utf8')
65
+ console.log(' ✓ vault/notes/index.md')
66
+ }
67
+
43
68
  // ---------------------------------------------------------------------------
44
69
  // trackfw.yaml
45
70
  // ---------------------------------------------------------------------------
@@ -54,7 +79,7 @@ backend: ${cfg.backend || ''}
54
79
  backend_framework: ${cfg.backendFramework || ''}
55
80
  pkg_manager: ${cfg.pkgManager || ''}
56
81
  hooks: ${cfg.hooks || ''}
57
- ci: ${cfg.ci || ''}
82
+ ci: ${cfg.ci || ''}${cfg.forge ? `\nforge: ${cfg.forge}` : ''}
58
83
  require_req_in_commit: ${cfg.requireReqInCommit ? 'true' : 'false'}
59
84
 
60
85
  # governance paths (edit to match your project structure)
@@ -585,6 +610,66 @@ function generateClaudeMD(cfg) {
585
610
  content += 'No CI gate configured.\n'
586
611
  }
587
612
 
613
+ // Harness sections — derived from project governance conventions
614
+ content += '\n## Branch strategy\n\n'
615
+ content += 'One active branch at a time. Name it `feat/<slug>`, `fix/<slug>` or `refactor/<slug>`. '
616
+ content += 'Before creating a new branch, verify no other is genuinely open: run `git fetch origin --prune`, '
617
+ content += 'then `git branch -r --no-merged origin/main`, then for each candidate `git diff origin/main <branch> --stat`. '
618
+ content += 'An empty diff means it was squash-merged — ignore it. '
619
+ content += 'Squash merges do not mark a branch as merged, so `--no-merged` alone is not evidence. '
620
+ content += 'If the branch is stale and the diff looks inflated by main\'s own evolution, '
621
+ content += 'compare only the files the branch itself touched since the merge base.\n\n'
622
+
623
+ content += '## Definition of done\n\n'
624
+ content += 'Green build and tests do not close a microbatch. '
625
+ content += 'It is done when the requirement and the roadmap sit in the correct state folder, '
626
+ content += 'their declared status matches that folder, the final validation is recorded with evidence, '
627
+ content += 'no duplicate copy remains in another state, and `trackfw validate` reports no violations.\n\n'
628
+
629
+ content += '## Requirement scope\n\n'
630
+ content += 'Every requirement must declare an explicit negative scope: what must not be implemented. '
631
+ content += 'Boundaries prevent an implementing agent from inventing work.\n\n'
632
+
633
+ content += '## State requirements\n\n'
634
+ content += '`blocked` requires a reason and an owner. '
635
+ content += '`abandoned` requires a reason and a successor. '
636
+ content += '`wip` must reflect work that is genuinely active; '
637
+ content += 'anything stalled moves to `blocked` or `abandoned` instead of rotting in `wip`.\n\n'
638
+
639
+ content += '## Roadmap format\n\n'
640
+ content += 'Organize work as waves of microbatches. '
641
+ content += 'A wave groups microbatches that can run in parallel; a barrier separates waves. '
642
+ content += 'Microbatches sharing any file — including generated trees and build outputs — must be sequential, '
643
+ content += 'and the reason is documented. '
644
+ content += 'Each microbatch declares exact files, exact actions, measurable acceptance criteria and exact validation commands, '
645
+ content += 'so that a small model can execute it without guessing.\n\n'
646
+
647
+ content += '## When governance is not required\n\n'
648
+ content += 'A closed list of exemptions: a typo or local variable rename; a documentation-only change; '
649
+ content += 'a configuration tweak with no runtime effect; a direct revert; '
650
+ content += 'answering a question or reviewing without changes. '
651
+ content += 'Additionally, when the user reports a concrete bug, fix it directly and do not open an architectural analysis for it. '
652
+ content += '**This section takes precedence over the general rule that requires a requirement and a roadmap.** '
653
+ content += 'Anything touching business logic, an API contract, a data schema, authentication or authorization, '
654
+ content += 'localization, or user-facing behavior always requires governance, regardless of how few files it touches.\n\n'
655
+
656
+ content += '## Production incidents\n\n'
657
+ content += 'Inspect the live environment before proposing a fix: real variables, active credentials, '
658
+ content += 'granted permissions, running processes. '
659
+ content += 'Confirm the root cause against real evidence, then implement the smallest fix. '
660
+ content += 'Never edit static configuration files as a response to a root cause that has not been confirmed in the running environment.\n\n'
661
+
662
+ content += '## Iterative prototyping\n\n'
663
+ content += 'For complex or uncertain user-facing work, validate the concept with a disposable, isolated prototype '
664
+ content += 'that the user reviews visually, and only then write the decision record and the production roadmap. '
665
+ content += 'Build and test success is not evidence that an interface is right.\n\n'
666
+
667
+ content += '## Autopilot\n\n'
668
+ content += 'Ask everything you need before starting. '
669
+ content += 'Once started, do not interrupt for confirmations that could have been anticipated. '
670
+ content += 'Decide low-risk details autonomously following existing project conventions, '
671
+ content += 'and record autonomous decisions in the commit message.\n'
672
+
588
673
  injectOrUpdateRules('CLAUDE.md', content)
589
674
  console.log(' ✓ CLAUDE.md')
590
675
  }
@@ -700,9 +785,16 @@ trackfw não está instalado. Instale com:
700
785
  Com base no conteúdo da REQ, gere um roadmap seguindo **estritamente** este formato:
701
786
 
702
787
  \`\`\`markdown
788
+ ---
789
+ status: backlog
790
+ date: <YYYY-MM-DD>
791
+ req: "docs/req/<arquivo-selecionado>.md"
792
+ squad: ""
793
+ ---
794
+
703
795
  # Roadmap: <título derivado da REQ>
704
796
 
705
- > Criado em: <YYYY-MM-DD> | Status: ⬜ Backlog
797
+ > Created: <YYYY-MM-DD> | Status: backlog
706
798
 
707
799
  ## Diagnóstico / Contexto
708
800
  <resumo do problema, motivação e escopo extraídos da REQ>
@@ -720,6 +812,13 @@ trackfw não está instalado. Instale com:
720
812
  - [ ] build sem erros
721
813
  - [ ] testes verdes
722
814
  **Comandos de validação:** \`<comando de build e teste do projeto>\`
815
+
816
+ ### ML-1B — <título> (se independente de ML-1A)
817
+ ...
818
+
819
+ ## Wave 2 — <nome> (depende de Wave 1)
820
+ > Dependências: Wave 1 completa
821
+ ...
723
822
  \`\`\`
724
823
 
725
824
  **Princípios obrigatórios:**
@@ -732,10 +831,12 @@ trackfw não está instalado. Instale com:
732
831
  4. **Salvar o arquivo**
733
832
  - Calcule o slug: título em lowercase, espaços → hifens, remova caracteres especiais
734
833
  - Crie o arquivo em \`docs/roadmaps/backlog/ROADMAP-<YYYY-MM-DD>-<slug>.md\`
834
+ - Preencha \`req:\` com o caminho relativo completo da REQ selecionada
735
835
  - Use a data de hoje
736
836
 
737
837
  5. **Confirmar**
738
- Informe o caminho do arquivo criado e um resumo das Waves e total de MLs gerados.`,
838
+ Informe o caminho do arquivo criado e um resumo das Waves e total de MLs gerados.
839
+ `,
739
840
 
740
841
  'implement.md': `Você é o orquestrador de implementação do trackfw. Siga o fluxo abaixo **sem pular etapas**.
741
842
 
@@ -1139,6 +1240,7 @@ module.exports = {
1139
1240
  generateClaudeMD,
1140
1241
  generateClaudeCommands,
1141
1242
  generateClaudeCommandsForce,
1243
+ generateVaultIndex,
1142
1244
  installSkillsForce,
1143
1245
  installAgents,
1144
1246
  installGemini,
@@ -0,0 +1,154 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { localDateISO } = require('./date')
6
+
7
+ const VAULT_DIR = 'vault/notes'
8
+ const INDEX_FILE = 'vault/notes/index.md'
9
+
10
+ /**
11
+ * Converte título em slug kebab-case lowercase sem acentos.
12
+ * Reutiliza a mesma lógica do adr.js (toSlug).
13
+ * @param {string} s
14
+ * @returns {string}
15
+ */
16
+ function toSlug(s) {
17
+ // NFKD normalization + remove combining marks (diacríticos) + lowercase + non-alphanumeric → hífen
18
+ return s
19
+ .normalize('NFKD')
20
+ .replace(/[̀-ͯ]/g, '')
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, '-')
23
+ .replace(/^-+|-+$/g, '')
24
+ }
25
+
26
+ /**
27
+ * Retorna a data LOCAL atual no formato YYYY-MM-DD.
28
+ * Delega para localDateISO() — usa getDate/getMonth/getFullYear (hora local),
29
+ * não toISOString (UTC).
30
+ * @returns {string}
31
+ */
32
+ function today() {
33
+ return localDateISO()
34
+ }
35
+
36
+ /**
37
+ * Cria uma nova nota em vault/notes/<slug>-YYYY-MM-DD.md e linka no index.md.
38
+ * Idempotente: falha com erro claro se a nota já existir.
39
+ * @param {string} title
40
+ * @param {string} [cwd]
41
+ */
42
+ function newNote(title, cwd) {
43
+ const root = cwd || process.cwd()
44
+ const vaultDir = path.join(root, VAULT_DIR)
45
+ fs.mkdirSync(vaultDir, { recursive: true })
46
+
47
+ const slug = toSlug(title)
48
+ const date = today()
49
+ const filename = `${slug}-${date}.md`
50
+ const notePath = path.join(vaultDir, filename)
51
+
52
+ if (fs.existsSync(notePath)) {
53
+ throw new Error(`nota "${filename}" já existe — não sobrescrita`)
54
+ }
55
+
56
+ const body = `---
57
+ title: "${title}"
58
+ tags: []
59
+ date: ${date}
60
+ related: []
61
+ ---
62
+
63
+ # ${title}
64
+
65
+ ## Problem
66
+
67
+ <!-- Descreva o problema ou situação que motivou esta nota. -->
68
+
69
+ ## Root cause
70
+
71
+ <!-- Qual foi a causa raiz identificada? -->
72
+
73
+ ## Solution
74
+
75
+ <!-- Como foi resolvido ou mitigado? O que deve ser feito? -->
76
+ `
77
+
78
+ fs.writeFileSync(notePath, body, 'utf8')
79
+ appendNoteToIndex(filename, root)
80
+ console.log(`created ${path.join(VAULT_DIR, filename)}`)
81
+ }
82
+
83
+ /**
84
+ * Acrescenta link para filename no vault/notes/index.md.
85
+ * Cria o index.md se não existir. Não duplica se já estiver linkado.
86
+ * @param {string} filename
87
+ * @param {string} [cwd]
88
+ */
89
+ function appendNoteToIndex(filename, cwd) {
90
+ const root = cwd || process.cwd()
91
+ const indexPath = path.join(root, INDEX_FILE)
92
+
93
+ if (!fs.existsSync(indexPath)) {
94
+ const initial = `# Vault de Conhecimento
95
+
96
+ > Ponto de entrada de conhecimento do projeto para agentes e pessoas.
97
+
98
+ ## Índice
99
+
100
+ `
101
+ fs.writeFileSync(indexPath, initial, 'utf8')
102
+ }
103
+
104
+ const content = fs.readFileSync(indexPath, 'utf8')
105
+ const nameWithoutExt = filename.replace(/\.md$/, '')
106
+
107
+ // Verifica se já linkado (link markdown ou wikilink)
108
+ if (
109
+ content.includes(`(${filename})`) ||
110
+ content.includes(`[[${nameWithoutExt}]]`) ||
111
+ content.includes(`[[${filename}]]`)
112
+ ) {
113
+ return
114
+ }
115
+
116
+ const link = `- [${nameWithoutExt}](${filename})\n`
117
+ fs.appendFileSync(indexPath, link, 'utf8')
118
+ }
119
+
120
+ /**
121
+ * Retorna todos os arquivos .md em vault/notes/ exceto index.md.
122
+ * Retorna [] se o diretório não existir.
123
+ * @param {string} [cwd]
124
+ * @returns {string[]}
125
+ */
126
+ function noteFiles(cwd) {
127
+ const root = cwd || process.cwd()
128
+ const vaultDir = path.join(root, VAULT_DIR)
129
+ if (!fs.existsSync(vaultDir)) return []
130
+ return fs
131
+ .readdirSync(vaultDir)
132
+ .filter((f) => f.endsWith('.md') && f !== 'index.md')
133
+ }
134
+
135
+ /**
136
+ * Retorna true se o index.md referencia filename.
137
+ * @param {string} filename
138
+ * @param {string} [cwd]
139
+ * @returns {boolean}
140
+ */
141
+ function indexContains(filename, cwd) {
142
+ const root = cwd || process.cwd()
143
+ const indexPath = path.join(root, INDEX_FILE)
144
+ if (!fs.existsSync(indexPath)) return false
145
+ const content = fs.readFileSync(indexPath, 'utf8')
146
+ const nameWithoutExt = filename.replace(/\.md$/, '')
147
+ return (
148
+ content.includes(`(${filename})`) ||
149
+ content.includes(`[[${nameWithoutExt}]]`) ||
150
+ content.includes(`[[${filename}]]`)
151
+ )
152
+ }
153
+
154
+ module.exports = { newNote, appendNoteToIndex, noteFiles, indexContains, toSlug, today }