gaia-framework 1.127.2 → 1.127.6

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 (33) hide show
  1. package/CLAUDE.md +3 -3
  2. package/_gaia/_config/global.yaml +1 -1
  3. package/_gaia/core/bridge/adapters/flutter-adapter.js +530 -0
  4. package/_gaia/core/bridge/adapters/go-adapter.js +600 -0
  5. package/_gaia/core/bridge/adapters/index.js +218 -0
  6. package/_gaia/core/bridge/adapters/java-adapter.js +589 -0
  7. package/_gaia/core/bridge/adapters/js-adapter.js +729 -0
  8. package/_gaia/core/bridge/adapters/python-adapter.js +534 -0
  9. package/_gaia/core/bridge/bridge-orchestrator.js +152 -0
  10. package/_gaia/core/bridge/bridge-post-flip-checks.js +242 -0
  11. package/_gaia/core/bridge/bridge-scope-guard.js +169 -0
  12. package/_gaia/core/bridge/bridge-toggle.js +313 -0
  13. package/_gaia/core/bridge/layer-0-environment-check.js +91 -0
  14. package/_gaia/core/bridge/layer-1-test-runner-discovery.js +89 -0
  15. package/_gaia/core/bridge/layer-2-ci-execution.js +363 -0
  16. package/_gaia/core/bridge/layer-2-local-execution.js +251 -0
  17. package/_gaia/core/bridge/layer-2-tier-selection.js +252 -0
  18. package/_gaia/core/bridge/layer-3-result-parsing.js +177 -0
  19. package/_gaia/core/bridge/review-gate-tier-mapping.js +215 -0
  20. package/_gaia/core/bridge/runner-compatibility-guard.js +192 -0
  21. package/_gaia/core/workflows/bridge-toggle/checklist.md +3 -0
  22. package/_gaia/core/workflows/bridge-toggle/instructions.xml +18 -15
  23. package/_gaia/lifecycle/workflows/4-implementation/dev-story/instructions.xml +1 -1
  24. package/gaia-install.sh +22 -0
  25. package/package.json +2 -1
  26. package/src/brownfield/browser-matrix-detector.js +274 -0
  27. package/src/brownfield/ci-test-detector.js +231 -0
  28. package/src/brownfield/design-extractor.js +523 -0
  29. package/src/brownfield/docker-test-detector.js +252 -0
  30. package/src/brownfield/test-environment-generator.js +416 -0
  31. package/src/brownfield/test-runner-detector.js +259 -0
  32. package/src/design-lifecycle/delta-sync.js +127 -0
  33. package/src/design-lifecycle/design-state.js +266 -0
@@ -0,0 +1,600 @@
1
+ /**
2
+ * E25-S3: Go Stack Adapter
3
+ *
4
+ * Plugs into the E25-S5 adapter registry and satisfies the StackAdapter
5
+ * contract (architecture §10.20.11.1). Unlike the Python and Java adapters
6
+ * (which read JUnit XML files from disk), the Go adapter consumes
7
+ * `go test -json` — a line-delimited JSON event stream emitted on stdout.
8
+ *
9
+ * Responsibilities:
10
+ * - Layer 0: readinessCheck — detect `go` on PATH + `go.mod` at project root
11
+ * - Layer 1: discoverRunners — `go list ./...` for single-module,
12
+ * `go list -m all` for nested-module monorepos
13
+ * - Layer 3: parseOutput — streaming JSON event parser correlating events
14
+ * by (Package, Test) keys; tolerant of panic truncation
15
+ *
16
+ * Detection semantics: AND over `go.mod` (single-file detection).
17
+ * No new runtime dependencies — line-delimited JSON.parse only.
18
+ *
19
+ * Traces to: FR-307, FR-310, NFR-047, ADR-028, ADR-038, architecture §10.20.11
20
+ */
21
+
22
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
23
+ import { join, relative } from "path";
24
+ import { execFileSync as realExecFileSync } from "child_process";
25
+
26
+ // ─── Constants ──────────────────────────────────────────────────────────────
27
+
28
+ const DETECTION_PATTERNS = ["go.mod"];
29
+
30
+ const DEFAULT_COMMAND = "go test -json ./...";
31
+ const STDERR_SNIPPET_MAX = 2048;
32
+ const RAW_OUTPUT_SNIPPET_MAX = 2048;
33
+
34
+ const DEFAULT_BUILD_TAGS = {
35
+ integration: "integration",
36
+ e2e: "e2e",
37
+ };
38
+
39
+ const REMEDIATION = {
40
+ missingGoToolchain: "Go not found — install from https://go.dev/dl/",
41
+ missingGoMod:
42
+ "No go.mod found at the project root. Run `go mod init <module>` to initialize a Go module.",
43
+ };
44
+
45
+ // ─── Layer 0 helpers ────────────────────────────────────────────────────────
46
+
47
+ function goAvailable(execFile) {
48
+ try {
49
+ execFile("go", ["version"], { stdio: "ignore" });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function hasGoMod(projectPath) {
57
+ return existsSync(join(projectPath, "go.mod"));
58
+ }
59
+
60
+ // ─── Layer 0: readinessCheck ────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Readiness check for Go projects (AC2).
64
+ *
65
+ * @param {string} projectPath
66
+ * @param {object} [options]
67
+ * @param {function} [options._execFile] - execFileSync override (tests only)
68
+ * @returns {object}
69
+ */
70
+ function readinessCheck(projectPath, options = {}) {
71
+ const started = Date.now();
72
+ const execFile = options._execFile || realExecFileSync;
73
+
74
+ if (!projectPath || typeof projectPath !== "string") {
75
+ throw new TypeError("readinessCheck: projectPath is required");
76
+ }
77
+
78
+ // NFR-035 bridge_enabled guard — parity with js/python/java adapters.
79
+ if (options?.test_execution_bridge?.bridge_enabled === false) {
80
+ return {
81
+ passed: true,
82
+ remediation: null,
83
+ ready: true,
84
+ skipped: true,
85
+ checks: [],
86
+ remediations: [],
87
+ report: "",
88
+ elapsedMs: Date.now() - started,
89
+ };
90
+ }
91
+
92
+ const checks = [];
93
+
94
+ const toolchainOk = goAvailable(execFile);
95
+ checks.push({
96
+ name: "go-toolchain",
97
+ passed: toolchainOk,
98
+ detected: toolchainOk ? "go" : null,
99
+ remediation: toolchainOk ? null : REMEDIATION.missingGoToolchain,
100
+ });
101
+
102
+ const goModOk = hasGoMod(projectPath);
103
+ checks.push({
104
+ name: "go-mod",
105
+ passed: goModOk,
106
+ remediation: goModOk ? null : REMEDIATION.missingGoMod,
107
+ });
108
+
109
+ // Priority: missing toolchain is the most actionable failure.
110
+ let remediation = null;
111
+ if (!toolchainOk) remediation = REMEDIATION.missingGoToolchain;
112
+ else if (!goModOk) remediation = REMEDIATION.missingGoMod;
113
+
114
+ const passed = toolchainOk && goModOk;
115
+ const elapsedMs = Date.now() - started;
116
+ const remediations = checks.filter((c) => !c.passed && c.remediation).map((c) => c.remediation);
117
+
118
+ return {
119
+ passed,
120
+ remediation,
121
+ ready: passed,
122
+ skipped: false,
123
+ checks,
124
+ remediations,
125
+ report: buildReport(checks, passed, elapsedMs),
126
+ elapsedMs,
127
+ };
128
+ }
129
+
130
+ function buildReport(checks, ready, elapsedMs) {
131
+ const rows = checks.map((c) => {
132
+ const status = c.passed ? "PASS" : "FAIL";
133
+ const detail = c.detected || "";
134
+ return ` ${status.padEnd(4)} ${c.name.padEnd(24)} ${detail}`;
135
+ });
136
+ return (
137
+ "Bridge Layer 0 — Go Readiness\n" +
138
+ "──────────────────────────────────────\n" +
139
+ rows.join("\n") +
140
+ `\n──────────────────────────────────────\n Overall: ${
141
+ ready ? "READY" : "NOT READY"
142
+ } (${elapsedMs}ms)`
143
+ );
144
+ }
145
+
146
+ // ─── Monorepo detection (Layer 1 helper) ────────────────────────────────────
147
+
148
+ /**
149
+ * Walk the project tree looking for nested `go.mod` files (excluding the
150
+ * root `go.mod`). Returns the list of module-root-relative directories.
151
+ * Stops at common vendor / build / node_modules directories to keep the
152
+ * scan cheap.
153
+ *
154
+ * @param {string} projectPath
155
+ * @returns {string[]} nested module directories (relative), excluding root
156
+ */
157
+ function findNestedModules(projectPath) {
158
+ const skipDirs = new Set([
159
+ "node_modules",
160
+ "vendor",
161
+ ".git",
162
+ "build",
163
+ "dist",
164
+ "target",
165
+ ".idea",
166
+ ".vscode",
167
+ ]);
168
+ const results = [];
169
+
170
+ function walk(dir, depth) {
171
+ if (depth > 6) return; // depth guard
172
+ let entries;
173
+ try {
174
+ entries = readdirSync(dir, { withFileTypes: true });
175
+ } catch {
176
+ return;
177
+ }
178
+ for (const entry of entries) {
179
+ if (!entry.isDirectory()) continue;
180
+ if (skipDirs.has(entry.name)) continue;
181
+ if (entry.name.startsWith(".")) continue;
182
+ const sub = join(dir, entry.name);
183
+ if (existsSync(join(sub, "go.mod"))) {
184
+ results.push(relative(projectPath, sub));
185
+ }
186
+ walk(sub, depth + 1);
187
+ }
188
+ }
189
+
190
+ walk(projectPath, 0);
191
+ return results;
192
+ }
193
+
194
+ // ─── Layer 1: discoverRunners ───────────────────────────────────────────────
195
+
196
+ /**
197
+ * Discover Go runners (AC3, AC7).
198
+ *
199
+ * Single-module projects emit one `go test -json ./...` runner.
200
+ * Monorepos (nested go.mod detected) emit one runner per module plus
201
+ * the root runner when the root itself is a module.
202
+ *
203
+ * @param {string} projectPath
204
+ * @param {object} [manifest]
205
+ * @returns {Promise<object>}
206
+ */
207
+ async function discoverRunners(projectPath /*, manifest */) {
208
+ if (!projectPath || typeof projectPath !== "string") {
209
+ throw new TypeError("discoverRunners: projectPath is required");
210
+ }
211
+
212
+ if (!hasGoMod(projectPath)) {
213
+ return {
214
+ status: "error",
215
+ message: "No go.mod found at the project root.",
216
+ };
217
+ }
218
+
219
+ const nested = findNestedModules(projectPath);
220
+
221
+ // Single-module project — flat runner manifest.
222
+ if (nested.length === 0) {
223
+ const primary = {
224
+ runner_name: "go-test",
225
+ command: DEFAULT_COMMAND,
226
+ source: "go.mod",
227
+ tier_mapping: { tier: "unit", gates: [] },
228
+ tier: "unit",
229
+ };
230
+ return {
231
+ status: "ok",
232
+ primary,
233
+ manifest: {
234
+ mode: "single-module",
235
+ primary_runner: primary,
236
+ runners: [primary],
237
+ tiers: { unit: { description: "Go unit tests (go test -json ./...)" } },
238
+ },
239
+ };
240
+ }
241
+
242
+ // Monorepo — one runner per module (root + nested).
243
+ const modules = ["."].concat(nested);
244
+ const runners = modules.map((modPath) => {
245
+ const label = modPath === "." ? "root" : modPath;
246
+ return {
247
+ runner_name: `go-test:${label}`,
248
+ command: DEFAULT_COMMAND,
249
+ source: join(modPath === "." ? "" : modPath, "go.mod"),
250
+ module: modPath,
251
+ cwd: modPath === "." ? "." : modPath,
252
+ tier_mapping: { tier: "unit", gates: [] },
253
+ tier: "unit",
254
+ };
255
+ });
256
+ const primary = runners[0];
257
+
258
+ return {
259
+ status: "ok",
260
+ primary,
261
+ manifest: {
262
+ mode: "multi-module",
263
+ modules,
264
+ primary_runner: primary,
265
+ runners,
266
+ tiers: { unit: { description: "Go unit tests per-module" } },
267
+ },
268
+ };
269
+ }
270
+
271
+ // ─── Build tag scanner (Layer 1 helper) ─────────────────────────────────────
272
+
273
+ const buildTagCache = new Map(); // projectPath → Map<filePath, tags[]>
274
+
275
+ /**
276
+ * Extract `//go:build <expr>` tags from the top of a `_test.go` file.
277
+ * Only scans the first 20 non-empty lines (build tags must appear before
278
+ * the package clause). Returns an array of individual tag tokens.
279
+ *
280
+ * @param {string} filePath
281
+ * @returns {string[]}
282
+ */
283
+ function scanFileBuildTags(filePath) {
284
+ let text;
285
+ try {
286
+ text = readFileSync(filePath, "utf8");
287
+ } catch {
288
+ return [];
289
+ }
290
+ const lines = text.split(/\r?\n/).slice(0, 40);
291
+ const tags = [];
292
+ for (const raw of lines) {
293
+ const line = raw.trim();
294
+ if (line === "") continue;
295
+ if (line.startsWith("package ")) break;
296
+ const m = /^\/\/go:build\s+(.+)$/.exec(line);
297
+ if (m) {
298
+ // Split on logical operators to extract tag identifiers.
299
+ const tokens = m[1].split(/\s+|\|\||&&|!|\(|\)/).filter(Boolean);
300
+ tags.push(...tokens);
301
+ continue;
302
+ }
303
+ // Legacy `// +build` syntax — supported defensively.
304
+ const legacy = /^\/\/\s*\+build\s+(.+)$/.exec(line);
305
+ if (legacy) {
306
+ const tokens = legacy[1].split(/\s+|,/).filter(Boolean);
307
+ tags.push(...tokens);
308
+ }
309
+ }
310
+ return Array.from(new Set(tags));
311
+ }
312
+
313
+ /**
314
+ * Scan a project for all `*_test.go` files and their build tags.
315
+ * Results are cached per project path.
316
+ *
317
+ * @param {string} projectPath
318
+ * @returns {Map<string, string[]>} file path → tags[]
319
+ */
320
+ function scanProjectBuildTags(projectPath) {
321
+ if (buildTagCache.has(projectPath)) return buildTagCache.get(projectPath);
322
+ const out = new Map();
323
+ const skipDirs = new Set([
324
+ "node_modules",
325
+ "vendor",
326
+ ".git",
327
+ "build",
328
+ "dist",
329
+ "target",
330
+ ".idea",
331
+ ".vscode",
332
+ ]);
333
+
334
+ function walk(dir, depth) {
335
+ if (depth > 8) return;
336
+ let entries;
337
+ try {
338
+ entries = readdirSync(dir, { withFileTypes: true });
339
+ } catch {
340
+ return;
341
+ }
342
+ for (const entry of entries) {
343
+ const sub = join(dir, entry.name);
344
+ if (entry.isDirectory()) {
345
+ if (skipDirs.has(entry.name) || entry.name.startsWith(".")) continue;
346
+ walk(sub, depth + 1);
347
+ continue;
348
+ }
349
+ if (entry.isFile() && entry.name.endsWith("_test.go")) {
350
+ out.set(sub, scanFileBuildTags(sub));
351
+ }
352
+ }
353
+ }
354
+
355
+ walk(projectPath, 0);
356
+ buildTagCache.set(projectPath, out);
357
+ return out;
358
+ }
359
+
360
+ /**
361
+ * Map a `*_test.go` file to a tier based on its build tags.
362
+ *
363
+ * @param {string[]} tags
364
+ * @param {object} [tagMapping] - { integration: string, e2e: string } override
365
+ * @returns {"unit"|"integration"|"e2e"}
366
+ */
367
+ function mapTagsToTier(tags, tagMapping = DEFAULT_BUILD_TAGS) {
368
+ if (!tags || tags.length === 0) return "unit";
369
+ if (tags.includes(tagMapping.e2e)) return "e2e";
370
+ if (tags.includes(tagMapping.integration)) return "integration";
371
+ return "unit";
372
+ }
373
+
374
+ /**
375
+ * Resolve build-tag tier mapping for a project. Exposed for AC6 testing
376
+ * and for downstream consumers (E25-S6 per-stack tier mapping).
377
+ *
378
+ * @param {string} projectPath
379
+ * @param {object} [options]
380
+ * @param {object} [options.stackHints] - test-environment.yaml tiers.stack_hints.go_build_tags override
381
+ * @returns {{ tierByFile: Map<string,string>, mapping: object }}
382
+ */
383
+ function resolveTierMapping(projectPath, options = {}) {
384
+ // E25-S6: `stackHints` accepts either a tag-mapping object (legacy shape
385
+ // used internally by this adapter — { integration: "integration", e2e: "e2e" })
386
+ // or the array form from test-environment.yaml
387
+ // (`tiers.stack_hints.go_build_tags: ["integration", "e2e"]`). When the
388
+ // array form is supplied, the first entry is treated as the integration
389
+ // tag and the second as the e2e tag, matching Dev Notes semantics.
390
+ let mapping;
391
+ let tierSource;
392
+ if (Array.isArray(options.stackHints)) {
393
+ const hints = options.stackHints;
394
+ mapping = {
395
+ integration: hints[0] || DEFAULT_BUILD_TAGS.integration,
396
+ e2e: hints[1] || DEFAULT_BUILD_TAGS.e2e,
397
+ };
398
+ tierSource = "stack_hints";
399
+ } else if (options.stackHints && typeof options.stackHints === "object") {
400
+ mapping = { ...DEFAULT_BUILD_TAGS, ...options.stackHints };
401
+ tierSource = "stack_hints";
402
+ } else {
403
+ mapping = DEFAULT_BUILD_TAGS;
404
+ tierSource = "adapter_default";
405
+ }
406
+ const tagged = scanProjectBuildTags(projectPath);
407
+ const tierByFile = new Map();
408
+ for (const [file, tags] of tagged.entries()) {
409
+ tierByFile.set(file, mapTagsToTier(tags, mapping));
410
+ }
411
+ // E25-S6 evidence entries — one per tier with tier_source recorded so
412
+ // downstream evidence files can record whether the resolution came from a
413
+ // project hint or the adapter default (ADR-038 §10.20.11).
414
+ const entries = [
415
+ { tier: "unit", tag: null, tier_source: tierSource },
416
+ { tier: "integration", tag: mapping.integration, tier_source: tierSource },
417
+ { tier: "e2e", tag: mapping.e2e, tier_source: tierSource },
418
+ ];
419
+ return { tierByFile, mapping, entries };
420
+ }
421
+
422
+ // ─── Layer 3: streaming JSON parser ─────────────────────────────────────────
423
+
424
+ /**
425
+ * Split a `go test -json` stdout buffer into parsed event objects.
426
+ * Silently skips non-JSON lines (Go occasionally emits plain text).
427
+ *
428
+ * @param {string} stdout
429
+ * @returns {Array<object>}
430
+ */
431
+ function parseJsonEventStream(stdout) {
432
+ if (!stdout || typeof stdout !== "string") return [];
433
+ const events = [];
434
+ const lines = stdout.split(/\r?\n/);
435
+ for (const line of lines) {
436
+ const trimmed = line.trim();
437
+ if (!trimmed || trimmed.charAt(0) !== "{") continue;
438
+ try {
439
+ const obj = JSON.parse(trimmed);
440
+ if (obj && typeof obj === "object") events.push(obj);
441
+ } catch {
442
+ // Skip malformed event — common with truncated panic output.
443
+ }
444
+ }
445
+ return events;
446
+ }
447
+
448
+ /**
449
+ * Correlate `go test -json` events into per-test records keyed by
450
+ * (Package, Test). Package-level events (no Test field) contribute to
451
+ * package summaries but are not emitted as test records.
452
+ *
453
+ * @param {Array<object>} events
454
+ * @returns {{ tests: Array<object>, packageSummaries: Map<string, object> }}
455
+ */
456
+ function correlateEvents(events) {
457
+ const byKey = new Map();
458
+ const pkgSummary = new Map();
459
+
460
+ for (const ev of events) {
461
+ const pkg = ev.Package || "";
462
+ const action = ev.Action || "";
463
+
464
+ if (!ev.Test) {
465
+ // Package-level event.
466
+ if (!pkgSummary.has(pkg)) {
467
+ pkgSummary.set(pkg, { status: null, elapsed: 0 });
468
+ }
469
+ if (["pass", "fail", "skip"].includes(action)) {
470
+ const entry = pkgSummary.get(pkg);
471
+ entry.status = action;
472
+ if (typeof ev.Elapsed === "number") entry.elapsed = ev.Elapsed;
473
+ }
474
+ continue;
475
+ }
476
+
477
+ const key = `${pkg}\u0000${ev.Test}`;
478
+ let rec = byKey.get(key);
479
+ if (!rec) {
480
+ rec = {
481
+ package: pkg,
482
+ test: ev.Test,
483
+ name: `${pkg}.${ev.Test}`,
484
+ status: "running",
485
+ duration_ms: 0,
486
+ output: [],
487
+ };
488
+ byKey.set(key, rec);
489
+ }
490
+
491
+ if (action === "output" && typeof ev.Output === "string") {
492
+ rec.output.push(ev.Output);
493
+ } else if (action === "pass") {
494
+ rec.status = "passed";
495
+ if (typeof ev.Elapsed === "number") {
496
+ rec.duration_ms = Math.round(ev.Elapsed * 1000);
497
+ }
498
+ } else if (action === "fail") {
499
+ rec.status = "failed";
500
+ if (typeof ev.Elapsed === "number") {
501
+ rec.duration_ms = Math.round(ev.Elapsed * 1000);
502
+ }
503
+ } else if (action === "skip") {
504
+ rec.status = "skipped";
505
+ if (typeof ev.Elapsed === "number") {
506
+ rec.duration_ms = Math.round(ev.Elapsed * 1000);
507
+ }
508
+ }
509
+ }
510
+
511
+ const tests = [];
512
+ for (const rec of byKey.values()) {
513
+ const entry = {
514
+ package: rec.package,
515
+ name: rec.name,
516
+ status: rec.status === "running" ? "error" : rec.status,
517
+ duration_ms: rec.duration_ms,
518
+ };
519
+ if (entry.status === "failed" || entry.status === "error") {
520
+ const joined = rec.output.join("");
521
+ if (joined) {
522
+ entry.failure_message =
523
+ joined.length > RAW_OUTPUT_SNIPPET_MAX ? joined.slice(0, RAW_OUTPUT_SNIPPET_MAX) : joined;
524
+ }
525
+ }
526
+ tests.push(entry);
527
+ }
528
+ return { tests, packageSummaries: pkgSummary };
529
+ }
530
+
531
+ /**
532
+ * Parse `go test -json` output (AC4, AC5).
533
+ *
534
+ * @param {string} stdout
535
+ * @param {string} stderr
536
+ * @param {number} exitCode
537
+ * @param {object} [options]
538
+ * @returns {object}
539
+ */
540
+ function parseOutput(stdout, stderr, exitCode /*, options = {} */) {
541
+ const stdoutStr = typeof stdout === "string" ? stdout : "";
542
+ const stderrStr = typeof stderr === "string" ? stderr : "";
543
+
544
+ const events = parseJsonEventStream(stdoutStr);
545
+
546
+ // No parseable events at all — fall back to parse_error record.
547
+ if (events.length === 0) {
548
+ return {
549
+ parse_error: true,
550
+ stderr_snippet: stderrStr.slice(0, STDERR_SNIPPET_MAX),
551
+ summary: { total: 0, passed: 0, failed: 0, skipped: 0 },
552
+ tests: [],
553
+ exit_code: exitCode,
554
+ };
555
+ }
556
+
557
+ const { tests } = correlateEvents(events);
558
+ const summary = {
559
+ total: tests.length,
560
+ passed: tests.filter((t) => t.status === "passed").length,
561
+ failed: tests.filter((t) => t.status === "failed").length,
562
+ skipped: tests.filter((t) => t.status === "skipped").length,
563
+ };
564
+
565
+ // AC5: partial / panic-truncated stream handling.
566
+ // Non-zero exit with at least one running/error record OR a stderr panic:
567
+ // emit partial evidence record with raw_output_snippet.
568
+ const hasRunning = tests.some((t) => t.status === "error");
569
+ const stderrHasPanic = /\bpanic:/i.test(stderrStr);
570
+ if (exitCode !== 0 && (hasRunning || stderrHasPanic)) {
571
+ return {
572
+ parse_error: false,
573
+ status: "error",
574
+ raw_output_snippet: stderrStr.slice(0, RAW_OUTPUT_SNIPPET_MAX),
575
+ summary,
576
+ tests,
577
+ exit_code: exitCode,
578
+ };
579
+ }
580
+
581
+ return { summary, tests, exit_code: exitCode };
582
+ }
583
+
584
+ // ─── Export ─────────────────────────────────────────────────────────────────
585
+
586
+ /**
587
+ * @type {import('./index.js').StackAdapter}
588
+ */
589
+ const goAdapter = {
590
+ name: "go",
591
+ detectionPatterns: DETECTION_PATTERNS,
592
+ // Single required file — default ALL semantics are fine.
593
+ readinessCheck,
594
+ discoverRunners,
595
+ parseOutput,
596
+ // Exposed for E25-S6 per-stack tier mapping consumers.
597
+ resolveTierMapping,
598
+ };
599
+
600
+ export default goAdapter;