diff --git a/.archgate/adrs/ARCH-013-version-synchronization.md b/.archgate/adrs/ARCH-013-version-synchronization.md index 0396d8f6..c007d547 100644 --- a/.archgate/adrs/ARCH-013-version-synchronization.md +++ b/.archgate/adrs/ARCH-013-version-synchronization.md @@ -3,7 +3,7 @@ id: ARCH-013 title: Version Synchronization domain: architecture rules: true -files: ["package.json", "docs/**"] +files: ["package.json", "docs/**", "shims/**"] --- # Version Synchronization @@ -14,25 +14,35 @@ The CLI version appears in multiple locations that must stay in sync: 1. `package.json` `version` — canonical source of truth 2. `docs/astro.config.mjs` — `softwareVersion` in the JSON-LD structured data +3. `shims/pypi/pyproject.toml` — PyPI package version +4. `shims/pypi/archgate/_version.py` — Python `__version__` constant +5. `shims/nuget/Archgate.Tool/Archgate.Tool.csproj` — NuGet package version +6. `shims/go/internal/shim/shim.go` — Go `Version` constant +7. `shims/maven/pom.xml` — Maven artifact version +8. `shims/rubygem/lib/archgate/version.rb` — RubyGem `VERSION` constant -When versions diverge, search engines display outdated version info. This was discovered during a consistency review where `package.json` was at `0.16.0` but `docs/astro.config.mjs` was still at `0.11.0`. +When versions diverge, users installing via different package managers get mismatched binaries. This was discovered during a consistency review where `package.json` was at `0.16.0` but `docs/astro.config.mjs` was still at `0.11.0`. ## Decision `package.json` `version` is the single source of truth. All other version references MUST match it. -**Automated via release process:** The `.simple-release.js` bump hook updates `softwareVersion` in `docs/astro.config.mjs` to match `package.json`. This is fully automated and requires no manual intervention. +**Automated via release process:** The `.simple-release.js` bump hook updates all version locations to match `package.json` during the release commit. This is fully automated and requires no manual intervention. + +The shim packages (npm, PyPI, NuGet, Go, Maven Central, RubyGems) are thin wrappers that download the platform binary from GitHub Releases. Their embedded version determines which release to download, so version drift causes download failures (404) or installs the wrong version. ## Do's and Don'ts ### Do -- Rely on `.simple-release.js` for `softwareVersion` sync (do not update manually) +- Rely on `.simple-release.js` for all version sync (do not update manually) - Use the companion rules to catch version drift in CI as a safety net +- When adding a new shim ecosystem, add its version file to `.simple-release.js` and the companion rules ### Don't - Don't manually edit `softwareVersion` in `docs/astro.config.mjs` — the release hook handles this +- Don't manually edit version strings in any `shims/` package — the release hook handles this ## Consequences @@ -49,10 +59,12 @@ When versions diverge, search engines display outdated version info. This was di ### Automated Enforcement -- **Release hook** `.simple-release.js`: Syncs `docs/astro.config.mjs` `softwareVersion` during `bump()`. Fully automated. +- **Release hook** `.simple-release.js`: Syncs all version locations during `bump()`. Fully automated. - **Archgate rule** `ARCH-013/docs-version-sync`: Checks that `softwareVersion` in `docs/astro.config.mjs` matches `package.json` version. Severity: `error`. +- **Archgate rule** `ARCH-013/shim-version-sync`: Checks that all shim package versions match `package.json` version. Severity: `error`. ## References - [GEN-001 — Documentation Site](./GEN-001-documentation-site.md) — Docs site structure and configuration -- [`.simple-release.js`](../../.simple-release.js) — Release bump hook that syncs softwareVersion +- [ARCH-017 — Multi-Ecosystem Distribution](./ARCH-017-multi-ecosystem-distribution.md) — Shim pattern and behavioral contract +- [`.simple-release.js`](../../.simple-release.js) — Release bump hook that syncs all version locations diff --git a/.archgate/adrs/ARCH-013-version-synchronization.rules.ts b/.archgate/adrs/ARCH-013-version-synchronization.rules.ts index 7edd8239..352e0858 100644 --- a/.archgate/adrs/ARCH-013-version-synchronization.rules.ts +++ b/.archgate/adrs/ARCH-013-version-synchronization.rules.ts @@ -31,5 +31,75 @@ export default { } }, }, + "shim-version-sync": { + description: "All shim package versions must match package.json version", + severity: "error", + async check(ctx) { + const pkgJson = await ctx.readJSON("package.json"); + if (!pkgJson.version) return; + const expected = pkgJson.version as string; + + const shimFiles: Array<{ + file: string; + pattern: RegExp; + label: string; + }> = [ + { + file: "shims/pypi/pyproject.toml", + pattern: /^version\s*=\s*"([^"]+)"/mu, + label: "PyPI pyproject.toml", + }, + { + file: "shims/pypi/archgate/_version.py", + pattern: /__version__\s*=\s*"([^"]+)"/u, + label: "PyPI _version.py", + }, + { + file: "shims/nuget/Archgate.Tool/Archgate.Tool.csproj", + pattern: /([^<]+)<\/Version>/u, + label: "NuGet .csproj", + }, + { + file: "shims/go/internal/shim/shim.go", + pattern: /const Version = "([^"]+)"/u, + label: "Go shim.go", + }, + { + file: "shims/maven/pom.xml", + pattern: + /archgate-cli<\/artifactId>\s*([^<]+)<\/version>/u, + label: "Maven pom.xml", + }, + { + file: "shims/rubygem/lib/archgate/version.rb", + pattern: /VERSION\s*=\s*"([^"]+)"/u, + label: "RubyGem version.rb", + }, + ]; + + for (const { file, pattern, label } of shimFiles) { + let content: string; + try { + // oxlint-disable-next-line no-await-in-loop -- sequential read is intentional; files are few and order-independent but must check each + content = await ctx.readFile(file); + } catch { + // Shim file may not exist yet + continue; + } + + const match = content.match(pattern); + if (!match) continue; + + const shimVersion = match[1]; + if (shimVersion !== expected) { + ctx.report.violation({ + message: `${label} version "${shimVersion}" does not match package.json version "${expected}"`, + file, + fix: `Update version to "${expected}" in ${file} (automated by .simple-release.js)`, + }); + } + } + }, + }, }, } satisfies RuleSet; diff --git a/.archgate/adrs/ARCH-017-multi-ecosystem-distribution.md b/.archgate/adrs/ARCH-017-multi-ecosystem-distribution.md new file mode 100644 index 00000000..a3d68aa3 --- /dev/null +++ b/.archgate/adrs/ARCH-017-multi-ecosystem-distribution.md @@ -0,0 +1,84 @@ +--- +id: ARCH-017 +title: Multi-Ecosystem Distribution +domain: distribution +rules: false +--- + +# Multi-Ecosystem Distribution + +## Context + +The archgate CLI is a standalone binary compiled with Bun. To maximize reach, it is distributed through multiple package managers (npm, PyPI, NuGet, Go, Maven Central, RubyGems) using a "thin shim" pattern: each package contains a minimal wrapper in the target ecosystem's language that downloads and caches the platform binary from GitHub Releases on first invocation. + +## Decision + +All distribution shims live under `shims/` in the main repository. Each shim is a self-contained package for its target ecosystem with zero runtime dependencies beyond the ecosystem's own standard library. + +### Shared Behavioral Contract + +Every shim implements the same algorithm: + +1. Detect platform/architecture and map to artifact name (`archgate-darwin-arm64`, `archgate-linux-x64`, `archgate-win32-x64`) +2. Check for cached binary at `~/.archgate/bin/archgate[.exe]` +3. If missing, download from `https://github.com/archgate/cli/releases/download/v{VERSION}/{artifact}.{ext}` +4. Verify SHA256 checksum against the companion `.sha256` file +5. Extract binary with proper permissions (0755 on Unix) +6. Execute the binary, forwarding all arguments and inheriting stdio +7. Propagate the exit code + +### Shared Cache + +All shim packages share the same cache directory (`~/.archgate/bin/`). If the binary is already cached by any install method (npm, pip, standalone installer, etc.), no download occurs. + +### Error Messages + +All shims produce identical user-facing error messages on stderr: + +- Unsupported platform: `archgate: Unsupported platform: {os}/{arch}\narchgate supports darwin/arm64, linux/x64, and win32/x64.` +- Download failure: `archgate: failed to download binary: {detail}\nVisit https://cli.archgate.dev/getting-started/installation/ for alternative install methods.` +- Checksum mismatch: `archgate: checksum verification failed for v{version} (expected {expected}, got {actual})` +- Download started: `archgate: binary not found, downloading v{version}...` +- Download complete: `archgate: binary downloaded successfully.` + +### Version Synchronization + +`package.json` `version` is the single source of truth. The `.simple-release.js` bump hook updates all shim version files automatically during the release commit. See ARCH-013 for enforcement details. + +## Do's and Don'ts + +### Do + +- Use only the target ecosystem's standard library (zero runtime dependencies) +- Share the `~/.archgate/bin/` cache directory across all shim packages +- Verify SHA256 checksums before extracting downloaded archives +- Use identical error messages across all shims +- Add new shim version files to `.simple-release.js` and the ARCH-013 companion rules + +### Don't + +- Don't bundle the compiled binary into any shim package (download on demand) +- Don't add runtime dependencies to any shim package +- Don't use a different cache location per ecosystem +- Don't skip SHA256 verification + +## Consequences + +### Positive + +- Users can install archgate through their preferred package manager without requiring Node.js or Bun +- All install methods converge on the same cached binary, avoiding duplicate downloads +- Thin packages are fast to install and have minimal footprint in each registry +- Version synchronization is automated via the release hook + +### Negative + +- First-run latency: the binary must be downloaded on the first invocation after install +- Multiple codebases to maintain (one per ecosystem), though the logic is simple and rarely changes +- Network dependency on GitHub Releases for the initial download + +## References + +- [ARCH-013 -- Version Synchronization](./ARCH-013-version-synchronization.md) -- Enforces version parity across all shim packages +- [CI-001 -- Pin GitHub Actions by Commit SHA](./CI-001-pin-github-actions-by-hash.md) -- SHA pinning for the publish-shims workflow +- [`.simple-release.js`](../../.simple-release.js) -- Release bump hook that syncs all shim versions diff --git a/.archgate/adrs/GEN-002-docs-i18n.md b/.archgate/adrs/GEN-002-docs-i18n.md index 740126cd..62dd6ba6 100644 --- a/.archgate/adrs/GEN-002-docs-i18n.md +++ b/.archgate/adrs/GEN-002-docs-i18n.md @@ -113,9 +113,11 @@ The sidebar in `docs/astro.config.mjs` does NOT need per-locale duplication. Sta ### Automated Enforcement -The companion rules file (`GEN-002-docs-i18n.rules.ts`) defines one rule: +The companion rules file (`GEN-002-docs-i18n.rules.ts`) defines three rules: - **`i18n-page-parity`** (severity: `error`) -- Verifies that every root MDX file has a corresponding translation in each configured locale directory, and that no orphan translations exist without a root source file. Runs as part of `archgate check`. +- **`i18n-translation-drift`** (severity: `error`) -- When running against a changeset (PR, staged files), verifies that if an English docs file was modified, the corresponding locale file was also modified. Catches content drift within existing files that page-parity alone would miss. +- **`no-locale-prefix-in-links`** (severity: `error`) -- Verifies that locale pages do not use locale-prefixed internal links (e.g., `/pt-br/guides/...`). Starlight resolves locale routes automatically. ### Manual Enforcement diff --git a/.archgate/adrs/GEN-002-docs-i18n.rules.ts b/.archgate/adrs/GEN-002-docs-i18n.rules.ts index 97b0ddc6..21bf18e8 100644 --- a/.archgate/adrs/GEN-002-docs-i18n.rules.ts +++ b/.archgate/adrs/GEN-002-docs-i18n.rules.ts @@ -107,5 +107,53 @@ export default { } }, }, + "i18n-translation-drift": { + description: + "When an English docs file is modified, the corresponding locale file must also be modified in the same changeset", + severity: "error", + async check(ctx) { + // Only meaningful when running against a changeset (PR, staged, etc.) + if (ctx.changedFiles.length === 0) return; + + const changedSet = new Set(ctx.changedFiles); + const rootPrefix = `${CONTENT_ROOT}/`; + + // Find changed root (English) MDX files that aren't inside a locale dir + const changedRootFiles = ctx.changedFiles.filter( + (f) => + f.startsWith(rootPrefix) && + f.endsWith(".mdx") && + !LOCALES.some((l) => f.startsWith(`${rootPrefix}${l}/`)) + ); + + if (changedRootFiles.length === 0) return; + + // Pre-build a set of all existing locale files for fast lookup + const localeFileArrays = await Promise.all( + LOCALES.map((locale) => + ctx.glob(`${CONTENT_ROOT}/${locale}/**/*.mdx`) + ) + ); + const allLocaleFiles = new Set(localeFileArrays.flat()); + + for (const rootFile of changedRootFiles) { + const relativePath = rootFile.replace(rootPrefix, ""); + + for (const locale of LOCALES) { + const localePath = `${CONTENT_ROOT}/${locale}/${relativePath}`; + + // Only flag if the locale file exists but wasn't changed. + // Missing locale files are already caught by i18n-page-parity. + if (allLocaleFiles.has(localePath) && !changedSet.has(localePath)) { + ctx.report.violation({ + message: `English file "${relativePath}" was modified but the ${locale} translation was not updated`, + file: localePath, + fix: `Update ${localePath} to reflect the changes in ${rootFile}`, + }); + } + } + } + }, + }, }, } satisfies RuleSet; diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index c444e686..b3480757 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -61,6 +61,7 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv - **macOS `/var` → `/private/var` symlink breaks temp dir path comparisons in tests** — On macOS, `/var` is a symlink to `/private/var`. `mkdtempSync(join(tmpdir(), ...))` returns `/var/folders/...` but `process.cwd()` after `chdir()` resolves the symlink to `/private/var/folders/...`. Tests that compare `tempDir` against paths derived from `process.cwd()` or `findProjectRoot()` will fail. Fix: always wrap `mkdtempSync` with `realpathSync` in test setup: `tempDir = realpathSync(mkdtempSync(join(tmpdir(), "archgate-test-")))`. This normalizes the path upfront. Discovered in v0.38.0/v0.39.0 release builds — PR CI runs on ubuntu-latest only, so macOS-specific issues are invisible until the release workflow. - **Always use `bun run test`, never bare `bun test`, in CI workflows** — The package.json `test` script includes `--timeout 60000`, but bare `bun test` uses Bun's default 5000ms timeout. Tests that perform filesystem operations or spawn subprocesses (e.g., session-context tests) can exceed 5s on slow CI runners. The `release-binaries.yml` Windows step originally used `bun test` and hit timeout failures. Same principle as "never use `bunx prettier` directly" — always prefer `bun run