@fontdata/ai-harness 3.3.1 → 4.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.
@@ -0,0 +1,325 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, join } from 'node:path';
3
+ export const VERSAO_CONFIG_PATH = join('.ai-harness', 'versao.json');
4
+ export const EXTRATORES_SIMPLES = ['dproj', 'pubspec', 'pyproject', 'csproj', 'versoes-txt'];
5
+ export const EXTRATORES_ACEITOS = ['json:<caminho>', 'dproj', 'regex', 'pubspec', 'pyproject', 'csproj', 'versoes-txt'];
6
+ export const BUMPS_PADRAO = ['auto', 'major', 'minor', 'patch', 'build'];
7
+ export const SEGMENTOS_MIN = 1;
8
+ export const SEGMENTOS_MAX = 4;
9
+ export class VersaoConfigErro extends Error {
10
+ problemas;
11
+ constructor(problemas, origem = 'versao.json') {
12
+ super(`${origem} inválido:\n${problemas.map((p) => `- ${p}`).join('\n')}`);
13
+ this.problemas = problemas;
14
+ this.name = 'VersaoConfigErro';
15
+ }
16
+ }
17
+ const CAMPOS_RAIZ = ['$schema', 'arquivos', 'tag', 'segmentos', 'formato', 'bump', 'apps', 'changelogs', 'gerador', 'naoTocar', 'commit', 'validar'];
18
+ const CAMPOS_ARQUIVO = ['caminho', 'extrator', 'padrao', 'formato', 'padding'];
19
+ const CAMPOS_APP = ['caminhos', 'arquivos', 'tag', 'changelogs'];
20
+ const CAMPOS_BUMP = ['padrao'];
21
+ const MODELO_FORMATO = /^[^{}]*(?:\{[1-9]\d*\}[^{}]*)+$/;
22
+ const MARCADORES_TAG = new Set(['{versao}', '{app}']);
23
+ function isObject(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+ /**
27
+ * Converte o caminho do extrator `json:` numa lista de chaves; `null` quando a sintaxe é inválida.
28
+ * Chaves separadas por ponto; `["..."]` (string JSON) aceita qualquer chave, inclusive vazia ou com ponto.
29
+ */
30
+ export function parseCaminhoJson(texto) {
31
+ const chaves = [];
32
+ let i = 0;
33
+ while (i < texto.length) {
34
+ const resto = texto.slice(i);
35
+ const colchete = /^\[("(?:[^"\\]|\\.)*")\]/.exec(resto);
36
+ if (colchete) {
37
+ try {
38
+ chaves.push(JSON.parse(colchete[1]));
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ i += colchete[0].length;
44
+ }
45
+ else {
46
+ const nome = /^[^.[\]"]+/.exec(resto);
47
+ if (!nome)
48
+ return null;
49
+ chaves.push(nome[0]);
50
+ i += nome[0].length;
51
+ }
52
+ if (i === texto.length || texto[i] === '[')
53
+ continue;
54
+ if (texto[i] !== '.')
55
+ return null;
56
+ i += 1;
57
+ if (i === texto.length || texto[i] === '[' || texto[i] === '.')
58
+ return null;
59
+ }
60
+ return chaves.length > 0 ? chaves : null;
61
+ }
62
+ function contarGrupos(padrao) {
63
+ return (new RegExp(`${padrao}|`).exec('')?.length ?? 1) - 1;
64
+ }
65
+ function contarSegmentosDoFormato(formato) {
66
+ return formato.match(/\{\d+\}/g)?.length ?? 0;
67
+ }
68
+ class Validador {
69
+ problemas = [];
70
+ erro(caminho, mensagem) {
71
+ this.problemas.push(`${caminho}: ${mensagem}`);
72
+ }
73
+ camposDesconhecidos(obj, aceitos, prefixo) {
74
+ for (const chave of Object.keys(obj)) {
75
+ if (!aceitos.includes(chave)) {
76
+ this.erro(prefixo ? `${prefixo}.${chave}` : chave, `campo desconhecido; os aceitos são ${aceitos.filter((c) => c !== '$schema').join(', ')}`);
77
+ }
78
+ }
79
+ }
80
+ texto(valor, caminho) {
81
+ if (valor === undefined)
82
+ return undefined;
83
+ if (typeof valor !== 'string' || valor.trim() === '') {
84
+ this.erro(caminho, 'deve ser um texto não vazio');
85
+ return undefined;
86
+ }
87
+ return valor;
88
+ }
89
+ caminhoRelativo(valor, caminho) {
90
+ const texto = this.texto(valor, caminho);
91
+ if (texto === undefined)
92
+ return undefined;
93
+ if (isAbsolute(texto) || /^[A-Za-z]:[\\/]/.test(texto) || texto.split(/[\\/]/).includes('..')) {
94
+ this.erro(caminho, `deve ser relativo à raiz do projeto, sem "..": "${texto}"`);
95
+ return undefined;
96
+ }
97
+ return texto;
98
+ }
99
+ listaDeCaminhos(valor, caminho, obrigatoria = false) {
100
+ if (valor === undefined) {
101
+ if (obrigatoria)
102
+ this.erro(caminho, 'é obrigatório: lista de caminhos relativos à raiz');
103
+ return [];
104
+ }
105
+ if (!Array.isArray(valor)) {
106
+ this.erro(caminho, 'deve ser uma lista de caminhos');
107
+ return [];
108
+ }
109
+ if (obrigatoria && valor.length === 0)
110
+ this.erro(caminho, 'deve ter ao menos um caminho');
111
+ return valor.flatMap((item, indice) => this.caminhoRelativo(item, `${caminho}[${indice}]`) ?? []);
112
+ }
113
+ tag(valor, caminho) {
114
+ const tag = this.texto(valor, caminho);
115
+ if (tag === undefined)
116
+ return undefined;
117
+ const desconhecidos = (tag.match(/\{[^}]*\}/g) ?? []).filter((m) => !MARCADORES_TAG.has(m));
118
+ if (!tag.includes('{versao}'))
119
+ this.erro(caminho, `deve conter {versao}: "${tag}"`);
120
+ else if (desconhecidos.length > 0)
121
+ this.erro(caminho, `marcador desconhecido ${desconhecidos.join(', ')}; use {versao} e {app}`);
122
+ else
123
+ return tag;
124
+ return undefined;
125
+ }
126
+ formato(valor, caminho, segmentos) {
127
+ const formato = this.texto(valor, caminho);
128
+ if (formato === undefined)
129
+ return undefined;
130
+ if (!MODELO_FORMATO.test(formato)) {
131
+ this.erro(caminho, `deve ser um modelo como "{1}.{3}.{2}.{2}", com a largura de cada segmento entre chaves: "${formato}"`);
132
+ return undefined;
133
+ }
134
+ if (segmentos !== undefined && contarSegmentosDoFormato(formato) !== segmentos) {
135
+ this.erro(caminho, `tem ${contarSegmentosDoFormato(formato)} segmentos, mas segmentos é ${segmentos}`);
136
+ return undefined;
137
+ }
138
+ return formato;
139
+ }
140
+ padding(valor, caminho, segmentos) {
141
+ if (valor === undefined)
142
+ return undefined;
143
+ if (!Array.isArray(valor) || valor.some((n) => typeof n !== 'number' || !Number.isInteger(n) || n < 1)) {
144
+ this.erro(caminho, 'deve ser uma lista com a largura mínima (inteiro >= 1) de cada segmento');
145
+ return undefined;
146
+ }
147
+ if (segmentos !== undefined && valor.length !== segmentos) {
148
+ this.erro(caminho, `tem ${valor.length} larguras, mas segmentos é ${segmentos}`);
149
+ return undefined;
150
+ }
151
+ return valor;
152
+ }
153
+ extrator(item, caminho) {
154
+ const bruto = item.extrator;
155
+ const aceitos = `os aceitos são ${EXTRATORES_ACEITOS.join(', ')}`;
156
+ if (typeof bruto !== 'string' || bruto === '') {
157
+ this.erro(`${caminho}.extrator`, `é obrigatório; ${aceitos}`);
158
+ return undefined;
159
+ }
160
+ if (bruto !== 'regex' && item.padrao !== undefined)
161
+ this.erro(`${caminho}.padrao`, 'só vale com o extrator regex');
162
+ if (bruto.startsWith('json:')) {
163
+ const chaves = parseCaminhoJson(bruto.slice('json:'.length));
164
+ if (chaves)
165
+ return { tipo: 'json', caminho: chaves };
166
+ this.erro(`${caminho}.extrator`, `caminho JSON inválido em "${bruto}"; use chaves separadas por ponto e ["..."] para chave vazia ou com ponto, ex.: json:packages[""].version`);
167
+ return undefined;
168
+ }
169
+ if (bruto === 'regex')
170
+ return this.regex(item.padrao, `${caminho}.padrao`);
171
+ if (EXTRATORES_SIMPLES.includes(bruto))
172
+ return { tipo: bruto };
173
+ this.erro(`${caminho}.extrator`, `extrator desconhecido "${bruto}"; ${aceitos}`);
174
+ return undefined;
175
+ }
176
+ regex(valor, caminho) {
177
+ if (typeof valor !== 'string' || valor === '') {
178
+ this.erro(caminho, 'é obrigatório com o extrator regex: expressão com um grupo de captura para a versão');
179
+ return undefined;
180
+ }
181
+ let grupos;
182
+ try {
183
+ grupos = contarGrupos(valor);
184
+ }
185
+ catch (erro) {
186
+ this.erro(caminho, `expressão regular inválida: ${erro.message}`);
187
+ return undefined;
188
+ }
189
+ if (grupos < 1) {
190
+ this.erro(caminho, 'a expressão precisa de um grupo de captura com a versão, ex.: version = "([^"]+)"');
191
+ return undefined;
192
+ }
193
+ return { tipo: 'regex', padrao: valor };
194
+ }
195
+ arquivos(valor, caminho, segmentos) {
196
+ if (valor === undefined)
197
+ return [];
198
+ if (!Array.isArray(valor)) {
199
+ this.erro(caminho, 'deve ser uma lista de { caminho, extrator }');
200
+ return [];
201
+ }
202
+ return valor.flatMap((item, indice) => {
203
+ const prefixo = `${caminho}[${indice}]`;
204
+ if (!isObject(item)) {
205
+ this.erro(prefixo, 'deve ser um objeto { caminho, extrator }');
206
+ return [];
207
+ }
208
+ this.camposDesconhecidos(item, CAMPOS_ARQUIVO, prefixo);
209
+ if (item.caminho === undefined)
210
+ this.erro(`${prefixo}.caminho`, 'é obrigatório');
211
+ const arquivoCaminho = this.caminhoRelativo(item.caminho, `${prefixo}.caminho`);
212
+ const extrator = this.extrator(item, prefixo);
213
+ const formato = this.formato(item.formato, `${prefixo}.formato`, segmentos);
214
+ const padding = this.padding(item.padding, `${prefixo}.padding`, segmentos);
215
+ if (arquivoCaminho === undefined || extrator === undefined)
216
+ return [];
217
+ const arquivo = { caminho: arquivoCaminho, extrator };
218
+ if (formato !== undefined)
219
+ arquivo.formato = formato;
220
+ if (padding !== undefined)
221
+ arquivo.padding = padding;
222
+ return [arquivo];
223
+ });
224
+ }
225
+ app(nome, valor, segmentos) {
226
+ const prefixo = `apps.${nome}`;
227
+ if (!isObject(valor)) {
228
+ this.erro(prefixo, 'deve ser um objeto com caminhos, arquivos, tag e changelogs');
229
+ return undefined;
230
+ }
231
+ this.camposDesconhecidos(valor, CAMPOS_APP, prefixo);
232
+ const app = {
233
+ caminhos: this.listaDeCaminhos(valor.caminhos, `${prefixo}.caminhos`, true),
234
+ arquivos: this.arquivos(valor.arquivos, `${prefixo}.arquivos`, segmentos),
235
+ changelogs: this.listaDeCaminhos(valor.changelogs, `${prefixo}.changelogs`),
236
+ };
237
+ const tag = this.tag(valor.tag, `${prefixo}.tag`);
238
+ if (tag !== undefined)
239
+ app.tag = tag;
240
+ return app;
241
+ }
242
+ }
243
+ /** Valida o JSON bruto do `versao.json` e devolve a config normalizada; lança `VersaoConfigErro` com todos os problemas. */
244
+ export function validarVersaoConfig(bruto, origem) {
245
+ const v = new Validador();
246
+ if (!isObject(bruto))
247
+ throw new VersaoConfigErro(['a raiz deve ser um objeto JSON'], origem);
248
+ v.camposDesconhecidos(bruto, CAMPOS_RAIZ, '');
249
+ let segmentos;
250
+ if (bruto.segmentos !== undefined) {
251
+ const s = bruto.segmentos;
252
+ if (typeof s === 'number' && Number.isInteger(s) && s >= SEGMENTOS_MIN && s <= SEGMENTOS_MAX)
253
+ segmentos = s;
254
+ else
255
+ v.erro('segmentos', `deve ser um inteiro de ${SEGMENTOS_MIN} a ${SEGMENTOS_MAX}`);
256
+ }
257
+ let padrao = 'auto';
258
+ if (bruto.bump !== undefined) {
259
+ if (!isObject(bruto.bump))
260
+ v.erro('bump', 'deve ser um objeto { padrao }');
261
+ else {
262
+ v.camposDesconhecidos(bruto.bump, CAMPOS_BUMP, 'bump');
263
+ const p = bruto.bump.padrao;
264
+ if (p === undefined)
265
+ padrao = 'auto';
266
+ else if (BUMPS_PADRAO.includes(p))
267
+ padrao = p;
268
+ else
269
+ v.erro('bump.padrao', `valor "${String(p)}" inválido; use ${BUMPS_PADRAO.join(', ')}`);
270
+ }
271
+ }
272
+ const apps = {};
273
+ if (bruto.apps !== undefined) {
274
+ if (!isObject(bruto.apps))
275
+ v.erro('apps', 'deve ser um objeto { <nome>: { caminhos, ... } }');
276
+ else {
277
+ for (const [nome, valor] of Object.entries(bruto.apps)) {
278
+ if (nome.trim() === '') {
279
+ v.erro('apps', 'o nome do app não pode ser vazio');
280
+ continue;
281
+ }
282
+ const app = v.app(nome, valor, segmentos);
283
+ if (app)
284
+ apps[nome] = app;
285
+ }
286
+ }
287
+ }
288
+ const config = {
289
+ arquivos: v.arquivos(bruto.arquivos, 'arquivos', segmentos),
290
+ bump: { padrao },
291
+ apps,
292
+ changelogs: v.listaDeCaminhos(bruto.changelogs, 'changelogs'),
293
+ naoTocar: v.listaDeCaminhos(bruto.naoTocar, 'naoTocar'),
294
+ };
295
+ const opcionais = {
296
+ tag: v.tag(bruto.tag, 'tag'),
297
+ segmentos,
298
+ formato: v.formato(bruto.formato, 'formato', segmentos),
299
+ gerador: v.texto(bruto.gerador, 'gerador'),
300
+ commit: v.texto(bruto.commit, 'commit'),
301
+ validar: v.texto(bruto.validar, 'validar'),
302
+ };
303
+ for (const [chave, valor] of Object.entries(opcionais)) {
304
+ if (valor !== undefined)
305
+ Object.assign(config, { [chave]: valor });
306
+ }
307
+ if (v.problemas.length > 0)
308
+ throw new VersaoConfigErro(v.problemas, origem);
309
+ return config;
310
+ }
311
+ /** Lê `.ai-harness/versao.json` da raiz; `null` quando o arquivo não existe, erro quando é inválido. */
312
+ export function carregarVersaoConfig(raiz) {
313
+ const caminho = join(raiz, VERSAO_CONFIG_PATH);
314
+ if (!existsSync(caminho))
315
+ return null;
316
+ const origem = VERSAO_CONFIG_PATH.split('\\').join('/');
317
+ let bruto;
318
+ try {
319
+ bruto = JSON.parse(readFileSync(caminho, 'utf8').replace(/^\uFEFF/, ''));
320
+ }
321
+ catch (erro) {
322
+ throw new VersaoConfigErro([`JSON inválido: ${erro.message}`], origem);
323
+ }
324
+ return validarVersaoConfig(bruto, origem);
325
+ }
@@ -0,0 +1,332 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { SEGMENTOS_MAX, SEGMENTOS_MIN } from './config.js';
6
+ import { criarExtrator } from './extratores/index.js';
7
+ import { formatarVersao } from './formato.js';
8
+ const json = (...caminho) => ({ tipo: 'json', caminho });
9
+ const CANDIDATOS = [
10
+ { casa: /^package\.json$/, extratores: [json('version')] },
11
+ { casa: /^package-lock\.json$/, extratores: [json('version'), json('packages', '', 'version')] },
12
+ { casa: /^app\.json$/, extratores: [json('expo', 'version')] },
13
+ { casa: /^composer\.json$/, extratores: [json('version')] },
14
+ { casa: /^pubspec\.yaml$/, extratores: [{ tipo: 'pubspec' }] },
15
+ { casa: /^pyproject\.toml$/, extratores: [{ tipo: 'pyproject' }] },
16
+ { casa: /\.csproj$/i, extratores: [{ tipo: 'csproj' }] },
17
+ { casa: /\.dproj$/i, extratores: [{ tipo: 'dproj' }] },
18
+ { casa: /^vers(?:o|õ)es\.txt$/i, extratores: [{ tipo: 'versoes-txt' }] },
19
+ ];
20
+ const PASTAS_IGNORADAS = new Set(['node_modules', 'dist', 'build', '__history', '__recovery', 'bin', 'obj']);
21
+ const MODELOS_TAG = [
22
+ ['V{versao}', /^V\d+(?:\.\d+)+$/],
23
+ ['v{versao}', /^v\d+(?:\.\d+)+$/],
24
+ ['{versao}', /^\d+(?:\.\d+)+$/],
25
+ ['{app}-v{versao}', /^.+-v\d+(?:\.\d+)+$/],
26
+ ];
27
+ /** Texto do extrator como escrito no `versao.json` (inverso do `parseCaminhoJson`). */
28
+ export function extratorParaTexto(cfg) {
29
+ if (cfg.tipo !== 'json')
30
+ return cfg.tipo;
31
+ const caminho = cfg.caminho
32
+ .map((chave, i) => (/^[^.[\]"]+$/.test(chave) ? `${i > 0 ? '.' : ''}${chave}` : `[${JSON.stringify(chave)}]`))
33
+ .join('');
34
+ return `json:${caminho}`;
35
+ }
36
+ function juntar(base, nome) {
37
+ return base ? `${base}/${nome}` : nome;
38
+ }
39
+ function varrer(raiz, base, profundidade) {
40
+ let entradas;
41
+ try {
42
+ entradas = readdirSync(join(raiz, base), { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ entradas.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
48
+ const achados = [];
49
+ for (const e of entradas) {
50
+ if (e.isFile()) {
51
+ const candidato = CANDIDATOS.find((c) => c.casa.test(e.name));
52
+ for (const extrator of candidato?.extratores ?? [])
53
+ achados.push({ caminho: juntar(base, e.name), extrator });
54
+ }
55
+ }
56
+ if (profundidade > 0) {
57
+ for (const e of entradas) {
58
+ if (e.isDirectory() && !e.name.startsWith('.') && !PASTAS_IGNORADAS.has(e.name)) {
59
+ achados.push(...varrer(raiz, juntar(base, e.name), profundidade - 1));
60
+ }
61
+ }
62
+ }
63
+ return achados;
64
+ }
65
+ function lerFontes(raiz, arquivos, problemas) {
66
+ return arquivos.map((arquivo) => {
67
+ const fonte = { caminho: arquivo.caminho, extrator: arquivo.extrator, ocorrencias: [] };
68
+ if (arquivo.formato !== undefined)
69
+ fonte.formato = arquivo.formato;
70
+ if (arquivo.padding !== undefined)
71
+ fonte.padding = arquivo.padding;
72
+ const absoluto = join(raiz, arquivo.caminho);
73
+ const descricao = `${arquivo.caminho} (${extratorParaTexto(arquivo.extrator)})`;
74
+ if (!existsSync(absoluto)) {
75
+ problemas.push(`${descricao}: arquivo não encontrado`);
76
+ return fonte;
77
+ }
78
+ try {
79
+ fonte.ocorrencias = criarExtrator(arquivo.extrator).ler(arquivo.caminho, readFileSync(absoluto));
80
+ }
81
+ catch (erro) {
82
+ problemas.push(`${descricao}: ${erro.message}`);
83
+ return fonte;
84
+ }
85
+ if (fonte.ocorrencias.length === 0)
86
+ problemas.push(`${descricao}: versão não encontrada`);
87
+ return fonte;
88
+ });
89
+ }
90
+ function numeros(valor) {
91
+ const m = /^\d+(?:\.\d+)*/.exec(valor.trim());
92
+ return m ? m[0].split('.').map(Number) : null;
93
+ }
94
+ function chaveDeComparacao(valor) {
95
+ return valor.trim().replace(/\d+/g, (d) => d.replace(/^0+(?=\d)/, ''));
96
+ }
97
+ function compararNumeros(a, b) {
98
+ if (!a || !b)
99
+ return (a ? 1 : 0) - (b ? 1 : 0);
100
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
101
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
102
+ if (d !== 0)
103
+ return d;
104
+ }
105
+ return 0;
106
+ }
107
+ function formatoDoValor(valor) {
108
+ const v = valor.trim();
109
+ if (!/^\d+(?:\.\d+)+$/.test(v))
110
+ return null;
111
+ return v.split('.').map((s) => `{${s.length}}`).join('.');
112
+ }
113
+ function temZeroAEsquerda(valor) {
114
+ return formatoDoValor(valor) !== null && valor.trim().split('.').some((s) => /^0\d/.test(s));
115
+ }
116
+ /**
117
+ * A versão atual é a maior entre as ocorrências, escrita no `formato` quando há; as que não batem com ela,
118
+ * comparadas por segmento numérico (`5.000.05.01` = `5.0.5.1`), viram divergências.
119
+ */
120
+ function analisar(fontes, formato) {
121
+ const ocorrencias = fontes.flatMap((f) => f.ocorrencias);
122
+ let maior;
123
+ for (const o of ocorrencias) {
124
+ if (!maior || compararNumeros(numeros(o.valor), numeros(maior.valor)) > 0)
125
+ maior = o;
126
+ }
127
+ if (!maior)
128
+ return { versaoAtual: null, divergencias: [] };
129
+ let versaoAtual = maior.valor.trim();
130
+ if (formato && /^\d+(?:\.\d+)*$/.test(versaoAtual)) {
131
+ try {
132
+ versaoAtual = formatarVersao(numeros(versaoAtual), formato);
133
+ }
134
+ catch {
135
+ versaoAtual = maior.valor.trim();
136
+ }
137
+ }
138
+ const referencia = chaveDeComparacao(maior.valor);
139
+ const divergencias = ocorrencias
140
+ .filter((o) => chaveDeComparacao(o.valor) !== referencia)
141
+ .map((o) => ({
142
+ arquivo: o.arquivo,
143
+ grupo: o.grupo ?? null,
144
+ linha: o.linha,
145
+ valor: o.valor,
146
+ versaoAtual,
147
+ mensagem: `${o.arquivo}${o.grupo ? ` (${o.grupo})` : ''}, linha ${o.linha}: ${o.valor} difere da versão atual ${versaoAtual}`,
148
+ }));
149
+ return { versaoAtual, divergencias };
150
+ }
151
+ function inferirTag(raiz, padrao) {
152
+ const r = spawnSync('git', ['tag', '--list'], { cwd: raiz, encoding: 'utf8' });
153
+ if (r.error || r.status !== 0)
154
+ return padrao;
155
+ const contagem = new Map();
156
+ for (const tag of r.stdout.split('\n').map((t) => t.trim())) {
157
+ const modelo = MODELOS_TAG.find(([, regex]) => regex.test(tag))?.[0];
158
+ if (modelo)
159
+ contagem.set(modelo, (contagem.get(modelo) ?? 0) + 1);
160
+ }
161
+ let melhor = null;
162
+ for (const [modelo] of MODELOS_TAG) {
163
+ const n = contagem.get(modelo) ?? 0;
164
+ if (n > 0 && (!melhor || n > melhor[1]))
165
+ melhor = [modelo, n];
166
+ }
167
+ return melhor?.[0] ?? padrao;
168
+ }
169
+ function ehMonorepoPnpm(raiz) {
170
+ const caminho = join(raiz, 'pnpm-workspace.yaml');
171
+ if (!existsSync(caminho))
172
+ return false;
173
+ try {
174
+ const pacotes = parseYaml(readFileSync(caminho, 'utf8'))?.packages;
175
+ return Array.isArray(pacotes) && pacotes.some((p) => typeof p === 'string' && /^(?:\.\/)?apps\//.test(p));
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }
181
+ function appsDoMonorepo(raiz) {
182
+ if (!ehMonorepoPnpm(raiz))
183
+ return [];
184
+ try {
185
+ return readdirSync(join(raiz, 'apps'), { withFileTypes: true })
186
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !PASTAS_IGNORADAS.has(e.name))
187
+ .map((e) => e.name)
188
+ .sort();
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ }
194
+ function segmentosUnanimes(fontes) {
195
+ const contagens = new Set(fontes.flatMap((f) => f.ocorrencias.map((o) => numeros(o.valor)?.length ?? 0)));
196
+ const [unica] = contagens;
197
+ return contagens.size === 1 && unica >= SEGMENTOS_MIN && unica <= SEGMENTOS_MAX ? unica : null;
198
+ }
199
+ function inferirFormato(fontes) {
200
+ let escolhida;
201
+ for (const o of fontes.flatMap((f) => f.ocorrencias)) {
202
+ if (!temZeroAEsquerda(o.valor))
203
+ continue;
204
+ if (!escolhida || compararNumeros(numeros(o.valor), numeros(escolhida.valor)) > 0)
205
+ escolhida = o;
206
+ }
207
+ return escolhida ? formatoDoValor(escolhida.valor) : null;
208
+ }
209
+ function aplicarFormatoPorArquivo(fontes, formato) {
210
+ if (!formato)
211
+ return;
212
+ for (const fonte of fontes) {
213
+ const valor = fonte.ocorrencias.find((o) => formatoDoValor(o.valor))?.valor;
214
+ if (!valor)
215
+ continue;
216
+ const proprio = temZeroAEsquerda(valor) ? formatoDoValor(valor) : valor.trim().split('.').map(() => '{1}').join('.');
217
+ if (proprio !== formato)
218
+ fonte.formato = proprio;
219
+ }
220
+ }
221
+ function proporArquivos(fontes) {
222
+ return fontes.map((f) => {
223
+ const arquivo = { caminho: f.caminho, extrator: extratorParaTexto(f.extrator) };
224
+ if (f.formato !== undefined)
225
+ arquivo.formato = f.formato;
226
+ return arquivo;
227
+ });
228
+ }
229
+ function changelogs(raiz, base, fontes) {
230
+ const lista = existsSync(join(raiz, base, 'CHANGELOG.md')) ? [juntar(base, 'CHANGELOG.md')] : [];
231
+ for (const f of fontes)
232
+ if (f.extrator.tipo === 'versoes-txt' && !lista.includes(f.caminho))
233
+ lista.push(f.caminho);
234
+ return lista;
235
+ }
236
+ function detectarSemConfig(raiz, app) {
237
+ const problemas = [];
238
+ const comVersao = (fontes) => fontes.filter((f) => f.ocorrencias.length > 0);
239
+ const raizFontes = comVersao(lerFontes(raiz, varrer(raiz, '', 1), problemas));
240
+ const fontesPorApp = new Map();
241
+ for (const nome of appsDoMonorepo(raiz)) {
242
+ const fontes = comVersao(lerFontes(raiz, varrer(raiz, `apps/${nome}`, 1), problemas));
243
+ if (fontes.length > 0)
244
+ fontesPorApp.set(nome, fontes);
245
+ }
246
+ if (app !== undefined && !fontesPorApp.has(app)) {
247
+ const conhecidos = [...fontesPorApp.keys()];
248
+ throw new Error(`app "${app}" não encontrado em apps/ do monorepo; ${conhecidos.length ? `os apps são ${conhecidos.join(', ')}` : 'nenhum app detectado'}`);
249
+ }
250
+ const todas = [...raizFontes, ...[...fontesPorApp.values()].flat()];
251
+ const formato = inferirFormato(todas);
252
+ aplicarFormatoPorArquivo(todas, formato);
253
+ const tag = inferirTag(raiz, fontesPorApp.size > 0 ? '{app}-v{versao}' : 'v{versao}');
254
+ const apps = {};
255
+ const appsPropostos = {};
256
+ for (const [nome, fontes] of fontesPorApp) {
257
+ apps[nome] = { caminhos: [`apps/${nome}/`], fontes, ...analisar(fontes, formato), tag };
258
+ const proposto = { caminhos: [`apps/${nome}/`], arquivos: proporArquivos(fontes) };
259
+ const logs = changelogs(raiz, `apps/${nome}`, fontes);
260
+ if (logs.length > 0)
261
+ proposto.changelogs = logs;
262
+ appsPropostos[nome] = proposto;
263
+ }
264
+ const fontes = app === undefined ? raizFontes : [...raizFontes, ...fontesPorApp.get(app)];
265
+ const { versaoAtual, divergencias } = analisar(fontes, formato);
266
+ const segmentosPropostos = segmentosUnanimes(todas);
267
+ const versaoJson = {
268
+ arquivos: proporArquivos(raizFontes),
269
+ tag,
270
+ bump: { padrao: todas.some((f) => f.extrator.tipo === 'dproj') ? 'build' : 'auto' },
271
+ changelogs: changelogs(raiz, '', raizFontes),
272
+ naoTocar: [],
273
+ };
274
+ if (segmentosPropostos !== null)
275
+ versaoJson.segmentos = segmentosPropostos;
276
+ if (formato)
277
+ versaoJson.formato = formato;
278
+ if (fontesPorApp.size > 0)
279
+ versaoJson.apps = appsPropostos;
280
+ return {
281
+ origem: 'deteccao',
282
+ app: app ?? null,
283
+ fontes,
284
+ versaoAtual,
285
+ divergencias,
286
+ tag,
287
+ segmentos: versaoAtual !== null ? (numeros(versaoAtual)?.length ?? null) : segmentosPropostos,
288
+ formato,
289
+ apps,
290
+ problemas,
291
+ versaoJson,
292
+ };
293
+ }
294
+ function detectarComConfig(raiz, config, app) {
295
+ const appConfig = app === undefined ? undefined : config.apps[app];
296
+ if (app !== undefined && !appConfig) {
297
+ const conhecidos = Object.keys(config.apps);
298
+ throw new Error(`app "${app}" não existe no versao.json; ${conhecidos.length ? `os apps são ${conhecidos.join(', ')}` : 'não há apps declarados'}`);
299
+ }
300
+ const problemas = [];
301
+ const formato = config.formato ?? null;
302
+ const tagRaiz = config.tag ?? inferirTag(raiz, Object.keys(config.apps).length > 0 ? '{app}-v{versao}' : 'v{versao}');
303
+ const raizFontes = lerFontes(raiz, config.arquivos, problemas);
304
+ const apps = {};
305
+ for (const [nome, a] of Object.entries(config.apps)) {
306
+ const fontes = lerFontes(raiz, a.arquivos, problemas);
307
+ apps[nome] = { caminhos: a.caminhos, fontes, ...analisar(fontes, formato), tag: a.tag ?? tagRaiz };
308
+ }
309
+ const fontes = app === undefined ? raizFontes : [...raizFontes, ...apps[app].fontes];
310
+ const { versaoAtual, divergencias } = analisar(fontes, formato);
311
+ return {
312
+ origem: 'config',
313
+ app: app ?? null,
314
+ fontes,
315
+ versaoAtual,
316
+ divergencias,
317
+ tag: appConfig?.tag ?? tagRaiz,
318
+ segmentos: config.segmentos ?? (versaoAtual !== null ? (numeros(versaoAtual)?.length ?? null) : null),
319
+ formato,
320
+ apps,
321
+ problemas: [...new Set(problemas)],
322
+ versaoJson: null,
323
+ };
324
+ }
325
+ /**
326
+ * Descobre as fontes de versão: com `config`, só os arquivos declarados (mais os do `app`); sem ela, varre a
327
+ * raiz e um nível abaixo, mais os `apps/*` de monorepo pnpm, e propõe um `versaoJson`. Lança erro para app
328
+ * desconhecido; config sem fonte devolve `fontes` vazias e quem chama decide o erro.
329
+ */
330
+ export function detectarFontes(raiz, config, app) {
331
+ return config ? detectarComConfig(raiz, config, app) : detectarSemConfig(raiz, app);
332
+ }