specatlas 0.1.8 → 0.1.10

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  signApproval
4
- } from "./chunk-IFALGLUJ.js";
4
+ } from "./chunk-X4FIHYOJ.js";
5
5
  import "./chunk-DKFIPJ74.js";
6
6
  export {
7
7
  signApproval
package/dist/bin.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  sha256,
9
9
  shortHash,
10
10
  signApproval
11
- } from "./chunk-IFALGLUJ.js";
11
+ } from "./chunk-X4FIHYOJ.js";
12
12
  import {
13
13
  __commonJS,
14
14
  __toESM,
@@ -9864,6 +9864,14 @@ function verifyApproval(change, approvals, cfg, specContent) {
9864
9864
  if (current !== approval.hash) return { status: "stale", approvedBy: approval.by, approvedAt: approval.at };
9865
9865
  return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
9866
9866
  }
9867
+ function requiresMockups(meta, cfg) {
9868
+ if (meta?.mockups === "required") return true;
9869
+ if (meta?.mockups === "skip") return false;
9870
+ return cfg.gates.mockup.require_approval;
9871
+ }
9872
+ function mockupOverride(change) {
9873
+ return (change.meta?.overrides ?? []).some((override) => override.gate === "mockup");
9874
+ }
9867
9875
  function deriveState(input) {
9868
9876
  const { change, cfg, approval, blockingFindings } = input;
9869
9877
  const lane = change.meta?.lane ?? cfg.lanes.default;
@@ -9896,6 +9904,14 @@ function deriveState(input) {
9896
9904
  blockedBy.push(`${blockingFindings} hallazgo(s) bloqueante(s)`);
9897
9905
  return { state: "spec_draft", blockedBy, nextAction: next(`satlas validate --change ${change.slug}`, "Corregir los hallazgos de la especificaci\xF3n"), progress };
9898
9906
  }
9907
+ if ((approval.status === "missing" || approval.status === "stale") && requiresMockups(change.meta, cfg) && input.mockupsReady !== true && !mockupOverride(change)) {
9908
+ return {
9909
+ state: "awaiting_mockups",
9910
+ blockedBy: ["mockups requeridos y no listos"],
9911
+ nextAction: next(`/satlas-mockup ${change.slug}`, "Generar los mockups (contrato visual) antes de aprobar", true),
9912
+ progress
9913
+ };
9914
+ }
9899
9915
  if (approval.status === "missing" || approval.status === "stale") {
9900
9916
  blockedBy.push(approval.status === "stale" ? "la firma de la spec qued\xF3 obsoleta (el archivo cambi\xF3)" : "la spec no est\xE1 aprobada");
9901
9917
  const presented = change.presentationPath !== void 0;
@@ -9926,6 +9942,7 @@ function stateLabel(state) {
9926
9942
  const labels = {
9927
9943
  draft: "borrador",
9928
9944
  spec_draft: "spec en borrador",
9945
+ awaiting_mockups: "esperando mockups",
9929
9946
  awaiting_approval: "esperando aprobaci\xF3n",
9930
9947
  approved: "aprobado",
9931
9948
  planned: "planificado",
@@ -10498,6 +10515,7 @@ function changeMetaYaml(meta) {
10498
10515
  if (meta.risk) doc["risk"] = meta.risk;
10499
10516
  if (meta.created) doc["created"] = meta.created;
10500
10517
  if (meta.owner) doc["owner"] = meta.owner;
10518
+ if (meta.mockups) doc["mockups"] = meta.mockups;
10501
10519
  return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
10502
10520
  ` + stringifyYaml3(doc, { lineWidth: 120 });
10503
10521
  }
@@ -10783,7 +10801,7 @@ async function approveFromGithub(opts) {
10783
10801
  if (!by) {
10784
10802
  return { approved: false, label, issueNumber: number, diagnostics: [diag("ATLAS-GH-011", "error", "No se pudo determinar qui\xE9n aprueba (--by ni `gh api user`)")] };
10785
10803
  }
10786
- const { signApproval: signApproval2 } = await import("./approvals-HCBC7QDM-PSBJC7EV.js");
10804
+ const { signApproval: signApproval2 } = await import("./approvals-4QHIT7F6-JAI5IHNX.js");
10787
10805
  const signed = await signApproval2({
10788
10806
  root: opts.root,
10789
10807
  artifact: path5.posix.join("changes", opts.slug, "spec.md"),
@@ -11302,6 +11320,28 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
11302
11320
  void now;
11303
11321
  return { screenshots, diagnostics };
11304
11322
  }
11323
+ async function mockupsReady(root, slug, change) {
11324
+ const check = await checkMockups(root, slug, change);
11325
+ return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
11326
+ }
11327
+ async function setMockupRequirement(root, slug, value) {
11328
+ const file = path8.join(root, ".sdd", "changes", slug, "meta.yaml");
11329
+ const raw = await readTextIfExists(file) ?? `schema_version: 1
11330
+ slug: ${slug}
11331
+ lane: standard
11332
+ `;
11333
+ const lines = raw.replace(/\r\n?/g, "\n").split("\n");
11334
+ const index = lines.findIndex((line) => /^mockups:/.test(line));
11335
+ if (index >= 0) {
11336
+ lines[index] = `mockups: ${value}`;
11337
+ } else {
11338
+ let last = lines.length;
11339
+ while (last > 0 && (lines[last - 1] ?? "").trim() === "") last -= 1;
11340
+ lines.splice(last, 0, `mockups: ${value}`);
11341
+ }
11342
+ await writeText(file, lines.join("\n"));
11343
+ return { path: file };
11344
+ }
11305
11345
  var checkSchema = z5.object({
11306
11346
  id: z5.string().min(1),
11307
11347
  title: z5.string().min(1),
@@ -11853,6 +11893,8 @@ async function generatePresentation(opts) {
11853
11893
  const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
11854
11894
  const proposalRaw = await readTextIfExists(path12.join(change.dir, "proposal.md"));
11855
11895
  const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
11896
+ const approvals = await loadApprovals(path12.join(root, ".sdd"));
11897
+ const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
11856
11898
  const labels = labelsFor(language);
11857
11899
  const html = page({
11858
11900
  language,
@@ -11869,7 +11911,8 @@ async function generatePresentation(opts) {
11869
11911
  evidence: new Map((change.verify?.evidence ?? []).map((e) => [e.scenario, e.result])),
11870
11912
  mockups: mockupInfo.screens,
11871
11913
  screenshots: mockupInfo.screenshots,
11872
- approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`
11914
+ approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
11915
+ ...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
11873
11916
  });
11874
11917
  const outFile = path12.join(presentationDir, "index.html");
11875
11918
  await writeText(outFile, html);
@@ -11920,6 +11963,7 @@ function labelsFor(language) {
11920
11963
  mockups: "Mockups",
11921
11964
  approve: "Approval",
11922
11965
  approveText: "This proposal is approved by signing the spec (hash + author). Any later change invalidates the signature.",
11966
+ approveDone: "Approved by {by} on {at}. Any later change invalidates the signature.",
11923
11967
  command: "Command",
11924
11968
  hash: "Hash",
11925
11969
  generated: "Generated",
@@ -11941,6 +11985,7 @@ function labelsFor(language) {
11941
11985
  mockups: "Mockups",
11942
11986
  approve: "Aprobaci\xF3n",
11943
11987
  approveText: "Esta propuesta se aprueba firmando la spec (hash + autor). Cualquier cambio posterior invalida la firma.",
11988
+ approveDone: "Aprobada por {by} el {at}. Cualquier cambio posterior invalida la firma.",
11944
11989
  command: "Comando",
11945
11990
  hash: "Hash",
11946
11991
  generated: "Generado",
@@ -12056,8 +12101,8 @@ function page(input) {
12056
12101
 
12057
12102
  <section>
12058
12103
  <h2>${esc(l.approve)}</h2>
12059
- <div class="callout">${esc(l.approveText)}</div>
12060
- <p>${esc(l.command)}: <code>${esc(input.approveHint)}</code></p>
12104
+ ${input.approval ? `<div class="callout" style="border-left-color:#15803d;background:rgba(21,128,61,.12)">\u2714 ${esc(l.approveDone.replace("{by}", input.approval.by).replace("{at}", input.approval.at))}</div>` : `<div class="callout">${esc(l.approveText)}</div>
12105
+ <p>${esc(l.command)}: <code>${esc(input.approveHint)}</code></p>`}
12061
12106
  </section>
12062
12107
 
12063
12108
  <footer>SpecAtlas \xB7 ${esc(input.project)} \xB7 ${esc(input.generatedAt)}</footer>
@@ -13475,10 +13520,26 @@ async function runApprove(ctx) {
13475
13520
  diagnostics: [{ code: "ATLAS-APPROVE-000", severity: "error", message: "Falta --by <nombre>: toda aprobaci\xF3n es nominal y auditada" }]
13476
13521
  };
13477
13522
  }
13478
- const { root } = await requireWorkspace(ctx);
13523
+ const { root, workspace, config } = await requireWorkspace(ctx);
13479
13524
  const artifact = target.includes("/") || target.includes("\\") ? target : path26.join("changes", target, "spec.md");
13480
13525
  const channelFlag = flagString(ctx.flags, "channel");
13481
13526
  const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
13527
+ const change = workspace.changes.find(
13528
+ (candidate) => candidate.slug === target || artifact.includes(`changes/${candidate.slug}/`) || artifact.includes(`changes\\${candidate.slug}\\`)
13529
+ );
13530
+ if (change && requiresMockups(change.meta, config) && !(change.meta?.overrides ?? []).some((override) => override.gate === "mockup") && !await mockupsReady(root, change.slug, change)) {
13531
+ return {
13532
+ exitCode: 1,
13533
+ diagnostics: [
13534
+ {
13535
+ code: "ATLAS-APPROVE-002",
13536
+ severity: "error",
13537
+ message: `El cambio "${change.slug}" exige mockups (meta.yaml: mockups: required) y no est\xE1n listos`,
13538
+ suggestion: `Genera el contrato visual con \`/satlas-mockup ${change.slug}\` y vuelve a aprobar (o registra un override del gate "mockup" en meta.yaml)`
13539
+ }
13540
+ ]
13541
+ };
13542
+ }
13482
13543
  const result = await signApproval({
13483
13544
  root,
13484
13545
  artifact,
@@ -13871,12 +13932,15 @@ async function runMockup(ctx) {
13871
13932
  const { root, workspace, config } = await requireWorkspace(ctx);
13872
13933
  const slug = ctx.positionals[0];
13873
13934
  if (!slug) {
13874
- return { exitCode: 2, diagnostics: [{ code: "ATLAS-MKP-000", severity: "error", message: "Falta el slug: satlas mockup <slug> [--plan|--check|--capture]" }] };
13935
+ return { exitCode: 2, diagnostics: [{ code: "ATLAS-MKP-000", severity: "error", message: "Falta el slug: satlas mockup <slug> [--plan|--check|--capture|--require]" }] };
13875
13936
  }
13876
13937
  const change = workspace.changes.find((c) => c.slug === slug);
13877
13938
  if (!change) {
13878
13939
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-MKP-000", severity: "error", message: `No existe el cambio "${slug}"` }] };
13879
13940
  }
13941
+ if (flagBool(ctx.flags, "require")) {
13942
+ await setMockupRequirement(root, slug, "required");
13943
+ }
13880
13944
  if (flagBool(ctx.flags, "check")) {
13881
13945
  const result = await checkMockups(root, slug, change);
13882
13946
  const errors = result.findings.filter((d) => d.severity === "error").length;
@@ -14045,7 +14109,14 @@ async function evaluateChange(workspace, config, change, approvals) {
14045
14109
  requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
14046
14110
  });
14047
14111
  const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
14048
- const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? blocking : 0 });
14112
+ const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
14113
+ const state = deriveState({
14114
+ change,
14115
+ cfg: config,
14116
+ approval,
14117
+ blockingFindings: change.delta ? blocking : 0,
14118
+ ...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
14119
+ });
14049
14120
  return { change, approval, lintFindings, trace, blocking, state };
14050
14121
  }
14051
14122
 
@@ -5,7 +5,7 @@ import {
5
5
  writeText
6
6
  } from "./chunk-DKFIPJ74.js";
7
7
 
8
- // ../core/dist/chunk-2BFXQ65Q.js
8
+ // ../core/dist/chunk-LMJURGRU.js
9
9
  import path from "path";
10
10
  import { stringify as stringifyYaml } from "yaml";
11
11
  import { createHash } from "crypto";
@@ -70,6 +70,7 @@ var changeMetaSchema = z.object({
70
70
  risk: z.enum(["low", "medium", "high"]).optional(),
71
71
  created: z.string().optional(),
72
72
  owner: z.string().optional(),
73
+ mockups: z.enum(["required", "skip"]).optional(),
73
74
  tracker: z.object({ provider: z.string(), id: z.string() }).optional(),
74
75
  paused: z.object({ reason: z.string(), at: z.string(), by: z.string() }).optional(),
75
76
  lane_history: z.array(z.object({ from: z.enum(["fix", "standard", "full"]), to: z.enum(["fix", "standard", "full"]), at: z.string(), by: z.string() })).optional(),
@@ -99,6 +100,7 @@ function parseChangeMeta(raw, filePath) {
99
100
  if (v.risk !== void 0) meta.risk = v.risk;
100
101
  if (v.created !== void 0) meta.created = v.created;
101
102
  if (v.owner !== void 0) meta.owner = v.owner;
103
+ if (v.mockups !== void 0) meta.mockups = v.mockups;
102
104
  if (v.tracker !== void 0) meta.tracker = v.tracker;
103
105
  if (v.paused !== void 0) meta.paused = v.paused;
104
106
  if (v.lane_history !== void 0) meta.laneHistory = v.lane_history;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specatlas",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "SpecAtlas: kernel determinista de Spec-Driven Development (CLI)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@specatlas/adapters": "0.1.1",
43
- "@specatlas/core": "0.1.6"
43
+ "@specatlas/core": "0.1.8"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --noEmit",
@@ -19,10 +19,10 @@ El contenido se escribe en **{{LANGUAGE_NAME}}**.
19
19
 
20
20
  ## Pasos
21
21
 
22
- 1. Prepara el plan y el manifiesto:
22
+ 1. Prepara el plan y el manifiesto (y marca el cambio como que exige mockups si aún no lo declara):
23
23
 
24
24
  ```
25
- satlas mockup {{SLUG}}
25
+ satlas mockup {{SLUG}} --require
26
26
  ```
27
27
 
28
28
  2. Lee el plan (`changes/{{SLUG}}/mockups/plan.yaml`), la spec, el glosario y, si existen, `design/tokens.json` o `DESIGN.md` (sistema de diseño del proyecto).
@@ -34,13 +34,14 @@ El contenido se escribe en **{{LANGUAGE_NAME}}**.
34
34
  - `MODIFIED` copia el bloque **completo** del requisito tal como está en la spec viva y lo edita.
35
35
  - `REMOVED` declara `- Motivo:` y `- Migración:`.
36
36
  5. Completa `.sdd/changes/{{SLUG}}/proposal.md` en lenguaje de negocio (sin tecnología y sin dejar los textos entre paréntesis de la plantilla): **Por qué** (problema u oportunidad, con la historia de usuario), **Qué cambia** (alcance funcional), **Fuera de alcance** (lo que no se hará) y **Cómo se mide el éxito** (indicadores observables).
37
- 6. Verifica con el CLI y corrige hasta que no haya errores:
37
+ 6. Si el cambio toca interfaz, **pregunta si llevará mockups** y anótalo en `.sdd/changes/{{SLUG}}/meta.yaml`: `mockups: required` (no se podrá aprobar sin ellos; el siguiente paso será `/satlas-mockup`) o `mockups: skip` (se aprueba sin contrato visual).
38
+ 7. Verifica con el CLI y corrige hasta que no haya errores:
38
39
 
39
40
  ```
40
41
  satlas validate --change {{SLUG}}
41
42
  ```
42
43
 
43
- 7. Reporta: número de requisitos y escenarios, supuestos, y las preguntas que quedaron abiertas.
44
+ 8. Reporta: número de requisitos y escenarios, supuestos, y las preguntas que quedaron abiertas.
44
45
 
45
46
  ## Prohibido
46
47