@semacode/cli 3.12.2 → 3.13.1

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.
@@ -201,7 +201,7 @@ Score composto, \`achados[]\` e \`decisaoAgente\` são sinais para guiar a próx
201
201
  - Consultas honestas: \`resumo\` e \`inspecionar\` usam \`--drift none\` por padrão; score, confiança, implementação, rotas e superfícies não observadas ficam nulos/não avaliados.
202
202
  - Se uma consulta executar drift explicitamente, \`payload.analiseDrift.sucesso\` expõe o resultado e uma falha solicitada retorna exit code diferente de zero.
203
203
  - Cache: \`sema drift\` usa \`fresh\` por padrão. \`--cache none\` ainda executa sem persistência; \`cache\` acelera extrações validadas, mas não é prova final.
204
- - Fechamento governado: se o \`payload\` de \`sema drift --cache fresh --json\` retornar \`sucesso:false\`, \`vinculos_quebrados\`, \`rotas_divergentes\` ou \`impls_quebrados\`, não diga que passou limpo. Corrija e rode drift de novo.
204
+ - Fechamento governado: se o \`payload\` de \`sema drift --cache fresh --json\` retornar \`sucesso:false\`, \`vinculos_quebrados\`, \`rotas_divergentes\` ou \`impls_quebrados\`, não diga que passou limpo. Corrija e rode drift de novo. \`vinculos_fora_do_escopo\` é informativo: o arquivo existe no workspace fora dos \`diretoriosCodigo\`; siga o \`diretorioSugerido\` do registro para incluir o diretório no \`sema.config.json\` — não bloqueia fechamento.
205
205
  - Experiência governada: se a tarefa cria ou altera site, sistema, app, UI, painel, jogo, CLI/TUI ou terminal, prove acabamento moderno, contextual e não genérico. Em UI estreita (ex. 390px), \`document.documentElement.scrollWidth <= document.documentElement.clientWidth\` precisa ser verdadeiro.
206
206
  - Caminho fora do workspace local aberto pelo usuário não substitui a pasta local.
207
207
 
@@ -265,7 +265,7 @@ Leia isto primeiro se você tem pouco contexto, pouca memória, pouco tool use o
265
265
  15. Payload inline acima de ${LIMITE_CARACTERES_PAYLOAD_INLINE} caracteres não é lentidão: divida por responsabilidade; não aumente timeout para tentar passar limite de transporte.
266
266
  16. Se aparecer caminho que não pertence ao workspace local aberto pelo usuário, pare e confirme a fonte antes de agir.
267
267
  17. Se Sema estourar por timeout local: aumente o timeout e tente de novo. Não trate isso como falha do Sema.
268
- 18. Para fechamento, rode \`sema drift --cache fresh --json\`; cache persistente só acelera navegação e não é prova final. Se o \`payload\` retornar \`sucesso:false\`, \`vinculos_quebrados\`, \`rotas_divergentes\` ou \`impls_quebrados\`, não conclua. Na 3.0.0, não procure campos de domínio no topo do envelope \`sema.cli.result/v1\` nem use \`ok\` como substituto do veredito do payload.
268
+ 18. Para fechamento, rode \`sema drift --cache fresh --json\`; cache persistente só acelera navegação e não é prova final. Se o \`payload\` retornar \`sucesso:false\`, \`vinculos_quebrados\`, \`rotas_divergentes\` ou \`impls_quebrados\`, não conclua. \`vinculos_fora_do_escopo\` é informativo com \`diretorioSugerido\` para o \`sema.config.json\`; não bloqueia fechamento. Na 3.0.0, não procure campos de domínio no topo do envelope \`sema.cli.result/v1\` nem use \`ok\` como substituto do veredito do payload.
269
269
  19. Se a tarefa tiver site, sistema, app, UI, painel, jogo, CLI/TUI ou terminal: aplique acabamento moderno, contextual, responsivo/ergonômico, com hierarquia clara, estados e evidência. Em UI, valide mobile/desktop e prove \`scrollWidth <= clientWidth\` em viewport estreito como 390px.
270
270
  20. Não substitua o contexto Sema por AGENTS.md, README.md, busca local, inferência por nome ou bom senso.
271
271
 
@@ -486,7 +486,7 @@ Sinal e evidência:
486
486
  - Score composto, \`achados[]\` e \`decisaoAgente\` orientam a ação; abaixo de 80 bloqueia, alvo evolui 0.5 ponto até 100, e nada substitui evidência concreta.
487
487
  - Palavra-chave ou regex passando não prova governança se contrato, código e comportamento não batem.
488
488
  - \`resumo\` e \`inspecionar\` usam \`--drift none\` por padrão; campos derivados nulos significam não avaliados, nunca zero.
489
- - \`sema drift --cache fresh --json\` com \`payload.sucesso:false\`, \`payload.vinculos_quebrados\`, \`payload.rotas_divergentes\` ou \`payload.impls_quebrados\` bloqueia fechamento. Cache persistente é aceleração, não prova final.
489
+ - \`sema drift --cache fresh --json\` com \`payload.sucesso:false\`, \`payload.vinculos_quebrados\`, \`payload.rotas_divergentes\` ou \`payload.impls_quebrados\` bloqueia fechamento. \`payload.vinculos_fora_do_escopo\` é informativo com \`diretorioSugerido\` para os \`diretoriosCodigo\`; não bloqueia. Cache persistente é aceleração, não prova final.
490
490
  - Caminho fora do workspace local aberto pelo usuário não substitui a pasta local.
491
491
 
492
492
  Acabamento visual e terminal:
@@ -685,9 +685,9 @@ This is the minimum workflow for Codex in a local workspace.
685
685
 
686
686
  Contract edit rule: \`.sema\` has its own size budget. Above ${LIMITE_AVISO_LINHAS_CONTRATO_SEMA} lines, plan a split by domain/capability; above ${LIMITE_BLOQUEIO_LINHAS_CONTRATO_SEMA}, do not create or edit before splitting. Do not use parte_1/parte_2 and do not force a 1:1 contract-to-file relationship; several contracts can govern the same file through \`vinculos\`.
687
687
 
688
- Closing rule: the \`payload\` of \`sema drift --cache fresh --json\` must return \`sucesso:true\`. If it reports \`sucesso:false\`, \`vinculos_quebrados\`, \`rotas_divergentes\`, or \`impls_quebrados\`, the task is still blocked. Passing unit tests or a cache hit do not replace fresh green drift. In 3.0.0, \`ok\` describes the CLI result path and never replaces a command-specific domain verdict.
688
+ Closing rule: the \`payload\` of \`sema drift --cache fresh --json\` must return \`sucesso:true\`. If it reports \`sucesso:false\`, \`vinculos_quebrados\`, \`rotas_divergentes\`, or \`impls_quebrados\`, the task is still blocked. Passing unit tests or a cache hit do not replace fresh green drift. \`vinculos_fora_do_escopo\` is informational: the file exists in the workspace outside the configured \`diretoriosCodigo\`, and each entry carries a \`diretorioSugerido\` to add to \`sema.config.json\`; it does not block closure. In 3.0.0, \`ok\` describes the CLI result path and never replaces a command-specific domain verdict.
689
689
 
690
- Focused drift rule: file and module scopes plan their physical file set before cataloging. They never fall back to a whole-project walk when no safe anchor exists. All indexers and semantic-budget checks reuse the same in-memory source read; a global walk is reserved for explicit \`--escopo projeto\`. Code discovery is deferred until that plan exists, homonymous candidates fail as ambiguous, and configured contract origins or code roots resolving outside the workspace are rejected before enumeration. \`inspecionar\`, \`impacto\`, and \`renomear-semantico\` preserve the same directed boundary. Missing unambiguous local dependencies make coverage partial and block success. In Python, an unresolved \`name\` from \`from module import name\` is not itself a missing file after \`module\` resolves: it may be an exported symbol, while a real child module is still included whenever its file exists.
690
+ Focused drift rule: file and module scopes plan their physical file set before cataloging. They never fall back to a whole-project walk when no safe anchor exists. All indexers and semantic-budget checks reuse the same in-memory source read; a global walk is reserved for explicit \`--escopo projeto\`. Code discovery is deferred until that plan exists, homonymous candidates fail as ambiguous, and configured contract origins or code roots resolving outside the workspace are rejected before enumeration. \`inspecionar\`, \`impacto\`, and \`renomear-semantico\` preserve the same directed boundary. Missing unambiguous local dependencies make coverage partial and block success. In Python, an unresolved \`name\` from \`from module import name\` is not itself a missing file after \`module\` resolves: it may be an exported symbol, while a real child module is still included whenever its file exists.
691
691
 
692
692
  Cache rule: a closure gate should use \`sema drift <contrato> --escopo modulo --cache fresh --json\`. \`--cache none\` executes the same analysis without persistent-cache I/O, while \`--cache cache\` may reuse only extraction data whose schema, workspace identity, Git HEAD, plan, configuration, contracts, member paths, and strong content digests all match. Cache objects are stored in the operating system's user-cache directory, never below the workspace. A hit is acceleration, not evidence: final links, diagnostics, score, and success are always recalculated. Corruption or an unavailable cache degrades to an in-memory calculation.
693
693
 
@@ -752,7 +752,7 @@ Then read every required doc returned by \`docs-impacto\`.
752
752
 
753
753
  ## Contract and Discovery
754
754
 
755
- - \`sema iniciar --template <base|nestjs|fastapi|nextjs-api|nextjs-consumer|react-vite-consumer|angular-consumer|flutter-consumer|node-firebase-worker|aspnet-api|springboot-api|go-http-api|rust-axum-api|cpp-service-bridge> [--force]\`: creates a new Sema project and preserves existing files by default; \`--force\` is the only explicit overwrite path.
755
+ - \`sema iniciar --template <base|nestjs|fastapi|nextjs-api|nextjs-consumer|react-vite-consumer|angular-consumer|flutter-consumer|node-firebase-worker|aspnet-api|springboot-api|go-http-api|rust-axum-api|cpp-service-bridge> [--force]\`: creates a new Sema project and preserves existing files by default; \`--force\` is the only explicit overwrite path. Inside a subfolder of an already governed workspace, the subfolder is registered under \`diretoriosCodigo\` of the ancestral \`sema.config.json\` (safe write, deduplicated) instead of creating a local config; if the parent config cannot be written, the scaffold proceeds and prints the exact line to add.
756
756
  - \`sema validar <arquivo-ou-pasta> --json\`: validates \`.sema\` contracts.
757
757
  - \`sema diagnosticos <arquivo.sema> --json\`: details errors and warnings.
758
758
  - \`sema formatar <arquivo-ou-pasta>\`: formats contracts.
@@ -772,9 +772,9 @@ Then read every required doc returned by \`docs-impacto\`.
772
772
  - \`sema verificar <arquivo-ou-pasta> --json\`: runs aggregated final verification.
773
773
  - \`sema finalizar-mudanca --intencao "<acao>" --doc-lida <arquivo> --json\`: proves documentation reading before closure.
774
774
 
775
- Honest closure: unwrap drift JSON and treat its command payload as the source of truth. \`payload.sucesso:false\`, non-empty \`payload.vinculos_quebrados\`, non-empty \`payload.rotas_divergentes\`, or non-empty \`payload.impls_quebrados\` mean the change is not complete yet. Do not report "clean drift" without a green payload.
775
+ Honest closure: unwrap drift JSON and treat its command payload as the source of truth. \`payload.sucesso:false\`, non-empty \`payload.vinculos_quebrados\`, non-empty \`payload.rotas_divergentes\`, or non-empty \`payload.impls_quebrados\` mean the change is not complete yet. Do not report "clean drift" without a green payload. \`payload.vinculos_fora_do_escopo\` is informational: the file exists in the workspace outside \`diretoriosCodigo\`, and each entry carries a \`diretorioSugerido\` for \`sema.config.json\`.
776
776
 
777
- Focused drift exposes its planned, declared, inferred, and missing files plus catalog visit/read metrics in \`escopo_aplicado\`. A missing declared file or a missing unambiguous code-referenced dependency is a blocker reported as \`vinculo_quebrado\`; a missing inferred candidate derived from an \`impl\` symbol is informational under \`arquivosAusentesInferidos\` and never blocks an implementation that resolved. For Python \`from module import name\`, Sema still includes \`name\` when it resolves as a real submodule, but never reports the absent submodule candidate as a broken link after the parent module resolved because \`name\` may be a function, class, or variable. File and module scopes fail closed without a safe anchor, with homonymous implementation candidates, or with missing local dependencies; only \`--escopo projeto\` may walk every configured code root. Logical roots such as \`src\` are probed deterministically without a discovery walk. Configured contract origins and code roots are confined before enumeration, and \`inspecionar\`, \`impacto\`, and \`renomear-semantico\` reuse the same directed boundary without reopening arbitrary external paths.
777
+ Focused drift exposes its planned, declared, inferred, and missing files plus catalog visit/read metrics in \`escopo_aplicado\`. A missing declared file or a missing unambiguous code-referenced dependency is a blocker reported as \`vinculo_quebrado\`; a missing inferred candidate derived from an \`impl\` symbol is informational under \`arquivosAusentesInferidos\` and never blocks an implementation that resolved. For Python \`from module import name\`, Sema still includes \`name\` when it resolves as a real submodule, but never reports the absent submodule candidate as a broken link after the parent module resolved because \`name\` may be a function, class, or variable. File and module scopes fail closed without a safe anchor, with homonymous implementation candidates, or with missing local dependencies; only \`--escopo projeto\` may walk every configured code root. Logical roots such as \`src\` are probed deterministically without a discovery walk. Configured contract origins and code roots are confined before enumeration, and \`inspecionar\`, \`impacto\`, and \`renomear-semantico\` reuse the same directed boundary without reopening arbitrary external paths.
778
778
 
779
779
  Drift analysis and cache modes are explicit:
780
780
 
@@ -877,7 +877,7 @@ Spatial model and render mode are orthogonal: \`THREE_D + HEADLESS\` is valid, w
877
877
  - Do not stop after \`sema compilar\` if the contract target files still do not exist.
878
878
  - Do not replace \`sema compilar\` with \`sema testar\` when the contract requires generated code.
879
879
  - Do not create a Markdown report to pretend a gate ran.
880
- - Do not say drift passed when the unwrapped \`sema drift --cache fresh --json\` payload returned \`sucesso:false\`, a non-empty \`vinculos_quebrados\`, \`rotas_divergentes\`, or \`impls_quebrados\` list.
880
+ - Do not say drift passed when the unwrapped \`sema drift --cache fresh --json\` payload returned \`sucesso:false\`, a non-empty \`vinculos_quebrados\`, \`rotas_divergentes\`, or \`impls_quebrados\` list. \`vinculos_fora_do_escopo\` entries are informational scope hints, not broken links.
881
881
  - Do not declare a UI responsive without mobile/desktop proof; horizontal scroll at 390px blocks closure.
882
882
 
883
883
  Governed code policy: keep the \`SEMA-GOVERNED\` marker, split large code by real responsibility, preserve contract links, and never treat a generated output directory as the final delivery.
@@ -26,7 +26,7 @@ export function criarAjudaRaiz(versao) {
26
26
  renderizarSecaoAscii("Fluxos rápidos", [
27
27
  "[1] Projeto novo / produção inicial",
28
28
  "sema iniciar --template <base|nestjs|fastapi|nextjs-api|nextjs-consumer|react-vite-consumer|angular-consumer|flutter-consumer|node-firebase-worker|aspnet-api|springboot-api|go-http-api|rust-axum-api|cpp-service-bridge> [--force] [--com-exemplos]",
29
- "Arquivos existentes são preservados; use --force somente para sobrescrita explícita. O starter de exemplos vem por padrão; --com-exemplos instala o pacote completo.",
29
+ "Arquivos existentes são preservados; use --force somente para sobrescrita explícita. O starter de exemplos vem por padrão; --com-exemplos instala o pacote completo. Em subpasta de workspace já governado, a subpasta é registrada nos diretoriosCodigo do sema.config.json ancestral em vez de criar config local.",
30
30
  "sema validar contratos/<modulo>.sema --json",
31
31
  "sema compilar <arquivo-ou-pasta> --alvo <typescript|python|php|dart|lua|javascript|html|css|dotnet|cpp> --saida <diretorio>",
32
32
  "sema verificar <arquivo-ou-pasta> [--saida <dir>] [--alvo <alvo>] [--sem-cache] --json",
@@ -19,6 +19,7 @@ export declare function executarDriftCheck(contratos: string[], codigoVivo: stri
19
19
  impls_quebrados: number;
20
20
  vinculos_validos: number;
21
21
  vinculos_quebrados: number;
22
+ vinculos_fora_do_escopo: number;
22
23
  score: number;
23
24
  piso_operacional: number;
24
25
  alvo_atual: number;
@@ -30,6 +30,7 @@ export function executarDriftCheck(contratos, codigoVivo) {
30
30
  impls_quebrados: implsQuebrados,
31
31
  vinculos_validos: vinculosValidos,
32
32
  vinculos_quebrados: vinculosQuebrados,
33
+ vinculos_fora_do_escopo: 0,
33
34
  score,
34
35
  piso_operacional: avaliacao.pontuacaoMinimaOperacional,
35
36
  alvo_atual: avaliacao.pontuacaoAlvoAtual,
@@ -39,7 +39,7 @@ export interface OpcoesDriftLegado {
39
39
  observador?: ObservadorOperacaoDrift | undefined;
40
40
  }
41
41
  export interface DiagnosticoDrift {
42
- tipo: "impl_quebrado" | "task_sem_impl" | "rota_divergente" | "recurso_divergente" | "vinculo_quebrado" | "seguranca_frouxa" | "pontuacao_semantica_insuficiente" | "contrato_monolitico" | "codigo_monolitico" | "codigo_governado_sem_cabecalho" | "escopo_estreito_sem_vinculos" | "escopo_estreito_ambiguo" | "design_nao_declarado";
42
+ tipo: "impl_quebrado" | "task_sem_impl" | "rota_divergente" | "recurso_divergente" | "vinculo_quebrado" | "vinculo_fora_do_escopo" | "seguranca_frouxa" | "pontuacao_semantica_insuficiente" | "contrato_monolitico" | "codigo_monolitico" | "codigo_governado_sem_cabecalho" | "escopo_estreito_sem_vinculos" | "escopo_estreito_ambiguo" | "design_nao_declarado";
43
43
  modulo: string;
44
44
  task?: string;
45
45
  route?: string;
@@ -149,8 +149,9 @@ export interface RegistroVinculoDrift {
149
149
  valor: string;
150
150
  arquivo?: string;
151
151
  simbolo?: string;
152
- status: "resolvido" | "parcial" | "nao_encontrado";
152
+ status: "resolvido" | "parcial" | "nao_encontrado" | "fora_do_escopo";
153
153
  confianca: NivelConfiancaSemantica;
154
+ diretorioSugerido?: string;
154
155
  }
155
156
  export interface RegistroPersistenciaRealDrift {
156
157
  modulo: string;
@@ -256,6 +257,7 @@ export interface ResultadoDrift {
256
257
  impls_quebrados: RegistroImplDrift[];
257
258
  vinculos_validos: RegistroVinculoDrift[];
258
259
  vinculos_quebrados: RegistroVinculoDrift[];
260
+ vinculos_fora_do_escopo: RegistroVinculoDrift[];
259
261
  rotas_divergentes: RegistroRotaDivergente[];
260
262
  recursos_validos: RegistroRecursoDrift[];
261
263
  recursos_divergentes: RegistroRecursoDrift[];
@@ -10,7 +10,8 @@ import { calcularConfiancaTask, calcularRiscoOperacional, calcularScoreTask, car
10
10
  import { analisarPersistenciaReal, ordenarCandidatos } from "./drift.part09.js";
11
11
  import { simboloEhBridgeConsumer } from "./drift.part05.js";
12
12
  import { prepararIndicesDrift } from "./drift.part13.js";
13
- import { analisarModulosSelecionadosDrift } from "./drift.part14.js";
13
+ import { sondarCaminhosDeclaradosExistentes } from "./driftEscopo.js";
14
+ import { analisarModulosSelecionadosDrift, coletarCaminhosDeclaradosVinculosDrift } from "./drift.part14.js";
14
15
  import { resolverPoliticaPontuacaoSemantica } from "./driftScore.js";
15
16
  function arquivosVinculadosFisicamenteDrift(contexto, modulo, task) {
16
17
  const arquivos = new Set();
@@ -158,6 +159,7 @@ export async function analisarDriftLegado(contexto, opcoes) {
158
159
  const implsQuebrados = [];
159
160
  const vinculosValidos = [];
160
161
  const vinculosQuebrados = [];
162
+ const vinculosForaDoEscopo = [];
161
163
  const rotasDivergentes = [];
162
164
  const recursosValidos = [];
163
165
  const recursosDivergentes = [];
@@ -209,6 +211,12 @@ export async function analisarDriftLegado(contexto, opcoes) {
209
211
  for (const substituicao of substituicoesCaminho) {
210
212
  substituicao.implementacao.caminho = substituicao.temporario;
211
213
  }
214
+ // Sonda previa: distingue vinculo apontando para arquivo inexistente de
215
+ // arquivo existente fora dos diretoriosCodigo (informativo, nao bloqueante).
216
+ const caminhosDeclaradosVinculos = coletarCaminhosDeclaradosVinculosDrift(contexto);
217
+ const arquivosDeclaradosExistentes = caminhosDeclaradosVinculos.length > 0
218
+ ? await sondarCaminhosDeclaradosExistentes(caminhosDeclaradosVinculos, contexto.baseProjeto)
219
+ : new Map();
212
220
  try {
213
221
  analisarModulosSelecionadosDrift({
214
222
  contexto,
@@ -222,6 +230,8 @@ export async function analisarDriftLegado(contexto, opcoes) {
222
230
  implsQuebrados,
223
231
  vinculosValidos,
224
232
  vinculosQuebrados,
233
+ vinculosForaDoEscopo,
234
+ arquivosDeclaradosExistentes,
225
235
  rotasDivergentes,
226
236
  recursosValidos,
227
237
  recursosDivergentes,
@@ -563,6 +573,7 @@ export async function analisarDriftLegado(contexto, opcoes) {
563
573
  impls_quebrados: implsQuebrados,
564
574
  vinculos_validos: vinculosValidos,
565
575
  vinculos_quebrados: vinculosQuebrados,
576
+ vinculos_fora_do_escopo: vinculosForaDoEscopo,
566
577
  rotas_divergentes: rotasDivergentes,
567
578
  recursos_validos: recursosValidos,
568
579
  recursos_divergentes: recursosDivergentes,
@@ -14,6 +14,7 @@ export interface GuardrailsTaskDrift {
14
14
  efeitoPrivilegiado: boolean;
15
15
  exigeSegredos: boolean;
16
16
  }
17
+ export declare function coletarCaminhosDeclaradosVinculosDrift(contexto: ContextoProjetoCarregado): string[];
17
18
  export interface ResumoVinculosTaskDrift {
18
19
  validos: number;
19
20
  quebrados: number;
@@ -31,6 +32,8 @@ export interface EstadoAnaliseModulosDrift {
31
32
  implsQuebrados: RegistroImplDrift[];
32
33
  vinculosValidos: RegistroVinculoDrift[];
33
34
  vinculosQuebrados: RegistroVinculoDrift[];
35
+ vinculosForaDoEscopo: RegistroVinculoDrift[];
36
+ arquivosDeclaradosExistentes: ReadonlyMap<string, boolean>;
34
37
  rotasDivergentes: RegistroRotaDivergente[];
35
38
  recursosValidos: RegistroRecursoDrift[];
36
39
  recursosDivergentes: RegistroRecursoDrift[];
@@ -2,7 +2,7 @@
2
2
  // Descricao: analisa modulos selecionados no drift; consulte contratos/sema/governanca_ia_contexto.sema antes de editar.
3
3
  import path from "node:path";
4
4
  import { escolherArquivoPorVinculo, escolherSimboloPorVinculo } from "./drift.part03.js";
5
- import { calcularRiscoOperacional, encontrarAncoraSuperficie } from "./drift.part04.js";
5
+ import { calcularRiscoOperacional, chaveCaminhoCanonicoDrift, encontrarAncoraSuperficie } from "./drift.part04.js";
6
6
  import { resolverPersistenciaLocalPorTask } from "./drift.part08.js";
7
7
  import { escolherRotasEsperadas, normalizarCaminhoRota, ordenarCandidatos, sugerirCandidatosParaImpl, sugerirCandidatosParaTaskSemImpl } from "./drift.part09.js";
8
8
  import { coletarVinculosIr, extrairRecursosEsperados, resolverRecursoEsperado } from "./drift.part10.js";
@@ -30,8 +30,61 @@ function redigirCaminhoDeclaradoExterno(baseProjeto, valor) {
30
30
  const nome = path.basename(path.normalize(valor)) || "arquivo";
31
31
  return `[fora_do_workspace]/${nome}`;
32
32
  }
33
+ function caminhoDeclaradoTentadoVinculo(vinculo) {
34
+ const arquivoDeclarado = vinculo.arquivo ?? (vinculo.tipo === "arquivo" ? vinculo.valor : undefined);
35
+ if (arquivoDeclarado) {
36
+ return arquivoDeclarado;
37
+ }
38
+ const recursoDeclarado = vinculo.recurso ?? (["recurso", "tabela", "fila", "cache", "storage"].includes(vinculo.tipo) ? vinculo.valor : undefined);
39
+ if (recursoDeclarado) {
40
+ return undefined;
41
+ }
42
+ const superficieDeclarada = vinculo.superficie ?? (["superficie", "rota", "worker", "cron", "webhook", "evento", "policy", "fila", "cache", "storage"].includes(vinculo.tipo) ? vinculo.valor : undefined);
43
+ return superficieDeclarada ?? vinculo.valor;
44
+ }
45
+ function sugerirDiretorioCodigoDrift(baseProjeto, caminhoAbsoluto) {
46
+ const diretorioRelativo = path.relative(path.resolve(baseProjeto), path.dirname(path.resolve(caminhoAbsoluto))).replace(/\\/g, "/");
47
+ return `./${diretorioRelativo || "."}`;
48
+ }
49
+ function arquivoDentroDoEscopoCodigo(contexto, caminhoAbsoluto) {
50
+ return contexto.diretoriosCodigo.some((diretorio) => {
51
+ const relativo = path.relative(path.resolve(diretorio), path.resolve(caminhoAbsoluto));
52
+ return relativo === "" || (!relativo.startsWith("..") && !path.isAbsolute(relativo));
53
+ });
54
+ }
55
+ // Simbolo declarado cujo dono ancora codigo em arquivo existente fora dos
56
+ // diretoriosCodigo: casa o caminho do simbolo com o diretorio do arquivo.
57
+ function escolherAncoraForaDoEscopoPorSimbolo(caminhos, baseProjeto, simbolo) {
58
+ const simboloComoCaminho = simbolo.toLowerCase().replaceAll(".", "/");
59
+ const porPrefixo = caminhos.find((caminho) => {
60
+ const diretorioRelativo = path.relative(path.resolve(baseProjeto), path.dirname(path.resolve(caminho))).replace(/\\/g, "/").toLowerCase();
61
+ return diretorioRelativo !== "" && simboloComoCaminho.startsWith(`${diretorioRelativo}/`);
62
+ });
63
+ if (porPrefixo) {
64
+ return porPrefixo;
65
+ }
66
+ return caminhos.length === 1 ? caminhos[0] : undefined;
67
+ }
68
+ // O analisador de modulos e sincrono: a existencia em disco dos caminhos
69
+ // declarados em vinculos e sondada antes, pelo chamador, e injetada no estado.
70
+ export function coletarCaminhosDeclaradosVinculosDrift(contexto) {
71
+ const caminhos = [];
72
+ for (const item of contexto.modulosSelecionados) {
73
+ const ir = item.resultado.ir;
74
+ if (!ir) {
75
+ continue;
76
+ }
77
+ for (const itemVinculo of coletarVinculosIr(ir)) {
78
+ const declarado = caminhoDeclaradoTentadoVinculo(itemVinculo.vinculo);
79
+ if (declarado) {
80
+ caminhos.push(path.resolve(contexto.baseProjeto, declarado));
81
+ }
82
+ }
83
+ }
84
+ return caminhos;
85
+ }
33
86
  export function analisarModulosSelecionadosDrift(estado) {
34
- const { contexto, mapaImpl, todosSimbolos, mapaRecursos, todosRecursos, todasRotasIndexadas, todosArquivosConhecidos, implsValidos, implsQuebrados, vinculosValidos, vinculosQuebrados, rotasDivergentes, recursosValidos, recursosDivergentes, diagnosticos, tasksResumo, taskPorChave, guardrailsPorTask, resumoVinculosPorTask, arquivosAncoraHerdadosPorTask, } = estado;
87
+ const { contexto, mapaImpl, todosSimbolos, mapaRecursos, todosRecursos, todasRotasIndexadas, todosArquivosConhecidos, implsValidos, implsQuebrados, vinculosValidos, vinculosQuebrados, vinculosForaDoEscopo, arquivosDeclaradosExistentes, rotasDivergentes, recursosValidos, recursosDivergentes, diagnosticos, tasksResumo, taskPorChave, guardrailsPorTask, resumoVinculosPorTask, arquivosAncoraHerdadosPorTask, } = estado;
35
88
  for (const item of contexto.modulosSelecionados) {
36
89
  const ir = item.resultado.ir;
37
90
  if (!ir) {
@@ -309,6 +362,23 @@ export function analisarModulosSelecionadosDrift(estado) {
309
362
  });
310
363
  }
311
364
  }
365
+ const arquivosForaDoEscopoPorDono = new Map();
366
+ for (const itemVinculoDono of coletarVinculosIr(ir)) {
367
+ const declarado = itemVinculoDono.vinculo.arquivo
368
+ ?? (itemVinculoDono.vinculo.tipo === "arquivo" ? itemVinculoDono.vinculo.valor : undefined);
369
+ if (!declarado) {
370
+ continue;
371
+ }
372
+ const absoluto = path.resolve(contexto.baseProjeto, declarado);
373
+ if (arquivosDeclaradosExistentes.get(chaveCaminhoCanonicoDrift(absoluto)) !== true
374
+ || arquivoDentroDoEscopoCodigo(contexto, absoluto)) {
375
+ continue;
376
+ }
377
+ const chaveDono = `${itemVinculoDono.donoTipo}:${itemVinculoDono.dono}`;
378
+ const lista = arquivosForaDoEscopoPorDono.get(chaveDono) ?? [];
379
+ lista.push(absoluto);
380
+ arquivosForaDoEscopoPorDono.set(chaveDono, lista);
381
+ }
312
382
  for (const itemVinculo of coletarVinculosIr(ir)) {
313
383
  const registro = {
314
384
  modulo: ir.nome,
@@ -385,13 +455,41 @@ export function analisarModulosSelecionadosDrift(estado) {
385
455
  }
386
456
  }
387
457
  if (registro.status === "nao_encontrado") {
388
- vinculosQuebrados.push(registro);
389
- diagnosticos.push({
390
- tipo: "vinculo_quebrado",
391
- modulo: ir.nome,
392
- mensagem: `Vinculo ${registro.tipo}="${registro.valor}" de ${registro.donoTipo} "${registro.dono}" nao foi resolvido no codigo vivo.`,
393
- ...(itemVinculo.donoTipo === "task" ? { task: itemVinculo.dono } : itemVinculo.donoTipo === "route" ? { route: itemVinculo.dono } : {}),
394
- });
458
+ const caminhoTentado = caminhoDeclaradoTentadoVinculo(itemVinculo.vinculo);
459
+ const caminhoAbsoluto = caminhoTentado ? path.resolve(contexto.baseProjeto, caminhoTentado) : undefined;
460
+ const existeNoDisco = caminhoAbsoluto !== undefined
461
+ && arquivosDeclaradosExistentes.get(chaveCaminhoCanonicoDrift(caminhoAbsoluto)) === true;
462
+ // Arquivo proprio declarado fora do escopo, ou simbolo que herda a ancora
463
+ // de arquivo declarado do mesmo dono existente fora dos diretoriosCodigo.
464
+ const alvoForaDoEscopo = existeNoDisco && caminhoAbsoluto !== undefined && !arquivoDentroDoEscopoCodigo(contexto, caminhoAbsoluto)
465
+ ? caminhoAbsoluto
466
+ : simboloDeclarado
467
+ ? escolherAncoraForaDoEscopoPorSimbolo(arquivosForaDoEscopoPorDono.get(`${itemVinculo.donoTipo}:${itemVinculo.dono}`) ?? [], contexto.baseProjeto, simboloDeclarado)
468
+ : undefined;
469
+ if (alvoForaDoEscopo) {
470
+ const diretorioSugerido = sugerirDiretorioCodigoDrift(contexto.baseProjeto, alvoForaDoEscopo);
471
+ registro.status = "fora_do_escopo";
472
+ registro.confianca = "baixa";
473
+ registro.arquivo = alvoForaDoEscopo;
474
+ registro.diretorioSugerido = diretorioSugerido;
475
+ vinculosForaDoEscopo.push(registro);
476
+ diagnosticos.push({
477
+ tipo: "vinculo_fora_do_escopo",
478
+ modulo: ir.nome,
479
+ severidade: "aviso",
480
+ mensagem: `Vinculo ${registro.tipo}="${registro.valor}" de ${itemVinculo.donoTipo} "${itemVinculo.dono}": o codigo existe no workspace mas esta fora dos diretoriosCodigo do sema.config.json; adicione "${diretorioSugerido}" a diretoriosCodigo para o drift verificar este vinculo.`,
481
+ ...(itemVinculo.donoTipo === "task" ? { task: itemVinculo.dono } : itemVinculo.donoTipo === "route" ? { route: itemVinculo.dono } : {}),
482
+ });
483
+ }
484
+ else {
485
+ vinculosQuebrados.push(registro);
486
+ diagnosticos.push({
487
+ tipo: "vinculo_quebrado",
488
+ modulo: ir.nome,
489
+ mensagem: `Vinculo ${registro.tipo}="${registro.valor}" de ${itemVinculo.donoTipo} "${itemVinculo.dono}" nao foi resolvido no codigo vivo.`,
490
+ ...(itemVinculo.donoTipo === "task" ? { task: itemVinculo.dono } : itemVinculo.donoTipo === "route" ? { route: itemVinculo.dono } : {}),
491
+ });
492
+ }
395
493
  }
396
494
  else {
397
495
  vinculosValidos.push(registro);
@@ -429,7 +527,7 @@ export function analisarModulosSelecionadosDrift(estado) {
429
527
  if (registro.status === "nao_encontrado") {
430
528
  resumo.quebrados += 1;
431
529
  }
432
- else {
530
+ else if (registro.status !== "fora_do_escopo") {
433
531
  resumo.validos += 1;
434
532
  }
435
533
  if (registro.arquivo) {
@@ -18,5 +18,6 @@ export interface PlanoEscopoDrift {
18
18
  bloqueios: string[];
19
19
  dependencias: Record<string, string[]>;
20
20
  }
21
+ export declare function sondarCaminhosDeclaradosExistentes(caminhos: readonly string[], baseProjeto: string): Promise<Map<string, boolean>>;
21
22
  export declare function planejarEscopoDrift(contexto: ContextoProjetoCarregado, configuracao: ConfiguracaoEscopoDriftAplicada): Promise<PlanoEscopoDrift>;
22
23
  export declare function expandirDependenciasPlanoDrift(contexto: ContextoProjetoCarregado, plano: PlanoEscopoDrift, catalogo: CatalogoDrift): Promise<PlanoEscopoDrift>;
@@ -84,6 +84,12 @@ async function sondarArquivosRegularesContidos(caminhos, raizReal) {
84
84
  const resultados = await mapearComLimite(unicos, CONCORRENCIA_SONDAGENS_DRIFT, async ([chave, caminho]) => [chave, await arquivoRegularContido(caminho, raizReal)]);
85
85
  return new Map(resultados);
86
86
  }
87
+ // Sonda arquivos declarados em vinculos contra o workspace real: alimenta a
88
+ // distincao entre "arquivo nao existe" e "existe fora dos diretoriosCodigo".
89
+ export async function sondarCaminhosDeclaradosExistentes(caminhos, baseProjeto) {
90
+ const raizReal = await realpath(baseProjeto);
91
+ return sondarArquivosRegularesContidos(caminhos, raizReal);
92
+ }
87
93
  function vinculosModulo(ir) {
88
94
  return [
89
95
  ...ir.vinculos,
@@ -13,6 +13,7 @@ export declare function resumirDriftPorModulo(modulo: string | null, caminho: st
13
13
  implsQuebrados: number;
14
14
  vinculosValidos: number;
15
15
  vinculosQuebrados: number;
16
+ vinculosForaDoEscopo: number;
16
17
  recursosValidos: number;
17
18
  recursosDivergentesCount: number;
18
19
  tasksSemImplementacao: number;
@@ -31,6 +32,7 @@ export declare function resumirDriftPorModulo(modulo: string | null, caminho: st
31
32
  rotasDivergentes: import("./drift.part01.js").RegistroRotaDivergente[];
32
33
  recursosDivergentes: import("./drift.part01.js").RegistroRecursoDrift[];
33
34
  vinculosQuebradosDetalhes: import("./drift.part01.js").RegistroVinculoDrift[];
35
+ vinculosForaDoEscopoDetalhes: import("./drift.part01.js").RegistroVinculoDrift[];
34
36
  };
35
37
  export declare function criarBriefingAgente(arquivo: string, modulo: string, ir: IrModulo | null, resumoDrift: ReturnType<typeof resumirDriftPorModulo>, resultadoDrift: Awaited<ReturnType<typeof analisarDriftLegado>>): {
36
38
  arquivo: string;
@@ -112,6 +112,9 @@ export function resumirDriftPorModulo(modulo, caminho, resultadoDrift) {
112
112
  const vinculosQuebrados = modulo
113
113
  ? resultadoDrift.vinculos_quebrados.filter((vinculo) => vinculo.modulo === modulo)
114
114
  : [];
115
+ const vinculosForaDoEscopo = modulo
116
+ ? resultadoDrift.vinculos_fora_do_escopo.filter((vinculo) => vinculo.modulo === modulo)
117
+ : [];
115
118
  const rotasDivergentes = modulo
116
119
  ? resultadoDrift.rotas_divergentes.filter((rota) => rota.modulo === modulo)
117
120
  : [];
@@ -136,6 +139,7 @@ export function resumirDriftPorModulo(modulo, caminho, resultadoDrift) {
136
139
  ...implsValidos.map((impl) => impl.arquivo).filter((item) => Boolean(item)),
137
140
  ...implsQuebrados.flatMap((impl) => impl.candidatos?.map((candidato) => candidato.arquivo) ?? []),
138
141
  ...vinculosValidos.map((vinculo) => vinculo.arquivo).filter((item) => Boolean(item)),
142
+ ...vinculosForaDoEscopo.map((vinculo) => vinculo.arquivo).filter((item) => Boolean(item)),
139
143
  ...recursosValidos.map((recurso) => recurso.arquivo).filter(Boolean),
140
144
  ...recursosDivergentes.map((recurso) => recurso.arquivo).filter(Boolean),
141
145
  ])].sort((a, b) => a.localeCompare(b, "pt-BR"));
@@ -176,6 +180,7 @@ export function resumirDriftPorModulo(modulo, caminho, resultadoDrift) {
176
180
  implsQuebrados: implsQuebrados.length,
177
181
  vinculosValidos: vinculosValidos.length,
178
182
  vinculosQuebrados: vinculosQuebrados.length,
183
+ vinculosForaDoEscopo: vinculosForaDoEscopo.length,
179
184
  recursosValidos: recursosValidos.length,
180
185
  recursosDivergentesCount: recursosDivergentes.length,
181
186
  tasksSemImplementacao: tasks.filter((task) => task.semImplementacao).length,
@@ -198,6 +203,7 @@ export function resumirDriftPorModulo(modulo, caminho, resultadoDrift) {
198
203
  rotasDivergentes,
199
204
  recursosDivergentes,
200
205
  vinculosQuebradosDetalhes: vinculosQuebrados,
206
+ vinculosForaDoEscopoDetalhes: vinculosForaDoEscopo,
201
207
  };
202
208
  }
203
209
  export function criarBriefingAgente(arquivo, modulo, ir, resumoDrift, resultadoDrift) {
@@ -173,6 +173,7 @@ export async function comandoDrift(entrada, args, emJson, cwd = process.cwd()) {
173
173
  console.log(`- Impl quebrados: ${resultado.impls_quebrados.length}`);
174
174
  console.log(`- Vinculos validos: ${resultado.vinculos_validos.length}`);
175
175
  console.log(`- Vinculos quebrados: ${resultado.vinculos_quebrados.length}`);
176
+ console.log(`- Vinculos fora do escopo (informativo): ${resultado.vinculos_fora_do_escopo.length}`);
176
177
  console.log(`- Rotas divergentes: ${resultado.rotas_divergentes.length}`);
177
178
  console.log(`- Recursos vivos validos: ${resultado.recursos_validos.length}`);
178
179
  console.log(`- Recursos vivos divergentes: ${resultado.recursos_divergentes.length}`);
@@ -229,6 +230,12 @@ export async function comandoDrift(entrada, args, emJson, cwd = process.cwd()) {
229
230
  console.log(` - ${vinculo.modulo}.${vinculo.dono} :: ${vinculo.tipo}=${vinculo.valor}`);
230
231
  }
231
232
  }
233
+ if (resultado.vinculos_fora_do_escopo.length > 0) {
234
+ console.log("- Vinculos fora do escopo (arquivo existe fora dos diretoriosCodigo; informativo):");
235
+ for (const vinculo of resultado.vinculos_fora_do_escopo) {
236
+ console.log(` - ${vinculo.modulo}.${vinculo.dono} :: ${vinculo.tipo}=${vinculo.valor} :: adicione "${vinculo.diretorioSugerido ?? "./"}" a diretoriosCodigo`);
237
+ }
238
+ }
232
239
  if (resultado.resumo_operacional.oQueTocar.length > 0) {
233
240
  console.log("- O que tocar primeiro:");
234
241
  for (const alvo of resultado.resumo_operacional.oQueTocar.slice(0, 8)) {
@@ -1,11 +1,35 @@
1
1
  // SEMA-GOVERNED: sema.produto.cli_init_templates, sema.produto.escrita_segura_workspace
2
2
  // Descrição: inicializa projetos preservando arquivos existentes e bloqueando escapes do workspace.
3
3
  import * as fs from "node:fs";
4
+ import { readFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { escreverArquivos } from "./fsGovernado.js";
6
7
  import { arquivosTemplateIniciar } from "./initTemplatesBase.js";
7
8
  import { materializarExemplosOficiais, normalizarCaminhoExemplo, planejarExemplosOficiais } from "./exemplosOficiais.js";
8
- import { validarDestinosEscritaWorkspace } from "./workspaceWrite.js";
9
+ import { carregarConfiguracaoProjeto } from "./projetoConfig.js";
10
+ import { escreverArquivoWorkspaceSeguro, validarDestinosEscritaWorkspace } from "./workspaceWrite.js";
11
+ async function registrarSubpastaNoConfigAncestral(caminhoConfig, subpastaAbsoluta) {
12
+ const basePai = path.dirname(caminhoConfig);
13
+ const relativoSubpasta = `./${path.relative(basePai, subpastaAbsoluta).replace(/\\/g, "/")}`;
14
+ try {
15
+ const configPai = JSON.parse(await readFile(caminhoConfig, "utf8"));
16
+ const diretoriosAtuais = Array.isArray(configPai.diretoriosCodigo)
17
+ ? configPai.diretoriosCodigo.filter((valor) => typeof valor === "string")
18
+ : [];
19
+ const jaRegistrado = diretoriosAtuais.some((diretorio) => diretorio.replace(/\\/g, "/").toLowerCase() === relativoSubpasta.toLowerCase());
20
+ if (jaRegistrado) {
21
+ console.log(`Subpasta ${relativoSubpasta} ja consta em diretoriosCodigo de ${caminhoConfig}.`);
22
+ return;
23
+ }
24
+ configPai.diretoriosCodigo = [...diretoriosAtuais, relativoSubpasta];
25
+ await escreverArquivoWorkspaceSeguro(basePai, "sema.config.json", `${JSON.stringify(configPai, null, 2)}\n`, { sobrescrever: true });
26
+ console.log(`Subpasta ${relativoSubpasta} registrada em diretoriosCodigo de ${caminhoConfig}.`);
27
+ }
28
+ catch (erro) {
29
+ console.error(`Nao foi possivel registrar a subpasta no config do workspace pai: ${erro instanceof Error ? erro.message : String(erro)}`);
30
+ console.error(`Adicione "${relativoSubpasta}" a diretoriosCodigo em ${caminhoConfig} para o drift cobrir esta subpasta.`);
31
+ }
32
+ }
9
33
  async function sincronizarKitIaInicial(cwd) {
10
34
  const [{ gerarResumoProjetoIa }, { sincronizarEntrypointCodex }] = await Promise.all([
11
35
  import("./index.part04.js"),
@@ -33,7 +57,19 @@ const EXEMPLOS_STARTER_INICIAR = new Set([
33
57
  "exemplos/autenticacao.sema",
34
58
  ]);
35
59
  export async function comandoIniciar(cwd, template, opcoes = {}) {
36
- const arquivos = arquivosTemplateIniciar(template);
60
+ const arquivosCompletos = arquivosTemplateIniciar(template);
61
+ const configAncestral = await carregarConfiguracaoProjeto(cwd);
62
+ const subpastaGovernada = Boolean(configAncestral
63
+ && path.resolve(configAncestral.baseDiretorio) !== path.resolve(cwd));
64
+ // Em subpasta de workspace ja governado o config do template e omitido: a
65
+ // subpasta e registrada nos diretoriosCodigo do sema.config.json ancestral
66
+ // e o lookup de workspace continua subindo para o pai.
67
+ const arquivos = subpastaGovernada && configAncestral
68
+ ? arquivosCompletos.filter((arquivo) => arquivo.caminhoRelativo !== "sema.config.json")
69
+ : arquivosCompletos;
70
+ if (subpastaGovernada && configAncestral) {
71
+ await registrarSubpastaNoConfigAncestral(configAncestral.caminho, path.resolve(cwd));
72
+ }
37
73
  const planoExemplos = await planejarExemplosOficiais();
38
74
  if (!planoExemplos.origem) {
39
75
  console.error("Diretorio de exemplos oficiais nao foi encontrado no pacote da CLI.");
@@ -18,7 +18,7 @@ This is the minimum workflow for Codex in a local workspace.
18
18
 
19
19
  Contract edit rule: `.sema` has its own size budget. Above 300 lines, plan a split by domain/capability; above 500, do not create or edit before splitting. Do not use parte_1/parte_2 and do not force a 1:1 contract-to-file relationship; several contracts can govern the same file through `vinculos`.
20
20
 
21
- Closing rule: the `payload` of `sema drift --cache fresh --json` must return `sucesso:true`. If it reports `sucesso:false`, `vinculos_quebrados`, `rotas_divergentes`, or `impls_quebrados`, the task is still blocked. Passing unit tests or a cache hit do not replace fresh green drift. In 3.0.0, `ok` describes the CLI result path and never replaces a command-specific domain verdict.
21
+ Closing rule: the `payload` of `sema drift --cache fresh --json` must return `sucesso:true`. If it reports `sucesso:false`, `vinculos_quebrados`, `rotas_divergentes`, or `impls_quebrados`, the task is still blocked. Passing unit tests or a cache hit do not replace fresh green drift. `vinculos_fora_do_escopo` is informational: the file exists in the workspace outside the configured `diretoriosCodigo`, and each entry carries a `diretorioSugerido` to add to `sema.config.json`; it does not block closure. In 3.0.0, `ok` describes the CLI result path and never replaces a command-specific domain verdict.
22
22
 
23
23
  Focused drift rule: file and module scopes plan their physical file set before cataloging. They never fall back to a whole-project walk when no safe anchor exists. All indexers and semantic-budget checks reuse the same in-memory source read; a global walk is reserved for explicit `--escopo projeto`. Code discovery is deferred until that plan exists, homonymous candidates fail as ambiguous, and configured contract origins or code roots resolving outside the workspace are rejected before enumeration. `inspecionar`, `impacto`, and `renomear-semantico` preserve the same directed boundary. Missing unambiguous local dependencies make coverage partial and block success. In Python, an unresolved `name` from `from module import name` is not itself a missing file after `module` resolves: it may be an exported symbol, while a real child module is still included whenever its file exists.
24
24
 
package/docs/commands.md CHANGED
@@ -31,7 +31,7 @@ Then read every required doc returned by `docs-impacto`.
31
31
 
32
32
  ## Contract and Discovery
33
33
 
34
- - `sema iniciar --template <base|nestjs|fastapi|nextjs-api|nextjs-consumer|react-vite-consumer|angular-consumer|flutter-consumer|node-firebase-worker|aspnet-api|springboot-api|go-http-api|rust-axum-api|cpp-service-bridge> [--force]`: creates a new Sema project and preserves existing files by default; `--force` is the only explicit overwrite path.
34
+ - `sema iniciar --template <base|nestjs|fastapi|nextjs-api|nextjs-consumer|react-vite-consumer|angular-consumer|flutter-consumer|node-firebase-worker|aspnet-api|springboot-api|go-http-api|rust-axum-api|cpp-service-bridge> [--force]`: creates a new Sema project and preserves existing files by default; `--force` is the only explicit overwrite path. Inside a subfolder of an already governed workspace, the subfolder is registered under `diretoriosCodigo` of the ancestral `sema.config.json` (safe write, deduplicated) instead of creating a local config; if the parent config cannot be written, the scaffold proceeds and prints the exact line to add.
35
35
  - `sema validar <arquivo-ou-pasta> --json`: validates `.sema` contracts.
36
36
  - `sema diagnosticos <arquivo.sema> --json`: details errors and warnings.
37
37
  - `sema formatar <arquivo-ou-pasta>`: formats contracts.
@@ -51,7 +51,7 @@ Then read every required doc returned by `docs-impacto`.
51
51
  - `sema verificar <arquivo-ou-pasta> --json`: runs aggregated final verification.
52
52
  - `sema finalizar-mudanca --intencao "<acao>" --doc-lida <arquivo> --json`: proves documentation reading before closure.
53
53
 
54
- Honest closure: unwrap drift JSON and treat its command payload as the source of truth. `payload.sucesso:false`, non-empty `payload.vinculos_quebrados`, non-empty `payload.rotas_divergentes`, or non-empty `payload.impls_quebrados` mean the change is not complete yet. Do not report "clean drift" without a green payload.
54
+ Honest closure: unwrap drift JSON and treat its command payload as the source of truth. `payload.sucesso:false`, non-empty `payload.vinculos_quebrados`, non-empty `payload.rotas_divergentes`, or non-empty `payload.impls_quebrados` mean the change is not complete yet. Do not report "clean drift" without a green payload. `payload.vinculos_fora_do_escopo` is informational: the file exists in the workspace outside `diretoriosCodigo`, and each entry carries a `diretorioSugerido` for `sema.config.json`.
55
55
 
56
56
  Focused drift exposes its planned, declared, inferred, and missing files plus catalog visit/read metrics in `escopo_aplicado`. A missing declared file or a missing unambiguous code-referenced dependency is a blocker reported as `vinculo_quebrado`; a missing inferred candidate derived from an `impl` symbol is informational under `arquivosAusentesInferidos` and never blocks an implementation that resolved. For Python `from module import name`, Sema still includes `name` when it resolves as a real submodule, but never reports the absent submodule candidate as a broken link after the parent module resolved because `name` may be a function, class, or variable. File and module scopes fail closed without a safe anchor, with homonymous implementation candidates, or with missing local dependencies; only `--escopo projeto` may walk every configured code root. Logical roots such as `src` are probed deterministically without a discovery walk. Configured contract origins and code roots are confined before enumeration, and `inspecionar`, `impacto`, and `renomear-semantico` reuse the same directed boundary without reopening arbitrary external paths.
57
57
 
@@ -156,7 +156,7 @@ Spatial model and render mode are orthogonal: `THREE_D + HEADLESS` is valid, whi
156
156
  - Do not stop after `sema compilar` if the contract target files still do not exist.
157
157
  - Do not replace `sema compilar` with `sema testar` when the contract requires generated code.
158
158
  - Do not create a Markdown report to pretend a gate ran.
159
- - Do not say drift passed when the unwrapped `sema drift --cache fresh --json` payload returned `sucesso:false`, a non-empty `vinculos_quebrados`, `rotas_divergentes`, or `impls_quebrados` list.
159
+ - Do not say drift passed when the unwrapped `sema drift --cache fresh --json` payload returned `sucesso:false`, a non-empty `vinculos_quebrados`, `rotas_divergentes`, or `impls_quebrados` list. `vinculos_fora_do_escopo` entries are informational scope hints, not broken links.
160
160
  - Do not declare a UI responsive without mobile/desktop proof; horizontal scroll at 390px blocks closure.
161
161
 
162
162
  Governed code policy: keep the `SEMA-GOVERNED` marker, split large code by real responsibility, preserve contract links, and never treat a generated output directory as the final delivery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semacode/cli",
3
- "version": "3.12.2",
3
+ "version": "3.13.1",
4
4
  "description": "Local-first semantic governance CLI for AI agents, proven with Claude, Codex, zCode (GLM) and Kimi: contracts, drift, impact, docs gates, and code generation.",
5
5
  "type": "module",
6
6
  "icon": "logo.png",