@rsaglobaltech/specgate 0.26.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 (58) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -0
  3. package/dist/packages/core/src/domain/ChangeMeasures.js +198 -0
  4. package/dist/packages/core/src/domain/ChangeMeasures.js.map +1 -0
  5. package/dist/packages/core/src/domain/ConventionalCommit.js +73 -0
  6. package/dist/packages/core/src/domain/ConventionalCommit.js.map +1 -0
  7. package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
  8. package/dist/packages/core/src/domain/HarnessReport.js +2 -0
  9. package/dist/packages/core/src/domain/HarnessReport.js.map +1 -1
  10. package/dist/packages/core/src/domain/QualityChecks.js +218 -0
  11. package/dist/packages/core/src/domain/QualityChecks.js.map +1 -0
  12. package/dist/packages/core/src/domain/QualitySummary.js +58 -0
  13. package/dist/packages/core/src/domain/QualitySummary.js.map +1 -0
  14. package/dist/packages/core/src/domain/Sarif.js +121 -0
  15. package/dist/packages/core/src/domain/Sarif.js.map +1 -0
  16. package/dist/packages/core/src/infrastructure/GitContributionStager.js +1 -1
  17. package/dist/packages/core/src/infrastructure/GitContributionStager.js.map +1 -1
  18. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +21 -0
  19. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
  20. package/dist/packages/core/src/infrastructure/QualityBaselineFile.js +67 -0
  21. package/dist/packages/core/src/infrastructure/QualityBaselineFile.js.map +1 -0
  22. package/dist/scripts/cli/commands/change/TicketCommand.js +1 -1
  23. package/dist/scripts/cli/commands/change/TicketCommand.js.map +1 -1
  24. package/dist/scripts/cli/commands/harness/RunCommand.js +212 -15
  25. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  26. package/dist/scripts/cli/commands/quality/CheckCommand.js +54 -3
  27. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
  28. package/dist/scripts/cli/commands/quality/QualityCommand.js +314 -0
  29. package/dist/scripts/cli/commands/quality/QualityCommand.js.map +1 -0
  30. package/dist/scripts/cli/commands/spec/DoneCommand.js +4 -1
  31. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
  32. package/dist/scripts/cli/commands/spec/StudioCommand.js +14 -0
  33. package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
  34. package/dist/scripts/docs/nav.js +13 -2
  35. package/dist/scripts/docs/nav.js.map +1 -1
  36. package/dist/scripts/harness/prompt.js +4 -0
  37. package/dist/scripts/harness/prompt.js.map +1 -1
  38. package/dist/scripts/harness/report.js +13 -0
  39. package/dist/scripts/harness/report.js.map +1 -1
  40. package/dist/scripts/lib/quality-presets.js +378 -0
  41. package/dist/scripts/lib/quality-presets.js.map +1 -0
  42. package/dist/scripts/lib/quality-run.js +579 -0
  43. package/dist/scripts/lib/quality-run.js.map +1 -0
  44. package/dist/scripts/lib/sonar-local.js +308 -0
  45. package/dist/scripts/lib/sonar-local.js.map +1 -0
  46. package/dist/scripts/lib/studio-app-page.js +74 -11
  47. package/dist/scripts/lib/studio-app-page.js.map +1 -1
  48. package/dist/scripts/lib/studio-app.js +80 -0
  49. package/dist/scripts/lib/studio-app.js.map +1 -1
  50. package/dist/scripts/lib/studio-route.js +64 -1
  51. package/dist/scripts/lib/studio-route.js.map +1 -1
  52. package/dist/scripts/lib/surface.js +11 -0
  53. package/dist/scripts/lib/surface.js.map +1 -1
  54. package/dist/scripts/lib/ticket-keys.js +1 -1
  55. package/dist/scripts/lib/ticket-keys.js.map +1 -1
  56. package/dist/scripts/quality.js +10 -0
  57. package/dist/scripts/quality.js.map +1 -0
  58. package/package.json +1 -1
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SONAR_SARIF = exports.SONAR = void 0;
37
+ exports.ensureServer = ensureServer;
38
+ exports.analysisToken = analysisToken;
39
+ exports.projectKey = projectKey;
40
+ exports.scannerProperties = scannerProperties;
41
+ exports.issuesToSarif = issuesToSarif;
42
+ exports.runSonar = runSonar;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const node_child_process_1 = require("node:child_process");
46
+ /**
47
+ * SonarQube Community, run locally in Docker, as one more check (REQ-226):
48
+ * no account and no server to host. The first run starts the server in a
49
+ * named container on a private network, bound to 127.0.0.1 only; a token for
50
+ * analysis is created once with the server's own default login and kept in
51
+ * `.specgate/sonar.local`, git-ignored — the person's admin password is never
52
+ * changed or stored, so they log in with admin/admin and choose their own.
53
+ * Each run analyses the project with the scanner image and writes the open
54
+ * issues as SARIF, so the baseline judges them like any other check's.
55
+ */
56
+ exports.SONAR = {
57
+ image: "sonarqube:25.9.0.112764-community",
58
+ scanner: "sonarsource/sonar-scanner-cli:11.4",
59
+ container: "specgate-sonarqube",
60
+ network: "specgate-sonar",
61
+ port: 9123,
62
+ };
63
+ const BASE = `http://127.0.0.1:${exports.SONAR.port}`;
64
+ const LOCAL = path.join(".specgate", "sonar.local");
65
+ exports.SONAR_SARIF = ".specgate/quality/sonar.sarif";
66
+ const docker = (args, timeout = 120000) => (0, node_child_process_1.spawnSync)("docker", args, { encoding: "utf8", timeout });
67
+ /**
68
+ * One HTTP call, synchronously: the checks run in spawnSync's world, so a
69
+ * short Node child does the fetch and prints the JSON.
70
+ */
71
+ function http(method, url, auth, form) {
72
+ const script = [
73
+ "(async () => {",
74
+ " try {",
75
+ " const [method, url, auth, form] = JSON.parse(process.argv[1]);",
76
+ " const r = await fetch(url, {",
77
+ " method,",
78
+ " headers: Object.assign(auth ? { Authorization: 'Basic ' + Buffer.from(auth).toString('base64') } : {},",
79
+ " form ? { 'content-type': 'application/x-www-form-urlencoded' } : {}),",
80
+ " body: form ? new URLSearchParams(form).toString() : undefined,",
81
+ " });",
82
+ " const text = await r.text();",
83
+ " let body = null; try { body = JSON.parse(text); } catch { body = text; }",
84
+ " process.stdout.write(JSON.stringify({ status: r.status, body }));",
85
+ " } catch (e) { process.stdout.write(JSON.stringify({ status: 0, body: String(e) })); }",
86
+ "})();",
87
+ ].join("\n");
88
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", script, JSON.stringify([method, url, auth, form || null])], {
89
+ encoding: "utf8",
90
+ timeout: 60000,
91
+ });
92
+ try {
93
+ return JSON.parse(r.stdout || "");
94
+ }
95
+ catch {
96
+ return { status: 0, body: r.stderr || "" };
97
+ }
98
+ }
99
+ const sleep = (ms) => (0, node_child_process_1.spawnSync)(process.execPath, ["-e", `setTimeout(() => {}, ${ms})`]);
100
+ /** The server, started if it is not, and answering UP. */
101
+ function ensureServer(timeoutMs = 5 * 60 * 1000) {
102
+ docker(["network", "create", exports.SONAR.network]);
103
+ const state = (docker(["inspect", "-f", "{{.State.Running}}", exports.SONAR.container]).stdout || "").trim();
104
+ if (state === "false")
105
+ docker(["start", exports.SONAR.container]);
106
+ else if (state !== "true") {
107
+ const run = docker([
108
+ "run",
109
+ "-d",
110
+ "--name",
111
+ exports.SONAR.container,
112
+ "--network",
113
+ exports.SONAR.network,
114
+ "-p",
115
+ `127.0.0.1:${exports.SONAR.port}:9000`,
116
+ // Its Elasticsearch asks the host for vm.max_map_count; Docker Desktop
117
+ // does not let a container set it, and one local instance does not need it.
118
+ "-e",
119
+ "SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true",
120
+ "-v",
121
+ "specgate-sonar-data:/opt/sonarqube/data",
122
+ "-v",
123
+ "specgate-sonar-extensions:/opt/sonarqube/extensions",
124
+ exports.SONAR.image,
125
+ ], 15 * 60 * 1000);
126
+ if (run.status !== 0) {
127
+ return { ok: false, message: `could not start SonarQube: ${(run.stderr || "").trim()}` };
128
+ }
129
+ }
130
+ const until = Date.now() + timeoutMs;
131
+ while (Date.now() < until) {
132
+ const r = http("GET", `${BASE}/api/system/status`, "");
133
+ if (r.status === 200 && r.body.status === "UP")
134
+ return { ok: true };
135
+ sleep(3000);
136
+ }
137
+ return {
138
+ ok: false,
139
+ message: `SonarQube did not come up at ${BASE} within ${timeoutMs / 60000} min`,
140
+ };
141
+ }
142
+ /** The analysis token: SONAR_TOKEN, else the one kept, else one created with the default login. */
143
+ function analysisToken(projectDir) {
144
+ if (process.env.SONAR_TOKEN)
145
+ return { ok: true, token: process.env.SONAR_TOKEN };
146
+ const file = path.join(projectDir, LOCAL);
147
+ try {
148
+ const kept = JSON.parse(fs.readFileSync(file, "utf8")).token;
149
+ const checked = kept ? http("GET", `${BASE}/api/authentication/validate`, `${kept}:`) : null;
150
+ if (checked && checked.status === 200 && checked.body.valid) {
151
+ return { ok: true, token: kept };
152
+ }
153
+ }
154
+ catch {
155
+ /* none kept yet */
156
+ }
157
+ const made = http("POST", `${BASE}/api/user_tokens/generate`, "admin:admin", {
158
+ name: `specgate-${path.basename(projectDir)}-${Date.now()}`,
159
+ type: "GLOBAL_ANALYSIS_TOKEN",
160
+ });
161
+ const token = made.body.token;
162
+ if (made.status !== 200 || !token) {
163
+ return {
164
+ ok: false,
165
+ message: "SonarQube's default login no longer works (its password was changed, as it should be). " +
166
+ `Create a token at ${BASE}/account/security and set it as SONAR_TOKEN in the environment.`,
167
+ };
168
+ }
169
+ fs.mkdirSync(path.dirname(file), { recursive: true });
170
+ fs.writeFileSync(file, JSON.stringify({ token }, null, 2) + "\n", "utf8");
171
+ ensureIgnored(projectDir, ".specgate/sonar.local");
172
+ return { ok: true, token };
173
+ }
174
+ /** A line in the project's .gitignore, added once. */
175
+ function ensureIgnored(projectDir, entry) {
176
+ const file = path.join(projectDir, ".gitignore");
177
+ const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
178
+ if (!text.split(/\r?\n/).includes(entry)) {
179
+ fs.writeFileSync(file, `${text.replace(/\n?$/, "\n")}${entry}\n`, "utf8");
180
+ }
181
+ }
182
+ /** The project key: the folder's name, as SonarQube accepts it. */
183
+ function projectKey(projectDir) {
184
+ return (path
185
+ .basename(path.resolve(projectDir))
186
+ .replace(/[^A-Za-z0-9_.:-]+/g, "-")
187
+ .replace(/^-|-$/g, "") || "project");
188
+ }
189
+ /** The scanner's properties for this project: Java sources and classes when it has them. */
190
+ function scannerProperties(projectDir, key) {
191
+ const has = (rel) => fs.existsSync(path.join(projectDir, rel));
192
+ const props = [
193
+ `-Dsonar.projectKey=${key}`,
194
+ "-Dsonar.scm.disabled=true",
195
+ "-Dsonar.working.directory=.specgate/quality/sonar",
196
+ ];
197
+ if (has("src/main/java")) {
198
+ props.push("-Dsonar.sources=src/main/java");
199
+ if (has("src/test/java"))
200
+ props.push("-Dsonar.tests=src/test/java");
201
+ props.push(`-Dsonar.java.binaries=${has("target/classes") ? "target/classes" : has("build/classes") ? "build/classes" : "."}`);
202
+ }
203
+ else {
204
+ props.push("-Dsonar.sources=.", "-Dsonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/.specgate/**,**/.harness/**");
205
+ }
206
+ return props;
207
+ }
208
+ /** SonarQube's open issues as SARIF 2.1.0, its line hash as the fingerprint. */
209
+ function issuesToSarif(issues, key) {
210
+ const level = (s) => s === "BLOCKER" || s === "CRITICAL" ? "error" : s === "MAJOR" ? "warning" : "note";
211
+ return JSON.stringify({
212
+ version: "2.1.0",
213
+ runs: [
214
+ {
215
+ tool: { driver: { name: "SonarQube Community" } },
216
+ results: issues.map((i) => ({
217
+ ruleId: i.rule,
218
+ level: level(i.severity),
219
+ message: { text: i.message },
220
+ locations: [
221
+ {
222
+ physicalLocation: {
223
+ artifactLocation: { uri: i.component.replace(new RegExp(`^${key}:`), "") },
224
+ region: { startLine: i.line || 1 },
225
+ },
226
+ },
227
+ ],
228
+ ...(i.hash ? { partialFingerprints: { sonarLineHash: i.hash } } : {}),
229
+ })),
230
+ },
231
+ ],
232
+ }, null, 2);
233
+ }
234
+ /** Analyse the project and write its open issues as SARIF; shaped like spawnSync's result. */
235
+ function runSonar(projectDir, timeoutMs) {
236
+ const fail = (message) => ({ status: 1, stdout: "", stderr: message });
237
+ const up = ensureServer();
238
+ if (!up.ok)
239
+ return fail(up.message);
240
+ const tok = analysisToken(projectDir);
241
+ if (!tok.ok)
242
+ return fail(tok.message);
243
+ const key = projectKey(projectDir);
244
+ const posix = process.platform !== "win32" && typeof process.getuid === "function";
245
+ const scan = (0, node_child_process_1.spawnSync)("docker", [
246
+ "run",
247
+ "--rm",
248
+ "--network",
249
+ exports.SONAR.network,
250
+ "-v",
251
+ `${path.resolve(projectDir)}:/usr/src`,
252
+ "-w",
253
+ "/usr/src",
254
+ ...(posix ? ["--user", `${process.getuid()}:${process.getgid ? process.getgid() : 0}`] : []),
255
+ "-e",
256
+ "SONAR_TOKEN",
257
+ "-e",
258
+ `SONAR_HOST_URL=http://${exports.SONAR.container}:9000`,
259
+ "-e",
260
+ "SONAR_USER_HOME=/tmp/.sonar",
261
+ exports.SONAR.scanner,
262
+ ...scannerProperties(projectDir, key),
263
+ ], {
264
+ encoding: "utf8",
265
+ timeout: timeoutMs,
266
+ maxBuffer: 64 * 1024 * 1024,
267
+ env: { ...process.env, SONAR_TOKEN: tok.token },
268
+ });
269
+ if (scan.status !== 0) {
270
+ return {
271
+ status: scan.status,
272
+ stdout: scan.stdout || "",
273
+ stderr: scan.stderr || "",
274
+ error: scan.error,
275
+ };
276
+ }
277
+ // The analysis is processed on the server after the scanner ends.
278
+ const report = path.join(projectDir, ".specgate", "quality", "sonar", "report-task.txt");
279
+ const task = (/ceTaskId=(\S+)/.exec(fs.existsSync(report) ? fs.readFileSync(report, "utf8") : "") || [])[1];
280
+ if (task) {
281
+ for (let i = 0; i < 120; i += 1) {
282
+ const t = http("GET", `${BASE}/api/ce/task?id=${task}`, `${tok.token}:`).body;
283
+ const status = t && t.task ? t.task.status : "";
284
+ if (status && status !== "PENDING" && status !== "IN_PROGRESS")
285
+ break;
286
+ sleep(2000);
287
+ }
288
+ }
289
+ const issues = [];
290
+ for (let page = 1; page <= 20; page += 1) {
291
+ const r = http("GET", `${BASE}/api/issues/search?componentKeys=${encodeURIComponent(key)}&resolved=false&ps=500&p=${page}`, `${tok.token}:`);
292
+ const body = r.body;
293
+ if (r.status !== 200 || !body.issues)
294
+ return fail(`could not read the issues: HTTP ${r.status}`);
295
+ issues.push(...body.issues);
296
+ if (issues.length >= (body.total || 0) || body.issues.length === 0)
297
+ break;
298
+ }
299
+ const out = path.join(projectDir, exports.SONAR_SARIF);
300
+ fs.mkdirSync(path.dirname(out), { recursive: true });
301
+ fs.writeFileSync(out, issuesToSarif(issues, key), "utf8");
302
+ return {
303
+ status: 0,
304
+ stdout: `SonarQube analysed ${key}: ${issues.length} open issue(s). ${BASE}/dashboard?id=${encodeURIComponent(key)}`,
305
+ stderr: "",
306
+ };
307
+ }
308
+ //# sourceMappingURL=sonar-local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sonar-local.js","sourceRoot":"","sources":["../../../scripts/lib/sonar-local.ts"],"names":[],"mappings":"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"}
@@ -128,7 +128,7 @@ function studioAppPage(projectName, token = "") {
128
128
  home:"Inicio", questions:"Preguntas", review:"Revisión", build:"Construir", simple:"Simple", adv:"Avanzado",
129
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
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:"Cuatro preguntas y specgate prepara esta carpeta.", go_project:"Empezar",
131
+ nx_project:"Crea tu proyecto", nxp_project:"Cinco preguntas y specgate prepara esta carpeta.", go_project:"Empezar",
132
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
133
  nx_drafts:"Redacta los borradores", nxp_drafts:"El asistente convierte cada módulo del brief en requisitos con ejemplos.", go_drafts:"Redactar",
134
134
  nx_questions:"Responde las preguntas", nxp_questions:"El asistente o un borrador espera una decisión tuya.", go_questions:"Responder",
@@ -143,7 +143,7 @@ function studioAppPage(projectName, token = "") {
143
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
144
  repeat:"Los pasos 3 a 6 se repiten por módulo: puedes construir uno mientras redactas otro.",
145
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. Cuatro preguntas y specgate la prepara: especificación, pruebas y el asistente que programa.",
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
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
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
149
  stNode:"Node.js + React (recomendado para una web)", stJava:"Java + Spring Boot", stPy:"Python + FastAPI", stCustom:"Otra… (la escribes tú)",
@@ -180,17 +180,25 @@ function studioAppPage(projectName, token = "") {
180
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
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
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: ", saved:"Guardado.", err:"No se pudo: ",
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
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
185
  download:"Descargar informe", print:"Imprimir / PDF", rBuilt:"Construido", rExamples:"Ejemplos probados", rGate:"Calidad", rGateOk:"Pasa", rGateBad:"No pasa", rCost:"Coste del asistente",
186
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
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."},
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"},
189
197
  en: {route:"Route", project:"Project", brief:"Brief", drafts:"Drafts", report:"Report",
190
198
  home:"Home", questions:"Questions", review:"Review", build:"Build", simple:"Simple", adv:"Advanced",
191
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.",
192
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.",
193
- nx_project:"Create your project", nxp_project:"Four questions and specgate prepares this folder.", go_project:"Start",
201
+ nx_project:"Create your project", nxp_project:"Five questions and specgate prepares this folder.", go_project:"Start",
194
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",
195
203
  nx_drafts:"Draft the modules", nxp_drafts:"The assistant turns each module of the brief into requirements with examples.", go_drafts:"Draft",
196
204
  nx_questions:"Answer the questions", nxp_questions:"The assistant or a draft is waiting for your decision.", go_questions:"Answer",
@@ -205,7 +213,7 @@ function studioAppPage(projectName, token = "") {
205
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",
206
214
  repeat:"Steps 3 to 6 repeat per module: you can build one while another is drafted.",
207
215
  loading:"Loading…", mods:"modules", sOf:"of", modules:"Modules", ready:"done", qWait:"question", draft:"draft to review", reqs:"requirements",
208
- newTitle:"Let's start a project", newIntro:"This folder is empty. Four questions and specgate prepares it: specification, tests and the assistant that codes.",
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.",
209
217
  q1:"1 · What is it called?", q2:"2 · What are you building?", q3:"3 · With which technology?", q4:"4 · Which assistant codes?",
210
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",
211
219
  stNode:"Node.js + React (recommended for a website)", stJava:"Java + Spring Boot", stPy:"Python + FastAPI", stCustom:"Other… (you write it)",
@@ -242,12 +250,20 @@ function studioAppPage(projectName, token = "") {
242
250
  whoCodes:"Who codes?", whoCodesP:"The assistants of this project. Your choice is remembered.", noEstimate:"No estimate yet", perRun:"per requirement",
243
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).",
244
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.",
245
- doneToday:"Finished", nothingReady:"Nothing is ready to build.", started:"Started: ", saved:"Saved.", err:"Could not: ",
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: ",
246
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.",
247
255
  download:"Download the report", print:"Print / PDF", rBuilt:"Built", rExamples:"Examples proven", rGate:"Quality", rGateOk:"Passes", rGateBad:"Fails", rCost:"Assistant cost",
248
256
  byModule:"By module", colModule:"Module", colBuilt:"Built", colEx:"Examples", colState:"State", complete:"Complete", waitingN:"N wait for an answer", notBuilt:"Not built",
249
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",
250
- decisionsP:"Q questions answered · A assumptions recorded.", nothingBuilt:"Nothing is built yet."}
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"}
251
267
  };
252
268
  function load(k, d){ try { return localStorage.getItem(k) || d; } catch (e) { return d; } }
253
269
  function save(k, v){ try { localStorage.setItem(k, v); } catch (e) {} }
@@ -366,7 +382,7 @@ function studioAppPage(projectName, token = "") {
366
382
  }
367
383
 
368
384
  // ── Project (REQ-213) ───────────────────────────────────────────────────
369
- var np = {name:"", kind:"web", stack:"node-react", customStack:"", customTesting:"", agent:""};
385
+ var np = {name:"", kind:"web", stack:"node-react", customStack:"", customTesting:"", agent:"", quality:"basic"};
370
386
  function viewProject(r, pr){
371
387
  var m = $("view"); m.replaceChildren(); m.className = "narrow";
372
388
  if (r.isProject) {
@@ -384,6 +400,7 @@ function studioAppPage(projectName, token = "") {
384
400
  m.appendChild(c);
385
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))));
386
402
  m.appendChild(hint);
403
+ if (cache.quality) m.appendChild(qualityCard(cache.quality));
387
404
  return;
388
405
  }
389
406
  if (!r.empty) {
@@ -427,6 +444,14 @@ function studioAppPage(projectName, token = "") {
427
444
  f4.appendChild(l);
428
445
  });
429
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);
430
455
  var f5 = el("div", "field"); flex(f5); f5.style.justifyContent = "space-between";
431
456
  var m0 = msgEl();
432
457
  var b = button("go", t("create"), function(){
@@ -441,6 +466,37 @@ function studioAppPage(projectName, token = "") {
441
466
  m.append(f, m0, cmd("specgate init --new · specgate harness init · specgate propose --from-brief --new-brief"));
442
467
  }
443
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
+
444
500
  // ── Profiles: who works, picked once for Brief, Drafts and Build (REQ-211) ──
445
501
  function profileSelect(pr){
446
502
  var wrap = el("label", "small muted"); flex(wrap, 6); wrap.appendChild(document.createTextNode(t("assistant")));
@@ -829,7 +885,7 @@ function studioAppPage(projectName, token = "") {
829
885
  var label = function(){ var n = chosen().length; return n === 1 ? t("build1") : t("buildN").split("N").join(String(n)); };
830
886
  var hasAgent = b.agent || !!who;
831
887
  var go0 = button("go", label(), function(){
832
- 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(", ") : t("err") + res.message; setTimeout(refresh, 1500); });
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); });
833
889
  });
834
890
  go0.disabled = !b.ready.length || !hasAgent || !!b.running;
835
891
  hd.append(info, go0); sec.appendChild(hd);
@@ -865,7 +921,7 @@ function studioAppPage(projectName, token = "") {
865
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";
866
922
  acts.append(pdf, dl); top.append(g, acts); m.appendChild(top);
867
923
  var s = el("section", "stats");
868
- [[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", ""]].forEach(function(x){
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){
869
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);
870
926
  });
871
927
  m.appendChild(s);
@@ -892,6 +948,12 @@ function studioAppPage(projectName, token = "") {
892
948
  var line = el("div", "line"), gg = el("div", "grow");
893
949
  gg.appendChild(el("strong", "", d.title));
894
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
+ }
895
957
  line.append(gg, el("span", "small muted", adv() ? d.id : "")); wd.appendChild(line);
896
958
  });
897
959
  m.appendChild(wd);
@@ -928,6 +990,7 @@ function studioAppPage(projectName, token = "") {
928
990
  var reads = [get("api/route").then(function(x){ cache.route = x; })];
929
991
  var isProject = !cache.route || cache.route.isProject;
930
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; }));
931
994
  if (view === "route" && isProject) reads.push(get("api/home").then(function(x){ cache.home = x; }).catch(function(){}));
932
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; })); }
933
996
  if (view === "drafts") reads.push(get("api/modules").then(function(x){ cache.modules = x; }));
@@ -1 +1 @@
1
- {"version":3,"file":"studio-app-page.js","sourceRoot":"","sources":["../../../scripts/lib/studio-app-page.ts"],"names":[],"mappings":";;AAUA,sCAi7BC;AA37BD;;;;;;;;;GASG;AACH,SAAgB,aAAa,CAAC,WAAmB,EAAE,KAAK,GAAG,EAAE;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO;;;uCAG8B,GAAG;2BACf,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iRA+FiP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eA00BvQ,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"studio-app-page.js","sourceRoot":"","sources":["../../../scripts/lib/studio-app-page.ts"],"names":[],"mappings":";;AAUA,sCAg/BC;AA1/BD;;;;;;;;;GASG;AACH,SAAgB,aAAa,CAAC,WAAmB,EAAE,KAAK,GAAG,EAAE;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO;;;uCAG8B,GAAG;2BACf,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iRA+FiP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAy4BvQ,CAAC;AAChB,CAAC"}
@@ -37,6 +37,7 @@ exports.drafts = drafts;
37
37
  exports.questionsView = questionsView;
38
38
  exports.buildView = buildView;
39
39
  exports.startBuild = startBuild;
40
+ exports.saveSpecification = saveSpecification;
40
41
  exports.stopBuild = stopBuild;
41
42
  exports.home = home;
42
43
  exports.gate = gate;
@@ -268,6 +269,9 @@ function startBuild(projectDir, reqs, profile = "") {
268
269
  message: "None of those requirements is ready to build.",
269
270
  };
270
271
  }
272
+ const saved = saveSpecification(projectDir);
273
+ if (!saved.ok)
274
+ return saved;
271
275
  const log = path.join(projectDir, ".harness", "events", "studio-run.log");
272
276
  fs.mkdirSync(path.dirname(log), { recursive: true });
273
277
  const out = fs.openSync(log, "a");
@@ -290,10 +294,86 @@ function startBuild(projectDir, reqs, profile = "") {
290
294
  return {
291
295
  ok: true,
292
296
  started: chosen,
297
+ saved: saved.committed,
293
298
  log: path.relative(projectDir, log).split(path.sep).join("/"),
294
299
  pid: child.pid || 0,
295
300
  };
296
301
  }
302
+ /** What the studio and the drafting write: the specification, never code. */
303
+ const SPECIFICATION = [
304
+ /^docs\/brief\.md$/,
305
+ /^spec\.md$/,
306
+ /^docs\/specs\//,
307
+ /^features\//,
308
+ /^harness\.config\.yaml$/,
309
+ /^\.harness\/(?:profiles\.yaml|prompt-prefix\.md)$/,
310
+ ];
311
+ /** Uncommitted paths, from `git status --porcelain -z`: renames give their new path. */
312
+ function uncommitted(projectDir) {
313
+ const r = (0, node_child_process_1.spawnSync)("git", ["-C", projectDir, "status", "--porcelain", "-z", "-uall"], {
314
+ encoding: "utf8",
315
+ });
316
+ if (r.status !== 0)
317
+ return null;
318
+ const parts = r.stdout.split("\0").filter(Boolean);
319
+ const out = [];
320
+ for (let i = 0; i < parts.length; i += 1) {
321
+ const status = parts[i].slice(0, 2);
322
+ out.push(parts[i].slice(3));
323
+ if (/[RC]/.test(status))
324
+ i += 1; // the old path of a rename follows
325
+ }
326
+ return out;
327
+ }
328
+ /**
329
+ * Commit the specification the studio wrote, so the harness finds the clean
330
+ * tree it needs (REQ-233). Anything else uncommitted is the person's work in
331
+ * progress: the build is refused and the files are named, never committed.
332
+ */
333
+ function saveSpecification(projectDir) {
334
+ const dirty = uncommitted(projectDir);
335
+ if (dirty === null) {
336
+ return { ok: false, code: "build_no_git", message: "This folder is not a git repository." };
337
+ }
338
+ if (dirty.length === 0)
339
+ return { ok: true, committed: [] };
340
+ const other = dirty.filter((f) => !SPECIFICATION.some((re) => re.test(f)));
341
+ if (other.length) {
342
+ return {
343
+ ok: false,
344
+ code: "build_uncommitted",
345
+ message: `Uncommitted files that are not the specification: ${other.slice(0, 8).join(", ")}` +
346
+ `${other.length > 8 ? ` and ${other.length - 8} more` : ""}. Commit or stash them first.`,
347
+ };
348
+ }
349
+ const email = (0, node_child_process_1.spawnSync)("git", ["-C", projectDir, "config", "user.email"], { encoding: "utf8" });
350
+ if (!String(email.stdout || "").trim()) {
351
+ return {
352
+ ok: false,
353
+ code: "build_no_identity",
354
+ message: 'Git does not know who you are. Run git config --global user.name "Your Name" and git config --global user.email you@example.com.',
355
+ };
356
+ }
357
+ const add = (0, node_child_process_1.spawnSync)("git", ["-C", projectDir, "add", "--", ...dirty], { encoding: "utf8" });
358
+ const commit = add.status === 0
359
+ ? (0, node_child_process_1.spawnSync)("git", [
360
+ "-C",
361
+ projectDir,
362
+ "commit",
363
+ "-q",
364
+ "-m",
365
+ "chore(spec): save the specification from the studio",
366
+ ], { encoding: "utf8" })
367
+ : add;
368
+ if (commit.status !== 0) {
369
+ return {
370
+ ok: false,
371
+ code: "build_commit_failed",
372
+ message: `git could not commit the specification: ${(commit.stderr || commit.stdout || "").trim()}`,
373
+ };
374
+ }
375
+ return { ok: true, committed: dirty };
376
+ }
297
377
  /** Ask the run in progress to stop after its current requirement (REQ-209). */
298
378
  function stopBuild(projectDir) {
299
379
  if (!(0, harness_guard_1.runLockHolder)(projectDir))