@rsaglobaltech/specgate 0.25.0 → 0.27.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +87 -42
  2. package/README.md +6 -0
  3. package/dist/packages/core/src/domain/AgentPrompt.js +12 -0
  4. package/dist/packages/core/src/domain/AgentPrompt.js.map +1 -1
  5. package/dist/packages/core/src/domain/ChangeMeasures.js +198 -0
  6. package/dist/packages/core/src/domain/ChangeMeasures.js.map +1 -0
  7. package/dist/packages/core/src/domain/Clarification.js +5 -0
  8. package/dist/packages/core/src/domain/Clarification.js.map +1 -1
  9. package/dist/packages/core/src/domain/ConventionalCommit.js +73 -0
  10. package/dist/packages/core/src/domain/ConventionalCommit.js.map +1 -0
  11. package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
  12. package/dist/packages/core/src/domain/HarnessReport.js +2 -0
  13. package/dist/packages/core/src/domain/HarnessReport.js.map +1 -1
  14. package/dist/packages/core/src/domain/QualityChecks.js +218 -0
  15. package/dist/packages/core/src/domain/QualityChecks.js.map +1 -0
  16. package/dist/packages/core/src/domain/QualitySummary.js +58 -0
  17. package/dist/packages/core/src/domain/QualitySummary.js.map +1 -0
  18. package/dist/packages/core/src/domain/RequirementPlan.js +11 -0
  19. package/dist/packages/core/src/domain/RequirementPlan.js.map +1 -1
  20. package/dist/packages/core/src/domain/Sarif.js +121 -0
  21. package/dist/packages/core/src/domain/Sarif.js.map +1 -0
  22. package/dist/packages/core/src/infrastructure/ChangeWorkspace.js +15 -3
  23. package/dist/packages/core/src/infrastructure/ChangeWorkspace.js.map +1 -1
  24. package/dist/packages/core/src/infrastructure/GitContributionStager.js +1 -1
  25. package/dist/packages/core/src/infrastructure/GitContributionStager.js.map +1 -1
  26. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +26 -0
  27. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
  28. package/dist/packages/core/src/infrastructure/IdsAcrossBranches.js +4 -1
  29. package/dist/packages/core/src/infrastructure/IdsAcrossBranches.js.map +1 -1
  30. package/dist/packages/core/src/infrastructure/QualityBaselineFile.js +67 -0
  31. package/dist/packages/core/src/infrastructure/QualityBaselineFile.js.map +1 -0
  32. package/dist/packages/core/test/unit/rework.test.js +62 -0
  33. package/dist/packages/core/test/unit/rework.test.js.map +1 -0
  34. package/dist/scripts/cli/commands/change/ChangeCommand.js +38 -1
  35. package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
  36. package/dist/scripts/cli/commands/change/TicketCommand.js +1 -1
  37. package/dist/scripts/cli/commands/change/TicketCommand.js.map +1 -1
  38. package/dist/scripts/cli/commands/harness/RunCommand.js +269 -17
  39. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  40. package/dist/scripts/cli/commands/quality/CheckCommand.js +54 -3
  41. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
  42. package/dist/scripts/cli/commands/quality/QualityCommand.js +314 -0
  43. package/dist/scripts/cli/commands/quality/QualityCommand.js.map +1 -0
  44. package/dist/scripts/cli/commands/spec/DoneCommand.js +4 -1
  45. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
  46. package/dist/scripts/cli/commands/spec/DraftCommand.js +44 -4
  47. package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
  48. package/dist/scripts/cli/commands/spec/StatusCommand.js +5 -1
  49. package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
  50. package/dist/scripts/cli/commands/spec/StudioCommand.js +208 -1
  51. package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
  52. package/dist/scripts/docs/nav.js +13 -2
  53. package/dist/scripts/docs/nav.js.map +1 -1
  54. package/dist/scripts/docs/record-journey.js +1 -0
  55. package/dist/scripts/docs/record-journey.js.map +1 -1
  56. package/dist/scripts/docs/record-terminal.js +18 -0
  57. package/dist/scripts/docs/record-terminal.js.map +1 -1
  58. package/dist/scripts/draft/drafting-guide.js +11 -1
  59. package/dist/scripts/draft/drafting-guide.js.map +1 -1
  60. package/dist/scripts/harness/prompt.js +4 -0
  61. package/dist/scripts/harness/prompt.js.map +1 -1
  62. package/dist/scripts/harness/report.js +13 -0
  63. package/dist/scripts/harness/report.js.map +1 -1
  64. package/dist/scripts/lib/change-review.js +145 -0
  65. package/dist/scripts/lib/change-review.js.map +1 -0
  66. package/dist/scripts/lib/clarification.js +24 -5
  67. package/dist/scripts/lib/clarification.js.map +1 -1
  68. package/dist/scripts/lib/harness-guard.js +18 -0
  69. package/dist/scripts/lib/harness-guard.js.map +1 -1
  70. package/dist/scripts/lib/in-flight.js +17 -11
  71. package/dist/scripts/lib/in-flight.js.map +1 -1
  72. package/dist/scripts/lib/live-model.js +7 -1
  73. package/dist/scripts/lib/live-model.js.map +1 -1
  74. package/dist/scripts/lib/live-page.js +72 -11
  75. package/dist/scripts/lib/live-page.js.map +1 -1
  76. package/dist/scripts/lib/quality-presets.js +378 -0
  77. package/dist/scripts/lib/quality-presets.js.map +1 -0
  78. package/dist/scripts/lib/quality-run.js +579 -0
  79. package/dist/scripts/lib/quality-run.js.map +1 -0
  80. package/dist/scripts/lib/screen-requirements.js +149 -0
  81. package/dist/scripts/lib/screen-requirements.js.map +1 -0
  82. package/dist/scripts/lib/sonar-local.js +308 -0
  83. package/dist/scripts/lib/sonar-local.js.map +1 -0
  84. package/dist/scripts/lib/studio-app-page.js +1023 -0
  85. package/dist/scripts/lib/studio-app-page.js.map +1 -0
  86. package/dist/scripts/lib/studio-app.js +442 -0
  87. package/dist/scripts/lib/studio-app.js.map +1 -0
  88. package/dist/scripts/lib/studio-route.js +996 -0
  89. package/dist/scripts/lib/studio-route.js.map +1 -0
  90. package/dist/scripts/lib/surface.js +11 -0
  91. package/dist/scripts/lib/surface.js.map +1 -1
  92. package/dist/scripts/lib/ticket-keys.js +1 -1
  93. package/dist/scripts/lib/ticket-keys.js.map +1 -1
  94. package/dist/scripts/quality.js +10 -0
  95. package/dist/scripts/quality.js.map +1 -0
  96. package/package.json +1 -1
  97. package/templates/harness/harness.config.yaml.tpl +10 -1
@@ -0,0 +1,1023 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.studioAppPage = studioAppPage;
4
+ /**
5
+ * The visual studio (REQ-204..217): a route of seven steps — Project, Brief,
6
+ * Drafts, Questions, Review, Build, Report — in a simple mode with no command
7
+ * in sight and an advanced one that shows them, in Spanish and English. Every
8
+ * value comes from `/api/*` and is set as text; every write is a POST
9
+ * carrying this page's token.
10
+ *
11
+ * The script lives inside this template literal: it uses no backticks, no
12
+ * dollar-brace, and no backslash escapes.
13
+ */
14
+ function studioAppPage(projectName, token = "") {
15
+ const title = String(projectName).replace(/[&<>"']/g, "");
16
+ const tok = String(token).replace(/[^0-9a-f]/g, "");
17
+ return `<!doctype html>
18
+ <html lang="es"><head><meta charset="utf-8"/>
19
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
20
+ <meta name="specgate-token" content="${tok}"/>
21
+ <title>Specgate Studio — ${title}</title>
22
+ <style>
23
+ :root{--bg:#f6f8fa;--panel:#fff;--line:#d1d9e0;--soft:#eff2f5;--ink:#1f2328;--muted:#59636e;--pass:#1a7f37;--pass-bg:#dafbe1;--go:#1f883d;
24
+ --warn:#7d4e00;--warn-bg:#fff8c5;--warn-line:#9a6700;--info:#0550ae;--info-bg:#ddf4ff;--accent:#0969da;--fail:#cf222e;--here:#f6faff;
25
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;color-scheme:light}
26
+ @media (prefers-color-scheme: dark){:root{--bg:#0d1117;--panel:#151b23;--line:#3d444d;--soft:#212830;--ink:#f0f6fc;--muted:#9198a1;
27
+ --pass:#3fb950;--pass-bg:#12261e;--go:#238636;--warn:#d29922;--warn-bg:#2c2513;--warn-line:#9e6a03;--info:#4493f8;--info-bg:#16263d;--accent:#4493f8;--fail:#f85149;--here:#121d2b;color-scheme:dark}}
28
+ *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--sans)}
29
+ a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline}
30
+ button{font:inherit;color:inherit;cursor:pointer}
31
+ header{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:8px 24px;min-height:60px;background:var(--panel);border-bottom:1px solid var(--line)}
32
+ .brand{display:flex;align-items:center;gap:8px;font-weight:600}
33
+ nav{display:flex;flex-wrap:wrap;align-items:center;gap:2px}
34
+ nav button{display:flex;align-items:center;gap:6px;border:0;background:none;padding:8px 10px;border-radius:6px;color:var(--muted)}
35
+ nav button[aria-current="page"]{background:var(--soft);color:var(--ink);font-weight:600}
36
+ nav button[disabled]{cursor:default;opacity:.7}
37
+ nav .sep{width:1px;height:20px;background:var(--line);margin:0 6px}
38
+ .num{width:20px;height:20px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;border:1px solid var(--line);flex:none}
39
+ .num.done{background:var(--pass-bg);color:var(--pass);border-color:transparent}
40
+ .num.current{background:var(--accent);color:#fff;border-color:transparent}
41
+ .num.locked{border-style:dashed}
42
+ .badge{padding:1px 7px;border-radius:999px;font-size:12px;font-weight:600}
43
+ .b-warn{background:var(--warn-bg);color:var(--warn)} .b-info{background:var(--info-bg);color:var(--info)} .b-pass{background:var(--pass-bg);color:var(--pass)} .b-soft{background:var(--soft);color:var(--muted)}
44
+ .toggles{margin-left:auto;display:flex;gap:14px;font-size:13px;color:var(--muted)}
45
+ .toggles button{border:0;background:none;padding:2px 4px;color:var(--accent)} .toggles button[aria-pressed="true"]{color:var(--ink);font-weight:600}
46
+ main{max-width:1120px;margin:0 auto;padding:28px 24px;display:flex;flex-direction:column;gap:20px}
47
+ main.narrow{max-width:780px}
48
+ h1{margin:0;font-size:24px;font-weight:600} h2{margin:0;font-size:16px;font-weight:600} p{margin:0}
49
+ .muted{color:var(--muted)} .small{font-size:13px}
50
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:18px 20px;display:flex;flex-direction:column;gap:10px}
51
+ .card.warn{border-color:var(--warn-line)} .card.info{border-color:var(--accent)}
52
+ .next{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:20px;padding:24px}
53
+ .next .t{flex:1 1 420px;display:flex;flex-direction:column;gap:6px}
54
+ .eyebrow{font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--info)}
55
+ .next h2{font-size:22px}
56
+ .go{min-height:44px;padding:0 22px;border:0;border-radius:8px;background:var(--go);color:#fff;font-weight:600}
57
+ .go[disabled],.ghost[disabled]{opacity:.5;cursor:not-allowed}
58
+ .ghost{min-height:38px;padding:0 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}
59
+ .link{border:0;background:none;padding:4px 0;color:var(--accent);font-size:13px;text-align:left}
60
+ .danger{color:var(--fail)}
61
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}
62
+ .stat{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:16px 18px;display:flex;flex-direction:column;gap:4px}
63
+ .stat b{font-size:24px;font-weight:600}
64
+ .rows{background:var(--panel);border:1px solid var(--line);border-radius:8px}
65
+ .row{display:grid;grid-template-columns:180px minmax(0,1fr) 170px;gap:12px;align-items:center;padding:12px 18px;border-bottom:1px solid var(--soft)}
66
+ .row:last-child{border-bottom:0}
67
+ .line{display:flex;flex-wrap:wrap;align-items:center;gap:14px;padding:14px 20px;border-bottom:1px solid var(--soft)}
68
+ .line:last-child{border-bottom:0} .line.here{background:var(--here)}
69
+ .line .grow{flex:999 1 300px;min-width:0;display:flex;flex-direction:column;gap:4px}
70
+ .line .name{flex:0 0 130px;font-weight:600}
71
+ .step .num{width:32px;height:32px;font-size:14px}
72
+ .pick{display:flex;gap:12px;align-items:center;padding:12px 20px;border-bottom:1px solid var(--soft);cursor:pointer}
73
+ .pick .pname{flex:1;min-width:0;display:flex;gap:10px;align-items:center}
74
+ .rows>.cmd{display:block;padding:10px 20px}
75
+ .bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--soft)} .bar i{display:block}
76
+ .pill{display:inline-block;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:600}
77
+ textarea,input[type=text],select{font:inherit;padding:9px 11px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);width:100%}
78
+ textarea{resize:vertical} select{width:auto}
79
+ .opt{display:flex;gap:10px;align-items:center;padding:10px 12px;border:1px solid var(--line);border-radius:6px}
80
+ .choices{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}
81
+ .choice{display:flex;flex-direction:column;gap:6px;padding:14px;border:1px solid var(--line);border-radius:8px;cursor:pointer}
82
+ .choice.on{border:2px solid var(--accent);background:var(--here);padding:13px}
83
+ .choice .h{display:flex;align-items:center;gap:8px;font-weight:600}
84
+ .field{display:flex;flex-direction:column;gap:8px;padding:18px 22px;border-bottom:1px solid var(--line)}
85
+ .field:last-child{border-bottom:0} .field>label,.field>.lab{font-weight:600}
86
+ .form{background:var(--panel);border:1px solid var(--line);border-radius:8px;display:flex;flex-direction:column}
87
+ .chips{display:flex;flex-wrap:wrap;gap:6px}
88
+ .chip{padding:6px 12px;border:1px solid var(--line);border-radius:999px;background:var(--panel)}
89
+ .chip[aria-selected="true"]{border-color:var(--accent);background:var(--info-bg);color:var(--info);font-weight:600}
90
+ .cards{display:flex;flex-direction:column;gap:14px;padding:16px;border:1px solid var(--line);border-radius:8px;background:var(--bg)}
91
+ .item{display:flex;gap:8px;align-items:center} .item input{flex:1}
92
+ .x{border:0;background:none;color:var(--muted);padding:4px 8px;border-radius:6px}
93
+ .steps{border-left:3px solid var(--line);padding:2px 0 2px 12px;display:flex;flex-direction:column;gap:4px;font-size:13.5px}
94
+ .kw{font-weight:600;color:var(--accent)}
95
+ .split{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start}
96
+ .split>.main{flex:999 1 600px;min-width:0;display:flex;flex-direction:column;gap:16px}
97
+ .split>aside{flex:1 1 300px;display:flex;flex-direction:column;gap:16px}
98
+ .check{display:flex;gap:8px;align-items:flex-start} .dot{width:16px;height:16px;border-radius:50%;flex:none;margin-top:2px}
99
+ .stages{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;font-size:13px}
100
+ .stages div{display:flex;flex-direction:column;gap:6px} .stages i{height:6px;border-radius:3px;background:var(--soft);display:block}
101
+ .run{background:repeating-linear-gradient(135deg,#bf8700 0 5px,#d4a72c 5px 10px)!important}
102
+ .progress{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px 18px;font-size:13px}
103
+ .progress .ok{color:var(--pass)} .progress .now{color:var(--info);font-weight:600} .progress .todo{color:var(--muted)}
104
+ .cmd{font:12px/1.7 var(--mono);color:var(--muted);white-space:pre-wrap;word-break:break-word}
105
+ .log{background:#1f2328;color:#d1d9e0;border-radius:8px;padding:12px 16px;font:12px/1.7 var(--mono);overflow-x:auto;white-space:pre-wrap}
106
+ .msg{font-size:13px} .msg.err{color:var(--fail)} .msg.ok{color:var(--pass)}
107
+ .empty{padding:32px;text-align:center;color:var(--muted)}
108
+ .table{width:100%;border-collapse:collapse} .table th,.table td{text-align:left;padding:10px 20px;border-top:1px solid var(--line)} .table th{font-size:12px;color:var(--muted);font-weight:600}
109
+ .scroll{overflow-x:auto}
110
+ [hidden]{display:none!important}
111
+ @media (max-width:700px){.row{grid-template-columns:1fr} header{padding:8px 16px} main{padding:20px 16px} .line .name{flex-basis:100%}}
112
+ @media print{header{display:none}}
113
+ </style></head>
114
+ <body>
115
+ <header>
116
+ <div class="brand"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg>${title}</div>
117
+ <nav aria-label="Steps" id="nav"></nav>
118
+ <div class="toggles">
119
+ <span><button type="button" id="mSimple"></button> · <button type="button" id="mAdv"></button></span>
120
+ <span><button type="button" id="lEs">ES</button> · <button type="button" id="lEn">EN</button></span>
121
+ </div>
122
+ </header>
123
+ <main id="view" aria-live="polite"></main>
124
+ <script>
125
+ (function(){
126
+ var T = {
127
+ es: {route:"Ruta", project:"Proyecto", brief:"Brief", drafts:"Borradores", report:"Informe",
128
+ home:"Inicio", questions:"Preguntas", review:"Revisión", build:"Construir", simple:"Simple", adv:"Avanzado",
129
+ routeTitle:"Tu proyecto, paso a paso", routeIntro:"Del brief a la app construida y probada. Cada paso se abre cuando el anterior está listo; puedes volver a cualquiera.",
130
+ next:"Lo siguiente", stepN:"paso", open:"Abrir", allDone:"Todo hecho", allDoneP:"Todo está construido y probado. Añade algo al brief cuando quieras crecer la app.",
131
+ nx_project:"Crea tu proyecto", nxp_project:"Cinco preguntas y specgate prepara esta carpeta.", go_project:"Empezar",
132
+ nx_brief:"Escribe el brief", nxp_brief:"Qué tiene que hacer la app, con tus palabras. De aquí salen los requisitos.", go_brief:"Abrir el brief",
133
+ nx_drafts:"Redacta los borradores", nxp_drafts:"El asistente convierte cada módulo del brief en requisitos con ejemplos.", go_drafts:"Redactar",
134
+ nx_questions:"Responde las preguntas", nxp_questions:"El asistente o un borrador espera una decisión tuya.", go_questions:"Responder",
135
+ nx_review:"Revisa los borradores", nxp_review:"Léelos como lo haría alguien de tu equipo y apruébalos o pide cambios.", go_review:"Revisar",
136
+ nx_build:"Construye lo que está listo", nxp_build:"El asistente programa, prueba y guarda cada requisito.", go_build:"Construir",
137
+ nx_report:"Mira lo que está hecho", nxp_report:"Lo que se construyó, cómo se probó y qué falta.", go_report:"Ver el informe",
138
+ s_project_done:"Specgate preparado en esta carpeta", s_project_todo:"Crea el proyecto en esta carpeta",
139
+ s_brief_done:"N módulos · las comprobaciones pasan", s_brief_open:"Escribe qué tiene que hacer la app", s_brief_fix:"N módulos · hay cosas por completar",
140
+ s_drafts:"D de N módulos redactados", s_questions_open:"N esperan tu respuesta", s_questions_done:"Nada que decidir",
141
+ s_review_open:"N borradores por leer y aprobar", s_review_done:"Nada por revisar",
142
+ s_build:"R listos · B de T construidos", s_build_locked:"Se abre cuando apruebes un borrador",
143
+ s_report:"B de T construidos y probados", s_report_locked:"Aparece con el primer requisito construido", s_locked:"Se abre con el paso anterior",
144
+ repeat:"Los pasos 3 a 6 se repiten por módulo: puedes construir uno mientras redactas otro.",
145
+ loading:"Cargando…", mods:"módulos", sOf:"de", modules:"Módulos", ready:"listo", qWait:"pregunta", draft:"borrador por revisar", reqs:"requisitos",
146
+ newTitle:"Empecemos un proyecto", newIntro:"Esta carpeta está vacía. Cinco preguntas y specgate la prepara: especificación, pruebas y el asistente que programa.",
147
+ q1:"1 · ¿Cómo se llama?", q2:"2 · ¿Qué vas a construir?", q3:"3 · ¿Con qué tecnología?", q4:"4 · ¿Qué asistente programa?",
148
+ kWeb:"Una web", kWebP:"Pantallas que se abren en el navegador", kApi:"Un servicio (API)", kApiP:"Datos y reglas para otras apps", kMob:"Una app móvil", kMobP:"Para el teléfono",
149
+ stNode:"Node.js + React (recomendado para una web)", stJava:"Java + Spring Boot", stPy:"Python + FastAPI", stCustom:"Otra… (la escribes tú)",
150
+ stackHint:"Decide el lenguaje y cómo se prueba.", customStack:"Tecnología", customTest:"Cómo se prueba (comando)",
151
+ aClaude:"Claude Code", aCodex:"Codex CLI", aNone:"Elegirlo más tarde", found:"detectado en este equipo", notFound:"no encontrado", liveTok:"tokens y coste en vivo", stepsOnly:"solo pasos",
152
+ create:"Crear proyecto", createP:"Crea la estructura, la especificación vacía y un brief para rellenar.", creating:"Creando…",
153
+ notEmpty:"Esta carpeta ya tiene archivos", notEmptyP:"Para no tocar lo que hay, adóptala desde una terminal con este comando y vuelve a abrir el studio:",
154
+ projReady:"Proyecto listo", projReadyP:"Specgate está preparado. Estos son los asistentes que puede usar:", noProfiles:"No hay asistentes configurados todavía.",
155
+ profilesHint:"Se añaden en .harness/profiles.yaml:", isDefault:"por defecto", assistant:"Asistente",
156
+ bTitleBrief:"El brief", bIntroBrief:"Qué tiene que hacer la app, dicho con tus palabras. De aquí salen los requisitos: lo que no está aquí, no se construye.",
157
+ paste:"¿Tienes el texto del cliente, un correo o notas?", pasteP:"Pégalo tal cual: el asistente propone producto, usuarios y módulos. Tú lo corriges después.",
158
+ pasteBtn:"Que el asistente lo estructure", replace:"Reemplazar el brief actual", fromScratch:"Empezar desde cero", fromScratchP:"Un formulario vacío: producto, usuarios y módulos.",
159
+ reading:"El asistente está leyendo el texto…", stopIt:"Parar",
160
+ product:"El producto, en una frase", who:"Quién lo usa", addActor:"+ Añadir otro tipo de usuario", actorName:"Tipo de usuario",
161
+ modsTitle:"Módulos: las partes de la app", addModule:"+ Módulo", moduleName:"Nombre del módulo", removeModule:"Quitar este módulo",
162
+ rules:"Reglas", rulesP:"lo que siempre se cumple", examples:"Ejemplos", examplesP:"una situación concreta, con nombres y números", doubts:"Dudas", doubtsP:"lo que aún no sabes; se convertirán en preguntas",
163
+ addRule:"+ Regla", addExample:"+ Ejemplo", addDoubt:"+ Duda", remove:"Quitar",
164
+ checksB:"Comprobaciones", checksBP:"Se revisan cada vez que guardas", briefOk:"Todo en orden", savedAt:"Guardado", saving:"Guardando…",
165
+ toDrafts:"Siguiente: redactar borradores", formTab:"Formulario", mdTab:"Markdown", saveMd:"Guardar",
166
+ dTitle:"Borradores", dIntro:"Cada módulo del brief se convierte en requisitos con ejemplos que se pueden probar. El asistente los escribe; tú los revisas antes de construir.",
167
+ noBrief:"Primero escribe el brief.", nOfM:"N módulos · D redactados", draftNext:"Redactar el siguiente", draftIt:"Redactar", retry:"Reintentar",
168
+ stTodo:"Sin redactar", stStopped:"Se detuvo sin escribir nada", stDrafting:"Redactando…", stReview:"N requisitos · Q preguntas abiertas", stApproved:"Aprobado · listo para construir", stArchived:"Construido",
169
+ pRead:"Lee su parte del brief", pReqs:"Escribe los requisitos", pScn:"Escribe los ejemplos (Dado / Cuando / Entonces)", pNotes:"Anota supuestos y preguntas", pCheck:"Comprueba el borrador",
170
+ checkOf:"Comprobación de M", toFix:"N cosas por resolver", lastRun:"Última ejecución",
171
+ qTitle:"Preguntas", qIntro:"Lo que hay que decidir antes de seguir. Cada respuesta queda escrita como una decisión del proyecto.",
172
+ fromAgent:"Preguntó el asistente", fromDraft:"Del borrador", fromProject:"Del proyecto", blocks:"bloquea", nothing:"nada",
173
+ yourAnswer:"Tu respuesta", other:"Otra respuesta…", answer:"Responder", optionsSeen:"Opciones que vio specgate",
174
+ toConfirm:"Respondidas, falta confirmar que la spec lo dice", inSpec:"Ya está en la spec", noQuestions:"No hay nada que decidir ahora.",
175
+ rTitle:"Revisión", rIntro:"Léelo como lo leería alguien de tu equipo. Si algo no es lo que querías, pide el cambio con tus palabras.",
176
+ checks:"Comprobaciones", allGood:"Todo listo para aprobar", approve:"Aprobar el borrador", approveWhen:"Se podrá aprobar cuando no quede nada pendiente.",
177
+ ask:"Pedir un cambio", askBtn:"Pedir este cambio", notes:"Cambios pedidos", noDrafts:"No hay borradores por revisar.",
178
+ given:"Dado", when:"Cuando", then:"Entonces", and:"Y", waitsAnswer:"espera una respuesta",
179
+ bTitle:"Construir", bIntro:"El asistente programa cada requisito, lo prueba y solo lo guarda si todo pasa. Puedes cerrar la página: sigue solo.",
180
+ whoCodes:"¿Quién programa?", whoCodesP:"Los asistentes de este proyecto. Se recuerda tu elección.", noEstimate:"Sin estimación todavía", perRun:"por requisito",
181
+ readyN:"Listos para construir", minutes:"min aprox.", perReq:"por requisito", buildN:"Construir los N", build1:"Construir este", noAgent:"Falta configurar el asistente (agent en harness.config.yaml).",
182
+ running:"En marcha", stP:"Programa", stT:"Prueba", stQ:"Revisa la calidad", stS:"Guarda", watch:"Ver en directo", stop:"Detener después de este", stopAsked:"Se detendrá al terminar este.",
183
+ doneToday:"Terminados", nothingReady:"No hay nada listo para construir.", started:"En marcha: ", savedGit:"la especificación quedó guardada en git (N archivos)", saved:"Guardado.", err:"No se pudo: ",
184
+ repTitle:"Lo que está hecho", repIntro:"Cada requisito se construyó y se probó con el ejemplo que lo describe. Este informe sale de la especificación y de las pruebas.",
185
+ download:"Descargar informe", print:"Imprimir / PDF", rBuilt:"Construido", rExamples:"Ejemplos probados", rGate:"Calidad", rGateOk:"Pasa", rGateBad:"No pasa", rCost:"Coste del asistente",
186
+ byModule:"Por módulo", colModule:"Módulo", colBuilt:"Hecho", colEx:"Ejemplos", colState:"Estado", complete:"Completo", waitingN:"N esperan una respuesta", notBuilt:"Sin construir",
187
+ whatDoes:"Qué hace ya la app", provedBy:"Probado con: ", missing:"Lo que falta", missingNone:"Nada.", waitsFor:"espera la respuesta a: ", decisions:"Decisiones y supuestos",
188
+ decisionsP:"Q preguntas respondidas · A supuestos anotados.", nothingBuilt:"Todavía no hay nada construido.",
189
+ qualTitle:"Calidad del código", qualIntro:"Comprobaciones que el asistente debe pasar en cada requisito, todas en Docker y sin cuentas.",
190
+ qualNone:"Sin comprobaciones de calidad todavía.", qualBasic:"Activar el nivel básico", qualStrict:"Activar el nivel estricto",
191
+ toBasic:"Cambiar a básico", toStrict:"Cambiar a estricto", gates:"bloquea", advises:"avisa", notRunnable:"no se puede ejecutar aquí",
192
+ dockerOn:"Docker responde", dockerOff:"Docker no responde: arranca Docker Desktop", thresholds:"Umbrales: cobertura de lo cambiado C % · mutación M %",
193
+ stAttempt:"en cada intento", stEnd:"al final", stCi:"en CI",
194
+ q5:"5 · ¿Qué nivel de calidad?", qlBasic:"Básico (recomendado)", qlBasicP:"Secretos, dependencias, tests que cubren lo cambiado y análisis estático. Todo en Docker.",
195
+ qlStrict:"Estricto", qlStrictP:"Además: tests de mutación, complejidad y SAST. Más lento, más exigente.", qlNone:"Ninguno por ahora", qlNoneP:"Se puede activar después en Proyecto.",
196
+ rQuality:"Calidad del código", rQualityP:"cobertura C % · mutación M %", covShort:"cobertura", mutShort:"mutación"},
197
+ en: {route:"Route", project:"Project", brief:"Brief", drafts:"Drafts", report:"Report",
198
+ home:"Home", questions:"Questions", review:"Review", build:"Build", simple:"Simple", adv:"Advanced",
199
+ routeTitle:"Your project, step by step", routeIntro:"From the brief to the app built and proven. Each step opens once the one before is ready; you can go back to any.",
200
+ next:"Next", stepN:"step", open:"Open", allDone:"All done", allDoneP:"Everything is built and proven. Add to the brief whenever you want the app to grow.",
201
+ nx_project:"Create your project", nxp_project:"Five questions and specgate prepares this folder.", go_project:"Start",
202
+ nx_brief:"Write the brief", nxp_brief:"What the app has to do, in your words. The requirements come from it.", go_brief:"Open the brief",
203
+ nx_drafts:"Draft the modules", nxp_drafts:"The assistant turns each module of the brief into requirements with examples.", go_drafts:"Draft",
204
+ nx_questions:"Answer the questions", nxp_questions:"The assistant or a draft is waiting for your decision.", go_questions:"Answer",
205
+ nx_review:"Review the drafts", nxp_review:"Read them as someone on your team would, then approve them or ask for changes.", go_review:"Review",
206
+ nx_build:"Build what is ready", nxp_build:"The assistant codes, tests and saves each requirement.", go_build:"Build",
207
+ nx_report:"See what is done", nxp_report:"What was built, how it was proven and what is missing.", go_report:"See the report",
208
+ s_project_done:"Specgate is set up in this folder", s_project_todo:"Create the project in this folder",
209
+ s_brief_done:"N modules · the checks pass", s_brief_open:"Write what the app has to do", s_brief_fix:"N modules · things to complete",
210
+ s_drafts:"D of N modules drafted", s_questions_open:"N wait for your answer", s_questions_done:"Nothing to decide",
211
+ s_review_open:"N drafts to read and approve", s_review_done:"Nothing to review",
212
+ s_build:"R ready · B of T built", s_build_locked:"Opens once you approve a draft",
213
+ s_report:"B of T built and proven", s_report_locked:"Appears with the first requirement built", s_locked:"Opens with the step before",
214
+ repeat:"Steps 3 to 6 repeat per module: you can build one while another is drafted.",
215
+ loading:"Loading…", mods:"modules", sOf:"of", modules:"Modules", ready:"done", qWait:"question", draft:"draft to review", reqs:"requirements",
216
+ newTitle:"Let's start a project", newIntro:"This folder is empty. Five questions and specgate prepares it: specification, tests and the assistant that codes.",
217
+ q1:"1 · What is it called?", q2:"2 · What are you building?", q3:"3 · With which technology?", q4:"4 · Which assistant codes?",
218
+ kWeb:"A website", kWebP:"Screens opened in the browser", kApi:"A service (API)", kApiP:"Data and rules for other apps", kMob:"A mobile app", kMobP:"For the phone",
219
+ stNode:"Node.js + React (recommended for a website)", stJava:"Java + Spring Boot", stPy:"Python + FastAPI", stCustom:"Other… (you write it)",
220
+ stackHint:"Decides the language and how it is tested.", customStack:"Technology", customTest:"How it is tested (command)",
221
+ aClaude:"Claude Code", aCodex:"Codex CLI", aNone:"Choose it later", found:"found on this machine", notFound:"not found", liveTok:"live tokens and cost", stepsOnly:"steps only",
222
+ create:"Create project", createP:"Creates the structure, an empty specification and a brief to fill.", creating:"Creating…",
223
+ notEmpty:"This folder already holds files", notEmptyP:"To leave what is here untouched, adopt it from a terminal with this command and open studio again:",
224
+ projReady:"Project ready", projReadyP:"Specgate is set up. These are the assistants it can use:", noProfiles:"No assistant is configured yet.",
225
+ profilesHint:"They are added in .harness/profiles.yaml:", isDefault:"default", assistant:"Assistant",
226
+ bTitleBrief:"The brief", bIntroBrief:"What the app has to do, in your words. The requirements come from it: what is not here is not built.",
227
+ paste:"Have the client's text, an email or notes?", pasteP:"Paste it as it is: the assistant proposes product, users and modules. You correct it after.",
228
+ pasteBtn:"Let the assistant structure it", replace:"Replace the current brief", fromScratch:"Start from scratch", fromScratchP:"An empty form: product, users and modules.",
229
+ reading:"The assistant is reading the text…", stopIt:"Stop",
230
+ product:"The product, in one sentence", who:"Who uses it", addActor:"+ Add another kind of user", actorName:"Kind of user",
231
+ modsTitle:"Modules: the parts of the app", addModule:"+ Module", moduleName:"Module name", removeModule:"Remove this module",
232
+ rules:"Rules", rulesP:"what always holds", examples:"Examples", examplesP:"a concrete situation, with names and numbers", doubts:"Open questions", doubtsP:"what you do not know yet; they become questions",
233
+ addRule:"+ Rule", addExample:"+ Example", addDoubt:"+ Question", remove:"Remove",
234
+ checksB:"Checks", checksBP:"Run each time you save", briefOk:"All in order", savedAt:"Saved", saving:"Saving…",
235
+ toDrafts:"Next: draft the modules", formTab:"Form", mdTab:"Markdown", saveMd:"Save",
236
+ dTitle:"Drafts", dIntro:"Each module of the brief becomes requirements with examples that can be tested. The assistant writes them; you review them before building.",
237
+ noBrief:"Write the brief first.", nOfM:"N modules · D drafted", draftNext:"Draft the next one", draftIt:"Draft", retry:"Try again",
238
+ stTodo:"Not drafted", stStopped:"Stopped without writing anything", stDrafting:"Drafting…", stReview:"N requirements · Q open questions", stApproved:"Approved · ready to build", stArchived:"Built",
239
+ pRead:"Reads its part of the brief", pReqs:"Writes the requirements", pScn:"Writes the examples (Given / When / Then)", pNotes:"Notes assumptions and questions", pCheck:"Checks the draft",
240
+ checkOf:"Check of M", toFix:"N things to resolve", lastRun:"Last run",
241
+ qTitle:"Questions", qIntro:"What has to be decided before going on. Each answer is written down as a decision of the project.",
242
+ fromAgent:"The assistant asked", fromDraft:"From the draft", fromProject:"From the project", blocks:"blocks", nothing:"nothing",
243
+ yourAnswer:"Your answer", other:"Another answer…", answer:"Answer", optionsSeen:"Options specgate saw",
244
+ toConfirm:"Answered, the spec still has to say it", inSpec:"The spec says it", noQuestions:"Nothing to decide right now.",
245
+ rTitle:"Review", rIntro:"Read it as someone on your team would. If something is not what you wanted, ask for the change in your words.",
246
+ checks:"Checks", allGood:"Ready to approve", approve:"Approve the draft", approveWhen:"It can be approved once nothing is pending.",
247
+ ask:"Ask for a change", askBtn:"Ask for this change", notes:"Changes asked", noDrafts:"No draft to review.",
248
+ given:"Given", when:"When", then:"Then", and:"And", waitsAnswer:"waits for an answer",
249
+ bTitle:"Build", bIntro:"The assistant codes each requirement, tests it and only saves it when everything passes. You can close the page: it goes on.",
250
+ whoCodes:"Who codes?", whoCodesP:"The assistants of this project. Your choice is remembered.", noEstimate:"No estimate yet", perRun:"per requirement",
251
+ readyN:"Ready to build", minutes:"min approx.", perReq:"per requirement", buildN:"Build the N", build1:"Build this one", noAgent:"The assistant is not configured yet (agent in harness.config.yaml).",
252
+ running:"Running", stP:"Codes", stT:"Tests", stQ:"Checks quality", stS:"Saves", watch:"Watch live", stop:"Stop after this one", stopAsked:"It will stop once this one ends.",
253
+ doneToday:"Finished", nothingReady:"Nothing is ready to build.", started:"Started: ", savedGit:"the specification was committed to git (N files)", saved:"Saved.", err:"Could not: ",
254
+ repTitle:"What is done", repIntro:"Each requirement was built and proven with the example that describes it. This report comes from the specification and the tests.",
255
+ download:"Download the report", print:"Print / PDF", rBuilt:"Built", rExamples:"Examples proven", rGate:"Quality", rGateOk:"Passes", rGateBad:"Fails", rCost:"Assistant cost",
256
+ byModule:"By module", colModule:"Module", colBuilt:"Built", colEx:"Examples", colState:"State", complete:"Complete", waitingN:"N wait for an answer", notBuilt:"Not built",
257
+ whatDoes:"What the app does", provedBy:"Proven with: ", missing:"What is missing", missingNone:"Nothing.", waitsFor:"waits for the answer to: ", decisions:"Decisions and assumptions",
258
+ decisionsP:"Q questions answered · A assumptions recorded.", nothingBuilt:"Nothing is built yet.",
259
+ qualTitle:"Code quality", qualIntro:"Checks the assistant must pass on every requirement, all in Docker, no account needed.",
260
+ qualNone:"No quality checks yet.", qualBasic:"Turn on the basic level", qualStrict:"Turn on the strict level",
261
+ toBasic:"Switch to basic", toStrict:"Switch to strict", gates:"gates", advises:"advises", notRunnable:"cannot run here",
262
+ dockerOn:"Docker answers", dockerOff:"Docker does not answer: start Docker Desktop", thresholds:"Thresholds: diff coverage C % · mutation M %",
263
+ stAttempt:"every attempt", stEnd:"at the end", stCi:"in CI",
264
+ q5:"5 · Which quality level?", qlBasic:"Basic (recommended)", qlBasicP:"Secrets, dependencies, tests that cover the change and static analysis. All in Docker.",
265
+ qlStrict:"Strict", qlStrictP:"Also: mutation tests, complexity and SAST. Slower, stricter.", qlNone:"None for now", qlNoneP:"It can be turned on later in Project.",
266
+ rQuality:"Code quality", rQualityP:"coverage C % · mutation M %", covShort:"coverage", mutShort:"mutation"}
267
+ };
268
+ function load(k, d){ try { return localStorage.getItem(k) || d; } catch (e) { return d; } }
269
+ function save(k, v){ try { localStorage.setItem(k, v); } catch (e) {} }
270
+ var STEPS = ["project", "brief", "drafts", "questions", "review", "build", "report"];
271
+ var lang = load("sg-app-lang", "es"), mode = load("sg-app-mode", "simple"), view = load("sg-app-view", "route");
272
+ if (view === "home") view = "route";
273
+ var profile = load("sg-app-profile", "");
274
+ var token = (document.querySelector('meta[name="specgate-token"]') || {}).content || "";
275
+ var cache = {}, ui = {briefTab:"form", module:"", draftCheck:""};
276
+ function t(k){ return (T[lang] && T[lang][k]) || T.es[k] || k; }
277
+ function fill(k, map){ var s = t(k); Object.keys(map).forEach(function(x){ s = s.split(x).join(String(map[x])); }); return s; }
278
+ function $(id){ return document.getElementById(id); }
279
+ function el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; }
280
+ function adv(){ return mode === "adv"; }
281
+ function cmd(text){ var c = el("code", "cmd", text); c.hidden = !adv(); c.setAttribute("data-adv", "1"); return c; }
282
+ function get(url){ return fetch(url).then(function(r){ return r.json(); }); }
283
+ function post(url, body){
284
+ return fetch(url, {method:"POST", headers:{"content-type":"application/json", "x-specgate-token":token}, body:JSON.stringify(body || {})})
285
+ .then(function(r){ return r.json(); });
286
+ }
287
+ function msgEl(){ return el("span", "msg"); }
288
+ function say(m, r){ m.className = "msg " + (r.ok ? "ok" : "err"); m.textContent = r.ok ? t("saved") : t("err") + (r.message || ""); }
289
+ function button(cls, text, on){ var b = el("button", cls, text); b.type = "button"; if (on) b.addEventListener("click", on); return b; }
290
+ function dot(color){ var d = el("span", "dot"); d.style.background = color; d.setAttribute("aria-hidden", "true"); return d; }
291
+ function cap(s){ s = String(s || ""); return s.charAt(0).toUpperCase() + s.slice(1); }
292
+ function flex(e, gap){ e.style.display = "flex"; e.style.flexWrap = "wrap"; e.style.alignItems = "center"; e.style.gap = (gap || 12) + "px"; return e; }
293
+ function intro(m, h, p){ var s = el("section"); s.style.display = "flex"; s.style.flexDirection = "column"; s.style.gap = "6px"; s.append(el("h1", "", h), el("p", "muted", p)); m.appendChild(s); }
294
+ var CHECK = "M5 12l5 5 9-10";
295
+ function num(state, n){
296
+ var s = el("span", "num " + state);
297
+ if (state === "done") {
298
+ var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "12"); svg.setAttribute("height", "12"); svg.setAttribute("viewBox", "0 0 24 24");
299
+ svg.setAttribute("fill", "none"); svg.setAttribute("stroke", "currentColor"); svg.setAttribute("stroke-width", "3"); svg.setAttribute("aria-hidden", "true");
300
+ var p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("d", CHECK); svg.appendChild(p); s.appendChild(svg);
301
+ } else s.textContent = String(n);
302
+ return s;
303
+ }
304
+ function stepOf(k){ var r = cache.route; return r ? r.steps.filter(function(s){ return s.key === k; })[0] : null; }
305
+
306
+ // ── The route's header: the seven steps (REQ-212) ──────────────────────
307
+ function header(){
308
+ var nav = $("nav"); nav.replaceChildren();
309
+ var r = cache.route;
310
+ var rb = button("", t("route"), function(){ go("route"); });
311
+ if (view === "route") rb.setAttribute("aria-current", "page");
312
+ rb.hidden = !!(r && !r.isProject);
313
+ nav.append(rb, el("span", "sep"));
314
+ STEPS.forEach(function(k, i){
315
+ var s = stepOf(k) || {state:"locked", counts:{}};
316
+ var b = button("", "", function(){ go(k); });
317
+ b.append(num(s.state, i + 1), document.createTextNode(t(k)));
318
+ if (view === k) b.setAttribute("aria-current", "page");
319
+ if (s.state === "locked" && k !== "project") b.disabled = true;
320
+ if (k === "questions" && s.counts.open) b.appendChild(el("span", "badge b-warn", String(s.counts.open)));
321
+ if (k === "review" && s.counts.open) b.appendChild(el("span", "badge b-info", String(s.counts.open)));
322
+ nav.appendChild(b);
323
+ });
324
+ $("mSimple").textContent = t("simple"); $("mAdv").textContent = t("adv");
325
+ $("mSimple").setAttribute("aria-pressed", String(!adv())); $("mAdv").setAttribute("aria-pressed", String(adv()));
326
+ $("lEs").setAttribute("aria-pressed", String(lang === "es")); $("lEn").setAttribute("aria-pressed", String(lang === "en"));
327
+ document.documentElement.lang = lang;
328
+ }
329
+
330
+ function summary(s){
331
+ var c = s.counts || {};
332
+ if (s.key === "project") return s.state === "done" ? t("s_project_done") : t("s_project_todo");
333
+ if (s.state === "locked") return s.key === "build" ? t("s_build_locked") : s.key === "report" ? t("s_report_locked") : t("s_locked");
334
+ if (s.key === "brief") return s.state === "done" ? fill("s_brief_done", {N:c.modules}) : c.modules ? fill("s_brief_fix", {N:c.modules}) : t("s_brief_open");
335
+ if (s.key === "drafts") return fill("s_drafts", {D:c.done, N:c.total});
336
+ if (s.key === "questions") return c.open ? fill("s_questions_open", {N:c.open}) : t("s_questions_done");
337
+ if (s.key === "review") return c.open ? fill("s_review_open", {N:c.open}) : t("s_review_done");
338
+ if (s.key === "build") return fill("s_build", {R:c.ready, B:c.built, T:c.total});
339
+ return fill("s_report", {B:c.built, T:c.total});
340
+ }
341
+
342
+ // ── Route (REQ-212) ─────────────────────────────────────────────────────
343
+ function viewRoute(r, h){
344
+ var m = $("view"); m.replaceChildren();
345
+ intro(m, t("routeTitle"), t("routeIntro"));
346
+ var n = el("section", "card next info"), tx = el("div", "t");
347
+ if (r.next) {
348
+ var at = STEPS.indexOf(r.next) + 1;
349
+ tx.append(el("span", "eyebrow", t("next") + " · " + t("stepN") + " " + at), el("h2", "", t("nx_" + r.next)), el("p", "muted", t("nxp_" + r.next)));
350
+ n.append(tx, button("go", t("go_" + r.next), function(){ go(r.next); }));
351
+ } else { tx.append(el("h2", "", t("allDone")), el("p", "muted", t("allDoneP"))); n.appendChild(tx); }
352
+ m.appendChild(n);
353
+ var list = el("ol", "rows"); list.style.listStyle = "none"; list.style.margin = "0"; list.style.padding = "0";
354
+ r.steps.forEach(function(s, i){
355
+ var li = el("li", "line step" + (s.state === "current" ? " here" : ""));
356
+ var g = el("div", "grow"); g.append(el("strong", "", (i + 1) + " · " + t(s.key)), el("span", "muted", summary(s)));
357
+ if (s.key === "drafts" && s.counts.total) {
358
+ var bar = el("div", "bar"); bar.style.maxWidth = "360px"; var a = el("i"); a.style.flex = String(s.counts.done); a.style.background = "var(--accent)";
359
+ var rest = el("i"); rest.style.flex = String(Math.max(0, s.counts.total - s.counts.done)); bar.append(a, rest); g.appendChild(bar);
360
+ }
361
+ li.append(num(s.state, i + 1), g);
362
+ if (s.state !== "locked") li.appendChild(button(s.state === "current" ? "go" : "ghost", t("open"), function(){ go(s.key); }));
363
+ list.appendChild(li);
364
+ });
365
+ m.appendChild(list);
366
+ m.appendChild(el("p", "small muted", t("repeat")));
367
+ if (h && h.modules && h.modules.length) {
368
+ var rows = el("section", "rows"), hd = el("div", "row"); hd.append(el("h2", "", t("modules"))); rows.appendChild(hd);
369
+ h.modules.forEach(function(g){
370
+ var row = el("div", "row"), bar = el("div", "bar");
371
+ var a = el("i"); a.style.flex = String(g.done); a.style.background = "var(--pass)";
372
+ var w = el("i"); w.style.flex = String(g.waiting); w.style.background = "#d4a72c";
373
+ var rest = el("i"); rest.style.flex = String(Math.max(0, g.total - g.done - g.waiting));
374
+ bar.append(a, w, rest);
375
+ var label = g.draft ? t("draft") : g.waiting ? g.done + " " + t("sOf") + " " + g.total + " · " + g.waiting + " " + t("qWait") : g.done + " " + t("sOf") + " " + g.total + (g.done === g.total ? " · " + t("ready") : "");
376
+ var lab = el("span", "small", label); lab.style.textAlign = "right"; lab.style.color = g.draft ? "var(--info)" : g.waiting ? "var(--warn)" : "var(--muted)";
377
+ row.append(el("b", "", cap(g.name)), bar, lab); rows.appendChild(row);
378
+ });
379
+ m.appendChild(rows);
380
+ }
381
+ m.appendChild(cmd("specgate status · specgate propose --from-brief · specgate questions · specgate harness run"));
382
+ }
383
+
384
+ // ── Project (REQ-213) ───────────────────────────────────────────────────
385
+ var np = {name:"", kind:"web", stack:"node-react", customStack:"", customTesting:"", agent:"", quality:"basic"};
386
+ function viewProject(r, pr){
387
+ var m = $("view"); m.replaceChildren(); m.className = "narrow";
388
+ if (r.isProject) {
389
+ intro(m, t("projReady"), t("projReadyP"));
390
+ var c = el("section", "rows");
391
+ if (!pr.profiles.length) c.appendChild(el("div", "empty", t("noProfiles")));
392
+ pr.profiles.forEach(function(p){
393
+ var l = el("div", "line"), g = el("div", "grow");
394
+ g.append(el("strong", "", p.name), cmd(p.agent));
395
+ l.appendChild(g);
396
+ if (p.default) l.appendChild(el("span", "pill b-soft", t("isDefault")));
397
+ l.appendChild(el("span", "pill " + (p.streams ? "b-pass" : "b-soft"), p.streams ? t("liveTok") : t("stepsOnly")));
398
+ c.appendChild(l);
399
+ });
400
+ m.appendChild(c);
401
+ var hint = el("div", "card"); hint.append(el("span", "small muted", t("profilesHint")), el("code", "cmd", ["profiles_version: 1", "profiles:", " my-agent:", " agent: " + String.fromCharCode(34) + "my-agent < {prompt_file}" + String.fromCharCode(34)].join(String.fromCharCode(10))));
402
+ m.appendChild(hint);
403
+ if (cache.quality) m.appendChild(qualityCard(cache.quality));
404
+ return;
405
+ }
406
+ if (!r.empty) {
407
+ intro(m, t("notEmpty"), t("notEmptyP"));
408
+ var cc = el("section", "card"); cc.appendChild(el("code", "cmd", "specgate init")); m.appendChild(cc);
409
+ return;
410
+ }
411
+ intro(m, t("newTitle"), t("newIntro"));
412
+ var f = el("form", "form"); f.addEventListener("submit", function(e){ e.preventDefault(); });
413
+ var f1 = el("div", "field"), name = el("input"); name.type = "text"; name.id = "np-name"; name.value = np.name;
414
+ name.addEventListener("input", function(){ np.name = name.value; });
415
+ var l1 = el("label", "", t("q1")); l1.htmlFor = "np-name"; f1.append(l1, name); f.appendChild(f1);
416
+ var f2 = el("div", "field"); f2.appendChild(el("span", "lab", t("q2")));
417
+ var ch = el("div", "choices");
418
+ [["web", "kWeb", "kWebP"], ["api", "kApi", "kApiP"], ["mobile", "kMob", "kMobP"]].forEach(function(k){
419
+ var l = el("label", "choice" + (np.kind === k[0] ? " on" : "")), rr = el("input"); rr.type = "radio"; rr.name = "np-kind"; rr.checked = np.kind === k[0];
420
+ rr.addEventListener("change", function(){ np.kind = k[0]; render(); });
421
+ var h = el("span", "h"); h.append(rr, document.createTextNode(t(k[1]))); l.append(h, el("span", "small muted", t(k[2]))); ch.appendChild(l);
422
+ });
423
+ f2.appendChild(ch); f.appendChild(f2);
424
+ var f3 = el("div", "field"), sel = el("select"); sel.id = "np-stack";
425
+ [["node-react", "stNode"], ["java-spring", "stJava"], ["python-fastapi", "stPy"], ["custom", "stCustom"]].forEach(function(o){ var op = el("option", "", t(o[1])); op.value = o[0]; op.selected = np.stack === o[0]; sel.appendChild(op); });
426
+ sel.addEventListener("change", function(){ np.stack = sel.value; render(); });
427
+ var l3 = el("label", "", t("q3")); l3.htmlFor = "np-stack"; f3.append(l3, sel, el("span", "small muted", t("stackHint")));
428
+ if (np.stack === "custom") {
429
+ var cs = el("input"); cs.type = "text"; cs.placeholder = t("customStack"); cs.value = np.customStack; cs.setAttribute("aria-label", t("customStack")); cs.addEventListener("input", function(){ np.customStack = cs.value; });
430
+ var ct = el("input"); ct.type = "text"; ct.placeholder = t("customTest"); ct.value = np.customTesting; ct.setAttribute("aria-label", t("customTest")); ct.addEventListener("input", function(){ np.customTesting = ct.value; });
431
+ f3.append(cs, ct);
432
+ }
433
+ f.appendChild(f3);
434
+ var f4 = el("div", "field"); f4.appendChild(el("span", "lab", t("q4")));
435
+ var inst = pr.installed || {};
436
+ if (!np.agent) np.agent = inst.claude ? "claude" : inst.codex ? "codex" : "none";
437
+ [["claude", "aClaude", inst.claude, true], ["codex", "aCodex", inst.codex, false], ["none", "aNone", null, false]].forEach(function(a){
438
+ var l = el("label", "choice" + (np.agent === a[0] ? " on" : "")), rr = el("input"); rr.type = "radio"; rr.name = "np-agent"; rr.checked = np.agent === a[0];
439
+ rr.addEventListener("change", function(){ np.agent = a[0]; render(); });
440
+ var h = el("span", "h"); h.append(rr, document.createTextNode(t(a[1])));
441
+ if (a[2] !== null) h.appendChild(el("span", "small muted", " · " + (a[2] ? t("found") : t("notFound"))));
442
+ l.appendChild(h);
443
+ if (a[0] !== "none") l.appendChild(el("span", "small " + (a[3] ? "" : "muted"), a[3] ? t("liveTok") : t("stepsOnly")));
444
+ f4.appendChild(l);
445
+ });
446
+ f.appendChild(f4);
447
+ var fq = el("div", "field"); fq.appendChild(el("span", "lab", t("q5")));
448
+ var qch = el("div", "choices");
449
+ [["basic", "qlBasic", "qlBasicP"], ["strict", "qlStrict", "qlStrictP"], ["none", "qlNone", "qlNoneP"]].forEach(function(k){
450
+ var l = el("label", "choice" + (np.quality === k[0] ? " on" : "")), rr = el("input"); rr.type = "radio"; rr.name = "np-quality"; rr.checked = np.quality === k[0];
451
+ rr.addEventListener("change", function(){ np.quality = k[0]; render(); });
452
+ var h = el("span", "h"); h.append(rr, document.createTextNode(t(k[1]))); l.append(h, el("span", "small muted", t(k[2]))); qch.appendChild(l);
453
+ });
454
+ fq.appendChild(qch); f.appendChild(fq);
455
+ var f5 = el("div", "field"); flex(f5); f5.style.justifyContent = "space-between";
456
+ var m0 = msgEl();
457
+ var b = button("go", t("create"), function(){
458
+ b.disabled = true; m0.className = "msg"; m0.textContent = t("creating");
459
+ post("api/project", {project:np}).then(function(res){
460
+ b.disabled = false; say(m0, res);
461
+ if (res.ok) { cache.route = null; go("brief"); }
462
+ });
463
+ });
464
+ f5.append(el("span", "small muted", t("createP")), b);
465
+ f.appendChild(f5);
466
+ m.append(f, m0, cmd("specgate init --new · specgate harness init · specgate propose --from-brief --new-brief"));
467
+ }
468
+
469
+ // ── Quality: the checks configured, how each runs here (REQ-229) ──────────
470
+ function qualityCard(q){
471
+ var c = el("section", "card");
472
+ c.append(el("h2", "", t("qualTitle")), el("span", "small muted", t("qualIntro")));
473
+ var dk = el("span", "small"); dk.textContent = q.docker ? t("dockerOn") : t("dockerOff"); dk.style.color = q.docker ? "var(--pass)" : "var(--fail)";
474
+ c.appendChild(dk);
475
+ if (!q.checks.length) c.appendChild(el("span", "muted", t("qualNone")));
476
+ else {
477
+ var level = el("div"); flex(level, 8);
478
+ if (q.level) level.appendChild(el("span", "pill b-info", q.level));
479
+ level.appendChild(el("span", "small muted", fill("thresholds", {C: q.diffCoverage === null ? "—" : q.diffCoverage, M: q.mutationScore === null ? "—" : q.mutationScore})));
480
+ c.appendChild(level);
481
+ var rows = el("div", "rows");
482
+ q.checks.forEach(function(k){
483
+ var l = el("div", "line"), g = el("div", "grow");
484
+ g.append(el("strong", "", k.name), el("span", "small muted", ({attempt:t("stAttempt"), end:t("stEnd"), ci:t("stCi")})[k.when] + " · " + (k.gate ? t("gates") : t("advises"))));
485
+ l.appendChild(g);
486
+ if (k.unavailable) { var u = el("span", "small", t("notRunnable")); u.style.color = "var(--fail)"; u.title = k.unavailable; l.appendChild(u); }
487
+ else l.appendChild(el("span", "pill b-soft", k.runner));
488
+ rows.appendChild(l);
489
+ });
490
+ c.appendChild(rows);
491
+ }
492
+ var acts = el("div"); flex(acts); var m0 = msgEl();
493
+ var level0 = q.checks.length ? q.level : "";
494
+ if (level0 !== "basic") acts.appendChild(button(level0 ? "ghost" : "go", level0 ? t("toBasic") : t("qualBasic"), function(){ post("api/quality/init", {level:"basic"}).then(function(r){ say(m0, r); if (r.ok) { cache.quality = r.view; render(); } }); }));
495
+ if (level0 !== "strict") acts.appendChild(button("ghost", level0 ? t("toStrict") : t("qualStrict"), function(){ post("api/quality/init", {level:"strict"}).then(function(r){ say(m0, r); if (r.ok) { cache.quality = r.view; render(); } }); }));
496
+ acts.appendChild(m0); c.append(acts, cmd("specgate quality init --level basic|strict · specgate quality doctor --pull · specgate quality baseline"));
497
+ return c;
498
+ }
499
+
500
+ // ── Profiles: who works, picked once for Brief, Drafts and Build (REQ-211) ──
501
+ function profileSelect(pr){
502
+ var wrap = el("label", "small muted"); flex(wrap, 6); wrap.appendChild(document.createTextNode(t("assistant")));
503
+ var s = el("select");
504
+ (pr.profiles || []).forEach(function(p){
505
+ var o = el("option", "", p.name + (p.default ? " · " + t("isDefault") : "")); o.value = p.name;
506
+ o.selected = profile ? profile === p.name : p.default; s.appendChild(o);
507
+ });
508
+ s.addEventListener("change", function(){ profile = s.value; save("sg-app-profile", profile); render(); });
509
+ if (!(pr.profiles || []).length) return el("span");
510
+ wrap.appendChild(s); return wrap;
511
+ }
512
+ function chosenProfile(pr){
513
+ var names = (pr.profiles || []).map(function(p){ return p.name; });
514
+ if (profile && names.indexOf(profile) >= 0) return profile;
515
+ var d = (pr.profiles || []).filter(function(p){ return p.default; })[0];
516
+ return d ? d.name : "";
517
+ }
518
+
519
+ // ── Brief (REQ-214, REQ-215) ────────────────────────────────────────────
520
+ var saveTimer = null;
521
+ function saveForm(form, status){
522
+ if (saveTimer) clearTimeout(saveTimer);
523
+ status.className = "small muted"; status.textContent = t("saving");
524
+ saveTimer = setTimeout(function(){
525
+ post("api/brief", {form:{product:form.product, actors:form.actors, modules:form.modules}}).then(function(res){
526
+ if (res.ok) { cache.brief.findings = res.brief.findings; cache.brief.markdown = res.brief.markdown; status.textContent = t("savedAt"); renderFindings(); }
527
+ else { status.className = "msg err"; status.textContent = t("err") + (res.message || ""); }
528
+ });
529
+ }, 700);
530
+ }
531
+ var findingsBox = null, nextBtn = null;
532
+ function renderFindings(){
533
+ if (!findingsBox || !cache.brief) return;
534
+ findingsBox.replaceChildren();
535
+ // A draft is not opened over a brief with errors: the button says so first.
536
+ if (nextBtn) nextBtn.disabled = cache.brief.findings.some(function(f){ return f.severity === "error"; });
537
+ var shown = cache.brief.findings.filter(function(f){ return f.severity !== "info"; });
538
+ if (!shown.length) { var ok = el("div", "check"); ok.append(dot("var(--pass)"), el("span", "", t("briefOk"))); findingsBox.appendChild(ok); }
539
+ shown.forEach(function(f){ var x = el("div", "check"); x.append(dot(f.severity === "error" ? "var(--fail)" : "#d4a72c"), el("span", "small", f.message + (f.fix && adv() ? " — " + f.fix : ""))); findingsBox.appendChild(x); });
540
+ }
541
+ function pasteCard(b, pr, exists){
542
+ var c = el("section", "card"), h = el("div"); flex(h);
543
+ var g = el("div"); g.style.flex = "999 1 380px"; g.style.display = "flex"; g.style.flexDirection = "column"; g.style.gap = "2px";
544
+ g.append(el("strong", "", t("paste")), el("span", "muted", t("pasteP")));
545
+ h.append(g, profileSelect(pr)); c.appendChild(h);
546
+ var ta = el("textarea"); ta.rows = exists ? 3 : 8; ta.setAttribute("aria-label", t("paste")); c.appendChild(ta);
547
+ var row = el("div"); flex(row);
548
+ var rep = null;
549
+ if (exists) { var rl = el("label", "small"); rep = el("input"); rep.type = "checkbox"; rl.append(rep, document.createTextNode(" " + t("replace"))); row.appendChild(rl); }
550
+ var m1 = msgEl();
551
+ var btn = button(exists ? "ghost" : "go", t("pasteBtn"), function(){
552
+ if (!ta.value.trim()) return;
553
+ post("api/brief/fill", {text:ta.value, profile:chosenProfile(pr), replace: rep ? rep.checked : false}).then(function(res){ say(m1, res); if (res.ok) refresh(); });
554
+ });
555
+ btn.disabled = !(pr.profiles || []).length;
556
+ row.append(btn, m1); c.append(row, cmd("specgate propose --from-brief --profile " + chosenProfile(pr)));
557
+ return c;
558
+ }
559
+ function listEditor(box, form, mod, kind, label, hint, addLabel, status){
560
+ var sec = el("div"); sec.style.display = "flex"; sec.style.flexDirection = "column"; sec.style.gap = "6px";
561
+ var hd = el("span"); hd.append(el("strong", "", label), el("span", "muted", " · " + hint)); sec.appendChild(hd);
562
+ mod[kind].forEach(function(text, i){
563
+ var it = el("div", "item"), inp = el("input"); inp.type = "text"; inp.value = text; inp.setAttribute("aria-label", label + " " + (i + 1));
564
+ inp.addEventListener("input", function(){ mod[kind][i] = inp.value; saveForm(form, status); });
565
+ it.append(inp, button("x", "×", function(){ mod[kind].splice(i, 1); saveForm(form, status); render(); }));
566
+ it.lastChild.setAttribute("aria-label", t("remove"));
567
+ sec.appendChild(it);
568
+ });
569
+ sec.appendChild(button("link", addLabel, function(){
570
+ mod[kind].push(""); render();
571
+ var inputs = document.querySelectorAll(".cards input"); if (inputs.length) inputs[inputs.length - 1].focus();
572
+ }));
573
+ box.appendChild(sec);
574
+ }
575
+ function viewBrief(b, pr, job){
576
+ var m = $("view"); m.replaceChildren();
577
+ intro(m, t("bTitleBrief"), t("bIntroBrief"));
578
+ if (job && job.kind === "brief") {
579
+ var w = el("section", "card warn"), r0 = el("div"); flex(r0);
580
+ r0.append(el("span", "pill b-warn", t("running")), el("strong", "", t("reading")), button("ghost", t("stopIt"), function(){ post("api/draft/stop", {}).then(refresh); }));
581
+ w.appendChild(r0); if (cache.modules && cache.modules.step) w.appendChild(el("p", "small muted", cache.modules.step));
582
+ m.appendChild(w); return;
583
+ }
584
+ if (!b.exists) {
585
+ m.appendChild(pasteCard(b, pr, false));
586
+ var sc = el("section", "card"); sc.append(el("strong", "", t("fromScratch")), el("span", "muted", t("fromScratchP")));
587
+ sc.appendChild(button("ghost", t("fromScratch"), function(){
588
+ post("api/brief", {form:{product:"", actors:[{name:"", surfaces:["web"]}], modules:[]}}).then(function(){ refresh(); });
589
+ }));
590
+ m.appendChild(sc); return;
591
+ }
592
+ m.appendChild(pasteCard(b, pr, true));
593
+ if (adv()) {
594
+ var tabs = el("div", "chips");
595
+ [["form", "formTab"], ["md", "mdTab"]].forEach(function(x){ var c = button("chip", t(x[1]), function(){ ui.briefTab = x[0]; render(); }); c.setAttribute("aria-selected", String(ui.briefTab === x[0])); tabs.appendChild(c); });
596
+ m.appendChild(tabs);
597
+ }
598
+ var split = el("div", "split"), main = el("div", "main"), aside = el("aside");
599
+ var status = el("span", "small muted", "");
600
+ if (adv() && ui.briefTab === "md") {
601
+ var mdc = el("section", "card"), ta = el("textarea"); ta.rows = 28; ta.value = b.markdown; ta.style.fontFamily = "var(--mono)"; ta.style.fontSize = "12.5px"; ta.setAttribute("aria-label", "docs/brief.md");
602
+ var m2 = msgEl();
603
+ mdc.append(el("code", "cmd", "docs/brief.md"), ta, button("go", t("saveMd"), function(){
604
+ post("api/brief", {markdown:ta.value}).then(function(res){ say(m2, res); if (res.ok) { cache.brief = res.brief; renderFindings(); } });
605
+ }), m2);
606
+ main.appendChild(mdc);
607
+ } else {
608
+ var form = b, f = el("form", "form"); f.addEventListener("submit", function(e){ e.preventDefault(); });
609
+ var p1 = el("div", "field"), pi = el("input"); pi.type = "text"; pi.id = "bf-product"; pi.value = form.product;
610
+ pi.addEventListener("input", function(){ form.product = pi.value; saveForm(form, status); });
611
+ var lp = el("label", "", t("product")); lp.htmlFor = "bf-product"; p1.append(lp, pi); f.appendChild(p1);
612
+ var p2 = el("div", "field"); p2.appendChild(el("span", "lab", t("who")));
613
+ if (!form.actors.length) form.actors.push({name:"", surfaces:["web"]});
614
+ form.actors.forEach(function(a, i){
615
+ var row = el("div", "opt"); flex(row, 10);
616
+ var n = el("input"); n.type = "text"; n.value = a.name; n.style.flex = "999 1 160px"; n.style.width = "auto"; n.setAttribute("aria-label", t("actorName"));
617
+ n.addEventListener("input", function(){ a.name = n.value; saveForm(form, status); });
618
+ row.appendChild(n);
619
+ ["web", "api", "mobile"].forEach(function(sf){
620
+ var l = el("label", "small"), cb = el("input"); cb.type = "checkbox"; cb.checked = a.surfaces.indexOf(sf) >= 0;
621
+ cb.addEventListener("change", function(){ if (cb.checked) a.surfaces.push(sf); else a.surfaces = a.surfaces.filter(function(x){ return x !== sf; }); saveForm(form, status); });
622
+ l.append(cb, document.createTextNode(" " + (sf === "api" ? "API" : cap(sf)))); row.appendChild(l);
623
+ });
624
+ var x = button("x", "×", function(){ form.actors.splice(i, 1); saveForm(form, status); render(); }); x.setAttribute("aria-label", t("remove"));
625
+ row.appendChild(x); p2.appendChild(row);
626
+ });
627
+ p2.appendChild(button("link", t("addActor"), function(){ form.actors.push({name:"", surfaces:["web"]}); render(); }));
628
+ f.appendChild(p2);
629
+ var p3 = el("div", "field"); p3.appendChild(el("span", "lab", t("modsTitle")));
630
+ var chips = el("div", "chips"); chips.setAttribute("role", "tablist");
631
+ if (!ui.module || !form.modules.some(function(x){ return x.key === ui.module; })) ui.module = form.modules.length ? form.modules[0].key : "";
632
+ form.modules.forEach(function(mo){
633
+ var c = button("chip", cap(mo.name || mo.key), function(){ ui.module = mo.key; render(); }); c.setAttribute("role", "tab"); c.setAttribute("aria-selected", String(ui.module === mo.key));
634
+ var issue = b.findings.some(function(fd){ return fd.severity !== "info" && fd.message.indexOf(mo.key) >= 0; });
635
+ if (issue) c.appendChild(el("span", "", " · !"));
636
+ chips.appendChild(c);
637
+ });
638
+ var addWrap = el("span"); flex(addWrap, 6);
639
+ var newName = el("input"); newName.type = "text"; newName.placeholder = t("moduleName"); newName.style.width = "180px"; newName.setAttribute("aria-label", t("moduleName"));
640
+ addWrap.append(newName, button("chip", t("addModule"), function(){
641
+ var nm = newName.value.trim(); if (!nm) { newName.focus(); return; }
642
+ var key = nm.toLowerCase().normalize("NFD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
643
+ if (!key) return;
644
+ form.modules.push({key:key, name:nm, prose:"", rules:[""], examples:[], questions:[]}); ui.module = key; saveForm(form, status); render();
645
+ }));
646
+ chips.appendChild(addWrap);
647
+ p3.appendChild(chips);
648
+ var mod = form.modules.filter(function(x){ return x.key === ui.module; })[0];
649
+ if (mod) {
650
+ var box = el("div", "cards");
651
+ listEditor(box, form, mod, "rules", t("rules"), t("rulesP"), t("addRule"), status);
652
+ listEditor(box, form, mod, "examples", t("examples"), t("examplesP"), t("addExample"), status);
653
+ listEditor(box, form, mod, "questions", t("doubts"), t("doubtsP"), t("addDoubt"), status);
654
+ var rm = button("link danger", t("removeModule"), function(){ form.modules = form.modules.filter(function(x){ return x !== mod; }); ui.module = ""; saveForm(form, status); render(); });
655
+ box.appendChild(rm);
656
+ p3.appendChild(box);
657
+ }
658
+ f.appendChild(p3);
659
+ main.appendChild(f);
660
+ }
661
+ var ch = el("section", "card"); ch.append(el("strong", "", t("checksB")), el("span", "small muted", t("checksBP")));
662
+ findingsBox = el("div"); findingsBox.style.display = "flex"; findingsBox.style.flexDirection = "column"; findingsBox.style.gap = "8px";
663
+ ch.appendChild(findingsBox); aside.appendChild(ch);
664
+ nextBtn = button("go", t("toDrafts"), function(){ go("drafts"); });
665
+ var nx = el("section", "card"); nx.append(status, nextBtn, cmd("specgate propose --from-brief --check"));
666
+ aside.appendChild(nx);
667
+ split.append(main, aside); m.appendChild(split);
668
+ renderFindings();
669
+ }
670
+
671
+ // ── Drafts (REQ-216) ────────────────────────────────────────────────────
672
+ function viewDrafts(d, pr){
673
+ var m = $("view"); m.replaceChildren();
674
+ var top = el("section"); flex(top); top.style.alignItems = "flex-end"; top.style.justifyContent = "space-between";
675
+ var g = el("div"); g.style.flex = "999 1 480px"; g.style.display = "flex"; g.style.flexDirection = "column"; g.style.gap = "6px";
676
+ g.append(el("h1", "", t("dTitle")), el("p", "muted", t("dIntro"))); top.append(g, profileSelect(pr)); m.appendChild(top);
677
+ if (!d.brief) { m.appendChild(el("div", "card empty", t("noBrief"))); return; }
678
+ var drafted = d.modules.filter(function(x){ return x.stage !== "todo" && x.stage !== "stopped" && x.stage !== "drafting"; }).length;
679
+ var sec = el("section", "rows"), hd = el("div", "line"); hd.style.justifyContent = "space-between";
680
+ hd.appendChild(el("h2", "", fill("nOfM", {N:d.modules.length, D:drafted})));
681
+ var nextMod = d.modules.filter(function(x){ return x.stage === "todo" || x.stage === "stopped"; })[0];
682
+ var m0 = msgEl();
683
+ function start(key){ post("api/draft", {module:key, profile:chosenProfile(pr)}).then(function(res){ m0.className = "msg " + (res.ok ? "ok" : "err"); m0.textContent = res.ok ? "" : t("err") + res.message; refresh(); }); }
684
+ if (nextMod) { var nb = button("go", t("draftNext"), function(){ start(nextMod.key); }); nb.disabled = !!d.job; hd.appendChild(nb); }
685
+ sec.appendChild(hd);
686
+ d.modules.forEach(function(x){
687
+ var line = el("div", "line" + (x.stage === "drafting" ? " here" : ""));
688
+ var name = el("span", "name", cap(x.key)), state = el("div", "grow");
689
+ if (x.stage === "todo") state.appendChild(el("span", "muted", t("stTodo")));
690
+ else if (x.stage === "stopped") state.appendChild(el("span", "", t("stStopped")));
691
+ else if (x.stage === "drafting") {
692
+ state.appendChild(el("strong", "", t("stDrafting") + " " + Math.max(1, Math.round(x.progress.seconds / 60)) + " min"));
693
+ var ol = el("ol", "progress"), p = x.progress;
694
+ var items = [[t("pRead"), true], [t("pReqs") + (p.requirements ? " (" + p.requirements + ")" : ""), p.requirements > 0], [t("pScn") + (p.scenarios ? " (" + p.scenarios + ")" : ""), p.scenarios > 0], [t("pNotes"), p.notes], [t("pCheck"), false]];
695
+ var nowAt = -1; items.forEach(function(it, i){ if (nowAt < 0 && !it[1]) nowAt = i; });
696
+ items.forEach(function(it, i){ ol.appendChild(el("li", it[1] ? "ok" : i === nowAt ? "now" : "todo", (it[1] ? "✓ " : i === nowAt ? "● " : "○ ") + it[0])); });
697
+ state.appendChild(ol);
698
+ if (p.lastStep) state.appendChild(el("span", "small muted", p.lastStep));
699
+ } else if (x.stage === "review") state.appendChild(el("span", "", fill("stReview", {N:x.requirements, Q:x.questions})));
700
+ else state.appendChild(el("span", "muted", x.requirements + " " + t("reqs")));
701
+ line.append(name, state);
702
+ if (x.stage === "todo" || x.stage === "stopped") { var b0 = button("ghost", x.stage === "stopped" ? t("retry") : t("draftIt"), function(){ start(x.key); }); b0.disabled = !!d.job; line.appendChild(b0); }
703
+ else if (x.stage === "drafting") line.appendChild(button("ghost danger", t("stopIt"), function(){ post("api/draft/stop", {}).then(refresh); }));
704
+ else if (x.stage === "review") line.appendChild(button("ghost", t("review"), function(){ go("review"); }));
705
+ else if (x.stage === "approved") line.appendChild(el("span", "pill b-pass", t("stApproved")));
706
+ else line.appendChild(el("span", "pill b-soft", t("stArchived")));
707
+ sec.appendChild(line);
708
+ });
709
+ sec.appendChild(m0);
710
+ m.appendChild(sec);
711
+ d.modules.filter(function(x){ return x.stage === "review" && x.findings.length; }).forEach(function(x){
712
+ var c = el("section", "card"), h = el("div"); flex(h); h.style.justifyContent = "space-between";
713
+ h.append(el("h2", "", fill("checkOf", {M:"«" + cap(x.key) + "»"})), el("span", "pill b-warn", fill("toFix", {N:x.findings.length})));
714
+ c.appendChild(h);
715
+ x.findings.forEach(function(f){ var r = el("div", "check"); r.append(dot(f.severity === "error" ? "var(--fail)" : "#d4a72c"), el("span", "small", f.message)); c.appendChild(r); });
716
+ m.appendChild(c);
717
+ });
718
+ if (adv() && d.lastJob && d.lastJob.tail) { var lg = el("div", "log"); lg.textContent = t("lastRun") + " · " + (d.lastJob.module || d.lastJob.kind) + String.fromCharCode(10) + d.lastJob.tail; m.appendChild(lg); }
719
+ m.appendChild(cmd("specgate propose --from-brief " + (nextMod ? nextMod.key : "<module>") + " --profile " + chosenProfile(pr)));
720
+ }
721
+
722
+ // ── Questions (REQ-210) ─────────────────────────────────────────────────
723
+ function viewQuestions(q){
724
+ var m = $("view"); m.replaceChildren();
725
+ intro(m, t("qTitle"), t("qIntro"));
726
+ if (!q.open.length && !q.toReview.length) { m.appendChild(el("div", "card empty", t("noQuestions"))); return; }
727
+ q.open.forEach(function(x){
728
+ var c = el("article", "card" + (x.from === "agent" ? " warn" : ""));
729
+ var meta = el("div", "small muted");
730
+ var pill = el("span", "pill " + (x.from === "agent" ? "b-warn" : "b-info"), x.from === "agent" ? t("fromAgent") : x.from === "project" ? t("fromProject") : t("fromDraft"));
731
+ meta.append(pill, document.createTextNode(" " + (x.change === "project" ? "" : x.change.replace("draft-", "") + " · ") + t("blocks") + " " + (x.blocks.length ? x.blocks.join(", ") : t("nothing"))));
732
+ c.append(meta, el("h2", "", x.text));
733
+ var ta = el("textarea"); ta.rows = 3; ta.setAttribute("aria-label", t("yourAnswer"));
734
+ var chosen = { v: "" };
735
+ if (x.options.length) {
736
+ var fs0 = el("fieldset"); fs0.style.border = "0"; fs0.style.padding = "0"; fs0.style.margin = "0"; fs0.style.display = "flex"; fs0.style.flexDirection = "column"; fs0.style.gap = "8px";
737
+ fs0.appendChild(el("legend", "small muted", t("optionsSeen")));
738
+ x.options.concat([t("other")]).forEach(function(o, i){
739
+ var l = el("label", "opt"), r = el("input"); r.type = "radio"; r.name = "o-" + x.change + "-" + x.id;
740
+ r.addEventListener("change", function(){ chosen.v = i < x.options.length ? o : ""; ta.hidden = i < x.options.length; });
741
+ l.append(r, document.createTextNode(o)); fs0.appendChild(l);
742
+ });
743
+ c.appendChild(fs0); ta.hidden = true;
744
+ } else c.appendChild(el("label", "small muted", t("yourAnswer")));
745
+ c.appendChild(ta);
746
+ var row = el("div"); flex(row);
747
+ var m0 = msgEl();
748
+ row.append(button("go", t("answer"), function(){
749
+ var text = chosen.v || ta.value.trim(); if (!text) return;
750
+ post("api/answer", {question:x.id, change:x.change === "project" ? undefined : x.change, answer:text}).then(function(r){ say(m0, r); if (r.ok) refresh(); });
751
+ }), m0);
752
+ c.append(row, cmd("specgate answer " + x.id + " " + String.fromCharCode(34) + "…" + String.fromCharCode(34) + (x.change === "project" ? "" : " --change " + x.change)));
753
+ m.appendChild(c);
754
+ });
755
+ if (q.toReview.length) {
756
+ var s = el("section", "card"); s.appendChild(el("h2", "", t("toConfirm")));
757
+ q.toReview.forEach(function(x){
758
+ var r = el("div"); flex(r);
759
+ var m1 = msgEl();
760
+ r.append(el("span", "", (x.change === "project" ? "" : x.change.replace("draft-", "") + " · ") + (x.text || x.question)), button("ghost", t("inSpec"), function(){
761
+ post("api/answer", {question:x.question, change:x.change === "project" ? undefined : x.change, reviewed:true}).then(function(res){ say(m1, res); if (res.ok) refresh(); });
762
+ }), m1);
763
+ s.append(r, cmd("specgate answer " + x.question + " --reviewed"));
764
+ });
765
+ m.appendChild(s);
766
+ }
767
+ }
768
+
769
+ // ── Review (REQ-206, REQ-207) ───────────────────────────────────────────
770
+ function kw(step){
771
+ var sp = step.indexOf(" "), k = (sp > 0 ? step.slice(0, sp) : step).toUpperCase(), rest = sp > 0 ? step.slice(sp + 1) : "";
772
+ var map = {GIVEN:"given", WHEN:"when", THEN:"then", AND:"and", DADO:"given", CUANDO:"when", ENTONCES:"then", Y:"and"};
773
+ var line = el("span"); line.append(el("b", "kw", map[k] ? t(map[k]) : k), document.createTextNode(" " + rest)); return line;
774
+ }
775
+ function viewReview(list){
776
+ var m = $("view"); m.replaceChildren();
777
+ intro(m, t("rTitle"), t("rIntro"));
778
+ if (!list.length) { m.appendChild(el("div", "card empty", t("noDrafts"))); return; }
779
+ list.forEach(function(d){
780
+ var split = el("section", "split"), main = el("div", "main"), aside = el("aside");
781
+ main.appendChild(el("h2", "", cap(d.module) + " · " + d.requirements.length + " " + t("reqs")));
782
+ d.requirements.forEach(function(r){
783
+ var waits = d.questions.filter(function(q){ return q.blocks.indexOf(r.id) >= 0; });
784
+ var c = el("article", "card" + (waits.length ? " warn" : ""));
785
+ var h = el("div"); h.style.display = "flex"; h.style.justifyContent = "space-between"; h.style.gap = "12px";
786
+ h.append(el("h2", "", r.title), el("span", "small muted", waits.length ? t("waitsAnswer") : r.id)); c.appendChild(h);
787
+ if (r.statement) c.appendChild(el("p", "", r.statement));
788
+ r.scenarios.forEach(function(s){ var st = el("div", "steps"); s.steps.forEach(function(x){ st.appendChild(kw(x)); }); c.appendChild(st); });
789
+ var det = el("details"), sum = el("summary", "small", t("ask")); det.appendChild(sum);
790
+ var ta = el("textarea"); ta.rows = 2; ta.setAttribute("aria-label", t("ask")); var m2 = msgEl();
791
+ det.append(ta, button("ghost", t("askBtn"), function(){
792
+ if (!ta.value.trim()) return;
793
+ post("api/note", {change:d.change, req:r.id, text:ta.value}).then(function(res){ say(m2, res); if (res.ok) refresh(); });
794
+ }), m2);
795
+ c.append(det, cmd(d.change + " · " + r.id + " · specgate change note " + d.change + " … --req " + r.id));
796
+ main.appendChild(c);
797
+ });
798
+ var ch = el("section", "card"); ch.appendChild(el("h2", "", t("checks")));
799
+ if (!d.blockers.length) { var ok = el("div", "check"); ok.append(dot("var(--pass)"), el("span", "", t("allGood"))); ch.appendChild(ok); }
800
+ d.blockers.forEach(function(b){ var x = el("div", "check"); x.append(dot("#d4a72c"), el("span", "small", b.message)); ch.appendChild(x); });
801
+ aside.appendChild(ch);
802
+ var act = el("section", "card"), m3 = msgEl();
803
+ var ap = button("go", t("approve"), function(){ post("api/approve", {change:d.change}).then(function(res){ say(m3, res); if (res.ok) refresh(); }); });
804
+ ap.disabled = d.blockers.length > 0;
805
+ act.append(ap);
806
+ if (d.blockers.length) act.appendChild(el("span", "small muted", t("approveWhen")));
807
+ act.append(m3, cmd("specgate change approve " + d.change));
808
+ aside.appendChild(act);
809
+ if (d.notes.length) {
810
+ var nt = el("section", "card"); nt.appendChild(el("h2", "", t("notes")));
811
+ d.notes.forEach(function(n){ nt.appendChild(el("div", "small", (n.requirement ? n.requirement + " · " : "") + n.text)); });
812
+ aside.appendChild(nt);
813
+ }
814
+ split.append(main, aside); m.appendChild(split);
815
+ });
816
+ }
817
+
818
+ // ── Build (REQ-208, REQ-209, REQ-211) ───────────────────────────────────
819
+ var picked = {};
820
+ function stageOf(r){
821
+ var gates = r.steps.filter(function(e){ return e.type === "gate" || e.type === "merge"; });
822
+ if (r.steps.some(function(e){ return e.type === "merge"; })) return 3;
823
+ if (gates.some(function(e){ return String(e.stage || "").indexOf("architecture") >= 0; })) return 2;
824
+ if (gates.length) return 1;
825
+ return 0;
826
+ }
827
+ function viewBuild(b, live, pr){
828
+ var m = $("view"); m.replaceChildren();
829
+ intro(m, t("bTitle"), t("bIntro"));
830
+ var cur = (live.requirements || []).filter(function(r){ return r.state === "running"; })[0];
831
+ if (cur) {
832
+ var rc = el("section", "card warn");
833
+ var h = el("div"); flex(h, 10);
834
+ h.append(el("span", "pill b-warn", t("running")), el("b", "", cur.title || cur.req), el("span", "small muted", Math.round(Math.max(0, Date.now() / 1000 - cur.started) / 60) + " min"));
835
+ rc.appendChild(h);
836
+ var stg = el("div", "stages"), at = stageOf(cur);
837
+ [t("stP"), t("stT"), t("stQ"), t("stS")].forEach(function(name, i){
838
+ var d = el("div"), bar = el("i"); if (i < at) bar.style.background = "var(--pass)"; if (i === at) bar.className = "run";
839
+ d.append(bar, el("span", i === at ? "" : "muted", name)); stg.appendChild(d);
840
+ });
841
+ rc.appendChild(stg);
842
+ var said = cur.steps.filter(function(e){ return e.type === "message"; })[0];
843
+ if (said) { var p0 = el("p", "small", String(said.text || "")); p0.style.padding = "10px 12px"; p0.style.background = "var(--soft)"; p0.style.borderRadius = "6px"; rc.appendChild(p0); }
844
+ var acts = el("div"); flex(acts);
845
+ var lk = el("a", "", t("watch")); lk.href = "live";
846
+ var m4 = msgEl();
847
+ acts.append(lk, button("ghost", t("stop"), function(){ post("api/stop", {}).then(function(res){ m4.className = "msg " + (res.ok ? "ok" : "err"); m4.textContent = res.ok ? t("stopAsked") : t("err") + res.message; }); }), m4);
848
+ rc.appendChild(acts);
849
+ if (adv()) {
850
+ var lg = el("div", "log"); lg.setAttribute("data-adv", "1");
851
+ cur.steps.slice(0, 8).forEach(function(e){ lg.appendChild(el("div", "", (e.kind || e.type) + " " + String(e.target || e.stage || e.text || ""))); });
852
+ if (cur.agent) lg.appendChild(el("div", "", cur.agent.model + (cur.usage ? " · " + Math.round(cur.usage.context / 1000) + "k / " + Math.round(cur.usage.window / 1000) + "k" : "")));
853
+ rc.appendChild(lg);
854
+ }
855
+ m.appendChild(rc);
856
+ }
857
+ var who = chosenProfile(pr);
858
+ if ((pr.profiles || []).length) {
859
+ var ps = el("section", "card");
860
+ ps.append(el("h2", "", t("whoCodes")), el("span", "small muted", t("whoCodesP")));
861
+ var grid = el("div", "choices"); grid.setAttribute("role", "radiogroup"); grid.setAttribute("aria-label", t("whoCodes"));
862
+ pr.profiles.forEach(function(p){
863
+ var l = el("label", "choice" + (who === p.name ? " on" : "")), rr = el("input"); rr.type = "radio"; rr.name = "profile"; rr.checked = who === p.name;
864
+ rr.addEventListener("change", function(){ profile = p.name; save("sg-app-profile", profile); render(); });
865
+ var hh = el("span", "h"); hh.append(rr, document.createTextNode(p.name)); l.appendChild(hh);
866
+ var cost = p.costHint !== null ? "≈ $" + Number(p.costHint).toFixed(2) + " " + t("perRun") : (p.default && b.estimate.costPerRequirement !== null ? "≈ $" + b.estimate.costPerRequirement.toFixed(2) + " " + t("perRun") : t("noEstimate"));
867
+ l.appendChild(el("span", "small muted", cost));
868
+ var tags = el("span"); flex(tags, 6);
869
+ if (p.default) tags.appendChild(el("span", "pill b-soft", t("isDefault")));
870
+ tags.appendChild(el("span", "pill " + (p.streams ? "b-pass" : "b-soft"), p.streams ? t("liveTok") : t("stepsOnly")));
871
+ l.appendChild(tags); l.appendChild(cmd(p.agent));
872
+ grid.appendChild(l);
873
+ });
874
+ ps.appendChild(grid); m.appendChild(ps);
875
+ }
876
+ var sec = el("section", "rows");
877
+ var hd = el("div"); flex(hd); hd.style.justifyContent = "space-between"; hd.style.padding = "16px 20px"; hd.style.borderBottom = "1px solid var(--line)";
878
+ var info = el("div"); info.append(el("h2", "", t("readyN") + " · " + b.ready.length));
879
+ var est = [];
880
+ if (b.estimate.minutes) est.push(b.estimate.minutes + " " + t("minutes"));
881
+ if (b.estimate.costPerRequirement !== null) est.push("$" + b.estimate.costPerRequirement.toFixed(2) + " " + t("perReq"));
882
+ if (est.length) info.appendChild(el("span", "small muted", est.join(" · ")));
883
+ var m5 = msgEl();
884
+ var chosen = function(){ return b.ready.filter(function(r){ return picked[r.id] !== false; }).map(function(r){ return r.id; }); };
885
+ var label = function(){ var n = chosen().length; return n === 1 ? t("build1") : t("buildN").split("N").join(String(n)); };
886
+ var hasAgent = b.agent || !!who;
887
+ var go0 = button("go", label(), function(){
888
+ post("api/build", {reqs:chosen(), profile:who}).then(function(res){ m5.className = "msg " + (res.ok ? "ok" : "err"); m5.textContent = res.ok ? t("started") + res.started.join(", ") + (res.saved && res.saved.length ? " · " + fill("savedGit", {N:res.saved.length}) : "") : t("err") + res.message; setTimeout(refresh, 1500); });
889
+ });
890
+ go0.disabled = !b.ready.length || !hasAgent || !!b.running;
891
+ hd.append(info, go0); sec.appendChild(hd);
892
+ if (!hasAgent) { var na = el("div", "msg err", t("noAgent")); na.style.padding = "10px 20px"; sec.appendChild(na); }
893
+ m5.style.padding = "0 20px"; sec.appendChild(m5);
894
+ if (!b.ready.length) sec.appendChild(el("div", "empty", t("nothingReady")));
895
+ b.ready.forEach(function(r){
896
+ var l = el("label", "pick"), cb = el("input"); cb.type = "checkbox"; cb.checked = picked[r.id] !== false;
897
+ cb.addEventListener("change", function(){ picked[r.id] = cb.checked; go0.textContent = label(); go0.disabled = !chosen().length || !hasAgent || !!b.running; });
898
+ var name = el("span", "pname"); name.append(cb, document.createTextNode(" " + r.title));
899
+ var mod = el("span", "small muted", r.module); mod.style.textAlign = "right";
900
+ l.append(name, el("span", "small muted", adv() ? r.id : ""), mod); sec.appendChild(l);
901
+ });
902
+ sec.appendChild(cmd("specgate harness run --chain " + chosen().map(function(x){ return "--req " + x; }).join(" ") + (who ? " --profile " + who : "")));
903
+ m.appendChild(sec);
904
+ var today = new Date(); today.setHours(0, 0, 0, 0);
905
+ var done = (live.requirements || []).filter(function(r){ return r.state === "pass" && r.updated * 1000 >= today.getTime(); }).slice(0, 6);
906
+ if (done.length) {
907
+ var fin = el("section", "card"); fin.appendChild(el("h2", "", t("doneToday")));
908
+ done.forEach(function(r){ var x = el("div", "check"); x.append(dot("var(--pass)"), el("span", "", r.title || r.req), el("span", "small muted", " " + Math.round(r.elapsed / 60) + " min")); fin.appendChild(x); });
909
+ m.appendChild(fin);
910
+ }
911
+ }
912
+
913
+ // ── Report (REQ-217) ────────────────────────────────────────────────────
914
+ function viewReport(r){
915
+ var m = $("view"); m.replaceChildren();
916
+ var top = el("section"); flex(top); top.style.alignItems = "flex-end"; top.style.justifyContent = "space-between";
917
+ var g = el("div"); g.style.flex = "999 1 480px"; g.style.display = "flex"; g.style.flexDirection = "column"; g.style.gap = "6px";
918
+ g.append(el("span", "small muted", r.date), el("h1", "", t("repTitle")), el("p", "muted", t("repIntro")));
919
+ var acts = el("div"); flex(acts, 8);
920
+ var pdf = el("a", "ghost", t("print")); pdf.href = "report"; pdf.target = "_blank"; pdf.rel = "noopener"; pdf.style.display = "inline-flex"; pdf.style.alignItems = "center";
921
+ var dl = el("a", "go", t("download")); dl.href = "report.html"; dl.style.display = "inline-flex"; dl.style.alignItems = "center"; dl.style.color = "#fff";
922
+ acts.append(pdf, dl); top.append(g, acts); m.appendChild(top);
923
+ var s = el("section", "stats");
924
+ [[t("rBuilt"), r.built + " " + t("sOf") + " " + r.total, t("reqs"), ""], [t("rExamples"), String(r.scenarios), "", "var(--pass)"], [t("rGate"), r.gate.ok ? t("rGateOk") : t("rGateBad"), r.gate.findings[0] || "", r.gate.ok ? "var(--pass)" : "var(--fail)"], [t("rCost"), r.cost === null ? "—" : "$" + r.cost.toFixed(2), r.minutes + " min", ""]].concat(r.quality && r.quality.measured ? [[t("rQuality"), (r.quality.coverage === null ? "—" : r.quality.coverage + "%"), fill("rQualityP", {C: r.quality.coverage === null ? "—" : r.quality.coverage, M: r.quality.mutation === null ? "—" : r.quality.mutation}), "var(--pass)"]] : []).forEach(function(x){
925
+ var d = el("div", "stat"), b = el("b", "", x[1]); if (x[3]) b.style.color = x[3]; d.append(el("span", "muted small", x[0]), b); if (x[2]) d.appendChild(el("span", "small muted", x[2])); s.appendChild(d);
926
+ });
927
+ m.appendChild(s);
928
+ var tb = el("section", "rows scroll"), hd = el("div", "line"); hd.appendChild(el("h2", "", t("byModule"))); tb.appendChild(hd);
929
+ var table = el("table", "table"), thead = el("tr");
930
+ [t("colModule"), t("colBuilt"), t("colEx"), t("colState")].forEach(function(x){ thead.appendChild(el("th", "", x)); });
931
+ table.appendChild(thead);
932
+ r.modules.forEach(function(mo){
933
+ var tr = el("tr"), st = mo.waiting ? fill("waitingN", {N:mo.waiting}) : mo.built === mo.total ? t("complete") : mo.built ? mo.built + " " + t("sOf") + " " + mo.total : t("notBuilt");
934
+ var sc = el("td", "", st); sc.style.color = mo.waiting ? "var(--warn)" : mo.built === mo.total ? "var(--pass)" : "var(--muted)";
935
+ tr.append(el("td", "", cap(mo.name)), el("td", "", mo.built + " " + t("sOf") + " " + mo.total), el("td", "", String(mo.scenarios)), sc);
936
+ tr.firstChild.style.fontWeight = "600"; table.appendChild(tr);
937
+ });
938
+ tb.appendChild(table); m.appendChild(tb);
939
+ var mods = r.modules.filter(function(mo){ return mo.built; }).map(function(mo){ return mo.name; });
940
+ if (!ui.repModule || mods.indexOf(ui.repModule) < 0) ui.repModule = mods[0] || "";
941
+ var wd = el("section", "rows"), wh = el("div", "line"); wh.style.justifyContent = "space-between";
942
+ wh.appendChild(el("h2", "", t("whatDoes")));
943
+ var chips = el("div", "chips");
944
+ mods.forEach(function(name){ var c = button("chip", cap(name), function(){ ui.repModule = name; render(); }); c.setAttribute("aria-selected", String(ui.repModule === name)); chips.appendChild(c); });
945
+ wh.appendChild(chips); wd.appendChild(wh);
946
+ if (!mods.length) wd.appendChild(el("div", "empty", t("nothingBuilt")));
947
+ r.delivered.filter(function(d){ return d.module === ui.repModule; }).forEach(function(d){
948
+ var line = el("div", "line"), gg = el("div", "grow");
949
+ gg.appendChild(el("strong", "", d.title));
950
+ d.scenarios.forEach(function(sc){ var sp = el("span", "small muted", t("provedBy") + (sc.title || sc.id)); sp.appendChild(el("span", "", " · ✓")); sp.lastChild.style.color = "var(--pass)"; gg.appendChild(sp); });
951
+ if (d.quality && (d.quality.coverage !== null || d.quality.mutation !== null)) {
952
+ var qs = [];
953
+ if (d.quality.coverage !== null) qs.push(t("covShort") + " " + d.quality.coverage + "%");
954
+ if (d.quality.mutation !== null) qs.push(t("mutShort") + " " + d.quality.mutation + "%");
955
+ gg.appendChild(el("span", "small", qs.join(" · ")));
956
+ }
957
+ line.append(gg, el("span", "small muted", adv() ? d.id : "")); wd.appendChild(line);
958
+ });
959
+ m.appendChild(wd);
960
+ var two = el("div", "stats");
961
+ var ms = el("section", "card"); ms.appendChild(el("h2", "", t("missing")));
962
+ if (!r.missing.length) ms.appendChild(el("span", "muted", t("missingNone")));
963
+ r.missing.slice(0, 12).forEach(function(x){
964
+ var row = el("div", "check"); row.append(dot(x.why === "waiting" ? "#d4a72c" : "var(--line)"), el("span", "small", cap(x.module) + " · " + x.title + (x.why === "waiting" ? " — " + t("waitsFor") + x.question : "")));
965
+ ms.appendChild(row);
966
+ });
967
+ if (r.missing.some(function(x){ return x.why === "waiting"; })) ms.appendChild(button("link", t("go_questions"), function(){ go("questions"); }));
968
+ var dc = el("section", "card"); dc.append(el("h2", "", t("decisions")), el("span", "", fill("decisionsP", {Q:r.answered, A:r.assumptions})));
969
+ two.append(ms, dc); m.appendChild(two);
970
+ m.appendChild(cmd("specgate report · specgate matrix · specgate check"));
971
+ }
972
+
973
+ function render(){
974
+ header();
975
+ var r = cache.route, m = $("view"); m.className = "";
976
+ if (!r) { m.replaceChildren(el("div", "card empty", t("loading"))); return; }
977
+ if (!r.isProject && view !== "project") view = "project";
978
+ var pr = cache.profiles || {profiles:[], installed:{}};
979
+ if (view === "route") viewRoute(r, cache.home);
980
+ else if (view === "project" && cache.profiles) viewProject(r, pr);
981
+ else if (view === "brief" && cache.brief) viewBrief(cache.brief, pr, cache.modules ? cache.modules.job : null);
982
+ else if (view === "drafts" && cache.modules) viewDrafts(cache.modules, pr);
983
+ else if (view === "questions" && cache.questions) viewQuestions(cache.questions);
984
+ else if (view === "review" && cache.drafts) viewReview(cache.drafts);
985
+ else if (view === "build" && cache.build) viewBuild(cache.build, cache.live || {}, pr);
986
+ else if (view === "report" && cache.report) viewReport(cache.report);
987
+ else m.replaceChildren(el("div", "card empty", t("loading")));
988
+ }
989
+ function refresh(){
990
+ var reads = [get("api/route").then(function(x){ cache.route = x; })];
991
+ var isProject = !cache.route || cache.route.isProject;
992
+ if (!cache.profiles || view === "project") reads.push(get("api/profiles").then(function(x){ cache.profiles = x; }));
993
+ if (view === "project" && isProject) reads.push(get("api/quality").then(function(x){ cache.quality = x; }));
994
+ if (view === "route" && isProject) reads.push(get("api/home").then(function(x){ cache.home = x; }).catch(function(){}));
995
+ if (view === "brief") { reads.push(get("api/modules").then(function(x){ cache.modules = x; })); if (!cache.brief || (cache.modules && cache.modules.job)) reads.push(get("api/brief").then(function(x){ cache.brief = x; })); }
996
+ if (view === "drafts") reads.push(get("api/modules").then(function(x){ cache.modules = x; }));
997
+ if (view === "questions") reads.push(get("api/questions").then(function(x){ cache.questions = x; }));
998
+ if (view === "review") reads.push(get("api/drafts").then(function(x){ cache.drafts = x; }));
999
+ if (view === "build") { reads.push(get("api/build").then(function(x){ cache.build = x; })); reads.push(get("live.json").then(function(x){ cache.live = x; })); }
1000
+ if (view === "report") reads.push(get("api/report").then(function(x){ cache.report = x; }));
1001
+ Promise.all(reads).then(render).catch(function(){});
1002
+ }
1003
+ function go(v){
1004
+ var k = {brief:"brief", drafts:"modules", questions:"questions", review:"drafts", build:"build", report:"report"}[v];
1005
+ if (k) cache[k] = null;
1006
+ view = v; save("sg-app-view", v); render(); refresh();
1007
+ }
1008
+ $("mSimple").addEventListener("click", function(){ mode = "simple"; save("sg-app-mode", mode); render(); });
1009
+ $("mAdv").addEventListener("click", function(){ mode = "adv"; save("sg-app-mode", mode); render(); });
1010
+ $("lEs").addEventListener("click", function(){ lang = "es"; save("sg-app-lang", lang); render(); });
1011
+ $("lEn").addEventListener("click", function(){ lang = "en"; save("sg-app-lang", lang); render(); });
1012
+ render();
1013
+ refresh();
1014
+ // The views that change on their own: the route, a build, a draft being written.
1015
+ setInterval(function(){
1016
+ var busy = cache.modules && cache.modules.job;
1017
+ if (view === "route" || view === "build" || view === "drafts" || (view === "brief" && busy)) refresh();
1018
+ }, 5000);
1019
+ })();
1020
+ </script>
1021
+ </body></html>`;
1022
+ }
1023
+ //# sourceMappingURL=studio-app-page.js.map