@orkastery/cli 0.2.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/LICENSE +21 -0
- package/README.md +87 -0
- package/adapters/README.md +22 -0
- package/adapters/claude-code/.claude-plugin/marketplace.json +15 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +31 -0
- package/adapters/claude-code/README.md +102 -0
- package/adapters/claude-code/agents/ork-check.md +32 -0
- package/adapters/claude-code/agents/ork-go.md +32 -0
- package/adapters/claude-code/agents/ork-goal.md +33 -0
- package/adapters/claude-code/agents/ork-master.md +32 -0
- package/adapters/claude-code/agents/ork-plan.md +32 -0
- package/adapters/claude-code/agents/ork-ship.md +32 -0
- package/adapters/claude-code/commands/check.md +45 -0
- package/adapters/claude-code/commands/go.md +46 -0
- package/adapters/claude-code/commands/goal.md +54 -0
- package/adapters/claude-code/commands/master.md +42 -0
- package/adapters/claude-code/commands/ork.md +37 -0
- package/adapters/claude-code/commands/plan.md +47 -0
- package/adapters/claude-code/commands/ship.md +45 -0
- package/adapters/claude-code/hooks/hooks.json +17 -0
- package/adapters/claude-code/hooks/ork-guard.js +130 -0
- package/adapters/hermes/README.md +45 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +25 -0
- package/adapters/hermes/hermes.plugin.json +21 -0
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +116 -0
- package/adapters/openclaw/README.md +63 -0
- package/adapters/openclaw/bin/ork-abrir-thread.sh +24 -0
- package/adapters/openclaw/openclaw.plugin.json +138 -0
- package/dist/adapters/claude-bg.js +308 -0
- package/dist/auditoria.js +848 -0
- package/dist/auditrun.js +976 -0
- package/dist/board.js +314 -0
- package/dist/canarios.js +271 -0
- package/dist/catalogo.js +124 -0
- package/dist/ciclos.js +156 -0
- package/dist/claims.js +226 -0
- package/dist/divida.js +374 -0
- package/dist/doctor.js +246 -0
- package/dist/evalrunner.js +458 -0
- package/dist/fix.js +450 -0
- package/dist/gates.js +92 -0
- package/dist/handoff.js +521 -0
- package/dist/hosts.js +331 -0
- package/dist/index.js +1910 -0
- package/dist/init.js +231 -0
- package/dist/leases.js +508 -0
- package/dist/ledger.js +138 -0
- package/dist/manifest.js +255 -0
- package/dist/master.js +453 -0
- package/dist/memoria.js +773 -0
- package/dist/modos.js +220 -0
- package/dist/orkmind.js +487 -0
- package/dist/orquestracao.js +559 -0
- package/dist/phase.js +572 -0
- package/dist/policies.js +176 -0
- package/dist/prompts.js +406 -0
- package/dist/ratelimit.js +179 -0
- package/dist/recall.js +252 -0
- package/dist/retry.js +917 -0
- package/dist/sandbox.js +94 -0
- package/dist/sessoes.js +104 -0
- package/dist/ship.js +551 -0
- package/dist/slug.js +100 -0
- package/dist/superficie.js +1347 -0
- package/dist/thread.js +333 -0
- package/dist/tokens.js +228 -0
- package/dist/types.js +20 -0
- package/dist/util.js +156 -0
- package/dist/verify.js +262 -0
- package/dist/worktree.js +576 -0
- package/dist/yaml.js +112 -0
- package/eval/README.md +85 -0
- package/eval/casos/check-quality.json +103 -0
- package/eval/casos/code-reviewer.json +102 -0
- package/eval/casos/decision-triage.json +103 -0
- package/eval/casos/go-implementation.json +103 -0
- package/eval/casos/goal-definition.json +103 -0
- package/eval/casos/master-metrics.json +103 -0
- package/eval/casos/narrative-guardian.json +163 -0
- package/eval/casos/orkastery-bootstrap.json +102 -0
- package/eval/casos/plan-specification.json +102 -0
- package/eval/casos/roadmap-keeper.json +103 -0
- package/eval/casos/scope-check-capability-map.json +83 -0
- package/eval/casos/security-auditor.json +102 -0
- package/eval/casos/ship-release.json +122 -0
- package/eval/casos/test-engineer.json +122 -0
- package/eval/casos/thread-state.json +82 -0
- package/eval/casos/thread-tracing.json +83 -0
- package/eval/casos/web-performance-auditor.json +102 -0
- package/eval/fixtures/b0-slug-e-modos/caso.json +56 -0
- package/eval/fixtures/b2-master-log/caso.json +91 -0
- package/eval/fixtures/fx-concurrency/caso.json +15 -0
- package/eval/fixtures/fx-hallucination/caso.json +12 -0
- package/eval/fixtures/fx-happy/caso.json +16 -0
- package/eval/fixtures/fx-schema-drift/caso.json +15 -0
- package/eval/fixtures/fx-stale-base/caso.json +12 -0
- package/eval/fixtures/fx-wiki-destroy/caso.json +16 -0
- package/eval/fixtures/superficie-de-rede/README.md +22 -0
- package/eval/fixtures/superficie-de-rede/api-express.js +30 -0
- package/eval/fixtures/superficie-de-rede/api_fastapi.py +17 -0
- package/eval/fixtures/superficie-de-rede/api_gin.go +18 -0
- package/package.json +56 -0
- package/references/README.md +25 -0
- package/references/code-review-axes.md +82 -0
- package/references/definition-of-done.md +74 -0
- package/references/performance-checklist.md +53 -0
- package/references/security-checklist.md +101 -0
- package/references/testing-patterns.md +51 -0
- package/skills/README.md +43 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +89 -0
- package/skills/core/thread-state/SKILL.md +86 -0
- package/skills/governance/decision-triage/SKILL.md +65 -0
- package/skills/governance/narrative-guardian/SKILL.md +66 -0
- package/skills/governance/roadmap-keeper/SKILL.md +60 -0
- package/skills/governance/scope-check-capability-map/SKILL.md +61 -0
- package/skills/observability/thread-tracing/SKILL.md +63 -0
- package/skills/phases/check-quality/SKILL.md +87 -0
- package/skills/phases/go-implementation/SKILL.md +70 -0
- package/skills/phases/goal-definition/SKILL.md +69 -0
- package/skills/phases/master-metrics/SKILL.md +65 -0
- package/skills/phases/plan-specification/SKILL.md +67 -0
- package/skills/phases/ship-release/SKILL.md +68 -0
- package/skills/reviewers/code-reviewer/SKILL.md +61 -0
- package/skills/reviewers/security-auditor/SKILL.md +68 -0
- package/skills/reviewers/test-engineer/SKILL.md +66 -0
- package/skills/reviewers/web-performance-auditor/SKILL.md +63 -0
package/dist/ship.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `ork ship`: merge e push CONTROLADOS e VERIFICADOS (bloco B1, item 5 da paridade).
|
|
4
|
+
*
|
|
5
|
+
* A ordem dos passos e o contrato, e ela nao muda por modo de conducao:
|
|
6
|
+
*
|
|
7
|
+
* 1. autorizacao de push, pelo gate do modo (#Look/#Ork pausam; #Classic usa a
|
|
8
|
+
* autorizacao antecipada registrada no ledger; #Maestro/#Auto usam a propria #TAG);
|
|
9
|
+
* 2. policies do manifesto no gate `ship` (push_direto_na_base e provider);
|
|
10
|
+
* 3. verificacao independente: `ork verify` reexecuta claims e comandos no HEAD real;
|
|
11
|
+
* 4. lease `main-tree`, que serializa: so uma thread mergeia por vez;
|
|
12
|
+
* 5. `git merge --no-ff`, conferido por `git merge-base --is-ancestor`;
|
|
13
|
+
* 6. push, PROVADO por `git ls-remote` batendo com o sha local (nao pelo codigo de
|
|
14
|
+
* saida do push, e nunca por `gh api`, que nao e confiavel nesta maquina);
|
|
15
|
+
* 7. `ship_done` no ledger com os dois shas, quem autorizou e a evidencia.
|
|
16
|
+
*
|
|
17
|
+
* Qualquer passo que reprove grava `ship_blocked` com MOTIVO TIPADO e nao avanca.
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.autorizacaoDePush = autorizacaoDePush;
|
|
54
|
+
exports.arvoreComBranch = arvoreComBranch;
|
|
55
|
+
exports.prepararArvore = prepararArvore;
|
|
56
|
+
exports.shaDaRef = shaDaRef;
|
|
57
|
+
exports.shaNoRemoto = shaNoRemoto;
|
|
58
|
+
exports.remotoConfigurado = remotoConfigurado;
|
|
59
|
+
exports.ship = ship;
|
|
60
|
+
exports.textoDoShip = textoDoShip;
|
|
61
|
+
const fs = __importStar(require("node:fs"));
|
|
62
|
+
const path = __importStar(require("node:path"));
|
|
63
|
+
const gates_1 = require("./gates");
|
|
64
|
+
const leases_1 = require("./leases");
|
|
65
|
+
const ledger_1 = require("./ledger");
|
|
66
|
+
const manifest_1 = require("./manifest");
|
|
67
|
+
const modos_1 = require("./modos");
|
|
68
|
+
const policies_1 = require("./policies");
|
|
69
|
+
const thread_1 = require("./thread");
|
|
70
|
+
const util_1 = require("./util");
|
|
71
|
+
const verify_1 = require("./verify");
|
|
72
|
+
/** Modos em que a propria #TAG do pedido autoriza o push antecipadamente. */
|
|
73
|
+
const MODOS_QUE_AUTORIZAM_PUSH = ['maestro', 'auto'];
|
|
74
|
+
/**
|
|
75
|
+
* Quem autoriza o push desta thread.
|
|
76
|
+
*
|
|
77
|
+
* `#Look` e `#Ork` pausam no proprio SHIP e esperam o humano. `#Classic` pausa antes,
|
|
78
|
+
* no gate de evidencias, com autorizacao ANTECIPADA de push: aqui isso e conferido no
|
|
79
|
+
* ledger, nao presumido. `#Maestro` e `#Auto` seguem pela #TAG, que fica registrada.
|
|
80
|
+
*/
|
|
81
|
+
function autorizacaoDePush(raiz, thread, opcoes) {
|
|
82
|
+
const tag = (0, modos_1.tagDoModo)(thread.modo);
|
|
83
|
+
if (typeof opcoes.autorizarPush === 'string' && opcoes.autorizarPush.trim() !== '') {
|
|
84
|
+
return {
|
|
85
|
+
autorizado: true,
|
|
86
|
+
tipo: 'humano-no-cli',
|
|
87
|
+
por: opcoes.autorizarPush.trim(),
|
|
88
|
+
razao: `pausa de push do modo ${tag} autorizada na linha de comando`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (MODOS_QUE_AUTORIZAM_PUSH.includes(thread.modo)) {
|
|
92
|
+
return {
|
|
93
|
+
autorizado: true,
|
|
94
|
+
tipo: 'modo',
|
|
95
|
+
por: `#TAG ${tag} no pedido do builder`,
|
|
96
|
+
razao: `o modo ${tag} entrega dentro de um bloco sem pausa: a #TAG e a autorizacao previa`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (thread.modo === 'classic') {
|
|
100
|
+
const antecipada = (0, gates_1.aprovacoesHumanas)(raiz, thread.id).find((e) => /push|evidencia/i.test(String(e.sobre ?? '')));
|
|
101
|
+
if (antecipada) {
|
|
102
|
+
return {
|
|
103
|
+
autorizado: true,
|
|
104
|
+
tipo: 'humano-antecipado',
|
|
105
|
+
por: String(antecipada.autorizadoPor ?? 'humano'),
|
|
106
|
+
razao: `autorizacao antecipada de push registrada no ledger em ${antecipada.ts}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
autorizado: false,
|
|
112
|
+
tipo: 'pendente',
|
|
113
|
+
por: '',
|
|
114
|
+
razao: `o modo ${tag} pausa no push e ainda nao ha autorizacao humana registrada. ` +
|
|
115
|
+
`Autorize com: ork gate approve ${thread.id} push --por <quem> ` +
|
|
116
|
+
`ou rode: ork ship ${thread.id} --para <branch> --autorizar-push <quem>`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Diretorio temporario onde a arvore de merge e montada quando preciso. */
|
|
120
|
+
function dirTemporarioDeShip(raiz, para) {
|
|
121
|
+
const seguro = para.replace(/[^A-Za-z0-9._-]/g, '-');
|
|
122
|
+
return path.join((0, manifest_1.dirEstado)(raiz), 'tmp', `ship-${seguro}-${Date.now()}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Diretorio da worktree que tem a branch em check-out, se alguma tiver.
|
|
126
|
+
*
|
|
127
|
+
* O git recusa `worktree add` de uma branch ja em check-out em outro lugar, e com razao:
|
|
128
|
+
* duas arvores na mesma branch e receita de estado inconsistente. Entao o `ork` procura
|
|
129
|
+
* quem a tem antes de tentar montar arvore nova.
|
|
130
|
+
*/
|
|
131
|
+
function arvoreComBranch(raiz, branch) {
|
|
132
|
+
const r = (0, util_1.exec)('git', ['worktree', 'list', '--porcelain'], raiz);
|
|
133
|
+
if (!r.ok)
|
|
134
|
+
return null;
|
|
135
|
+
let atual = null;
|
|
136
|
+
for (const linha of r.stdout.split('\n')) {
|
|
137
|
+
if (linha.startsWith('worktree '))
|
|
138
|
+
atual = linha.slice('worktree '.length).trim();
|
|
139
|
+
else if (linha.trim() === `branch refs/heads/${branch}`)
|
|
140
|
+
return atual;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/** A arvore tem alteracao nao commitada em arquivo versionado? (arquivo novo nao conta) */
|
|
145
|
+
function temAlteracaoNaoCommitada(dir) {
|
|
146
|
+
return !(0, util_1.exec)('git', ['diff', '--quiet'], dir).ok || !(0, util_1.exec)('git', ['diff', '--cached', '--quiet'], dir).ok;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Prepara a arvore onde o merge acontece.
|
|
150
|
+
*
|
|
151
|
+
* Se a branch de destino ja esta em check-out em alguma arvore do repositorio (a raiz do
|
|
152
|
+
* projeto ou o check-out principal, quando o `ork` roda de dentro de uma worktree), o
|
|
153
|
+
* merge acontece LA, exigindo arvore limpa: e a unica forma de o resultado do merge nao
|
|
154
|
+
* deixar aquela arvore inconsistente com a propria branch.
|
|
155
|
+
*
|
|
156
|
+
* Se ninguem tem a branch, o `ork` monta uma worktree temporaria e a descarta ao fim:
|
|
157
|
+
* entregar nunca pode trocar a branch em que o builder esta trabalhando.
|
|
158
|
+
*/
|
|
159
|
+
function prepararArvore(raiz, para) {
|
|
160
|
+
const dona = arvoreComBranch(raiz, para);
|
|
161
|
+
if (dona) {
|
|
162
|
+
if (temAlteracaoNaoCommitada(dona)) {
|
|
163
|
+
throw new Error(`a branch de destino "${para}" esta em check-out em ${dona} com alteracoes nao commitadas; ` +
|
|
164
|
+
'commite ou reverta la antes do ship');
|
|
165
|
+
}
|
|
166
|
+
return { dir: dona, temporaria: false };
|
|
167
|
+
}
|
|
168
|
+
const dir = dirTemporarioDeShip(raiz, para);
|
|
169
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
170
|
+
const r = (0, util_1.exec)('git', ['worktree', 'add', dir, para], raiz);
|
|
171
|
+
if (!r.ok) {
|
|
172
|
+
throw new Error(`nao foi possivel montar a arvore de merge de "${para}": ${(r.stderr || r.stdout).trim()}`);
|
|
173
|
+
}
|
|
174
|
+
return { dir, temporaria: true };
|
|
175
|
+
}
|
|
176
|
+
/** Remove a worktree temporaria do merge (best effort: o lease ja foi liberado). */
|
|
177
|
+
function descartarArvore(raiz, arvore) {
|
|
178
|
+
if (!arvore.temporaria)
|
|
179
|
+
return;
|
|
180
|
+
(0, util_1.exec)('git', ['worktree', 'remove', '--force', arvore.dir], raiz);
|
|
181
|
+
(0, util_1.exec)('git', ['worktree', 'prune'], raiz);
|
|
182
|
+
}
|
|
183
|
+
/** sha de uma referencia local, ou null quando ela nao existe. */
|
|
184
|
+
function shaDaRef(dir, ref) {
|
|
185
|
+
const r = (0, util_1.exec)('git', ['rev-parse', '--verify', '--quiet', ref], dir);
|
|
186
|
+
const sha = r.stdout.trim();
|
|
187
|
+
return r.ok && /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Le no REMOTO o sha da branch, com `git ls-remote`.
|
|
191
|
+
*
|
|
192
|
+
* E o unico jeito aceito de provar push nesta maquina: `gh api` e imprevisivel aqui, e o
|
|
193
|
+
* codigo de saida do `git push` nao prova que o remoto ficou com o commit certo.
|
|
194
|
+
*/
|
|
195
|
+
function shaNoRemoto(dir, remoto, branch) {
|
|
196
|
+
const r = (0, util_1.exec)('git', ['ls-remote', remoto, `refs/heads/${branch}`], dir, 120000);
|
|
197
|
+
if (!r.ok)
|
|
198
|
+
return null;
|
|
199
|
+
const linha = r.stdout.split('\n').find((l) => l.trim() !== '');
|
|
200
|
+
if (!linha)
|
|
201
|
+
return null;
|
|
202
|
+
const sha = linha.split(/\s+/)[0];
|
|
203
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
204
|
+
}
|
|
205
|
+
/** O remoto esta configurado neste repositorio? */
|
|
206
|
+
function remotoConfigurado(dir, remoto) {
|
|
207
|
+
return (0, util_1.exec)('git', ['remote', 'get-url', remoto], dir).ok;
|
|
208
|
+
}
|
|
209
|
+
function baseDoResultado(thread, de, opcoes) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
bloqueado: false,
|
|
213
|
+
motivo: null,
|
|
214
|
+
detalhe: '',
|
|
215
|
+
correcao: '',
|
|
216
|
+
thread: thread.id,
|
|
217
|
+
de,
|
|
218
|
+
para: opcoes.para,
|
|
219
|
+
shaDe: '',
|
|
220
|
+
shaParaAntes: '',
|
|
221
|
+
mergeSha: null,
|
|
222
|
+
jaIncorporado: false,
|
|
223
|
+
remoto: opcoes.remoto ?? 'origin',
|
|
224
|
+
shaRemoto: null,
|
|
225
|
+
pushVerificado: false,
|
|
226
|
+
autorizacao: { autorizado: false, tipo: 'pendente', por: '', razao: '' },
|
|
227
|
+
verificacao: null,
|
|
228
|
+
violacoes: [],
|
|
229
|
+
lease: null,
|
|
230
|
+
leaseOcupadoPor: null,
|
|
231
|
+
passos: [],
|
|
232
|
+
dryRun: opcoes.dryRun === true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Executa o SHIP da thread. Nunca lanca por reprovacao: reprovacao volta tipada. */
|
|
236
|
+
function ship(carregado, threadId, opcoes) {
|
|
237
|
+
const { raiz, manifesto } = carregado;
|
|
238
|
+
const thread = (0, thread_1.lerThread)(raiz, threadId);
|
|
239
|
+
const dir = (0, thread_1.dirThread)(raiz, threadId);
|
|
240
|
+
const de = opcoes.de ?? thread.base.branch;
|
|
241
|
+
const para = opcoes.para;
|
|
242
|
+
const remoto = opcoes.remoto ?? 'origin';
|
|
243
|
+
const r = baseDoResultado(thread, de, opcoes);
|
|
244
|
+
const bloquear = (motivo, detalhe, correcao) => {
|
|
245
|
+
(0, gates_1.registrarGateBloqueado)(dir, threadId, {
|
|
246
|
+
gate: 'ship',
|
|
247
|
+
motivo,
|
|
248
|
+
modo: thread.modo,
|
|
249
|
+
detalhe,
|
|
250
|
+
correcao,
|
|
251
|
+
de,
|
|
252
|
+
para,
|
|
253
|
+
});
|
|
254
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipBloqueado, {
|
|
255
|
+
de,
|
|
256
|
+
para,
|
|
257
|
+
motivo,
|
|
258
|
+
detalhe,
|
|
259
|
+
correcao,
|
|
260
|
+
modo: thread.modo,
|
|
261
|
+
dryRun: r.dryRun,
|
|
262
|
+
});
|
|
263
|
+
return { ...r, bloqueado: true, ok: false, motivo, detalhe, correcao };
|
|
264
|
+
};
|
|
265
|
+
// 1. Autorizacao de push pelo gate do modo de conducao.
|
|
266
|
+
const autorizacao = autorizacaoDePush(raiz, thread, opcoes);
|
|
267
|
+
r.autorizacao = autorizacao;
|
|
268
|
+
if (!autorizacao.autorizado) {
|
|
269
|
+
return bloquear('human.pending', autorizacao.razao, `ork gate approve ${threadId} push --por <quem>`);
|
|
270
|
+
}
|
|
271
|
+
// 2. Policies do manifesto que valem no gate `ship`.
|
|
272
|
+
const violacoes = (0, policies_1.avaliarPolicies)(manifesto, {
|
|
273
|
+
gate: 'ship',
|
|
274
|
+
de,
|
|
275
|
+
para,
|
|
276
|
+
baseBranch: manifesto.worktree.base_branch,
|
|
277
|
+
});
|
|
278
|
+
r.violacoes = violacoes;
|
|
279
|
+
const bloqueiam = (0, policies_1.bloqueantes)(violacoes);
|
|
280
|
+
if (bloqueiam.length > 0) {
|
|
281
|
+
return bloquear('policy.violation', bloqueiam.map((v) => `${v.policy}: ${v.detalhe}`).join('; '), bloqueiam.map((v) => v.correcao).join('; '));
|
|
282
|
+
}
|
|
283
|
+
// 3. Verificacao independente: reexecuta claims e comandos no HEAD real da thread.
|
|
284
|
+
const verificacao = (0, verify_1.verificar)(carregado, threadId);
|
|
285
|
+
r.verificacao = verificacao;
|
|
286
|
+
if (!verificacao.ok) {
|
|
287
|
+
const motivo = verificacao.motivos.find((m) => m !== 'claims.unverifiable');
|
|
288
|
+
return bloquear(motivo, `verificacao reprovou no HEAD ${verificacao.commit}: ${verificacao.motivos.join(', ')}`, 'rode `ork verify ' + threadId + '` e corrija antes de entregar');
|
|
289
|
+
}
|
|
290
|
+
// Referencias locais, resolvidas antes de tomar o lease.
|
|
291
|
+
const shaDe = shaDaRef(raiz, `refs/heads/${de}`);
|
|
292
|
+
const shaPara = shaDaRef(raiz, `refs/heads/${para}`);
|
|
293
|
+
if (!shaDe) {
|
|
294
|
+
return bloquear('artifact.missing', `branch de origem "${de}" nao existe neste repositorio`, `use --de <branch> ou crie a branch da thread`);
|
|
295
|
+
}
|
|
296
|
+
if (!shaPara) {
|
|
297
|
+
return bloquear('artifact.missing', `branch de destino "${para}" nao existe neste repositorio`, `crie a branch de destino ou informe outra em --para`);
|
|
298
|
+
}
|
|
299
|
+
r.shaDe = shaDe;
|
|
300
|
+
r.shaParaAntes = shaPara;
|
|
301
|
+
// 4. Lease `main-tree`: so uma thread mergeia por vez.
|
|
302
|
+
if (r.dryRun) {
|
|
303
|
+
const ocupado = (0, leases_1.lerLease)(raiz, leases_1.LEASE_MAIN_TREE);
|
|
304
|
+
r.leaseOcupadoPor = ocupado;
|
|
305
|
+
// O ensaio tambem responde ONDE o merge aconteceria e se aquela arvore esta limpa:
|
|
306
|
+
// descobrir isso so na hora do merge real seria descobrir tarde demais.
|
|
307
|
+
const dona = arvoreComBranch(raiz, para);
|
|
308
|
+
const arvoreDoMerge = dona
|
|
309
|
+
? `${dona} (arvore que tem ${para} em check-out; ${temAlteracaoNaoCommitada(dona) ? 'SUJA, o ship reprovaria com tree.blocked' : 'limpa'})`
|
|
310
|
+
: `worktree temporaria em ${path.join((0, manifest_1.dirEstado)(raiz), 'tmp')} (ninguem tem ${para} em check-out)`;
|
|
311
|
+
r.detalhe = `arvore de merge: ${arvoreDoMerge}`;
|
|
312
|
+
r.passos = [
|
|
313
|
+
`arvore de merge: ${arvoreDoMerge}`,
|
|
314
|
+
`git merge --no-ff ${de} -m "<mensagem de ship>"`,
|
|
315
|
+
`git push ${remoto} ${para}`,
|
|
316
|
+
`git ls-remote ${remoto} refs/heads/${para} (prova do push)`,
|
|
317
|
+
];
|
|
318
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipIniciado, {
|
|
319
|
+
de,
|
|
320
|
+
para,
|
|
321
|
+
shaDe,
|
|
322
|
+
shaPara,
|
|
323
|
+
modo: thread.modo,
|
|
324
|
+
autorizadoPor: autorizacao.por,
|
|
325
|
+
dryRun: true,
|
|
326
|
+
leaseLivre: ocupado === null,
|
|
327
|
+
});
|
|
328
|
+
return { ...r, ok: true, bloqueado: false };
|
|
329
|
+
}
|
|
330
|
+
// A merge queue do B2: quem nao pega o lease entra na FILA, em vez de sumir. A fila
|
|
331
|
+
// fica visivel em `ork lease list` e em `ork board --all`, e e ela que serializa o
|
|
332
|
+
// merge de N threads paralelas sem ninguem perder a vez.
|
|
333
|
+
const aquisicao = (0, leases_1.adquirirRegiao)(raiz, leases_1.LEASE_MAIN_TREE, {
|
|
334
|
+
thread: threadId,
|
|
335
|
+
motivo: `ship de ${de} para ${para}`,
|
|
336
|
+
ttlMs: opcoes.ttlLeaseMs,
|
|
337
|
+
});
|
|
338
|
+
if (!aquisicao.ok) {
|
|
339
|
+
r.leaseOcupadoPor = aquisicao.ocupadoPor;
|
|
340
|
+
const dono = aquisicao.ocupadoPor;
|
|
341
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.leaseEnfileirado, {
|
|
342
|
+
lease: leases_1.LEASE_MAIN_TREE,
|
|
343
|
+
posicao: aquisicao.posicaoNaFila,
|
|
344
|
+
naFrente: (0, leases_1.esperandoPor)(raiz, leases_1.LEASE_MAIN_TREE).length,
|
|
345
|
+
bloqueadaPor: dono?.thread ?? '(desconhecida)',
|
|
346
|
+
motivo: 'lease.busy',
|
|
347
|
+
});
|
|
348
|
+
return bloquear('lease.busy', `lease ${leases_1.LEASE_MAIN_TREE} esta com a thread ${dono?.thread ?? '(desconhecida)'} ` +
|
|
349
|
+
`desde ${dono?.adquiridoEm ?? '?'} (${dono?.motivo ?? 'sem motivo'}); ` +
|
|
350
|
+
`esta thread entrou na merge queue na posicao ${aquisicao.posicaoNaFila}`, `espere a vez na fila (ork lease list), ou libere com: ork lease release ${leases_1.LEASE_MAIN_TREE} --forcar`);
|
|
351
|
+
}
|
|
352
|
+
r.lease = aquisicao.lease;
|
|
353
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.leaseAdquirido, {
|
|
354
|
+
lease: leases_1.LEASE_MAIN_TREE,
|
|
355
|
+
expiraEm: aquisicao.lease?.expiraEm,
|
|
356
|
+
tomadoDeVencido: aquisicao.tomadoDeVencido,
|
|
357
|
+
motivo: `ship de ${de} para ${para}`,
|
|
358
|
+
});
|
|
359
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipIniciado, {
|
|
360
|
+
de,
|
|
361
|
+
para,
|
|
362
|
+
shaDe,
|
|
363
|
+
shaPara,
|
|
364
|
+
modo: thread.modo,
|
|
365
|
+
autorizadoPor: autorizacao.por,
|
|
366
|
+
tipoDeAutorizacao: autorizacao.tipo,
|
|
367
|
+
verificadoNoCommit: verificacao.commit,
|
|
368
|
+
});
|
|
369
|
+
let arvore = null;
|
|
370
|
+
try {
|
|
371
|
+
try {
|
|
372
|
+
arvore = prepararArvore(raiz, para);
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
// Arvore de destino indisponivel e reprovacao tipada, nao excecao vazando pelo CLI.
|
|
376
|
+
return bloquear('tree.blocked', e.message, 'deixe a arvore da branch de destino limpa (ou libere a branch) e rode o ship de novo');
|
|
377
|
+
}
|
|
378
|
+
const passos = [];
|
|
379
|
+
// 5. Merge --no-ff, conferido no proprio git.
|
|
380
|
+
const jaIncorporado = (0, util_1.exec)('git', ['merge-base', '--is-ancestor', shaDe, shaPara], raiz).ok;
|
|
381
|
+
r.jaIncorporado = jaIncorporado;
|
|
382
|
+
let mergeSha = shaPara;
|
|
383
|
+
if (!jaIncorporado) {
|
|
384
|
+
const mensagem = opcoes.mensagem ??
|
|
385
|
+
`ship(${threadId}): merge de ${de} em ${para}\n\n` +
|
|
386
|
+
`thread: ${thread.id} (${thread.slug})\n` +
|
|
387
|
+
`modo: ${(0, modos_1.tagDoModo)(thread.modo)}\n` +
|
|
388
|
+
`autorizado por: ${autorizacao.por} (${autorizacao.tipo})\n` +
|
|
389
|
+
`verificado em: HEAD ${verificacao.commit}\n`;
|
|
390
|
+
const merge = (0, util_1.exec)('git', ['merge', '--no-ff', de, '-m', mensagem], arvore.dir, 300000);
|
|
391
|
+
passos.push(`git merge --no-ff ${de}`);
|
|
392
|
+
if (!merge.ok) {
|
|
393
|
+
(0, util_1.exec)('git', ['merge', '--abort'], arvore.dir);
|
|
394
|
+
r.passos = [...passos, 'git merge --abort'];
|
|
395
|
+
return bloquear('artifact.missing', `merge de ${de} em ${para} falhou: ${(merge.stderr || merge.stdout).trim().slice(0, 400)}`, 'resolva o conflito na branch da thread e rode o ship de novo');
|
|
396
|
+
}
|
|
397
|
+
const novo = shaDaRef(arvore.dir, 'HEAD');
|
|
398
|
+
if (!novo) {
|
|
399
|
+
return bloquear('artifact.missing', 'nao foi possivel ler o HEAD apos o merge', 'confira o repositorio');
|
|
400
|
+
}
|
|
401
|
+
mergeSha = novo;
|
|
402
|
+
// Verificacao independente do merge: a origem precisa ser ancestral do resultado.
|
|
403
|
+
const incorporou = (0, util_1.exec)('git', ['merge-base', '--is-ancestor', shaDe, mergeSha], arvore.dir).ok;
|
|
404
|
+
passos.push(`git merge-base --is-ancestor ${shaDe.slice(0, 8)} ${mergeSha.slice(0, 8)}`);
|
|
405
|
+
if (!incorporou) {
|
|
406
|
+
return bloquear('artifact.missing', `o commit de ${de} nao aparece como ancestral do merge ${mergeSha}`, 'nao empurre: investigue o estado do repositorio');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
passos.push(`(nada a mergear: ${de} ja e ancestral de ${para})`);
|
|
411
|
+
}
|
|
412
|
+
r.mergeSha = mergeSha;
|
|
413
|
+
// 6. Push PROVADO por ls-remote.
|
|
414
|
+
const temRemoto = remotoConfigurado(arvore.dir, remoto);
|
|
415
|
+
r.remoto = temRemoto ? remoto : null;
|
|
416
|
+
if (opcoes.semPush || !temRemoto) {
|
|
417
|
+
r.pushVerificado = false;
|
|
418
|
+
r.detalhe = opcoes.semPush
|
|
419
|
+
? 'push nao executado por --sem-push (merge local concluido)'
|
|
420
|
+
: `remoto "${remoto}" nao configurado: merge local concluido, sem push a provar`;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const existeNoRemoto = shaNoRemoto(arvore.dir, remoto, para) !== null;
|
|
424
|
+
const argsPush = existeNoRemoto
|
|
425
|
+
? ['push', remoto, `${para}:refs/heads/${para}`]
|
|
426
|
+
: ['push', '-u', remoto, `${para}:refs/heads/${para}`];
|
|
427
|
+
const push = (0, util_1.exec)('git', argsPush, arvore.dir, 300000);
|
|
428
|
+
passos.push(`git ${argsPush.join(' ')}`);
|
|
429
|
+
if (!push.ok) {
|
|
430
|
+
r.passos = passos;
|
|
431
|
+
return bloquear('runtime.unavailable', `push para ${remoto}/${para} falhou: ${(push.stderr || push.stdout).trim().slice(0, 400)}`, 'resolva o acesso ao remoto e rode o ship de novo (o merge local ja aconteceu)');
|
|
432
|
+
}
|
|
433
|
+
// A prova: o sha que o REMOTO devolve tem que ser o mesmo do destino local.
|
|
434
|
+
const shaRemoto = shaNoRemoto(arvore.dir, remoto, para);
|
|
435
|
+
passos.push(`git ls-remote ${remoto} refs/heads/${para}`);
|
|
436
|
+
r.shaRemoto = shaRemoto;
|
|
437
|
+
r.pushVerificado = shaRemoto !== null && shaRemoto === mergeSha;
|
|
438
|
+
if (!r.pushVerificado) {
|
|
439
|
+
r.passos = passos;
|
|
440
|
+
return bloquear('runtime.unavailable', `ls-remote devolveu ${shaRemoto ?? '(nada)'} e o destino local esta em ${mergeSha}: ` +
|
|
441
|
+
'o push nao ficou provado', 'rode o ship de novo e confira o remoto');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
r.passos = passos;
|
|
445
|
+
// 7. Evidencia no ledger e estado da thread.
|
|
446
|
+
(0, gates_1.registrarGateLiberado)(dir, threadId, {
|
|
447
|
+
gate: 'ship',
|
|
448
|
+
modo: thread.modo,
|
|
449
|
+
autorizadoPor: autorizacao.por,
|
|
450
|
+
evidencia: `merge ${mergeSha} verificado; ls-remote ${r.shaRemoto ?? '(sem remoto)'}`,
|
|
451
|
+
tipoDeAutorizacao: autorizacao.tipo,
|
|
452
|
+
});
|
|
453
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
|
|
454
|
+
de,
|
|
455
|
+
para,
|
|
456
|
+
shaDe,
|
|
457
|
+
shaParaAntes: shaPara,
|
|
458
|
+
mergeSha,
|
|
459
|
+
jaIncorporado,
|
|
460
|
+
remoto: r.remoto,
|
|
461
|
+
shaRemoto: r.shaRemoto,
|
|
462
|
+
pushVerificado: r.pushVerificado,
|
|
463
|
+
fonteDaProva: r.pushVerificado ? `git ls-remote ${remoto} refs/heads/${para}` : null,
|
|
464
|
+
autorizadoPor: autorizacao.por,
|
|
465
|
+
tipoDeAutorizacao: autorizacao.tipo,
|
|
466
|
+
modo: thread.modo,
|
|
467
|
+
tag: (0, modos_1.tagDoModo)(thread.modo),
|
|
468
|
+
evidencia: {
|
|
469
|
+
verificadoNoCommit: verificacao.commit,
|
|
470
|
+
claimsVerificadas: verificacao.claims.filter((c) => c.verificado).map((c) => c.claim.id),
|
|
471
|
+
comandos: verificacao.comandos.map((c) => ({ nome: c.nome, ok: c.ok })),
|
|
472
|
+
passos,
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
const atualizada = (0, thread_1.lerThread)(raiz, threadId);
|
|
476
|
+
atualizada.faseAtual = 'SHIP';
|
|
477
|
+
atualizada.leases = [
|
|
478
|
+
...(atualizada.leases ?? []),
|
|
479
|
+
{ nome: leases_1.LEASE_MAIN_TREE, adquiridoEm: aquisicao.lease?.adquiridoEm ?? (0, util_1.agora)(), liberadoEm: (0, util_1.agora)() },
|
|
480
|
+
];
|
|
481
|
+
(0, thread_1.gravarThread)(raiz, atualizada);
|
|
482
|
+
return { ...r, ok: true, bloqueado: false };
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
if (arvore)
|
|
486
|
+
descartarArvore(raiz, arvore);
|
|
487
|
+
const solto = (0, leases_1.liberar)(raiz, leases_1.LEASE_MAIN_TREE, threadId);
|
|
488
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.leaseLiberado, {
|
|
489
|
+
lease: leases_1.LEASE_MAIN_TREE,
|
|
490
|
+
ok: solto.ok,
|
|
491
|
+
detalhe: solto.detalhe,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/** Texto de `ork ship` para o CLI. */
|
|
496
|
+
function textoDoShip(r) {
|
|
497
|
+
const linhas = [];
|
|
498
|
+
const cabecalho = r.dryRun ? 'ork ship (--dry-run, nada foi executado)' : 'ork ship';
|
|
499
|
+
linhas.push(`${cabecalho}: thread ${r.thread}`);
|
|
500
|
+
linhas.push(` de ${r.de}${r.shaDe ? ` @ ${r.shaDe.slice(0, 8)}` : ''}`);
|
|
501
|
+
linhas.push(` para ${r.para}${r.shaParaAntes ? ` @ ${r.shaParaAntes.slice(0, 8)}` : ''}`);
|
|
502
|
+
linhas.push(` autorizacao ${r.autorizacao.autorizado ? `AUTORIZADO por ${r.autorizacao.por} (${r.autorizacao.tipo})` : 'AGUARDANDO AUTORIZACAO HUMANA'}`);
|
|
503
|
+
linhas.push(` ${r.autorizacao.razao}`);
|
|
504
|
+
if (r.verificacao) {
|
|
505
|
+
const v = r.verificacao;
|
|
506
|
+
linhas.push(` verificacao ${v.ok ? 'passou' : 'REPROVOU'} no HEAD ${v.commit.slice(0, 8)} ` +
|
|
507
|
+
`(${v.claims.filter((c) => c.verificado).length}/${v.claims.length} claims, ` +
|
|
508
|
+
`${v.comandos.filter((c) => c.ok).length}/${v.comandos.length} comandos)`);
|
|
509
|
+
}
|
|
510
|
+
if (r.leaseOcupadoPor) {
|
|
511
|
+
linhas.push(` lease ${leases_1.LEASE_MAIN_TREE} OCUPADO pela thread ${r.leaseOcupadoPor.thread} (desde ${r.leaseOcupadoPor.adquiridoEm})`);
|
|
512
|
+
}
|
|
513
|
+
else if (r.lease) {
|
|
514
|
+
linhas.push(` lease ${leases_1.LEASE_MAIN_TREE} tomado por ${r.thread} e liberado ao fim`);
|
|
515
|
+
}
|
|
516
|
+
else if (r.dryRun) {
|
|
517
|
+
linhas.push(` lease ${leases_1.LEASE_MAIN_TREE} livre`);
|
|
518
|
+
}
|
|
519
|
+
if (r.bloqueado) {
|
|
520
|
+
linhas.push('');
|
|
521
|
+
linhas.push(`BLOQUEADO. motivo tipado: ${r.motivo}`);
|
|
522
|
+
linhas.push(` detalhe ${r.detalhe}`);
|
|
523
|
+
linhas.push(` correcao ${r.correcao}`);
|
|
524
|
+
return linhas.join('\n');
|
|
525
|
+
}
|
|
526
|
+
if (r.dryRun) {
|
|
527
|
+
linhas.push('');
|
|
528
|
+
linhas.push('Passos que o ship real executaria, nesta ordem:');
|
|
529
|
+
for (const p of r.passos)
|
|
530
|
+
linhas.push(` ${p}`);
|
|
531
|
+
return linhas.join('\n');
|
|
532
|
+
}
|
|
533
|
+
linhas.push('');
|
|
534
|
+
linhas.push(r.jaIncorporado
|
|
535
|
+
? `Nada a mergear: ${r.de} ja estava em ${r.para}.`
|
|
536
|
+
: `Merge --no-ff concluido: ${r.mergeSha}`);
|
|
537
|
+
if (r.pushVerificado) {
|
|
538
|
+
linhas.push(`Push PROVADO: ls-remote de ${r.remoto}/${r.para} devolveu ${r.shaRemoto}`);
|
|
539
|
+
linhas.push(` e o destino local esta no mesmo sha: ${r.mergeSha}`);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
linhas.push(`Push nao provado: ${r.detalhe || 'sem remoto configurado'}`);
|
|
543
|
+
}
|
|
544
|
+
linhas.push('');
|
|
545
|
+
linhas.push('Evidencia (comandos executados):');
|
|
546
|
+
for (const p of r.passos)
|
|
547
|
+
linhas.push(` ${p}`);
|
|
548
|
+
linhas.push('');
|
|
549
|
+
linhas.push(`Ledger: ork phase list ${r.thread} (evento ship_done)`);
|
|
550
|
+
return linhas.join('\n');
|
|
551
|
+
}
|
package/dist/slug.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Slug de sessao em 3 partes (visao unificada, secao 3.4).
|
|
4
|
+
*
|
|
5
|
+
* <produto ate 3 letras>-<assunto ate 12 chars>-<fases>[-<rotacao>]
|
|
6
|
+
*
|
|
7
|
+
* A parte 1 vem de `project.abbrev` no manifesto. A parte 2 nao tem hifen interno,
|
|
8
|
+
* o que preserva o parse deterministico. A parte 3 diz que fases a sessao conduz.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.REGEX_SLUG = void 0;
|
|
12
|
+
exports.normalizarAbbrev = normalizarAbbrev;
|
|
13
|
+
exports.normalizarAssunto = normalizarAssunto;
|
|
14
|
+
exports.validarAbbrev = validarAbbrev;
|
|
15
|
+
exports.montarSlug = montarSlug;
|
|
16
|
+
exports.parseSlug = parseSlug;
|
|
17
|
+
exports.slugValido = slugValido;
|
|
18
|
+
exports.idDaThreadPeloSlug = idDaThreadPeloSlug;
|
|
19
|
+
exports.proximaRotacao = proximaRotacao;
|
|
20
|
+
/** Regex canonica do slug, aplicada em toda geracao e todo override `--slug`. */
|
|
21
|
+
exports.REGEX_SLUG = /^[a-z0-9]{1,3}-[a-z0-9]{1,12}-(goal|plan|go|check|ship|master|f[1-6]{2,6}|full)(-[0-9]+)?$/;
|
|
22
|
+
/** Remove acentos e reduz a `[a-z0-9]`, o alfabeto aceito nas partes 1 e 2. */
|
|
23
|
+
function apenasAlfanumerico(texto) {
|
|
24
|
+
return texto
|
|
25
|
+
.normalize('NFD')
|
|
26
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9]/g, '');
|
|
29
|
+
}
|
|
30
|
+
/** Normaliza a abreviacao do produto para a parte 1 (ate 3 caracteres). */
|
|
31
|
+
function normalizarAbbrev(bruto) {
|
|
32
|
+
return apenasAlfanumerico(bruto).slice(0, 3);
|
|
33
|
+
}
|
|
34
|
+
/** Normaliza o nome dado pelo builder para a parte 2 (ate 12 caracteres). */
|
|
35
|
+
function normalizarAssunto(bruto) {
|
|
36
|
+
return apenasAlfanumerico(bruto).slice(0, 12);
|
|
37
|
+
}
|
|
38
|
+
/** Valida a abreviacao do projeto, com mensagem de erro acionavel. */
|
|
39
|
+
function validarAbbrev(abbrev) {
|
|
40
|
+
if (!abbrev) {
|
|
41
|
+
return { ok: false, erro: 'project.abbrev ausente no manifesto (parte 1 do slug)' };
|
|
42
|
+
}
|
|
43
|
+
if (!/^[a-z0-9]{1,3}$/.test(abbrev)) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
erro: `project.abbrev invalido: "${abbrev}" (esperado ate 3 caracteres [a-z0-9])`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
51
|
+
/** Monta o slug de 3 partes. Lanca erro se o resultado nao casar com a regex canonica. */
|
|
52
|
+
function montarSlug(abbrev, assunto, fases, rotacao) {
|
|
53
|
+
const partes = [normalizarAbbrev(abbrev), normalizarAssunto(assunto), fases.toLowerCase()];
|
|
54
|
+
let slug = partes.join('-');
|
|
55
|
+
if (rotacao && rotacao > 1)
|
|
56
|
+
slug += `-${rotacao}`;
|
|
57
|
+
if (!exports.REGEX_SLUG.test(slug)) {
|
|
58
|
+
throw new Error(`slug gerado invalido: "${slug}" (regex canonica: ${exports.REGEX_SLUG.source})`);
|
|
59
|
+
}
|
|
60
|
+
return slug;
|
|
61
|
+
}
|
|
62
|
+
/** Faz o parse do slug nas 3 partes. Retorna null quando o slug e invalido. */
|
|
63
|
+
function parseSlug(slug) {
|
|
64
|
+
if (!exports.REGEX_SLUG.test(slug))
|
|
65
|
+
return null;
|
|
66
|
+
const partes = slug.split('-');
|
|
67
|
+
const rotacao = partes.length === 4 ? Number(partes[3]) : null;
|
|
68
|
+
return {
|
|
69
|
+
produto: partes[0],
|
|
70
|
+
assunto: partes[1],
|
|
71
|
+
fases: partes[2],
|
|
72
|
+
rotacao,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** O slug e valido pela regex canonica? */
|
|
76
|
+
function slugValido(slug) {
|
|
77
|
+
return exports.REGEX_SLUG.test(slug);
|
|
78
|
+
}
|
|
79
|
+
/** Id da thread a partir do slug: as duas primeiras partes (`<produto>-<assunto>`). */
|
|
80
|
+
function idDaThreadPeloSlug(slug) {
|
|
81
|
+
const p = parseSlug(slug);
|
|
82
|
+
return p ? `${p.produto}-${p.assunto}` : null;
|
|
83
|
+
}
|
|
84
|
+
/** Proximo slug de rotacao de sessao dentro do mesmo bloco (gate de tokens, B1). */
|
|
85
|
+
function proximaRotacao(slug, jaExistentes) {
|
|
86
|
+
const p = parseSlug(slug);
|
|
87
|
+
if (!p)
|
|
88
|
+
throw new Error(`slug invalido para rotacao: ${slug}`);
|
|
89
|
+
const base = `${p.produto}-${p.assunto}-${p.fases}`;
|
|
90
|
+
let n = 1;
|
|
91
|
+
for (const existente of jaExistentes) {
|
|
92
|
+
const q = parseSlug(existente);
|
|
93
|
+
if (!q)
|
|
94
|
+
continue;
|
|
95
|
+
if (`${q.produto}-${q.assunto}-${q.fases}` !== base)
|
|
96
|
+
continue;
|
|
97
|
+
n = Math.max(n, q.rotacao ?? 1);
|
|
98
|
+
}
|
|
99
|
+
return montarSlug(p.produto, p.assunto, p.fases, n + 1);
|
|
100
|
+
}
|