@neetru/cli 2.29.0 → 2.32.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/CHANGELOG.md +141 -0
- package/README.md +63 -17
- package/dist/cli-kit/choices.d.ts +33 -0
- package/dist/cli-kit/choices.js +32 -0
- package/dist/cli-kit/command-context.d.ts +98 -0
- package/dist/cli-kit/command-context.js +108 -0
- package/dist/cli-kit/errors.d.ts +79 -0
- package/dist/cli-kit/errors.js +94 -0
- package/dist/cli-kit/index.d.ts +8 -0
- package/dist/cli-kit/index.js +8 -0
- package/dist/cli-kit/suggest.d.ts +74 -0
- package/dist/cli-kit/suggest.js +207 -0
- package/dist/commands/admin/oidc-login.js +1 -1
- package/dist/commands/admin/sdk-credential.js +1 -1
- package/dist/commands/autocomplete.d.ts +1 -1
- package/dist/commands/autocomplete.js +1 -0
- package/dist/commands/bug.d.ts +59 -0
- package/dist/commands/bug.js +114 -2
- package/dist/commands/build.d.ts +24 -1
- package/dist/commands/build.js +195 -73
- package/dist/commands/chat.d.ts +94 -1
- package/dist/commands/chat.js +258 -26
- package/dist/commands/customers.js +1 -1
- package/dist/commands/db-move.js +3 -3
- package/dist/commands/db.js +17 -24
- package/dist/commands/deploy.d.ts +136 -2
- package/dist/commands/deploy.js +547 -69
- package/dist/commands/deployments.js +11 -6
- package/dist/commands/dev-context.js +15 -5
- package/dist/commands/dev-up.d.ts +7 -5
- package/dist/commands/dev-up.js +24 -23
- package/dist/commands/docs-produto.d.ts +90 -0
- package/dist/commands/docs-produto.js +384 -0
- package/dist/commands/doctor.js +36 -1
- package/dist/commands/domains.js +85 -4
- package/dist/commands/env.d.ts +9 -3
- package/dist/commands/env.js +23 -3
- package/dist/commands/infra-read.js +2 -2
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +14 -1
- package/dist/commands/login.js +35 -8
- package/dist/commands/logs.js +12 -4
- package/dist/commands/mobile.js +33 -9
- package/dist/commands/new.d.ts +31 -1
- package/dist/commands/new.js +94 -4
- package/dist/commands/open.js +16 -6
- package/dist/commands/ops.js +1 -1
- package/dist/commands/products-db.d.ts +1 -0
- package/dist/commands/products-db.js +41 -8
- package/dist/commands/products.d.ts +17 -3
- package/dist/commands/products.js +84 -16
- package/dist/commands/promote.js +30 -5
- package/dist/commands/secrets.d.ts +91 -0
- package/dist/commands/secrets.js +471 -0
- package/dist/commands/servers.js +6 -4
- package/dist/commands/status.js +2 -2
- package/dist/commands/support.js +5 -3
- package/dist/commands/tenants.js +28 -16
- package/dist/commands/upgrade.d.ts +9 -0
- package/dist/commands/upgrade.js +50 -1
- package/dist/commands/vm-memory.js +1 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +9 -3
- package/dist/commands/workspaces.js +42 -11
- package/dist/index.js +142 -20
- package/dist/lib/api-client.d.ts +22 -1
- package/dist/lib/api-client.js +79 -8
- package/dist/lib/api-error.d.ts +14 -0
- package/dist/lib/api-error.js +107 -46
- package/dist/lib/build-targets.d.ts +127 -0
- package/dist/lib/build-targets.js +348 -0
- package/dist/lib/cli-error.d.ts +13 -0
- package/dist/lib/cli-error.js +24 -0
- package/dist/lib/cli-write.d.ts +43 -9
- package/dist/lib/cli-write.js +154 -45
- package/dist/lib/compose-generator.d.ts +13 -9
- package/dist/lib/compose-generator.js +28 -60
- package/dist/lib/config-schema.d.ts +36 -2
- package/dist/lib/config-schema.js +11 -0
- package/dist/lib/db-local/db-json.d.ts +1 -1
- package/dist/lib/db-local/db-json.js +13 -5
- package/dist/lib/db-local/env.d.ts +15 -3
- package/dist/lib/db-local/env.js +37 -10
- package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
- package/dist/lib/db-pipeline/build-deps.js +23 -3
- package/dist/lib/db-pipeline/types.d.ts +6 -2
- package/dist/lib/db-pipeline/types.js +0 -18
- package/dist/lib/deploy-target-contract.d.ts +152 -0
- package/dist/lib/deploy-target-contract.js +411 -0
- package/dist/lib/env-file-do-build.d.ts +19 -0
- package/dist/lib/env-file-do-build.js +44 -0
- package/dist/lib/envs.d.ts +108 -0
- package/dist/lib/envs.js +127 -0
- package/dist/lib/fs-seguro.d.ts +63 -0
- package/dist/lib/fs-seguro.js +108 -0
- package/dist/lib/mobile-local/mobile-json.js +10 -4
- package/dist/lib/pickers.d.ts +44 -7
- package/dist/lib/pickers.js +60 -4
- package/dist/lib/product-docs/check.d.ts +62 -0
- package/dist/lib/product-docs/check.js +489 -0
- package/dist/lib/product-docs/config.d.ts +66 -0
- package/dist/lib/product-docs/config.js +234 -0
- package/dist/lib/product-docs/dates.d.ts +17 -0
- package/dist/lib/product-docs/dates.js +41 -0
- package/dist/lib/product-docs/frontmatter.d.ts +43 -0
- package/dist/lib/product-docs/frontmatter.js +302 -0
- package/dist/lib/product-docs/git.d.ts +16 -0
- package/dist/lib/product-docs/git.js +92 -0
- package/dist/lib/product-docs/markdown.d.ts +27 -0
- package/dist/lib/product-docs/markdown.js +98 -0
- package/dist/lib/product-docs/ops.d.ts +133 -0
- package/dist/lib/product-docs/ops.js +975 -0
- package/dist/lib/product-docs/structure.d.ts +61 -0
- package/dist/lib/product-docs/structure.js +239 -0
- package/dist/lib/product-docs/sync.d.ts +107 -0
- package/dist/lib/product-docs/sync.js +322 -0
- package/dist/lib/product-docs/taxonomy.d.ts +46 -0
- package/dist/lib/product-docs/taxonomy.js +163 -0
- package/dist/lib/product-docs/templates.d.ts +53 -0
- package/dist/lib/product-docs/templates.js +245 -0
- package/dist/lib/public-secret-scan.d.ts +117 -0
- package/dist/lib/public-secret-scan.js +771 -0
- package/dist/lib/render.d.ts +16 -0
- package/dist/lib/render.js +34 -4
- package/dist/lib/segredo-produto.d.ts +124 -0
- package/dist/lib/segredo-produto.js +336 -0
- package/dist/lib/texto-utf8.d.ts +96 -0
- package/dist/lib/texto-utf8.js +196 -0
- package/package.json +2 -1
- package/dist/cli-kit/format.js.map +0 -1
- package/dist/cli-kit/glyphs.js.map +0 -1
- package/dist/cli-kit/index.js.map +0 -1
- package/dist/cli-kit/palette.js.map +0 -1
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/admin/iam-check.js.map +0 -1
- package/dist/commands/admin/oidc-login.js.map +0 -1
- package/dist/commands/admin/sdk-credential.js.map +0 -1
- package/dist/commands/agent-release.js.map +0 -1
- package/dist/commands/agent-write.js.map +0 -1
- package/dist/commands/ai-models.js.map +0 -1
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/api-catalog.js.map +0 -1
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/artifact-registry.js.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/autocomplete.js.map +0 -1
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/bootstrap.js.map +0 -1
- package/dist/commands/bug.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/chat-menu.js.map +0 -1
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/cloud-run.js.map +0 -1
- package/dist/commands/code.js.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/customers.js.map +0 -1
- package/dist/commands/db-move.js.map +0 -1
- package/dist/commands/db.js.map +0 -1
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/deployments.js.map +0 -1
- package/dist/commands/dev-context.js.map +0 -1
- package/dist/commands/dev-up.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/docs.js.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/domains.js.map +0 -1
- package/dist/commands/dr.js.map +0 -1
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/hosting-write.js.map +0 -1
- package/dist/commands/infra-read.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/mobile.js.map +0 -1
- package/dist/commands/mocks.js.map +0 -1
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/open.js.map +0 -1
- package/dist/commands/ops.js.map +0 -1
- package/dist/commands/products-db.js.map +0 -1
- package/dist/commands/products.js.map +0 -1
- package/dist/commands/promote.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/servers.js.map +0 -1
- package/dist/commands/smoke.js.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/support.js.map +0 -1
- package/dist/commands/surface-status.js.map +0 -1
- package/dist/commands/tenants.js.map +0 -1
- package/dist/commands/ui.js.map +0 -1
- package/dist/commands/upgrade.js.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/vm-memory.js.map +0 -1
- package/dist/commands/vm.js.map +0 -1
- package/dist/commands/webhooks.js.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/commands/workspaces.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-artifact-url.js.map +0 -1
- package/dist/lib/agent-commands.js.map +0 -1
- package/dist/lib/ai/auth-discovery.js.map +0 -1
- package/dist/lib/ai/context.js.map +0 -1
- package/dist/lib/ai/orchestrator.js.map +0 -1
- package/dist/lib/api-client.js.map +0 -1
- package/dist/lib/api-error.js.map +0 -1
- package/dist/lib/auth.js.map +0 -1
- package/dist/lib/cli-read.js.map +0 -1
- package/dist/lib/cli-write.js.map +0 -1
- package/dist/lib/code-agent-budget.js.map +0 -1
- package/dist/lib/code-agent-files.js.map +0 -1
- package/dist/lib/code-agent-gateway.js.map +0 -1
- package/dist/lib/code-agent-git-tools.js.map +0 -1
- package/dist/lib/code-agent-protocol.js.map +0 -1
- package/dist/lib/code-agent-session.js.map +0 -1
- package/dist/lib/code-agent-settings.js.map +0 -1
- package/dist/lib/code-agent-skills.js.map +0 -1
- package/dist/lib/code-agent-subagents.js.map +0 -1
- package/dist/lib/code-agent-tools.js.map +0 -1
- package/dist/lib/code-agent-tui.js.map +0 -1
- package/dist/lib/code-agent-types.js.map +0 -1
- package/dist/lib/code-agent-ui.js.map +0 -1
- package/dist/lib/code-agent-web-tools.js.map +0 -1
- package/dist/lib/code-agent-write-tools.js.map +0 -1
- package/dist/lib/compose-generator.js.map +0 -1
- package/dist/lib/config-schema.js.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/db-local/db-json.js.map +0 -1
- package/dist/lib/db-local/env.js.map +0 -1
- package/dist/lib/db-local/fingerprint.js.map +0 -1
- package/dist/lib/db-local/index.js.map +0 -1
- package/dist/lib/db-pipeline/build-deps.js.map +0 -1
- package/dist/lib/db-pipeline/errors.js.map +0 -1
- package/dist/lib/db-pipeline/index.js.map +0 -1
- package/dist/lib/db-pipeline/pipeline.js.map +0 -1
- package/dist/lib/db-pipeline/rehearse.js.map +0 -1
- package/dist/lib/db-pipeline/types.js.map +0 -1
- package/dist/lib/dev-local-context.js.map +0 -1
- package/dist/lib/iam-preflight.js.map +0 -1
- package/dist/lib/mobile-local/index.js.map +0 -1
- package/dist/lib/mobile-local/mobile-json.js.map +0 -1
- package/dist/lib/pickers.js.map +0 -1
- package/dist/lib/product-config.js.map +0 -1
- package/dist/lib/render.js.map +0 -1
- package/dist/lib/schema-cache.js.map +0 -1
- package/dist/lib/sse.js.map +0 -1
- package/dist/lib/telemetry.js.map +0 -1
- package/dist/lib/token-resolver.js.map +0 -1
- package/dist/lib/url-allowlist.js.map +0 -1
- package/dist/lib/version-check.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/logo.js.map +0 -1
- package/dist/utils/safe-exit.js.map +0 -1
- package/dist/utils/spinner-registry.js.map +0 -1
- package/dist/version.js.map +0 -1
- package/templates/dev/Dockerfile.agent +0 -39
- package/templates/dev/agent-entrypoint.sh +0 -41
- package/templates/dev/docker-compose.dev.yml +0 -63
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validador OFFLINE dos docs de um produto SaaS — `neetru docs check`.
|
|
3
|
+
*
|
|
4
|
+
* Mesmos códigos de regra do validador do Core (PROTOCOLO_WIKI_INTERNA §9.2:
|
|
5
|
+
* FM, TI, NM, LK, PB, LC) mais os de produto (PROTOCOLO_DOCS_PRODUTOS_SAAS
|
|
6
|
+
* §8.1: PR001–PR004). IX001 (catálogo do INDEX_MESTRE) não se aplica a produto.
|
|
7
|
+
*
|
|
8
|
+
* Níveis: `erro` (falha o check), `aviso` (só falha com --strict) e
|
|
9
|
+
* `sugestao` (nunca falha — itens "do primeiro mês").
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs/promises';
|
|
12
|
+
import * as fsSync from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { parseDoc } from './frontmatter.js';
|
|
15
|
+
import { extractHeadings, extractLinks, normalizeSection, plainHeadingText } from './markdown.js';
|
|
16
|
+
import { DAY_ONE, DAY_ONE_MODELS, FILE_NAME_REGEX, FIRST_MONTH, README_SECTIONS, SKIP_DIRS, TYPE_MODELS, folderRule, topFolder, } from './structure.js';
|
|
17
|
+
import { productCategories, statusesForType } from './taxonomy.js';
|
|
18
|
+
import { productExposesApi, readProductVersion } from './config.js';
|
|
19
|
+
import { changedFiles } from './git.js';
|
|
20
|
+
import { todayIso } from './dates.js';
|
|
21
|
+
const REQUIRED_FIELDS = ['title', 'description', 'category', 'subcategory', 'type', 'status', 'date', 'owner', 'audience'];
|
|
22
|
+
const KNOWN_KEYS = new Set([
|
|
23
|
+
'title', 'description', 'product', 'category', 'subcategory', 'type', 'status', 'date', 'updated',
|
|
24
|
+
'owner', 'audience', 'superseded_by', 'archived_at', 'archived_from', 'archive_reason', 'status_evidence',
|
|
25
|
+
'status_checked_at', 'supersedes', 'related', 'tags', 'status_note', 'order', 'icon', 'adr', 'command',
|
|
26
|
+
]);
|
|
27
|
+
/** §6.2 — chave legada → o que usar. */
|
|
28
|
+
const LEGACY_KEYS = {
|
|
29
|
+
last_updated: 'updated', lastUpdated: 'updated', updatedAt: 'updated',
|
|
30
|
+
created: 'date', data: 'date',
|
|
31
|
+
autor: '(remover — a autoria está no git)', author: '(remover — a autoria está no git)',
|
|
32
|
+
authors: '(remover — a autoria está no git)', owners: 'owner',
|
|
33
|
+
relacionado: 'related', relatedDocs: 'related', allowOthers: '(remover)',
|
|
34
|
+
};
|
|
35
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
36
|
+
const DATE_IN_TEXT = /\d{4}-\d{2}(-\d{2})?/;
|
|
37
|
+
const STAFF_DOMAIN = /core\.neetru\.com/i;
|
|
38
|
+
const PERSONAL_EMAIL = /[\w.+-]+@(gmail|googlemail|hotmail|outlook|live|yahoo|icloud|me|bol|uol|terra|ig)\.[a-z.]+/i;
|
|
39
|
+
function str(v) {
|
|
40
|
+
if (typeof v === 'string')
|
|
41
|
+
return v.trim() || undefined;
|
|
42
|
+
if (typeof v === 'number')
|
|
43
|
+
return String(v);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function isValidDate(s) {
|
|
47
|
+
if (!DATE_RE.test(s))
|
|
48
|
+
return false;
|
|
49
|
+
const d = new Date(`${s}T00:00:00Z`);
|
|
50
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === s;
|
|
51
|
+
}
|
|
52
|
+
function daysBetween(a, b) {
|
|
53
|
+
return Math.floor((Date.parse(`${b}T00:00:00Z`) - Date.parse(`${a}T00:00:00Z`)) / 86_400_000);
|
|
54
|
+
}
|
|
55
|
+
function requiredSections(relInDocs, type) {
|
|
56
|
+
const dayOne = DAY_ONE_MODELS[relInDocs];
|
|
57
|
+
if (dayOne)
|
|
58
|
+
return dayOne.filter((x) => x.required);
|
|
59
|
+
if (!type)
|
|
60
|
+
return [];
|
|
61
|
+
return (TYPE_MODELS[type] ?? []).filter((x) => x.required);
|
|
62
|
+
}
|
|
63
|
+
/** Valida UM doc. `relInDocs` é relativo à pasta de docs, com `/`. Função pura. */
|
|
64
|
+
export function checkDocFile(relInDocs, raw, ctx) {
|
|
65
|
+
const file = `${ctx.docsRoot}/${relInDocs}`;
|
|
66
|
+
const out = [];
|
|
67
|
+
const add = (code, level, message, line) => out.push({ file, code, level, message, ...(line ? { line } : {}) });
|
|
68
|
+
const top = topFolder(relInDocs);
|
|
69
|
+
const base = path.posix.basename(relInDocs);
|
|
70
|
+
const isArchive = top === '_archive';
|
|
71
|
+
const isAudit = top === '_audit';
|
|
72
|
+
const parsed = parseDoc(raw);
|
|
73
|
+
const fm = parsed.data;
|
|
74
|
+
const lineOf = (bodyLine) => parsed.bodyStartLine + bodyLine - 1;
|
|
75
|
+
// ── FM001 ────────────────────────────────────────────────────────────────
|
|
76
|
+
if (!parsed.hasFrontmatter) {
|
|
77
|
+
add('FM001', 'erro', 'Sem frontmatter. Rode `neetru docs init --migrar` para inferir um, ou copie o modelo do protocolo.', 1);
|
|
78
|
+
}
|
|
79
|
+
else if (parsed.error) {
|
|
80
|
+
add('FM001', 'erro', `Frontmatter com YAML inválido: ${parsed.error}.`, 1);
|
|
81
|
+
}
|
|
82
|
+
if (isAudit)
|
|
83
|
+
return out; // §9.4: _audit/ só FM001
|
|
84
|
+
const fmOk = parsed.hasFrontmatter && !parsed.error;
|
|
85
|
+
const title = str(fm.title);
|
|
86
|
+
const status = str(fm.status);
|
|
87
|
+
// ── _archive/: regras reduzidas (FM001, FM009 e title) ───────────────────
|
|
88
|
+
if (isArchive) {
|
|
89
|
+
if (fmOk) {
|
|
90
|
+
if (!title)
|
|
91
|
+
add('FM002', 'erro', 'Falta o campo obrigatório `title`.');
|
|
92
|
+
if (status !== 'archived')
|
|
93
|
+
add('FM009', 'erro', 'Doc em `_archive/` precisa de `status: archived`.');
|
|
94
|
+
const missing = ['archived_at', 'archived_from', 'archive_reason'].filter((k) => !str(fm[k]));
|
|
95
|
+
if (missing.length)
|
|
96
|
+
add('FM009', 'erro', `Carimbo de arquivamento incompleto: falta ${missing.join(', ')}.`);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
const type = str(fm.type);
|
|
101
|
+
const family = type ? ctx.taxonomy.types[type] : undefined;
|
|
102
|
+
const category = str(fm.category);
|
|
103
|
+
const subcategory = str(fm.subcategory);
|
|
104
|
+
if (fmOk) {
|
|
105
|
+
// ── FM002 ──────────────────────────────────────────────────────────────
|
|
106
|
+
const missing = REQUIRED_FIELDS.filter((k) => !str(fm[k]));
|
|
107
|
+
if (family === 'vivo' && !str(fm.updated))
|
|
108
|
+
missing.push('updated');
|
|
109
|
+
if (missing.length)
|
|
110
|
+
add('FM002', 'erro', `Falta campo obrigatório: ${missing.join(', ')}.`);
|
|
111
|
+
// ── PR001 ──────────────────────────────────────────────────────────────
|
|
112
|
+
const product = str(fm.product);
|
|
113
|
+
if (!product)
|
|
114
|
+
add('PR001', 'erro', `Falta \`product: ${ctx.slug}\` (obrigatório em doc de produto).`);
|
|
115
|
+
else if (product !== ctx.slug)
|
|
116
|
+
add('PR001', 'erro', `\`product: ${product}\` não bate com o slug do neetru.config.json (${ctx.slug}).`);
|
|
117
|
+
// ── FM003 / PR003 / FM004 ─────────────────────────────────────────────
|
|
118
|
+
const cats = productCategories(ctx.taxonomy);
|
|
119
|
+
if (category) {
|
|
120
|
+
if (ctx.taxonomy.coreOnlyCategories.includes(category)) {
|
|
121
|
+
add('PR003', 'erro', `A categoria "${category}" só existe no Core. Em produto use: ${Object.keys(cats).join(', ')}.`);
|
|
122
|
+
}
|
|
123
|
+
else if (!cats[category]) {
|
|
124
|
+
add('FM003', 'erro', `Categoria "${category}" fora da lista. Aceitas: ${Object.keys(cats).join(', ')}.`);
|
|
125
|
+
}
|
|
126
|
+
else if (subcategory && !cats[category].includes(subcategory)) {
|
|
127
|
+
add('FM004', 'erro', `Subcategoria "${subcategory}" não pertence a "${category}". Aceitas: ${cats[category].join(', ')}.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── FM005 ──────────────────────────────────────────────────────────────
|
|
131
|
+
if (type) {
|
|
132
|
+
if (!family) {
|
|
133
|
+
add('FM005', 'erro', `Tipo "${type}" fora da lista. Aceitos: ${Object.keys(ctx.taxonomy.types).join(', ')}.`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const rule = folderRule(relInDocs);
|
|
137
|
+
if (rule && rule.types !== 'qualquer' && !rule.types.includes(type)) {
|
|
138
|
+
const where = rule.folder ? `${rule.folder}/` : 'a raiz de docs/';
|
|
139
|
+
add('FM005', 'erro', `Tipo "${type}" não é aceito em ${where} (aceitos: ${rule.types.join(', ')}).`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── FM006 ──────────────────────────────────────────────────────────────
|
|
144
|
+
if (type && family && status) {
|
|
145
|
+
const allowed = statusesForType(ctx.taxonomy, type);
|
|
146
|
+
if (!allowed.includes(status))
|
|
147
|
+
add('FM006', 'erro', `Status "${status}" não vale para o tipo "${type}". Aceitos: ${allowed.join(', ')}.`);
|
|
148
|
+
}
|
|
149
|
+
// ── FM007 ──────────────────────────────────────────────────────────────
|
|
150
|
+
for (const k of ['date', 'updated', 'archived_at', 'status_checked_at']) {
|
|
151
|
+
const v = str(fm[k]);
|
|
152
|
+
if (v && !isValidDate(v))
|
|
153
|
+
add('FM007', 'erro', `\`${k}: ${v}\` fora do formato AAAA-MM-DD.`);
|
|
154
|
+
}
|
|
155
|
+
const date = str(fm.date);
|
|
156
|
+
const updated = str(fm.updated);
|
|
157
|
+
if (date && updated && isValidDate(date) && isValidDate(updated) && updated < date) {
|
|
158
|
+
add('FM007', 'erro', `\`updated\` (${updated}) é anterior a \`date\` (${date}).`);
|
|
159
|
+
}
|
|
160
|
+
// ── FM008 ──────────────────────────────────────────────────────────────
|
|
161
|
+
if (status === 'superseded') {
|
|
162
|
+
const sb = str(fm.superseded_by);
|
|
163
|
+
if (!sb)
|
|
164
|
+
add('FM008', 'erro', '`status: superseded` sem `superseded_by`.');
|
|
165
|
+
else if (!ctx.exists(`${ctx.docsRoot}/${sb.replace(/^\.?\/+/, '')}`)) {
|
|
166
|
+
add('FM008', 'erro', `\`superseded_by: ${sb}\` aponta para arquivo inexistente (caminho relativo a ${ctx.docsRoot}/).`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ── FM009 ──────────────────────────────────────────────────────────────
|
|
170
|
+
if (status === 'archived')
|
|
171
|
+
add('FM009', 'erro', '`status: archived` fora de `_archive/` — mova o arquivo (espelhando o caminho) ou mude o status.');
|
|
172
|
+
// ── FM013 / FM010 ─────────────────────────────────────────────────────
|
|
173
|
+
const owner = str(fm.owner);
|
|
174
|
+
if (owner) {
|
|
175
|
+
if (owner.includes('@') || /^[A-ZÀ-Ú][a-zà-ú]+(\s+[A-ZÀ-Ú][a-zà-ú]+)+$/.test(owner)) {
|
|
176
|
+
add('FM013', 'erro', `\`owner: ${owner}\` parece pessoa ou e-mail. Use a raia (em produto, \`produto\`).`);
|
|
177
|
+
}
|
|
178
|
+
else if (!ctx.taxonomy.owners.includes(owner)) {
|
|
179
|
+
add('FM010', 'erro', `\`owner: ${owner}\` fora da lista: ${ctx.taxonomy.owners.join(', ')}.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const audience = str(fm.audience);
|
|
183
|
+
if (audience && !ctx.taxonomy.audiences.includes(audience)) {
|
|
184
|
+
add('FM010', 'erro', `\`audience: ${audience}\` fora da lista: ${ctx.taxonomy.audiences.join(', ')}.`);
|
|
185
|
+
}
|
|
186
|
+
// ── FM011 ──────────────────────────────────────────────────────────────
|
|
187
|
+
if (family === 'plano' && status && ['done', 'in_progress', 'abandoned'].includes(status) && !str(fm.status_evidence)) {
|
|
188
|
+
add('FM011', 'aviso', `Plano \`${status}\` sem \`status_evidence\` (arquivo:linha, commit ou PR).`);
|
|
189
|
+
}
|
|
190
|
+
// ── FM012 ──────────────────────────────────────────────────────────────
|
|
191
|
+
for (const k of Object.keys(fm)) {
|
|
192
|
+
if (KNOWN_KEYS.has(k))
|
|
193
|
+
continue;
|
|
194
|
+
if (LEGACY_KEYS[k])
|
|
195
|
+
add('FM012', 'aviso', `Chave legada \`${k}\` — use ${LEGACY_KEYS[k]}.`);
|
|
196
|
+
else
|
|
197
|
+
add('FM012', 'aviso', `Chave desconhecida \`${k}\` (aceita só como metadado de registro).`);
|
|
198
|
+
}
|
|
199
|
+
// ── LC001 / LC002 ─────────────────────────────────────────────────────
|
|
200
|
+
if (family === 'vivo' && status === 'active' && updated && isValidDate(updated) && daysBetween(updated, ctx.today) > 365) {
|
|
201
|
+
add('LC001', 'aviso', `Doc vivo sem revisão há mais de 365 dias (updated ${updated}) — revisar.`);
|
|
202
|
+
}
|
|
203
|
+
if (family === 'plano' && (status === 'proposed' || status === 'in_progress')) {
|
|
204
|
+
const ref = str(fm.status_checked_at) ?? date;
|
|
205
|
+
if (ref && isValidDate(ref) && daysBetween(ref, ctx.today) > 90) {
|
|
206
|
+
add('LC002', 'aviso', `Plano \`${status}\` sem conferência há mais de 90 dias (${ref}) — status incerto.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ── PB001 ──────────────────────────────────────────────────────────────
|
|
210
|
+
if (audience === 'publico') {
|
|
211
|
+
if (STAFF_DOMAIN.test(raw))
|
|
212
|
+
add('PB001', 'erro', 'Doc público cita o domínio do painel staff do Core. Tire a menção.');
|
|
213
|
+
const em = raw.match(PERSONAL_EMAIL);
|
|
214
|
+
if (em)
|
|
215
|
+
add('PB001', 'erro', `Doc público com e-mail pessoal (${em[0]}).`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ── TI001 / TI002 / TI003–TI007 ──────────────────────────────────────────
|
|
219
|
+
const headings = extractHeadings(parsed.body);
|
|
220
|
+
const h1s = headings.filter((h) => h.level === 1);
|
|
221
|
+
if (h1s.length === 0)
|
|
222
|
+
add('TI001', 'erro', 'Nenhum H1. O doc precisa de exatamente um H1, igual ao `title`.');
|
|
223
|
+
else if (h1s.length > 1)
|
|
224
|
+
add('TI001', 'erro', `${h1s.length} H1 — deixe só um (igual ao \`title\`).`, lineOf(h1s[1].line));
|
|
225
|
+
if (title && h1s.length >= 1) {
|
|
226
|
+
const h1 = plainHeadingText(h1s[0].text);
|
|
227
|
+
if (h1 !== title)
|
|
228
|
+
add('TI002', 'erro', `H1 ("${h1}") diferente do \`title\` ("${title}").`, lineOf(h1s[0].line));
|
|
229
|
+
}
|
|
230
|
+
if (title) {
|
|
231
|
+
if (title.length > 100)
|
|
232
|
+
add('TI003', 'erro', `Título com ${title.length} caracteres (máximo 100; ideal até 70).`);
|
|
233
|
+
else if (title.length > 70)
|
|
234
|
+
add('TI003', 'aviso', `Título com ${title.length} caracteres (ideal até 70).`);
|
|
235
|
+
const hasDate = DATE_IN_TEXT.test(title);
|
|
236
|
+
if (family && family !== 'registro' && hasDate)
|
|
237
|
+
add('TI004', 'aviso', 'Data no título só na família registro (relatório, incidente, ata).');
|
|
238
|
+
if (family === 'registro' && !/\d{4}-\d{2}(-\d{2})?\s*$/.test(title))
|
|
239
|
+
add('TI004', 'aviso', 'Registro sem a data no fim do título ("Assunto — AAAA-MM-DD").');
|
|
240
|
+
const letters = title.replace(/[^A-Za-zÀ-ÿ]/g, '');
|
|
241
|
+
const problems = [];
|
|
242
|
+
if (letters.length > 3 && letters === letters.toUpperCase())
|
|
243
|
+
problems.push('tudo em MAIÚSCULAS');
|
|
244
|
+
if (/\p{Extended_Pictographic}/u.test(title))
|
|
245
|
+
problems.push('emoji');
|
|
246
|
+
if (/\.$/.test(title))
|
|
247
|
+
problems.push('ponto final');
|
|
248
|
+
if (/[`*]|\[.*\]\(/.test(title))
|
|
249
|
+
problems.push('markdown ou crase');
|
|
250
|
+
if (/^[A-Z0-9]+(_[A-Z0-9]+)+$/.test(title))
|
|
251
|
+
problems.push('nome de arquivo como título');
|
|
252
|
+
if (/^(runbook|spec|guia|adr|rfc|plano|norma|relat[óo]rio|refer[êe]ncia)\s*:/i.test(title))
|
|
253
|
+
problems.push('rótulo de tipo com dois-pontos');
|
|
254
|
+
if (/^(neetru core|plataforma interna neetru)\s*[—-]/i.test(title))
|
|
255
|
+
problems.push('prefixo "Neetru Core —"');
|
|
256
|
+
if ((title.match(/ — /g) ?? []).length > 1)
|
|
257
|
+
problems.push('mais de um travessão');
|
|
258
|
+
if (problems.length)
|
|
259
|
+
add('TI005', 'aviso', `Título com ${problems.join(', ')}.`);
|
|
260
|
+
}
|
|
261
|
+
const description = str(fm.description);
|
|
262
|
+
if (description) {
|
|
263
|
+
const issues = [];
|
|
264
|
+
if (description.length < 40 || description.length > 200)
|
|
265
|
+
issues.push(`${description.length} caracteres (40 a 200)`);
|
|
266
|
+
if (!/\.$/.test(description))
|
|
267
|
+
issues.push('não termina em ponto');
|
|
268
|
+
if (/[.!?]\s+[A-ZÀ-Ú]/.test(description.replace(/\b(ex|etc|p\.ex|vs|sr|sra|dr)\.\s/gi, '')))
|
|
269
|
+
issues.push('mais de uma frase');
|
|
270
|
+
if (issues.length)
|
|
271
|
+
add('TI006', 'aviso', `Descrição com ${issues.join(', ')}.`);
|
|
272
|
+
}
|
|
273
|
+
// TI007: salto de nível e mistura de H2 numerada/não numerada
|
|
274
|
+
let prev = 1;
|
|
275
|
+
for (const h of headings) {
|
|
276
|
+
if (h.level === 1) {
|
|
277
|
+
prev = 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (h.level > prev + 1) {
|
|
281
|
+
add('TI007', 'aviso', `Salto de cabeçalho: H${prev} → H${h.level} ("${plainHeadingText(h.text)}").`, lineOf(h.line));
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
prev = h.level;
|
|
285
|
+
}
|
|
286
|
+
const h2s = headings.filter((h) => h.level === 2);
|
|
287
|
+
const numbered = h2s.filter((h) => /^\d+(\.\d+)*[.)]?\s/.test(plainHeadingText(h.text))).length;
|
|
288
|
+
if (numbered > 0 && numbered < h2s.length)
|
|
289
|
+
add('TI007', 'aviso', 'Mistura de H2 com e sem número — numere todas ou nenhuma.');
|
|
290
|
+
// TI008: seções obrigatórias do modelo
|
|
291
|
+
const req = requiredSections(relInDocs, type);
|
|
292
|
+
if (req.length) {
|
|
293
|
+
const have = h2s.map((h) => normalizeSection(h.text));
|
|
294
|
+
const missingSec = req.filter((r) => {
|
|
295
|
+
const want = normalizeSection(r.title);
|
|
296
|
+
return !have.some((x) => x === want || x.startsWith(`${want} `) || x.startsWith(`${want}(`) || x.startsWith(`${want}:`));
|
|
297
|
+
});
|
|
298
|
+
if (missingSec.length)
|
|
299
|
+
add('TI008', 'aviso', `Falta seção obrigatória do modelo: ${missingSec.map((m) => m.title).join(' · ')}.`);
|
|
300
|
+
}
|
|
301
|
+
// ── NM001 / NM002 ─────────────────────────────────────────────────────────
|
|
302
|
+
const nameExempt = relInDocs === 'index.md' || base === 'INDEX.md' || top === 'superpowers' || ctx.legacyNames.has(relInDocs);
|
|
303
|
+
if (!nameExempt && !FILE_NAME_REGEX.test(base)) {
|
|
304
|
+
add('NM001', 'erro', `Nome "${base}" fora da regra <PREFIXO>_<ASSUNTO>[_<DATA>].md (MAIÚSCULAS, sem acento, com _). Renomear é decisão humana; para manter, liste em ${ctx.docsRoot}/_nomes-legados.json.`);
|
|
305
|
+
}
|
|
306
|
+
if (!nameExempt && family) {
|
|
307
|
+
const nameHasDate = /_\d{4}-\d{2}(-\d{2})?\.md$/.test(base);
|
|
308
|
+
if (family === 'registro' && !nameHasDate)
|
|
309
|
+
add('NM002', 'aviso', 'Registro sem data no nome (…_AAAA-MM-DD.md).');
|
|
310
|
+
if (family === 'vivo' && nameHasDate)
|
|
311
|
+
add('NM002', 'aviso', 'Doc vivo com data no nome — data no nome é só de registro.');
|
|
312
|
+
}
|
|
313
|
+
// ── LK001 / LK002 ─────────────────────────────────────────────────────────
|
|
314
|
+
const dirInRepo = path.posix.dirname(file);
|
|
315
|
+
for (const link of extractLinks(parsed.body)) {
|
|
316
|
+
const t = link.target;
|
|
317
|
+
if (/^(https?:)?\/\/core\.neetru\.com\/docs/i.test(t) || /^\/docs\//.test(t)) {
|
|
318
|
+
add('LK002', 'aviso', `Link absoluto para o portal de docs (${t}) — use link relativo com .md.`, lineOf(link.line));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(t) || t.startsWith('#') || t.startsWith('/') || t.startsWith('//'))
|
|
322
|
+
continue;
|
|
323
|
+
let p = t.split('#')[0].split('?')[0];
|
|
324
|
+
try {
|
|
325
|
+
p = decodeURIComponent(p);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* mantém */
|
|
329
|
+
}
|
|
330
|
+
if (!p)
|
|
331
|
+
continue;
|
|
332
|
+
const target = path.posix.normalize(path.posix.join(dirInRepo, p));
|
|
333
|
+
if (target.startsWith('..'))
|
|
334
|
+
continue; // fora do repo — não dá para conferir
|
|
335
|
+
if (!ctx.exists(target)) {
|
|
336
|
+
add('LK001', ctx.changed ? 'erro' : 'aviso', `Link quebrado: ${t}.`, lineOf(link.line));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
async function walkMd(absDir, relBase = '') {
|
|
342
|
+
let entries;
|
|
343
|
+
try {
|
|
344
|
+
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const out = [];
|
|
350
|
+
for (const e of entries) {
|
|
351
|
+
if (e.isSymbolicLink())
|
|
352
|
+
continue;
|
|
353
|
+
const rel = relBase ? `${relBase}/${e.name}` : e.name;
|
|
354
|
+
if (e.isDirectory()) {
|
|
355
|
+
if (SKIP_DIRS.has(e.name))
|
|
356
|
+
continue;
|
|
357
|
+
out.push(...(await walkMd(path.join(absDir, e.name), rel)));
|
|
358
|
+
}
|
|
359
|
+
else if (e.isFile() && e.name.toLowerCase().endsWith('.md')) {
|
|
360
|
+
out.push(rel);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return out.sort();
|
|
364
|
+
}
|
|
365
|
+
/** Lista os `.md` da pasta de docs (relativos a ela, com `/`). */
|
|
366
|
+
export async function listDocFiles(repo) {
|
|
367
|
+
return walkMd(path.join(repo.cwd, repo.docs.root));
|
|
368
|
+
}
|
|
369
|
+
/** Lê `docs/_nomes-legados.json` (array ou `{ arquivos: [...] }`). */
|
|
370
|
+
export function readLegacyNames(repo) {
|
|
371
|
+
const p = path.join(repo.cwd, repo.docs.root, '_nomes-legados.json');
|
|
372
|
+
try {
|
|
373
|
+
const j = JSON.parse(fsSync.readFileSync(p, 'utf8'));
|
|
374
|
+
const arr = Array.isArray(j) ? j : j.arquivos;
|
|
375
|
+
return new Set(Array.isArray(arr) ? arr.filter((x) => typeof x === 'string').map((x) => x.replace(/\\/g, '/')) : []);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return new Set();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/** Hoje no fuso dos docs (America/Sao_Paulo ou NEETRU_TZ) — nunca UTC. */
|
|
382
|
+
export { todayIso } from './dates.js';
|
|
383
|
+
/** Checagens do repositório inteiro: mínimo do primeiro dia (PR002), README, CHANGELOG (PR004). */
|
|
384
|
+
export function checkRepoLevel(repo) {
|
|
385
|
+
const out = [];
|
|
386
|
+
const exists = (rel) => fsSync.existsSync(path.join(repo.cwd, rel));
|
|
387
|
+
const withApi = productExposesApi(repo);
|
|
388
|
+
const docsRoot = repo.docs.root;
|
|
389
|
+
const remap = (p) => (p.startsWith('docs/') ? `${docsRoot}/${p.slice(5)}` : p);
|
|
390
|
+
if (!exists(docsRoot)) {
|
|
391
|
+
out.push({ file: docsRoot, code: 'PR002', level: 'aviso', message: `Pasta ${docsRoot}/ não existe. Rode \`neetru docs init\`.` });
|
|
392
|
+
}
|
|
393
|
+
for (const item of DAY_ONE) {
|
|
394
|
+
if (item.soComApi && !withApi)
|
|
395
|
+
continue;
|
|
396
|
+
const p = remap(item.path);
|
|
397
|
+
if (!exists(p)) {
|
|
398
|
+
out.push({
|
|
399
|
+
file: p,
|
|
400
|
+
code: 'PR002',
|
|
401
|
+
level: 'aviso',
|
|
402
|
+
message: `Falta item do mínimo do primeiro dia: ${p}.${item.soComApi ? ' (o produto expõe /api/v1)' : ''} \`neetru docs init\` cria o que falta sem sobrescrever nada.`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// README: 3 seções
|
|
407
|
+
if (exists('README.md')) {
|
|
408
|
+
const body = fsSync.readFileSync(path.join(repo.cwd, 'README.md'), 'utf8');
|
|
409
|
+
const hs = extractHeadings(body).filter((h) => h.level >= 2).map((h) => plainHeadingText(h.text));
|
|
410
|
+
const missing = README_SECTIONS.filter((sec) => !hs.some((h) => sec.match.test(h)));
|
|
411
|
+
if (missing.length) {
|
|
412
|
+
out.push({ file: 'README.md', code: 'PR002', level: 'aviso', message: `README sem a seção: ${missing.map((m) => m.label).join(' · ')}.` });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// PR004: CHANGELOG com a versão do package.json
|
|
416
|
+
if (exists('CHANGELOG.md')) {
|
|
417
|
+
const version = readProductVersion(repo);
|
|
418
|
+
if (version) {
|
|
419
|
+
const cl = fsSync.readFileSync(path.join(repo.cwd, 'CHANGELOG.md'), 'utf8');
|
|
420
|
+
const esc = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
421
|
+
if (!new RegExp(`^#{2,3}\\s*\\[?v?${esc}\\]?(\\s|$)`, 'm').test(cl)) {
|
|
422
|
+
out.push({ file: 'CHANGELOG.md', code: 'PR004', level: 'aviso', message: `CHANGELOG.md sem entrada para a versão ${version} do package.json ("## [${version}] — AAAA-MM-DD").` });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const p of FIRST_MONTH) {
|
|
427
|
+
const rp = remap(p);
|
|
428
|
+
if (!exists(rp))
|
|
429
|
+
out.push({ file: rp, code: 'PR002', level: 'sugestao', message: `Recomendado no primeiro mês: ${rp}.` });
|
|
430
|
+
}
|
|
431
|
+
if (exists(docsRoot)) {
|
|
432
|
+
const dec = path.join(repo.cwd, docsRoot, 'decisoes');
|
|
433
|
+
const hasAdr = fsSync.existsSync(dec) && fsSync.readdirSync(dec).some((f) => /^ADR-\d{4}_/.test(f));
|
|
434
|
+
if (!hasAdr)
|
|
435
|
+
out.push({ file: `${docsRoot}/decisoes`, code: 'PR002', level: 'sugestao', message: 'Recomendado no primeiro mês: o primeiro ADR (`neetru docs new adr "<decisão>"`).' });
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
export function summarize(findings, strict) {
|
|
440
|
+
const counts = { erro: 0, aviso: 0, sugestao: 0 };
|
|
441
|
+
for (const f of findings)
|
|
442
|
+
counts[f.level]++;
|
|
443
|
+
return { ok: counts.erro === 0 && (!strict || counts.aviso === 0), counts };
|
|
444
|
+
}
|
|
445
|
+
/** Roda o validador no repositório do produto. Nunca lança por conteúdo de doc. */
|
|
446
|
+
export async function checkProductDocs(repo, opts) {
|
|
447
|
+
const docsRoot = repo.docs.root;
|
|
448
|
+
const all = await listDocFiles(repo);
|
|
449
|
+
const today = opts.today ?? todayIso();
|
|
450
|
+
const legacy = readLegacyNames(repo);
|
|
451
|
+
let targets = all;
|
|
452
|
+
let changedSet = new Set();
|
|
453
|
+
let changedBase;
|
|
454
|
+
let changedFallback = false;
|
|
455
|
+
const ch = changedFiles(repo.cwd);
|
|
456
|
+
if (ch) {
|
|
457
|
+
changedBase = ch.base;
|
|
458
|
+
changedSet = new Set(ch.files.filter((f) => f.startsWith(`${docsRoot}/`)).map((f) => f.slice(docsRoot.length + 1)));
|
|
459
|
+
}
|
|
460
|
+
if (opts.changed) {
|
|
461
|
+
if (ch)
|
|
462
|
+
targets = all.filter((f) => changedSet.has(f));
|
|
463
|
+
else
|
|
464
|
+
changedFallback = true;
|
|
465
|
+
}
|
|
466
|
+
const existsCache = new Map();
|
|
467
|
+
const exists = (repoRel) => {
|
|
468
|
+
if (!existsCache.has(repoRel))
|
|
469
|
+
existsCache.set(repoRel, fsSync.existsSync(path.join(repo.cwd, repoRel)));
|
|
470
|
+
return existsCache.get(repoRel);
|
|
471
|
+
};
|
|
472
|
+
const findings = [];
|
|
473
|
+
for (const rel of targets) {
|
|
474
|
+
const raw = await fs.readFile(path.join(repo.cwd, docsRoot, rel), 'utf8');
|
|
475
|
+
findings.push(...checkDocFile(rel, raw, {
|
|
476
|
+
taxonomy: opts.taxonomy,
|
|
477
|
+
slug: repo.slug,
|
|
478
|
+
docsRoot,
|
|
479
|
+
exists,
|
|
480
|
+
legacyNames: legacy,
|
|
481
|
+
today,
|
|
482
|
+
changed: opts.changed ? true : changedSet.has(rel),
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
findings.push(...checkRepoLevel(repo));
|
|
486
|
+
const { ok, counts } = summarize(findings, !!opts.strict);
|
|
487
|
+
return { ok, findings, counts, filesChecked: targets.length, docsRoot, changedBase, changedFallback };
|
|
488
|
+
}
|
|
489
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export declare const CONFIG_FILES: readonly ["neetru.config.json", ".neetru.json"];
|
|
2
|
+
export interface DocsConfig {
|
|
3
|
+
protocolVersion: number;
|
|
4
|
+
root: string;
|
|
5
|
+
publish: boolean;
|
|
6
|
+
publishOnDeploy: boolean;
|
|
7
|
+
api: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_DOCS_CONFIG: DocsConfig;
|
|
10
|
+
export interface ProductRepo {
|
|
11
|
+
/**
|
|
12
|
+
* Base dos caminhos dos docs: a pasta que contém a pasta de docs. É a pasta do
|
|
13
|
+
* `neetru.config.json` no caso comum; num monorepo com `docs.root` acima do
|
|
14
|
+
* config (ex.: `apps/painel/neetru.config.json` + `docs/` na raiz), é a raiz
|
|
15
|
+
* do repositório git. `README.md`/`CHANGELOG.md` do produto ficam aqui.
|
|
16
|
+
*/
|
|
17
|
+
cwd: string;
|
|
18
|
+
/** Pasta do `neetru.config.json` (e do `package.json` do produto). */
|
|
19
|
+
configDir: string;
|
|
20
|
+
configPath: string;
|
|
21
|
+
/** Raiz do repositório git (null fora de git). */
|
|
22
|
+
gitRoot: string | null;
|
|
23
|
+
raw: Record<string, unknown>;
|
|
24
|
+
slug: string;
|
|
25
|
+
name: string;
|
|
26
|
+
/** `docs.root` aqui é RELATIVO a `cwd`, com `/` (ex.: `docs`). */
|
|
27
|
+
docs: DocsConfig;
|
|
28
|
+
/** A chave `docs` existia no arquivo (antes de qualquer default). */
|
|
29
|
+
hasDocsKey: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* `docs.root` não estava no config e a pasta foi achada na raiz do git
|
|
32
|
+
* (monorepo). Valor RELATIVO AO CONFIG a gravar (ex.: `../../docs`).
|
|
33
|
+
*/
|
|
34
|
+
docsRootInferred?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class ProductConfigMissingError extends Error {
|
|
37
|
+
constructor(cwd: string);
|
|
38
|
+
}
|
|
39
|
+
export declare class ProductConfigInvalidError extends Error {
|
|
40
|
+
constructor(file: string, why: string);
|
|
41
|
+
}
|
|
42
|
+
export declare function normalizeDocsConfig(x: unknown): DocsConfig;
|
|
43
|
+
/**
|
|
44
|
+
* Lê o config do produto procurando `neetru.config.json` (ou `.neetru.json`)
|
|
45
|
+
* em `start` e nas pastas ACIMA até a raiz do repositório git (fora de git,
|
|
46
|
+
* só em `start`). `docs.root` é relativo ao config e pode apontar acima dele
|
|
47
|
+
* (`../../docs`), desde que fique DENTRO da raiz do repositório git — fora
|
|
48
|
+
* disso (ou caminho absoluto) é recusado.
|
|
49
|
+
* Lança `ProductConfigMissingError`/`ProductConfigInvalidError`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadProductRepo(start: string): Promise<ProductRepo>;
|
|
52
|
+
/** Versão do `package.json` do produto (pasta do config; senão a base). */
|
|
53
|
+
export declare function readProductVersion(repo: ProductRepo): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Garante a chave `docs` no config, acrescentando SÓ as subchaves que faltam
|
|
56
|
+
* (nunca muda valor existente, salvo as listadas em `override`). Devolve as
|
|
57
|
+
* subchaves acrescentadas/alteradas.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ensureDocsKey(repo: ProductRepo, extra?: Partial<DocsConfig>, dryRun?: boolean,
|
|
60
|
+
/** Subchaves de `extra` que valem mesmo se já existirem (pedido explícito do usuário). */
|
|
61
|
+
override?: string[]): Promise<string[]>;
|
|
62
|
+
/**
|
|
63
|
+
* O produto expõe `/api/v1`? `docs.api: true`, ou app móvel registrado
|
|
64
|
+
* (`.neetru/mobile.json` com apps), ou o config declara `mobile`/`integrations`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function productExposesApi(repo: ProductRepo): boolean;
|