@pellux/goodvibes-toolchain 1.11.0

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 (80) hide show
  1. package/README.md +50 -0
  2. package/dist/bin/build-binaries.d.ts +3 -0
  3. package/dist/bin/build-binaries.d.ts.map +1 -0
  4. package/dist/bin/build-binaries.js +67 -0
  5. package/dist/bin/changelog-gate.d.ts +3 -0
  6. package/dist/bin/changelog-gate.d.ts.map +1 -0
  7. package/dist/bin/changelog-gate.js +13 -0
  8. package/dist/bin/coverage-gate.d.ts +3 -0
  9. package/dist/bin/coverage-gate.d.ts.map +1 -0
  10. package/dist/bin/coverage-gate.js +19 -0
  11. package/dist/bin/package-install-check.d.ts +3 -0
  12. package/dist/bin/package-install-check.d.ts.map +1 -0
  13. package/dist/bin/package-install-check.js +14 -0
  14. package/dist/bin/per-job-green.d.ts +3 -0
  15. package/dist/bin/per-job-green.d.ts.map +1 -0
  16. package/dist/bin/per-job-green.js +54 -0
  17. package/dist/bin/post-build-smoke.d.ts +3 -0
  18. package/dist/bin/post-build-smoke.d.ts.map +1 -0
  19. package/dist/bin/post-build-smoke.js +14 -0
  20. package/dist/bin/publish-package.d.ts +3 -0
  21. package/dist/bin/publish-package.d.ts.map +1 -0
  22. package/dist/bin/publish-package.js +25 -0
  23. package/dist/bin/release-cut.d.ts +3 -0
  24. package/dist/bin/release-cut.d.ts.map +1 -0
  25. package/dist/bin/release-cut.js +27 -0
  26. package/dist/bin/sdk-pin-gate.d.ts +3 -0
  27. package/dist/bin/sdk-pin-gate.d.ts.map +1 -0
  28. package/dist/bin/sdk-pin-gate.js +15 -0
  29. package/dist/bin/sha256sums.d.ts +3 -0
  30. package/dist/bin/sha256sums.d.ts.map +1 -0
  31. package/dist/bin/sha256sums.js +37 -0
  32. package/dist/bin/verification-ledger.d.ts +3 -0
  33. package/dist/bin/verification-ledger.d.ts.map +1 -0
  34. package/dist/bin/verification-ledger.js +28 -0
  35. package/dist/config.d.ts +147 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +62 -0
  38. package/dist/index.d.ts +23 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +22 -0
  41. package/dist/lib/build-binaries.d.ts +45 -0
  42. package/dist/lib/build-binaries.d.ts.map +1 -0
  43. package/dist/lib/build-binaries.js +98 -0
  44. package/dist/lib/changelog-gate.d.ts +18 -0
  45. package/dist/lib/changelog-gate.d.ts.map +1 -0
  46. package/dist/lib/changelog-gate.js +32 -0
  47. package/dist/lib/coverage-gate.d.ts +25 -0
  48. package/dist/lib/coverage-gate.d.ts.map +1 -0
  49. package/dist/lib/coverage-gate.js +46 -0
  50. package/dist/lib/effects.d.ts +53 -0
  51. package/dist/lib/effects.d.ts.map +1 -0
  52. package/dist/lib/effects.js +78 -0
  53. package/dist/lib/load-config.d.ts +8 -0
  54. package/dist/lib/load-config.d.ts.map +1 -0
  55. package/dist/lib/load-config.js +15 -0
  56. package/dist/lib/package-install-check.d.ts +49 -0
  57. package/dist/lib/package-install-check.d.ts.map +1 -0
  58. package/dist/lib/package-install-check.js +66 -0
  59. package/dist/lib/per-job-green.d.ts +54 -0
  60. package/dist/lib/per-job-green.d.ts.map +1 -0
  61. package/dist/lib/per-job-green.js +247 -0
  62. package/dist/lib/post-build-smoke.d.ts +24 -0
  63. package/dist/lib/post-build-smoke.d.ts.map +1 -0
  64. package/dist/lib/post-build-smoke.js +33 -0
  65. package/dist/lib/publish-package.d.ts +45 -0
  66. package/dist/lib/publish-package.d.ts.map +1 -0
  67. package/dist/lib/publish-package.js +58 -0
  68. package/dist/lib/release-cut.d.ts +52 -0
  69. package/dist/lib/release-cut.d.ts.map +1 -0
  70. package/dist/lib/release-cut.js +120 -0
  71. package/dist/lib/sdk-pin-gate.d.ts +25 -0
  72. package/dist/lib/sdk-pin-gate.d.ts.map +1 -0
  73. package/dist/lib/sdk-pin-gate.js +146 -0
  74. package/dist/lib/sha256sums.d.ts +41 -0
  75. package/dist/lib/sha256sums.d.ts.map +1 -0
  76. package/dist/lib/sha256sums.js +53 -0
  77. package/dist/lib/verification-ledger.d.ts +30 -0
  78. package/dist/lib/verification-ledger.d.ts.map +1 -0
  79. package/dist/lib/verification-ledger.js +37 -0
  80. package/package.json +59 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * release-cut — local release preparation ONLY.
3
+ *
4
+ * Per the CI/CD design's principle 4 (CI owns validation), this tool never
5
+ * re-runs gates. It: guards a clean tree on the release branch, bumps the
6
+ * version across the root and configured manifests, runs the repo's version-
7
+ * sync commands, prepends a CHANGELOG section, commits, and creates an
8
+ * annotated tag. Validation happened on the push CI run; the tag is cut from an
9
+ * already-green tree and verified by-reference downstream.
10
+ */
11
+ import { readFileSync, writeFileSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
13
+ import { realExec, consoleLogger } from './effects.js';
14
+ /** Parse an exact X.Y.Z string; throws on anything else. */
15
+ export function parseSemver(version) {
16
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
17
+ if (!match)
18
+ throw new Error(`Not an exact X.Y.Z version: ${version}`);
19
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
20
+ }
21
+ /** Compute the next version for a bump kind. */
22
+ export function nextVersion(current, kind) {
23
+ const { major, minor, patch } = parseSemver(current);
24
+ if (kind === 'major')
25
+ return `${major + 1}.0.0`;
26
+ if (kind === 'minor')
27
+ return `${major}.${minor + 1}.0`;
28
+ return `${major}.${minor}.${patch + 1}`;
29
+ }
30
+ /** Render a CHANGELOG section heading for a version. */
31
+ export function changelogHeading(version, style, date) {
32
+ return style === 'bracket' ? `## [${version}] - ${date}` : `## ${version} - ${date}`;
33
+ }
34
+ /** Build a full CHANGELOG section body from a heading and notes/commit lines. */
35
+ export function buildChangelogSection(version, style, date, bodyLines) {
36
+ const lines = bodyLines.length > 0 ? bodyLines : ['- Release maintenance.'];
37
+ return `${changelogHeading(version, style, date)}\n\n### Changes\n\n${lines.join('\n')}\n`;
38
+ }
39
+ /**
40
+ * Insert a new section into changelog text. `top` prepends above the first
41
+ * `## ` heading; `first-separator` inserts after the first `---` line.
42
+ */
43
+ export function insertChangelogSection(changelog, section, marker) {
44
+ if (marker === 'first-separator') {
45
+ const idx = changelog.indexOf('\n---\n');
46
+ if (idx === -1)
47
+ throw new Error('CHANGELOG has no "---" separator to insert after.');
48
+ const cut = idx + '\n---\n'.length;
49
+ return `${changelog.slice(0, cut)}\n${section}\n${changelog.slice(cut).replace(/^\n+/, '')}`;
50
+ }
51
+ const firstHeading = changelog.search(/^## /m);
52
+ if (firstHeading === -1)
53
+ return `${changelog.trimEnd()}\n\n${section}\n`;
54
+ return `${changelog.slice(0, firstHeading)}${section}\n\n${changelog.slice(firstHeading)}`;
55
+ }
56
+ function setManifestVersion(path, version) {
57
+ const text = readFileSync(path, 'utf8');
58
+ const manifest = JSON.parse(text);
59
+ manifest.version = version;
60
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
61
+ }
62
+ function requireClean(exec, cwd, branch) {
63
+ const status = exec('git', ['status', '--porcelain'], { cwd });
64
+ if (status.status !== 0)
65
+ throw new Error(`git status failed: ${status.stderr.trim()}`);
66
+ if (status.stdout.trim().length > 0) {
67
+ throw new Error(`Working tree is not clean:\n${status.stdout.trim()}`);
68
+ }
69
+ const current = exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
70
+ if (current.stdout.trim() !== branch) {
71
+ throw new Error(`Release cut must run on '${branch}' (on '${current.stdout.trim()}').`);
72
+ }
73
+ }
74
+ /**
75
+ * Perform a release cut. Uses real fs writes + the injected exec (git) against
76
+ * `cwd`; tests drive it against a temp-dir git fixture.
77
+ */
78
+ export function runReleaseCut(options) {
79
+ const exec = options.exec ?? realExec;
80
+ const logger = options.logger ?? consoleLogger;
81
+ const { cwd, config } = options;
82
+ const date = options.date ?? new Date().toISOString().slice(0, 10);
83
+ requireClean(exec, cwd, config.branch);
84
+ const rootManifestPath = resolve(cwd, 'package.json');
85
+ const current = JSON.parse(readFileSync(rootManifestPath, 'utf8')).version;
86
+ const version = nextVersion(current, options.bump);
87
+ const tag = `v${version}`;
88
+ logger.info(`[release-cut] ${current} -> ${version} (${options.bump})`);
89
+ if (options.dryRun) {
90
+ logger.info(`[release-cut] dry-run: would bump, changelog, commit, and tag ${tag}`);
91
+ return { version, tag, committed: false };
92
+ }
93
+ setManifestVersion(rootManifestPath, version);
94
+ for (const rel of config.versionFiles) {
95
+ setManifestVersion(resolve(cwd, rel), version);
96
+ }
97
+ for (const cmd of config.syncCommands) {
98
+ const [bin, ...args] = cmd;
99
+ if (!bin)
100
+ continue;
101
+ const res = exec(bin, args, { cwd });
102
+ if (res.status !== 0)
103
+ throw new Error(`version-sync command failed: ${cmd.join(' ')}\n${res.stderr}`);
104
+ }
105
+ const changelogPath = resolve(cwd, 'CHANGELOG.md');
106
+ const changelog = readFileSync(changelogPath, 'utf8');
107
+ const section = buildChangelogSection(version, config.changelogHeading, date, options.notes ?? []);
108
+ writeFileSync(changelogPath, insertChangelogSection(changelog, section, config.changelogInsertMarker));
109
+ const add = exec('git', ['add', ...config.commitPaths], { cwd });
110
+ if (add.status !== 0)
111
+ throw new Error(`git add failed: ${add.stderr}`);
112
+ const commit = exec('git', ['commit', '-m', `chore: release ${version}`], { cwd });
113
+ if (commit.status !== 0)
114
+ throw new Error(`git commit failed: ${commit.stderr}`);
115
+ const tagRes = exec('git', ['tag', '-a', tag, '-m', `release ${version}`], { cwd });
116
+ if (tagRes.status !== 0)
117
+ throw new Error(`git tag failed: ${tagRes.stderr}`);
118
+ logger.info(`[release-cut] committed and tagged ${tag}. Push with: git push && git push origin ${tag}`);
119
+ return { version, tag, committed: true };
120
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * sdk-pin-gate — verifies a consumer repo pins the SDK correctly.
3
+ *
4
+ * Absorbs three parallel copies: tui/scripts/publish-check.ts (pin in
5
+ * `dependencies`), agent/scripts/sdk-release-gates.ts (pin in
6
+ * `devDependencies`), and webui/scripts/release-gate.ts (adds the exports-map
7
+ * import sweep). The tri-agreement (pin ⇄ lockfile ⇄ installed) catches the
8
+ * "pin bumped but lockfile lagged, ships old SDK silently" failure class.
9
+ */
10
+ import type { FsReader } from './effects.js';
11
+ import { type SdkPinConfig } from '../config.js';
12
+ /** One gate outcome; ok=false carries a human-readable reason. */
13
+ export interface GateResult {
14
+ readonly id: string;
15
+ readonly ok: boolean;
16
+ readonly detail: string;
17
+ }
18
+ /** Read the SDK pin from the configured manifest group. */
19
+ export declare function readSdkPin(fs: FsReader, config: SdkPinConfig): string | null;
20
+ /**
21
+ * Run every sdk-pin gate and return one result per gate. Pure over the injected
22
+ * FsReader — no process exit, no console. The bin maps results to exit codes.
23
+ */
24
+ export declare function runSdkPinGate(fs: FsReader, partial: Partial<SdkPinConfig> | undefined): GateResult[];
25
+ //# sourceMappingURL=sdk-pin-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-pin-gate.d.ts","sourceRoot":"","sources":["../../src/lib/sdk-pin-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,KAAK,YAAY,EAAuB,MAAM,cAAc,CAAC;AAItE,kEAAkE;AAClE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAYD,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAK5E;AA4CD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,GAAG,UAAU,EAAE,CA8EpG"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * sdk-pin-gate — verifies a consumer repo pins the SDK correctly.
3
+ *
4
+ * Absorbs three parallel copies: tui/scripts/publish-check.ts (pin in
5
+ * `dependencies`), agent/scripts/sdk-release-gates.ts (pin in
6
+ * `devDependencies`), and webui/scripts/release-gate.ts (adds the exports-map
7
+ * import sweep). The tri-agreement (pin ⇄ lockfile ⇄ installed) catches the
8
+ * "pin bumped but lockfile lagged, ships old SDK silently" failure class.
9
+ */
10
+ import { resolveSdkPinConfig } from '../config.js';
11
+ const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
12
+ function readManifest(fs, path) {
13
+ return JSON.parse(fs.readText(path));
14
+ }
15
+ /** Read the SDK pin from the configured manifest group. */
16
+ export function readSdkPin(fs, config) {
17
+ const manifest = readManifest(fs, 'package.json');
18
+ const group = config.pinSource === 'devDependencies' ? manifest.devDependencies : manifest.dependencies;
19
+ const pin = group?.[config.sdkPackage];
20
+ return typeof pin === 'string' ? pin : null;
21
+ }
22
+ function collectImportSpecifiers(fs, roots, sdkPackage) {
23
+ const pattern = /(?:from\s+|require\(|import\()\s*['"]([^'"]*goodvibes-sdk[^'"]*)['"]/g;
24
+ const found = [];
25
+ const walk = (dir) => {
26
+ let entries;
27
+ try {
28
+ entries = fs.readDir(dir);
29
+ }
30
+ catch {
31
+ return;
32
+ }
33
+ for (const entry of entries) {
34
+ const path = `${dir}/${entry}`;
35
+ if (entry.endsWith('.ts') || entry.endsWith('.tsx')) {
36
+ const text = fs.readText(path);
37
+ for (const match of text.matchAll(pattern)) {
38
+ if (match[1])
39
+ found.push(match[1]);
40
+ }
41
+ }
42
+ else if (!entry.includes('.')) {
43
+ // No extension → treat as a subdirectory to recurse into.
44
+ walk(path);
45
+ }
46
+ }
47
+ };
48
+ for (const root of roots)
49
+ walk(root);
50
+ return found;
51
+ }
52
+ /** Build the set of allowed import specifiers from the installed SDK's exports map. */
53
+ function allowedExportSpecifiers(fs, sdkPackage) {
54
+ const installed = `node_modules/${sdkPackage}/package.json`;
55
+ if (!fs.exists(installed))
56
+ return null;
57
+ const manifest = readManifest(fs, installed);
58
+ if (!manifest.exports || typeof manifest.exports !== 'object')
59
+ return new Set([sdkPackage]);
60
+ const allow = new Set();
61
+ for (const key of Object.keys(manifest.exports)) {
62
+ if (key === '.')
63
+ allow.add(sdkPackage);
64
+ else if (key.startsWith('./'))
65
+ allow.add(`${sdkPackage}/${key.slice(2)}`);
66
+ }
67
+ allow.add(sdkPackage);
68
+ return allow;
69
+ }
70
+ /**
71
+ * Run every sdk-pin gate and return one result per gate. Pure over the injected
72
+ * FsReader — no process exit, no console. The bin maps results to exit codes.
73
+ */
74
+ export function runSdkPinGate(fs, partial) {
75
+ const config = resolveSdkPinConfig(partial);
76
+ const results = [];
77
+ const overlayPresent = fs.exists(config.overlayMarker);
78
+ results.push({
79
+ id: 'local-sdk-overlay-absent',
80
+ ok: !overlayPresent,
81
+ detail: overlayPresent
82
+ ? `dev-link overlay marker present at ${config.overlayMarker} — restore the published SDK before cutting`
83
+ : 'no dev-link overlay marker',
84
+ });
85
+ const pin = readSdkPin(fs, config);
86
+ const pinExact = pin !== null && EXACT_SEMVER.test(pin);
87
+ results.push({
88
+ id: 'sdk-pin-exact-semver',
89
+ ok: pinExact,
90
+ detail: pinExact
91
+ ? `${config.sdkPackage} pinned to ${pin}`
92
+ : `${config.sdkPackage} pin in ${config.pinSource} must be exact X.Y.Z (found: ${pin ?? 'missing'})`,
93
+ });
94
+ if (pinExact && pin) {
95
+ const installedPath = `node_modules/${config.sdkPackage}/package.json`;
96
+ const installedVersion = fs.exists(installedPath)
97
+ ? readManifest(fs, installedPath).version ?? null
98
+ : null;
99
+ results.push({
100
+ id: 'installed-matches-pin',
101
+ ok: installedVersion === pin,
102
+ detail: installedVersion === pin
103
+ ? `installed ${config.sdkPackage}@${installedVersion} matches pin`
104
+ : `installed ${config.sdkPackage}@${installedVersion ?? 'missing'} != pin ${pin} (run install)`,
105
+ });
106
+ const lockPresent = fs.exists(config.lockfile);
107
+ const lockResolves = lockPresent && fs.readText(config.lockfile).includes(`${config.sdkPackage}@${pin}`);
108
+ results.push({
109
+ id: 'lockfile-resolves-pin',
110
+ ok: lockResolves,
111
+ detail: lockResolves
112
+ ? `${config.lockfile} resolves ${config.sdkPackage}@${pin}`
113
+ : `${config.lockfile} does not resolve ${config.sdkPackage}@${pin} — lockfile lagged the pin bump`,
114
+ });
115
+ }
116
+ const specifiers = collectImportSpecifiers(fs, config.sourceRoots, config.sdkPackage);
117
+ const nonNpm = specifiers.filter((s) => !s.startsWith(config.sdkPackage));
118
+ results.push({
119
+ id: 'npm-specifier-only-imports',
120
+ ok: nonNpm.length === 0,
121
+ detail: nonNpm.length === 0
122
+ ? 'all SDK imports use the npm specifier'
123
+ : `non-npm SDK import(s): ${[...new Set(nonNpm)].join(', ')}`,
124
+ });
125
+ if (config.enforceExportsMap) {
126
+ const allow = allowedExportSpecifiers(fs, config.sdkPackage);
127
+ if (allow === null) {
128
+ results.push({
129
+ id: 'exports-map-only-imports',
130
+ ok: true,
131
+ detail: 'installed SDK package.json absent — exports-map check skipped',
132
+ });
133
+ }
134
+ else {
135
+ const offenders = specifiers.filter((s) => s.startsWith(config.sdkPackage) && !allow.has(s));
136
+ results.push({
137
+ id: 'exports-map-only-imports',
138
+ ok: offenders.length === 0,
139
+ detail: offenders.length === 0
140
+ ? 'all SDK subpath imports resolve to a published exports key'
141
+ : `SDK import(s) not in the published exports map: ${[...new Set(offenders)].join(', ')}`,
142
+ });
143
+ }
144
+ }
145
+ return results;
146
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * sha256sums — generate and verify a SHA256SUMS manifest over release assets.
3
+ *
4
+ * Emits the standard `<hex> <name>` format (two spaces, coreutils-compatible).
5
+ * A missing asset is a hard failure — a release must never ship an unlisted or
6
+ * absent binary.
7
+ */
8
+ export interface Sha256Entry {
9
+ /** Name recorded in the manifest (typically the asset basename). */
10
+ readonly name: string;
11
+ /** Absolute or root-relative path to hash. */
12
+ readonly path: string;
13
+ }
14
+ /** Reads a file's bytes; returns null when the file is absent. */
15
+ export type ReadBytes = (path: string) => Uint8Array | null;
16
+ /** Computes a lowercase hex sha256 of bytes. */
17
+ export type HashBytes = (bytes: Uint8Array) => string;
18
+ export interface Sha256SumsResult {
19
+ readonly ok: boolean;
20
+ /** Manifest text (only meaningful when ok). */
21
+ readonly manifest: string;
22
+ /** Entries whose file was missing. */
23
+ readonly missing: readonly string[];
24
+ }
25
+ /**
26
+ * Produce a SHA256SUMS manifest. Every entry must resolve to bytes; any missing
27
+ * file yields ok=false and is listed in `missing`.
28
+ */
29
+ export declare function generateSha256Sums(entries: readonly Sha256Entry[], readBytes: ReadBytes, hashBytes: HashBytes): Sha256SumsResult;
30
+ export interface VerifyResult {
31
+ readonly ok: boolean;
32
+ /** Names whose recomputed hash differs from the manifest. */
33
+ readonly mismatched: readonly string[];
34
+ /** Names present in the manifest but absent on disk. */
35
+ readonly missing: readonly string[];
36
+ }
37
+ /** Parse a manifest into name→hash pairs. */
38
+ export declare function parseSha256Manifest(manifest: string): Map<string, string>;
39
+ /** Verify assets against a manifest by recomputing each hash. */
40
+ export declare function verifySha256Sums(manifest: string, readBytes: ReadBytes, hashBytes: HashBytes): VerifyResult;
41
+ //# sourceMappingURL=sha256sums.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sha256sums.d.ts","sourceRoot":"","sources":["../../src/lib/sha256sums.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAAC;AAC5D,gDAAgD;AAChD,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,gBAAgB,CAehI;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,6CAA6C;AAC7C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOzE;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,YAAY,CAa3G"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * sha256sums — generate and verify a SHA256SUMS manifest over release assets.
3
+ *
4
+ * Emits the standard `<hex> <name>` format (two spaces, coreutils-compatible).
5
+ * A missing asset is a hard failure — a release must never ship an unlisted or
6
+ * absent binary.
7
+ */
8
+ /**
9
+ * Produce a SHA256SUMS manifest. Every entry must resolve to bytes; any missing
10
+ * file yields ok=false and is listed in `missing`.
11
+ */
12
+ export function generateSha256Sums(entries, readBytes, hashBytes) {
13
+ const missing = [];
14
+ const lines = [];
15
+ for (const entry of entries) {
16
+ const bytes = readBytes(entry.path);
17
+ if (bytes === null) {
18
+ missing.push(entry.name);
19
+ continue;
20
+ }
21
+ lines.push(`${hashBytes(bytes)} ${entry.name}`);
22
+ }
23
+ if (missing.length > 0) {
24
+ return { ok: false, manifest: '', missing };
25
+ }
26
+ return { ok: true, manifest: `${lines.join('\n')}\n`, missing: [] };
27
+ }
28
+ /** Parse a manifest into name→hash pairs. */
29
+ export function parseSha256Manifest(manifest) {
30
+ const map = new Map();
31
+ for (const line of manifest.split('\n')) {
32
+ const match = /^([0-9a-f]{64})\s+(.+)$/.exec(line.trim());
33
+ if (match?.[1] && match[2])
34
+ map.set(match[2], match[1]);
35
+ }
36
+ return map;
37
+ }
38
+ /** Verify assets against a manifest by recomputing each hash. */
39
+ export function verifySha256Sums(manifest, readBytes, hashBytes) {
40
+ const expected = parseSha256Manifest(manifest);
41
+ const mismatched = [];
42
+ const missing = [];
43
+ for (const [name, hash] of expected) {
44
+ const bytes = readBytes(name);
45
+ if (bytes === null) {
46
+ missing.push(name);
47
+ continue;
48
+ }
49
+ if (hashBytes(bytes) !== hash)
50
+ mismatched.push(name);
51
+ }
52
+ return { ok: mismatched.length === 0 && missing.length === 0, mismatched, missing };
53
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * verification-ledger — aggregation + rendering of a per-area verification
3
+ * inventory.
4
+ *
5
+ * The data collection is deeply repo-specific (each repo introspects its own
6
+ * settings/commands/panels), so it stays a per-repo hook. What is genuinely
7
+ * shared — the totals math and the JSON/Markdown rendering — lives here.
8
+ */
9
+ export interface LedgerArea {
10
+ readonly area: string;
11
+ readonly total: number;
12
+ readonly localSignal: number;
13
+ readonly localBehavior: number;
14
+ readonly externalRequired: number;
15
+ }
16
+ export interface LedgerTotals {
17
+ readonly total: number;
18
+ readonly localSignal: number;
19
+ readonly localBehavior: number;
20
+ readonly externalRequired: number;
21
+ readonly localSignalPercent: number;
22
+ readonly localBehaviorPercent: number;
23
+ }
24
+ /** Sum areas and compute coverage percentages. */
25
+ export declare function computeLedgerTotals(areas: readonly LedgerArea[]): LedgerTotals;
26
+ /** Render the ledger as Markdown. */
27
+ export declare function renderLedgerMarkdown(areas: readonly LedgerArea[]): string;
28
+ /** Render the ledger as JSON (areas + computed totals). */
29
+ export declare function renderLedgerJson(areas: readonly LedgerArea[]): string;
30
+ //# sourceMappingURL=verification-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-ledger.d.ts","sourceRoot":"","sources":["../../src/lib/verification-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,YAAY,CAO9E;AAED,qCAAqC;AACrC,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAczE;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAErE"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * verification-ledger — aggregation + rendering of a per-area verification
3
+ * inventory.
4
+ *
5
+ * The data collection is deeply repo-specific (each repo introspects its own
6
+ * settings/commands/panels), so it stays a per-repo hook. What is genuinely
7
+ * shared — the totals math and the JSON/Markdown rendering — lives here.
8
+ */
9
+ /** Sum areas and compute coverage percentages. */
10
+ export function computeLedgerTotals(areas) {
11
+ const total = areas.reduce((n, a) => n + a.total, 0);
12
+ const localSignal = areas.reduce((n, a) => n + a.localSignal, 0);
13
+ const localBehavior = areas.reduce((n, a) => n + a.localBehavior, 0);
14
+ const externalRequired = areas.reduce((n, a) => n + a.externalRequired, 0);
15
+ const pct = (n) => (total === 0 ? 0 : Math.round((n / total) * 1000) / 10);
16
+ return { total, localSignal, localBehavior, externalRequired, localSignalPercent: pct(localSignal), localBehaviorPercent: pct(localBehavior) };
17
+ }
18
+ /** Render the ledger as Markdown. */
19
+ export function renderLedgerMarkdown(areas) {
20
+ const totals = computeLedgerTotals(areas);
21
+ const rows = areas.map((a) => `| ${a.area} | ${a.total} | ${a.localSignal} | ${a.localBehavior} | ${a.externalRequired} |`);
22
+ return [
23
+ '# Verification Ledger',
24
+ '',
25
+ '| Area | Total | Local signal | Local behavior | External required |',
26
+ '| --- | ---: | ---: | ---: | ---: |',
27
+ ...rows,
28
+ `| **Total** | **${totals.total}** | **${totals.localSignal}** | **${totals.localBehavior}** | **${totals.externalRequired}** |`,
29
+ '',
30
+ `Local-signal coverage: ${totals.localSignalPercent}% · local-behavior coverage: ${totals.localBehaviorPercent}%`,
31
+ '',
32
+ ].join('\n');
33
+ }
34
+ /** Render the ledger as JSON (areas + computed totals). */
35
+ export function renderLedgerJson(areas) {
36
+ return `${JSON.stringify({ areas, totals: computeLedgerTotals(areas) }, null, 2)}\n`;
37
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@pellux/goodvibes-toolchain",
3
+ "version": "1.11.0",
4
+ "engines": {
5
+ "bun": "1.3.10",
6
+ "node": ">=22.0.0"
7
+ },
8
+ "description": "Shared GoodVibes CI/CD toolchain: parameterized release, publish, and verification tools (sdk-pin gate, build-binaries, release-cut, coverage-gate, per-job-green, and more) consumed by every GoodVibes repo.",
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./config": {
18
+ "types": "./dist/config.d.ts",
19
+ "import": "./dist/config.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "bin": {
24
+ "goodvibes-sdk-pin-gate": "./dist/bin/sdk-pin-gate.js",
25
+ "goodvibes-build-binaries": "./dist/bin/build-binaries.js",
26
+ "goodvibes-release-cut": "./dist/bin/release-cut.js",
27
+ "goodvibes-coverage-gate": "./dist/bin/coverage-gate.js",
28
+ "goodvibes-verification-ledger": "./dist/bin/verification-ledger.js",
29
+ "goodvibes-post-build-smoke": "./dist/bin/post-build-smoke.js",
30
+ "goodvibes-package-install-check": "./dist/bin/package-install-check.js",
31
+ "goodvibes-publish-package": "./dist/bin/publish-package.js",
32
+ "goodvibes-per-job-green": "./dist/bin/per-job-green.js",
33
+ "goodvibes-changelog-gate": "./dist/bin/changelog-gate.js",
34
+ "goodvibes-sha256sums": "./dist/bin/sha256sums.js"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "sideEffects": false,
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/mgd34msu/goodvibes-sdk.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/mgd34msu/goodvibes-sdk/issues"
47
+ },
48
+ "homepage": "https://github.com/mgd34msu/goodvibes-sdk",
49
+ "keywords": [
50
+ "goodvibes",
51
+ "toolchain",
52
+ "ci",
53
+ "release",
54
+ "publish"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }