@orkastery/cli 0.5.1 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/adapters/claude-code/README.md +158 -0
  2. package/adapters/claude-code/agents/ork-master.md +2 -0
  3. package/adapters/claude-code/commands/master.md +8 -0
  4. package/adapters/claude-code/commands/ork.md +9 -0
  5. package/adapters/claude-code/mods/autoridade.json +56 -0
  6. package/adapters/claude-code/mods/ork-clima/.claude-plugin/plugin.json +14 -0
  7. package/adapters/claude-code/mods/ork-clima/hooks/contas.ts +283 -0
  8. package/adapters/claude-code/mods/ork-clima/hooks/hooks.json +3 -0
  9. package/adapters/claude-code/mods/ork-clima/hooks/previsao.ts +393 -0
  10. package/adapters/claude-code/mods/ork-clima/hooks/register.ts +205 -0
  11. package/adapters/claude-code/mods/ork-clima/orkastery-mod.json +5 -0
  12. package/adapters/claude-code/mods/ork-clima/types/index.d.ts +80 -0
  13. package/adapters/claude-code/mods/ork-freio/.claude-plugin/plugin.json +14 -0
  14. package/adapters/claude-code/mods/ork-freio/hooks/carga.ts +160 -0
  15. package/adapters/claude-code/mods/ork-freio/hooks/conferencia.ts +309 -0
  16. package/adapters/claude-code/mods/ork-freio/hooks/hooks.json +3 -0
  17. package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +270 -0
  18. package/adapters/claude-code/mods/ork-freio/hooks/register.ts +285 -0
  19. package/adapters/claude-code/mods/ork-freio/orkastery-mod.json +5 -0
  20. package/adapters/claude-code/mods/ork-freio/types/index.d.ts +127 -0
  21. package/adapters/claude-code/mods/ork-mesa/.claude-plugin/plugin.json +14 -0
  22. package/adapters/claude-code/mods/ork-mesa/hooks/cartoes.tsx +246 -0
  23. package/adapters/claude-code/mods/ork-mesa/hooks/decidir.ts +110 -0
  24. package/adapters/claude-code/mods/ork-mesa/hooks/fila.ts +253 -0
  25. package/adapters/claude-code/mods/ork-mesa/hooks/hooks.json +3 -0
  26. package/adapters/claude-code/mods/ork-mesa/hooks/obvia.ts +98 -0
  27. package/adapters/claude-code/mods/ork-mesa/hooks/register.tsx +493 -0
  28. package/adapters/claude-code/mods/ork-mesa/orkastery-mod.json +5 -0
  29. package/adapters/claude-code/mods/ork-mesa/types/index.d.ts +131 -0
  30. package/adapters/claude-code/mods/ork-pista/.claude-plugin/plugin.json +14 -0
  31. package/adapters/claude-code/mods/ork-pista/hooks/acoes.ts +71 -0
  32. package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +117 -0
  33. package/adapters/claude-code/mods/ork-pista/hooks/hooks.json +3 -0
  34. package/adapters/claude-code/mods/ork-pista/hooks/pista.tsx +356 -0
  35. package/adapters/claude-code/mods/ork-pista/hooks/register.tsx +439 -0
  36. package/adapters/claude-code/mods/ork-pista/orkastery-mod.json +5 -0
  37. package/adapters/claude-code/mods/ork-pista/types/index.d.ts +85 -0
  38. package/adapters/claude-code/mods/ork-ponte/.claude-plugin/plugin.json +13 -0
  39. package/adapters/claude-code/mods/ork-ponte/hooks/faixa.tsx +57 -0
  40. package/adapters/claude-code/mods/ork-ponte/hooks/guarda.ts +410 -0
  41. package/adapters/claude-code/mods/ork-ponte/hooks/hooks.json +3 -0
  42. package/adapters/claude-code/mods/ork-ponte/hooks/mostrado.ts +55 -0
  43. package/adapters/claude-code/mods/ork-ponte/hooks/nucleo.ts +157 -0
  44. package/adapters/claude-code/mods/ork-ponte/hooks/papel.ts +42 -0
  45. package/adapters/claude-code/mods/ork-ponte/hooks/register.ts +331 -0
  46. package/adapters/claude-code/mods/ork-ponte/hooks/vigia.ts +76 -0
  47. package/adapters/claude-code/mods/ork-ponte/orkastery-mod.json +5 -0
  48. package/adapters/claude-code/mods/ork-ponte/types/index.d.ts +178 -0
  49. package/adapters/claude-code/mods/ork-portao/.claude-plugin/plugin.json +14 -0
  50. package/adapters/claude-code/mods/ork-portao/hooks/hooks.json +3 -0
  51. package/adapters/claude-code/mods/ork-portao/hooks/lease.ts +201 -0
  52. package/adapters/claude-code/mods/ork-portao/hooks/plano.ts +304 -0
  53. package/adapters/claude-code/mods/ork-portao/hooks/register.ts +339 -0
  54. package/adapters/claude-code/mods/ork-portao/hooks/verify.ts +152 -0
  55. package/adapters/claude-code/mods/ork-portao/orkastery-mod.json +5 -0
  56. package/adapters/claude-code/mods/ork-portao/types/index.d.ts +60 -0
  57. package/adapters/claude-code/mods/ork-radar/.claude-plugin/plugin.json +14 -0
  58. package/adapters/claude-code/mods/ork-radar/hooks/colisao.ts +420 -0
  59. package/adapters/claude-code/mods/ork-radar/hooks/hooks.json +3 -0
  60. package/adapters/claude-code/mods/ork-radar/hooks/mapa.tsx +322 -0
  61. package/adapters/claude-code/mods/ork-radar/hooks/register.tsx +470 -0
  62. package/adapters/claude-code/mods/ork-radar/orkastery-mod.json +5 -0
  63. package/adapters/claude-code/mods/ork-radar/types/index.d.ts +72 -0
  64. package/adapters/claude-code/mods/ork-sentinela/.claude-plugin/plugin.json +14 -0
  65. package/adapters/claude-code/mods/ork-sentinela/hooks/alertas.ts +85 -0
  66. package/adapters/claude-code/mods/ork-sentinela/hooks/despacho.ts +156 -0
  67. package/adapters/claude-code/mods/ork-sentinela/hooks/hooks.json +3 -0
  68. package/adapters/claude-code/mods/ork-sentinela/hooks/register.ts +511 -0
  69. package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +186 -0
  70. package/adapters/claude-code/mods/ork-sentinela/orkastery-mod.json +5 -0
  71. package/adapters/claude-code/mods/ork-sentinela/types/index.d.ts +65 -0
  72. package/adapters/claude-code/mods/ork-torre/.claude-plugin/plugin.json +14 -0
  73. package/adapters/claude-code/mods/ork-torre/hooks/agenda.ts +60 -0
  74. package/adapters/claude-code/mods/ork-torre/hooks/detalhe.tsx +236 -0
  75. package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +42 -0
  76. package/adapters/claude-code/mods/ork-torre/hooks/hooks.json +3 -0
  77. package/adapters/claude-code/mods/ork-torre/hooks/painel.tsx +344 -0
  78. package/adapters/claude-code/mods/ork-torre/hooks/rede.ts +97 -0
  79. package/adapters/claude-code/mods/ork-torre/hooks/register.tsx +437 -0
  80. package/adapters/claude-code/mods/ork-torre/orkastery-mod.json +5 -0
  81. package/adapters/claude-code/mods/ork-torre/types/index.d.ts +108 -0
  82. package/adapters/codex/skills/ork/SKILL.md +18 -3
  83. package/adapters/hermes/README.md +1 -0
  84. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  85. package/adapters/hermes/hermes.plugin.json +1 -0
  86. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  87. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +7 -6
  88. package/adapters/openclaw/README.md +26 -5
  89. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  90. package/adapters/openclaw/dist/index.js +46 -11
  91. package/adapters/openclaw/openclaw.plugin.json +5 -1
  92. package/adapters/openclaw/package.json +2 -2
  93. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  94. package/adapters/openclaw/src/index.ts +49 -11
  95. package/assets/docs-en/markdownlint-cli2.jsonc +22 -0
  96. package/assets/docs-en/padroes/documentacao-de-produto.md +135 -0
  97. package/assets/docs-en/padroes/roadmap-de-produto.md +100 -0
  98. package/assets/docs-en/produto/README.md +14 -0
  99. package/assets/docs-en/produto/_modelo-feature.md +62 -0
  100. package/assets/docs-en/roadmap/README.md +14 -0
  101. package/assets/docs-en/roadmap/_modelo-item.md +75 -0
  102. package/assets/orkmind_bridge.py +117 -9
  103. package/dist/adapters/claude-bg.js +116 -18
  104. package/dist/adapters/codex.js +14 -4
  105. package/dist/agents-md.js +24 -14
  106. package/dist/auditrun.js +22 -3
  107. package/dist/board.js +20 -17
  108. package/dist/branch-de-estado.js +88 -10
  109. package/dist/busca-semantica.js +52 -12
  110. package/dist/canarios-rm057.js +149 -0
  111. package/dist/canarios.js +2 -0
  112. package/dist/catalogo.js +33 -0
  113. package/dist/ci.js +21 -8
  114. package/dist/claims-conferir.js +342 -0
  115. package/dist/claims.js +41 -1
  116. package/dist/company-brain-client.js +36 -1
  117. package/dist/company-brain-context.js +158 -22
  118. package/dist/company-brain-journal.js +2 -1
  119. package/dist/company-brain-migration.js +36 -1
  120. package/dist/conducao-sentinela.js +388 -0
  121. package/dist/conducao-texto.js +12 -18
  122. package/dist/conducao.js +122 -30
  123. package/dist/conhecimento.js +490 -0
  124. package/dist/docs.js +70 -17
  125. package/dist/doctor-pulse-cron.js +349 -0
  126. package/dist/doctor-rede.js +150 -0
  127. package/dist/doctor.js +195 -111
  128. package/dist/entrega-pr.js +42 -29
  129. package/dist/escopo-escrita.js +10 -1
  130. package/dist/esgotamentos.js +181 -0
  131. package/dist/estado-thread.js +23 -1
  132. package/dist/experiencia-instalacao.js +45 -25
  133. package/dist/experiencia.js +12 -2
  134. package/dist/fabrica-batida.js +310 -0
  135. package/dist/fabrica-estado.js +52 -14
  136. package/dist/fabrica-publicar.js +9 -1
  137. package/dist/fechamento.js +20 -66
  138. package/dist/fila-pesada.js +444 -0
  139. package/dist/fix.js +3 -2
  140. package/dist/forja.js +31 -7
  141. package/dist/gates.js +3 -24
  142. package/dist/handoff.js +10 -6
  143. package/dist/hitl-contract.js +59 -4
  144. package/dist/hitl-curto.js +11 -6
  145. package/dist/hitl-gates.js +5 -0
  146. package/dist/hitl-local-receipt.js +2 -0
  147. package/dist/hitl-lote.js +5 -2
  148. package/dist/hitl-resumo.js +38 -3
  149. package/dist/hitl-tempo-parado.js +201 -0
  150. package/dist/hitl-texto-livre.js +5 -5
  151. package/dist/hitl.js +29 -33
  152. package/dist/horario.js +41 -7
  153. package/dist/hosts.js +104 -11
  154. package/dist/impedimento.js +299 -0
  155. package/dist/index.js +961 -542
  156. package/dist/indice-vetorial.js +87 -10
  157. package/dist/indice.js +9 -1
  158. package/dist/init.js +56 -43
  159. package/dist/instabilidade.js +144 -0
  160. package/dist/integracoes-locais.js +4 -0
  161. package/dist/intelligence-graph-cli.js +170 -16
  162. package/dist/intelligence-graph-contexto.js +254 -0
  163. package/dist/intelligence-graph-contract.js +2 -1
  164. package/dist/intelligence-graph-extract-md.js +56 -4
  165. package/dist/intelligence-graph-extract-ts.js +71 -3
  166. package/dist/intelligence-graph-parsers.js +6 -4
  167. package/dist/intelligence-graph-query.js +66 -19
  168. package/dist/leases.js +664 -87
  169. package/dist/ledger-stats.js +15 -1
  170. package/dist/ledger.js +18 -1
  171. package/dist/licoes.js +1 -0
  172. package/dist/liveness.js +6 -3
  173. package/dist/locale-ajuda.js +761 -0
  174. package/dist/locale-gerados.js +160 -0
  175. package/dist/locale-master.js +99 -0
  176. package/dist/locale-onboarding.js +92 -0
  177. package/dist/locale-phase.js +151 -0
  178. package/dist/locale-pulse.js +211 -0
  179. package/dist/locale-retry.js +385 -0
  180. package/dist/locale-roadmap.js +102 -0
  181. package/dist/locale-ship.js +494 -0
  182. package/dist/locale-thread.js +277 -0
  183. package/dist/locale-verify.js +125 -0
  184. package/dist/locale.js +714 -0
  185. package/dist/maestro-runtime.js +4 -1
  186. package/dist/manifest.js +149 -4
  187. package/dist/maquina.js +23 -4
  188. package/dist/master-digest.js +6 -1
  189. package/dist/master-nota.js +18 -11
  190. package/dist/master.js +145 -31
  191. package/dist/mcp-artifacts.js +43 -3
  192. package/dist/mcp-git.js +42 -8
  193. package/dist/mcp-grafo-worker.js +5 -3
  194. package/dist/mcp-grafo.js +55 -6
  195. package/dist/mcp-server.js +22 -3
  196. package/dist/mcp-ship.js +3 -1
  197. package/dist/mcp-verify.js +6 -1
  198. package/dist/memoria.js +48 -6
  199. package/dist/modos.js +3 -6
  200. package/dist/network-roadmap.js +196 -18
  201. package/dist/noctum-arco.js +911 -0
  202. package/dist/noctum-capacidade.js +141 -0
  203. package/dist/noctum-envelope.js +810 -0
  204. package/dist/noctum-execucao.js +304 -0
  205. package/dist/noctum-fila.js +280 -0
  206. package/dist/ocupacao.js +40 -5
  207. package/dist/onboarding.js +40 -21
  208. package/dist/orkmind.js +99 -7
  209. package/dist/orquestracao.js +24 -21
  210. package/dist/parado-no-condutor.js +219 -38
  211. package/dist/phase.js +160 -52
  212. package/dist/policies.js +56 -5
  213. package/dist/portfolio.js +8 -3
  214. package/dist/postura-local.js +234 -0
  215. package/dist/preflight.js +11 -10
  216. package/dist/procedencia.js +225 -0
  217. package/dist/projeto-alvo.js +132 -5
  218. package/dist/prompts.js +40 -1
  219. package/dist/prova-ativacao.js +75 -6
  220. package/dist/pulse-delivery.js +47 -4
  221. package/dist/pulse.js +98 -19
  222. package/dist/ratelimit.js +24 -6
  223. package/dist/recall.js +3 -1
  224. package/dist/redacao-saida.js +63 -0
  225. package/dist/rede-adesao.js +399 -0
  226. package/dist/rede-forja.js +289 -0
  227. package/dist/rede-projetos.js +239 -0
  228. package/dist/rede-status.js +337 -0
  229. package/dist/rede.js +980 -0
  230. package/dist/retry.js +291 -140
  231. package/dist/roadmap-reservas.js +20 -3
  232. package/dist/roadmap-status.js +150 -34
  233. package/dist/runtime-context.js +181 -3
  234. package/dist/runtime-profiles.js +22 -1
  235. package/dist/runtimes.js +2 -1
  236. package/dist/saida-segura.js +58 -0
  237. package/dist/session-events.js +7 -3
  238. package/dist/session-watcher-claude.js +120 -24
  239. package/dist/session-watcher.js +185 -33
  240. package/dist/sessoes-contas.js +187 -0
  241. package/dist/sessoes-inventario.js +6 -18
  242. package/dist/sessoes.js +83 -2
  243. package/dist/ship-publicar.js +800 -0
  244. package/dist/ship.js +157 -88
  245. package/dist/steal.js +77 -0
  246. package/dist/thread-close.js +3 -2
  247. package/dist/thread.js +299 -49
  248. package/dist/util.js +8 -1
  249. package/dist/verify.js +141 -52
  250. package/dist/vida-da-thread.js +254 -0
  251. package/dist/worktree-local.js +165 -0
  252. package/dist/worktree.js +25 -2
  253. package/dist/yaml.js +27 -8
  254. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  255. package/package.json +7 -3
  256. package/references/definition-of-done.md +1 -1
  257. package/schemas/code-artifact-graph.v1.schema.json +2 -1
  258. package/skills/README.md +1 -1
  259. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  260. package/skills/phases/master-metrics/SKILL.md +8 -3
@@ -0,0 +1,490 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LIMITES_DO_CONHECIMENTO = exports.REFERENCIA_DE_DECISAO = exports.NOME_DE_COLECAO = exports.ID_DE_DOCUMENTO = exports.CONTRATO_CONHECIMENTO = void 0;
37
+ exports.lerConhecimento = lerConhecimento;
38
+ /**
39
+ * RM-026 K7 (fatia 1): conhecimento escrito, no contrato `ork.conhecimento/v1`.
40
+ *
41
+ * Cada documento é um Markdown versionado na pasta que o `orkastery.yaml` declara (`conhecimento.dir`), com
42
+ * frontmatter fechado: `id` (`doc-...`), `titulo`, `colecoes` e, opcional, `referencias` a ids do Brain e a
43
+ * decisões `thread#linha` do ledger. A coleção é o nome no frontmatter, sem arquivo próprio.
44
+ *
45
+ * Só leitura. Documento inválido ou referência que não confere vira lacuna tipada, nunca resposta (D4). A pasta
46
+ * vem com o repositório e segue a fronteira da RM-047 (D6): relativa, dentro da raiz pelo caminho real, sem
47
+ * seguir link e sem executar nada. Nesta fatia a decisão confere no ledger canônico e o id do Brain só pela forma
48
+ * (D5); a CLI, as tools MCP e a conferência pelo Brain chegam nas fatias 2 e 3. Contrato:
49
+ * docs/referencia/contratos/conhecimento-rm026.md.
50
+ */
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
53
+ const company_brain_journal_1 = require("./company-brain-journal");
54
+ const docs_1 = require("./docs");
55
+ const portfolio_1 = require("./portfolio");
56
+ const saida_segura_1 = require("./saida-segura");
57
+ const thread_1 = require("./thread");
58
+ const yaml_1 = require("./yaml");
59
+ exports.CONTRATO_CONHECIMENTO = 'ork.conhecimento/v1';
60
+ /** D3: a forma do id do documento (a regra dos ids do portfólio), da coleção e da citação de decisão. */
61
+ exports.ID_DE_DOCUMENTO = /^doc-[a-z0-9][a-z0-9-]{2,47}$/;
62
+ exports.NOME_DE_COLECAO = /^[a-z0-9][a-z0-9-]{0,47}$/;
63
+ exports.REFERENCIA_DE_DECISAO = /^([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})#([1-9][0-9]{0,8})$/;
64
+ const LIMITE_DO_TITULO = 160;
65
+ const COLECOES_POR_DOCUMENTO = 16;
66
+ const REFERENCIAS_POR_LISTA = 64;
67
+ /** D2: as chaves do frontmatter; qualquer outra invalida o documento. */
68
+ const CHAVES_DO_DOCUMENTO = ['id', 'titulo', 'colecoes', 'referencias'];
69
+ const CHAVES_DAS_REFERENCIAS = ['brain', 'decisoes'];
70
+ /** D5: as linhas do ledger que são decisão, as do dossiê; `human_decision` é relato sem recibo e não conta. */
71
+ const TIPOS_DE_DECISAO = ['autonomous_decision', 'hitl_requested', 'human_gate'];
72
+ /** D6: pastas do repositório que nunca guardam conhecimento, sem distinguir caixa (o disco do macOS não distingue). */
73
+ const PASTAS_RESERVADAS = ['.git', '.orkastery'];
74
+ const reservada = (segmento) => PASTAS_RESERVADAS.includes(segmento.toLowerCase());
75
+ /** D10: o invisível é o do `saida-segura.ts`, a regra única do núcleo para texto alheio (ZWJ e seletores de emoji valem). */
76
+ const INVISIVEIS = new RegExp(saida_segura_1.INVISIVEL.source, 'gu');
77
+ exports.LIMITES_DO_CONHECIMENTO = Object.freeze({
78
+ entradas: 5000, profundidade: 8, bytesPorDocumento: 1024 * 1024, bytesDoFrontmatter: 16 * 1024,
79
+ bytesNoTotal: 32 * 1024 * 1024, bytesDoLedger: 16 * 1024 * 1024,
80
+ });
81
+ const comparar = (a, b) => a < b ? -1 : a > b ? 1 : 0;
82
+ const ehMapa = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
83
+ /** Texto que veio do repositório, pronto para a resposta: numa linha, sem invisível e, com teto, cortado sem partir caractere. */
84
+ const limpo = (v, teto = Infinity) => Array.from((0, saida_segura_1.emUmaLinha)(v)).slice(0, teto).join('');
85
+ const relativo = (raizReal, abs) => path.relative(raizReal, abs).split(path.sep).join('/');
86
+ const LINK = 'link simbólico não é seguido; o documento é o arquivo versionado';
87
+ /** `stat` que nunca lança: a pasta que some ou não abre vale como ausente. */
88
+ const statOuNulo = (p) => { try {
89
+ return fs.statSync(p, { throwIfNoEntry: false }) ?? null;
90
+ }
91
+ catch {
92
+ return null;
93
+ } };
94
+ function lacuna(codigo, detalhe, onde = {}) {
95
+ return { codigo, arquivo: onde.arquivo ?? null, documento: onde.documento ?? null,
96
+ referencia: typeof onde.referencia === 'string' ? limpo(onde.referencia, 200) : null, detalhe };
97
+ }
98
+ /**
99
+ * D6: a pasta declarada, pelo caminho real. A forma recusa antes de tocar o disco (vazia, absoluta, `..`, `\`,
100
+ * invisível, a própria raiz, `.git` e `.orkastery`); o caminho real, com os links resolvidos, precisa ficar dentro
101
+ * da raiz.
102
+ */
103
+ function pastaDoConhecimento(raiz, dir) {
104
+ const segmentos = dir.split('/').filter((s) => s !== '' && s !== '.');
105
+ if (path.isAbsolute(dir) || dir.includes('\\') || saida_segura_1.INVISIVEL.test(dir) || !segmentos.length || segmentos.includes('..') ||
106
+ reservada(segmentos[0])) {
107
+ return lacuna('conhecimento.dir-invalido', 'conhecimento.dir precisa ser uma pasta relativa à raiz, sem "..", ' +
108
+ 'fora de .git e .orkastery, como docs/conhecimento; nada foi lido');
109
+ }
110
+ let raizReal, real;
111
+ try {
112
+ raizReal = fs.realpathSync(raiz);
113
+ real = fs.realpathSync(path.join(raizReal, ...segmentos));
114
+ }
115
+ catch {
116
+ return lacuna('conhecimento.dir-ausente', 'a pasta de conhecimento.dir não existe ou não abre');
117
+ }
118
+ const rel = path.relative(raizReal, real);
119
+ if (rel === '..' || rel.startsWith('..' + path.sep) || path.isAbsolute(rel)) {
120
+ return lacuna('conhecimento.dir-fora-da-raiz', 'pelo caminho real (links resolvidos), conhecimento.dir sai da raiz ' +
121
+ 'do projeto; o manifesto vem com o repositório e não escolhe caminho fora dela; nada foi lido');
122
+ }
123
+ if (rel === '' || reservada(rel.split(path.sep)[0]) || saida_segura_1.INVISIVEL.test(rel)) {
124
+ return lacuna('conhecimento.dir-invalido', 'pelo caminho real, conhecimento.dir é a própria raiz, cai em .git ou ' +
125
+ '.orkastery, ou tem caractere invisível; nada foi lido');
126
+ }
127
+ if (!statOuNulo(real)?.isDirectory())
128
+ return lacuna('conhecimento.dir-ausente', 'conhecimento.dir não é uma pasta');
129
+ return { real, raizReal };
130
+ }
131
+ /**
132
+ * Varre a pasta sem seguir link (D6) e sem ler entrada oculta (D8). Devolve os candidatos a documento, ou o
133
+ * detalhe do teto estourado (D7), que falha fechado. Entrada que não abre vira lacuna, nunca exceção.
134
+ */
135
+ function varrer(real, raizReal, limites, lacunas) {
136
+ const candidatos = [];
137
+ const pilha = [{ abs: real, profundidade: 0 }];
138
+ let entradas = 0;
139
+ while (pilha.length) {
140
+ const atual = pilha.pop();
141
+ // Uma pasta trocada por link depois de entrar na pilha não leva a varredura para fora: o caminho real é
142
+ // conferido logo antes da listagem. Num clone parado não há troca; a conferência fecha a janela larga.
143
+ let realDaPasta;
144
+ try {
145
+ realDaPasta = fs.realpathSync(atual.abs);
146
+ }
147
+ catch {
148
+ realDaPasta = null;
149
+ }
150
+ if (realDaPasta !== atual.abs) {
151
+ lacunas.push(lacuna('documento.link', LINK, { arquivo: relativo(raizReal, atual.abs) }));
152
+ continue;
153
+ }
154
+ let nomes;
155
+ try {
156
+ nomes = fs.readdirSync(atual.abs, { encoding: 'buffer' }).sort(Buffer.compare);
157
+ }
158
+ catch {
159
+ lacunas.push(lacuna('documento.nao-regular', 'a pasta não abriu para leitura; o que está nela não foi lido', { arquivo: relativo(raizReal, atual.abs) }));
160
+ continue;
161
+ }
162
+ for (const bruto of nomes) {
163
+ if (bruto[0] === 0x2e)
164
+ continue;
165
+ if (++entradas > limites.entradas)
166
+ return { teto: `a pasta passa de ${limites.entradas} entradas` };
167
+ const nome = bruto.toString('utf8');
168
+ if (!Buffer.from(nome, 'utf8').equals(bruto) || saida_segura_1.INVISIVEL.test(nome)) {
169
+ lacunas.push(lacuna('documento.nome-invalido', 'nome fora de UTF-8 ou com caractere invisível; não foi lido', { arquivo: relativo(raizReal, path.join(atual.abs, nome.replace(INVISIVEIS, '?'))) }));
170
+ continue;
171
+ }
172
+ const abs = path.join(atual.abs, nome), arquivo = relativo(raizReal, abs);
173
+ let st;
174
+ try {
175
+ st = fs.lstatSync(abs, { throwIfNoEntry: false });
176
+ }
177
+ catch {
178
+ lacunas.push(lacuna('documento.nao-regular', 'a entrada não abriu para leitura; não foi lida', { arquivo }));
179
+ continue;
180
+ }
181
+ if (!st)
182
+ continue;
183
+ if (st.isSymbolicLink()) {
184
+ lacunas.push(lacuna('documento.link', LINK, { arquivo }));
185
+ continue;
186
+ }
187
+ if (st.isDirectory()) {
188
+ if (atual.profundidade + 1 > limites.profundidade)
189
+ return { teto: `a pasta passa de ${limites.profundidade} níveis` };
190
+ pilha.push({ abs, profundidade: atual.profundidade + 1 });
191
+ continue;
192
+ }
193
+ if (!(0, docs_1.ehPaginaDeDocs)(nome))
194
+ continue;
195
+ if (!st.isFile()) {
196
+ lacunas.push(lacuna('documento.nao-regular', 'não é arquivo comum; não foi lido', { arquivo }));
197
+ continue;
198
+ }
199
+ candidatos.push({ abs, arquivo, st });
200
+ }
201
+ }
202
+ return candidatos;
203
+ }
204
+ /**
205
+ * Lê um arquivo comum sem seguir link (`O_NOFOLLOW`) e sem travar em FIFO (`O_NONBLOCK`), conferindo que é o
206
+ * mesmo do `lstat` e que cabe no teto. Devolve os bytes ou o motivo. Com `crescer` (o ledger, que só recebe linha
207
+ * no fim), vale o que existia no `fstat`, mesmo que o arquivo cresça durante a leitura; o documento que muda
208
+ * durante a leitura não vale.
209
+ */
210
+ function lerArquivo(abs, st, teto, crescer = false) {
211
+ let fd;
212
+ try {
213
+ fd = fs.openSync(abs, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
214
+ }
215
+ catch (e) {
216
+ return e.code === 'ELOOP' ? 'link' : 'nao-regular';
217
+ }
218
+ try {
219
+ const f = fs.fstatSync(fd);
220
+ if (!f.isFile() || f.dev !== st.dev || f.ino !== st.ino)
221
+ return 'nao-regular';
222
+ if (f.size > teto)
223
+ return 'grande';
224
+ const bytes = Buffer.alloc(crescer ? f.size : f.size + 1);
225
+ let lidos = 0, n;
226
+ while (lidos < bytes.length && (n = fs.readSync(fd, bytes, lidos, bytes.length - lidos, null)) > 0)
227
+ lidos += n;
228
+ if (crescer)
229
+ return bytes.subarray(0, lidos);
230
+ return lidos === f.size ? bytes.subarray(0, lidos) : 'nao-regular';
231
+ }
232
+ finally {
233
+ fs.closeSync(fd);
234
+ }
235
+ }
236
+ /**
237
+ * Valor de lista do frontmatter: lista de textos ou um texto só. Inteiro vira texto, sem perda; nulo, booleano
238
+ * e decimal (o `yes` que o YAML lê como `true`, a vírgula sobrando que vira nulo) recusam em vez de virar texto.
239
+ */
240
+ function lista(v) {
241
+ if (v === undefined || v === null)
242
+ return [];
243
+ const itens = [];
244
+ for (const b of Array.isArray(v) ? v : [v]) {
245
+ if (typeof b === 'string')
246
+ itens.push(b);
247
+ else if (typeof b === 'number' && Number.isSafeInteger(b))
248
+ itens.push(String(b));
249
+ else
250
+ return null;
251
+ }
252
+ return itens;
253
+ }
254
+ /** D2 e D3: o frontmatter vale inteiro ou devolve a lacuna do primeiro problema, com o id quando ele já vale. */
255
+ function validarFrontmatter(dados, arquivo) {
256
+ if (!ehMapa(dados))
257
+ return lacuna('documento.frontmatter-invalido', 'o frontmatter precisa ser um mapa de chaves', { arquivo });
258
+ const id = typeof dados.id === 'string' && exports.ID_DE_DOCUMENTO.test(dados.id) ? dados.id : null;
259
+ if (!id) {
260
+ return lacuna('documento.id-invalido', 'id precisa ser doc- seguido de 3 a 48 letras minúsculas, números ou hífens, ' +
261
+ 'começando por letra ou número', { arquivo });
262
+ }
263
+ const onde = { arquivo, documento: id };
264
+ const estranhas = Object.keys(dados).filter((k) => !CHAVES_DO_DOCUMENTO.includes(k));
265
+ if (estranhas.length) {
266
+ return lacuna('documento.chave-desconhecida', `chave fora do contrato: ${estranhas.map((k) => JSON.stringify(limpo(k, 40))).join(', ')} ` +
267
+ `(o frontmatter tem só ${CHAVES_DO_DOCUMENTO.join(', ')})`, onde);
268
+ }
269
+ const titulo = typeof dados.titulo === 'string' ? dados.titulo.trim() : '';
270
+ if (!titulo || titulo.length > LIMITE_DO_TITULO || saida_segura_1.INVISIVEL.test(titulo)) {
271
+ return lacuna('documento.titulo-invalido', `titulo precisa ser texto de 1 a ${LIMITE_DO_TITULO} caracteres, ` +
272
+ 'sem caractere de controle', onde);
273
+ }
274
+ const colecoes = lista(dados.colecoes);
275
+ if (!colecoes || !colecoes.length || colecoes.length > COLECOES_POR_DOCUMENTO ||
276
+ colecoes.some((c) => !exports.NOME_DE_COLECAO.test(c)) || new Set(colecoes).size !== colecoes.length) {
277
+ return lacuna('documento.colecoes-invalidas', `colecoes precisa listar de 1 a ${COLECOES_POR_DOCUMENTO} nomes ` +
278
+ 'de 1 a 48 letras minúsculas, números ou hífens, sem repetir', onde);
279
+ }
280
+ const refs = dados.referencias ?? null;
281
+ const brain = ehMapa(refs) ? lista(refs.brain) : [], decisoes = ehMapa(refs) ? lista(refs.decisoes) : [];
282
+ if ((refs !== null && !ehMapa(refs)) || (ehMapa(refs) && Object.keys(refs).some((k) => !CHAVES_DAS_REFERENCIAS.includes(k))) ||
283
+ !brain || brain.length > REFERENCIAS_POR_LISTA || !decisoes || decisoes.length > REFERENCIAS_POR_LISTA) {
284
+ return lacuna('documento.referencias-invalidas', 'referencias é um mapa só com brain e decisoes, cada uma uma lista ' +
285
+ `de até ${REFERENCIAS_POR_LISTA} textos`, onde);
286
+ }
287
+ return { id, titulo, colecoes: [...colecoes].sort(comparar), brain: [...new Set(brain)], decisoes: [...new Set(decisoes)] };
288
+ }
289
+ /**
290
+ * As linhas citadas do ledger canônico de uma thread (D5). O ledger é lido como a captura do Brain o lê
291
+ * (`readCycle`): só linhas completas, em UTF-8 exato, para o sha256 da linha ser o `source_hash` dela no Brain.
292
+ * Cada ledger é lido uma vez, e só as linhas citadas ficam na memória.
293
+ */
294
+ function linhasCitadas(raiz, thread, citadas, teto) {
295
+ let arquivo;
296
+ try {
297
+ arquivo = path.join((0, thread_1.dirThread)(raiz, thread), 'ledger.jsonl');
298
+ }
299
+ catch {
300
+ return { motivo: 'a pasta da thread no estado do projeto é link ou fica fora da regra de id' };
301
+ }
302
+ let st;
303
+ try {
304
+ st = fs.lstatSync(arquivo, { throwIfNoEntry: false });
305
+ }
306
+ catch (e) {
307
+ // A pasta da thread que é arquivo comum dá ENOTDIR: a thread não tem ledger, e nada lança.
308
+ if (e.code !== 'ENOTDIR')
309
+ return { motivo: `o ledger da thread ${thread} não abriu para leitura` };
310
+ }
311
+ if (!st)
312
+ return { motivo: `a thread ${thread} não tem ledger neste projeto` };
313
+ const bytes = lerArquivo(arquivo, st, teto, true);
314
+ if (!Buffer.isBuffer(bytes))
315
+ return { motivo: `o ledger da thread ${thread} não é arquivo comum de até ${teto / 1024 / 1024} MiB` };
316
+ const fim = bytes.lastIndexOf(10) + 1;
317
+ const completo = bytes.subarray(0, fim).toString('utf8');
318
+ if (!Buffer.from(completo, 'utf8').equals(bytes.subarray(0, fim)))
319
+ return { motivo: `o ledger da thread ${thread} não é UTF-8` };
320
+ const todas = completo.split('\n').slice(0, -1);
321
+ const linhas = new Map();
322
+ for (const n of citadas)
323
+ if (n <= todas.length)
324
+ linhas.set(n, todas[n - 1]);
325
+ return { linhas, completas: todas.length };
326
+ }
327
+ /** D5: a referência `thread#linha` confere quando a linha completa existe, é da thread e é uma decisão. */
328
+ function conferirDecisao(ref, ledgers) {
329
+ const m = exports.REFERENCIA_DE_DECISAO.exec(ref);
330
+ if (!m)
331
+ return { codigo: 'referencia.invalida', detalhe: 'decisão é <thread>#<linha>, com o id da thread e a linha do ledger a partir de 1' };
332
+ const thread = m[1], linha = Number(m[2]);
333
+ const ledger = ledgers.get(thread);
334
+ if ('motivo' in ledger)
335
+ return { codigo: 'referencia.decisao-ausente', detalhe: ledger.motivo };
336
+ const bruta = ledger.linhas.get(linha);
337
+ if (bruta === undefined) {
338
+ return { codigo: 'referencia.decisao-ausente', detalhe: `o ledger da thread ${thread} tem ${ledger.completas} linha(s) completa(s)` };
339
+ }
340
+ let evento = null;
341
+ try {
342
+ const lido = JSON.parse(bruta);
343
+ if (lido && typeof lido === 'object' && !Array.isArray(lido))
344
+ evento = lido;
345
+ }
346
+ catch { /* linha que não é JSON não é decisão */ }
347
+ const onde = `a linha ${linha} do ledger da thread ${thread}`;
348
+ if (!evento)
349
+ return { codigo: 'referencia.nao-e-decisao', detalhe: `${onde} não é um evento JSON` };
350
+ if (evento.thread !== thread)
351
+ return { codigo: 'referencia.nao-e-decisao', detalhe: `${onde} é de outra thread` };
352
+ const tipo = TIPOS_DE_DECISAO.find((t) => t === evento?.tipo);
353
+ if (!tipo) {
354
+ const visto = typeof evento.tipo === 'string' && /^[a-z_]{1,40}$/.test(evento.tipo) ? evento.tipo : 'sem tipo conhecido';
355
+ return { codigo: 'referencia.nao-e-decisao', detalhe: `${onde} é ${visto}, não decisão (${TIPOS_DE_DECISAO.join(', ')})` };
356
+ }
357
+ return { ref, thread, linha, tipo, fonte: `threads/${thread}/ledger.jsonl#L${linha}`, sha256: (0, company_brain_journal_1.bytesHash)(bruta) };
358
+ }
359
+ /**
360
+ * Lê o conhecimento declarado no manifesto: documentos válidos, coleções e lacunas, tudo em ordem estável.
361
+ * Nunca lança por causa do conteúdo do repositório; o que não vale vira lacuna tipada.
362
+ */
363
+ function lerConhecimento(c, limites = exports.LIMITES_DO_CONHECIMENTO) {
364
+ const declarado = c.manifesto.conhecimento?.dir ?? null;
365
+ const dir = declarado === null ? null : limpo(declarado);
366
+ const so = (l) => ({ contrato: exports.CONTRATO_CONHECIMENTO, dir, documentos: [], colecoes: [], lacunas: [l] });
367
+ if (declarado === null)
368
+ return so(lacuna('conhecimento.nao-declarado', 'o orkastery.yaml não declara conhecimento.dir'));
369
+ const pasta = pastaDoConhecimento(c.raiz, declarado);
370
+ if ('codigo' in pasta)
371
+ return so(pasta);
372
+ const teto = (detalhe) => so(lacuna('conhecimento.limite', `${detalhe}; nenhum documento foi devolvido`));
373
+ const lacunas = [];
374
+ const candidatos = varrer(pasta.real, pasta.raizReal, limites, lacunas);
375
+ if (!Array.isArray(candidatos))
376
+ return teto(candidatos.teto);
377
+ const decodificador = new TextDecoder('utf-8', { fatal: true });
378
+ const validos = [];
379
+ const declarados = new Map();
380
+ let total = 0;
381
+ for (const { abs, arquivo, st } of candidatos) {
382
+ const bytes = lerArquivo(abs, st, limites.bytesPorDocumento);
383
+ if (bytes === 'link') {
384
+ lacunas.push(lacuna('documento.link', LINK, { arquivo }));
385
+ continue;
386
+ }
387
+ if (bytes === 'nao-regular') {
388
+ lacunas.push(lacuna('documento.nao-regular', 'não abriu como arquivo comum, ou mudou durante a leitura', { arquivo }));
389
+ continue;
390
+ }
391
+ if (bytes === 'grande') {
392
+ lacunas.push(lacuna('documento.grande-demais', `o documento passa de ${limites.bytesPorDocumento / 1024} KiB`, { arquivo }));
393
+ continue;
394
+ }
395
+ total += bytes.length;
396
+ if (total > limites.bytesNoTotal)
397
+ return teto(`os documentos passam de ${limites.bytesNoTotal / 1024 / 1024} MiB no total`);
398
+ let texto;
399
+ try {
400
+ texto = decodificador.decode(bytes);
401
+ }
402
+ catch {
403
+ lacunas.push(lacuna('documento.codificacao', 'o arquivo não é UTF-8', { arquivo }));
404
+ continue;
405
+ }
406
+ const { bruto } = (0, docs_1.separarFrontmatter)(texto);
407
+ if (bruto === null) {
408
+ lacunas.push(lacuna('documento.sem-frontmatter', 'o arquivo não começa com o frontmatter entre duas linhas ---', { arquivo }));
409
+ continue;
410
+ }
411
+ if (Buffer.byteLength(bruto, 'utf8') > limites.bytesDoFrontmatter) {
412
+ lacunas.push(lacuna('documento.frontmatter-invalido', `o frontmatter passa de ${limites.bytesDoFrontmatter / 1024} KiB`, { arquivo }));
413
+ continue;
414
+ }
415
+ let dados;
416
+ // A linha do `---` de abertura entra na leitura (e é pulada nela) para o erro dizer a linha do arquivo.
417
+ try {
418
+ dados = (0, yaml_1.lerYaml)('---\n' + bruto, { estrito: true });
419
+ }
420
+ catch (e) {
421
+ lacunas.push(lacuna('documento.frontmatter-invalido', limpo(e.message, 200), { arquivo }));
422
+ continue;
423
+ }
424
+ // D4: o id na forma certa conta para a regra do repetido mesmo quando o resto do documento não vale.
425
+ const idDeclarado = ehMapa(dados) && typeof dados.id === 'string' && exports.ID_DE_DOCUMENTO.test(dados.id) ? dados.id : null;
426
+ if (idDeclarado)
427
+ declarados.set(idDeclarado, (declarados.get(idDeclarado) ?? 0) + 1);
428
+ const v = validarFrontmatter(dados, arquivo);
429
+ if ('codigo' in v) {
430
+ lacunas.push(v);
431
+ continue;
432
+ }
433
+ validos.push({ ...v, arquivo, bytes: bytes.length, sha256: (0, company_brain_journal_1.bytesHash)(bytes) });
434
+ }
435
+ // D4: id repetido não escolhe arquivo; todos os documentos com ele saem da resposta, também quando a outra
436
+ // cópia não vale por outro motivo (ela já tem a lacuna dela).
437
+ const unicos = validos.filter((v) => {
438
+ const vezes = declarados.get(v.id);
439
+ if (vezes > 1) {
440
+ lacunas.push(lacuna('documento.id-repetido', `o id aparece em ${vezes} arquivos; nenhum deles vale até sobrar um`, { arquivo: v.arquivo, documento: v.id }));
441
+ }
442
+ return vezes === 1;
443
+ });
444
+ // D5: cada ledger citado é lido uma vez, com as linhas que os documentos válidos pedem.
445
+ const citadas = new Map();
446
+ for (const v of unicos)
447
+ for (const ref of v.decisoes) {
448
+ const m = exports.REFERENCIA_DE_DECISAO.exec(ref);
449
+ if (m)
450
+ citadas.set(m[1], (citadas.get(m[1]) ?? new Set()).add(Number(m[2])));
451
+ }
452
+ const ledgers = new Map();
453
+ for (const [thread, linhas] of citadas)
454
+ ledgers.set(thread, linhasCitadas(c.raiz, thread, linhas, limites.bytesDoLedger));
455
+ const documentos = [];
456
+ for (const v of unicos) {
457
+ const onde = { arquivo: v.arquivo, documento: v.id };
458
+ const brain = [], decisoes = [];
459
+ for (const referencia of v.brain) {
460
+ if (portfolio_1.ID_DE_ENTIDADE.test(referencia))
461
+ brain.push(referencia);
462
+ else
463
+ lacunas.push(lacuna('referencia.invalida', 'id do Brain é prod-, proj- ou init-, na regra dos ids do portfólio', { ...onde, referencia }));
464
+ }
465
+ for (const referencia of v.decisoes) {
466
+ const r = conferirDecisao(referencia, ledgers);
467
+ if ('codigo' in r)
468
+ lacunas.push(lacuna(r.codigo, r.detalhe, { ...onde, referencia }));
469
+ else
470
+ decisoes.push(r);
471
+ }
472
+ documentos.push({ id: v.id, titulo: v.titulo, colecoes: v.colecoes,
473
+ referencias: { brain: brain.sort(comparar), decisoes: decisoes.sort((a, b) => comparar(a.thread, b.thread) || a.linha - b.linha) },
474
+ arquivo: v.arquivo, bytes: v.bytes, sha256: v.sha256 });
475
+ }
476
+ const colecoes = new Map();
477
+ for (const d of documentos)
478
+ for (const nome of d.colecoes) {
479
+ const ids = colecoes.get(nome) ?? [];
480
+ ids.push(d.id);
481
+ colecoes.set(nome, ids);
482
+ }
483
+ return {
484
+ contrato: exports.CONTRATO_CONHECIMENTO, dir,
485
+ documentos: documentos.sort((a, b) => comparar(a.id, b.id)),
486
+ colecoes: [...colecoes].map(([nome, ids]) => ({ nome, documentos: ids.sort(comparar) })).sort((a, b) => comparar(a.nome, b.nome)),
487
+ lacunas: lacunas.sort((a, b) => comparar(a.arquivo ?? '', b.arquivo ?? '') || comparar(a.codigo, b.codigo) ||
488
+ comparar(a.referencia ?? '', b.referencia ?? '')),
489
+ };
490
+ }
package/dist/docs.js CHANGED
@@ -43,6 +43,7 @@ exports.comandosDaAjuda = comandosDaAjuda;
43
43
  exports.verificarDocs = verificarDocs;
44
44
  exports.renderizarRelance = renderizarRelance;
45
45
  exports.renderizarEstado = renderizarEstado;
46
+ exports.docsPendentesDoMerge = docsPendentesDoMerge;
46
47
  exports.textoDaVerificacao = textoDaVerificacao;
47
48
  exports.escreverYaml = escreverYaml;
48
49
  exports.mergeDaThread = mergeDaThread;
@@ -78,6 +79,8 @@ const node_child_process_1 = require("node:child_process");
78
79
  const yaml_1 = require("./yaml");
79
80
  const thread_1 = require("./thread");
80
81
  const ledger_1 = require("./ledger");
82
+ const branch_de_estado_1 = require("./branch-de-estado");
83
+ const locale_1 = require("./locale");
81
84
  exports.DIR_PRODUTO = 'docs/produto';
82
85
  exports.DIR_ROADMAP = 'docs/roadmap';
83
86
  exports.DIR_PADROES = 'docs/padroes';
@@ -198,6 +201,9 @@ function git(raiz, args) {
198
201
  }
199
202
  /** Resolve a branch base: local, depois `origin/`, porque o CI faz checkout destacado. */
200
203
  function resolverBase(raiz, preferida = 'main') {
204
+ // RM-047 (fronteira de confiança): `preferida` vem do `worktree.base_branch` do clone.
205
+ if (!(0, branch_de_estado_1.branchValida)(preferida))
206
+ return null;
201
207
  for (const ref of [preferida, `origin/${preferida}`]) {
202
208
  if (git(raiz, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]).ok)
203
209
  return ref;
@@ -560,7 +566,8 @@ function verificarEstadoDoRoadmap(raiz, d, base, achados) {
560
566
  achados.push(erro(d.arquivo, d.id, 'docs.paridade.git', 'código Mesclado sem o commit do merge', 'preencha evidencias.codigo.commit (ork docs sincronizar faz isso a partir do ledger e do git)'));
561
567
  }
562
568
  if (commit && base) {
563
- if (!git(raiz, ['cat-file', '-e', `${commit}^{commit}`]).ok) {
569
+ // RM-047: o commit vem do frontmatter do clone; só sha hexadecimal vai ao git.
570
+ if (!(0, branch_de_estado_1.shaValido)(commit) || !git(raiz, ['cat-file', '-e', `${commit}^{commit}`]).ok) {
564
571
  achados.push(erro(d.arquivo, d.id, 'docs.paridade.git', `o commit ${commit} não existe neste repositório`));
565
572
  }
566
573
  else if (codigo === 'Mesclado' && !git(raiz, ['merge-base', '--is-ancestor', commit, base]).ok) {
@@ -576,20 +583,58 @@ function verificarEstadoDoRoadmap(raiz, d, base, achados) {
576
583
  * mesmo `ship(<thread>)` do `sincronizarDocs`: tudo o que ela acusa o `sincronizar` corrige.
577
584
  */
578
585
  function verificarMergeDaThread(raiz, d, base, achados, pr = false) {
579
- const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
580
- const thread = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread.trim() : '';
581
- if (!thread)
582
- return;
583
- const merges = mergesDaThreadNoGit(raiz, thread, base);
584
- if (merges.length === 0)
585
- return;
586
- const codigo = String((ehMapa(d.dados.estado) ? d.dados.estado.codigo : null) ?? '');
587
- if (codigo === 'Mesclado')
586
+ const pendente = pendenciaDeMerge(raiz, d, base);
587
+ if (!pendente)
588
588
  return;
589
+ const { thread, merges, codigo } = pendente;
589
590
  achados.push((pr ? aviso : erro)(d.arquivo, d.id, 'docs.paridade.merge', `a thread ${thread} entrou na ${base} pelo merge ${merges[0].slice(0, 7)} (ship(${thread})), e o estado.codigo diz "${codigo || 'vazio'}"` +
590
591
  (pr ? ' (no PR, aviso: o push da main reprova)' : ''), `abra um PR de docs sobre a ${base} atualizada com ork docs sincronizar --escrever --so ${d.id} (o item e os índices); ` +
591
592
  `se o item tem outra fatia em curso, aponte sdlc.thread para a thread dela`));
592
593
  }
594
+ /**
595
+ * A regra do `docs.paridade.merge`, isolada para o `ork ship registrar-pr` (RM-044, A3): a pagina cujo
596
+ * `sdlc.thread` ja tem merge `ship(<thread>)` na base e cujo `estado.codigo` ainda nao diz `Mesclado`.
597
+ * `thread`, quando dada, restringe a pagina a essa thread.
598
+ */
599
+ function pendenciaDeMerge(raiz, d, base, thread) {
600
+ const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
601
+ const daPagina = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread.trim() : '';
602
+ if (!daPagina || (thread !== undefined && daPagina !== thread))
603
+ return null;
604
+ const codigo = String((ehMapa(d.dados.estado) ? d.dados.estado.codigo : null) ?? '');
605
+ if (codigo === 'Mesclado')
606
+ return null;
607
+ const merges = mergesDaThreadNoGit(raiz, daPagina, base);
608
+ if (merges.length === 0)
609
+ return null;
610
+ return { thread: daPagina, merges, codigo };
611
+ }
612
+ /**
613
+ * RM-044 (A3): as paginas do roadmap, lidas da `ref` (a base remota depois do fetch, nao a copia local,
614
+ * que pode estar atras), que a regra `docs.paridade.merge` vai acusar por causa da `thread`. Nao grava
615
+ * nada: so lista. Ref ilegivel ou sem `docs/roadmap` devolve lista vazia.
616
+ */
617
+ function docsPendentesDoMerge(raiz, thread, ref) {
618
+ const ls = git(raiz, ['ls-tree', '--name-only', `${ref}:${exports.DIR_ROADMAP}`]);
619
+ if (!ls.ok || !ls.saida)
620
+ return [];
621
+ const pendentes = [];
622
+ for (const nome of ls.saida.split('\n').filter(ehPaginaDeDocs).sort()) {
623
+ const arquivo = `${exports.DIR_ROADMAP}/${nome}`;
624
+ const blob = git(raiz, ['show', `${ref}:${arquivo}`]);
625
+ if (!blob.ok)
626
+ continue;
627
+ const lido = documentoDeTexto(arquivo, `${blob.saida}\n`);
628
+ if (!('doc' in lido) || lido.doc.tipo !== 'roadmap')
629
+ continue;
630
+ const p = pendenciaDeMerge(raiz, lido.doc, ref, thread);
631
+ if (!p)
632
+ continue;
633
+ pendentes.push({ id: lido.doc.id, arquivo, codigo: p.codigo, merge: p.merges[0],
634
+ comando: `ork docs sincronizar --escrever --so ${lido.doc.id}` });
635
+ }
636
+ return pendentes;
637
+ }
593
638
  /** Os indices gerados (`docs/roadmap/README.md`, `docs/produto/README.md`) e o que o frontmatter diz. */
594
639
  const INDICES_GERADOS = [
595
640
  [`${exports.DIR_ROADMAP}/README.md`, (docs) => tabelaDoRoadmap(docs)],
@@ -716,7 +761,7 @@ function mergeDaThread(raiz, thread, base) {
716
761
  try {
717
762
  const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread));
718
763
  const ship = eventos.filter((e) => e.tipo === 'ship_done' && typeof e.mergeSha === 'string')
719
- .find((e) => git(raiz, ['merge-base', '--is-ancestor', String(e.mergeSha), base]).ok);
764
+ .find((e) => (0, branch_de_estado_1.shaValido)(e.mergeSha) && (0, branch_de_estado_1.branchValida)(base) && git(raiz, ['merge-base', '--is-ancestor', e.mergeSha, base]).ok);
720
765
  if (ship)
721
766
  return { sha: String(ship.mergeSha).slice(0, 7), fonte: 'ledger ship_done' };
722
767
  }
@@ -734,6 +779,9 @@ function mergesDaThreadNoGit(raiz, thread, base) {
734
779
  // Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
735
780
  // GO-FIX 2 (achado 4 do CHECK): so a linha de primeiro pai da base e o assunto que COMECA com
736
781
  // `ship(<thread>)`, como no `mergeDaEntrega`. Commit que cita o merge no corpo, ou o Revert dele, nao e merge.
782
+ // RM-047 (fronteira de confiança): a base pode ser o `worktree.base_branch` cru do clone.
783
+ if (!(0, branch_de_estado_1.branchValida)(base))
784
+ return [];
737
785
  const log = git(raiz, ['log', base, '--first-parent', '--reverse', '--format=%H%x09%s', '--fixed-strings', `--grep=ship(${thread})`]);
738
786
  if (!log.ok)
739
787
  return [];
@@ -915,16 +963,21 @@ const ARQUIVOS_DO_PADRAO = [
915
963
  ['roadmap/README.md', `${exports.DIR_ROADMAP}/README.md`],
916
964
  ['markdownlint-cli2.jsonc', '.markdownlint-cli2.jsonc'],
917
965
  ];
918
- function dirDosModelos() {
919
- for (const c of [path.join(__dirname, '..', 'assets', 'docs'), path.join(__dirname, '..', '..', 'assets', 'docs')]) {
966
+ /**
967
+ * A pasta dos modelos do nucleo na lingua pedida. RM-061 (F3a, D3 e D6): o ingles mora em `assets/docs-en/`, com os
968
+ * mesmos nomes; o contrato que o `ork docs verificar` le (frontmatter, secoes, `Em uma frase`) e o mesmo nas duas.
969
+ */
970
+ function dirDosModelos(locale = locale_1.LOCALE_PADRAO) {
971
+ const pasta = locale === 'en' ? 'docs-en' : 'docs';
972
+ for (const c of [path.join(__dirname, '..', 'assets', pasta), path.join(__dirname, '..', '..', 'assets', pasta)]) {
920
973
  if (fs.existsSync(c))
921
974
  return c;
922
975
  }
923
- throw new Error('docs.modelos: assets/docs do nucleo nao encontrado');
976
+ throw new Error(`docs.modelos: assets/${pasta} do nucleo nao encontrado`);
924
977
  }
925
- /** Cria o que falta; nunca sobrescreve pagina existente. Devolve o que criou. */
926
- function iniciarDocs(raiz) {
927
- const origem = dirDosModelos();
978
+ /** Cria o que falta, no `locale` pedido (pt-BR por omissao); nunca sobrescreve pagina existente. Devolve o que criou. */
979
+ function iniciarDocs(raiz, locale = locale_1.LOCALE_PADRAO) {
980
+ const origem = dirDosModelos(locale);
928
981
  const criados = [];
929
982
  for (const [de, para] of ARQUIVOS_DO_PADRAO) {
930
983
  const destino = path.join(raiz, para);