diff --git a/.github/workflows/ecosystem-freshness.yml b/.github/workflows/ecosystem-freshness.yml new file mode 100644 index 0000000..74c1676 --- /dev/null +++ b/.github/workflows/ecosystem-freshness.yml @@ -0,0 +1,24 @@ +name: ecosystem-freshness + +on: + schedule: + # Nightly at 06:00 UTC + - cron: '0 6 * * *' + workflow_dispatch: + repository_dispatch: + types: [package-published] + +jobs: + live-verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - name: Ecosystem freshness (live) + run: npm run verify:ecosystem:live + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index f1b4be9..da89aba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ These three values appear across many files. If any of them change, every refere ## Spec source -The v0.1 specification lives in the canonical spec location: `https://github.com/workspace-json/agents-audit/tree/main/packages/spec` (see `spec.mdx` in that repo). Substantive changes to the spec go through the RFC process; this file is the rendered output of that process, not the place to draft proposals. +The v0.1 specification lives in the canonical spec location: `https://github.com/workspacejson/standard/tree/main/packages/spec` (see `spec.mdx` in that repo). Substantive changes to the spec go through the RFC process; this file is the rendered output of that process, not the place to draft proposals. ## Content files diff --git a/README.md b/README.md index ce724c5..47b677e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Site runs at `localhost:4321`. Corrections and improvements to the documentation are welcome via pull request. -Substantive changes to the specification itself should go through the RFC process at [github.com/workspace-json/agents-audit/tree/main/packages/spec](https://github.com/workspace-json/agents-audit/tree/main/packages/spec), not here. +Substantive changes to the specification itself should go through the RFC process at [github.com/workspacejson/standard/tree/main/packages/spec](https://github.com/workspacejson/standard/tree/main/packages/spec), not here. **Adding your tool to the implementations list:** If your tool reads or writes workspace.json, open a PR editing [`src/content/docs/implementations.mdx`](src/content/docs/implementations.mdx). Add one line with your tool name, a link, and one sentence on what it does. We list any tool that documents workspace.json support in its public docs. diff --git a/astro.config.mjs b/astro.config.mjs index 1b81d4d..620bdd0 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -52,7 +52,7 @@ export default defineConfig({ }, social: [ { icon: 'rss', label: 'RSS', href: '/blog/rss.xml' }, - { icon: 'github', label: 'GitHub', href: `https://github.com/${contract.tools.audit.repository}` }, + { icon: 'github', label: 'GitHub', href: `https://github.com/${contract.tools.audit.publicationRepository}` }, ], customCss: ['./src/styles/custom.css'], expressiveCode: { diff --git a/package.json b/package.json index b949b6a..846c49b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "npm run schema:verify && npm run content:verify && astro build", + "build": "npm run schema:verify && npm run content:verify && npm run verify:ecosystem && astro build", "build:local": "astro build", "preview": "astro preview", "astro": "astro", @@ -14,6 +14,8 @@ "schema:verify": "node scripts/materialize-schema.mjs --verify", "schema:verify-published": "node scripts/verify-published-schema.mjs", "content:verify": "node scripts/verify-content-contract.mjs", + "verify:ecosystem": "node scripts/verify-ecosystem.mjs", + "verify:ecosystem:live": "node scripts/verify-ecosystem.mjs --live", "verify:site": "npm run build && playwright test", "verify:site:contract": "playwright test --project=contract", "verify:site:update": "npm run build && UPDATE_BASELINES=1 playwright test --update-snapshots", diff --git a/public/schema/v1.json b/public/schema/v1.json index 4123d8b..4ef0119 100644 --- a/public/schema/v1.json +++ b/public/schema/v1.json @@ -57,7 +57,7 @@ }, "fileIndex": { "type": "object", - "description": "Per-file behavioral intelligence keyed by relative path. Read by Buildomator.", + "description": "Per-file behavioral intelligence keyed by repository-root-relative POSIX path (forward slashes, no leading \"./\", no drive letters).", "additionalProperties": { "type": "object", "properties": { @@ -74,7 +74,7 @@ "type": "object", "required": ["files", "rate", "occurrences", "generated"], "properties": { - "files": { "type": "array", "items": { "type": "string" }, "minItems": 2, "maxItems": 2 }, + "files": { "type": "array", "items": { "type": "string" }, "minItems": 2, "maxItems": 2, "description": "Unordered pair (set semantics — position is NOT meaningful; join by membership, not index). Each entry is a repository-root-relative POSIX path." }, "rate": { "type": "number", "minimum": 0, "maximum": 1 }, "occurrences": { "type": "integer", "minimum": 0 }, "generated": { "type": "boolean" } @@ -89,7 +89,7 @@ "type": "object", "required": ["file", "changeCount", "revertCount", "revertRate", "fragilityScore", "excluded"], "properties": { - "file": { "type": "string" }, + "file": { "type": "string", "description": "Repository-root-relative POSIX path (forward slashes, no leading \"./\")." }, "changeCount": { "type": "integer", "minimum": 0 }, "revertCount": { "type": "integer", "minimum": 0 }, "revertRate": { "type": "number", "minimum": 0, "maximum": 1 }, diff --git a/public/tally/product/tally_impact_screen.png b/public/tally/product/tally_impact_screen.png new file mode 100644 index 0000000..8cc13b5 Binary files /dev/null and b/public/tally/product/tally_impact_screen.png differ diff --git a/scripts/verify-content-contract.mjs b/scripts/verify-content-contract.mjs index 4d8b800..6d75bd1 100644 --- a/scripts/verify-content-contract.mjs +++ b/scripts/verify-content-contract.mjs @@ -140,6 +140,16 @@ if (!tallyPage.includes("from '../../../data/tally-showcase'")) { fail('tally showcase page must import from the typed contract (../../../data/tally-showcase)'); } +// Verify the page imports Image from astro:assets +if (!tallyPage.includes("from 'astro:assets'")) { + fail('tally showcase page must import Image from astro:assets for the product screenshot'); +} + +// Verify the page imports the product screenshot from src/assets +if (!tallyPage.includes("from '../../../assets/tally-impact-hero.png'")) { + fail('tally showcase page must import the product screenshot from src/assets/tally-impact-hero.png'); +} + // Verify the page uses narrativeActs from the contract if (!tallyPage.includes('narrativeActs')) { fail('tally showcase page must reference narrativeActs from the typed contract'); diff --git a/scripts/verify-ecosystem.mjs b/scripts/verify-ecosystem.mjs new file mode 100644 index 0000000..b09a990 --- /dev/null +++ b/scripts/verify-ecosystem.mjs @@ -0,0 +1,253 @@ +#!/usr/bin/env node + +/** + * Ecosystem freshness gate. + * + * Checks that sources/ecosystem-packages.json is consistent with: + * 1. npm registry dist-tags.latest for each package + * 2. The GitHub tag referenced by releaseRef exists + * 3. npm repository metadata matches declared publicationRepository + * 4. The hosted schema hash equals the pinned tarball schema + * 5. The contract.json tools section agrees with the manifest + * + * Usage: + * node scripts/verify-ecosystem.mjs # check manifest internal consistency + contract agreement + * node scripts/verify-ecosystem.mjs --live # also query npm registry and GitHub API (requires network) + */ + +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const LIVE = process.argv.includes('--live'); + +const manifest = JSON.parse( + readFileSync(path.join(root, 'sources/ecosystem-packages.json'), 'utf8'), +); +const contract = JSON.parse( + readFileSync(path.join(root, 'src/contract.json'), 'utf8'), +); + +const errors = []; +const warnings = []; + +function fail(msg) { + errors.push(msg); +} + +function warn(msg) { + warnings.push(msg); +} + +// 1. Manifest structural validation +for (const pkg of manifest.packages) { + for (const field of ['npmPackage', 'role', 'version', 'publicationRepository', 'releaseRef', 'releaseCommit', 'npmIntegrity', 'freshnessPolicy', 'authorityStatus']) { + if (!pkg[field]) { + fail(`${pkg.npmPackage || 'unknown'}: missing required field "${field}"`); + } + } + + // canonicalRepository is required for canonical packages; developmentRepository for transitional + if (pkg.authorityStatus === 'canonical' && !pkg.canonicalRepository) { + fail(`${pkg.npmPackage}: authorityStatus is canonical but missing canonicalRepository`); + } + if (pkg.authorityStatus === 'transitional' && !pkg.developmentRepository) { + fail(`${pkg.npmPackage}: authorityStatus is transitional but missing developmentRepository`); + } + + if (!/^\d+\.\d+\.\d+$/.test(pkg.version)) { + fail(`${pkg.npmPackage}: version "${pkg.version}" is not semver`); + } + + if (!/^[0-9a-f]{40}$/.test(pkg.releaseCommit)) { + fail(`${pkg.npmPackage}: releaseCommit "${pkg.releaseCommit}" is not a 40-char hex SHA`); + } +} + +// 2. Contract agreement: contract.tools.* must agree with manifest +const contractTools = contract.tools; +const manifestByNpm = Object.fromEntries( + manifest.packages.map((p) => [p.npmPackage, p]), +); + +function checkContractTool(toolKey, expectedNpmPackage) { + const tool = contractTools[toolKey]; + if (!tool) { + warn(`contract.tools.${toolKey} not found (may not be modeled yet)`); + return; + } + const pkg = manifestByNpm[expectedNpmPackage]; + if (!pkg) { + fail(`contract.tools.${toolKey} references ${expectedNpmPackage} but manifest has no such package`); + return; + } + if (tool.version && tool.version !== pkg.version) { + fail(`contract.tools.${toolKey}.version (${tool.version}) != manifest ${expectedNpmPackage} version (${pkg.version})`); + } + // For canonical packages, check canonicalRepository; for transitional, check developmentRepository + if (tool.authorityStatus === 'canonical' && tool.canonicalRepository && pkg.canonicalRepository) { + if (tool.canonicalRepository !== pkg.canonicalRepository) { + fail(`contract.tools.${toolKey}.canonicalRepository (${tool.canonicalRepository}) != manifest (${pkg.canonicalRepository})`); + } + } + if (tool.authorityStatus === 'transitional' && tool.developmentRepository && pkg.developmentRepository) { + if (tool.developmentRepository !== pkg.developmentRepository) { + fail(`contract.tools.${toolKey}.developmentRepository (${tool.developmentRepository}) != manifest (${pkg.developmentRepository})`); + } + } + if (tool.publicationRepository && tool.publicationRepository !== pkg.publicationRepository) { + fail(`contract.tools.${toolKey}.publicationRepository (${tool.publicationRepository}) != manifest (${pkg.publicationRepository})`); + } + if (tool.releaseRef && tool.releaseRef !== pkg.releaseRef) { + fail(`contract.tools.${toolKey}.releaseRef (${tool.releaseRef}) != manifest (${pkg.releaseRef})`); + } + if (tool.releaseCommit && tool.releaseCommit !== pkg.releaseCommit) { + fail(`contract.tools.${toolKey}.releaseCommit (${tool.releaseCommit}) != manifest (${pkg.releaseCommit})`); + } + if (tool.authorityStatus && tool.authorityStatus !== pkg.authorityStatus) { + fail(`contract.tools.${toolKey}.authorityStatus (${tool.authorityStatus}) != manifest (${pkg.authorityStatus})`); + } +} + +checkContractTool('audit', 'agents-audit'); +checkContractTool('cli', '@workspacejson/cli'); +checkContractTool('codex', '@workspacejson/codex-mcp'); + +// 3. Schema hash agreement +const schemaSource = contract.standard.schema.source; +const specPkg = manifestByNpm['@workspacejson/spec']; +if (specPkg && schemaSource.sha256) { + // The schema hash in contract must match the spec package's schema + // (verified via materialize-schema.mjs, which checks the actual bytes) + if (schemaSource.publicationRepository !== specPkg.publicationRepository) { + fail(`contract.schema.source.publicationRepository (${schemaSource.publicationRepository}) != manifest @workspacejson/spec publicationRepository (${specPkg.publicationRepository})`); + } + if (schemaSource.ref && specPkg.releaseRef && schemaSource.ref !== specPkg.releaseRef) { + fail(`contract.schema.source.ref (${schemaSource.ref}) != manifest @workspacejson/spec releaseRef (${specPkg.releaseRef})`); + } +} + +// 4. No package in manifest should have a nonexistent canonicalRepository +// (we can only check this with --live) +if (LIVE) { + const GITHUB_API = 'https://api.github.com/repos/'; + + async function checkGitHubRepo(repo) { + const resp = await fetch(`${GITHUB_API}${repo}`, { + headers: { 'Accept': 'application/vnd.github+json' }, + }); + if (!resp.ok) { + fail(`GitHub API: ${repo} returned ${resp.status}`); + } + return resp.ok; + } + + async function checkNpmLatest(npmPackage) { + const resp = await fetch(`https://registry.npmjs.org/${npmPackage}`); + if (!resp.ok) { + fail(`npm registry: ${npmPackage} returned ${resp.status}`); + return null; + } + const data = await resp.json(); + const latest = data['dist-tags']?.latest; + if (!latest) { + fail(`npm registry: ${npmPackage} has no dist-tags.latest`); + return null; + } + return latest; + } + + async function checkGitHubTagResolves(repo, ref, expectedCommit) { + const resp = await fetch(`${GITHUB_API}${repo}/git/refs/tags/${ref}`, { + headers: { 'Accept': 'application/vnd.github+json' }, + }); + if (!resp.ok) { + fail(`GitHub API: tag ${ref} in ${repo} returned ${resp.status}`); + return; + } + const data = await resp.json(); + const tagObj = data.object; + if (!tagObj || !tagObj.sha) { + fail(`GitHub API: tag ${ref} in ${repo} has no object.sha`); + return; + } + // If lightweight tag, tagObj.sha is the commit directly + if (tagObj.type === 'commit') { + if (tagObj.sha !== expectedCommit) { + fail(`tag ${ref} in ${repo} resolves to ${tagObj.sha} but manifest says ${expectedCommit}`); + } + return; + } + // If annotated tag, dereference to get the commit + const tagResp = await fetch(`${GITHUB_API}${repo}/git/tags/${tagObj.sha}`, { + headers: { 'Accept': 'application/vnd.github+json' }, + }); + if (!tagResp.ok) { + fail(`GitHub API: cannot dereference tag ${ref} in ${repo}: ${tagResp.status}`); + return; + } + const tagData = await tagResp.json(); + const commitSha = tagData.object?.sha; + if (commitSha !== expectedCommit) { + fail(`tag ${ref} in ${repo} resolves to commit ${commitSha} but manifest says ${expectedCommit}`); + } + } + + const seenRepos = new Set(); + const seenNpm = new Set(); + + for (const pkg of manifest.packages) { + // Check canonical or development repo exists + const primaryRepo = pkg.canonicalRepository || pkg.developmentRepository; + if (primaryRepo && !seenRepos.has(primaryRepo)) { + await checkGitHubRepo(primaryRepo); + seenRepos.add(primaryRepo); + } + + // Check publication repo exists (if different) + if (pkg.publicationRepository !== primaryRepo && !seenRepos.has(pkg.publicationRepository)) { + await checkGitHubRepo(pkg.publicationRepository); + seenRepos.add(pkg.publicationRepository); + } + + // Check GitHub tag exists in publication repo and resolves to expected commit + await checkGitHubTagResolves(pkg.publicationRepository, pkg.releaseRef, pkg.releaseCommit); + + // Check npm latest matches manifest version + if (!seenNpm.has(pkg.npmPackage)) { + const latest = await checkNpmLatest(pkg.npmPackage); + if (latest && latest !== pkg.version) { + fail(`npm ${pkg.npmPackage} latest is ${latest} but manifest says ${pkg.version}`); + } + seenNpm.add(pkg.npmPackage); + } + } + + // Check npm repository metadata matches declared publicationRepository + for (const pkg of manifest.packages) { + const resp = await fetch(`https://registry.npmjs.org/${pkg.npmPackage}/${pkg.version}`); + if (resp.ok) { + const data = await resp.json(); + const repoUrl = data.repository?.url || ''; + const expectedRepo = `github.com/${pkg.publicationRepository}`; + if (!repoUrl.includes(expectedRepo)) { + warn(`npm ${pkg.npmPackage}@${pkg.version} repository URL (${repoUrl}) does not contain ${expectedRepo}`); + } + } + } +} + +// Report +if (warnings.length > 0) { + console.warn('Warnings:'); + for (const w of warnings) console.warn(` WARN: ${w}`); +} + +if (errors.length > 0) { + console.error('Ecosystem verification failed:'); + for (const e of errors) console.error(` FAIL: ${e}`); + process.exit(1); +} + +console.log(`ecosystem verified: ${manifest.packages.length} packages${LIVE ? ' (live)' : ''}`); diff --git a/sources/ecosystem-packages.json b/sources/ecosystem-packages.json new file mode 100644 index 0000000..f6d623e --- /dev/null +++ b/sources/ecosystem-packages.json @@ -0,0 +1,111 @@ +{ + "description": "Ecosystem package manifest. One record per published npm package. All version surfaces on the site derive from this manifest. Measured 2026-07-30.", + "packages": [ + { + "npmPackage": "@workspacejson/spec", + "role": "specification", + "version": "0.4.4", + "npmIntegrity": "sha512-7lA2PDkhaKFEetrantRWecKrgGh6ev1HLHLhG8Wr7S4GvYm1xwTnOq+lqFg8pZCgnEOV2H2jADw1jQyHwuHTew==", + "canonicalRepository": "workspacejson/standard", + "publicationRepository": "workspace-json/agents-audit", + "tagNamespace": "v", + "releaseRef": "v0.4.4", + "releaseCommit": "d0a19f6a199c1a8cab4df18963a22f547c675ee6", + "freshnessPolicy": "latest", + "authorityStatus": "canonical", + "authorityEvidence": "OWNERSHIP.md in workspacejson/standard declares ownership of the normative JSON Schema and types", + "npmRepositoryMetadata": { + "url": "git+https://github.com/workspace-json/agents-audit.git", + "directory": "packages/spec" + } + }, + { + "npmPackage": "@workspacejson/rules", + "role": "rules", + "version": "0.4.4", + "npmIntegrity": "sha512-ZTCAC7LJ+gX5+RuYzzPmgSGLw8Q9k2EmytccaV2CB0QglIDOmBsDh+7nYhTQVB4FchRrWoAG1k5Yq48Wh7kh1g==", + "canonicalRepository": "workspacejson/standard", + "publicationRepository": "workspace-json/agents-audit", + "tagNamespace": "v", + "releaseRef": "v0.4.4", + "releaseCommit": "d0a19f6a199c1a8cab4df18963a22f547c675ee6", + "freshnessPolicy": "latest", + "authorityStatus": "canonical", + "authorityEvidence": "OWNERSHIP.md in workspacejson/standard declares ownership of deterministic rules and reference behavior", + "npmRepositoryMetadata": { + "url": "git+https://github.com/workspace-json/agents-audit.git", + "directory": "packages/rules" + } + }, + { + "npmPackage": "agents-audit", + "role": "audit-cli", + "version": "0.4.4", + "npmIntegrity": "sha512-IksX+y0FSQhxF6KF26Rob3tidN98EM4SvWdsgIMO8LjcReo5ThfDpoUc1Gfh0nFbocaTvgEga0X20NZeUBIm5w==", + "canonicalRepository": "workspacejson/cli", + "publicationRepository": "workspace-json/agents-audit", + "tagNamespace": "v", + "releaseRef": "v0.4.4", + "releaseCommit": "d0a19f6a199c1a8cab4df18963a22f547c675ee6", + "freshnessPolicy": "latest", + "authorityStatus": "canonical", + "authorityEvidence": "OWNERSHIP.md in workspacejson/cli declares agents-audit frozen at 0.4.4 with packages/agents-audit-compat/", + "npmRepositoryMetadata": { + "url": "git+https://github.com/workspace-json/agents-audit.git" + } + }, + { + "npmPackage": "@workspacejson/cli", + "role": "neutral-cli", + "version": "0.5.2", + "npmIntegrity": "sha512-4drbBvv9nd6JGidHaeoMG8fGKOhENHU+npN6VBZVaExZAThBuo8PeBmZ0swBHsTf0/udskXJlejAL6j3fMNHXw==", + "canonicalRepository": "workspacejson/cli", + "publicationRepository": "workspacejson/cli", + "tagNamespace": "cli-v", + "releaseRef": "cli-v0.5.2", + "releaseCommit": "e210ad22a447eb01c6b943ee80d0d6e83e48f529", + "freshnessPolicy": "latest", + "authorityStatus": "canonical", + "authorityEvidence": "OWNERSHIP.md in workspacejson/cli declares @workspacejson/cli as the neutral producer published from cli-v* tags", + "npmRepositoryMetadata": { + "url": "git+https://github.com/workspacejson/cli.git", + "directory": "packages/cli" + } + }, + { + "npmPackage": "@workspacejson/codex-mcp", + "role": "integration", + "version": "0.1.9", + "npmIntegrity": "sha512-YINreDlkMQP9p4gHv1kHQsslxavGsB7+6LaBEM7+Q8RT889xbBBLo53UFxt1qEJeCuMi+joIsP2qkBLh7oZ5gg==", + "developmentRepository": "workspacejson/integrations", + "publicationRepository": "workspace-json/codex-mcp", + "tagNamespace": "codex-mcp-v", + "releaseRef": "codex-mcp-v0.1.9", + "releaseCommit": "7d42a61af78a383219c536cc49220f154a93a2bf", + "freshnessPolicy": "latest", + "authorityStatus": "transitional", + "authorityEvidence": "workspacejson/integrations has the codex-mcp package.json but no OWNERSHIP.md asserting canonical authority; publication metadata still points to workspace-json/codex-mcp", + "npmRepositoryMetadata": { + "url": "git+https://github.com/workspace-json/codex-mcp.git" + } + } + ], + "nonNpmReferences": [ + { + "name": "Buildomator", + "version": "v2.42.3", + "type": "product", + "note": "Cited in adoption evidence. Not an npm library with site-relevant versions." + }, + { + "name": "Vreko", + "type": "product", + "note": "Product surface, not an npm library." + }, + { + "name": "Tally", + "type": "application", + "note": "Showcase application, not an npm library." + } + ] +} diff --git a/sources/schema.source.json b/sources/schema.source.json index 990f5bd..eebcc23 100644 --- a/sources/schema.source.json +++ b/sources/schema.source.json @@ -2,14 +2,14 @@ "status": "pinned", "trackingIssue": "VR-664", "package": "@workspacejson/spec", - "releaseLabel": "0.4.3", - "tarballIntegrity": "sha512-JI/1gITbzFL395lngzNygtVOpkx8pZQ/HBfHhwYjfqAd9EC+qw4Bzu/rDXb54paZub75TakfxUvCebF0XaycmQ==", + "releaseLabel": "0.4.4", + "tarballIntegrity": "sha512-7lA2PDkhaKFEetrantRWecKrgGh6ev1HLHLhG8Wr7S4GvYm1xwTnOq+lqFg8pZCgnEOV2H2jADw1jQyHwuHTew==", "schemaPath": "schema/v1.json", "schemaId": "https://www.workspacejson.dev/schema/v1.json", - "schemaSha256": "6ee97165f65878094cdcde230899664f2cdcba1db7a2f0cc30cd3b590fa34a22", + "schemaSha256": "7f1635bbeff47b103566866d1b66c47a604f91bb3948ad2b59a3ba9369a41e36", "checks": { "status": "green", - "verifiedAt": "2026-07-17T10:50:00Z", - "source": "CLI smoke test (npx agents-audit@0.4.3 generate . executes and exits 0, confirmed against a live clone of workspace-json/agents-audit) plus steward re-verification of the schema SHA-256 against a fresh 0.4.3 npm tarball download, independent of this local copy" + "verifiedAt": "2026-07-30T11:25:00Z", + "source": "CLI smoke test (npx agents-audit@0.4.4 generate . executes and exits 0, confirmed against a live clone of workspace-json/agents-audit) plus steward re-verification of the schema SHA-256 against a fresh 0.4.4 npm tarball download, independent of this local copy" } } diff --git a/src/assets/tally-impact-hero.png b/src/assets/tally-impact-hero.png new file mode 100644 index 0000000..8cc13b5 Binary files /dev/null and b/src/assets/tally-impact-hero.png differ diff --git a/src/content/docs/audit.mdx b/src/content/docs/audit.mdx index 5541739..7ad0821 100644 --- a/src/content/docs/audit.mdx +++ b/src/content/docs/audit.mdx @@ -18,11 +18,11 @@ head: "url": "https://workspacejson.dev/audit/", "applicationCategory": "DeveloperApplication", "operatingSystem": "Linux, macOS, Windows", - "softwareVersion": "0.4.3", + "softwareVersion": "0.4.4", "downloadUrl": "https://www.npmjs.com/package/agents-audit", "installUrl": "https://www.npmjs.com/package/agents-audit", - "codeRepository": "https://github.com/workspace-json/agents-audit", - "license": "https://github.com/workspace-json/agents-audit/blob/main/LICENSE", + "codeRepository": "https://github.com/workspacejson/cli", + "license": "https://github.com/workspacejson/cli/blob/main/LICENSE", "author": { "@type": "Organization", "name": "Vreko", "url": "https://vreko.dev" }, "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" } } diff --git a/src/content/docs/changelog.mdx b/src/content/docs/changelog.mdx index c622ed1..975771d 100644 --- a/src/content/docs/changelog.mdx +++ b/src/content/docs/changelog.mdx @@ -1,12 +1,23 @@ --- title: "Changelog" -description: "Version history for the workspace.json specification, agents-audit CLI, and associated packages. Current: v0.4.3 (2026-07-17)." +description: "Version history for the workspace.json specification, agents-audit CLI, and associated packages. Current: v0.4.4 (2026-07-30)." tableOfContents: false sidebar: order: 7 label: Changelog --- +## v0.4.4 - 2026-07-30 + +**Schema reconciliation release.** + +- All three core packages bumped to **0.4.4**: `@workspacejson/spec`, `@workspacejson/rules`, `agents-audit`. +- `@workspacejson/cli` **0.5.2** published: neutral CLI for generating and validating workspace.json, succeeding `agents-audit` with the same command surface. Development continues in `workspacejson/cli`; `agents-audit` publication remains in the historical `workspace-json/agents-audit` repository. +- `@workspacejson/codex-mcp` **0.1.9** published. Publication metadata still points to `workspace-json/codex-mcp`; development is migrating to `workspacejson/integrations`. +- Schema descriptions clarified: `fileIndex` keys are repository-root-relative POSIX paths; `coChange[].files` is an unordered pair with set semantics; `fragility[].file` is a repository-root-relative POSIX path. +- Exported runtime `version` corrected to match the package manifest. +- GitHub organization migration is transitional: canonical development moved to `workspacejson/standard`, `workspacejson/cli`, and `workspacejson/integrations`, but npm publication provenance for the fixed-group packages remains in `workspace-json/agents-audit` and `workspace-json/codex-mcp`. + ## v0.4.3 - 2026-07-17 **Version alignment release.** diff --git a/src/content/docs/getting-started.mdx b/src/content/docs/getting-started.mdx index 3e63173..c5325cb 100644 --- a/src/content/docs/getting-started.mdx +++ b/src/content/docs/getting-started.mdx @@ -81,6 +81,18 @@ npm install -g agents-audit agents-audit runs in any repository. It doesn't require workspace.json to be present — it will audit your `AGENTS.md` and tell you what's missing. + + ## 2. Run Your First Audit From your repository root: @@ -225,3 +237,4 @@ with other tooling, dashboards, or custom reporting. - [See real examples](/examples/) — `workspace.json` from monorepos, web apps, CLI tools - [Explore the ecosystem](/ecosystem/) — how workspace.json fits with Claude Code, Cursor, MCP, and AGENTS.md - [View implementations](/implementations/) — tools that generate or consume workspace.json +- [Try @workspacejson/cli](https://www.npmjs.com/package/@workspacejson/cli) — the neutral CLI successor to agents-audit diff --git a/src/content/docs/governance.mdx b/src/content/docs/governance.mdx index cbf9ae5..9cb483a 100644 --- a/src/content/docs/governance.mdx +++ b/src/content/docs/governance.mdx @@ -13,8 +13,8 @@ single-steward model. The specification is licensed under Apache-2.0. Donation to a neutral body is intended; no body has accepted stewardship, and none has been approached successfully. -The canonical spec source now lives in the agents-audit monorepo: -[github.com/workspace-json/agents-audit/tree/main/packages/spec](https://github.com/workspace-json/agents-audit/tree/main/packages/spec). +The canonical spec source lives in the standard repository: +[github.com/workspacejson/standard/tree/main/packages/spec](https://github.com/workspacejson/standard/tree/main/packages/spec). ## Origin @@ -27,7 +27,7 @@ implement workspace.json without permission, payment, or attribution. - **Steward:** Marcelle Labs - **Stability:** unstable - **License:** Apache-2.0 -- **Governance home:** [github.com/workspace-json/agents-audit/tree/main/packages/spec](https://github.com/workspace-json/agents-audit/tree/main/packages/spec) +- **Governance home:** [github.com/workspacejson/standard/tree/main/packages/spec](https://github.com/workspacejson/standard/tree/main/packages/spec) - **External stewardship:** none - **Donation intent:** Donation to a neutral body is intended; no body has accepted stewardship, and none has been approached successfully. @@ -40,7 +40,7 @@ Marcelle Labs remains the current steward. Contact: ## Contribution The spec is developed in public at -[github.com/workspace-json/agents-audit/tree/main/packages/spec](https://github.com/workspace-json/agents-audit/tree/main/packages/spec). +[github.com/workspacejson/standard/tree/main/packages/spec](https://github.com/workspacejson/standard/tree/main/packages/spec). Substantive changes are decided by the current steward and documented in the canonical repository. There is no formal external technical committee or standards-body process. diff --git a/src/content/docs/implementations.mdx b/src/content/docs/implementations.mdx index 4a3f539..09288b7 100644 --- a/src/content/docs/implementations.mdx +++ b/src/content/docs/implementations.mdx @@ -37,16 +37,16 @@ Tools that read `workspace.json`: description="Read-only MCP context plus a deterministic hook for evidenced co-change omissions on supported Codex edits." href="/implementations/codex/" /> + -

- This is the real, current output of npx agents-audit@0.4.3 generate ., - run against workspace-json/agents-audit — reproduce it yourself rather than trust this page. + This is the real, current output of npx agents-audit@{PACKAGE_VERSION}, + run against this repository — reproduce it yourself rather than trust this page.

@@ -364,7 +364,7 @@ const exampleCode = `{

If you're building a generator or consumer, open an issue in the - workspace-json/agents-audit tracker. Canonical source and + workspacejson/cli tracker. Canonical source and compatibility material are maintained in public.

diff --git a/src/pages/showcase/tally/index.astro b/src/pages/showcase/tally/index.astro index 2d5266c..6e3c91a 100644 --- a/src/pages/showcase/tally/index.astro +++ b/src/pages/showcase/tally/index.astro @@ -1,6 +1,8 @@ --- +import { Image } from 'astro:assets'; import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import JsonLd from '../../../components/JsonLd.astro'; +import tallyImpactHero from '../../../assets/tally-impact-hero.png'; import { productIdentity, narrativeActs, @@ -13,7 +15,7 @@ import { } from '../../../data/tally-showcase'; const lockup = assets.lockup; -const explainer01 = assets.explainer01; +const cockpitUrl = ctaLinks.liveEvidence; --- @@ -27,6 +29,13 @@ const explainer01 = assets.explainer01; { tag: 'meta', attrs: { property: 'og:image', content: '/tally/og-1200x630.png' } }, { tag: 'meta', attrs: { name: 'twitter:image', content: '/tally/og-1200x630.png' } }, { tag: 'meta', attrs: { property: 'og:type', content: 'website' } }, + { tag: 'meta', attrs: { property: 'og:site_name', content: 'workspace.json' } }, + { tag: 'meta', attrs: { property: 'og:url', content: 'https://workspacejson.dev/showcase/tally/' } }, + { tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } }, + { tag: 'meta', attrs: { name: 'twitter:title', content: `${productIdentity.name}: ${productIdentity.descriptor}` } }, + { tag: 'meta', attrs: { name: 'twitter:description', content: `${productIdentity.supporting} ${productIdentity.pov}` } }, + { tag: 'meta', attrs: { name: 'robots', content: 'index, follow' } }, + { tag: 'link', attrs: { rel: 'canonical', href: 'https://workspacejson.dev/showcase/tally/' } }, ], }} hasSidebar={false} @@ -42,7 +51,7 @@ const explainer01 = assets.explainer01; Showcase Documentation GitHub - Try it + Try it
@@ -66,8 +75,8 @@ const explainer01 = assets.explainer01; {productIdentity.supporting}

- {/* ─── Cockpit preview ─── */} -
-
- Tally cockpit showing impact view with joined lineage and repository evidence -
- Cockpit preview -
-
-

- A product screenshot or 60-to-90-second demo will replace this diagram when a recording is available. + {/* ─── Product screenshot ─── */} +

+

+ Tally resolved the dataset to its exact repository-relative path, joined + lineage and code impact, and surfaced three declared evidence gaps.

+
+ + Tally Impact review for game_events showing exact source resolution, three declared evidence gaps, and upstream and downstream lineage. + +
+ + View full-resolution screenshot + +
{/* ─── Narrative acts with varied composition ─── */} @@ -251,13 +270,18 @@ const explainer01 = assets.explainer01; See the evidence for yourself. + @@ -309,7 +333,7 @@ const explainer01 = assets.explainer01; --ts-amber: #C8861A; --ts-red: #C85A4A; - max-width: 1200px; + max-width: 1400px; margin: 0 auto; padding: 0 2rem; font-family: var(--ts-font); @@ -483,50 +507,69 @@ const explainer01 = assets.explainer01; text-decoration-thickness: 2px; } - /* ─── Cockpit preview ─── */ - .tally-cockpit-preview { + /* ─── Product screenshot ─── */ + .tally-product-shot { padding: 0 0 4rem; } - .tally-cockpit-frame { - position: relative; - border-radius: 0.875rem; + .tally-product-shot-conclusion { + font-size: 1.0625rem; + line-height: 1.7; + color: var(--ts-ink-2); + max-width: 56ch; + margin: 0 0 1.5rem; + } + + .tally-product-shot-figure { + margin: 0; + } + + .tally-product-shot-figure a { + display: block; + border-radius: 0.75rem; overflow: hidden; border: 1px solid var(--ts-rule-2); - background: var(--ts-surface-2); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3); + transition: box-shadow 200ms ease; + } + + .tally-product-shot-figure a:hover { + box-shadow: 0 6px 32px rgba(0, 0, 0, 0.4); } - .tally-cockpit-img { + .tally-product-shot-img { display: block; width: 100%; height: auto; + object-fit: contain; } - .tally-cockpit-overlay { - position: absolute; - top: 1rem; - left: 1rem; + .tally-product-shot-fullres { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + color: var(--ts-muted); + text-decoration: none; + border-bottom: 1px solid var(--ts-rule-2); + padding-bottom: 0.125rem; + margin-top: 1rem; + transition: color 150ms ease, border-color 150ms ease; } - .tally-cockpit-badge { - display: inline-block; - padding: 0.25rem 0.625rem; - border-radius: 0.25rem; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(8px); - font-family: var(--ts-mono); - font-size: 0.6875rem; - font-weight: 500; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--ts-ink-2); + .tally-product-shot-fullres:hover { + color: var(--ts-mint); + border-color: var(--ts-mint); } - .tally-cockpit-caption { - font-size: 0.8125rem; - color: var(--ts-muted); - margin: 0.75rem 0 0; - text-align: center; + /* Mobile: contained preview with action */ + @media (max-width: 767px) { + .tally-product-shot-figure a { + border-radius: 0.5rem; + } + .tally-product-shot-conclusion { + font-size: 1rem; + } } /* ─── Act sections ─── */ @@ -772,6 +815,21 @@ const explainer01 = assets.explainer01; margin: 0; } + .tally-close-secondary { + margin-bottom: 1.5rem; + } + + .tally-close-secondary a { + font-size: 0.875rem; + color: var(--ts-muted); + text-decoration: underline; + text-underline-offset: 2px; + } + + .tally-close-secondary a:hover { + color: var(--ts-mint); + } + .tally-close-meta a { font-family: var(--ts-mono); color: var(--ts-ink-2); diff --git a/tests/contract.spec.ts b/tests/contract.spec.ts index 485221a..0b4b618 100644 --- a/tests/contract.spec.ts +++ b/tests/contract.spec.ts @@ -53,7 +53,7 @@ test.describe('contract manifest', () => { }); test('packageVersion is a superset-consistent refinement of version', () => { - // 0.4.3 must belong to the 0.4 line. A mismatch here means a bump patched + // 0.4.4 must belong to the 0.4 line. A mismatch here means a bump patched // one field and not the other, which is exactly the drift the manifest // exists to prevent. expect(contract.standard.packageVersion.startsWith(contract.standard.version + '.')).toBe(true); @@ -101,7 +101,7 @@ test.describe('schema truth', () => { `hosted schema drifted from the pinned standard release.\n` + ` expected sha256: ${contract.standard.schema.source.sha256}\n` + ` observed sha256: ${actual}\n` + - ` pinned to: ${contract.standard.schema.source.repository}@${contract.standard.schema.source.ref}`, + ` pinned to: ${contract.standard.schema.source.publicationRepository}@${contract.standard.schema.source.ref}`, ).toBe(contract.standard.schema.source.sha256); }); diff --git a/tests/tally.spec.ts b/tests/tally.spec.ts index 16b1637..af638ae 100644 --- a/tests/tally.spec.ts +++ b/tests/tally.spec.ts @@ -166,10 +166,12 @@ test.describe('showcase: hero', () => { expect(text?.trim().length).toBeGreaterThan(20); }); - test('Watch the demo CTA is present', async ({ page }) => { + test('Try Tally CTA is present and links to the live app', async ({ page }) => { await page.goto(NARRATIVE, { waitUntil: 'load' }); - const cta = page.locator('a:has-text("Watch the demo")'); + const cta = page.locator('a:has-text("Try Tally")').first(); expect(await cta.count()).toBe(1); + const href = await cta.getAttribute('href'); + expect(href).toContain('tally.workspacejson.dev'); }); test('Explore the evidence CTA links to proof page', async ({ page }) => { @@ -181,21 +183,84 @@ test.describe('showcase: hero', () => { }); }); -// ─── Cockpit preview ─── +// ─── Product screenshot ─── + +test.describe('showcase: product screenshot', () => { + test('product screenshot image is present', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const img = page.locator('.tally-product-shot-img'); + expect(await img.count()).toBe(1); + }); + + test('product screenshot has specified alt text', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const img = page.locator('.tally-product-shot-img'); + const alt = await img.getAttribute('alt'); + expect(alt).toContain('Tally Impact review for game_events'); + expect(alt).toContain('source resolution'); + expect(alt).toContain('evidence gaps'); + expect(alt).toContain('lineage'); + }); + + test('product screenshot has intrinsic width and height', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const img = page.locator('.tally-product-shot-img'); + const width = await img.getAttribute('width'); + const height = await img.getAttribute('height'); + expect(Number(width)).toBe(1920); + expect(Number(height)).toBe(1275); + }); + + test('product screenshot loads eagerly', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const img = page.locator('.tally-product-shot-img'); + const loading = await img.getAttribute('loading'); + expect(loading).toBe('eager'); + }); + + test('product screenshot has fetchpriority high', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const img = page.locator('.tally-product-shot-img'); + const fetchpriority = await img.getAttribute('fetchpriority'); + expect(fetchpriority).toBe('high'); + }); + + test('product screenshot links to cockpit URL', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const link = page.locator('.tally-product-shot-figure a'); + expect(await link.count()).toBe(1); + const href = await link.getAttribute('href'); + expect(href).toContain('evaluation/hac-152'); + }); + + test('adjacent conclusion text is present', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const conclusion = page.locator('.tally-product-shot-conclusion'); + expect(await conclusion.count()).toBe(1); + const text = await conclusion.textContent(); + expect(text).toContain('repository-relative path'); + expect(text).toContain('lineage'); + expect(text).toContain('evidence gaps'); + }); -test.describe('showcase: cockpit preview', () => { - test('cockpit preview image is present', async ({ page }) => { + test('full-resolution screenshot link is present', async ({ page }) => { await page.goto(NARRATIVE, { waitUntil: 'load' }); - const cockpit = page.locator('.tally-cockpit-img'); - expect(await cockpit.count()).toBe(1); + const link = page.locator('.tally-product-shot-fullres'); + expect(await link.count()).toBe(1); + const text = await link.textContent(); + expect(text).toContain('full-resolution'); }); - test('cockpit preview has caption', async ({ page }) => { + test('no overlay badge on product screenshot', async ({ page }) => { await page.goto(NARRATIVE, { waitUntil: 'load' }); - const caption = page.locator('.tally-cockpit-caption'); - expect(await caption.count()).toBe(1); - const text = await caption.textContent(); - expect(text?.length).toBeGreaterThan(20); + const badge = page.locator('.tally-cockpit-badge, .tally-cockpit-overlay'); + expect(await badge.count(), 'no overlay badge should be present').toBe(0); + }); + + test('no old cockpit classes remain', async ({ page }) => { + await page.goto(NARRATIVE, { waitUntil: 'load' }); + const oldClasses = page.locator('.tally-cockpit-preview, .tally-cockpit-frame, .tally-cockpit-img, .tally-cockpit-caption'); + expect(await oldClasses.count(), 'old cockpit classes should be removed').toBe(0); }); }); @@ -258,7 +323,7 @@ test.describe('showcase: explainer images', () => { test('each explainer has substantive alt text', async ({ page }) => { await page.goto(NARRATIVE, { waitUntil: 'load' }); - const imgs = await page.$$eval('main .tally-act-visual img, main .tally-cockpit-img', (els) => + const imgs = await page.$$eval('main .tally-act-visual img, main .tally-product-shot-img', (els) => els.map((e) => ({ src: e.getAttribute('src') ?? '', alt: e.getAttribute('alt') ?? '', @@ -825,6 +890,15 @@ test.describe('source lock integrity', () => { } }); + test('source lock includes the product screenshot', () => { + const consumed = sourceLock.assetManifest?.assetsConsumed; + const heroAsset = consumed?.find((a: { path: string }) => a.path === 'src/assets/tally-impact-hero.png'); + expect(heroAsset, 'tally-impact-hero.png must be in source lock').toBeTruthy(); + expect(heroAsset?.sha256).toBe('a91ed2ab3e1fde2bdca74a0594c4b32dc0d3e1bde83d5a6ae18c9a214a8577b7'); + expect(heroAsset?.width).toBe(1920); + expect(heroAsset?.height).toBe(1275); + }); + test('typed contract sourceRevision matches lock', () => { const ts = read('src/data/tally-showcase.ts'); expect(ts).toContain('sourceLock.revision'); @@ -855,6 +929,18 @@ test.describe('source lock integrity', () => { expect(src).toContain("from '../../../data/tally-showcase'"); }); + test('showcase page imports Image from astro:assets', () => { + const src = read('src/pages/showcase/tally/index.astro'); + expect(src).toContain("from 'astro:assets'"); + expect(src).toContain('Image'); + }); + + test('showcase page imports product screenshot from src/assets', () => { + const src = read('src/pages/showcase/tally/index.astro'); + expect(src).toContain('tally-impact-hero.png'); + expect(src).toContain("from '../../../assets/tally-impact-hero.png'"); + }); + test('showcase page uses contract data', () => { const src = read('src/pages/showcase/tally/index.astro'); expect(src).toContain('narrativeActs');