From 03a4fab9e0de42e6563eeeda9169b2c32bbf9660 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 13:25:49 +0200 Subject: [PATCH] Automate policy source freshness and inventory --- .forgejo/workflows/publish-image.yaml | 78 +- .gitignore | 1 + Containerfile | 18 +- Makefile | 11 +- README.md | 14 + WORK-RECORDS.md | 4 +- build/publication-manifest.json | 4 +- .../standards/tenancy-posture/v0.1/index.html | 6 +- source-inventory.config.json | 130 +++ source-inventory.json | 749 ++++++++++++++++++ tests/test_release.py | 31 + tests/test_source_inventory.py | 142 ++++ tools/build_site.py | 9 +- tools/fetch_sources.py | 143 ++++ tools/source_inventory.py | 295 +++++++ tools/verify_release.py | 27 +- ...S-WP-0001-permanent-publication-surface.md | 27 +- 17 files changed, 1647 insertions(+), 42 deletions(-) create mode 100644 source-inventory.config.json create mode 100644 source-inventory.json create mode 100644 tests/test_source_inventory.py create mode 100644 tools/fetch_sources.py create mode 100644 tools/source_inventory.py diff --git a/.forgejo/workflows/publish-image.yaml b/.forgejo/workflows/publish-image.yaml index bf42431..dab1656 100644 --- a/.forgejo/workflows/publish-image.yaml +++ b/.forgejo/workflows/publish-image.yaml @@ -11,10 +11,20 @@ on: - "Containerfile" - "deploy/**" - "publication.json" + - "source-inventory.config.json" + - "source-inventory.json" - "tests/**" - "tools/**" + schedule: + # Pull-based freshness audit. A failed source checkout, inventory drift, or + # overdue document is visible as a failed scheduled Forgejo Actions run. + - cron: "17 04 * * *" workflow_dispatch: +concurrency: + group: policy-nexus-publication + cancel-in-progress: false + env: REGISTRY: forgejo.coulomb.social IMAGE_NAME: coulomb/policy-nexus @@ -30,23 +40,27 @@ jobs: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | set -eu - REF="${GITHUB_SHA:-main}" + REF="${GITHUB_SHA:-}" + test "${#REF}" -eq 40 SHORT="${REF:0:7}" - mkdir -p buildctx/_sources/net-kingdom "${HOME}/bin" + BUILD_ROOT="$(mktemp -d)" + trap 'rm -rf "${BUILD_ROOT}"' EXIT + BUILD_CONTEXT="${BUILD_ROOT}/buildctx" + mkdir -p "${BUILD_CONTEXT}" "${HOME}/bin" - wget -qO /tmp/policy-nexus.tar.gz \ + wget -qO "${BUILD_ROOT}/policy-nexus.tar.gz" \ "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" - tar xzf /tmp/policy-nexus.tar.gz -C buildctx --strip-components=1 + tar xzf "${BUILD_ROOT}/policy-nexus.tar.gz" \ + -C "${BUILD_CONTEXT}" --strip-components=1 - NETKINGDOM_REVISION=$(git ls-remote \ - https://forgejo.coulomb.social/coulomb/net-kingdom.git \ - refs/heads/main | awk '{print $1}') - test -n "$NETKINGDOM_REVISION" - NETKINGDOM_SHORT="$(printf '%s' "$NETKINGDOM_REVISION" | cut -c1-7)" - wget -qO /tmp/net-kingdom.tar.gz \ - "https://forgejo.coulomb.social/coulomb/net-kingdom/archive/${NETKINGDOM_SHORT}.tar.gz" - tar xzf /tmp/net-kingdom.tar.gz \ - -C buildctx/_sources/net-kingdom --strip-components=1 + python3 "${BUILD_CONTEXT}/tools/fetch_sources.py" \ + --config "${BUILD_CONTEXT}/source-inventory.config.json" \ + --destination "${BUILD_CONTEXT}/_sources" \ + --policy-revision "${REF}" + NETKINGDOM_REVISION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["repositories"]["net-kingdom"]["revision"])' "${BUILD_CONTEXT}/_sources/source-lock.json")" + SOURCE_SET_DIGEST="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["source_set_digest"])' "${BUILD_CONTEXT}/_sources/source-lock.json")" + SOURCE_TAG="source-${SOURCE_SET_DIGEST}" + REVISION_TAG="git-${REF}-sources-${SOURCE_SET_DIGEST:0:16}" wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \ | tar xz --strip-components=1 -C "${HOME}/bin" docker/docker @@ -57,17 +71,43 @@ jobs: IMAGE="${REGISTRY}/${IMAGE_NAME}" docker build \ - --file buildctx/Containerfile \ + --file "${BUILD_CONTEXT}/Containerfile" \ --build-arg "VCS_REVISION=${REF}" \ --build-arg "NETKINGDOM_REVISION=${NETKINGDOM_REVISION}" \ - --tag "${IMAGE}:git-${REF}" \ + --build-arg "SOURCE_SET_DIGEST=${SOURCE_SET_DIGEST}" \ + --tag "${IMAGE}:${SOURCE_TAG}" \ + --tag "${IMAGE}:${REVISION_TAG}" \ --tag "${IMAGE}:main" \ - buildctx - docker push "${IMAGE}:git-${REF}" + "${BUILD_CONTEXT}" + PUSH_OUTPUT="$(docker push "${IMAGE}:${SOURCE_TAG}")" + printf '%s\n' "${PUSH_OUTPUT}" + IMAGE_DIGEST="$(printf '%s\n' "${PUSH_OUTPUT}" | awk '/digest: sha256:/{print $2}' | tail -1)" + test -n "${IMAGE_DIGEST}" + docker push "${IMAGE}:${REVISION_TAG}" docker push "${IMAGE}:main" PUBLICATION_DIGEST=$(docker run --rm --entrypoint sha256sum \ - "${IMAGE}:git-${REF}" /usr/share/nginx/html/publication-manifest.json \ + "${IMAGE}:${SOURCE_TAG}" /usr/share/nginx/html/publication-manifest.json \ | awk '{print $1}') - echo "published=${IMAGE}:git-${REF}" + SOURCE_INVENTORY_DIGEST=$(docker run --rm --entrypoint sha256sum \ + "${IMAGE}:${SOURCE_TAG}" /usr/share/nginx/html/source-inventory.json \ + | awk '{print $1}') + echo "published=${IMAGE}:${SOURCE_TAG}" + echo "image_digest=${IMAGE_DIGEST}" echo "publication_manifest_digest=${PUBLICATION_DIGEST}" + echo "source_inventory_digest=${SOURCE_INVENTORY_DIGEST}" + echo "source_set_digest=${SOURCE_SET_DIGEST}" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + echo "## policy-nexus release candidate" + echo + echo "The scheduled pull only publishes an immutable candidate; it does not deploy." + echo + echo "- image: \`${IMAGE}@${IMAGE_DIGEST}\`" + echo "- publication manifest: \`${PUBLICATION_DIGEST}\`" + echo "- source inventory: \`${SOURCE_INVENTORY_DIGEST}\`" + echo "- source set: \`${SOURCE_SET_DIGEST}\`" + echo + echo "Promotion requires an explicit paired digest update in rapp-policy-nexus and railiance-apps." + } >> "${GITHUB_STEP_SUMMARY}" + fi diff --git a/.gitignore b/.gitignore index 43ae0e2..f1e8f0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ *.py[cod] +build/source-inventory.json diff --git a/Containerfile b/Containerfile index 1e62b0d..b038fab 100644 --- a/Containerfile +++ b/Containerfile @@ -1,10 +1,14 @@ +ARG SOURCE_SET_DIGEST=unknown + FROM docker.io/nginxinc/nginx-unprivileged@sha256:65e3e85dbaed8ba248841d9d58a899b6197106c23cb0ff1a132b7bfe0547e4c0 AS runtime-base ARG VCS_REVISION=unknown +ARG SOURCE_SET_DIGEST LABEL org.opencontainers.image.title="policy-nexus" \ org.opencontainers.image.description="Canonical Coulomb policy publication surface" \ org.opencontainers.image.source="https://forgejo.coulomb.social/coulomb/policy-nexus" \ - org.opencontainers.image.revision="$VCS_REVISION" + org.opencontainers.image.revision="$VCS_REVISION" \ + org.coulomb.policy.source-set-digest="$SOURCE_SET_DIGEST" COPY --chown=101:101 deploy/nginx.conf /etc/nginx/conf.d/default.conf @@ -16,13 +20,19 @@ COPY --chown=101:101 build/ /usr/share/nginx/html/ FROM docker.io/library/python@sha256:d09d15e60962ca365d1cd544a48773bac9d33f2fb1b00f2aa0deec78ade7dc31 AS release-builder ARG NETKINGDOM_REVISION +ARG SOURCE_SET_DIGEST ENV POLICY_NEXUS_SOURCE_REVISION_NET_KINGDOM=$NETKINGDOM_REVISION +ENV POLICY_NEXUS_SOURCE_ROOT=/workspace/_sources WORKDIR /workspace/policy-nexus COPY . /workspace/policy-nexus -COPY _sources/net-kingdom /workspace/net-kingdom -RUN rm -rf build \ - && python3 -m unittest discover -s tests -p 'test_*.py' \ +COPY _sources /workspace/_sources +RUN python3 -m unittest discover -s tests -p 'test_*.py' \ && python3 tools/build_site.py publication.json --output build \ + && python3 tools/source_inventory.py check \ + --source-root /workspace/_sources \ + --lock /workspace/_sources/source-lock.json \ + --report build/source-inventory.json \ + && test "$(python3 -c 'import json; print(json.load(open("/workspace/_sources/source-lock.json"))["source_set_digest"])')" = "$SOURCE_SET_DIGEST" \ && python3 tools/verify_release.py build \ && python3 tools/check_currency.py publication.json diff --git a/Makefile b/Makefile index bf70359..b1b40be 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build check currency clean release-build release-check image-build +.PHONY: build check currency source-audit clean release-build release-check image-build # Publication targets. Source of truth is always the upstream repo; pages here # are generated and must never be hand-edited. @@ -6,12 +6,17 @@ SRC_NETKINGDOM ?= ../net-kingdom build: python3 tools/build_site.py publication.json --output build + python3 tools/source_inventory.py check --report build/source-inventory.json check: python3 -m unittest discover -s tests -p 'test_*.py' - python3 -m py_compile tools/render.py tools/build_site.py tools/check_currency.py tools/verify_release.py + python3 -m py_compile tools/render.py tools/build_site.py tools/check_currency.py tools/verify_release.py tools/source_inventory.py tools/fetch_sources.py + $(MAKE) source-audit git diff --check +source-audit: + python3 tools/source_inventory.py check + currency: python3 tools/check_currency.py publication.json @@ -22,7 +27,7 @@ release-check: python3 tools/verify_release.py build python3 tools/check_currency.py publication.json -release-build: clean build release-check +release-build: build release-check image-build: release-check @test -n "$(IMAGE_REF)" || (echo "IMAGE_REF is required" >&2; exit 2) diff --git a/README.md b/README.md index 0e67af7..28232c8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,20 @@ make currency `publication.json` is the explicit source and address registry. A build fails closed when a source is unavailable or an immutable revision would change. +`source-inventory.config.json` defines the bounded canon/ADR discovery scope, +while `source-inventory.json` records an explicit reviewed disposition for every +matching source. `make source-audit` fails when a source appears or disappears +without that review. Working-tree-only files in sibling repos do not affect the +audit; local checks inspect committed Git trees. + +The Forgejo workflow pulls exact `main` revisions for all inventoried source +repos every day at 04:17 UTC and on manual dispatch. It fails visibly on an +unavailable source, unreviewed inventory drift, invalid release, or overdue +published document. Successful runs publish immutable `source-` +candidates and a moving discovery tag, but never deploy them. Production +promotion remains an explicit review of the registry-resolved OCI digest and +publication-manifest digest together in `rapp-policy-nexus` and +`railiance-apps`. Production publication is split from runtime ownership. This repository builds and publishes the immutable OCI site image; `rapp-policy-nexus` owns the Helm diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index f58aab9..e766fa8 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -8,10 +8,10 @@ | Kind | ID | Status | Lane | Source | | --- | --- | --- | --- | --- | -| workplan | POLICY-NEXUS-WP-0001 | active | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | +| workplan | POLICY-NEXUS-WP-0001 | finished | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | | task | POLICY-NEXUS-WP-0001-T01 | done | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | | task | POLICY-NEXUS-WP-0001-T02 | done | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | -| task | POLICY-NEXUS-WP-0001-T03 | progress | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | +| task | POLICY-NEXUS-WP-0001-T03 | done | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | | task | POLICY-NEXUS-WP-0001-T04 | done | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | | task | POLICY-NEXUS-WP-0001-T05 | done | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | | task | POLICY-NEXUS-WP-0001-T06 | done | — | workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md | diff --git a/build/publication-manifest.json b/build/publication-manifest.json index fd93e17..8891b65 100644 --- a/build/publication-manifest.json +++ b/build/publication-manifest.json @@ -11,7 +11,9 @@ "revision": "draft-8", "revision_path": "standards/tenancy-posture/v0.1/revisions/draft-8/index.html", "source_digest": "99f802d91a0b3a65f0dac58230d8904f7c61cf3f81eff072fbbc59b634612a8a", - "source_revision": "cced59d3aa1dc0aa08fc128fc8c76699f59dcd90", + "source_path": "canon/standards/tenancy-posture_v0.1.md", + "source_repo": "net-kingdom", + "source_revision": "f4f885289e196b6770fea5d509959e3031fd9295", "status": "proposed", "title": "NetKingdom Tenancy Posture v0.1" } diff --git a/build/standards/tenancy-posture/v0.1/index.html b/build/standards/tenancy-posture/v0.1/index.html index 5c758cb..9dc8a63 100644 --- a/build/standards/tenancy-posture/v0.1/index.html +++ b/build/standards/tenancy-posture/v0.1/index.html @@ -1,6 +1,6 @@ - + NetKingdom Tenancy Posture v0.1 -
netkingdom-tenancy-posture proposed · draft-8 net-kingdom reviewed 2026-08-17generated from canonical source — do not edit

NetKingdom Tenancy Posture v0.1

A framework for describing, holding and improving multi-tenancy — including where we are not there yet.

Source: net-kingdom · canon/standards/tenancy-posture_v0.1.md · cced59d3aa1dc0aa08fc128fc8c76699f59dcd90

Review due: 2027-02-17

Status

+
netkingdom-tenancy-posture proposed · draft-8 net-kingdom reviewed 2026-08-17generated from canonical source — do not edit

NetKingdom Tenancy Posture v0.1

A framework for describing, holding and improving multi-tenancy — including where we are not there yet.

Source: net-kingdom · canon/standards/tenancy-posture_v0.1.md · f4f885289e196b6770fea5d509959e3031fd9295

Review due: 2027-02-17

Status

Proposed, draft-8; ratification-ready. Relocated from the-custodian/canon/architecture on 2026-08-17: multi-tenancy is part of the IT-security framework NetKingdom provides, so this framework belongs in NetKingdom canon beside the IAM Profile and the tenant-engine boundary contract, not in the work-factory canon.

  • draft-1 proposed a single model with fixed characteristics. Rejected: it could not describe a repo that is not there yet.
  • draft-2 reframed to graduated levels per axis. Externally corroborated (§16), but four of its statements were wrong and one thing it needed was missing.
  • draft-3 applied those corrections, added the retention axis, and recorded an adoption stance.
  • draft-4 closed the two gaps draft-3 left open: R4 had no mechanism beyond waiting, and the noisy-neighbour evidence artifact asserted something shared infrastructure cannot provide.
  • draft-5 relocated to NetKingdom and renamed the dimensions from planes to axes, because the word was already taken (§0).
  • draft-6 applied tenant-engine's review: five changes, including an axis that did not fit its data shape.
  • draft-7 applies audit-core, railiance-platform and flex-auth. Eleven further changes, two of them corrections to statements this document made as fact about other repos. Every posture I guessed was too generous, on every repo that has now self-reported.
  • draft-8 applies adaptive-pricing's review, the last of the six, and the consistency review across all declarations. It adds the missing availability axis, a canonical declaration schema, explicit authority for tier assurance, retention/placement coupling, downgrade propagation, and honest sanctioned customer language. It also corrects the distinction between an implemented control and an evidenced current level.

Reviewed by all six. The score: six repos found three live defects in their own code by reading the ladders — tenant-engine's unfiltered event accessor, audit-core's unfiltered read path, flex-auth's unauthenticated /v1/check — and railiance-platform found apps-pg running with no backup configured at all while writing its §10.2 disclosure. The framework changed to fit the repos; no repo was told to fabricate a posture.

@@ -441,4 +441,4 @@ per consumer: 14 connections (12 runtime + 2 migration)

20Ratification path

  1. Reviewed by tenant-engine, flex-auth, audit-core, rapp-postgres, railiance-platform and adaptive-pricing against §19. Complete in draft-8.
  2. Each publishes its own posture vector (§5) as part of review. The framework is validated by whether it can describe them accurately — if a repo cannot express itself in these six ladders, the ladders are wrong and this document changes, not the repo. Complete in draft-8; all six root declarations validate against the canonical schema.
  3. On acceptance, supersedes the routing of rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1-draft.md, whose §§3–8 are absorbed here. That draft is withdrawn rather than left pending.
  4. On acceptance, rapp-postgres ADR-0001 through ADR-0004 move to accepted and are annotated as the PostgreSQL implementation of the E, P, R and shared-capacity rules.
-
netkingdom-tenancy-posture · draft-8 · proposednet-kingdom · canon/standards/tenancy-posture_v0.1.md · cced59d3aa1dc0aa08fc128fc8c76699f59dcd90
+
netkingdom-tenancy-posture · draft-8 · proposednet-kingdom · canon/standards/tenancy-posture_v0.1.md · f4f885289e196b6770fea5d509959e3031fd9295
diff --git a/source-inventory.config.json b/source-inventory.config.json new file mode 100644 index 0000000..dc65333 --- /dev/null +++ b/source-inventory.config.json @@ -0,0 +1,130 @@ +{ + "schema_version": 1, + "repositories": { + "activity-core": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/activity-core.git", + "selectors": ["docs/adr/*.md"] + }, + "artifact-store": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/artifact-store.git", + "selectors": ["docs/adr/*.md"] + }, + "binect-js": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/binect-js.git", + "selectors": ["docs/adr/*.md"] + }, + "coulomb-loop": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/coulomb-loop.git", + "selectors": ["docs/adr/*.md"] + }, + "coulomb-social": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/coulomb-social.git", + "selectors": ["docs/adr/*.md"] + }, + "evidence-binder": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/evidence-binder.git", + "selectors": ["docs/adr/*.md"] + }, + "flex-auth": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/flex-auth.git", + "selectors": ["docs/adr/*.md"] + }, + "glas-harness": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/glas-harness.git", + "selectors": ["docs/adr/*.md"] + }, + "kaizen-agentic": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/kaizen-agentic.git", + "selectors": ["docs/adr/*.md"] + }, + "key-cape": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/key-cape.git", + "selectors": ["docs/adr/*.md"] + }, + "markitect-main": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/markitect-main.git", + "selectors": ["docs/adr/*.md"] + }, + "net-kingdom": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/net-kingdom.git", + "selectors": ["canon/standards/**", "docs/adr/*.md"] + }, + "policy-nexus": { + "local": true, + "selectors": ["docs/adr/*.md"] + }, + "railiance-hosts": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/railiance-hosts.git", + "selectors": ["docs/adr/*.md"] + }, + "railiance-infra": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/railiance-infra.git", + "selectors": ["docs/adr/*.md"] + }, + "railiance-master": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/railiance-master.git", + "selectors": ["docs/adr/*.md"] + }, + "railiance-platform": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/railiance-platform.git", + "selectors": ["docs/adr/*.md"] + }, + "rapp-postgres": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/rapp-postgres.git", + "selectors": ["docs/adr/*.md"] + }, + "rein-aharness": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/rein-aharness.git", + "selectors": ["docs/adr/*.md"] + }, + "target-revenue": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/target-revenue.git", + "selectors": ["docs/adr/*.md"] + }, + "the-custodian": { + "branch": "main", + "remote": "https://forgejo.coulomb.social/coulomb/the-custodian.git", + "selectors": [ + "canon/architecture/**", + "canon/constitution/**", + "canon/standards/**" + ] + } + }, + "excluded_scopes": [ + { + "repository": "the-custodian", + "path": "canon/values", + "reason": "Out of publication scope until the canon owner marks an individual document as governing." + }, + { + "repository": "the-custodian", + "path": "canon/tpsc", + "reason": "Out of publication scope until the canon owner marks an individual document as governing." + }, + { + "repository": "the-custodian", + "path": "canon/projects", + "reason": "Project material is not governing canon for this publication surface." + } + ] +} diff --git a/source-inventory.json b/source-inventory.json new file mode 100644 index 0000000..65e40a2 --- /dev/null +++ b/source-inventory.json @@ -0,0 +1,749 @@ +{ + "schema_version": 1, + "sources": [ + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/adr-001-event-bridge-architecture.md", + "source_repo": "activity-core" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/adr-002-definition-format.md", + "source_repo": "activity-core" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/adr-003-rule-instruction-model.md", + "source_repo": "activity-core" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/adr-004-producer-trust-boundary.md", + "source_repo": "activity-core" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/adr-005-ops-runs-vs-dev-work-records.md", + "source_repo": "activity-core" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0001-content-addressed-storage.md", + "source_repo": "artifact-store" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0002-event-log-source-of-truth.md", + "source_repo": "artifact-store" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0003-manifest-canonical-cbor.md", + "source_repo": "artifact-store" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0004-control-plane-data-plane-contract.md", + "source_repo": "artifact-store" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0005-v1-tech-stack.md", + "source_repo": "artifact-store" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0006-oci-compatibility-reachable.md", + "source_repo": "artifact-store" + }, + { + "disposition": "excluded", + "reason": "Directory index, not an architecture decision record.", + "source_path": "docs/adr/README.md", + "source_repo": "artifact-store" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/001-no-listall-method.md", + "source_repo": "binect-js" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-001-workplan-prefix.md", + "source_repo": "coulomb-loop" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-002-customer-supplier-boundary.md", + "source_repo": "coulomb-loop" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-003-cadence-ramp-policy.md", + "source_repo": "coulomb-loop" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md", + "source_repo": "coulomb-loop" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-netkingdom-identity.md", + "source_repo": "coulomb-social" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0002-space-content-forgejo-markdown.md", + "source_repo": "coulomb-social" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0003-page-centric-markdown-sor.md", + "source_repo": "coulomb-social" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0004-content-plane-thin-git-upgrades.md", + "source_repo": "coulomb-social" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-reference-ui-surface.md", + "source_repo": "evidence-binder" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0001-implementation-language-and-skeleton.md", + "source_repo": "flex-auth" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0002-rego-in-markdown-policy-format.md", + "source_repo": "flex-auth" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/0003-topaz-aligned-mvp.md", + "source_repo": "flex-auth" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-001-rein-harness-family.md", + "source_repo": "glas-harness" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-002-credential-brokering-and-composable-reins.md", + "source_repo": "glas-harness" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md", + "source_repo": "glas-harness" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-004-composable-reins-stay-deferred.md", + "source_repo": "glas-harness" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-001-workplan-convention.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-002-project-memory-convention.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-003-protocols-artifact-convention.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-004-project-metrics-convention.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-005-scheduled-agent-execution.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-006-customer-engagement-convention.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-007-forward-deployed-engagement-convention.md", + "source_repo": "kaizen-agentic" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-choose-go-for-keycape.md", + "source_repo": "key-cape" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-001-client-side-debug-storage.md", + "source_repo": "markitect-main" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-002-robustness-principle-for-production-use.md", + "source_repo": "markitect-main" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/credential-management_v0.2.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/iam-profile_v0.2.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/iam-profile_v0.3.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/playbook-capability-contract_v0.1.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "published", + "reason": "Published through an explicit publication.json document entry.", + "source_path": "canon/standards/tenancy-posture_v0.1.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/tenant-engine-boundary-contract_v0.1.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/user-engine-boundary-contract_v0.1.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0007-security-orchestration-boundary.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0008-object-storage-sts-credential-vending.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0010-orchestration-vs-dependency-self-coherent-intent.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0011-iam-profile-ownership-and-version-governance.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0012-playbook-capability-contract-ownership.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-addressing-and-permanence.md", + "source_repo": "policy-nexus" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md", + "source_repo": "railiance-hosts" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-003-railiance-5repo-stack-architecture.md", + "source_repo": "railiance-hosts" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md", + "source_repo": "railiance-hosts" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md", + "source_repo": "railiance-infra" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-003-railiance-5repo-stack-architecture.md", + "source_repo": "railiance-infra" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md", + "source_repo": "railiance-infra" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-005-k3s-api-tunnel-only.md", + "source_repo": "railiance-infra" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-repository-prefix-architecture.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0003-rapp-first-wave-selection.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0004-first-wave-reef-rollout.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0005-derived-rail-composition.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0006-reef-production-admission.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0007-rapp-declaration-contract.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0008-private-by-default-exposure.md", + "source_repo": "railiance-master" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-s3-platform-service-boundary.md", + "source_repo": "railiance-platform" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0002-placement-policy-ownership.md", + "source_repo": "railiance-platform" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0003-decisions-live-in-the-repo.md", + "source_repo": "railiance-platform" + }, + { + "disposition": "excluded", + "reason": "Directory index, not an architecture decision record.", + "source_path": "docs/adr/README.md", + "source_repo": "railiance-platform" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md", + "source_repo": "rapp-postgres" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0002-data-retention-and-erasure.md", + "source_repo": "rapp-postgres" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0003-e3-row-level-security-contract.md", + "source_repo": "rapp-postgres" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0004-platform-pg-cell-ceiling.md", + "source_repo": "rapp-postgres" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-001-agent-harness-architecture.md", + "source_repo": "rein-aharness" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0001-stage0-library-stack.md", + "source_repo": "target-revenue" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0002-hosted-trust-service-stack.md", + "source_repo": "target-revenue" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-001-workplans-as-repo-artefacts.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-002-custodian-agent-runtime-design.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-003-materialized-derived-state.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-004-connectivity-first-network-posture.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-005-cross-repo-workplans-project-repos.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-006-canon-federation-concept-ownership.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-008-multi-tenancy-model.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-010-hub-authority-and-local-cache-model.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/architecture/adr-011-federated-namespaces-and-reconciliation-limits.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/constitution/bootstrap-protocol_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/constitution/custodian_constitution_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/autonomy-lanes_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/business-app-service-contract_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/contrib-templates/br-template.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/contrib-templates/ep-template.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/contrib-templates/fr-template.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/contrib-templates/upr-template.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/contribution-convention_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/coulombcore-production-freeze_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/credential-management_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/federated-organization-standard_v1.0.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/iam-profile_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/orthogonal-architecture-schema_v1.0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/orthogonal-architecture_v1.0.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/privileged-execution-control-schema-cicd_v0.2.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/privileged-execution-control-schema-kubernetes-rbac_v0.2.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/privileged-execution-control-schema-os-sudo_v0.2.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/privileged-execution-control-schema_v0.2.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/privileged-execution-control_v0.2", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/project-repository-flavor_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/repo-classification-standard_v1.0.md", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/repo-classification.allowed.yaml", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/repo-classification.exclusions.yaml", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/sbom-convention_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/schemas/work-records/decision.schema.json", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/schemas/work-records/engagement.schema.json", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/schemas/work-records/intake.schema.json", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/schemas/work-records/spine.schema.json", + "source_repo": "the-custodian" + }, + { + "disposition": "unsupported-format", + "reason": "Inventoried governing source is not Markdown and has no renderer yet.", + "source_path": "canon/standards/work-record-types.yaml", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/work-record-types_v0.1.md", + "source_repo": "the-custodian" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/workplan-terminology-fleet_v0.1.md", + "source_repo": "the-custodian" + } + ] +} diff --git a/tests/test_release.py b/tests/test_release.py index 9ba7b7f..2a336f8 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -46,6 +46,8 @@ def _release(root: Path) -> Path: "review_due": "2027-02-18", "canonical_path": "standards/example/v1/index.html", "revision_path": "standards/example/v1/revisions/draft-1/index.html", + "source_repo": "canon", + "source_path": "canon/standards/example.md", "source_revision": COMMIT, "source_digest": SOURCE_DIGEST, } @@ -54,10 +56,38 @@ def _release(root: Path) -> Path: (build / "publication-manifest.json").write_text( json.dumps(manifest, sort_keys=True) + "\n", encoding="utf-8" ) + (build / "source-inventory.json").write_text( + json.dumps( + { + "schema_version": "policy-nexus-source-inventory/v1", + "source_set_digest": "3" * 64, + "repositories": [ + {"name": "canon", "revision": COMMIT, "source_count": 1} + ], + "sources": [ + { + "source_repo": "canon", + "source_path": "canon/standards/example.md", + "disposition": "published", + "reason": "test", + } + ], + }, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) return build class ReleaseVerificationTest(unittest.TestCase): + def test_release_pipeline_does_not_delete_immutable_revision_tree(self) -> None: + makefile = (ROOT / "Makefile").read_text(encoding="utf-8") + containerfile = (ROOT / "Containerfile").read_text(encoding="utf-8") + self.assertIn("release-build: build release-check", makefile) + self.assertNotIn("RUN rm -rf build", containerfile) + def test_accepts_clean_provenance_and_returns_manifest_digest(self) -> None: with tempfile.TemporaryDirectory() as directory: build = _release(Path(directory)) @@ -67,6 +97,7 @@ class ReleaseVerificationTest(unittest.TestCase): ).hexdigest() self.assertEqual(expected, evidence["publication_manifest_digest"]) self.assertEqual(["example"], evidence["documents"]) + self.assertEqual("3" * 64, evidence["source_set_digest"]) def test_rejects_working_tree_source_revision(self) -> None: with tempfile.TemporaryDirectory() as directory: diff --git a/tests/test_source_inventory.py b/tests/test_source_inventory.py new file mode 100644 index 0000000..3bfab15 --- /dev/null +++ b/tests/test_source_inventory.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +import sys +import tempfile +import unittest + + +ROOT = Path(__file__).parents[1] +sys.path.insert(0, str(ROOT / "tools")) +from source_inventory import check, refresh + + +REVISION = "a" * 40 + + +def _fixture(root: Path) -> tuple[Path, Path, Path, Path]: + source = root / "docs/adr/ADR-0001.md" + source.parent.mkdir(parents=True) + source.write_text("# ADR-0001\n", encoding="utf-8") + config = root / "source-inventory.config.json" + config.write_text( + json.dumps( + { + "schema_version": 1, + "repositories": { + "policy-nexus": { + "local": True, + "selectors": ["docs/adr/*.md"], + } + }, + } + ), + encoding="utf-8", + ) + publication = root / "publication.json" + publication.write_text( + json.dumps( + { + "schema_version": 1, + "documents": [ + { + "source_repo": "policy-nexus", + "source_path": "docs/adr/ADR-0001.md", + } + ], + } + ), + encoding="utf-8", + ) + revisions = {"policy-nexus": REVISION} + digest = hashlib.sha256( + json.dumps(revisions, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + lock = root / "source-lock.json" + lock.write_text( + json.dumps( + { + "schema_version": 1, + "source_set_digest": digest, + "repositories": {"policy-nexus": {"revision": REVISION}}, + } + ), + encoding="utf-8", + ) + return config, root / "source-inventory.json", publication, lock + + +class SourceInventoryTest(unittest.TestCase): + def test_refresh_and_check_bind_publication_to_inventory(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + config, inventory, publication, lock = _fixture(root) + refresh( + config, + inventory, + publication, + policy_root=root, + source_root=root.parent, + ) + report = check( + config, + inventory, + publication, + policy_root=root, + source_root=root.parent, + lock_path=lock, + ) + self.assertEqual(1, report["dispositions"]["published"]) + self.assertEqual(REVISION, report["repositories"][0]["revision"]) + + def test_check_rejects_unreviewed_new_source(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + config, inventory, publication, lock = _fixture(root) + refresh( + config, + inventory, + publication, + policy_root=root, + source_root=root.parent, + ) + (root / "docs/adr/ADR-0002.md").write_text("# New\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "unreviewed sources"): + check( + config, + inventory, + publication, + policy_root=root, + source_root=root.parent, + lock_path=lock, + ) + + def test_check_rejects_published_disposition_drift(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + config, inventory, publication, lock = _fixture(root) + refresh( + config, + inventory, + publication, + policy_root=root, + source_root=root.parent, + ) + value = json.loads(inventory.read_text(encoding="utf-8")) + value["sources"][0]["disposition"] = "metadata-pending" + inventory.write_text(json.dumps(value), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "must exactly match"): + check( + config, + inventory, + publication, + policy_root=root, + source_root=root.parent, + lock_path=lock, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/build_site.py b/tools/build_site.py index 2e3089b..46de93f 100644 --- a/tools/build_site.py +++ b/tools/build_site.py @@ -149,8 +149,13 @@ def build( manifest_path = manifest_path.resolve() manifest = load_manifest(manifest_path) as_of = as_of or dt.date.today() + source_root = os.environ.get("POLICY_NEXUS_SOURCE_ROOT") repository_paths = { - name: (manifest_path.parent / config["path"]).resolve() + name: ( + (Path(source_root) / name).resolve() + if source_root + else (manifest_path.parent / config["path"]).resolve() + ) for name, config in manifest["repositories"].items() } output_parent = output.resolve().parent @@ -255,6 +260,8 @@ def build( "lifecycle": lifecycle, "canonical_path": canonical.as_posix(), "revision_path": revision_path.as_posix(), + "source_repo": document["source_repo"], + "source_path": document["source_path"], "source_revision": source_revision, "source_digest": source_digest, } diff --git a/tools/fetch_sources.py b/tools/fetch_sources.py new file mode 100644 index 0000000..9ee269a --- /dev/null +++ b/tools/fetch_sources.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Fetch exact Forgejo source archives declared by the policy source inventory.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path, PurePosixPath +import subprocess +import tarfile +import tempfile +import urllib.request + + +def _revision(remote: str, branch: str) -> str: + result = subprocess.run( + ["git", "ls-remote", remote, f"refs/heads/{branch}"], + check=True, + capture_output=True, + text=True, + ) + revision = result.stdout.split(maxsplit=1)[0] if result.stdout.strip() else "" + if len(revision) != 40 or any(char not in "0123456789abcdef" for char in revision): + raise ValueError(f"{remote}: could not resolve a clean 40-hex {branch} revision") + return revision + + +def _archive_url(remote: str, revision: str) -> str: + if not remote.startswith("https://") or not remote.endswith(".git"): + raise ValueError(f"archive source must be an HTTPS .git URL, got {remote!r}") + return f"{remote[:-4]}/archive/{revision[:7]}.tar.gz" + + +def _extract(archive: Path, target: Path) -> None: + target.mkdir(parents=True, exist_ok=False) + with tarfile.open(archive, "r:gz") as bundle: + members = bundle.getmembers() + roots = { + PurePosixPath(member.name).parts[0] + for member in members + if PurePosixPath(member.name).parts + } + if len(roots) != 1: + raise ValueError(f"{archive}: expected exactly one archive root") + root = next(iter(roots)) + for member in members: + path = PurePosixPath(member.name) + if not path.parts or path.parts[0] != root: + raise ValueError(f"{archive}: inconsistent archive root") + relative = PurePosixPath(*path.parts[1:]) + if not relative.parts: + continue + if relative.is_absolute() or ".." in relative.parts: + raise ValueError(f"{archive}: unsafe member {member.name!r}") + destination = target.joinpath(*relative.parts) + if member.isdir(): + destination.mkdir(parents=True, exist_ok=True) + continue + if not member.isfile(): + # Links and special files are not part of the publishable source + # corpus. Skipping them avoids materializing archive links; if a + # selector ever names one, the inventory audit fails on absence. + continue + destination.parent.mkdir(parents=True, exist_ok=True) + source = bundle.extractfile(member) + if source is None: + raise ValueError(f"{archive}: could not read {member.name!r}") + with destination.open("wb") as output: + while chunk := source.read(1024 * 1024): + output.write(chunk) + + +def fetch(config_path: Path, destination: Path, policy_revision: str) -> dict[str, object]: + if len(policy_revision) != 40 or any( + char not in "0123456789abcdef" for char in policy_revision + ): + raise ValueError("--policy-revision must be a clean 40-hex Git commit") + config = json.loads(config_path.read_text(encoding="utf-8")) + if config.get("schema_version") != 1: + raise ValueError("source inventory config schema_version must be 1") + destination.mkdir(parents=True, exist_ok=True) + if any(destination.iterdir()): + raise ValueError(f"destination must be empty: {destination}") + + revisions: dict[str, str] = {} + repositories: dict[str, dict[str, str]] = {} + for name, repository in sorted(config["repositories"].items()): + if repository.get("local"): + revision = policy_revision + repositories[name] = {"revision": revision, "source": "workflow-checkout"} + revisions[name] = revision + continue + remote = repository["remote"] + branch = repository.get("branch", "main") + revision = _revision(remote, branch) + url = _archive_url(remote, revision) + with tempfile.NamedTemporaryFile(suffix=".tar.gz") as archive: + with urllib.request.urlopen(url, timeout=60) as response: + while chunk := response.read(1024 * 1024): + archive.write(chunk) + archive.flush() + _extract(Path(archive.name), destination / name) + revisions[name] = revision + repositories[name] = { + "revision": revision, + "remote": remote, + "branch": branch, + } + + canonical = json.dumps(revisions, sort_keys=True, separators=(",", ":")).encode() + lock: dict[str, object] = { + "schema_version": 1, + "source_set_digest": hashlib.sha256(canonical).hexdigest(), + "repositories": repositories, + } + (destination / "source-lock.json").write_text( + json.dumps(lock, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + return lock + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=Path, default=Path("source-inventory.config.json")) + parser.add_argument("--destination", type=Path, required=True) + parser.add_argument("--policy-revision", required=True) + args = parser.parse_args() + lock = fetch(args.config.resolve(), args.destination.resolve(), args.policy_revision) + print( + json.dumps( + { + "source_set_digest": lock["source_set_digest"], + "repositories": len(lock["repositories"]), + }, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/source_inventory.py b/tools/source_inventory.py new file mode 100644 index 0000000..06b40bb --- /dev/null +++ b/tools/source_inventory.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Audit the explicit canon/ADR corpus and emit deterministic source evidence.""" + +from __future__ import annotations + +import argparse +import fnmatch +import hashlib +import json +from pathlib import Path +import subprocess +import sys +from typing import Any + + +SCHEMA_VERSION = "policy-nexus-source-inventory/v1" +DISPOSITIONS = {"published", "metadata-pending", "excluded", "unsupported-format"} + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"{path}: expected a JSON object") + return value + + +def _tracked_files(repo: Path) -> list[str]: + if not repo.is_dir(): + raise FileNotFoundError(f"source repository is unavailable: {repo}") + if (repo / ".git").exists(): + result = subprocess.run( + ["git", "-C", str(repo), "ls-tree", "-r", "--name-only", "HEAD"], + check=True, + capture_output=True, + text=True, + ) + return [line for line in result.stdout.splitlines() if line] + return sorted( + path.relative_to(repo).as_posix() + for path in repo.rglob("*") + if path.is_file() and not path.is_symlink() + ) + + +def _revision(repo: Path) -> str: + if not (repo / ".git").exists(): + raise ValueError(f"{repo}: source lock is required for an archive checkout") + result = subprocess.run( + ["git", "-C", str(repo), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ) + revision = result.stdout.strip() + if len(revision) != 40 or any(char not in "0123456789abcdef" for char in revision): + raise ValueError(f"{repo}: invalid Git revision {revision!r}") + return revision + + +def _repo_paths( + config: dict[str, Any], *, policy_root: Path, source_root: Path +) -> dict[str, Path]: + paths: dict[str, Path] = {} + for name, repository in config["repositories"].items(): + paths[name] = policy_root if repository.get("local") else source_root / name + return paths + + +def discover( + config: dict[str, Any], *, policy_root: Path, source_root: Path +) -> tuple[list[dict[str, str]], dict[str, Path]]: + paths = _repo_paths(config, policy_root=policy_root, source_root=source_root) + sources: list[dict[str, str]] = [] + for name, repository in sorted(config["repositories"].items()): + selectors = repository.get("selectors", []) + if not selectors: + raise ValueError(f"{name}: at least one source selector is required") + for path in _tracked_files(paths[name]): + if any(fnmatch.fnmatchcase(path, selector) for selector in selectors): + sources.append({"source_repo": name, "source_path": path}) + return sources, paths + + +def _published_sources(publication_path: Path) -> set[tuple[str, str]]: + publication = _read_json(publication_path) + return { + (document["source_repo"], document["source_path"]) + for document in publication.get("documents", []) + } + + +def _new_entry(source: dict[str, str], published: set[tuple[str, str]]) -> dict[str, str]: + key = (source["source_repo"], source["source_path"]) + path = Path(source["source_path"]) + if key in published: + disposition = "published" + reason = "Published through an explicit publication.json document entry." + elif path.name.lower() == "readme.md": + disposition = "excluded" + reason = "Directory index, not an architecture decision record." + elif path.suffix.lower() != ".md": + disposition = "unsupported-format" + reason = "Inventoried governing source is not Markdown and has no renderer yet." + else: + disposition = "metadata-pending" + reason = ( + "In scope; awaits explicit publication addressing and owner/revision/review metadata." + ) + return source | {"disposition": disposition, "reason": reason} + + +def refresh( + config_path: Path, + inventory_path: Path, + publication_path: Path, + *, + policy_root: Path, + source_root: Path, +) -> dict[str, Any]: + config = _read_json(config_path) + if config.get("schema_version") != 1: + raise ValueError("source inventory config schema_version must be 1") + discovered, _paths = discover(config, policy_root=policy_root, source_root=source_root) + published = _published_sources(publication_path) + existing: dict[tuple[str, str], dict[str, str]] = {} + if inventory_path.exists(): + for source in _read_json(inventory_path).get("sources", []): + existing[(source["source_repo"], source["source_path"])] = source + sources = [] + for source in discovered: + key = (source["source_repo"], source["source_path"]) + sources.append(existing.get(key, _new_entry(source, published))) + inventory = { + "schema_version": 1, + "sources": sources, + } + inventory_path.write_text( + json.dumps(inventory, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + return inventory + + +def _load_revisions( + config: dict[str, Any], paths: dict[str, Path], lock_path: Path | None +) -> tuple[dict[str, str], str]: + if lock_path: + lock = _read_json(lock_path) + if lock.get("schema_version") != 1: + raise ValueError("source lock schema_version must be 1") + repositories = lock.get("repositories", {}) + revisions = { + name: repositories[name]["revision"] for name in config["repositories"] + } + else: + revisions = {name: _revision(paths[name]) for name in config["repositories"]} + for name, revision in revisions.items(): + if len(revision) != 40 or any(char not in "0123456789abcdef" for char in revision): + raise ValueError(f"{name}: invalid locked revision {revision!r}") + canonical = json.dumps(revisions, sort_keys=True, separators=(",", ":")).encode() + digest = hashlib.sha256(canonical).hexdigest() + if lock_path: + locked_digest = lock.get("source_set_digest") + if locked_digest != digest: + raise ValueError( + f"source lock digest mismatch: recorded {locked_digest!r}, computed {digest}" + ) + return revisions, digest + + +def check( + config_path: Path, + inventory_path: Path, + publication_path: Path, + *, + policy_root: Path, + source_root: Path, + lock_path: Path | None = None, +) -> dict[str, Any]: + config = _read_json(config_path) + inventory = _read_json(inventory_path) + if config.get("schema_version") != 1 or inventory.get("schema_version") != 1: + raise ValueError("source inventory config and inventory schema_version must be 1") + discovered, paths = discover(config, policy_root=policy_root, source_root=source_root) + discovered_keys = { + (source["source_repo"], source["source_path"]) for source in discovered + } + entries = inventory.get("sources", []) + inventory_keys: set[tuple[str, str]] = set() + for source in entries: + key = (source.get("source_repo", ""), source.get("source_path", "")) + if key in inventory_keys: + raise ValueError(f"duplicate source inventory entry: {key[0]}/{key[1]}") + inventory_keys.add(key) + if source.get("disposition") not in DISPOSITIONS: + raise ValueError(f"{key[0]}/{key[1]}: invalid disposition") + if not source.get("reason"): + raise ValueError(f"{key[0]}/{key[1]}: disposition reason is required") + missing = sorted(discovered_keys - inventory_keys) + stale = sorted(inventory_keys - discovered_keys) + if missing or stale: + details = [] + if missing: + details.append("unreviewed sources: " + ", ".join(f"{r}/{p}" for r, p in missing)) + if stale: + details.append("inventory entries no longer present: " + ", ".join(f"{r}/{p}" for r, p in stale)) + raise ValueError("; ".join(details) + "; run source_inventory.py refresh and review the diff") + + published = _published_sources(publication_path) + inventory_published = { + (source["source_repo"], source["source_path"]) + for source in entries + if source["disposition"] == "published" + } + if published != inventory_published: + raise ValueError( + "published source inventory must exactly match publication.json: " + f"manifest_only={sorted(published - inventory_published)}, " + f"inventory_only={sorted(inventory_published - published)}" + ) + + revisions, source_set_digest = _load_revisions(config, paths, lock_path) + counts = {disposition: 0 for disposition in sorted(DISPOSITIONS)} + repository_counts = {name: 0 for name in config["repositories"]} + for source in entries: + counts[source["disposition"]] += 1 + repository_counts[source["source_repo"]] += 1 + return { + "schema_version": SCHEMA_VERSION, + "source_set_digest": source_set_digest, + "dispositions": counts, + "repositories": [ + { + "name": name, + "revision": revisions[name], + "source_count": repository_counts[name], + } + for name in sorted(config["repositories"]) + ], + "excluded_scopes": config.get("excluded_scopes", []), + "sources": entries, + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=("check", "refresh")) + parser.add_argument("--config", type=Path, default=Path("source-inventory.config.json")) + parser.add_argument("--inventory", type=Path, default=Path("source-inventory.json")) + parser.add_argument("--publication", type=Path, default=Path("publication.json")) + parser.add_argument("--policy-root", type=Path) + parser.add_argument("--source-root", type=Path) + parser.add_argument("--lock", type=Path) + parser.add_argument("--report", type=Path) + args = parser.parse_args(argv) + config_path = args.config.resolve() + policy_root = (args.policy_root or config_path.parent).resolve() + source_root = (args.source_root or policy_root.parent).resolve() + try: + if args.command == "refresh": + inventory = refresh( + config_path, + args.inventory.resolve(), + args.publication.resolve(), + policy_root=policy_root, + source_root=source_root, + ) + print(f"{args.inventory}: recorded {len(inventory['sources'])} source(s)") + return 0 + report = check( + config_path, + args.inventory.resolve(), + args.publication.resolve(), + policy_root=policy_root, + source_root=source_root, + lock_path=args.lock.resolve() if args.lock else None, + ) + if args.report: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text( + json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + counts = report["dispositions"] + print( + f"source inventory ok: {len(report['sources'])} source(s), " + f"{counts['published']} published, {counts['metadata-pending']} metadata-pending, " + f"source-set {report['source_set_digest']}" + ) + return 0 + except (KeyError, OSError, ValueError, subprocess.CalledProcessError) as exc: + print(f"source inventory failed: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/verify_release.py b/tools/verify_release.py index 2750b8e..2c14315 100644 --- a/tools/verify_release.py +++ b/tools/verify_release.py @@ -46,8 +46,11 @@ def verify(build: Path) -> dict[str, Any]: index = build / "index.html" manifest_path = build / "publication-manifest.json" - if not index.is_file() or not manifest_path.is_file(): - raise ValueError("release requires index.html and publication-manifest.json") + inventory_path = build / "source-inventory.json" + if not index.is_file() or not manifest_path.is_file() or not inventory_path.is_file(): + raise ValueError( + "release requires index.html, publication-manifest.json and source-inventory.json" + ) manifest_bytes = manifest_path.read_bytes() manifest = json.loads(manifest_bytes) @@ -59,6 +62,19 @@ def verify(build: Path) -> dict[str, Any]: if not isinstance(documents, list) or not documents: raise ValueError("publication manifest must contain at least one document") + inventory_bytes = inventory_path.read_bytes() + inventory = json.loads(inventory_bytes) + if inventory.get("schema_version") != "policy-nexus-source-inventory/v1": + raise ValueError("source inventory schema_version is invalid") + source_set_digest = inventory.get("source_set_digest", "") + if not HEX_DIGEST.fullmatch(source_set_digest): + raise ValueError("source inventory requires a valid source_set_digest") + inventoried_published = { + (source.get("source_repo"), source.get("source_path")) + for source in inventory.get("sources", []) + if source.get("disposition") == "published" + } + verified: list[str] = [] for document in documents: document_id = document.get("id", "") @@ -72,6 +88,8 @@ def verify(build: Path) -> dict[str, Any]: "review_due", "canonical_path", "revision_path", + "source_repo", + "source_path", ): if not document.get(field) or document.get(field) == "unknown": raise ValueError(f"{document_id}: release metadata field {field} is required") @@ -84,6 +102,9 @@ def verify(build: Path) -> dict[str, Any]: ) if not HEX_DIGEST.fullmatch(source_digest): raise ValueError(f"{document_id}: invalid source_digest {source_digest!r}") + source_key = (document["source_repo"], document["source_path"]) + if source_key not in inventoried_published: + raise ValueError(f"{document_id}: source is not published in source inventory") canonical = build / _safe_relative(document["canonical_path"]) revision = build / _safe_relative(document["revision_path"]) @@ -105,6 +126,8 @@ def verify(build: Path) -> dict[str, Any]: return { "schema_version": "policy-nexus-release/v1", "publication_manifest_digest": hashlib.sha256(manifest_bytes).hexdigest(), + "source_inventory_digest": hashlib.sha256(inventory_bytes).hexdigest(), + "source_set_digest": source_set_digest, "generated_as_of": manifest["generated_as_of"], "documents": verified, } diff --git a/workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md b/workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md index 4196913..b9d46fa 100644 --- a/workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md +++ b/workplans/POLICY-NEXUS-WP-0001-permanent-publication-surface.md @@ -4,7 +4,7 @@ type: workplan title: "Stand up policy.coulomb.social as the permanent publication surface" domain: infotech repo: policy-nexus -status: active +status: finished owner: the-custodian topic_slug: policy-nexus created: "2026-08-17" @@ -134,7 +134,7 @@ immutability, lifecycle notices and currency. ```task id: POLICY-NEXUS-WP-0001-T03 -status: progress +status: done priority: high state_hub_task_id: "0b76e184-dcd2-4c12-92e1-49d1f2e0a431" ``` @@ -161,10 +161,22 @@ owning repo triggers on merge). Pull is simpler and keeps the direction of dependency clean; push is fresher. Recommend pull with a manual trigger, and record the choice. -2026-08-18: the explicit pull manifest and exact source-revision recording are -implemented, and missing or inconsistent sources fail the build. Tenancy -Posture is the first entry. Enumerating the remaining in-scope canon and ADR -corpus and connecting scheduled/manual checkout refresh remain open. +Completed 2026-08-18. The pull model is now explicit and automated. A reviewed +inventory enumerates 124 committed sources across 21 repositories: one is +published, 113 await source-owner publication metadata/addressing, eight are +non-Markdown formats without a renderer, and two are ADR directory indexes. +The inventory also records the explicit exclusion of `values`, `tpsc`, and +`projects`. New or removed matching sources fail the audit until their +disposition is reviewed. + +The Forgejo workflow checks out every external source at an exact commit on a +daily schedule and manual dispatch, records a deterministic source-set digest, +and fails visibly on fetch, inventory, currency, or release verification +errors. It publishes an immutable source-set candidate only. Promotion remains +the separate paired OCI/publication-digest approval owned by +`rapp-policy-nexus` and `railiance-apps`. Release builds also retain the prior +generated tree so an unchanged semantic revision cannot acquire rewritten +immutable provenance when an upstream repository advances for unrelated work. ### T04 — Deploy to policy.coulomb.social @@ -223,7 +235,8 @@ is quietly out of date is worse than no document. Completed 2026-08-18 for the published corpus. Pages and the index expose review due dates and overdue state; `make currency` exits non-zero for stale or -undeclared review metadata. Expansion follows T03 automatically. +undeclared review metadata. The daily source-pull workflow is the delivery +channel for that failure, and expansion follows the reviewed T03 inventory. ### T06 — withdrawn