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.
- package/agents/engineering/eng.docs-consolidator.agent.md +81 -0
- package/agents/engineering/qa/eng.qa.test-planner.md +7 -7
- package/agents/product/prod.discovery-interviewer.md +1 -1
- package/bin/README.md +14 -14
- package/bin/commands/docs-publish.js +61 -93
- package/bin/commands/docs-sync.js +31 -63
- package/bin/lib/cli/program.js +23 -25
- package/bin/lib/docs/fetch-file.js +29 -26
- package/bin/lib/docs/publish-file.js +98 -159
- package/bin/lib/docs/storage/build-index.js +108 -0
- package/bin/lib/docs/storage/filesystem.js +203 -0
- package/bin/lib/docs/storage/index.js +75 -0
- package/bin/lib/docs/validate-frontmatter.js +18 -4
- package/bin/lib/vcs/api.js +0 -125
- package/package.json +2 -2
- package/rules/AGENTS.md +12 -9
- package/rules/engineering/eng.docs-scraping-rules.md +4 -4
- package/rules/engineering/eng.integrations-rules.md +3 -2
- package/rules/engineering/rpa/eng.rpa-rules.md +1 -1
- package/rules/product/prod-rules.md +5 -6
- package/rules-on-demand/engineering/eng.pr-rules.md +9 -31
- package/rules-on-demand/engineering/eng.pre-pr-rules.md +12 -19
- package/rules-on-demand/product/prod.guided-rules.md +104 -0
- package/skills/eng-backend-arch-c4/SKILL.md +3 -3
- package/skills/eng-global-docs-write/SKILL.md +1 -1
- package/skills/eng-qa-planner/SKILL.md +5 -5
- package/skills/jarvis-docs-central/README.md +51 -275
- package/skills/jarvis-docs-central/SKILL.md +114 -301
- package/skills/jarvis-init/SKILL.md +81 -8
- package/templates/ENV-template.md +28 -7
- package/templates/engineering/ARD-template.md +40 -7
- package/templates/engineering/architecture-template.md +1 -0
- package/templates/engineering/central-delivery-template.md +65 -0
- package/templates/engineering/central-feature-template.md +86 -0
- package/templates/product/prod-breakdown-template.md +20 -2
- package/templates/product/prod-discovery-template.md +24 -1
- package/templates/product/prod-epic-template.md +32 -1
- package/templates/product/prod-frd-template.md +29 -2
- package/templates/product/prod-issue-template.md +36 -1
- package/templates/product/prod-prd-template.md +33 -2
- package/workflows/engineering/eng.build-tech-spec.md +4 -4
- package/workflows/engineering/eng.create-ard-from-code.md +15 -41
- package/workflows/engineering/eng.create-ard.md +22 -44
- package/workflows/engineering/eng.create-rfc.md +10 -36
- package/workflows/engineering/eng.pr.md +35 -66
- package/workflows/engineering/eng.pre-pr.md +52 -89
- package/workflows/engineering/eng.rpa.robot.md +9 -7
- package/workflows/engineering/eng.spec-consolidate.md +140 -0
- package/workflows/engineering/eng.start.md +23 -22
- package/workflows/engineering/eng.work.md +1 -1
- package/workflows/engineering/qa/eng.qa-refinement-entry.md +1 -1
- package/workflows/engineering/qa/eng.qa-release-signoff.md +3 -3
- package/workflows/engineering/qa/eng.qa-sprint-planning.md +2 -2
- package/workflows/product/prod.roadmap.preview.md +4 -1
- package/workflows/product/prod.spec.ard.md +9 -5
- package/workflows/product/prod.spec.breakdown.md +8 -4
- package/workflows/product/prod.spec.clarify.md +5 -1
- package/workflows/product/prod.spec.discovery.md +7 -3
- package/workflows/product/prod.spec.epic.md +12 -40
- package/workflows/product/prod.spec.frd.md +13 -39
- package/workflows/product/prod.spec.issue.md +5 -1
- package/workflows/product/prod.spec.md +3 -1
- package/workflows/product/prod.spec.prd.md +13 -40
- package/workflows/warm-up.md +10 -12
- package/bin/lib/docs/redis-cache.js +0 -49
- package/bin/lib/vcs/fetch-raw.js +0 -42
package/bin/lib/cli/program.js
CHANGED
|
@@ -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
|
|
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
|
|
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>", "
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
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>", "
|
|
139
|
-
.option("--
|
|
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(
|
|
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
|
|
3
|
-
*
|
|
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 {
|
|
9
|
-
import {
|
|
7
|
+
import { loadEnv, resolveEnvPath } from "../env-loader.js";
|
|
8
|
+
import { getStorage } from "./storage/index.js";
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param {
|
|
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.
|
|
16
|
-
* @param {
|
|
17
|
-
* @returns {
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
9
|
-
import { validateFrontmatter
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @param {string}
|
|
56
|
-
* @param {
|
|
57
|
-
* @param {string}
|
|
58
|
-
* @
|
|
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
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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 {
|
|
79
|
-
* @param {string} opts.
|
|
80
|
-
* @param {string} opts.
|
|
81
|
-
* @param {string} opts.squad
|
|
82
|
-
* @param {string} opts.
|
|
83
|
-
* @param {string} opts.
|
|
84
|
-
* @
|
|
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(
|
|
89
|
-
|
|
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
|
-
"
|
|
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(`
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
+
}
|