diff --git a/docs/releases/2.0.0-rc.1/preview-pack-manifest.md b/docs/releases/2.0.0-rc.1/preview-pack-manifest.md index 6f7f4010..c17ff744 100644 --- a/docs/releases/2.0.0-rc.1/preview-pack-manifest.md +++ b/docs/releases/2.0.0-rc.1/preview-pack-manifest.md @@ -16,6 +16,7 @@ surfaces, or posting announcements. | `docs/architecture/harness-adapter-compliance.md` | Adapter matrix and scorecard | Verified by `npm run harness:adapters -- --check` | | `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` | | `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --format json --allow-untracked docs/drafts/` | +| `scripts/preview-pack-smoke.js` | Deterministic preview-pack smoke gate | Verified by `npm run preview-pack:smoke` | | `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication | | `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch | | `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions | @@ -69,6 +70,7 @@ Run these from the exact release commit before publication: ```bash git status --short --branch node scripts/platform-audit.js --format json --allow-untracked docs/drafts/ +npm run preview-pack:smoke npm run harness:adapters -- --check npm run harness:audit -- --format json npm run observability:ready diff --git a/docs/releases/2.0.0-rc.1/publication-readiness.md b/docs/releases/2.0.0-rc.1/publication-readiness.md index 37fcbbd5..4eaabb7b 100644 --- a/docs/releases/2.0.0-rc.1/publication-readiness.md +++ b/docs/releases/2.0.0-rc.1/publication-readiness.md @@ -78,6 +78,7 @@ Record the exact commit SHA and command output before any publication action: | Evidence | Command | Required result | Recorded output | | --- | --- | --- | --- | | Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Pending final strict clean-checkout release pass; `publication-evidence-2026-05-17.md` records current `main` with unrelated untracked `docs/drafts/` | +| Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | Pending final strict clean-checkout release pass; deterministic smoke gate is in-tree | | Harness audit | `npm run harness:audit -- --format json` | 70/70 passing | `publication-evidence-2026-05-17.md`: 70/70 | | Adapter scorecard | `npm run harness:adapters -- --check` | PASS | `publication-evidence-2026-05-16.md`: PASS, 11 adapters | | Observability readiness | `npm run observability:ready` | 21/21 passing | `publication-evidence-2026-05-17.md`: 21/21, ready yes | diff --git a/package.json b/package.json index 4776e902..72d9735d 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "scripts/observability-readiness.js", "scripts/operator-readiness-dashboard.js", "scripts/platform-audit.js", + "scripts/preview-pack-smoke.js", "scripts/hooks/", "scripts/install-apply.js", "scripts/install-plan.js", @@ -300,6 +301,7 @@ "harness:audit": "node scripts/harness-audit.js", "observability:ready": "node scripts/observability-readiness.js", "operator:dashboard": "node scripts/operator-readiness-dashboard.js", + "preview-pack:smoke": "node scripts/preview-pack-smoke.js", "platform:audit": "node scripts/platform-audit.js", "discussion:audit": "node scripts/discussion-audit.js", "security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js", diff --git a/scripts/operator-readiness-dashboard.js b/scripts/operator-readiness-dashboard.js index 247f0606..1bb1e081 100644 --- a/scripts/operator-readiness-dashboard.js +++ b/scripts/operator-readiness-dashboard.js @@ -469,6 +469,7 @@ function buildRequirements(rootDir, platformReport) { const publicationReadiness = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md'); const namingMatrix = readText(rootDir, 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md'); const previewManifest = readText(rootDir, 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md'); + const previewPackSmoke = readText(rootDir, 'scripts/preview-pack-smoke.js'); const progressSync = readText(rootDir, 'docs/architecture/progress-sync-contract.md'); const observabilityReadiness = readText(rootDir, 'docs/architecture/observability-readiness.md'); const stalePrSalvage = readText(rootDir, 'docs/stale-pr-salvage-ledger.md'); @@ -478,6 +479,22 @@ function buildRequirements(rootDir, platformReport) { const packageJson = readPackage(rootDir); const scripts = packageJson.scripts || {}; const legacyContext = { stalePrSalvage, legacyInventory, roadmap }; + const previewPackManifestReady = includesAll(previewManifest, [ + 'publication-readiness.md', + 'release-notes.md', + 'quickstart.md' + ]); + const previewPackSmokeReady = scripts['preview-pack:smoke'] === 'node scripts/preview-pack-smoke.js' + && fileExists(rootDir, 'scripts/preview-pack-smoke.js') + && includesAll(previewManifest, ['scripts/preview-pack-smoke.js', 'npm run preview-pack:smoke']) + && includesAll(previewPackSmoke, [ + 'ecc.preview-pack-smoke.v1', + 'preview-pack-artifacts-present', + 'hermes-boundary-sanitized', + 'publication-blockers-preserved' + ]); + const hermesArtifactsReady = fileExists(rootDir, 'docs/HERMES-SETUP.md') + && fileExists(rootDir, 'skills/hermes-imports/SKILL.md'); const githubLive = !platformReport.github.skipped && platformReport.github.totals.errors === 0; const queuesCurrent = githubLive @@ -535,23 +552,29 @@ function buildRequirements(rootDir, platformReport) { 'ecc-preview-pack', 'ECC 2.0 preview pack ready', 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md', - includesAll(previewManifest, ['publication-readiness.md', 'release-notes.md', 'quickstart.md']) ? 'in_progress' : 'not_complete', - includesAll(previewManifest, ['publication-readiness.md', 'release-notes.md', 'quickstart.md']) + previewPackManifestReady && previewPackSmokeReady ? 'current' : previewPackManifestReady ? 'in_progress' : 'not_complete', + previewPackManifestReady && previewPackSmokeReady + ? 'preview pack manifest and deterministic smoke gate are in-tree' + : previewPackManifestReady ? 'preview pack manifest is in-tree' : 'preview pack manifest is incomplete', - 'final clean-checkout release approval and publish evidence still pending' + previewPackManifestReady && previewPackSmokeReady + ? 'repeat clean-checkout preview-pack smoke before publication' + : 'final clean-checkout release approval and publish evidence still pending' ), buildRequirement( 'hermes-specialized-skills', 'Include Hermes specialized skills safely', 'docs/HERMES-SETUP.md and skills/hermes-imports/SKILL.md', - fileExists(rootDir, 'docs/HERMES-SETUP.md') && fileExists(rootDir, 'skills/hermes-imports/SKILL.md') - ? 'in_progress' - : 'not_complete', - fileExists(rootDir, 'docs/HERMES-SETUP.md') && fileExists(rootDir, 'skills/hermes-imports/SKILL.md') + hermesArtifactsReady && previewPackSmokeReady ? 'current' : hermesArtifactsReady ? 'in_progress' : 'not_complete', + hermesArtifactsReady && previewPackSmokeReady + ? 'Hermes setup/import artifacts are covered by preview-pack smoke' + : hermesArtifactsReady ? 'Hermes setup and import skill are present' : 'Hermes setup/import artifacts missing', - 'final preview-pack smoke and release review pending' + hermesArtifactsReady && previewPackSmokeReady + ? 'repeat preview-pack smoke before release review' + : 'final preview-pack smoke and release review pending' ), buildRequirement( 'naming-and-plugin-publication', diff --git a/scripts/preview-pack-smoke.js b/scripts/preview-pack-smoke.js new file mode 100644 index 00000000..f76e07aa --- /dev/null +++ b/scripts/preview-pack-smoke.js @@ -0,0 +1,353 @@ +#!/usr/bin/env node +'use strict'; + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const RELEASE = '2.0.0-rc.1'; +const RELEASE_DIR = `docs/releases/${RELEASE}`; +const SCHEMA_VERSION = 'ecc.preview-pack-smoke.v1'; + +const REQUIRED_ARTIFACTS = [ + 'README.md', + 'docs/HERMES-SETUP.md', + 'skills/hermes-imports/SKILL.md', + 'docs/architecture/cross-harness.md', + 'docs/architecture/harness-adapter-compliance.md', + 'docs/architecture/observability-readiness.md', + 'docs/architecture/progress-sync-contract.md', + 'scripts/preview-pack-smoke.js', + `${RELEASE_DIR}/release-notes.md`, + `${RELEASE_DIR}/quickstart.md`, + `${RELEASE_DIR}/launch-checklist.md`, + `${RELEASE_DIR}/publication-readiness.md`, + `${RELEASE_DIR}/publication-evidence-2026-05-15.md`, + `${RELEASE_DIR}/publication-evidence-2026-05-16.md`, + `${RELEASE_DIR}/publication-evidence-2026-05-17.md`, + `${RELEASE_DIR}/operator-readiness-dashboard-2026-05-17.md`, + `${RELEASE_DIR}/naming-and-publication-matrix.md`, + `${RELEASE_DIR}/x-thread.md`, + `${RELEASE_DIR}/linkedin-post.md`, + `${RELEASE_DIR}/article-outline.md`, + `${RELEASE_DIR}/telegram-handoff.md`, + `${RELEASE_DIR}/demo-prompts.md`, +]; + +const REQUIRED_VERIFICATION_COMMANDS = [ + 'git status --short --branch', + 'node scripts/platform-audit.js --format json --allow-untracked docs/drafts/', + 'npm run preview-pack:smoke', + 'npm run harness:adapters -- --check', + 'npm run harness:audit -- --format json', + 'npm run observability:ready', + 'npm run security:ioc-scan', + 'npm audit --audit-level=moderate', + 'npm audit signatures', + 'node tests/docs/ecc2-release-surface.test.js', + 'node tests/run-all.js', + 'cd ecc2 && cargo test', +]; + +const REQUIRED_PUBLICATION_BLOCKERS = [ + 'GitHub prerelease `v2.0.0-rc.1`', + 'npm `ecc-universal@2.0.0-rc.1`', + 'Claude plugin tag', + 'Codex repo-marketplace distribution evidence', + 'ECC Tools billing/product readiness', +]; + +const HERMES_BOUNDARY_MARKERS = [ + 'Public Release Candidate Scope', + 'ECC v2.0.0-rc.1 documents the Hermes surface', + 'Sanitization Checklist', + 'Do not ship raw workspace exports', + 'Output Contract', +]; + +function usage() { + console.log([ + 'Usage: node scripts/preview-pack-smoke.js [--format ] [--root ]', + '', + 'Deterministic smoke gate for the ECC 2.0 rc.1 preview pack.', + '', + 'Options:', + ' --format Output format (default: text)', + ' --root Repository root to inspect (default: cwd)', + ' --help, -h Show this help', + ].join('\n')); +} + +function readArgValue(args, index, flagName) { + const value = args[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`${flagName} requires a value`); + } + return value; +} + +function parseArgs(argv) { + const args = argv.slice(2); + const parsed = { + format: 'text', + help: false, + root: path.resolve(process.cwd()), + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + + if (arg === '--help' || arg === '-h') { + parsed.help = true; + continue; + } + + if (arg === '--format') { + parsed.format = readArgValue(args, index, arg).toLowerCase(); + index += 1; + continue; + } + + if (arg.startsWith('--format=')) { + parsed.format = arg.slice('--format='.length).toLowerCase(); + continue; + } + + if (arg === '--root') { + parsed.root = path.resolve(readArgValue(args, index, arg)); + index += 1; + continue; + } + + if (arg.startsWith('--root=')) { + parsed.root = path.resolve(arg.slice('--root='.length)); + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + if (!['text', 'json'].includes(parsed.format)) { + throw new Error(`Invalid format: ${parsed.format}. Use text or json.`); + } + + return parsed; +} + +function readText(rootDir, relativePath) { + try { + return fs.readFileSync(path.join(rootDir, relativePath), 'utf8'); + } catch (_error) { + return ''; + } +} + +function fileExists(rootDir, relativePath) { + return fs.existsSync(path.join(rootDir, relativePath)); +} + +function safeParseJson(text) { + if (!text.trim()) { + return null; + } + + try { + return JSON.parse(text); + } catch (_error) { + return null; + } +} + +function includesAll(text, needles) { + return needles.every(needle => text.includes(needle)); +} + +function lineNumberForIndex(text, index) { + return text.slice(0, index).split('\n').length; +} + +function findForbiddenContent(rootDir, relativePaths) { + const offenders = []; + const privatePathPattern = /\/Users\/(?!\.\.\.)[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/g; + + for (const relativePath of relativePaths) { + const text = readText(rootDir, relativePath); + if (!text) { + continue; + } + + for (const match of text.matchAll(privatePathPattern)) { + offenders.push({ + path: relativePath, + line: lineNumberForIndex(text, match.index), + marker: match[0], + }); + } + } + + return offenders; +} + +function makeCheck(id, status, evidence, fix) { + return { + id, + status, + evidence, + fix: status === 'pass' ? '' : fix, + }; +} + +function buildReport(options = {}) { + const rootDir = path.resolve(options.root || process.cwd()); + const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {}; + const packageScripts = packageJson.scripts || {}; + const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : []; + const manifestPath = `${RELEASE_DIR}/preview-pack-manifest.md`; + const manifest = readText(rootDir, manifestPath); + const hermesSetup = readText(rootDir, 'docs/HERMES-SETUP.md'); + const hermesSkill = readText(rootDir, 'skills/hermes-imports/SKILL.md'); + + const missingArtifacts = REQUIRED_ARTIFACTS.filter(relativePath => !fileExists(rootDir, relativePath)); + const unlistedArtifacts = REQUIRED_ARTIFACTS.filter(relativePath => !manifest.includes(`\`${relativePath}\``)); + const missingCommands = REQUIRED_VERIFICATION_COMMANDS.filter(command => !manifest.includes(command)); + const missingBlockers = REQUIRED_PUBLICATION_BLOCKERS.filter(blocker => !manifest.includes(blocker)); + const missingHermesMarkers = HERMES_BOUNDARY_MARKERS.filter(marker => !`${hermesSetup}\n${hermesSkill}`.includes(marker)); + const forbiddenContent = findForbiddenContent(rootDir, [ + ...REQUIRED_ARTIFACTS, + manifestPath, + 'docs/business/social-launch-copy.md', + ]); + + const checks = [ + makeCheck( + 'preview-pack-script-registered', + packageScripts['preview-pack:smoke'] === 'node scripts/preview-pack-smoke.js' + && packageFiles.includes('scripts/preview-pack-smoke.js') + && fileExists(rootDir, 'scripts/preview-pack-smoke.js') + ? 'pass' + : 'fail', + 'package script and npm package file entry for preview-pack smoke gate', + 'Add preview-pack:smoke to package scripts and include scripts/preview-pack-smoke.js in package files.' + ), + makeCheck( + 'preview-pack-artifacts-present', + missingArtifacts.length === 0 && unlistedArtifacts.length === 0 ? 'pass' : 'fail', + missingArtifacts.length === 0 && unlistedArtifacts.length === 0 + ? `${REQUIRED_ARTIFACTS.length} required artifacts exist and are listed in the manifest` + : `missing artifacts: ${missingArtifacts.join(', ') || 'none'}; unlisted artifacts: ${unlistedArtifacts.join(', ') || 'none'}`, + 'Restore missing preview-pack artifacts and list every required artifact in preview-pack-manifest.md.' + ), + makeCheck( + 'final-verification-commands-listed', + missingCommands.length === 0 ? 'pass' : 'fail', + missingCommands.length === 0 + ? `${REQUIRED_VERIFICATION_COMMANDS.length} final verification commands are listed` + : `missing commands: ${missingCommands.join('; ')}`, + 'Add the missing final verification commands to preview-pack-manifest.md.' + ), + makeCheck( + 'hermes-boundary-sanitized', + missingHermesMarkers.length === 0 && forbiddenContent.length === 0 ? 'pass' : 'fail', + missingHermesMarkers.length === 0 && forbiddenContent.length === 0 + ? 'Hermes setup and import skill preserve the public sanitization boundary' + : `missing markers: ${missingHermesMarkers.join(', ') || 'none'}; forbidden content: ${forbiddenContent.map(item => `${item.path}:${item.line}`).join(', ') || 'none'}`, + 'Restore Hermes sanitization language and remove private local paths from preview-pack docs.' + ), + makeCheck( + 'publication-blockers-preserved', + missingBlockers.length === 0 + && /approval-gated release, package, plugin, and\s+announcement steps/.test(manifest) + ? 'pass' + : 'fail', + missingBlockers.length === 0 + ? 'publication remains explicitly approval-gated' + : `missing blockers: ${missingBlockers.join(', ')}`, + 'Keep publication blockers explicit until the live release, package, plugin, and billing surfaces exist.' + ), + ]; + + const failed = checks.filter(check => check.status !== 'pass'); + const digest = crypto + .createHash('sha256') + .update(JSON.stringify(checks.map(check => [check.id, check.status, check.evidence]))) + .digest('hex') + .slice(0, 12); + + return { + schema_version: SCHEMA_VERSION, + release: RELEASE, + ready: failed.length === 0, + digest, + summary: { + passed: checks.length - failed.length, + failed: failed.length, + total: checks.length, + }, + checks, + }; +} + +function renderText(report) { + const lines = [ + 'ECC preview pack smoke', + `Release: ${report.release}`, + `Ready: ${report.ready ? 'yes' : 'no'}`, + `Digest: ${report.digest}`, + '', + 'Checks:', + ]; + + for (const check of report.checks) { + lines.push(`- ${check.status} ${check.id}: ${check.evidence}`); + if (check.fix) { + lines.push(` fix: ${check.fix}`); + } + } + + lines.push(''); + lines.push(`Passed: ${report.summary.passed}`); + lines.push(`Failed: ${report.summary.failed}`); + + return `${lines.join('\n')}\n`; +} + +function main() { + let parsed; + + try { + parsed = parseArgs(process.argv); + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } + + if (parsed.help) { + usage(); + return; + } + + const report = buildReport({ root: parsed.root }); + + if (parsed.format === 'json') { + console.log(JSON.stringify(report, null, 2)); + } else { + process.stdout.write(renderText(report)); + } + + if (!report.ready) { + process.exit(2); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { + REQUIRED_ARTIFACTS, + REQUIRED_PUBLICATION_BLOCKERS, + REQUIRED_VERIFICATION_COMMANDS, + buildReport, + parseArgs, + renderText, +}; diff --git a/tests/docs/ecc2-release-surface.test.js b/tests/docs/ecc2-release-surface.test.js index 9d506d8a..0193c8c7 100644 --- a/tests/docs/ecc2-release-surface.test.js +++ b/tests/docs/ecc2-release-surface.test.js @@ -171,6 +171,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', ( 'docs/HERMES-SETUP.md', 'skills/hermes-imports/SKILL.md', 'docs/architecture/harness-adapter-compliance.md', + 'scripts/preview-pack-smoke.js', 'docs/releases/2.0.0-rc.1/publication-readiness.md', 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md', ]) { @@ -189,6 +190,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', ( assert.ok(manifest.includes('no raw workspace exports')); assert.ok(manifest.includes('Final Verification Commands')); + assert.ok(manifest.includes('npm run preview-pack:smoke')); assert.ok(manifest.includes('Reference-Inspired Adapter Direction')); }); @@ -263,6 +265,8 @@ test('publication readiness checklist gates public release actions on evidence', } assert.ok(source.includes('publication-evidence-2026-05-15.md')); + assert.ok(source.includes('Preview-pack smoke')); + assert.ok(source.includes('npm run preview-pack:smoke')); assert.ok(may15Evidence.includes('PR #1921')); assert.ok(may15Evidence.includes('PR #1933')); assert.ok(may15Evidence.includes('PR #1934')); diff --git a/tests/scripts/npm-publish-surface.test.js b/tests/scripts/npm-publish-surface.test.js index df4042d4..7cb3857d 100644 --- a/tests/scripts/npm-publish-surface.test.js +++ b/tests/scripts/npm-publish-surface.test.js @@ -59,6 +59,7 @@ function buildExpectedPublishPaths(repoRoot) { "scripts/observability-readiness.js", "scripts/operator-readiness-dashboard.js", "scripts/platform-audit.js", + "scripts/preview-pack-smoke.js", "scripts/skill-create-output.js", "scripts/repair.js", "scripts/harness-adapter-compliance.js", @@ -129,6 +130,7 @@ function main() { "scripts/consult.js", "scripts/discussion-audit.js", "scripts/operator-readiness-dashboard.js", + "scripts/preview-pack-smoke.js", "scripts/work-items.js", "scripts/platform-audit.js", ".gemini/GEMINI.md", diff --git a/tests/scripts/operator-readiness-dashboard.test.js b/tests/scripts/operator-readiness-dashboard.test.js index 91a3c30e..6b4fac72 100644 --- a/tests/scripts/operator-readiness-dashboard.test.js +++ b/tests/scripts/operator-readiness-dashboard.test.js @@ -32,18 +32,26 @@ function seedRepo(rootDir, overrides = {}) { files: [ 'scripts/observability-readiness.js', 'scripts/operator-readiness-dashboard.js', - 'scripts/platform-audit.js' + 'scripts/platform-audit.js', + 'scripts/preview-pack-smoke.js' ], scripts: { 'discussion:audit': 'node scripts/discussion-audit.js', 'observability:ready': 'node scripts/observability-readiness.js', 'operator:dashboard': 'node scripts/operator-readiness-dashboard.js', 'platform:audit': 'node scripts/platform-audit.js', + 'preview-pack:smoke': 'node scripts/preview-pack-smoke.js', 'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js', 'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js' } }, null, 2), 'scripts/operator-readiness-dashboard.js': 'operator dashboard generator', + 'scripts/preview-pack-smoke.js': [ + 'ecc.preview-pack-smoke.v1', + 'preview-pack-artifacts-present', + 'hermes-boundary-sanitized', + 'publication-blockers-preserved' + ].join('\n'), 'docs/ECC-2.0-GA-ROADMAP.md': [ 'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1', 'Linear ITO-44 ITO-59', @@ -63,7 +71,11 @@ function seedRepo(rootDir, overrides = {}) { ].join('\n'), 'docs/releases/2.0.0-rc.1/publication-readiness.md': 'Claude plugin Codex plugin', 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md': 'Claude plugin Codex plugin npm package Publication Paths', - 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md', + 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': [ + 'publication-readiness.md release-notes.md quickstart.md', + '`scripts/preview-pack-smoke.js`', + 'npm run preview-pack:smoke' + ].join('\n'), 'docs/releases/2.0.0-rc.1/release-notes.md': 'release notes', 'docs/releases/2.0.0-rc.1/x-thread.md': 'x thread', 'docs/releases/2.0.0-rc.1/linkedin-post.md': 'linkedin post', @@ -77,8 +89,8 @@ function seedRepo(rootDir, overrides = {}) { 'PR queue', 'Not complete' ].join('\n'), - 'docs/HERMES-SETUP.md': 'Hermes setup', - 'skills/hermes-imports/SKILL.md': 'Hermes imports', + 'docs/HERMES-SETUP.md': 'Hermes setup Public Release Candidate Scope', + 'skills/hermes-imports/SKILL.md': 'Hermes imports Sanitization Checklist Do not ship raw workspace exports Output Contract', 'docs/stale-pr-salvage-ledger.md': [ 'Remaining Manual-Review Backlog', 'Linear ITO-55', @@ -218,6 +230,18 @@ function runTests() { assert.strictEqual(report.ready, false); assert.strictEqual(report.publicationReady, false); assert.ok(report.requirements.some(item => item.id === 'completion-dashboard' && item.status === 'complete')); + assert.ok(report.requirements.some(item => ( + item.id === 'ecc-preview-pack' + && item.status === 'current' + && item.evidence.includes('deterministic smoke gate') + && item.gap === 'repeat clean-checkout preview-pack smoke before publication' + ))); + assert.ok(report.requirements.some(item => ( + item.id === 'hermes-specialized-skills' + && item.status === 'current' + && item.evidence.includes('covered by preview-pack smoke') + && item.gap === 'repeat preview-pack smoke before release review' + ))); assert.ok(report.requirements.some(item => item.id === 'ecc-tools-next-level' && item.status === 'in_progress')); assert.ok(report.requirements.some(item => ( item.id === 'agentshield-enterprise-iteration' @@ -253,6 +277,8 @@ function runTests() { && item.gap === 'repeat Linear/project status update and local work-items sync after each significant merge batch' ))); assert.ok(report.top_actions.some(item => item.id === 'naming-and-plugin-publication')); + assert.ok(!report.top_actions.some(item => item.id === 'ecc-preview-pack')); + assert.ok(!report.top_actions.some(item => item.id === 'hermes-specialized-skills')); assert.ok(!report.top_actions.some(item => item.id === 'legacy-salvage')); assert.ok(!report.top_actions.some(item => item.id === 'linear-roadmap-and-progress')); } finally { @@ -287,6 +313,45 @@ function runTests() { } })) passed++; else failed++; + if (test('preview pack and Hermes gates stay in progress until smoke gate is wired', () => { + const rootDir = createTempDir('operator-dashboard-preview-smoke-'); + + try { + seedRepo(rootDir, { + 'package.json': JSON.stringify({ + files: [ + 'scripts/observability-readiness.js', + 'scripts/operator-readiness-dashboard.js', + 'scripts/platform-audit.js' + ], + scripts: { + 'discussion:audit': 'node scripts/discussion-audit.js', + 'observability:ready': 'node scripts/observability-readiness.js', + 'operator:dashboard': 'node scripts/operator-readiness-dashboard.js', + 'platform:audit': 'node scripts/platform-audit.js', + 'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js', + 'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js' + } + }, null, 2), + 'scripts/preview-pack-smoke.js': null, + 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md' + }); + + const report = buildSeededReport(rootDir); + const previewPack = report.requirements.find(item => item.id === 'ecc-preview-pack'); + const hermes = report.requirements.find(item => item.id === 'hermes-specialized-skills'); + + assert.strictEqual(previewPack.status, 'in_progress'); + assert.strictEqual(previewPack.gap, 'final clean-checkout release approval and publish evidence still pending'); + assert.strictEqual(hermes.status, 'in_progress'); + assert.strictEqual(hermes.gap, 'final preview-pack smoke and release review pending'); + assert.ok(report.top_actions.some(item => item.id === 'ecc-preview-pack')); + assert.ok(report.top_actions.some(item => item.id === 'hermes-specialized-skills')); + } finally { + cleanup(rootDir); + } + })) passed++; else failed++; + if (test('AgentShield enterprise evidence covers export and policy promotion markers', () => { const cases = [ { diff --git a/tests/scripts/preview-pack-smoke.test.js b/tests/scripts/preview-pack-smoke.test.js new file mode 100644 index 00000000..8048d059 --- /dev/null +++ b/tests/scripts/preview-pack-smoke.test.js @@ -0,0 +1,260 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { execFileSync, spawnSync } = require('child_process'); + +const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'preview-pack-smoke.js'); +const { + REQUIRED_ARTIFACTS, + REQUIRED_PUBLICATION_BLOCKERS, + REQUIRED_VERIFICATION_COMMANDS, + buildReport, + parseArgs, + renderText, +} = require(SCRIPT); + +const RELEASE_DIR = 'docs/releases/2.0.0-rc.1'; + +function createTempDir(prefix) { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function cleanup(dirPath) { + fs.rmSync(dirPath, { recursive: true, force: true }); +} + +function writeFile(rootDir, relativePath, content) { + const targetPath = path.join(rootDir, relativePath); + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, content); +} + +function manifestContent() { + return [ + '# ECC v2.0.0-rc.1 Preview Pack Manifest', + '', + '## Pack Contents', + '', + '| Artifact | Role | Gate |', + '| --- | --- | --- |', + ...REQUIRED_ARTIFACTS.map(artifact => `| \`${artifact}\` | release artifact | checked |`), + '', + '## Hermes Skill Boundary', + '', + '- no raw workspace exports;', + '', + '## Final Verification Commands', + '', + '```bash', + ...REQUIRED_VERIFICATION_COMMANDS, + '```', + '', + '## Publication Blockers', + '', + ...REQUIRED_PUBLICATION_BLOCKERS.map(blocker => `- ${blocker}`), + '', + 'The preview pack is not public without approval-gated release, package, plugin, and announcement steps.', + ].join('\n'); +} + +function seedRepo(rootDir, overrides = {}) { + const files = { + 'package.json': JSON.stringify({ + files: ['scripts/preview-pack-smoke.js'], + scripts: { + 'preview-pack:smoke': 'node scripts/preview-pack-smoke.js', + }, + }, null, 2), + 'scripts/preview-pack-smoke.js': 'preview pack smoke script', + [`${RELEASE_DIR}/preview-pack-manifest.md`]: manifestContent(), + 'docs/HERMES-SETUP.md': [ + '# Hermes Setup', + 'Public Release Candidate Scope', + 'ECC v2.0.0-rc.1 documents the Hermes surface', + 'No raw workspace export is included.', + ].join('\n'), + 'skills/hermes-imports/SKILL.md': [ + '---', + 'name: hermes-imports', + '---', + 'Sanitization Checklist', + 'Do not ship raw workspace exports', + 'Output Contract', + ].join('\n'), + }; + + for (const artifact of REQUIRED_ARTIFACTS) { + if (!Object.prototype.hasOwnProperty.call(files, artifact)) { + files[artifact] = `${artifact} public preview-pack content`; + } + } + + for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) { + if (content === null) { + continue; + } + writeFile(rootDir, relativePath, content); + } +} + +function run(args = [], options = {}) { + return execFileSync('node', [SCRIPT, ...args], { + cwd: options.cwd || path.join(__dirname, '..', '..'), + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 10000, + }); +} + +function runProcess(args = [], options = {}) { + return spawnSync('node', [SCRIPT, ...args], { + cwd: options.cwd || path.join(__dirname, '..', '..'), + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 10000, + }); +} + +function test(name, fn) { + try { + fn(); + console.log(` PASS ${name}`); + return true; + } catch (error) { + console.log(` FAIL ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +function runTests() { + console.log('\n=== Testing preview-pack-smoke.js ===\n'); + + let passed = 0; + let failed = 0; + + if (test('parseArgs accepts smoke flags and rejects invalid values', () => { + const rootDir = createTempDir('preview-pack-smoke-args-'); + + try { + const parsed = parseArgs([ + 'node', + 'script', + '--format=json', + `--root=${rootDir}`, + ]); + + assert.strictEqual(parsed.format, 'json'); + assert.strictEqual(parsed.root, path.resolve(rootDir)); + assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/); + assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/); + assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/); + } finally { + cleanup(rootDir); + } + })) passed++; else failed++; + + if (test('seeded release pack passes every smoke check', () => { + const rootDir = createTempDir('preview-pack-smoke-pass-'); + + try { + seedRepo(rootDir); + const report = buildReport({ root: rootDir }); + + assert.strictEqual(report.schema_version, 'ecc.preview-pack-smoke.v1'); + assert.strictEqual(report.ready, true); + assert.strictEqual(report.summary.failed, 0); + assert.ok(report.checks.every(check => check.status === 'pass')); + + const text = renderText(report); + assert.ok(text.includes('Ready: yes')); + assert.ok(text.includes('Passed: 5')); + assert.ok(text.includes('Failed: 0')); + } finally { + cleanup(rootDir); + } + })) passed++; else failed++; + + if (test('script registration fails closed without package wiring', () => { + const rootDir = createTempDir('preview-pack-smoke-package-'); + + try { + seedRepo(rootDir, { + 'package.json': JSON.stringify({ files: [], scripts: {} }, null, 2), + }); + + const report = buildReport({ root: rootDir }); + const registration = report.checks.find(check => check.id === 'preview-pack-script-registered'); + + assert.strictEqual(report.ready, false); + assert.strictEqual(registration.status, 'fail'); + assert.ok(registration.fix.includes('preview-pack:smoke')); + } finally { + cleanup(rootDir); + } + })) passed++; else failed++; + + if (test('Hermes boundary fails closed on private local paths', () => { + const rootDir = createTempDir('preview-pack-smoke-private-path-'); + + try { + seedRepo(rootDir, { + [`${RELEASE_DIR}/quickstart.md`]: 'Do not ship /Users/affoon/private-state in public docs.', + }); + + const report = buildReport({ root: rootDir }); + const boundary = report.checks.find(check => check.id === 'hermes-boundary-sanitized'); + + assert.strictEqual(report.ready, false); + assert.strictEqual(boundary.status, 'fail'); + assert.ok(boundary.evidence.includes(`${RELEASE_DIR}/quickstart.md:1`)); + } finally { + cleanup(rootDir); + } + })) passed++; else failed++; + + if (test('CLI emits json and uses status 2 for failed smoke reports', () => { + const rootDir = createTempDir('preview-pack-smoke-cli-'); + + try { + seedRepo(rootDir); + const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir }); + const parsed = JSON.parse(stdout); + assert.strictEqual(parsed.ready, true); + + writeFile(rootDir, 'package.json', JSON.stringify({ files: [], scripts: {} }, null, 2)); + const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir }); + assert.strictEqual(failedRun.status, 2); + assert.strictEqual(failedRun.stderr, ''); + assert.ok(failedRun.stdout.includes('"ready": false')); + } finally { + cleanup(rootDir); + } + })) passed++; else failed++; + + if (test('CLI help exits successfully and invalid flags fail before reporting', () => { + const help = runProcess(['--help']); + assert.strictEqual(help.status, 0); + assert.strictEqual(help.stderr, ''); + assert.ok(help.stdout.includes('Usage: node scripts/preview-pack-smoke.js')); + + const invalid = runProcess(['--format=xml']); + assert.strictEqual(invalid.status, 1); + assert.strictEqual(invalid.stdout, ''); + assert.match(invalid.stderr, /Error: Invalid format/); + })) passed++; else failed++; + + console.log(`\nPassed: ${passed}`); + console.log(`Failed: ${failed}`); + + if (failed > 0) { + process.exit(1); + } +} + +if (require.main === module) { + runTests(); +}