From 01668e612a7af8b931337ec7ef5fbd54ad0a2c8b Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 20:29:45 -0700 Subject: [PATCH 1/7] feat(docs): add offline pinned repository source parity --- .github/workflows/documentation-tool.yml | 20 ++++ .gitignore | 2 + AGENTS.md | 11 +++ docs/DOCUMENTATION-PARITY.md | 64 +++++++++++++ scripts/repository_docs.py | 113 +++++++++++++++++++++++ tests/test_repository_docs.py | 113 +++++++++++++++++++++++ 6 files changed, 323 insertions(+) create mode 100644 .github/workflows/documentation-tool.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 docs/DOCUMENTATION-PARITY.md create mode 100644 scripts/repository_docs.py create mode 100644 tests/test_repository_docs.py diff --git a/.github/workflows/documentation-tool.yml b/.github/workflows/documentation-tool.yml new file mode 100644 index 0000000..f9495c8 --- /dev/null +++ b/.github/workflows/documentation-tool.yml @@ -0,0 +1,20 @@ +name: Documentation tool qualification +on: + pull_request: + paths: ['scripts/repository_docs.py', 'tests/test_repository_docs.py', '.github/workflows/documentation-tool.yml'] + push: + branches: [main] + paths: ['scripts/repository_docs.py', 'tests/test_repository_docs.py', '.github/workflows/documentation-tool.yml'] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + - run: python3 -m pip install 'coverage>=7.6,<8' + - run: python3 -m coverage run --branch --source=scripts -m unittest discover -s tests -p test_repository_docs.py -v + - run: python3 -m coverage report --show-missing --fail-under=85 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1e64c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.coverage diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9e9a05f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# Shared CI repository + +Keep consumer builds and releases independent. Preserve existing required checks +and native merge queues. New fleet checks qualify in shadow before enforcement. +Use full commit pins and separate consumer upgrade/rollback PRs (JOV-6555). +Do not change credentials, branch protections, deployments or live controllers. + +For documentation tooling, read docs/DOCUMENTATION-PARITY.md and run its exact +coverage commands. Local consumer README/AGENTS and manifests remain repo-owned. +The older policy/POLICY.md and README distribution claims predate JOV-6555; +this task does not certify their planned workflows as implemented. diff --git a/docs/DOCUMENTATION-PARITY.md b/docs/DOCUMENTATION-PARITY.md new file mode 100644 index 0000000..05eec8d --- /dev/null +++ b/docs/DOCUMENTATION-PARITY.md @@ -0,0 +1,64 @@ +# Repository documentation parity + +Decision (2026-09-24): COMPOSE Git-owned Markdown, existing repository freshness +checks, SHA-pinned file projections and repo-local manifests. Extend JOV-6555's +independent distribution boundary without replacing its shared-workflow work. + +The MIT-licensed `scripts/repository_docs.py` uses only Python 3.9+ stdlib. It +fetches nothing, executes no imports, needs no credentials and changes no service. +Consumers vendor the exact script and license using a full revision and SHA-256. +`repository-docs.json` names local entry points, manifest/workflow sources, output +and shared imports. Package facts are rendered directly; other sources have +fingerprints. A fingerprint is a review trigger, not proof of prose accuracy. + +## Update, verify and roll back + +1. Review the upstream commit and preserve the known-good consumer commit. +2. Update the selected import's full Git revision and SHA-256 in the registry. +3. From the reviewed tooling checkout run: + + ```sh + python3 scripts/repository_docs.py --root /path/to/consumer --write \ + --upstream JovieInc/ci=/path/to/ci \ + --upstream JovieInc/Jovie=/path/to/Jovie + ``` + +4. Review the diff and run the vendored `python3 tools/repository_docs.py`. +5. Open an independently checked consumer PR. Revert that consumer commit to + roll back; sibling repos retain their pins and continue shipping. + +All source hashes are validated before writes. Only listed imports and the +projection are written. README and AGENTS stay locally owned. Without upstream +arguments the check proves offline parity to the declared pin, not freshness of +the pin or remote availability. Update-time Git readback proves source bytes. +After a local manifest/workflow edit, review its documentation and regenerate +with `python3 tools/repository_docs.py --write` in the consumer. + +## Qualification + +Run `python3 -m coverage run --branch --source=scripts -m unittest discover -s tests -p test_repository_docs.py -v` +and `python3 -m coverage report --show-missing --fail-under=85`. +Tests cover source drift, absent entry points, path escape, bad pins, independent +upgrades and rollback. Consumer CI uses `continue-on-error` during shadow +qualification. Do not require it in branch protection. Promotion requires actual +ship samples, correctness/pass rate, p95 duration, throughput/cost and failure +isolation, an accountable owner and a tested rollback. No numeric promotion +threshold is asserted here. Existing gates remain enforced. + +## Adopt-first receipt + +- Preserve Jovie's existing `scripts/lib/doc-freshness.mjs` for local links and + computed markers, and JOV-6555 for pinned CI releases and consumer canaries. +- GitHub [reusable workflows and composite actions](https://docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow-configurations) + are the maintained CI substrate; they do not render offline repo docs. +- [Copier template updates](https://copier.readthedocs.io/en/stable/generating/) + were evaluated but not adopted: whole-project templating is unnecessary for + a few shared files in independently maintained repositories. +- Custom code is limited to offline projection and exact-byte copy verification. + No new service catalog, platform, scheduler, updater bot or paid service. + Only reviewed non-secret policy and tool files cross repository boundaries. + Git and ordinary files preserve portability and independent rollback. + +Ship now: shadow checks in separate consumer PRs. Re-evaluate after two update +cycles with repeated drift or substantial manual work. Then extend the existing +updater under JOV-6555 rather than creating a second bot. diff --git a/scripts/repository_docs.py b/scripts/repository_docs.py new file mode 100644 index 0000000..5b06b18 --- /dev/null +++ b/scripts/repository_docs.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Offline, repo-local documentation projection and pinned source parity (MIT).""" +import argparse +import hashlib +import json +import re +import subprocess +from pathlib import Path + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def local(root, name): + path = Path(name) + if path.is_absolute() or ".." in path.parts or not path.parts: + raise ValueError("unsafe repository path: " + name) + result = root / path + if root.resolve() not in (result.resolve(), *result.resolve().parents): + raise ValueError("path escapes repository: " + name) + return result + + +def render(root, config): + lines = ["# Repository source map", "", "Generated by repository_docs.py; edit the source files and regenerate.", + "This is source evidence, not deployment or operational certification.", "", + "Repository: `" + config["repository"] + "`", "", "## Source-owned entry points", ""] + for entry in config["entrypoints"]: + path = local(root, entry) + if not path.is_file(): + raise ValueError("missing entry point: " + entry) + lines.append("- `" + entry + "`") + lines += ["", "## Manifest and workflow fingerprints", "", + "A changed fingerprint requires review of its related documentation; it does not prove prose is accurate.", ""] + for item in config["sources"]: + name = item["path"] + data = local(root, name).read_bytes() + lines += ["- `" + name + "` — " + item["purpose"] + " (SHA-256 `" + digest(data) + "`)"] + if name.endswith("package.json"): + manifest = json.loads(data) + for field in ("packageManager", "engines", "scripts", "dependencies", "devDependencies"): + if field in manifest: + lines += ["", "### `" + name + "`: " + field, "", "```json", json.dumps(manifest[field], indent=2, sort_keys=True), "```", ""] + lines += ["", "## Pinned shared sources", "", "Updates are independent reviewable consumer changes. Local instructions remain source-owned.", ""] + for item in config["imports"]: + lines.append("- `" + item["destination"] + "` ← `" + item["repository"] + "@" + item["revision"] + ":" + item["path"] + "` (SHA-256 `" + item["sha256"] + "`)") + return ("\n".join(lines) + "\n").encode() + + +def run(root, write=False, upstreams=None): + root = Path(root).resolve() + config = json.loads((root / "repository-docs.json").read_text()) + if config["schemaVersion"] != 1 or not config["sources"] or not config["entrypoints"]: + raise ValueError("invalid documentation registry") + upstreams = upstreams or {} + pending, errors, destinations = [], [], set() + # Validate every import before writing anything; never run upstream code. + for item in config["imports"]: + target = local(root, item["destination"]) + if target in destinations or target == local(root, config["output"]) or target == root / "repository-docs.json": + raise ValueError("duplicate or reserved import destination") + destinations.add(target) + if not re.fullmatch(r"[0-9a-f]{40}", item["revision"]) or not re.fullmatch(r"[0-9a-f]{64}", item["sha256"]): + raise ValueError("imports require immutable revision and SHA-256") + # Validate the Git source path separately from the destination. + local(root, item["path"]) + source = upstreams.get(item["repository"]) + if source: + data = subprocess.run(["git", "-C", str(source), "show", item["revision"] + ":" + item["path"]], check=True, capture_output=True).stdout + if digest(data) != item["sha256"]: + raise ValueError("upstream content does not match pin: " + item["destination"]) + if write: + pending.append((target, data)) + elif not target.is_file() or target.read_bytes() != data: + errors.append("shared source drift: " + item["destination"]) + elif not target.is_file() or digest(target.read_bytes()) != item["sha256"]: + errors.append("pinned copy missing or changed: " + item["destination"]) + output = local(root, config["output"]) + if output == root / "repository-docs.json" or output in {local(root, i["path"]) for i in config["sources"]}: + raise ValueError("projection cannot overwrite a source") + projection = render(root, config) + if errors: + return errors + if write: + for target, data in pending + [(output, projection)]: + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + elif not output.is_file() or output.read_bytes() != projection: + errors.append("source projection stale: " + config["output"]) + return errors + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=".") + parser.add_argument("--write", action="store_true") + parser.add_argument("--upstream", action="append", default=[], metavar="OWNER/REPO=CHECKOUT") + args = parser.parse_args() + try: + errors = run(args.root, args.write, dict(item.split("=", 1) for item in args.upstream)) + except (ValueError, KeyError, TypeError, OSError, subprocess.CalledProcessError) as exc: + parser.exit(1, "repository-docs: " + str(exc) + "\n") + for error in errors: + print("repository-docs: " + error) + if errors: + return 1 + print("repository-docs: source parity verified (no runtime claim)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_repository_docs.py b/tests/test_repository_docs.py new file mode 100644 index 0000000..becbb13 --- /dev/null +++ b/tests/test_repository_docs.py @@ -0,0 +1,113 @@ +import importlib.util +import json +import subprocess +import tempfile +import unittest +from pathlib import Path + +spec = importlib.util.spec_from_file_location("repository_docs", Path(__file__).resolve().parents[1] / "scripts/repository_docs.py") +docs = importlib.util.module_from_spec(spec) +spec.loader.exec_module(docs) + + +class DocumentationParity(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + (self.root / "README.md").write_text("Local owner instructions\n") + (self.root / "package.json").write_text(json.dumps({"packageManager": "pnpm@9.15.4", "engines": {"node": ">=24"}, "scripts": {"test": "vitest run"}, "dependencies": {"eve": "0.47.7"}})) + self.config = {"schemaVersion": 1, "repository": "example/app", "entrypoints": ["README.md"], "sources": [{"path": "package.json", "purpose": "Actual runtime"}], "imports": [], "output": "docs/SOURCES.md"} + self.save() + + def save(self): + (self.root / "repository-docs.json").write_text(json.dumps(self.config)) + + def test_source_change_detected_and_regeneration_preserves_local_instructions(self): + self.assertTrue(docs.run(self.root)) + self.assertEqual(docs.run(self.root, True), []) + self.assertEqual(docs.run(self.root), []) + before = (self.root / "README.md").read_bytes() + (self.root / "package.json").write_text('{"dependencies":{"eve":"1.0.0"}}') + self.assertTrue(docs.run(self.root)) + docs.run(self.root, True) + self.assertIn('"eve": "1.0.0"', (self.root / "docs/SOURCES.md").read_text()) + self.assertEqual((self.root / "README.md").read_bytes(), before) + + def test_missing_entrypoint_and_invalid_registry_fail(self): + (self.root / "README.md").unlink() + with self.assertRaises(ValueError): docs.run(self.root, True) + self.config["schemaVersion"] = 2 + self.save() + with self.assertRaises(ValueError): docs.run(self.root) + + def test_traversal_and_symlink_escape_rejected(self): + for name in ("../escape", "/tmp/escape", ""): + with self.assertRaises(ValueError): docs.local(self.root, name) + (self.root / "escape").symlink_to(self.root.parent, target_is_directory=True) + with self.assertRaises(ValueError): docs.local(self.root, "escape/out") + + def test_pinned_update_drift_and_independent_rollback(self): + upstream = self.root / "upstream" + upstream.mkdir() + def git(*args): + return subprocess.run(["git", "-C", str(upstream), *args], check=True, capture_output=True).stdout.decode().strip() + git("init") + git("config", "user.email", "test@example.invalid") + git("config", "user.name", "Test") + (upstream / "policy.md").write_text("first") + git("add", ".") + git("commit", "-m", "first") + first = git("rev-parse", "HEAD") + item = {"repository": "example/canon", "revision": first, "path": "policy.md", "destination": "company/policy.md", "sha256": docs.digest(b"first")} + self.config["imports"] = [item] + self.save() + self.assertTrue(docs.run(self.root)) + mapping = {"example/canon": upstream} + self.assertTrue(docs.run(self.root, upstreams=mapping)) + docs.run(self.root, True, mapping) + self.assertEqual(docs.run(self.root, upstreams=mapping), []) + (upstream / "policy.md").write_text("second") + git("commit", "-am", "second") + # An upstream change leaves the pinned consumer unchanged. + self.assertEqual(docs.run(self.root, upstreams=mapping), []) + item["revision"] = git("rev-parse", "HEAD") + self.save() + with self.assertRaises(ValueError): docs.run(self.root, True, mapping) + self.assertEqual((self.root / "company/policy.md").read_text(), "first") + item["sha256"] = docs.digest(b"second") + self.save() + docs.run(self.root, True, mapping) + self.assertEqual((self.root / "company/policy.md").read_text(), "second") + item.update(revision=first, sha256=docs.digest(b"first")) + self.save() + docs.run(self.root, True, mapping) + self.assertEqual((self.root / "company/policy.md").read_text(), "first") + item["revision"] = "main" + self.save() + with self.assertRaises(ValueError): docs.run(self.root) + + def test_duplicate_destinations_and_source_overwrite_rejected(self): + item = {"repository": "x/y", "revision": "a" * 40, "path": "p", "destination": "copy", "sha256": docs.digest(b"ok")} + (self.root / "copy").write_bytes(b"ok") + self.config["imports"] = [item, item] + self.save() + with self.assertRaises(ValueError): docs.run(self.root, True) + self.config["imports"] = [] + self.config["output"] = "package.json" + self.save() + with self.assertRaises(ValueError): docs.run(self.root, True) + + def test_cli_success_drift_and_malformed_config(self): + script = str(Path(docs.__file__)) + def cli(*args): + return subprocess.run(["python3", script, "--root", str(self.root), *args], capture_output=True) + self.assertEqual(cli().returncode, 1) + self.assertEqual(cli("--write").returncode, 0) + self.assertEqual(cli().returncode, 0) + (self.root / "repository-docs.json").write_text("{") + self.assertEqual(cli().returncode, 1) + + +if __name__ == "__main__": + unittest.main() From dce01db11cb65fc7c35f13d91081f7075baa047f Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 20:31:28 -0700 Subject: [PATCH 2/7] docs: reconcile shared CI claims with implemented source and JOV-6555 --- README.md | 38 ++++++++++++++++++++------------- policy/POLICY.md | 55 ++++++++++++++++++++++++------------------------ 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 4057f6f..aedf3f9 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,30 @@ # JovieInc/ci -Centralized, opinionated CI for all Jovie repositories — one set of reusable GitHub Actions workflows, shared composite actions, and a single strict policy for gates, distribution, and agent mediation. +Shared CI source for independently released company repositories. Consumers own +their build commands, tests, required checks, deployment and rollback. -**Principle:** update one workflow here → every repo benefits. Consumer repos keep only ~15-line caller stubs pinned to `@v1`. +## Implemented on this branch -## Layout -- `.github/workflows/` — reusable workflows (`on: workflow_call`) -- `actions/` — shared composite actions -- `policy/` — POLICY.md (branch rules, gate tiers, agent PR contract), risk-rules.yml, gitleaks.toml +- `.github/workflows/dependabot-auto-merge.yml`: callable dependency mediation. +- `actions/setup-node-pnpm`, `actions/setup-ios-build`, `actions/asc-api-key`: + composite actions; inspect their own inputs and prerequisites before use. +- `scripts/repository_docs.py`: offline manifest projection and pinned shared-file + parity. See [documentation parity](docs/DOCUMENTATION-PARITY.md) for tests, + scope, update procedure, shadow qualification and rollback. +- `policy/`: source policy, risk rules and secret-scanner configuration. -## Gate tiers -1. **Pre-PR (local/agent harness)** — typecheck, lint, affected unit tests. Never burn a runner on what the agent can run itself. -2. **PR (required)** — `rw-gate-fast` + risk classifier only. Deep gates run only when `rw-gate-risk` says risk ≥ standard. -3. **Merge queue** (`merge_group`) — build + E2E smoke on the queued batch. -4. **Post-merge** — deploy + canary + error gate, **fix-forward**: failures auto-file P1 + capped autofix; never retro-block the queue. -5. **Scheduled** — nightly full suite, security, CodeQL. +This list describes source, not consumer adoption or effective branch controls. +Previously described `rw-gate-fast`, `rw-gate-risk`, `rw-gate-deep` and +`rw-merge-queue` workflows are not present on this revision. JOV-2970/JOV-2975 +track shared CI; existing PRs retain ownership of their separate implementation. -## Versioning -Consumers pin `@v1` (moving major tag). Breaking changes bump major. Changes here run actionlint + smoke against consumer repos before tagging. +## Distribution -Tracked in Linear: JOV-2970 (epic), JOV-2975 (Phase 1). +JOV-6555 supersedes the old moving-`@v1` proposal: consumers use reviewed full +commit SHAs and separate upgrade PRs. A central commit does not automatically +change consumers. Qualify a candidate with relevant tests and a consumer canary, +then roll out independently; retain known-good pins for rollback. + +Existing consumer checks and release guarantees remain in force. New fleet +requirements qualify in shadow before enforcement. No sibling repo's healthy +build depends on every other consumer being green. diff --git a/policy/POLICY.md b/policy/POLICY.md index 0e1cd44..e1d3733 100644 --- a/policy/POLICY.md +++ b/policy/POLICY.md @@ -1,32 +1,31 @@ -# Jovie CI & Agent Delivery Policy (v1) +# Shared CI source policy -The single strict policy inherited by every Jovie repository. Mastered here; the -"Agent PR contract" section is synced verbatim into each repo's AGENTS.md. +Updated 2026-09-24. The old moving-major, universal gate-tier and automatic +AGENTS-sync claims were proposals, not implementation receipts. They are +superseded by the approved JOV-6555 independent pinned rollout contract. -## 1. Gate tiers -1. **Pre-PR (local/agent harness)** — typecheck, lint, affected unit tests via the repo's validate hook. Agents MUST pass these before opening a PR. Never burn a runner on what the agent can run itself. -2. **PR (required checks)** — `rw-gate-fast` (~5–8 min) + `rw-gate-risk` only. Deep gates run conditionally when risk ≥ standard. Everything else is advisory. -3. **Merge queue** — all merges to main go through the queue (`merge_group`); `rw-merge-queue` runs build + E2E smoke on the batch. Batch 5, timeout 30 min. -4. **Post-merge** — deploy + canary health + error gate with auto-rollback. **Fix-forward:** post-merge failures auto-file a P1 and dispatch capped autofix (max 3 attempts → `needs-human`); they never retro-block the queue. Revert is reserved for canary/error-gate failures. -5. **Scheduled** — nightly full suite, mutation tests, security, CodeQL. +- Full immutable commit-SHA pins; separate reviewed consumer upgrades and rollback. +- Preserve existing required-check names and real repo-specific test selectors. +- Validate the candidate against applicable representative fixtures and one actual + consumer canary. Unchanged pinned consumers remain independently usable. +- Each repo owns its runtime/toolchain, credentials, builds, releases and gates. + Do not copy another repo's Node/Python/Xcode requirements as company policy. +- Read-only untrusted PR validation is isolated from protected deployment/signing. + Use explicit least-privilege permissions and named secrets. +- Preserve native merge queue and existing gates. Applicable failed, missing or + unknown evidence cannot be presented as success. +- New delivery requirements qualify in nonblocking shadow before promotion, with + actual ship samples, correctness/pass rate, p95 duration, throughput/cost, + failure isolation, owner and tested rollback. No fixed new threshold is claimed. +- Publish a coherent draft early, then qualify the exact final head with required + tests, coverage and independent source review before promotion. +- Legacy `needs-human`, `human-review-required` and `no-auto` labels are not blanket + implementation/landing holds. Actual consequential-action authority still applies. -## 2. Risk filtering -`policy/risk-rules.yml` maps changed paths → low / standard / high. High-risk paths (auth, payments, migrations, token-vault, `.github/**`, public API) get the aggressive gates and a mandatory review label. Flaky or slow checks NEVER become required PR checks. +Shared source distribution must preserve local README/AGENTS instructions. The +[documentation parity procedure](../docs/DOCUMENTATION-PARITY.md) copies only +explicitly pinned files and generates a local source map. It neither rewrites a +consumer's whole AGENTS file nor establishes a fleet-wide enforced gate. -## 3. Branch & distribution rules -- Trunk-based: target `main` by default. -- `integration/` branches ONLY when: a feature spans >3 coordinated PRs, a churny shared area, or a mechanical agent sweep (codemods). The dispatching orchestrator owns the branch; it merges to main as one train via the queue. -- Concurrency: `cancel-in-progress: true` only for PR-scoped groups keyed by PR number. NEVER on main. NEVER on deploy/release workflows. - -## 4. Agent PR contract (synced into AGENTS.md) -1. Branch naming: `codex/*`, `claude/*`, `agent//*`, `*/jov--*`. -2. Target branch: `main` unless the orchestrator directs `integration/`. -3. Run the local validate hook pre-PR; never open a PR red on typecheck/lint/affected tests. -4. PR size ≤ 10 files / 400 lines unless labeled `sweep`. -5. Conventional commits; open as draft; the mediation pipeline flips to ready. -6. Never merge directly. Auto-merge only via the mediation pipeline. `needs-human` is a hard stop. -7. No force-push after entering the merge queue; rebase via the landing sweeper. -8. Labels: `automerge`, `needs-human`, `testing`, `sweep`. CI autofix is capped at 3 attempts per origin PR. - -## 5. Versioning -Consumers pin `@v1`. Breaking changes bump the major tag. Central-repo changes run actionlint + consumer smoke before tagging. +This policy does not mutate repository protections, rename required checks, +enable workflow consumers or authorize live runtime changes. From 8141deb174ae936db61a32d6d70c9ca243432db5 Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 20:32:14 -0700 Subject: [PATCH 3/7] docs: align consumer command with canonical script path --- docs/DOCUMENTATION-PARITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DOCUMENTATION-PARITY.md b/docs/DOCUMENTATION-PARITY.md index 05eec8d..cd4ff7f 100644 --- a/docs/DOCUMENTATION-PARITY.md +++ b/docs/DOCUMENTATION-PARITY.md @@ -23,7 +23,7 @@ fingerprints. A fingerprint is a review trigger, not proof of prose accuracy. --upstream JovieInc/Jovie=/path/to/Jovie ``` -4. Review the diff and run the vendored `python3 tools/repository_docs.py`. +4. Review the diff and run the vendored `python3 scripts/repository_docs.py`. 5. Open an independently checked consumer PR. Revert that consumer commit to roll back; sibling repos retain their pins and continue shipping. @@ -32,7 +32,7 @@ projection are written. README and AGENTS stay locally owned. Without upstream arguments the check proves offline parity to the declared pin, not freshness of the pin or remote availability. Update-time Git readback proves source bytes. After a local manifest/workflow edit, review its documentation and regenerate -with `python3 tools/repository_docs.py --write` in the consumer. +with `python3 scripts/repository_docs.py --write` in the consumer. ## Qualification From 192faf12e4bfdd024ee3badfe83f60c7d65bb65b Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 20:37:43 -0700 Subject: [PATCH 4/7] test(docs): cover all parity branches and self-qualify shared CI --- .github/workflows/documentation-tool.yml | 2 +- .github/workflows/repository-docs-shadow.yml | 22 ++++++++++ AGENTS.md | 6 +++ README.md | 6 +++ docs/DOCUMENTATION-PARITY.md | 2 +- docs/DOCUMENTATION.md | 42 ++++++++++++++++++++ docs/REPOSITORY_SOURCES.md | 26 ++++++++++++ docs/company/REPOSITORY_SOURCE_CONTRACT.md | 38 ++++++++++++++++++ repository-docs.json | 33 +++++++++++++++ tests/test_repository_docs.py | 19 +++++++++ 10 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/repository-docs-shadow.yml create mode 100644 docs/DOCUMENTATION.md create mode 100644 docs/REPOSITORY_SOURCES.md create mode 100644 docs/company/REPOSITORY_SOURCE_CONTRACT.md create mode 100644 repository-docs.json diff --git a/.github/workflows/documentation-tool.yml b/.github/workflows/documentation-tool.yml index f9495c8..e25ea5f 100644 --- a/.github/workflows/documentation-tool.yml +++ b/.github/workflows/documentation-tool.yml @@ -17,4 +17,4 @@ jobs: python-version: '3.12' - run: python3 -m pip install 'coverage>=7.6,<8' - run: python3 -m coverage run --branch --source=scripts -m unittest discover -s tests -p test_repository_docs.py -v - - run: python3 -m coverage report --show-missing --fail-under=85 + - run: python3 -m coverage report --show-missing --fail-under=100 diff --git a/.github/workflows/repository-docs-shadow.yml b/.github/workflows/repository-docs-shadow.yml new file mode 100644 index 0000000..4ac1e82 --- /dev/null +++ b/.github/workflows/repository-docs-shadow.yml @@ -0,0 +1,22 @@ +name: Repository documentation shadow +on: + pull_request: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +jobs: + parity: + continue-on-error: true + runs-on: ubuntu-24.04 + timeout-minutes: 3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + - run: python3 -m pip install 'coverage>=7.6,<8' + - run: python3 -m coverage run --branch --include=scripts/repository_docs.py -m unittest discover -s tests -p test_repository_docs.py -v + - run: python3 -m coverage report --show-missing --fail-under=100 + - run: python3 scripts/repository_docs.py diff --git a/AGENTS.md b/AGENTS.md index 9e9a05f..91656f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,3 +9,9 @@ For documentation tooling, read docs/DOCUMENTATION-PARITY.md and run its exact coverage commands. Local consumer README/AGENTS and manifests remain repo-owned. The older policy/POLICY.md and README distribution claims predate JOV-6555; this task does not certify their planned workflows as implemented. + +## Repository documentation + +See [source parity](docs/DOCUMENTATION.md) and the generated +[source map](docs/REPOSITORY_SOURCES.md). Shared policy is pinned; local +manifest, build, test and release instructions remain owned by this repository. diff --git a/README.md b/README.md index aedf3f9..96a93e1 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,9 @@ then roll out independently; retain known-good pins for rollback. Existing consumer checks and release guarantees remain in force. New fleet requirements qualify in shadow before enforcement. No sibling repo's healthy build depends on every other consumer being green. + +## Repository documentation + +See [source parity](docs/DOCUMENTATION.md) and the generated +[source map](docs/REPOSITORY_SOURCES.md). Shared policy is pinned; local +manifest, build, test and release instructions remain owned by this repository. diff --git a/docs/DOCUMENTATION-PARITY.md b/docs/DOCUMENTATION-PARITY.md index cd4ff7f..8eed3f8 100644 --- a/docs/DOCUMENTATION-PARITY.md +++ b/docs/DOCUMENTATION-PARITY.md @@ -37,7 +37,7 @@ with `python3 scripts/repository_docs.py --write` in the consumer. ## Qualification Run `python3 -m coverage run --branch --source=scripts -m unittest discover -s tests -p test_repository_docs.py -v` -and `python3 -m coverage report --show-missing --fail-under=85`. +and `python3 -m coverage report --show-missing --fail-under=100`. Tests cover source drift, absent entry points, path escape, bad pins, independent upgrades and rollback. Consumer CI uses `continue-on-error` during shadow qualification. Do not require it in branch protection. Promotion requires actual diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md new file mode 100644 index 0000000..71163db --- /dev/null +++ b/docs/DOCUMENTATION.md @@ -0,0 +1,42 @@ +# Documentation ownership and parity + +The repository owns README/AGENTS, local manifests, runtime declarations and +release workflows. [The portable company contract](company/REPOSITORY_SOURCE_CONTRACT.md) +is shared source policy; it does not change the local stack or release gates. + +`repository-docs.json` selects source-owned entry points, local manifests and +workflows, generated output and immutable shared imports. The generated +[repository source map](REPOSITORY_SOURCES.md) renders package facts directly and +fingerprints other sources. Review related prose whenever a fingerprint changes. +This is source evidence, not proof of deployment, current credentials or runtime. + +From the repository root (Python 3.9+; no network or secrets): + +```sh +python3 scripts/repository_docs.py +python3 scripts/repository_docs.py --write +``` + +Use `--write` after reviewing local source changes. Shared-file edits belong in +the canonical repository named by each import, followed by a reviewed full Git +revision and SHA-256 update. From the reviewed JovieInc/ci tooling checkout: + +```sh +python3 scripts/repository_docs.py --root /path/to/this-repo --write \ + --upstream JovieInc/ci=/path/to/ci \ + --upstream JovieInc/Jovie=/path/to/Jovie +``` + +The default check verifies local copies against declared hashes offline. It does +not claim that upstream pins are latest. Update-time Git readback verifies the +exact upstream bytes. Only listed imports and generated output are written; +README/AGENTS remain local. Review changes in a separate PR per repository. +Revert the consumer commit to restore its previous pin; siblings remain usable. + +CI runs the same deterministic parity tests and coverage as shared tooling, then +checks the local projection in shadow. No new required gate or branch protection +is added. Existing required checks remain enforced. JOV-6607 tracks qualification; +JOV-6555 owns shared-CI release distribution and updater integration. Promote only +with representative ship samples, correctness/pass rate, p95, throughput/cost, +failure isolation, an accountable owner and tested rollback. No approved numeric +promotion threshold is implied. No scheduler, new bot or paid service is added. diff --git a/docs/REPOSITORY_SOURCES.md b/docs/REPOSITORY_SOURCES.md new file mode 100644 index 0000000..ea5190d --- /dev/null +++ b/docs/REPOSITORY_SOURCES.md @@ -0,0 +1,26 @@ +# Repository source map + +Generated by repository_docs.py; edit the source files and regenerate. +This is source evidence, not deployment or operational certification. + +Repository: `JovieInc/ci` + +## Source-owned entry points + +- `README.md` +- `AGENTS.md` +- `docs/DOCUMENTATION.md` + +## Manifest and workflow fingerprints + +A changed fingerprint requires review of its related documentation; it does not prove prose is accurate. + +- `.github/workflows/dependabot-auto-merge.yml` — Local manifest or workflow; review related instructions when changed (SHA-256 `432fca1d9fe7beb3dc78598971c321d5a7983d93d9fad2eb2f8ab6b5758139eb`) +- `.github/workflows/documentation-tool.yml` — Local manifest or workflow; review related instructions when changed (SHA-256 `e4212c1929eddfabcd48a6d0dfdebd00a7359640f77d447c764906d471f8350b`) +- `.github/workflows/repository-docs-shadow.yml` — Nonblocking parity qualification (SHA-256 `2b9ccbfcdef4df6d31e17ec1dbad5fffe82c01263af0c08b8b0481b31e7af48e`) + +## Pinned shared sources + +Updates are independent reviewable consumer changes. Local instructions remain source-owned. + +- `docs/company/REPOSITORY_SOURCE_CONTRACT.md` ← `JovieInc/Jovie@b9c1bb7ca2ba0dd0bbb79cbeff265e21ef8791b1:docs/company/REPOSITORY_SOURCE_CONTRACT.md` (SHA-256 `f6bb8cdfdc317ed4ac3b99f8d2fc18a9647124c4fb807b1eef274892447b184f`) diff --git a/docs/company/REPOSITORY_SOURCE_CONTRACT.md b/docs/company/REPOSITORY_SOURCE_CONTRACT.md new file mode 100644 index 0000000..cfe3b47 --- /dev/null +++ b/docs/company/REPOSITORY_SOURCE_CONTRACT.md @@ -0,0 +1,38 @@ +# Company repository source contract + +Owner: company engineering / Tim White. Updated: 2026-09-24. +Scope: shared source/documentation boundaries, not product-specific configuration. + +Company canon is owned in Jovie's `canon/OPERATING_SYSTEM.md`, +`canon/ENGINEERING.md` and `canon/FLEET.md`. This portable projection summarizes +those boundaries for other repositories. Update this source first and distribute +reviewed immutable revisions; never overwrite a consumer's local instructions. + +- Each repository owns its README, AGENTS, manifests, workflows, dependencies, + tests, build, release, rollback and runtime declarations. Source code and real + manifests outrank stale prose. A common standard does not imply one stack. +- Company repositories ship independently under their current owners and gates. + A cross-repo hold requires fresh evidence of a specific shared bottleneck, + named owner, expiry and release condition. Concurrent CI alone is not evidence. +- Shared policy/tool copies record canonical repository, path, full Git revision + and content hash. Updates and rollback are independent consumer PRs. A pinned + copy is reproducible; it is not automatically the newest revision. +- Generated documentation is derived from local source. Review related prose + when source changes. Hash parity cannot establish semantic correctness. +- New or changed fleet delivery gates qualify in nonblocking shadow first. + Existing required checks remain enforced. Promotion needs representative actual + ships, correctness, pass rate, p95 duration, throughput/cost, failure isolation, + accountable ownership and tested rollback. Do not invent approved thresholds. +- Preserve meaningful tests and current coverage for executable changes. Clearly + distinguish source, local tests, hosted CI, review, native queue/merge, + deployment, observed runtime, persistence and customer outcome. +- Preserve active writers and the existing deployment/commissioning controller. + Documentation does not grant credentials, host changes, service activation, + external messaging, release-control changes or production mutation authority. +- Keep secrets, sessions, private memory, operational logs and host state out of + shared projections. Copy only reviewed non-secret source appropriate to the + destination's visibility. Retired instructions are history, never activation. + +Distribution mechanism: JovieInc/ci `docs/DOCUMENTATION-PARITY.md`, tracked by +JOV-6607 alongside JOV-6555. Existing repo-local freshness checks remain in place. +This source contract does not certify that a consumer implements any mechanism. diff --git a/repository-docs.json b/repository-docs.json new file mode 100644 index 0000000..5142709 --- /dev/null +++ b/repository-docs.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "repository": "JovieInc/ci", + "entrypoints": [ + "README.md", + "AGENTS.md", + "docs/DOCUMENTATION.md" + ], + "sources": [ + { + "path": ".github/workflows/dependabot-auto-merge.yml", + "purpose": "Local manifest or workflow; review related instructions when changed" + }, + { + "path": ".github/workflows/documentation-tool.yml", + "purpose": "Local manifest or workflow; review related instructions when changed" + }, + { + "path": ".github/workflows/repository-docs-shadow.yml", + "purpose": "Nonblocking parity qualification" + } + ], + "imports": [ + { + "repository": "JovieInc/Jovie", + "revision": "b9c1bb7ca2ba0dd0bbb79cbeff265e21ef8791b1", + "path": "docs/company/REPOSITORY_SOURCE_CONTRACT.md", + "destination": "docs/company/REPOSITORY_SOURCE_CONTRACT.md", + "sha256": "f6bb8cdfdc317ed4ac3b99f8d2fc18a9647124c4fb807b1eef274892447b184f" + } + ], + "output": "docs/REPOSITORY_SOURCES.md" +} diff --git a/tests/test_repository_docs.py b/tests/test_repository_docs.py index becbb13..2ba8033 100644 --- a/tests/test_repository_docs.py +++ b/tests/test_repository_docs.py @@ -2,6 +2,8 @@ import json import subprocess import tempfile +import runpy +from unittest.mock import patch import unittest from pathlib import Path @@ -98,6 +100,23 @@ def test_duplicate_destinations_and_source_overwrite_rejected(self): self.save() with self.assertRaises(ValueError): docs.run(self.root, True) + def test_cli_entrypoint_and_non_json_source_are_measured(self): + self.config["sources"].append({"path": "README.md", "purpose": "Local guidance"}) + self.save() + argv = [str(Path(docs.__file__)), "--root", str(self.root)] + with patch("sys.argv", argv): + self.assertEqual(docs.main(), 1) + with patch("sys.argv", argv + ["--write"]): + self.assertEqual(docs.main(), 0) + with patch("sys.argv", argv): + with self.assertRaises(SystemExit) as result: + runpy.run_path(docs.__file__, run_name="__main__") + self.assertEqual(result.exception.code, 0) + with patch("sys.argv", argv + ["--upstream", "malformed"]): + with self.assertRaises(SystemExit) as result: + docs.main() + self.assertEqual(result.exception.code, 1) + def test_cli_success_drift_and_malformed_config(self): script = str(Path(docs.__file__)) def cli(*args): From f4dd7982b527a94bab3581c7507a4094d6b47bdf Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 20:59:46 -0700 Subject: [PATCH 5/7] fix(docs): protect entrypoints and project pending imports --- scripts/repository_docs.py | 19 +++++++----- tests/test_repository_docs.py | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/scripts/repository_docs.py b/scripts/repository_docs.py index 5b06b18..da81cab 100644 --- a/scripts/repository_docs.py +++ b/scripts/repository_docs.py @@ -19,10 +19,11 @@ def local(root, name): result = root / path if root.resolve() not in (result.resolve(), *result.resolve().parents): raise ValueError("path escapes repository: " + name) - return result + return result.resolve() -def render(root, config): +def render(root, config, pending=None): + pending = pending or {} lines = ["# Repository source map", "", "Generated by repository_docs.py; edit the source files and regenerate.", "This is source evidence, not deployment or operational certification.", "", "Repository: `" + config["repository"] + "`", "", "## Source-owned entry points", ""] @@ -35,7 +36,8 @@ def render(root, config): "A changed fingerprint requires review of its related documentation; it does not prove prose is accurate.", ""] for item in config["sources"]: name = item["path"] - data = local(root, name).read_bytes() + source = local(root, name) + data = pending[source] if source in pending else source.read_bytes() lines += ["- `" + name + "` — " + item["purpose"] + " (SHA-256 `" + digest(data) + "`)"] if name.endswith("package.json"): manifest = json.loads(data) @@ -55,10 +57,14 @@ def run(root, write=False, upstreams=None): raise ValueError("invalid documentation registry") upstreams = upstreams or {} pending, errors, destinations = [], [], set() + entrypoints = {local(root, entry) for entry in config["entrypoints"]} + output = local(root, config["output"]) + if output in entrypoints or output == root / "repository-docs.json" or output in {local(root, i["path"]) for i in config["sources"]}: + raise ValueError("projection cannot overwrite a source or entry point") # Validate every import before writing anything; never run upstream code. for item in config["imports"]: target = local(root, item["destination"]) - if target in destinations or target == local(root, config["output"]) or target == root / "repository-docs.json": + if target in destinations or target in entrypoints or target == output or target == root / "repository-docs.json": raise ValueError("duplicate or reserved import destination") destinations.add(target) if not re.fullmatch(r"[0-9a-f]{40}", item["revision"]) or not re.fullmatch(r"[0-9a-f]{64}", item["sha256"]): @@ -76,10 +82,7 @@ def run(root, write=False, upstreams=None): errors.append("shared source drift: " + item["destination"]) elif not target.is_file() or digest(target.read_bytes()) != item["sha256"]: errors.append("pinned copy missing or changed: " + item["destination"]) - output = local(root, config["output"]) - if output == root / "repository-docs.json" or output in {local(root, i["path"]) for i in config["sources"]}: - raise ValueError("projection cannot overwrite a source") - projection = render(root, config) + projection = render(root, config, dict(pending)) if errors: return errors if write: diff --git a/tests/test_repository_docs.py b/tests/test_repository_docs.py index 2ba8033..0d8896e 100644 --- a/tests/test_repository_docs.py +++ b/tests/test_repository_docs.py @@ -100,6 +100,62 @@ def test_duplicate_destinations_and_source_overwrite_rejected(self): self.save() with self.assertRaises(ValueError): docs.run(self.root, True) + def test_entrypoints_and_aliases_cannot_be_overwritten(self): + original = (self.root / "README.md").read_bytes() + (self.root / "readme-alias").symlink_to("README.md") + for destination in ("README.md", "readme-alias"): + with self.subTest(destination=destination): + self.config["imports"] = [] + self.config["output"] = destination + self.save() + with self.assertRaises(ValueError): docs.run(self.root, True) + self.config["output"] = "docs/SOURCES.md" + self.config["imports"] = [{"repository": "x/y", "revision": "a" * 40, "path": "p", "destination": destination, "sha256": docs.digest(original)}] + self.save() + with self.assertRaises(ValueError): docs.run(self.root, True) + self.assertEqual((self.root / "README.md").read_bytes(), original) + self.assertFalse((self.root / "docs/SOURCES.md").exists()) + + def test_imported_sources_use_pending_bytes_before_any_mutation(self): + upstream = self.root / "upstream" + upstream.mkdir() + def git(*args): + return subprocess.run(["git", "-C", str(upstream), *args], check=True, capture_output=True).stdout.decode().strip() + git("init") + git("config", "user.email", "test@example.invalid") + git("config", "user.name", "Test") + old = (self.root / "package.json").read_bytes() + (upstream / "package.json").write_bytes(old) + git("add", ".") + git("commit", "-m", "old manifest") + first = git("rev-parse", "HEAD") + item = {"repository": "x/y", "revision": first, "path": "package.json", "destination": "package.json", "sha256": docs.digest(old)} + self.config["imports"] = [item] + self.save() + mapping = {"x/y": upstream} + docs.run(self.root, True, mapping) + original_projection = (self.root / "docs/SOURCES.md").read_bytes() + for content in (b'{"dependencies":{"eve":"2.0.0"}}', b'{'): + (upstream / "package.json").write_bytes(content) + git("commit", "-am", "update manifest") + item.update(revision=git("rev-parse", "HEAD"), sha256=docs.digest(content)) + self.save() + before = (self.root / "package.json").read_bytes() + projection = (self.root / "docs/SOURCES.md").read_bytes() + if content == b'{': + with self.assertRaises(ValueError): docs.run(self.root, True, mapping) + self.assertEqual((self.root / "package.json").read_bytes(), before) + self.assertEqual((self.root / "docs/SOURCES.md").read_bytes(), projection) + else: + self.assertEqual(docs.run(self.root, True, mapping), []) + self.assertEqual(docs.run(self.root), []) + self.assertIn('"eve": "2.0.0"', (self.root / "docs/SOURCES.md").read_text()) + item.update(revision=first, sha256=docs.digest(old)) + self.save() + self.assertEqual(docs.run(self.root, True, mapping), []) + self.assertEqual(docs.run(self.root), []) + self.assertEqual((self.root / "docs/SOURCES.md").read_bytes(), original_projection) + def test_cli_entrypoint_and_non_json_source_are_measured(self): self.config["sources"].append({"path": "README.md", "purpose": "Local guidance"}) self.save() From 285fb2e0e27e9ba8a63566e8ca71b3807479950b Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 21:01:48 -0700 Subject: [PATCH 6/7] fix(docs): protect resolved registry from projection writes --- scripts/repository_docs.py | 7 ++++--- tests/test_repository_docs.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/repository_docs.py b/scripts/repository_docs.py index da81cab..f390cd7 100644 --- a/scripts/repository_docs.py +++ b/scripts/repository_docs.py @@ -52,19 +52,20 @@ def render(root, config, pending=None): def run(root, write=False, upstreams=None): root = Path(root).resolve() - config = json.loads((root / "repository-docs.json").read_text()) + registry = local(root, "repository-docs.json") + config = json.loads(registry.read_text()) if config["schemaVersion"] != 1 or not config["sources"] or not config["entrypoints"]: raise ValueError("invalid documentation registry") upstreams = upstreams or {} pending, errors, destinations = [], [], set() entrypoints = {local(root, entry) for entry in config["entrypoints"]} output = local(root, config["output"]) - if output in entrypoints or output == root / "repository-docs.json" or output in {local(root, i["path"]) for i in config["sources"]}: + if output in entrypoints or output == registry or output in {local(root, i["path"]) for i in config["sources"]}: raise ValueError("projection cannot overwrite a source or entry point") # Validate every import before writing anything; never run upstream code. for item in config["imports"]: target = local(root, item["destination"]) - if target in destinations or target in entrypoints or target == output or target == root / "repository-docs.json": + if target in destinations or target in entrypoints or target == output or target == registry: raise ValueError("duplicate or reserved import destination") destinations.add(target) if not re.fullmatch(r"[0-9a-f]{40}", item["revision"]) or not re.fullmatch(r"[0-9a-f]{64}", item["sha256"]): diff --git a/tests/test_repository_docs.py b/tests/test_repository_docs.py index 0d8896e..ba8fb61 100644 --- a/tests/test_repository_docs.py +++ b/tests/test_repository_docs.py @@ -116,6 +116,23 @@ def test_entrypoints_and_aliases_cannot_be_overwritten(self): self.assertEqual((self.root / "README.md").read_bytes(), original) self.assertFalse((self.root / "docs/SOURCES.md").exists()) + def test_symlinked_registry_cannot_be_overwritten(self): + registry = self.root / "repository-docs.json" + registry.rename(self.root / "config.json") + registry.symlink_to("config.json") + (self.root / "config-alias").symlink_to("config.json") + for destination in ("repository-docs.json", "config.json", "config-alias"): + for writer in ("output", "import"): + with self.subTest(destination=destination, writer=writer): + self.config["output"] = destination if writer == "output" else "docs/SOURCES.md" + self.config["imports"] = [] if writer == "output" else [{"repository": "x/y", "revision": "a" * 40, "path": "p", "destination": destination, "sha256": docs.digest(b"replacement")}] + self.save() + before = registry.read_bytes() + with self.assertRaises(ValueError): docs.run(self.root, True) + self.assertEqual(registry.read_bytes(), before) + self.assertTrue(registry.is_symlink()) + self.assertFalse((self.root / "docs/SOURCES.md").exists()) + def test_imported_sources_use_pending_bytes_before_any_mutation(self): upstream = self.root / "upstream" upstream.mkdir() From abde4fd58ff2ff9b3d34d2a2a0ace8349ba97804 Mon Sep 17 00:00:00 2001 From: Tim White Date: Thu, 24 Sep 2026 21:33:36 -0700 Subject: [PATCH 7/7] docs: pin company policy to merged canonical revision --- docs/REPOSITORY_SOURCES.md | 2 +- repository-docs.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/REPOSITORY_SOURCES.md b/docs/REPOSITORY_SOURCES.md index ea5190d..34561a0 100644 --- a/docs/REPOSITORY_SOURCES.md +++ b/docs/REPOSITORY_SOURCES.md @@ -23,4 +23,4 @@ A changed fingerprint requires review of its related documentation; it does not Updates are independent reviewable consumer changes. Local instructions remain source-owned. -- `docs/company/REPOSITORY_SOURCE_CONTRACT.md` ← `JovieInc/Jovie@b9c1bb7ca2ba0dd0bbb79cbeff265e21ef8791b1:docs/company/REPOSITORY_SOURCE_CONTRACT.md` (SHA-256 `f6bb8cdfdc317ed4ac3b99f8d2fc18a9647124c4fb807b1eef274892447b184f`) +- `docs/company/REPOSITORY_SOURCE_CONTRACT.md` ← `JovieInc/Jovie@b193bc6a360b20df6933d45b4abefe0736c09493:docs/company/REPOSITORY_SOURCE_CONTRACT.md` (SHA-256 `f6bb8cdfdc317ed4ac3b99f8d2fc18a9647124c4fb807b1eef274892447b184f`) diff --git a/repository-docs.json b/repository-docs.json index 5142709..1c1f58b 100644 --- a/repository-docs.json +++ b/repository-docs.json @@ -23,7 +23,7 @@ "imports": [ { "repository": "JovieInc/Jovie", - "revision": "b9c1bb7ca2ba0dd0bbb79cbeff265e21ef8791b1", + "revision": "b193bc6a360b20df6933d45b4abefe0736c09493", "path": "docs/company/REPOSITORY_SOURCE_CONTRACT.md", "destination": "docs/company/REPOSITORY_SOURCE_CONTRACT.md", "sha256": "f6bb8cdfdc317ed4ac3b99f8d2fc18a9647124c4fb807b1eef274892447b184f"