@decohere/ci-tools 0.0.1-dev.125 → 0.0.1-dev.13

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 (40) hide show
  1. package/package.json +3 -18
  2. package/src/audit/enforce.mjs +5 -7
  3. package/src/audit/summarize.mjs +32 -10
  4. package/src/audit/upsert-issue.mjs +74 -25
  5. package/src/bench/bench.mjs +368 -134
  6. package/src/bench/trace.mjs +14 -18
  7. package/src/coverage/enforce-thresholds.mjs +144 -0
  8. package/src/pages/artifact.mjs +183 -38
  9. package/src/pages/clear-bench-history.mjs +27 -0
  10. package/src/pages/keep-prior-bench-badge.mjs +38 -0
  11. package/src/pages/restore-bench-history.mjs +28 -15
  12. package/src/release/npm-links.mjs +13 -6
  13. package/src/release/sync-dependencies.mjs +2 -1
  14. package/tests/lib/lcov.test.mjs +4 -4
  15. package/LICENSE +0 -15
  16. package/src/audit/format.mjs +0 -133
  17. package/src/bench/file-regressions.mjs +0 -76
  18. package/src/bench/history.mjs +0 -248
  19. package/src/bench/nightly.mjs +0 -118
  20. package/src/bench/regression-issue.mjs +0 -44
  21. package/src/github/credential-nudge.mjs +0 -226
  22. package/src/github/npm-bootstrap.mjs +0 -179
  23. package/src/github/oidc-bootstrap-nudge.mjs +0 -214
  24. package/src/github/publish-verify.mjs +0 -206
  25. package/src/github/smoke-create-launcher.mjs +0 -70
  26. package/src/lib/github-issue.mjs +0 -75
  27. package/src/lib/workspace-packages.mjs +0 -30
  28. package/src/pages/bench-page.mjs +0 -323
  29. package/src/pages/restore-badges.mjs +0 -58
  30. package/tests/audit/format.test.mjs +0 -115
  31. package/tests/bench/history.test.mjs +0 -279
  32. package/tests/bench/regression-issue.test.mjs +0 -42
  33. package/tests/github/credential-nudge.test.mjs +0 -112
  34. package/tests/github/npm-bootstrap.test.mjs +0 -125
  35. package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -89
  36. package/tests/github/package-metadata.test.mjs +0 -69
  37. package/tests/github/publish-verify.test.mjs +0 -115
  38. package/tests/github/smoke-create-launcher.test.mjs +0 -71
  39. package/tests/lib/github-issue.test.mjs +0 -85
  40. package/tests/pages/bench-page.test.mjs +0 -123
@@ -0,0 +1,144 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { parseLcovByProject } from "../lib/lcov.mjs";
5
+
6
+ function parseArgs(argv) {
7
+ const options = {
8
+ lcov: ".forge/coverage/lcov.info",
9
+ thresholds: null,
10
+ thresholdsModule: null,
11
+ projectsDir: "projects",
12
+ };
13
+
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const arg = argv[index];
16
+ if (arg === "--lcov") {
17
+ options.lcov = argv[index + 1] ?? options.lcov;
18
+ index += 1;
19
+ continue;
20
+ }
21
+ if (arg === "--thresholds") {
22
+ options.thresholds = argv[index + 1] ?? options.thresholds;
23
+ index += 1;
24
+ continue;
25
+ }
26
+ if (arg === "--thresholds-module") {
27
+ options.thresholdsModule = argv[index + 1] ?? options.thresholdsModule;
28
+ index += 1;
29
+ continue;
30
+ }
31
+ if (arg === "--projectsDir") {
32
+ options.projectsDir = argv[index + 1] ?? options.projectsDir;
33
+ index += 1;
34
+ }
35
+ }
36
+
37
+ return options;
38
+ }
39
+
40
+ async function resolveThresholds(options) {
41
+ if (options.thresholdsModule) {
42
+ const moduleUrl = pathToFileURL(resolve(options.thresholdsModule)).href;
43
+ const mod = await import(moduleUrl);
44
+ const thresholds = mod.projectCoverageThresholds;
45
+ if (typeof thresholds !== "object" || thresholds == null) {
46
+ console.error(
47
+ `Module ${options.thresholdsModule} must export a 'projectCoverageThresholds' object.`,
48
+ );
49
+ process.exit(1);
50
+ }
51
+ return thresholds;
52
+ }
53
+
54
+ const path = options.thresholds ?? "coverage.config.json";
55
+ if (!existsSync(path)) {
56
+ console.error(`Thresholds config not found: ${path}`);
57
+ process.exit(1);
58
+ }
59
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
60
+ if (typeof parsed !== "object" || parsed == null) {
61
+ console.error(
62
+ "Thresholds config must be a JSON object mapping project => minimum coverage percentage.",
63
+ );
64
+ process.exit(1);
65
+ }
66
+ return parsed;
67
+ }
68
+
69
+ function formatPct(value) {
70
+ return `${value.toFixed(2)}%`;
71
+ }
72
+
73
+ async function main() {
74
+ const options = parseArgs(process.argv.slice(2));
75
+ const lcovPath = resolve(options.lcov);
76
+ const projectsDirPath = resolve(options.projectsDir);
77
+
78
+ if (!existsSync(lcovPath)) {
79
+ console.error(`LCOV file not found: ${lcovPath}`);
80
+ console.error("Run coverage first (example: npm run test:coverage)");
81
+ process.exit(1);
82
+ }
83
+
84
+ if (!existsSync(projectsDirPath)) {
85
+ console.error(`Projects directory not found: ${projectsDirPath}`);
86
+ process.exit(1);
87
+ }
88
+
89
+ const thresholds = await resolveThresholds(options);
90
+ const projectDirs = readdirSync(projectsDirPath, { withFileTypes: true })
91
+ .filter((entry) => entry.isDirectory())
92
+ .map((entry) => entry.name)
93
+ .sort((left, right) => left.localeCompare(right));
94
+
95
+ const coverageByProject = parseLcovByProject(readFileSync(lcovPath, "utf8"));
96
+
97
+ const missingThresholds = projectDirs.filter(
98
+ (projectName) => thresholds[projectName] == null,
99
+ );
100
+ if (missingThresholds.length > 0) {
101
+ console.error("Missing coverage thresholds for projects:");
102
+ for (const projectName of missingThresholds) console.error(`- ${projectName}`);
103
+ console.error(
104
+ "Update projectCoverageThresholds in vitest.config.ts to include all project directories.",
105
+ );
106
+ process.exit(1);
107
+ }
108
+
109
+ const failures = [];
110
+ console.log("Project line coverage thresholds");
111
+
112
+ for (const projectName of projectDirs) {
113
+ const projectCoverage = coverageByProject.get(projectName) ?? {
114
+ totalLines: 0,
115
+ coveredLines: 0,
116
+ };
117
+ const currentPct =
118
+ projectCoverage.totalLines === 0
119
+ ? 0
120
+ : (projectCoverage.coveredLines / projectCoverage.totalLines) * 100;
121
+ const minimumPct = Number(thresholds[projectName]);
122
+ const passed = Number.isFinite(minimumPct) && currentPct >= minimumPct;
123
+
124
+ console.log(
125
+ `${passed ? "✅" : "❌"} ${projectName}: ${formatPct(currentPct)} (min ${formatPct(minimumPct)})`,
126
+ );
127
+
128
+ if (!passed) failures.push({ projectName, currentPct, minimumPct });
129
+ }
130
+
131
+ if (failures.length > 0) {
132
+ console.error("\nCoverage threshold check failed:");
133
+ for (const failure of failures) {
134
+ console.error(
135
+ `- ${failure.projectName}: ${formatPct(failure.currentPct)} is below ${formatPct(failure.minimumPct)}`,
136
+ );
137
+ }
138
+ process.exit(1);
139
+ }
140
+
141
+ console.log("\nAll project coverage thresholds passed.");
142
+ }
143
+
144
+ main();
@@ -1,8 +1,5 @@
1
- import { existsSync } from "node:fs";
2
- import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { normalize } from "../bench/history.mjs";
5
- import { buildBenchmarkHtml, buildStylesCss } from "./bench-page.mjs";
6
3
 
7
4
  const branch = process.env.GITHUB_REF_NAME ?? "local";
8
5
  const sha = process.env.GITHUB_SHA ?? "local";
@@ -11,53 +8,201 @@ const outputDir = process.env.PAGES_OUTPUT_DIR ?? "site";
11
8
  const badgesDir = "projects/ci-tools/dist/ci/badges";
12
9
  const benchHistoryPath = "projects/ci-tools/dist/ci/bench/bench-history.json";
13
10
 
14
- /** Every badge file under `dir`, relative to it: the manifest restore-badges.mjs reads back. */
15
- async function listBadges(dir, prefix = "") {
16
- const entries = await readdir(path.join(dir, prefix), { withFileTypes: true });
17
- const nested = await Promise.all(
18
- entries.map((entry) =>
19
- entry.isDirectory()
20
- ? listBadges(dir, path.posix.join(prefix, entry.name))
21
- : entry.name.endsWith(".svg")
22
- ? [path.posix.join(prefix, entry.name)]
23
- : [],
24
- ),
25
- );
26
- return nested.flat().sort();
11
+ const numericFields = ["hz", "mean", "p99", "rme", "sampleCount"];
12
+
13
+ function fmtMs(ms) {
14
+ if (typeof ms !== "number" || Number.isNaN(ms)) return "-";
15
+ if (ms >= 100) return `${Math.round(ms)} ms`;
16
+ return `${ms.toFixed(1)} ms`;
17
+ }
18
+
19
+ function fmtHz(hz) {
20
+ if (typeof hz !== "number" || Number.isNaN(hz)) return "-";
21
+ return `${hz.toFixed(2)}/s`;
22
+ }
23
+
24
+ function averageBenchmarks(runs) {
25
+ const first = runs[0]?.vitestOutput;
26
+ if (first == null) return [];
27
+
28
+ const latestBenchmarks = first.files
29
+ .flatMap((file) => file.groups)
30
+ .flatMap((group) => group.benchmarks);
31
+
32
+ return latestBenchmarks.map((benchmark) => {
33
+ const peers = runs
34
+ .flatMap((run) => run.vitestOutput?.files ?? [])
35
+ .flatMap((file) => file.groups ?? [])
36
+ .flatMap((group) => group.benchmarks ?? [])
37
+ .filter((candidate) => candidate.name === benchmark.name);
38
+
39
+ const averaged = { name: benchmark.name };
40
+ for (const field of numericFields) {
41
+ const values = peers
42
+ .map((peer) => peer[field])
43
+ .filter((value) => typeof value === "number");
44
+ if (values.length > 0) {
45
+ averaged[field] = values.reduce((sum, value) => sum + value, 0) / values.length;
46
+ }
47
+ }
48
+
49
+ return averaged;
50
+ });
51
+ }
52
+
53
+ export function buildBenchmarkHtml(benchHistory) {
54
+ const contexts = Object.entries(benchHistory.runs ?? {})
55
+ .filter(([, entry]) => Array.isArray(entry.runs) && entry.runs.length > 0)
56
+ .sort((a, b) => new Date(b[1].runs[0].date) - new Date(a[1].runs[0].date));
57
+
58
+ if (contexts.length === 0) {
59
+ return `<!doctype html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="utf-8" />
63
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
64
+ <title>Ark - Benchmarks</title>
65
+ <link rel="stylesheet" href="./styles.css" />
66
+ </head>
67
+ <body>
68
+ <main class="container">
69
+ <header class="hero">
70
+ <h1>Benchmarks</h1>
71
+ <p class="note">No benchmark history is available yet.</p>
72
+ </header>
73
+ </main>
74
+ </body>
75
+ </html>
76
+ `;
77
+ }
78
+
79
+ const rows = [];
80
+ for (const [, entry] of contexts) {
81
+ const benchmarks = averageBenchmarks(entry.runs);
82
+ const ctx = entry.context;
83
+ const environment = `${ctx.benchEnv ?? "unknown"}-${ctx.os.arch}`;
84
+ for (const benchmark of benchmarks) {
85
+ rows.push(`<tr>
86
+ <td>${environment}</td>
87
+ <td><code>${benchmark.name}</code></td>
88
+ <td>${fmtMs(benchmark.mean)}</td>
89
+ <td>${typeof benchmark.rme === "number" ? `±${benchmark.rme.toFixed(1)}%` : "-"}</td>
90
+ <td>${fmtMs(benchmark.p99)}</td>
91
+ <td>${fmtHz(benchmark.hz)}</td>
92
+ <td>${typeof benchmark.sampleCount === "number" ? Math.round(benchmark.sampleCount) : "-"}</td>
93
+ </tr>`);
94
+ }
95
+ }
96
+
97
+ const details = contexts
98
+ .map(([, entry]) => {
99
+ const ctx = entry.context;
100
+ return `<li><strong>${ctx.benchEnv ?? "unknown"}-${ctx.os.arch}</strong> - ${ctx.os.platform} - ${ctx.cpu.model} x ${ctx.cpu.count} - ${ctx.memoryGb} GB - Node ${ctx.node} - rolling mean of ${entry.runs.length} run(s)</li>`;
101
+ })
102
+ .join("\n");
103
+
104
+ const updatedDate = contexts[0][1].runs[0].date.slice(0, 10);
105
+
106
+ return `<!doctype html>
107
+ <html lang="en">
108
+ <head>
109
+ <meta charset="utf-8" />
110
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
111
+ <title>Ark - Benchmarks</title>
112
+ <link rel="stylesheet" href="./styles.css" />
113
+ </head>
114
+ <body>
115
+ <main class="container">
116
+ <div class="hero">
117
+ <h1>Benchmarks</h1>
118
+ <p class="note">Updated ${updatedDate} · Branch: ${branch} · Commit: ${sha.slice(0, 12)}</p>
119
+ </div>
120
+ <section class="section">
121
+ <h2>Performance Table</h2>
122
+ <table>
123
+ <thead>
124
+ <tr>
125
+ <th>Environment</th>
126
+ <th>Benchmark</th>
127
+ <th>Mean</th>
128
+ <th>±RME</th>
129
+ <th>P99</th>
130
+ <th>Ops/s</th>
131
+ <th>Runs</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody>
135
+ ${rows.join("\n")}
136
+ </tbody>
137
+ </table>
138
+ </section>
139
+ <section class="section">
140
+ <h2>Environment Notes</h2>
141
+ <ul>
142
+ ${details}
143
+ </ul>
144
+ </section>
145
+ </main>
146
+ </body>
147
+ </html>
148
+ `;
149
+ }
150
+
151
+ function buildStylesCss() {
152
+ return `:root {
153
+ --bg: #f4f7f3;
154
+ --surface: #ffffff;
155
+ --text: #132b24;
156
+ --muted: #4f6f65;
157
+ --accent: #0f9d72;
158
+ --accent-strong: #0d7f5c;
159
+ --border: #d6e3dd;
160
+ --shadow: 0 14px 40px rgba(12, 58, 45, 0.08);
161
+ }
162
+ * { box-sizing: border-box; }
163
+ body { margin: 0; font-family: "Segoe UI", "Helvetica Neue", sans-serif; color: var(--text); background: radial-gradient(circle at top right, #dbefe7, var(--bg) 40%); }
164
+ a { color: var(--accent-strong); }
165
+ .container { max-width: 1100px; margin: 0 auto; padding: 28px 18px 60px; }
166
+ .hero { background: linear-gradient(145deg, #183a31, #0e7f5c); color: #f3fff8; padding: 26px; border-radius: 18px; box-shadow: var(--shadow); }
167
+ .hero h1 { margin: 6px 0 10px; }
168
+ .note { color: #d8f5eb; margin: 0; }
169
+ .section { margin-top: 26px; }
170
+ table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: 12px; overflow: hidden; }
171
+ th, td { border-bottom: 1px solid var(--border); padding: 9px 10px; text-align: left; }
172
+ th { color: var(--muted); font-weight: 600; background: #f3faf7; }
173
+ td code { color: var(--accent-strong); }
174
+ @media (max-width: 700px) { .hero { padding: 20px; } .container { padding: 18px 12px 40px; } }
175
+ `;
27
176
  }
28
177
 
29
178
  await rm(outputDir, { recursive: true, force: true });
30
- await mkdir(path.join(outputDir, "data"), { recursive: true });
179
+ await mkdir(outputDir, { recursive: true });
31
180
 
32
181
  await writeFile(path.join(outputDir, "styles.css"), buildStylesCss());
33
182
 
34
183
  // index.html redirects to benchmark.html — no separate dashboard needed
35
184
  await writeFile(
36
185
  path.join(outputDir, "index.html"),
37
- `<!doctype html><html><head><meta charset="utf-8"/><meta http-equiv="refresh" content="0;url=./benchmark.html"/><title>Decohere Pages</title></head><body><p><a href="./benchmark.html">Benchmarks</a></p></body></html>\n`,
186
+ `<!doctype html><html><head><meta charset="utf-8"/><meta http-equiv="refresh" content="0;url=./benchmark.html"/><title>Ark Pages</title></head><body><p><a href="./benchmark.html">Benchmarks</a></p></body></html>\n`,
38
187
  );
39
188
 
40
- // No history yet (first deploy, or cleared) publishes the empty page, not a failure.
41
- const benchHistory = normalize(
42
- existsSync(benchHistoryPath) ? JSON.parse(await readFile(benchHistoryPath, "utf8")) : null,
43
- );
44
- await writeFile(
45
- path.join(outputDir, "benchmark.html"),
46
- buildBenchmarkHtml(benchHistory, { branch, sha }),
47
- );
48
- await writeFile(
49
- path.join(outputDir, "data", "bench-history.json"),
50
- `${JSON.stringify(benchHistory, null, 2)}\n`,
51
- );
52
-
53
- // Badges are generated by earlier steps (CI status, coverage) or restored from the live site.
54
- if (existsSync(badgesDir)) {
55
- await cp(badgesDir, path.join(outputDir, "badges"), { recursive: true });
56
- const badges = await listBadges(badgesDir);
189
+ try {
190
+ const benchHistory = JSON.parse(await readFile(benchHistoryPath, "utf8"));
191
+ await writeFile(path.join(outputDir, "benchmark.html"), buildBenchmarkHtml(benchHistory));
192
+ const benchmarkDataDir = path.join(outputDir, "data");
193
+ await mkdir(benchmarkDataDir, { recursive: true });
57
194
  await writeFile(
58
- path.join(outputDir, "badges", "manifest.json"),
59
- `${JSON.stringify({ badges }, null, 2)}\n`,
195
+ path.join(benchmarkDataDir, "bench-history.json"),
196
+ `${JSON.stringify(benchHistory, null, 2)}\n`,
60
197
  );
198
+ } catch {
199
+ await writeFile(path.join(outputDir, "benchmark.html"), buildBenchmarkHtml({ runs: {} }));
200
+ }
201
+
202
+ try {
203
+ await cp(badgesDir, path.join(outputDir, "badges"), { recursive: true });
204
+ } catch {
205
+ // Badges may not exist yet for some runs.
61
206
  }
62
207
 
63
208
  await writeFile(path.join(outputDir, ".nojekyll"), "");
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Clear benchmark history for this publish (workflow_dispatch with clear-history=true)
4
+
5
+ import { writeFileSync } from "node:fs";
6
+ import { mkdir } from "node:fs/promises";
7
+
8
+ const HISTORY_DIR = "projects/ci-tools/dist/ci/bench";
9
+ const HISTORY_FILE = `${HISTORY_DIR}/bench-history.json`;
10
+
11
+ try {
12
+ await mkdir(HISTORY_DIR, { recursive: true });
13
+ writeFileSync(
14
+ HISTORY_FILE,
15
+ JSON.stringify(
16
+ {
17
+ runs: {},
18
+ },
19
+ null,
20
+ 2,
21
+ ),
22
+ );
23
+ console.log("✓ Cleared benchmark history");
24
+ } catch (error) {
25
+ console.error("Error clearing benchmark history:", error.message);
26
+ process.exit(1);
27
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Keep prior bench badge if no code changes; create fallback if none exists
4
+
5
+ import { execSync } from "node:child_process";
6
+ import { existsSync, writeFileSync } from "node:fs";
7
+ import { mkdir } from "node:fs/promises";
8
+
9
+ const BADGES_DIR = "projects/ci-tools/dist/ci/badges";
10
+ const BADGE_FILE = `${BADGES_DIR}/bench.svg`;
11
+ const PAGES_URL = "https://typesmiths.github.io/decohere/badges/bench.svg";
12
+
13
+ const FALLBACK_BADGE = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20" role="img" aria-label="bench: unavailable">
14
+ <title>bench: unavailable</title>
15
+ <rect width="120" height="20" fill="#555"/>
16
+ <rect x="46" width="74" height="20" fill="#9f9f9f"/>
17
+ <text x="24" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">bench</text>
18
+ <text x="83" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">unavailable</text>
19
+ </svg>`;
20
+
21
+ try {
22
+ if (!existsSync(BADGE_FILE)) {
23
+ await mkdir(BADGES_DIR, { recursive: true });
24
+
25
+ try {
26
+ execSync(`curl -fsSL "${PAGES_URL}" -o "${BADGE_FILE}"`, {
27
+ stdio: "pipe",
28
+ });
29
+ console.log("✓ Restored prior bench badge from Pages");
30
+ } catch {
31
+ console.warn("::warning::No prior bench badge found on Pages; creating fallback badge.");
32
+ writeFileSync(BADGE_FILE, FALLBACK_BADGE);
33
+ }
34
+ }
35
+ } catch (error) {
36
+ console.error("Error keeping prior bench badge:", error.message);
37
+ process.exit(1);
38
+ }
@@ -1,24 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Restore benchmark history from Pages. Only a 404 (never published, or cleared) starts an
4
- // empty history: any other failure stops the job, so a network blip can't wipe the trend on
5
- // the next save.
3
+ // Restore benchmark history from Pages, or initialize empty if not found
6
4
 
7
- import { mkdir, writeFile } from "node:fs/promises";
5
+ import { execSync } from "node:child_process";
6
+ import { writeFileSync } from "node:fs";
7
+ import { mkdir } from "node:fs/promises";
8
8
 
9
9
  const HISTORY_DIR = "projects/ci-tools/dist/ci/bench";
10
10
  const HISTORY_FILE = `${HISTORY_DIR}/bench-history.json`;
11
- const PAGES_URL = process.env.PAGES_URL ?? "https://typesmiths.github.io/decohere";
11
+ const PAGES_URL = "https://typesmiths.github.io/decohere/data/bench-history.json";
12
12
 
13
- await mkdir(HISTORY_DIR, { recursive: true });
14
- const response = await fetch(`${PAGES_URL}/data/bench-history.json`);
15
- if (response.status === 404) {
16
- console.log("No benchmark history on Pages; starting from empty history");
17
- await writeFile(HISTORY_FILE, `${JSON.stringify({ version: 2, contexts: {} }, null, 2)}\n`);
18
- } else if (!response.ok) {
19
- console.error(`Failed to restore benchmark history: HTTP ${response.status}`);
13
+ try {
14
+ await mkdir(HISTORY_DIR, { recursive: true });
15
+
16
+ try {
17
+ execSync(`curl -fsSL "${PAGES_URL}" -o "${HISTORY_FILE}"`, {
18
+ stdio: "pipe",
19
+ });
20
+ console.log("✓ Restored benchmark history from Pages");
21
+ } catch {
22
+ console.log("No benchmark history on Pages; starting from empty history");
23
+ writeFileSync(
24
+ HISTORY_FILE,
25
+ JSON.stringify(
26
+ {
27
+ runs: {},
28
+ },
29
+ null,
30
+ 2,
31
+ ),
32
+ );
33
+ }
34
+ } catch (error) {
35
+ console.error("Error restoring benchmark history:", error.message);
20
36
  process.exit(1);
21
- } else {
22
- await writeFile(HISTORY_FILE, await response.text());
23
- console.log("✓ Restored benchmark history from Pages");
24
37
  }
@@ -4,9 +4,8 @@
4
4
  * Outputs markdown formatted links for GitHub release notes
5
5
  */
6
6
 
7
- import { readFileSync } from "node:fs";
7
+ import { readdirSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { listProjectDirs } from "../lib/workspace-packages.mjs";
10
9
 
11
10
  const VERSION = process.argv[2] || process.env.VERSION;
12
11
 
@@ -16,12 +15,20 @@ if (!VERSION) {
16
15
  }
17
16
 
18
17
  let links = "## Published Packages\n\n";
18
+ const projectsDir = join(process.cwd(), "projects");
19
19
 
20
20
  try {
21
- for (const projectDir of listProjectDirs(process.cwd())) {
22
- const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
23
- if (pkg.name?.startsWith("@decohere/")) {
24
- links += `- [\`${pkg.name}@${VERSION}\`](https://www.npmjs.com/package/${pkg.name}/v/${VERSION})\n`;
21
+ const projects = readdirSync(projectsDir);
22
+
23
+ for (const project of projects) {
24
+ const pkgPath = join(projectsDir, project, "package.json");
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
27
+ if (pkg.name?.startsWith("@decohere/")) {
28
+ links += `- [\`${pkg.name}@${VERSION}\`](https://www.npmjs.com/package/${pkg.name}/v/${VERSION})\n`;
29
+ }
30
+ } catch {
31
+ // Skip projects without package.json
25
32
  }
26
33
  }
27
34
 
@@ -7,9 +7,10 @@
7
7
  import { execSync } from "node:child_process";
8
8
 
9
9
  const DRY_RUN = process.env.DRY_RUN === "true";
10
+ const TERMINAL_CLI = process.env.TERMINAL_CLI || "dist/debug/projects/terminal/src/cli.js";
10
11
  const DECOHERE_CONFIG = process.env.DECOHERE_CONFIG || ".decohere.js";
11
12
 
12
- const baseCmd = `pnpm forge --log-level info --config-file "${DECOHERE_CONFIG}" dependencies --configuration release sync`;
13
+ const baseCmd = `node "${TERMINAL_CLI}" --log-level trace --config-file "${DECOHERE_CONFIG}" forge dependencies --configuration release sync`;
13
14
 
14
15
  try {
15
16
  if (DRY_RUN) {
@@ -13,7 +13,7 @@ SF:projects/forge/src/build/index.ts
13
13
  LF:20
14
14
  LH:15
15
15
  end_of_record
16
- SF:projects/core/src/index.ts
16
+ SF:projects/nexus/src/index.ts
17
17
  LF:5
18
18
  LH:5
19
19
  end_of_record
@@ -46,9 +46,9 @@ describe("parseLcovByProject", () => {
46
46
  expect(result.get("forge")).toBeDefined();
47
47
  expect(result.get("forge").totalLines).toBe(30);
48
48
  expect(result.get("forge").coveredLines).toBe(24);
49
- expect(result.get("core")).toBeDefined();
50
- expect(result.get("core").totalLines).toBe(5);
51
- expect(result.get("core").coveredLines).toBe(5);
49
+ expect(result.get("nexus")).toBeDefined();
50
+ expect(result.get("nexus").totalLines).toBe(5);
51
+ expect(result.get("nexus").coveredLines).toBe(5);
52
52
  });
53
53
 
54
54
  it("ignores records with LF:0", () => {
package/LICENSE DELETED
@@ -1,15 +0,0 @@
1
- ISC License
2
-
3
- Copyright (c) 2025-2026 Typesmiths
4
-
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
8
-
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.