diff --git a/.doctrine/project.json b/.doctrine/project.json index ecaf9e7..8451a06 100644 --- a/.doctrine/project.json +++ b/.doctrine/project.json @@ -63,6 +63,26 @@ "type": "workflow", "name": "CI and release workflows", "location": ".github/workflows/" + }, + { + "type": "manifest", + "name": "Vendor-neutral project manifest", + "location": "project.manifest.json" + }, + { + "type": "test", + "name": "Project-control boundary test", + "location": "test/project-control.node-test.mjs" + }, + { + "type": "documentation", + "name": "Project-control gate spec", + "location": "docs/specs/project-control-gate.md" + }, + { + "type": "documentation", + "name": "Security policy", + "location": "SECURITY.md" } ], "allowedDependencies": [ @@ -75,6 +95,11 @@ "repo": "SylphxAI/doctrine", "surface": "project manifest schema, runner policy, CI admission doctrine, and agent execution standards", "direction": "peer-public" + }, + { + "repo": "SylphxAI/groundatlas", + "surface": "Released GroundAtlas package/action for vendor-neutral project manifest validation and fleet dogfooding.", + "direction": "peer-public" } ], "forbiddenCouplings": [ @@ -86,18 +111,18 @@ "documentation": { "adr": { "path": "docs/adr/", - "status": "planned" + "status": "present" }, "specs": { - "path": "README.md and docs/", + "path": "README.md, docs/, and docs/specs/", "status": "present" }, "catalog": { - "path": ".doctrine/project.json", + "path": "PROJECT.md, project.manifest.json, and .doctrine/project.json", "status": "present" }, "runbooks": { - "path": "docs/guide/", + "path": "AGENTS.md, PROJECT.md, README.md, CONTRIBUTING.md, SECURITY.md, and docs/specs/project-control-gate.md", "status": "present" }, "generatedReferences": { @@ -106,20 +131,21 @@ } }, "delivery": { - "ciModel": "legacy-ci", + "ciModel": "legacy-ci-with-groundatlas", "requiredContexts": ["test"], - "deployPath": "Pull requests and merge groups run the repository CI workflow. Main releases use .github/workflows/release.yml, which delegates package publication to the shared SylphxAI/.github release workflow.", - "productionProof": "Package-impacting work must prove CI, affected package tests, build output, Changesets release intent when publishing, and npm package/version readback after release.", + "deployPath": "Pull requests and merge groups run the repository CI workflow with runtime checks for runtime-affecting changes plus project-control test and GroundAtlas package dogfood. Main releases use .github/workflows/release.yml, which delegates package publication to the shared SylphxAI/.github release workflow.", + "productionProof": "Package-impacting work must prove CI, affected package tests, build output, Changesets release intent when publishing, GroundAtlas package dogfood, and npm package/version readback after release.", "recoveryClass": "source-revertable", "packageRelease": { "publishesPackages": true, "ecosystems": ["npm"], "releaseIntent": "Changesets files in .changeset/ declare package-level release intent and changelog content.", "versionPr": "Changesets versioning prepares package version and changelog updates before publication.", - "publisher": "The Release workflow delegates to SylphxAI/.github/.github/workflows/release.yml@main with inherited secrets.", + "publisher": "The Release workflow delegates to SylphxAI/.github/.github/workflows/release.yml@main with inherited secrets and caller-side id-token: write for trusted publish identity.", "requiredContexts": ["test"], - "publishProof": "Release proof is the shared release workflow conclusion plus npm version readback for every affected @sylphx/cat package." - } + "publishProof": "Release proof is the shared release workflow conclusion plus npm registry readback for every affected @sylphx/cat package, with GroundAtlas project-control evidence preserved as CI artifact." + }, + "localContexts": ["ci includes project-control boundary test and GroundAtlas package dogfood"] }, "commercial": { "status": "not-applicable", @@ -133,7 +159,7 @@ ] }, "adoption": { - "status": "baseline", + "status": "adopted", "gaps": [ { "id": "adr29-admission", @@ -146,6 +172,12 @@ "description": "Cat currently uses Changesets through the shared release workflow; future doctrine should state the standard package-release path and bot ownership explicitly.", "owner": "SylphxAI/doctrine", "target": "Package release doctrine ADR and shared workflow catalog." + }, + { + "id": "groundatlas-fleet-ruleset", + "description": "Organization-wide mandatory GroundAtlas enforcement belongs in the owning fleet control plane, not this logging package repo.", + "owner": "SylphxAI/groundatlas", + "target": "fleet control-plane rollout" } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4310688..7e7939a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: fi git diff --name-only "$base" HEAD | tee /tmp/changed-files - if grep -Ev '^(\.doctrine/|\.github/|AGENTS\.md$|PROJECT\.md$|README\.md$|docs/)' /tmp/changed-files | grep -q .; then + if grep -Ev '^(\.doctrine/|\.github/|AGENTS\.md$|PROJECT\.md$|README\.md$|SECURITY\.md$|\.gitignore$|project\.manifest\.json$|docs/|test/project-control\.node-test\.mjs$)' /tmp/changed-files | grep -q .; then echo "runtime=true" >> "$GITHUB_OUTPUT" else echo "runtime=false" >> "$GITHUB_OUTPUT" @@ -62,3 +62,53 @@ jobs: - name: Build if: steps.changes.outputs.runtime == 'true' run: bun run build + + - name: Setup Node.js for project control + uses: actions/setup-node@v5 + with: + node-version: "22.14.0" + + - name: Test project-control boundary + run: node --test test/project-control.node-test.mjs + + - name: GroundAtlas package dogfood + id: groundatlas + uses: SylphxAI/groundatlas@v0.1.2 + with: + package-spec: groundatlas@0.1.2 + output-dir: .groundatlas-pilot + require-atlas: "true" + strict: "true" + + - name: Assert GroundAtlas truth boundary + run: | + node - "${{ steps.groundatlas.outputs.manifest-report-path }}" "${{ steps.groundatlas.outputs.fleet-report-path }}" <<'NODE' + const [manifestPath, fleetPath] = process.argv.slice(2); + const { readFileSync } = require("node:fs"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + const fleet = JSON.parse(readFileSync(fleetPath, "utf8")); + const project = fleet.projects?.[0]; + + function assert(condition, message) { + if (!condition) throw new Error(message); + } + + assert(manifest.selected?.path === "project.manifest.json", "GroundAtlas must select the vendor-neutral project.manifest.json file."); + assert(manifest.selected?.adapter === false, "Selected GroundAtlas manifest must not be an ecosystem adapter."); + assert(manifest.adapters?.some((item) => item.path === ".doctrine/project.json" && item.adapter === true), "Doctrine project manifest must be reported only as an adapter."); + assert(fleet.summary?.blocked === 0, "GroundAtlas fleet report must not have blocked projects."); + assert(fleet.summary?.warning === 0, "GroundAtlas strict fleet report must not have warning projects."); + assert(project?.status === "adopted", "This repository must be adopted in the GroundAtlas fleet report."); + assert(project?.manifest?.path === "project.manifest.json", "Fleet report must select project.manifest.json."); + assert(project?.manifestAdapters?.some((item) => item.path === ".doctrine/project.json" && item.adapter === true), "Fleet report must keep .doctrine/project.json as an adapter."); + NODE + + - name: Upload GroundAtlas reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: groundatlas-package-dogfood + path: | + ${{ steps.groundatlas.outputs.manifest-report-path }} + ${{ steps.groundatlas.outputs.fleet-report-path }} + if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d89a25..1d6d4cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ jobs: permissions: actions: write contents: write + id-token: write pull-requests: write uses: SylphxAI/.github/.github/workflows/release.yml@main secrets: inherit diff --git a/.gitignore b/.gitignore index 5083e56..d871916 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ logs/ node_modules/ Thumbs.db .turbo +.groundatlas/ +.groundatlas-* +.groundatlas-pilot/ diff --git a/AGENTS.md b/AGENTS.md index 6735d97..ee90fee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,26 @@ This repository follows the central Sylphx doctrine in `SylphxAI/doctrine`. Before changing code, read: -- `.doctrine/project.json` - `PROJECT.md` -- The affected package README and tests +- `project.manifest.json` +- `.doctrine/project.json` +- The affected package README, docs, and tests Keep Cat focused on reusable logging packages. Do not add product-specific logging behavior, platform deployment policy, or organization-wide release policy here. + +`project.manifest.json` is the vendor-neutral GroundAtlas control file. `.doctrine/project.json` is the Sylphx Doctrine adapter and org-local governance catalog. Generated `.groundatlas*` reports are evidence/navigation only, not source of truth. + +## Local Validation + +For project-control-only changes: + +```bash +bun install --frozen-lockfile +git diff --check +node --test test/project-control.node-test.mjs +npm exec --yes --package groundatlas@0.1.2 -- ga update --out .groundatlas-pilot +npm exec --yes --package groundatlas@0.1.2 -- ga audit --out .groundatlas-pilot +npm exec --yes --package groundatlas@0.1.2 -- ga fleet . --out .groundatlas-pilot --require-atlas --strict --json +``` + +For runtime package changes, also run the relevant package tests/build and do not expand this repo's logging scope to cover downstream product policy. diff --git a/PROJECT.md b/PROJECT.md index 244e007..24ab558 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -18,8 +18,12 @@ Cat is the Sylphx TypeScript logging package ecosystem. Its purpose is to ship s - Public API, package boundary, benchmark, release, or roadmap decisions should be recorded in ADRs under `docs/adr/` before they become durable policy. - Package publication uses Changesets release intent in `.changeset/` and the shared release workflow. -- The machine-readable project source of truth is `.doctrine/project.json`. +- The vendor-neutral project-control source of truth is `project.manifest.json`; `.doctrine/project.json` is the Sylphx Doctrine adapter and org-local governance catalog. ## Agent Entry -Before changing behavior, read `.doctrine/project.json`, this file, the affected package README, and the relevant package tests. Keep Cat zero-knowledge of downstream products. +Before changing behavior, read this file, `project.manifest.json`, `.doctrine/project.json`, the affected package README, and the relevant package tests. Keep Cat zero-knowledge of downstream products. + +## Project Control + +`project.manifest.json` is the vendor-neutral GroundAtlas control file for external agents and fleet dogfooding. `.doctrine/project.json` remains the Sylphx Doctrine adapter and local governance catalog. Generated `.groundatlas*` reports are evidence and navigation only; they are not source of truth. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e02bf43 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +Report vulnerabilities through GitHub private vulnerability reporting or the organization security contact. Do not disclose exploit details publicly before maintainers confirm impact and a remediation path. + +This repository handles reusable logging packages, redaction behavior, tracing context propagation, transports, and npm release credentials through CI. Security-sensitive changes require relevant package tests, redaction/tracing coverage when affected, and package registry readback after any future npm release. diff --git a/docs/adr/0001-groundatlas-project-control-gate.md b/docs/adr/0001-groundatlas-project-control-gate.md new file mode 100644 index 0000000..4583201 --- /dev/null +++ b/docs/adr/0001-groundatlas-project-control-gate.md @@ -0,0 +1,23 @@ +--- +status: accepted +slug: groundatlas-project-control-gate +--- + +# ADR 0001: GroundAtlas Project-Control Gate + +## Context + +Cat already has a Sylphx Doctrine adapter in `.doctrine/project.json`, but external project-control consumers need a vendor-neutral manifest and a machine gate that proves the repository can be understood without copying Sylphx-only governance into every tool. + +## Decision + +Adopt `project.manifest.json` as Cat's vendor-neutral GroundAtlas control file and keep `.doctrine/project.json` as the Sylphx-specific adapter and org-local governance catalog. + +CI will dogfood the released GroundAtlas package/action (`groundatlas@0.1.2` and `SylphxAI/groundatlas@v0.1.2`) on pull requests, merge groups, and main pushes. The gate must prove that GroundAtlas selects `project.manifest.json`, treats `.doctrine/project.json` only as an adapter, and reports the repository as adopted under strict fleet policy. + +## Consequences + +- Generated `.groundatlas*` reports are evidence/navigation only, never source of truth. +- Package runtime behavior remains owned by package source, tests, docs, and Changesets release intent. +- Package publication remains controlled by the shared release workflow, caller-side OIDC permission, and npm registry readback; GroundAtlas adoption alone does not publish a package. +- Future project-control changes must update the owning manifest/spec/test/workflow rather than duplicating policy in generated maps. diff --git a/docs/specs/project-control-gate.md b/docs/specs/project-control-gate.md new file mode 100644 index 0000000..96ab052 --- /dev/null +++ b/docs/specs/project-control-gate.md @@ -0,0 +1,37 @@ +# Project Control Gate + +## Purpose + +Make Cat dogfood GroundAtlas as a vendor-neutral project-control consumer while preserving Cat's package-specific release and observability-library boundaries. + +## Truth Boundary + +- `project.manifest.json` is the vendor-neutral GroundAtlas control file. +- `.doctrine/project.json` is the Sylphx Doctrine adapter and org-local governance catalog. +- Generated `.groundatlas*` and `.groundatlas-pilot/**` files are evidence/navigation only, not source of truth. +- Public package behavior lives in package source, package README files, tests, docs, and Changesets release intent. + +## Files Agents Must Read Before Adoption Changes + +1. `AGENTS.md` +2. `PROJECT.md` +3. `project.manifest.json` +4. `.doctrine/project.json` +5. `README.md`, package READMEs, and relevant `docs/**` +6. `docs/adr/**` when a durable package or release decision changes +7. `.github/workflows/ci.yml` and `.github/workflows/release.yml` +8. `package.json`, `turbo.json`, `biome.json`, package manifests, source, and tests touched by the change + +## CI Contract + +Pull requests, merge groups, and main pushes must: + +- keep runtime package checks on runtime-affecting changes; +- run the project-control boundary test; +- run `SylphxAI/groundatlas@v0.1.2` with `package-spec: groundatlas@0.1.2`, `require-atlas: true`, and `strict: true`; +- assert that GroundAtlas selects `project.manifest.json` and keeps `.doctrine/project.json` as an adapter; +- upload GroundAtlas reports as CI artifacts. + +## Release Boundary + +GroundAtlas adoption does not itself publish Cat packages. Package publication remains controlled by Changesets release intent, `.github/workflows/release.yml`, caller-side `id-token: write` permission for trusted publish identity, the shared Sylphx release workflow, and npm registry readback for affected `@sylphx/cat*` packages. diff --git a/project.manifest.json b/project.manifest.json new file mode 100644 index 0000000..dd6a9a6 --- /dev/null +++ b/project.manifest.json @@ -0,0 +1,135 @@ +{ + "$schema": "https://raw.githubusercontent.com/SylphxAI/groundatlas/main/schemas/project.manifest.schema.json", + "schemaVersion": 1, + "project": { + "id": "cat", + "name": "Cat / @sylphx/cat", + "summary": "TypeScript logging package ecosystem for lightweight structured logging, transports, formatting, tracing, redaction, OTLP export, and tail sampling.", + "lifecycle": "production", + "visibility": "open-source", + "repository": "https://github.com/SylphxAI/cat", + "homepage": "https://github.com/SylphxAI/cat#readme", + "tags": ["typescript", "logging", "observability", "structured-logs", "foundation-library"] + }, + "truth": { + "humanProjectFile": "PROJECT.md", + "agentAdapter": "AGENTS.md", + "specs": [ + "README.md", + "docs/guide/", + "docs/api/", + "docs/specs/project-control-gate.md", + "BUNDLE-SIZE-VERIFICATION.md", + "TEST-COVERAGE.md" + ], + "adrs": ["docs/adr/0001-groundatlas-project-control-gate.md", "docs/adr/"], + "source": [ + "packages/cat/src/", + "packages/cat-*/src/", + "package.json", + "bun.lock", + "turbo.json", + "biome.json", + ".github/workflows/ci.yml", + ".github/workflows/release.yml" + ], + "tests": [ + "packages/*/src/**/*.test.ts", + "test/project-control.node-test.mjs", + ".github/workflows/ci.yml" + ], + "runbooks": [ + "AGENTS.md", + "PROJECT.md", + "README.md", + "CONTRIBUTING.md", + "SECURITY.md", + "docs/specs/project-control-gate.md" + ] + }, + "surfaces": [ + { + "type": "package", + "name": "@sylphx/cat", + "path": "packages/cat/package.json", + "description": "Primary public logger package and core API contract." + }, + { + "type": "package", + "name": "@sylphx/cat-* extension packages", + "path": "packages/cat-*/package.json", + "description": "Optional public transports, formatters, redaction, tracing, OTLP, and tail-sampling packages." + }, + { + "type": "docs", + "name": "Repository README", + "path": "README.md", + "description": "Public package positioning, install guidance, examples, and benchmark claims." + }, + { + "type": "docs", + "name": "Documentation site source", + "path": "docs/", + "description": "Guide, API, examples, and troubleshooting documentation for package consumers." + }, + { + "type": "workflow", + "name": "CI", + "path": ".github/workflows/ci.yml", + "description": "Runtime checks plus project-control boundary test and GroundAtlas package/action dogfood." + }, + { + "type": "workflow", + "name": "Release", + "path": ".github/workflows/release.yml", + "description": "Main-branch package release workflow delegating to the shared Sylphx release workflow." + }, + { + "type": "docs", + "name": "Doctrine adapter manifest", + "path": ".doctrine/project.json", + "description": "Sylphx Doctrine adapter and org-local governance catalog; not the vendor-neutral GroundAtlas default." + } + ], + "commands": [ + { + "name": "install", + "command": "bun install --frozen-lockfile", + "purpose": "Install locked Bun workspace dependencies." + }, + { + "name": "check", + "command": "bun run check", + "purpose": "Run Biome checks for formatting and linting." + }, + { + "name": "test", + "command": "bun run test", + "purpose": "Run workspace package tests through Turbo." + }, + { + "name": "build", + "command": "bun run build", + "purpose": "Build workspace packages through Turbo." + }, + { + "name": "validate", + "command": "bun run check && bun run test && bun run build", + "purpose": "Run the practical runtime validation set used before package changes." + }, + { + "name": "test:project-control", + "command": "node --test test/project-control.node-test.mjs", + "purpose": "Verify neutral manifest, Doctrine adapter, CI dogfood, and release boundary." + }, + { + "name": "groundatlas:fleet", + "command": "npm exec --yes --package groundatlas@0.1.2 -- ga fleet . --out .groundatlas-pilot --require-atlas --strict --json", + "purpose": "Prove the vendor-neutral manifest is selected and generated GroundAtlas files are navigation-only." + } + ], + "adoption": { + "status": "adopted", + "notes": "Vendor-neutral GroundAtlas manifest for project-control dogfooding. .doctrine/project.json remains the Sylphx Doctrine adapter and local governance catalog. Generated .groundatlas* reports are evidence/navigation only, not source of truth. Package publication still requires the shared release workflow plus npm registry readback for affected packages." + } +} diff --git a/test/project-control.node-test.mjs b/test/project-control.node-test.mjs new file mode 100644 index 0000000..d46a20e --- /dev/null +++ b/test/project-control.node-test.mjs @@ -0,0 +1,71 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" +import test from "node:test" + +const readJson = (path) => JSON.parse(readFileSync(path, "utf8")) +const readText = (path) => readFileSync(path, "utf8") + +test("project manifest is the vendor-neutral GroundAtlas control file", () => { + const manifest = readJson("project.manifest.json") + + assert.equal(manifest.schemaVersion, 1) + assert.equal(manifest.project.id, "cat") + assert.equal(manifest.project.repository, "https://github.com/SylphxAI/cat") + assert.equal(manifest.project.visibility, "open-source") + assert.equal(manifest.project.lifecycle, "production") + assert.equal(manifest.adoption.status, "adopted") + assert.equal(manifest.truth.agentAdapter, "AGENTS.md") + assert.ok(manifest.truth.specs.includes("docs/specs/project-control-gate.md")) + assert.ok(manifest.truth.runbooks.includes("SECURITY.md")) + assert.ok( + manifest.surfaces.some( + (surface) => + surface.path === ".doctrine/project.json" && + surface.description.includes("not the vendor-neutral GroundAtlas default"), + ), + ) +}) + +test("Doctrine adapter remains Sylphx-specific and release boundary is explicit", () => { + const doctrine = readJson(".doctrine/project.json") + + assert.equal(doctrine.project.repo, "SylphxAI/cat") + assert.equal(doctrine.project.lifecycle, "production") + assert.ok( + doctrine.boundaries.publicSurfaces.some( + (surface) => surface.type === "manifest" && surface.location === "project.manifest.json", + ), + ) + assert.ok(doctrine.delivery.ciModel.includes("groundatlas")) + assert.ok(doctrine.delivery.productionProof.includes("GroundAtlas package dogfood")) + assert.ok(doctrine.delivery.packageRelease.publishProof.includes("npm registry readback")) +}) + +test("CI runs project-control and dogfoods the released GroundAtlas package/action", () => { + const workflow = readText(".github/workflows/ci.yml") + + assert.ok(workflow.includes("node --test test/project-control.node-test.mjs")) + assert.ok(workflow.includes("uses: SylphxAI/groundatlas@v0.1.2")) + assert.ok(workflow.includes("package-spec: groundatlas@0.1.2")) + assert.ok(workflow.includes('require-atlas: "true"')) + assert.ok(workflow.includes('strict: "true"')) + assert.ok(workflow.includes("project.manifest.json")) + assert.ok(workflow.includes(".doctrine/project.json")) +}) + +test("release workflow grants publish identity permission through the caller", () => { + const workflow = readText(".github/workflows/release.yml") + + assert.ok(workflow.includes("id-token: write")) + assert.ok(workflow.includes("uses: SylphxAI/.github/.github/workflows/release.yml@main")) + assert.ok(workflow.includes("secrets: inherit")) +}) + +test("root package keeps direct publish disabled", () => { + const pkg = readJson("package.json") + + assert.equal( + pkg.scripts.release, + 'echo "Direct changeset publish is unsafe for workspace packages; use the Sylphx release workflow." && exit 1', + ) +})