jarvis-ai-framework 3.2.0 → 5.0.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 (66) hide show
  1. package/agents/engineering/eng.docs-consolidator.agent.md +81 -0
  2. package/agents/engineering/qa/eng.qa.test-planner.md +7 -7
  3. package/agents/product/prod.discovery-interviewer.md +1 -1
  4. package/bin/README.md +14 -14
  5. package/bin/commands/docs-publish.js +61 -93
  6. package/bin/commands/docs-sync.js +31 -63
  7. package/bin/lib/cli/program.js +23 -25
  8. package/bin/lib/docs/fetch-file.js +29 -26
  9. package/bin/lib/docs/publish-file.js +98 -159
  10. package/bin/lib/docs/storage/build-index.js +108 -0
  11. package/bin/lib/docs/storage/filesystem.js +203 -0
  12. package/bin/lib/docs/storage/index.js +75 -0
  13. package/bin/lib/docs/validate-frontmatter.js +18 -4
  14. package/bin/lib/vcs/api.js +0 -125
  15. package/package.json +2 -2
  16. package/rules/AGENTS.md +12 -9
  17. package/rules/engineering/eng.docs-scraping-rules.md +4 -4
  18. package/rules/engineering/eng.integrations-rules.md +3 -2
  19. package/rules/engineering/rpa/eng.rpa-rules.md +1 -1
  20. package/rules/product/prod-rules.md +5 -6
  21. package/rules-on-demand/engineering/eng.pr-rules.md +9 -31
  22. package/rules-on-demand/engineering/eng.pre-pr-rules.md +12 -19
  23. package/rules-on-demand/product/prod.guided-rules.md +104 -0
  24. package/skills/eng-backend-arch-c4/SKILL.md +3 -3
  25. package/skills/eng-global-docs-write/SKILL.md +1 -1
  26. package/skills/eng-qa-planner/SKILL.md +5 -5
  27. package/skills/jarvis-docs-central/README.md +51 -275
  28. package/skills/jarvis-docs-central/SKILL.md +114 -301
  29. package/skills/jarvis-init/SKILL.md +81 -8
  30. package/templates/ENV-template.md +28 -7
  31. package/templates/engineering/ARD-template.md +40 -7
  32. package/templates/engineering/architecture-template.md +1 -0
  33. package/templates/engineering/central-delivery-template.md +65 -0
  34. package/templates/engineering/central-feature-template.md +86 -0
  35. package/templates/product/prod-breakdown-template.md +20 -2
  36. package/templates/product/prod-discovery-template.md +24 -1
  37. package/templates/product/prod-epic-template.md +32 -1
  38. package/templates/product/prod-frd-template.md +29 -2
  39. package/templates/product/prod-issue-template.md +36 -1
  40. package/templates/product/prod-prd-template.md +33 -2
  41. package/workflows/engineering/eng.build-tech-spec.md +4 -4
  42. package/workflows/engineering/eng.create-ard-from-code.md +15 -41
  43. package/workflows/engineering/eng.create-ard.md +22 -44
  44. package/workflows/engineering/eng.create-rfc.md +10 -36
  45. package/workflows/engineering/eng.pr.md +35 -66
  46. package/workflows/engineering/eng.pre-pr.md +52 -89
  47. package/workflows/engineering/eng.rpa.robot.md +9 -7
  48. package/workflows/engineering/eng.spec-consolidate.md +140 -0
  49. package/workflows/engineering/eng.start.md +23 -22
  50. package/workflows/engineering/eng.work.md +1 -1
  51. package/workflows/engineering/qa/eng.qa-refinement-entry.md +1 -1
  52. package/workflows/engineering/qa/eng.qa-release-signoff.md +3 -3
  53. package/workflows/engineering/qa/eng.qa-sprint-planning.md +2 -2
  54. package/workflows/product/prod.roadmap.preview.md +4 -1
  55. package/workflows/product/prod.spec.ard.md +9 -5
  56. package/workflows/product/prod.spec.breakdown.md +8 -4
  57. package/workflows/product/prod.spec.clarify.md +5 -1
  58. package/workflows/product/prod.spec.discovery.md +7 -3
  59. package/workflows/product/prod.spec.epic.md +12 -40
  60. package/workflows/product/prod.spec.frd.md +13 -39
  61. package/workflows/product/prod.spec.issue.md +5 -1
  62. package/workflows/product/prod.spec.md +3 -1
  63. package/workflows/product/prod.spec.prd.md +13 -40
  64. package/workflows/warm-up.md +10 -12
  65. package/bin/lib/docs/redis-cache.js +0 -49
  66. package/bin/lib/vcs/fetch-raw.js +0 -42
@@ -28,7 +28,6 @@ import { tokens } from "../../commands/tokens.js";
28
28
  import { map } from "../../commands/map.js";
29
29
  import { runCreateMerge } from "../vcs/create-merge.js";
30
30
  import { runCreateIssue } from "../vcs/create-issue.js";
31
- import { runFetchRaw } from "../vcs/fetch-raw.js";
32
31
  import { runComment } from "../tasks/comment.js";
33
32
  import { fetchFile } from "../docs/fetch-file.js";
34
33
  import { formatIDEsList } from "../config/ide-config.js";
@@ -112,33 +111,38 @@ export function buildProgram() {
112
111
  const docs = cmd(program, "docs").description("Sincronizar ou publicar documentação");
113
112
 
114
113
  const docsSyncCmd = cmd(docs, "sync").description(
115
- "Sincronizar docs do central-docs (usado por /warm-up)"
114
+ "Sincronizar docs do Central docs pelo adaptador de CENTRAL_DOCS_STORAGE (usado por /warm-up)"
116
115
  );
117
116
  withEnvOptions(withLogOptions(docsSyncCmd)).action((options) => docsSync(toLegacyFlags(options)));
118
117
 
119
- cmd(docs, "fetch")
118
+ const docsFetchCmd = cmd(docs, "fetch")
120
119
  .description(
121
- "Buscar um arquivo do central-docs, com cache Redis best-effort (lê CENTRAL_DOCS_REPO do ambiente)"
120
+ "Buscar um arquivo do Central docs pelo adaptador de CENTRAL_DOCS_STORAGE (com git: pull antes de ler)"
122
121
  )
123
- .argument("<file-path>", "Path do arquivo dentro do repo central-docs")
124
- .argument("[ref]", "Branch/ref", "main")
125
- .action(async (filePath, ref) => {
126
- try {
127
- const content = await fetchFile(filePath, ref);
128
- process.stdout.write(content);
129
- } catch (error) {
130
- console.error(error.message);
131
- process.exit(error.code === 404 ? 2 : 1);
132
- }
133
- });
122
+ .argument("<file-path>", "Caminho do arquivo dentro do Central docs (ex: CORE/index.md)");
123
+ withEnvOptions(docsFetchCmd).action(async (filePath, options) => {
124
+ try {
125
+ const content = await fetchFile(filePath, { flags: toLegacyFlags(options) });
126
+ process.stdout.write(content);
127
+ } catch (error) {
128
+ console.error(error.message);
129
+ process.exit(error.code === 404 ? 2 : 1);
130
+ }
131
+ });
134
132
 
135
133
  const docsPublishCmd = cmd(docs, "publish")
136
- .description("Publicar documento no central-docs (branch + MR/PR)")
134
+ .description(
135
+ "Publicar a consolidação (feature/delivery) no Central docs (pasta: copia e regenera o índice; com git: branch + PR)"
136
+ )
137
137
  .option("--file <path>", "Caminho do arquivo local")
138
- .option("--tipo <tipo>", "prd|frd|ard|rfc|swagger|qa-report")
139
- .option("--feature <slug>", "Slug da feature (kebab-case)")
138
+ .option("--tipo <tipo>", "feature|delivery (consolidação do /eng.spec-consolidate)")
139
+ .option("--produto <produto>", "Pasta do produto no Central docs (padrão: WORKSPACE do ENV.md)")
140
+ .option("--feature <slug>", "Só se o nome do arquivo fugir do padrão {tipo}-{nome}.md")
140
141
  .option("--squad <squad>", "Override do SQUAD do ENV.md")
141
- .option("--workspace <workspace>", "Override do WORKSPACE do ENV.md");
142
+ .option(
143
+ "--workspace <workspace>",
144
+ "Override do WORKSPACE do ENV.md (mesmo efeito de --produto)"
145
+ );
142
146
  withEnvOptions(withLogOptions(docsPublishCmd)).action((options) =>
143
147
  docsPublish(toLegacyFlags(options))
144
148
  );
@@ -201,12 +205,6 @@ export function buildProgram() {
201
205
  runCreateIssue({ repo: options.repo, title: options.title, bodyFile: options.bodyFile })
202
206
  );
203
207
 
204
- cmd(vcs, "fetch-raw")
205
- .description("Buscar o conteúdo bruto de um arquivo no central-docs (lê CENTRAL_DOCS_REPO)")
206
- .argument("[file-path]")
207
- .argument("[ref]", "Branch/ref", "main")
208
- .action((filePath, ref) => runFetchRaw(filePath, ref));
209
-
210
208
  const tasks = cmd(program, "tasks").description("Adapters de task manager");
211
209
 
212
210
  cmd(tasks, "comment")
@@ -1,39 +1,42 @@
1
1
  /**
2
- * @fileoverview Busca um arquivo do central-docs (GitLab/GitHub/Bitbucket), com
3
- * cache Redis best-effort. Substitui `fetch-file.sh` (removido) — mesmo
4
- * comportamento, sem subprocess: chama `fetchRawFile` de `vcs/api.js` direto.
2
+ * @fileoverview Leitura do Central docs pelo adaptador configurado
3
+ * (`CENTRAL_DOCS_STORAGE`), venha o conteúdo de onde vier.
5
4
  * @module docs/fetch-file
6
5
  */
7
6
 
8
- import { fetchRawFile } from "../vcs/api.js";
9
- import { redisGet, redisSetex } from "./redis-cache.js";
7
+ import { loadEnv, resolveEnvPath } from "../env-loader.js";
8
+ import { getStorage } from "./storage/index.js";
10
9
 
11
10
  /**
12
- * @param {string} filePath - Path do arquivo dentro do repo central-docs
13
- * @param {string} [ref] - Branch/ref (padrão: "main")
11
+ * Carrega o ENV.md e devolve o adaptador do Central docs.
12
+ * @param {Object} [flags] - `envFile`/`ide` (mesmas opções dos outros comandos)
14
13
  * @param {Object} [opts]
15
- * @param {string} [opts.repo] - CENTRAL_DOCS_REPO (padrão: `process.env.CENTRAL_DOCS_REPO`)
16
- * @param {string|number} [opts.cacheTtl] - TTL do cache em segundos (padrão: `process.env.CENTRAL_DOCS_CACHE_TTL` ou 3600)
17
- * @returns {Promise<string>} conteúdo do arquivo
18
- * @throws {Error} sem `filePath`, sem `repo`, ou erro do adapter (404/401/403 — ver `fetchRawFile`)
14
+ * @param {string} [opts.cwd]
15
+ * @param {Object} [opts.deps] - Dependências injetáveis do adaptador (testes)
16
+ * @returns {{ env: Record<string, string>, root: string, storage: Object|null }}
19
17
  */
20
- export async function fetchFile(filePath, ref = "main", opts = {}) {
18
+ export function openStorageFromEnv(flags = {}, { cwd = process.cwd(), deps } = {}) {
19
+ const { path, root } = resolveEnvPath(cwd, flags);
20
+ const env = loadEnv(cwd, path);
21
+ return { env, root, storage: getStorage(env, { root, deps }) };
22
+ }
23
+
24
+ /**
25
+ * Lê um arquivo do Central docs.
26
+ * @param {string} filePath - Caminho dentro do Central docs (ex: `CORE/index.md`)
27
+ * @param {Object} [opts]
28
+ * @param {Object} [opts.storage] - Adaptador já aberto; sem ele, abre pelo ENV.md
29
+ * @param {Object} [opts.flags]
30
+ * @returns {Promise<string>}
31
+ * @throws {Error} sem `filePath`, Central docs desligado, ou `code = 404`
32
+ */
33
+ export async function fetchFile(filePath, opts = {}) {
21
34
  if (!filePath) {
22
35
  throw new Error("fetchFile: file-path é obrigatório");
23
36
  }
24
-
25
- const repo = opts.repo ?? process.env.CENTRAL_DOCS_REPO;
26
- if (!repo) {
27
- throw new Error("CENTRAL_DOCS_REPO não definido no ENV.md");
37
+ const storage = opts.storage ?? openStorageFromEnv(opts.flags).storage;
38
+ if (!storage) {
39
+ throw new Error("Central docs desligado (CENTRAL_DOCS_STORAGE vazio no ENV.md).");
28
40
  }
29
-
30
- const cacheKey = `docs:${filePath}:${ref}`;
31
- const cached = redisGet(cacheKey);
32
- if (cached) return cached;
33
-
34
- const content = await fetchRawFile(repo, filePath, ref);
35
-
36
- redisSetex(cacheKey, opts.cacheTtl ?? process.env.CENTRAL_DOCS_CACHE_TTL ?? "3600", content);
37
-
38
- return content;
41
+ return storage.fetch(filePath);
39
42
  }
@@ -1,194 +1,133 @@
1
1
  /**
2
- * @fileoverview Publica um documento no central-docs (branch + commit + MR/PR).
3
- * Substitui `publish-file.sh` (removido) — chama os adapters de `vcs/api.js`
4
- * direto, sem subprocess, sem arquivo temporário para passar JSON/body.
2
+ * @fileoverview Publica no Central docs, pelo adaptador configurado
3
+ * (`CENTRAL_DOCS_STORAGE`), a consolidação do que foi implementado, gerada pelo
4
+ * `/eng.spec-consolidate`: o documento da funcionalidade (`feature-*.md`, em
5
+ * `{SQUAD}/product/{produto}/`) e o registro da entrega (`delivery-*.md`, em
6
+ * `{SQUAD}/engineering/{produto}/`). Documentos de trabalho (PRD, FRD, ARD, RFC…)
7
+ * não são publicados. O índice do squad é regenerado a cada publicação.
5
8
  * @module docs/publish-file
6
9
  */
7
10
 
8
- import { readFileSync } from "node:fs";
9
- import { validateFrontmatter, extractMetadata } from "./validate-frontmatter.js";
10
- import { commitFiles, createMergeRequest } from "../vcs/api.js";
11
- import { fetchFile } from "./fetch-file.js";
12
- import { redisDel } from "./redis-cache.js";
11
+ import { basename, extname } from "node:path";
12
+ import { validateFrontmatter } from "./validate-frontmatter.js";
13
13
  import { logger } from "../utils/logger.js";
14
14
 
15
- const VALID_TYPES = ["prd", "frd", "ard", "rfc", "swagger", "qa-report"];
16
-
17
- /**
18
- * @param {{ tipo: string, squad: string, workspace: string, feature: string, localFile: string }} p
19
- * @returns {{ destPath: string, area: "product"|"engineering" }}
20
- */
21
- export function resolveDestPath({ tipo, squad, workspace, feature, localFile }) {
22
- const base = `${squad}/${workspace}`;
23
-
24
- if (tipo === "prd" || tipo === "frd") {
25
- return { destPath: `${base}/product/${tipo}-${feature}.md`, area: "product" };
26
- }
27
- if (tipo === "ard") {
28
- return { destPath: `${base}/engineering/ARD/${tipo}-${feature}.md`, area: "engineering" };
29
- }
30
- if (tipo === "rfc") {
31
- return { destPath: `${base}/engineering/RFC/${tipo}-${feature}.md`, area: "engineering" };
32
- }
33
- if (tipo === "swagger") {
34
- const ext = localFile.split(".").pop().toLowerCase();
35
- const swaggerExt = ext === "yaml" || ext === "yml" ? ext : ext === "json" ? "json" : null;
36
- const destPath = swaggerExt
37
- ? `${base}/engineering/swagger/api-${feature}.${swaggerExt}`
38
- : `${base}/engineering/swagger/swagger-${feature}.md`; // default: .md (wrapper com metadados)
39
- return { destPath, area: "engineering" };
40
- }
41
- if (tipo === "qa-report") {
42
- return { destPath: `${base}/engineering/qa/qa-report-${feature}.md`, area: "engineering" };
15
+ /** @constant {Readonly<Record<string, "product"|"engineering">>} Área de cada tipo */
16
+ export const AREA_BY_TYPE = Object.freeze({
17
+ feature: "product",
18
+ delivery: "engineering",
19
+ });
20
+
21
+ /** @constant {ReadonlyArray<string>} Documentos de trabalho, que não vão mais para o Central docs */
22
+ export const WORKING_DOC_TYPES = Object.freeze([
23
+ "prd",
24
+ "frd",
25
+ "feat",
26
+ "epic",
27
+ "story",
28
+ "task",
29
+ "ard",
30
+ "rfc",
31
+ "swagger",
32
+ "qa-report",
33
+ ]);
34
+
35
+ /** @constant {ReadonlyArray<string>} Tipos aceitos pelo `jarvis docs publish` */
36
+ export const VALID_TYPES = Object.freeze(Object.keys(AREA_BY_TYPE));
37
+
38
+ const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
39
+
40
+ function assertSegment(label, value) {
41
+ if (!value || !SEGMENT.test(value)) {
42
+ throw new Error(
43
+ `${label} inválido: "${value ?? ""}" (use letras, números, "-" ou "_", sem "/" nem "..")`
44
+ );
43
45
  }
44
-
45
- throw new Error(`Tipo inválido: ${tipo}\nTipos válidos: ${VALID_TYPES.join(", ")}`);
46
46
  }
47
47
 
48
48
  /**
49
- * Insere `newLine` logo após o header `## {area}` — sem nunca descartar uma
50
- * linha existente. O `publish-file.sh` original usava `getline` do AWK para
51
- * "pular" a linha seguinte ao header antes de imprimir a nova entrada; essa
52
- * linha pulada nunca era reimpressa, o que apagava silenciosamente a entrada
53
- * anterior do índice na segunda publicação numa mesma seção (reproduzido e
54
- * documentado em `architecture.md` desta sessão, seção 3.4).
55
- * @param {string} content
56
- * @param {"product"|"engineering"} area
57
- * @param {string} newLine
58
- * @returns {{ content: string, found: boolean }}
49
+ * Calcula o destino do documento no Central docs.
50
+ * O nome é o do arquivo local quando ele já segue `{tipo}-…` (ex: `feature-checkout.md`,
51
+ * `delivery-2026-10-15-task-123.md`); senão vem de `--feature` (`{tipo}-{feature}.md`).
52
+ *
53
+ * @param {Object} p
54
+ * @param {string} p.tipo
55
+ * @param {string} p.squad
56
+ * @param {string} p.produto - Padrão: WORKSPACE (o caller resolve)
57
+ * @param {string} p.localFile
58
+ * @param {string} [p.feature]
59
+ * @returns {{ destPath: string, area: "product"|"engineering", fileName: string }}
59
60
  */
60
- export function insertIndexLine(content, area, newLine) {
61
- const lines = content.split("\n");
62
- const headerPattern = /^## (product|engineering)\b/;
63
-
64
- for (let i = 0; i < lines.length; i++) {
65
- const match = lines[i].match(headerPattern);
66
- if (match && match[1] === area) {
67
- lines.splice(i + 1, 0, newLine);
68
- return { content: lines.join("\n"), found: true };
69
- }
61
+ export function resolveDestPath({ tipo, squad, produto, localFile, feature }) {
62
+ const area = AREA_BY_TYPE[tipo];
63
+ if (!area && WORKING_DOC_TYPES.includes(tipo)) {
64
+ throw new Error(
65
+ `O Central docs guarda só o que foi implementado: "${tipo}" é um documento de trabalho e fica em docs/jarvis/.\n` +
66
+ "Gere a consolidação com /eng.spec-consolidate (tipos: feature, delivery)."
67
+ );
68
+ }
69
+ if (!area) {
70
+ throw new Error(`Tipo inválido: ${tipo}\nTipos válidos: ${VALID_TYPES.join(", ")}`);
71
+ }
72
+ assertSegment("Squad", squad);
73
+ assertSegment("Produto", produto);
74
+
75
+ const local = basename(localFile);
76
+ const ext = extname(local).toLowerCase() || ".md";
77
+ const followsPattern = local.toLowerCase().startsWith(`${tipo}-`) && ext === ".md";
78
+
79
+ let fileName;
80
+ if (followsPattern) {
81
+ fileName = local;
82
+ } else if (feature) {
83
+ assertSegment("Feature", feature);
84
+ fileName = `${tipo}-${feature}${ext}`;
85
+ } else {
86
+ throw new Error(
87
+ `O arquivo "${local}" não segue o padrão ${tipo}-{nome}.md.\n` +
88
+ `Renomeie o arquivo ou informe --feature <slug>.`
89
+ );
70
90
  }
71
91
 
72
- const trimmed = content.replace(/\n+$/, "");
73
- return { content: `${trimmed}\n\n## ${area}\n${newLine}\n`, found: false };
92
+ return { destPath: `${squad}/${area}/${produto}/${fileName}`, area, fileName };
74
93
  }
75
94
 
76
95
  /**
77
96
  * @param {Object} opts
78
- * @param {string} opts.localFile - Caminho do arquivo local (ex: `./docs/engineering/ard-x.md`)
79
- * @param {string} opts.tipo - prd|frd|ard|rfc|swagger|qa-report
80
- * @param {string} opts.feature - Slug da feature (kebab-case)
81
- * @param {string} opts.squad - Squad (já resolvido pelo caller: override ou ENV.md)
82
- * @param {string} opts.workspace - Workspace (já resolvido pelo caller: override ou ENV.md)
83
- * @param {string} opts.repo - CENTRAL_DOCS_REPO
84
- * @param {string} [opts.targetBranch] - Branch base do MR/PR (padrão: "dev")
85
- * @param {string|number} [opts.cacheTtl] - TTL do cache do index.md em segundos (padrão: 3600)
86
- * @returns {Promise<{ sha: string, mr: { vendor: string, iid: number|string, url: string } }>}
97
+ * @param {Object} opts.storage - Adaptador (`getStorage`)
98
+ * @param {string} opts.localFile
99
+ * @param {string} opts.tipo
100
+ * @param {string} opts.squad
101
+ * @param {string} opts.produto
102
+ * @param {string} [opts.feature]
103
+ * @returns {Promise<{ destPath: string, indexPath: string, sha?: string, branch?: string, mr?: Object }>}
87
104
  */
88
- export async function publishFile(opts) {
89
- const {
90
- localFile,
91
- tipo,
92
- feature,
93
- squad,
94
- workspace,
95
- repo,
96
- targetBranch = "dev",
97
- cacheTtl = "3600",
98
- } = opts;
99
-
100
- if (!repo) {
105
+ export async function publishFile({ storage, localFile, tipo, squad, produto, feature }) {
106
+ if (!storage) {
101
107
  throw new Error(
102
- "CENTRAL_DOCS_REPO não configurado no ENV.md\n\nConfigure a URL do repositório central:\n CENTRAL_DOCS_REPO=https://gitlab.com/org/central-docs.git"
108
+ "Central docs desligado (CENTRAL_DOCS_STORAGE vazio). Configure-o no /jarvis-init."
103
109
  );
104
110
  }
105
111
 
112
+ const { destPath } = resolveDestPath({ tipo, squad, produto, localFile, feature });
113
+
106
114
  logger.info("Validando frontmatter...");
107
115
  validateFrontmatter(localFile, tipo);
108
116
  logger.info("✅ Frontmatter válido");
109
117
 
110
- const metadata = extractMetadata(localFile);
111
- const version = metadata.version || "1.0.0";
112
- const status = metadata.status || "draft";
113
- const jira = metadata.jira || "";
114
- const docName = metadata.name || "";
115
-
116
- const { destPath, area } = resolveDestPath({ tipo, squad, workspace, feature, localFile });
117
- const indexPath = `${squad}/${workspace}/index.md`;
118
- const branchName = `docs/${squad}/${workspace}/${tipo}-${feature}`;
119
-
120
118
  logger.info("");
121
119
  logger.info("📋 Resumo da Publicação:");
122
- logger.info(` Squad: ${squad}`);
123
- logger.info(` Workspace: ${workspace}`);
124
- logger.info(` Tipo: ${tipo}`);
125
- logger.info(` Feature: ${feature}`);
120
+ logger.info(` Armazenamento: ${storage.kind}${storage.isGit ? " (git)" : ""}`);
126
121
  logger.info(` Destino: ${destPath}`);
127
- logger.info(` Branch: ${branchName} → ${targetBranch}`);
128
122
  logger.info("");
129
123
 
130
- const fileContentBase64 = readFileSync(localFile).toString("base64");
131
-
132
- logger.info("Buscando index.md do workspace...");
133
- let indexContent = "";
134
- try {
135
- indexContent = await fetchFile(indexPath, targetBranch, { repo, cacheTtl });
136
- } catch {
137
- indexContent = "";
138
- }
124
+ const result = await storage.publish({ localFile, destPath, squad, tipo });
139
125
 
140
- let indexAction;
141
- if (!indexContent) {
142
- logger.info("⚠️ index.md não encontrado, será criado");
143
- indexAction = "create";
144
- indexContent = `# ${workspace}\n\n**Squad:** ${squad}\n\n## ${area}\n\n`;
145
- } else {
146
- indexAction = "update";
126
+ logger.info(`✅ Publicado em ${destPath}`);
127
+ logger.info(`🗂️ Índice regenerado: ${result.indexPath}`);
128
+ if (result.mr) {
129
+ logger.info(`🔀 Pull/Merge Request: ${result.mr.url}`);
147
130
  }
148
131
 
149
- let newLine = `- [${tipo}-${feature}.md](${area}/${tipo}-${feature}.md) | v${version} | ${status}`;
150
- if (jira) newLine += ` | jira: ${jira}`;
151
- if (docName) newLine += ` | ${docName}`;
152
-
153
- const { content: updatedIndex } = insertIndexLine(indexContent, area, newLine);
154
- const updatedIndexBase64 = Buffer.from(updatedIndex, "utf-8").toString("base64");
155
-
156
- logger.info("Criando branch e commit...");
157
- const commitResult = await commitFiles(repo, {
158
- branch: branchName,
159
- startBranch: targetBranch,
160
- message: `docs(${squad}): add ${tipo} ${feature}`,
161
- files: [
162
- { path: destPath, contentBase64: fileContentBase64, action: "create" },
163
- { path: indexPath, contentBase64: updatedIndexBase64, action: indexAction },
164
- ],
165
- });
166
- logger.info(`✅ Commit criado: ${commitResult.sha}`);
167
-
168
- logger.info("Criando Merge/Pull Request...");
169
- let mrDescription = `## Documento\n\n- **Tipo**: ${tipo}\n- **Workspace**: ${workspace}\n- **Feature**: ${feature}`;
170
- if (jira) mrDescription += `\n- **Card**: ${jira}`;
171
- if (docName) mrDescription += `\n- **Nome**: ${docName}`;
172
-
173
- const mr = await createMergeRequest(repo, {
174
- source: branchName,
175
- target: targetBranch,
176
- title: `docs(${squad}): ${tipo} ${feature}`,
177
- description: mrDescription,
178
- });
179
-
180
- logger.info("");
181
- logger.info("✅ Merge/Pull Request criado com sucesso!");
182
- logger.info("");
183
- logger.info(` #${mr.iid}: ${branchName} → ${targetBranch}`);
184
- logger.info(` URL: ${mr.url}`);
185
- logger.info("");
186
-
187
- redisDel(`docs:${indexPath}:${targetBranch}`);
188
- redisDel(`docs:${indexPath}:main`);
189
- logger.info("🔄 Cache Redis invalidado");
190
- logger.info("");
191
- logger.info("✅ Publicação concluída!");
192
-
193
- return { sha: commitResult.sha, mr };
132
+ return result;
194
133
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview Gera o `{SQUAD}/index.md` do Central docs a partir do frontmatter
3
+ * da consolidação (funcionalidades e entregas). O índice é sempre regenerado pela listagem (nunca editado), para
4
+ * duas publicações seguidas não apagarem a entrada uma da outra.
5
+ * @module docs/storage/build-index
6
+ */
7
+
8
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
+ import { join, relative, sep } from "node:path";
10
+ import { parseFrontmatter } from "../../utils/frontmatter.js";
11
+
12
+ /** @constant {ReadonlyArray<string>} Áreas do Central docs, na ordem do índice */
13
+ export const AREAS = Object.freeze(["product", "engineering"]);
14
+
15
+ /**
16
+ * Lista arquivos (recursivo) de uma pasta, em ordem alfabética.
17
+ * @param {string} dir
18
+ * @returns {string[]} caminhos absolutos
19
+ */
20
+ export function walkFiles(dir) {
21
+ if (!existsSync(dir)) return [];
22
+ return readdirSync(dir)
23
+ .sort()
24
+ .flatMap((name) => {
25
+ const full = join(dir, name);
26
+ return statSync(full).isDirectory() ? walkFiles(full) : [full];
27
+ });
28
+ }
29
+
30
+ /** @constant {Readonly<Record<string, {prefix: string, title: string}>>} O que cada área lista no índice */
31
+ export const INDEXED = Object.freeze({
32
+ product: { prefix: "feature-", title: "Funcionalidades (estado atual)" },
33
+ engineering: { prefix: "delivery-", title: "Entregas recentes" },
34
+ });
35
+
36
+ /** @constant {number} Entregas listadas por produto (as mais recentes; o nome começa pela data) */
37
+ export const MAX_DELIVERIES_PER_PRODUCT = 10;
38
+
39
+ /**
40
+ * Monta o conteúdo do índice de um squad. Lista só a consolidação do
41
+ * `/eng.spec-consolidate`: funcionalidades (`feature-*`) e as entregas mais recentes
42
+ * (`delivery-*`). Outros arquivos (ex.: PRD/ARD publicados na 4.0.0) ficam fora.
43
+ * Formato: `# SQUAD` › `## área` › `### produto` › `- [arquivo](caminho) | status | nome | atualizado`.
44
+ * @param {string} root - Pasta do Central docs
45
+ * @param {string} squad
46
+ * @returns {string}
47
+ */
48
+ export function renderIndex(root, squad) {
49
+ const out = [`# ${squad}`, ""];
50
+
51
+ for (const area of AREAS) {
52
+ const { prefix, title } = INDEXED[area];
53
+ const areaDir = join(root, squad, area);
54
+ const files = walkFiles(areaDir).filter((f) => {
55
+ const name = f.split(sep).pop();
56
+ return name.startsWith(prefix) && name.endsWith(".md");
57
+ });
58
+ out.push(`## ${area}`, "", `> ${title}`, "");
59
+ if (files.length === 0) {
60
+ out.push("_Nenhum documento._", "");
61
+ continue;
62
+ }
63
+
64
+ const byProduct = new Map();
65
+ for (const file of files) {
66
+ const [product = "", ...rest] = relative(areaDir, file).split(sep);
67
+ if (rest.length === 0) continue; // arquivo solto na área, sem pasta de produto
68
+ if (!byProduct.has(product)) byProduct.set(product, []);
69
+ byProduct.get(product).push(file);
70
+ }
71
+
72
+ for (const [product, productFiles] of byProduct) {
73
+ const listed =
74
+ area === "engineering"
75
+ ? [...productFiles].sort().reverse().slice(0, MAX_DELIVERIES_PER_PRODUCT)
76
+ : productFiles;
77
+ out.push(`### ${product}`);
78
+ for (const file of listed) {
79
+ const rel = relative(join(root, squad), file).split(sep).join("/");
80
+ const name = rel.split("/").pop();
81
+ const meta = parseFrontmatter(readFileSync(file, "utf-8"));
82
+ let line = `- [${name}](${rel})`;
83
+ if (meta.status) line += ` | ${meta.status}`;
84
+ if (meta.name) line += ` | ${meta.name}`;
85
+ if (meta.task_key) line += ` | ${meta.task_key}`;
86
+ const when = meta.updated_at || meta.date;
87
+ if (when) line += ` | ${when}`;
88
+ out.push(line);
89
+ }
90
+ out.push("");
91
+ }
92
+ }
93
+
94
+ return out.join("\n").replace(/\n+$/, "\n");
95
+ }
96
+
97
+ /**
98
+ * Regenera e grava `{root}/{squad}/index.md`.
99
+ * @param {string} root - Pasta do Central docs
100
+ * @param {string} squad
101
+ * @returns {{ path: string, content: string }}
102
+ */
103
+ export function buildIndex(root, squad) {
104
+ const content = renderIndex(root, squad);
105
+ const path = join(root, squad, "index.md");
106
+ writeFileSync(path, content);
107
+ return { path, content };
108
+ }