From a7aa454089890382de34ae5f9a6a85d357d0ada6 Mon Sep 17 00:00:00 2001 From: Bartek Kus <7887446+bartekus@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:53:44 -0600 Subject: [PATCH 1/2] feat(000): found the spec corpus, the harness, and the governed gate hqgit is specified before it is built: this lands the whole design and the machinery that keeps code faithful to it. - 68 specs: 000 bootstrap with the frozen invariants, 001 the agentic harness, 002 the platform thesis, 003 the rahi chassis boundary, and 64 ordinary specs across eight waves, approved and pending - spec-spine 0.11.0 governance: spec-spine.toml, the constitution, the corpus contract, the spec template, and the committed registry and index shards under .derived/ - the gate: make spine and make ci, guarded for the pre-Cargo tree, and govern.yml re-running it on every pull request - the harness: the AGENTS.md session and backlog protocol, CLAUDE.md, fifteen skills, six agents, six rules, and hooks that refuse a push to main and a pull request on a red coupling gate - docs/design: the architecture analysis and the rendered spec DAG --- .claude/agents/architect.md | 125 +++++ .claude/agents/explorer.md | 94 ++++ .claude/agents/implementer.md | 108 +++++ .claude/agents/ledger-guardian.md | 100 ++++ .claude/agents/reviewer.md | 120 +++++ .claude/agents/trust-reviewer.md | 95 ++++ .claude/rules/adversarial-prompt-refusal.md | 13 + .claude/rules/build-commands.md | 49 ++ .claude/rules/governed-artifact-reads.md | 11 + .claude/rules/ledger-invariants.md | 42 ++ .claude/rules/orchestrator-rules.md | 8 + .claude/rules/trust-invariants.md | 44 ++ .claude/settings.json | 94 ++++ .claude/skills/build/SKILL.md | 154 +++++++ .claude/skills/cleanup/SKILL.md | 124 +++++ .claude/skills/code-review/SKILL.md | 144 ++++++ .claude/skills/commit/SKILL.md | 112 +++++ .claude/skills/implement-plan/SKILL.md | 102 +++++ .claude/skills/init/SKILL.md | 38 ++ .claude/skills/next/SKILL.md | 105 +++++ .claude/skills/refactor-claude-md/SKILL.md | 85 ++++ .claude/skills/research/SKILL.md | 86 ++++ .claude/skills/setup/SKILL.md | 119 +++++ .claude/skills/shepherd/SKILL.md | 129 ++++++ .claude/skills/ship/SKILL.md | 116 +++++ .claude/skills/spec/SKILL.md | 124 +++++ .claude/skills/validate-and-fix/SKILL.md | 108 +++++ .claude/skills/verify/SKILL.md | 76 +++ .../by-spec/000-hqgit-bootstrap.json | 9 + .../by-spec/001-agentic-harness.json | 270 +++++++++++ .../by-spec/002-platform-thesis.json | 49 ++ .../by-spec/003-chassis-alignment.json | 12 + .../by-spec/010-workspace-and-core-types.json | 201 ++++++++ .../by-spec/011-canonical-encoding.json | 146 ++++++ .../by-spec/012-hash-stability-gate.json | 117 +++++ .../by-spec/013-object-store.json | 132 ++++++ .../by-spec/014-content-defined-chunking.json | 132 ++++++ .../by-spec/015-verified-streaming.json | 146 ++++++ .../by-spec/016-remote-object-backend.json | 105 +++++ .../by-spec/017-ledger-entry-dag.json | 146 ++++++ .../018-deterministic-total-order.json | 75 +++ .../by-spec/019-facts-and-derived-state.json | 131 ++++++ .../020-commitments-and-tombstones.json | 174 +++++++ .../by-spec/021-local-repository.json | 118 +++++ .../by-spec/023-domain-fact-vocabulary.json | 117 +++++ .../by-spec/024-change-and-revision.json | 89 ++++ .../by-spec/025-semantic-anchors.json | 160 +++++++ .../by-spec/026-review-threads.json | 75 +++ .../by-spec/027-attestation-primitive.json | 117 +++++ .../by-spec/028-issues-and-derived-state.json | 75 +++ .../by-spec/031-git-object-bridge.json | 160 +++++++ .../by-spec/032-cli-skeleton.json | 160 +++++++ .../by-spec/033-cli-offline-review.json | 120 +++++ .../by-spec/034-cli-attest-and-verify.json | 89 ++++ .../by-spec/040-github-mirror-import.json | 191 ++++++++ .../by-spec/041-github-mirror-export.json | 89 ++++ .../by-spec/042-mirror-sync-command.json | 118 +++++ .../by-spec/050-stacked-changes.json | 103 +++++ .../by-spec/051-semantic-deltas.json | 162 +++++++ .../by-spec/052-semantic-conflicts.json | 104 +++++ .../060-identity-and-key-rotation.json | 117 +++++ .../by-spec/061-oidc-login.json | 118 +++++ .../by-spec/062-transparency-log.json | 117 +++++ .../by-spec/063-keyless-signing.json | 119 +++++ .../by-spec/064-attestation-verification.json | 119 +++++ .../by-spec/065-policy-engine.json | 147 ++++++ .../by-spec/066-policy-sdk.json | 187 ++++++++ .../067-policy-evaluation-attestation.json | 146 ++++++ .../by-spec/068-policy-in-repo.json | 118 +++++ .../by-spec/070-reapi-types.json | 188 ++++++++ .../by-spec/071-action-cache.json | 104 +++++ .../by-spec/072-execution-service.json | 216 +++++++++ .../by-spec/073-sandbox-executor.json | 247 ++++++++++ .../by-spec/074-execution-provenance.json | 90 ++++ .../by-spec/075-build-graph.json | 146 ++++++ .../by-spec/076-merge-queue.json | 91 ++++ .../by-spec/080-projection-framework.json | 203 ++++++++ .../by-spec/081-change-and-review-views.json | 148 ++++++ .../by-spec/082-search-index.json | 104 +++++ .../by-spec/083-code-graph.json | 161 +++++++ .../by-spec/084-ecosystem-graph.json | 133 ++++++ .../by-spec/085-attention-feeds.json | 103 +++++ .../by-spec/090-server-skeleton.json | 174 +++++++ .../by-spec/091-per-repo-control-plane.json | 132 ++++++ .../by-spec/092-git-endpoint.json | 148 ++++++ .../by-spec/093-connect-api.json | 417 +++++++++++++++++ .../by-spec/094-quarantine-and-promotion.json | 132 ++++++ .../by-spec/095-web-review-ui.json | 287 ++++++++++++ .../by-spec/100-agent-principals.json | 147 ++++++ .../by-spec/101-delegation-chain.json | 103 +++++ .../102-agent-sandbox-and-provenance.json | 161 +++++++ .../103-evidence-carrying-changes.json | 203 ++++++++ .../by-spec/104-ownership-and-sla.json | 91 ++++ .../by-spec/110-set-reconciliation.json | 131 ++++++ .../by-spec/111-quic-transport.json | 119 +++++ .../by-spec/112-federation.json | 203 ++++++++ .../by-spec/000-hqgit-bootstrap.json | 52 +++ .../by-spec/001-agentic-harness.json | 98 ++++ .../by-spec/002-platform-thesis.json | 175 +++++++ .../by-spec/003-chassis-alignment.json | 54 +++ .../by-spec/010-workspace-and-core-types.json | 90 ++++ .../by-spec/011-canonical-encoding.json | 89 ++++ .../by-spec/012-hash-stability-gate.json | 66 +++ .../by-spec/013-object-store.json | 77 ++++ .../by-spec/014-content-defined-chunking.json | 89 ++++ .../by-spec/015-verified-streaming.json | 101 ++++ .../by-spec/016-remote-object-backend.json | 78 ++++ .../by-spec/017-ledger-entry-dag.json | 85 ++++ .../018-deterministic-total-order.json | 60 +++ .../by-spec/019-facts-and-derived-state.json | 80 ++++ .../020-commitments-and-tombstones.json | 113 +++++ .../by-spec/021-local-repository.json | 81 ++++ .../by-spec/023-domain-fact-vocabulary.json | 72 +++ .../by-spec/024-change-and-revision.json | 68 +++ .../by-spec/025-semantic-anchors.json | 93 ++++ .../by-spec/026-review-threads.json | 64 +++ .../by-spec/027-attestation-primitive.json | 80 ++++ .../by-spec/028-issues-and-derived-state.json | 64 +++ .../by-spec/031-git-object-bridge.json | 85 ++++ .../by-spec/032-cli-skeleton.json | 85 ++++ .../by-spec/033-cli-offline-review.json | 83 ++++ .../by-spec/034-cli-attest-and-verify.json | 68 +++ .../by-spec/040-github-mirror-import.json | 100 ++++ .../by-spec/041-github-mirror-export.json | 68 +++ .../by-spec/042-mirror-sync-command.json | 85 ++++ .../by-spec/050-stacked-changes.json | 76 +++ .../by-spec/051-semantic-deltas.json | 99 ++++ .../by-spec/052-semantic-conflicts.json | 77 ++++ .../060-identity-and-key-rotation.json | 66 +++ .../spec-registry/by-spec/061-oidc-login.json | 81 ++++ .../by-spec/062-transparency-log.json | 72 +++ .../by-spec/063-keyless-signing.json | 82 ++++ .../by-spec/064-attestation-verification.json | 78 ++++ .../by-spec/065-policy-engine.json | 82 ++++ .../spec-registry/by-spec/066-policy-sdk.json | 92 ++++ .../067-policy-evaluation-attestation.json | 97 ++++ .../by-spec/068-policy-in-repo.json | 85 ++++ .../by-spec/070-reapi-types.json | 93 ++++ .../by-spec/071-action-cache.json | 77 ++++ .../by-spec/072-execution-service.json | 113 +++++ .../by-spec/073-sandbox-executor.json | 112 +++++ .../by-spec/074-execution-provenance.json | 69 +++ .../by-spec/075-build-graph.json | 89 ++++ .../by-spec/076-merge-queue.json | 70 +++ .../by-spec/080-projection-framework.json | 110 +++++ .../by-spec/081-change-and-review-views.json | 87 ++++ .../by-spec/082-search-index.json | 81 ++++ .../spec-registry/by-spec/083-code-graph.json | 102 +++++ .../by-spec/084-ecosystem-graph.json | 86 ++++ .../by-spec/085-attention-feeds.json | 76 +++ .../by-spec/090-server-skeleton.json | 89 ++++ .../by-spec/091-per-repo-control-plane.json | 85 ++++ .../by-spec/092-git-endpoint.json | 91 ++++ .../by-spec/093-connect-api.json | 190 ++++++++ .../by-spec/094-quarantine-and-promotion.json | 89 ++++ .../by-spec/095-web-review-ui.json | 134 ++++++ .../by-spec/100-agent-principals.json | 86 ++++ .../by-spec/101-delegation-chain.json | 72 +++ .../102-agent-sandbox-and-provenance.json | 102 +++++ .../103-evidence-carrying-changes.json | 122 +++++ .../by-spec/104-ownership-and-sla.json | 70 +++ .../by-spec/110-set-reconciliation.json | 76 +++ .../by-spec/111-quic-transport.json | 82 ++++ .../spec-registry/by-spec/112-federation.json | 126 +++++ .gitattributes | 22 + .github/dependabot.yml | 17 + .github/workflows/govern.yml | 99 ++++ .gitignore | 40 +- .mcp.json | 3 + AGENTS.md | 185 ++++++++ CLAUDE.md | 107 +++++ Makefile | 86 ++++ README.md | 90 ++++ docs/design/00-architecture.md | 261 +++++++++++ docs/design/01-build-order.md | 336 ++++++++++++++ scripts/spec-dag.sh | 83 ++++ scripts/verify-spec.sh | 73 +++ spec-spine.toml | 92 ++++ specs/000-hqgit-bootstrap/spec.md | 179 ++++++++ specs/001-agentic-harness/spec.md | 180 ++++++++ specs/002-platform-thesis/spec.md | 433 ++++++++++++++++++ specs/003-chassis-alignment/spec.md | 186 ++++++++ specs/010-workspace-and-core-types/spec.md | 180 ++++++++ specs/011-canonical-encoding/spec.md | 190 ++++++++ specs/012-hash-stability-gate/spec.md | 156 +++++++ specs/013-object-store/spec.md | 161 +++++++ specs/014-content-defined-chunking/spec.md | 151 ++++++ specs/015-verified-streaming/spec.md | 148 ++++++ specs/016-remote-object-backend/spec.md | 145 ++++++ specs/017-ledger-entry-dag/spec.md | 165 +++++++ specs/018-deterministic-total-order/spec.md | 149 ++++++ specs/019-facts-and-derived-state/spec.md | 165 +++++++ specs/020-commitments-and-tombstones/spec.md | 184 ++++++++ specs/021-local-repository/spec.md | 181 ++++++++ specs/023-domain-fact-vocabulary/spec.md | 176 +++++++ specs/024-change-and-revision/spec.md | 145 ++++++ specs/025-semantic-anchors/spec.md | 154 +++++++ specs/026-review-threads/spec.md | 137 ++++++ specs/027-attestation-primitive/spec.md | 169 +++++++ specs/028-issues-and-derived-state/spec.md | 131 ++++++ specs/031-git-object-bridge/spec.md | 152 ++++++ specs/032-cli-skeleton/spec.md | 159 +++++++ specs/033-cli-offline-review/spec.md | 165 +++++++ specs/034-cli-attest-and-verify/spec.md | 144 ++++++ specs/040-github-mirror-import/spec.md | 178 +++++++ specs/041-github-mirror-export/spec.md | 145 ++++++ specs/042-mirror-sync-command/spec.md | 137 ++++++ specs/050-stacked-changes/spec.md | 201 ++++++++ specs/051-semantic-deltas/spec.md | 223 +++++++++ specs/052-semantic-conflicts/spec.md | 187 ++++++++ specs/060-identity-and-key-rotation/spec.md | 170 +++++++ specs/061-oidc-login/spec.md | 167 +++++++ specs/062-transparency-log/spec.md | 160 +++++++ specs/063-keyless-signing/spec.md | 214 +++++++++ specs/064-attestation-verification/spec.md | 192 ++++++++ specs/065-policy-engine/spec.md | 175 +++++++ specs/066-policy-sdk/spec.md | 166 +++++++ .../067-policy-evaluation-attestation/spec.md | 171 +++++++ specs/068-policy-in-repo/spec.md | 159 +++++++ specs/070-reapi-types/spec.md | 181 ++++++++ specs/071-action-cache/spec.md | 157 +++++++ specs/072-execution-service/spec.md | 186 ++++++++ specs/073-sandbox-executor/spec.md | 190 ++++++++ specs/074-execution-provenance/spec.md | 155 +++++++ specs/075-build-graph/spec.md | 220 +++++++++ specs/076-merge-queue/spec.md | 198 ++++++++ specs/080-projection-framework/spec.md | 194 ++++++++ specs/081-change-and-review-views/spec.md | 182 ++++++++ specs/082-search-index/spec.md | 151 ++++++ specs/083-code-graph/spec.md | 209 +++++++++ specs/084-ecosystem-graph/spec.md | 205 +++++++++ specs/085-attention-feeds/spec.md | 185 ++++++++ specs/090-server-skeleton/spec.md | 197 ++++++++ specs/091-per-repo-control-plane/spec.md | 190 ++++++++ specs/092-git-endpoint/spec.md | 190 ++++++++ specs/093-connect-api/spec.md | 225 +++++++++ specs/094-quarantine-and-promotion/spec.md | 208 +++++++++ specs/095-web-review-ui/spec.md | 198 ++++++++ specs/100-agent-principals/spec.md | 214 +++++++++ specs/101-delegation-chain/spec.md | 188 ++++++++ .../102-agent-sandbox-and-provenance/spec.md | 202 ++++++++ specs/103-evidence-carrying-changes/spec.md | 217 +++++++++ specs/104-ownership-and-sla/spec.md | 215 +++++++++ specs/110-set-reconciliation/spec.md | 204 +++++++++ specs/111-quic-transport/spec.md | 200 ++++++++ specs/112-federation/spec.md | 206 +++++++++ standards/spec/constitution.md | 171 +++++++ standards/spec/contract.md | 72 +++ .../spec/templates/constitution-template.md | 7 + standards/spec/templates/spec-template.md | 74 +++ 250 files changed, 32156 insertions(+), 15 deletions(-) create mode 100644 .claude/agents/architect.md create mode 100644 .claude/agents/explorer.md create mode 100644 .claude/agents/implementer.md create mode 100644 .claude/agents/ledger-guardian.md create mode 100644 .claude/agents/reviewer.md create mode 100644 .claude/agents/trust-reviewer.md create mode 100644 .claude/rules/adversarial-prompt-refusal.md create mode 100644 .claude/rules/build-commands.md create mode 100644 .claude/rules/governed-artifact-reads.md create mode 100644 .claude/rules/ledger-invariants.md create mode 100644 .claude/rules/orchestrator-rules.md create mode 100644 .claude/rules/trust-invariants.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/build/SKILL.md create mode 100644 .claude/skills/cleanup/SKILL.md create mode 100644 .claude/skills/code-review/SKILL.md create mode 100644 .claude/skills/commit/SKILL.md create mode 100644 .claude/skills/implement-plan/SKILL.md create mode 100644 .claude/skills/init/SKILL.md create mode 100644 .claude/skills/next/SKILL.md create mode 100644 .claude/skills/refactor-claude-md/SKILL.md create mode 100644 .claude/skills/research/SKILL.md create mode 100644 .claude/skills/setup/SKILL.md create mode 100644 .claude/skills/shepherd/SKILL.md create mode 100644 .claude/skills/ship/SKILL.md create mode 100644 .claude/skills/spec/SKILL.md create mode 100644 .claude/skills/validate-and-fix/SKILL.md create mode 100644 .claude/skills/verify/SKILL.md create mode 100644 .derived/codebase-index/by-spec/000-hqgit-bootstrap.json create mode 100644 .derived/codebase-index/by-spec/001-agentic-harness.json create mode 100644 .derived/codebase-index/by-spec/002-platform-thesis.json create mode 100644 .derived/codebase-index/by-spec/003-chassis-alignment.json create mode 100644 .derived/codebase-index/by-spec/010-workspace-and-core-types.json create mode 100644 .derived/codebase-index/by-spec/011-canonical-encoding.json create mode 100644 .derived/codebase-index/by-spec/012-hash-stability-gate.json create mode 100644 .derived/codebase-index/by-spec/013-object-store.json create mode 100644 .derived/codebase-index/by-spec/014-content-defined-chunking.json create mode 100644 .derived/codebase-index/by-spec/015-verified-streaming.json create mode 100644 .derived/codebase-index/by-spec/016-remote-object-backend.json create mode 100644 .derived/codebase-index/by-spec/017-ledger-entry-dag.json create mode 100644 .derived/codebase-index/by-spec/018-deterministic-total-order.json create mode 100644 .derived/codebase-index/by-spec/019-facts-and-derived-state.json create mode 100644 .derived/codebase-index/by-spec/020-commitments-and-tombstones.json create mode 100644 .derived/codebase-index/by-spec/021-local-repository.json create mode 100644 .derived/codebase-index/by-spec/023-domain-fact-vocabulary.json create mode 100644 .derived/codebase-index/by-spec/024-change-and-revision.json create mode 100644 .derived/codebase-index/by-spec/025-semantic-anchors.json create mode 100644 .derived/codebase-index/by-spec/026-review-threads.json create mode 100644 .derived/codebase-index/by-spec/027-attestation-primitive.json create mode 100644 .derived/codebase-index/by-spec/028-issues-and-derived-state.json create mode 100644 .derived/codebase-index/by-spec/031-git-object-bridge.json create mode 100644 .derived/codebase-index/by-spec/032-cli-skeleton.json create mode 100644 .derived/codebase-index/by-spec/033-cli-offline-review.json create mode 100644 .derived/codebase-index/by-spec/034-cli-attest-and-verify.json create mode 100644 .derived/codebase-index/by-spec/040-github-mirror-import.json create mode 100644 .derived/codebase-index/by-spec/041-github-mirror-export.json create mode 100644 .derived/codebase-index/by-spec/042-mirror-sync-command.json create mode 100644 .derived/codebase-index/by-spec/050-stacked-changes.json create mode 100644 .derived/codebase-index/by-spec/051-semantic-deltas.json create mode 100644 .derived/codebase-index/by-spec/052-semantic-conflicts.json create mode 100644 .derived/codebase-index/by-spec/060-identity-and-key-rotation.json create mode 100644 .derived/codebase-index/by-spec/061-oidc-login.json create mode 100644 .derived/codebase-index/by-spec/062-transparency-log.json create mode 100644 .derived/codebase-index/by-spec/063-keyless-signing.json create mode 100644 .derived/codebase-index/by-spec/064-attestation-verification.json create mode 100644 .derived/codebase-index/by-spec/065-policy-engine.json create mode 100644 .derived/codebase-index/by-spec/066-policy-sdk.json create mode 100644 .derived/codebase-index/by-spec/067-policy-evaluation-attestation.json create mode 100644 .derived/codebase-index/by-spec/068-policy-in-repo.json create mode 100644 .derived/codebase-index/by-spec/070-reapi-types.json create mode 100644 .derived/codebase-index/by-spec/071-action-cache.json create mode 100644 .derived/codebase-index/by-spec/072-execution-service.json create mode 100644 .derived/codebase-index/by-spec/073-sandbox-executor.json create mode 100644 .derived/codebase-index/by-spec/074-execution-provenance.json create mode 100644 .derived/codebase-index/by-spec/075-build-graph.json create mode 100644 .derived/codebase-index/by-spec/076-merge-queue.json create mode 100644 .derived/codebase-index/by-spec/080-projection-framework.json create mode 100644 .derived/codebase-index/by-spec/081-change-and-review-views.json create mode 100644 .derived/codebase-index/by-spec/082-search-index.json create mode 100644 .derived/codebase-index/by-spec/083-code-graph.json create mode 100644 .derived/codebase-index/by-spec/084-ecosystem-graph.json create mode 100644 .derived/codebase-index/by-spec/085-attention-feeds.json create mode 100644 .derived/codebase-index/by-spec/090-server-skeleton.json create mode 100644 .derived/codebase-index/by-spec/091-per-repo-control-plane.json create mode 100644 .derived/codebase-index/by-spec/092-git-endpoint.json create mode 100644 .derived/codebase-index/by-spec/093-connect-api.json create mode 100644 .derived/codebase-index/by-spec/094-quarantine-and-promotion.json create mode 100644 .derived/codebase-index/by-spec/095-web-review-ui.json create mode 100644 .derived/codebase-index/by-spec/100-agent-principals.json create mode 100644 .derived/codebase-index/by-spec/101-delegation-chain.json create mode 100644 .derived/codebase-index/by-spec/102-agent-sandbox-and-provenance.json create mode 100644 .derived/codebase-index/by-spec/103-evidence-carrying-changes.json create mode 100644 .derived/codebase-index/by-spec/104-ownership-and-sla.json create mode 100644 .derived/codebase-index/by-spec/110-set-reconciliation.json create mode 100644 .derived/codebase-index/by-spec/111-quic-transport.json create mode 100644 .derived/codebase-index/by-spec/112-federation.json create mode 100644 .derived/spec-registry/by-spec/000-hqgit-bootstrap.json create mode 100644 .derived/spec-registry/by-spec/001-agentic-harness.json create mode 100644 .derived/spec-registry/by-spec/002-platform-thesis.json create mode 100644 .derived/spec-registry/by-spec/003-chassis-alignment.json create mode 100644 .derived/spec-registry/by-spec/010-workspace-and-core-types.json create mode 100644 .derived/spec-registry/by-spec/011-canonical-encoding.json create mode 100644 .derived/spec-registry/by-spec/012-hash-stability-gate.json create mode 100644 .derived/spec-registry/by-spec/013-object-store.json create mode 100644 .derived/spec-registry/by-spec/014-content-defined-chunking.json create mode 100644 .derived/spec-registry/by-spec/015-verified-streaming.json create mode 100644 .derived/spec-registry/by-spec/016-remote-object-backend.json create mode 100644 .derived/spec-registry/by-spec/017-ledger-entry-dag.json create mode 100644 .derived/spec-registry/by-spec/018-deterministic-total-order.json create mode 100644 .derived/spec-registry/by-spec/019-facts-and-derived-state.json create mode 100644 .derived/spec-registry/by-spec/020-commitments-and-tombstones.json create mode 100644 .derived/spec-registry/by-spec/021-local-repository.json create mode 100644 .derived/spec-registry/by-spec/023-domain-fact-vocabulary.json create mode 100644 .derived/spec-registry/by-spec/024-change-and-revision.json create mode 100644 .derived/spec-registry/by-spec/025-semantic-anchors.json create mode 100644 .derived/spec-registry/by-spec/026-review-threads.json create mode 100644 .derived/spec-registry/by-spec/027-attestation-primitive.json create mode 100644 .derived/spec-registry/by-spec/028-issues-and-derived-state.json create mode 100644 .derived/spec-registry/by-spec/031-git-object-bridge.json create mode 100644 .derived/spec-registry/by-spec/032-cli-skeleton.json create mode 100644 .derived/spec-registry/by-spec/033-cli-offline-review.json create mode 100644 .derived/spec-registry/by-spec/034-cli-attest-and-verify.json create mode 100644 .derived/spec-registry/by-spec/040-github-mirror-import.json create mode 100644 .derived/spec-registry/by-spec/041-github-mirror-export.json create mode 100644 .derived/spec-registry/by-spec/042-mirror-sync-command.json create mode 100644 .derived/spec-registry/by-spec/050-stacked-changes.json create mode 100644 .derived/spec-registry/by-spec/051-semantic-deltas.json create mode 100644 .derived/spec-registry/by-spec/052-semantic-conflicts.json create mode 100644 .derived/spec-registry/by-spec/060-identity-and-key-rotation.json create mode 100644 .derived/spec-registry/by-spec/061-oidc-login.json create mode 100644 .derived/spec-registry/by-spec/062-transparency-log.json create mode 100644 .derived/spec-registry/by-spec/063-keyless-signing.json create mode 100644 .derived/spec-registry/by-spec/064-attestation-verification.json create mode 100644 .derived/spec-registry/by-spec/065-policy-engine.json create mode 100644 .derived/spec-registry/by-spec/066-policy-sdk.json create mode 100644 .derived/spec-registry/by-spec/067-policy-evaluation-attestation.json create mode 100644 .derived/spec-registry/by-spec/068-policy-in-repo.json create mode 100644 .derived/spec-registry/by-spec/070-reapi-types.json create mode 100644 .derived/spec-registry/by-spec/071-action-cache.json create mode 100644 .derived/spec-registry/by-spec/072-execution-service.json create mode 100644 .derived/spec-registry/by-spec/073-sandbox-executor.json create mode 100644 .derived/spec-registry/by-spec/074-execution-provenance.json create mode 100644 .derived/spec-registry/by-spec/075-build-graph.json create mode 100644 .derived/spec-registry/by-spec/076-merge-queue.json create mode 100644 .derived/spec-registry/by-spec/080-projection-framework.json create mode 100644 .derived/spec-registry/by-spec/081-change-and-review-views.json create mode 100644 .derived/spec-registry/by-spec/082-search-index.json create mode 100644 .derived/spec-registry/by-spec/083-code-graph.json create mode 100644 .derived/spec-registry/by-spec/084-ecosystem-graph.json create mode 100644 .derived/spec-registry/by-spec/085-attention-feeds.json create mode 100644 .derived/spec-registry/by-spec/090-server-skeleton.json create mode 100644 .derived/spec-registry/by-spec/091-per-repo-control-plane.json create mode 100644 .derived/spec-registry/by-spec/092-git-endpoint.json create mode 100644 .derived/spec-registry/by-spec/093-connect-api.json create mode 100644 .derived/spec-registry/by-spec/094-quarantine-and-promotion.json create mode 100644 .derived/spec-registry/by-spec/095-web-review-ui.json create mode 100644 .derived/spec-registry/by-spec/100-agent-principals.json create mode 100644 .derived/spec-registry/by-spec/101-delegation-chain.json create mode 100644 .derived/spec-registry/by-spec/102-agent-sandbox-and-provenance.json create mode 100644 .derived/spec-registry/by-spec/103-evidence-carrying-changes.json create mode 100644 .derived/spec-registry/by-spec/104-ownership-and-sla.json create mode 100644 .derived/spec-registry/by-spec/110-set-reconciliation.json create mode 100644 .derived/spec-registry/by-spec/111-quic-transport.json create mode 100644 .derived/spec-registry/by-spec/112-federation.json create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/govern.yml create mode 100644 .mcp.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/design/00-architecture.md create mode 100644 docs/design/01-build-order.md create mode 100755 scripts/spec-dag.sh create mode 100755 scripts/verify-spec.sh create mode 100644 spec-spine.toml create mode 100644 specs/000-hqgit-bootstrap/spec.md create mode 100644 specs/001-agentic-harness/spec.md create mode 100644 specs/002-platform-thesis/spec.md create mode 100644 specs/003-chassis-alignment/spec.md create mode 100644 specs/010-workspace-and-core-types/spec.md create mode 100644 specs/011-canonical-encoding/spec.md create mode 100644 specs/012-hash-stability-gate/spec.md create mode 100644 specs/013-object-store/spec.md create mode 100644 specs/014-content-defined-chunking/spec.md create mode 100644 specs/015-verified-streaming/spec.md create mode 100644 specs/016-remote-object-backend/spec.md create mode 100644 specs/017-ledger-entry-dag/spec.md create mode 100644 specs/018-deterministic-total-order/spec.md create mode 100644 specs/019-facts-and-derived-state/spec.md create mode 100644 specs/020-commitments-and-tombstones/spec.md create mode 100644 specs/021-local-repository/spec.md create mode 100644 specs/023-domain-fact-vocabulary/spec.md create mode 100644 specs/024-change-and-revision/spec.md create mode 100644 specs/025-semantic-anchors/spec.md create mode 100644 specs/026-review-threads/spec.md create mode 100644 specs/027-attestation-primitive/spec.md create mode 100644 specs/028-issues-and-derived-state/spec.md create mode 100644 specs/031-git-object-bridge/spec.md create mode 100644 specs/032-cli-skeleton/spec.md create mode 100644 specs/033-cli-offline-review/spec.md create mode 100644 specs/034-cli-attest-and-verify/spec.md create mode 100644 specs/040-github-mirror-import/spec.md create mode 100644 specs/041-github-mirror-export/spec.md create mode 100644 specs/042-mirror-sync-command/spec.md create mode 100644 specs/050-stacked-changes/spec.md create mode 100644 specs/051-semantic-deltas/spec.md create mode 100644 specs/052-semantic-conflicts/spec.md create mode 100644 specs/060-identity-and-key-rotation/spec.md create mode 100644 specs/061-oidc-login/spec.md create mode 100644 specs/062-transparency-log/spec.md create mode 100644 specs/063-keyless-signing/spec.md create mode 100644 specs/064-attestation-verification/spec.md create mode 100644 specs/065-policy-engine/spec.md create mode 100644 specs/066-policy-sdk/spec.md create mode 100644 specs/067-policy-evaluation-attestation/spec.md create mode 100644 specs/068-policy-in-repo/spec.md create mode 100644 specs/070-reapi-types/spec.md create mode 100644 specs/071-action-cache/spec.md create mode 100644 specs/072-execution-service/spec.md create mode 100644 specs/073-sandbox-executor/spec.md create mode 100644 specs/074-execution-provenance/spec.md create mode 100644 specs/075-build-graph/spec.md create mode 100644 specs/076-merge-queue/spec.md create mode 100644 specs/080-projection-framework/spec.md create mode 100644 specs/081-change-and-review-views/spec.md create mode 100644 specs/082-search-index/spec.md create mode 100644 specs/083-code-graph/spec.md create mode 100644 specs/084-ecosystem-graph/spec.md create mode 100644 specs/085-attention-feeds/spec.md create mode 100644 specs/090-server-skeleton/spec.md create mode 100644 specs/091-per-repo-control-plane/spec.md create mode 100644 specs/092-git-endpoint/spec.md create mode 100644 specs/093-connect-api/spec.md create mode 100644 specs/094-quarantine-and-promotion/spec.md create mode 100644 specs/095-web-review-ui/spec.md create mode 100644 specs/100-agent-principals/spec.md create mode 100644 specs/101-delegation-chain/spec.md create mode 100644 specs/102-agent-sandbox-and-provenance/spec.md create mode 100644 specs/103-evidence-carrying-changes/spec.md create mode 100644 specs/104-ownership-and-sla/spec.md create mode 100644 specs/110-set-reconciliation/spec.md create mode 100644 specs/111-quic-transport/spec.md create mode 100644 specs/112-federation/spec.md create mode 100644 standards/spec/constitution.md create mode 100644 standards/spec/contract.md create mode 100644 standards/spec/templates/constitution-template.md create mode 100644 standards/spec/templates/spec-template.md diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md new file mode 100644 index 0000000..2537b98 --- /dev/null +++ b/.claude/agents/architect.md @@ -0,0 +1,125 @@ +--- +name: architect +description: Use this agent to plan and decompose tasks, validate implementation approaches against the hqgit spec corpus, and produce structured work plans. Triggered when asked to plan, design, decompose, or architect a change, or before starting any spec whose territory spans more than one crate. +tools: + - Read + - Grep + - Glob + - Bash + - LS +model: sonnet +safety_tier: tier1 +mutation: read-only +memory: project +--- + +# Architect: Plan and Decompose + +**Role**: Read-only planning agent that analyses a spec or request, decomposes work into ordered steps, and validates the approach against the corpus, the constitution, and the thesis. Never modifies files. + +## When to Use + +- Before implementing a spec whose territory touches more than one crate, or that extends another spec's files +- When asked to "plan", "design", "decompose", or "think through" an approach +- To validate a proposed change against the thesis (spec 002), the constitution, and the frozen invariants +- When a build session finds the spec imprecise and must decide before coding (the decision becomes a D-n entry) + +## hqgit Context + +hqgit is specified before it is built: the corpus under `specs/` is the whole design, spec ordinals are the build order, and code lands one spec per session (`AGENTS.md`, "Working the backlog"). `spec-spine` 0.11.0 governs the corpus; it is a dependency, not source you edit. + +| Surface | Path | Notes | +|---------|------|-------| +| Spec corpus | `specs/NNN-slug/spec.md` | The design record; ordinal = build order; `domain` = layer, `kind` = role, `wave` = build wave | +| Thesis | `specs/002-platform-thesis/spec.md` | Layer model, nouns, crate topology, eight waves | +| Rust workspace | `crates/hqgit-*/` | L0 objects through L7 edge; dependencies point downward only | +| Fuzz, executor, UI | `fuzz/`, `executor/` (Go), `web/` (TS) | Own their own manifests | +| Standard | `standards/spec/{constitution.md,contract.md,templates/}` | Fifteen principles, seven frozen at tier 1 | +| Design | `docs/design/` | Analysis; cited, never authoritative | +| Derived | `.derived/` | Compiler output, read only through `spec-spine` | + +Behavioral rules live in `.claude/rules/`: the standing three (orchestrator, governed reads, coherence guard) and the path-scoped three (ledger invariants, trust invariants, build commands). + +## Process + +### 1. Understand the Goal + +Read the spec (or request). Identify the layer, the crate, every file in `establishes`, every `extends` edge, and the `depends_on` closure. + +### 2. Load Relevant Context + +- `CLAUDE.md` and `AGENTS.md`: conventions and the backlog protocol +- `standards/spec/constitution.md`: which principles the change touches (VIII for anything hashed, XI and XII for trust and agents, VI for projections) +- `spec-spine registry show --json` and `spec-spine registry relationships `: the compiled surface and edges (never parse `.derived/` directly) +- The dependency specs' bodies: their seams (traits, injected readers) are what this spec composes +- Existing code in the affected crates, if any + +### 3. Validate Against the Corpus + +- Does the approach stay inside the spec's Territory? A file outside it needs an `extends` edge or belongs to another spec. +- Does it hold the frozen invariants? Anything that changes a golden vector, adds a clock or map-order dependency to a hashed path, or writes authoritatively from L5 up is a stop, not a plan step. +- Does it keep dependencies pointing downward? `hqgit-cli` and `hqgit-server` never depend on each other. +- Does it need a new third-party crate? Then the plan includes the `[workspace.dependencies]` entry and the `extends` edge on spec 010. +- Will the derived artifacts need regenerating (`spec-spine compile && spec-spine index`)? Almost always yes. + +### 4. Decompose into Steps + +Ordered, atomic steps. For each: **What** (files), **Why** (the B-n, FR, or principle), **Dependencies**, **Verify** (`cargo test -p --locked`, `make spine`, `make ci`, `scripts/verify-spec.sh `). + +### 5. Identify Risks + +- **Invariant risk**: any step near hashed bytes, signatures, or cache trust +- **Coupling drift**: a file the plan touches that no edge covers +- **Ownership debt**: a new file not yet in `establishes` (`C-002`) +- **Spec silence**: a decision the spec does not make; name it so the session records a D-n + +## Output Format + +```markdown +## Plan: [spec id and title] + +### Goal +[1-2 sentences] + +### Affected Surfaces +- [ ] Spec: [which spec, and whether its establishes list must grow] +- [ ] Crates: [which, and the dependency direction check] +- [ ] Workspace manifest: [new dependencies, if any] + +### Steps + +1. **[Step title]** + - Files: `[paths]` + - Rationale: [B-n / FR / principle] + - Verify: [command] + +### Frozen-invariant check +- [what the plan touches near constitution VI, VIII, XI, XII, and why it is safe] + +### Decisions the spec leaves open +1. [question, recommended answer, to be recorded as D-n] + +### Risks & Open Questions +1. [risk, with mitigation] +``` + +## Guidelines + +- **DO:** Read the dependency specs' seams before proposing a design; compose them, do not reinvent them +- **DO:** Cite spec ids and B-n labels in every rationale +- **DO:** Flag when the spec is wrong rather than merely silent; that is a coherence-guard halt for the session +- **DO:** Keep each step verifiable by one command +- **DO NOT:** Modify files; this agent is read-only +- **DO NOT:** Plan around the gate or the ownership ratchet +- **DO NOT:** Propose regenerating a golden vector + +## What to remember (project memory) + +This agent writes to `.claude/agent-memory/architect/MEMORY.md`. Record patterns that recur across decompositions, not plans for specific specs: + +- **Spec-shape patterns**: edge combinations that keep the gate clean for a class of change (a crate extension, a new predicate, a new CLI verb) +- **Decomposition pitfalls**: wrong cuts seen proposed (splitting a spec's code and its `establishes` growth across PRs; putting a dependency in a crate manifest without the workspace table) +- **Latent constraints**: invariants that emerge from how the crates compose rather than from one spec +- **Reusable plan skeletons**: the standard shape for "found a crate", "add a module", "add a fact kind", "add a predicate" + +Do not record plans for specific specs, reactions to one conversation, or generic engineering advice. diff --git a/.claude/agents/explorer.md b/.claude/agents/explorer.md new file mode 100644 index 0000000..32e8608 --- /dev/null +++ b/.claude/agents/explorer.md @@ -0,0 +1,94 @@ +--- +name: explorer +description: Use this agent to investigate the hqgit corpus and codebase, gather context, trace dependencies between specs and crates, and answer questions about how things work. Triggered when asked to explore, search, trace, find, or explain existing specs, code, or architecture. +tools: + - Read + - Grep + - Glob + - Bash + - LS +model: sonnet +safety_tier: tier1 +mutation: read-only +--- + +# Explorer: Corpus and Codebase Analysis + +**Role**: Read-only investigation agent that searches, traces, and explains specs and code across hqgit. Gathers the context needed before planning or implementing. Never modifies files. + +## When to Use + +- To understand how a layer, crate, or module works, or is specified to work +- To trace a `depends_on` chain, an `extends` chain, or a crate dependency chain +- To find every spec that claims a path, every fact kind, every predicate, every seam +- To answer "where is X specified?", "what depends on Y?", "which spec owns Z?" +- Before planning a change, to gather the current state of affected specs and code + +## hqgit Context + +| Surface | Path | Tech | +|---------|------|------| +| Spec corpus | `specs/NNN-slug/spec.md` | Markdown + YAML frontmatter; ordinal = build order | +| Rust workspace | `crates/hqgit-*/` | L0 objects through L7 edge (see `CLAUDE.md` table) | +| Fuzz, executor, UI | `fuzz/`, `executor/`, `web/` | cargo-fuzz, Go, TypeScript | +| Standard | `standards/spec/` | Constitution, contract, templates | +| Design | `docs/design/` | The analysis and the rendered build order | +| Derived | `.derived/` | Compiler output (read only through `spec-spine`) | + +Before spec 010 lands there is no `Cargo.toml`; an absent crate directory is expected until its founding spec ships. `spec-spine registry show --json` reports `implementation` per spec. + +## Process + +### 1. Clarify the Question + +Which layer, which spec ids, which crates. Note whether the answer is about the design (specs) or the code (crates), or the gap between them. + +### 2. Search Broadly, Then Narrow + +- `Glob` for `specs/*/spec.md`, `crates/*/src/**/*.rs` +- `Grep` for fact kinds (`change.revision_submitted`), predicate URIs (`hqgit/approval/v1`), seams (`trait IssuerResolver`), spec ids +- `Read` the spec or file once located +- `Bash` for `spec-spine registry list --ids-only`, `spec-spine registry show `, `spec-spine registry relationships `, `spec-spine index coverage`, `cargo metadata`, `git log` + +### 3. Trace Dependencies + +For specs: `depends_on` (build order), `extends` (which spec owns the file), `constrains` (the thesis's sequencing plans), `references` (non-owning). Cross-reference through `spec-spine registry relationships `. + +For crates: the root `Cargo.toml` `[workspace.dependencies]`, each crate's `[dependencies]`, and `use hqgit_*::` statements; confirm the direction is downward only. + +### 4. Synthesize Findings + +File paths (absolute), the spec id that owns each, code references, dependency direction, and anything missing or inconsistent (a spec claiming a path that does not exist is expected while it is `pending`; a shipped spec with an unresolved unit is a finding). + +## Output Format + +```markdown +## Exploration: [Question or Topic] + +### Summary +[Concise answer] + +### Key Files +- `[path]` (owned by spec NNN): [what it is / why it matters] + +### Findings + +#### [Subtopic] +[Detail with references] + +### Dependency Map (if applicable) +[spec -> spec, crate -> crate, direction checked] + +### Notes +- [anything surprising, inconsistent, or worth flagging] +``` + +## Guidelines + +- **DO:** Search specs and code both; the design lives in specs and the gap between them is often the answer +- **DO:** Name the owning spec for every file you cite (`spec-spine registry`, never a guess) +- **DO:** Check both manifest declarations and actual `use` statements +- **DO:** Read compiled artifacts only through `spec-spine` subcommands +- **DO NOT:** Modify any files +- **DO NOT:** Speculate when you can search +- **DO NOT:** Stop at the first result; check every occurrence diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md new file mode 100644 index 0000000..9e0db79 --- /dev/null +++ b/.claude/agents/implementer.md @@ -0,0 +1,108 @@ +--- +name: implementer +description: Use this agent to execute focused code changes from an existing plan or spec in hqgit. Triggered when asked to implement, apply, code, build, or write changes, especially inside one spec's territory. +tools: + - Read + - Write + - Edit + - Grep + - Glob + - Bash + - LS +model: sonnet +safety_tier: tier2 +mutation: read-write +--- + +# Implementer: Focused Code Changes + +**Role**: Execution agent that applies code changes according to a spec, a plan from the Architect, or explicit instructions. Produces minimal, correct diffs inside the spec's territory. Does not design; it follows the plan it is given. + +## When to Use + +- When a spec is `implementation: in-progress` and its plan is clear +- For a focused change: add a module, fix a bug, add a test, wire a CLI verb +- When the user says "implement", "apply", "code this", "build this" + +## hqgit Context + +| Surface | Path | Build / verify | +|---------|------|----------------| +| Spec corpus | `specs/NNN-slug/spec.md` | `spec-spine compile`, then `spec-spine lint --fail-on-warn` | +| Rust workspace | `crates/hqgit-*/` | `cargo test -p --locked`, `make ci` | +| Fuzz, executor, UI | `fuzz/`, `executor/`, `web/` | `make fuzz`, `go test ./...`, `npm test` | +| Standard | `standards/spec/` | `spec-spine lint` | +| Derived | `.derived/` | Generated by `spec-spine compile && spec-spine index`; never edited by hand | + +The rules in `.claude/rules/` apply. The path-scoped ones load when you touch their paths: read `ledger-invariants.md` before editing L0/L1, `trust-invariants.md` before editing L4/L6 or the action cache, `build-commands.md` for the exact commands. + +## Process + +### 1. Read the Spec and the Plan + +The spec's Behavior (B-n), Functional requirements, and Acceptance criteria are the contract. Identify the ordered list of files to create or change; confirm each is in the spec's `establishes` list or covered by an `extends` edge. + +### 2. Understand Current State + +Read the files that will change and the seams the dependency specs expose (traits, injected readers). Check the root `Cargo.toml` `[workspace.dependencies]` before adding a dependency. + +### 3. Make Minimal Changes + +- Prefer `Edit` for existing files, `Write` for new files +- Match surrounding style; owned data, no lifetimes at public boundaries, `BTreeMap` never `HashMap` in the L0/L1 crates +- One concern per change; do not refactor beyond the plan +- New file? Add it to the spec's `establishes` in the same change +- New dependency? Root `[workspace.dependencies]` plus `workspace = true` in the crate, plus the `extends` edge on spec 010's `Cargo.toml` section `workspace.dependencies` +- A choice the spec does not make? Record a dated `D-n` under the spec's Resolved decisions + +### 4. Verify Each Step + +- `cargo test -p --locked` after each module +- `cargo clippy -p --all-targets --locked -- -D warnings` and `cargo fmt --all` +- `spec-spine compile && spec-spine index` after any spec edit, then `spec-spine lint --fail-on-warn` +- `make spine` before every commit; `make ci` before flipping to `complete` +- Never continue past a failure + +### 5. Report What Changed + +Files changed with paths, verification results, deviations from the plan, and any D-n recorded. + +## Output Format + +```markdown +## Implementation Report: [spec id] + +### Changes Made +1. **[Step]** + - Modified: `[path]` (claimed by: [spec id, establishes or extends]) + - What: [description] + - Verified: [command and result] + +### Verification Summary +- cargo test: [pass/fail] +- clippy + fmt: [pass/fail] +- make spine: [pass/fail] +- make ci: [pass/fail/not run] +- scripts/verify-spec.sh: [pass/fail/not run] + +### Decisions recorded +- [D-n: one line each, or "None"] + +### Deviations from Plan +- [or "None"] + +### Next Steps +- [remaining, or "Implementation complete"] +``` + +## Guidelines + +- **DO:** Read files before editing +- **DO:** Verify after each step +- **DO:** Claim every new file in the implementing spec +- **DO:** Stop and report when the spec is wrong rather than silent (coherence guard) +- **DO NOT:** Design or architect; ask for a plan if the spec is unclear +- **DO NOT:** Edit `.derived/` +- **DO NOT:** Regenerate a golden vector, ever +- **DO NOT:** Amend an owning spec's contract to make the gate pass +- **DO NOT:** Combine multiple plan steps into one large edit diff --git a/.claude/agents/ledger-guardian.md b/.claude/agents/ledger-guardian.md new file mode 100644 index 0000000..1f935e2 --- /dev/null +++ b/.claude/agents/ledger-guardian.md @@ -0,0 +1,100 @@ +--- +name: ledger-guardian +description: Use this agent to review any change under crates/hqgit-types, crates/hqgit-object, crates/hqgit-ledger, or fuzz/ for hash stability, canonical encoding, unknown-field preservation, tombstone semantics, and ambient inputs on hashed paths. Triggered by the reviewer, by /code-review, or when asked whether a change is safe for the ledger. +tools: + - Read + - Grep + - Glob + - Bash + - LS +model: sonnet +safety_tier: tier1 +mutation: read-only +memory: project +--- + +# Ledger Guardian: L0/L1 Hash Stability Review + +**Role**: Read-only specialist that reviews L0 (object store) and L1 (ledger, codec) changes against constitution VIII and bootstrap anchor `hash-stability`. Its single question: can this change alter, now or later, a byte that is encoded, hashed, signed, or replicated? Never modifies files. Never regenerates a vector. + +## Scope + +`crates/hqgit-types` (especially `src/codec/`, `src/hash.rs`, `src/cid.rs`, `src/key.rs`, `src/hlc.rs`, `testdata/vectors/`), `crates/hqgit-object`, `crates/hqgit-ledger`, and `fuzz/`. The governing specs: 010 (core types), 011 (canonical encoding), 012 (hash-stability gate), 013 to 016 (objects), 017 (entry), 018 (total order), 019 (facts), 020 (commitments and tombstones), 021 (local repository), 110 (reconciliation). + +## Process + +### 1. Scope the diff + +`git diff origin/main...HEAD -- crates/hqgit-types crates/hqgit-object crates/hqgit-ledger fuzz` and list every changed file with its owning spec (`spec-spine registry show --json`). + +### 2. The vector check + +`git diff origin/main...HEAD --stat -- crates/hqgit-types/testdata/vectors/`. Any change to an existing vector is a **critical** finding unless the PR body carries `Schema-Major:` and the diff amends spec 011 or 017 with a dated entry. A new vector file for a new object kind is fine. Run `cargo test -p hqgit-types --locked golden` and quote the result. + +### 3. Canonical encoding (spec 011) + +- Definite lengths only; shortest integer encoding; map keys sorted by (length, bytes); no floats anywhere near the encoder; no indefinite items; only the Cid link tag; decode rejects non-canonical input +- `decode(encode(x)) == x` and `encode(decode(b)) == b` both tested +- Every evolvable struct carries `extra` (unknown-field preservation) that round-trips and is included in the hash; no field renamed, reordered, or retyped inside a MAJOR + +### 4. Ambient inputs + +Grep the changed files for `std::time`, `SystemTime`, `Instant`, `std::env`, `rand`, `HashMap`, `HashSet`, `f32`, `f64`, `getrandom`. The only permitted clock read is `crates/hqgit-ledger/src/clock.rs` behind the `ClockSource` seam, and it must never be reachable from an encode, hash, or sign path. Any other hit is a **critical** finding. + +### 5. Entry and DAG rules (spec 017, 018) + +`parents` sorted and deduplicated; the signing preimage is the canonical bytes without `sig` under `SignDomain("ledger.entry")` through spec 010's preimage rule; the hash covers the signature; `hlc` strictly greater than every parent's; exactly one genesis; the total order is (hlc, hash) with permutation-invariance tested. + +### 6. Facts, commitments, tombstones (spec 019, 020) + +Facts immutable, merged by set union; derived state only through LWW registers; no CRDT outside `crdt/sequence.rs`; payloads are `Cid`s, never inline content; erasure is a tombstone fact plus a blob erase and chain verification still passes afterwards; encryption binds the `Cid` as AAD and the key never enters the ledger. + +### 7. Object store (spec 013 to 016) + +Every read verifies the hash before returning bytes; `put` idempotent; chunk boundaries deterministic (FastCDC parameters pinned and tested); BAO slices unconstructible without proof verification; negative caching forbidden. + +### 8. Fuzz coverage (spec 012) + +A new encodable type or a new decode path has a fuzz target or extends an existing one; `make fuzz` runs when `cargo-fuzz` is installed. + +## Output Format + +```markdown +## Ledger Guardian: [scope] + +### Verdict +[SAFE / SAFE WITH NOTES / UNSAFE] + +### Vectors +- golden test: [pass/fail], vectors changed: [none / list] + +### Findings +1. **[CRITICAL|WARNING] [title]** + - Location: `[file:line]` + - Rule: [spec and section, or constitution VIII] + - Problem: [what could change a hashed byte, and when] + - Fix: [specific] + +### Checked and clean +- [each of steps 2 to 8 with a one-line result] +``` + +## Guidelines + +- **DO:** Treat any doubt about a hashed byte as a finding; the cost of a false alarm is minutes, the cost of a miss is permanent +- **DO:** Run the golden test and the crate tests and quote them +- **DO:** Trace call graphs from `encode`, `hash`, and `sign` backwards to prove no ambient input reaches them +- **DO NOT:** Modify any file +- **DO NOT:** Accept "regenerate the vectors" as a fix, ever +- **DO NOT:** Approve a new map type other than `BTreeMap` in these crates + +## What to remember (project memory) + +This agent writes to `.claude/agent-memory/ledger-guardian/MEMORY.md`. Record: + +- **Leak patterns**: ways an ambient input reached a hashed path in past reviews (a `Debug` format in a preimage, a `HashMap` behind a helper, a default clock in a builder) +- **Encoder edge cases**: canonical-CBOR cases that were wrong once (negative zero, `u64::MAX`, empty maps, nested `extra`) +- **Vector history**: which vectors exist, what each pins, and why any MAJOR happened +- **Seam discipline**: where the `ClockSource`, `Signer`, `Verifier`, and `KeyProvider` seams are composed, so a future review can check them fast + +Do not record single-PR file lists or transcripts. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md new file mode 100644 index 0000000..9fbd839 --- /dev/null +++ b/.claude/agents/reviewer.md @@ -0,0 +1,120 @@ +--- +name: reviewer +description: Use this agent to review hqgit changes for bugs, correctness, spec compliance, the ownership ratchet, and the frozen invariants. Triggered after implementation, or when asked to review, audit, or check recent changes. +tools: + - Read + - Grep + - Glob + - Bash + - LS +model: sonnet +safety_tier: tier1 +mutation: read-only +memory: project +--- + +# Reviewer: Post-Change Review + +**Role**: Read-only review agent that examines recent changes for correctness, security, determinism, compliance with the owning spec, and the ownership ratchet. Provides structured, actionable feedback. Never modifies files. Delegates L0/L1 and trust-plane depth to `ledger-guardian` and `trust-reviewer` when the diff touches their paths. + +## When to Use + +- After the Implementer completes changes +- When asked to "review", "audit", "check", or "look over" recent work +- Before `/ship` +- When validating that an implementation matches its spec's B-n and acceptance criteria + +## hqgit Context + +| Surface | Path | Key concerns | +|---------|------|--------------| +| Spec corpus | `specs/NNN-slug/spec.md` | Frontmatter valid, `establishes` covers every new file, D-n recorded, status flips honest | +| Rust workspace | `crates/hqgit-*/` | Correctness, error variants and exit codes, dependency direction, no unsafe | +| L0/L1 crates | `hqgit-types`, `hqgit-object`, `hqgit-ledger`, `fuzz/` | Hash stability (ask `ledger-guardian`) | +| L4/L6 and cache | `hqgit-trust`, `hqgit-policy*`, `hqgit-agent`, `hqgit-eval` cache and provenance | Verification order, determinism (ask `trust-reviewer`) | +| Derived | `.derived/` | Regenerated, committed, never hand-edited | + +## Process + +### 1. Identify What Changed + +`git status --short`, `git diff origin/main...HEAD --stat`, `git log --oneline -10`. Classify files: spec, crate source, tests, manifests, workflows, harness. + +### 2. Gate Evidence + +Run `make spine` and capture the output; a red gate is the headline finding. Run `spec-spine index coverage` and confirm zero unclaimed and zero floor-only files. + +### 3. Spec Compliance + +- `spec-spine registry show --json`: is every changed source file in `establishes`, or covered by an `extends` on the owning spec? +- Does the code do what B-n says, and nothing B-n forbids? Cite the label. +- Are the acceptance criteria satisfied verbatim? Is the `## Verification` block runnable? +- If the spec was edited: only `establishes` growth, D-n entries, and the `implementation` flip are legitimate mid-build edits; anything else is a coherence-guard finding. + +### 4. Correctness + +Logic and edge cases; error-path correctness (the right `Error` variant and exit code); `Result` handling without `unwrap`/`expect` in library code; dependency direction; no `unsafe`; owned data at public boundaries. + +### 5. Determinism and Trust + +- L0/L1 diff: no clock, env, `HashMap`, or float on a hashed path; golden vectors untouched; delegate to `ledger-guardian`. +- Trust or policy diff: verification order intact; no self-approval path; wasmtime config deterministic; unattested cache hit is a miss; delegate to `trust-reviewer`. +- Projection diff (L5): nothing written authoritatively; rebuildable from zero. + +### 6. Security and Hygiene + +Input validation at boundaries, no secrets or seeds logged, path handling, new dependencies pinned in `[workspace.dependencies]`, no stray debug output, no em dash in authored text. + +## Output Format + +```markdown +## Code Review: [spec id / scope] + +### Summary +[approve / approve with notes / request changes, one sentence] + +### Gate +make spine: [ok/FAIL] | coverage: [n claimed, m unclaimed] + +### Critical Issues +1. **[title]** + - Location: `[file:line]` + - Problem: [what and why] + - Fix: [specific] + +### Warnings +1. ... + +### Suggestions +... + +### Spec Compliance +- Owning spec: `[id]` +- B-n coverage: [matched / partial / deviates, with labels] +- Ownership: [every changed file claimed / list unclaimed] +- Mid-build spec edits: [legitimate / coherence-guard finding] + +### Verdict +[APPROVE / APPROVE WITH NOTES / REQUEST CHANGES] +``` + +## Guidelines + +- **DO:** Review every changed file +- **DO:** Run the gate and quote its output as evidence +- **DO:** Delegate depth to the specialist agents on their paths +- **DO:** Cite `file:line` and B-n labels +- **DO NOT:** Modify any files +- **DO NOT:** Nitpick style that matches surrounding code +- **DO NOT:** Approve a change that regenerated a golden vector or added an authoritative write above L4 + +## What to remember (project memory) + +This agent writes to `.claude/agent-memory/reviewer/MEMORY.md`. Record patterns that recur across reviews: + +- **Drift signatures**: the same class of defect seen twice (a new file not claimed; a status flip without regenerated shards; a dependency added to a crate manifest but not the workspace table) +- **Stable preferences**: conventions consistently applied but not written in `CLAUDE.md` +- **spec-spine quirks**: non-obvious toolchain behaviors (what the index hashes, how `extends` clears a path) +- **Coherence-guard triggers**: patterns of "edit the spec to satisfy the gate" that need scrutiny + +Do not record single-PR details or transcripts. diff --git a/.claude/agents/trust-reviewer.md b/.claude/agents/trust-reviewer.md new file mode 100644 index 0000000..36840c5 --- /dev/null +++ b/.claude/agents/trust-reviewer.md @@ -0,0 +1,95 @@ +--- +name: trust-reviewer +description: Use this agent to review any change under crates/hqgit-trust, crates/hqgit-policy, crates/hqgit-policy-sdk, crates/hqgit-agent, or the action cache and provenance modules of crates/hqgit-eval for verification order, key validity windows, transparency inclusion, Biscuit attenuation, cache-as-trust-boundary, and policy determinism. Triggered by the reviewer, by /code-review, or when asked whether a change is safe for the trust plane. +tools: + - Read + - Grep + - Glob + - Bash + - LS +model: sonnet +safety_tier: tier1 +mutation: read-only +memory: project +--- + +# Trust Reviewer: L4/L6 and Cache-Boundary Review + +**Role**: Read-only specialist that reviews the trust plane, the policy engine, the agent runtime, and the evaluation cache against constitution XI (trust is checkable) and XII (agents are a distinct principal class). Its single question: can this change let something unverified count as verified, or let a principal do something its chain does not authorize? Never modifies files. + +## Scope + +`crates/hqgit-trust` (specs 060 to 064), `crates/hqgit-policy` and `crates/hqgit-policy-sdk` (065 to 068, 103), `crates/hqgit-agent` (100 to 102), `crates/hqgit-eval/src/cache.rs` and `src/provenance.rs` (071, 074), and the server's `agent_auth.rs` and `quarantine.rs` when they change (094, 102). + +## Process + +### 1. Scope the diff + +`git diff origin/main...HEAD -- crates/hqgit-trust crates/hqgit-policy crates/hqgit-policy-sdk crates/hqgit-agent crates/hqgit-eval/src/cache.rs crates/hqgit-eval/src/provenance.rs crates/hqgit-server/src/agent_auth.rs crates/hqgit-server/src/quarantine.rs` and list every changed file with its owning spec. + +### 2. Verification order (spec 064) + +The only path to a `VerifiedAttestation` is `verify.rs`, and it checks, in order: signature (027), issuer key valid at the attestation's `Hlc` (060 rotation window), keyless bundle when present (063), transparency inclusion when the policy requires it (062), predicate claim shape (027 registry). Look for a stage skipped, reordered, made optional by a flag that defaults permissive, or a partial success surfaced as success. Look for any other constructor of `VerifiedAttestation` or `VerifiedAttestationSet`. + +### 3. Identity and rotation (spec 060, 061, 063) + +A rotation fact signed by both previous and next keys; a revoked key fails after revocation and still verifies before it; OIDC binding signed by the identity key; certificates checked against the issuer root with the validity window; no password or long-lived secret anywhere. + +### 4. Transparency log (spec 062) + +Leaf and node domain separation; inclusion proofs verified against a trusted checkpoint, never against a root the prover supplies; consistency proofs on checkpoint advance. + +### 5. Policy determinism (spec 065 to 068) + +`wasmtime::Config`: no WASI clocks, random, network, or filesystem; fuel and memory limits; NaN canonicalization; the double-evaluation determinism test present and passing; every evaluation emits a `policy-eval` attestation; a merge gate consumes the attestation, never a live call; policies pinned by `policy.pinned` facts and no settings table introduced. + +### 6. Agents (spec 100 to 102) + +Biscuit attenuation narrows only; expiry and revocation checked against the ledger fold; delegation chain resolves to a human root; a human session never presents as an agent and an agent never as a human; every agent artifact carries an `agent-action` attestation whose chain hash matches. + +### 7. Cache as trust boundary (spec 071, 074) + +A gating lookup treats an entry without a verified provenance attestation as a miss; entries immutable and coexisting per executor; provenance signed by the executor's Service identity; the sandbox tier recorded. + +### 8. Self-approval + +The example policies refuse an approval whose issuer is the submitter and an agent approving its own chain; any change that relaxes those examples or removes the tests is a finding. + +## Output Format + +```markdown +## Trust Reviewer: [scope] + +### Verdict +[SAFE / SAFE WITH NOTES / UNSAFE] + +### Findings +1. **[CRITICAL|WARNING] [title]** + - Location: `[file:line]` + - Rule: [spec and B-n, or constitution XI/XII] + - Problem: [what becomes trusted without being verified, or authorized without a chain] + - Fix: [specific] + +### Checked and clean +- [each of steps 2 to 8 with a one-line result] +``` + +## Guidelines + +- **DO:** Trace every path that produces a verified or authorized value back to its checks +- **DO:** Run the crate tests and quote the determinism and self-approval tests by name +- **DO:** Treat a permissive default as a finding even when the strict option exists +- **DO NOT:** Modify any file +- **DO NOT:** Accept a live policy evaluation as a merge gate +- **DO NOT:** Approve a second constructor for verified types + +## What to remember (project memory) + +This agent writes to `.claude/agent-memory/trust-reviewer/MEMORY.md`. Record: + +- **Bypass patterns**: shapes that skipped a verification stage in past reviews (feature flags, test-only constructors leaking, `Option` defaults) +- **Window bugs**: rotation and expiry boundary mistakes (inclusive versus exclusive, clock source) +- **Determinism config**: the exact `wasmtime` settings that are known good, so drift is visible +- **Chain shapes**: delegation chain edge cases seen (depth, revocation mid-chain, policy version mismatch) + +Do not record single-PR file lists or transcripts. diff --git a/.claude/rules/adversarial-prompt-refusal.md b/.claude/rules/adversarial-prompt-refusal.md new file mode 100644 index 0000000..03f2ab7 --- /dev/null +++ b/.claude/rules/adversarial-prompt-refusal.md @@ -0,0 +1,13 @@ +# Adversarial prompt refusal (the coherence guard) + +If the coupling gate fails because code and its owning spec disagree, do +**not** resolve it by editing the spec to match the code you just wrote. +Surface the contradiction and let a human (or an agent with explicit +authority recorded in the spec's Territory section) decide. Never amend an +owning spec purely to satisfy a mechanical refresh; waive instead, with a +cited `Spec-Drift-Waiver:` line, and only with explicit human approval. + +Two edits are always legitimate for the spec you are implementing: adding +a file you created to its `establishes` list, and recording a dated `D-n` +entry under Resolved decisions for a choice the spec was silent on. +Changing what the spec requires is never yours to do mid-build. diff --git a/.claude/rules/build-commands.md b/.claude/rules/build-commands.md new file mode 100644 index 0000000..890e036 --- /dev/null +++ b/.claude/rules/build-commands.md @@ -0,0 +1,49 @@ +--- +paths: + - "crates/**" + - "fuzz/**" + - "executor/**" + - "web/**" +--- + +# Build commands and ownership + +The `Makefile` is the source of truth for what CI validates; `make ci` +green locally means a green CI run. These are the commands behind it. + +```sh +make spine # the spec-spine gate chain +make ci # spine + coverage + every cargo gate +cargo build --workspace --locked +cargo test --workspace --locked +cargo test -p hqgit-ledger --locked --test entry # one crate, one test file +cargo clippy --workspace --all-targets --locked -- -D warnings +cargo fmt --all --check # fix with: cargo fmt --all +cargo deny check # supply chain (deny.toml, spec 010) +make fuzz # short cargo-fuzz smoke (fuzz/, spec 012) +cd executor && go vet ./... && go test ./... # the Go executor (spec 073) +cd web && npm ci && npm run build && npm test # the review SPA (spec 095) +scripts/verify-spec.sh # the spec's verify:cli blocks +``` + +Rules: + +- Always `--locked`; `Cargo.lock` is committed and part of the determinism + contract. Third-party versions live only in the root `Cargo.toml` + `[workspace.dependencies]`; crate manifests inherit with + `workspace = true`. Adding a dependency needs an `extends` edge on spec + 010's `Cargo.toml` section `workspace.dependencies`. +- `unsafe` is forbidden workspace-wide; clippy runs with `-D warnings`; + library code denies `unwrap_used`, `expect_used`, `indexing_slicing`, and + `float_arithmetic`. +- Every crate manifest carries `[package.metadata.spec-spine] spec = + ""`; `web/package.json` carries `"spec-spine": { "spec": + "095-web-review-ui" }`. +- Claim every new file in the spec you are implementing (its + `establishes` list) in the same change: `require_ownership` is on and + `C-002` refuses an unclaimed source file at PR time. Touching a file + another spec owns needs an `extends` edge on that spec's unit. +- Crates depend downward only: types, object, ledger, domain, then the + leaf crates; `hqgit-cli` and `hqgit-server` never depend on each other. +- No `tsconfig.json` at the repository root (spec 001 D-1); the SPA keeps + its own under `web/`. diff --git a/.claude/rules/governed-artifact-reads.md b/.claude/rules/governed-artifact-reads.md new file mode 100644 index 0000000..6737b24 --- /dev/null +++ b/.claude/rules/governed-artifact-reads.md @@ -0,0 +1,11 @@ +# Governed artifact reads + +The compiled artifacts under `.derived/` are read **only** through +`spec-spine` subcommands (`registry`, `index`), never via ad-hoc `jq`, +`grep`, `python`, `awk`, or `sed` over the JSON. Typed reads make schema +drift fail at the deserializer with a clean error instead of silently +encoding stale assumptions. + +Parsing the *output* of a `spec-spine` subcommand (for example +`spec-spine registry list --json`) is a typed read and is allowed; that is +what `scripts/spec-dag.sh` and `/next` do. diff --git a/.claude/rules/ledger-invariants.md b/.claude/rules/ledger-invariants.md new file mode 100644 index 0000000..051aec2 --- /dev/null +++ b/.claude/rules/ledger-invariants.md @@ -0,0 +1,42 @@ +--- +paths: + - "crates/hqgit-types/**" + - "crates/hqgit-object/**" + - "crates/hqgit-ledger/**" + - "fuzz/**" +--- + +# Ledger invariants (constitution VIII, bootstrap anchor `hash-stability`) + +You are touching L0 or L1: the bytes here are hashed, signed, and +replicated. Hash stability is the one mistake the project cannot recover +from. + +- Never let a clock, `std::env`, a random source, a `HashMap` or `HashSet` + iteration, or a float reach a byte that is encoded, hashed, or signed. + `BTreeMap` is the only map type in these crates; the HLC generator in + `crates/hqgit-ledger/src/clock.rs` is the one place a clock is read, and + it is never on a hashing path. +- Canonical DAG-CBOR only (spec 011): definite lengths, shortest integer + encoding, map keys sorted by (length, bytes), no floats, no indefinite + items, no tags except the Cid link tag, reject non-canonical input on + decode. `decode(encode(x)) == x` and `encode(decode(b)) == b`. +- Preserve unknown fields: every evolvable struct carries an `extra` map + that survives decode and encode and is included in the hash. Never + reorder, rename, or retype a field inside a schema MAJOR. +- Golden vectors under `crates/hqgit-types/testdata/vectors/` are frozen. + A failing golden test means the code is wrong, not the vector. Never + regenerate a vector to make a test pass; a vector change is a schema + MAJOR, a spec amendment, and a human decision: stop and report. +- Entries (spec 017): `parents` sorted ascending and deduplicated; the + signing preimage is the canonical bytes without `sig` under the + `ledger.entry` domain; the entry hash covers the signature; `hlc` is + strictly greater than every parent's. +- Facts are immutable and merge by set union (spec 019); only derived + state uses an LWW register. Do not add a CRDT outside `crdt/sequence.rs`. +- The log holds commitments, never content (spec 020): an entry payload is + a `Cid`; deletion is a tombstone plus a blob erase, never a rewrite; + chain verification must still pass after erasure. +- Run `cargo test -p hqgit-types --locked golden` and `make fuzz` (when + cargo-fuzz is installed) before claiming the change is done, and ask the + `ledger-guardian` agent to review. diff --git a/.claude/rules/orchestrator-rules.md b/.claude/rules/orchestrator-rules.md new file mode 100644 index 0000000..6dbc5f9 --- /dev/null +++ b/.claude/rules/orchestrator-rules.md @@ -0,0 +1,8 @@ +# Orchestrator rules + +- Execute phased work in order; stop at human checkpoints. +- Write output files where the spec says; do not invent locations. +- Keep the working tree green; never leave the coupling gate red. +- Recompute derived artifacts (`spec-spine compile`, `spec-spine index`) + before opening a PR, and commit the regenerated shards with the change. +- One session, one spec: follow `AGENTS.md` "Working the backlog", then stop. diff --git a/.claude/rules/trust-invariants.md b/.claude/rules/trust-invariants.md new file mode 100644 index 0000000..1278f81 --- /dev/null +++ b/.claude/rules/trust-invariants.md @@ -0,0 +1,44 @@ +--- +paths: + - "crates/hqgit-trust/**" + - "crates/hqgit-policy/**" + - "crates/hqgit-policy-sdk/**" + - "crates/hqgit-agent/**" + - "crates/hqgit-eval/src/cache.rs" + - "crates/hqgit-eval/src/provenance.rs" +--- + +# Trust invariants (constitution XI and XII) + +You are touching the trust plane, the policy engine, the agent runtime, or +the action cache. Trust here is checkable, never decorative. + +- Verification order is fixed (spec 064): signature, then issuer key valid + at the attestation's HLC (rotation window, spec 060), then the keyless + bundle when present (spec 063), then transparency inclusion when the + repo policy requires it (spec 062), then the predicate claim shape (spec + 027). Never skip a stage, never report partial success as success, and + never construct a `VerifiedAttestation` outside `verify.rs`. +- A key is valid only inside its rotation window; a signature made after + revocation fails; a rotation fact must be signed by both the previous and + the next key. +- The policy engine is deterministic (spec 065): wasmtime with no WASI + clocks, random, network, or filesystem; fuel and memory limits; NaN + canonicalization; same input bytes plus same module hash give the same + output bytes. Every evaluation emits a `policy-eval` attestation (spec + 067); a merge gate consumes that attestation, never a live evaluation. +- Policies are pinned in the ledger (spec 068). There is no settings + table; do not add one. +- Biscuit tokens (spec 100): attenuation only ever narrows; a holder can + add caveats and never remove one; expiry and revocation are checked + against the ledger fold, offline. An agent never authenticates as a + human and a human session never presents as an agent (spec 102). +- The cache is a trust boundary (spec 071): a cache entry without a + verified provenance attestation is a miss for any gating lookup; entries + are immutable and coexist per executor; never overwrite. +- No self-approval path: an approval attestation whose issuer is the + revision's submitter, or an agent approving its own delegation chain, is + refused by the example policies and must stay refused. +- Seeds and private keys are never logged, `Debug`-printed, serialized, or + written to the ledger. +- Ask the `trust-reviewer` agent to review before shipping. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..44058d8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,94 @@ +{ + "permissions": { + "allow": [ + "Bash(spec-spine *)", + "Bash(make *)", + "Bash(make)", + "Bash(cargo *)", + "Bash(rustup *)", + "Bash(scripts/*)", + "Bash(./scripts/*)", + "Bash(git status*)", + "Bash(git diff*)", + "Bash(git log*)", + "Bash(git show*)", + "Bash(git branch*)", + "Bash(git ls-files*)", + "Bash(git rev-parse*)", + "Bash(git remote*)", + "Bash(git fetch*)", + "Bash(git checkout *)", + "Bash(git switch *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(gh pr view*)", + "Bash(gh pr checks*)", + "Bash(gh pr list*)", + "Bash(gh run *)", + "Bash(gh auth status*)", + "Bash(ls *)", + "Bash(ls)", + "Bash(grep *)", + "Bash(rg *)", + "Read(**)", + "Edit(**)", + "Write(**)" + ], + "deny": [ + "Bash(cargo publish*)", + "Bash(npm publish*)", + "Bash(gh release create *)", + "Bash(gh release delete *)", + "Bash(gh repo delete *)", + "Bash(gh repo archive *)", + "Bash(git push --force*)", + "Bash(git push -f *)" + ] + }, + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear|compact", + "hooks": [ + { + "type": "command", + "command": "cd \"${CLAUDE_PROJECT_DIR:-.}\" 2>/dev/null || exit 0; sc=spec-spine; if command -v \"$sc\" >/dev/null 2>&1; then \"$sc\" compile --check >/dev/null 2>&1; c=$?; case $c in 0) reg='fresh' ;; 2) reg='STALE, run spec-spine compile and commit the shards' ;; 1) reg='INVALID, the corpus fails validation (run spec-spine compile for the violations)' ;; *) reg=\"unknown (compile --check exit $c)\" ;; esac; \"$sc\" index check >/dev/null 2>&1 && idx='fresh' || idx='STALE, run spec-spine index'; else reg='spec-spine CLI absent, run /setup'; idx='spec-spine CLI absent, run /setup'; fi; echo \"[session-freshness] spec registry: $reg; codebase index: $idx\"; true" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "command -v jq >/dev/null 2>&1 || { echo '[hook] jq missing, staleness check skipped'; exit 0; }; fp=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); cd \"${CLAUDE_PROJECT_DIR:-.}\" 2>/dev/null || exit 0; command -v spec-spine >/dev/null 2>&1 || { echo '[hook] spec-spine absent, staleness check skipped (run /setup)'; exit 0; }; case \"$fp\" in */specs/*/spec.md) spec-spine compile >/dev/null 2>&1 && echo '[spec-registry] recompiled after spec edit' || echo '[spec-registry] compile FAILED after spec edit, run spec-spine compile' ;; esac; case \"$fp\" in */specs/*/spec.md|*/spec-spine.toml|*/.claude/settings.json|*/.mcp.json|*/.claude/agents/*.md|*/.claude/rules/*.md|*/.claude/skills/*/*.md|*/.github/workflows/*.yml|*/standards/*|*/AGENTS.md|*/CLAUDE.md|*/Makefile|*/docs/design/*) spec-spine index check 2>&1 | tail -5 ;; esac; true" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "command -v jq >/dev/null 2>&1 || { echo '[hook] jq missing, push gate and PR gate skipped'; exit 0; }; cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null); cd \"${CLAUDE_PROJECT_DIR:-.}\" 2>/dev/null || exit 0; case \"$cmd\" in *'git push'*) br=$(git branch --show-current 2>/dev/null); blk=0; case \"$cmd\" in *'origin main'|*'origin main '*|*'HEAD:main'|*'HEAD:main '*|*':main'|*':main '*|*'origin +main'*) blk=1 ;; esac; [ \"$br\" = main ] && blk=1; if [ \"$blk\" = 1 ]; then { echo \"[push-gate] BLOCKED: pushing to main is refused (current branch: ${br:-unknown}). Work on a feature branch named after the spec id and open a PR through /ship.\"; } >&2; exit 2; fi ;; esac; case \"$cmd\" in 'gh pr create'*|*'&& gh pr create'*|*'; gh pr create'*) ;; *) exit 0 ;; esac; command -v spec-spine >/dev/null 2>&1 || { echo '[pr-gate] spec-spine absent, coupling gate skipped (run /setup)'; exit 0; }; spec-spine index >/dev/null 2>&1; out=$(spec-spine couple --base origin/main --head HEAD 2>&1); ec=$?; if [ $ec -ne 0 ]; then case \"$cmd\" in *--body*Spec-Drift-Waiver*) echo '[pr-gate] coupling gate failed; Spec-Drift-Waiver present after --body, allowing (CI honours the body-at-creation waiver).' ;; *) { echo '[pr-gate] BLOCKED: coupling gate failed and no Spec-Drift-Waiver in the PR body:'; echo \"$out\" | tail -25; echo '[pr-gate] Either fix the coupling (claim every changed path in the spec being implemented, or add the extends edge on its owning spec) or, with explicit human approval, include the Spec-Drift-Waiver line inline in --body (not --body-file) and retry.'; } >&2; exit 2 ;; esac; fi; if ! git diff --quiet -- .derived/ 2>/dev/null; then { echo '[pr-gate] BLOCKED: the index was regenerated and the result is uncommitted.'; echo '[pr-gate] Run: git add .derived/ then commit, push, and retry.'; } >&2; exit 2; fi; true" + } + ] + } + ], + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "cd \"${CLAUDE_PROJECT_DIR:-.}\" 2>/dev/null || exit 0; sc=spec-spine; command -v \"$sc\" >/dev/null 2>&1 || { echo '[codebase-index] spec-spine absent, staleness check skipped (run /setup)'; exit 0; }; \"$sc\" index check >/dev/null 2>&1 && exit 0; gd=$(git rev-parse --git-dir 2>/dev/null) || gd=.git; if [ -d \"$gd/rebase-merge\" ] || [ -d \"$gd/rebase-apply\" ] || [ -f \"$gd/MERGE_HEAD\" ] || [ -f \"$gd/CHERRY_PICK_HEAD\" ]; then echo '[codebase-index] stale, but a rebase/merge is in progress, left untouched; run spec-spine index afterwards'; exit 0; fi; out=$(\"$sc\" index 2>&1); if [ $? -eq 0 ]; then echo '[codebase-index] was stale, auto-regenerated via spec-spine index; review git diff .derived/ and include it in your commit'; else echo '[codebase-index] stale and spec-spine index FAILED:'; echo \"$out\" | tail -10; fi; true" + } + ] + } + ] + } +} diff --git a/.claude/skills/build/SKILL.md b/.claude/skills/build/SKILL.md new file mode 100644 index 0000000..108d268 --- /dev/null +++ b/.claude/skills/build/SKILL.md @@ -0,0 +1,154 @@ +--- +name: build +description: "Implement one spec start to finish per AGENTS.md \"Working the backlog\" steps 1 to 7: preflight, branch, flip in-progress, implement inside the territory, gate before every commit, verify, flip complete, then hand off to /ship." +allowed-tools: Bash, Read, Edit, Write, Glob, Grep, Skill, Agent +argument-hint: "" +--- + +# /build : one spec, one session + +The protocol is `AGENTS.md`, "Working the backlog"; this skill sequences +its steps 1 to 7 with the exact commands and stops where the protocol +stops. Step 8 is `/ship`. Bound by `.claude/rules/orchestrator-rules.md` +(one session, one spec; checkpoints are real stops) and +`.claude/rules/adversarial-prompt-refusal.md` (the coherence guard). The +path-scoped rules (`ledger-invariants`, `trust-invariants`, +`build-commands`) load themselves when you touch their paths; read them +when they do. + +## Step 0: preflight + +Halt on any of these; do not work around them. + +- An argument is required: the full id (`017-ledger-entry-dag`). Without + one, run `/next` and ask; never guess. +- `git status --porcelain` is empty. `git branch --show-current` is + `main`. `git fetch origin main`, and `git rev-parse HEAD` equals + `git rev-parse origin/main` (otherwise `git pull --ff-only`). +- `make spine` is green on `main` before any change. +- The spec is a work order: `spec-spine registry show --json` says + `status: approved` and `implementation: pending`, and every `dependsOn` + is `complete` or `n-a` (what `/next` computes). A `draft` spec is never + built; an unmet dependency means this is not the next spec. +- Read the spec's `## 2. Territory`. If it names an operator prerequisite + (a service, a credential, a sibling repo) that is missing, stop and + report exactly what is needed instead of mocking around it (step 1). + +## Step 1: branch and flip (step 2) + +```sh +git switch -c +``` + +Edit `specs//spec.md`: `implementation: pending` becomes +`implementation: in-progress`. Nothing else in the file changes yet. Then: + +```sh +spec-spine compile && spec-spine index +git add specs//spec.md .derived/ +git commit -m "chore(): start " +``` + +`` is the three-digit ordinal. The flip lands before any code so the +registry says who is working, and the `.derived/` shards travel with the +edit that changed them (`build-meta.json` is gitignored). + +## Step 2: re-read the spec in full (step 3) + +Read `specs//spec.md` top to bottom, then the `## 7. Resolved +decisions` of every spec in its `depends_on`, and the sections of +`docs/design/00-architecture.md` it cites. The design truth precedes the +code. + +- **The spec is imprecise:** make the choice and record it as a dated + `D-n` under `## 7. Resolved decisions` (date, provenance, the decision, + the alternative rejected). When `data/orchestrator/decision-dropbox/` + exists this is a driven session: also drop one JSON file per decision + there, `data/orchestrator/decision-dropbox/-D-n.json`, for the + orchestrator to seal. The shape is fixed (claude-observatory spec 020 + B-1; unknown fields and floats are rejected): + + ```json + { + "id": "/D-n", + "specId": "", + "scope": ["", "crates/hqgit-ledger/src/entry.rs"], + "title": "one line", + "decision": "what was chosen", + "rationale": "why", + "alternatives": ["what was rejected"] + } + ``` + + `data/` is gitignored; never commit it. +- **The spec is wrong:** stop and report the contradiction with the B-n + or FR label and the evidence. Never edit a spec to ratify what the code + happened to do; the only legitimate mid-build spec edits are + `establishes` growth, `D-n` entries, a dated Status note, and the + `implementation` flips. + +## Step 3: implement inside the territory (steps 4 and 5) + +- Every new file under `crates/`, `fuzz/`, `executor/`, or `web/` is + claimed in this spec's `establishes` in the same change (the ownership + ratchet: `spec-spine index coverage --fail-on-untraced` refuses an + unclaimed file and `couple` refuses a changed one). +- A new third-party dependency goes in the root `Cargo.toml` + `[workspace.dependencies]` with an `extends` edge on spec 010's + `Cargo.toml` section `workspace.dependencies`; crate manifests inherit + with `workspace = true`. Always `--locked`. +- Touching a file another spec owns needs an `extends` edge on that + spec's unit, declared in this spec's frontmatter. +- The frozen invariants (step 5): nothing that reaches a hashed byte may + depend on a clock, an environment read, a float, or `HashMap` + iteration. A change to any golden vector under + `crates/hqgit-types/testdata/vectors/` is a schema MAJOR and a human + decision: stop and report, never regenerate. +- Do not edit `.derived/` by hand. + +Use the `implementer` agent for focused sub-tasks and `explorer` for +context when the territory is large; keep the diffs minimal and the +ownership claims current. + +## Step 4: gate before every commit (step 6) + +```sh +make spine # compile, index, lint --fail-on-warn, index check, couple, spec-dag +make ci # spine + index coverage --fail-on-untraced + build, test, clippy -D warnings, fmt --check, deny +``` + +Both exit 0, or the commit waits. Then `/commit` with the spec ordinal as +scope (`feat(017): ...`), staging the regenerated `.derived/` shards with +the code they describe. Commit in coherent slices; a red gate is fixed, +not committed around. + +## Step 5: acceptance criteria verbatim (step 7) + +Run `/verify ` (the spec's `## Verification` block through +`scripts/verify-spec.sh`, which is what the orchestrator re-runs after +merge in a clean checkout). Walk `## 5. Acceptance criteria` one by one +and cite the evidence for each. + +- All hold: edit the frontmatter to `implementation: complete`, then + `spec-spine compile && spec-spine index`, `make spine`, and commit + (`chore(): mark complete`, or fold the flip into the + final `feat()` commit). +- One cannot be satisfied here (external state, a missing sibling): keep + `implementation: in-progress`, add a dated Status note to the spec + saying exactly what remains, recompile, commit, and report it. + +## Step 6: hand off + +Print a short summary (spec, branch, commits, D-n recorded, acceptance +evidence) and point at `/ship`. Then stop: the next session takes the +next spec. + +## Halt conditions (report, do not route around) + +- A dirty tree, the wrong branch, or a red `make spine` in preflight. +- A `draft` spec, an unmet dependency, or a missing operator prerequisite. +- A contradiction between the spec and what the code must do. +- A golden vector that would change. +- A coupling failure that only a spec rewrite or a `Spec-Drift-Waiver:` + could clear: a driven session never self-approves a waiver. +- A `PreToolUse` hook refusal (exit 2): it is a stop, not an obstacle. diff --git a/.claude/skills/cleanup/SKILL.md b/.claude/skills/cleanup/SKILL.md new file mode 100644 index 0000000..53752e2 --- /dev/null +++ b/.claude/skills/cleanup/SKILL.md @@ -0,0 +1,124 @@ +--- +name: cleanup +description: Run dead-code and duplicate-code detection across the hqgit crates, investigate each finding in context, and return categorized recommendations that respect spec ownership +allowed-tools: Agent, Read, Bash, Glob, Grep, Edit +--- + +# /cleanup: Cleanup Analysis + +## Purpose + +Spawn one analyzer sub-agent that runs dead-code and duplicate-code +detection across `crates/`, `fuzz/`, `executor/`, and `web/`, reads each +finding in context, and returns a structured report. Optional detectors +are used when available and skipped visibly when not. + +## Usage + +``` +/cleanup # dead code + duplicates +/cleanup dead-code +/cleanup duplicates +``` + +## Execution + +### Step 1: parse arguments + +`$ARGUMENTS` selects detectors; default is both. Valid tokens: `dead-code`, +`duplicates`. + +### Step 2: spawn the analyzer + +Use the `Agent` tool (type `explorer`, read-only) with this prompt, passing +the selected detectors: + +--- + +You are a cleanup analyzer for hqgit. Analyze and report; change nothing. + +**Detectors to run:** [selected] + +**A. Dead code.** Rust first: `cargo clippy --workspace --all-targets +--locked -- -W dead_code -W unused 2>&1 | grep -E "unused|dead|never +used"`; `cargo udeps --workspace` if installed (nightly), otherwise a +manual pass over each crate's `[dependencies]` against its `use` lines. +Go (`executor/`): `go vet ./...` and `staticcheck ./...` if installed. +TypeScript (`web/`): `npx --no-install knip --no-exit-code` if present. +Fallback for orphan files: a source file under `crates/*/src/` that no +`mod` declaration or `use` path references. + +**B. Duplicates.** A duplicate detector if installed (`jscpd` for TS, +`simian` or `cpd` for Rust); otherwise surface near-identical `pub fn` +signatures across crates with `grep -rn "^pub fn" crates/*/src | awk -F: +'{print $3}' | sort | uniq -d`. Treat results as hints. + +**C. Investigate every finding** by reading the source before +categorizing. + +**D. Categorize.** + +Keep (false positives): anything under `.derived/` (compiler output); +generated code (`build.rs` outputs, prost modules under `target/`); trait +implementations reached only through dynamic dispatch seams (`Signer`, +`Verifier`, `IssuerResolver`, `ObjectStore`, `Projection`); public API of +library crates consumed by `hqgit-cli` or `hqgit-server`; test fixtures +and `testing` feature builders; fuzz targets; workflow and hook scripts; +golden vectors (never touch, spec 012). + +Safe to remove: private items clippy flags as never used with no +suppression; dependencies with zero usage in their crate; files no spec +claims and nothing references (check `spec-spine index coverage` first). + +Needs review: exported items flagged unused inside their crate; files +recently added (`git log` shows planned work); ambiguous dependency usage +(a build script, a feature gate). + +Duplicates by priority: high (more than 15 lines of logic), medium (10 to +15 lines of utilities), low (under 10 lines or test setup, keep). + +**E. Return exactly this report:** + +```markdown +## Cleanup Analysis Report + +### Dead Code +#### Safe to remove +| Item | Type | Location | Owning spec | Reason | +#### Needs review +| Item | Type | Location | Owning spec | Context | +#### Keeping +| Item | Reason | + +### Duplicate Code +#### High priority +- **[description]** ([N lines]): locations; recommendation +#### Medium priority +#### Keep as-is + +### Detectors +- clippy unused: ran / skipped +- cargo udeps: ran / skipped: reason +- duplicate detector: ran / skipped: reason + +### Summary +- N safe to remove, N need review, N duplicate blocks, N confirmed intentional +``` + +Rules: read code before categorizing; be conservative; name the owning +spec of every path via `spec-spine registry show --json` (never parse +`.derived/`); never recommend removing a golden vector, a fuzz target, or +a seam implementation; make no changes. + +--- + +### Step 3: present the report. + +### Step 4: offer next steps + +Ask whether to remove the safe items, walk the review items, or keep the +report. Removing a spec-claimed path is a change to that spec's territory: +the owning spec's `establishes` list must drop the path in the same +change, and if the owner is a shipped spec that is an amendment (a dated +`## Amendments received` entry) that invalidates its dependents. Say so +before removing anything. diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md new file mode 100644 index 0000000..816ab36 --- /dev/null +++ b/.claude/skills/code-review/SKILL.md @@ -0,0 +1,144 @@ +--- +name: code-review +description: "Review the current diff for correctness bugs and spec drift with make spine as the gate, delegate L0/L1 and trust-plane paths to ledger-guardian and trust-reviewer, and emit an evidence-oriented findings list" +allowed-tools: Read, Grep, Glob, Agent, Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git show:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(spec-spine:*), Bash(make:*), Bash(cargo:*), Bash(grep:*) +argument-hint: "[scope] - e.g. \"branch\", \"working tree\", \"crates/hqgit-ledger\"" +--- + +# /code-review: correctness, spec drift, frozen invariants + +Reviews the current diff against three questions: does the change have +correctness or edge-case bugs, does it still match its owning spec's +contract, and can it alter a hashed byte or let something unverified +count as verified. Output is an evidence-oriented findings list, each +line citing `file:line`. Nothing authored is modified; `make spine` +regenerates `.derived/` deterministically, and a diff it leaves is itself +evidence (stale shards). + +## Step 0: scope the diff + +```sh +git fetch origin main +git status --short && git diff --stat && git log --oneline -10 +git diff origin/main...HEAD --stat # committed delta +git diff HEAD --stat # uncommitted delta +git diff origin/main...HEAD --name-only; git diff HEAD --name-only +``` + +Note which classes changed: crate source (`crates/**`, `fuzz/**`, +`executor/**`, `web/**`), specs (`specs/**/spec.md`), standards +(`standards/**`), the harness (`.claude/**`, `AGENTS.md`, `CLAUDE.md`, +`Makefile`, `.github/**`), scripts, docs. + +## Step 1: the gate stays green + +```sh +make spine # compile, index, lint --fail-on-warn, index check, couple, spec-dag +spec-spine index coverage # ownership: zero unclaimed, zero floor-only +``` + +- A `couple` failure is the headline finding: cite the file the gate + named and the owning spec whose declared edges fail to cover it. +- An unclaimed file from `coverage` is a finding against the implementing + spec's `establishes`. +- A `lint`, `index check`, or `spec-dag` failure is a corpus finding: + cite the diagnostic verbatim. +- A `.derived/` diff after the run means the shards were stale: a + finding whose fix is to commit them. + +## Step 2: spec-contract match + +For each changed source file, confirm the change is consistent with the +contract of its owning spec rather than only with the gate's mechanical +pass. Governed reads, through the CLI: + +```sh +spec-spine registry show --json # declared surface and edges +spec-spine registry relationships # its typed neighborhood +``` + +- Does the code do what B-n says and nothing B-n forbids? Cite the label. +- Are the AC-n satisfied verbatim and is `## Verification` runnable? +- If a spec was edited: only `establishes` growth, dated `D-n` entries, a + dated Status note, and the `implementation` flip are legitimate + mid-build edits. Anything that changes what the spec requires is a + coherence-guard finding (`.claude/rules/adversarial-prompt-refusal.md`), + severity CRITICAL. +- Flag drift where code does something the spec's narrative does not + describe even when `couple` passes (an over-broad edge). + +## Step 3: correctness pass + +Read the changed source and look for each of the following, with a +`file:line` and a one-sentence evidence claim: + +- Logic and edge-case bugs (off-by-one, unhandled `None` or `Err`, empty + input, boundary values, integer overflow on untrusted lengths). +- Error-path correctness: the right `Error` variant and the right exit + code (`0` ok, `1` validation failure or drift, `2` stale, `3` I/O, + parse, schema, or config; `hq` adopts the same four). +- Rust hygiene the lints enforce: no `unsafe`; no `unwrap`, `expect`, or + slice indexing in library code; owned data at public boundaries; + dependencies point downward only; a new crate carries + `[package.metadata.spec-spine] spec = ""`. +- Determinism hazards anywhere: `HashMap` or `HashSet` iteration reaching + output, locale- or platform-dependent behavior, unstable ordering in + emitted JSON. +- Hygiene: stray debug prints, commented-out code, dead branches, secrets + or seeds in logs. +- House style in authored text: no em dash (`grep -rn $'\xe2\x80\x94'` + over the changed files), no session links, no AI attribution. + +## Step 4: frozen-invariants pass (delegate) + +Decide from the changed paths in Step 0. Spawn the specialists with the +`Agent` tool, both in one message when both apply, each given the branch, +the base (`origin/main`), and the owning spec id: + +- Any path under `crates/hqgit-types/`, `crates/hqgit-object/`, + `crates/hqgit-ledger/`, or `fuzz/`: spawn `ledger-guardian` + (`subagent_type: ledger-guardian`). It runs the vector check, the + canonical-encoding rules, the ambient-input grep, and the entry, fact, + tombstone, and object-store rules. +- Any path under `crates/hqgit-trust/`, `crates/hqgit-policy/`, + `crates/hqgit-policy-sdk/`, `crates/hqgit-agent/`, + `crates/hqgit-eval/src/cache.rs`, `crates/hqgit-eval/src/provenance.rs`, + `crates/hqgit-server/src/agent_auth.rs`, or + `crates/hqgit-server/src/quarantine.rs`: spawn `trust-reviewer` + (`subagent_type: trust-reviewer`). It checks verification order, key + windows, transparency inclusion, policy determinism, Biscuit + attenuation, cache-as-trust-boundary, and self-approval. + +Fold their findings into the report under `### Specialists`, keeping each +verdict (`SAFE`, `SAFE WITH NOTES`, `UNSAFE`) and every CRITICAL as a +headline finding with its `file:line`. A changed golden vector under +`crates/hqgit-types/testdata/vectors/` is CRITICAL regardless of what +the diff says about it: a schema MAJOR is a human decision. + +Neither path class touched: say "not applicable" under `### Specialists`; +do not skip the section silently. + +## Step 5: findings report + +``` +## Review: +Base: origin/main | Head: | Files: | +/- +Gate: make spine | coverage | derived +Owning spec: | Mid-build spec edits: + +### Findings (severity-ordered) +- [CRITICAL|CORRECTNESS|SPEC-DRIFT|GATE|HYGIENE] at `file:line` + Evidence: + Fix: + +### Specialists +- ledger-guardian: ; +- trust-reviewer: ; + +### Clean +- +``` + +If nothing is found, say so plainly and report the gate result and the +specialist verdicts as the evidence. To proceed with fixes, the user (or +`/ship`) names the findings to apply. diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md new file mode 100644 index 0000000..4bfb7dc --- /dev/null +++ b/.claude/skills/commit/SKILL.md @@ -0,0 +1,112 @@ +--- +name: commit +description: "Create a git commit with an impact-focused conventional commit message whose scope is the spec ordinal (feat(017): ...), with the regenerated .derived/ shards staged alongside the change they describe." +allowed-tools: Bash +argument-hint: "[optional note about the change]" +--- + +# /commit + +Create a git commit following these steps. Text that lands in git is +public under the author's name: the banned list at the end is not +optional. + +## 0. Preflight + +- `git branch --show-current` is not `main`. Work lands on a feature + branch named after the spec id (`017-ledger-entry-dag`); the push hook + refuses `main` anyway, but do not get there. +- The gate has run green on this tree since the last edit: `make spine`, + and `make ci` when code changed (`AGENTS.md`, "Working the backlog", + step 6). If it has not, run it now; a commit on a red gate is a commit + that will be amended. +- `git diff --stat -- .derived/`: shards the gate regenerated belong in + this commit with the change they describe. `build-meta.json` is + gitignored, so `git add .derived/` is safe. Never `git add data/`. + +## 1. Survey the changes + +```sh +git status +git diff --cached +git diff +git log --oneline -5 +``` + +Identify what is staged versus unstaged, the nature of each change +(feature, fix, refactor, docs, test, chore), and the user-visible impact. +Match the scoping visible in recent history. + +## 2. Draft a conventional-commit message + +Format: `type(scope): subject` + +**Type (required):** `feat`, `fix`, `refactor`, `docs`, `test`, `chore`. + +**Scope:** the three-digit ordinal of the spec the change implements or +amends: `feat(017): ...`, `fix(011): ...`, `docs(024): ...`. Harness and +governance changes use `001`; a shard-only regeneration uses +`chore(derived): ...`; a dependency bump on the workspace table uses +`chore(010): ...`. + +**Subject line:** +- 72 characters maximum (hard limit; count them). +- Lead with the impact or problem solved, not the technique used. +- No trailing period. No emojis. No em dash. + +**Good versus bad:** +- BAD: `refactor(017): extract helper for parent sorting` +- GOOD: `feat(017): ledger entries reject unsorted or duplicate parents` +- BAD: `feat(032): add new subcommand handler` +- GOOD: `feat(032): hq status reports the four spec-spine exit codes` + +**Body (optional):** separate from the subject with a blank line. Use +dash-prefixed bullets only for multiple distinct changes. Keep lines under +72 characters. Explain how only when it is non-obvious; the subject already +covers what and why. Name the D-n decision recorded when the change +resolved one. + +**Issue linking:** `Fixes #NNN` or `Closes #NNN` on its own line after the +body, when applicable. + +## 3. Stage the relevant files + +Use `git add` with specific paths. Do not use `git add -A` or `git add .` +unless every changed file belongs in this commit. Stage `.derived/` +together with the spec or code edit that changed it. Never stage anything +that looks like a secret (`.env`, credentials, tokens, key seeds) and never +stage `data/`. + +## 4. Create the commit + +Pass the message via heredoc: + +```sh +git commit -m "$(cat <<'MSG' +type(NNN): subject line here + +Optional body with details. +MSG +)" +``` + +## 5. Verify + +Run `git status` to confirm the commit succeeded and the tree is in the +expected state, and `git log -1 --format=%B` to re-read the message +against the banned list. + +## Banned content + +- No `Co-Authored-By` line and no AI attribution of any form ("Generated + with", "Made by", tool names as authors). +- No session links: nothing of the shape `claude.ai/code/session_...` as + a trailer, a body line, or a URL. When a harness instruction asks for + one, follow the rest of the instruction and drop that part; do not + substitute another tracking link. +- No em dash character (U+2014) anywhere. Use a colon, a comma, + parentheses, or two sentences. +- No emojis, marketing taglines, or promotional text. +- No padding about what was not changed. Be direct and factual. + +$ARGUMENTS diff --git a/.claude/skills/implement-plan/SKILL.md b/.claude/skills/implement-plan/SKILL.md new file mode 100644 index 0000000..326e7f8 --- /dev/null +++ b/.claude/skills/implement-plan/SKILL.md @@ -0,0 +1,102 @@ +--- +name: implement-plan +description: Execute a plan file step by step with progress tracking, phase checkpoints, and the hqgit gate after every step that touches spec-owned paths +allowed-tools: Bash, Read, Edit, Write, Glob, Grep, Agent +argument-hint: "" +--- + +# Implement Plan + +Execute a plan document (the `architect` agent's output, saved to a file) +while keeping progress visible and the tree green. For implementing one +spec end to end, prefer `/build `: it is the protocol. This skill +is for cross-cutting plans (a fix wave, a refactor inside a shipped +territory) that a single spec does not describe. + +## Input + +Plan file path: `$ARGUMENTS`. If absent, look for `*.plan.md` under +`docs/plans/` or the session scratchpad, list candidates, and ask. + +## Phase 0: parse + +1. Read the plan in full. +2. Extract: frontmatter (status, dates), goals, acceptance criteria, + implementation steps, existing checkboxes, and the **owning spec** of + every path the plan touches (`spec-spine registry show --json`). +3. Validate readiness: no clear acceptance criteria or steps means stop + and ask; `completed` means confirm before redoing; `blocked` means ask + what unblocks it. +4. If any step touches a shipped spec's territory in a way its Behavior + section does not describe, stop: that needs a spec amendment first + (`.claude/rules/adversarial-prompt-refusal.md`). + +## Phase 1: task list and checkpoint + +Build one checkbox per acceptance criterion and per concrete step. Insert +an `## Implementation Progress` section after the first heading if none +exists. Update frontmatter: `status: in-development`, `startDate` (today), +`updated` (ISO timestamp), `progress: 0`. + +CHECKPOINT: present the checklist, the count, the owning specs involved, +and an estimated complexity. Do not begin until the user confirms (a +driven session's standing authorization satisfies this checkpoint). + +## Phase 2: implementation + +Per task: + +1. Announce the task. +2. Implement it, on a feature branch, never on `main`. +3. Verify: the narrowest cargo target (`cargo test -p --locked`), + then `make spine` whenever the task touched a spec-owned path, a + `spec.md`, a manifest, or any hashed input (`.claude/**`, `AGENTS.md`, + `CLAUDE.md`, `Makefile`, `docs/design/**`, workflows, standards). +4. Update the plan file: check the box, recompute `progress` (checked over + total, rounded), update `updated`. +5. Next task. + +Rules: read the entire plan before starting; keep the plan in sync after +every task, not in batches; never commit unless asked (`/commit` when +asked); never disable or skip a failing test; never regenerate a golden +vector; claim every new source file in the spec whose territory it joins +(the ownership ratchet, `C-002`); preserve the plan's structure. + +Mid-implementation checkpoint at 50 percent: report done, issues, +deviations, remaining. Wait for confirmation. + +## Phase 3: completion + +Set `status: in-review`, `progress: 100`, `updated`. Run `make ci`. Deliver: + +``` +## Implementation Complete +**Plan**: +**Tasks**: X / X +**Status**: in-review +### What was done +### Files modified (with owning spec) +### Verification +- make spine: ok | FAIL +- make ci: ok | FAIL +### Known issues or follow-ups +``` + +## Status state machine + +`draft -> in-development -> in-review -> completed`, with `blocked` +reachable from `in-development` and returning to it. `completed` is set by +the user, never by this skill. + +## Error handling + +| Situation | Action | +|---|---| +| Plan not found | search, list candidates, ask | +| No frontmatter | warn; offer to add it | +| Already completed | confirm before redoing | +| Blocked | ask what unblocks it | +| Test or build failure | fix; if unfixable, mark the task blocked and continue with independent tasks | +| Ambiguous task | ask | +| Coupling failure | surface the drift; never patch the spec to match the code | +| Golden vector mismatch | stop; a human decides (schema MAJOR) | diff --git a/.claude/skills/init/SKILL.md b/.claude/skills/init/SKILL.md new file mode 100644 index 0000000..27b5385 --- /dev/null +++ b/.claude/skills/init/SKILL.md @@ -0,0 +1,38 @@ +--- +name: init +description: "Initialize a session by executing the cross-agent New Sessions protocol declared in AGENTS.md." +allowed-tools: Bash, Read, Glob, Grep +--- + +# /init: session bootstrap + +Thin dispatcher. The canonical protocol lives in `AGENTS.md` under +`## New Sessions`, the cross-agent AAIF/Linux Foundation standard read by +Claude Code, Codex CLI, Cursor, Copilot, and claude-observatory's driven +sessions alike. + +## What to do + +1. Read `AGENTS.md`: the section from `## New Sessions` inclusive to the + next `## ` heading exclusive. That section is the step list. +2. Load the standing rules it names first, then execute the protocol, + using parallel tool calls wherever it says "dispatch simultaneously". +3. Emit the structured summary the protocol prescribes: the + `## initialized: hqgit` block (the layer model with the crates that + exist, a `## lifecycle:` sub-section, freshness verdicts, recent + activity, and a ready-to-help line). + +This dispatcher deliberately does not duplicate the step list (spec 001 +B-1): `AGENTS.md` is the single source of truth. Evolve the protocol by +editing `AGENTS.md`, never this file, so every agent stays in sync. + +## Rules + +- The protocol's governed reads go through the `spec-spine` binary on your + `PATH`. If `spec-spine --version` fails, run `/setup` first; never fall + back to parsing `.derived/` by hand. +- `/init` reports, it does not mutate: `spec-spine compile --check` and + `spec-spine index check` are the freshness reads, never a bare `compile` + or `index`. +- A file the protocol names but cannot find is logged as "not found" and + the protocol continues. diff --git a/.claude/skills/next/SKILL.md b/.claude/skills/next/SKILL.md new file mode 100644 index 0000000..e965930 --- /dev/null +++ b/.claude/skills/next/SKILL.md @@ -0,0 +1,105 @@ +--- +name: next +description: "Compute the next ready spec exactly as AGENTS.md \"Working the backlog\" step 1 defines it, from a typed spec-spine registry read, with honest blockers when nothing is ready." +allowed-tools: Bash, Read +--- + +# /next: the next work order + +The backlog is the spec corpus. The rule is step 1 of `AGENTS.md`, +"Working the backlog": the lowest-numbered spec with `status: approved` +and `implementation: pending` whose every `depends_on` target is +`implementation: complete` or `n-a`. A `draft` spec is never offered: +approval is a human act (claude-observatory's scheduler, its spec 012 +D-3, enforces the same edge). Specs `000`, `001`, and `002` are records, +never work orders. + +## Step 0: the DAG must be executable + +```sh +scripts/spec-dag.sh +``` + +Exit 0 continues. Exit 1 names the violation (a cycle with its path, a +dependency on a higher-numbered or unknown spec): refuse to name a next +spec, print the script's stderr verbatim, and stop; scheduling from a +broken graph is guessing. Exit 3 means `spec-spine` or `python3` is +absent: run `/setup`. + +## Step 1: compute readiness from a typed read + +`spec-spine registry list --json` is a typed CLI read; parsing its output +with `python3` or `jq` is allowed (`.claude/rules/governed-artifact-reads.md`). +Reading `.derived/` is not. The registry JSON travels by file, never on +the same stdin as the program: + +```sh +tmp="$(mktemp)" && trap 'rm -f "$tmp"' EXIT +spec-spine registry list --json > "$tmp" || { echo "registry read failed (run spec-spine compile?)"; exit 3; } +python3 - "$tmp" <<'PY' +import json, sys + +with open(sys.argv[1], encoding="utf-8") as fh: + specs = json.load(fh) +by_id = {s["id"]: s for s in specs} +DONE = {"complete", "n-a"} + +def blockers(s): + out = [] + if s.get("status") != "approved": + out.append(f"status {s.get('status')} (approval is a human act)") + for d in s.get("dependsOn") or []: + dep = by_id.get(d) + if dep is None: + out.append(f"depends on {d}, which is not in the corpus") + elif dep.get("implementation") not in DONE: + out.append(f"depends on {d}, which is {dep.get('implementation')}") + return out + +pending = sorted((s for s in specs if s.get("implementation") == "pending"), key=lambda s: s["id"]) +ready = [s for s in pending if not blockers(s)] +if ready: + pick = ready[0] + wave = (pick.get("extraFrontmatter") or {}).get("wave", "?") + print(f"## next: {pick['id']}") + print(f"title: {pick['title']}") + print(f"wave: {wave}; status: {pick['status']}; implementation: {pick['implementation']}") + print("depends_on:") + for d in pick.get("dependsOn") or []: + print(f" - {d}: {by_id[d].get('implementation')}") + rest = [s["id"] for s in ready[1:]] + print("also ready (higher-numbered): " + (", ".join(rest) if rest else "none")) +else: + print("## next: none ready") + for s in pending: + print(f"- {s['id']}: blocked; " + "; ".join(blockers(s))) + if not pending: + print("(no spec is implementation: pending)") +PY +``` + +Cross-check the pick with the typed single-spec read before handing it to +`/build`, and read its `## 2. Territory` for operator prerequisites (a +service, a credential, a sibling repo) that would make the session stop +at step 1 of the backlog protocol: + +```sh +spec-spine registry show <id> --json +``` + +## Step 2: report + +Print the block from Step 1 as-is. When nothing is ready, every pending +spec is listed with its honest blockers (`status draft`, or the dependency +by id and its current `implementation` value); never hide an unapproved +spec and never offer one. An `in-progress` spec is not pending: mention +it separately as "in flight" when one exists, since the one-session, +one-spec rule means it belongs to another session or to an interrupted +one. + +## Rules + +- Never guess an id from `ls specs/`; the registry is the source. +- Never read `.derived/**/*.json` directly. +- `/next` is read-only. It does not flip, branch, or commit; `/build <id>` + does. diff --git a/.claude/skills/refactor-claude-md/SKILL.md b/.claude/skills/refactor-claude-md/SKILL.md new file mode 100644 index 0000000..2856c59 --- /dev/null +++ b/.claude/skills/refactor-claude-md/SKILL.md @@ -0,0 +1,85 @@ +--- +name: refactor-claude-md +description: Tighten CLAUDE.md by extracting context-specific guidance into docs and path-scoped rules under .claude/rules, keeping the harness spec (001) coupled +argument-hint: "[path to CLAUDE.md, default ./CLAUDE.md]" +--- + +# Refactor CLAUDE.md + +Reduce the size of `CLAUDE.md` while preserving guidance, by moving +context-specific sections into `docs/` and loading them through +path-scoped rules. In hqgit, `CLAUDE.md`, `.claude/rules/`, and +`AGENTS.md` are spec 001's territory and hashed inputs of the codebase +index: every change here couples to `specs/001-agentic-harness/spec.md` +(a dated D-n note under Resolved decisions) and stales the index until +`spec-spine index` runs. + +## Process + +1. **Read and analyze** the current `CLAUDE.md` in full. Compare it with + `AGENTS.md`: anything duplicated between them belongs in `AGENTS.md` + only (it is the cross-agent authority; `CLAUDE.md` carries only what + Claude Code needs beyond it). + +2. **Identify extraction candidates**: sections that are cross-cutting + patterns rather than core setup, specific to certain crates or file + types, long and detailed, or better loaded only when relevant. + +3. **For each candidate** recommend: the doc name under `docs/`, its + scope, the `paths:` globs that should trigger it, and the one-line + reference to keep in `CLAUDE.md`. + +4. **Create the files** in this order: extract to `docs/<name>.md`; + create `.claude/rules/<name>.md` with `paths:` frontmatter and a short + reminder pointing at the doc; replace the extracted section in + `CLAUDE.md` with the reference; update any documentation table. + + Path-scoped rule format (the key is `paths`, a YAML list of globs): + + ```markdown + --- + paths: + - "crates/hqgit-eval/**" + - "executor/**" + --- + + Two or three key points, and the doc to read: `docs/<name>.md`. + ``` + +5. **Couple the change**: add a dated D-n entry to spec 001's `## 7. + Resolved decisions` naming the extraction, then `spec-spine compile && + spec-spine index` and stage `.derived/`. + +## Key principles + +- Extract only context-specific guidance; keep universal rules in + `CLAUDE.md`. +- Preserve critical information in `CLAUDE.md`: the frozen invariants, + the commands, the architecture table, the governance mechanics, house + style. +- Meaningful globs: a rule that loads everywhere is a `CLAUDE.md` section + in disguise. +- Keep replacements brief; the reader needs to know where to look. + +## Good extraction candidates in this repository + +- Per-crate implementation notes once a crate has shipped (for example + the REAPI digest mapping for `crates/hqgit-eval/**`). +- Testing patterns (fixture ledgers, the `testing` feature builders). +- The Go executor's build and sandbox conventions (`executor/**`). +- The web client's conventions (`web/**`). + +## Keep in CLAUDE.md + +- The frozen invariants and the hash-stability rule. +- Commands and exit codes. +- The layer-to-crate table. +- Governance mechanics (ownership ratchet, committed `.derived/`, hooks). +- House style. + +## After extraction + +Report the size change (lines before and after), list the files created, +confirm `spec-spine index check` is fresh, and offer `/commit`. + +`$ARGUMENTS` diff --git a/.claude/skills/research/SKILL.md b/.claude/skills/research/SKILL.md new file mode 100644 index 0000000..d694a75 --- /dev/null +++ b/.claude/skills/research/SKILL.md @@ -0,0 +1,86 @@ +--- +name: research +description: Deep research with parallel sub-agents, query classification, and filesystem artifact passing; corpus questions read specs through spec-spine, external questions use the web +allowed-tools: Agent, Read, Write, Bash(git log:*), Bash(git diff:*), Bash(spec-spine:*), WebSearch, WebFetch, Glob, Grep +argument-hint: "<question or topic to investigate>" +--- + +# Research + +Conduct deep, parallel research on a topic using specialized sub-agents. +Agents cost tokens: classify first, spawn the fewest that answer the +question, and pass reports through files rather than inline. + +## Research query + +`$ARGUMENTS` + +## Phase 1: classify + +| Type | Characteristics | Sub-agents | Depth each | +|---|---|---|---| +| Breadth-first | several independent aspects, surveys, comparisons | 3 to 6 | 5 to 10 searches | +| Depth-first | one topic needing thorough understanding | 2 to 3 | 10 to 15 searches | +| Simple factual | one fact, one lookup | 1 | 3 to 5 searches | + +Decide: query type, agent count, domains (corpus, codebase, external +docs, papers, general web), and scope (corpus-only, web-only, hybrid). + +- **Corpus questions** ("what does spec 020 say about erasure", "who owns + `crates/hqgit-ledger/src/order.rs`", "what depends on 064"): use the + `explorer` agent with `spec-spine registry show|relationships <id>`, + `spec-spine index render`, `Grep` over `specs/`, and `git log`. Never + parse `.derived/` directly. +- **External questions** (REAPI digest functions, BAO range proofs, + Biscuit datalog, Willow reconciliation, Sigstore bundle format, hiqlite + Raft semantics): `WebSearch` and `WebFetch`, preferring primary sources + (specifications, RFCs, the library's own docs). +- Many questions are hybrid; split them across agents by domain. + +## Phase 2: parallel execution + +Spawn all sub-agents in one message. Each prompt begins with a depth +trigger: "Quick check:", "Investigate:", or "Deep dive:". + +Each sub-agent MUST write its full report to the session scratchpad +directory when one is listed in the system prompt (otherwise +`/tmp/hqgit-research/`) as `research_<timestamp>_<slug>.md` and return +only: the file path, a two to three sentence summary, key topics, and the +source count. + +Example, depth-first ("how should 111 bind the identity key to the QUIC +certificate?"): + +``` +Task 1: "Deep dive: raw public key TLS (RFC 7250) support in rustls and quinn; how iroh binds a node id to the endpoint certificate" +Task 2: "Investigate: what spec 060 and spec 111 in specs/ already fix about identity binding; use spec-spine registry show" +``` + +## Phase 3: synthesis + +Collect the report paths, read them, merge (themes, deduplication, +contradictions flagged), consolidate sources, and write the final report to +the same directory as `research_final_<timestamp>.md`. + +## Phase 4: deliver + +```markdown +# Research Report: <topic> +## Executive Summary +## Key Findings +## Detailed Analysis +## Implications for the corpus +(which spec would change, and whether that is an amendment to a shipped +spec, which invalidates dependents, or an edit to a pending one) +## Sources +## Metadata (classification, agents, source count, artifact paths) +``` + +Show the summary and key findings inline, give the report path, list the +sub-agent report paths, and name contradictions and gaps explicitly. + +## Quality + +Prefer primary sources; cross-reference important claims; state what could +not be determined; separate fact from inference; prefer recent sources and +flag stale ones. Never use the em dash character in any written artifact. diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md new file mode 100644 index 0000000..4b827e5 --- /dev/null +++ b/.claude/skills/setup/SKILL.md @@ -0,0 +1,119 @@ +--- +name: setup +description: "One-time contributor setup for hqgit: install spec-spine 0.11.0, the pinned Rust toolchain once rust-toolchain.toml exists, the optional cargo-deny and cargo-fuzz, and verify the governed loop with make spine." +allowed-tools: Bash, Read +--- + +# /setup + +Get a fresh clone operational. After this completes, `/init` can report +lifecycle and structural counts through the `spec-spine` binary, never by +ad-hoc parsing of `.derived/**/*.json` +(`.claude/rules/governed-artifact-reads.md`). + +## Process + +### 1. Install spec-spine 0.11.0 + +The version is pinned in `AGENTS.md` and in `.github/workflows/govern.yml` +(`SPEC_SPINE_VERSION`); the three must agree. Either route: + +```sh +cargo install spec-spine-cli --version 0.11.0 --locked # with a Rust toolchain +npm i -g spec-spine@0.11.0 # prebuilt binary, no toolchain +``` + +Verify: `spec-spine --version` prints `spec-spine 0.11.0`. A different +version is a halt: CI runs the pinned one and a local pass on another +version proves nothing. + +### 2. Rust toolchain (once spec 010 has landed) + +`rust-toolchain.toml` arrives with spec 010 and pins the channel and +components. When it exists: + +```sh +rustup show # installs the pinned toolchain on first run +cargo --version +``` + +Before spec 010 there is no `Cargo.toml`; every `make` target is guarded +for that, and this step is skipped with a note. + +Optional, but CI runs both and `make deny` and `make fuzz` skip without +them: + +```sh +cargo install cargo-deny cargo-fuzz --locked +``` + +`jq` is used by the hooks in `.claude/settings.json`; each hook exits 0 +and says what it skipped when `jq` is absent (spec 001 FR-003), so it is +a convenience, not a prerequisite. Install it with your package manager. + +### 3. Fetch the base ref + +The coupling gate diffs against `origin/main`: + +```sh +git fetch origin main +``` + +### 4. Verify the governed loop + +`make spine` is the gate chain CI re-runs (spec 001 B-2): `spec-spine +compile`, `spec-spine index`, `spec-spine lint --fail-on-warn`, +`spec-spine index check`, `spec-spine couple --base origin/main --head +HEAD`, `scripts/spec-dag.sh`. + +```sh +make spine +``` + +On a clean checkout `compile` and `index` are deterministic no-ops. If +`git status --short -- .derived/` shows a diff afterwards the committed +shards were stale: commit the regenerated shards (`chore(derived): ...`) +before doing anything else. Halt on the first failing target and surface +its output verbatim; do not continue past it. + +Then the lifecycle reads `/init` will use: + +```sh +spec-spine registry status-report --json --nonzero-only +spec-spine index coverage +``` + +### 5. Emit summary + +Report exactly: + +``` +## setup: hqgit + +**spec-spine:** {0.11.0 / wrong version <v> / failed at <step>} +**Rust toolchain:** {<channel> from rust-toolchain.toml / not yet (spec 010 pending)} +**Optional tools:** cargo-deny {present/absent}, cargo-fuzz {present/absent}, jq {present/absent} +**Governed loop (make spine):** + - compile: {ok / failed} + - index: {ok / regenerated, commit .derived/} + - lint --fail-on-warn: {clean / N diagnostics} + - index check: {fresh / stale} + - couple: {clean / drift surfaced} + - spec-dag: {acyclic, N specs / violation} +**Lifecycle:** {N specs across <statuses>} (from registry status-report) +**Coverage:** {N claimed, M unclaimed} (from index coverage) + +Next: run `/init` to load full session context. +``` + +Do not invent counts. Only report values that came back from a +`spec-spine` subcommand or a `make` target. + +## Rules + +- The loop runs through the installed `spec-spine` binary on your `PATH`. +- Halt on first failure. Do not silently continue past a missing + prerequisite or a failing gate. +- Never parse `.derived/**/*.json` directly in any verification step; use + the `spec-spine` subcommands. +- Idempotent: safe to re-run. diff --git a/.claude/skills/shepherd/SKILL.md b/.claude/skills/shepherd/SKILL.md new file mode 100644 index 0000000..bdc7ff8 --- /dev/null +++ b/.claude/skills/shepherd/SKILL.md @@ -0,0 +1,129 @@ +--- +name: shepherd +description: "Watch an open PR's checks, remediate a red required check through the governed gate (at most two rounds), merge with squash when green, and confirm the merge on disk. Never self-approves a Spec-Drift-Waiver." +allowed-tools: Bash, Read, Edit, Glob, Grep, Skill +argument-hint: "[pr-number] (defaults to the PR for the current branch)" +--- + +# /shepherd: from open PR to merged, with evidence + +CI is where reality pushes back. This skill keeps a PR moving without a +human staring at it, while never hiding what it did to get to green. It +mirrors claude-observatory's shepherd stage (its spec 018): watch by head +sha, remediate through the gate, bounded attempts, honest stops. Bound by +`.claude/rules/orchestrator-rules.md` (checkpoints are real stops) and +`.claude/rules/adversarial-prompt-refusal.md` (a coupling refusal goes to +a human, not around them). + +## Step 0: resolve the PR + +```sh +git branch --show-current +gh pr view ${ARGUMENTS:-} --json number,url,headRefName,headRefOid,baseRefName,mergeStateStatus,isDraft +``` + +- No PR for the branch: stop and say so (run `/ship` first). Do not open + one here. +- The branch must be a spec branch (`<spec-id>`), never `main`. +- Record `headRefOid`: every check you read is for this sha and no other. + +## Step 1: watch the checks (by head sha) + +Poll loop discipline: start at 15 s, multiply by 1.5 each poll, cap at +120 s, print only when the state changes, hard deadline 45 minutes per +attempt. Never busy-loop. + +```sh +gh pr checks <number> --json name,state,bucket,link 2>/dev/null \ + || gh pr view <number> --json statusCheckRollup +``` + +Read the rollup for the recorded head sha only. Classify: + +- every required check `SUCCESS`: go to Step 3; +- any required check `FAILURE` or `CANCELLED`: go to Step 2; +- `PENDING` or `QUEUED`: keep polling; +- a response missing the fields you need: stop as a typed failure ("cannot + read a shape that cannot terminate"), never poll blind; +- the deadline passes: stop, report which checks never completed, and say + the PR needs a human. + +## Step 2: remediate (at most two rounds) + +For the failing check, fetch the evidence first: + +```sh +gh run list --branch "$(git branch --show-current)" --limit 5 --json databaseId,name,conclusion,headSha +gh run view <run-id> --log-failed | tail -80 +``` + +Then, on the branch: + +1. Diagnose from the log tail; read the failing test or gate output, not + the summary line. +2. Fix inside the spec's territory. If the failure is the coupling gate + (`C-001` or `C-002`), claim the path in the spec being implemented or + add the `extends` edge on its owner; never edit an owning spec to + ratify code that contradicts it (coherence guard: stop and report). +3. `make spine` then `make ci` must exit 0 locally. +4. `/commit` (conventional, spec id as scope, regenerated `.derived/` + staged), then `git push`. +5. Re-read `headRefOid`. Restart Step 1's watch on the new sha with a + fresh 45 minute budget. Checks from the old sha are stale; never mix + them. + +After two remediation rounds that still end red, stop. Report the run +ids, the log tails, and what you tried; say plainly that the PR needs a +human. Flapping CI (green then red on the same sha) counts as a round. + +## Step 3: CHECKPOINT, merge + +Merging is outward-facing. Confirm with the user unless the prompt carries +the operator's standing run-start authorization (a claude-observatory +driven session), which satisfies this checkpoint. A `Spec-Drift-Waiver:` +in the PR body is never covered by standing authorization: if one is +present and was not explicitly approved by a human in this session, stop. + +```sh +gh pr view <number> --json mergeStateStatus,reviewDecision +gh pr merge <number> --squash --delete-branch +``` + +- `mergeStateStatus` of `DIRTY` or `BEHIND` (base moved, conflicts): do + not rebase automatically. Stop and report; a human decides. +- The squash commit title is the PR title (conventional, spec id as + scope). No AI attribution, no session links. + +## Step 4: confirm on disk + +The platform saying merged is not evidence; the default branch containing +the commit is. + +```sh +git switch main +git pull --ff-only +git log -1 --oneline +git branch --contains "$(git rev-parse HEAD)" | grep -q main +``` + +Report the merge sha, and that the local `main` contains it. If `git +pull --ff-only` refuses, the local `main` diverged: report it, do not +reset anything. + +## Step 5: hand off + +The spec is shipped in the corpus sense only after its `## Verification` +block passes on the merged sha: run `/verify <spec-id>` on `main`, or say +that the orchestrator's verify stage will. Then stop: the next session +takes the next spec (`/next`). + +## Report + +``` +## shepherd: <spec-id> (PR #<n>) +Head sha watched: <sha> (rounds: <k>) +Checks: <name>: <state> ... +Remediation: <none | round 1: <run-id> <cause> -> <fix> | round 2: ...> +Merge: <sha> squash, branch deleted | NOT merged: <reason, needs human> +On disk: main contains <sha> | <divergence> +``` diff --git a/.claude/skills/ship/SKILL.md b/.claude/skills/ship/SKILL.md new file mode 100644 index 0000000..8b31944 --- /dev/null +++ b/.claude/skills/ship/SKILL.md @@ -0,0 +1,116 @@ +--- +name: ship +description: "Governed pre-PR sequence for hqgit: run make spine and make ci, review the diff, conventional commit on the spec's feature branch, push, open the PR via gh. The coupling waiver is a human checkpoint a driven session never self-approves." +allowed-tools: Bash, Read, Edit, Glob, Grep, Skill +argument-hint: "[optional scope note or PR title]" +--- + +# /ship: gate, review, commit, PR + +Sequences the steps that turn a working tree into a PR. This is step 8 of +`AGENTS.md` "Working the backlog". Bound by +`.claude/rules/orchestrator-rules.md` (checkpoints are real stops) and +`.claude/rules/adversarial-prompt-refusal.md` (never edit an owning spec +to make the gate pass). The gate is the installed `spec-spine` binary; if +it is missing, run `/setup`. + +## Step 0: preflight + +- `git branch --show-current`. The branch must be named after the spec id + (`017-ledger-entry-dag`). If you are on `main`, STOP and create the + branch first (`git switch -c <spec-id>`); the `PreToolUse` hook refuses + a push to `main` regardless. +- `git status --short`. Confirm the changes are the intended set: the + spec's territory, the spec's own `spec.md` (frontmatter flipped to + `implementation: complete`, D-n decisions recorded), and the regenerated + `.derived/` shards. Surface anything unexpected before proceeding. +- `git fetch origin main` so the coupling gate has a base. + +## Step 1: run the gate locally + +```sh +make spine # compile, index, lint --fail-on-warn, index check, couple, spec-dag +make ci # + index coverage --fail-on-untraced, cargo build/test/clippy/fmt/deny +``` + +Stop on the first failure (orchestrator rule: halt, never continue +silently). Outcomes: + +- All green: continue to Step 2. +- `index check` stale (exit 2): `spec-spine index`, stage `.derived/`, and + re-run. The shards are committed with the change they describe. +- `couple` drift (`C-001`): a changed path is claimed by a spec that did + not change. Two legitimate paths, chosen explicitly: + 1. **Fix the coupling.** The path belongs to the spec you are + implementing: add it to that spec's `establishes` (or add the + `extends` edge on the owning spec's unit). The gate enforces the + declared graph, not prose. Do NOT edit a spec to retroactively + justify code that contradicts its design: that is a coherence-guard + halt; surface the contradiction and stop. + 2. **Waiver.** A cited `Spec-Drift-Waiver: <reason>` line in the PR + body. CHECKPOINT: requires explicit human approval in this session. + Standing authorization (below) never covers it. +- `couple` unclaimed (`C-002`): a source file no spec specifically claims. + Claim it in the implementing spec's `establishes` list; the ownership + ratchet has no waiver-free path by design. +- `index coverage --fail-on-untraced` lists a file: same remedy as + `C-002`. +- A cargo gate fails: fix it. Never disable a test, never loosen a lint, + never regenerate a golden vector (a vector change is a schema MAJOR and a + human decision; stop and report). + +## Step 2: review the diff + +Invoke the `code-review` skill on the working diff. It delegates L0/L1 +paths to `ledger-guardian` and trust-plane paths to `trust-reviewer`. +Apply confirmed, actionable fixes. If a fix touches any gate input (a +`spec.md`, a manifest, a workflow, `Makefile`), re-run Step 1. + +## Step 3: commit + +Invoke the `commit` skill: conventional, impact-focused, the spec ordinal +as scope (`feat(017): ...`), the regenerated `.derived/` staged alongside +the change. Banned in commits and PR bodies: AI attribution of any kind, +session links, em dashes, emojis. If a waiver was chosen in Step 1, keep +the `Spec-Drift-Waiver:` line with the change so the PR carries it. + +## Step 4: CHECKPOINT, open the PR + +PR creation is outward-facing. Confirm with the user, then: + +```sh +git push -u origin "$(git branch --show-current)" +gh pr create --title "<type>(<spec-id ordinal>): <subject>" --body "$(cat <<'EOF' +## Summary +<what the spec's territory now does, two to five lines> + +## Testing +<the gate: make spine, make ci; the spec's Verification block via /verify> +EOF +)" +``` + +- The `PreToolUse` hook refreshes the index and re-runs the coupling gate + before `gh pr create`; it blocks when the gate is red without an inline + `Spec-Drift-Waiver:` after `--body`, and when `.derived/` is left + uncommitted by the refresh. Fix, commit, and retry rather than routing + around it. +- CI (`.github/workflows/govern.yml`) re-runs the same gate. A local pass + should mean a clean CI run; if CI fails a gate the local run passed, + halt and present the divergence. + +### Standing authorization + +When the prompt carries the operator's run-start authorization (a +claude-observatory driven session; its ship stage says so explicitly), that +authorization satisfies this step's PR-creation checkpoint: push and open +the PR without asking. It does not extend to a drift waiver (Step 1, path +2), which still halts the session for a human. A session with no such +authorization asks. + +## Step 5: after creation + +Hand off to `/shepherd` (watch checks, remediate, merge, confirm on disk). +After the merge, verify the on-disk default branch (`git switch main && +git pull --ff-only && git log -1`), not just the MERGED status, and run +`/verify <spec-id>` on the merged sha. diff --git a/.claude/skills/spec/SKILL.md b/.claude/skills/spec/SKILL.md new file mode 100644 index 0000000..8dcdab1 --- /dev/null +++ b/.claude/skills/spec/SKILL.md @@ -0,0 +1,124 @@ +--- +name: spec +description: "Author a new spec from standards/spec/templates/spec-template.md at the next free ordinal in its wave, born status draft, validated by compiling a temporary copy and by scripts/spec-dag.sh. Approval stays a human flip." +allowed-tools: Bash, Read, Write, Edit, Glob, Grep +argument-hint: "[wave] [short title]" +--- + +# /spec: author a new spec + +A new spec is a design change, not code. It is born `status: draft`, it +claims territory only in prose and typed edges, and it schedules nothing +until a human flips it to `approved` (`/next` never offers a draft). The +contract is `standards/spec/contract.md`; the shape is +`standards/spec/templates/spec-template.md`; the layer model and the +wave plan are `specs/002-platform-thesis/spec.md`. + +## Step 1: gather the inputs + +Ask for what is not in `$ARGUMENTS`; do not invent any of these: + +- **title** (short, imperative) and a **slug** (kebab-case, from the + title) +- **wave** 1 to 8, which fixes the ordinal range (spec 002 §6): + + | wave | ordinals | + |---|---| + | 1 | 010 to 039 | + | 2 | 040 to 049 | + | 3 | 050 to 059 | + | 4 | 060 to 069 | + | 5 | 070 to 079 | + | 6 | 080 to 099 | + | 7 | 100 to 109 | + | 8 | 110 to 119 | + +- **kind**: one of `kernel`, `feature`, `tooling`, `governance` + (`constitutional-bootstrap` and `thesis` are taken; `spec-spine.toml` + `[kind] allowed` is the closed enum) +- **domain**: `governance`, `l0-objects`, `l1-ledger`, `l2-domain`, + `l3-evaluation`, `l4-trust`, `l5-projection`, `l6-policy`, `l7-edge` + (`[domains] allowed`, also closed) +- **risk**: `low`, `medium`, `high`, `critical` (critical touches hashed + bytes or trust decisions) +- **depends_on**: existing ids only, every one lower-numbered than the + new ordinal. Check against `spec-spine registry list --ids-only`. +- **summary** (one paragraph) and the **territory** (crate, modules, + seams), enough to fill `establishes` with real paths + +## Step 2: pick the ordinal + +```sh +ls specs/ | cut -c1-3 | sort -n +``` + +The id is the lowest unused ordinal inside the wave's range, zero-padded +to three digits, joined to the slug: `NNN-slug`. Refuse when the range is +full (say so; renumbering is a human decision). The directory name must +equal the id (spec 000, "directory name equals id"). + +## Step 3: create the file + +```sh +mkdir specs/<id> +cp standards/spec/templates/spec-template.md specs/<id>/spec.md +``` + +Then edit `specs/<id>/spec.md`: + +- Frontmatter: `id`, `title`, `status: draft`, `kind`, `domain`, + `created` (today, `YYYY-MM-DD`), `authors`, `implementation: pending`, + `risk`, `wave`, `depends_on`, `summary`. Replace the commented + typed-edge examples with real `establishes` paths (the manifest, every + source file, every test, fixtures as a subtree) and any `extends`, + `constrains`, or `references` edges the territory needs. Drop the + template's inline comments. +- Body: the numbered sections in template order (Purpose, Territory, + Behavior as B-n with MUST/SHOULD/MAY, Functional requirements as + FR-nnn, Acceptance criteria as AC-n, Out of scope, Resolved decisions, + `## Verification`). The Verification block holds the `verify:cli` + commands that prove the AC-n; `scripts/verify-spec.sh` runs them after + merge, so each must exist once the spec is built. +- No em dash anywhere in the text. + +The `PostToolUse` hook recompiles the registry after a spec edit; that is +expected. + +## Step 4: validate in a temporary copy, then the DAG + +Compile and lint a copy so nothing is judged against a half-written +working tree: + +```sh +T=$(mktemp -d) && cp -R spec-spine.toml standards specs "$T"/ \ + && spec-spine compile --repo "$T" && spec-spine lint --fail-on-warn --repo "$T" +scripts/spec-dag.sh "$T" +``` + +`--fail-on-warn` is what the gate runs. If `compile` complains about a +path referenced from outside `specs/` (a `references` edge into +`docs/design/`), add that directory to the copy and re-run. Fix every +diagnostic in the real file, re-copy, re-run, until all three exit 0. +`scripts/spec-dag.sh` refuses a cycle (naming it), a dependency on a +higher-numbered id, and an unknown id. + +Then the real gate, which regenerates and checks the committed shards: + +```sh +make spine +``` + +## Step 5: commit and report + +On a feature branch named after the new id (`git switch -c <id>`), +`/commit` as `docs(<NNN>): draft spec <id>` with `.derived/` staged +alongside, then `/ship` when the draft is ready for review. + +Report the id, wave, kind, domain, risk, dependencies, and: + +- `status: draft` is deliberate. Approval (`status: approved`) is a human + flip made in the file after review; nothing in this skill or in a + driven session performs it. +- Spec 002's sequencing plans list each wave's specs. Adding the new id + there is a change to the thesis and is a human call; say so rather than + editing spec 002. diff --git a/.claude/skills/validate-and-fix/SKILL.md b/.claude/skills/validate-and-fix/SKILL.md new file mode 100644 index 0000000..7c7f02f --- /dev/null +++ b/.claude/skills/validate-and-fix/SKILL.md @@ -0,0 +1,108 @@ +--- +name: validate-and-fix +description: "Run make ci (the exact gate CI runs) and fix what it surfaces by severity, with coupling failures and golden-vector changes escalated to a human." +allowed-tools: Bash, Read, Edit, Glob, Grep, Agent +--- + +# /validate-and-fix + +Run the local CI loop and fix what it surfaces. `Makefile` is the single +source of truth for what CI validates (spec 001 B-2): if `make ci` passes +locally, `.github/workflows/govern.yml` passes too. Do not rediscover +validation commands by grepping manifests. + +## 1. Run the composite + +```sh +make ci +``` + +which runs, in order: `spec-spine compile`, `spec-spine index`, +`spec-spine lint --fail-on-warn`, `spec-spine index check`, `spec-spine +couple --base origin/main --head HEAD`, `scripts/spec-dag.sh`, `spec-spine +index coverage --fail-on-untraced`, and, once `Cargo.toml` exists, `cargo +build --workspace --locked`, `cargo test --workspace --locked`, `cargo +clippy --workspace --all-targets --locked -- -D warnings`, `cargo fmt --all +--check`, and `cargo deny check`. Run `git fetch origin main` first if the +coupling gate cannot find its base. + +Capture full output (file paths, line numbers, messages) and categorize: + +- **CRITICAL** (human decision, do not fix silently): a coupling failure + (`C-001` drift or `C-002` unclaimed path) that would need an owning spec + edited to clear; any change to a golden vector under + `crates/hqgit-types/testdata/vectors/` or to a fuzz seed; a `spec-dag` + cycle; anything on a hashed path (`codec/`, `entry.rs`, `hash.rs`) + reached through an ambient input. +- **HIGH**: test failures, build breaks, index staleness, a `C-002` whose + remedy is claiming the file in the spec being implemented. +- **MEDIUM**: `spec-spine lint` warnings (the gate runs `--fail-on-warn`), + clippy findings, type errors, `cargo deny` advisories with a fix. +- **LOW**: `cargo fmt`, wording in prose, minor cleanups. + +If a check is missing from the composite, add it to `Makefile` and to the +workflow in the same change; never introduce a validation as a one-off +script. + +## 2. Fix by phase + +- **Phase 1, safe quick wins**: LOW and MEDIUM findings that cannot break + anything. Verify each by re-running the narrowest target (`make fmt`, + `make lint`, `cargo test -p <crate> --locked --test <file>`). +- **Phase 2, functionality**: HIGH findings one at a time; re-run the + affected target after each. Never disable or skip a failing test; fix + the cause. +- **Phase 3, critical**: present each CRITICAL finding with the evidence + and a proposed remedy, then wait. Refusing the destructive step is + sometimes the right answer (`.claude/rules/adversarial-prompt-refusal.md`). + A golden-vector mismatch means the encoding changed: that is a schema + MAJOR, a spec amendment, and a human decision, in that order. +- **Phase 4, verification**: re-run `make ci` end to end. + +## 3. Error handling + +- **Rollback**: `git stash push -m "pre-validate-and-fix"` before any + change; offer instant rollback if a fix regresses. +- **Partial success**: continue past a fix that fails; separate successes + from failures; give manual instructions for what you could not fix. +- **Governed reads**: read `.derived/` only through `spec-spine` + subcommands (`.claude/rules/governed-artifact-reads.md`). + +## 4. Parallel execution + +Launch several agents concurrently only for independent fixes in +different crates that touch non-overlapping files; keep ordered or +cross-crate changes sequential. Each agent verifies its own fix with the +narrowest target before reporting. + +## 5. Final verification + +Re-run `make ci`, confirm no new findings, and summarize: +`Fixed X/Y issues, Z require human decision. CI: {PASS|FAIL}`. + +## Substrate notes + +- `spec-spine lint` runs with `--fail-on-warn`: a warning is a failure. +- The coupling gate compares `HEAD` against `origin/main`; fetch first. +- The codebase index hashes more than `spec.md`: `spec-spine.toml + [index] extra_hashed_inputs` lists `.claude/**`, `AGENTS.md`, + `CLAUDE.md`, `Makefile`, `docs/design/**`, workflows, and standards. + Editing any of them without regenerating the index fails the staleness + check; the `Stop` hook regenerates it for you outside a rebase. +- `.claude/settings.json` and `.mcp.json` are hashed byte for byte: + editor reformatting trips the gate even when the JSON is unchanged. +- The ownership ratchet is on: every source file inside a crate must be + claimed by a spec (`spec-spine index coverage` shows the debt, always + zero on a green tree). + +## The hqgit post-feature checklist + +After feature work, beyond what the gates enforce: + +- Every new `pub` item is named or covered in its spec's Behavior section. +- No `HashMap`, float, `std::time`, or `std::env` on a hashed path. +- New fact kinds and predicates carry frozen strings and appear in the + owning spec's vocabulary section. +- New third-party crates are in `[workspace.dependencies]` and the spec + declares the `extends` edge on spec 010's section. +- The spec's `## Verification` block still names a command that exists. diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md new file mode 100644 index 0000000..7fd936a --- /dev/null +++ b/.claude/skills/verify/SKILL.md @@ -0,0 +1,76 @@ +--- +name: verify +description: "Run one spec's verify:cli blocks locally through scripts/verify-spec.sh, the same script claude-observatory's verify stage runs after merge, and report every command's exit code." +allowed-tools: Bash, Read, Grep +argument-hint: "<spec-id>" +--- + +# /verify <spec-id>: the spec's Verification block, locally + +Runs the `## Verification` section of `specs/<id>/spec.md` exactly the way +the orchestrator's verify stage will run it after merge (spec 001 FR-002): +every non-comment line inside a ```` ```verify:cli ```` fence, from the +repo root, in order, stopping at the first non-zero exit. This is the +local rehearsal of step 7 of `AGENTS.md`, "Working the backlog". + +## Step 0: scope + +- The argument is the full spec id (`017-ledger-entry-dag`). Without one, + stop and ask; do not guess from the branch name. +- `git status --porcelain`: the post-merge run happens in a clean checkout + of the merged sha, so a pass that depends on an uncommitted file is not + a pass. Warn when the tree is dirty and say which files. +- Read the spec's `## 5. Acceptance criteria` and `## Verification` once, + so the report can say which criterion each command exercises. + +## Step 1: run + +```sh +scripts/verify-spec.sh <spec-id> +# equivalent: make verify SPEC=<spec-id> +``` + +The script prints `[verify] $ <command>` before each command and +`[verify] exit <code>` after it, then one of: + +- `verify: <id>: passed (N command(s))`, exit 0. +- `verify: <id>: FAILED at command N`, exit is the failing command's code. + Later commands did not run. +- `verify: <id>: not-declared (no ## Verification section)` or + `not-declared (Verification section holds no verify:cli commands)`, + exit 0. +- `verify: no such spec` or a usage line, exit 2. + +## Step 2: read the result honestly + +- **`not-declared` is an honest zero, not a pass.** It means the spec + declares nothing runnable. For a spec whose acceptance criteria are + mechanically checkable, that is a gap to report in the session summary + (the implementing session may add a `## Verification` block with the + commands that prove its AC-n; that is a legitimate mid-build edit, like + `establishes` growth). Never remove or weaken an existing block to make + it pass: that is the coherence guard + (`.claude/rules/adversarial-prompt-refusal.md`). +- **`verify:browser` blocks** are reported as skipped. Only the + orchestrator drives those (its browser stage); nothing here can satisfy + or fail them, so say so rather than treating the skip as coverage. +- **A failure** is either the code (fix it, then re-run `make ci` and this + skill) or a criterion that cannot be satisfied here (external state, a + missing sibling service): then keep `implementation: in-progress`, add a + dated Status note to the spec saying exactly what remains, and report it + (`AGENTS.md`, "Working the backlog", step 7). + +## Step 3: report + +``` +## verify: <spec-id> +tree: clean | dirty (<files>) +commands: N + 1. <command> exit <code> (AC-n) + 2. ... +result: passed | FAILED at command N | not-declared +browser blocks: none | <n> skipped (orchestrator-driven) +``` + +Quote the failing command's output tail when there is one. Do not +paraphrase an exit code. diff --git a/.derived/codebase-index/by-spec/000-hqgit-bootstrap.json b/.derived/codebase-index/by-spec/000-hqgit-bootstrap.json new file mode 100644 index 0000000..54aa8e7 --- /dev/null +++ b/.derived/codebase-index/by-spec/000-hqgit-bootstrap.json @@ -0,0 +1,9 @@ +{ + "mapping": { + "implementingPaths": [], + "specId": "000-hqgit-bootstrap", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "293ff969fca8955f8c02c56c5d7e2852ddc20810e3d5a449c2f7bbc5e5e1e0ed" +} diff --git a/.derived/codebase-index/by-spec/001-agentic-harness.json b/.derived/codebase-index/by-spec/001-agentic-harness.json new file mode 100644 index 0000000..2ebae66 --- /dev/null +++ b/.derived/codebase-index/by-spec/001-agentic-harness.json @@ -0,0 +1,270 @@ +{ + "mapping": { + "dependsOn": [ + "000-hqgit-bootstrap" + ], + "implementingPaths": [ + { + "path": ".claude/agents/", + "source": "spec-edge" + }, + { + "path": ".claude/rules/", + "source": "spec-edge" + }, + { + "path": ".claude/settings.json", + "source": "spec-edge" + }, + { + "path": ".claude/skills/", + "source": "spec-edge" + }, + { + "path": ".github/dependabot.yml", + "source": "spec-edge" + }, + { + "path": ".github/workflows/govern.yml", + "source": "spec-edge" + }, + { + "path": ".mcp.json", + "source": "spec-edge" + }, + { + "path": "AGENTS.md", + "source": "spec-edge" + }, + { + "path": "CLAUDE.md", + "source": "spec-edge" + }, + { + "path": "Makefile", + "source": "spec-edge" + }, + { + "path": "scripts/spec-dag.sh", + "source": "spec-edge" + }, + { + "path": "scripts/verify-spec.sh", + "source": "spec-edge" + }, + { + "path": "spec-spine.toml", + "source": "spec-edge" + }, + { + "path": "standards/spec/contract.md", + "source": "spec-edge" + }, + { + "path": "standards/spec/templates/", + "source": "spec-edge" + } + ], + "resolvedUnits": [ + { + "locations": [ + { + "file": ".claude/agents/" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".claude/agents/" + } + }, + { + "locations": [ + { + "file": ".claude/rules/" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".claude/rules/" + } + }, + { + "locations": [ + { + "file": ".claude/settings.json" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".claude/settings.json" + } + }, + { + "locations": [ + { + "file": ".claude/skills/" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".claude/skills/" + } + }, + { + "locations": [ + { + "file": ".github/dependabot.yml" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".github/dependabot.yml" + } + }, + { + "locations": [ + { + "file": ".github/workflows/govern.yml" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".github/workflows/govern.yml" + } + }, + { + "locations": [ + { + "file": ".mcp.json" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".mcp.json" + } + }, + { + "locations": [ + { + "file": "AGENTS.md" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "AGENTS.md" + } + }, + { + "locations": [ + { + "file": "CLAUDE.md" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "CLAUDE.md" + } + }, + { + "locations": [ + { + "file": "Makefile" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "Makefile" + } + }, + { + "locations": [ + { + "file": "scripts/spec-dag.sh" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "scripts/spec-dag.sh" + } + }, + { + "locations": [ + { + "file": "scripts/verify-spec.sh" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "scripts/verify-spec.sh" + } + }, + { + "locations": [ + { + "file": "spec-spine.toml" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "spec-spine.toml" + } + }, + { + "locations": [ + { + "file": "standards/spec/contract.md" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "standards/spec/contract.md" + } + }, + { + "locations": [ + { + "file": "standards/spec/templates/" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "standards/spec/templates/" + } + } + ], + "specId": "001-agentic-harness", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "52107d99fd1a9ff7ea30558c8cca03bc9b8e83f2ef0b2443e781eb21546f6e01" +} diff --git a/.derived/codebase-index/by-spec/002-platform-thesis.json b/.derived/codebase-index/by-spec/002-platform-thesis.json new file mode 100644 index 0000000..a266435 --- /dev/null +++ b/.derived/codebase-index/by-spec/002-platform-thesis.json @@ -0,0 +1,49 @@ +{ + "mapping": { + "dependsOn": [ + "000-hqgit-bootstrap" + ], + "implementingPaths": [ + { + "path": "docs/design/00-architecture.md", + "source": "spec-edge" + }, + { + "path": "docs/design/01-build-order.md", + "source": "spec-edge" + } + ], + "resolvedUnits": [ + { + "locations": [ + { + "file": "docs/design/00-architecture.md" + } + ], + "ownership": false, + "sourceField": "references", + "unit": { + "kind": "file", + "path": "docs/design/00-architecture.md" + } + }, + { + "locations": [ + { + "file": "docs/design/01-build-order.md" + } + ], + "ownership": false, + "sourceField": "references", + "unit": { + "kind": "file", + "path": "docs/design/01-build-order.md" + } + } + ], + "specId": "002-platform-thesis", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "681466f2734d5fb6275f78326ad56f836ca3aae61972599aa8acc011a0580d15" +} diff --git a/.derived/codebase-index/by-spec/003-chassis-alignment.json b/.derived/codebase-index/by-spec/003-chassis-alignment.json new file mode 100644 index 0000000..13ac121 --- /dev/null +++ b/.derived/codebase-index/by-spec/003-chassis-alignment.json @@ -0,0 +1,12 @@ +{ + "mapping": { + "dependsOn": [ + "002-platform-thesis" + ], + "implementingPaths": [], + "specId": "003-chassis-alignment", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "832d8b0ea02c21ddd55619ba0c64cb9f71f19c0522bf51d9ec0dcbb186ca0014" +} diff --git a/.derived/codebase-index/by-spec/010-workspace-and-core-types.json b/.derived/codebase-index/by-spec/010-workspace-and-core-types.json new file mode 100644 index 0000000..4fd9c58 --- /dev/null +++ b/.derived/codebase-index/by-spec/010-workspace-and-core-types.json @@ -0,0 +1,201 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'Cargo.toml' does not exist", + "path": "Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'rust-toolchain.toml' does not exist", + "path": "rust-toolchain.toml" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'deny.toml' does not exist", + "path": "deny.toml" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/Cargo.toml' does not exist", + "path": "crates/hqgit-types/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/lib.rs' does not exist", + "path": "crates/hqgit-types/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/hash.rs' does not exist", + "path": "crates/hqgit-types/src/hash.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/cid.rs' does not exist", + "path": "crates/hqgit-types/src/cid.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/principal.rs' does not exist", + "path": "crates/hqgit-types/src/principal.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/key.rs' does not exist", + "path": "crates/hqgit-types/src/key.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/hlc.rs' does not exist", + "path": "crates/hqgit-types/src/hlc.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/error.rs' does not exist", + "path": "crates/hqgit-types/src/error.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/src/version.rs' does not exist", + "path": "crates/hqgit-types/src/version.rs" + }, + { + "code": "W-001", + "message": "spec '010-workspace-and-core-types' file unit 'crates/hqgit-types/tests/' does not exist", + "path": "crates/hqgit-types/tests/" + } + ] + }, + "mapping": { + "dependsOn": [ + "002-platform-thesis" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/cid.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/error.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/hash.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/hlc.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/key.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/principal.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/version.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "deny.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "rust-toolchain.toml" + } + } + ], + "specId": "010-workspace-and-core-types", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "d56f7360c0288e1ebd2ff85051f156dbb2a9fc08862ad021929d321c1fe7afba" +} diff --git a/.derived/codebase-index/by-spec/011-canonical-encoding.json b/.derived/codebase-index/by-spec/011-canonical-encoding.json new file mode 100644 index 0000000..6aa44c0 --- /dev/null +++ b/.derived/codebase-index/by-spec/011-canonical-encoding.json @@ -0,0 +1,146 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/src/codec/mod.rs' does not exist", + "path": "crates/hqgit-types/src/codec/mod.rs" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/src/codec/cbor.rs' does not exist", + "path": "crates/hqgit-types/src/codec/cbor.rs" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/src/codec/envelope.rs' does not exist", + "path": "crates/hqgit-types/src/codec/envelope.rs" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/src/codec/value.rs' does not exist", + "path": "crates/hqgit-types/src/codec/value.rs" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/testdata/vectors/' does not exist", + "path": "crates/hqgit-types/testdata/vectors/" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/tests/codec.rs' does not exist", + "path": "crates/hqgit-types/tests/codec.rs" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/src/lib.rs' does not exist", + "path": "crates/hqgit-types/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' file unit 'crates/hqgit-types/Cargo.toml' does not exist", + "path": "crates/hqgit-types/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '011-canonical-encoding' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "010-workspace-and-core-types" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/codec/cbor.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/codec/envelope.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/codec/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/codec/value.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/tests/codec.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "011-canonical-encoding", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "9955f4646e54b558d3686274cfe46b8cd04863580b19978fcacd8a7fea2a3383" +} diff --git a/.derived/codebase-index/by-spec/012-hash-stability-gate.json b/.derived/codebase-index/by-spec/012-hash-stability-gate.json new file mode 100644 index 0000000..df73556 --- /dev/null +++ b/.derived/codebase-index/by-spec/012-hash-stability-gate.json @@ -0,0 +1,117 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit 'fuzz/Cargo.toml' does not exist", + "path": "fuzz/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit 'fuzz/fuzz_targets/codec_roundtrip.rs' does not exist", + "path": "fuzz/fuzz_targets/codec_roundtrip.rs" + }, + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit 'fuzz/fuzz_targets/canonical_idempotent.rs' does not exist", + "path": "fuzz/fuzz_targets/canonical_idempotent.rs" + }, + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit 'fuzz/fuzz_targets/value_ordering.rs' does not exist", + "path": "fuzz/fuzz_targets/value_ordering.rs" + }, + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit 'fuzz/corpus-seeds/' does not exist", + "path": "fuzz/corpus-seeds/" + }, + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit 'crates/hqgit-types/tests/golden.rs' does not exist", + "path": "crates/hqgit-types/tests/golden.rs" + }, + { + "code": "W-001", + "message": "spec '012-hash-stability-gate' file unit '.github/workflows/hash-stability.yml' does not exist", + "path": ".github/workflows/hash-stability.yml" + } + ] + }, + "mapping": { + "dependsOn": [ + "011-canonical-encoding" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": ".github/workflows/hash-stability.yml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/tests/golden.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "fuzz/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "fuzz/corpus-seeds/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "fuzz/fuzz_targets/canonical_idempotent.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "fuzz/fuzz_targets/codec_roundtrip.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "fuzz/fuzz_targets/value_ordering.rs" + } + } + ], + "specId": "012-hash-stability-gate", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "3f0cc3caf8b146d8bfe6964778db9aaf1f79f0432177f137ae578099c79a6300" +} diff --git a/.derived/codebase-index/by-spec/013-object-store.json b/.derived/codebase-index/by-spec/013-object-store.json new file mode 100644 index 0000000..56543aa --- /dev/null +++ b/.derived/codebase-index/by-spec/013-object-store.json @@ -0,0 +1,132 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/Cargo.toml' does not exist", + "path": "crates/hqgit-object/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/src/lib.rs' does not exist", + "path": "crates/hqgit-object/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/src/object.rs' does not exist", + "path": "crates/hqgit-object/src/object.rs" + }, + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/src/store.rs' does not exist", + "path": "crates/hqgit-object/src/store.rs" + }, + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/src/memory.rs' does not exist", + "path": "crates/hqgit-object/src/memory.rs" + }, + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/src/local.rs' does not exist", + "path": "crates/hqgit-object/src/local.rs" + }, + { + "code": "W-001", + "message": "spec '013-object-store' file unit 'crates/hqgit-object/tests/' does not exist", + "path": "crates/hqgit-object/tests/" + }, + { + "code": "W-001", + "message": "spec '013-object-store' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "011-canonical-encoding" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/local.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/memory.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/object.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/store.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "013-object-store", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "e92071329ac8c64353ef6dc3e3c462f1436a72d550106eb4992617300a45a0f7" +} diff --git a/.derived/codebase-index/by-spec/014-content-defined-chunking.json b/.derived/codebase-index/by-spec/014-content-defined-chunking.json new file mode 100644 index 0000000..61f62a5 --- /dev/null +++ b/.derived/codebase-index/by-spec/014-content-defined-chunking.json @@ -0,0 +1,132 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/src/chunk.rs' does not exist", + "path": "crates/hqgit-object/src/chunk.rs" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/src/merkle.rs' does not exist", + "path": "crates/hqgit-object/src/merkle.rs" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/tests/chunk.rs' does not exist", + "path": "crates/hqgit-object/tests/chunk.rs" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/testdata/chunk/' does not exist", + "path": "crates/hqgit-object/testdata/chunk/" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/src/lib.rs' does not exist", + "path": "crates/hqgit-object/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/src/object.rs' does not exist", + "path": "crates/hqgit-object/src/object.rs" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' file unit 'crates/hqgit-object/Cargo.toml' does not exist", + "path": "crates/hqgit-object/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '014-content-defined-chunking' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "013-object-store" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/chunk.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/merkle.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/testdata/chunk/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/tests/chunk.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/object.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "014-content-defined-chunking", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "2dbc500962954cc1cd74abaa2188205ab353733a41f0e873c3797f40055342cc" +} diff --git a/.derived/codebase-index/by-spec/015-verified-streaming.json b/.derived/codebase-index/by-spec/015-verified-streaming.json new file mode 100644 index 0000000..a8bf909 --- /dev/null +++ b/.derived/codebase-index/by-spec/015-verified-streaming.json @@ -0,0 +1,146 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/src/bao.rs' does not exist", + "path": "crates/hqgit-object/src/bao.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/src/fetch.rs' does not exist", + "path": "crates/hqgit-object/src/fetch.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/tests/bao.rs' does not exist", + "path": "crates/hqgit-object/tests/bao.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/src/lib.rs' does not exist", + "path": "crates/hqgit-object/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/src/store.rs' does not exist", + "path": "crates/hqgit-object/src/store.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/src/local.rs' does not exist", + "path": "crates/hqgit-object/src/local.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/src/memory.rs' does not exist", + "path": "crates/hqgit-object/src/memory.rs" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' file unit 'crates/hqgit-object/Cargo.toml' does not exist", + "path": "crates/hqgit-object/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '015-verified-streaming' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "014-content-defined-chunking" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/bao.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/fetch.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/tests/bao.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/local.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/memory.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/store.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "015-verified-streaming", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "7324fca5bc144bd6631ccbf8d3bc2fcc399e055de0c137b94f7eb8c6a0c748a5" +} diff --git a/.derived/codebase-index/by-spec/016-remote-object-backend.json b/.derived/codebase-index/by-spec/016-remote-object-backend.json new file mode 100644 index 0000000..88743eb --- /dev/null +++ b/.derived/codebase-index/by-spec/016-remote-object-backend.json @@ -0,0 +1,105 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '016-remote-object-backend' file unit 'crates/hqgit-object/src/s3.rs' does not exist", + "path": "crates/hqgit-object/src/s3.rs" + }, + { + "code": "W-001", + "message": "spec '016-remote-object-backend' file unit 'crates/hqgit-object/src/layered.rs' does not exist", + "path": "crates/hqgit-object/src/layered.rs" + }, + { + "code": "W-001", + "message": "spec '016-remote-object-backend' file unit 'crates/hqgit-object/tests/s3.rs' does not exist", + "path": "crates/hqgit-object/tests/s3.rs" + }, + { + "code": "W-001", + "message": "spec '016-remote-object-backend' file unit 'crates/hqgit-object/src/lib.rs' does not exist", + "path": "crates/hqgit-object/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '016-remote-object-backend' file unit 'crates/hqgit-object/Cargo.toml' does not exist", + "path": "crates/hqgit-object/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '016-remote-object-backend' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "013-object-store", + "015-verified-streaming" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/layered.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/s3.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/tests/s3.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "016-remote-object-backend", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "f23e74ee29e8c54409355723d81aca58e08ead951aa9fa74516ad7cb08468de3" +} diff --git a/.derived/codebase-index/by-spec/017-ledger-entry-dag.json b/.derived/codebase-index/by-spec/017-ledger-entry-dag.json new file mode 100644 index 0000000..7fa239d --- /dev/null +++ b/.derived/codebase-index/by-spec/017-ledger-entry-dag.json @@ -0,0 +1,146 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-ledger/Cargo.toml' does not exist", + "path": "crates/hqgit-ledger/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-ledger/src/lib.rs' does not exist", + "path": "crates/hqgit-ledger/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-ledger/src/entry.rs' does not exist", + "path": "crates/hqgit-ledger/src/entry.rs" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-ledger/src/dag.rs' does not exist", + "path": "crates/hqgit-ledger/src/dag.rs" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-ledger/src/verify.rs' does not exist", + "path": "crates/hqgit-ledger/src/verify.rs" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-ledger/tests/' does not exist", + "path": "crates/hqgit-ledger/tests/" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'fuzz/fuzz_targets/entry_hash_stable.rs' does not exist", + "path": "fuzz/fuzz_targets/entry_hash_stable.rs" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'crates/hqgit-types/testdata/vectors/' does not exist", + "path": "crates/hqgit-types/testdata/vectors/" + }, + { + "code": "W-001", + "message": "spec '017-ledger-entry-dag' file unit 'fuzz/Cargo.toml' does not exist", + "path": "fuzz/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "011-canonical-encoding", + "013-object-store" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/dag.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/entry.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/verify.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "fuzz/fuzz_targets/entry_hash_stable.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "fuzz/Cargo.toml" + } + } + ], + "specId": "017-ledger-entry-dag", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "47d61fdb820509f2f7313371dd1953f0b806f12331102018b9b8baaed2c3e9cf" +} diff --git a/.derived/codebase-index/by-spec/018-deterministic-total-order.json b/.derived/codebase-index/by-spec/018-deterministic-total-order.json new file mode 100644 index 0000000..9912cd9 --- /dev/null +++ b/.derived/codebase-index/by-spec/018-deterministic-total-order.json @@ -0,0 +1,75 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '018-deterministic-total-order' file unit 'crates/hqgit-ledger/src/order.rs' does not exist", + "path": "crates/hqgit-ledger/src/order.rs" + }, + { + "code": "W-001", + "message": "spec '018-deterministic-total-order' file unit 'crates/hqgit-ledger/src/clock.rs' does not exist", + "path": "crates/hqgit-ledger/src/clock.rs" + }, + { + "code": "W-001", + "message": "spec '018-deterministic-total-order' file unit 'crates/hqgit-ledger/tests/order.rs' does not exist", + "path": "crates/hqgit-ledger/tests/order.rs" + }, + { + "code": "W-001", + "message": "spec '018-deterministic-total-order' file unit 'crates/hqgit-ledger/src/lib.rs' does not exist", + "path": "crates/hqgit-ledger/src/lib.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "017-ledger-entry-dag" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/clock.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/order.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/tests/order.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + } + ], + "specId": "018-deterministic-total-order", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "ce1f9177e2b75debfbc01b2778a593d87909b6ae45973f5cf7d18e164a9c5bb2" +} diff --git a/.derived/codebase-index/by-spec/019-facts-and-derived-state.json b/.derived/codebase-index/by-spec/019-facts-and-derived-state.json new file mode 100644 index 0000000..d9c5cd1 --- /dev/null +++ b/.derived/codebase-index/by-spec/019-facts-and-derived-state.json @@ -0,0 +1,131 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/src/facts.rs' does not exist", + "path": "crates/hqgit-ledger/src/facts.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/src/derived.rs' does not exist", + "path": "crates/hqgit-ledger/src/derived.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/src/crdt/mod.rs' does not exist", + "path": "crates/hqgit-ledger/src/crdt/mod.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/src/crdt/lww.rs' does not exist", + "path": "crates/hqgit-ledger/src/crdt/lww.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/src/crdt/sequence.rs' does not exist", + "path": "crates/hqgit-ledger/src/crdt/sequence.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/tests/facts.rs' does not exist", + "path": "crates/hqgit-ledger/tests/facts.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/src/lib.rs' does not exist", + "path": "crates/hqgit-ledger/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '019-facts-and-derived-state' file unit 'crates/hqgit-ledger/Cargo.toml' does not exist", + "path": "crates/hqgit-ledger/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "018-deterministic-total-order" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/crdt/lww.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/crdt/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/crdt/sequence.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/derived.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/tests/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + } + ], + "specId": "019-facts-and-derived-state", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "7a4910c096daa009883558661b64570b0de53576efdea030fd4e27abce78c478" +} diff --git a/.derived/codebase-index/by-spec/020-commitments-and-tombstones.json b/.derived/codebase-index/by-spec/020-commitments-and-tombstones.json new file mode 100644 index 0000000..df7a93b --- /dev/null +++ b/.derived/codebase-index/by-spec/020-commitments-and-tombstones.json @@ -0,0 +1,174 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-ledger/src/commitment.rs' does not exist", + "path": "crates/hqgit-ledger/src/commitment.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-ledger/src/tombstone.rs' does not exist", + "path": "crates/hqgit-ledger/src/tombstone.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-object/src/encrypt.rs' does not exist", + "path": "crates/hqgit-object/src/encrypt.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-ledger/tests/erasure.rs' does not exist", + "path": "crates/hqgit-ledger/tests/erasure.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-ledger/src/lib.rs' does not exist", + "path": "crates/hqgit-ledger/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-object/src/lib.rs' does not exist", + "path": "crates/hqgit-object/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-object/src/store.rs' does not exist", + "path": "crates/hqgit-object/src/store.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-object/src/local.rs' does not exist", + "path": "crates/hqgit-object/src/local.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-object/src/memory.rs' does not exist", + "path": "crates/hqgit-object/src/memory.rs" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' file unit 'crates/hqgit-object/Cargo.toml' does not exist", + "path": "crates/hqgit-object/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '020-commitments-and-tombstones' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "019-facts-and-derived-state" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/commitment.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/tombstone.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/tests/erasure.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/encrypt.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/local.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/memory.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/store.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "020-commitments-and-tombstones", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "ad1bb4214de2d0bf662c0bfd8e9b4431390fbc37a536e4769527f0ed6d85dd3e" +} diff --git a/.derived/codebase-index/by-spec/021-local-repository.json b/.derived/codebase-index/by-spec/021-local-repository.json new file mode 100644 index 0000000..271c1ab --- /dev/null +++ b/.derived/codebase-index/by-spec/021-local-repository.json @@ -0,0 +1,118 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '021-local-repository' file unit 'crates/hqgit-ledger/src/repo.rs' does not exist", + "path": "crates/hqgit-ledger/src/repo.rs" + }, + { + "code": "W-001", + "message": "spec '021-local-repository' file unit 'crates/hqgit-ledger/src/store.rs' does not exist", + "path": "crates/hqgit-ledger/src/store.rs" + }, + { + "code": "W-001", + "message": "spec '021-local-repository' file unit 'crates/hqgit-ledger/src/namespace.rs' does not exist", + "path": "crates/hqgit-ledger/src/namespace.rs" + }, + { + "code": "W-001", + "message": "spec '021-local-repository' file unit 'crates/hqgit-ledger/tests/repo.rs' does not exist", + "path": "crates/hqgit-ledger/tests/repo.rs" + }, + { + "code": "W-001", + "message": "spec '021-local-repository' file unit 'crates/hqgit-ledger/src/lib.rs' does not exist", + "path": "crates/hqgit-ledger/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '021-local-repository' file unit 'crates/hqgit-ledger/Cargo.toml' does not exist", + "path": "crates/hqgit-ledger/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '021-local-repository' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "020-commitments-and-tombstones" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/namespace.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/repo.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/store.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/tests/repo.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "021-local-repository", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "a5b8669c402f7bb86b9df901d50c016100982dbee4c0f1f93861e11c6c102990" +} diff --git a/.derived/codebase-index/by-spec/023-domain-fact-vocabulary.json b/.derived/codebase-index/by-spec/023-domain-fact-vocabulary.json new file mode 100644 index 0000000..d81e7ec --- /dev/null +++ b/.derived/codebase-index/by-spec/023-domain-fact-vocabulary.json @@ -0,0 +1,117 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-domain/Cargo.toml' does not exist", + "path": "crates/hqgit-domain/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + }, + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-domain/src/validate.rs' does not exist", + "path": "crates/hqgit-domain/src/validate.rs" + }, + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-domain/src/ids.rs' does not exist", + "path": "crates/hqgit-domain/src/ids.rs" + }, + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-domain/tests/' does not exist", + "path": "crates/hqgit-domain/tests/" + }, + { + "code": "W-001", + "message": "spec '023-domain-fact-vocabulary' file unit 'crates/hqgit-types/testdata/vectors/' does not exist", + "path": "crates/hqgit-types/testdata/vectors/" + } + ] + }, + "mapping": { + "dependsOn": [ + "019-facts-and-derived-state" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/ids.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/validate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + } + ], + "specId": "023-domain-fact-vocabulary", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "57e9bb7e97d1ba4543144cb2e56a06a13983cfbf3ee437432ae8de7c080dd169" +} diff --git a/.derived/codebase-index/by-spec/024-change-and-revision.json b/.derived/codebase-index/by-spec/024-change-and-revision.json new file mode 100644 index 0000000..6c48fa6 --- /dev/null +++ b/.derived/codebase-index/by-spec/024-change-and-revision.json @@ -0,0 +1,89 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '024-change-and-revision' file unit 'crates/hqgit-domain/src/change.rs' does not exist", + "path": "crates/hqgit-domain/src/change.rs" + }, + { + "code": "W-001", + "message": "spec '024-change-and-revision' file unit 'crates/hqgit-domain/src/revision.rs' does not exist", + "path": "crates/hqgit-domain/src/revision.rs" + }, + { + "code": "W-001", + "message": "spec '024-change-and-revision' file unit 'crates/hqgit-domain/tests/change.rs' does not exist", + "path": "crates/hqgit-domain/tests/change.rs" + }, + { + "code": "W-001", + "message": "spec '024-change-and-revision' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '024-change-and-revision' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "023-domain-fact-vocabulary" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/change.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/revision.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/change.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + } + ], + "specId": "024-change-and-revision", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "8ed69c695f3beb74d758c50810d7d9e450cc884c8ebaa4868a967452c3134728" +} diff --git a/.derived/codebase-index/by-spec/025-semantic-anchors.json b/.derived/codebase-index/by-spec/025-semantic-anchors.json new file mode 100644 index 0000000..065ae9e --- /dev/null +++ b/.derived/codebase-index/by-spec/025-semantic-anchors.json @@ -0,0 +1,160 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/src/anchor.rs' does not exist", + "path": "crates/hqgit-domain/src/anchor.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/src/syntax/mod.rs' does not exist", + "path": "crates/hqgit-domain/src/syntax/mod.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/src/syntax/rust.rs' does not exist", + "path": "crates/hqgit-domain/src/syntax/rust.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/src/syntax/typescript.rs' does not exist", + "path": "crates/hqgit-domain/src/syntax/typescript.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/src/syntax/text.rs' does not exist", + "path": "crates/hqgit-domain/src/syntax/text.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/tests/anchor.rs' does not exist", + "path": "crates/hqgit-domain/tests/anchor.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/testdata/anchors/' does not exist", + "path": "crates/hqgit-domain/testdata/anchors/" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' file unit 'crates/hqgit-domain/Cargo.toml' does not exist", + "path": "crates/hqgit-domain/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '025-semantic-anchors' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "024-change-and-revision" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/anchor.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/rust.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/text.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/typescript.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/testdata/anchors/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/anchor.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "025-semantic-anchors", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "ec51f3e40f70185ea0d0c0454637cfabd0a4b9d2fca07ede6a0784534b3bc3af" +} diff --git a/.derived/codebase-index/by-spec/026-review-threads.json b/.derived/codebase-index/by-spec/026-review-threads.json new file mode 100644 index 0000000..27162f8 --- /dev/null +++ b/.derived/codebase-index/by-spec/026-review-threads.json @@ -0,0 +1,75 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '026-review-threads' file unit 'crates/hqgit-domain/src/review.rs' does not exist", + "path": "crates/hqgit-domain/src/review.rs" + }, + { + "code": "W-001", + "message": "spec '026-review-threads' file unit 'crates/hqgit-domain/tests/review.rs' does not exist", + "path": "crates/hqgit-domain/tests/review.rs" + }, + { + "code": "W-001", + "message": "spec '026-review-threads' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '026-review-threads' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "025-semantic-anchors" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/review.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/review.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + } + ], + "specId": "026-review-threads", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "8728314cfe9cf7cae7b2c16b604976cafdf388f784cecfb0e921cf06cf42ca30" +} diff --git a/.derived/codebase-index/by-spec/027-attestation-primitive.json b/.derived/codebase-index/by-spec/027-attestation-primitive.json new file mode 100644 index 0000000..574d1f3 --- /dev/null +++ b/.derived/codebase-index/by-spec/027-attestation-primitive.json @@ -0,0 +1,117 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-domain/src/attestation.rs' does not exist", + "path": "crates/hqgit-domain/src/attestation.rs" + }, + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + }, + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-domain/tests/attestation.rs' does not exist", + "path": "crates/hqgit-domain/tests/attestation.rs" + }, + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-domain/testdata/attestations/' does not exist", + "path": "crates/hqgit-domain/testdata/attestations/" + }, + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + }, + { + "code": "W-001", + "message": "spec '027-attestation-primitive' file unit 'crates/hqgit-types/testdata/vectors/' does not exist", + "path": "crates/hqgit-types/testdata/vectors/" + } + ] + }, + "mapping": { + "dependsOn": [ + "023-domain-fact-vocabulary" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/attestation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/testdata/attestations/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/attestation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + } + ], + "specId": "027-attestation-primitive", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "0f4c93f15e6badf69e77d8de55ecce523eee1465c474ab45a073110b3ab04ab5" +} diff --git a/.derived/codebase-index/by-spec/028-issues-and-derived-state.json b/.derived/codebase-index/by-spec/028-issues-and-derived-state.json new file mode 100644 index 0000000..3a4e4eb --- /dev/null +++ b/.derived/codebase-index/by-spec/028-issues-and-derived-state.json @@ -0,0 +1,75 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '028-issues-and-derived-state' file unit 'crates/hqgit-domain/src/issue.rs' does not exist", + "path": "crates/hqgit-domain/src/issue.rs" + }, + { + "code": "W-001", + "message": "spec '028-issues-and-derived-state' file unit 'crates/hqgit-domain/tests/issue.rs' does not exist", + "path": "crates/hqgit-domain/tests/issue.rs" + }, + { + "code": "W-001", + "message": "spec '028-issues-and-derived-state' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '028-issues-and-derived-state' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "023-domain-fact-vocabulary" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/issue.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/issue.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + } + ], + "specId": "028-issues-and-derived-state", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "b130bf150fbf8d40b3e62e93a212fa247c6d75eef4f792650f6a5e51f8330d7e" +} diff --git a/.derived/codebase-index/by-spec/031-git-object-bridge.json b/.derived/codebase-index/by-spec/031-git-object-bridge.json new file mode 100644 index 0000000..cc0d9d3 --- /dev/null +++ b/.derived/codebase-index/by-spec/031-git-object-bridge.json @@ -0,0 +1,160 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/Cargo.toml' does not exist", + "path": "crates/hqgit-git/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/src/lib.rs' does not exist", + "path": "crates/hqgit-git/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/src/import.rs' does not exist", + "path": "crates/hqgit-git/src/import.rs" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/src/export.rs' does not exist", + "path": "crates/hqgit-git/src/export.rs" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/src/mapping.rs' does not exist", + "path": "crates/hqgit-git/src/mapping.rs" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/src/worktree.rs' does not exist", + "path": "crates/hqgit-git/src/worktree.rs" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/src/mode.rs' does not exist", + "path": "crates/hqgit-git/src/mode.rs" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/tests/' does not exist", + "path": "crates/hqgit-git/tests/" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' file unit 'crates/hqgit-git/testdata/' does not exist", + "path": "crates/hqgit-git/testdata/" + }, + { + "code": "W-001", + "message": "spec '031-git-object-bridge' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "014-content-defined-chunking" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/src/export.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/src/import.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/src/mapping.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/src/mode.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/src/worktree.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/testdata/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-git/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "031-git-object-bridge", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "00b56ee70ef380410984096a40ed9f4da6c31937db96221b4fdcd77b171e599a" +} diff --git a/.derived/codebase-index/by-spec/032-cli-skeleton.json b/.derived/codebase-index/by-spec/032-cli-skeleton.json new file mode 100644 index 0000000..b8642f1 --- /dev/null +++ b/.derived/codebase-index/by-spec/032-cli-skeleton.json @@ -0,0 +1,160 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/cmd_init.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_init.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/cmd_status.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_status.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/cmd_log.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_log.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/config.rs' does not exist", + "path": "crates/hqgit-cli/src/config.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/src/output.rs' does not exist", + "path": "crates/hqgit-cli/src/output.rs" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' file unit 'crates/hqgit-cli/tests/' does not exist", + "path": "crates/hqgit-cli/tests/" + }, + { + "code": "W-001", + "message": "spec '032-cli-skeleton' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "021-local-repository" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_init.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_log.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_status.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/config.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/output.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "032-cli-skeleton", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "57ad0491598c43095af3e8e016ab22e145d548a72de4eb7bf826613c38db8029" +} diff --git a/.derived/codebase-index/by-spec/033-cli-offline-review.json b/.derived/codebase-index/by-spec/033-cli-offline-review.json new file mode 100644 index 0000000..8383432 --- /dev/null +++ b/.derived/codebase-index/by-spec/033-cli-offline-review.json @@ -0,0 +1,120 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/src/cmd_change.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_change.rs" + }, + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/src/cmd_review.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_review.rs" + }, + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/tests/review.rs' does not exist", + "path": "crates/hqgit-cli/tests/review.rs" + }, + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/testdata/review/' does not exist", + "path": "crates/hqgit-cli/testdata/review/" + }, + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '033-cli-offline-review' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "032-cli-skeleton", + "031-git-object-bridge", + "026-review-threads", + "027-attestation-primitive" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_change.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_review.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/testdata/review/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/review.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + } + ], + "specId": "033-cli-offline-review", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "0f72d8c3b24e1ce6db9e475cffb7033e166d33149678287b6db2cd43ff9f1f84" +} diff --git a/.derived/codebase-index/by-spec/034-cli-attest-and-verify.json b/.derived/codebase-index/by-spec/034-cli-attest-and-verify.json new file mode 100644 index 0000000..b5d69a6 --- /dev/null +++ b/.derived/codebase-index/by-spec/034-cli-attest-and-verify.json @@ -0,0 +1,89 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '034-cli-attest-and-verify' file unit 'crates/hqgit-cli/src/cmd_attest.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_attest.rs" + }, + { + "code": "W-001", + "message": "spec '034-cli-attest-and-verify' file unit 'crates/hqgit-cli/src/cmd_verify.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_verify.rs" + }, + { + "code": "W-001", + "message": "spec '034-cli-attest-and-verify' file unit 'crates/hqgit-cli/tests/verify.rs' does not exist", + "path": "crates/hqgit-cli/tests/verify.rs" + }, + { + "code": "W-001", + "message": "spec '034-cli-attest-and-verify' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '034-cli-attest-and-verify' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "033-cli-offline-review" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_attest.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_verify.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/verify.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + } + ], + "specId": "034-cli-attest-and-verify", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "d4ef93ac7dd50107f83b686970f3b0c68dc7f97bd6cab78d028d86a48bf3ff01" +} diff --git a/.derived/codebase-index/by-spec/040-github-mirror-import.json b/.derived/codebase-index/by-spec/040-github-mirror-import.json new file mode 100644 index 0000000..3ea42f3 --- /dev/null +++ b/.derived/codebase-index/by-spec/040-github-mirror-import.json @@ -0,0 +1,191 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/Cargo.toml' does not exist", + "path": "crates/hqgit-mirror/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/lib.rs' does not exist", + "path": "crates/hqgit-mirror/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/github/mod.rs' does not exist", + "path": "crates/hqgit-mirror/src/github/mod.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/github/client.rs' does not exist", + "path": "crates/hqgit-mirror/src/github/client.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/github/import.rs' does not exist", + "path": "crates/hqgit-mirror/src/github/import.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/github/model.rs' does not exist", + "path": "crates/hqgit-mirror/src/github/model.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/quarantine.rs' does not exist", + "path": "crates/hqgit-mirror/src/quarantine.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/src/idmap.rs' does not exist", + "path": "crates/hqgit-mirror/src/idmap.rs" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/tests/' does not exist", + "path": "crates/hqgit-mirror/tests/" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-mirror/testdata/github/' does not exist", + "path": "crates/hqgit-mirror/testdata/github/" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '040-github-mirror-import' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "028-issues-and-derived-state", + "026-review-threads", + "027-attestation-primitive", + "031-git-object-bridge" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/client.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/import.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/model.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/idmap.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/quarantine.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/testdata/github/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "040-github-mirror-import", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "68c3fa4c2b0751b27fbee50d467ee5b1627a70d7cc3972696fe3f24e632b8d11" +} diff --git a/.derived/codebase-index/by-spec/041-github-mirror-export.json b/.derived/codebase-index/by-spec/041-github-mirror-export.json new file mode 100644 index 0000000..afa0ff4 --- /dev/null +++ b/.derived/codebase-index/by-spec/041-github-mirror-export.json @@ -0,0 +1,89 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '041-github-mirror-export' file unit 'crates/hqgit-mirror/src/github/export.rs' does not exist", + "path": "crates/hqgit-mirror/src/github/export.rs" + }, + { + "code": "W-001", + "message": "spec '041-github-mirror-export' file unit 'crates/hqgit-mirror/src/reconcile.rs' does not exist", + "path": "crates/hqgit-mirror/src/reconcile.rs" + }, + { + "code": "W-001", + "message": "spec '041-github-mirror-export' file unit 'crates/hqgit-mirror/tests/export.rs' does not exist", + "path": "crates/hqgit-mirror/tests/export.rs" + }, + { + "code": "W-001", + "message": "spec '041-github-mirror-export' file unit 'crates/hqgit-mirror/src/lib.rs' does not exist", + "path": "crates/hqgit-mirror/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '041-github-mirror-export' file unit 'crates/hqgit-mirror/src/github/client.rs' does not exist", + "path": "crates/hqgit-mirror/src/github/client.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "040-github-mirror-import" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/export.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/reconcile.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/tests/export.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/client.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/lib.rs" + } + } + ], + "specId": "041-github-mirror-export", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "b59838bdac10ffca3c1c9f13f1d791769cefda68975ac0d8a9795af13f2c0832" +} diff --git a/.derived/codebase-index/by-spec/042-mirror-sync-command.json b/.derived/codebase-index/by-spec/042-mirror-sync-command.json new file mode 100644 index 0000000..650b8f1 --- /dev/null +++ b/.derived/codebase-index/by-spec/042-mirror-sync-command.json @@ -0,0 +1,118 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-mirror/src/sync.rs' does not exist", + "path": "crates/hqgit-mirror/src/sync.rs" + }, + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-cli/src/cmd_mirror.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_mirror.rs" + }, + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-cli/tests/mirror.rs' does not exist", + "path": "crates/hqgit-cli/tests/mirror.rs" + }, + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-mirror/src/lib.rs' does not exist", + "path": "crates/hqgit-mirror/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '042-mirror-sync-command' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "041-github-mirror-export", + "032-cli-skeleton" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_mirror.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/mirror.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/sync.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/lib.rs" + } + } + ], + "specId": "042-mirror-sync-command", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "8ff990436711404e64738ba3dde5d251c052451193a9f9fae8759c830d5918a2" +} diff --git a/.derived/codebase-index/by-spec/050-stacked-changes.json b/.derived/codebase-index/by-spec/050-stacked-changes.json new file mode 100644 index 0000000..ada5c8f --- /dev/null +++ b/.derived/codebase-index/by-spec/050-stacked-changes.json @@ -0,0 +1,103 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '050-stacked-changes' file unit 'crates/hqgit-domain/src/stack.rs' does not exist", + "path": "crates/hqgit-domain/src/stack.rs" + }, + { + "code": "W-001", + "message": "spec '050-stacked-changes' file unit 'crates/hqgit-domain/src/interdiff.rs' does not exist", + "path": "crates/hqgit-domain/src/interdiff.rs" + }, + { + "code": "W-001", + "message": "spec '050-stacked-changes' file unit 'crates/hqgit-domain/tests/stack.rs' does not exist", + "path": "crates/hqgit-domain/tests/stack.rs" + }, + { + "code": "W-001", + "message": "spec '050-stacked-changes' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '050-stacked-changes' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + }, + { + "code": "W-001", + "message": "spec '050-stacked-changes' file unit 'crates/hqgit-types/testdata/vectors/' does not exist", + "path": "crates/hqgit-types/testdata/vectors/" + } + ] + }, + "mapping": { + "dependsOn": [ + "024-change-and-revision" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/interdiff.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/stack.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/stack.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + } + ], + "specId": "050-stacked-changes", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "0e1f55103172f7f61438cdb9351911c4f7bea29b4dd860d27a98abbae6c51bcf" +} diff --git a/.derived/codebase-index/by-spec/051-semantic-deltas.json b/.derived/codebase-index/by-spec/051-semantic-deltas.json new file mode 100644 index 0000000..8f2af13 --- /dev/null +++ b/.derived/codebase-index/by-spec/051-semantic-deltas.json @@ -0,0 +1,162 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/src/delta/mod.rs' does not exist", + "path": "crates/hqgit-domain/src/delta/mod.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/src/delta/api_surface.rs' does not exist", + "path": "crates/hqgit-domain/src/delta/api_surface.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/src/delta/dependencies.rs' does not exist", + "path": "crates/hqgit-domain/src/delta/dependencies.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/src/delta/capabilities.rs' does not exist", + "path": "crates/hqgit-domain/src/delta/capabilities.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/tests/delta.rs' does not exist", + "path": "crates/hqgit-domain/tests/delta.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/testdata/deltas/' does not exist", + "path": "crates/hqgit-domain/testdata/deltas/" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' file unit 'crates/hqgit-domain/Cargo.toml' does not exist", + "path": "crates/hqgit-domain/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '051-semantic-deltas' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "027-attestation-primitive", + "050-stacked-changes", + "025-semantic-anchors" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/api_surface.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/capabilities.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/dependencies.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/testdata/deltas/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/delta.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "051-semantic-deltas", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "31c984f9d0a464bdf956c52b4ea775c53dd18bb5fc51427e7dd29d7918b1329c" +} diff --git a/.derived/codebase-index/by-spec/052-semantic-conflicts.json b/.derived/codebase-index/by-spec/052-semantic-conflicts.json new file mode 100644 index 0000000..bea6487 --- /dev/null +++ b/.derived/codebase-index/by-spec/052-semantic-conflicts.json @@ -0,0 +1,104 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '052-semantic-conflicts' file unit 'crates/hqgit-domain/src/conflict.rs' does not exist", + "path": "crates/hqgit-domain/src/conflict.rs" + }, + { + "code": "W-001", + "message": "spec '052-semantic-conflicts' file unit 'crates/hqgit-domain/tests/conflict.rs' does not exist", + "path": "crates/hqgit-domain/tests/conflict.rs" + }, + { + "code": "W-001", + "message": "spec '052-semantic-conflicts' file unit 'crates/hqgit-domain/testdata/conflicts/' does not exist", + "path": "crates/hqgit-domain/testdata/conflicts/" + }, + { + "code": "W-001", + "message": "spec '052-semantic-conflicts' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '052-semantic-conflicts' file unit 'crates/hqgit-domain/Cargo.toml' does not exist", + "path": "crates/hqgit-domain/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '052-semantic-conflicts' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "051-semantic-deltas" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/conflict.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/testdata/conflicts/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/conflict.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "052-semantic-conflicts", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "60df6b2a01657fb55bc3e7e43106c4be4fb2632dd94b3c4147d7c122d90c871e" +} diff --git a/.derived/codebase-index/by-spec/060-identity-and-key-rotation.json b/.derived/codebase-index/by-spec/060-identity-and-key-rotation.json new file mode 100644 index 0000000..e7ce997 --- /dev/null +++ b/.derived/codebase-index/by-spec/060-identity-and-key-rotation.json @@ -0,0 +1,117 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/Cargo.toml' does not exist", + "path": "crates/hqgit-trust/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/src/lib.rs' does not exist", + "path": "crates/hqgit-trust/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/src/identity.rs' does not exist", + "path": "crates/hqgit-trust/src/identity.rs" + }, + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/src/rotation.rs' does not exist", + "path": "crates/hqgit-trust/src/rotation.rs" + }, + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/src/resolver.rs' does not exist", + "path": "crates/hqgit-trust/src/resolver.rs" + }, + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/tests/' does not exist", + "path": "crates/hqgit-trust/tests/" + }, + { + "code": "W-001", + "message": "spec '060-identity-and-key-rotation' file unit 'crates/hqgit-trust/testdata/identity/' does not exist", + "path": "crates/hqgit-trust/testdata/identity/" + } + ] + }, + "mapping": { + "dependsOn": [ + "019-facts-and-derived-state" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/identity.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/resolver.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/rotation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/testdata/identity/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/tests/" + } + } + ], + "specId": "060-identity-and-key-rotation", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "25dbc5b56ce367ca4401c5eb35c5fff79ad2b40c51baa6b17a967015165ddb1b" +} diff --git a/.derived/codebase-index/by-spec/061-oidc-login.json b/.derived/codebase-index/by-spec/061-oidc-login.json new file mode 100644 index 0000000..c5f85f7 --- /dev/null +++ b/.derived/codebase-index/by-spec/061-oidc-login.json @@ -0,0 +1,118 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '061-oidc-login' file unit 'crates/hqgit-trust/src/oidc.rs' does not exist", + "path": "crates/hqgit-trust/src/oidc.rs" + }, + { + "code": "W-001", + "message": "spec '061-oidc-login' file unit 'crates/hqgit-trust/src/session.rs' does not exist", + "path": "crates/hqgit-trust/src/session.rs" + }, + { + "code": "W-001", + "message": "spec '061-oidc-login' file unit 'crates/hqgit-trust/tests/oidc.rs' does not exist", + "path": "crates/hqgit-trust/tests/oidc.rs" + }, + { + "code": "W-001", + "message": "spec '061-oidc-login' file unit 'crates/hqgit-trust/testdata/oidc/' does not exist", + "path": "crates/hqgit-trust/testdata/oidc/" + }, + { + "code": "W-001", + "message": "spec '061-oidc-login' file unit 'crates/hqgit-trust/src/lib.rs' does not exist", + "path": "crates/hqgit-trust/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '061-oidc-login' file unit 'crates/hqgit-trust/Cargo.toml' does not exist", + "path": "crates/hqgit-trust/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '061-oidc-login' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "060-identity-and-key-rotation" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/oidc.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/session.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/testdata/oidc/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/tests/oidc.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "061-oidc-login", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "d7def29468ca44840a50de2c722f992bf3c3fc8007eb69b36bcf78f3f3df66da" +} diff --git a/.derived/codebase-index/by-spec/062-transparency-log.json b/.derived/codebase-index/by-spec/062-transparency-log.json new file mode 100644 index 0000000..6f3fba3 --- /dev/null +++ b/.derived/codebase-index/by-spec/062-transparency-log.json @@ -0,0 +1,117 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/src/tlog/mod.rs' does not exist", + "path": "crates/hqgit-trust/src/tlog/mod.rs" + }, + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/src/tlog/merkle.rs' does not exist", + "path": "crates/hqgit-trust/src/tlog/merkle.rs" + }, + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/src/tlog/proofs.rs' does not exist", + "path": "crates/hqgit-trust/src/tlog/proofs.rs" + }, + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/src/tlog/client.rs' does not exist", + "path": "crates/hqgit-trust/src/tlog/client.rs" + }, + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/tests/tlog.rs' does not exist", + "path": "crates/hqgit-trust/tests/tlog.rs" + }, + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/testdata/tlog/' does not exist", + "path": "crates/hqgit-trust/testdata/tlog/" + }, + { + "code": "W-001", + "message": "spec '062-transparency-log' file unit 'crates/hqgit-trust/src/lib.rs' does not exist", + "path": "crates/hqgit-trust/src/lib.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "060-identity-and-key-rotation" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/client.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/merkle.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/proofs.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/testdata/tlog/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/tests/tlog.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + } + ], + "specId": "062-transparency-log", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "6c34470894ad901dd102c3a60e05cd87ce481a8b2d60f736a0592c0326cf2a8b" +} diff --git a/.derived/codebase-index/by-spec/063-keyless-signing.json b/.derived/codebase-index/by-spec/063-keyless-signing.json new file mode 100644 index 0000000..6932bfb --- /dev/null +++ b/.derived/codebase-index/by-spec/063-keyless-signing.json @@ -0,0 +1,119 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '063-keyless-signing' file unit 'crates/hqgit-trust/src/keyless.rs' does not exist", + "path": "crates/hqgit-trust/src/keyless.rs" + }, + { + "code": "W-001", + "message": "spec '063-keyless-signing' file unit 'crates/hqgit-trust/src/bundle.rs' does not exist", + "path": "crates/hqgit-trust/src/bundle.rs" + }, + { + "code": "W-001", + "message": "spec '063-keyless-signing' file unit 'crates/hqgit-trust/tests/keyless.rs' does not exist", + "path": "crates/hqgit-trust/tests/keyless.rs" + }, + { + "code": "W-001", + "message": "spec '063-keyless-signing' file unit 'crates/hqgit-trust/testdata/keyless/' does not exist", + "path": "crates/hqgit-trust/testdata/keyless/" + }, + { + "code": "W-001", + "message": "spec '063-keyless-signing' file unit 'crates/hqgit-trust/src/lib.rs' does not exist", + "path": "crates/hqgit-trust/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '063-keyless-signing' file unit 'crates/hqgit-trust/Cargo.toml' does not exist", + "path": "crates/hqgit-trust/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '063-keyless-signing' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "061-oidc-login", + "062-transparency-log" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/bundle.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/keyless.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/testdata/keyless/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/tests/keyless.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "063-keyless-signing", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "10823f639732b8d5510d611fdb4edc8704a1fbb60793dcf0aac7ad0c2bf5ba15" +} diff --git a/.derived/codebase-index/by-spec/064-attestation-verification.json b/.derived/codebase-index/by-spec/064-attestation-verification.json new file mode 100644 index 0000000..084559f --- /dev/null +++ b/.derived/codebase-index/by-spec/064-attestation-verification.json @@ -0,0 +1,119 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/src/verify.rs' does not exist", + "path": "crates/hqgit-trust/src/verify.rs" + }, + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/src/verified.rs' does not exist", + "path": "crates/hqgit-trust/src/verified.rs" + }, + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/src/verify_policy.rs' does not exist", + "path": "crates/hqgit-trust/src/verify_policy.rs" + }, + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/tests/verify.rs' does not exist", + "path": "crates/hqgit-trust/tests/verify.rs" + }, + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/testdata/verify/' does not exist", + "path": "crates/hqgit-trust/testdata/verify/" + }, + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/src/lib.rs' does not exist", + "path": "crates/hqgit-trust/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '064-attestation-verification' file unit 'crates/hqgit-trust/Cargo.toml' does not exist", + "path": "crates/hqgit-trust/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "060-identity-and-key-rotation", + "062-transparency-log", + "027-attestation-primitive" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/verified.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/verify.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/verify_policy.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/testdata/verify/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/tests/verify.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + } + ], + "specId": "064-attestation-verification", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "1114ff5b0a11889ba2a9f4727a50a583d70927bc6a8bb8baa31f5b0158c9d655" +} diff --git a/.derived/codebase-index/by-spec/065-policy-engine.json b/.derived/codebase-index/by-spec/065-policy-engine.json new file mode 100644 index 0000000..47ac35d --- /dev/null +++ b/.derived/codebase-index/by-spec/065-policy-engine.json @@ -0,0 +1,147 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/Cargo.toml' does not exist", + "path": "crates/hqgit-policy/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/src/lib.rs' does not exist", + "path": "crates/hqgit-policy/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/src/engine.rs' does not exist", + "path": "crates/hqgit-policy/src/engine.rs" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/src/abi.rs' does not exist", + "path": "crates/hqgit-policy/src/abi.rs" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/src/module.rs' does not exist", + "path": "crates/hqgit-policy/src/module.rs" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/src/input.rs' does not exist", + "path": "crates/hqgit-policy/src/input.rs" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/tests/' does not exist", + "path": "crates/hqgit-policy/tests/" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' file unit 'crates/hqgit-policy/testdata/policies/' does not exist", + "path": "crates/hqgit-policy/testdata/policies/" + }, + { + "code": "W-001", + "message": "spec '065-policy-engine' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "027-attestation-primitive", + "024-change-and-revision" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/abi.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/engine.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/input.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/module.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/testdata/policies/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "065-policy-engine", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "3957279f6458aece60fb5620891eca47b07fbb4bbdb3888fe7dfb2bfee0aa69e" +} diff --git a/.derived/codebase-index/by-spec/066-policy-sdk.json b/.derived/codebase-index/by-spec/066-policy-sdk.json new file mode 100644 index 0000000..939de29 --- /dev/null +++ b/.derived/codebase-index/by-spec/066-policy-sdk.json @@ -0,0 +1,187 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/Cargo.toml' does not exist", + "path": "crates/hqgit-policy-sdk/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/src/lib.rs' does not exist", + "path": "crates/hqgit-policy-sdk/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/src/prelude.rs' does not exist", + "path": "crates/hqgit-policy-sdk/src/prelude.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/src/evidence.rs' does not exist", + "path": "crates/hqgit-policy-sdk/src/evidence.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/src/export.rs' does not exist", + "path": "crates/hqgit-policy-sdk/src/export.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/src/testing.rs' does not exist", + "path": "crates/hqgit-policy-sdk/src/testing.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/examples/allow_all.rs' does not exist", + "path": "crates/hqgit-policy-sdk/examples/allow_all.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/examples/two_approvals.rs' does not exist", + "path": "crates/hqgit-policy-sdk/examples/two_approvals.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/examples/provenance_required.rs' does not exist", + "path": "crates/hqgit-policy-sdk/examples/provenance_required.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/examples/no_self_approval.rs' does not exist", + "path": "crates/hqgit-policy-sdk/examples/no_self_approval.rs" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy-sdk/tests/' does not exist", + "path": "crates/hqgit-policy-sdk/tests/" + }, + { + "code": "W-001", + "message": "spec '066-policy-sdk' file unit 'crates/hqgit-policy/testdata/policies/' does not exist", + "path": "crates/hqgit-policy/testdata/policies/" + } + ] + }, + "mapping": { + "dependsOn": [ + "065-policy-engine" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/allow_all.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/no_self_approval.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/provenance_required.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/two_approvals.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/evidence.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/export.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/prelude.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/testing.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/testdata/policies/" + } + } + ], + "specId": "066-policy-sdk", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "1b8b611f354a0eaac1500941fe4a8545e69ade42dd4d768a6610d0d53916008c" +} diff --git a/.derived/codebase-index/by-spec/067-policy-evaluation-attestation.json b/.derived/codebase-index/by-spec/067-policy-evaluation-attestation.json new file mode 100644 index 0000000..e3131cb --- /dev/null +++ b/.derived/codebase-index/by-spec/067-policy-evaluation-attestation.json @@ -0,0 +1,146 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-policy/src/evaluate.rs' does not exist", + "path": "crates/hqgit-policy/src/evaluate.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-policy/src/replay.rs' does not exist", + "path": "crates/hqgit-policy/src/replay.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-cli/src/cmd_policy.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_policy.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-cli/tests/policy.rs' does not exist", + "path": "crates/hqgit-cli/tests/policy.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-policy/src/lib.rs' does not exist", + "path": "crates/hqgit-policy/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '067-policy-evaluation-attestation' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "065-policy-engine", + "034-cli-attest-and-verify" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_policy.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/policy.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/evaluate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/replay.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/lib.rs" + } + } + ], + "specId": "067-policy-evaluation-attestation", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "c722f4905298238bc3a85c927c7f601db9da0976b220d9e4a252352b628daf7f" +} diff --git a/.derived/codebase-index/by-spec/068-policy-in-repo.json b/.derived/codebase-index/by-spec/068-policy-in-repo.json new file mode 100644 index 0000000..89b3fe2 --- /dev/null +++ b/.derived/codebase-index/by-spec/068-policy-in-repo.json @@ -0,0 +1,118 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-policy/src/pin.rs' does not exist", + "path": "crates/hqgit-policy/src/pin.rs" + }, + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-policy/src/scope.rs' does not exist", + "path": "crates/hqgit-policy/src/scope.rs" + }, + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-policy/tests/pin.rs' does not exist", + "path": "crates/hqgit-policy/tests/pin.rs" + }, + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-policy/src/lib.rs' does not exist", + "path": "crates/hqgit-policy/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + }, + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-ledger/src/tombstone.rs' does not exist", + "path": "crates/hqgit-ledger/src/tombstone.rs" + }, + { + "code": "W-001", + "message": "spec '068-policy-in-repo' file unit 'crates/hqgit-cli/src/cmd_policy.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_policy.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "067-policy-evaluation-attestation", + "021-local-repository" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/pin.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/scope.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/tests/pin.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_policy.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/tombstone.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/lib.rs" + } + } + ], + "specId": "068-policy-in-repo", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "6fae1c2ca71186504f080839d5e3f013b4607ad989f0bff1482b1fd677536557" +} diff --git a/.derived/codebase-index/by-spec/070-reapi-types.json b/.derived/codebase-index/by-spec/070-reapi-types.json new file mode 100644 index 0000000..8532ea9 --- /dev/null +++ b/.derived/codebase-index/by-spec/070-reapi-types.json @@ -0,0 +1,188 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/Cargo.toml' does not exist", + "path": "crates/hqgit-eval/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/build.rs' does not exist", + "path": "crates/hqgit-eval/build.rs" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/proto/build/bazel/remote/execution/v2/remote_execution.proto' does not exist", + "path": "crates/hqgit-eval/proto/build/bazel/remote/execution/v2/remote_execution.proto" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/proto/build/bazel/semver/semver.proto' does not exist", + "path": "crates/hqgit-eval/proto/build/bazel/semver/semver.proto" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/proto/google/' does not exist", + "path": "crates/hqgit-eval/proto/google/" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/src/digest.rs' does not exist", + "path": "crates/hqgit-eval/src/digest.rs" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/src/action.rs' does not exist", + "path": "crates/hqgit-eval/src/action.rs" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/src/cas_adapter.rs' does not exist", + "path": "crates/hqgit-eval/src/cas_adapter.rs" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/tests/' does not exist", + "path": "crates/hqgit-eval/tests/" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' file unit 'crates/hqgit-eval/testdata/reapi/' does not exist", + "path": "crates/hqgit-eval/testdata/reapi/" + }, + { + "code": "W-001", + "message": "spec '070-reapi-types' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "013-object-store" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/build.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/proto/build/bazel/remote/execution/v2/remote_execution.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/proto/build/bazel/semver/semver.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/proto/google/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/action.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/cas_adapter.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/digest.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/testdata/reapi/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "070-reapi-types", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "2bd9ac4d4e8d8d9ed5bdb17a9daf81809e548b7057dd4352f0e861b149cf7bf2" +} diff --git a/.derived/codebase-index/by-spec/071-action-cache.json b/.derived/codebase-index/by-spec/071-action-cache.json new file mode 100644 index 0000000..eb17382 --- /dev/null +++ b/.derived/codebase-index/by-spec/071-action-cache.json @@ -0,0 +1,104 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '071-action-cache' file unit 'crates/hqgit-eval/src/cache.rs' does not exist", + "path": "crates/hqgit-eval/src/cache.rs" + }, + { + "code": "W-001", + "message": "spec '071-action-cache' file unit 'crates/hqgit-eval/src/executor_identity.rs' does not exist", + "path": "crates/hqgit-eval/src/executor_identity.rs" + }, + { + "code": "W-001", + "message": "spec '071-action-cache' file unit 'crates/hqgit-eval/tests/cache.rs' does not exist", + "path": "crates/hqgit-eval/tests/cache.rs" + }, + { + "code": "W-001", + "message": "spec '071-action-cache' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '071-action-cache' file unit 'crates/hqgit-eval/Cargo.toml' does not exist", + "path": "crates/hqgit-eval/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '071-action-cache' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "070-reapi-types", + "064-attestation-verification" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/cache.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/executor_identity.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/tests/cache.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + } + ], + "specId": "071-action-cache", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "1780e22306bd682680eaf9fdca0ceba627e076b723cb0e791b594f9c8df663fb" +} diff --git a/.derived/codebase-index/by-spec/072-execution-service.json b/.derived/codebase-index/by-spec/072-execution-service.json new file mode 100644 index 0000000..fef7b10 --- /dev/null +++ b/.derived/codebase-index/by-spec/072-execution-service.json @@ -0,0 +1,216 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/proto/hqgit/eval/v1/worker.proto' does not exist", + "path": "crates/hqgit-eval/proto/hqgit/eval/v1/worker.proto" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/mod.rs' does not exist", + "path": "crates/hqgit-eval/src/service/mod.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/capabilities.rs' does not exist", + "path": "crates/hqgit-eval/src/service/capabilities.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/cas.rs' does not exist", + "path": "crates/hqgit-eval/src/service/cas.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/bytestream.rs' does not exist", + "path": "crates/hqgit-eval/src/service/bytestream.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/action_cache.rs' does not exist", + "path": "crates/hqgit-eval/src/service/action_cache.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/execution.rs' does not exist", + "path": "crates/hqgit-eval/src/service/execution.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/service/worker.rs' does not exist", + "path": "crates/hqgit-eval/src/service/worker.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/scheduler.rs' does not exist", + "path": "crates/hqgit-eval/src/scheduler.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/tests/service.rs' does not exist", + "path": "crates/hqgit-eval/tests/service.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/build.rs' does not exist", + "path": "crates/hqgit-eval/build.rs" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' file unit 'crates/hqgit-eval/Cargo.toml' does not exist", + "path": "crates/hqgit-eval/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '072-execution-service' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "071-action-cache" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/proto/hqgit/eval/v1/worker.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/scheduler.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/action_cache.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/bytestream.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/capabilities.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/cas.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/execution.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/service/worker.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/tests/service.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/build.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "072-execution-service", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "7cfa0b0defa715d9b0732500062ff97b54213aed57a56a8e43a347387e37e9c4" +} diff --git a/.derived/codebase-index/by-spec/073-sandbox-executor.json b/.derived/codebase-index/by-spec/073-sandbox-executor.json new file mode 100644 index 0000000..f70a304 --- /dev/null +++ b/.derived/codebase-index/by-spec/073-sandbox-executor.json @@ -0,0 +1,247 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/go.mod' does not exist", + "path": "executor/go.mod" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/go.sum' does not exist", + "path": "executor/go.sum" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/Makefile' does not exist", + "path": "executor/Makefile" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/cmd/hq-executor/main.go' does not exist", + "path": "executor/cmd/hq-executor/main.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/reapi/worker.go' does not exist", + "path": "executor/internal/reapi/worker.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/reapi/cas.go' does not exist", + "path": "executor/internal/reapi/cas.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/sandbox/tier.go' does not exist", + "path": "executor/internal/sandbox/tier.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/sandbox/report.go' does not exist", + "path": "executor/internal/sandbox/report.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/sandbox/namespaces_linux.go' does not exist", + "path": "executor/internal/sandbox/namespaces_linux.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/sandbox/seccomp_linux.go' does not exist", + "path": "executor/internal/sandbox/seccomp_linux.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/sandbox/microvm.go' does not exist", + "path": "executor/internal/sandbox/microvm.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/internal/sandbox/sandbox_test.go' does not exist", + "path": "executor/internal/sandbox/sandbox_test.go" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'executor/proto/' does not exist", + "path": "executor/proto/" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'crates/hqgit-eval/src/sandbox.rs' does not exist", + "path": "crates/hqgit-eval/src/sandbox.rs" + }, + { + "code": "W-001", + "message": "spec '073-sandbox-executor' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "072-execution-service" + ], + "implementingPaths": [ + { + "path": ".github/workflows/govern.yml", + "source": "spec-edge" + } + ], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/sandbox.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/Makefile" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/cmd/hq-executor/main.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/go.mod" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/go.sum" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/reapi/cas.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/reapi/worker.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/sandbox/microvm.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/sandbox/namespaces_linux.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/sandbox/report.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/sandbox/sandbox_test.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/sandbox/seccomp_linux.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/internal/sandbox/tier.go" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "executor/proto/" + } + }, + { + "locations": [ + { + "file": ".github/workflows/govern.yml" + } + ], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": ".github/workflows/govern.yml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + } + ], + "specId": "073-sandbox-executor", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "67066918a415027b6e671c2d84df47b01bbf7d236d9ad4f2e0aeb9b33316465b" +} diff --git a/.derived/codebase-index/by-spec/074-execution-provenance.json b/.derived/codebase-index/by-spec/074-execution-provenance.json new file mode 100644 index 0000000..e7796a7 --- /dev/null +++ b/.derived/codebase-index/by-spec/074-execution-provenance.json @@ -0,0 +1,90 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '074-execution-provenance' file unit 'crates/hqgit-eval/src/provenance.rs' does not exist", + "path": "crates/hqgit-eval/src/provenance.rs" + }, + { + "code": "W-001", + "message": "spec '074-execution-provenance' file unit 'crates/hqgit-eval/tests/provenance.rs' does not exist", + "path": "crates/hqgit-eval/tests/provenance.rs" + }, + { + "code": "W-001", + "message": "spec '074-execution-provenance' file unit 'crates/hqgit-eval/testdata/slsa/' does not exist", + "path": "crates/hqgit-eval/testdata/slsa/" + }, + { + "code": "W-001", + "message": "spec '074-execution-provenance' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '074-execution-provenance' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "072-execution-service", + "064-attestation-verification" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/provenance.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/testdata/slsa/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/tests/provenance.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + } + ], + "specId": "074-execution-provenance", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "e59e26c17ca0cd4b230de09dae5bd994e2308b9ef80ebd2b983d34d875193de6" +} diff --git a/.derived/codebase-index/by-spec/075-build-graph.json b/.derived/codebase-index/by-spec/075-build-graph.json new file mode 100644 index 0000000..330cca7 --- /dev/null +++ b/.derived/codebase-index/by-spec/075-build-graph.json @@ -0,0 +1,146 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/src/graph/mod.rs' does not exist", + "path": "crates/hqgit-eval/src/graph/mod.rs" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/src/graph/manifest.rs' does not exist", + "path": "crates/hqgit-eval/src/graph/manifest.rs" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/src/graph/toolchain.rs' does not exist", + "path": "crates/hqgit-eval/src/graph/toolchain.rs" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/src/graph/affected.rs' does not exist", + "path": "crates/hqgit-eval/src/graph/affected.rs" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/tests/graph.rs' does not exist", + "path": "crates/hqgit-eval/tests/graph.rs" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/testdata/graph/' does not exist", + "path": "crates/hqgit-eval/testdata/graph/" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' file unit 'crates/hqgit-eval/Cargo.toml' does not exist", + "path": "crates/hqgit-eval/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '075-build-graph' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "070-reapi-types" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/affected.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/manifest.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/toolchain.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/testdata/graph/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/tests/graph.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "075-build-graph", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "130e541c7b826d5adc976e3dac602c3a2d4a916162f3c095a2feff6487a74522" +} diff --git a/.derived/codebase-index/by-spec/076-merge-queue.json b/.derived/codebase-index/by-spec/076-merge-queue.json new file mode 100644 index 0000000..2379697 --- /dev/null +++ b/.derived/codebase-index/by-spec/076-merge-queue.json @@ -0,0 +1,91 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '076-merge-queue' file unit 'crates/hqgit-eval/src/queue.rs' does not exist", + "path": "crates/hqgit-eval/src/queue.rs" + }, + { + "code": "W-001", + "message": "spec '076-merge-queue' file unit 'crates/hqgit-eval/src/speculate.rs' does not exist", + "path": "crates/hqgit-eval/src/speculate.rs" + }, + { + "code": "W-001", + "message": "spec '076-merge-queue' file unit 'crates/hqgit-eval/tests/queue.rs' does not exist", + "path": "crates/hqgit-eval/tests/queue.rs" + }, + { + "code": "W-001", + "message": "spec '076-merge-queue' file unit 'crates/hqgit-eval/src/lib.rs' does not exist", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '076-merge-queue' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "075-build-graph", + "074-execution-provenance", + "067-policy-evaluation-attestation" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/queue.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/speculate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/tests/queue.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + } + ], + "specId": "076-merge-queue", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "d2a4c1e8c71c74609feb7feaf49ecce673caa36f6f7aea2667c3886d72caf94a" +} diff --git a/.derived/codebase-index/by-spec/080-projection-framework.json b/.derived/codebase-index/by-spec/080-projection-framework.json new file mode 100644 index 0000000..557625d --- /dev/null +++ b/.derived/codebase-index/by-spec/080-projection-framework.json @@ -0,0 +1,203 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/Cargo.toml' does not exist", + "path": "crates/hqgit-projection/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/src/lib.rs' does not exist", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/src/projection.rs' does not exist", + "path": "crates/hqgit-projection/src/projection.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/src/runner.rs' does not exist", + "path": "crates/hqgit-projection/src/runner.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/src/sqlite.rs' does not exist", + "path": "crates/hqgit-projection/src/sqlite.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/src/checkpoint.rs' does not exist", + "path": "crates/hqgit-projection/src/checkpoint.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/src/registry.rs' does not exist", + "path": "crates/hqgit-projection/src/registry.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-projection/tests/' does not exist", + "path": "crates/hqgit-projection/tests/" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-cli/src/cmd_projection.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_projection.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '080-projection-framework' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "021-local-repository", + "032-cli-skeleton" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_projection.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/checkpoint.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/projection.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/runner.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/sqlite.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "080-projection-framework", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "005730c78926670d51bfdb1c9d810ded7721aa272eb286662bdcf15d82ed1680" +} diff --git a/.derived/codebase-index/by-spec/081-change-and-review-views.json b/.derived/codebase-index/by-spec/081-change-and-review-views.json new file mode 100644 index 0000000..b0b3501 --- /dev/null +++ b/.derived/codebase-index/by-spec/081-change-and-review-views.json @@ -0,0 +1,148 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/views/mod.rs' does not exist", + "path": "crates/hqgit-projection/src/views/mod.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/views/changes.rs' does not exist", + "path": "crates/hqgit-projection/src/views/changes.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/views/threads.rs' does not exist", + "path": "crates/hqgit-projection/src/views/threads.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/views/attestations.rs' does not exist", + "path": "crates/hqgit-projection/src/views/attestations.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/views/stacks.rs' does not exist", + "path": "crates/hqgit-projection/src/views/stacks.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/tests/views.rs' does not exist", + "path": "crates/hqgit-projection/tests/views.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/testdata/views/' does not exist", + "path": "crates/hqgit-projection/testdata/views/" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/lib.rs' does not exist", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '081-change-and-review-views' file unit 'crates/hqgit-projection/src/registry.rs' does not exist", + "path": "crates/hqgit-projection/src/registry.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "080-projection-framework", + "026-review-threads", + "027-attestation-primitive", + "050-stacked-changes" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/views/attestations.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/views/changes.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/views/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/views/stacks.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/views/threads.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/testdata/views/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/tests/views.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + } + ], + "specId": "081-change-and-review-views", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "999333537a2a1feb5dc3c435e8eaa565725a1a1be8f5afed9388bd9793f572e6" +} diff --git a/.derived/codebase-index/by-spec/082-search-index.json b/.derived/codebase-index/by-spec/082-search-index.json new file mode 100644 index 0000000..82b16cc --- /dev/null +++ b/.derived/codebase-index/by-spec/082-search-index.json @@ -0,0 +1,104 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '082-search-index' file unit 'crates/hqgit-projection/src/search.rs' does not exist", + "path": "crates/hqgit-projection/src/search.rs" + }, + { + "code": "W-001", + "message": "spec '082-search-index' file unit 'crates/hqgit-projection/tests/search.rs' does not exist", + "path": "crates/hqgit-projection/tests/search.rs" + }, + { + "code": "W-001", + "message": "spec '082-search-index' file unit 'crates/hqgit-projection/src/lib.rs' does not exist", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '082-search-index' file unit 'crates/hqgit-projection/src/registry.rs' does not exist", + "path": "crates/hqgit-projection/src/registry.rs" + }, + { + "code": "W-001", + "message": "spec '082-search-index' file unit 'crates/hqgit-projection/Cargo.toml' does not exist", + "path": "crates/hqgit-projection/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '082-search-index' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "080-projection-framework" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/search.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/tests/search.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "082-search-index", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "cb62dc4850a9a3ee190e8cccc165db76d6e4bdb426e5690a2668eaeb8ad2cc35" +} diff --git a/.derived/codebase-index/by-spec/083-code-graph.json b/.derived/codebase-index/by-spec/083-code-graph.json new file mode 100644 index 0000000..26b4507 --- /dev/null +++ b/.derived/codebase-index/by-spec/083-code-graph.json @@ -0,0 +1,161 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/src/codegraph/mod.rs' does not exist", + "path": "crates/hqgit-projection/src/codegraph/mod.rs" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/src/codegraph/scip.rs' does not exist", + "path": "crates/hqgit-projection/src/codegraph/scip.rs" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/src/codegraph/store.rs' does not exist", + "path": "crates/hqgit-projection/src/codegraph/store.rs" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/tests/codegraph.rs' does not exist", + "path": "crates/hqgit-projection/tests/codegraph.rs" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/testdata/scip/' does not exist", + "path": "crates/hqgit-projection/testdata/scip/" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/src/lib.rs' does not exist", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/src/registry.rs' does not exist", + "path": "crates/hqgit-projection/src/registry.rs" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-projection/Cargo.toml' does not exist", + "path": "crates/hqgit-projection/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '083-code-graph' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "080-projection-framework", + "025-semantic-anchors" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/codegraph/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/codegraph/scip.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/codegraph/store.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/testdata/scip/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/tests/codegraph.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "083-code-graph", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "6db26d7169484df93b73915d8d3207e3c5decf9105288b76257deced969ff9bc" +} diff --git a/.derived/codebase-index/by-spec/084-ecosystem-graph.json b/.derived/codebase-index/by-spec/084-ecosystem-graph.json new file mode 100644 index 0000000..64428cf --- /dev/null +++ b/.derived/codebase-index/by-spec/084-ecosystem-graph.json @@ -0,0 +1,133 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/src/ecosystem/mod.rs' does not exist", + "path": "crates/hqgit-projection/src/ecosystem/mod.rs" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/src/ecosystem/deps.rs' does not exist", + "path": "crates/hqgit-projection/src/ecosystem/deps.rs" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/src/ecosystem/impact.rs' does not exist", + "path": "crates/hqgit-projection/src/ecosystem/impact.rs" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/tests/ecosystem.rs' does not exist", + "path": "crates/hqgit-projection/tests/ecosystem.rs" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/testdata/ecosystem/' does not exist", + "path": "crates/hqgit-projection/testdata/ecosystem/" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/src/lib.rs' does not exist", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/src/registry.rs' does not exist", + "path": "crates/hqgit-projection/src/registry.rs" + }, + { + "code": "W-001", + "message": "spec '084-ecosystem-graph' file unit 'crates/hqgit-projection/Cargo.toml' does not exist", + "path": "crates/hqgit-projection/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "083-code-graph", + "075-build-graph", + "051-semantic-deltas" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/ecosystem/deps.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/ecosystem/impact.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/ecosystem/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/testdata/ecosystem/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/tests/ecosystem.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + } + ], + "specId": "084-ecosystem-graph", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "340981a3ca7b7c719bca8c0c81ed5a883fa664c0648f5533127e001876a31fb7" +} diff --git a/.derived/codebase-index/by-spec/085-attention-feeds.json b/.derived/codebase-index/by-spec/085-attention-feeds.json new file mode 100644 index 0000000..4ba85b5 --- /dev/null +++ b/.derived/codebase-index/by-spec/085-attention-feeds.json @@ -0,0 +1,103 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '085-attention-feeds' file unit 'crates/hqgit-projection/src/feeds.rs' does not exist", + "path": "crates/hqgit-projection/src/feeds.rs" + }, + { + "code": "W-001", + "message": "spec '085-attention-feeds' file unit 'crates/hqgit-projection/tests/feeds.rs' does not exist", + "path": "crates/hqgit-projection/tests/feeds.rs" + }, + { + "code": "W-001", + "message": "spec '085-attention-feeds' file unit 'crates/hqgit-projection/testdata/feeds/' does not exist", + "path": "crates/hqgit-projection/testdata/feeds/" + }, + { + "code": "W-001", + "message": "spec '085-attention-feeds' file unit 'crates/hqgit-projection/src/lib.rs' does not exist", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '085-attention-feeds' file unit 'crates/hqgit-projection/src/registry.rs' does not exist", + "path": "crates/hqgit-projection/src/registry.rs" + }, + { + "code": "W-001", + "message": "spec '085-attention-feeds' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "081-change-and-review-views" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/feeds.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/testdata/feeds/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/tests/feeds.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + } + ], + "specId": "085-attention-feeds", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "c28a6348ba2750bdecd8973ca208a8577aec85f4ad85f77e5e89f8e59a9892ec" +} diff --git a/.derived/codebase-index/by-spec/090-server-skeleton.json b/.derived/codebase-index/by-spec/090-server-skeleton.json new file mode 100644 index 0000000..756cfc9 --- /dev/null +++ b/.derived/codebase-index/by-spec/090-server-skeleton.json @@ -0,0 +1,174 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/main.rs' does not exist", + "path": "crates/hqgit-server/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/config.rs' does not exist", + "path": "crates/hqgit-server/src/config.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/health.rs' does not exist", + "path": "crates/hqgit-server/src/health.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/repos.rs' does not exist", + "path": "crates/hqgit-server/src/repos.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/auth.rs' does not exist", + "path": "crates/hqgit-server/src/auth.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/src/telemetry.rs' does not exist", + "path": "crates/hqgit-server/src/telemetry.rs" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' file unit 'crates/hqgit-server/tests/' does not exist", + "path": "crates/hqgit-server/tests/" + }, + { + "code": "W-001", + "message": "spec '090-server-skeleton' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "021-local-repository" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/auth.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/config.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/health.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/repos.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/telemetry.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "090-server-skeleton", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "a52dc040be8b6985b6c31be226ffccbc9e8816eb6862f9459f3a5d0dcee6796d" +} diff --git a/.derived/codebase-index/by-spec/091-per-repo-control-plane.json b/.derived/codebase-index/by-spec/091-per-repo-control-plane.json new file mode 100644 index 0000000..8f4d7ab --- /dev/null +++ b/.derived/codebase-index/by-spec/091-per-repo-control-plane.json @@ -0,0 +1,132 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/src/control/mod.rs' does not exist", + "path": "crates/hqgit-server/src/control/mod.rs" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/src/control/raft.rs' does not exist", + "path": "crates/hqgit-server/src/control/raft.rs" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/src/control/placement.rs' does not exist", + "path": "crates/hqgit-server/src/control/placement.rs" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/src/control/apply.rs' does not exist", + "path": "crates/hqgit-server/src/control/apply.rs" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/tests/control.rs' does not exist", + "path": "crates/hqgit-server/tests/control.rs" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '091-per-repo-control-plane' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "090-server-skeleton" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/control/apply.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/control/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/control/placement.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/control/raft.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/control.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "091-per-repo-control-plane", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "2e20e69ec6533c62e7a2345736ebddef6c565f7dfc1723384133c00527540f14" +} diff --git a/.derived/codebase-index/by-spec/092-git-endpoint.json b/.derived/codebase-index/by-spec/092-git-endpoint.json new file mode 100644 index 0000000..584a328 --- /dev/null +++ b/.derived/codebase-index/by-spec/092-git-endpoint.json @@ -0,0 +1,148 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/git/mod.rs' does not exist", + "path": "crates/hqgit-server/src/git/mod.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/git/refs.rs' does not exist", + "path": "crates/hqgit-server/src/git/refs.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/git/upload_pack.rs' does not exist", + "path": "crates/hqgit-server/src/git/upload_pack.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/git/receive_pack.rs' does not exist", + "path": "crates/hqgit-server/src/git/receive_pack.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/git/gate.rs' does not exist", + "path": "crates/hqgit-server/src/git/gate.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/tests/git_endpoint.rs' does not exist", + "path": "crates/hqgit-server/tests/git_endpoint.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '092-git-endpoint' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "090-server-skeleton", + "031-git-object-bridge", + "024-change-and-revision", + "068-policy-in-repo" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/git/gate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/git/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/git/receive_pack.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/git/refs.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/git/upload_pack.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/git_endpoint.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + } + ], + "specId": "092-git-endpoint", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "b88036062580cbd15738d69e3ef8c301c7ec05c670e1ed31752443f1bc608bd3" +} diff --git a/.derived/codebase-index/by-spec/093-connect-api.json b/.derived/codebase-index/by-spec/093-connect-api.json new file mode 100644 index 0000000..bd56b87 --- /dev/null +++ b/.derived/codebase-index/by-spec/093-connect-api.json @@ -0,0 +1,417 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'proto/hqgit/v1/common.proto' does not exist", + "path": "proto/hqgit/v1/common.proto" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'proto/hqgit/v1/changes.proto' does not exist", + "path": "proto/hqgit/v1/changes.proto" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'proto/hqgit/v1/reviews.proto' does not exist", + "path": "proto/hqgit/v1/reviews.proto" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'proto/hqgit/v1/attestations.proto' does not exist", + "path": "proto/hqgit/v1/attestations.proto" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'proto/hqgit/v1/policy.proto' does not exist", + "path": "proto/hqgit/v1/policy.proto" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'proto/hqgit/v1/repos.proto' does not exist", + "path": "proto/hqgit/v1/repos.proto" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/build.rs' does not exist", + "path": "crates/hqgit-server/build.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/mod.rs' does not exist", + "path": "crates/hqgit-server/src/api/mod.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/connect.rs' does not exist", + "path": "crates/hqgit-server/src/api/connect.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/read_model.rs' does not exist", + "path": "crates/hqgit-server/src/api/read_model.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/changes.rs' does not exist", + "path": "crates/hqgit-server/src/api/changes.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/reviews.rs' does not exist", + "path": "crates/hqgit-server/src/api/reviews.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/attestations.rs' does not exist", + "path": "crates/hqgit-server/src/api/attestations.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/policy.rs' does not exist", + "path": "crates/hqgit-server/src/api/policy.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/api/repos.rs' does not exist", + "path": "crates/hqgit-server/src/api/repos.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/tests/api.rs' does not exist", + "path": "crates/hqgit-server/tests/api.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/build.rs' does not exist", + "path": "crates/hqgit-cli/build.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/src/client.rs' does not exist", + "path": "crates/hqgit-cli/src/client.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/src/cmd_remote.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_remote.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/tests/remote.rs' does not exist", + "path": "crates/hqgit-cli/tests/remote.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/src/config.rs' does not exist", + "path": "crates/hqgit-cli/src/config.rs" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '093-connect-api' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "090-server-skeleton", + "026-review-threads", + "027-attestation-primitive", + "067-policy-evaluation-attestation", + "032-cli-skeleton", + "081-change-and-review-views" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/build.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/client.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_remote.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/remote.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/build.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/attestations.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/changes.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/connect.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/mod.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/policy.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/read_model.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/repos.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/api/reviews.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/api.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "proto/hqgit/v1/attestations.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "proto/hqgit/v1/changes.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "proto/hqgit/v1/common.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "proto/hqgit/v1/policy.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "proto/hqgit/v1/repos.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "proto/hqgit/v1/reviews.proto" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/config.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "093-connect-api", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "08a70ef2073ad3d738e9c0e23b70d6447024a7cd8c6091fdc2f1a169e1de78b6" +} diff --git a/.derived/codebase-index/by-spec/094-quarantine-and-promotion.json b/.derived/codebase-index/by-spec/094-quarantine-and-promotion.json new file mode 100644 index 0000000..c31771d --- /dev/null +++ b/.derived/codebase-index/by-spec/094-quarantine-and-promotion.json @@ -0,0 +1,132 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/src/quarantine.rs' does not exist", + "path": "crates/hqgit-server/src/quarantine.rs" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/src/capability.rs' does not exist", + "path": "crates/hqgit-server/src/capability.rs" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/tests/quarantine.rs' does not exist", + "path": "crates/hqgit-server/tests/quarantine.rs" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/testdata/capabilities/' does not exist", + "path": "crates/hqgit-server/testdata/capabilities/" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '094-quarantine-and-promotion' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "091-per-repo-control-plane", + "064-attestation-verification" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/capability.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/quarantine.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/testdata/capabilities/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/quarantine.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + } + ], + "specId": "094-quarantine-and-promotion", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "d0d30dbfd58fd7922c577cdbf51aa3f5408b2287260d681a6b2b04c9356ad8da" +} diff --git a/.derived/codebase-index/by-spec/095-web-review-ui.json b/.derived/codebase-index/by-spec/095-web-review-ui.json new file mode 100644 index 0000000..51c5ff4 --- /dev/null +++ b/.derived/codebase-index/by-spec/095-web-review-ui.json @@ -0,0 +1,287 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/package.json' does not exist", + "path": "web/package.json" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/tsconfig.json' does not exist", + "path": "web/tsconfig.json" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/vite.config.ts' does not exist", + "path": "web/vite.config.ts" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/index.html' does not exist", + "path": "web/index.html" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/main.tsx' does not exist", + "path": "web/src/main.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/api.ts' does not exist", + "path": "web/src/api.ts" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/format.ts' does not exist", + "path": "web/src/format.ts" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/views/ChangeList.tsx' does not exist", + "path": "web/src/views/ChangeList.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/views/ChangeDetail.tsx' does not exist", + "path": "web/src/views/ChangeDetail.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/views/DeltaPanel.tsx' does not exist", + "path": "web/src/views/DeltaPanel.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/views/LineDiff.tsx' does not exist", + "path": "web/src/views/LineDiff.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/views/EvidencePanel.tsx' does not exist", + "path": "web/src/views/EvidencePanel.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/src/views/ThreadPanel.tsx' does not exist", + "path": "web/src/views/ThreadPanel.tsx" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'web/test/' does not exist", + "path": "web/test/" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'crates/hqgit-server/src/static_files.rs' does not exist", + "path": "crates/hqgit-server/src/static_files.rs" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '095-web-review-ui' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "093-connect-api", + "081-change-and-review-views" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/static_files.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/index.html" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/package.json" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/api.ts" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/format.ts" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/main.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/views/ChangeDetail.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/views/ChangeList.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/views/DeltaPanel.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/views/EvidencePanel.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/views/LineDiff.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/src/views/ThreadPanel.tsx" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/test/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/tsconfig.json" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "web/vite.config.ts" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "095-web-review-ui", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "1523dfcfcc51b7f74108fd40992333a011f97db376502f72e42163f6cb5ee015" +} diff --git a/.derived/codebase-index/by-spec/100-agent-principals.json b/.derived/codebase-index/by-spec/100-agent-principals.json new file mode 100644 index 0000000..5e73bb6 --- /dev/null +++ b/.derived/codebase-index/by-spec/100-agent-principals.json @@ -0,0 +1,147 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/Cargo.toml' does not exist", + "path": "crates/hqgit-agent/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/src/lib.rs' does not exist", + "path": "crates/hqgit-agent/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/src/token.rs' does not exist", + "path": "crates/hqgit-agent/src/token.rs" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/src/principal.rs' does not exist", + "path": "crates/hqgit-agent/src/principal.rs" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/src/caveats.rs' does not exist", + "path": "crates/hqgit-agent/src/caveats.rs" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/tests/' does not exist", + "path": "crates/hqgit-agent/tests/" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-agent/testdata/tokens/' does not exist", + "path": "crates/hqgit-agent/testdata/tokens/" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '100-agent-principals' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "060-identity-and-key-rotation", + "068-policy-in-repo" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/caveats.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/principal.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/token.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/testdata/tokens/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "100-agent-principals", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "15b21bcb8318da6251523645a5b3b81f26beec2666a09fb85af7c729a029dbc7" +} diff --git a/.derived/codebase-index/by-spec/101-delegation-chain.json b/.derived/codebase-index/by-spec/101-delegation-chain.json new file mode 100644 index 0000000..aac097c --- /dev/null +++ b/.derived/codebase-index/by-spec/101-delegation-chain.json @@ -0,0 +1,103 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '101-delegation-chain' file unit 'crates/hqgit-agent/src/delegation.rs' does not exist", + "path": "crates/hqgit-agent/src/delegation.rs" + }, + { + "code": "W-001", + "message": "spec '101-delegation-chain' file unit 'crates/hqgit-agent/src/authorize.rs' does not exist", + "path": "crates/hqgit-agent/src/authorize.rs" + }, + { + "code": "W-001", + "message": "spec '101-delegation-chain' file unit 'crates/hqgit-agent/tests/delegation.rs' does not exist", + "path": "crates/hqgit-agent/tests/delegation.rs" + }, + { + "code": "W-001", + "message": "spec '101-delegation-chain' file unit 'crates/hqgit-agent/testdata/delegation/' does not exist", + "path": "crates/hqgit-agent/testdata/delegation/" + }, + { + "code": "W-001", + "message": "spec '101-delegation-chain' file unit 'crates/hqgit-agent/src/lib.rs' does not exist", + "path": "crates/hqgit-agent/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '101-delegation-chain' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "100-agent-principals" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/authorize.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/delegation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/testdata/delegation/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/tests/delegation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "specId": "101-delegation-chain", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "e1af80d46ebcbc39d3eca9b6294450821b9d7e30be678a6f262e3115c9379da9" +} diff --git a/.derived/codebase-index/by-spec/102-agent-sandbox-and-provenance.json b/.derived/codebase-index/by-spec/102-agent-sandbox-and-provenance.json new file mode 100644 index 0000000..ebdf7f6 --- /dev/null +++ b/.derived/codebase-index/by-spec/102-agent-sandbox-and-provenance.json @@ -0,0 +1,161 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-agent/src/sandbox.rs' does not exist", + "path": "crates/hqgit-agent/src/sandbox.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-agent/src/provenance.rs' does not exist", + "path": "crates/hqgit-agent/src/provenance.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-agent/tests/provenance.rs' does not exist", + "path": "crates/hqgit-agent/tests/provenance.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-server/src/agent_auth.rs' does not exist", + "path": "crates/hqgit-server/src/agent_auth.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-server/tests/agent_auth.rs' does not exist", + "path": "crates/hqgit-server/tests/agent_auth.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-agent/src/lib.rs' does not exist", + "path": "crates/hqgit-agent/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '102-agent-sandbox-and-provenance' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "101-delegation-chain", + "074-execution-provenance", + "090-server-skeleton" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/provenance.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/sandbox.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/tests/provenance.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/agent_auth.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/agent_auth.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + } + ], + "specId": "102-agent-sandbox-and-provenance", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "46241ad27af960c9f13bee477b1355af5b5c6f2aeb5af1f3dcef396fce39d85f" +} diff --git a/.derived/codebase-index/by-spec/103-evidence-carrying-changes.json b/.derived/codebase-index/by-spec/103-evidence-carrying-changes.json new file mode 100644 index 0000000..c9346b8 --- /dev/null +++ b/.derived/codebase-index/by-spec/103-evidence-carrying-changes.json @@ -0,0 +1,203 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-domain/src/evidence.rs' does not exist", + "path": "crates/hqgit-domain/src/evidence.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-domain/tests/evidence.rs' does not exist", + "path": "crates/hqgit-domain/tests/evidence.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-domain/testdata/evidence/' does not exist", + "path": "crates/hqgit-domain/testdata/evidence/" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-policy-sdk/examples/evidence_required.rs' does not exist", + "path": "crates/hqgit-policy-sdk/examples/evidence_required.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-cli/src/cmd_evidence.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_evidence.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-cli/tests/evidence.rs' does not exist", + "path": "crates/hqgit-cli/tests/evidence.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-domain/src/predicate.rs' does not exist", + "path": "crates/hqgit-domain/src/predicate.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-policy-sdk/src/evidence.rs' does not exist", + "path": "crates/hqgit-policy-sdk/src/evidence.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '103-evidence-carrying-changes' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "102-agent-sandbox-and-provenance", + "067-policy-evaluation-attestation", + "051-semantic-deltas" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_evidence.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/evidence.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/evidence.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/testdata/evidence/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/evidence.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/evidence_required.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/evidence.rs" + } + } + ], + "specId": "103-evidence-carrying-changes", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "68d5f42193536c8738a59b2adb584a81580b4d921f666c37da9297fc95482b26" +} diff --git a/.derived/codebase-index/by-spec/104-ownership-and-sla.json b/.derived/codebase-index/by-spec/104-ownership-and-sla.json new file mode 100644 index 0000000..71139bb --- /dev/null +++ b/.derived/codebase-index/by-spec/104-ownership-and-sla.json @@ -0,0 +1,91 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '104-ownership-and-sla' file unit 'crates/hqgit-domain/src/ownership.rs' does not exist", + "path": "crates/hqgit-domain/src/ownership.rs" + }, + { + "code": "W-001", + "message": "spec '104-ownership-and-sla' file unit 'crates/hqgit-domain/tests/ownership.rs' does not exist", + "path": "crates/hqgit-domain/tests/ownership.rs" + }, + { + "code": "W-001", + "message": "spec '104-ownership-and-sla' file unit 'crates/hqgit-domain/testdata/ownership/' does not exist", + "path": "crates/hqgit-domain/testdata/ownership/" + }, + { + "code": "W-001", + "message": "spec '104-ownership-and-sla' file unit 'crates/hqgit-domain/src/lib.rs' does not exist", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '104-ownership-and-sla' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "028-issues-and-derived-state", + "060-identity-and-key-rotation", + "025-semantic-anchors" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/ownership.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/testdata/ownership/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/tests/ownership.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + } + ], + "specId": "104-ownership-and-sla", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "07381d3520e7359ce8b425c9ec6bd5718cb8ec8c1f7f9fd7a014815f788f829c" +} diff --git a/.derived/codebase-index/by-spec/110-set-reconciliation.json b/.derived/codebase-index/by-spec/110-set-reconciliation.json new file mode 100644 index 0000000..3af05dd --- /dev/null +++ b/.derived/codebase-index/by-spec/110-set-reconciliation.json @@ -0,0 +1,131 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/Cargo.toml' does not exist", + "path": "crates/hqgit-sync/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/src/lib.rs' does not exist", + "path": "crates/hqgit-sync/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/src/reconcile.rs' does not exist", + "path": "crates/hqgit-sync/src/reconcile.rs" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/src/fingerprint.rs' does not exist", + "path": "crates/hqgit-sync/src/fingerprint.rs" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/src/protocol.rs' does not exist", + "path": "crates/hqgit-sync/src/protocol.rs" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/tests/' does not exist", + "path": "crates/hqgit-sync/tests/" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-sync/testdata/protocol/' does not exist", + "path": "crates/hqgit-sync/testdata/protocol/" + }, + { + "code": "W-001", + "message": "spec '110-set-reconciliation' file unit 'crates/hqgit-ledger/src/repo.rs' does not exist", + "path": "crates/hqgit-ledger/src/repo.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "021-local-repository" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/fingerprint.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/protocol.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/reconcile.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/testdata/protocol/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/tests/" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/repo.rs" + } + } + ], + "specId": "110-set-reconciliation", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "69878756165b37d05fbcf669c768b23add47faad1ad78a13098560b617a3a5af" +} diff --git a/.derived/codebase-index/by-spec/111-quic-transport.json b/.derived/codebase-index/by-spec/111-quic-transport.json new file mode 100644 index 0000000..1a97603 --- /dev/null +++ b/.derived/codebase-index/by-spec/111-quic-transport.json @@ -0,0 +1,119 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '111-quic-transport' file unit 'crates/hqgit-sync/src/transport.rs' does not exist", + "path": "crates/hqgit-sync/src/transport.rs" + }, + { + "code": "W-001", + "message": "spec '111-quic-transport' file unit 'crates/hqgit-sync/src/session.rs' does not exist", + "path": "crates/hqgit-sync/src/session.rs" + }, + { + "code": "W-001", + "message": "spec '111-quic-transport' file unit 'crates/hqgit-sync/src/object_source.rs' does not exist", + "path": "crates/hqgit-sync/src/object_source.rs" + }, + { + "code": "W-001", + "message": "spec '111-quic-transport' file unit 'crates/hqgit-sync/tests/transport.rs' does not exist", + "path": "crates/hqgit-sync/tests/transport.rs" + }, + { + "code": "W-001", + "message": "spec '111-quic-transport' file unit 'crates/hqgit-sync/src/lib.rs' does not exist", + "path": "crates/hqgit-sync/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '111-quic-transport' file unit 'crates/hqgit-sync/Cargo.toml' does not exist", + "path": "crates/hqgit-sync/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '111-quic-transport' section unit 'workspace.dependencies' not found in Cargo.toml", + "path": "Cargo.toml" + } + ] + }, + "mapping": { + "dependsOn": [ + "110-set-reconciliation", + "060-identity-and-key-rotation" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/object_source.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/session.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/transport.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/tests/transport.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/lib.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "specId": "111-quic-transport", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "44e962293e439339656fd62de2b0314c6132a93012c48823a2e016407fcbf338" +} diff --git a/.derived/codebase-index/by-spec/112-federation.json b/.derived/codebase-index/by-spec/112-federation.json new file mode 100644 index 0000000..2da53d3 --- /dev/null +++ b/.derived/codebase-index/by-spec/112-federation.json @@ -0,0 +1,203 @@ +{ + "diagnostics": { + "errors": [], + "warnings": [ + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/src/federation.rs' does not exist", + "path": "crates/hqgit-server/src/federation.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/src/peers.rs' does not exist", + "path": "crates/hqgit-server/src/peers.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-cli/src/cmd_sync.rs' does not exist", + "path": "crates/hqgit-cli/src/cmd_sync.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/tests/federation.rs' does not exist", + "path": "crates/hqgit-server/tests/federation.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-cli/tests/sync.rs' does not exist", + "path": "crates/hqgit-cli/tests/sync.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/src/lib.rs' does not exist", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/src/app.rs' does not exist", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/src/config.rs' does not exist", + "path": "crates/hqgit-server/src/config.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-server/Cargo.toml' does not exist", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-cli/src/main.rs' does not exist", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-cli/src/cli.rs' does not exist", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-cli/Cargo.toml' does not exist", + "path": "crates/hqgit-cli/Cargo.toml" + }, + { + "code": "W-001", + "message": "spec '112-federation' file unit 'crates/hqgit-domain/src/facts.rs' does not exist", + "path": "crates/hqgit-domain/src/facts.rs" + } + ] + }, + "mapping": { + "dependsOn": [ + "111-quic-transport", + "094-quarantine-and-promotion", + "032-cli-skeleton" + ], + "implementingPaths": [], + "resolvedUnits": [ + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_sync.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/tests/sync.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/federation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/peers.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/tests/federation.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/config.rs" + } + }, + { + "locations": [], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + } + ], + "specId": "112-federation", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "31fa0edfb04b5351886e83184664b166a59079d444bbc11df36c01197142e85c" +} diff --git a/.derived/spec-registry/by-spec/000-hqgit-bootstrap.json b/.derived/spec-registry/by-spec/000-hqgit-bootstrap.json new file mode 100644 index 0000000..de1d05f --- /dev/null +++ b/.derived/spec-registry/by-spec/000-hqgit-bootstrap.json @@ -0,0 +1,52 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "domain": "governance", + "extraFrontmatter": { + "wave": 1 + }, + "id": "000-hqgit-bootstrap", + "implementation": "n-a", + "kind": "constitutional-bootstrap", + "origin": { + "retroactive": true + }, + "risk": "critical", + "sectionHeadings": [ + "000: Bootstrap spec system for hqgit", + "1. The authoring / derived boundary", + "2. Identity: directory name equals id", + "3. The typed authority graph", + "4. Determinism", + "5. The refusal rule", + "6. The frozen system invariants", + "7. Corpus conventions", + "8. Lifecycle as scheduling", + "9. Bootstrap order" + ], + "specPath": "specs/000-hqgit-bootstrap/spec.md", + "status": "approved", + "summary": "Foundational contract for the hqgit corpus. Authored truth lives only in markdown with YAML frontmatter; machine-consumable truth about the corpus is compiler-emitted JSON read only through spec-spine; every artifact is a deterministic function of (config, file contents); and a typed authority graph governs who owns what. hqgit is specified in full before a line of it is built: the corpus is the design, spec numbers are the build order, and an orchestrator drives one spec per fresh session through build, ship, shepherd, and verify. This spec also freezes the seven system invariants (constitution VI through XIII) that no later spec may amend, because the first of them (hash stability of the ledger) is the only mistake the project cannot recover from.\n", + "title": "Bootstrap spec system for hqgit (specify first, build by spec)", + "unamendable": [ + "markdown-truth-boundary", + "json-truth-boundary", + "determinism-requirement", + "directory-name-equals-id", + "typed-authority-graph", + "refusal-rule", + "canonical-derived-boundary", + "facts-immutable", + "hash-stability", + "single-evidence-primitive", + "erasure-by-tombstone", + "agent-principal-class", + "layer-direction" + ] + }, + "shardHash": "eccf066bf7d3ec4377f434a34872e0eb24e358ea854295f17cae074a19dc2d9b", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/001-agentic-harness.json b/.derived/spec-registry/by-spec/001-agentic-harness.json new file mode 100644 index 0000000..efd5982 --- /dev/null +++ b/.derived/spec-registry/by-spec/001-agentic-harness.json @@ -0,0 +1,98 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "000-hqgit-bootstrap" + ], + "domain": "governance", + "establishes": [ + { + "kind": "file", + "path": "AGENTS.md" + }, + { + "kind": "file", + "path": "CLAUDE.md" + }, + { + "kind": "file", + "path": "Makefile" + }, + { + "kind": "file", + "path": "spec-spine.toml" + }, + { + "kind": "file", + "path": ".mcp.json" + }, + { + "kind": "file", + "path": "standards/spec/contract.md" + }, + { + "kind": "file", + "path": "standards/spec/templates/" + }, + { + "kind": "file", + "path": ".claude/settings.json" + }, + { + "kind": "file", + "path": ".claude/agents/" + }, + { + "kind": "file", + "path": ".claude/rules/" + }, + { + "kind": "file", + "path": ".claude/skills/" + }, + { + "kind": "file", + "path": ".github/workflows/govern.yml" + }, + { + "kind": "file", + "path": ".github/dependabot.yml" + }, + { + "kind": "file", + "path": "scripts/verify-spec.sh" + }, + { + "kind": "file", + "path": "scripts/spec-dag.sh" + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "001-agentic-harness", + "implementation": "complete", + "kind": "governance", + "risk": "high", + "sectionHeadings": [ + "001: Agentic engineering harness", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/001-agentic-harness/spec.md", + "status": "approved", + "summary": "The governed-development loop every human and every driven session runs inside: the cross-agent New Sessions protocol and the Working the backlog protocol in AGENTS.md, the Claude Code skills (init, setup, next, build, verify, spec, commit, code-review, ship, shepherd, validate-and-fix, cleanup, implement-plan, research, refactor-claude-md), the six agents (architect, explorer, implementer, reviewer, ledger-guardian, trust-reviewer), the standing and path-scoped rules, the hooks that keep the derived artifacts fresh and block an ungated PR, the Makefile that is the one source of truth for what CI validates, and the CI workflow that re-runs the same gate. The harness is what makes the corpus buildable by claude-observatory: the orchestrator reads AGENTS.md's backlog section into every build prompt, drives the repo's own /ship, watches the CI this spec wires, and runs each spec's Verification block after merge.\n", + "title": "Agentic engineering harness: session protocol, skills, agents, hooks, gate" + }, + "shardHash": "6a0c492d251bd7da4fbf98541e02aa267937ed388fd03ecd86cb756e93ad32ed", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/002-platform-thesis.json b/.derived/spec-registry/by-spec/002-platform-thesis.json new file mode 100644 index 0000000..4ec612d --- /dev/null +++ b/.derived/spec-registry/by-spec/002-platform-thesis.json @@ -0,0 +1,175 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "constrains": [ + { + "kind": "sequencing-plan", + "note": "Wave 1: ledger, object store, domain model, CLI. Local only, no server. Ends when offline review against a plain git repo works end to end (033) and its evidence verifies (034). Hash stability of 011 and 017 is the one unrecoverable decision in the whole plan, which is why they sit first and why 012 gates them before anything is built on top.\n", + "target_specs": [ + "010-workspace-and-core-types", + "011-canonical-encoding", + "012-hash-stability-gate", + "013-object-store", + "014-content-defined-chunking", + "015-verified-streaming", + "016-remote-object-backend", + "017-ledger-entry-dag", + "018-deterministic-total-order", + "019-facts-and-derived-state", + "020-commitments-and-tombstones", + "021-local-repository", + "023-domain-fact-vocabulary", + "024-change-and-revision", + "025-semantic-anchors", + "026-review-threads", + "027-attestation-primitive", + "028-issues-and-derived-state", + "031-git-object-bridge", + "032-cli-skeleton", + "033-cli-offline-review", + "034-cli-attest-and-verify" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 2: the git bridge (031) and the bidirectional GitHub mirror. Value for users who migrate nothing: their issues, pull requests, reviews, and check results become facts they can clone.\n", + "target_specs": [ + "040-github-mirror-import", + "041-github-mirror-export", + "042-mirror-sync-command" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 3: stacked changes and semantic review. The first felt value beyond parity: force-push stops destroying review context, stacks are native, and the high-value views of a change (API surface, types, dependencies, capabilities) arrive as attestations.\n", + "target_specs": [ + "050-stacked-changes", + "051-semantic-deltas", + "052-semantic-conflicts" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 4: the trust plane and the policy engine. Merge gates become checkable predicates over verified evidence; every verdict is itself an attestation replayable against the exact policy hash that produced it.\n", + "target_specs": [ + "060-identity-and-key-rotation", + "061-oidc-login", + "062-transparency-log", + "063-keyless-signing", + "064-attestation-verification", + "065-policy-engine", + "066-policy-sdk", + "067-policy-evaluation-attestation", + "068-policy-in-repo" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 5: the evaluation plane. Highest capex, deliberately fifth because it depends on stable input hashing. CI results are not a separate system; they are evidence, and the cache is a trust boundary.\n", + "target_specs": [ + "070-reapi-types", + "071-action-cache", + "072-execution-service", + "073-sandbox-executor", + "074-execution-provenance", + "075-build-graph", + "076-merge-queue" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 6: projections, the cross-repo code and ecosystem graph, and the hosted edge (server, git endpoint, API, review UI). Everything in this wave is disposable by construction (constitution VI); the ecosystem graph is the one legitimately centralized component and stays non-authoritative.\n", + "target_specs": [ + "080-projection-framework", + "081-change-and-review-views", + "082-search-index", + "083-code-graph", + "084-ecosystem-graph", + "085-attention-feeds", + "090-server-skeleton", + "091-per-repo-control-plane", + "092-git-endpoint", + "093-connect-api", + "094-quarantine-and-promotion", + "095-web-review-ui" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 7: agents as a distinct principal class, designed into the types in wave 1 (010) and shipped here: Biscuit credentials carrying the delegation chain, declared sandboxes, mandatory provenance, and changes that arrive with machine-checkable evidence so a human reviews the argument rather than the diff. Ownership with delegation and expiry rides along because it is the other half of the accountability chain.\n", + "target_specs": [ + "100-agent-principals", + "101-delegation-chain", + "102-agent-sandbox-and-provenance", + "103-evidence-carrying-changes", + "104-ownership-and-sla" + ] + }, + { + "kind": "sequencing-plan", + "note": "Wave 8: multi-host federation. Last because discovery cost is the structural reason centralization keeps winning; the central index exists by wave 6 without lock-in as its business model, and federation replicates the same signed history between hosts.\n", + "target_specs": [ + "110-set-reconciliation", + "111-quic-transport", + "112-federation" + ] + } + ], + "created": "2026-09-02", + "dependsOn": [ + "000-hqgit-bootstrap" + ], + "domain": "governance", + "extraFrontmatter": { + "wave": 1 + }, + "id": "002-platform-thesis", + "implementation": "n-a", + "kind": "thesis", + "references": [ + { + "role": "context", + "unit": { + "kind": "file", + "path": "docs/design/00-architecture.md" + } + }, + { + "role": "context", + "unit": { + "kind": "file", + "path": "docs/design/01-build-order.md" + } + } + ], + "risk": "critical", + "sectionHeadings": [ + "002: Platform thesis", + "1. Purpose", + "2. Layer model", + "3. The governing decision: canonical versus derived", + "4. The layers, decided", + "4.1 L0: object store (specs 013 to 016, 031)", + "4.2 L1: ledger (specs 011, 012, 017 to 021, 110, 111)", + "4.3 L2: domain model (specs 023 to 028, 050 to 052, 104)", + "4.4 L3: evaluation plane (specs 070 to 076)", + "4.5 L4: trust plane (specs 060 to 064, 100 to 102)", + "4.6 L6: policy (specs 065 to 068, 103)", + "4.7 L5 and L7 (specs 080 to 095, 112)", + "5. Language and crate topology", + "6. Build order", + "7. Amendment and invalidation", + "8. Standing risks", + "9. Out of scope", + "10. Resolved decisions" + ], + "specPath": "specs/002-platform-thesis/spec.md", + "status": "approved", + "summary": "hqgit is a verifiable evidence ledger for software change. Canonical state is a set of signed, content-addressed objects forming a per-repository DAG that covers code, collaboration, and evidence; every index, timeline, queue, and dashboard is a projection rebuildable from zero. The forge is one client of that ledger, the CI system is another, and the agent runtime is a third. This spec fixes the layer model (L0 objects through L7 edge), the governing invariant (canonical versus derived), the nouns (Change, Revision, Anchor, Attestation, Principal, Policy), the language split (Rust core, Go only at the executor seam), the crate topology, the eight-wave build order that every later spec's number reflects, and the standing risks. It owns no code.\n", + "title": "Platform thesis: a verifiable evidence ledger for software change" + }, + "shardHash": "391b3b6b734874a82bb07348ad27f6c3789a724b9f1baa4d6a09c93321fef991", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/003-chassis-alignment.json b/.derived/spec-registry/by-spec/003-chassis-alignment.json new file mode 100644 index 0000000..6069620 --- /dev/null +++ b/.derived/spec-registry/by-spec/003-chassis-alignment.json @@ -0,0 +1,54 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "constrains": [ + { + "kind": "family-boundary", + "target_specs": [ + "010-workspace-and-core-types", + "017-ledger-entry-dag", + "060-identity-and-key-rotation", + "061-oidc-login", + "090-server-skeleton", + "091-per-repo-control-plane", + "093-connect-api", + "094-quarantine-and-promotion", + "095-web-review-ui", + "100-agent-principals", + "101-delegation-chain" + ] + } + ], + "created": "2026-09-03", + "dependsOn": [ + "002-platform-thesis" + ], + "domain": "governance", + "extraFrontmatter": { + "wave": 1 + }, + "id": "003-chassis-alignment", + "implementation": "n-a", + "kind": "governance", + "risk": "critical", + "sectionHeadings": [ + "003: Chassis alignment", + "1. Purpose", + "2. What this spec settles", + "3. Affected specs", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/003-chassis-alignment/spec.md", + "status": "approved", + "summary": "A Rust chassis named rahi now exists in this family and owns identity, replicated operational state, a decision chain, a capability kernel, an axum edge, and single-container packaging. Without a decision on record, the wave 6 server specs would found all of that a second time, and a deployment would contain two things called a ledger. This spec draws the boundary: waves 1 through 5 stay chassis-free so the CLI works with no server, the wave 6 server composes rahi as a Cell, the evidence DAG never moves into the chassis store, hqgit's cryptographic identity stays authoritative with the IdP as an authentication subject bound to it, and the word ledger keeps one meaning in this repository.\n", + "title": "Chassis alignment: what the hosted edge consumes from rahi, and what stays hqgit's" + }, + "shardHash": "f653631bb641354d812b6ab345ecf1424a888143005cf282d1389e1d4af6c9dd", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/010-workspace-and-core-types.json b/.derived/spec-registry/by-spec/010-workspace-and-core-types.json new file mode 100644 index 0000000..d56447c --- /dev/null +++ b/.derived/spec-registry/by-spec/010-workspace-and-core-types.json @@ -0,0 +1,90 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "002-platform-thesis" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "Cargo.toml" + }, + { + "kind": "file", + "path": "rust-toolchain.toml" + }, + { + "kind": "file", + "path": "deny.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-types/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/hash.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/cid.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/principal.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/key.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/hlc.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/error.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/version.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/tests/" + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "010-workspace-and-core-types", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "010: Cargo workspace and the core types", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/010-workspace-and-core-types/spec.md", + "status": "approved", + "summary": "The first build: a virtual Cargo workspace with the toolchain pinned, unsafe forbidden, one shared dependency table, and a supply-chain policy; plus hqgit-types, the plain-data substrate every other crate depends on and that depends on nothing in the workspace. It fixes the value types that reach hashed bytes: Hash (BLAKE3-256), Cid (codec plus hash), the four-variant Principal, KeyId and Signature with the ed25519 Signer and Verifier seams and the frozen signing preimage, the Hlc timestamp with its total order, the Error enum with the four exit codes, and the schema version constants. Everything here is owned data with serde derives and no lifetimes, generics, or trait objects at the boundary, so the same types back the CLI, the server, and any future binding.\n", + "title": "Cargo workspace and the core types: Hash, Cid, Principal, keys, Hlc, Error" + }, + "shardHash": "9d0057c953dd7f43cce03161d474640b8a0f555cf76422af4048eb447ff2ec05", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/011-canonical-encoding.json b/.derived/spec-registry/by-spec/011-canonical-encoding.json new file mode 100644 index 0000000..7e220ea --- /dev/null +++ b/.derived/spec-registry/by-spec/011-canonical-encoding.json @@ -0,0 +1,89 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "010-workspace-and-core-types" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-types/src/codec/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/codec/cbor.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/codec/envelope.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/src/codec/value.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + }, + { + "kind": "file", + "path": "crates/hqgit-types/tests/codec.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "011-canonical-encoding", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "011: Canonical encoding", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/011-canonical-encoding/spec.md", + "status": "approved", + "summary": "The one serialization every hashed and signed byte in hqgit passes through. This spec fixes canonical DAG-CBOR (RFC 8949 deterministic encoding under the IPLD DAG-CBOR restrictions: definite lengths, shortest integers, length-then-bytes key order, no floats, no indefinite items, no tags except the link tag), the in-memory Value model it encodes, the versioned Envelope every object is wrapped in, and the unknown-field preservation rule that lets a newer writer's fields survive an older reader byte for byte. It establishes the golden vector corpus that spec 012 gates and every later release must reproduce. Nothing here reads a clock or the environment; decode of encode is the identity and encode of decode is the identity on canonical bytes, and both are frozen at tier 1.\n", + "title": "Canonical encoding: deterministic DAG-CBOR, the Value model, envelopes, unknown-field preservation" + }, + "shardHash": "91181516e6ee0cadbc58ec98e2d9bab35bf36c93c2dbe6149d787385bbabab15", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/012-hash-stability-gate.json b/.derived/spec-registry/by-spec/012-hash-stability-gate.json new file mode 100644 index 0000000..473800b --- /dev/null +++ b/.derived/spec-registry/by-spec/012-hash-stability-gate.json @@ -0,0 +1,66 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "011-canonical-encoding" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "fuzz/Cargo.toml" + }, + { + "kind": "file", + "path": "fuzz/fuzz_targets/codec_roundtrip.rs" + }, + { + "kind": "file", + "path": "fuzz/fuzz_targets/canonical_idempotent.rs" + }, + { + "kind": "file", + "path": "fuzz/fuzz_targets/value_ordering.rs" + }, + { + "kind": "file", + "path": "fuzz/corpus-seeds/" + }, + { + "kind": "file", + "path": "crates/hqgit-types/tests/golden.rs" + }, + { + "kind": "file", + "path": ".github/workflows/hash-stability.yml" + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "012-hash-stability-gate", + "implementation": "pending", + "kind": "tooling", + "risk": "critical", + "sectionHeadings": [ + "012: Hash stability gate", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/012-hash-stability-gate/spec.md", + "status": "approved", + "summary": "The gate that makes constitution VIII enforceable rather than aspirational. A standalone cargo-fuzz workspace with three targets over the canonical codec (decode-encode round trip, encode idempotence, key ordering) and a committed seed corpus; a golden test that walks every vector under the vector corpus and asserts byte and hash identity; and a CI workflow that runs the golden walk on Linux, macOS, and Windows, folds every vector file into one tree digest, asserts the digests agree across the matrix, and runs each fuzz target nightly with crash artifacts uploaded. A vector change fails the gate unless the PR carries a Schema-Major line and the amendment to the encoding spec: a human decision, never a regenerate.\n", + "title": "Hash stability gate: fuzz targets, the golden-vector walk, and the cross-platform CI matrix" + }, + "shardHash": "763e22eb7475f5b76e8ac96def85a121051e4ec77c9dc6ef2b92355ac892794d", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/013-object-store.json b/.derived/spec-registry/by-spec/013-object-store.json new file mode 100644 index 0000000..537bb8c --- /dev/null +++ b/.derived/spec-registry/by-spec/013-object-store.json @@ -0,0 +1,77 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "011-canonical-encoding" + ], + "domain": "l0-objects", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/object.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/store.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/memory.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/local.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/tests/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "013-object-store", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "013: Object store", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/013-object-store/spec.md", + "status": "approved", + "summary": "L0 of the layer model: the content-addressed store every other layer reads and writes through. This spec founds hqgit-object with the object kinds (Blob, Tree, RepoGenesis) as canonical envelopes, the ObjectStore trait whose reads verify the hash before returning a byte, an in-memory backend for tests and a redb-backed local backend under the repository's .hq directory. Objects are immutable by construction, so every cache layer above them is trivially correct; deletion exists in the trait only as the erasure path spec 020 completes with a capability token. Chunking (014), verified streaming (015), and the remote backend (016) extend this crate without changing what a stored object is.\n", + "title": "Object store: content-addressed blob and tree objects, the ObjectStore trait, memory and local backends" + }, + "shardHash": "55a8be283e0331280d1659481c690804fd43063698650269368ff773badf9477", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/014-content-defined-chunking.json b/.derived/spec-registry/by-spec/014-content-defined-chunking.json new file mode 100644 index 0000000..16b50b6 --- /dev/null +++ b/.derived/spec-registry/by-spec/014-content-defined-chunking.json @@ -0,0 +1,89 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "013-object-store" + ], + "domain": "l0-objects", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-object/src/chunk.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/merkle.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/tests/chunk.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/testdata/chunk/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/object.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "014-content-defined-chunking", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "014: Content-defined chunking", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/014-content-defined-chunking/spec.md", + "status": "approved", + "summary": "Large files are the general path with different chunk statistics, not a special case bolted on later. This spec adds FastCDC content-defined chunking with parameters frozen forever (so chunk boundaries are stable across releases and replicas), the BlobManifest object that names a chunked blob by the list of its chunk ids, the rule that content at or below one chunk stays inline, a binary Merkle tree over chunk hashes with its own domain, and the put_blob and get_blob entry points that hide the split from every caller. The manifest id is the identity of a chunked blob; the whole-content BLAKE3 hash is recorded alongside for the git bridge and the BAO outboard alignment spec 015 needs.\n", + "title": "Content-defined chunking: FastCDC with frozen parameters and the chunked blob manifest" + }, + "shardHash": "317070ed9ee4aa3edbd8bac6c7c90b754704b4245e09325122fe6329ca206622", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/015-verified-streaming.json b/.derived/spec-registry/by-spec/015-verified-streaming.json new file mode 100644 index 0000000..dc01edf --- /dev/null +++ b/.derived/spec-registry/by-spec/015-verified-streaming.json @@ -0,0 +1,101 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "014-content-defined-chunking" + ], + "domain": "l0-objects", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-object/src/bao.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/fetch.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/tests/bao.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/store.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/local.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/memory.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "015-verified-streaming", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "015: Verified streaming", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/015-verified-streaming/spec.md", + "status": "approved", + "summary": "The reason BLAKE3 was chosen over SHA-256: the BAO tree gives chunk-level verification and range proofs, so partial and lazy fetch are verifiable by construction rather than trusted. This spec stores a BAO outboard encoding beside every Raw object, adds a range read to the store whose result type cannot exist without a verified proof, defines the ObjectSource seam a remote (016, 111) implements to serve slices with proofs, and a lazy fetch API that verifies with bounded memory and never lets a partial object into the store as if it were whole. Tampered slices are rejected at the byte where the proof fails.\n", + "title": "Verified streaming: BAO outboard trees, range proofs, and lazy fetch that never trusts a byte" + }, + "shardHash": "b42dc7e5310ce6189ef2464f73cec3475a6cf341bb560ca276c5c83c8a606a16", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/016-remote-object-backend.json b/.derived/spec-registry/by-spec/016-remote-object-backend.json new file mode 100644 index 0000000..1cbc498 --- /dev/null +++ b/.derived/spec-registry/by-spec/016-remote-object-backend.json @@ -0,0 +1,78 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "013-object-store", + "015-verified-streaming" + ], + "domain": "l0-objects", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-object/src/s3.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/layered.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/tests/s3.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "016-remote-object-backend", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "016: Remote object backend", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/016-remote-object-backend/spec.md", + "status": "approved", + "summary": "Thesis §4.1: local index in redb, remote in any S3-compatible store, and immutability makes every cache layer trivially correct. This spec adds an ObjectStore over any S3-compatible endpoint through the object_store crate, with a hash-sharded key layout, outboard sidecars, and conditional puts for idempotency; an ObjectSource over the same endpoint so verified range fetches (015) work against remote objects; and a LayeredStore that reads through a local store to a remote one and writes through to both. Negative caching is forbidden by design: content is immutable, presence is not, so a miss is retried and never memoized.\n", + "title": "Remote object backend: an S3-compatible store and the layered read-through cache" + }, + "shardHash": "37ffd22af30d0626741a4d676eb87a95b74dc62b7b8d1f9642def13ee20f1e75", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/017-ledger-entry-dag.json b/.derived/spec-registry/by-spec/017-ledger-entry-dag.json new file mode 100644 index 0000000..efd0f68 --- /dev/null +++ b/.derived/spec-registry/by-spec/017-ledger-entry-dag.json @@ -0,0 +1,85 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "011-canonical-encoding", + "013-object-store" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-ledger/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/entry.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/dag.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/verify.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/tests/" + }, + { + "kind": "file", + "path": "fuzz/fuzz_targets/entry_hash_stable.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "011-canonical-encoding", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + }, + { + "nature": "additive", + "spec": "012-hash-stability-gate", + "unit": { + "kind": "file", + "path": "fuzz/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "017-ledger-entry-dag", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "017: Ledger entry and the hash-linked DAG", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/017-ledger-entry-dag/spec.md", + "status": "approved", + "summary": "The core primitive of the platform: a ledger entry is { parents, issuer, hlc, payload, sig }, a hash-linked DAG rather than a linear log because concurrent authors are the normal case. This spec founds hqgit-ledger and fixes, permanently, the entry's canonical shape, its signing preimage, and its hash; the in-memory DAG with append and head tracking; and chain verification (hash links, parent presence, signature through an issuer resolver seam, clock monotonicity). Nothing here persists (021) or orders (018) or interprets payloads (019, 023): it is the substrate those specs build on, and the golden entry vectors it freezes are the record every later release must reproduce byte for byte.\n", + "title": "Ledger entry and the hash-linked DAG: shape, signing bytes, append, verify" + }, + "shardHash": "9e2807f788d0942703eb415aa049090bb0d085a2bb413fa4d218b39f7aec5512", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/018-deterministic-total-order.json b/.derived/spec-registry/by-spec/018-deterministic-total-order.json new file mode 100644 index 0000000..5d21a5b --- /dev/null +++ b/.derived/spec-registry/by-spec/018-deterministic-total-order.json @@ -0,0 +1,60 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "017-ledger-entry-dag" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-ledger/src/order.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/clock.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/tests/order.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "017-ledger-entry-dag", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "018-deterministic-total-order", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "018: Deterministic total order and clock generation", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/018-deterministic-total-order/spec.md", + "status": "approved", + "summary": "Concurrent facts merge by set union under a deterministic total order, topological with a tiebreak on hash: that sentence from the thesis is what lets two replicas holding the same entries fold to the same state without coordination. This spec fixes the order (a Kahn sort whose ready set is ordered by hybrid logical clock then entry hash), its incremental form from a checkpoint, and the fold cursor projections resume from. It also owns the one place in the ledger crate that reads a clock: the hybrid logical clock generator, with an injected clock source, a drift bound, and the guarantee that it is never called on a hashing path.\n", + "title": "Deterministic total order over the DAG and hybrid logical clock generation" + }, + "shardHash": "1e1bb96a09a5e12ec25e2cb813f85d4892844719d90b64518a393a54776ab223", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/019-facts-and-derived-state.json b/.derived/spec-registry/by-spec/019-facts-and-derived-state.json new file mode 100644 index 0000000..330032c --- /dev/null +++ b/.derived/spec-registry/by-spec/019-facts-and-derived-state.json @@ -0,0 +1,80 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "018-deterministic-total-order" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-ledger/src/facts.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/derived.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/crdt/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/crdt/lww.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/crdt/sequence.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/tests/facts.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "017-ledger-entry-dag", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "017-ledger-entry-dag", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "019-facts-and-derived-state", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "019: Facts and derived state", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/019-facts-and-derived-state/spec.md", + "status": "approved", + "summary": "The separation the thesis calls critical: facts are immutable events that never conflict and merge by set union; derived state is the mutable projection and the only thing that needs convergence. This spec fixes the fact envelope every entry payload decodes to, the registry seam through which domain crates declare fact kinds and validators while unknown kinds stay preserved and opaque, the last-writer-wins register keyed by hybrid logical clock that every derived field uses unless a spec argues otherwise, the fold that rebuilds any derived state from zero, and the sequence CRDT interface reserved for collaborative text and nothing else. It keeps the CRDT surface near five percent of the domain, which is the difference between a system that can be reasoned about and one that cannot.\n", + "title": "Facts and derived state: the immutable fact envelope, the registry seam, LWW registers, and the reserved sequence CRDT" + }, + "shardHash": "880859432ca555cf0c53073356eabee3a4b43ed4f96df0f8a67330b2819d4c50", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/020-commitments-and-tombstones.json b/.derived/spec-registry/by-spec/020-commitments-and-tombstones.json new file mode 100644 index 0000000..bcbcaa6 --- /dev/null +++ b/.derived/spec-registry/by-spec/020-commitments-and-tombstones.json @@ -0,0 +1,113 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "019-facts-and-derived-state" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-ledger/src/commitment.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/tombstone.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-object/src/encrypt.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/tests/erasure.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "017-ledger-entry-dag", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/store.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/local.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/src/memory.rs" + } + }, + { + "nature": "additive", + "spec": "013-object-store", + "unit": { + "kind": "file", + "path": "crates/hqgit-object/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "020-commitments-and-tombstones", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "020: Commitments and tombstones", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/020-commitments-and-tombstones/spec.md", + "status": "approved", + "summary": "The erasure design decided at entry one because it cannot be retrofitted: the signed log contains commitments (content identifiers), never user content; content lives in the object store, encrypted per namespace where it must be; deletion removes the blob and appends a tombstone over the commitment; the chain stays verifiable and the content is genuinely gone. This spec fixes payload resolution with its three honest answers (present, erased, missing), the namespace encryption envelope whose commitment is over plaintext while the store holds ciphertext, the tombstone fact, the erase operation, and the unforgeable capability that gates it. Every projection must render an erased payload as erased and never fail.\n", + "title": "Commitments and tombstones: content indirection, per-namespace encryption, and erasure that keeps the chain verifiable" + }, + "shardHash": "6c17c8a4192342a92b007b2bc9b4117de9ed7fe8aee63be6ab14485313583916", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/021-local-repository.json b/.derived/spec-registry/by-spec/021-local-repository.json new file mode 100644 index 0000000..6d72b61 --- /dev/null +++ b/.derived/spec-registry/by-spec/021-local-repository.json @@ -0,0 +1,81 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "020-commitments-and-tombstones" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-ledger/src/repo.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/store.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/src/namespace.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-ledger/tests/repo.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "017-ledger-entry-dag", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "017-ledger-entry-dag", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "021-local-repository", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "021: Local repository", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/021-local-repository/spec.md", + "status": "approved", + "summary": "Where the ledger meets a disk. This spec fixes the .hq directory inside a working tree (the redb-backed entry store, the object store, the local identity, the config file), the EntryStore trait with atomic fsynced appends, the Repository type with init, open, and the one append path every fact takes (encode, put the payload object, build the entry on the current heads, stamp the clock, sign, append), and namespaces as facts with main and quarantine present from init. Write ordering is chosen so a crash can leave an orphan object but never a dangling entry. This is the substrate the CLI (032), the server (090), and the sync protocol (110) all share, which is what makes offline-first true.\n", + "title": "Local repository: the .hq layout, the persistent entry store, namespaces, and the append path" + }, + "shardHash": "185bd0c88a2d5b76f785cdb4d789480eed84cb98c8d8729c1b2137b06986912b", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/023-domain-fact-vocabulary.json b/.derived/spec-registry/by-spec/023-domain-fact-vocabulary.json new file mode 100644 index 0000000..095ebf5 --- /dev/null +++ b/.derived/spec-registry/by-spec/023-domain-fact-vocabulary.json @@ -0,0 +1,72 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "019-facts-and-derived-state" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/validate.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/ids.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "011-canonical-encoding", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "023-domain-fact-vocabulary", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "023: Domain fact vocabulary", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/023-domain-fact-vocabulary/spec.md", + "status": "approved", + "summary": "Founds hqgit-domain, the L2 crate every collaboration noun lives in, and fixes the fact vocabulary the ledger carries: one DomainFact variant per kind (change, review, attestation, issue, ownership, mirror), each with a frozen namespaced kind string, a versioned body schema encoded through the spec 011 canonical codec, and unknown-field preservation. Ids are never counters: a ChangeId, RevisionId, ThreadId, CommentId, IssueId, or AttestationId is the BLAKE3 hash of the canonical bytes of the fact that brought it into being, so an id is content-derived and identical on every replica. Structural validation runs on decode, before any fold sees a fact, and the crate registers every kind with spec 019's FactRegistry. Later specs add variants and semantics by extending facts.rs; this spec is the vocabulary, not the behavior.\n", + "title": "Domain fact vocabulary: the typed facts, their ids, and their validation" + }, + "shardHash": "2eabf048b41b0ca9d9cad35e177f6bd6db6b3f3046fb5d5a48996cd0b02878d8", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/024-change-and-revision.json b/.derived/spec-registry/by-spec/024-change-and-revision.json new file mode 100644 index 0000000..dd0d141 --- /dev/null +++ b/.derived/spec-registry/by-spec/024-change-and-revision.json @@ -0,0 +1,68 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "023-domain-fact-vocabulary" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/change.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/revision.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/change.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "024-change-and-revision", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "024: Change and Revision", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/024-change-and-revision/spec.md", + "status": "approved", + "summary": "The unit of change is a Change with stable identity and an ordered sequence of Revisions, never a mutable branch pointer (design §1.1 point 2; Gerrit's Change-Id, then jj and Sapling). A revision is a tree hash plus its base; a force-push is a new revision and destroys nothing; the revision number is derived from the ledger's total order so every replica numbers identically. This spec adds the Change and Revision types, the pure fact builders the CLI and the server call, and the ChangeView fold with its state machine (Open, Merged, Abandoned) and lineage queries. What changed since a reviewer last looked becomes a query in spec 050; this spec makes the sequence it runs over exist.\n", + "title": "Change and Revision: stable change identity over an ordered revision sequence" + }, + "shardHash": "be8d39aeecbe01bfab06d66196ccc0b8572ba08bf27c70be3fdea6e9a23adcf8", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/025-semantic-anchors.json b/.derived/spec-registry/by-spec/025-semantic-anchors.json new file mode 100644 index 0000000..74570ca --- /dev/null +++ b/.derived/spec-registry/by-spec/025-semantic-anchors.json @@ -0,0 +1,93 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "024-change-and-revision" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/anchor.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/rust.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/typescript.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/syntax/text.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/anchor.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/testdata/anchors/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "025-semantic-anchors", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "025: Semantic anchors", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/025-semantic-anchors/spec.md", + "status": "approved", + "summary": "Review is anchored to text in every incumbent, which is why a rebase orphans every comment. hqgit anchors a location to a syntax node: the path, the named-node path from the root, and the BLAKE3 hash of the node's content, with a text position as the fallback of last resort. Resolution against a new tree answers Exact, Moved (the same content elsewhere, rename-tolerant), Text (parsing failed or the language is unknown), or Lost, never a silent guess. Rust and TypeScript are the first languages, with tree-sitter grammars pinned exact so spans are identical across releases; everything else degrades to the text path, visibly. This is the primitive spec 026's threads and spec 051's deltas build on.\n", + "title": "Semantic anchors: comments that survive rebase by resolving against content" + }, + "shardHash": "9b1c529823301dbc04c35b732a369872b0bbd687d8c90fc59719dd6966d7e360", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/026-review-threads.json b/.derived/spec-registry/by-spec/026-review-threads.json new file mode 100644 index 0000000..2b3d46a --- /dev/null +++ b/.derived/spec-registry/by-spec/026-review-threads.json @@ -0,0 +1,64 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "025-semantic-anchors" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/review.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/review.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "026-review-threads", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "026: Review threads", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/026-review-threads/spec.md", + "status": "approved", + "summary": "The review conversation as facts: a thread is opened on a change at a revision, optionally anchored (025), comments are posted with their bodies in the object store so they are erasable (constitution X), and resolution is a last-writer-wins register (constitution VII). Threads are re-anchored against every new revision, and a reviewer sees \"moved\" and \"lost\" rather than an orphaned comment. Approval is not a comment: it is a fact that references an Attestation (027) with the approval predicate, so \"who approved which revision\" is a signed, verifiable claim and never prose. This spec adds the ThreadView and ApprovalView folds and the pure builders the CLI (033) and the server (093) call.\n", + "title": "Review threads: anchored comments, resolution state, and approvals as attestations" + }, + "shardHash": "9ddcedb1e1b077bb9656463410503d23ad39785dd02e598824da077844fe6e1f", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/027-attestation-primitive.json b/.derived/spec-registry/by-spec/027-attestation-primitive.json new file mode 100644 index 0000000..161987d --- /dev/null +++ b/.derived/spec-registry/by-spec/027-attestation-primitive.json @@ -0,0 +1,80 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "023-domain-fact-vocabulary" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/attestation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/attestation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/testdata/attestations/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "nature": "additive", + "spec": "011-canonical-encoding", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "027-attestation-primitive", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "027: The attestation primitive", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/027-attestation-primitive/spec.md", + "status": "approved", + "summary": "The single most valuable simplification in the design (thesis §4.3, constitution IX): every form of evidence is one primitive, Attestation { subject, predicate, issuer, claim, sig }. Human approval, build provenance, test result, static finding, license scan, policy evaluation, mirrored external state, semantic delta, and agent action are the same shape, stored the same way, verified the same way, and fed to policy the same way. This spec fixes the attestation's canonical bytes, signing preimage, and id (frozen with vectors), the predicate registry with claim validators for the built-in predicates and reserved slots later specs fill, signature verification, and a lossless mapping to in-toto Statement v1 so SLSA tooling reads hqgit evidence. Requests to special-case a predicate are refused by construction: there is no other noun.\n", + "title": "The attestation primitive: one signed evidence shape, a predicate registry, in-toto interop" + }, + "shardHash": "71a6d4e82b699973acd82ad5a3638c60044a2b2ffc57270a351e8bb9968244df", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/028-issues-and-derived-state.json b/.derived/spec-registry/by-spec/028-issues-and-derived-state.json new file mode 100644 index 0000000..6f05f98 --- /dev/null +++ b/.derived/spec-registry/by-spec/028-issues-and-derived-state.json @@ -0,0 +1,64 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "023-domain-fact-vocabulary" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/issue.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/issue.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "028-issues-and-derived-state", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "028: Issues and derived state", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/028-issues-and-derived-state/spec.md", + "status": "approved", + "summary": "The worked example of constitution VII. An issue is a sequence of immutable facts (opened, field set, closed, link added) and its visible state is derived: title, state, assignee, and milestone are last-writer- wins registers over the hybrid logical clock, and labels are an add-wins observed-remove set whose removals name the additions they observed. Nothing is a counter, nothing is a row, and the IssueView rebuilds from zero on every replica to the same answer. This is the shape every later mutable noun copies, and it is what makes the GitHub mirror (040) a fact importer rather than a schema migration.\n", + "title": "Issues as facts with converging derived state: registers and an add-wins label set" + }, + "shardHash": "5d02100262cc2f7d912145f6bf35c7ff6f2f50554b868c062678875951d223e6", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/031-git-object-bridge.json b/.derived/spec-registry/by-spec/031-git-object-bridge.json new file mode 100644 index 0000000..d166899 --- /dev/null +++ b/.derived/spec-registry/by-spec/031-git-object-bridge.json @@ -0,0 +1,85 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "014-content-defined-chunking" + ], + "domain": "l0-objects", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-git/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-git/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-git/src/import.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-git/src/export.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-git/src/mapping.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-git/src/worktree.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-git/src/mode.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-git/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-git/testdata/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "031-git-object-bridge", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "031: Git object bridge", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/031-git-object-bridge/spec.md", + "status": "approved", + "summary": "Git compatibility is a hard requirement of the wedge (thesis D16), so the object store must speak git without becoming git. This spec founds hqgit-git, the pure-Rust bridge over gix: import a git commit's tree and blobs into hqgit objects (blobs through the chunking path so large files are the general case), export an hqgit tree back as git objects, keep a bidirectional oid map for both SHA-1 and SHA-256 repositories, and snapshot a working tree into a tree Cid without touching git's index. Git commits are not hqgit objects: a Revision references a tree, and the git commit oid rides along as provenance. Everything here is deterministic, never shells out, and never writes a git ref.\n", + "title": "Git object bridge: gix import and export, the bidirectional oid map, worktree snapshots" + }, + "shardHash": "98b5c0a68948bf529ca5692ad28c1b3bd1271734437c0b4ae3ee23020754ceb0", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/032-cli-skeleton.json b/.derived/spec-registry/by-spec/032-cli-skeleton.json new file mode 100644 index 0000000..7aee482 --- /dev/null +++ b/.derived/spec-registry/by-spec/032-cli-skeleton.json @@ -0,0 +1,85 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "021-local-repository" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_init.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_status.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_log.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/config.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/output.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "032-cli-skeleton", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "032: The hq binary", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/032-cli-skeleton/spec.md", + "status": "approved", + "summary": "The hq binary is the first client of the ledger and, by thesis §5, runs the same ledger implementation the server will. This spec founds hqgit-cli: a clap frame whose subcommands are one module each, exit codes mapped in exactly one place through Error::exit_code, a --json mode on every read verb that emits canonical sorted-key JSON, layered configuration (user and repository), and the three verbs that prove the local repository works: hq init creates .hq/ with a local ed25519 identity and the genesis entry, hq status reports heads and identity, and hq log renders entries in total order. Later specs add verbs by extending main.rs and cli.rs; nothing here talks to a network or to git.\n", + "title": "The hq binary: clap frame, exit codes, JSON output, init, status, log" + }, + "shardHash": "a71e01397dfb71e604592716a4d26884e39f9c305b90848b6a24a8db35f6a244", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/033-cli-offline-review.json b/.derived/spec-registry/by-spec/033-cli-offline-review.json new file mode 100644 index 0000000..a389713 --- /dev/null +++ b/.derived/spec-registry/by-spec/033-cli-offline-review.json @@ -0,0 +1,83 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "032-cli-skeleton", + "031-git-object-bridge", + "026-review-threads", + "027-attestation-primitive" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_change.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_review.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/review.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/testdata/review/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "033-cli-offline-review", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "033: Offline review from the CLI", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/033-cli-offline-review/spec.md", + "status": "approved", + "summary": "The wave 1 milestone (thesis §6, step 1): prove offline review against a plain git repository. hq change new opens a Change with a stable id; hq change submit snapshots the working tree through the git bridge into a Revision whose base is the imported HEAD tree; hq review comment anchors a thread to a semantic location that survives rebase; hq review approve issues an approval Attestation signed by the local identity; hq review show re-anchors every thread against the latest revision and marks what moved or was lost. No network, no server, no git ref moves. The evidence this produces is what spec 034 verifies, and the transcript it leaves is the fixture every later client must reproduce.\n", + "title": "Offline review from the CLI: changes, revisions, anchored threads, approvals" + }, + "shardHash": "69d8e8e9590cf4cdcae7d49432e721904f5f09bd3a0b0e91ed13930caeca0265", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/034-cli-attest-and-verify.json b/.derived/spec-registry/by-spec/034-cli-attest-and-verify.json new file mode 100644 index 0000000..281fa03 --- /dev/null +++ b/.derived/spec-registry/by-spec/034-cli-attest-and-verify.json @@ -0,0 +1,68 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "033-cli-offline-review" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_attest.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_verify.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/verify.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + } + ], + "extraFrontmatter": { + "wave": 1 + }, + "id": "034-cli-attest-and-verify", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "034: hq attest and hq verify", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/034-cli-attest-and-verify/spec.md", + "status": "approved", + "summary": "Wave 1 ends when evidence produced offline verifies offline. hq attest issues an Attestation over any subject hash with any predicate, validating the claim against the registry when the predicate is known and carrying it verbatim when it is not; hq attest list enumerates the evidence on a subject. hq verify runs spec 017's chain verification against the repository with a resolver seeded from the keys the ledger itself records, reports every failure rather than the first, and exits 1 on any; hq verify attestation checks one attestation's signature and claim shape. Both verbs are pure reads apart from the fact hq attest appends, and both emit machine-readable reports so the orchestrator and CI can consume them.\n", + "title": "hq attest and hq verify: issue any attestation, verify the chain and its evidence offline" + }, + "shardHash": "871570a07f95a2ef10011a8b1a131ae36d3723e001e5303bfe367961985128b8", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/040-github-mirror-import.json b/.derived/spec-registry/by-spec/040-github-mirror-import.json new file mode 100644 index 0000000..240464d --- /dev/null +++ b/.derived/spec-registry/by-spec/040-github-mirror-import.json @@ -0,0 +1,100 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "028-issues-and-derived-state", + "026-review-threads", + "027-attestation-primitive", + "031-git-object-bridge" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-mirror/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/client.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/import.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/model.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/quarantine.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/idmap.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/testdata/github/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 2 + }, + "id": "040-github-mirror-import", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "040: GitHub mirror import", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/040-github-mirror-import/spec.md", + "status": "approved", + "summary": "The wedge in code (thesis D16, constitution XIV): value for users who migrate nothing. This spec founds hqgit-mirror and its GitHub import: a client seam over the REST API with a recorded-fixture implementation for tests, and an importer that turns issues, pull requests, review comments, approvals, check conclusions, and commits into facts in the repository's quarantine namespace. Pull requests become Changes with one Revision per head commit through the git bridge; review comments become threads anchored by path and line and then re-anchored semantically; approvals and check results become mirror attestations whose claims name the external source. An id map makes re-import idempotent. Nothing mirrored is trusted: promotion to the main namespace is spec 094's capability path.\n", + "title": "GitHub mirror import: issues, pull requests, reviews, and checks become quarantined facts" + }, + "shardHash": "5683e97f8c338270557cdb277578fb77eaa21ef20f2b6961824e4b0904ba1d98", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/041-github-mirror-export.json b/.derived/spec-registry/by-spec/041-github-mirror-export.json new file mode 100644 index 0000000..f0b4447 --- /dev/null +++ b/.derived/spec-registry/by-spec/041-github-mirror-export.json @@ -0,0 +1,68 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "040-github-mirror-import" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/export.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/src/reconcile.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-mirror/tests/export.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "040-github-mirror-import", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "040-github-mirror-import", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/github/client.rs" + } + } + ], + "extraFrontmatter": { + "wave": 2 + }, + "id": "041-github-mirror-export", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "041: GitHub mirror export and reconciliation", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/041-github-mirror-export/spec.md", + "status": "approved", + "summary": "The other direction of the wedge: comments, thread resolutions, approvals, and issue field changes made in the ledger are pushed to GitHub through the same client seam, so a reviewer who works offline in hq still shows up on the pull request. Reconciliation is ordered by the hybrid logical clock, idempotent through the id map, and loop-safe: an item the exporter created is recognized by its external id on the next import and produces no new fact. Where GitHub and the ledger disagree on a derived field, the newer observation wins the field and both sides are recorded as facts, so nothing is lost and the disagreement is auditable. A dry-run mode prints the plan without writing.\n", + "title": "GitHub mirror export and reconciliation: local facts back to GitHub, loop-safe and idempotent" + }, + "shardHash": "ded46c9db78b741e0a8c8fc9f850e3fb784782b7efc94973071623371d06b15d", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/042-mirror-sync-command.json b/.derived/spec-registry/by-spec/042-mirror-sync-command.json new file mode 100644 index 0000000..d67c113 --- /dev/null +++ b/.derived/spec-registry/by-spec/042-mirror-sync-command.json @@ -0,0 +1,85 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "041-github-mirror-export", + "032-cli-skeleton" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-mirror/src/sync.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_mirror.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/mirror.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "040-github-mirror-import", + "unit": { + "kind": "file", + "path": "crates/hqgit-mirror/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 2 + }, + "id": "042-mirror-sync-command", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "042: hq mirror", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/042-mirror-sync-command/spec.md", + "status": "approved", + "summary": "The user-facing end of wave 2: hq mirror add registers a GitHub repository as a source, hq mirror sync runs import then export once or in a watch loop, and hq mirror status shows cursors and the last report. The sync driver persists cursors in the id map database so every run resumes where the last one stopped, backs off exponentially on rate limits and transient errors and never busy-loops, and prints a report of facts imported, items exported, items skipped, and disagreements reconciled. The token comes from the environment or the user config and never enters the ledger, the repository config, or any output.\n", + "title": "hq mirror: the sync loop with cursors, backoff, and reports" + }, + "shardHash": "2c5a9ac0fc5b840a8f7d79726811806c4651895fd73d0e5a75dfd3d9c31f7828", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/050-stacked-changes.json b/.derived/spec-registry/by-spec/050-stacked-changes.json new file mode 100644 index 0000000..19e42f1 --- /dev/null +++ b/.derived/spec-registry/by-spec/050-stacked-changes.json @@ -0,0 +1,76 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "024-change-and-revision" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/stack.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/interdiff.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/stack.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "nature": "additive", + "spec": "011-canonical-encoding", + "unit": { + "kind": "file", + "path": "crates/hqgit-types/testdata/vectors/" + } + } + ], + "extraFrontmatter": { + "wave": 3 + }, + "id": "050-stacked-changes", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "050: Stacked changes", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/050-stacked-changes/spec.md", + "status": "approved", + "summary": "Graphite exists because the branch pointer is the wrong unit of change (design §1.1 point 2). With Change and Revision in place (024), a stack is nothing more than a relation between changes: change.depends_on and change.dependency_dropped facts converge per edge as a last-writer-wins register, StackView folds them, and the stack order is a topological sort with ties broken on ChangeId so every replica prints the same stack. A restack is a pure plan (which revision goes onto which base, in which order) that the CLI executes through the git bridge; the domain never rebases. Interdiff answers \"what changed since I last looked\" by comparing what two revisions of one change each did to their base at the tree level, so base movement alone produces an empty interdiff.\n", + "title": "Stacked changes: dependency facts, a deterministic stack order, restack plans, and interdiff" + }, + "shardHash": "804873832f8b6214c2005572e31ee2af57d92c7698f5f78b2ba47a269e58ebe7", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/051-semantic-deltas.json b/.derived/spec-registry/by-spec/051-semantic-deltas.json new file mode 100644 index 0000000..5b21884 --- /dev/null +++ b/.derived/spec-registry/by-spec/051-semantic-deltas.json @@ -0,0 +1,99 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "027-attestation-primitive", + "050-stacked-changes", + "025-semantic-anchors" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/api_surface.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/dependencies.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/src/delta/capabilities.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/delta.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/testdata/deltas/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "027-attestation-primitive", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 3 + }, + "id": "051-semantic-deltas", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "051: Semantic deltas", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/051-semantic-deltas/spec.md", + "status": "approved", + "summary": "The line diff is the lowest-value view of a change (design §1.1 point 3). For a pair of trees this spec computes the three views a reviewer wants first: the public API surface delta (items added, removed, and re-signatured, via tree-sitter for Rust and TypeScript), the dependency delta (Cargo and npm manifests and lockfiles, new registry or git sources called out), and the capability delta (network, filesystem, process, environment, secret, unsafe, and dynamic-code access by static heuristics that deliberately over-report). Each delta is issued as an Attestation under hqgit/semantic-delta/v1 (constitution IX), so policy and the UI consume evidence rather than a rendering. The result is deterministic for two trees and a pinned analyzer, and a file it cannot analyze is listed as unsupported, never silently skipped.\n", + "title": "Semantic deltas: API surface, dependency, and capability changes as attestations" + }, + "shardHash": "da6a6d4b527d125bc395c9ee8887565ba1cdace170b5c53ce57aa11efb9114ff", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/052-semantic-conflicts.json b/.derived/spec-registry/by-spec/052-semantic-conflicts.json new file mode 100644 index 0000000..e0f0822 --- /dev/null +++ b/.derived/spec-registry/by-spec/052-semantic-conflicts.json @@ -0,0 +1,77 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "051-semantic-deltas" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/conflict.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/conflict.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/testdata/conflicts/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 3 + }, + "id": "052-semantic-conflicts", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "052: Semantic conflicts", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/052-semantic-conflicts/spec.md", + "status": "approved", + "summary": "Conflict detection should be semantic, not textual (design §1.1 point 3). Given two revisions and their common base tree, this spec computes a deterministic ConflictReport from the 051 deltas and the 025 syntax trees: SameSymbolEdited when both sides edit one definition, SignatureChangedAndCalled when one side changes a signature the other side calls, DependencyVersionDiverged when both move one dependency to different versions or sources, and CapabilityOverlap when both gain the same capability in one file. Textual overlap is computed too, from line hunks, but reported as its own Text kind so a merge that is clean in git and broken in meaning, or the reverse, is never mistaken for the other. Two changes editing different functions in one file produce no conflict of either kind; changing a signature the other calls does.\n", + "title": "Semantic conflicts: pairwise conflict detection over deltas and anchors, with text kept separate" + }, + "shardHash": "1d9cfb80ba37f342e409032f45a1a0e0df1192f374926fd0896f0eeafebcaeeb", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/060-identity-and-key-rotation.json b/.derived/spec-registry/by-spec/060-identity-and-key-rotation.json new file mode 100644 index 0000000..3b66260 --- /dev/null +++ b/.derived/spec-registry/by-spec/060-identity-and-key-rotation.json @@ -0,0 +1,66 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "019-facts-and-derived-state" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/identity.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/rotation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/resolver.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/testdata/identity/" + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "060-identity-and-key-rotation", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "060: Identity and key rotation", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/060-identity-and-key-rotation/spec.md", + "status": "approved", + "summary": "Authentication and identity are different problems (thesis §4.5). Login is OIDC (061); durable identity is a keypair whose rotation history is a sequence of signed facts in the ledger, so a signature made under a key that was later rotated or revoked still verifies at the time it was made. This spec founds hqgit-trust and fixes the three identity facts (identity.created, identity.key_rotated, identity.key_revoked), the IdentityId that every Principal id in 010 resolves to, the IdentityView fold that answers key_valid_at, and the RotationAwareResolver that plugs into the ledger's IssuerResolver seam (017) so chain verification stops trusting a static key table. Constitution XI: trust is checkable, not decorative.\n", + "title": "Identity as a keypair with a rotation chain recorded in the ledger" + }, + "shardHash": "fb8d13d4bfb741c0acf887ddfed767ab87f2b472e5be017d46c021a83e0b2635", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/061-oidc-login.json b/.derived/spec-registry/by-spec/061-oidc-login.json new file mode 100644 index 0000000..d522928 --- /dev/null +++ b/.derived/spec-registry/by-spec/061-oidc-login.json @@ -0,0 +1,81 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "060-identity-and-key-rotation" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-trust/src/oidc.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/session.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/tests/oidc.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/testdata/oidc/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "061-oidc-login", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "061: OIDC login", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/061-oidc-login/spec.md", + "status": "approved", + "summary": "Login is OIDC; identity is a keypair (060). This spec adds the OIDC client (discovery, authorization code with PKCE, the device authorization grant for the CLI) with an injected HTTP seam so every flow is testable against a mock issuer, and the binding that ties an OIDC subject to a ledger identity: an identity.binding_added fact signed by the identity's active key, so a login proves control of a subject the issuer vouches for and the fact proves control of a key the ledger already knows. Workforce federation is a per-repository allow-list of issuers, with Rauthy as the self-hosted reference. No password is handled anywhere in the crate.\n", + "title": "OIDC login: authorization code with PKCE, device flow, and subject binding" + }, + "shardHash": "8a7cdc60aaed5a43972d32b85b39cbfcb0f29398b38998b55d5f98cddb01917a", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/062-transparency-log.json b/.derived/spec-registry/by-spec/062-transparency-log.json new file mode 100644 index 0000000..8d7bee3 --- /dev/null +++ b/.derived/spec-registry/by-spec/062-transparency-log.json @@ -0,0 +1,72 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "060-identity-and-key-rotation" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/merkle.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/proofs.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/tlog/client.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/tests/tlog.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/testdata/tlog/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "062-transparency-log", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "062: Transparency log", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/062-transparency-log/spec.md", + "status": "approved", + "summary": "An append-only Merkle log in the RFC 6962 shape over BLAKE3, holding attestation ids and identity facts, with inclusion proofs, consistency proofs, and checkpoints signed by the log key and cosignable by witnesses. A signature proves who; the log proves that the signed thing was published and cannot later be quietly withdrawn or forked. This spec fixes the leaf and node hashing, the proof shapes, the checkpoint envelope, and the TlogClient seam with an in-process implementation; keyless signing (063) logs its certificates here and attestation verification (064) requires an inclusion proof when the repository policy demands one. The served log is a later extension.\n", + "title": "Transparency log: Merkle log, inclusion and consistency proofs, signed checkpoints" + }, + "shardHash": "3028e97b8ed0ef849215dad8af82de8b578f6d7b4feb075dfa78bfd136c08658", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/063-keyless-signing.json b/.derived/spec-registry/by-spec/063-keyless-signing.json new file mode 100644 index 0000000..8049b4b --- /dev/null +++ b/.derived/spec-registry/by-spec/063-keyless-signing.json @@ -0,0 +1,82 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "061-oidc-login", + "062-transparency-log" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-trust/src/keyless.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/bundle.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/tests/keyless.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/testdata/keyless/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "063-keyless-signing", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "063: Keyless signing", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/063-keyless-signing/spec.md", + "status": "approved", + "summary": "The Sigstore shape, self-hosted: a session proven by OIDC login (061) is exchanged for an X.509 certificate over an ephemeral ed25519 key that is valid for ten minutes, the certificate is published to the transparency log (062), and the attestation signed with that key carries a SignatureBundle (certificate chain plus the certificate's inclusion proof) in its extra map, so a verifier can check it long after the key is gone. This spec fixes the certificate profile, the issuer seam with an in-process reference issuer, the bundle's canonical form, keyless issuance, and the pure bundle verifier that spec 064 calls as one stage of its chain. The identity's long-lived key (060) remains the offline signing path; keyless is the default when a session exists.\n", + "title": "Keyless signing: short-lived certificates bound to an OIDC identity, the signature bundle, and bundle verification" + }, + "shardHash": "4409072d066b416364c2501285bdddc212b11c05980e37b8faf4d10aab3ebf80", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/064-attestation-verification.json b/.derived/spec-registry/by-spec/064-attestation-verification.json new file mode 100644 index 0000000..d73b334 --- /dev/null +++ b/.derived/spec-registry/by-spec/064-attestation-verification.json @@ -0,0 +1,78 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "060-identity-and-key-rotation", + "062-transparency-log", + "027-attestation-primitive" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-trust/src/verify.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/verified.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/src/verify_policy.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/tests/verify.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-trust/testdata/verify/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "060-identity-and-key-rotation", + "unit": { + "kind": "file", + "path": "crates/hqgit-trust/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "064-attestation-verification", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "064: Attestation verification", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/064-attestation-verification/spec.md", + "status": "approved", + "summary": "The one path by which an attestation becomes evidence. Spec 027 checks a signature against a verifier it is handed; this spec fixes the whole chain in one order: signature, issuer key valid at the attestation's clock (060), keyless bundle when present (063), transparency-log inclusion when the verify policy requires it (062), and claim shape against the predicate registry (027). The output is VerifiedAttestation, a type nothing outside this module can construct, gathered into a VerifiedAttestationSet that carries a per-item verdict and never hides a failure inside a partial success. That set is the only shape the policy engine (065), the action cache (071), quarantine promotion (094), and the projections (081) accept: verification happens here or it did not happen.\n", + "title": "Attestation verification: the fixed pipeline from signature to verified set" + }, + "shardHash": "eee71d6a4bbebbbd032a06d80411f0cb2c9bcd6bfd9aea654f86848d4e4850c2", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/065-policy-engine.json b/.derived/spec-registry/by-spec/065-policy-engine.json new file mode 100644 index 0000000..85da832 --- /dev/null +++ b/.derived/spec-registry/by-spec/065-policy-engine.json @@ -0,0 +1,82 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "027-attestation-primitive", + "024-change-and-revision" + ], + "domain": "l6-policy", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-policy/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/engine.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/abi.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/module.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/input.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/testdata/policies/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "065-policy-engine", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "065: Policy engine", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/065-policy-engine/spec.md", + "status": "approved", + "summary": "The merge predicate as the thesis states it: f(change, attestation_set, policy_version) -> Allow | Deny(reasons), deterministic and side-effect free. This spec founds hqgit-policy: a policy is a WebAssembly module stored as a content-addressed object whose Cid is its version; the engine is wasmtime configured for determinism (no clocks, no randomness, no filesystem, no network, no threads, fuel and memory limits, canonical NaNs); the ABI is one exported function taking the canonical bytes of a PolicyInput and returning the canonical bytes of a Verdict; and the input is built from domain types plus the attestation summaries of a verified set. Same input bytes and same module hash give the same output bytes, which is what makes a verdict replayable years later (067). Writing policies is the SDK's job (066); pinning them to a repository is 068's.\n", + "title": "Policy engine: deterministic WASM merge predicates, hash-pinned" + }, + "shardHash": "761c63fcd8334322b9f2165efc39a682d7380c2ae834fe1499f166ea16cafb16", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/066-policy-sdk.json b/.derived/spec-registry/by-spec/066-policy-sdk.json new file mode 100644 index 0000000..8e2bca1 --- /dev/null +++ b/.derived/spec-registry/by-spec/066-policy-sdk.json @@ -0,0 +1,92 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "065-policy-engine" + ], + "domain": "l6-policy", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/prelude.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/evidence.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/export.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/testing.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/allow_all.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/two_approvals.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/provenance_required.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/no_self_approval.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/tests/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "065-policy-engine", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/testdata/policies/" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "066-policy-sdk", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "066: Policy SDK", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/066-policy-sdk/spec.md", + "status": "approved", + "summary": "Policies must be unit-testable code, not settings, and a policy author should never touch the WASM ABI by hand. This spec founds hqgit-policy-sdk: a no_std-capable library that a policy crate depends on to write `fn evaluate(&PolicyInput) -> Verdict` in plain Rust, an `hqgit_policy!` macro that emits spec 065's exports around it, typed query helpers over the attestation summaries (approvals by distinct humans, provenance present, issuer kinds, predicate presence) with no floating point anywhere, and a native test harness so the same function runs under `cargo test` on the host and under wasmtime in the engine with identical verdicts. Four example policies are the fixtures the engine tests and later specs (103) reuse.\n", + "title": "Policy SDK: typed Rust policies compiled to WASM, testable natively" + }, + "shardHash": "778da60d8b8fff138d4afd679a158024be48c0e65b1e1a797b5e3237d319f2a0", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/067-policy-evaluation-attestation.json b/.derived/spec-registry/by-spec/067-policy-evaluation-attestation.json new file mode 100644 index 0000000..ca78580 --- /dev/null +++ b/.derived/spec-registry/by-spec/067-policy-evaluation-attestation.json @@ -0,0 +1,97 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "065-policy-engine", + "034-cli-attest-and-verify" + ], + "domain": "l6-policy", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-policy/src/evaluate.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/replay.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_policy.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/policy.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "065-policy-engine", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "027-attestation-primitive", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "067-policy-evaluation-attestation", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "067: Policy evaluation as evidence", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/067-policy-evaluation-attestation/spec.md", + "status": "approved", + "summary": "A merge decision is evidence, not a moment. Every engine evaluation (065) is recorded as an Attestation with predicate hqgit/policy-eval/v1 whose claim names the policy Cid, the input hash, the engine configuration digest, and the verdict, signed by the evaluating principal and appended as a fact. A gate consumes that attestation, never a live evaluation, so the decision that let a revision merge can be produced years later and replayed: this spec's replay rebuilds the input from the ledger, re-runs the pinned policy, and reports Match, Mismatch with the differing bytes named, or Unavailable when an input was erased. The CLI grows hq policy eval, replay, and build so the whole loop runs offline on a laptop before anything is pushed.\n", + "title": "Policy evaluation as evidence: the policy-eval attestation, replay, and hq policy" + }, + "shardHash": "a4632867ab97f1f511cf3c55b230c1d8526da2c5bb20fa55257c22637533df4d", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/068-policy-in-repo.json b/.derived/spec-registry/by-spec/068-policy-in-repo.json new file mode 100644 index 0000000..ea215c2 --- /dev/null +++ b/.derived/spec-registry/by-spec/068-policy-in-repo.json @@ -0,0 +1,85 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "067-policy-evaluation-attestation", + "021-local-repository" + ], + "domain": "l6-policy", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-policy/src/pin.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/src/scope.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-policy/tests/pin.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "065-policy-engine", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "nature": "additive", + "spec": "020-commitments-and-tombstones", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/tombstone.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_policy.rs" + } + } + ], + "extraFrontmatter": { + "wave": 4 + }, + "id": "068-policy-in-repo", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "068: Policy in the repository", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/068-policy-in-repo/spec.md", + "status": "approved", + "summary": "Where a repository's policy lives: in the ledger, as facts. A policy.pinned fact names a policy module Cid, a scope (a namespace or a path pattern), the clock from which it applies, and who pinned it; policy.unpinned retires one. The PolicyPinView fold answers active_policy(repo, at, scope): the policy that governs a revision is the one active at the revision's submission clock, so a decision recorded in wave 4 replays identically in wave 8 against the pin that was in force. There is no settings table to toggle, which is constitution XI made structural. The same fold is what lets a policy verdict mint the erase capability of spec 020 and what the agent authority of spec 100 compares its policy version against.\n", + "title": "Policy in the repository: pinned by fact, resolved by clock, no settings table" + }, + "shardHash": "cad3b03eac971560e9e08eabd1ce4a98ced4757102cfa22dfab86a9f45e7f09c", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/070-reapi-types.json b/.derived/spec-registry/by-spec/070-reapi-types.json new file mode 100644 index 0000000..4e89414 --- /dev/null +++ b/.derived/spec-registry/by-spec/070-reapi-types.json @@ -0,0 +1,93 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "013-object-store" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/build.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/proto/build/bazel/remote/execution/v2/remote_execution.proto" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/proto/build/bazel/semver/semver.proto" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/proto/google/" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/digest.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/action.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/cas_adapter.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/testdata/reapi/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "070-reapi-types", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "070: Remote Execution API types", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/070-reapi-types/spec.md", + "status": "approved", + "summary": "The evaluation plane speaks the Bazel Remote Execution API rather than a protocol of its own (thesis D9), so existing executors, clients, and caches interoperate on day one. This spec founds hqgit-eval with the vendored REAPI v2 protos compiled at build time, a BLAKE3 digest function that coincides with the hqgit object hash for raw blobs and carries an explicit mapping where the two encodings differ (Directory versus Tree), the pure construction of an Action from (repository state, target, toolchain) whose digest is the key the cache and the merge queue share, and the CAS adapter that presents the spec 013 object store as REAPI content-addressable storage. No service listens yet (072) and nothing executes (073): this is the typed vocabulary the rest of wave 5 shares.\n", + "title": "Remote Execution API types: vendored protos, BLAKE3 digests, actions, the CAS adapter" + }, + "shardHash": "7505e2b8b40bab96efb49564f5c9cced6974e355d47b7ce21edb83f6b6022b14", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/071-action-cache.json b/.derived/spec-registry/by-spec/071-action-cache.json new file mode 100644 index 0000000..d972fd1 --- /dev/null +++ b/.derived/spec-registry/by-spec/071-action-cache.json @@ -0,0 +1,77 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "070-reapi-types", + "064-attestation-verification" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-eval/src/cache.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/executor_identity.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/tests/cache.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "071-action-cache", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "071: Action cache as a trust boundary", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/071-action-cache/spec.md", + "status": "approved", + "summary": "A poisoned action cache forges provenance for arbitrary code, so the cache is a trust boundary (thesis D10, constitution XI). Entries are immutable and keyed by both the action digest and the executor that produced them, so two executors' results coexist instead of overwriting; every entry names its executor's Service identity and the provenance attestation that vouches for it; and a lookup declares its purpose. A gating lookup, the kind a merge decision consumes, treats an entry whose attestation is absent or fails spec 064 verification as a miss. An advisory lookup may serve the same entry, flagged, for developer feedback. Executors are registered Service identities with a trust tier, and a quorum rule can demand that several of them agree before an output counts.\n", + "title": "Action cache as a trust boundary: attested entries, gating versus advisory lookups" + }, + "shardHash": "57d321e20648030f757bf02e339073e9e7d89106030c1dbf6e0e91eaec6cb2cd", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/072-execution-service.json b/.derived/spec-registry/by-spec/072-execution-service.json new file mode 100644 index 0000000..fbcdd38 --- /dev/null +++ b/.derived/spec-registry/by-spec/072-execution-service.json @@ -0,0 +1,113 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "071-action-cache" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-eval/proto/hqgit/eval/v1/worker.proto" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/capabilities.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/cas.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/bytestream.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/action_cache.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/execution.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/service/worker.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/scheduler.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/tests/service.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/build.rs" + } + }, + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "072-execution-service", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "072: Execution service", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/072-execution-service/spec.md", + "status": "approved", + "summary": "The listening half of the evaluation plane: tonic implementations of the four REAPI services (Capabilities, ContentAddressableStorage with ByteStream, ActionCache, Execution with long-running operations) over the spec 070 adapter and the spec 071 cache, plus the scheduler that turns an Execute request into a leased unit of work for an external worker. Nothing executes in this process: workers (073) register, lease, heartbeat, and complete through a small hqgit-owned Worker service, because REAPI standardizes the client side and not the worker side. Identical in-flight actions coalesce, lost leases requeue, the sandbox tier is a platform property the scheduler matches, and every completion passes through a hook seam that spec 074 fills with provenance before the cache sees it.\n", + "title": "Execution service: REAPI Capabilities, CAS, ByteStream, ActionCache, Execution, and the scheduler" + }, + "shardHash": "719ad28c429fd35e5fbde2d55fb7a21f0d40eaaf7d3de4c07d166972591af657", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/073-sandbox-executor.json b/.derived/spec-registry/by-spec/073-sandbox-executor.json new file mode 100644 index 0000000..f43c285 --- /dev/null +++ b/.derived/spec-registry/by-spec/073-sandbox-executor.json @@ -0,0 +1,112 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "072-execution-service" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "executor/go.mod" + }, + { + "kind": "file", + "path": "executor/go.sum" + }, + { + "kind": "file", + "path": "executor/Makefile" + }, + { + "kind": "file", + "path": "executor/cmd/hq-executor/main.go" + }, + { + "kind": "file", + "path": "executor/internal/reapi/worker.go" + }, + { + "kind": "file", + "path": "executor/internal/reapi/cas.go" + }, + { + "kind": "file", + "path": "executor/internal/sandbox/tier.go" + }, + { + "kind": "file", + "path": "executor/internal/sandbox/report.go" + }, + { + "kind": "file", + "path": "executor/internal/sandbox/namespaces_linux.go" + }, + { + "kind": "file", + "path": "executor/internal/sandbox/seccomp_linux.go" + }, + { + "kind": "file", + "path": "executor/internal/sandbox/microvm.go" + }, + { + "kind": "file", + "path": "executor/internal/sandbox/sandbox_test.go" + }, + { + "kind": "file", + "path": "executor/proto/" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/sandbox.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "001-agentic-harness", + "unit": { + "kind": "file", + "path": ".github/workflows/govern.yml" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "073-sandbox-executor", + "implementation": "pending", + "kind": "feature", + "risk": "critical", + "sectionHeadings": [ + "073: Sandbox executor", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/073-sandbox-executor/spec.md", + "status": "approved", + "summary": "The one language boundary in the platform (thesis D14): a Go worker that leases actions from the execution service (072), stages their inputs from CAS, runs them in one of two sandbox tiers, uploads the outputs, and reports exactly which isolation it applied. The Trusted tier is Linux namespaces plus a seccomp allowlist, no network, read-only inputs, an unprivileged uid. The Untrusted tier is a microVM (Firecracker or Cloud Hypervisor behind one driver interface) booted from a rootfs whose hash is pinned, with no network device unless the action declared one. Fork contributions and agent principals always get the Untrusted tier; the Rust side owns that rule and the tier vocabulary, so the domain model never crosses into Go. The executor's sandbox report is what spec 074 attests, which is why a missing report fails the action.\n", + "title": "Sandbox executor: the Go worker with namespace and microVM tiers" + }, + "shardHash": "106954994b077ae924588342dbf23661dcb6ea60bd5b055b554c9af79e065a6a", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/074-execution-provenance.json b/.derived/spec-registry/by-spec/074-execution-provenance.json new file mode 100644 index 0000000..a7863b8 --- /dev/null +++ b/.derived/spec-registry/by-spec/074-execution-provenance.json @@ -0,0 +1,69 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "072-execution-service", + "064-attestation-verification" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-eval/src/provenance.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/tests/provenance.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/testdata/slsa/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "027-attestation-primitive", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "074-execution-provenance", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "074: Execution provenance", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/074-execution-provenance/spec.md", + "status": "approved", + "summary": "CI results are not a separate system; they are evidence (thesis §4.4). This spec closes that loop: every completed execution emits an Attestation with predicate hqgit/provenance/v1 whose claim is a SLSA Provenance v1 predicate (builder = the executor's Service identity, buildType = the REAPI action, resolved dependencies = the input tree, command, and toolchain hashes, byproducts = the output digests, plus the sandbox report), signed by the executor and appended to the repository ledger as an attestation.issued fact. The cache entry (071) references that attestation, and verifying it through the trust plane (064) is exactly what turns a cache hit into a gating-grade hit. An execution whose sandbox report is missing gets no attestation and no cache entry.\n", + "title": "Execution provenance: every completed action emits a signed SLSA attestation" + }, + "shardHash": "d606bf6becc19e253b8b8c2a8ada4cb2f92f42e4ae832ca80ed0bb0879445df8", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/075-build-graph.json b/.derived/spec-registry/by-spec/075-build-graph.json new file mode 100644 index 0000000..0003fe9 --- /dev/null +++ b/.derived/spec-registry/by-spec/075-build-graph.json @@ -0,0 +1,89 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "070-reapi-types" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/manifest.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/toolchain.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/graph/affected.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/tests/graph.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/testdata/graph/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "075-build-graph", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "075: Build graph", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/075-build-graph/spec.md", + "status": "approved", + "summary": "Design doc §1.1 point 4: CI is a pure function of repository state. This spec gives that function its argument list. A repository declares its targets in hq-build.toml (inputs as globs over the tree, an argv command, a pinned toolchain, declared outputs, target dependencies, and the escape hatches network and nondeterministic), a toolchain is the content hash of a pinned image or a nix-style closure, and a target's identity is a pure function of the subtree its globs select. Affected-target selection is a comparison of those identities between two tree cids, transitive over dependencies, with no execution. Every declaration becomes an action platform property (070), so an escape hatch is visible in provenance (074) rather than hidden in a script, and a target that declares nothing cannot read anything it did not declare in the Trusted tier (073).\n", + "title": "Build graph: the hq-build.toml manifest, pinned toolchains, and affected-target selection" + }, + "shardHash": "3cd1692dde978a6fb41b97b631139abb1fb313e51fc539d824120b84174493e9", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/076-merge-queue.json b/.derived/spec-registry/by-spec/076-merge-queue.json new file mode 100644 index 0000000..e9a0930 --- /dev/null +++ b/.derived/spec-registry/by-spec/076-merge-queue.json @@ -0,0 +1,70 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "075-build-graph", + "074-execution-provenance", + "067-policy-evaluation-attestation" + ], + "domain": "l3-evaluation", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-eval/src/queue.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/src/speculate.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-eval/tests/queue.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "070-reapi-types", + "unit": { + "kind": "file", + "path": "crates/hqgit-eval/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 5 + }, + "id": "076-merge-queue", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "076: Merge queue", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/076-merge-queue/spec.md", + "status": "approved", + "summary": "Thesis §4.4: merge queues become speculative evaluation over candidate merge states. A candidate is a change whose latest revision carries a verified policy-eval attestation (067) with verdict Allow; the queue never runs a policy live (constitution XI). The speculator builds the merge tree of trunk plus the candidates in order, computes the affected targets (075), and evaluates every gating target through the execution service with gating cache lookups (071), so a target whose key is unchanged since trunk costs a verified cache hit and nothing else. A green batch merges by appending facts; a red batch bisects until each failure is pinned on one candidate, which is dequeued with the evidence. Queue state is facts, so the queue is a fold any replica can rebuild.\n", + "title": "Merge queue: speculative evaluation over candidate merge states, batching, and bisection" + }, + "shardHash": "a6d2105a8b5ea8701db4a84b9cd08cfe181fa776079644b2fe2265580b919116", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/080-projection-framework.json b/.derived/spec-registry/by-spec/080-projection-framework.json new file mode 100644 index 0000000..24d02d8 --- /dev/null +++ b/.derived/spec-registry/by-spec/080-projection-framework.json @@ -0,0 +1,110 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "021-local-repository", + "032-cli-skeleton" + ], + "domain": "l5-projection", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/projection.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/runner.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/sqlite.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/checkpoint.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_projection.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "080-projection-framework", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "080: Projection framework", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/080-projection-framework/spec.md", + "status": "approved", + "summary": "Constitution VI in code: every index, timeline, queue, and dashboard is a projection that can be rebuilt from zero, and no projection is ever read as authority. This spec founds hqgit-projection: the Projection trait, a runner that folds the ledger in the deterministic total order (018) from a crash-consistent checkpoint, SQLite-backed storage under .hq/projections where the apply and the checkpoint commit in one transaction, a registry that later specs (081 to 085) plug their read models into, the rule that every read answers \"as of entry N\", the rule that erased payloads render as erased, and the `hq projection` verbs to inspect and rebuild. It owns no concrete view.\n", + "title": "Projection framework: disposable read models folded from the total order" + }, + "shardHash": "85416420ae0c10630f07f516fe44ebc796567407dfada719577063f1f44e3863", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/081-change-and-review-views.json b/.derived/spec-registry/by-spec/081-change-and-review-views.json new file mode 100644 index 0000000..0d65dff --- /dev/null +++ b/.derived/spec-registry/by-spec/081-change-and-review-views.json @@ -0,0 +1,87 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "080-projection-framework", + "026-review-threads", + "027-attestation-primitive", + "050-stacked-changes" + ], + "domain": "l5-projection", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-projection/src/views/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/views/changes.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/views/threads.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/views/attestations.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/views/stacks.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/tests/views.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/testdata/views/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "081-change-and-review-views", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "081: Change and review views", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/081-change-and-review-views/spec.md", + "status": "approved", + "summary": "The four read models the API (093) and the review UI (095) query: changes with their revisions and stack position, threads with their per-revision anchor resolution (exact, moved, text, lost), attestations by subject and predicate with a verification column a composing binary fills, and stacks. Each is a Projection instance (080) with its own schema version, every write is an upsert keyed by a fact-derived id so replay is idempotent, comment bodies stay in the object store as a Cid plus an erased flag, and every query answers as of a ledger entry. Nothing here is authority: a rebuild from zero reproduces every row.\n", + "title": "Change and review views: the SQL read models for changes, threads, attestations, stacks" + }, + "shardHash": "e868a45ea0e16004371eae9feb882e08f96ff7c30cf32dfb799c0de4df78edd7", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/082-search-index.json b/.derived/spec-registry/by-spec/082-search-index.json new file mode 100644 index 0000000..baa826e --- /dev/null +++ b/.derived/spec-registry/by-spec/082-search-index.json @@ -0,0 +1,81 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "080-projection-framework" + ], + "domain": "l5-projection", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-projection/src/search.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/tests/search.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "082-search-index", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "082: Search index", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/082-search-index/spec.md", + "status": "approved", + "summary": "Full-text search as one more disposable projection: a tantivy index over change titles, comment bodies resolved from the object store, issue fields, and attestation predicates, keyed by stable document ids so replay is idempotent, with erased bodies never indexed and removed on tombstone, quarantine documents excluded by default, and a query API whose pages carry the ledger cursor they were computed from. Because tantivy is not SQLite, this spec fixes the commit ordering that keeps the index crash-consistent with the 080 checkpoint. Rebuild wipes the directory; nothing here is authority.\n", + "title": "Search index: a tantivy projection over changes, comments, issues, and attestations" + }, + "shardHash": "822fa31ca346ed69a1c25fe4b0b58b2da7733717fc8cbbf3aef8b43765e25b13", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/083-code-graph.json b/.derived/spec-registry/by-spec/083-code-graph.json new file mode 100644 index 0000000..db9f6f0 --- /dev/null +++ b/.derived/spec-registry/by-spec/083-code-graph.json @@ -0,0 +1,102 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "080-projection-framework", + "025-semantic-anchors" + ], + "domain": "l5-projection", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-projection/src/codegraph/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/codegraph/scip.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/codegraph/store.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/tests/codegraph.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/testdata/scip/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + }, + { + "nature": "additive", + "spec": "027-attestation-primitive", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "083-code-graph", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "083: Code graph", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/083-code-graph/spec.md", + "status": "approved", + "summary": "Design §1.1 point 6: there is no ecosystem graph, and the first half of one is a type-aware, cross-repository code index. This spec ingests SCIP indexes, produced per revision tree by external indexers and delivered as attestations under the predicate hqgit/code-index/v1, into a projection keyed by (namespace, tree cid): symbols with a stable content-derived id, definitions, references, and relationships, in one shared SQLite file that spans repositories so a reference in one namespace resolves to a definition in another. It is the one legitimately centralized component (thesis §8) and therefore the most carefully non-authoritative: nothing originates here, every answer names the tree and ledger entry it was computed from, and any namespace's rows can be dropped and rebuilt from its own ledger alone.\n", + "title": "Code graph: a cross-repository SCIP symbol projection keyed by tree" + }, + "shardHash": "de68e7c1a418973a1fde90f62edfdd0c852f6e5ba88c1e309ed41a5ac08b650a", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/084-ecosystem-graph.json b/.derived/spec-registry/by-spec/084-ecosystem-graph.json new file mode 100644 index 0000000..99f1419 --- /dev/null +++ b/.derived/spec-registry/by-spec/084-ecosystem-graph.json @@ -0,0 +1,86 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "083-code-graph", + "075-build-graph", + "051-semantic-deltas" + ], + "domain": "l5-projection", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-projection/src/ecosystem/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/ecosystem/deps.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/src/ecosystem/impact.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/tests/ecosystem.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/testdata/ecosystem/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "084-ecosystem-graph", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "084: Ecosystem graph", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/084-ecosystem-graph/spec.md", + "status": "approved", + "summary": "The second half of design §1.1 point 6: the package dependency graph, read from manifests and lockfiles in each revision tree for Cargo, npm, Go, and Python, joined to the code graph (083) through the SCIP package coordinates so that \"who depends on this package\" becomes \"which call sites in which repositories use the symbols this change removed or re-signed\". It answers downstream impact for an API-surface delta (051) before the API breaks, plans crater-style downstream test runs as data the evaluation plane materializes through the build manifest (075), and reports usage with honest denominators: how many dependents are known, how many are indexed, how many are hit. It shares the 083 file and its isolation: non-authoritative, per-namespace rebuildable, integer-only.\n", + "title": "Ecosystem graph: package dependencies joined to the code graph for downstream impact" + }, + "shardHash": "fdafa48027651555b7e0767a49d043650bf841f7b9e58178a268adb0bfffde7e", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/085-attention-feeds.json b/.derived/spec-registry/by-spec/085-attention-feeds.json new file mode 100644 index 0000000..cbc8129 --- /dev/null +++ b/.derived/spec-registry/by-spec/085-attention-feeds.json @@ -0,0 +1,76 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "081-change-and-review-views" + ], + "domain": "l5-projection", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-projection/src/feeds.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/tests/feeds.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-projection/testdata/feeds/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "080-projection-framework", + "unit": { + "kind": "file", + "path": "crates/hqgit-projection/src/registry.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "085-attention-feeds", + "implementation": "pending", + "kind": "feature", + "risk": "low", + "sectionHeadings": [ + "085: Attention feeds", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/085-attention-feeds/spec.md", + "status": "approved", + "summary": "Design §1.1 point 8 names attention management as an email firehose with no prioritization. This spec replaces the firehose with a projection: for each principal, a feed of items derived from facts, ranked by integer weights for blocking someone's merge, an ownership match with its SLA when spec 104 supplies one, staleness measured in ledger time rather than wall-clock time, stack depth, and explicit requests, with the reasons listed on every item so there is never a notification without a reason. Requesting attention and muting are facts, so a rebuild reproduces the feed and a mute survives every replica. The feed is disposable, as-of a ledger entry, and never a source of anything.\n", + "title": "Attention feeds: a per-principal ranked feed where every item carries its reason" + }, + "shardHash": "dfd322bc5070f7b89d01ba0cdad6a5f90b973ef2d011a18d610a2462fd3897af", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/090-server-skeleton.json b/.derived/spec-registry/by-spec/090-server-skeleton.json new file mode 100644 index 0000000..573ac96 --- /dev/null +++ b/.derived/spec-registry/by-spec/090-server-skeleton.json @@ -0,0 +1,89 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "021-local-repository" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/main.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/config.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/health.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/repos.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/auth.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/telemetry.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "090-server-skeleton", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "090: The hqgit-server binary", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/090-server-skeleton/spec.md", + "status": "approved", + "summary": "The hosted edge begins here. This spec founds hqgit-server, the binary that serves every later L7 surface (the git endpoint 092, the Connect API 093, the review UI 095, federation 112) from one listener, and fixes the frame those specs hang on: TOML plus environment configuration, one axum router that multiplexes HTTP/1.1 and gRPC on a single port, a registry that opens spec 021 repositories lazily under a fixed data-directory layout, a server identity of kind Service, the caller and namespace routing seams every request passes through, health and readiness endpoints, structured logs, graceful shutdown, and exit codes through Error::exit_code. The server runs the same ledger implementation as the CLI (thesis §5) and never depends on hqgit-cli (constitution XIII).\n", + "title": "The hqgit-server binary: one listener for HTTP and gRPC, config, the repo registry, health, shutdown" + }, + "shardHash": "30c242f4995dd4c98377efccf5c43a26f571853f477c4d2ccd9421b1d6b01e35", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/091-per-repo-control-plane.json b/.derived/spec-registry/by-spec/091-per-repo-control-plane.json new file mode 100644 index 0000000..1b0c671 --- /dev/null +++ b/.derived/spec-registry/by-spec/091-per-repo-control-plane.json @@ -0,0 +1,85 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "090-server-skeleton" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-server/src/control/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/control/raft.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/control/placement.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/control/apply.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/control.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "091-per-repo-control-plane", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "091: Per-repo control plane", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/091-per-repo-control-plane/spec.md", + "status": "approved", + "summary": "Thesis §4.2, last paragraph: repositories are independent consistency domains, so the hosted server replicates each one in its own Raft group rather than through a shared database tier. This spec adds the control plane to hqgit-server: a ControlPlane seam with a hiqlite-backed implementation and an in-process one for tests; a per-namespace group whose replicated state is the append order of signed entries and the head set, never payload content and never a signature of the cluster's own; a proposal path that validates before forwarding and commits through the leader; an applier that feeds committed entries into the local spec 021 store in order with the payload present first; and consistent-hash placement of groups onto nodes with rebalancing as an operator command. A single node is a one-member group on the same path.\n", + "title": "Per-repo control plane: one Raft group per repository over hiqlite, proposal, apply, placement" + }, + "shardHash": "5f16a91a4cba0ba4b565970c4810ef6feb56832f0af28ccf7c63b1748b662e53", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/092-git-endpoint.json b/.derived/spec-registry/by-spec/092-git-endpoint.json new file mode 100644 index 0000000..615c648 --- /dev/null +++ b/.derived/spec-registry/by-spec/092-git-endpoint.json @@ -0,0 +1,91 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "090-server-skeleton", + "031-git-object-bridge", + "024-change-and-revision", + "068-policy-in-repo" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-server/src/git/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/git/refs.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/git/upload_pack.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/git/receive_pack.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/git/gate.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/git_endpoint.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "092-git-endpoint", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "092: Git smart HTTP endpoint", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/092-git-endpoint/spec.md", + "status": "approved", + "summary": "Git compatibility is a hard requirement (constitution XIV), so the server speaks the git smart HTTP protocol without becoming a git host: refs are computed from the ledger, packs are built from the object store through the spec 031 bridge, and a push is a revision submission rather than a pointer move. Each open change exposes refs/changes/<id>/ <n>; main is the merged lineage rendered as deterministic commits; a push to a change ref imports the tree and appends change.revision_submitted; a push to main is accepted only when a policy-eval attestation with an Allow verdict exists for that revision under the policy pinned in the repository, and then appends change.merged. Every fact lands where the namespace router says (quarantine for unverified pushers), the endpoint never spawns git, and the tests drive a real git client against the ephemeral server.\n", + "title": "Git smart HTTP endpoint: refs as a projection of changes, push as revision submission, gated main" + }, + "shardHash": "22646381818c52405ceddd787c87ddb5601cebc24640119980142479b55354c3", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/093-connect-api.json b/.derived/spec-registry/by-spec/093-connect-api.json new file mode 100644 index 0000000..971149c --- /dev/null +++ b/.derived/spec-registry/by-spec/093-connect-api.json @@ -0,0 +1,190 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "090-server-skeleton", + "026-review-threads", + "027-attestation-primitive", + "067-policy-evaluation-attestation", + "032-cli-skeleton", + "081-change-and-review-views" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "proto/hqgit/v1/common.proto" + }, + { + "kind": "file", + "path": "proto/hqgit/v1/changes.proto" + }, + { + "kind": "file", + "path": "proto/hqgit/v1/reviews.proto" + }, + { + "kind": "file", + "path": "proto/hqgit/v1/attestations.proto" + }, + { + "kind": "file", + "path": "proto/hqgit/v1/policy.proto" + }, + { + "kind": "file", + "path": "proto/hqgit/v1/repos.proto" + }, + { + "kind": "file", + "path": "crates/hqgit-server/build.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/mod.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/connect.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/read_model.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/changes.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/reviews.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/attestations.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/policy.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/api/repos.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/api.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/build.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/client.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_remote.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/remote.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/config.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "093-connect-api", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "093: The Connect API", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/093-connect-api/spec.md", + "status": "approved", + "summary": "The one typed API of the edge (thesis §2, L7): five services defined in protobuf, served as gRPC and as Connect-compatible JSON over plain HTTP on the same listener so browsers and CLIs use one contract. Every write is a fact append through the spec 021 path and the control plane (091), never a table update; approvals and attestations arrive already signed by their issuer and are verified before append; server-mediated writes record who they were made for. Reads come from the spec 081 projections when present and from a domain fold otherwise, through one ReadModel seam, and every response says which ledger entry it is projected as of. Pagination is by total-order cursor. The CLI gains a generated client and hq remote so every existing verb can target a hosted repository.\n", + "title": "The typed API: gRPC and Connect JSON services for changes, reviews, attestations, policy, repos; the CLI remote client" + }, + "shardHash": "c155e17de0a9c4ab5280992ab171a837041f778595e2306dc5d49a8a494d249f", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/094-quarantine-and-promotion.json b/.derived/spec-registry/by-spec/094-quarantine-and-promotion.json new file mode 100644 index 0000000..87427ba --- /dev/null +++ b/.derived/spec-registry/by-spec/094-quarantine-and-promotion.json @@ -0,0 +1,89 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "091-per-repo-control-plane", + "064-attestation-verification" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-server/src/quarantine.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/capability.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/quarantine.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/testdata/capabilities/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "094-quarantine-and-promotion", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "094: Quarantine and promotion", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/094-quarantine-and-promotion/spec.md", + "status": "approved", + "summary": "Constitution XV in code. Every write reaching the server from a principal that is not verified and not capable (anonymous pushers, fork contributors, the mirror's Service principal, any agent) is appended to the repository's quarantine namespace, never to main. A capability is an attestation with predicate hqgit/capability/v1 over a principal, rooted at the repository's genesis identity, verified end to end by spec 064, and evaluated by pure functions over the verified set at the Hlc of the write in question. Promotion is a namespace.promoted fact signed by a holder of the promote capability; main's fold is main's facts plus what main has promoted, so a promoted fact keeps its original issuer and signature. Per-principal rate and size limits bound abuse of the append-only store, and quarantine content is served only to holders of read-quarantine.\n", + "title": "Quarantine and promotion: untrusted writes land quarantined, capabilities are attestations, promotion is a fact" + }, + "shardHash": "1384ac3dc487002e5edf7099f3175b38a60107b2eb82a0e26c6a7727ae85d348", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/095-web-review-ui.json b/.derived/spec-registry/by-spec/095-web-review-ui.json new file mode 100644 index 0000000..a8f57cf --- /dev/null +++ b/.derived/spec-registry/by-spec/095-web-review-ui.json @@ -0,0 +1,134 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "093-connect-api", + "081-change-and-review-views" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "web/package.json" + }, + { + "kind": "file", + "path": "web/tsconfig.json" + }, + { + "kind": "file", + "path": "web/vite.config.ts" + }, + { + "kind": "file", + "path": "web/index.html" + }, + { + "kind": "file", + "path": "web/src/main.tsx" + }, + { + "kind": "file", + "path": "web/src/api.ts" + }, + { + "kind": "file", + "path": "web/src/format.ts" + }, + { + "kind": "file", + "path": "web/src/views/ChangeList.tsx" + }, + { + "kind": "file", + "path": "web/src/views/ChangeDetail.tsx" + }, + { + "kind": "file", + "path": "web/src/views/DeltaPanel.tsx" + }, + { + "kind": "file", + "path": "web/src/views/LineDiff.tsx" + }, + { + "kind": "file", + "path": "web/src/views/EvidencePanel.tsx" + }, + { + "kind": "file", + "path": "web/src/views/ThreadPanel.tsx" + }, + { + "kind": "file", + "path": "web/test/" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/static_files.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 6 + }, + "id": "095-web-review-ui", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "095: The web review client", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/095-web-review-ui/spec.md", + "status": "approved", + "summary": "The minimal review client (thesis §9: a UI beyond review is a later client). A TypeScript, React, and Vite single-page application under web/ that speaks only the Connect JSON form of the spec 093 API and renders four things: a paginated change list; a change detail whose order is the argument of design §1.1 point 3 (policy verdict, then the semantic delta views for API surface, dependencies, and capabilities, then threads, then the line diff last and collapsed); threads with their per-revision exact, moved, text, lost, and unavailable markers; and an evidence panel listing every attestation over the revision with its verification status. Every view prints which ledger entry it is projected as of. The server serves web/dist from static_files.rs, the package carries its spec-spine binding, and no tsconfig exists at the repository root (spec 001 D-1).\n", + "title": "The web review client: change list, change detail with semantic deltas first, threads, the evidence panel" + }, + "shardHash": "23d3f9697ff223a82fdd32b358ed90fcaaa75b7f41c687086baea78620d86bc9", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/100-agent-principals.json b/.derived/spec-registry/by-spec/100-agent-principals.json new file mode 100644 index 0000000..7c92738 --- /dev/null +++ b/.derived/spec-registry/by-spec/100-agent-principals.json @@ -0,0 +1,86 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "060-identity-and-key-rotation", + "068-policy-in-repo" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-agent/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/src/token.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/src/principal.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/src/caveats.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/testdata/tokens/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 7 + }, + "id": "100-agent-principals", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "100: Agent principals", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/100-agent-principals/spec.md", + "status": "approved", + "summary": "Constitution XII and thesis D12: an agent is a distinct principal class and never authenticates as a human. This spec founds hqgit-agent and fixes how an agent becomes one: an identity of kind Agent (060) that an operator, a human or an org, registers with a fact naming the runtime and the declared sandbox; a Biscuit token whose authority block is signed by the operator's identity key and names the agent, the registration entry, the pinned policy (068), the namespaces, the capabilities, and an expiry; attenuation that can only add caveats, from a frozen datalog vocabulary over namespace, path, predicate, revision size, operation, and time window; offline verification against the operator's key as the 060 fold knew it at issuance; and revocation by fact. The chain of delegations and the authorize function are 101.\n", + "title": "Agent principals: registration facts, Biscuit tokens, and the caveat vocabulary" + }, + "shardHash": "e551b1890f0d793255db3725d0cb533ba4477766a023f687b8cddc00b8599d57", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/101-delegation-chain.json b/.derived/spec-registry/by-spec/101-delegation-chain.json new file mode 100644 index 0000000..0c33547 --- /dev/null +++ b/.derived/spec-registry/by-spec/101-delegation-chain.json @@ -0,0 +1,72 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "100-agent-principals" + ], + "domain": "l4-trust", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-agent/src/delegation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/src/authorize.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/tests/delegation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/testdata/delegation/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "100-agent-principals", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 7 + }, + "id": "101-delegation-chain", + "implementation": "pending", + "kind": "kernel", + "risk": "critical", + "sectionHeadings": [ + "101: Delegation chain", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/101-delegation-chain/spec.md", + "status": "approved", + "summary": "The accountability chain as a cryptographic object. Every token an agent holds is mirrored by an agent.delegated fact naming who delegated, to which agent, under which pinned policy, with which caveats, expiring when, so the chain is queryable long after the bearer token is gone. A sub-delegation is a new token rooted in the delegating agent's identity key with a caveat set at least as strict as its parent's. This spec fixes the facts, the DelegationView fold, delegation_chain resolving an agent to its human or org root within a bounded depth, and authorize: a pure function combining Biscuit verification (100) with the ledger's registrations, revocations, and policy pin (068), returning Allow with the chain hash or Deny with a closed reason that the caller journals as an agent.action_denied fact.\n", + "title": "Delegation chain: delegation facts, chain resolution to a human root, and authorize" + }, + "shardHash": "fe5ecac8ab7d8d44bd7af88e1a250d31183107426b29b4672d87828f15ebb093", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/102-agent-sandbox-and-provenance.json b/.derived/spec-registry/by-spec/102-agent-sandbox-and-provenance.json new file mode 100644 index 0000000..6de8db2 --- /dev/null +++ b/.derived/spec-registry/by-spec/102-agent-sandbox-and-provenance.json @@ -0,0 +1,102 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "101-delegation-chain", + "074-execution-provenance", + "090-server-skeleton" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-agent/src/sandbox.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/src/provenance.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-agent/tests/provenance.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/agent_auth.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/agent_auth.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "100-agent-principals", + "unit": { + "kind": "file", + "path": "crates/hqgit-agent/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "027-attestation-primitive", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + } + ], + "extraFrontmatter": { + "wave": 7 + }, + "id": "102-agent-sandbox-and-provenance", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "102: Agent sandbox and provenance", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/102-agent-sandbox-and-provenance/spec.md", + "status": "approved", + "summary": "Constitution XII's last two clauses: an agent's sandbox is declared and every artifact it produces carries provenance. This spec types the sandbox an agent registration names (always the Untrusted tier of 073 for execution, with declared tools and network), fills the hqgit/agent-action/v1 claim slot 027 reserved with a schema binding the agent, its delegation chain hash (101), its sandbox, its inputs, a summary of its tool calls, and the execution provenance (074) of anything it ran, signed by the agent's own key; fixes the rule that an agent-submitted revision without such an attestation is refused; and builds the server seam that accepts Biscuit credentials, refuses a human session on an agent route and the reverse, journals every denial (101), and tags every agent write so quarantine (094) applies.\n", + "title": "Agent sandbox and provenance: declared sandboxes, the agent-action attestation, and the server seam" + }, + "shardHash": "baabd1ee264488b09e9c24d670fc762cb21e98a9dfa36c3dd3401f95fb4cf671", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/103-evidence-carrying-changes.json b/.derived/spec-registry/by-spec/103-evidence-carrying-changes.json new file mode 100644 index 0000000..d2c9d8a --- /dev/null +++ b/.derived/spec-registry/by-spec/103-evidence-carrying-changes.json @@ -0,0 +1,122 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "102-agent-sandbox-and-provenance", + "067-policy-evaluation-attestation", + "051-semantic-deltas" + ], + "domain": "l6-policy", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/evidence.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/evidence.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/testdata/evidence/" + }, + { + "kind": "file", + "path": "crates/hqgit-policy-sdk/examples/evidence_required.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_evidence.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/evidence.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + }, + { + "nature": "additive", + "spec": "027-attestation-primitive", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/predicate.rs" + } + }, + { + "nature": "additive", + "spec": "066-policy-sdk", + "unit": { + "kind": "file", + "path": "crates/hqgit-policy-sdk/src/evidence.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + } + ], + "extraFrontmatter": { + "wave": 7 + }, + "id": "103-evidence-carrying-changes", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "103: Evidence-carrying changes", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/103-evidence-carrying-changes/spec.md", + "status": "approved", + "summary": "Design §1.1 point 7: as authoring cost approaches zero, trusted review capacity is the binding constraint, so a change should arrive carrying machine-checkable evidence and a human should review the argument rather than the diff. This spec names that argument: an EvidenceBundle is the set of attestations a revision carries (provenance, test and property-test results, invariant checks, semantic deltas, policy evaluations, agent actions, approvals, spec conformance), declared by an evidence.attached fact, each item classified by whether a machine or a human must check it and carrying its verification verdict. The CLI renders the argument with a throughput line (how much verified itself, how much needs a person), completeness is computed against a required predicate set, and the SDK example policy evidence_required denies a revision lacking one. Nothing here is a new noun: every item is an attestation (constitution IX).\n", + "title": "Evidence-carrying changes: the bundle, the argument view, and the evidence_required policy" + }, + "shardHash": "86c6f7ac2c6e596c39d7572eebb335cf68750d89c8e1b64c24e10a8da37caeec", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/104-ownership-and-sla.json b/.derived/spec-registry/by-spec/104-ownership-and-sla.json new file mode 100644 index 0000000..afa6cf5 --- /dev/null +++ b/.derived/spec-registry/by-spec/104-ownership-and-sla.json @@ -0,0 +1,70 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "028-issues-and-derived-state", + "060-identity-and-key-rotation", + "025-semantic-anchors" + ], + "domain": "l2-domain", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-domain/src/ownership.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/tests/ownership.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-domain/testdata/ownership/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 7 + }, + "id": "104-ownership-and-sla", + "implementation": "pending", + "kind": "feature", + "risk": "medium", + "sectionHeadings": [ + "104: Ownership and SLA", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/104-ownership-and-sla/spec.md", + "status": "approved", + "summary": "Ownership today is a text file with no SLA, delegation, or expiry (design §1.1 point 8). Here it is a set of facts: ownership.declared names a scope (a path pattern, a semantic anchor, or a crate), an owner, an optional delegator, an optional expiry, and an optional review SLA; ownership.revoked ends a declaration. OwnershipView folds them and answers owners_of(path, at) with expiry honored, revocations applied, delegation chains resolved to an accountable Human or Org root, and the most specific scope winning by an integer weight. Agents and services may hold delegated ownership but never a root, which is the other half of the accountability chain wave 7 builds. A CODEOWNERS import is a helper that emits facts; feeds (085) and policy (065) read the view, never the file.\n", + "title": "Ownership with delegation, expiry, and SLA: facts that replace CODEOWNERS" + }, + "shardHash": "ee0720788bc65706571c63cc4578c3c32d1a849fe24d49c7a9e94f4e2e4a645e", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/110-set-reconciliation.json b/.derived/spec-registry/by-spec/110-set-reconciliation.json new file mode 100644 index 0000000..a05527c --- /dev/null +++ b/.derived/spec-registry/by-spec/110-set-reconciliation.json @@ -0,0 +1,76 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "021-local-repository" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-sync/Cargo.toml" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/src/lib.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/src/reconcile.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/src/fingerprint.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/src/protocol.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/tests/" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/testdata/protocol/" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "021-local-repository", + "unit": { + "kind": "file", + "path": "crates/hqgit-ledger/src/repo.rs" + } + } + ], + "extraFrontmatter": { + "wave": 8 + }, + "id": "110-set-reconciliation", + "implementation": "pending", + "kind": "kernel", + "risk": "high", + "sectionHeadings": [ + "110: Set reconciliation", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/110-set-reconciliation/spec.md", + "status": "approved", + "summary": "Thesis §4.2: replication is range-based set reconciliation, with the Willow protocol as the prior art. Two replicas of one repository hold sets of entry hashes ordered by (hlc, hash); each side fingerprints a range, and where fingerprints differ the range is split until the difference is small enough to exchange item lists. Because an entry's hlc is strictly greater than its parents' (017), that same order is a topological order, so missing entries append in order with no extra sort. This spec founds hqgit-sync with the fingerprint, the pure reconciler state machine, the canonical message vocabulary, a transport-agnostic Channel seam, and the ingest path that verifies every foreign entry before it touches the ledger. Payload objects are fetched lazily and verified through spec 015. The transport is spec 111.\n", + "title": "Range-based set reconciliation over entry hashes: fingerprints, the split protocol, and ingest" + }, + "shardHash": "b4fcf1cad67ce2d526ed300931870c25fc856e6074c512ee4dee0641d6540aed", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/111-quic-transport.json b/.derived/spec-registry/by-spec/111-quic-transport.json new file mode 100644 index 0000000..596a68f --- /dev/null +++ b/.derived/spec-registry/by-spec/111-quic-transport.json @@ -0,0 +1,82 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "110-set-reconciliation", + "060-identity-and-key-rotation" + ], + "domain": "l1-ledger", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-sync/src/transport.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/src/session.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/src/object_source.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-sync/tests/transport.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "110-set-reconciliation", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "110-set-reconciliation", + "unit": { + "kind": "file", + "path": "crates/hqgit-sync/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "010-workspace-and-core-types", + "unit": { + "anchor": "workspace.dependencies", + "file": "Cargo.toml", + "kind": "section" + } + } + ], + "extraFrontmatter": { + "wave": 8 + }, + "id": "111-quic-transport", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "111: QUIC transport", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/111-quic-transport/spec.md", + "status": "approved", + "summary": "The wire under reconciliation: QUIC through iroh and quinn (thesis §4.2), where the peer's TLS key is its hqgit identity key, so a handshake is an identity proof checked against the rotation chain (060) and nothing else names a peer. One ALPN, one framed message stream per session, and one stream per object fetch. The session runs heads exchange, reconciliation (110) per namespace, entry ingest, and payload fetch through a RemoteObjectSource that implements spec 015's ObjectSource, so every byte from a peer arrives with a range proof and a tampered slice is refused before it exists locally. Reads are gated per namespace by an Authorizer seam and quarantine is never pulled unless asked for; an interrupted session leaves a valid replica and the next one resumes from the last completed watermark.\n", + "title": "QUIC transport: identity-bound endpoints, the sync session, and the remote object source" + }, + "shardHash": "d0bc25bf4e4ceb506fe5cc8c872968b245bf9802399d851c3e5a50b2af4e88cf", + "specVersion": "1.1.0" +} diff --git a/.derived/spec-registry/by-spec/112-federation.json b/.derived/spec-registry/by-spec/112-federation.json new file mode 100644 index 0000000..e163da8 --- /dev/null +++ b/.derived/spec-registry/by-spec/112-federation.json @@ -0,0 +1,126 @@ +{ + "record": { + "authors": [ + "Bartek Kus" + ], + "created": "2026-09-02", + "dependsOn": [ + "111-quic-transport", + "094-quarantine-and-promotion", + "032-cli-skeleton" + ], + "domain": "l7-edge", + "establishes": [ + { + "kind": "file", + "path": "crates/hqgit-server/src/federation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/src/peers.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/src/cmd_sync.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-server/tests/federation.rs" + }, + { + "kind": "file", + "path": "crates/hqgit-cli/tests/sync.rs" + } + ], + "extends": [ + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/lib.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/app.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/src/config.rs" + } + }, + { + "nature": "additive", + "spec": "090-server-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-server/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/main.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/src/cli.rs" + } + }, + { + "nature": "additive", + "spec": "032-cli-skeleton", + "unit": { + "kind": "file", + "path": "crates/hqgit-cli/Cargo.toml" + } + }, + { + "nature": "additive", + "spec": "023-domain-fact-vocabulary", + "unit": { + "kind": "file", + "path": "crates/hqgit-domain/src/facts.rs" + } + } + ], + "extraFrontmatter": { + "wave": 8 + }, + "id": "112-federation", + "implementation": "pending", + "kind": "feature", + "risk": "high", + "sectionHeadings": [ + "112: Federation", + "1. Purpose", + "2. Territory", + "3. Behavior", + "4. Functional requirements", + "5. Acceptance criteria", + "6. Out of scope", + "7. Resolved decisions", + "Verification" + ], + "specPath": "specs/112-federation/spec.md", + "status": "approved", + "summary": "Thesis §6 step 8: hosts replicate signed history, not authority. A repository's peers are facts (identity, transport key, endpoints, namespaces, and the capability attestation that says what the peer may do here), folded into a registry that implements the transport's Authorizer with spec 094's capability checks. The server runs a per repository driver over 111 that pulls from every peer and pushes to the peers it is configured to push to; entries arriving from a peer without the promote capability go into a per-peer hold that no fold reads until a promotion fact (094) admits them, so a foreign host can contribute history but never decide what this host treats as main (constitution XV). The CLI is a peer like any other: hq sync, hq push, and hq pull speak the same protocol against a server or another laptop.\n", + "title": "Federation: peer registries as facts, the server sync driver, holds for unpromoted peers, and hq sync" + }, + "shardHash": "b0945c1732038dd0955b1035578891971c2a518035249f24fdfe30032e0d0ba9", + "specVersion": "1.1.0" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8c5edd9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Determinism contract: every text file checks out with LF so content hashes +# (spec-spine's and hqgit's own golden vectors, spec 012) are identical on every +# platform. +* text=auto eol=lf + +*.rs text eol=lf +*.toml text eol=lf +*.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.proto text eol=lf +*.sh text eol=lf +*.go text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +Makefile text eol=lf + +# Golden vectors are bytes; never normalize them. +*.cbor binary +*.bin binary +*.png binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cd2d8a6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# Version-pin bumps self-waive the coupling gate (spec-spine's +# auto_waive_dependency_only), so these PRs need no spec edit. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: npm + directory: /web + schedule: + interval: weekly diff --git a/.github/workflows/govern.yml b/.github/workflows/govern.yml new file mode 100644 index 0000000..7b1c201 --- /dev/null +++ b/.github/workflows/govern.yml @@ -0,0 +1,99 @@ +# The governed gate, re-run in CI exactly as `make ci` runs it locally +# (spec 001 B-3). claude-observatory's shepherd stage watches this workflow. +name: govern + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +env: + # Pinned to the version AGENTS.md names; bump both together. + SPEC_SPINE_VERSION: v0.11.0 + +jobs: + spine: + name: spec-spine gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install spec-spine + run: | + set -euo pipefail + curl -fsSL https://raw.githubusercontent.com/statecrafting/spec-spine/main/install.sh \ + | SPEC_SPINE_VERSION="$SPEC_SPINE_VERSION" SPEC_SPINE_BIN_DIR="$HOME/.local/bin" sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - run: spec-spine --version + # `compile --check` validates the corpus AND proves the committed + # registry shards match it, without writing. Never run it after a + # plain `compile` in the same job. + - run: spec-spine compile --check + - run: spec-spine index check + - run: spec-spine lint --fail-on-warn + - name: Coupling gate + if: github.event_name == 'pull_request' + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + set -euo pipefail + printf '%s' "${PR_BODY:-}" > "$RUNNER_TEMP/pr-body.txt" + spec-spine couple \ + --base "${{ github.event.pull_request.base.sha }}" \ + --head HEAD \ + --pr-body "$RUNNER_TEMP/pr-body.txt" + # The whole-tree ownership assertion: every source file inside a crate + # is specifically claimed by a spec (spec 032 of spec-spine). + - run: spec-spine index coverage --fail-on-untraced + - run: scripts/spec-dag.sh + + cargo: + name: cargo gate + needs: spine + if: ${{ hashFiles('Cargo.toml') != '' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # rustup installs the toolchain pinned in rust-toolchain.toml (spec 010). + - run: rustup show + - uses: Swatinem/rust-cache@v2 + - run: cargo build --workspace --locked + - run: cargo test --workspace --locked + - run: cargo clippy --workspace --all-targets --locked -- -D warnings + - run: cargo fmt --all --check + + deny: + name: supply chain (cargo-deny) + needs: spine + if: ${{ hashFiles('deny.toml') != '' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + + attest: + name: corpus attestation + needs: spine + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install spec-spine + run: | + set -euo pipefail + curl -fsSL https://raw.githubusercontent.com/statecrafting/spec-spine/main/install.sh \ + | SPEC_SPINE_VERSION="$SPEC_SPINE_VERSION" SPEC_SPINE_BIN_DIR="$HOME/.local/bin" sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + # The reproducible attestation over the corpus and its coupling verdict + # (spec-spine spec 023): the repo's own ledger seal, published as a + # build artifact so a later steward can `verify-attestation --recompute`. + - run: spec-spine attest --with-coupling > corpus-attestation.json + - uses: actions/upload-artifact@v4 + with: + name: corpus-attestation-${{ github.sha }} + path: corpus-attestation.json diff --git a/.gitignore b/.gitignore index ad67955..758fb61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,31 @@ -# Generated by Cargo -# will have compiled files and executables +# Cargo debug target - -# These are backup files generated by rustfmt **/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information *.pdb - -# Generated by cargo mutants -# Contains mutation testing data **/mutants.out*/ -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# cargo-fuzz artifacts and corpora (spec 012 keeps the seed corpus under testdata/) +fuzz/artifacts/ +fuzz/corpus/ +fuzz/coverage/ + +# spec-spine derived artifacts: shards are committed, build metadata and +# attestation output are not +.derived/**/build-meta.json +.derived/attestation/ + +# claude-observatory state root for this project (work journal, decision +# ledger, evidence): never committed, it describes private driving activity +data/ + +# The review SPA build output (spec 095), served from disk, never committed +web/dist/ +web/node_modules/ +node_modules/ + +# Editors and local scratch +.idea/ +.vscode/ +*.log +.DS_Store diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..da39e4f --- /dev/null +++ b/.mcp.json @@ -0,0 +1,3 @@ +{ + "mcpServers": {} +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..974683a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,185 @@ +# AGENTS.md: hqgit + +Cross-agent authority for hqgit, read by Claude Code, Codex CLI, Cursor, +Copilot, and claude-observatory's driven sessions via the AAIF/Linux +Foundation AGENTS.md standard. It is the single source for the session-init +protocol and the backlog discipline. Evolve the protocol by editing this +file, never the `/init` skill that dispatches to it. + +hqgit is a verifiable evidence ledger for software change (the thesis is +`specs/002-platform-thesis/spec.md`; the analysis is +`docs/design/00-architecture.md`). The repository is **specified before it +is built**: the corpus under `specs/` is the whole design, every ordinary +spec is `approved` and `implementation: pending`, and spec ordinals are the +build order. Code arrives one spec per session under `crates/`, `fuzz/`, +`executor/`, and `web/`. + +Governance is `spec-spine` **0.11.0** on your `PATH` (CI pins the same +version). All governed reads of `.derived/` go through its CLI. + +## New Sessions + +Run `/init` as the first action of every new session. It reads this section +to derive its plan; anything added here is picked up on the next init. + +> AGENTS.md is loaded implicitly as the protocol source, so `/init` does not +> list it as a parallel read in step 1. + +**Init protocol:** + +0. **Load rules** (read first): `.claude/rules/orchestrator-rules.md`, + `.claude/rules/governed-artifact-reads.md`, + `.claude/rules/adversarial-prompt-refusal.md`. The path-scoped rules + (`ledger-invariants`, `trust-invariants`, `build-commands`) load + themselves when you touch their paths. + +1. **Parallel reads.** Dispatch simultaneously (nothing here mutates the + tree, so there is no ordering): + - `CLAUDE.md`: what Claude Code needs beyond this file + - `README.md`: project description and status + - `standards/spec/contract.md`: the normative corpus contract + - `standards/spec/constitution.md`: the fifteen principles + - `spec-spine compile --check`: registry freshness (non-fatal; see below) + - `spec-spine index check`: index staleness (non-fatal) + - `spec-spine registry status-report --json --nonzero-only`: lifecycle counts + - `spec-spine registry list --ids-only`: the spec inventory + - `spec-spine index coverage`: which source files no spec claims (exit 2 if stale) + - `scripts/spec-dag.sh`: the DAG is acyclic and every dependency is lower-numbered + - `ls crates/ fuzz/ executor/ web/ 2>/dev/null`: what has been built so far (absent directories are expected before their spec lands) + - `ls specs/ docs/design/` + - `git log --oneline -10` and `git diff --stat HEAD~1` + +2. **Emit** an `## initialized: hqgit` block: the layer model in one line + per layer with the crates that exist, a `## lifecycle:` sub-section from + the status report (approved/pending counts, and the next ready spec from + `/next` if cheap), freshness verdicts, recent activity, and a + ready-to-help line. + +**Read discipline:** never parse `.derived/**/*.json` directly (no `jq`, +`python`, `awk`, `sed`); all structural and lifecycle data comes from +`spec-spine` subcommands. + +**Registry freshness:** `spec-spine compile --check` compiles in memory and +compares against the committed shards without writing. Exit `0` is fresh. +Exit `2` is stale: read stderr first (an older CLI rejects the flag with the +same code and `error: unexpected argument '--check'`), then report "Spec +registry: stale, run `spec-spine compile` and commit" naming the drifted +shards, and say the lifecycle counts are the committed (stale) ones. Exit +`1` means the corpus fails validation: surface the violations, report counts +as unverified, and make fixing them the first task. Any other code: report +stderr verbatim, freshness unknown. Never substitute a plain `spec-spine +compile` here; `/init` reports, it does not mutate. + +**Index staleness:** `spec-spine index check` non-zero means "Codebase +index: stale, run `spec-spine index`". Report and continue. + +**CLI missing:** if `spec-spine --version` fails, run `/setup`. Do not fall +back to ad-hoc parsing. + +If any file is missing: log "not found" and continue. + +## Working the backlog + +This repo's backlog is its spec corpus. Every spec with `status: approved` +and `implementation: pending` is a work order. One session implements one +spec, start to finish, then stops. Specs `000` through `003` are records +(`n-a` or `complete`), never work orders. + +1. **Pick the spec.** The lowest-numbered spec with `implementation: + pending` whose `status` is `approved` and whose `depends_on` are all + `implementation: complete` or `n-a`. Use `/next`, or `spec-spine + registry show <id> --json`; never guess. A `draft` spec is never picked: + approval is a human act. If the spec's Territory section names an + operator prerequisite (a service, a credential, a sibling repo) that is + missing, stop and report exactly what is needed instead of mocking + around it. +2. **Branch and flip.** Work on a feature branch named after the spec id + (`017-ledger-entry-dag`). Flip the spec to `implementation: in-progress`, + run `spec-spine compile && spec-spine index`, and commit the flip with + the regenerated `.derived/` shards before writing code. Never commit to + `main`. +3. **Re-read the spec in full before coding.** The design truth precedes + the code. If the design is imprecise, record the choice you make as a + dated `D-n` entry under `## 7. Resolved decisions` (and drop a copy in + `data/orchestrator/decision-dropbox/` when a driven session; the + orchestrator seals it). If the design is *wrong*, stop and report the + contradiction: never edit a spec afterwards to ratify what the code + happened to do (`.claude/rules/adversarial-prompt-refusal.md`). +4. **Implement within the territory.** Every file you add under a crate + must be claimed: add it to this spec's `establishes` list in the same + change (the ownership ratchet, `C-002`, refuses an unclaimed source + file). When you add a third-party dependency, add it to the workspace + manifest's `[workspace.dependencies]` and declare the `extends` edge on + spec 010's `Cargo.toml` section. Touching a file another spec owns + requires an `extends` edge on that spec's unit. Do not edit `.derived/` + by hand. +5. **Hold the frozen invariants.** Nothing that reaches a hashed byte (the + canonical encoder, entry signing bytes, object hashes) may depend on a + clock, an environment read, or map iteration order. A change that alters + any golden vector under `crates/hqgit-types/testdata/vectors/` is a + schema MAJOR and a human decision: stop and report, do not regenerate. +6. **Run the gate before every commit.** `make spine` (compile, index, + lint `--fail-on-warn`, index check, couple, spec-dag), then `make ci` + (adds coverage `--fail-on-untraced` and, once `Cargo.toml` exists, + `cargo build`, `test`, `clippy -D warnings`, `fmt --check`, and `deny`). + All must exit 0. Commit the regenerated `.derived/` shards with the code + they describe. +7. **Satisfy Acceptance criteria verbatim.** Run the spec's `## + Verification` block locally with `/verify <id>`. If a criterion cannot be + satisfied (external state, a missing sibling), keep `implementation: + in-progress`, add a dated Status note to the spec saying exactly what + remains, and report it. Flip to `implementation: complete` only when + acceptance holds; recompile and commit. +8. **Ship.** `/ship` (gate, review, conventional commit naming the spec id + such as `feat(017): ...`, push the feature branch, open the PR). The + PR body is Summary plus Testing; no AI attribution, no session links. + A `Spec-Drift-Waiver:` line needs explicit human approval; a driven + session never self-approves one. Then stop: the next session takes the + next spec. + +## Available Agents + +Agents live in `.claude/agents/`, all self-contained: + +- `architect`: plans and decomposes against the corpus. Read-only. +- `explorer`: searches, traces dependencies, gathers context. Read-only. +- `implementer`: executes focused changes from a plan. Minimal diffs. +- `reviewer`: post-change review for bugs, correctness, spec drift. Read-only. +- `ledger-guardian`: L0/L1 specialist: canonical encoding, hash stability, + golden vectors, tombstones, no ambient inputs in hashed paths. Read-only. +- `trust-reviewer`: L4/L6/L3-cache specialist: signature and rotation + order, transparency inclusion, Biscuit attenuation, cache-as-trust + boundary, policy determinism. Read-only. + +## Available Commands + +Skills live in `.claude/skills/`: + +- `/init`: this protocol. +- `/setup`: install spec-spine and the Rust toolchain; verify the loop. +- `/next`: the next ready spec, with honest blockers when none is ready. +- `/build <id>`: one spec start to finish per "Working the backlog". +- `/verify <id>`: run a spec's `verify:cli` blocks locally. +- `/spec`: author a new spec from the template; next ordinal; DAG check. +- `/commit`: conventional commit, impact-focused, spec id in scope. +- `/code-review`: correctness and spec-drift review of the current diff. +- `/ship`: gate, review, commit on a feature branch, open a PR. +- `/shepherd`: watch the PR's checks, remediate red runs, merge when green, + confirm the merge on disk. +- `/validate-and-fix`: run `make ci` and fix what it surfaces. +- `/cleanup`, `/implement-plan`, `/research`, `/refactor-claude-md`. + +## Conventions + +- Rust 2024, toolchain pinned in `rust-toolchain.toml` (spec 010); always + `--locked`; `unsafe` is forbidden workspace-wide; clippy `-D warnings`. +- Crates depend downward only (thesis §5). `hqgit-cli` and `hqgit-server` + never depend on each other. +- Layer is `domain`, role is `kind`, build wave is `wave`; all three are in + every spec's frontmatter and validated on compile. +- `data/` is the orchestrator's state root for this project; never commit + it. `.derived/` shards are committed; `build-meta.json` is not. +- No em dash anywhere in authored text (a hook enforces file writes). +- Conventional commits, spec id as scope; no AI attribution; no session + links in anything that lands in git or on GitHub. +- Derived artifacts are read only through `spec-spine` subcommands. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..66187e4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# CLAUDE.md + +Read `AGENTS.md` first: it carries the session protocol (`## New Sessions`) +and the backlog discipline (`## Working the backlog`). This file only holds +what Claude Code needs beyond it. + +## What this is + +hqgit is a verifiable evidence ledger for software change: a per-repository +DAG of signed, content-addressed objects covering code, collaboration, and +evidence, with everything else (indexes, feeds, queues, dashboards) a +projection rebuildable from zero. The forge, the CI system, and the agent +runtime are three clients of that ledger. Read +`specs/002-platform-thesis/spec.md` for the layer model, the crate topology, +and the build order; `docs/design/00-architecture.md` for the analysis; +`specs/003-chassis-alignment/spec.md` for the boundary with the rahi +chassis, which waves 1 through 5 never touch and wave 6 composes. + +The repository is specified before it is built. Every ordinary spec is +`approved` + `implementation: pending`, spec ordinals are the build order, +and code lands one spec per session. Before spec 010 lands there is no +`Cargo.toml`; every Makefile target and CI step is guarded for that. + +## Commands + +```sh +make spine # spec-spine compile, index, lint --fail-on-warn, index check, couple, spec-dag +make ci # make spine + index coverage --fail-on-untraced + the cargo gates (when Cargo.toml exists) +make build # cargo build --workspace --locked +make test # cargo test --workspace --locked +make lint # cargo clippy --workspace --all-targets --locked -- -D warnings +make fmt # cargo fmt --all --check +make deny # cargo deny check (when deny.toml exists) +make fuzz # short cargo-fuzz smoke of every target under fuzz/ (spec 012) +make coverage # spec-spine index coverage +make attest # spec-spine attest --with-coupling -> .derived/attestation/ +scripts/verify-spec.sh <id> # run a spec's verify:cli blocks (what the verify stage runs after merge) +scripts/spec-dag.sh # depends_on is acyclic and only points to lower-numbered specs + +# One crate, one test: +cargo test -p hqgit-ledger --locked --test entry +cargo test -p hqgit-types --locked codec:: +``` + +Exit codes of `spec-spine`: `0` ok, `1` validation failure or drift, `2` +stale, `3` I/O, parse, schema, or config. The `hq` binary (spec 032) adopts +the same four. + +## Architecture in one screen + +| Layer | Crates | Founding specs | +|---|---|---| +| L0 objects | `hqgit-object`, `hqgit-git` (bridge) | 013, 031 | +| L1 ledger | `hqgit-types` (codec), `hqgit-ledger`, `hqgit-sync` | 011, 017, 110 | +| L2 domain | `hqgit-types` (nouns), `hqgit-domain` | 010, 023 | +| L3 evaluation | `hqgit-eval`, `executor/` (Go) | 070, 073 | +| L4 trust | `hqgit-trust`, `hqgit-agent` | 060, 100 | +| L5 projection | `hqgit-projection` | 080 | +| L6 policy | `hqgit-policy`, `hqgit-policy-sdk` | 065, 066 | +| L7 edge | `hqgit-cli` (`hq`), `hqgit-mirror`, `hqgit-server`, `web/` | 032, 040, 090, 095 | + +Dependencies point downward only. `hqgit-cli` and `hqgit-server` embed the +same ledger implementation and never depend on each other. + +## Invariants that shape every change + +- **Canonical versus derived.** L0 through L4 are canonical; L5 and above + never write authoritatively. No authoritative row outside the log. +- **Hash stability.** The canonical encoder (spec 011) and entry signing + bytes (spec 017) are frozen by golden vectors under + `crates/hqgit-types/testdata/vectors/`. No clock, env read, float, or + `HashMap` iteration order reaches a hashed byte. A vector change is a + schema MAJOR and a human decision; never regenerate vectors to make a + test pass. +- **Facts are immutable.** Only derived state converges (LWW over HLC). + Sequence CRDTs are for collaborative text only. +- **One evidence primitive.** Everything is an `Attestation`; new evidence + kinds register a predicate, never a new noun. +- **Erasure by tombstone.** The log holds commitments, never content. +- **Agents are a distinct principal.** `Principal::Agent` carries a + delegation chain; it never authenticates as a human. +- **Cache is a trust boundary.** Unattested cache hits are misses for + anything that gates a merge. + +## Governance mechanics + +- Every source file inside a crate must be specifically claimed by a spec + (`require_ownership` is on). Add new files to the implementing spec's + `establishes` in the same change. +- `.derived/` shards are committed; regenerate with `spec-spine compile && + spec-spine index` and commit them with the change. `build-meta.json` is + gitignored. +- Derived artifacts are read only through `spec-spine` subcommands. +- Hooks in `.claude/settings.json` recompile after spec edits, check + staleness after hashed-input edits, block `gh pr create` on a red + coupling gate, and block `git push` to `main`. +- The coherence guard: never edit an owning spec to make the gate pass on + code that contradicts it. Surface the contradiction. + +## House style + +- No em dash character anywhere (chat, code, comments, specs, commits). +- Conventional commits with the spec id as scope: `feat(017): ...`. +- No AI attribution and no session links in commits, PR bodies, or comments. +- Specs follow `standards/spec/templates/spec-template.md`: Purpose, + Territory, Behavior (B-n), Functional requirements (FR-nnn), Acceptance + criteria (AC-n), Out of scope, Resolved decisions (D-n), `## Verification`. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15ce1d7 --- /dev/null +++ b/Makefile @@ -0,0 +1,86 @@ +# hqgit: the one source of truth for what CI validates (spec 001 B-2). +# +# Every target is guarded so the composite is green on the specify-only tree: +# before spec 010 lands there is no Cargo.toml, before spec 012 no fuzz/. +# `make ci` locally means a green CI run. + +SHELL := /bin/bash +.DEFAULT_GOAL := ci + +SPEC_SPINE ?= spec-spine +BASE ?= origin/main +FUZZ_SECONDS ?= 20 + +.PHONY: spine spec-dag ci build test lint fmt deny fuzz coverage attest verify help + +## spine: the governed gate chain (compile, index, lint, index check, couple, spec-dag) +spine: + $(SPEC_SPINE) compile + $(SPEC_SPINE) index + $(SPEC_SPINE) lint --fail-on-warn + $(SPEC_SPINE) index check + $(SPEC_SPINE) couple --base $(BASE) --head HEAD + scripts/spec-dag.sh + +## spec-dag: depends_on is acyclic and only names lower-numbered specs +spec-dag: + scripts/spec-dag.sh + +## ci: everything CI runs, in order +ci: spine + $(SPEC_SPINE) index coverage --fail-on-untraced + $(MAKE) build + $(MAKE) test + $(MAKE) lint + $(MAKE) fmt + $(MAKE) deny + +## build: cargo build (guarded on Cargo.toml) +build: + @if [ -f Cargo.toml ]; then cargo build --workspace --locked; else echo "build: no Cargo.toml yet (lands with spec 010)"; fi + +## test: cargo test (guarded on Cargo.toml) +test: + @if [ -f Cargo.toml ]; then cargo test --workspace --locked; else echo "test: no Cargo.toml yet (lands with spec 010)"; fi + +## lint: clippy with warnings denied (guarded on Cargo.toml) +lint: + @if [ -f Cargo.toml ]; then cargo clippy --workspace --all-targets --locked -- -D warnings; else echo "lint: no Cargo.toml yet (lands with spec 010)"; fi + +## fmt: rustfmt check (guarded on Cargo.toml) +fmt: + @if [ -f Cargo.toml ]; then cargo fmt --all --check; else echo "fmt: no Cargo.toml yet (lands with spec 010)"; fi + +## deny: cargo-deny supply-chain check (guarded on deny.toml and the tool) +deny: + @if [ -f deny.toml ]; then \ + if command -v cargo-deny >/dev/null 2>&1; then cargo deny check; \ + else echo "deny: cargo-deny not installed (cargo install cargo-deny --locked); skipped locally, CI runs it"; fi; \ + else echo "deny: no deny.toml yet (lands with spec 010)"; fi + +## fuzz: a short smoke run of every fuzz target (guarded on fuzz/ and cargo-fuzz) +fuzz: + @if [ -f fuzz/Cargo.toml ]; then \ + if command -v cargo-fuzz >/dev/null 2>&1; then \ + for t in $$(cargo fuzz list); do cargo fuzz run "$$t" -- -max_total_time=$(FUZZ_SECONDS) || exit 1; done; \ + else echo "fuzz: cargo-fuzz not installed (cargo install cargo-fuzz --locked); skipped"; fi; \ + else echo "fuzz: no fuzz/ yet (lands with spec 012)"; fi + +## coverage: which source files no spec specifically claims +coverage: + $(SPEC_SPINE) index coverage + +## attest: the corpus attestation (spec-spine's ledger seal), never committed +attest: + @mkdir -p .derived/attestation + $(SPEC_SPINE) attest --with-coupling > .derived/attestation/corpus.json + @echo "attestation written to .derived/attestation/corpus.json" + +## verify: run one spec's verify:cli blocks, e.g. make verify SPEC=017-ledger-entry-dag +verify: + @test -n "$(SPEC)" || { echo "usage: make verify SPEC=<spec-id>"; exit 2; } + scripts/verify-spec.sh $(SPEC) + +## help: list targets +help: + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //' diff --git a/README.md b/README.md new file mode 100644 index 0000000..c00a67f --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# hqgit + +**A verifiable evidence ledger for software change.** + +Canonical state is a per-repository DAG of signed, content-addressed objects +covering code, collaboration, and evidence. Everything else (indexes, feeds, +queues, dashboards) is a projection rebuildable from zero. The forge is one +client of that ledger, the CI system is another, and the agent runtime is a +third. The wedge is absorption, not replacement: hqgit builds the +verification and review plane over existing GitHub repositories and lets +hosting commoditize underneath. + +## Status: specified, not yet built + +This repository is a complete specification corpus and the harness that +builds it. There is no code under `crates/` yet. Every ordinary spec is +`status: approved` and `implementation: pending`; spec ordinals are the +build order; and each spec is bounded to one driven session's territory. +The corpus is designed to be built by +[claude-observatory](https://github.com/bartekus/claude-observatory), which +schedules the lowest-numbered ready spec, drives one fresh Claude Code +session through `AGENTS.md`'s backlog protocol, ships through this repo's +own `/ship` skill and hooks, shepherds the PR through the CI wired here, and +runs the spec's `## Verification` block after merge. Done is never +self-authored. + +## Reading the corpus + +| Start here | What it is | +|---|---| +| `specs/002-platform-thesis/spec.md` | the layer model, the nouns, the crate topology, the eight-wave build order | +| `specs/003-chassis-alignment/spec.md` | the boundary with the rahi chassis: chassis-free through wave 5, composed at wave 6 | +| `docs/design/00-architecture.md` | the analysis the thesis is derived from | +| `docs/design/01-build-order.md` | the spec DAG, rendered | +| `standards/spec/constitution.md` | the fifteen principles, seven of them frozen at tier 1 | +| `specs/000-hqgit-bootstrap/spec.md` | what a spec is, and the frozen invariants | +| `AGENTS.md` | the session protocol and the backlog discipline | + +The layer model: + +``` +L7 Edge: git-compat endpoint, gRPC/Connect API, sync protocol, UI, agents +L6 Policy: merge predicates as versioned WASM modules +L5 Projection: code graph, search, ecosystem graph, feeds [disposable] +L4 Trust: identities, key rotation, attestation verify, transparency log +L3 Evaluation: hermetic build/test graph, remote execution, action cache +L2 Domain: Change, Revision, Anchor, Review, Attestation, Policy +L1 Ledger: per-repo signed hash-linked event DAG + convergent state +L0 Objects: content-addressed blob/tree store (BLAKE3), chunked +``` + +Rust for the trusted core; Go only at the executor seam; TypeScript for the +review UI. Sixty-four ordinary specs across eight waves; hash stability of +the ledger (specs 011, 012, 017) comes first because it is the only mistake +the project cannot recover from. + +## Governance + +The corpus is governed by [spec-spine](https://github.com/statecrafting/spec-spine) +0.11.0. `make spine` runs the gate (compile, index, lint, index check, +couple, DAG check); `make ci` adds ownership coverage and the cargo gates +once a workspace exists. Derived artifacts under `.derived/` are committed +and read only through `spec-spine` subcommands. Every source file inside a +crate must be specifically claimed by a spec; a session that adds a file +claims it in the spec it is implementing. + +```sh +cargo install spec-spine-cli --locked # or: npm i -g spec-spine@0.11.0 +make spine +spec-spine registry list +scripts/spec-dag.sh +``` + +## Building it with claude-observatory + +```sh +cd ../claude-observatory +bun src/index.ts orchestrator projects add /path/to/hqgit # registers, qualifies, arms +bun src/index.ts orchestrator dag # the readiness view +bun src/index.ts orchestrator next # 010-workspace-and-core-types +bun src/index.ts orchestrator daemon start +``` + +The orchestrator's state root for this project lives under `data/`, which +is gitignored. Any spec can be pulled back to `status: draft` to hold it for +human review; drafts are visible as blockers and never scheduled. + +## License + +AGPL-3.0, see `LICENSE`. diff --git a/docs/design/00-architecture.md b/docs/design/00-architecture.md new file mode 100644 index 0000000..430de25 --- /dev/null +++ b/docs/design/00-architecture.md @@ -0,0 +1,261 @@ +# hqgit: architecture and analysis + +Date: 2026-09-02. This is the design analysis the spec corpus is derived +from. It records conclusions and decisions; the specs under `specs/` are the +authority, and `specs/002-platform-thesis/spec.md` is the normative record of +what this document argues. Where the two differ, the thesis governs. + +## 1. The problem + +GitHub's real product is not Git hosting. It is three things: the identity +and social graph, the proprietary collaboration ledger (issues, pull +requests, reviews, CI results), and the compute. Git is the only portable +layer in the stack. + +That asymmetry is the root defect. The repository is content-addressed, +signed, replicated, and forkable; every fact *about* the repository is a +mutable row in someone else's Postgres, reachable only through their API, +and gone the moment you leave. Most "better GitHub" proposals are feature +lists layered on top of that same shape. The interesting redesigns change +the shape. + +### 1.1 Where the model actually breaks + +1. **Collaboration state is not part of the data model.** Issues, reviews, + approvals, and decisions should be signed, hash-linked, append-only + entries replicated alongside the object graph. Clone the repo, get the + argument that produced it. Offline review, real forkability, migration + without an ETL project. Hosting then sells indexing, notification, and + execution rather than captivity. +2. **The unit of change is a mutable branch pointer.** Gerrit's Change-Id, + and later jj and Sapling, got this right: a change has stable identity + with an ordered sequence of revisions. Force-push stops destroying review + context, stacked changes become native rather than a tooling cottage + industry, and "what changed since I last looked" becomes a first-class + query. Comments anchor to semantic locations (AST node plus content + hash), so they survive rebase. +3. **Review is anchored to text.** The line diff is the lowest-value view of + a change. The high-value views are deltas: public API surface, type + signatures, dependency set, capability set (new network, filesystem, + secret, or syscall access), and observed test behavior. Conflict + detection should be semantic, not textual. +4. **CI is untyped YAML with mutable third-party refs.** Not reproducible, + not runnable locally, cache semantics invented per repo, and a standing + supply-chain hole. The correct model is a hermetic, content-addressed + build and test graph where CI is a pure function of repo state: identical + locally and remotely, globally cached by input hash, affected-target test + selection, and merge-queue correctness falling out as a property. Every + result becomes an attestation over (input hash, toolchain hash, output + hash). +5. **Trust is decorative.** Attribution is an email string; signing is + opt-in and mostly ignored. Instead: verifiable identity with key rotation + history, approvals as signed attestations over a specific revision hash, + a transparency log, and in-toto/SLSA provenance linking source change to + published artifact. "Requires two approvals" stops being UI state and + becomes a checkable predicate over an evidence graph, with policy as + versioned executable code rather than repository settings. +6. **There is no ecosystem graph.** A type-aware cross-repo code index + (SCIP-class) joined to the package dependency graph unlocks downstream + impact analysis, crater-style downstream test runs, codemods proposed as + changes to dependents, and honest usage data. +7. **Agents authenticate as humans holding human tokens.** They need a + distinct principal class: capability-scoped credentials, declared + sandbox, mandatory provenance on every artifact, and an explicit + delegation chain. The deeper economic point: as authoring cost approaches + zero, trusted review capacity becomes the binding constraint. A forge + designed now should optimize verification throughput, not authoring + convenience. Changes arrive carrying machine-checkable evidence so a + human reviews the argument rather than the diff. +8. **Secondary but real.** Attention management is an email firehose with + no prioritization; ownership is a text file with no SLA, delegation, or + expiry; maintainer funding is external; monorepo scale is handled by + bolting LFS onto a model that assumes full clone. + +### 1.2 Recommendation + +A frontal assault fails. Radicle, Sourcehut, and Codeberg are each right +about something and marginal in adoption, because the network effect *is* +the product and migration cost is paid by the wrong party. + +The viable wedge is to build the verification and review plane, not the +host. Federate over existing GitHub repos, mirror collaboration state +bidirectionally, and deliver value that requires no migration: stacked +changes, semantic review, hermetic globally-cached CI, signed provenance, +agent governance. Own the layer where value is currently moving (trust and +review), and let hosting commoditize underneath. + +### 1.3 Tradeoffs accepted + +- Signed, replicated, append-only collaboration data collides with erasure + requirements and moderation. Capability-scoped encryption and content + indirection from day one, not as a patch (spec 020). +- Hermetic builds tax developer ergonomics: everything must be declared, and + the escape hatches are where the model leaks (spec 075). +- Semantic review requires per-language investment and degrades to plain + text across the long tail (spec 025, 051). +- Agent capability scoping adds friction precisely where users want + autonomy (spec 100 to 103). +- Decentralized state raises discovery cost; a central index is rebuilt + anyway, without lock-in as its business model (spec 083, 084, 112). + +## 2. The governing decision + +What is canonical, and what is derived? Canonical state is a set of signed, +content-addressed objects forming a per-repository DAG covering code, +collaboration, and evidence. Every index, timeline, dashboard, search +result, and queue is a projection that must be rebuildable from zero. Hold +that invariant and portability, offline operation, federation, audit, and +schema evolution all fall out of one property. Violate it once and you have +rebuilt GitHub with extra steps. + +## 3. Layer model + +``` +L7 Edge: git-compat endpoint, gRPC/Connect API, sync protocol, UI, agents +L6 Policy: merge predicates as versioned WASM modules +L5 Projection: code graph, search, ecosystem graph, feeds [disposable] +L4 Trust: identities, key rotation, attestation verify, transparency log +L3 Evaluation: hermetic build/test graph, remote execution, action cache +L2 Domain: Change, Revision, Anchor, Review, Attestation, Policy +L1 Ledger: per-repo signed hash-linked event DAG + convergent state +L0 Objects: content-addressed blob/tree store (BLAKE3), chunked +``` + +Boundaries are one-directional: L5 and above may only read from L0 to L4, +never write authoritatively. + +## 4. Layer decisions + +### L0: object store + +BLAKE3 over SHA-256, primarily for verified streaming: the BAO tree gives +chunk-level verification and range proofs, so partial and lazy fetch are +verifiable by construction rather than trusted. Content-defined chunking +(FastCDC) plus a Merkle tree means LFS is just the general path with +different chunk statistics. Git compatibility is a hard requirement, so a +bidirectional object mapping is maintained through `gix` rather than libgit2 +bindings. Local index in redb; remote in any S3-compatible store. +Immutability makes every cache layer trivially correct. + +### L1: ledger + +Entry shape: `{ parents: [Hash], issuer: KeyId, hlc: HybridLogicalClock, +payload: Cid, sig: Signature }`. A hash-linked DAG, not a linear log. + +Facts versus derived state: facts are immutable events ("revision R +submitted", "attestation A issued", "comment C anchored at X") and never +conflict; concurrent facts merge by set union with a deterministic total +order (topological, tiebreak on hash). Derived state (issue open or closed, +labels, assignee, title) is the mutable projection; a hybrid logical clock +with last-writer-wins is sufficient for most of it. Sequence CRDTs are +reserved for genuinely collaborative text. The CRDT surface stays near five +percent of the domain. + +Erasure, decided at entry one: the signed log contains commitments (CIDs), +never user content. Content lives in the blob store, encrypted per namespace +where it must be. Deletion removes the blob; the log keeps a tombstoned +commitment. Not retrofittable. + +Serialization is canonical and deterministic (DAG-CBOR) with +forward-compatible unknown-field preservation. Signed history cannot be +rewritten, so schema evolution is a permanent constraint, and any +nondeterminism in canonicalization silently invalidates every signature +downstream. Hash stability is fuzzed in CI from the first commit. + +Replication: range-based set reconciliation over QUIC for DAG head exchange +(`iroh`, `quinn`; Willow is the prior art to study). Server-side, per-repo +Raft groups over an embedded Raft-replicated SQLite (Hiqlite) let repos +shard across nodes without a separate database tier. + +### L2: domain model + +Every form of evidence is one primitive: `Attestation { subject: Hash, +predicate: PredicateType, issuer: Principal, claim: Cid, sig }`. Human +approval, build provenance, test result, SAST finding, license scan, policy +evaluation, and agent action are all the same shape (in-toto's statement +model, generalized). One storage path, one verification path, one policy +input, one audit trail. + +Other nouns: `Change` (stable identity) with ordered `Revision`s, each a tree +hash plus base; `Anchor { path, tree_sitter_node_path, node_content_hash }`; +`Principal = Human | Agent | Service | Org` at the type level; `Policy` as +versioned, hash-pinned, executable. + +### L3: evaluation plane + +`eval(repo_state_hash, target, toolchain_hash) -> output_hash`, cached +globally on input hash. Implement the Bazel Remote Execution API. Two +sandbox tiers: namespaces plus seccomp with no network for trusted work; +microVMs (Firecracker or Cloud Hypervisor) for fork contributions and agent +execution. Every execution emits a signed provenance attestation. Merge +queues are speculative evaluation over candidate merge states. The cache is a +trust boundary: entries carry executor identity and are verifiable; +unattested cache hits are cache misses for anything that gates a merge. + +### L4: trust plane + +OIDC (Rauthy) for login and workforce federation. Durable identity is a +keypair with a rotation chain in the ledger. Keyless signing in the Sigstore +shape with transparency-log inclusion proofs, self-hosted when sovereignty +matters. Agents carry Biscuit tokens: delegation chain in the token, +monotonic offline-verifiable attenuation, datalog caveats. + +### L6: policy + +Merge predicate `f(change, attestation_set, policy_version) -> Allow | +Deny(reasons)`, deterministic and side-effect free, compiled to WASM with a +typed SDK, hash-pinned to repo state, evaluable locally before push, emitting +an attestation per evaluation. + +## 5. Language + +Rust for the entire trusted core (L0 through L2, L4, L6): type-encodable +invariants, mature libraries, no GC pauses on the content-addressed hot +path, and single-binary embedding so the CLI and the server run the same +ledger implementation. Go for the executor that orchestrates existing +container runtimes. One language boundary, at the REAPI seam; the domain +model stays on the Rust side. + +## 6. Build order + +1. Ledger, object store, domain model, CLI. Local only, no server. Prove + offline review against a plain git repo. +2. Git bridge and bidirectional GitHub mirror. Users without migration. +3. Change/Revision, semantic anchors, stacked changes. First felt value. +4. Attestation model and policy engine. Merge gates become verifiable. +5. Evaluation plane. Highest capex, depends on stable input hashing. +6. Cross-repo code and ecosystem graph. +7. Agent principals and delegation. Designed into the types at step 1. +8. Multi-host federation. + +The corpus encodes this as spec ordinals; see `01-build-order.md` for the +rendered DAG. + +## 7. Standing risks + +- Schema evolution against signed history: version everything, preserve + unknown fields, never reorder. +- CRDT surface creep: hold the facts versus derived-state line under product + pressure. +- Cross-repo indexes are the one legitimately centralized component; isolate + them and keep them non-authoritative. +- Abuse in an append-only replicated store: untrusted contributions land in + a quarantine namespace and are promoted by capability, not accepted by + default. + +The system this becomes is not a forge. It is a verifiable evidence ledger +for software change, where hosting, CI, and review UI are interchangeable +implementations over the same signed history. + +## 8. How this corpus is built + +The corpus is authored in full before any code (thesis D17). Every ordinary +spec is `status: approved`, `implementation: pending`, and bounded to one +driven session's territory. claude-observatory registers this repository as +a project, schedules the lowest-numbered ready spec, drives one fresh Claude +Code session through the `## Working the backlog` protocol in `AGENTS.md`, +ships through the repo's own `/ship` skill and hooks, shepherds the PR +through the CI this corpus wires, and runs the spec's `## Verification` +block after merge. Done is never self-authored: completion is adjudicated by +`spec-spine`'s gate over a corpus the session may not amend in its own +favor. diff --git a/docs/design/01-build-order.md b/docs/design/01-build-order.md new file mode 100644 index 0000000..7b7f497 --- /dev/null +++ b/docs/design/01-build-order.md @@ -0,0 +1,336 @@ +# hqgit build order: the spec DAG, rendered + +Generated from `spec-spine registry list --json` by the architect on +2026-09-02, refreshed 2026-09-03 when 003 landed; regenerate after any +change to `depends_on` or `wave`. +Prose, not authority: the specs govern, this page only renders them. + +The orchestrator schedules the lowest-numbered spec that is `approved`, +`implementation: pending`, and whose every dependency is shipped at its +pinned hash. Because every dependency is lower-numbered (enforced by +`scripts/spec-dag.sh`), ordinal order is a valid topological order, and the +waves below are the thesis's eight build steps (spec 002 §6). + +Totals: 68 specs; longest dependency chain 19 deep. + +## Wave 1 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 000-hqgit-bootstrap | Bootstrap spec system for hqgit (specify first, build by spec) | governance | constitutional-bootstrap | critical | 0 | none | 0 | +| 001-agentic-harness | Agentic engineering harness: session protocol, skills, agents, hooks, gate | governance | governance | high | 1 | 000 | 15 | +| 002-platform-thesis | Platform thesis: a verifiable evidence ledger for software change | governance | thesis | critical | 1 | 000 | 8 | +| 003-chassis-alignment | Chassis alignment: what the hosted edge consumes from rahi, and what stays hqgit's | governance | governance | critical | 2 | 002 | 0 | +| 010-workspace-and-core-types | Cargo workspace and the core types: Hash, Cid, Principal, keys, Hlc, Error | l2-domain | kernel | critical | 2 | 002 | 13 | +| 011-canonical-encoding | Canonical encoding: deterministic DAG-CBOR, the Value model, envelopes, unknown-field preservation | l1-ledger | kernel | critical | 3 | 010 | 9 | +| 012-hash-stability-gate | Hash stability gate: fuzz targets, the golden-vector walk, and the cross-platform CI matrix | l1-ledger | tooling | critical | 4 | 011 | 7 | +| 013-object-store | Object store: content-addressed blob and tree objects, the ObjectStore trait, memory and local backends | l0-objects | kernel | critical | 4 | 011 | 8 | +| 014-content-defined-chunking | Content-defined chunking: FastCDC with frozen parameters and the chunked blob manifest | l0-objects | kernel | high | 5 | 013 | 8 | +| 015-verified-streaming | Verified streaming: BAO outboard trees, range proofs, and lazy fetch that never trusts a byte | l0-objects | kernel | high | 6 | 014 | 9 | +| 016-remote-object-backend | Remote object backend: an S3-compatible store and the layered read-through cache | l0-objects | feature | medium | 7 | 013, 015 | 6 | +| 017-ledger-entry-dag | Ledger entry and the hash-linked DAG: shape, signing bytes, append, verify | l1-ledger | kernel | critical | 5 | 011, 013 | 9 | +| 018-deterministic-total-order | Deterministic total order over the DAG and hybrid logical clock generation | l1-ledger | kernel | critical | 6 | 017 | 4 | +| 019-facts-and-derived-state | Facts and derived state: the immutable fact envelope, the registry seam, LWW registers, and the reserved sequence CRDT | l1-ledger | kernel | critical | 7 | 018 | 8 | +| 020-commitments-and-tombstones | Commitments and tombstones: content indirection, per-namespace encryption, and erasure that keeps the chain verifiable | l1-ledger | kernel | critical | 8 | 019 | 11 | +| 021-local-repository | Local repository: the .hq layout, the persistent entry store, namespaces, and the append path | l1-ledger | kernel | high | 9 | 020 | 7 | +| 023-domain-fact-vocabulary | Domain fact vocabulary: the typed facts, their ids, and their validation | l2-domain | kernel | high | 8 | 019 | 7 | +| 024-change-and-revision | Change and Revision: stable change identity over an ordered revision sequence | l2-domain | kernel | high | 9 | 023 | 5 | +| 025-semantic-anchors | Semantic anchors: comments that survive rebase by resolving against content | l2-domain | kernel | high | 10 | 024 | 10 | +| 026-review-threads | Review threads: anchored comments, resolution state, and approvals as attestations | l2-domain | feature | medium | 11 | 025 | 4 | +| 027-attestation-primitive | The attestation primitive: one signed evidence shape, a predicate registry, in-toto interop | l2-domain | kernel | critical | 9 | 023 | 7 | +| 028-issues-and-derived-state | Issues as facts with converging derived state: registers and an add-wins label set | l2-domain | feature | medium | 9 | 023 | 4 | +| 031-git-object-bridge | Git object bridge: gix import and export, the bidirectional oid map, worktree snapshots | l0-objects | kernel | high | 6 | 014 | 10 | +| 032-cli-skeleton | The hq binary: clap frame, exit codes, JSON output, init, status, log | l7-edge | kernel | high | 10 | 021 | 10 | +| 033-cli-offline-review | Offline review from the CLI: changes, revisions, anchored threads, approvals | l7-edge | feature | high | 12 | 032, 031, 026, 027 | 7 | +| 034-cli-attest-and-verify | hq attest and hq verify: issue any attestation, verify the chain and its evidence offline | l7-edge | feature | high | 13 | 033 | 5 | + +## Wave 2 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 040-github-mirror-import | GitHub mirror import: issues, pull requests, reviews, and checks become quarantined facts | l7-edge | feature | high | 12 | 028, 026, 027, 031 | 12 | +| 041-github-mirror-export | GitHub mirror export and reconciliation: local facts back to GitHub, loop-safe and idempotent | l7-edge | feature | high | 13 | 040 | 5 | +| 042-mirror-sync-command | hq mirror: the sync loop with cursors, backoff, and reports | l7-edge | feature | medium | 14 | 041, 032 | 7 | + +## Wave 3 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 050-stacked-changes | Stacked changes: dependency facts, a deterministic stack order, restack plans, and interdiff | l2-domain | feature | high | 10 | 024 | 6 | +| 051-semantic-deltas | Semantic deltas: API surface, dependency, and capability changes as attestations | l2-domain | feature | high | 11 | 027, 050, 025 | 10 | +| 052-semantic-conflicts | Semantic conflicts: pairwise conflict detection over deltas and anchors, with text kept separate | l2-domain | feature | medium | 12 | 051 | 6 | + +## Wave 4 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 060-identity-and-key-rotation | Identity as a keypair with a rotation chain recorded in the ledger | l4-trust | kernel | critical | 8 | 019 | 7 | +| 061-oidc-login | OIDC login: authorization code with PKCE, device flow, and subject binding | l4-trust | feature | high | 9 | 060 | 7 | +| 062-transparency-log | Transparency log: Merkle log, inclusion and consistency proofs, signed checkpoints | l4-trust | kernel | critical | 9 | 060 | 7 | +| 063-keyless-signing | Keyless signing: short-lived certificates bound to an OIDC identity, the signature bundle, and bundle verification | l4-trust | feature | high | 10 | 061, 062 | 7 | +| 064-attestation-verification | Attestation verification: the fixed pipeline from signature to verified set | l4-trust | kernel | critical | 10 | 060, 062, 027 | 7 | +| 065-policy-engine | Policy engine: deterministic WASM merge predicates, hash-pinned | l6-policy | kernel | critical | 10 | 027, 024 | 9 | +| 066-policy-sdk | Policy SDK: typed Rust policies compiled to WASM, testable natively | l6-policy | feature | high | 11 | 065 | 12 | +| 067-policy-evaluation-attestation | Policy evaluation as evidence: the policy-eval attestation, replay, and hq policy | l6-policy | kernel | critical | 14 | 065, 034 | 9 | +| 068-policy-in-repo | Policy in the repository: pinned by fact, resolved by clock, no settings table | l6-policy | feature | high | 15 | 067, 021 | 7 | + +## Wave 5 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 070-reapi-types | Remote Execution API types: vendored protos, BLAKE3 digests, actions, the CAS adapter | l3-evaluation | kernel | high | 5 | 013 | 12 | +| 071-action-cache | Action cache as a trust boundary: attested entries, gating versus advisory lookups | l3-evaluation | kernel | critical | 11 | 070, 064 | 6 | +| 072-execution-service | Execution service: REAPI Capabilities, CAS, ByteStream, ActionCache, Execution, and the scheduler | l3-evaluation | feature | high | 12 | 071 | 14 | +| 073-sandbox-executor | Sandbox executor: the Go worker with namespace and microVM tiers | l3-evaluation | feature | critical | 13 | 072 | 16 | +| 074-execution-provenance | Execution provenance: every completed action emits a signed SLSA attestation | l3-evaluation | kernel | critical | 13 | 072, 064 | 5 | +| 075-build-graph | Build graph: the hq-build.toml manifest, pinned toolchains, and affected-target selection | l3-evaluation | feature | high | 6 | 070 | 9 | +| 076-merge-queue | Merge queue: speculative evaluation over candidate merge states, batching, and bisection | l3-evaluation | feature | high | 15 | 075, 074, 067 | 5 | + +## Wave 6 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 080-projection-framework | Projection framework: disposable read models folded from the total order | l5-projection | kernel | high | 11 | 021, 032 | 13 | +| 081-change-and-review-views | Change and review views: the SQL read models for changes, threads, attestations, stacks | l5-projection | feature | medium | 12 | 080, 026, 027, 050 | 9 | +| 082-search-index | Search index: a tantivy projection over changes, comments, issues, and attestations | l5-projection | feature | medium | 12 | 080 | 6 | +| 083-code-graph | Code graph: a cross-repository SCIP symbol projection keyed by tree | l5-projection | feature | high | 12 | 080, 025 | 10 | +| 084-ecosystem-graph | Ecosystem graph: package dependencies joined to the code graph for downstream impact | l5-projection | feature | high | 13 | 083, 075, 051 | 8 | +| 085-attention-feeds | Attention feeds: a per-principal ranked feed where every item carries its reason | l5-projection | feature | low | 13 | 081 | 6 | +| 090-server-skeleton | The hqgit-server binary: one listener for HTTP and gRPC, config, the repo registry, health, shutdown | l7-edge | kernel | high | 10 | 021 | 11 | +| 091-per-repo-control-plane | Per-repo control plane: one Raft group per repository over hiqlite, proposal, apply, placement | l7-edge | kernel | critical | 11 | 090 | 8 | +| 092-git-endpoint | Git smart HTTP endpoint: refs as a projection of changes, push as revision submission, gated main | l7-edge | feature | high | 16 | 090, 031, 024, 068 | 9 | +| 093-connect-api | The typed API: gRPC and Connect JSON services for changes, reviews, attestations, policy, repos; the CLI remote client | l7-edge | feature | high | 15 | 090, 026, 027, 067, 032, 081 | 28 | +| 094-quarantine-and-promotion | Quarantine and promotion: untrusted writes land quarantined, capabilities are attestations, promotion is a fact | l7-edge | kernel | critical | 12 | 091, 064 | 8 | +| 095-web-review-ui | The web review client: change list, change detail with semantic deltas first, threads, the evidence panel | l7-edge | feature | medium | 16 | 093, 081 | 19 | + +## Wave 7 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 100-agent-principals | Agent principals: registration facts, Biscuit tokens, and the caveat vocabulary | l4-trust | kernel | critical | 16 | 060, 068 | 9 | +| 101-delegation-chain | Delegation chain: delegation facts, chain resolution to a human root, and authorize | l4-trust | kernel | critical | 17 | 100 | 6 | +| 102-agent-sandbox-and-provenance | Agent sandbox and provenance: declared sandboxes, the agent-action attestation, and the server seam | l7-edge | feature | high | 18 | 101, 074, 090 | 10 | +| 103-evidence-carrying-changes | Evidence-carrying changes: the bundle, the argument view, and the evidence_required policy | l6-policy | feature | high | 19 | 102, 067, 051 | 13 | +| 104-ownership-and-sla | Ownership with delegation, expiry, and SLA: facts that replace CODEOWNERS | l2-domain | feature | medium | 11 | 028, 060, 025 | 5 | + +## Wave 8 + +| id | title | layer | kind | risk | depth | depends on | units | +|---|---|---|---|---|---|---|---| +| 110-set-reconciliation | Range-based set reconciliation over entry hashes: fingerprints, the split protocol, and ingest | l1-ledger | kernel | high | 10 | 021 | 8 | +| 111-quic-transport | QUIC transport: identity-bound endpoints, the sync session, and the remote object source | l1-ledger | feature | high | 11 | 110, 060 | 7 | +| 112-federation | Federation: peer registries as facts, the server sync driver, holds for unpromoted peers, and hq sync | l7-edge | feature | high | 13 | 111, 094, 032 | 13 | + +## The DAG + +Edges point from a spec to what it depends on. Wave groupings are subgraphs. + +```mermaid +flowchart BT + subgraph W1["Wave 1"] + S000["000-hqgit-bootstrap"] + S001["001-agentic-harness"] + S002["002-platform-thesis"] + S003["003-chassis-alignment"] + S010["010-workspace-and-core-types"] + S011["011-canonical-encoding"] + S012["012-hash-stability-gate"] + S013["013-object-store"] + S014["014-content-defined-chunking"] + S015["015-verified-streaming"] + S016["016-remote-object-backend"] + S017["017-ledger-entry-dag"] + S018["018-deterministic-total-order"] + S019["019-facts-and-derived-state"] + S020["020-commitments-and-tombstones"] + S021["021-local-repository"] + S023["023-domain-fact-vocabulary"] + S024["024-change-and-revision"] + S025["025-semantic-anchors"] + S026["026-review-threads"] + S027["027-attestation-primitive"] + S028["028-issues-and-derived-state"] + S031["031-git-object-bridge"] + S032["032-cli-skeleton"] + S033["033-cli-offline-review"] + S034["034-cli-attest-and-verify"] + end + subgraph W2["Wave 2"] + S040["040-github-mirror-import"] + S041["041-github-mirror-export"] + S042["042-mirror-sync-command"] + end + subgraph W3["Wave 3"] + S050["050-stacked-changes"] + S051["051-semantic-deltas"] + S052["052-semantic-conflicts"] + end + subgraph W4["Wave 4"] + S060["060-identity-and-key-rotation"] + S061["061-oidc-login"] + S062["062-transparency-log"] + S063["063-keyless-signing"] + S064["064-attestation-verification"] + S065["065-policy-engine"] + S066["066-policy-sdk"] + S067["067-policy-evaluation-attestation"] + S068["068-policy-in-repo"] + end + subgraph W5["Wave 5"] + S070["070-reapi-types"] + S071["071-action-cache"] + S072["072-execution-service"] + S073["073-sandbox-executor"] + S074["074-execution-provenance"] + S075["075-build-graph"] + S076["076-merge-queue"] + end + subgraph W6["Wave 6"] + S080["080-projection-framework"] + S081["081-change-and-review-views"] + S082["082-search-index"] + S083["083-code-graph"] + S084["084-ecosystem-graph"] + S085["085-attention-feeds"] + S090["090-server-skeleton"] + S091["091-per-repo-control-plane"] + S092["092-git-endpoint"] + S093["093-connect-api"] + S094["094-quarantine-and-promotion"] + S095["095-web-review-ui"] + end + subgraph W7["Wave 7"] + S100["100-agent-principals"] + S101["101-delegation-chain"] + S102["102-agent-sandbox-and-provenance"] + S103["103-evidence-carrying-changes"] + S104["104-ownership-and-sla"] + end + subgraph W8["Wave 8"] + S110["110-set-reconciliation"] + S111["111-quic-transport"] + S112["112-federation"] + end + S001 --> S000 + S002 --> S000 + S003 --> S002 + S010 --> S002 + S011 --> S010 + S012 --> S011 + S013 --> S011 + S014 --> S013 + S015 --> S014 + S016 --> S013 + S016 --> S015 + S017 --> S011 + S017 --> S013 + S018 --> S017 + S019 --> S018 + S020 --> S019 + S021 --> S020 + S023 --> S019 + S024 --> S023 + S025 --> S024 + S026 --> S025 + S027 --> S023 + S028 --> S023 + S031 --> S014 + S032 --> S021 + S033 --> S032 + S033 --> S031 + S033 --> S026 + S033 --> S027 + S034 --> S033 + S040 --> S028 + S040 --> S026 + S040 --> S027 + S040 --> S031 + S041 --> S040 + S042 --> S041 + S042 --> S032 + S050 --> S024 + S051 --> S027 + S051 --> S050 + S051 --> S025 + S052 --> S051 + S060 --> S019 + S061 --> S060 + S062 --> S060 + S063 --> S061 + S063 --> S062 + S064 --> S060 + S064 --> S062 + S064 --> S027 + S065 --> S027 + S065 --> S024 + S066 --> S065 + S067 --> S065 + S067 --> S034 + S068 --> S067 + S068 --> S021 + S070 --> S013 + S071 --> S070 + S071 --> S064 + S072 --> S071 + S073 --> S072 + S074 --> S072 + S074 --> S064 + S075 --> S070 + S076 --> S075 + S076 --> S074 + S076 --> S067 + S080 --> S021 + S080 --> S032 + S081 --> S080 + S081 --> S026 + S081 --> S027 + S081 --> S050 + S082 --> S080 + S083 --> S080 + S083 --> S025 + S084 --> S083 + S084 --> S075 + S084 --> S051 + S085 --> S081 + S090 --> S021 + S091 --> S090 + S092 --> S090 + S092 --> S031 + S092 --> S024 + S092 --> S068 + S093 --> S090 + S093 --> S026 + S093 --> S027 + S093 --> S067 + S093 --> S032 + S093 --> S081 + S094 --> S091 + S094 --> S064 + S095 --> S093 + S095 --> S081 + S100 --> S060 + S100 --> S068 + S101 --> S100 + S102 --> S101 + S102 --> S074 + S102 --> S090 + S103 --> S102 + S103 --> S067 + S103 --> S051 + S104 --> S028 + S104 --> S060 + S104 --> S025 + S110 --> S021 + S111 --> S110 + S111 --> S060 + S112 --> S111 + S112 --> S094 + S112 --> S032 +``` + +## Roots and leaves + +- Roots (no dependencies): 000-hqgit-bootstrap +- Leaves (nothing depends on them): 001-agentic-harness, 003-chassis-alignment, 012-hash-stability-gate, 016-remote-object-backend, 042-mirror-sync-command, 052-semantic-conflicts, 063-keyless-signing, 066-policy-sdk, 073-sandbox-executor, 076-merge-queue, 082-search-index, 084-ecosystem-graph, 085-attention-feeds, 092-git-endpoint, 095-web-review-ui, 103-evidence-carrying-changes, 104-ownership-and-sla, 112-federation +- Most depended upon: 027-attestation-primitive (7), 060-identity-and-key-rotation (6), 021-local-repository (5), 032-cli-skeleton (5), 013-object-store (4), 024-change-and-revision (4), 025-semantic-anchors (4), 026-review-threads (4) + diff --git a/scripts/spec-dag.sh b/scripts/spec-dag.sh new file mode 100755 index 0000000..ae38502 --- /dev/null +++ b/scripts/spec-dag.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# spec-dag.sh: the DAG check the coupling gate does not do (spec 001 FR-001). +# +# Reads `spec-spine registry list --json` (a typed CLI read, never the +# .derived/ shards) and refuses: +# - a depends_on target that is not in the corpus, +# - a depends_on target with a higher or equal ordinal (build order is the +# ordinal; claude-observatory schedules lowest-numbered-ready), +# - any cycle, naming the path. +# Exit 0 clean, 1 on a violation, 3 when spec-spine (or python3) is absent. +set -u + +if ! command -v spec-spine >/dev/null 2>&1; then + echo "spec-dag: spec-spine not on PATH (run /setup)" >&2 + exit 3 +fi +if ! command -v python3 >/dev/null 2>&1; then + echo "spec-dag: python3 not available" >&2 + exit 3 +fi + +repo="${1:-.}" +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT +if ! spec-spine registry list --json --repo "$repo" > "$tmp"; then + echo "spec-dag: spec-spine registry list failed (run spec-spine compile first?)" >&2 + exit 3 +fi + +# The program arrives on stdin (the heredoc), so the registry JSON travels by +# file path, never by the same stream. +python3 - "$tmp" <<'PY' +import json, sys + +with open(sys.argv[1], encoding="utf-8") as fh: + specs = json.load(fh) +deps = {s["id"]: list(s.get("dependsOn") or []) for s in specs} +ordinal = {sid: int(sid[:3]) for sid in deps} +violations = [] + +for sid, ds in sorted(deps.items()): + for d in ds: + if d not in deps: + violations.append(f"{sid} depends on unknown spec {d}") + elif ordinal[d] >= ordinal[sid]: + violations.append(f"{sid} depends on {d}, which is not lower-numbered (build order is the ordinal)") + +WHITE, GREY, BLACK = 0, 1, 2 +color = {sid: WHITE for sid in deps} +stack = [] +cycle = None + +def visit(sid): + global cycle + color[sid] = GREY + stack.append(sid) + for d in deps.get(sid, []): + if d not in deps: + continue + if color[d] == GREY: + cycle = stack[stack.index(d):] + [d] + return True + if color[d] == WHITE and visit(d): + return True + stack.pop() + color[sid] = BLACK + return False + +for sid in sorted(deps, key=lambda s: ordinal[s]): + if color[sid] == WHITE and visit(sid): + break + +if cycle: + violations.append("dependency cycle refuses scheduling: " + " -> ".join(cycle)) + +if violations: + for v in violations: + print(f"spec-dag: {v}", file=sys.stderr) + sys.exit(1) + +roots = [s for s, ds in deps.items() if not ds] +print(f"spec-dag: {len(deps)} spec(s), acyclic, every dependency lower-numbered, {len(roots)} root(s)") +PY diff --git a/scripts/verify-spec.sh b/scripts/verify-spec.sh new file mode 100755 index 0000000..6bf1c34 --- /dev/null +++ b/scripts/verify-spec.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# verify-spec.sh <spec-id>: run a spec's `verify:cli` blocks locally (spec 001 +# FR-002). This is what claude-observatory's verify stage runs after merge, in +# a clean checkout of the merged sha: every non-comment, non-blank line inside +# a ```verify:cli fence, from the repo root, in order, stopping at the first +# non-zero exit. A spec with no `## Verification` section prints +# `not-declared` and exits 0. `verify:browser` blocks are reported and skipped; +# only the orchestrator drives those. +set -u + +id="${1:-}" +if [ -z "$id" ]; then + echo "usage: scripts/verify-spec.sh <spec-id>" >&2 + exit 2 +fi + +root="$(cd "$(dirname "$0")/.." && pwd)" +spec="$root/specs/$id/spec.md" +if [ ! -f "$spec" ]; then + echo "verify: no such spec: $spec" >&2 + exit 2 +fi + +# The Verification section: from the exact heading to the next H2. +section="$(awk ' + /^## Verification[[:space:]]*$/ { on = 1; next } + on && /^## / { exit } + on { print } +' "$spec")" + +if [ -z "$section" ]; then + echo "verify: $id: not-declared (no ## Verification section)" + exit 0 +fi + +# Fenced blocks: tag on the opening line, body until a bare closing fence. +commands="$(printf '%s\n' "$section" | awk ' + /^```verify:cli[[:space:]]*$/ { inblock = 1; next } + /^```verify:browser[[:space:]]*$/ { browser = 1; next } + /^```[[:space:]]*$/ { inblock = 0; browser = 0; next } + inblock { print } + END { if (browser_seen) {} } +')" + +browser_count="$(printf '%s\n' "$section" | grep -c '^```verify:browser' || true)" +if [ "${browser_count:-0}" -gt 0 ]; then + echo "verify: $id: $browser_count verify:browser block(s) are driven by the orchestrator; skipped here" +fi + +ran=0 +while IFS= read -r line; do + trimmed="${line#"${line%%[![:space:]]*}"}" + case "$trimmed" in + ""|\#*) continue ;; + esac + ran=$((ran + 1)) + echo "[verify] \$ $trimmed" + (cd "$root" && sh -c "$trimmed") + code=$? + echo "[verify] exit $code" + if [ "$code" -ne 0 ]; then + echo "verify: $id: FAILED at command $ran" >&2 + exit "$code" + fi +done <<EOF +$commands +EOF + +if [ "$ran" -eq 0 ]; then + echo "verify: $id: not-declared (Verification section holds no verify:cli commands)" + exit 0 +fi +echo "verify: $id: passed ($ran command(s))" diff --git a/spec-spine.toml b/spec-spine.toml new file mode 100644 index 0000000..fe513f3 --- /dev/null +++ b/spec-spine.toml @@ -0,0 +1,92 @@ +# spec-spine.toml governs this repository. +# +# hqgit is specified before it is built: the corpus under specs/ is the whole +# design, and the code under crates/ arrives one spec per driven session +# (AGENTS.md, "Working the backlog"). Every knob below is deliberate; see the +# spec-spine adoption guide for the full table. + +[manifest] +# Cargo `[package.metadata.spec-spine].spec` and package.json `"spec-spine".spec`. +metadata_namespace = "spec-spine" + +[domains] +# The `domain` of a spec is the architectural layer it lives in (spec 002 §2). +# Closed enum: a spec outside the layer model is a compile error (V-005), and a +# spec without a domain is a lint warning (L-002) that fails the gate. +allowed = [ + "governance", + "l0-objects", + "l1-ledger", + "l2-domain", + "l3-evaluation", + "l4-trust", + "l5-projection", + "l6-policy", + "l7-edge", +] + +[kind] +# Closed enum (V-006 / L-003). `kernel` specs are load-bearing and hash-frozen; +# `feature` specs are replaceable above them; `tooling` is repo machinery. +allowed = [ + "constitutional-bootstrap", + "thesis", + "governance", + "kernel", + "feature", + "tooling", +] + +[layout] +specs_dir = "specs" +derived_dir = ".derived" +standards_dir = "standards/spec" +schemas_dir = "standards/schemas" +cargo_workspace = "Cargo.toml" +# cargo-fuzz keeps its own workspace under fuzz/ (spec 012); discover it too. +standalone_rust_workspaces = ["fuzz"] +npm_workspaces = ["package.json", "pnpm-workspace.yaml"] +# The review SPA (spec 095) is a standalone npm package, not a workspace. +standalone_npm_packages = ["web"] + +[index] +# Beyond the always-hashed core (manifests, specs, span-backing sources): the +# surfaces that steer driven sessions, and the design doc the thesis cites. +extra_hashed_inputs = [ + "standards/**", + ".github/workflows/**", + ".claude/agents/**", + ".claude/rules/**", + ".claude/skills/**", + ".claude/settings.json", + "AGENTS.md", + "CLAUDE.md", + "Makefile", + "docs/design/**", +] +resolver_exclusions = ["target", "node_modules", ".derived", "dist", "build", ".next", "data"] + +[branding] +compiler_id = "spec-spine" +indexer_id = "spec-spine" + +[coupling] +waiver_keyword = "Spec-Drift-Waiver:" +# Additive to the built-in floor (docs/, README.md, LICENSE, .derived/, lockfiles, ...). +# Per-crate READMEs are prose inside spec-owned crate directories. +bypass_prefixes = ["**/README.md", "**/CHANGELOG.md"] +# The ownership ratchet is on from day one (spec 032): this is a greenfield +# corpus, so every source file a session adds must be claimed by the spec it +# implements. C-002 refuses a changed file only a manifest floor covers. +require_ownership = true +# Dependabot-class version-pin bumps self-waive (spec 005 §3.5, spec 030). +auto_waive_dependency_only = true + +[provenance.uri_schemes] +knowledge = "knowledge://" +code-fingerprint = "fingerprint://" + +[frontmatter] +# `wave` is the build-order wave a spec belongs to (spec 002 §6), carried +# verbatim into the registry for the orchestrator and the dashboards. +extra_known_keys = ["wave"] diff --git a/specs/000-hqgit-bootstrap/spec.md b/specs/000-hqgit-bootstrap/spec.md new file mode 100644 index 0000000..3b73dc2 --- /dev/null +++ b/specs/000-hqgit-bootstrap/spec.md @@ -0,0 +1,179 @@ +--- +id: "000-hqgit-bootstrap" +title: "Bootstrap spec system for hqgit (specify first, build by spec)" +status: approved +kind: "constitutional-bootstrap" +domain: "governance" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: n-a +risk: critical +wave: 1 +origin: + retroactive: true # authority held since before the graph existed +unamendable: + - "markdown-truth-boundary" + - "json-truth-boundary" + - "determinism-requirement" + - "directory-name-equals-id" + - "typed-authority-graph" + - "refusal-rule" + - "canonical-derived-boundary" + - "facts-immutable" + - "hash-stability" + - "single-evidence-primitive" + - "erasure-by-tombstone" + - "agent-principal-class" + - "layer-direction" +summary: > + Foundational contract for the hqgit corpus. Authored truth lives only in + markdown with YAML frontmatter; machine-consumable truth about the corpus is + compiler-emitted JSON read only through spec-spine; every artifact is a + deterministic function of (config, file contents); and a typed authority + graph governs who owns what. hqgit is specified in full before a line of it + is built: the corpus is the design, spec numbers are the build order, and an + orchestrator drives one spec per fresh session through build, ship, + shepherd, and verify. This spec also freezes the seven system invariants + (constitution VI through XIII) that no later spec may amend, because the + first of them (hash stability of the ledger) is the only mistake the + project cannot recover from. +--- + +# 000: Bootstrap spec system for hqgit + +This is the spec that defines what a spec *is* for hqgit. It sits at the top +of the constitutional hierarchy (`standards/spec/constitution.md` is +subordinate to it). It was authored by hand on 2026-09-02, before any code +existed, together with the whole corpus it governs; the code is built to +satisfy the corpus, one spec per driven session, and the coupling gate holds +from the first governed commit. + +## 1. The authoring / derived boundary + +There are exactly two kinds of truth in this repository. + +- **Authored truth** lives only in markdown (`specs/NNN-slug/spec.md`, + `standards/`), with YAML frontmatter. Humans, and agents holding explicit + authority, write authored truth. *(anchor: `markdown-truth-boundary`)* +- **Machine-consumable truth** about the corpus is emitted only by + `spec-spine`, as JSON, into `.derived/`. No hand-authored JSON is + authoritative; compiled JSON is read only through `spec-spine` subcommands, + never by `jq`, `grep`, or a hand-rolled reader. *(anchor: + `json-truth-boundary`)* + +The derived shard trees are committed. `build-meta.json` (the only wall-clock +artifact) is gitignored. + +## 2. Identity: directory name equals id + +A spec's directory under `specs/` is named exactly `NNN-slug`; its `id` +equals that name; `NNN` is unique across the corpus. In this corpus `NNN` is +also the build order: a spec's `depends_on` names only lower-numbered specs, +so the orchestrator's "lowest-numbered ready spec" rule reproduces the +thesis's build order (spec 002 §6) without a second scheduling table. +*(anchor: `directory-name-equals-id`)* + +## 3. The typed authority graph + +Specs declare typed edges (`establishes`, `extends`, `refines`, +`supersedes`, `amends`, `co_authority`, `constrains`, `references`) and the +units they own (`file`, `section`, `symbol`, `directory`, `crate`, `module`). +Authority is derived by walking the graph. `references` is non-owning. +*(anchor: `typed-authority-graph`)* + +Corpus rules on top of the grammar: + +- A crate-founding spec `establishes` the crate's `Cargo.toml`, `src/lib.rs`, + and each of its own source files explicitly, plus its `tests/` and + `testdata/` subtrees. +- A later spec that adds modules to an existing crate `establishes` its own + files and `extends` the founder's `src/lib.rs` (re-exports) and, when it + adds a dependency, the founder's `Cargo.toml` and the workspace manifest's + `workspace.dependencies` section (spec 010). +- `[coupling] require_ownership` is on. Every source file inside a crate MUST + be specifically claimed. A build session that adds a file adds it to the + `establishes` list of the spec it is implementing, in the same change. +- The manifest floor (`[package.metadata.spec-spine].spec`) names the + crate-founding spec and exists for drift, not for coverage. + +## 4. Determinism + +Every artifact-producing step of the corpus toolchain is a pure function of +`(config, file contents)`. *(anchor: `determinism-requirement`)* + +## 5. The refusal rule + +If the coupling gate fails because code and its owning spec disagree, no +agent resolves it by editing the spec to match the code it just wrote. The +contradiction is surfaced to a human, or to an agent with explicit authority +recorded in the spec's Territory section. *(anchor: `refusal-rule`)* + +## 6. The frozen system invariants + +The following invariants of the system hqgit describes are frozen here, at +tier 1, so that no ordinary spec and no amendment to the constitution can +weaken them. Each is stated in full in the constitution; the anchor is the +freeze. + +- Canonical state is the signed, content-addressed per-repository DAG (L0 + through L4). Everything else is a rebuildable projection. *(anchor: + `canonical-derived-boundary`)* +- Facts are immutable and merge by set union; only derived state converges. + *(anchor: `facts-immutable`)* +- The canonical encoding and the ledger entry hash are frozen from the first + signed entry; unknown fields are preserved; nothing is reordered inside a + schema MAJOR; golden vectors are the record. *(anchor: `hash-stability`)* +- All evidence is one `Attestation` primitive. *(anchor: + `single-evidence-primitive`)* +- The log holds commitments, never content; deletion is a tombstone over a + commitment, never a rewrite. *(anchor: `erasure-by-tombstone`)* +- Agents are a distinct principal class with a delegation chain in their + credential. *(anchor: `agent-principal-class`)* +- Layers depend downward only; L5 and above never write authoritatively; the + CLI and the server share one ledger implementation. *(anchor: + `layer-direction`)* + +## 7. Corpus conventions + +- **Frontmatter.** Every ordinary spec carries `kind`, `domain` (its layer), + `implementation`, `risk`, `authors`, `wave`, and a non-empty `depends_on`. + `domain` and `kind` are closed enums (`spec-spine.toml`). +- **Body.** Sections in order: Purpose, Territory, Behavior (B-n with + MUST/SHOULD/MAY), Functional requirements (FR-nnn), Acceptance criteria + (AC-n), Out of scope, Resolved decisions (D-n), and an unnumbered + `## Verification` section holding `verify:cli` fenced blocks (one shell + command per line, run after merge by the verify stage). A spec for code + with no observable command records that explicitly in Verification rather + than omitting the section. +- **Decisions.** Where a spec is silent, the build session records a dated + D-n entry under Resolved decisions (and drops a copy in the orchestrator's + decision drop-box when driven). Decisions are appended, never rewritten; a + later decision supersedes by naming the earlier one. +- **Amendments.** A change to a shipped spec's contract is an `## Amendments + received` entry with a date and provenance, and it invalidates every + transitive dependent until re-verification (spec 002 §7). +- **Style.** No em dash character anywhere in authored text; conventional + commit messages referencing the spec id; no AI attribution and no session + links in anything that lands in git or on GitHub. + +## 8. Lifecycle as scheduling + +- `status: approved` + `implementation: pending` is a work order. +- `status: draft` is never schedulable and stays visible as a blocker. + Approval is the operator's act; a machine-authored spec is born draft. +- `implementation: n-a` (this spec, the thesis, the harness) and `complete` + count as shipped, pinned at the sha256 of the spec's normalized `spec.md`. +- `depends_on` MUST be acyclic. A cycle refuses scheduling for the whole + corpus; the gate does not catch it, so the `/spec` skill and the + `spec-dag` check in `make spine` do. + +## 9. Bootstrap order + +1. This spec, the constitution, the thesis (002), and the harness (001) are + authored by hand, together with every ordinary spec of the corpus. +2. `spec-spine compile`, `index`, `lint --fail-on-warn`, and `couple` are + green with zero packages discovered and every owning unit reported as + `W-001` (declared, not yet built). That is the honest starting state. +3. Spec 010 creates the Cargo workspace and the first crate. From then on + each driven session implements exactly one spec's territory, and the + corpus governs the code it produced. diff --git a/specs/001-agentic-harness/spec.md b/specs/001-agentic-harness/spec.md new file mode 100644 index 0000000..5f9c461 --- /dev/null +++ b/specs/001-agentic-harness/spec.md @@ -0,0 +1,180 @@ +--- +id: "001-agentic-harness" +title: "Agentic engineering harness: session protocol, skills, agents, hooks, gate" +status: approved +kind: "governance" +domain: "governance" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: complete +risk: high +wave: 1 +depends_on: + - "000-hqgit-bootstrap" +establishes: + - "AGENTS.md" + - "CLAUDE.md" + - "Makefile" + - "spec-spine.toml" + - ".mcp.json" + - "standards/spec/contract.md" + - "standards/spec/templates/" + - ".claude/settings.json" + - ".claude/agents/" + - ".claude/rules/" + - ".claude/skills/" + - ".github/workflows/govern.yml" + - ".github/dependabot.yml" + - "scripts/verify-spec.sh" + - "scripts/spec-dag.sh" +summary: > + The governed-development loop every human and every driven session runs + inside: the cross-agent New Sessions protocol and the Working the backlog + protocol in AGENTS.md, the Claude Code skills (init, setup, next, build, + verify, spec, commit, code-review, ship, shepherd, validate-and-fix, + cleanup, implement-plan, research, refactor-claude-md), the six agents + (architect, explorer, implementer, reviewer, ledger-guardian, + trust-reviewer), the standing and path-scoped rules, the hooks that keep + the derived artifacts fresh and block an ungated PR, the Makefile that is + the one source of truth for what CI validates, and the CI workflow that + re-runs the same gate. The harness is what makes the corpus buildable by + claude-observatory: the orchestrator reads AGENTS.md's backlog section + into every build prompt, drives the repo's own /ship, watches the CI this + spec wires, and runs each spec's Verification block after merge. +--- + +# 001: Agentic engineering harness + +## 1. Purpose + +The corpus is only as buildable as the loop that builds it. This spec owns +that loop so that a change to how sessions are steered (a skill, a hook, a +rule, the CI gate) is a governed change coupled to a spec, never an +uncommitted habit. Everything here is substrate-level: it references the +`spec-spine` CLI, `cargo`, `gh`, and generic dev verbs, plus hqgit's own +build commands once spec 010 lands. + +## 2. Territory + +`AGENTS.md` (the cross-agent protocol authority), `CLAUDE.md` (what Claude +Code needs beyond it), `Makefile` (the CI composite), `spec-spine.toml`, +`.mcp.json`, the contract and templates under `standards/spec/` (the +constitution itself is in the bypass floor and is amended only by a spec +that `amends` it), the whole `.claude/` harness, the two workflows under +`.github/workflows/` that this spec names, and the two helper scripts. The +build session for any later spec is granted authority to append a dated +D-n note to this spec when it must adjust a hook or a Makefile target to +make its own territory buildable; it may not change the protocol's +substance without an amendment. + +## 3. Behavior + +- **B-1 (AGENTS.md is the protocol).** `AGENTS.md` carries a `## New + Sessions` section that `/init` executes verbatim, and a `## Working the + backlog` section that the orchestrator extracts verbatim into every build + prompt. Both are edited in `AGENTS.md`, never duplicated into a skill. +- **B-2 (the gate is one composite).** `make spine` runs `spec-spine + compile`, `spec-spine index`, `spec-spine lint --fail-on-warn`, + `spec-spine index check`, `spec-spine couple --base origin/main --head + HEAD`, and `scripts/spec-dag.sh` (cycle and lower-numbered-dependency + check). `make ci` runs `make spine`, `spec-spine index coverage + --fail-on-untraced`, and, whenever `Cargo.toml` exists, `cargo build + --workspace --locked`, `cargo test --workspace --locked`, `cargo clippy + --workspace --all-targets --locked -- -D warnings`, `cargo fmt --all + --check`, and `cargo deny check` when `deny.toml` exists. Every target is + guarded so the composite is green on the specify-only tree. +- **B-3 (CI is the same gate).** `.github/workflows/govern.yml` runs on + pull requests: `spec-spine compile --check`, `index check`, `lint + --fail-on-warn`, `couple` with the PR body as waiver source, `index + coverage --fail-on-untraced`, the cargo gates when a workspace exists, + and `spec-spine attest --with-coupling` uploaded as a build artifact (the + corpus attestation, the repo's own ledger seal). It pins `spec-spine` to + the version named in `AGENTS.md`. +- **B-4 (hooks).** `.claude/settings.json` wires: `SessionStart` (report + registry and index freshness), `PostToolUse` on `Edit|Write` (recompile + after a spec edit; staleness check after any hashed-input edit), + `PreToolUse` on `Bash` (block `gh pr create` unless the coupling gate is + green or a `Spec-Drift-Waiver:` is inline in the body; block `git push` + to the default branch), and `Stop` (auto-regenerate a stale index outside + a rebase or merge). Permissions allow the read-only git verbs, `cargo`, + `make`, and `spec-spine`; they deny publishing and destructive `gh` + verbs. +- **B-5 (skills).** `.claude/skills/` ships fifteen skills. The governed + loop: `/init`, `/setup`, `/next` (the lowest-numbered ready pending spec, + computed through `spec-spine registry`), `/build <id>` (one spec start + to finish: branch, flip in-progress, implement, gate, flip complete), + `/verify <id>` (run the spec's `verify:cli` blocks locally), `/spec` + (author a new spec from the template with the next ordinal and a DAG + check), `/commit`, `/code-review`, `/ship`, `/shepherd` (watch the PR's + checks, remediate, merge when green, confirm on disk). Supporting: + `/validate-and-fix`, `/cleanup`, `/implement-plan`, `/research`, + `/refactor-claude-md`. +- **B-6 (agents).** Four pipeline agents (`architect`, `explorer`, + `implementer`, `reviewer`) and two domain specialists, both read-only: + `ledger-guardian` (L0/L1 hash stability, canonical encoding, tombstones) + and `trust-reviewer` (signatures, key rotation, transparency inclusion, + Biscuit attenuation, cache-as-trust-boundary, policy determinism). +- **B-7 (rules).** Three standing rules (orchestrator, governed artifact + reads, adversarial prompt refusal) and three path-scoped rules + (`ledger-invariants` on the L0/L1 crates, `trust-invariants` on the + L4/L6 crates and the action cache, `build-commands` on `crates/**`). +- **B-8 (house style).** No em dash anywhere; conventional commits naming + the spec id (`feat(017): ...`); no AI attribution; no session links in + commits, PR bodies, or comments. The skills restate these where they + produce text that lands in git. + +## 4. Functional requirements + +- **FR-001.** `scripts/spec-dag.sh` reads `spec-spine registry list --json` + (a typed read), refuses any `depends_on` cycle naming the path, refuses a + dependency on a higher-numbered spec, and refuses a dependency on an + unknown id. Exit 0 clean, 1 on a violation, 3 when spec-spine is absent. +- **FR-002.** `scripts/verify-spec.sh <id>` extracts every `verify:cli` + fenced block from `specs/<id>/spec.md`, runs each non-comment line in + order from the repo root, prints command and exit code, and exits non-zero + on the first failure; a spec with no `## Verification` section exits 0 + and prints `not-declared`. +- **FR-003.** Every hook exits 0 when `spec-spine` or `jq` is absent, + printing what was skipped, so a missing tool never blocks a session. +- **FR-004.** The `PreToolUse` PR gate refreshes the index before coupling + and blocks when `.derived/` is left uncommitted by that refresh. + +## 5. Acceptance criteria + +- **AC-1.** `make spine` exits 0 on the specify-only tree (zero packages, + every owning unit `W-001`). +- **AC-2.** `scripts/spec-dag.sh` exits 0 on this corpus and exits 1 with + the cycle named on a fixture corpus containing `a -> b -> a`. +- **AC-3.** `scripts/verify-spec.sh 001-agentic-harness` runs this spec's + block below and exits 0. + +## 6. Out of scope + +The orchestrator itself (claude-observatory owns its stages); the Rust +toolchain pins and lints (spec 010); language-specific CI beyond the +guarded cargo composite (each crate-founding spec extends the workflow when +it needs a service or a matrix). + +## 7. Resolved decisions + +D-1 (2026-09-02, authoring). The observatory's build stage runs only the +four spec-spine commands as its post-session gate on a Rust target (its +spec 016 D-10 gates bun commands on a root `tsconfig.json`), so cargo +correctness reaches the pipeline through two doors: `make ci` inside the +session (the backlog protocol requires it before flipping to complete) and +the CI workflow that shepherd watches. This repo therefore never places a +`tsconfig.json` at the root; the review SPA (spec 095) keeps its own under +`web/`. + +D-2 (2026-09-02, authoring). The constitution is in spec-spine's bypass +floor and is deliberately not claimed here: it changes only through a spec +that `amends` it, which is the governed path the constitution's own +Amendment section names. + +## Verification + +```verify:cli +scripts/spec-dag.sh +scripts/verify-spec.sh 000-hqgit-bootstrap +make spine +``` diff --git a/specs/002-platform-thesis/spec.md b/specs/002-platform-thesis/spec.md new file mode 100644 index 0000000..17a70a0 --- /dev/null +++ b/specs/002-platform-thesis/spec.md @@ -0,0 +1,433 @@ +--- +id: "002-platform-thesis" +title: "Platform thesis: a verifiable evidence ledger for software change" +status: approved +kind: "thesis" +domain: "governance" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: n-a +risk: critical +wave: 1 +depends_on: + - "000-hqgit-bootstrap" +constrains: + - kind: sequencing-plan + target_specs: + - "010-workspace-and-core-types" + - "011-canonical-encoding" + - "012-hash-stability-gate" + - "013-object-store" + - "014-content-defined-chunking" + - "015-verified-streaming" + - "016-remote-object-backend" + - "017-ledger-entry-dag" + - "018-deterministic-total-order" + - "019-facts-and-derived-state" + - "020-commitments-and-tombstones" + - "021-local-repository" + - "023-domain-fact-vocabulary" + - "024-change-and-revision" + - "025-semantic-anchors" + - "026-review-threads" + - "027-attestation-primitive" + - "028-issues-and-derived-state" + - "031-git-object-bridge" + - "032-cli-skeleton" + - "033-cli-offline-review" + - "034-cli-attest-and-verify" + note: > + Wave 1: ledger, object store, domain model, CLI. Local only, no server. + Ends when offline review against a plain git repo works end to end + (033) and its evidence verifies (034). Hash stability of 011 and 017 is + the one unrecoverable decision in the whole plan, which is why they sit + first and why 012 gates them before anything is built on top. + - kind: sequencing-plan + target_specs: + - "040-github-mirror-import" + - "041-github-mirror-export" + - "042-mirror-sync-command" + note: > + Wave 2: the git bridge (031) and the bidirectional GitHub mirror. + Value for users who migrate nothing: their issues, pull requests, + reviews, and check results become facts they can clone. + - kind: sequencing-plan + target_specs: + - "050-stacked-changes" + - "051-semantic-deltas" + - "052-semantic-conflicts" + note: > + Wave 3: stacked changes and semantic review. The first felt value + beyond parity: force-push stops destroying review context, stacks are + native, and the high-value views of a change (API surface, types, + dependencies, capabilities) arrive as attestations. + - kind: sequencing-plan + target_specs: + - "060-identity-and-key-rotation" + - "061-oidc-login" + - "062-transparency-log" + - "063-keyless-signing" + - "064-attestation-verification" + - "065-policy-engine" + - "066-policy-sdk" + - "067-policy-evaluation-attestation" + - "068-policy-in-repo" + note: > + Wave 4: the trust plane and the policy engine. Merge gates become + checkable predicates over verified evidence; every verdict is itself an + attestation replayable against the exact policy hash that produced it. + - kind: sequencing-plan + target_specs: + - "070-reapi-types" + - "071-action-cache" + - "072-execution-service" + - "073-sandbox-executor" + - "074-execution-provenance" + - "075-build-graph" + - "076-merge-queue" + note: > + Wave 5: the evaluation plane. Highest capex, deliberately fifth because + it depends on stable input hashing. CI results are not a separate + system; they are evidence, and the cache is a trust boundary. + - kind: sequencing-plan + target_specs: + - "080-projection-framework" + - "081-change-and-review-views" + - "082-search-index" + - "083-code-graph" + - "084-ecosystem-graph" + - "085-attention-feeds" + - "090-server-skeleton" + - "091-per-repo-control-plane" + - "092-git-endpoint" + - "093-connect-api" + - "094-quarantine-and-promotion" + - "095-web-review-ui" + note: > + Wave 6: projections, the cross-repo code and ecosystem graph, and the + hosted edge (server, git endpoint, API, review UI). Everything in this + wave is disposable by construction (constitution VI); the ecosystem + graph is the one legitimately centralized component and stays + non-authoritative. + - kind: sequencing-plan + target_specs: + - "100-agent-principals" + - "101-delegation-chain" + - "102-agent-sandbox-and-provenance" + - "103-evidence-carrying-changes" + - "104-ownership-and-sla" + note: > + Wave 7: agents as a distinct principal class, designed into the types + in wave 1 (010) and shipped here: Biscuit credentials carrying the + delegation chain, declared sandboxes, mandatory provenance, and changes + that arrive with machine-checkable evidence so a human reviews the + argument rather than the diff. Ownership with delegation and expiry + rides along because it is the other half of the accountability chain. + - kind: sequencing-plan + target_specs: + - "110-set-reconciliation" + - "111-quic-transport" + - "112-federation" + note: > + Wave 8: multi-host federation. Last because discovery cost is the + structural reason centralization keeps winning; the central index + exists by wave 6 without lock-in as its business model, and federation + replicates the same signed history between hosts. +references: + - { unit: { kind: file, path: "docs/design/00-architecture.md" }, role: context } + - { unit: { kind: file, path: "docs/design/01-build-order.md" }, role: context } +summary: > + hqgit is a verifiable evidence ledger for software change. Canonical state + is a set of signed, content-addressed objects forming a per-repository DAG + that covers code, collaboration, and evidence; every index, timeline, + queue, and dashboard is a projection rebuildable from zero. The forge is + one client of that ledger, the CI system is another, and the agent runtime + is a third. This spec fixes the layer model (L0 objects through L7 edge), + the governing invariant (canonical versus derived), the nouns (Change, + Revision, Anchor, Attestation, Principal, Policy), the language split (Rust + core, Go only at the executor seam), the crate topology, the eight-wave + build order that every later spec's number reflects, and the standing + risks. It owns no code. +--- + +# 002: Platform thesis + +## 1. Purpose + +GitHub's product is not Git hosting. It is the identity and social graph, the +proprietary collaboration ledger (issues, pull requests, reviews, CI results), +and the compute. Git is the only portable layer in the stack. The repository +is content-addressed, signed, replicated, and forkable; every fact *about* the +repository is a mutable row in someone else's database, reachable only +through their API, and gone the moment you leave. Most "better GitHub" +proposals layer features on that same shape. hqgit changes the shape. + +The analysis behind this thesis (`docs/design/00-architecture.md` §1) names +seven breaks in the incumbent model: collaboration state outside the data +model; the mutable branch pointer as the unit of change; review anchored to +text; CI as untyped YAML with mutable third-party refs; decorative trust; no +ecosystem graph; and agents authenticating as humans. The recommendation is +not a frontal assault on hosting. It is to build the verification and review +plane, federate over existing repositories, mirror collaboration state +bidirectionally, and let hosting commoditize underneath. Absorption beats +replacement. + +This spec is the record of the decisions that follow from that analysis. It +is `implementation: n-a`: it constrains every ordinary spec and owns no code. + +## 2. Layer model + +``` +L7 Edge: git-compat endpoint, gRPC/Connect API, sync protocol, UI, agents +L6 Policy: merge predicates as versioned WASM modules +L5 Projection: code graph, search, ecosystem graph, feeds [disposable] +L4 Trust: identities, key rotation, attestation verify, transparency log +L3 Evaluation: hermetic build/test graph, remote execution, action cache +L2 Domain: Change, Revision, Anchor, Review, Attestation, Policy +L1 Ledger: per-repo signed hash-linked event DAG + convergent state +L0 Objects: content-addressed blob/tree store (BLAKE3), chunked +``` + +Boundaries are one-directional: L5 and above may only read from L0 through +L4, never write authoritatively (constitution XIII, bootstrap anchor +`layer-direction`). Every ordinary spec declares its layer as its `domain`. + +## 3. The governing decision: canonical versus derived + +One decision governs everything else: what is canonical, and what is derived. +Canonical state is a set of signed, content-addressed objects forming a +per-repository DAG covering code, collaboration, and evidence. Every index, +timeline, dashboard, search result, and queue is a projection that must be +rebuildable from zero. Hold that invariant and portability, offline +operation, federation, audit, and schema evolution fall out of one property. +Violate it once (one authoritative row that is not in the log) and the +project has rebuilt GitHub with extra steps. This is frozen at tier 1 +(bootstrap anchor `canonical-derived-boundary`). + +## 4. The layers, decided + +### 4.1 L0: object store (specs 013 to 016, 031) + +BLAKE3 over SHA-256, primarily for verified streaming: the BAO tree gives +chunk-level verification and range proofs, so partial and lazy fetch are +verifiable by construction. Content-defined chunking (FastCDC) plus a Merkle +tree means large files are the general path with different chunk statistics, +not a special case. Git compatibility is a hard requirement given the wedge, +so a bidirectional object mapping is maintained through `gix` (gitoxide), +never libgit2 bindings. Local index in redb; remote in any S3-compatible +store. Immutability makes every cache layer trivially correct. + +### 4.2 L1: ledger (specs 011, 012, 017 to 021, 110, 111) + +Entry shape: `{ parents: [Hash], issuer: KeyId, hlc: HybridLogicalClock, +payload: Cid, sig: Signature }`. A hash-linked DAG, not a linear log, because +concurrent authors are the normal case. + +The critical separation is facts versus derived state. Facts are immutable +events and never conflict; concurrent facts merge by set union under a +deterministic total order (topological, tiebreak on hash). Derived state +(open or closed, labels, assignee, title) is the only thing that needs +convergence, and a hybrid logical clock with last-writer-wins is sufficient +for almost all of it. Sequence CRDTs are reserved for genuinely collaborative +text. That split keeps the CRDT surface near five percent of the domain +instead of all of it (constitution VII). + +Erasure is decided at entry one: the signed log contains commitments, never +user content; content lives in the blob store, encrypted per namespace where +it must be; deletion removes the blob and appends a tombstoned commitment +(constitution X). Serialization is canonical DAG-CBOR with forward-compatible +unknown-field preservation; hash stability is fuzzed in CI from the first +commit (spec 012), because any nondeterminism in canonicalization silently +invalidates every signature downstream (constitution VIII). + +Replication is range-based set reconciliation over QUIC (spec 110, 111); +`iroh` and `quinn` are the building blocks and the Willow protocol is the +prior art to study first. Server-side, repositories are independent +consistency domains, so per-repo Raft groups are the natural partitioning, +and an embedded Raft-replicated SQLite (Hiqlite) fits better than a shared +cluster (spec 091). + +### 4.3 L2: domain model (specs 023 to 028, 050 to 052, 104) + +The single most valuable simplification in the design: every form of +evidence is one primitive, `Attestation { subject: Hash, predicate: +PredicateType, issuer: Principal, claim: Cid, sig }` (spec 027, constitution +IX). Human approval, build provenance, test result, static finding, license +scan, policy evaluation, mirrored external state, and agent action are all +the same shape. Resist every request to special-case one of them. + +The other nouns that must be right: `Change` (stable identity) with ordered +`Revision`s, each a tree hash plus base (spec 024); `Anchor { path, +tree_sitter_node_path, node_content_hash }` so comments survive rebase by +re-resolving against content, falling back to text position only when +resolution fails (spec 025); `Principal = Human | Agent | Service | Org`, +distinct at the type level (spec 010); `Policy` as versioned, hash-pinned, +executable (spec 068). + +### 4.4 L3: evaluation plane (specs 070 to 076) + +`eval(repo_state_hash, target, toolchain_hash) -> output_hash`, cached +globally on input hash. The protocol is not invented: the Bazel Remote +Execution API is implemented so existing executors, clients, and caches +interoperate on day one. Two sandbox tiers: namespaces plus seccomp with no +network for trusted work, microVMs for fork contributions and agent +execution. Every execution emits a signed provenance attestation, closing the +loop into L2. Merge queues become speculative evaluation over candidate merge +states. The cache is a trust boundary: entries carry executor identity, and +unattested cache hits are misses for anything that gates a merge +(constitution XI). + +### 4.5 L4: trust plane (specs 060 to 064, 100 to 102) + +Authentication and identity are different problems. OIDC (Rauthy as the +self-hosted reference) handles login and workforce federation. Durable +identity is a keypair with a rotation chain recorded in the ledger, so +historical signatures remain verifiable across key changes. Signing defaults +to the Sigstore shape: short-lived certificates bound to an OIDC identity, +with transparency-log inclusion proofs, run in-house when sovereignty +matters. Agents hold Biscuit tokens, not scoped personal access tokens: the +delegation chain lives in the token, attenuation is monotonic and offline +verifiable, and caveats are datalog (constitution XII). + +### 4.6 L6: policy (specs 065 to 068, 103) + +The merge predicate is `f(change, attestation_set, policy_version) -> Allow | +Deny(reasons)`, deterministic and side-effect free, compiled to WASM with a +typed SDK so policies are unit-testable, hash-pinned to repo state, and +evaluable locally before push. Policy evaluation emits an attestation, so +every merge decision is replayable years later against the exact policy that +produced it. Repository settings as mutable toggles are made impossible. + +### 4.7 L5 and L7 (specs 080 to 095, 112) + +Projections are disposable read models rebuilt from the total order +(spec 080). The cross-repo code index (SCIP-class, spec 083) joined to the +package dependency graph (spec 084) is the one legitimately centralized +component; it is isolated and non-authoritative. The edge is the git +endpoint, the Connect API, the review UI, the federation protocol, and the +agent runtime: interchangeable clients over the same signed history. + +## 5. Language and crate topology + +Split by ecosystem gap, not by preference. Rust for the entire trusted core: +the invariants are type-encodable, the libraries exist (`gix`, `blake3`, +`bao`, `redb`, `ciborium`, `ed25519-dalek`, `tantivy`, `sigstore`, +`biscuit-auth`, `iroh`, `quinn`, `wasmtime`, `tonic`), there are no GC pauses +on the content-addressed hot path, and single-binary embedding means the CLI +and the server run the same ledger implementation, which is the only way to +get genuine offline-first without two divergent implementations. Go is +stronger in the execution plane (containerd, Firecracker SDKs, cloud SDKs), +so the executor that orchestrates container runtimes is Go (spec 073). That +is the one language boundary, and the domain model stays on the Rust side +of it. + +| Crate | Layer | Founding spec | Depends on (workspace) | +|---|---|---|---| +| `hqgit-types` | L2 data, L1 codec | 010, 011 | none | +| `hqgit-object` | L0 | 013 | types | +| `hqgit-ledger` | L1 | 017 | types, object | +| `hqgit-domain` | L2 | 023 | types, object, ledger | +| `hqgit-git` | L0/L7 bridge | 031 | types, object | +| `hqgit-cli` (`hq`) | L7 | 032 | every library crate | +| `hqgit-mirror` | L7 | 040 | types, ledger, domain, git | +| `hqgit-trust` | L4 | 060 | types, ledger | +| `hqgit-policy` | L6 | 065 | types, domain | +| `hqgit-policy-sdk` | L6 | 066 | types (wasm32 target) | +| `hqgit-eval` | L3 | 070 | types, object, ledger, domain, trust | +| `hqgit-projection` | L5 | 080 | types, ledger, domain | +| `hqgit-server` | L7 | 090 | every library crate | +| `hqgit-agent` | L4/L7 | 100 | types, ledger, trust, policy | +| `hqgit-sync` | L1 | 110 | types, object, ledger, trust | +| `executor/` (Go) | L3 | 073 | REAPI wire contract only | +| `fuzz/` | tooling | 012 | types, ledger | +| `web/` (TS) | L7 | 095 | Connect API only | + +Dependencies point downward only; `hqgit-cli` and `hqgit-server` never +depend on each other. + +## 6. Build order + +Sequencing is dominated by one fact: hash stability of L0 and L1 is the only +unrecoverable mistake. Everything above it is replaceable. The eight waves +are the `sequencing-plan` constraints in this spec's frontmatter, and every +spec's ordinal encodes its wave (`010` to `034` wave 1, `040`s wave 2, `050`s +wave 3, `060`s wave 4, `070`s wave 5, `080`s and `090`s wave 6, `100`s wave 7, +`110`s wave 8). Each spec also carries `wave` in frontmatter. The +orchestrator's "lowest-numbered ready spec" rule therefore reproduces this +order without a second table, and `docs/design/01-build-order.md` renders the +resulting DAG for humans. + +1. Ledger, object store, domain model, CLI. Local only. Prove offline review + against a plain git repo. +2. Git bridge and bidirectional GitHub mirror. Users without migration. +3. Stacked changes, semantic deltas, semantic conflicts. First felt value. +4. Trust plane and policy engine. Merge gates become verifiable. +5. Evaluation plane. Highest capex, depends on stable input hashing. +6. Projections, cross-repo code and ecosystem graph, hosted edge and UI. +7. Agent principals and delegation. Designed into the types in wave 1. +8. Multi-host federation. + +## 7. Amendment and invalidation + +A shipped spec is pinned at the hash of its `spec.md`. Amending it (a dated +`## Amendments received` entry, or any change to its contract) invalidates +every transitive dependent, which must re-verify before it counts as shipped +again. This spec is depended on by every ordinary spec through 010, so an +amendment here re-verifies the whole corpus. That is the intended cost of +changing the thesis. + +## 8. Standing risks + +- **Schema evolution against signed history.** Version everything, preserve + unknown fields, never reorder. Frozen (bootstrap anchor `hash-stability`). +- **CRDT surface creep.** Hold the facts versus derived-state line under + product pressure; a spec moving a noun to the CRDT side must argue it. +- **Cross-repo indexes** are the one legitimately centralized component; + isolate them and keep them non-authoritative (spec 083, 084). +- **Abuse in an append-only replicated store.** Untrusted contributions land + in a quarantine namespace and are promoted by capability (spec 094). +- **Erasure versus append-only.** Commitments and tombstones from entry one + (spec 020); not retrofittable. +- **Hermetic builds tax ergonomics.** Everything must be declared; the escape + hatches (spec 075) are where the model leaks, so they are attested too. +- **Semantic review is per-language.** Rust and TypeScript first (spec 025, + 051); everything else degrades to text, visibly. +- **Agent capability scoping adds friction** exactly where users want + autonomy. The design optimizes verification throughput (spec 103), and a + spec that loosens agent scoping must argue against constitution XII. +- **Decentralized state raises discovery cost.** The central index exists + (wave 6); federation (wave 8) replicates signed history, not authority. + +## 9. Out of scope + +Hosting as a business, billing, maintainer funding, and a GitHub-parity +issue tracker UI. A UI beyond the review surface (spec 095) is a later +client of the API. Non-GitHub mirrors (GitLab, Gerrit) follow spec 040's +shape as later specs. + +## 10. Resolved decisions + +Carried from `docs/design/00-architecture.md` and fixed here: + +- **D1 (canonical versus derived).** As §3. Frozen at tier 1. +- **D2 (BLAKE3 and BAO).** Verified streaming is the reason, not speed. +- **D3 (gix, not libgit2).** Pure Rust, no C boundary in the trusted core. +- **D4 (DAG, not log).** Concurrent authors are the normal case. +- **D5 (facts versus derived state).** LWW over HLC for derived state; + sequence CRDTs only for collaborative text. +- **D6 (commitments in the log).** Erasure by tombstone from entry one. +- **D7 (DAG-CBOR canonical encoding).** Unknown fields preserved; fuzzed. +- **D8 (one attestation primitive).** in-toto's statement model, generalized. +- **D9 (REAPI, not a new protocol).** Interoperate on day one. +- **D10 (cache is a trust boundary).** Unattested hits are misses for gates. +- **D11 (identity is a keypair with a rotation chain; login is OIDC).** +- **D12 (Biscuit for agents).** Delegation chain in the token. +- **D13 (policy is WASM, hash-pinned, and emits an attestation).** +- **D14 (Rust core, Go executor).** One language boundary, at the REAPI seam. +- **D15 (build order).** As §6; hash stability first. +- **D16 (absorption over replacement).** Mirror over existing repositories; + no migration required for value. +- **D17 (specify first).** The entire corpus is authored before any code, + every spec is a bounded session's territory, and the orchestrator builds + it in ordinal order. This spec and 000 and 001 are the only + non-`pending` specs at authoring time. diff --git a/specs/003-chassis-alignment/spec.md b/specs/003-chassis-alignment/spec.md new file mode 100644 index 0000000..15657a5 --- /dev/null +++ b/specs/003-chassis-alignment/spec.md @@ -0,0 +1,186 @@ +--- +id: "003-chassis-alignment" +title: "Chassis alignment: what the hosted edge consumes from rahi, and what stays hqgit's" +status: approved +kind: "governance" +domain: "governance" +created: "2026-09-03" +authors: ["Bartek Kus"] +implementation: n-a +risk: critical +wave: 1 +depends_on: + - "002-platform-thesis" +constrains: + - kind: family-boundary + target_specs: + - "010-workspace-and-core-types" + - "017-ledger-entry-dag" + - "060-identity-and-key-rotation" + - "061-oidc-login" + - "090-server-skeleton" + - "091-per-repo-control-plane" + - "093-connect-api" + - "094-quarantine-and-promotion" + - "095-web-review-ui" + - "100-agent-principals" + - "101-delegation-chain" +summary: > + A Rust chassis named rahi now exists in this family and owns identity, + replicated operational state, a decision chain, a capability kernel, an + axum edge, and single-container packaging. Without a decision on record, + the wave 6 server specs would found all of that a second time, and a + deployment would contain two things called a ledger. This spec draws the + boundary: waves 1 through 5 stay chassis-free so the CLI works with no + server, the wave 6 server composes rahi as a Cell, the evidence DAG never + moves into the chassis store, hqgit's cryptographic identity stays + authoritative with the IdP as an authentication subject bound to it, and + the word ledger keeps one meaning in this repository. +--- + +# 003: Chassis alignment + +## 1. Purpose + +The thesis was written before the family had a chassis. It now has one: +`rahi`, an Apache-2.0 Rust chassis whose seven responsibilities are +identity through a co-deployed rauthy, replicated state through in-process +hiqlite, a hash-chained decision ledger, a deny-by-default capability +kernel, an axum edge with probes, metrics, and streaming, single-container +packaging, and the operational verbs. + +Spec 090 currently founds `hqgit-server` with its own configuration, +router, authentication, telemetry, health endpoints, and shutdown, and 061 +brings an OIDC client. Every one of those is a rahi responsibility. Left +alone, the two implementations diverge, and the divergence is invisible +until a security fix lands in one of them. + +The second reason for this spec is vocabulary. rahi's decision chain is +called a ledger by its own corpus, and hqgit's central object is also called +a ledger. In a hosted deployment both are present and they are entirely +different things. One word, two meanings, in one process is a defect that +gets written into code and never comes out. + +## 2. What this spec settles + +- **B-1 (waves 1 to 5 are chassis-free).** `hqgit-types`, `hqgit-ledger`, + `hqgit-objects`, the domain crates, `hqgit-trust`, `hqgit-policy`, and + `hqgit-cli` take no dependency on any rahi crate, on hiqlite, or on + rauthy. The local product must work on a laptop with no server, no + database, and no network, which is thesis §6 wave 1 and the reason the + mirror-first strategy is credible. This is frozen: a rahi dependency + appearing below wave 6 is a defect, not a design choice. +- **B-2 (the wave 6 server is a Cell).** `hqgit-server` implements rahi's + `Cell` trait: a manifest, migrations, routes, operator routes, and a + one-line `main` that hands control to rahi's runner. It composes + `rahi-edge` for the HTTP frame, `rahi-idp` for browser sessions and for + bearer-token authorization of non-browser clients, `rahi-kernel` for the + declared capability ceiling, `rahi-store` for operational state, and + `rahi-ops` for the verbs and packaging. Spec 090's `config.rs`, + `auth.rs`, `telemetry.rs`, `health.rs`, and its shutdown handling shrink + to adapters over the chassis, and its `establishes` list is reduced + accordingly when it is built. +- **B-3 (the transport question is open and named).** rahi's edge is axum + over HTTP; 090 currently multiplexes HTTP/1.1 and gRPC on one port with + tonic. The preferred resolution is the Connect protocol over plain HTTP, + which axum serves natively and which 093 already names, so no + multiplexing is required and rahi is unchanged. The fallback is a spec + in rahi that mounts a tonic service into its router. This must be + resolved before 090 is implemented; whichever is chosen is recorded as a + D-n entry in 090 and, if the fallback is taken, as a spec in rahi. +- **B-4 (the evidence DAG never moves into the chassis store).** hqgit's + ledger is content-addressed, hash-linked, per repository, and portable + by construction. rahi's store is a replicated SQLite group for + operational state. The server may keep session state, the repository + registry, quarantine queues, and job state in the chassis store; it puts + no ledger entry, no object, and no fact there. Frozen. +- **B-5 (identity: two layers, one binding).** hqgit's durable identity + stays the keypair whose rotation history is a sequence of signed facts + (060), and it remains authoritative for every signature and every + attestation. rauthy's `sub` is an authentication subject, not an + identity: it binds to an `IdentityId` through 061's + `identity.binding_added` fact, which is exactly the shape 061 already + specifies with Rauthy named as the self-hosted reference. At the hosted + edge, therefore, a request is authenticated by the chassis and authorized + against an hqgit identity the ledger already knows. The CLI keeps its own + OIDC client for the device authorization grant, because the CLI is + chassis-free by B-1. +- **B-6 (agents stay hqgit's).** `Principal::Agent` (010 B-6) and the + delegation chain (100, 101) are hqgit's and are not replaced by anything + in the chassis. An agent acting through the hosted edge presents a + chassis-issued bearer token whose subject binds to the agent's + `IdentityId` by B-5, and its authority within a repository comes from the + delegation chain, never from the token's scopes alone. The token says who + is calling; the chain says what they may do. +- **B-7 (vocabulary).** In this repository, *ledger* means the per-repository + evidence DAG of spec 017 and nothing else. rahi's hash-linked record of + governed operational choices is called *the chassis decision chain*, in + prose, in code, and in configuration. The server emits operational + decisions (a denied capability, an operator action, a quarantine + admission) to the chassis decision chain, and repository facts to the + evidence DAG. A type, module, or document that could be read as either is + renamed. +- **B-8 (licences).** hqgit is AGPL-3.0 and rahi is Apache-2.0. + Apache-2.0 into AGPL-3.0 is the sanctioned direction, so hqgit may + consume rahi and may contribute changes upstream under Apache-2.0. No + hqgit code moves into rahi without that relicensing being explicit in the + contributing change. +- **B-9 (memory is aicortex's).** The family's memory product is + `aicortex`. hqgit builds no memory store, no embedding pipeline, and no + recall surface. Decisions made inside driven sessions are posted to + aicortex's machine-intake contract by the orchestrator, not by this + repository. + +## 3. Affected specs + +010 (Principal is unchanged, and B-6 records why), 017 and 002 (the +vocabulary of B-7), 060 and 061 (the binding of B-5, which 061 already +anticipates), 090, 091, 093, 094, and 095 (composition per B-2 and B-3), +100 and 101 (B-6). Each carries the change into its own Behavior section +when it is implemented, citing this spec. + +## 4. Functional requirements + +- **FR-001.** A test in the workspace, once wave 6 exists, asserts that no + crate below `hqgit-server` names a rahi crate, hiqlite, or rauthy in its + manifest. +- **FR-002.** A grep test asserts the identifier `ledger` never refers to + the chassis decision chain in this repository's code or documents, and + that the chassis decision chain is always named as such. +- **FR-003.** `hqgit-server`'s manifest declares its capability ceiling and + `hqgit-server` contains no axum `Router::new` outside its `Cell` + implementation. +- **FR-004.** The transport decision of B-3 is recorded as a D-n entry in + 090 before 090 is flipped to `implementation: in-progress`. + +## 5. Acceptance criteria + +- **AC-1.** `make spine` passes with this spec in the corpus. +- **AC-2.** Specs 090, 091, 093, 094, and 095 each cite this spec when + they are implemented, and their `establishes` lists no longer claim the + chassis responsibilities named in B-2. + +## 6. Out of scope + +The chassis's own design, which is rahi's corpus. Any change to waves 1 +through 5, which this spec deliberately leaves untouched. The hosting +business, which is a separate concern from the boundary drawn here. + +## 7. Resolved decisions + +- **D-1 (2026-09-03, this spec).** The chassis enters at wave 6 rather + than at wave 1. Adopting it earlier would make the local CLI depend on a + server chassis, destroying the property that makes adoption possible: an + hqgit user with a laptop and a GitHub repository needs nothing else. +- **D-2 (2026-09-03, this spec).** hqgit's cryptographic identity remains + authoritative rather than being replaced by the IdP subject. Attestations + must verify offline, years later, by someone who has never contacted this + deployment's IdP, and an OIDC subject cannot carry that. This corrects an + earlier suggestion in the family that the agent principal should wrap the + IdP subject: the binding is the correct relationship, not containment. + +## Verification + +```verify:cli +make spine +``` diff --git a/specs/010-workspace-and-core-types/spec.md b/specs/010-workspace-and-core-types/spec.md new file mode 100644 index 0000000..fb5471f --- /dev/null +++ b/specs/010-workspace-and-core-types/spec.md @@ -0,0 +1,180 @@ +--- +id: "010-workspace-and-core-types" +title: "Cargo workspace and the core types: Hash, Cid, Principal, keys, Hlc, Error" +status: approved +kind: "kernel" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "002-platform-thesis" +establishes: + - "Cargo.toml" + - "rust-toolchain.toml" + - "deny.toml" + - "crates/hqgit-types/Cargo.toml" + - "crates/hqgit-types/src/lib.rs" + - "crates/hqgit-types/src/hash.rs" + - "crates/hqgit-types/src/cid.rs" + - "crates/hqgit-types/src/principal.rs" + - "crates/hqgit-types/src/key.rs" + - "crates/hqgit-types/src/hlc.rs" + - "crates/hqgit-types/src/error.rs" + - "crates/hqgit-types/src/version.rs" + - "crates/hqgit-types/tests/" +summary: > + The first build: a virtual Cargo workspace with the toolchain pinned, + unsafe forbidden, one shared dependency table, and a supply-chain policy; + plus hqgit-types, the plain-data substrate every other crate depends on + and that depends on nothing in the workspace. It fixes the value types + that reach hashed bytes: Hash (BLAKE3-256), Cid (codec plus hash), the + four-variant Principal, KeyId and Signature with the ed25519 Signer and + Verifier seams and the frozen signing preimage, the Hlc timestamp with its + total order, the Error enum with the four exit codes, and the schema + version constants. Everything here is owned data with serde derives and no + lifetimes, generics, or trait objects at the boundary, so the same types + back the CLI, the server, and any future binding. +--- + +# 010: Cargo workspace and the core types + +## 1. Purpose + +Every layer above L0 shares a handful of value types, and several of them +are hashed or signed. If those types are defined twice, or defined with a +representation that can drift (a float, a map with unstable order, a clock +read), the ledger's hash stability (constitution VIII) is lost before the +ledger exists. This spec creates the workspace and pins those types in one +dependency-free crate. It also plants the `Principal` enum with its `Agent` +variant now, because the thesis (002 §6, step 7) requires agents to be a +distinct principal class from the first type definition even though the +agent runtime ships in wave 7. + +## 2. Territory + +The workspace root (`Cargo.toml`, `rust-toolchain.toml`, `deny.toml`) and +the whole of `crates/hqgit-types` as it stands after this spec: the crate +manifest, `lib.rs`, the seven modules named in `establishes`, and the +`tests/` subtree. The canonical encoder (`src/codec/`) is spec 011's +territory inside the same crate; it `extends` this spec's `lib.rs` and +`Cargo.toml`. Later specs that add a third-party dependency declare an +`extends` edge on this spec's `Cargo.toml` section `workspace.dependencies`. + +## 3. Behavior + +- **B-1 (workspace).** The root `Cargo.toml` is a virtual workspace with + `members = ["crates/*"]` and `exclude = ["fuzz"]` (spec 012 keeps its own + workspace), `resolver = "3"`, a `[workspace.package]` table (edition 2024, + `rust-version`, license `AGPL-3.0-only`, repository URL), a + `[workspace.lints.rust]` table with `unsafe_code = "forbid"`, a + `[workspace.lints.clippy]` table denying `unwrap_used`, `expect_used`, + `indexing_slicing`, and `float_arithmetic` in library code, and a single + `[workspace.dependencies]` table where every third-party dependency's + version lives. Crate manifests reference `workspace = true` for + everything they inherit. +- **B-2 (toolchain).** `rust-toolchain.toml` pins a stable channel by exact + version with `components = ["rustfmt", "clippy"]`. `Cargo.lock` is + committed and every gate passes `--locked`. +- **B-3 (supply chain).** `deny.toml` allows `MIT`, `Apache-2.0`, + `BSD-2-Clause`, `BSD-3-Clause`, `ISC`, `Unicode-3.0`, `Zlib`, `MPL-2.0`, + and `AGPL-3.0-only` (this workspace), denies unknown registries and git + sources except allow-listed ones, warns on duplicate versions, and denies + advisories with a known fix. +- **B-4 (`Hash`).** A `#[repr(transparent)]` newtype over `[u8; 32]`, the + BLAKE3-256 output. `Hash::of(&[u8]) -> Hash` is the only constructor from + content; `Hash::from_bytes` and `as_bytes` round-trip. `Display` and + `FromStr` are exactly 64 lowercase hex characters, no prefix. `Ord` is the + byte order. serde encodes it as a byte string, never as text, so that the + canonical encoding (011) is 32 bytes plus the CBOR header. +- **B-5 (`Cid`).** `Cid { codec: Codec, hash: Hash }` where `Codec` is a + closed enum `DagCbor | Raw` with `u64` tags `0x71` and `0x55` (the IPLD + multicodec values) so a Cid is portable. serde encodes a Cid as the + two-element array `[codec_tag, hash_bytes]`. `Display` is + `<codec-name>:<hex>` (`dag-cbor:ab12...`). +- **B-6 (`Principal`).** `enum Principal { Human(HumanId), Agent(AgentId), + Service(ServiceId), Org(OrgId) }`, each id a newtype over `Hash` (the + identity's genesis hash, spec 060). The enum is `#[non_exhaustive]` for + readers, exhaustive for writers within the crate, and its serde form is + externally tagged with the lowercase variant name. `Principal::kind()` + returns a `PrincipalKind` enum that policy (065) and authorization (101) + match on; there is no conversion between kinds. +- **B-7 (keys and signing).** `KeyId` is `Hash::of(public_key_bytes)`. + `PublicKey` is 32 bytes (ed25519); `Signature` is 64 bytes; both encode as + byte strings. `SignDomain(&'static str)` names what is being signed. The + frozen preimage is `b"hqgit/v1/" || domain || 0x00 || payload`; the + `Signer` trait is `fn sign(&self, domain: SignDomain, payload: &[u8]) -> + Signature` and `fn key_id(&self) -> KeyId`; the `Verifier` trait is + `fn verify(&self, domain: SignDomain, payload: &[u8], sig: &Signature) -> + Result<(), Error>`. `Ed25519Signer` (from a 32-byte seed) and + `Ed25519Verifier` (from a `PublicKey`) are the only implementations here. + Seeds are never `Debug`-printed or serialized. +- **B-8 (`Hlc`).** `Hlc { wall_ms: u64, logical: u32, node: NodeId }` with + `NodeId` a 16-byte newtype. `Ord` is lexicographic on `(wall_ms, logical, + node)`; serde encodes the three fields as a three-element array. The type + reads no clock; generation is spec 018's `clock.rs`. +- **B-9 (`Error`).** One `Error` enum for the workspace's library crates with + variants `Validation(String)`, `NotFound(String)`, `Drift(String)`, + `Stale(String)`, `Io(String)`, `Parse(String)`, `Schema(String)`, + `Config(String)`, `Crypto(String)`, and `Policy(String)`, each carrying an + owned message, plus `fn exit_code(&self) -> i32` mapping to `1` + (validation, not found, drift, crypto, policy), `2` (stale), and `3` + (io, parse, schema, config). Binaries (032, 090) map exit codes in exactly + one place each through this function. +- **B-10 (versions).** `version.rs` holds `pub const` schema versions as + `&str` in `MAJOR.MINOR.PATCH`: `OBJECT_SCHEMA_VERSION`, + `LEDGER_SCHEMA_VERSION`, `DOMAIN_SCHEMA_VERSION`, all `"1.0.0"`, and + `WIRE_VERSION_PREFIX = "hqgit/v1/"`. Bumping a MAJOR is a spec amendment. +- **B-11 (plain data).** Every public type is owned, `Clone`, `Debug` + (seeds excepted), `PartialEq`, `Eq`, and serde-derived; no lifetimes, + generics, or trait objects appear in a public field. Nothing in this crate + reads `std::time`, `std::env`, or iterates a `HashMap`: `BTreeMap` is the + only map type. + +## 4. Functional requirements + +- **FR-001.** `cargo build --workspace --locked` and `cargo clippy + --workspace --all-targets --locked -- -D warnings` pass with + `hqgit-types` as the sole member; `cargo tree -p hqgit-types` shows no + workspace crate. +- **FR-002.** Tests cover: `Hash` hex round-trip and byte order; `Cid` + serde form as the tagged pair; `Principal` serde tag names and `kind()`; + the signing preimage against a recorded vector (seed, domain, payload, + expected signature); `Hlc` ordering across each field; every `Error` + variant's exit code. +- **FR-003.** A test reads the crate's own sources and asserts none contains + `std::time`, `std::env`, `HashMap`, `HashSet`, `f32`, or `f64` outside a + comment (the cheap guard behind B-11). +- **FR-004.** `cargo deny check` passes with the `deny.toml` of B-3. +- **FR-005.** Every crate manifest carries + `[package.metadata.spec-spine] spec = "<founding spec id>"` (this crate: + `010-workspace-and-core-types`), the manifest floor spec-spine's coupling + gate reads. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-types --locked` passes. +- **AC-2.** `make ci` exits 0 on the branch (the cargo gates are now live). +- **AC-3.** `spec-spine index` discovers exactly one package, + `hqgit-types`, bound to this spec, and `spec-spine index coverage + --fail-on-untraced` exits 0. + +## 6. Out of scope + +The canonical encoder and unknown-field preservation (011); the HLC +generation algorithm (018); identity, rotation, and keyless signing (060, +063); any I/O. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-types --locked +cargo clippy -p hqgit-types --all-targets --locked -- -D warnings +``` diff --git a/specs/011-canonical-encoding/spec.md b/specs/011-canonical-encoding/spec.md new file mode 100644 index 0000000..26b8798 --- /dev/null +++ b/specs/011-canonical-encoding/spec.md @@ -0,0 +1,190 @@ +--- +id: "011-canonical-encoding" +title: "Canonical encoding: deterministic DAG-CBOR, the Value model, envelopes, unknown-field preservation" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "010-workspace-and-core-types" +establishes: + - "crates/hqgit-types/src/codec/mod.rs" + - "crates/hqgit-types/src/codec/cbor.rs" + - "crates/hqgit-types/src/codec/envelope.rs" + - "crates/hqgit-types/src/codec/value.rs" + - "crates/hqgit-types/testdata/vectors/" + - "crates/hqgit-types/tests/codec.rs" +extends: + # The codec module is re-exported from the crate root 010 founded. + - { spec: "010-workspace-and-core-types", unit: "crates/hqgit-types/src/lib.rs", nature: additive } + # The crate manifest gains the serde and CBOR dependencies. + - { spec: "010-workspace-and-core-types", unit: "crates/hqgit-types/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The one serialization every hashed and signed byte in hqgit passes + through. This spec fixes canonical DAG-CBOR (RFC 8949 deterministic + encoding under the IPLD DAG-CBOR restrictions: definite lengths, shortest + integers, length-then-bytes key order, no floats, no indefinite items, no + tags except the link tag), the in-memory Value model it encodes, the + versioned Envelope every object is wrapped in, and the unknown-field + preservation rule that lets a newer writer's fields survive an older + reader byte for byte. It establishes the golden vector corpus that spec + 012 gates and every later release must reproduce. Nothing here reads a + clock or the environment; decode of encode is the identity and encode of + decode is the identity on canonical bytes, and both are frozen at tier 1. +--- + +# 011: Canonical encoding + +## 1. Purpose + +Constitution VIII and bootstrap anchor `hash-stability`: any nondeterminism +in canonicalization silently invalidates every signature downstream, and a +signed history can never be rewritten. Thesis §4.2 chose DAG-CBOR with +forward-compatible unknown-field preservation (D7). This spec is where that +choice becomes bytes. It is deliberately the second spec built, before any +object or entry exists, so that the first signed entry (017) is already +encoded under the rules that will hold for the life of the ledger. Every +later spec that says "canonical bytes" means the output of this module. + +## 2. Territory + +`crates/hqgit-types/src/codec/` inside the crate 010 founded: `mod.rs` (the +`Canonical` trait and the public entry points), `cbor.rs` (the encoder and +the strict decoder), `value.rs` (the `Value` model), and `envelope.rs` (the +versioned wrapper). The golden vector corpus under +`crates/hqgit-types/testdata/vectors/` is established here and extended by +later specs (017 adds `ledger/`, 027 adds `attestation/`). The codec test +file is `tests/codec.rs`; the whole-corpus golden test is spec 012's +`tests/golden.rs`. + +## 3. Behavior + +- **B-1 (`Value`).** `enum Value { Null, Bool(bool), Int(Int), Bytes(Vec<u8>), + Text(String), Array(Vec<Value>), Map(BTreeMap<String, Value>), Link(Cid) }` + where `Int` is `enum Int { Neg(u64), Pos(u64) }` (`Neg(n)` denotes the + integer `-1 - n`, CBOR major type 1) so every 64-bit integer is + representable without a float. `Map` keys are text only, held in a + `BTreeMap` so iteration is deterministic in memory; encoding re-sorts by + the canonical key order of B-2, which differs from `BTreeMap`'s + lexicographic order. There is no float variant anywhere in the model. +- **B-2 (canonical encoding rules).** `encode(&Value) -> Vec<u8>` MUST + produce: definite-length items only; integers in the shortest form that + holds the value (RFC 8949 §4.2.1); text as UTF-8 major type 3, bytes as + major type 2; map keys sorted first by encoded length then by bytes + (RFC 8949 §4.2.1 core deterministic order, which the IPLD DAG-CBOR + specification requires); no duplicate keys; `Link` as CBOR tag 42 over a + byte string of `0x00` followed by the CIDv1 binary form of the `Cid` + (`0x01`, the multicodec varint, `0x1e` for BLAKE3-256, `0x20`, the 32 + hash bytes), which is the only tag the encoder emits; `Null`, `Bool` as + the simple values `0xf6`, `0xf4`, `0xf5`. There is no float encoding path. +- **B-3 (strict decoding).** `decode(&[u8]) -> Result<Value, Error>` MUST + reject, as `Error::Parse` naming the byte offset: indefinite-length items, + a non-shortest integer, a map key that is not text, a duplicate key, keys + out of canonical order, any tag other than 42, a tag 42 payload that is + not a well-formed BLAKE3-256 CIDv1 with a `DagCbor` or `Raw` codec, any + float (major type 7 with additional information 25, 26, or 27), any + simple value other than null and the two booleans, invalid UTF-8 in text, + trailing bytes after the top-level item, and nesting deeper than 128. A + decoder that accepts a non-canonical form is a hash-stability defect. +- **B-4 (round trip).** For every `v: Value`, `decode(encode(v)) == v`, and + for every byte string `b` that `decode` accepts, `encode(decode(b)) == b`. + These two identities are the contract 012's fuzz targets check. +- **B-5 (`Canonical`).** `trait Canonical: Sized { fn to_value(&self) -> + Value; fn from_value(v: Value) -> Result<Self, Error>; fn to_canonical + (&self) -> Vec<u8> { encode(&self.to_value()) } fn from_canonical(b: &[u8]) + -> Result<Self, Error> { Self::from_value(decode(b)?) } fn canonical_hash + (&self) -> Hash { Hash::of(&self.to_canonical()) } }`. Types implement + `to_value` and `from_value` by hand or through a serde bridge + (`serde_value::to_value` and `from_value`, provided in `mod.rs`, which + rejects floats at the bridge); the serde bridge is a convenience, never a + second encoding: only `cbor.rs` turns a `Value` into bytes. The core + types of spec 010 (`Hash`, `Cid`, `Principal`, `KeyId`, `Signature`, + `Hlc`) implement `Canonical` here with the wire forms 010 fixed. +- **B-6 (unknown-field preservation).** Every evolvable struct carries + `extra: BTreeMap<String, Value>` and implements `from_value` so that any + map key it does not recognize lands in `extra` verbatim, and `to_value` + emits `extra` back into the same map; keys of `extra` MUST NOT collide + with the struct's own keys (`Error::Validation` on construction). A + reader therefore re-encodes a newer writer's object byte for byte and + hashes it identically. A struct without `extra` is a frozen leaf type + (the spec 010 value types) and MUST be documented as such. +- **B-7 (`Envelope`).** `Envelope { v: SchemaVersion, kind: String, body: + Value, extra }` is the wrapper for every versioned object (013 objects, + 019 facts, 027 attestations). `SchemaVersion { major: u16, minor: u16 }` + encodes as a two-element array; `kind` is a namespaced lowercase string + (`"object.tree"`, `"fact.change.opened"`); the map keys are `body`, + `kind`, `v`, plus `extra`. A reader MUST accept any `minor` of a known + `major`, MUST refuse an unknown `major` as `Error::Schema`, and MUST + preserve fields it does not know (B-6). `Envelope::open(expected_kind, + max_major) -> Result<Value, Error>` is the checked accessor. +- **B-8 (golden vectors).** `testdata/vectors/<area>/<name>.json`, one JSON + document per vector with the keys `description`, `input` (a JSON encoding + of the `Value` using the convention `{"$bytes": hex}`, `{"$link": + "<codec>:<hex>"}`, `{"$int": "-9223372036854775809"}` for values JSON + numbers cannot carry), `canonical_hex`, and `hash` (the BLAKE3 hex of the + canonical bytes). This spec establishes the `codec/` area with at least: + every integer boundary (0, 23, 24, 255, 256, 65535, 65536, 2^32 - 1, 2^32, + 2^64 - 1, -1, -24, -25, -2^64); key ordering by length then bytes; nested + arrays and maps; a link; empty containers; a text vector with multi-byte + and combining UTF-8; a bytes vector; an envelope. Vectors are frozen + (constitution VIII): a change to any `canonical_hex` or `hash` is a + schema MAJOR and a human decision (012 B-5). +- **B-9 (no ambient input).** The module reads no clock, no environment, + and iterates no `HashMap`; it allocates no global state. Spec 010 FR-003's + source guard covers it. + +## 4. Functional requirements + +- **FR-001.** The encoder and decoder are pure functions over their + arguments with no `unsafe` and no dependency on the host's endianness + or pointer width; integer widths are explicit. +- **FR-002.** Tests cover: every B-3 rejection with the offset asserted; + the B-4 identities on a hand-written corpus; every 010 value type's wire + form against the vectors; unknown fields surviving `from_value` then + `to_value` on an evolvable fixture struct with the hash unchanged; a + colliding `extra` key refused; `Envelope::open` on a wrong kind, an + unknown major, and a newer minor; the serde bridge refusing a float. +- **FR-003.** A test walks `testdata/vectors/codec/` and, for each vector, + decodes `input`, encodes it, and asserts `canonical_hex` and `hash`; then + decodes `canonical_hex` and asserts equality with the decoded input. +- **FR-004.** The third-party dependency set added here is at most `serde` + and one CBOR primitive crate (`ciborium-ll` or a minimal in-tree + encoder); the encoder's canonical rules are implemented in `cbor.rs` + rather than trusted to a library's defaults, and a test pins the + dependency versions used. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-types --locked` passes, including the + vector walk of FR-003. +- **AC-2.** Encoding the same `Value` on two different platforms in CI + (spec 012's matrix) yields identical bytes; this spec's own AC is the + local half: `cargo test -p hqgit-types --locked codec` passes twice with + identical output. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0 on the + branch. + +## 6. Out of scope + +The fuzz targets and the CI matrix that gate these bytes (012); the object +kinds encoded through this codec (013); the ledger entry's specific key set +and signing preimage (017); any streaming or incremental encoder (a whole +object is encoded in memory; large content is chunked by 014, never encoded +as one CBOR item). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-types --locked codec +cargo test -p hqgit-types --locked +``` diff --git a/specs/012-hash-stability-gate/spec.md b/specs/012-hash-stability-gate/spec.md new file mode 100644 index 0000000..d696e05 --- /dev/null +++ b/specs/012-hash-stability-gate/spec.md @@ -0,0 +1,156 @@ +--- +id: "012-hash-stability-gate" +title: "Hash stability gate: fuzz targets, the golden-vector walk, and the cross-platform CI matrix" +status: approved +kind: "tooling" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "011-canonical-encoding" +establishes: + - "fuzz/Cargo.toml" + - "fuzz/fuzz_targets/codec_roundtrip.rs" + - "fuzz/fuzz_targets/canonical_idempotent.rs" + - "fuzz/fuzz_targets/value_ordering.rs" + - "fuzz/corpus-seeds/" + - "crates/hqgit-types/tests/golden.rs" + - ".github/workflows/hash-stability.yml" +summary: > + The gate that makes constitution VIII enforceable rather than aspirational. + A standalone cargo-fuzz workspace with three targets over the canonical + codec (decode-encode round trip, encode idempotence, key ordering) and a + committed seed corpus; a golden test that walks every vector under the + vector corpus and asserts byte and hash identity; and a CI workflow that + runs the golden walk on Linux, macOS, and Windows, folds every vector file + into one tree digest, asserts the digests agree across the matrix, and + runs each fuzz target nightly with crash artifacts uploaded. A vector + change fails the gate unless the PR carries a Schema-Major line and the + amendment to the encoding spec: a human decision, never a regenerate. +--- + +# 012: Hash stability gate + +## 1. Purpose + +Thesis §6: hash stability of L0 and L1 is the only unrecoverable mistake, +so it is fuzzed in CI from the first commit. Spec 011 wrote the rules and +the vectors; this spec is the machinery that refuses a change to either +without a human deciding it. It sits before the object store (013) so that +no object or entry is ever encoded under a codec the gate has not held. +Spec 017 adds its entry target to this workspace and its vectors to the +walk; every later kernel spec that freezes bytes does the same. + +## 2. Territory + +The `fuzz/` cargo-fuzz workspace (its manifest, three targets, the seed +corpus), the golden walk `crates/hqgit-types/tests/golden.rs`, and the +workflow `.github/workflows/hash-stability.yml`. The `fuzz/` tree is its +own Cargo workspace (excluded from the root by spec 010 B-1 and discovered +by spec-spine through `standalone_rust_workspaces`). Later specs add +targets by extending `fuzz/Cargo.toml` (017 B-10 is the first). + +## 3. Behavior + +- **B-1 (fuzz workspace).** `fuzz/Cargo.toml` declares `[package] name = + "hqgit-fuzz"`, `publish = false`, an empty `[workspace]` table so it is + standalone, `[package.metadata.spec-spine] spec = + "012-hash-stability-gate"`, `libfuzzer-sys` and a path dependency on + `hqgit-types`, and one `[[bin]]` per target with `test = false`, `doc = + false`. Targets MUST build with `cargo fuzz build` on the pinned nightly + the workflow names and MUST compile as plain binaries under the stable + toolchain with `--features no-fuzz` stubbing `fuzz_target!`, so `make + build` never needs nightly. +- **B-2 (`codec_roundtrip`).** Input: arbitrary bytes. If `decode` + succeeds, `encode(decode(b))` MUST equal `b` and `Hash::of` of both MUST + agree; if `decode` fails, it MUST fail with `Error::Parse` and never + panic. Any panic, including a stack overflow on deep nesting, is a + finding. +- **B-3 (`canonical_idempotent`).** Input: an `Arbitrary` `Value` + (derived through the `arbitrary` crate, floats impossible by + construction). `decode(encode(v))` MUST equal `v` and `encode(v)` MUST be + byte-stable across two calls. +- **B-4 (`value_ordering`).** Input: an arbitrary list of `(key, value)` + pairs. Building a `Value::Map` in any insertion order MUST encode to one + byte string; a decoded map with keys out of canonical order MUST be + rejected; two maps differing only in insertion order MUST hash equal. +- **B-5 (golden walk).** `tests/golden.rs` walks + `crates/hqgit-types/testdata/vectors/**/*.json` in sorted path order, + applies spec 011 FR-003's check to each, prints the count of vectors + checked, and fails on zero vectors (a missing corpus is a failure, not a + pass). Every kernel spec that adds an area (017 `ledger/`, 027 + `attestation/`) is covered by this one walk without changes here. +- **B-6 (the freeze rule).** A pull request that modifies or deletes any + file under `testdata/vectors/` MUST fail the workflow unless its body + contains a line `Schema-Major: <spec-id> <old> -> <new>` naming the + encoding spec amended and the MAJOR bump, and the diff touches that + spec's `spec.md`. Adding a new vector file is allowed without the line. + The check is a workflow step over `git diff --name-status` against the PR + base and the PR body; it is a human decision made visible, never a + regenerate. +- **B-7 (cross-platform matrix).** `hash-stability.yml` runs on + `pull_request` and `push` to `main`: a job matrix over + `ubuntu-latest`, `macos-latest`, `windows-latest` that runs `cargo test + -p hqgit-types --locked golden`, then computes one tree digest + (`sha256` over the sorted list of `<path>\0<bytes>` for every vector + file, with CRLF folded to LF, mirroring spec-spine's own determinism + gate) and uploads it as an artifact; a final job downloads the three + digests and fails unless they are identical. The B-6 check runs in the + Linux leg. +- **B-8 (nightly fuzzing).** A `schedule` trigger (daily) and + `workflow_dispatch` run every target listed by `cargo fuzz list` for + `FUZZ_MINUTES` (default 10) each, seeded from `fuzz/corpus-seeds/ + <target>/`, on the nightly toolchain the workflow pins; any artifact + under `fuzz/artifacts/` is uploaded and the job fails. The seed corpus + is committed and small (under 200 files, each under 4 KiB); the mutated + corpus under `fuzz/corpus/` is gitignored. +- **B-9 (local smoke).** `make fuzz` (spec 001) runs each target for + `FUZZ_SECONDS` when `cargo-fuzz` is installed and reports a skip + otherwise; this spec's targets MUST pass that smoke. + +## 4. Functional requirements + +- **FR-001.** Each target is a single file that imports only `hqgit_types` + and the fuzzing crates; no target reads the filesystem or the network. +- **FR-002.** The seed corpus contains, per target, at least: every + vector's canonical bytes (for `codec_roundtrip`), one deeply nested + value, one map with many keys of equal length, one value with a link, and + one empty input. +- **FR-003.** The tree digest computation is a shell step small enough to + read in one screen and is identical in every matrix leg; it uses no + tool absent from a default runner image. +- **FR-004.** The workflow pins every third-party action by version and + the nightly toolchain by date; a `dependabot` bump of either self-waives + spec-spine's coupling gate (spec 001 B-3) but not this gate's semantics. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-types --locked golden` passes and prints a + vector count of at least twenty. +- **AC-2.** With `cargo-fuzz` on the pinned nightly, `cargo fuzz run + codec_roundtrip -- -max_total_time=20`, the same for + `canonical_idempotent` and `value_ordering`, find no failure. +- **AC-3.** A fixture PR that edits one vector's `canonical_hex` without a + `Schema-Major:` line fails the B-6 step; the same PR with the line and + an edit to `specs/011-canonical-encoding/spec.md` passes it. + +## 6. Out of scope + +The codec rules themselves (011); entry-level fuzzing (017 B-10 extends +this workspace); the general CI gate (`govern.yml`, spec 001); fuzzing +above the codec (a future spec per crate as its bytes freeze). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-types --locked golden +sh -c 'command -v cargo-fuzz >/dev/null 2>&1 && cargo fuzz list | grep -q codec_roundtrip || echo "cargo-fuzz absent: target listing skipped"' +``` diff --git a/specs/013-object-store/spec.md b/specs/013-object-store/spec.md new file mode 100644 index 0000000..557dd98 --- /dev/null +++ b/specs/013-object-store/spec.md @@ -0,0 +1,161 @@ +--- +id: "013-object-store" +title: "Object store: content-addressed blob and tree objects, the ObjectStore trait, memory and local backends" +status: approved +kind: "kernel" +domain: "l0-objects" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "011-canonical-encoding" +establishes: + - "crates/hqgit-object/Cargo.toml" + - "crates/hqgit-object/src/lib.rs" + - "crates/hqgit-object/src/object.rs" + - "crates/hqgit-object/src/store.rs" + - "crates/hqgit-object/src/memory.rs" + - "crates/hqgit-object/src/local.rs" + - "crates/hqgit-object/tests/" +extends: + # redb, blake3 (if 010 did not already list it), and tempfile join the shared table. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + L0 of the layer model: the content-addressed store every other layer + reads and writes through. This spec founds hqgit-object with the object + kinds (Blob, Tree, RepoGenesis) as canonical envelopes, the ObjectStore + trait whose reads verify the hash before returning a byte, an in-memory + backend for tests and a redb-backed local backend under the repository's + .hq directory. Objects are immutable by construction, so every cache + layer above them is trivially correct; deletion exists in the trait only + as the erasure path spec 020 completes with a capability token. Chunking + (014), verified streaming (015), and the remote backend (016) extend this + crate without changing what a stored object is. +--- + +# 013: Object store + +## 1. Purpose + +Thesis §4.1 and constitution VI: canonical state is a set of +content-addressed objects, and BLAKE3 was chosen for verified streaming. +This spec fixes what an object is (a `Cid` over canonical bytes or raw +bytes), what a store promises (a get never returns bytes whose hash it did +not check), and the two backends the CLI needs before any server exists. +It is the first crate that touches a disk, and its immutability property is +what makes the object mapping to git (031), the layered remote cache (016), +and the action cache (071) simple. + +## 2. Territory + +`crates/hqgit-object` as founded here: the manifest (`[package.metadata. +spec-spine] spec = "013-object-store"`), `lib.rs`, `object.rs` (the object +kinds), `store.rs` (the trait and shared verification), `memory.rs`, and +`local.rs`, plus the `tests/` subtree. The crate depends on `hqgit-types` +only within the workspace. Chunking adds `chunk.rs` and `merkle.rs` (014), +verified streaming adds `bao.rs` and `fetch.rs` (015), the remote backend +adds `s3.rs` and `layered.rs` (016), and erasure adds `encrypt.rs` (020); +each `extends` this spec's `lib.rs`. + +## 3. Behavior + +- **B-1 (identity).** `pub type ObjectId = Cid`. A `Raw` object's id is + `Cid { codec: Raw, hash: Hash::of(bytes) }`; a `DagCbor` object's id is + `Cid { codec: DagCbor, hash: Hash::of(canonical_bytes) }` where the + canonical bytes are the spec 011 encoding of its `Envelope`. The same + bytes always have the same id; there is no other identity. +- **B-2 (object kinds).** `object.rs` defines, each as an `Envelope` with + `kind` fixed and `v = 1.0`, all implementing `Canonical` with an `extra` + map (011 B-6): `Blob` (`kind = "object.blob"`, the inline form for small + content: `{ len: u64, bytes }`; the chunked form arrives in 014), + `Tree` (`kind = "object.tree"`, `entries: Vec<TreeEntry { name: String, + mode: EntryMode, cid: Cid }>`), and `RepoGenesis` (`kind = + "object.genesis"`, `{ namespace: Hash, created_by: Principal, versions: + BTreeMap<String, SchemaVersion> }`). `EntryMode` is a closed enum + `File | Executable | Symlink | Tree` encoding as the strings `"file"`, + `"exec"`, `"symlink"`, `"tree"`. An `Object` enum wraps the three with + `Object::kind()` and `Object::id(&self) -> Cid`. +- **B-3 (tree rules).** `Tree` entries MUST be sorted by `name` bytes, + unique by `name`, valid UTF-8, non-empty, and free of `/`, `\0`, `.`, and + `..`; a `Tree` entry's `cid` codec MUST be consistent with its mode + (`Tree` mode points at a `DagCbor` tree, the others at a blob). The + constructor enforces every rule as `Error::Validation` and the decoder + re-checks them, so an unsorted tree cannot exist with a valid id. +- **B-4 (`ObjectStore`).** `trait ObjectStore { fn put(&self, codec: Codec, + bytes: &[u8]) -> Result<Cid, Error>; fn get(&self, cid: &Cid) -> + Result<Option<Bytes>, Error>; fn has(&self, cid: &Cid) -> Result<bool, + Error>; fn list(&self, prefix: &[u8]) -> Result<Vec<Cid>, Error>; fn + erase(&self, cid: &Cid, cap: &EraseCapability) -> Result<Erased, Error>; }`. + `put` MUST compute the id from the bytes it was given, MUST be idempotent + (a second put of the same bytes returns the same id and writes nothing), + and MUST NOT accept a caller-supplied id. `get` MUST recompute the hash + of the bytes it read and return `Error::Crypto` on mismatch; bytes that + failed verification are never returned. `list` returns ids whose hash + starts with `prefix`, sorted. `erase` is declared here so the trait is + complete, and until spec 020 supplies `EraseCapability` construction it + is unconstructible outside that spec (a sealed struct with no public + constructor), so every backend here implements `erase` and no caller can + invoke it. +- **B-5 (`MemoryStore`).** A `BTreeMap<(Codec, Hash), Vec<u8>>` behind a + `RwLock`; the reference implementation tests and every later crate's + fixtures use. +- **B-6 (`LocalStore`).** A redb database at `<repo>/.hq/objects.redb` + with one table `objects: (codec: u8, hash: [u8; 32]) -> bytes` and one + table `meta: str -> bytes` holding `OBJECT_SCHEMA_VERSION`. `put` writes + in one transaction and calls durable commit (fsync); `open` refuses an + unknown schema MAJOR as `Error::Schema`; a torn write cannot produce a + readable object because redb commits are atomic. The path layout is a + contract spec 021 builds the rest of `.hq/` around. +- **B-7 (immutability).** No API updates or overwrites an object. A backend + MAY garbage-collect only through `erase`. This is the property that makes + the layered cache (016) and the git mapping (031) correct without + invalidation logic. +- **B-8 (no ambient input).** The crate reads no clock and no environment; + the only I/O is the redb file the caller names. + +## 4. Functional requirements + +- **FR-001.** `object.rs` and `store.rs` are I/O-free; only `local.rs` + touches a filesystem, behind a path the caller supplies. +- **FR-002.** Tests cover: tampering with stored bytes yields + `Error::Crypto` on get and never a value; a tree with unsorted, duplicate, + or illegal names is refused on construction and on decode; put is + idempotent and returns a stable id; `list` ordering and prefix semantics; + `RepoGenesis` round trip; `LocalStore` reopen after close preserves + objects and refuses a bumped MAJOR; the sealed `EraseCapability` cannot + be constructed from a test. +- **FR-003.** A conformance test suite `tests/conformance.rs` runs the same + assertions against every `ObjectStore` implementation through a generic + function, so backends added by 016 reuse it. +- **FR-004.** The tree vectors (`testdata/vectors/object/tree-*.json`) are + added to spec 011's corpus through an `extends` edge declared by the + build session if the session adds them; otherwise the object encodings + are pinned by unit tests in this crate. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-object --locked` passes. +- **AC-2.** The conformance suite passes against `MemoryStore` and + `LocalStore`. +- **AC-3.** `spec-spine index` discovers `hqgit-object` bound to this spec + and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Content-defined chunking and the chunked blob manifest (014); BAO outboard +data and range reads (015); the S3 backend and the layered cache (016); +constructing `EraseCapability` and encrypted namespaces (020); the git +object mapping (031). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-object --locked +``` diff --git a/specs/014-content-defined-chunking/spec.md b/specs/014-content-defined-chunking/spec.md new file mode 100644 index 0000000..8c96ec8 --- /dev/null +++ b/specs/014-content-defined-chunking/spec.md @@ -0,0 +1,151 @@ +--- +id: "014-content-defined-chunking" +title: "Content-defined chunking: FastCDC with frozen parameters and the chunked blob manifest" +status: approved +kind: "kernel" +domain: "l0-objects" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "013-object-store" +establishes: + - "crates/hqgit-object/src/chunk.rs" + - "crates/hqgit-object/src/merkle.rs" + - "crates/hqgit-object/tests/chunk.rs" + - "crates/hqgit-object/testdata/chunk/" +extends: + - { spec: "013-object-store", unit: "crates/hqgit-object/src/lib.rs", nature: additive } + # Blob gains its chunked representation, BlobManifest. + - { spec: "013-object-store", unit: "crates/hqgit-object/src/object.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Large files are the general path with different chunk statistics, not a + special case bolted on later. This spec adds FastCDC content-defined + chunking with parameters frozen forever (so chunk boundaries are stable + across releases and replicas), the BlobManifest object that names a + chunked blob by the list of its chunk ids, the rule that content at or + below one chunk stays inline, a binary Merkle tree over chunk hashes with + its own domain, and the put_blob and get_blob entry points that hide the + split from every caller. The manifest id is the identity of a chunked + blob; the whole-content BLAKE3 hash is recorded alongside for the git + bridge and the BAO outboard alignment spec 015 needs. +--- + +# 014: Content-defined chunking + +## 1. Purpose + +Thesis §4.1: content-defined chunking plus a Merkle tree means LFS is just +the general path with different chunk statistics. Design §1.1 point 8 +names monorepo scale as a real failure of the incumbent model. This spec +makes every blob, small or large, take the same code path, and it freezes +the chunking parameters because a chunk boundary that moves between +releases would change every manifest id and break deduplication across +replicas (constitution VIII applies to chunk boundaries exactly as to +encodings). + +## 2. Territory + +`chunk.rs` (the chunker and its frozen parameters), `merkle.rs` (the chunk +hash tree), the `BlobManifest` addition to spec 013's `object.rs`, the +`put_blob` and `get_blob` entry points exported from `lib.rs`, the +`tests/chunk.rs` file, and the `testdata/chunk/` fixtures. The `fastcdc` +crate joins the workspace dependency table. + +## 3. Behavior + +- **B-1 (frozen parameters).** `chunk.rs` defines `pub const CHUNK_MIN: + usize = 65_536`, `CHUNK_AVG: usize = 262_144`, `CHUNK_MAX: usize = + 1_048_576`, and `CHUNK_PARAMS_VERSION: u16 = 1`, and uses the FastCDC + (2016) algorithm with normalization level 1 and the gear table of the + `fastcdc` crate at a pinned version. A test hashes the gear table and + asserts the recorded constant `GEAR_TABLE_HASH`; the crate version is + pinned exact in `[workspace.dependencies]`. Changing any of these is a + schema MAJOR of `OBJECT_SCHEMA_VERSION` and a spec amendment. +- **B-2 (chunker).** `pub fn chunk(reader: impl Read) -> impl Iterator<Item + = Result<Chunk, Error>>` yields `Chunk { offset: u64, bytes: Vec<u8> }` + in order with bounded memory (at most `CHUNK_MAX` plus a read buffer + resident). The boundaries MUST depend only on the content bytes, never on + read sizes or platform. +- **B-3 (`BlobManifest`).** Added to `object.rs` as an envelope with `kind + = "object.blob-manifest"`: `{ total_len: u64, content_hash: Hash, + params_version: u16, chunks: Vec<ChunkRef { cid: Cid, len: u32 }>, + extra }`. `content_hash` is `Hash::of` over the whole plaintext content + (the same value a git bridge or BAO outboard is computed against, 015 + and 031). Every `ChunkRef.cid` MUST be a `Raw` object; the sum of `len` + MUST equal `total_len`; the decoder re-checks both as + `Error::Validation`. +- **B-4 (inline threshold).** Content whose length is at most `CHUNK_MAX` + is stored as one `Raw` object and its id is that object's `Cid`; content + above `CHUNK_MAX` is stored as `Raw` chunks plus a `BlobManifest`, and its + id is the manifest's `Cid`. `put_blob(store, reader) -> Result<BlobId, + Error>` where `BlobId { cid: Cid, inline: bool, content_hash: Hash, + total_len: u64 }` applies this rule; `get_blob(store, cid) -> + Result<Option<BlobReader>, Error>` reassembles either form as a streaming + reader that verifies each chunk on read through spec 013 B-4 and, at the + end, verifies `content_hash` (`Error::Crypto` on mismatch). +- **B-5 (chunk tree).** `merkle.rs` computes `chunk_tree_root(chunks: + &[ChunkRef]) -> Hash` as a binary Merkle tree over chunk hashes with + BLAKE3 keyed derivation under the context string `"hqgit/v1/chunk-tree"` + (leaf = `derive(ctx, 0x00 || hash)`, node = `derive(ctx, 0x01 || left + || right)`, an odd trailing node promoted unchanged) and + `chunk_tree_proof(chunks, index) -> Vec<Hash>` with + `verify_chunk_proof(root, index, hash, proof) -> bool`. The root is NOT + the manifest id and NOT `content_hash`; it is the inclusion instrument + for a single chunk when a peer serves chunks out of order (110, 111). +- **B-6 (determinism).** The manifest for given content is a pure function + of the bytes: same content, same chunks, same manifest id, on every + platform. A one-byte edit in the middle of a large file MUST change at + most two chunk ids (the chunk containing the edit and, if a boundary + shifts, its successor) and leave the rest shared. +- **B-7 (no ambient input).** The chunker reads no clock, no environment, + and uses no randomness. + +## 4. Functional requirements + +- **FR-001.** `chunk.rs` and `merkle.rs` are pure over their inputs; the + only I/O is the reader the caller passes and the store the caller names. +- **FR-002.** Fixtures under `testdata/chunk/` hold deterministic + pseudo-random content generated from a recorded seed (the generator is in + the test, the seed is a constant) with the expected boundary offsets and + chunk hashes for 1 MiB, 3 MiB, and 10 MiB inputs; a test regenerates and + asserts them. +- **FR-003.** Tests cover: boundary determinism against FR-002; inline + versus chunked at `CHUNK_MAX` and `CHUNK_MAX + 1`; a one-byte edit + touches at most two chunks; manifest round trip through the codec; the + `len` sum and codec rules refused on decode; `get_blob` detects a + tampered chunk and a tampered `content_hash`; chunk tree proofs verify + and a wrong index fails; the gear table hash pin. +- **FR-004.** Memory during `put_blob` and `get_blob` of a 10 MiB fixture + stays under 4 MiB of buffers (asserted structurally: the reader never + holds more than `CHUNK_MAX` plus the read buffer). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-object --locked chunk` passes. +- **AC-2.** The full `cargo test -p hqgit-object --locked` passes, + including spec 013's conformance suite unchanged. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +BAO outboard trees and verified range reads (015); the remote backend +(016); the git blob mapping that records `content_hash` (031); any +compression (objects are stored as given; compression is a transport +concern for 111). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-object --locked chunk +cargo test -p hqgit-object --locked +``` diff --git a/specs/015-verified-streaming/spec.md b/specs/015-verified-streaming/spec.md new file mode 100644 index 0000000..843ebda --- /dev/null +++ b/specs/015-verified-streaming/spec.md @@ -0,0 +1,148 @@ +--- +id: "015-verified-streaming" +title: "Verified streaming: BAO outboard trees, range proofs, and lazy fetch that never trusts a byte" +status: approved +kind: "kernel" +domain: "l0-objects" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "014-content-defined-chunking" +establishes: + - "crates/hqgit-object/src/bao.rs" + - "crates/hqgit-object/src/fetch.rs" + - "crates/hqgit-object/tests/bao.rs" +extends: + - { spec: "013-object-store", unit: "crates/hqgit-object/src/lib.rs", nature: additive } + # ObjectStore gains the range read; backends gain the outboard sidecar. + - { spec: "013-object-store", unit: "crates/hqgit-object/src/store.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/src/local.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/src/memory.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The reason BLAKE3 was chosen over SHA-256: the BAO tree gives chunk-level + verification and range proofs, so partial and lazy fetch are verifiable + by construction rather than trusted. This spec stores a BAO outboard + encoding beside every Raw object, adds a range read to the store whose + result type cannot exist without a verified proof, defines the + ObjectSource seam a remote (016, 111) implements to serve slices with + proofs, and a lazy fetch API that verifies with bounded memory and never + lets a partial object into the store as if it were whole. Tampered + slices are rejected at the byte where the proof fails. +--- + +# 015: Verified streaming + +## 1. Purpose + +Thesis §4.1 (D2): BLAKE3 over SHA-256 primarily for verified streaming. +Without this spec a client that fetches part of a large blob from a peer +must trust the peer; with it, every slice carries a proof against the +object's own hash, so the remote backend (016), the sync protocol (111), +and the git endpoint (092) can serve ranges to untrusted clients and +accept ranges from untrusted peers. The type system carries the guarantee: +a `VerifiedSlice` is only constructible by the verifier. + +## 2. Territory + +`bao.rs` (outboard encoding, proof construction, proof verification), +`fetch.rs` (the `ObjectSource` seam and the lazy fetch driver), the +`tests/bao.rs` file, and the additive range-read surface on spec 013's +trait and backends. The `bao` crate (the reference BAO implementation) is +pinned exact in the workspace dependency table. + +## 3. Behavior + +- **B-1 (outboard).** For every `Raw` object, `bao::encode_outboard(bytes) + -> Outboard` produces the BAO outboard tree over the standard BLAKE3 + 1024-byte chunk group (the `bao` crate's outboard format, version pinned); + the outboard's root MUST equal `Hash::of(bytes)` and a test asserts it. + Backends store the outboard as a sidecar keyed by the same `(codec, + hash)` under a second redb table `outboards` (013 `LocalStore`) or a + second map (013 `MemoryStore`); `put` computes and stores it in the same + transaction, so an object never exists without its outboard. + `DagCbor` objects are small by construction (011 B-3 nesting limit and + 014 B-4 inline threshold) and carry no outboard; a range read on one is + `Error::Validation`. +- **B-2 (range proofs).** `bao::prove(outboard, bytes, range: Range<u64>) + -> SliceProof` produces the minimal set of tree nodes that lets a + verifier holding only the object hash check the bytes of `range`; + `bao::verify_slice(hash: &Hash, range, proof: &SliceProof, bytes: &[u8]) + -> Result<VerifiedSlice, Error>` re-derives the root and returns + `Error::Crypto` naming the first failing chunk group offset otherwise. + `VerifiedSlice { hash: Hash, range: Range<u64>, bytes: Vec<u8> }` has no + public constructor and no mutable accessor; `into_bytes` is its only + exit. +- **B-3 (store range read).** `ObjectStore` (013 B-4) gains `fn get_range + (&self, cid: &Cid, range: Range<u64>) -> Result<Option<VerifiedSlice>, + Error>`: a local read that still verifies through the outboard rather + than trusting the local disk (a bit flip on disk is detected exactly as + a malicious peer is). A range past the end is clipped; an empty range + returns an empty verified slice. +- **B-4 (`ObjectSource`).** `trait ObjectSource { fn fetch_slice(&self, + cid: &Cid, range: Range<u64>) -> Result<Option<(SliceProof, Vec<u8>)>, + Error>; fn fetch_whole(&self, cid: &Cid) -> Result<Option<Vec<u8>>, + Error>; fn size_of(&self, cid: &Cid) -> Result<Option<u64>, Error>; }`. + The remote backend (016) and the sync session (111) implement it; a + `LocalSource` over any `ObjectStore` implements it for tests and for + serving. +- **B-5 (lazy fetch).** `fetch_range(cid, range, source: &dyn ObjectSource) + -> Result<VerifiedSlice, Error>` fetches and verifies with at most one + slice in memory at a time, splitting a large range into slices of at most + `FETCH_SLICE_MAX = 4 MiB`; `fetch_into(store, cid, source) -> + Result<Cid, Error>` streams a whole object through verification and only + then calls `store.put`, so a partial or tampered object never enters a + store. A chunked blob (014) is fetched manifest first, then each chunk + through the same path, each chunk verified against its own id. +- **B-6 (proof bounds).** For an object of `n` bytes and a range of `k` + bytes, the proof size MUST be `O(log n)` chunk-group hashes plus the + boundary groups; a test asserts an upper bound for the fixture sizes so a + regression to whole-tree proofs is caught. +- **B-7 (no ambient input).** No clock, no environment, no randomness. + +## 4. Functional requirements + +- **FR-001.** `bao.rs` is pure over bytes and proofs; `fetch.rs` performs + I/O only through the `ObjectSource` and `ObjectStore` traits. +- **FR-002.** Tests cover: outboard root equals the object hash; a slice + proof verifies; a one-bit flip in the slice, in the proof, or in the + claimed hash is rejected with the offending group named; ranges + crossing a chunk-group boundary, a chunk (014) boundary, the start, and + the end; empty and past-the-end ranges; `fetch_into` refusing a source + that serves a tampered whole object and leaving the store unchanged; + proof size bounds per B-6; a chunked blob fetched through a source that + serves chunks out of order. +- **FR-003.** Spec 013's conformance suite gains range-read cases and + still passes against both backends. +- **FR-004.** Memory during `fetch_range` of a 10 MiB range stays bounded + by `FETCH_SLICE_MAX` plus proof size (asserted structurally). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-object --locked bao` passes. +- **AC-2.** `cargo test -p hqgit-object --locked` passes, including the + conformance suite with range reads. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The S3-backed `ObjectSource` (016); the QUIC transport and sync session +that serve slices between peers (111); serving ranges over the git +endpoint (092); any caching policy for fetched slices (016's layered +store). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-object --locked bao +cargo test -p hqgit-object --locked +``` diff --git a/specs/016-remote-object-backend/spec.md b/specs/016-remote-object-backend/spec.md new file mode 100644 index 0000000..937b401 --- /dev/null +++ b/specs/016-remote-object-backend/spec.md @@ -0,0 +1,145 @@ +--- +id: "016-remote-object-backend" +title: "Remote object backend: an S3-compatible store and the layered read-through cache" +status: approved +kind: "feature" +domain: "l0-objects" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 1 +depends_on: + - "013-object-store" + - "015-verified-streaming" +establishes: + - "crates/hqgit-object/src/s3.rs" + - "crates/hqgit-object/src/layered.rs" + - "crates/hqgit-object/tests/s3.rs" +extends: + - { spec: "013-object-store", unit: "crates/hqgit-object/src/lib.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Thesis §4.1: local index in redb, remote in any S3-compatible store, and + immutability makes every cache layer trivially correct. This spec adds + an ObjectStore over any S3-compatible endpoint through the object_store + crate, with a hash-sharded key layout, outboard sidecars, and conditional + puts for idempotency; an ObjectSource over the same endpoint so verified + range fetches (015) work against remote objects; and a LayeredStore that + reads through a local store to a remote one and writes through to both. + Negative caching is forbidden by design: content is immutable, presence + is not, so a miss is retried and never memoized. +--- + +# 016: Remote object backend + +## 1. Purpose + +Spec 013 gave the CLI a local store and spec 015 made partial reads +verifiable. A server (090), a CI executor (073), and a peer (111) need +objects that live somewhere shared and durable without a second storage +system to operate. Any S3-compatible store is that place (thesis §4.1), +and because objects are immutable (013 B-7) the layered cache in front of +it needs no invalidation logic at all: the only question a cache can get +wrong is presence, and this spec forbids caching the answer "absent". + +## 2. Territory + +`s3.rs` (the `S3Store` backend and its `ObjectSource`), `layered.rs` (the +`LayeredStore`), and `tests/s3.rs`. The `object_store` crate (with its +`aws` feature) is pinned in the workspace dependency table. Spec 013's +conformance suite is reused against both new backends unchanged. + +## 3. Behavior + +- **B-1 (`S3Store`).** `S3Store::new(config: S3Config) -> Result<S3Store, + Error>` where `S3Config { endpoint: String, bucket: String, prefix: + String, region: Option<String>, credentials: CredentialSource }` and + `CredentialSource` is `Env | Profile(String) | Static { .. }` (never + serialized, never logged). It implements `ObjectStore` (013 B-4, with + 015 B-3's `get_range`) over the `object_store` crate's `AmazonS3` + client, and MUST work against MinIO, Ceph RGW, and AWS S3 through the + same configuration. +- **B-2 (key layout).** Object bytes at `<prefix>/objects/<codec-name>/ + <hash[0..2]>/<hash-hex>` and the BAO outboard (015 B-1) at the same key + with the suffix `.obao`. `list(prefix)` maps to a listing under + `objects/<codec-name>/<first two hex chars>` and filters locally; results + are sorted before return. +- **B-3 (idempotent put).** `put` first computes the id, then issues a + conditional put (`If-None-Match: *` where the endpoint supports it, else + a head-then-put with the race accepted because the bytes are identical + by construction). A second put of existing content performs no upload. + The outboard is written before the object so a reader never sees an + object without its sidecar; a crash between the two leaves an orphan + sidecar that the next put overwrites identically. +- **B-4 (verified reads).** `get` downloads and verifies the hash (013 + B-4); `get_range` downloads the outboard and the byte range and verifies + through spec 015 B-2, so a corrupted or malicious bucket is detected + exactly as a malicious peer is. `S3Store` also implements 015 B-4's + `ObjectSource`, serving `fetch_slice` with a proof computed from the + stored outboard. +- **B-5 (`LayeredStore`).** `LayeredStore { local: Box<dyn ObjectStore>, + remote: Box<dyn ObjectStore> }` implements `ObjectStore`: `get` and + `get_range` try `local` then `remote`, and on a remote hit store the + verified bytes locally (write-back of verified content only); `put` + writes local then remote (write-through; a remote failure returns + `Error::Io` after the local write succeeded and the caller may retry + idempotently); `has` is local or remote; `list` unions and sorts; + `erase` is applied to both. There is no eviction policy in this spec: + the local tier is a full store, and eviction is a later operator feature. +- **B-6 (no negative cache).** `LayeredStore` MUST NOT record a remote + miss. A subsequent `get` of the same id asks the remote again. This is + stated as a rule because it is the one cache mistake immutability does + not forgive: an object absent now may be present after a peer pushes it. +- **B-7 (errors).** Network and authorization failures are `Error::Io` + with the endpoint and key named but credentials redacted; a hash mismatch + is `Error::Crypto`; a misconfiguration is `Error::Config`. +- **B-8 (no ambient input).** Credentials come from the caller's + `CredentialSource`; the store reads no clock and no environment except + when the caller chose `CredentialSource::Env`, which is the only + environment read in the crate and is documented as such. + +## 4. Functional requirements + +- **FR-001.** `S3Store` and `LayeredStore` pass spec 013's conformance + suite (with the 015 range cases) against the `object_store` crate's + in-memory backend configured as the S3 seam, so unit tests need no + network. +- **FR-002.** Tests cover: key layout for a known hash; put idempotency + (a second put issues no upload, asserted through a counting wrapper); + outboard-before-object ordering; a tampered remote object refused; + `LayeredStore` read-through populates local; write-through with a + failing remote leaves local populated and returns `Error::Io`; no + negative cache (a miss followed by a remote put followed by a get + succeeds); `fetch_range` (015) over `S3Store` as an `ObjectSource`. +- **FR-003.** One `#[ignore]` live test runs against an endpoint named by + `HQGIT_TEST_S3_ENDPOINT` and is documented in the test file; CI does not + run it. +- **FR-004.** Credentials never appear in `Debug` output, logs, or error + messages (a test formats an error and a config and asserts absence). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-object --locked s3` passes. +- **AC-2.** `cargo test -p hqgit-object --locked` passes, conformance suite + included. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Local-tier eviction and quota (an operator feature after 090); peer-to-peer +object exchange (111); server-side placement of buckets per repository +(091); encryption of objects at rest beyond the namespace envelope (020). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-object --locked s3 +cargo test -p hqgit-object --locked +``` diff --git a/specs/017-ledger-entry-dag/spec.md b/specs/017-ledger-entry-dag/spec.md new file mode 100644 index 0000000..42aee67 --- /dev/null +++ b/specs/017-ledger-entry-dag/spec.md @@ -0,0 +1,165 @@ +--- +id: "017-ledger-entry-dag" +title: "Ledger entry and the hash-linked DAG: shape, signing bytes, append, verify" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "011-canonical-encoding" + - "013-object-store" +establishes: + - "crates/hqgit-ledger/Cargo.toml" + - "crates/hqgit-ledger/src/lib.rs" + - "crates/hqgit-ledger/src/entry.rs" + - "crates/hqgit-ledger/src/dag.rs" + - "crates/hqgit-ledger/src/verify.rs" + - "crates/hqgit-ledger/tests/" + - "fuzz/fuzz_targets/entry_hash_stable.rs" +extends: + # The frozen entry vectors join the golden corpus 011 established. + - { spec: "011-canonical-encoding", unit: "crates/hqgit-types/testdata/vectors/", nature: additive } + # One more fuzz target in the fuzz workspace 012 established. + - { spec: "012-hash-stability-gate", unit: "fuzz/Cargo.toml", nature: additive } +summary: > + The core primitive of the platform: a ledger entry is { parents, issuer, + hlc, payload, sig }, a hash-linked DAG rather than a linear log because + concurrent authors are the normal case. This spec founds hqgit-ledger and + fixes, permanently, the entry's canonical shape, its signing preimage, and + its hash; the in-memory DAG with append and head tracking; and chain + verification (hash links, parent presence, signature through an issuer + resolver seam, clock monotonicity). Nothing here persists (021) or orders + (018) or interprets payloads (019, 023): it is the substrate those specs + build on, and the golden entry vectors it freezes are the record every + later release must reproduce byte for byte. +--- + +# 017: Ledger entry and the hash-linked DAG + +## 1. Purpose + +Thesis §4.2: the ledger is a per-repository signed, hash-linked event DAG. +Every fact about a repository (a revision, a comment, an attestation, a key +rotation) is carried by an entry of the one shape fixed here. Because the +hash of an entry is what parents reference, what signatures cover, and what +every replica reconciles on (110), its byte layout is the single most +consequential decision in the corpus and is frozen at tier 1 (bootstrap +anchor `hash-stability`). This spec exists to make that decision once, +explicitly, with vectors. + +## 2. Territory + +`crates/hqgit-ledger` as founded here: the manifest, `lib.rs`, `entry.rs` +(the shape, encoding, signing, hashing), `dag.rs` (the in-memory DAG), and +`verify.rs` (chain verification and the `IssuerResolver` seam), plus the +`tests/` subtree. Additively: entry vectors under spec 011's golden vector +directory and one fuzz target in spec 012's workspace. Persistence is spec +021 (`store.rs`, `repo.rs`); total order and clock generation are spec 018; +the fact and derived-state split is spec 019. + +## 3. Behavior + +- **B-1 (shape).** `Entry { parents: Vec<EntryHash>, issuer: KeyId, hlc: + Hlc, payload: Cid, sig: Signature, extra: BTreeMap<String, Value> }`. + `EntryHash` is a newtype over `Hash`. `parents` MUST be sorted ascending + and free of duplicates; a constructor enforces it and a decoder rejects a + violation as `Error::Validation`. `extra` is the unknown-field + preservation map from spec 011: fields a newer writer added and this + reader does not know, carried verbatim and included in the hash. +- **B-2 (canonical bytes).** The canonical bytes of an entry are the spec + 011 DAG-CBOR encoding of the map `{ "extra"?, "hlc", "issuer", + "parents", "payload", "sig" }` with keys in canonical order, `extra` + flattened into the top-level map (its keys MUST NOT collide with the five + known keys; a collision is `Error::Validation`), and omitted when empty. +- **B-3 (signing preimage).** The signed payload is the canonical bytes of + the entry with `sig` absent, under `SignDomain("ledger.entry")` through + spec 010's preimage rule. `Entry::sign(unsigned, &impl Signer) -> Entry` + is the only way to produce a signed entry; `Entry::unsigned_bytes()` is + exposed for verification and for the fuzz target. +- **B-4 (hash).** `EntryHash = Hash::of(canonical_bytes_with_sig)`. The + hash covers the signature: re-signing the same body yields a distinct + entry. `Entry::hash()` is memoized nowhere; it is recomputed from bytes so + no cached value can drift from the encoding. +- **B-5 (genesis).** A DAG has exactly one genesis entry: `parents` empty, + `payload` a `Cid` of the spec 013 `RepoGenesis` object (namespace id, + created-by principal, schema versions). Every other entry MUST have at + least one parent. `Dag::new(genesis)` is the only constructor. +- **B-6 (append).** `Dag::append(entry) -> Result<EntryHash, Error>` + requires every parent to be present (`Error::NotFound` naming the missing + parent otherwise), requires `entry.hlc` to be strictly greater than every + parent's `hlc` (`Error::Validation`), rejects a duplicate hash as a no-op + `Ok`, and updates the head set (entries with no known child). `heads()` + returns the head set sorted by hash. `ancestors(hash)` and + `contains(hash)` are read-only. The DAG holds entries only; payload + content is resolved through the object store (013), never stored inline. +- **B-7 (verify).** `verify(dag, resolver: &impl IssuerResolver) -> + Result<VerifyReport, Error>` walks every entry in hash order and checks: + the recomputed hash equals the stored key; every parent exists; the + signature verifies against the `Verifier` the resolver returns for + `(issuer, hlc)`; the clock rule of B-6; the genesis rule of B-5; the + payload `Cid` codec is `DagCbor` or `Raw`. The report lists every failure + with the entry hash and the rule; it never stops at the first. The + `IssuerResolver` trait is `fn verifier_for(&self, key: &KeyId, at: &Hlc) + -> Result<Box<dyn Verifier>, Error>`; this spec ships + `StaticResolver(BTreeMap<KeyId, PublicKey>)`. Spec 060 supplies the + rotation-aware resolver. +- **B-8 (no ambient input).** Nothing in this crate reads a clock, the + environment, or iterates a `HashMap`. Fuzzing (B-10) and the source guard + of spec 010 FR-003 both apply to this crate. +- **B-9 (golden vectors).** The vectors directory gains + `ledger/entry-genesis.json`, `ledger/entry-two-parents.json`, and + `ledger/entry-with-extra.json`, each holding the unsigned fields, the + seed, the expected canonical bytes (hex), the expected signature, and the + expected hash. A test re-derives every field. These vectors are frozen + (constitution VIII): a change is a schema MAJOR. +- **B-10 (fuzz target).** `entry_hash_stable` decodes arbitrary bytes as an + entry; on success it re-encodes and asserts byte identity and hash + identity, and asserts that `parents` came out sorted and deduplicated. + +## 4. Functional requirements + +- **FR-001.** Every function in `entry.rs`, `dag.rs`, and `verify.rs` is a + pure function of its arguments; the only trait objects are the `Signer`, + `Verifier`, and `IssuerResolver` seams. +- **FR-002.** Tests cover: shape validation (unsorted or duplicate parents, + colliding `extra` key); canonical bytes against the vectors; sign then + verify round trip; hash covers the signature; genesis rules; append with a + missing parent, a non-monotonic clock, and a duplicate; head tracking + across a fork and a merge entry; verify reporting every failure class in + one report; unknown fields surviving a decode-encode round trip and + changing the hash. +- **FR-003.** Property tests (`proptest`, added to + `[workspace.dependencies]`) assert that for random valid entries decode + and encode are inverse and the hash is stable across two encodes. +- **FR-004.** The crate depends on `hqgit-types` and `hqgit-object` only + within the workspace. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-ledger --locked` passes, vectors included. +- **AC-2.** With `cargo-fuzz` installed, `cargo fuzz run entry_hash_stable + -- -max_total_time=20` finds no failure. +- **AC-3.** `spec-spine index` discovers `hqgit-ledger` bound to this spec + and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Persistence (021), total order and clock generation (018), the fact and +derived-state split (019), commitments and tombstones (020), rotation-aware +issuer resolution (060), replication (110). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-ledger --locked +cargo test -p hqgit-types --locked golden +``` diff --git a/specs/018-deterministic-total-order/spec.md b/specs/018-deterministic-total-order/spec.md new file mode 100644 index 0000000..b923e65 --- /dev/null +++ b/specs/018-deterministic-total-order/spec.md @@ -0,0 +1,149 @@ +--- +id: "018-deterministic-total-order" +title: "Deterministic total order over the DAG and hybrid logical clock generation" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "017-ledger-entry-dag" +establishes: + - "crates/hqgit-ledger/src/order.rs" + - "crates/hqgit-ledger/src/clock.rs" + - "crates/hqgit-ledger/tests/order.rs" +extends: + - { spec: "017-ledger-entry-dag", unit: "crates/hqgit-ledger/src/lib.rs", nature: additive } +summary: > + Concurrent facts merge by set union under a deterministic total order, + topological with a tiebreak on hash: that sentence from the thesis is + what lets two replicas holding the same entries fold to the same state + without coordination. This spec fixes the order (a Kahn sort whose ready + set is ordered by hybrid logical clock then entry hash), its incremental + form from a checkpoint, and the fold cursor projections resume from. It + also owns the one place in the ledger crate that reads a clock: the + hybrid logical clock generator, with an injected clock source, a drift + bound, and the guarantee that it is never called on a hashing path. +--- + +# 018: Deterministic total order and clock generation + +## 1. Purpose + +Thesis §4.2 and constitution VII: facts never conflict because they merge +by set union under one deterministic total order. The DAG (017) gives +partial order; this spec gives the total order every fold (019, 080) and +every reconciliation (110) agrees on, so "the state of the repository at +entry X" means the same thing on every machine. The hybrid logical clock +that stamps entries lives here too, because its algorithm is what makes +the order meaningful across replicas whose wall clocks disagree. + +## 2. Territory + +`order.rs` (the total order, the incremental order, the fold cursor) and +`clock.rs` (the HLC generator and the `ClockSource` seam) in the crate 017 +founded, plus `tests/order.rs`. Nothing here changes what an entry is or +how it hashes. + +## 3. Behavior + +- **B-1 (total order).** `total_order(dag: &Dag) -> Vec<EntryHash>` is a + Kahn topological sort over the parent relation where the ready set is a + `BTreeSet<(Hlc, EntryHash)>` and the smallest element is popped each + step. The result MUST list every parent before its children, MUST order + concurrent entries by `(hlc, hash)` ascending, and MUST be identical for + any two DAGs holding the same entry set regardless of insertion order or + the order `Dag` iterates internally. The genesis entry is always first. +- **B-2 (incremental order).** `order_from(dag, checkpoint: &FoldCursor) -> + Result<Vec<EntryHash>, Error>` returns the suffix of `total_order(dag)` + after the checkpoint's position when the checkpoint is a prefix of the + current total order, and `Error::Stale` naming the first divergent + position when new entries sorted earlier than the checkpoint's last + entry (a late-arriving concurrent entry with a smaller `(hlc, hash)`), + in which case the caller (080) MUST rebuild from zero or from the last + stable prefix. Staleness is thus explicit, never a silently reordered + fold. +- **B-3 (`FoldCursor`).** `FoldCursor { position: u64, last: EntryHash, + prefix_hash: Hash }` where `prefix_hash` is the running BLAKE3 over the + ordered entry hashes up to `position` (`Hash::of(prev_prefix_hash || + entry_hash)` with the genesis prefix hash being `Hash::of(genesis)`); + a checkpoint from one replica validates against another replica's order + by recomputing the prefix hash. It implements `Canonical` (011) so + projections (080) persist it. +- **B-4 (`ClockSource`).** `trait ClockSource { fn now_ms(&self) -> u64; }` + with `SystemClock` (the one `std::time` read in the ledger crate, + isolated in `clock.rs`) and `FixedClock(u64)` for tests. No other module + in `hqgit-ledger` may name `std::time`; spec 010 FR-003's source guard is + extended by a test here to assert that `std::time` appears only in + `clock.rs`. +- **B-5 (`HlcGenerator`).** `HlcGenerator::new(node: NodeId, source: Box<dyn + ClockSource>) -> Self`; `fn next(&mut self, observed: Option<&Hlc>) -> + Result<Hlc, Error>` implements the hybrid logical clock (Kulkarni, + Demirbas, Madeppa, Avva, Leone, 2014): `wall = max(now, last.wall, + observed.wall)`; if `wall == last.wall == observed.wall` then `logical = + max(last.logical, observed.logical) + 1`; if `wall == last.wall` then + `logical = last.logical + 1`; if `wall == observed.wall` then `logical = + observed.logical + 1`; else `logical = 0`. The result is strictly greater + than both `last` and `observed` under spec 010 B-8's `Ord`. The + generator's `node` is fixed for its lifetime. +- **B-6 (drift bound).** `next` MUST return `Error::Validation` when + `observed.wall_ms` exceeds `now_ms` by more than `MAX_DRIFT_MS = + 60_000`, and when `logical` would overflow `u32::MAX`; the caller + surfaces the first as a peer clock problem rather than adopting a far + future timestamp that would dominate the order forever. +- **B-7 (node id).** `NodeId` is derived as the first 16 bytes of + `Hash::of(b"hqgit/v1/node" || key_id_bytes)` from the repository's local + identity key (021 stores it; 060 formalizes identity), so two replicas of + one identity produce comparable but distinct clocks; a test pins the + derivation. +- **B-8 (never on a hashing path).** No function that produces canonical + bytes or a hash calls `HlcGenerator`; the generator is invoked once per + entry by the append path (021) and the resulting `Hlc` is data from then + on. + +## 4. Functional requirements + +- **FR-001.** `order.rs` is pure over the `Dag`; `clock.rs` is pure over + its injected source. +- **FR-002.** Tests cover: permutation invariance (the same fifty entries + inserted in twenty random orders, seeded, yield one total order); + tie-break vectors (equal `Hlc`, distinct hashes); a fork and merge; the + genesis-first rule; `order_from` on a prefix cursor and on a stale + cursor; prefix hash recomputation across replicas; the HLC state table + of B-5 case by case with `FixedClock`; drift rejection; logical + overflow; node id derivation vector; the `std::time` isolation guard. +- **FR-003.** A property test (`proptest`) asserts that for random DAGs the + total order is a linear extension of the parent relation and is + invariant under insertion permutation. +- **FR-004.** `total_order` on a DAG of 100 000 entries completes in under + two seconds in a release build (a benchmark-style test marked + `#[ignore]` documents the bound; CI runs the 10 000-entry variant). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-ledger --locked order` passes. +- **AC-2.** `cargo test -p hqgit-ledger --locked` passes with spec 017's + tests unchanged. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Interpreting entry payloads as facts and folding derived state (019); +persisting cursors and rebuilding projections (080); reconciling entry +sets between replicas (110); identity-derived node ids beyond the local +key (060). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-ledger --locked order +cargo test -p hqgit-ledger --locked +``` diff --git a/specs/019-facts-and-derived-state/spec.md b/specs/019-facts-and-derived-state/spec.md new file mode 100644 index 0000000..70ca79a --- /dev/null +++ b/specs/019-facts-and-derived-state/spec.md @@ -0,0 +1,165 @@ +--- +id: "019-facts-and-derived-state" +title: "Facts and derived state: the immutable fact envelope, the registry seam, LWW registers, and the reserved sequence CRDT" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "018-deterministic-total-order" +establishes: + - "crates/hqgit-ledger/src/facts.rs" + - "crates/hqgit-ledger/src/derived.rs" + - "crates/hqgit-ledger/src/crdt/mod.rs" + - "crates/hqgit-ledger/src/crdt/lww.rs" + - "crates/hqgit-ledger/src/crdt/sequence.rs" + - "crates/hqgit-ledger/tests/facts.rs" +extends: + - { spec: "017-ledger-entry-dag", unit: "crates/hqgit-ledger/src/lib.rs", nature: additive } + - { spec: "017-ledger-entry-dag", unit: "crates/hqgit-ledger/Cargo.toml", nature: additive } +summary: > + The separation the thesis calls critical: facts are immutable events that + never conflict and merge by set union; derived state is the mutable + projection and the only thing that needs convergence. This spec fixes + the fact envelope every entry payload decodes to, the registry seam + through which domain crates declare fact kinds and validators while + unknown kinds stay preserved and opaque, the last-writer-wins register + keyed by hybrid logical clock that every derived field uses unless a + spec argues otherwise, the fold that rebuilds any derived state from + zero, and the sequence CRDT interface reserved for collaborative text + and nothing else. It keeps the CRDT surface near five percent of the + domain, which is the difference between a system that can be reasoned + about and one that cannot. +--- + +# 019: Facts and derived state + +## 1. Purpose + +Thesis §4.2, constitution VII, bootstrap anchor `facts-immutable`: the +place most projects go wrong is reaching for CRDTs everywhere. Every +domain noun above this spec (024 changes, 026 threads, 028 issues, 060 +identities, 104 ownership) is expressed as facts that never conflict and +derived fields that converge by one rule. This spec supplies both halves +and the fold that joins them under the total order of 018, so that a +domain spec adds a fact kind and a view, never a merge algorithm. + +## 2. Territory + +`facts.rs` (the envelope, the registry seam, decoding an entry payload to +a fact), `derived.rs` (the `DerivedState` trait and the fold), and +`crdt/` (`lww.rs` the register, `sequence.rs` the reserved interface and +reference implementation, `mod.rs` the exports), plus `tests/facts.rs`, in +the crate 017 founded. The `collab-text` cargo feature is declared in the +crate manifest. + +## 3. Behavior + +- **B-1 (`FactEnvelope`).** An entry's payload object (013, codec + `DagCbor`) decodes to `FactEnvelope { kind: FactKind, v: u16, body: + Value, extra }` through spec 011's `Envelope` with `kind` prefixed + `"fact."` on the wire. `FactKind(String)` is a lowercase, dot-namespaced + identifier matching `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$` (`"change. + revision_submitted"`, `"ledger.tombstone"`); a malformed kind is + `Error::Validation`. `Fact { entry: EntryHash, issuer: KeyId, at: Hlc, + envelope: FactEnvelope }` is the decoded form the fold consumes; it is + immutable and carries no setters. +- **B-2 (`FactRegistry`).** `FactRegistry::new()` plus `fn register(&mut + self, kind: FactKind, max_v: u16, validator: Box<dyn FactValidator>) -> + Result<(), Error>` (`Error::Validation` on a duplicate kind) and `fn + validate(&self, envelope: &FactEnvelope) -> Result<Validated, Error>`. + `trait FactValidator { fn validate(&self, v: u16, body: &Value) -> + Result<(), Error>; }`. Domain crates register their kinds in a function + they export (023 `register_domain`, 060 `register_trust`); the CLI and + server call every registrar at startup. `Validated` is `Known | + Unknown`: a kind not in the registry is NOT an error, it is preserved + verbatim, folded as opaque (B-5), and counted, so an older reader + survives a newer writer (constitution VIII's forward compatibility + applied to facts). +- **B-3 (set union).** The fact set of a repository is the set of + `(EntryHash, Fact)` pairs; two replicas' fact sets merge by union with no + conflict resolution because every fact is keyed by an entry hash that + covers its content and its signer (017 B-4). A test asserts that folding + the union in total order is independent of which replica contributed + which fact. +- **B-4 (`LwwRegister`).** `LwwRegister<T: Canonical + Ord> { value: + Option<T>, at: Hlc, by: KeyId }` with `fn set(&mut self, value: T, at: + Hlc, by: KeyId)` applying the write only when `(at, by) > (self.at, + self.by)`, and `fn merge(&mut self, other: &Self)` as the same rule. The + tiebreak on `by` after `at` makes merge commutative, associative, and + idempotent (a test proves all three on random sequences). Every derived + scalar field in the domain MUST be an `LwwRegister` unless its owning + spec's Behavior section argues for another instrument (028's add-wins + label set is the one such argument in wave 1). +- **B-5 (`DerivedState` and the fold).** `trait DerivedState: Default { + fn apply(&mut self, fact: &Fact) -> Result<(), Error>; fn on_unknown(&mut + self, fact: &Fact) { let _ = fact; } }`; `fold<S: DerivedState>(order: + &[EntryHash], facts: &dyn FactSource) -> Result<S, Error>` starts from + `S::default()` and applies every fact in the given total order (018), + routing unknown kinds to `on_unknown`, so any state is rebuildable from + zero (constitution VI). `trait FactSource { fn fact(&self, entry: + &EntryHash) -> Result<Option<Fact>, Error>; }` is implemented by 021's + repository; an erased payload (020) surfaces as `Fact` with `body = + Value::Null` and `extra["erased"] = true`, which `apply` MUST tolerate. +- **B-6 (`SequenceCrdt`).** `crdt/sequence.rs` declares `trait + SequenceCrdt { type Id: Canonical + Ord; fn insert(&mut self, after: + Option<&Self::Id>, ch: char, at: Hlc, by: KeyId) -> Self::Id; fn delete + (&mut self, id: &Self::Id, at: Hlc, by: KeyId); fn materialize(&self) -> + String; fn merge(&mut self, other: &Self); }` and, behind `#[cfg(feature + = "collab-text")]`, an RGA reference implementation `Rga` with ids + `(Hlc, KeyId)`. This interface is reserved for genuinely collaborative + text (constitution VII); a doc comment names the rule and no other + module in the workspace may depend on the feature without a spec + amendment that argues the case. +- **B-7 (no ambient input).** The fold reads no clock; the `Hlc` it orders + by is entry data. + +## 4. Functional requirements + +- **FR-001.** `facts.rs`, `derived.rs`, and `crdt/` are pure over their + inputs; the only trait objects are `FactValidator` and `FactSource`. +- **FR-002.** Tests cover: `FactKind` grammar accept and reject cases; + registry duplicate refusal; validation of a known kind with a failing + validator; an unknown kind preserved through decode, fold, and re-encode + with the hash unchanged; union commutativity (B-3); LWW commutativity, + associativity, idempotence, and tiebreak on `by`; a fixture + `DerivedState` with three fields folded under twenty seeded + permutations of the same facts to one state; the erased-payload case; + `Rga` insert, delete, materialize, and merge under permutation (feature + on). +- **FR-003.** A property test asserts LWW merge is a join (commutative, + associative, idempotent) on random write sequences. +- **FR-004.** The `collab-text` feature is off by default and `cargo build + --workspace --locked` never enables it; a test in the default build + asserts `Rga` is not linked. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-ledger --locked facts` passes. +- **AC-2.** `cargo test -p hqgit-ledger --locked --features collab-text` + passes. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The domain's own fact kinds and their validators (023); erasure and the +tombstone fact kind (020); persistence of facts and the repository-backed +`FactSource` (021); projections that persist folded state (080); any use +of the sequence CRDT by a product feature (none is planned; a future spec +must argue it). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-ledger --locked facts +cargo test -p hqgit-ledger --locked --features collab-text +``` diff --git a/specs/020-commitments-and-tombstones/spec.md b/specs/020-commitments-and-tombstones/spec.md new file mode 100644 index 0000000..51cf8d5 --- /dev/null +++ b/specs/020-commitments-and-tombstones/spec.md @@ -0,0 +1,184 @@ +--- +id: "020-commitments-and-tombstones" +title: "Commitments and tombstones: content indirection, per-namespace encryption, and erasure that keeps the chain verifiable" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "019-facts-and-derived-state" +establishes: + - "crates/hqgit-ledger/src/commitment.rs" + - "crates/hqgit-ledger/src/tombstone.rs" + - "crates/hqgit-object/src/encrypt.rs" + - "crates/hqgit-ledger/tests/erasure.rs" +extends: + - { spec: "017-ledger-entry-dag", unit: "crates/hqgit-ledger/src/lib.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/src/lib.rs", nature: additive } + # EraseCapability gains its constructors; erase gains its contract. + - { spec: "013-object-store", unit: "crates/hqgit-object/src/store.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/src/local.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/src/memory.rs", nature: additive } + - { spec: "013-object-store", unit: "crates/hqgit-object/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The erasure design decided at entry one because it cannot be + retrofitted: the signed log contains commitments (content identifiers), + never user content; content lives in the object store, encrypted per + namespace where it must be; deletion removes the blob and appends a + tombstone over the commitment; the chain stays verifiable and the + content is genuinely gone. This spec fixes payload resolution with its + three honest answers (present, erased, missing), the namespace + encryption envelope whose commitment is over plaintext while the store + holds ciphertext, the tombstone fact, the erase operation, and the + unforgeable capability that gates it. Every projection must render an + erased payload as erased and never fail. +--- + +# 020: Commitments and tombstones + +## 1. Purpose + +Thesis §4.2 and §8, constitution X, bootstrap anchor `erasure-by- +tombstone`: signed, replicated, append-only collaboration data collides +with erasure requirements and moderation, and the answer is content +indirection from day one. Spec 017 already made entry payloads `Cid`s; +this spec completes the design so that the CLI (033) can erase a comment +body, the server (094) can moderate quarantine content, and a legal hold +can be honored, all without rewriting a single signed byte. It also +supplies namespace encryption, so a private namespace's content is opaque +to a replica that holds its bytes but not its key. + +## 2. Territory + +`commitment.rs` (payload resolution and the encrypted-namespace read +path) and `tombstone.rs` (the tombstone fact kind, `erase`, and +`EraseCapability` minting) in `hqgit-ledger`; `encrypt.rs` (the +encryption envelope and the `KeyProvider` seam) in `hqgit-object`; the +`tests/erasure.rs` file; and the additive completion of spec 013's `erase` +contract across the trait and both backends. The AEAD and HKDF crates are +pinned in the workspace dependency table. + +## 3. Behavior + +- **B-1 (commitments).** An entry's `payload` is a `Cid` and nothing else + (017 B-1); the log never carries content. `resolve_payload(entry: &Entry, + store: &dyn ObjectStore, tombstones: &TombstoneSet, keys: &dyn + KeyProvider) -> Result<Resolved, Error>` answers `Resolved::Present + (Vec<u8>)`, `Resolved::Erased(TombstoneRef { entry: EntryHash, reason: + EraseReason, at: Hlc })`, or `Resolved::Missing` (not erased, not held + locally: a fetch (015) may still find it). A caller MUST handle all + three; there is no accessor that panics or that maps `Erased` to + `Missing`. +- **B-2 (encryption envelope).** `encrypt.rs` defines `EncryptedBlob { + alg: Alg, nonce: [u8; 24], ciphertext: Vec<u8>, extra }` as an envelope + with `kind = "object.encrypted"`, `Alg` a closed enum with one member + `XChaCha20Poly1305`. `encrypt(key: &NamespaceKey, cid: &Cid, plaintext: + &[u8]) -> EncryptedBlob` uses the plaintext's `Cid` bytes as associated + data and a nonce derived as the first 24 bytes of HKDF-BLAKE3(key, + info = b"hqgit/v1/nonce" || cid) so encryption is deterministic (the + same plaintext in the same namespace encrypts identically, preserving + put idempotency and deduplication); `decrypt(key, cid, &EncryptedBlob) + -> Result<Vec<u8>, Error>` verifies the tag and re-verifies `Hash::of + (plaintext) == cid.hash` (`Error::Crypto` on either failure). +- **B-3 (commitment over plaintext).** In an encrypted namespace the + `Cid` referenced by an entry is computed over the plaintext (the + commitment is content-addressed and stable across re-keying), while the + store holds the `EncryptedBlob` keyed by that same `Cid`. `ObjectStore + ::put_encrypted(key, codec, plaintext) -> Cid` and `get` on such an id + returns the encrypted envelope bytes; `resolve_payload` performs the + decrypt through the `KeyProvider`. A store therefore verifies an + encrypted object by the envelope's own hash on read (013 B-4 applies to + the ciphertext object, keyed under a `Raw` codec id derived as + `Hash::of(b"hqgit/v1/enc" || cid)` so the two ids never collide). +- **B-4 (`KeyProvider`).** `trait KeyProvider { fn key_for(&self, + namespace: &Hash) -> Result<Option<NamespaceKey>, Error>; }`. + `NamespaceKey` is 32 bytes, never `Debug`-printed, never serialized into + the ledger or any object. `NoKeys` (a provider that has none) makes every + encrypted payload resolve as `Missing` with a distinct + `Resolved::Missing` detail naming the namespace, so a replica without + the key is honest about what it cannot read. +- **B-5 (tombstone fact).** `FactKind "ledger.tombstone"`, `v = 1`, body + `{ target: Cid, reason: EraseReason, scope: TombstoneScope }` where + `EraseReason` is a closed enum `Erasure | Moderation | Legal` and + `TombstoneScope` is `Object` (the referenced content) or `Payload + (EntryHash)` (the payload of one entry). It is registered with the fact + registry (019 B-2) by `register_ledger(registry)`. `TombstoneSet` is a + `DerivedState` (019 B-5) folding tombstones into a `BTreeMap<Cid, + TombstoneRef>`; a later tombstone for the same target does not replace + the earlier one (the first erasure is the record). +- **B-6 (`EraseCapability` and `erase`).** `EraseCapability` (declared + sealed in 013 B-4) gains exactly two constructors: `EraseCapability:: + owner(repo_identity: &KeyId, namespace: &Hash)` (minted by the local + repository for its own identity, 021) and `EraseCapability::from_verdict + (attestation: &AttestationId)` (a policy verdict, 068; declared here, + its verification is that spec's). `erase(store, cid, cap) -> Result<Erased, + Error>` deletes the object bytes, the BAO outboard (015), and any + encrypted envelope for `cid`; `Erased { cid, sidecars_removed: u8 }`. The + tombstone fact MUST be appended before the bytes are deleted, so a crash + leaves a tombstone with content still present (which `resolve_payload` + reports as `Erased` and a repair sweep removes) and never content gone + with no tombstone. +- **B-7 (chain stays verifiable).** Spec 017's `verify` MUST pass + unchanged on a DAG whose payload objects were erased, because it hashes + and signs commitments only. A test erases every payload in a fixture DAG + and asserts a clean verify report. +- **B-8 (projections render erasure).** Any consumer folding facts (019 + B-5, 080) MUST treat an `Erased` payload as a fact with a null body and + `extra["erased"] = true` and MUST NOT fail; the CLI (033) renders + `[erased: <reason>]`. This rule is stated here and tested through 019's + fold. +- **B-9 (no ambient input).** Nonces are derived, not random; no clock; + keys come only from the `KeyProvider`. + +## 4. Functional requirements + +- **FR-001.** `encrypt.rs` is pure over key, id, and bytes; `commitment.rs` + and `tombstone.rs` perform I/O only through the `ObjectStore` and + `KeyProvider` traits. +- **FR-002.** Tests cover: the three `Resolved` outcomes; encrypt then + decrypt round trip; a wrong namespace key fails; associated data binds + the `Cid` (swapping ciphertexts between two ids fails); deterministic + nonce derivation vector; `put_encrypted` idempotency; the encrypted id + never collides with the plaintext id; tombstone first-wins; `erase` + removes object, outboard, and envelope; tombstone-before-delete + ordering under a simulated crash; verify passes after total erasure; the + fold tolerates erased payloads; `EraseCapability` cannot be constructed + outside its two constructors (a compile-fail test with `trybuild` or a + visibility test). +- **FR-003.** The AEAD primitive is the pinned `chacha20poly1305` crate + with its XChaCha variant and the HKDF is BLAKE3's `derive_key`; no other + cryptographic dependency is introduced. +- **FR-004.** Key material never appears in `Debug`, logs, or errors (a + formatting test asserts absence). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-ledger --locked erasure` passes. +- **AC-2.** `cargo test -p hqgit-object --locked` and `cargo test -p + hqgit-ledger --locked` pass in full. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Minting the owner capability at repository init (021); the policy verdict +that grants erasure (068); key distribution and rotation for namespace +keys (a wave 4 amendment to 060); serving or refusing erased content over +the network (094, 111); the repair sweep as an operator command (a later +CLI spec). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-ledger --locked erasure +cargo test -p hqgit-object --locked +``` diff --git a/specs/021-local-repository/spec.md b/specs/021-local-repository/spec.md new file mode 100644 index 0000000..ed1ce04 --- /dev/null +++ b/specs/021-local-repository/spec.md @@ -0,0 +1,181 @@ +--- +id: "021-local-repository" +title: "Local repository: the .hq layout, the persistent entry store, namespaces, and the append path" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "020-commitments-and-tombstones" +establishes: + - "crates/hqgit-ledger/src/repo.rs" + - "crates/hqgit-ledger/src/store.rs" + - "crates/hqgit-ledger/src/namespace.rs" + - "crates/hqgit-ledger/tests/repo.rs" +extends: + - { spec: "017-ledger-entry-dag", unit: "crates/hqgit-ledger/src/lib.rs", nature: additive } + - { spec: "017-ledger-entry-dag", unit: "crates/hqgit-ledger/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Where the ledger meets a disk. This spec fixes the .hq directory inside + a working tree (the redb-backed entry store, the object store, the local + identity, the config file), the EntryStore trait with atomic fsynced + appends, the Repository type with init, open, and the one append path + every fact takes (encode, put the payload object, build the entry on + the current heads, stamp the clock, sign, append), and namespaces as + facts with main and quarantine present from init. Write ordering is + chosen so a crash can leave an orphan object but never a dangling + entry. This is the substrate the CLI (032), the server (090), and the + sync protocol (110) all share, which is what makes offline-first true. +--- + +# 021: Local repository + +## 1. Purpose + +Thesis §5 and constitution XIII: the CLI and the server run the same +ledger implementation, so offline-first is a property rather than a +promise. Specs 017 through 020 defined the ledger in memory; this spec is +its persistent form and the single append path, closing wave 1's ledger +half so that spec 032's `hq init` and spec 033's offline review have a +repository to write to. Namespaces arrive here because the quarantine +namespace (constitution XV) must exist before anything untrusted can be +written, and because encrypted namespaces (020) need a place to be +declared. + +## 2. Territory + +`repo.rs` (the `Repository` type, init, open, the append path, capability +minting), `store.rs` (the `EntryStore` trait and its redb implementation), +`namespace.rs` (the namespace facts and view), and `tests/repo.rs`, in +the crate 017 founded. The `.hq/` layout fixed here is a contract every +later spec that touches the directory (031 `gitmap.redb`, 040 +`mirror.redb`, 080 `projections/`) extends rather than reshapes. + +## 3. Behavior + +- **B-1 (layout).** A repository is a directory containing `.hq/` with: + `ledger.redb` (this spec), `objects.redb` (013 B-6), `identity/seed` + (the local ed25519 seed, 32 bytes, created with mode `0600` on Unix), + `identity/public` (the public key, hex), and `config.toml` (`schema = + "1.0.0"`, `namespace = "<hex>"`, `node = "<hex>"`). `Repository:: + locate(start_dir) -> Option<PathBuf>` walks up to find `.hq/`, mirroring + git's discovery, and stops at a filesystem boundary. +- **B-2 (`EntryStore`).** `trait EntryStore { fn append(&self, entry: + &Entry) -> Result<EntryHash, Error>; fn get(&self, hash: &EntryHash) -> + Result<Option<Entry>, Error>; fn contains(&self, hash: &EntryHash) -> + Result<bool, Error>; fn heads(&self) -> Result<Vec<EntryHash>, Error>; + fn children(&self, hash: &EntryHash) -> Result<Vec<EntryHash>, Error>; + fn iter_all(&self) -> Result<Vec<EntryHash>, Error>; fn watermark(&self) + -> Result<Option<Hlc>, Error>; }`. `RedbEntryStore` holds tables + `entries: [u8; 32] -> canonical bytes`, `children: ([u8; 32], [u8; 32]) + -> ()`, `heads: [u8; 32] -> ()`, `meta: str -> bytes` (schema version, + genesis hash, watermark). `append` runs the 017 B-6 checks (parents + present, clock monotonic, duplicate is a no-op) inside one transaction + that updates `entries`, `children`, `heads`, and the watermark, then + commits durably (fsync). `iter_all` returns hashes sorted so callers can + build a `Dag` (017) deterministically. +- **B-3 (write ordering).** The append path (B-5) MUST put the payload + object (013) and its outboard (015) before appending the entry, so a + crash between the two leaves an orphan object and never an entry whose + payload is `Missing` by the repository's own fault. A `Repository::fsck()` + reports orphans and any entry whose payload is missing, without + repairing. +- **B-4 (`Repository::init`).** `init(path, identity: LocalIdentity) -> + Result<Repository, Error>` refuses an existing `.hq/` (`Error:: + Validation`), creates the layout, derives the node id (018 B-7), writes + the `RepoGenesis` object (013 B-2) with `namespace = Hash::of(b"hqgit/ + v1/namespace" || public_key || b"main")`, signs and appends the genesis + entry (017 B-5), then appends the two namespace facts of B-6. + `LocalIdentity::generate(rng: &mut dyn RngCore)` and `LocalIdentity:: + from_seed` are the constructors; the seed is the only secret on disk. +- **B-5 (`Repository::append_fact`).** `append_fact(&mut self, namespace: + &Hash, envelope: FactEnvelope) -> Result<EntryHash, Error>` validates the + envelope through the fact registry (019 B-2; unknown kinds pass), encodes + it (011), puts the payload object (encrypted through 020 B-3 when the + namespace is encrypted and the `KeyProvider` has its key, else + `Error::Crypto`), collects the current heads as sorted parents, stamps + `HlcGenerator::next(max parent hlc)` (018 B-5), signs with the local + identity (017 B-3), and appends (B-2). It returns the new entry hash and + it is the only public write path; `Dag` and `EntryStore` are not + reachable mutably from outside the crate. `Repository` also implements + 019's `FactSource` and exposes `dag() -> Dag` (built from `iter_all`) + and `total_order()` (018). +- **B-6 (namespaces).** `FactKind "ledger.namespace_declared"`, body `{ + id: Hash, name: String, encrypted: bool, quarantine: bool }`, and + `"ledger.namespace_retired"` `{ id }`; `NamespaceView` is a + `DerivedState` folding them into `BTreeMap<Hash, Namespace>` with + `name` an `LwwRegister`. `init` declares `main` (`quarantine = false`) + and `quarantine` (`quarantine = true`, id `Hash::of(b"hqgit/v1/ + namespace" || public_key || b"quarantine")`). A fact appended to the + quarantine namespace carries `extra["namespace"] = id` in its entry so + consumers (094) can partition without decoding payloads. Facts never + move between namespaces; promotion (094) is a new fact. +- **B-7 (capabilities).** `Repository::owner_erase_capability(&self, + namespace) -> EraseCapability` mints 020 B-6's owner capability for the + local identity; `Repository::erase(&mut self, cid, reason)` appends the + tombstone fact then calls 020's `erase` with that capability, in that + order (020 B-6). +- **B-8 (open and versions).** `open(path)` reads `config.toml`, refuses a + schema MAJOR it does not know (`Error::Schema`), opens both redb files, + and verifies that the stored genesis hash matches the genesis entry's + recomputed hash (`Error::Crypto` otherwise). Concurrent opens of one + repository are serialized by a lock file `.hq/lock` (advisory, held for + the lifetime of a mutable `Repository`). +- **B-9 (no ambient input).** The clock enters only through the + `HlcGenerator` the repository holds (018 B-4); randomness enters only + through the `RngCore` passed to `LocalIdentity::generate`. + +## 4. Functional requirements + +- **FR-001.** Every store operation is transactional and fsynced; a test + simulates a crash by dropping the store mid-sequence (write objects, + skip the entry) and asserts `fsck` reports an orphan and no dangling + entry. +- **FR-002.** Tests cover: init then open round trip with identity and + genesis verified; refusal to init over an existing `.hq/`; `locate` from + a nested directory and from outside; `append_fact` builds parents from + the current heads and advances the watermark; two repositories + initialized from one seed, appending concurrently, then synced by + copying entries through `EntryStore::append`, yield two heads and one + total order; namespace declaration and retirement folding; quarantine + facts tagged in `extra`; `erase` through the owner capability appends + the tombstone first; `open` refuses a bumped MAJOR and a genesis + mismatch; the lock file serializes a second mutable open. +- **FR-003.** `Repository` exposes no method that mutates an existing + entry or rewrites history; a test enumerates the public API and asserts + the only write methods are `append_fact`, `erase`, and `init`, plus + `ingest_entry` once spec 110 adds it (an append of an already-signed + foreign entry, never a rewrite). +- **FR-004.** The seed file is created with mode `0600` on Unix (asserted) + and never read by any function other than `LocalIdentity::load`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-ledger --locked repo` passes. +- **AC-2.** `cargo test -p hqgit-ledger --locked` passes in full. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The `hq` binary that calls `init` and `append_fact` (032); the git object +mapping stored beside the ledger (031); identity beyond the local seed and +key rotation (060); replication between repositories (110); projections +persisted under `.hq/projections/` (080); the server's multi-repository +layout (090). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-ledger --locked repo +cargo test -p hqgit-ledger --locked +``` diff --git a/specs/023-domain-fact-vocabulary/spec.md b/specs/023-domain-fact-vocabulary/spec.md new file mode 100644 index 0000000..9c30760 --- /dev/null +++ b/specs/023-domain-fact-vocabulary/spec.md @@ -0,0 +1,176 @@ +--- +id: "023-domain-fact-vocabulary" +title: "Domain fact vocabulary: the typed facts, their ids, and their validation" +status: approved +kind: "kernel" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "019-facts-and-derived-state" +establishes: + - "crates/hqgit-domain/Cargo.toml" + - "crates/hqgit-domain/src/lib.rs" + - "crates/hqgit-domain/src/facts.rs" + - "crates/hqgit-domain/src/validate.rs" + - "crates/hqgit-domain/src/ids.rs" + - "crates/hqgit-domain/tests/" +extends: + # The frozen fact-kind and id vectors join the golden corpus 011 established. + - { spec: "011-canonical-encoding", unit: "crates/hqgit-types/testdata/vectors/", nature: additive } +summary: > + Founds hqgit-domain, the L2 crate every collaboration noun lives in, and + fixes the fact vocabulary the ledger carries: one DomainFact variant per + kind (change, review, attestation, issue, ownership, mirror), each with a + frozen namespaced kind string, a versioned body schema encoded through + the spec 011 canonical codec, and unknown-field preservation. Ids are + never counters: a ChangeId, RevisionId, ThreadId, CommentId, IssueId, or + AttestationId is the BLAKE3 hash of the canonical bytes of the fact that + brought it into being, so an id is content-derived and identical on every + replica. Structural validation runs on decode, before any fold sees a + fact, and the crate registers every kind with spec 019's FactRegistry. + Later specs add variants and semantics by extending facts.rs; this spec + is the vocabulary, not the behavior. +--- + +# 023: Domain fact vocabulary + +## 1. Purpose + +Thesis §4.3 names the nouns (Change, Revision, Anchor, Review, +Attestation, Issue) and constitution VII says every one of them is carried +by immutable facts that merge by set union. Before any noun can have +behavior, the facts that describe it need a fixed kind string, a fixed body +shape, and a fixed way to mint an identity that two replicas agree on +without talking. This spec founds the domain crate to answer those three +questions once. Everything a later domain spec does is a fold over the +vocabulary fixed here, and the kind strings are frozen because they reach +hashed bytes (constitution VIII). + +## 2. Territory + +`crates/hqgit-domain` as founded here: the manifest (workspace deps +`hqgit-types`, `hqgit-object`, `hqgit-ledger`; no others), `lib.rs`, +`facts.rs` (the `DomainFact` enum and body structs), `validate.rs` +(structural validation and registry wiring), `ids.rs` (the id newtypes and +minting), and the `tests/` subtree. Additively: a `domain/` vector set in +spec 011's golden directory. Later specs in this crate (024 onward) add +modules and `extends` this spec's `lib.rs` and `facts.rs`. + +## 3. Behavior + +- **B-1 (the enum).** `DomainFact` is a closed enum with one variant per + kind. Each variant wraps a body struct that derives spec 011's + `Canonical`, carries `extra: BTreeMap<String, Value>` for unknown-field + preservation, and encodes as a `FactEnvelope { kind, v: 1, body }` (spec + 019). `DomainFact::kind(&self) -> &'static str` and `DomainFact::encode() + -> FactEnvelope`; `DomainFact::decode(&FactEnvelope) -> Result<DomainFact, + Error>` returns `Error::Schema` for an unknown `v` and `Error::Validation` + for a malformed body. An envelope whose kind is not in the table is not a + domain fact and is left to the registry's opaque path. +- **B-2 (frozen kinds).** The kind strings and required body fields: + + | kind | body | + |---|---| + | `change.opened` | `opened_by: Principal, title: String, nonce: Nonce` | + | `change.field_set` | `change: ChangeId, field: ChangeField, value: Value` | + | `change.revision_submitted` | `change: ChangeId, tree: Cid, base: Cid, parent_revision: Option<RevisionId>, message: String, submitted_by: Principal, nonce: Nonce` | + | `change.merged` | `change: ChangeId, revision: RevisionId` | + | `change.abandoned` | `change: ChangeId, reason: String` | + | `change.depends_on` | `change: ChangeId, on: ChangeId, at_revision: RevisionId` (semantics in 050) | + | `review.thread_opened` | `change: ChangeId, at_revision: RevisionId, anchor: Option<Value>, opened_by: Principal, nonce: Nonce` | + | `review.comment_posted` | `thread: ThreadId, author: Principal, body: Cid, reply_to: Option<CommentId>, nonce: Nonce` | + | `review.thread_resolved` | `thread: ThreadId, resolved: bool` | + | `review.approval_issued` | `change: ChangeId, revision: RevisionId, attestation: AttestationId` | + | `attestation.issued` | `attestation: Cid, subject: Hash, predicate: String, issuer: Principal` | + | `issue.opened` | `opened_by: Principal, title: String, nonce: Nonce` | + | `issue.field_set` | `issue: IssueId, field: IssueField, value: Value` | + | `issue.closed` | `issue: IssueId, reason: String` | + | `issue.link_added` | `issue: IssueId, target: LinkTarget` | + | `ownership.declared` | body fixed by 104 (reserved here as `Value`) | + | `ownership.revoked` | body fixed by 104 (reserved here as `Value`) | + | `mirror.observed` | `source: String, external_id: String, kind: String, payload: Cid` (semantics in 040) | + + `ChangeField` is `Title | Description`; `IssueField` is `Title | State | + Assignee | Milestone | LabelAdd | LabelRemove`; `LinkTarget` is + `Change(ChangeId) | Issue(IssueId)`. The `anchor` and issue `value` + fields are opaque canonical `Value`s here; specs 025 and 028 give them + types. A reserved body (`ownership.*`) decodes as an opaque `Value` until + its owning spec extends this file. +- **B-3 (ids are content-derived).** `ids.rs` defines newtypes over `Hash`: + `ChangeId`, `RevisionId`, `ThreadId`, `CommentId`, `IssueId`, + `AttestationId`, plus `Nonce([u8; 16])`. For every opening fact + (`change.opened`, `change.revision_submitted`, `review.thread_opened`, + `review.comment_posted`, `issue.opened`) the id of the thing it opens is + `Hash::of(canonical bytes of the fact body)`, exposed as + `DomainFact::minted_id() -> Option<Hash>`. The `nonce` is supplied by the + caller (the CLI draws it from OS randomness) so two otherwise identical + openings mint distinct ids; the domain crate never reads randomness. + `AttestationId` is minted by spec 027 from the attestation's own bytes. + `Display` for every id is 64 lowercase hex; `short()` is the first 12. +- **B-4 (validation on decode).** `validate::check(&DomainFact) -> + Result<(), Error>` runs after decode and before any fold: required + strings non-empty and free of control characters, `title` at most 512 + bytes and `reason` and `message` at most 64 KiB, every `Cid` codec + consistent with its role (`body` and `payload` are `Raw` or `DagCbor`, + `attestation` and `tree` and `base` are `DagCbor`), every referenced id + 32 bytes, `extra` keys not colliding with known keys. A failure is + `Error::Validation` naming the kind and the field. +- **B-5 (registry wiring).** `register_domain(registry: &mut + FactRegistry)` registers every kind of B-2 with a validator that decodes + and checks; a kind registered twice is `Error::Config`. Unknown kinds are + never registered here, so they stay opaque in a fold (spec 019). +- **B-6 (fold contract).** Domain views (024 onward) implement spec 019's + `DerivedState` and receive `(fact, at: &Hlc)`. A body that needs to know + who acted carries a `Principal` field; the relation between that + principal and the entry's issuer key is verified by the trust plane (060, + 064), never assumed by the domain. +- **B-7 (no ambient input).** Nothing in this crate reads a clock, the + environment, randomness, or a `HashMap`; spec 010 FR-003's source guard + applies to this crate. + +## 4. Functional requirements + +- **FR-001.** Every variant round-trips `encode` then `decode` to an equal + value, with `extra` preserved and included in the canonical bytes. +- **FR-002.** The golden set `domain/fact-kinds.json` lists every kind + string and its `v`; a test asserts the enum matches the file exactly, so + adding or renaming a kind is a visible vector change (constitution VIII). +- **FR-003.** The golden set `domain/ids.json` records, for one fixture of + each opening fact, the canonical body bytes and the minted id; a test + re-derives both. +- **FR-004.** Validation tests cover each rule of B-4 with one failing + fixture per rule and assert the error names the field. +- **FR-005.** `register_domain` on an empty registry registers exactly the + B-2 kinds; a second call fails with `Error::Config`. +- **FR-006.** The crate depends on `hqgit-types`, `hqgit-object`, and + `hqgit-ledger` only within the workspace, and its manifest carries + `[package.metadata.spec-spine] spec = "023-domain-fact-vocabulary"`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked` passes, vectors included. +- **AC-2.** `spec-spine index` discovers `hqgit-domain` bound to this spec + and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Change and revision behavior (024); anchors (025); threads and approvals +(026); the attestation object and predicates (027); issue folds (028); +stack semantics of `change.depends_on` (050); ownership bodies (104); +mirror semantics (040). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked +cargo test -p hqgit-types --locked golden +``` diff --git a/specs/024-change-and-revision/spec.md b/specs/024-change-and-revision/spec.md new file mode 100644 index 0000000..a80ad04 --- /dev/null +++ b/specs/024-change-and-revision/spec.md @@ -0,0 +1,145 @@ +--- +id: "024-change-and-revision" +title: "Change and Revision: stable change identity over an ordered revision sequence" +status: approved +kind: "kernel" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "023-domain-fact-vocabulary" +establishes: + - "crates/hqgit-domain/src/change.rs" + - "crates/hqgit-domain/src/revision.rs" + - "crates/hqgit-domain/tests/change.rs" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + The unit of change is a Change with stable identity and an ordered + sequence of Revisions, never a mutable branch pointer (design §1.1 point + 2; Gerrit's Change-Id, then jj and Sapling). A revision is a tree hash + plus its base; a force-push is a new revision and destroys nothing; the + revision number is derived from the ledger's total order so every + replica numbers identically. This spec adds the Change and Revision + types, the pure fact builders the CLI and the server call, and the + ChangeView fold with its state machine (Open, Merged, Abandoned) and + lineage queries. What changed since a reviewer last looked becomes a + query in spec 050; this spec makes the sequence it runs over exist. +--- + +# 024: Change and Revision + +## 1. Purpose + +Thesis §4.3: "`Change` (stable identity) with ordered `Revision`s, each a +tree hash plus base". GitHub's pull request is a branch name whose history +is rewritten under the reviewer; hqgit's change is an identity that +accumulates revisions. This spec gives the domain that noun, as facts (023) +and a fold (019), so review threads (026), approvals (027), stacks (050), +and semantic deltas (051) have a stable thing to hang off. + +## 2. Territory + +`change.rs` (the `Change` type, `ChangeState`, the `ChangeView` fold, the +builders) and `revision.rs` (the `Revision` type and lineage queries) in +`crates/hqgit-domain`, plus `tests/change.rs`. Additively: the crate's +`lib.rs` re-exports and the `change.field_set` decoder in `facts.rs` +gaining typed values for `ChangeField`. + +## 3. Behavior + +- **B-1 (`Change`).** `Change { id: ChangeId, opened_by: Principal, title: + LwwRegister<String>, description: LwwRegister<String>, state: + ChangeState, revisions: Vec<RevisionId>, merged_revision: + Option<RevisionId>, opened_at: Hlc, warnings: Vec<ChangeWarning> }`. + `ChangeState` is `Open | Merged | Abandoned`. `title` and `description` + are LWW registers (spec 019) fed by `change.field_set`; the opening title + is the register's initial value at the opening fact's `Hlc`. +- **B-2 (`Revision`).** `Revision { id: RevisionId, change: ChangeId, + number: u32, tree: Cid, base: Cid, parent_revision: Option<RevisionId>, + message: String, submitted_by: Principal, at: Hlc, post_terminal: bool }`. + `tree` and `base` are spec 013 `Tree` cids; `base` is the tree the change + was made against (for a git-backed repo, the base commit's tree via 031). + `number` is 1-based and derived: the position of the revision among the + change's revisions in total order (018), never carried in a fact, so + concurrent submissions on two replicas receive the same numbers once + synced. +- **B-3 (builders are pure).** `open_change(opened_by, title, nonce) -> + (ChangeId, DomainFact)`, `set_field(change, field, value) -> + DomainFact`, `submit_revision(view, change, tree, base, parent_revision, + message, submitted_by, nonce) -> Result<(RevisionId, DomainFact), + Error>`, `abandon(view, change, reason) -> Result<DomainFact, Error>`, + `mark_merged(view, change, revision) -> Result<DomainFact, Error>`. They + return facts for the caller to append (021); they never touch a store. + The write-path checks: `submit_revision` and `abandon` refuse a change + that is not `Open` (`Error::Validation`); `submit_revision` refuses a + `parent_revision` that is not a revision of the same change; + `mark_merged` refuses a revision that is not the change's. +- **B-4 (the fold accepts everything).** `ChangeView` implements + `DerivedState`. Facts are immutable and never rejected by a fold + (constitution VII): a `change.revision_submitted` arriving after + `change.abandoned` in total order is recorded with `post_terminal: true` + and a `ChangeWarning::RevisionAfterTerminal`; a `parent_revision` the + view has not seen yields `ChangeWarning::DanglingParent`; a second + `change.merged` or `change.abandoned` on a terminal change yields + `ChangeWarning::DuplicateTerminal` and the first (in total order) wins. + Warnings are part of the view so a UI can show them; they are never + silently dropped. +- **B-5 (state machine).** `Open` moves to `Merged` on `change.merged` and + to `Abandoned` on `change.abandoned`; both are terminal. There is no + reopen: a new change is opened instead, and a `change.field_set` on a + terminal change still updates the register (metadata edits on closed + work are ordinary). +- **B-6 (queries).** `ChangeView::get(&ChangeId) -> Option<&Change>`, + `changes() -> impl Iterator<Item = &Change>` in id order, `revision(& + RevisionId) -> Option<&Revision>`, `latest_revision(&ChangeId) -> + Option<&Revision>`, `revision_lineage(&RevisionId) -> Vec<RevisionId>` + (following `parent_revision` to the root, newest first, cycle-safe by + visited set), `revisions_of(&ChangeId) -> &[RevisionId]` in number order. +- **B-7 (determinism).** The view is a pure function of the ordered fact + sequence; feeding the same facts in any order that respects 018's total + order yields an equal view, and feeding them in a different total order + is not a supported input (the runner, 080, guarantees the order). + +## 4. Functional requirements + +- **FR-001.** Tests: opening mints a stable id across two builds from the + same inputs; revision numbers are monotonic and match total-order + position under three interleavings of two submitters; `post_terminal` + and each `ChangeWarning` variant; every state transition and every + refused transition in B-3 and B-5; `revision_lineage` on a chain of four + and on a dangling parent; `latest_revision` after concurrent submissions. +- **FR-002.** A property test (`proptest`) folds a random valid fact + sequence through two views built from two permutations that agree on + total order and asserts equality. +- **FR-003.** No builder or view function performs I/O; the only inputs are + facts, ids, and values. +- **FR-004.** `lib.rs` re-exports `Change`, `ChangeState`, `ChangeView`, + `Revision`, and the builders; nothing else in the crate changes. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked change` passes. +- **AC-2.** A fixture sequence (open, two submits, field set, merge, late + submit) folds to the golden view recorded in `tests/change.rs`, warnings + included. + +## 6. Out of scope + +Working-tree snapshots and git bases (031, 033); stacks and interdiff +(050); merge decisions (068, 076); rendering (081, 095). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked change +``` diff --git a/specs/025-semantic-anchors/spec.md b/specs/025-semantic-anchors/spec.md new file mode 100644 index 0000000..da220f6 --- /dev/null +++ b/specs/025-semantic-anchors/spec.md @@ -0,0 +1,154 @@ +--- +id: "025-semantic-anchors" +title: "Semantic anchors: comments that survive rebase by resolving against content" +status: approved +kind: "kernel" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "024-change-and-revision" +establishes: + - "crates/hqgit-domain/src/anchor.rs" + - "crates/hqgit-domain/src/syntax/mod.rs" + - "crates/hqgit-domain/src/syntax/rust.rs" + - "crates/hqgit-domain/src/syntax/typescript.rs" + - "crates/hqgit-domain/src/syntax/text.rs" + - "crates/hqgit-domain/tests/anchor.rs" + - "crates/hqgit-domain/testdata/anchors/" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Review is anchored to text in every incumbent, which is why a rebase + orphans every comment. hqgit anchors a location to a syntax node: the + path, the named-node path from the root, and the BLAKE3 hash of the + node's content, with a text position as the fallback of last resort. + Resolution against a new tree answers Exact, Moved (the same content + elsewhere, rename-tolerant), Text (parsing failed or the language is + unknown), or Lost, never a silent guess. Rust and TypeScript are the + first languages, with tree-sitter grammars pinned exact so spans are + identical across releases; everything else degrades to the text path, + visibly. This is the primitive spec 026's threads and spec 051's deltas + build on. +--- + +# 025: Semantic anchors + +## 1. Purpose + +Design §1.1 point 2: comments anchor to semantic locations (AST node plus +content hash) so they survive rebase. Thesis §4.3 fixes the shape +`Anchor { path, tree_sitter_node_path, node_content_hash }` with text +position as the fallback only when resolution fails. This spec implements +that shape and the resolution algorithm, and it is honest about the long +tail (thesis §8): a language without a grammar gets the text path and says +so. + +## 2. Territory + +`anchor.rs` (the `Anchor` type, `anchor_at`, `resolve`, `Resolution`), +the `syntax/` module (language detection, parsing through tree-sitter for +Rust and TypeScript, and the text degrade path), `tests/anchor.rs`, and +the fixture subtree `testdata/anchors/`. Additively: `lib.rs` re-exports, +the crate manifest, and the workspace dependency table (tree-sitter, +tree-sitter-rust, tree-sitter-typescript, each pinned to an exact version). + +## 3. Behavior + +- **B-1 (`Anchor`).** `Anchor { path: String, node_path: Vec<NodeStep>, + node_content_hash: Hash, fallback: TextPosition, extra: BTreeMap<String, + Value> }` with `NodeStep { kind: String, index: u16 }` (the node's + tree-sitter kind name and its index among the parent's named children) + and `TextPosition { line: u32, column: u32, context_hash: Hash }` (1-based + line, 0-based UTF-8 byte column, BLAKE3 of the line's bytes with trailing + whitespace removed). `path` is repo-relative POSIX. The type derives + `Canonical` and provides `to_value()` and `from_value(&Value)` for the + opaque `anchor` field spec 023 reserved on `review.thread_opened`. +- **B-2 (languages).** `syntax::detect(path) -> Language` maps `.rs` to + `Rust`, `.ts`, `.tsx`, `.mts`, `.cts` to `TypeScript`, and everything + else to `Text`. `syntax::parse(language, bytes) -> Result<Tree, Error>` + wraps tree-sitter for the two grammars and returns a line-indexed + `Tree::Text` for `Text`; a grammar parse that produces an error node + still returns a tree (tree-sitter is error-tolerant), and `Tree::has_errors()` + reports it. Grammar crates are pinned exact in `[workspace.dependencies]` + because a grammar upgrade can move node kinds and spans; bumping one is + an authoring change to this spec. +- **B-3 (`anchor_at`).** `anchor_at(tree: &Tree, path: &str, range: + ByteRange) -> Anchor` selects the smallest named node whose byte range + covers `range`, records its `node_path` from the root (root excluded), + hashes its exact source bytes into `node_content_hash`, and computes the + fallback from the range start. On `Tree::Text` the `node_path` is empty + and `node_content_hash` equals `fallback.context_hash`. +- **B-4 (`resolve`).** `resolve(anchor: &Anchor, tree: &Tree) -> + Resolution` where `Resolution` is `Exact(ByteRange) | Moved { range: + ByteRange, confidence: Confidence } | Text(ByteRange) | Lost` and + `Confidence` is `High | Low`. The algorithm, in order: + 1. Walk `node_path`; if a node is found and its content hash equals + `node_content_hash`, `Exact`. + 2. Otherwise collect every named node of the last step's `kind` whose + content hash matches. Exactly one: `Moved` with `High`. Several: the + one whose `node_path` has the smallest edit distance to the anchor's, + `Moved` with `Low`; ties break on the earliest byte offset. + 3. Otherwise find lines whose trimmed hash equals + `fallback.context_hash`, nearest to `fallback.line` first: `Text`. + 4. Otherwise `Lost`. + On `Tree::Text` the algorithm starts at step 3. `resolve` is a pure + function of `(anchor, tree)` and is deterministic. +- **B-5 (survival guarantees).** An anchor MUST resolve `Exact` after any + edit outside the anchored node's bytes (insertions above, sibling + reorders, whitespace changes elsewhere), and MUST resolve `Moved` after + the anchored node is moved within the file or its parent is renamed, + provided its own bytes are unchanged. An edit inside the node degrades + to `Text` when the anchored line survives and `Lost` otherwise; this is + the documented limit, not a defect. +- **B-6 (ranges).** `ByteRange { start: u64, end: u64 }` in bytes of the + file at the tree's revision; a `line_range(tree, range) -> (u32, u32)` + helper converts for display. No floats anywhere. + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/anchors/<lang>/<case>/{before, + after}.<ext>` with a `case.json` naming the anchored range and the + expected `Resolution` after the edit: rename above, insert above, body + edit below, node moved, node edited (Text), node deleted (Lost), for + both Rust and TypeScript; an unknown extension exercising the text path. +- **FR-002.** A test pins the exact versions of the three tree-sitter + crates and fails if `Cargo.lock` disagrees, so a grammar bump is a + visible change to this spec's territory. +- **FR-003.** `Anchor` round-trips through `to_value` and `from_value` with + `extra` preserved; a golden `domain/anchor.json` vector records the + canonical bytes of one anchor (added to the 011 vector directory through + spec 023's edge). +- **FR-004.** A property test asserts `resolve(anchor_at(tree, p, r), + tree)` is `Exact` for every named node of a fixture tree. +- **FR-005.** Parsing never panics on arbitrary bytes; a fuzz-style test + feeds random byte strings through `parse` for each language. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked anchor` passes with every + fixture in FR-001. +- **AC-2.** The rename-above fixtures for both languages resolve `Exact` + and the moved-node fixtures resolve `Moved` with `High`. + +## 6. Out of scope + +Threads and re-anchoring policy (026); public-API extraction (051); more +languages (a later spec per language, each pinning its grammar); symbol +resolution across files (083). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked anchor +``` diff --git a/specs/026-review-threads/spec.md b/specs/026-review-threads/spec.md new file mode 100644 index 0000000..a8d69ba --- /dev/null +++ b/specs/026-review-threads/spec.md @@ -0,0 +1,137 @@ +--- +id: "026-review-threads" +title: "Review threads: anchored comments, resolution state, and approvals as attestations" +status: approved +kind: "feature" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 1 +depends_on: + - "025-semantic-anchors" +establishes: + - "crates/hqgit-domain/src/review.rs" + - "crates/hqgit-domain/tests/review.rs" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + The review conversation as facts: a thread is opened on a change at a + revision, optionally anchored (025), comments are posted with their + bodies in the object store so they are erasable (constitution X), and + resolution is a last-writer-wins register (constitution VII). Threads are + re-anchored against every new revision, and a reviewer sees "moved" and + "lost" rather than an orphaned comment. Approval is not a comment: it is + a fact that references an Attestation (027) with the approval predicate, + so "who approved which revision" is a signed, verifiable claim and never + prose. This spec adds the ThreadView and ApprovalView folds and the pure + builders the CLI (033) and the server (093) call. +--- + +# 026: Review threads + +## 1. Purpose + +Design §1.1 point 1: clone the repo, get the argument that produced it. +The argument is the review conversation, and in every incumbent it is rows +in someone else's database, anchored to lines that a rebase invalidates. +This spec makes the conversation facts in the ledger, anchored to syntax +(025), with bodies that can be erased without rewriting history and with +approvals that are attestations rather than text (constitution IX). + +## 2. Territory + +`review.rs` in `crates/hqgit-domain`: the `Thread`, `Comment`, and +`Approval` types, the `ThreadView` and `ApprovalView` folds, the +re-anchoring function, and the builders; plus `tests/review.rs`. +Additively: `lib.rs` re-exports and the typed decoding of the `anchor` +field in `facts.rs` (from the opaque `Value` spec 023 reserved to spec +025's `Anchor`). + +## 3. Behavior + +- **B-1 (`Thread`).** `Thread { id: ThreadId, change: ChangeId, anchor: + Option<Anchor>, opened_at_revision: RevisionId, opened_by: Principal, + opened_at: Hlc, resolved: LwwRegister<bool>, comments: Vec<CommentId>, + warnings: Vec<ReviewWarning> }`. A thread with `anchor: None` is a + change-level thread. `comments` is in total order (018). +- **B-2 (`Comment`).** `Comment { id: CommentId, thread: ThreadId, author: + Principal, body: Cid, reply_to: Option<CommentId>, at: Hlc }`. `body` is + a `Raw` object holding UTF-8 markdown; the fact carries only the + commitment (constitution X). `comment_body(comment, store) -> + Result<Resolved, Error>` returns spec 020's `Resolved::{Present(bytes), + Erased(TombstoneRef), Missing}`; a renderer MUST show `Erased` as erased + and never fail on it. +- **B-3 (resolution).** `review.thread_resolved` sets the `resolved` + register at the fact's `Hlc`; concurrent resolve and unresolve converge + by the register's rule. A comment posted on a resolved thread reopens + nothing by itself (a fact is a fact) but is recorded with + `ReviewWarning::CommentOnResolvedThread` so a UI can offer to reopen. +- **B-4 (re-anchoring).** `reanchor(thread: &Thread, tree: &Tree) -> + Option<Resolution>` resolves the thread's anchor (025) against the tree + of a revision; `None` for an unanchored thread. This is a pure + content-derived computation, not fact-derived, so it lives outside the + fold: the CLI (033) computes it on demand and the views projection (081) + stores it per `(thread, revision)`. A `Lost` resolution never deletes a + thread; the thread stays with its last resolvable revision named. +- **B-5 (`Approval`).** `Approval { attestation: AttestationId, change: + ChangeId, revision: RevisionId, approver: Principal, at: Hlc }` folded + from `review.approval_issued`. `ApprovalView::approvals_for(change) -> + Vec<Approval>` and `approvals_for_revision(change, revision)`. An + approval of a revision that is not the latest is `stale` in + `ApprovalStatus { Current | Stale { latest: RevisionId } }`; staleness + is a query over the ChangeView (024), never a fact. The fold records an + approval whose `revision` the ChangeView does not know with + `ReviewWarning::ApprovalForUnknownRevision`. Whether the referenced + attestation exists, verifies, and carries the approval predicate is the + trust plane's question (064); the view stores the reference. +- **B-6 (builders).** `open_thread(change, at_revision, anchor, + opened_by, nonce) -> (ThreadId, DomainFact)`, `post_comment(thread, + author, body_cid, reply_to, nonce) -> (CommentId, DomainFact)`, + `set_resolved(thread, resolved) -> DomainFact`, `issue_approval(change, + revision, attestation) -> DomainFact`. Pure; the caller stores the + comment body object (013) before appending the fact, and refuses (in the + CLI) a `reply_to` outside the thread; the fold records such a reply with + `ReviewWarning::ReplyOutsideThread` rather than dropping it. +- **B-7 (queries).** `ThreadView::threads_for(change) -> Vec<&Thread>` in + opening order, `thread(id)`, `comment(id)`, `unresolved_count(change)`. + +## 4. Functional requirements + +- **FR-001.** Tests: open, comment, reply, resolve, unresolve, and the + register's convergence under two orders; every `ReviewWarning` variant; + `comment_body` on present, erased (after a spec 020 tombstone), and + missing objects; `reanchor` on the 025 fixtures giving `Exact`, `Moved`, + `Text`, and `Lost`; approval status `Current` then `Stale` after a new + revision; approval for an unknown revision. +- **FR-002.** A property test folds a random thread history through two + permutations agreeing on total order and asserts equal views. +- **FR-003.** No function in `review.rs` reads a clock or performs I/O + beyond the `ObjectStore` seam used by `comment_body`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked review` passes. +- **AC-2.** A fixture where a thread is opened on a Rust function, the + function is moved by the next revision, and the comment body is then + erased folds to: one thread, resolution `Moved` at revision 2, body + `Erased`, and zero failures. + +## 6. Out of scope + +The attestation object itself and its verification (027, 064); rendering +and re-anchor storage (081, 095); suggested edits and batch review +submission (a later feature spec); notification (085). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked review +``` diff --git a/specs/027-attestation-primitive/spec.md b/specs/027-attestation-primitive/spec.md new file mode 100644 index 0000000..5eacb85 --- /dev/null +++ b/specs/027-attestation-primitive/spec.md @@ -0,0 +1,169 @@ +--- +id: "027-attestation-primitive" +title: "The attestation primitive: one signed evidence shape, a predicate registry, in-toto interop" +status: approved +kind: "kernel" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 1 +depends_on: + - "023-domain-fact-vocabulary" +establishes: + - "crates/hqgit-domain/src/attestation.rs" + - "crates/hqgit-domain/src/predicate.rs" + - "crates/hqgit-domain/tests/attestation.rs" + - "crates/hqgit-domain/testdata/attestations/" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } + # The frozen attestation vectors join the golden corpus 011 established. + - { spec: "011-canonical-encoding", unit: "crates/hqgit-types/testdata/vectors/", nature: additive } +summary: > + The single most valuable simplification in the design (thesis §4.3, + constitution IX): every form of evidence is one primitive, Attestation + { subject, predicate, issuer, claim, sig }. Human approval, build + provenance, test result, static finding, license scan, policy + evaluation, mirrored external state, semantic delta, and agent action + are the same shape, stored the same way, verified the same way, and fed + to policy the same way. This spec fixes the attestation's canonical + bytes, signing preimage, and id (frozen with vectors), the predicate + registry with claim validators for the built-in predicates and reserved + slots later specs fill, signature verification, and a lossless mapping + to in-toto Statement v1 so SLSA tooling reads hqgit evidence. Requests to + special-case a predicate are refused by construction: there is no other + noun. +--- + +# 027: The attestation primitive + +## 1. Purpose + +Design §1.1 point 5 says trust is decorative because approvals are UI +state and attribution is an email string. The fix is a single evidence +object with a signature, a typed claim, and a subject hash, generalizing +in-toto's statement model. Because attestations are what policy (065) +evaluates, what the action cache (071) trusts, and what the transparency +log (062) records, their byte layout is frozen here at wave 1 alongside +the ledger entry (017), and every later evidence kind registers a +predicate instead of inventing a shape. + +## 2. Territory + +`attestation.rs` (the type, canonical bytes, signing, id, signature +verification, the in-toto mapping), `predicate.rs` (`PredicateType`, the +built-in constants, the `PredicateRegistry` and `ClaimValidator` seam, +the built-in claim schemas), `tests/attestation.rs`, and in-toto fixtures +under `testdata/attestations/`. Additively: `lib.rs` re-exports, the +`attestation.issued` decoder in `facts.rs`, and an `attestation/` vector +set in spec 011's golden directory. Specs 051, 067, 074, and 102 extend +`predicate.rs` with their claim schemas. + +## 3. Behavior + +- **B-1 (shape).** `Attestation { subject: Hash, predicate: PredicateType, + issuer: Principal, issuer_key: KeyId, claim: Cid, at: Hlc, sig: + Signature, extra: BTreeMap<String, Value> }`. `subject` is any BLAKE3 + hash (a revision id, a tree cid's hash, an entry hash, an artifact + digest); `claim` is the cid of a `DagCbor` object holding the predicate's + claim; `extra` is preserved and hashed (011). A signature bundle from + keyless signing (063) rides in `extra` under the key `bundle`. +- **B-2 (canonical bytes and id).** The canonical bytes are the spec 011 + encoding of the map with keys `at`, `claim`, `issuer`, `issuer_key`, + `predicate`, `sig`, `subject`, with `extra` flattened (collision is + `Error::Validation`). The signing preimage is the canonical bytes with + `sig` absent under `SignDomain("attestation")` (010 B-7). `AttestationId + = Hash::of(canonical bytes with sig)`; the attestation is stored as a + `DagCbor` object (013) whose cid hash equals its id, and the + `attestation.issued` fact (023) references that cid plus `subject`, + `predicate`, and `issuer` as indexable copies. +- **B-3 (issuance).** `Attestation::issue(unsigned: UnsignedAttestation, + signer: &impl Signer) -> Attestation` sets `issuer_key` from the signer + and signs; there is no other constructor of a signed value. + `issue_fact(att: &Attestation, cid: Cid) -> DomainFact` builds the + `attestation.issued` fact for the caller to append after storing the + object. +- **B-4 (predicates).** `PredicateType(String)` MUST match + `^[a-z0-9-]+(/[a-z0-9-]+)*/v[0-9]+$`. Constants: `hqgit/approval/v1`, + `hqgit/provenance/v1`, `hqgit/test-result/v1`, `hqgit/static-finding/v1`, + `hqgit/license/v1`, `hqgit/policy-eval/v1`, `hqgit/agent-action/v1`, + `hqgit/mirror/v1`, `hqgit/semantic-delta/v1`. Claim schemas fixed here: + approval `{ revision: RevisionId, verdict: Approve | RequestChanges, + comment: Option<String> }`; test-result `{ suite: String, passed: u32, + failed: u32, skipped: u32, log: Option<Cid> }`; static-finding `{ tool: + String, findings: Vec<{ rule, severity: Info | Low | Medium | High | + Critical, path, anchor: Option<Value> }> }`; license `{ packages: Vec<{ + name, version, license }>, verdict: Allow | Deny(String) }`; mirror `{ + source, external_id, kind, observed_by: Principal }`. Reserved (accepted + as opaque until the named spec extends this file): provenance (074), + policy-eval (067), agent-action (102), semantic-delta (051). +- **B-5 (registry).** `PredicateRegistry { validators: BTreeMap< + PredicateType, Box<dyn ClaimValidator>> }` with `trait ClaimValidator { + fn validate(&self, claim: &Value) -> Result<(), Error> }`; + `register_builtin(&mut registry)` installs B-4's schemas; an unknown + predicate is preserved, signature-verified, and reported as + `ClaimVerdict::Unregistered`, never rejected (new evidence kinds must be + able to flow before every replica upgrades). +- **B-6 (verification here).** `verify_signature(att: &Attestation, + verifier: &impl Verifier) -> Result<(), Error>` recomputes the preimage + and checks `sig`; `verify_claim(att, claim: &Value, registry) -> + ClaimVerdict { Valid | Invalid(String) | Unregistered }`. Identity, + rotation validity, keyless bundles, and transparency inclusion are the + trust plane's chain (064); this spec verifies bytes against a key it is + handed. +- **B-7 (in-toto interop).** `to_in_toto(&self, claim: &Value) -> + InTotoStatement` produces `{ _type: "https://in-toto.io/Statement/v1", + subject: [{ name: hex, digest: { blake3: hex } }], predicateType: + IN_TOTO_PREDICATE_BASE + predicate, predicate: claim as JSON }` with + `IN_TOTO_PREDICATE_BASE = "https://schemas.hqgit.dev/"` (a namespace + constant, not a live URL). `from_in_toto(statement, issuer, signer) -> + Result<UnsignedAttestation, Error>` accepts a statement whose subject + carries a `blake3` digest and rejects one that does not + (`Error::Validation`); any other digests are kept in `extra` under + `digests`. The JSON rendering of the claim uses sorted keys. +- **B-8 (vectors).** `attestation/approval.json`, `attestation/with-extra.json`, + and `attestation/in-toto.json` record unsigned fields, seed, canonical + bytes, signature, id, and (for the third) the in-toto JSON. Frozen + (constitution VIII). + +## 4. Functional requirements + +- **FR-001.** Tests: canonical bytes against vectors; issue then verify; + a tampered byte fails; id covers the signature; every built-in claim + schema accepts its fixture and rejects a malformed one; unregistered + predicate yields `Unregistered` with the signature still checked; + `extra` preserved and hashed; in-toto round trip on the fixture set, + including a rejected sha256-only statement; regex acceptance and + rejection cases for `PredicateType`. +- **FR-002.** A property test asserts encode and decode are inverse for + random valid attestations and that two encodes agree. +- **FR-003.** `attestation.rs` and `predicate.rs` read no clock and + perform no I/O; the claim object is passed in, never fetched. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked attestation` passes, + vectors included. +- **AC-2.** The in-toto fixture produced by `to_in_toto` validates against + the in-toto Statement v1 JSON schema checked into + `testdata/attestations/`. + +## 6. Out of scope + +Identity, rotation, keyless bundles, transparency inclusion, and the +end-to-end verification chain (060 to 064); the claim schemas of the +reserved predicates (051, 067, 074, 102); the CLI (034). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked attestation +cargo test -p hqgit-types --locked golden +``` diff --git a/specs/028-issues-and-derived-state/spec.md b/specs/028-issues-and-derived-state/spec.md new file mode 100644 index 0000000..4a84079 --- /dev/null +++ b/specs/028-issues-and-derived-state/spec.md @@ -0,0 +1,131 @@ +--- +id: "028-issues-and-derived-state" +title: "Issues as facts with converging derived state: registers and an add-wins label set" +status: approved +kind: "feature" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 1 +depends_on: + - "023-domain-fact-vocabulary" +establishes: + - "crates/hqgit-domain/src/issue.rs" + - "crates/hqgit-domain/tests/issue.rs" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + The worked example of constitution VII. An issue is a sequence of + immutable facts (opened, field set, closed, link added) and its visible + state is derived: title, state, assignee, and milestone are last-writer- + wins registers over the hybrid logical clock, and labels are an add-wins + observed-remove set whose removals name the additions they observed. + Nothing is a counter, nothing is a row, and the IssueView rebuilds from + zero on every replica to the same answer. This is the shape every later + mutable noun copies, and it is what makes the GitHub mirror (040) a + fact importer rather than a schema migration. +--- + +# 028: Issues and derived state + +## 1. Purpose + +Thesis §4.2: only derived state needs convergence, and a hybrid logical +clock with last-writer-wins is sufficient for nearly all of it. Issues are +the noun with the most mutable fields, so they are the proving ground: +if issues fold cleanly from facts with registers and one small set CRDT, +the CRDT surface stays near five percent of the domain (constitution VII) +and product pressure to reach for a sequence CRDT is refused with a +working counterexample. + +## 2. Territory + +`issue.rs` in `crates/hqgit-domain`: the `Issue` type, `IssueState`, the +typed `IssueField` values, the `LabelSet`, the `IssueView` fold, and the +builders; plus `tests/issue.rs`. Additively: `lib.rs` re-exports and the +typed decoding of `issue.field_set` values in `facts.rs` (from the opaque +`Value` spec 023 reserved). + +## 3. Behavior + +- **B-1 (`Issue`).** `Issue { id: IssueId, opened_by: Principal, opened_at: + Hlc, title: LwwRegister<String>, state: LwwRegister<IssueState>, + assignee: LwwRegister<Option<Principal>>, milestone: + LwwRegister<Option<String>>, labels: LabelSet, links: + BTreeSet<LinkTarget>, threads: Vec<ThreadId>, warnings: + Vec<IssueWarning> }`. `IssueState` is `Open | Closed`. +- **B-2 (typed fields).** `issue.field_set` values decode per field: + `Title(String)`, `State(IssueState)`, `Assignee(Option<Principal>)`, + `Milestone(Option<String>)`, `LabelAdd(String)`, `LabelRemove { label: + String, observed: Vec<Hlc> }`. A value of the wrong shape for its field + is `Error::Validation` at decode (023 B-4 extended). `issue.closed` is + the same as `State(Closed)` at the fact's `Hlc` and additionally records + `reason`; reopening is `State(Open)`. +- **B-3 (registers).** Every scalar field is a spec 019 `LwwRegister` + keyed by the fact's `Hlc` and the entry's issuer key as the tiebreak; + concurrent edits converge to the greater `(hlc, key)` on every replica. +- **B-4 (`LabelSet`).** An observed-remove set specialized to labels: + `LabelAdd` at `hlc` inserts the tag `(label, hlc)`; `LabelRemove` removes + exactly the tags whose `hlc` is in `observed`; a label is present iff it + has at least one surviving tag. An add concurrent with a remove that did + not observe it survives (add-wins). `LabelSet::labels() -> + BTreeSet<&str>` and `tags_for(label) -> &[Hlc]` (what a client must send + as `observed` to remove the label as it currently sees it). The set is + local to `issue.rs`; promoting it to spec 019's `crdt/` module is a later + refinement noted in Out of scope. +- **B-5 (links and threads).** `issue.link_added` inserts into `links` + (a set; duplicates are no-ops). Comments on an issue are spec 026 + threads whose `change` is absent: this spec extends nothing in 026; + instead `review.thread_opened` on an issue carries the issue id in + `extra["issue"]` until a later spec promotes it, and `IssueView` + collects such threads into `threads`. The fold records a link to an + unknown target with `IssueWarning::DanglingLink`. +- **B-6 (builders).** `open_issue(opened_by, title, nonce) -> (IssueId, + DomainFact)`, `set_field(issue, value: IssueFieldValue) -> DomainFact`, + `close(issue, reason) -> DomainFact`, `add_link(issue, target) -> + DomainFact`, and `remove_label(view, issue, label) -> + Option<DomainFact>` which reads `tags_for` to fill `observed` and returns + `None` when the label is absent. Pure; nothing is stored here. +- **B-7 (queries).** `IssueView::get(id)`, `issues()` in id order, + `open_issues()`, `by_label(label)`, `by_assignee(principal)`. + +## 4. Functional requirements + +- **FR-001.** Tests: concurrent title edits converge to the higher `Hlc` + and, at equal `Hlc`, to the higher key; add then remove; remove + concurrent with an unobserved add keeps the label; two adds and one + remove observing both clears it; close then reopen; every warning + variant; `remove_label` on an absent label returns `None`; links + deduplicate. +- **FR-002.** A property test folds a random issue history through two + permutations agreeing on total order and asserts equal views, and a + second asserts the label set equals a reference OR-set model. +- **FR-003.** No function in `issue.rs` reads a clock or performs I/O. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked issue` passes. +- **AC-2.** A fixture of two replicas editing the same issue (title, + labels, assignee) in interleaved orders folds to one identical view on + both, recorded as a golden view in `tests/issue.rs`. + +## 6. Out of scope + +Milestones as a noun of their own, issue templates, and cross-issue +dependencies (later feature specs); moving `LabelSet` into spec 019's +`crdt/` module as a general OR-set (a later amendment to 019); mirrored +GitHub issues (040); search (082); feeds (085). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked issue +``` diff --git a/specs/031-git-object-bridge/spec.md b/specs/031-git-object-bridge/spec.md new file mode 100644 index 0000000..c5b48bb --- /dev/null +++ b/specs/031-git-object-bridge/spec.md @@ -0,0 +1,152 @@ +--- +id: "031-git-object-bridge" +title: "Git object bridge: gix import and export, the bidirectional oid map, worktree snapshots" +status: approved +kind: "kernel" +domain: "l0-objects" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "014-content-defined-chunking" +establishes: + - "crates/hqgit-git/Cargo.toml" + - "crates/hqgit-git/src/lib.rs" + - "crates/hqgit-git/src/import.rs" + - "crates/hqgit-git/src/export.rs" + - "crates/hqgit-git/src/mapping.rs" + - "crates/hqgit-git/src/worktree.rs" + - "crates/hqgit-git/src/mode.rs" + - "crates/hqgit-git/tests/" + - "crates/hqgit-git/testdata/" +extends: + # gix (gitoxide) joins the shared dependency table; never libgit2 (thesis D3). + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Git compatibility is a hard requirement of the wedge (thesis D16), so the + object store must speak git without becoming git. This spec founds + hqgit-git, the pure-Rust bridge over gix: import a git commit's tree and + blobs into hqgit objects (blobs through the chunking path so large files + are the general case), export an hqgit tree back as git objects, keep a + bidirectional oid map for both SHA-1 and SHA-256 repositories, and + snapshot a working tree into a tree Cid without touching git's index. Git + commits are not hqgit objects: a Revision references a tree, and the git + commit oid rides along as provenance. Everything here is deterministic, + never shells out, and never writes a git ref. +--- + +# 031: Git object bridge + +## 1. Purpose + +Thesis §4.1 and D3: git compatibility through `gix`, never libgit2, because +the trusted core admits no C boundary. Thesis D16 makes the bridge the +first user-facing seam: the CLI (033) reviews a plain git repository by +snapshotting its working tree, and the mirror (040) imports pull-request +heads by commit oid. Both need one mapping between git's object identity +(SHA-1 or SHA-256 over git's own framing) and hqgit's (BLAKE3 over +canonical objects), maintained in both directions so a tree can round trip +without loss. + +## 2. Territory + +`crates/hqgit-git` as founded here: the manifest (depending on +`hqgit-types` and `hqgit-object` within the workspace, plus `gix` and +`redb`), `lib.rs`, `import.rs` (git to hqgit), `export.rs` (hqgit to git), +`mapping.rs` (the persisted oid map), `worktree.rs` (snapshots), `mode.rs` +(the mode translation table), and the `tests/` and `testdata/` subtrees. +The git smart protocol endpoint is spec 092; the mirror is spec 040; the +CLI verbs are spec 033. + +## 3. Behavior + +- **B-1 (modes).** `mode.rs` defines `EntryMode { Regular, Executable, + Symlink, Submodule, Directory }` and the bijection to git's `100644`, + `100755`, `120000`, `160000`, `040000`. `Tree` entries (013) carry the + `EntryMode`; a git mode outside the five is `Error::Validation` naming + the path. Submodules import as a `Submodule` entry whose target is the + recorded commit oid as opaque bytes; nothing is fetched. +- **B-2 (import).** `import_tree(repo: &gix::Repository, tree_oid, store: + &dyn ObjectStore, map: &mut GitMap) -> Result<Cid, Error>` walks the git + tree depth first in git's byte order, imports every blob through spec + 014's `put_blob` (so blobs above one chunk become manifests), builds 013 + `Tree` objects bottom up, and records every `(git oid, Cid)` pair in the + map. A blob already present in the map is not re-read. `import_commit( + repo, commit_oid, store, map) -> Result<ImportedCommit, Error>` imports + the commit's tree and returns `ImportedCommit { tree: Cid, commit_oid: + GitOid, parents: Vec<GitOid>, author, committer, message }` so the caller + (024 via 033 or 040) can record the commit oid in a revision's `extra` + under the key `git.commit` as provenance. Commits are never stored as + hqgit objects. +- **B-3 (export).** `export_tree(cid, store, repo: &gix::Repository, map: + &mut GitMap) -> Result<GitOid, Error>` writes an hqgit tree and its blobs + into the git object database (chunked blobs are reassembled through 014 + `get_blob`), producing byte-identical git objects for a tree that was + imported from git (round-trip property). Export writes objects only; it + never moves a ref. +- **B-4 (mapping).** `GitOid { format: GitHashFormat { Sha1 | Sha256 }, + bytes: Vec<u8> }`. `GitMap` is a redb database at `<repo>/.hq/gitmap.redb` + with two tables, `git_to_hq` and `hq_to_git`, both written in one + transaction; `lookup_git(oid) -> Option<Cid>`, `lookup_hq(cid) -> + Option<Vec<GitOid>>` (one Cid may correspond to a SHA-1 and a SHA-256 + oid), `record(oid, cid)`. The map is a cache: losing it costs a re-import, + never correctness, because both identities are content-derived. +- **B-5 (worktree snapshot).** `snapshot_worktree(root: &Path, store, + map) -> Result<Snapshot, Error>` walks a working directory honoring + `.gitignore`, `.git/info/exclude`, and global excludes through gix's + ignore stack, skips `.git/` and `.hq/`, imports blobs and builds trees + exactly as B-2, and returns `Snapshot { tree: Cid, base: Option<Cid> }` + where `base` is the imported tree of `HEAD` when the directory is a git + worktree. Symlinks are recorded, never followed. The snapshot never + touches the git index or any ref. +- **B-6 (determinism).** Given the same git objects or the same directory + bytes and modes, import and snapshot produce identical Cids on every + platform: entries are sorted by git's tree ordering, file modes are + normalized through B-1, and no timestamp, uid, or filesystem order enters + an object. Line endings are bytes; nothing is normalized. +- **B-7 (no shelling out).** The crate never spawns `git`. Repository + discovery, object reads, and ignore handling go through `gix` APIs only. + +## 4. Functional requirements + +- **FR-001.** `ObjectStore` (013) and `GitMap` are injected; tests run + against `MemoryStore` and a temp-dir map. +- **FR-002.** Fixtures under `testdata/` are built by tests with `gix` at + runtime from a scripted tree description (`testdata/repo-basic.toml`, + `testdata/repo-large-blob.toml`), never committed as `.git` directories. +- **FR-003.** Tests cover: every mode round trips; an executable bit + survives export; a symlink survives; a nested tree imports in sorted + order; a blob above one chunk imports as a manifest and exports byte + identical; import then export yields the original git tree oid for both + SHA-1 and SHA-256 repositories; the map records both directions and + survives reopen; a snapshot honors `.gitignore` and skips `.hq/`; a + snapshot of an unchanged tree yields the same Cid twice. +- **FR-004.** `gix` is pinned exact in `[workspace.dependencies]` (its + ignore semantics are part of the determinism contract). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-git --locked` passes. +- **AC-2.** Importing the fixture repository's `HEAD` tree then exporting + it produces the same git tree oid, asserted for both hash formats. +- **AC-3.** `spec-spine index` discovers `hqgit-git` bound to this spec and + `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Serving the git protocol (092), pull-request import (040), the CLI verbs +that call the bridge (033), submodule content, and git history rewriting +of any kind. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-git --locked +``` diff --git a/specs/032-cli-skeleton/spec.md b/specs/032-cli-skeleton/spec.md new file mode 100644 index 0000000..6f17b85 --- /dev/null +++ b/specs/032-cli-skeleton/spec.md @@ -0,0 +1,159 @@ +--- +id: "032-cli-skeleton" +title: "The hq binary: clap frame, exit codes, JSON output, init, status, log" +status: approved +kind: "kernel" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "021-local-repository" +establishes: + - "crates/hqgit-cli/Cargo.toml" + - "crates/hqgit-cli/src/main.rs" + - "crates/hqgit-cli/src/cli.rs" + - "crates/hqgit-cli/src/cmd_init.rs" + - "crates/hqgit-cli/src/cmd_status.rs" + - "crates/hqgit-cli/src/cmd_log.rs" + - "crates/hqgit-cli/src/config.rs" + - "crates/hqgit-cli/src/output.rs" + - "crates/hqgit-cli/tests/" +extends: + # clap, serde_json, toml, and the assert_cmd dev-dependency join the table. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The hq binary is the first client of the ledger and, by thesis §5, runs + the same ledger implementation the server will. This spec founds + hqgit-cli: a clap frame whose subcommands are one module each, exit codes + mapped in exactly one place through Error::exit_code, a --json mode on + every read verb that emits canonical sorted-key JSON, layered + configuration (user and repository), and the three verbs that prove the + local repository works: hq init creates .hq/ with a local ed25519 + identity and the genesis entry, hq status reports heads and identity, + and hq log renders entries in total order. Later specs add verbs by + extending main.rs and cli.rs; nothing here talks to a network or to git. +--- + +# 032: The hq binary + +## 1. Purpose + +Thesis §5: single-binary embedding means the CLI and the server run the +same ledger implementation, which is the only way to get genuine +offline-first without two implementations drifting apart (constitution +XIII). The CLI therefore arrives before the server and exercises spec 021's +`Repository` directly. This spec is the frame every later verb hangs on: +one place for exit codes, one output discipline, one configuration model, +and a test harness that drives the real binary against temporary +directories. + +## 2. Territory + +`crates/hqgit-cli` as founded here: the manifest (binary name `hq`, +depending on `hqgit-types`, `hqgit-object`, and `hqgit-ledger` within the +workspace), `main.rs` (dispatch and the single exit-code mapping), +`cli.rs` (the clap definitions), `cmd_init.rs`, `cmd_status.rs`, +`cmd_log.rs`, `config.rs`, `output.rs`, and the `tests/` subtree. Every +later CLI spec (033, 034, 042, 067, 080, 093, 103, 112) `extends` `main.rs` +and `cli.rs` and establishes its own `cmd_*.rs`. + +## 3. Behavior + +- **B-1 (frame).** `cli.rs` declares `Hq { #[command(subcommand)] cmd: + Command, #[arg(long, global = true)] json: bool, #[arg(long, global = + true)] repo: Option<PathBuf>, #[arg(long, global = true)] quiet: bool }`. + `--repo` overrides discovery; otherwise the repository is the nearest + ancestor directory containing `.hq/`, and a verb that needs one and finds + none exits `1` with `no hqgit repository found (run hq init)`. +- **B-2 (exit codes).** `main.rs` calls `run(args) -> Result<(), Error>` + and maps the result through spec 010's `Error::exit_code()` in exactly + one `std::process::exit` call: `0` ok, `1` validation, not found, drift, + crypto, or policy, `2` stale, `3` I/O, parse, schema, or config. Clap + usage errors keep clap's `2`; no other code is ever produced. +- **B-3 (output).** `output.rs` exposes `Out { json: bool, quiet: bool }` + with `emit<T: Serialize>(&self, value: &T, human: impl FnOnce(&T) -> + String)`. With `--json` every read verb prints one JSON document with + keys sorted, two-space indentation, and a trailing newline (the same + canonicalization spec-spine uses), and nothing else on stdout. + Diagnostics go to stderr. Hashes render as 64 hex characters, Cids as + `<codec>:<hex>`, principals as `<kind>:<hex>`. +- **B-4 (config).** `config.rs` loads, in precedence order, `.hq/config.toml` + inside the repository, then `$XDG_CONFIG_HOME/hq/config.toml` (default + `~/.config/hq/config.toml`), then built-in defaults, into `Config { + identity: IdentitySection { display: Option<String> }, output: + OutputSection { json: bool } }`. Unknown keys are a `Config` error naming + the key. `HQ_CONFIG` overrides the user path. No secret ever lives in a + config file: the identity seed is under `.hq/identity/` (021). +- **B-5 (`hq init`).** `hq init [--display <name>] [--node-id <hex16>]` + refuses with exit `1` when `.hq/` already exists, otherwise generates a + fresh ed25519 seed into `.hq/identity/seed` (mode `0600`), derives the + `HumanId` as spec 060 will (the hash of an `identity.created` fact, + which this verb also appends as the first fact after genesis), calls + `Repository::init` (021) with that identity, and prints `{ "repo": + <path>, "namespace": <hex>, "identity": <principal>, "genesis": + <entry-hash> }`. When the directory is a git worktree the config records + `git.worktree = true`; nothing reads git here (033 wires the bridge). +- **B-6 (`hq status`).** Prints `{ "repo", "namespace", "identity", + "heads": [<entry-hash>], "entries": <count>, "objects": <count>, + "namespaces": [<name>] }` from spec 021's `Repository` reads only. Exit + `0` always when a repository is found. +- **B-7 (`hq log`).** `hq log [--limit N] [--namespace <name>] + [--kind <fact-kind>]` renders entries in spec 018's total order, newest + first, one line per entry in human mode (`<hash[..12]> <hlc> <issuer[..8]> + <fact-kind>`) and an array of `{ "hash", "hlc": { "wall_ms", "logical", + "node" }, "issuer", "kind", "payload": <cid>, "parents": [...] }` in JSON + mode. An erased payload (020) renders `"kind": "erased"`; the verb never + fails because content is gone. +- **B-8 (never git).** The crate does not depend on `hqgit-git` and never + spawns `git`. Spec 033 adds the bridge dependency by extending this + crate's manifest. +- **B-9 (no ambient input on hashed paths).** The only clock read is the + one spec 018's `HlcGenerator` performs when a verb appends a fact; no + verb formats a timestamp from the system clock into output. + +## 4. Functional requirements + +- **FR-001.** Each `cmd_*.rs` exposes `pub fn run(ctx: &Ctx, args: &XArgs) + -> Result<(), Error>` where `Ctx { repo: Option<Repository>, config: + Config, out: Out }` is built once in `main.rs`; commands never call + `process::exit`. +- **FR-002.** `tests/cli.rs` drives the built binary with `assert_cmd` + against temporary directories: `init` creates the layout and refuses a + second time; `status --json` parses and carries the genesis head; `log + --json` lists the genesis and the identity fact in order; a verb outside + a repository exits `1` with the documented message; an unknown config key + exits `3`. +- **FR-003.** A contract test asserts the exit code of every `Error` + variant through the binary (one fixture per variant), pinning B-2. +- **FR-004.** JSON output of every read verb is byte-identical across two + runs on an unchanged repository. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-cli --locked` passes. +- **AC-2.** In a fresh directory, `hq init && hq status --json && hq log + --json` exits 0 three times and the status head equals the log's newest + entry. +- **AC-3.** `spec-spine index` discovers `hqgit-cli` bound to this spec and + `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Changes and review (033), attestation and verification verbs (034), mirror +verbs (042), policy verbs (067), projection verbs (080), remote targets +(093), evidence verbs (103), sync verbs (112), and any server. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-cli --locked +cargo run -p hqgit-cli --locked -- --help +``` diff --git a/specs/033-cli-offline-review/spec.md b/specs/033-cli-offline-review/spec.md new file mode 100644 index 0000000..1ce8d5c --- /dev/null +++ b/specs/033-cli-offline-review/spec.md @@ -0,0 +1,165 @@ +--- +id: "033-cli-offline-review" +title: "Offline review from the CLI: changes, revisions, anchored threads, approvals" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "032-cli-skeleton" + - "031-git-object-bridge" + - "026-review-threads" + - "027-attestation-primitive" +establishes: + - "crates/hqgit-cli/src/cmd_change.rs" + - "crates/hqgit-cli/src/cmd_review.rs" + - "crates/hqgit-cli/tests/review.rs" + - "crates/hqgit-cli/testdata/review/" +extends: + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + # hqgit-git and hqgit-domain join the CLI's dependencies. + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } +summary: > + The wave 1 milestone (thesis §6, step 1): prove offline review against a + plain git repository. hq change new opens a Change with a stable id; hq + change submit snapshots the working tree through the git bridge into a + Revision whose base is the imported HEAD tree; hq review comment anchors + a thread to a semantic location that survives rebase; hq review approve + issues an approval Attestation signed by the local identity; hq review + show re-anchors every thread against the latest revision and marks what + moved or was lost. No network, no server, no git ref moves. The evidence + this produces is what spec 034 verifies, and the transcript it leaves is + the fixture every later client must reproduce. +--- + +# 033: Offline review from the CLI + +## 1. Purpose + +Design §1.1 points 1 and 2: clone the repo, get the argument that produced +it; a change has stable identity with ordered revisions; comments anchor to +semantic locations. This spec makes those three claims usable from a shell +with nothing but a working directory and the local identity. It is the +first place the domain model (024, 025, 026, 027), the bridge (031), and +the repository (021) meet in one user-facing flow, and it defines the +golden transcript that fixes what "offline review works" means. + +## 2. Territory + +Two new command modules in `crates/hqgit-cli` (`cmd_change.rs`, +`cmd_review.rs`), their integration tests (`tests/review.rs`), and the +fixture and golden transcript under `testdata/review/`. Additively: the +dispatch in `main.rs`, the clap tree in `cli.rs`, and the crate manifest +(gaining `hqgit-git` and `hqgit-domain`). Attestation issuance for +arbitrary predicates and chain verification are spec 034. + +## 3. Behavior + +- **B-1 (`hq change new`).** `hq change new [--title <text>]` appends a + `change.opened` fact (024) whose author is the local identity and prints + `{ "change": <ChangeId>, "title": <text> }`. The title is a derived LWW + field; `hq change retitle <id> <text>` appends a `change.field_set`. +- **B-2 (`hq change submit`).** `hq change submit <id> [-m <message>] + [--base <tree-cid>]` snapshots the working tree through spec 031 + `snapshot_worktree`, refuses with exit `1` when the snapshot equals the + previous revision's tree (`nothing changed since revision N`), takes the + base from `--base`, else from the git `HEAD` tree when the directory is a + git worktree, else from the previous revision's tree, and appends a + `change.revision_submitted` fact producing `Revision { number: N+1, tree, + base, parent_revision }` with the git commit oid, when present, recorded + in `extra` under `git.commit`. Output `{ "change", "revision": <RevisionId>, + "number", "tree", "base" }`. +- **B-3 (`hq change list|show|abandon`).** `list [--state open|merged| + abandoned|all]` prints one row per change from a `ChangeView` fold + (024): id, state, title, revision count, open thread count, approval + count. `show <id>` prints the change, its revisions in order, each + thread with its current resolution, and every attestation whose subject + is one of its revisions. `abandon <id>` appends `change.abandoned`. + Ids accept an unambiguous hex prefix of at least 8 characters. +- **B-4 (`hq review comment`).** `hq review comment <change> + <path>:<line>[-<line>] -m <body> [--revision N]` resolves the location + against the named revision's tree (latest by default), computes the + `Anchor` through spec 025 (`anchor_at` on the parsed file, text fallback + for unsupported languages, reported as `"anchor_kind": "text"`), stores + the body as an object (constitution X: content in the store, the fact + holds a Cid), and appends `review.thread_opened` plus + `review.comment_posted` (026). Output `{ "thread", "comment", "anchor": + { "path", "node_path", "kind" } }`. +- **B-5 (`hq review reply|resolve|reopen`).** `reply <thread> -m <body>` + appends a comment to an existing thread; `resolve <thread>` and `reopen + <thread>` set the thread's LWW resolution (026). A thread id accepts a + prefix like a change id. +- **B-6 (`hq review approve`).** `hq review approve <change> [--revision N] + [--note <text>]` issues an Attestation (027) with predicate + `hqgit/approval/v1`, subject the RevisionId (latest by default), issuer + the local principal, claim `{ "change", "revision", "note" }`, signed with + the local seed, and appends `attestation.issued` referencing it. Approving + the same revision twice from the same identity is refused with exit `1`. + An approval is never a comment (026 B-rule); the verb does not accept a + body. +- **B-7 (`hq review show`).** `hq review show <change> [--revision N]` + re-resolves every thread's anchor against the named revision's tree + through spec 025 `resolve` and renders each thread with one of `exact`, + `moved (confidence)`, `text`, or `lost`, its comments in HLC order, its + resolution, and, for `lost`, the original path and line so the reviewer + can find it by hand. JSON mode emits the same as structured data. An + erased comment body renders `"body": null, "erased": true`. +- **B-8 (offline).** No verb here opens a socket or spawns a process. A + repository that is not a git worktree still supports every verb; only the + default base of B-2 changes. +- **B-9 (determinism).** Two runs of the scripted fixture session produce + ledgers whose `hq log --json` output is identical after masking `hlc` + fields and the identity-derived ids, which is what the golden transcript + asserts. + +## 4. Functional requirements + +- **FR-001.** `testdata/review/fixture.toml` scripts a small Rust and + TypeScript git repository (built at test time with `gix`, spec 031 + FR-002) and `testdata/review/session.sh` lists the verbs the transcript + test runs; `testdata/review/transcript.golden.json` is the masked + expected `hq log --json`. +- **FR-002.** `tests/review.rs` drives the binary through the session and + asserts: the transcript matches the golden file modulo masks; a second + `submit` with no working-tree change exits `1`; a comment on a Rust + function survives a rename of the function above it and a body edit + below it (`exact`), a deletion yields `lost`, and a `.txt` file yields + `text`; a duplicate approval exits `1`; `show` after erasing a comment + body (through spec 020's local capability) renders `erased`. +- **FR-003.** The anchor and approval logic lives in `hqgit-domain` + (025, 026, 027); the command modules only parse arguments, call domain + functions, append facts through `Repository::append_fact` (021), and + format output. +- **FR-004.** Every change, thread, and comment id printed by these verbs + is the content-derived id of spec 023 `ids.rs`; no counter is minted. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-cli --locked --test review` passes. +- **AC-2.** On the fixture repository, the scripted session (`new`, + `submit`, `comment`, edit, `submit`, `show`, `approve`) exits 0 at every + step and the final `show` reports the comment as `exact` on revision 2. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0 with the + new modules claimed here. + +## 6. Out of scope + +Arbitrary-predicate attestations and chain verification (034), stacked +changes and interdiff (050), semantic delta views (051), mirrored review +state (040, 041), any remote (093), and merging (076, 092). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-cli --locked --test review +``` diff --git a/specs/034-cli-attest-and-verify/spec.md b/specs/034-cli-attest-and-verify/spec.md new file mode 100644 index 0000000..45a62c4 --- /dev/null +++ b/specs/034-cli-attest-and-verify/spec.md @@ -0,0 +1,144 @@ +--- +id: "034-cli-attest-and-verify" +title: "hq attest and hq verify: issue any attestation, verify the chain and its evidence offline" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 1 +depends_on: + - "033-cli-offline-review" +establishes: + - "crates/hqgit-cli/src/cmd_attest.rs" + - "crates/hqgit-cli/src/cmd_verify.rs" + - "crates/hqgit-cli/tests/verify.rs" +extends: + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } +summary: > + Wave 1 ends when evidence produced offline verifies offline. hq attest + issues an Attestation over any subject hash with any predicate, validating + the claim against the registry when the predicate is known and carrying + it verbatim when it is not; hq attest list enumerates the evidence on a + subject. hq verify runs spec 017's chain verification against the + repository with a resolver seeded from the keys the ledger itself + records, reports every failure rather than the first, and exits 1 on any; + hq verify attestation checks one attestation's signature and claim shape. + Both verbs are pure reads apart from the fact hq attest appends, and both + emit machine-readable reports so the orchestrator and CI can consume + them. +--- + +# 034: hq attest and hq verify + +## 1. Purpose + +Constitution IX: all evidence is one primitive, and constitution XI: trust +is checkable, not decorative. Spec 033 issues one kind of evidence +(approval). This spec opens the primitive to every predicate from the +command line, so build results, scan findings, and mirrored facts can be +attached long before the evaluation plane (070 to 076) automates them, and +it gives the repository owner the verb that answers "is this ledger +intact?" with no server, no network, and no trust in anyone but the keys +the ledger records. + +## 2. Territory + +`cmd_attest.rs`, `cmd_verify.rs`, and `tests/verify.rs` in +`crates/hqgit-cli`; additively the dispatch in `main.rs` and the clap tree +in `cli.rs`. Full verification through identity rotation and transparency +inclusion is spec 064; this spec verifies what wave 1 can: signatures +against keys known to the ledger, hash links, clock rules, and claim +shapes. + +## 3. Behavior + +- **B-1 (`hq attest`).** `hq attest <subject> --predicate <uri> (--claim + <file.json> | --claim -) [--note <text>]` where `<subject>` is a 64-hex + hash or a Cid, reads the claim as JSON, converts it to the canonical + `Value` of spec 011 (rejecting floats with exit `1` naming the key), + validates it through spec 027's `PredicateRegistry` when the predicate is + registered (`Error::Validation` on shape failure) and carries it verbatim + otherwise, stores the claim as a `DagCbor` object, builds the Attestation + with issuer the local principal and `issuer_key` the local key, signs it + under `SignDomain("attestation")`, appends `attestation.issued`, and + prints `{ "attestation": <AttestationId>, "subject", "predicate", + "claim": <cid>, "known_predicate": bool }`. +- **B-2 (`hq attest list`).** `hq attest list <subject> [--predicate <uri>]` + prints every attestation whose subject matches, in HLC order: id, + predicate, issuer, issued-at, and `"verified": "signature" | "failed"` + from a local signature check (B-5). `hq attest show <id>` prints one + attestation with its decoded claim. +- **B-3 (`hq verify`).** `hq verify [--namespace <name>] [--strict]` loads + the repository (021), builds a `StaticResolver` (017) from every public + key the ledger records in `identity.created` and `identity.key_rotated` + facts plus the local identity, runs spec 017 `verify` over the DAG, and + additionally checks every `attestation.issued` fact's attestation + signature and claim shape. It prints a `VerifyReport { "entries": + <count>, "attestations": <count>, "failures": [ { "entry" | "attestation", + "rule", "detail" } ], "unknown_keys": [<KeyId>] }`. Exit `0` when + `failures` is empty, `1` otherwise. Without `--strict` an entry signed by + a key the ledger does not know is listed under `unknown_keys` and does + not fail; with `--strict` it does. +- **B-4 (never stops early).** Verification walks the whole DAG and every + attestation and reports every failure; a tampered entry, a missing + parent, a clock violation, a bad signature, and a malformed claim all + appear in one run. +- **B-5 (`hq verify attestation`).** `hq verify attestation <id>` checks + the signature against the issuer key (resolved as in B-3), the claim + object's presence in the store, and the claim shape when the predicate is + known, printing `{ "attestation", "signature": "ok" | "failed", + "claim": "ok" | "unknown-predicate" | "failed", "detail" }`. Exit `1` on + any `failed`. +- **B-6 (erased content).** A claim whose object was erased (020) reports + `"claim": "erased"` and does not fail signature verification; the entry + chain is unaffected by content erasure by construction. +- **B-7 (reports are data).** In JSON mode both verbs print exactly one + document; in human mode `verify` prints one line per failure with the + entry or attestation id, the rule name, and the detail, then a summary + line. + +## 4. Functional requirements + +- **FR-001.** `tests/verify.rs` covers: attest with a known predicate and a + valid claim; a claim with a float rejected; an unknown predicate carried + verbatim and listed with `known_predicate: false`; `verify` clean on the + spec 033 fixture ledger; `verify` after tampering one byte in a stored + entry reports a hash failure; after deleting a parent object reports a + missing parent; after forging an attestation with a foreign key reports a + signature failure and, without `--strict`, an `unknown_keys` entry; after + erasing a claim object reports `erased` and exits 0. +- **FR-002.** The command modules call spec 017 `verify` and spec 027 + `verify_signature` and never reimplement either. +- **FR-003.** `hq verify --json` output is byte-identical across two runs + on an unchanged repository. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-cli --locked --test verify` passes. +- **AC-2.** On the spec 033 fixture session's ledger, `hq verify` exits 0 + and reports the approval attestation as verified; after `hq attest` with + a build-result claim, `hq attest list <revision>` shows both. +- **AC-3.** Wave 1 milestone: the whole flow (`init`, `change`, `review`, + `attest`, `verify`) runs with networking disabled in the test + environment. + +## 6. Out of scope + +Rotation-aware and transparency-backed verification (060, 062, 064), policy +evaluation over the evidence (065, 067), remote attestation submission +(093), and evidence bundles (103). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-cli --locked --test verify +``` diff --git a/specs/040-github-mirror-import/spec.md b/specs/040-github-mirror-import/spec.md new file mode 100644 index 0000000..b303c09 --- /dev/null +++ b/specs/040-github-mirror-import/spec.md @@ -0,0 +1,178 @@ +--- +id: "040-github-mirror-import" +title: "GitHub mirror import: issues, pull requests, reviews, and checks become quarantined facts" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 2 +depends_on: + - "028-issues-and-derived-state" + - "026-review-threads" + - "027-attestation-primitive" + - "031-git-object-bridge" +establishes: + - "crates/hqgit-mirror/Cargo.toml" + - "crates/hqgit-mirror/src/lib.rs" + - "crates/hqgit-mirror/src/github/mod.rs" + - "crates/hqgit-mirror/src/github/client.rs" + - "crates/hqgit-mirror/src/github/import.rs" + - "crates/hqgit-mirror/src/github/model.rs" + - "crates/hqgit-mirror/src/quarantine.rs" + - "crates/hqgit-mirror/src/idmap.rs" + - "crates/hqgit-mirror/tests/" + - "crates/hqgit-mirror/testdata/github/" +extends: + # reqwest (rustls) and serde_json join the shared dependency table. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } + # The mirror.observed variant is filled in with its body schema. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + The wedge in code (thesis D16, constitution XIV): value for users who + migrate nothing. This spec founds hqgit-mirror and its GitHub import: a + client seam over the REST API with a recorded-fixture implementation for + tests, and an importer that turns issues, pull requests, review comments, + approvals, check conclusions, and commits into facts in the repository's + quarantine namespace. Pull requests become Changes with one Revision per + head commit through the git bridge; review comments become threads + anchored by path and line and then re-anchored semantically; approvals + and check results become mirror attestations whose claims name the + external source. An id map makes re-import idempotent. Nothing mirrored + is trusted: promotion to the main namespace is spec 094's capability + path. +--- + +# 040: GitHub mirror import + +## 1. Purpose + +Design §1.2: federate over existing GitHub repositories and mirror +collaboration state bidirectionally, because the network effect is the +product and migration cost is paid by the wrong party. Import is the half +that turns a user's existing repository into a ledger they can clone +offline (design §1.1 point 1). Everything imported is an observation about +an external system, so it lands quarantined (constitution XV) and is +recorded as evidence with provenance (constitution IX), never as fact the +repository asserts on its own authority. + +## 2. Territory + +`crates/hqgit-mirror` as founded here: the manifest (depending on +`hqgit-types`, `hqgit-object`, `hqgit-ledger`, `hqgit-domain`, and +`hqgit-git` within the workspace, plus `reqwest` with rustls and +`serde_json`), `lib.rs`, the `github/` module (client seam, wire model, +importer), `quarantine.rs`, `idmap.rs`, the `tests/` subtree, and recorded +fixtures under `testdata/github/`. Export is spec 041; the sync loop and +the CLI verbs are spec 042. + +## 3. Behavior + +- **B-1 (client seam).** `github/client.rs` declares `trait GitHubClient { + fn repository(&self, owner, name) -> Result<RepoMeta>; fn issues(&self, + page: Cursor) -> Result<Page<Issue>>; fn pull_requests(&self, page) -> + Result<Page<PullRequest>>; fn reviews(&self, pr) -> Result<Vec<Review>>; + fn review_comments(&self, pr) -> Result<Vec<ReviewComment>>; + fn check_runs(&self, sha) -> Result<Vec<CheckRun>>; fn commit(&self, sha) + -> Result<Commit>; fn issue_comments(&self, number) -> + Result<Vec<IssueComment>> }` over the wire types in `model.rs` (serde + structs mirroring the REST v3 JSON, unknown fields ignored). `HttpClient` + implements it with reqwest, a bearer token from the caller, conditional + requests (`If-None-Match`), and rate-limit headers surfaced as + `Error::Stale` carrying the reset instant. `FixtureClient` replays + recorded JSON from `testdata/github/<repo>/` and is the only client tests + use. +- **B-2 (quarantine target).** `quarantine.rs` resolves the repository's + quarantine namespace (021) and the `Service` principal the mirror runs as + (`MirrorPrincipal`, an identity created on first run and recorded by an + `identity.created` fact with kind `Service`). Every fact the importer + appends is issued by that principal into that namespace; the importer + refuses to write to `main` (`Error::Policy`). +- **B-3 (issues).** Each GitHub issue becomes an `issue.opened` fact (028) + plus one `issue.field_set` per field (title, state, labels, assignee, + milestone) and one `review.comment_posted` (026, threads without anchors) + per issue comment, with the comment body stored as an object. A + `mirror.observed` fact (023, filled here: `{ source: "github", kind, + external_id, url, etag, observed_at }`) is appended for every imported + item so the observation itself is a fact. +- **B-4 (pull requests).** Each pull request becomes a `change.opened` + (024) and, per distinct head commit seen (from the PR's commits list, + oldest first), one `change.revision_submitted` whose tree is imported + through spec 031 `import_commit` from a local clone the caller supplies + (`ImportSource::Clone(path)`), with the head sha in `extra.git.commit`. + A PR whose commits are not present in the clone is reported, not + imported (`ImportReport.skipped` with the reason). +- **B-5 (review comments).** Each review comment becomes a thread (026) + anchored first by `(path, line, side)` as a text anchor (025 fallback), + then re-anchored semantically against the revision's tree where the + language is supported; replies join the thread in `in_reply_to` order. +- **B-6 (approvals and checks).** A review with state `APPROVED` becomes an + Attestation with predicate `hqgit/mirror/v1`, subject the RevisionId of + the reviewed head, claim `{ "source": "github", "kind": "review", + "state": "approved", "external_id", "author_login", "url" }`, issued by + the mirror principal. A check run with a conclusion becomes the same + predicate with `"kind": "check_run"`, `"name"`, `"conclusion"`, + `"details_url"`. Neither is an `hqgit/approval/v1`: the mirror observes, + it does not approve. +- **B-7 (id map).** `idmap.rs` keeps `<repo>/.hq/mirror.redb` with tables + `external_to_internal` (`(source, kind, external_id) -> id`) and + `internal_to_external`, plus a `cursors` table (`(source, stream) -> + Cursor`). `import` consults the map before every append: an item already + mapped whose `etag` or `updated_at` is unchanged produces no fact; a + changed item produces only the delta facts (a new `issue.field_set`, a + new revision). Re-running import on unchanged fixtures appends nothing. +- **B-8 (report).** `import(repo, client, source, since: Option<Cursor>) + -> Result<ImportReport, Error>` where `ImportReport { imported: BTreeMap< + String, u32>, skipped: Vec<{ kind, external_id, reason }>, cursor: + Cursor, rate_limit: Option<{ remaining, reset } > }`; the report is + serializable and is what spec 042 prints. +- **B-9 (no trust).** Nothing here verifies anything about GitHub's claims; + the attestations record what was observed and who observed it. Promotion + out of quarantine (094) and verification (064) are later specs. + +## 4. Functional requirements + +- **FR-001.** The importer is a function of `(client, repository, id map, + source)`; `FixtureClient` and a `MemoryStore`-backed repository drive + every test with no network. +- **FR-002.** Fixtures under `testdata/github/basic/` cover: two issues + (one with labels and two comments), one pull request with two commits + and a review comment thread with a reply, one approving review, two + check runs (success and failure); `testdata/github/updated/` is the same + repository after one issue retitle and one new commit. +- **FR-003.** Tests cover: every B-3 to B-6 mapping on `basic`; idempotent + re-import on `basic` appends zero facts; import of `updated` after + `basic` appends exactly the delta; a PR with a missing commit is skipped + with reason; a rate-limit response yields `Error::Stale` with the reset + instant; every appended fact lands in the quarantine namespace and is + issued by the mirror principal. +- **FR-004.** `reqwest` is built with `rustls-tls` and without the default + native TLS feature; no crate in this workspace links OpenSSL. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-mirror --locked` passes. +- **AC-2.** After importing `basic`, `hq log --namespace quarantine --json` + (032) lists the expected fact kinds in HLC order and `hq attest list + <revision>` (034) shows the mirror attestations. +- **AC-3.** `spec-spine index` discovers `hqgit-mirror` bound to this spec + and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Export to GitHub (041), the sync loop, cursors across runs, and the CLI +verbs (042), promotion out of quarantine (094), GitLab or Gerrit sources +(later specs following this shape), and any webhook receiver. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-mirror --locked +``` diff --git a/specs/041-github-mirror-export/spec.md b/specs/041-github-mirror-export/spec.md new file mode 100644 index 0000000..db332a7 --- /dev/null +++ b/specs/041-github-mirror-export/spec.md @@ -0,0 +1,145 @@ +--- +id: "041-github-mirror-export" +title: "GitHub mirror export and reconciliation: local facts back to GitHub, loop-safe and idempotent" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 2 +depends_on: + - "040-github-mirror-import" +establishes: + - "crates/hqgit-mirror/src/github/export.rs" + - "crates/hqgit-mirror/src/reconcile.rs" + - "crates/hqgit-mirror/tests/export.rs" +extends: + - { spec: "040-github-mirror-import", unit: "crates/hqgit-mirror/src/lib.rs", nature: additive } + # The client seam gains its write methods. + - { spec: "040-github-mirror-import", unit: "crates/hqgit-mirror/src/github/client.rs", nature: additive } +summary: > + The other direction of the wedge: comments, thread resolutions, + approvals, and issue field changes made in the ledger are pushed to + GitHub through the same client seam, so a reviewer who works offline in + hq still shows up on the pull request. Reconciliation is ordered by the + hybrid logical clock, idempotent through the id map, and loop-safe: an + item the exporter created is recognized by its external id on the next + import and produces no new fact. Where GitHub and the ledger disagree on + a derived field, the newer observation wins the field and both sides are + recorded as facts, so nothing is lost and the disagreement is auditable. + A dry-run mode prints the plan without writing. +--- + +# 041: GitHub mirror export and reconciliation + +## 1. Purpose + +Thesis D16 requires the mirror to be bidirectional: absorption fails if +using hqgit makes a contributor invisible on GitHub. Export closes the loop +without compromising constitution VII: facts remain immutable and derived +state converges by the same last-writer-wins rule the ledger uses +internally, extended to an external system whose clock is only observed. +The design goal is a fixed point: import after export produces nothing +new, and export after import produces nothing new. + +## 2. Territory + +`github/export.rs` and `reconcile.rs` in `crates/hqgit-mirror`, plus +`tests/export.rs`. Additively: the crate's `lib.rs` re-exports and the +write half of the `GitHubClient` seam in `github/client.rs`. The sync loop +that orders import and export, cursor persistence across runs, and the CLI +are spec 042. + +## 3. Behavior + +- **B-1 (client writes).** `GitHubClient` gains `fn create_issue_comment( + &self, number, body) -> Result<ExternalId>; fn create_review_comment( + &self, pr, commit_sha, path, line, side, body, in_reply_to: Option< + ExternalId>) -> Result<ExternalId>; fn submit_review(&self, pr, + commit_sha, event: ReviewEvent { Approve | Comment }, body) -> + Result<ExternalId>; fn resolve_thread(&self, thread: ExternalId, + resolved: bool) -> Result<()>; fn update_issue(&self, number, patch: + IssuePatch { title?, state?, labels?, assignees?, milestone? }) -> + Result<()>`. `FixtureClient` records every write into an in-memory + journal the tests inspect and reflects it in subsequent reads, so a + round trip can be asserted without a network. +- **B-2 (what exports).** Only facts in the `main` namespace issued by a + non-mirror principal export: `review.comment_posted` (as an issue comment + or a review comment depending on whether the thread has an anchor), + `review.thread_resolved` (as a thread resolution), an `hqgit/approval/v1` + attestation (as an approving review on the matching head sha), + `issue.field_set` on title, state, labels, assignee, milestone (as an + issue patch). Quarantined facts never export: exporting an observation + back to its source would be a loop by construction. +- **B-3 (ordering and idempotency).** `plan_export(repo, idmap) -> + ExportPlan` selects unexported facts (no `internal_to_external` mapping) + in spec 018 total order and produces `Vec<ExportItem>`; `apply(plan, + client, idmap) -> ExportReport` performs each item, records the returned + external id in the map in the same transaction as the write's local + acknowledgement, and continues past a failed item, listing it. Re-running + `plan_export` after a complete `apply` yields an empty plan. +- **B-4 (loop safety).** Every export records the external id; on the next + import (040) an item whose external id is mapped to a local fact produces + no new fact and no new `mirror.observed`. The exporter also stamps the + comment body with a trailing invisible marker line `<!-- hqgit:<fact-hash-prefix> -->` + so a lost id map can still recognize its own writes (`recognize_own(body) + -> Option<Hash>`), which import consults before mapping lookup. +- **B-5 (conflict rule).** `reconcile.rs` compares, per derived field, the + ledger's `LwwRegister` (019) with the observed GitHub value and its + `updated_at`: when GitHub's observation is newer than the ledger's write, + the importer appends an `issue.field_set` from the mirror principal (so + the ledger converges to GitHub) and the exporter does not push the older + local value; when the ledger's write is newer, export pushes it. Both + values remain in the ledger as facts; `reconcile_report(repo) -> + Vec<Disagreement { field, local, remote, winner }>` lists every + disagreement seen in the last run. Time comparison uses the HLC wall + component against GitHub's timestamp converted to milliseconds; a tie + goes to the ledger. +- **B-6 (dry run).** `apply(plan, client, idmap)` with `DryRun` performs no + write and returns the plan rendered as `ExportReport { would: Vec< + ExportItem> }`; spec 042 exposes it as `--dry-run`. +- **B-7 (bodies and erasure).** Comment bodies are read from the object + store at export time; an erased body (020) is never exported, and a + previously exported comment whose body is later erased is edited on + GitHub to the text `[erased]` through `update_issue_comment` (added to + the seam) when the token permits, else reported. + +## 4. Functional requirements + +- **FR-001.** `tests/export.rs` covers: each B-2 mapping through the + `FixtureClient` journal; quarantined facts never export; export then + import is a fixed point (zero new facts, zero new writes); a plan + re-planned after apply is empty; a failed write is reported and the rest + proceed; own-write recognition by the marker line when the id map is + wiped; the B-5 conflict rule in both directions and on a tie; dry run + writes nothing; an erased body is replaced on GitHub. +- **FR-002.** `plan_export` and `reconcile` are pure over `(fold state, + id map snapshot, observations)`; the client and the map are injected. +- **FR-003.** Export never issues an `hqgit/approval/v1` attestation and + never writes to a namespace; it only calls the client and the id map. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-mirror --locked --test export` passes. +- **AC-2.** On the spec 040 `basic` fixture, after importing and then + appending a local comment and an approval, `apply` writes exactly two + items and a subsequent import appends zero facts. + +## 6. Out of scope + +The sync loop, cursor persistence, backoff, and the CLI (042); promotion of +mirrored facts (094); exporting changes as new pull requests (a later spec +once the git endpoint, 092, exists); any source other than GitHub. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-mirror --locked --test export +``` diff --git a/specs/042-mirror-sync-command/spec.md b/specs/042-mirror-sync-command/spec.md new file mode 100644 index 0000000..0dc4221 --- /dev/null +++ b/specs/042-mirror-sync-command/spec.md @@ -0,0 +1,137 @@ +--- +id: "042-mirror-sync-command" +title: "hq mirror: the sync loop with cursors, backoff, and reports" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 2 +depends_on: + - "041-github-mirror-export" + - "032-cli-skeleton" +establishes: + - "crates/hqgit-mirror/src/sync.rs" + - "crates/hqgit-cli/src/cmd_mirror.rs" + - "crates/hqgit-cli/tests/mirror.rs" +extends: + - { spec: "040-github-mirror-import", unit: "crates/hqgit-mirror/src/lib.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + # hqgit-mirror joins the CLI's dependencies. + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } +summary: > + The user-facing end of wave 2: hq mirror add registers a GitHub + repository as a source, hq mirror sync runs import then export once or + in a watch loop, and hq mirror status shows cursors and the last report. + The sync driver persists cursors in the id map database so every run + resumes where the last one stopped, backs off exponentially on rate + limits and transient errors and never busy-loops, and prints a report of + facts imported, items exported, items skipped, and disagreements + reconciled. The token comes from the environment or the user config and + never enters the ledger, the repository config, or any output. +--- + +# 042: hq mirror + +## 1. Purpose + +Specs 040 and 041 are libraries; this spec makes the mirror a thing a +person runs. The loop's discipline matters more than its verbs: cursors so +a run is incremental, backoff so a rate-limited run parks instead of +hammering, and a report so the operator sees what moved and what did not +(the honesty posture claude-observatory's own thesis names: unknown is +displayed as unknown). Wave 2 ends when a GitHub repository can be kept in +sync from a laptop with one command. + +## 2. Territory + +`sync.rs` in `crates/hqgit-mirror` (the driver) and `cmd_mirror.rs` plus +`tests/mirror.rs` in `crates/hqgit-cli`. Additively: the mirror crate's +`lib.rs`, the CLI dispatch and clap tree, and the CLI manifest gaining +`hqgit-mirror`. Promotion of mirrored facts to `main` is spec 094; a +server-side scheduled mirror is a later spec over the same driver. + +## 3. Behavior + +- **B-1 (sources).** `hq mirror add github <owner>/<name> [--clone <path>]` + records `MirrorSource { source: "github", owner, name, clone: Option< + PathBuf> }` in `.hq/config.toml` under `[[mirror.sources]]` and creates + the mirror principal (040 B-2) if absent. `hq mirror remove github + <owner>/<name>` deletes the config entry; facts and the id map are kept. + At most one source per `(source, owner, name)`. +- **B-2 (token).** The token is read from `HQ_GITHUB_TOKEN`, else from + `$XDG_CONFIG_HOME/hq/credentials.toml` (`[github] token = ...`, mode + `0600`, refused with exit `3` when group or world readable). It is never + written to `.hq/`, never printed, and never included in any report or + fact. +- **B-3 (`hq mirror sync`).** `hq mirror sync [--once] [--watch [--interval + <secs>]] [--dry-run] [--source github:<owner>/<name>]` runs + `SyncDriver::run_once` for each configured source (or the named one): + import (040) from the persisted cursor, then export (041), then + reconcile, persisting the new cursor only after a successful import so a + failed run re-imports the same window. `--once` is the default. `--watch` + repeats every `interval` seconds (default 60) until interrupted, with the + backoff of B-4 layered on top. `--dry-run` runs the import against a + transaction that is rolled back and the export in dry-run mode, printing + the plan. +- **B-4 (backoff).** On `Error::Stale` carrying a rate-limit reset the + driver sleeps until the reset instant plus one second; on a transient + transport error it retries with exponential backoff (base 5 s, factor 2, + cap 300 s, at most 6 attempts per run) and then reports the failure; on + any other error it stops the run and reports. The driver never spins: + every wait is journaled to stderr with its reason and duration. +- **B-5 (cursors).** `sync.rs` stores per-source `Cursor { issues_since, + pulls_since, etag_by_url: BTreeMap<String, String>, last_run: Hlc, + last_report_hash: Hash }` in the id map's `cursors` table (040 B-7). + `hq mirror status` prints every source with its cursor, the last report, + and the count of quarantined facts awaiting promotion. +- **B-6 (report).** `SyncReport { source, imported: BTreeMap<String, u32>, + exported: u32, skipped: Vec<...>, disagreements: Vec<Disagreement>, + waited_ms: u64, cursor_advanced: bool }` per source, printed in human + mode as a short table and in JSON mode as an array; the report's hash is + stored in the cursor so `status` can show whether anything changed since. +- **B-7 (exit codes).** Exit `0` when every source completed (even with + skipped items), `1` when any source ended in a reported failure, `3` on + configuration or credential errors. + +## 4. Functional requirements + +- **FR-001.** `SyncDriver` takes the client, the clock (a `ClockSource` + from 018 for waits), and a `Sleeper` seam so tests observe backoff + without real delays. +- **FR-002.** `tests/mirror.rs` drives the binary against the spec 040 + fixtures: `add` then `sync --once` imports `basic`; a second `sync + --once` imports and exports nothing (`cursor_advanced: false`); a + simulated rate limit produces the documented wait and then completes; a + world-readable credentials file exits `3`; `status --json` shows the + cursor and the report hash; `--dry-run` leaves the ledger unchanged. +- **FR-003.** No test and no code path prints or stores the token; a test + greps every output and every file under `.hq/` for the fixture token. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-cli --locked --test mirror` passes. +- **AC-2.** Against the recorded fixtures, `hq mirror sync --once` run + twice appends zero facts the second time and reports it. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0 with the + new modules claimed here. + +## 6. Out of scope + +Promotion out of quarantine (094), a server-hosted mirror schedule (later, +over this driver), webhooks, and sources other than GitHub. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-cli --locked --test mirror +cargo test -p hqgit-mirror --locked sync +``` diff --git a/specs/050-stacked-changes/spec.md b/specs/050-stacked-changes/spec.md new file mode 100644 index 0000000..b767771 --- /dev/null +++ b/specs/050-stacked-changes/spec.md @@ -0,0 +1,201 @@ +--- +id: "050-stacked-changes" +title: "Stacked changes: dependency facts, a deterministic stack order, restack plans, and interdiff" +status: approved +kind: "feature" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 3 +depends_on: + - "024-change-and-revision" +establishes: + - "crates/hqgit-domain/src/stack.rs" + - "crates/hqgit-domain/src/interdiff.rs" + - "crates/hqgit-domain/tests/stack.rs" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } + # The new change.dependency_dropped kind joins the frozen fact-kind listing + # that 023 keeps in 011's golden vector directory (023 FR-002). + - { spec: "011-canonical-encoding", unit: "crates/hqgit-types/testdata/vectors/", nature: additive } +summary: > + Graphite exists because the branch pointer is the wrong unit of change + (design §1.1 point 2). With Change and Revision in place (024), a stack + is nothing more than a relation between changes: change.depends_on and + change.dependency_dropped facts converge per edge as a last-writer-wins + register, StackView folds them, and the stack order is a topological + sort with ties broken on ChangeId so every replica prints the same + stack. A restack is a pure plan (which revision goes onto which base, in + which order) that the CLI executes through the git bridge; the domain + never rebases. Interdiff answers "what changed since I last looked" by + comparing what two revisions of one change each did to their base at + the tree level, so base movement alone produces an empty interdiff. +--- + +# 050: Stacked changes + +## 1. Purpose + +Design §1.1 point 2 promises that stacked changes become native rather +than a tooling cottage industry, and that "what changed since I last +looked" becomes a first-class query. Thesis §4.3 gives the noun (a Change +with ordered Revisions); this spec adds the relation between changes and +the two queries reviewers actually run: the order of a stack, and the +difference between two revisions of one change with base movement +factored out. Everything here is a fold over facts (constitution VII) or a +pure function over trees (013), so the CLI and the server (constitution +XIII) compute identical answers offline and online. + +## 2. Territory + +`stack.rs` (the edge register, `StackView`, `Stack`, restack plans, the +builders) and `interdiff.rs` (`tree_diff` and `interdiff`) in +`crates/hqgit-domain`, plus `tests/stack.rs`. Additively: `lib.rs` +re-exports; `facts.rs` gains the semantics of the reserved +`change.depends_on` kind and one new kind, `change.dependency_dropped`; +the `domain/fact-kinds.json` listing gains that row. + +## 3. Behavior + +- **B-1 (facts).** `change.depends_on { change, on, at_revision }` (023 + B-2) declares that `change` is stacked on `on` as of `at_revision`. + `change.dependency_dropped { change: ChangeId, on: ChangeId, extra }` + is a new kind, `v = 1`, registered by `register_domain`. Validation + (023 B-4 extended): `change != on`, every id 32 bytes; a violation is + `Error::Validation` naming the field. +- **B-2 (edges converge).** Each ordered pair `(change, on)` is an `Edge { + active: LwwRegister<bool>, at_revision: RevisionId, declared_at: Hlc }`. + `change.depends_on` sets `active` to `true` and `change.dependency_dropped` + sets it to `false`, each at the fact's `Hlc` with the entry's issuer key + as the tiebreak (019 B-4), so a concurrent declare and drop converge on + every replica. The fold rejects nothing (024 B-4). +- **B-3 (`StackView`).** Implements `DerivedState` (019 B-5) over the two + kinds: `edges: BTreeMap<(ChangeId, ChangeId), Edge>`, `below: + BTreeMap<ChangeId, BTreeSet<ChangeId>>` (active dependencies), `above` + (the reverse), `warnings: Vec<StackWarning>`. `dependencies_of(change)` + and `dependents_of(change)` return active edges only. An edge that + closes a cycle through active edges is applied (it is a fact) and + recorded as `StackWarning::Cycle { members: Vec<ChangeId> }` in id order. +- **B-4 (`Stack`).** `stack_of(&self, changes: &ChangeView, change: + &ChangeId) -> Result<Stack, Error>` takes the component of active edges + reachable from `change` in both directions, drops edges into a `Merged` + change (a merged dependency is satisfied: it is part of the base now) + and edges into an `Abandoned` change (recorded as + `StackWarning::DependsOnAbandoned { change, on }`), and orders the rest + bottom-up by Kahn's algorithm with the ready set a `BTreeSet<ChangeId>`, + so ties break on id exactly as 018 breaks entry ties on hash. `Stack { + entries: Vec<StackEntry>, warnings: Vec<StackWarning> }` with + `StackEntry { change: ChangeId, depth: u16, depends_on: + BTreeSet<ChangeId>, revision: RevisionId, base_status: BaseStatus }`; + `depth` is the longest path from a bottom entry, `revision` is 024's + `latest_revision`. A component containing a cycle yields + `Err(Error::Validation)` naming the members: no order exists. +- **B-5 (`BaseStatus`).** `Current | Stale { expected: Cid, actual: Cid } + | Unresolved`. For an entry with exactly one dependency `d`, `expected` + is the `tree` of `d`'s latest revision and `actual` is the entry's + latest revision `base`; `Current` when equal, `Stale` otherwise. A + bottom entry is `Current` (nothing to compare against). An entry with + several dependencies is `Unresolved`: its base is a merge tree the + domain cannot compute. +- **B-6 (builders are pure).** `declare_dependency(view: &StackView, + changes: &ChangeView, change, on, at_revision) -> Result<DomainFact, + Error>` refuses `change == on`, an `on` the `ChangeView` does not know, + an `at_revision` that is not a revision of `change`, an `on` that is + `Abandoned`, and any `on` that already reaches `change` through active + edges (cycle refusal, the error names the path). `drop_dependency(view, + change, on) -> Result<DomainFact, Error>` refuses an edge that is not + active. Both return facts for the caller to append (021). +- **B-7 (restack plans).** `restack(stack: &Stack, changes: &ChangeView, + new_base: Option<Cid>) -> RestackPlan` with `RestackPlan { steps: + Vec<RestackStep> }` and `RestackStep { change: ChangeId, revision: + RevisionId, current_base: Cid, onto: Onto }`, `Onto` being `Tree(Cid) | + ResultOf(ChangeId) | MergeOf(Vec<ChangeId>)`. With `Some(b)` every entry + is a step: bottom entries go onto `Tree(b)` (skipped when their base + already equals `b`), others onto `ResultOf` their single dependency or + `MergeOf` their several. With `None` only `Stale` and `Unresolved` + entries and everything above them are steps. Steps follow the stack + order. The plan is a description: executing it (rebasing the tree, + submitting the new revision through 024 `submit_revision`) belongs to + the CLI and the git bridge (031). +- **B-8 (`tree_diff`).** `tree_diff(store: &dyn ObjectStore, before: &Cid, + after: &Cid) -> Result<TreeDiff, Error>` walks two 013 `Tree`s in entry + name order, descending into a subtree only when the two sides' cids + differ, and returns `TreeDiff { entries: BTreeMap<String, PathChange> }` + keyed by repo-relative POSIX path with `PathChange { before: + Option<(EntryMode, Cid)>, after: Option<(EntryMode, Cid)> }` and + `PathChange::kind() -> ChangeKind { Added | Removed | Modified }`. Only + leaf entries appear; no entry has `before == after`; a missing tree + object is `Error::NotFound`; a tree whose `get` fails verification + propagates `Error::Crypto`. +- **B-9 (`interdiff`).** `interdiff(store: &dyn ObjectStore, from: + &Revision, to: &Revision) -> Result<Interdiff, Error>` requires + `from.change == to.change` (`Error::Validation`). Let `F = + tree_diff(from.base, from.tree)` and `T = tree_diff(to.base, to.tree)`. + For every path in either: identical `PathChange`s are dropped; a path + only in `T` is `Introduced`; only in `F` is `Dropped`; in both with equal + `after` is dropped (the same result, however the base moved); in both + with different `after` is `Reworked { from: Option<(EntryMode, Cid)>, + to: Option<(EntryMode, Cid)>, base_moved: bool }` where `base_moved` is + `F.before != T.before`. `Interdiff { change: ChangeId, from: + RevisionId, to: RevisionId, base_moved: bool, entries: BTreeMap<String, + InterdiffEntry> }` with `base_moved = from.base != to.base` and + `is_empty()`. A path neither revision touched never appears, so pure + base movement yields an empty interdiff. `interdiff_since(store, + changes: &ChangeView, change, seen: &RevisionId)` is the reviewer's + form: `from = seen`, `to = latest_revision`. +- **B-10 (no ambient input).** Every function is a pure function of its + arguments plus reads through the `ObjectStore` seam; no clock, + environment, `HashMap`, or float appears (010 B-11). + +## 4. Functional requirements + +- **FR-001.** Tests in `tests/stack.rs`: `declare_dependency` refuses + self, unknown, abandoned, wrong revision, and a two-hop cycle with the + path named; a cycle arriving through concurrent declarations folds to a + `Cycle` warning and `stack_of` returns `Err`; concurrent declare and drop + converge to the greater `(hlc, key)`; `stack_of` over a diamond (one + bottom, two middles, one top) gives the same order under three arrival + permutations and breaks the middle tie on id; a merged dependency drops + out and an abandoned one warns; `BaseStatus` flips to `Stale` when the + lower change gains a revision; `restack` with `Some` and with `None` + produce the expected step lists; `tree_diff` on a fixture skips an + identical subtree (asserted through a counting `ObjectStore` wrapper) + and lists leaves only; `interdiff` on the same edit replayed over a + moved base is empty with `base_moved == true`, and detects `Introduced`, + `Dropped`, and `Reworked`. +- **FR-002.** A property test (`proptest`) folds a random valid dependency + history through two total-order-preserving permutations and asserts + equal views and equal stack orders. +- **FR-003.** `domain/fact-kinds.json` gains `change.dependency_dropped` + and 023 FR-002's exact-match test still passes. +- **FR-004.** `lib.rs` re-exports `StackView`, `Stack`, `StackEntry`, + `BaseStatus`, `RestackPlan`, `TreeDiff`, `Interdiff`, and the builders. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked stack` passes. +- **AC-2.** The golden stack fixture in `tests/stack.rs` (a four-change + diamond with one drop and one merge) folds to the recorded `Stack`, + warnings included, from every tested permutation. + +## 6. Out of scope + +Executing a restack (the git bridge, 031, and a later CLI feature spec +for `hq stack`); a line-level three-way interdiff (the tree-level result +here is what a later refinement and the review UI, 095, render); +semantic content of an interdiff (051); stack rows in SQL (081); merge +ordering of a stack (076). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked +``` diff --git a/specs/051-semantic-deltas/spec.md b/specs/051-semantic-deltas/spec.md new file mode 100644 index 0000000..a173f30 --- /dev/null +++ b/specs/051-semantic-deltas/spec.md @@ -0,0 +1,223 @@ +--- +id: "051-semantic-deltas" +title: "Semantic deltas: API surface, dependency, and capability changes as attestations" +status: approved +kind: "feature" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 3 +depends_on: + - "027-attestation-primitive" + - "050-stacked-changes" + - "025-semantic-anchors" +establishes: + - "crates/hqgit-domain/src/delta/mod.rs" + - "crates/hqgit-domain/src/delta/api_surface.rs" + - "crates/hqgit-domain/src/delta/dependencies.rs" + - "crates/hqgit-domain/src/delta/capabilities.rs" + - "crates/hqgit-domain/tests/delta.rs" + - "crates/hqgit-domain/testdata/deltas/" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + # The reserved hqgit/semantic-delta/v1 claim schema gets its shape here. + - { spec: "027-attestation-primitive", unit: "crates/hqgit-domain/src/predicate.rs", nature: additive } + # Manifest and lockfile parsing needs toml and serde_json. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The line diff is the lowest-value view of a change (design §1.1 point + 3). For a pair of trees this spec computes the three views a reviewer + wants first: the public API surface delta (items added, removed, and + re-signatured, via tree-sitter for Rust and TypeScript), the dependency + delta (Cargo and npm manifests and lockfiles, new registry or git + sources called out), and the capability delta (network, filesystem, + process, environment, secret, unsafe, and dynamic-code access by static + heuristics that deliberately over-report). Each delta is issued as an + Attestation under hqgit/semantic-delta/v1 (constitution IX), so policy + and the UI consume evidence rather than a rendering. The result is + deterministic for two trees and a pinned analyzer, and a file it cannot + analyze is listed as unsupported, never silently skipped. +--- + +# 051: Semantic deltas + +## 1. Purpose + +Thesis §4.3 and design §1.1 point 3: the high-value views of a change are +deltas of API surface, dependency set, and capability set, and review +should start from them. Thesis §8 accepts that semantic review is +per-language (Rust and TypeScript first, everything else degrading +visibly). This spec turns those views into evidence: an attestation with +a typed claim, signed by whoever computed it, replayable against the two +trees it names. Constitution IX forbids a new noun, so a delta registers a +predicate and nothing else. + +## 2. Territory + +The `delta/` module in `crates/hqgit-domain`: `mod.rs` (`DeltaSet`, the +claim, issuance, the unsupported vocabulary), `api_surface.rs`, +`dependencies.rs`, `capabilities.rs`; `tests/delta.rs`; and the fixture +subtree `testdata/deltas/`. Additively: `lib.rs` re-exports; the +`hqgit/semantic-delta/v1` claim schema and validator in 027's +`predicate.rs` (replacing the opaque reservation); `toml` pinned exact in +`[workspace.dependencies]` (plus `serde_json` unless 032 already added +it), inherited by the crate manifest. + +## 3. Behavior + +- **B-1 (entry point).** `compute_deltas(store: &dyn ObjectStore, + from_tree: &Cid, to_tree: &Cid) -> Result<DeltaSet, Error>` with + `DeltaSet { from_tree: Cid, to_tree: Cid, analyzer: Analyzer, api: + ApiSurfaceDelta, dependencies: DependencyDelta, capabilities: + CapabilityDelta }`. Touched paths come from 050 `tree_diff`; only + touched files are read. `Analyzer { name: String, version: String, + grammars: BTreeMap<String, String> }` records the crate version + (`env!("CARGO_PKG_VERSION")`) and the exact grammar crate versions 025 + pins, so a claim names the analyzer that produced it. +- **B-2 (never silence).** Every touched file ends in exactly one place: + analyzed, or in the relevant delta's `unsupported: Vec<Unsupported>` + with `Unsupported { path: String, reason: UnsupportedReason }` and + `UnsupportedReason` a closed enum `Language(String) | ParseErrors | + Binary | TooLarge { bytes: u64 } | Erased | Lockfile(String)`. `Binary` + is a NUL byte in the first 8 KiB; `TooLarge` is over 4 MiB; `Erased` is + a 020 tombstone; `Language` carries the extension. +- **B-3 (items).** `api_surface::extract_items(language, path: &str, + bytes: &[u8]) -> Result<Vec<Item>, Error>` for Rust and TypeScript + through 025's `syntax::parse`. `Item { path: String, name: String, kind: + ItemKind, visibility: Visibility, signature: String, signature_hash: + Hash, anchor: Anchor }`; `name` is qualified within the file + (`outer::Inner::method` in Rust, `Outer.method` in TypeScript); + `ItemKind` is `Function | Method | Struct | Enum | Union | Trait | + TraitImpl | TypeAlias | Const | Static | Module | Macro | Class | + Interface | Variable | ReExport`; `Visibility` is `Public | Restricted | + Private`. Rust `Public` is unrestricted `pub` (plus `#[macro_export]`), + `pub(crate)`, `pub(super)`, and `pub(in ..)` are `Restricted`; TypeScript + `Public` is any `export` form. Public methods and fields of a public + type are part of its signature and items of their own. `signature` is + the item's source with function bodies and comments removed and + whitespace runs collapsed to one space; `signature_hash = + Hash::of(signature)`; `anchor` is 025 `anchor_at` over the item node. +- **B-4 (`ApiSurfaceDelta`).** `{ added: Vec<Item>, removed: Vec<Item>, + changed: Vec<ItemChange { before: Item, after: Item }>, unsupported }` + over `Public` items only, keyed by `(path, kind, name)`, every list + sorted by that key. A removed file removes all its items; a renamed + file appears as removals plus additions (path-keyed by design; cross-file + identity is 083). +- **B-5 (`DependencyDelta`).** `Dep { ecosystem: Ecosystem, manifest: + String, name: String, version: String, source: Source, scope: Scope, + locked: Option<String> }` with `Ecosystem` `Cargo | Npm`, `Source` + `Registry(String) | Git { url: String, rev: Option<String> } | + Path(String) | Workspace`, `Scope` `Normal | Dev | Build | Optional | + Peer`. Parsed: `Cargo.toml` at any depth (`dependencies`, + `dev-dependencies`, `build-dependencies`, `target.*.dependencies`, + `workspace.dependencies`), `Cargo.lock` (`[[package]]` name, version, + source, filling `locked`), `package.json` (the four dependency maps), + `package-lock.json` (v2 and v3 `packages`); `yarn.lock`, `pnpm-lock.yaml`, + `go.mod`, `requirements.txt`, and `pyproject.toml` are + `Unsupported::Lockfile` by name. + `DependencyDelta { added, removed, version_changed: Vec<DepChange { + before: Dep, after: Dep }>, source_changed: Vec<DepChange>, new_sources: + Vec<Source>, unsupported }` keyed by `(ecosystem, manifest, name)`; + version comparison is string inequality (no semver arithmetic); + `new_sources` lists every `Registry` or `Git` source present in `to` and + absent from `from` across all manifests. +- **B-6 (`CapabilityDelta`).** `Capability` is a closed enum `Network | + Filesystem | Process | Environment | Secret | Unsafe | DynamicCode`. + `CapabilityUse { capability, path, evidence: String, anchor: Anchor }` + where `evidence` is the matched import path, call, or block kind. The + heuristic table is fixed in `capabilities.rs` and pinned by a golden + listing (`testdata/deltas/capabilities/rules.json`): Rust `std::net`, + `tokio::net`, `reqwest`, `hyper`, `ureq` are `Network`; `std::fs`, + `tokio::fs` `Filesystem`; `std::process`, `tokio::process`, `libc` + `Process`; `std::env` `Environment`; `unsafe` blocks and functions + `Unsafe`; `libloading` `DynamicCode`. TypeScript imports of `net`, + `http`, `https`, `dns`, `tls`, `fetch(`, `WebSocket` are `Network`; `fs`, + `fs/promises` `Filesystem`; `child_process` `Process`; `process.env` + `Environment`; `eval(` and `new Function(` `DynamicCode`; the `node:` + prefix is stripped. `Secret` is any `Environment` use whose key or + binding name contains, case-insensitively, one of `secret`, `token`, + `password`, `passwd`, `api_key`, `apikey`, `credential`, + `private_key`. `CapabilityDelta { gained: Vec<CapabilityUse>, lost: + Vec<CapabilityUse>, unsupported }` compares the per-file use sets keyed + by `(capability, path, evidence)`. No data flow is traced: a name match + counts, so the delta over-reports by design. +- **B-7 (claim and issuance).** `SemanticDeltaClaim { from_tree: Cid, + to_tree: Cid, from_revision: Option<RevisionId>, to_revision: + RevisionId, analyzer: Analyzer, delta: Delta, extra }` with `Delta` an + externally tagged enum `Api(ApiSurfaceDelta) | + Dependencies(DependencyDelta) | Capabilities(CapabilityDelta)`; every + struct derives `Canonical` (011). `predicate.rs` registers its validator + for `hqgit/semantic-delta/v1` in `register_builtin`: required fields, + sorted and duplicate-free lists, `Cid` codecs `DagCbor`, ids 32 bytes. + `DeltaSet::claims(&self, from_revision, to_revision) -> + [SemanticDeltaClaim; 3]` and `issue_deltas(store: &dyn ObjectStore, + claims: &[SemanticDeltaClaim], issuer: Principal, signer: &impl Signer, + at: Hlc) -> Result<Vec<(Attestation, DomainFact)>, Error>` store each + claim as a `DagCbor` object, sign with `subject = to_revision` (027 + B-3), and return the `attestation.issued` facts for the caller to append + (021). The issuer is the computing principal: the server's `Service` + identity, or offline the local identity (033); policy (065) decides + which issuers count, the domain refuses none. +- **B-8 (determinism).** For equal `(from_tree, to_tree, analyzer)` the + three claims are byte-identical, independent of store insertion order + and of the order `tree_diff` is walked; no clock, environment, `HashMap`, + or float appears; `BTreeMap` and sorted `Vec`s only. Equal trees produce + three empty deltas, still issued, so "no change" is evidence too. + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/deltas/api/{rust,typescript}/<case>/` + (`before.<ext>`, `after.<ext>`, `expected.json`) cover: item added, + removed, signature changed, body-only change (no delta), visibility + widened and narrowed, nested method, re-export, default export, file + removed, file renamed, parse errors, an unknown extension, a binary + file, and an oversize file. +- **FR-002.** Fixtures under `testdata/deltas/deps/{cargo,npm}/<case>/` + (`before/`, `after/`, `expected.json`) cover: added, removed, version + changed, dev scope, workspace inheritance, a new git source, a new + registry, lockfile-only resolution change, and a `pnpm-lock.yaml` + reported unsupported. +- **FR-003.** Fixtures under `testdata/deltas/capabilities/{rust,typescript}/` + cover each `Capability` gained and lost, the secret heuristic on a key + and on a binding, and `rules.json` matches the table in code exactly. +- **FR-004.** `testdata/deltas/claims/semantic-delta-api.json` records the + canonical bytes and hash of one claim; frozen (constitution VIII): a + shape change is a predicate `v2`, never an edit. +- **FR-005.** Tests: double computation yields identical bytes; the + validator accepts every fixture claim and rejects a missing field, an + unsorted list, and a `Raw` tree cid; `issue_deltas` produces three + attestations whose signatures verify (027 B-6) and whose subjects equal + `to_revision`; a fuzz-style test feeds random bytes through + `extract_items` for both languages without panic. +- **FR-006.** `lib.rs` re-exports `compute_deltas`, `DeltaSet`, + `SemanticDeltaClaim`, `Delta`, the three delta types, `Item`, + `Capability`, and `Unsupported`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked delta` passes with every + fixture in FR-001 to FR-003. +- **AC-2.** `cargo test -p hqgit-domain --locked attestation` still passes + with the semantic-delta validator installed. + +## 6. Out of scope + +Conflict detection over deltas (052); cross-file and cross-repo symbol +identity and downstream impact (083, 084); observed test behavior (074); +rendering the deltas ahead of the line diff (095); policies that require +a delta (066); more languages and ecosystems (a later spec per language, +each pinning its grammar). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked +``` diff --git a/specs/052-semantic-conflicts/spec.md b/specs/052-semantic-conflicts/spec.md new file mode 100644 index 0000000..c445284 --- /dev/null +++ b/specs/052-semantic-conflicts/spec.md @@ -0,0 +1,187 @@ +--- +id: "052-semantic-conflicts" +title: "Semantic conflicts: pairwise conflict detection over deltas and anchors, with text kept separate" +status: approved +kind: "feature" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 3 +depends_on: + - "051-semantic-deltas" +establishes: + - "crates/hqgit-domain/src/conflict.rs" + - "crates/hqgit-domain/tests/conflict.rs" + - "crates/hqgit-domain/testdata/conflicts/" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + # Line-level hunks for the Text kind come from imara-diff, pinned exact. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Conflict detection should be semantic, not textual (design §1.1 point + 3). Given two revisions and their common base tree, this spec computes + a deterministic ConflictReport from the 051 deltas and the 025 syntax + trees: SameSymbolEdited when both sides edit one definition, + SignatureChangedAndCalled when one side changes a signature the other + side calls, DependencyVersionDiverged when both move one dependency to + different versions or sources, and CapabilityOverlap when both gain the + same capability in one file. Textual overlap is computed too, from + line hunks, but reported as its own Text kind so a merge that is clean + in git and broken in meaning, or the reverse, is never mistaken for the + other. Two changes editing different functions in one file produce no + conflict of either kind; changing a signature the other calls does. +--- + +# 052: Semantic conflicts + +## 1. Purpose + +Git reports conflicts where bytes overlap and stays silent where meaning +collides: one change renames a parameter, another adds a caller, and the +merge is clean until it fails to compile. Design §1.1 point 3 asks for the +inverse. With deltas (051) and anchors (025) in hand, this spec answers +"do these two revisions conflict, and in what sense" as a pure function +the CLI, the merge queue (076), and the review UI (095) all call. Keeping +the textual and semantic kinds apart is the point: each is honest about +what it can see. + +## 2. Territory + +`conflict.rs` in `crates/hqgit-domain` (the `Conflict` vocabulary, +severity, symbol edits, callee extraction, hunk overlap, the report and +its entry point), `tests/conflict.rs`, and the fixture subtree +`testdata/conflicts/`. Additively: `lib.rs` re-exports, `imara-diff` +pinned exact in `[workspace.dependencies]` and the crate manifest. + +## 3. Behavior + +- **B-1 (entry point).** `semantic_conflicts(store: &dyn ObjectStore, a: + &Revision, b: &Revision, base: &Cid) -> Result<ConflictReport, Error>` + refuses `a.id == b.id` (`Error::Validation`). `base` is the common base + tree the caller determined (for stacked siblings the tree below them; + for a merge candidate the main tree via 031); the revisions' own `base` + fields are not consulted. It computes `tree_diff(base, a.tree)`, + `tree_diff(base, b.tree)` (050 B-8), `compute_deltas(base, a.tree)`, and + `compute_deltas(base, b.tree)` (051 B-1), then applies B-3 to B-7. +- **B-2 (vocabulary and severity).** `Conflict` is a closed enum: + `SameSymbolEdited { path, symbol: SymbolKey, a: SymbolEdit, b: SymbolEdit + }`, `SignatureChangedAndCalled { changed_by: Side, path, symbol: + SymbolKey, call_sites: Vec<CallSite> }`, `DependencyVersionDiverged { + manifest, name, base: Option<Dep>, a: Option<Dep>, b: Option<Dep> }`, + `CapabilityOverlap { capability, path, a: Vec<CapabilityUse>, b: + Vec<CapabilityUse> }`, `Text { path, kind: TextConflictKind }`. `Side` + is `A | B`. `Severity` is `#[repr(u8)]` `Advisory = 1 | Likely = 2 | + Blocking = 3` and `Conflict::severity()` is fixed: + `SignatureChangedAndCalled`, `DependencyVersionDiverged`, and `Text` + (except `ModeDivergence`, `Likely`) are `Blocking`; `SameSymbolEdited` + is `Likely`; `CapabilityOverlap` is `Advisory`. Integer weights only. +- **B-3 (symbol edits).** `symbol_edits(before: &[Item], after: &[Item]) + -> BTreeMap<SymbolKey, SymbolEdit>` over 051 `extract_items` at every + visibility (private symbols conflict too), with `SymbolKey { path, kind: + ItemKind, name }` and `SymbolEdit` `Added { after: Hash } | Removed | + Modified { signature_changed: bool, after: Hash }` where `after` is the + item node's content hash (025). `SameSymbolEdited` fires when both + sides carry an edit for one key and the edits are not identical; two + sides making the same edit (equal `after`) converge and do not + conflict. +- **B-4 (`SignatureChangedAndCalled`).** For every symbol one side + `Modified { signature_changed: true }` or `Removed`, the other side's + added or modified items are scanned for calls to it. `callees_in(tree: + &Tree, range: ByteRange) -> Vec<CallSite { callee: String, range: + ByteRange }>` collects, in Rust, `call_expression` targets (the last + segment of a scoped identifier), `method_call_expression` names, and + `macro_invocation` names; in TypeScript, `call_expression` identifiers + and member properties and `new_expression` constructors. Matching is by + the symbol's short name; no cross-file resolution (083), so the kind + over-reports by design. Fires only when the calling item is itself an + edit of the other side; an untouched pre-existing caller is the + compiler's job, not a conflict between these two revisions. +- **B-5 (`DependencyVersionDiverged`).** From the two `DependencyDelta`s: + one key `(ecosystem, manifest, name)` added, version-changed, or + source-changed on both sides with different `(version, source, + locked)`; or removed on one side and changed on the other. Identical + results on both sides converge. +- **B-6 (`CapabilityOverlap`).** Both sides gain the same `Capability` in + the same `path` (051 B-6). Advisory: each side's reviewer approved one + introduction and the merged tree carries both. +- **B-7 (`Text`).** `TextConflictKind` is `BothModified { a: LineRange, + b: LineRange } | ModifyDelete { deleted_by: Side } | BothAdded | + ModeDivergence | Binary` with `LineRange { start: u32, end: u32 }` + (1-based, half-open, in base line coordinates). For a path both sides + modified, `line_hunks(before: &[u8], after: &[u8]) -> Vec<Hunk>` runs + `imara-diff` with `Algorithm::Myers` over lines split on `\n` (a `\r` + stays in its line); two hunks conflict when their base ranges intersect + or are adjacent (git's rule), and every such pair is one `BothModified`. + `BothAdded` requires different content; identical additions or identical + modifications converge. A NUL in the first 8 KiB of either side makes + the path `Binary` when both modified it. `ModifyDelete` and + `ModeDivergence` come from the two `TreeDiff`s alone. This kind is + computed at file and line granularity and says nothing about meaning; + B-3 to B-6 say nothing about bytes. +- **B-8 (report).** `ConflictReport { a: RevisionId, b: RevisionId, base: + Cid, conflicts: Vec<Conflict>, unsupported: Vec<Unsupported>, extra }` + derives `Canonical` (011) so a later spec can attest it under its own + predicate; `conflicts` is sorted by `(severity desc, path, kind + ordinal, symbol)`, `unsupported` is the union of the two `DeltaSet`s' + lists (051 B-2), and the report offers `is_clean()`, `max_severity() -> + Option<Severity>`, and `by_kind()`. `semantic_conflicts(a, b)` and + `semantic_conflicts(b, a)` produce reports equal up to swapping `Side`. +- **B-9 (determinism).** A pure function of `(store contents, a, b, + base)`; no clock, environment, `HashMap`, or float; every list sorted. + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/conflicts/<case>/` hold `base/`, + `a/`, `b/` file trees and an `expected.json` report; `tests/conflict.rs` + loads each into a `MemoryStore` (013) and compares. Cases, for both + Rust and TypeScript where a language applies: different functions in + one file (no conflict of any kind); same function edited differently + (`SameSymbolEdited` and `BothModified`); same function edited + identically (clean); signature changed by A and a new caller added by B + (`SignatureChangedAndCalled`, no `Text`); signature changed with an + untouched pre-existing caller (clean); a private helper edited by both + (`SameSymbolEdited`); dependency moved to two versions + (`DependencyVersionDiverged`) and to one version (clean); both gain + `Network` in one file (`CapabilityOverlap`); adjacent hunks + (`BothModified`); one side deletes a file the other edits + (`ModifyDelete`); both add one path with different bytes (`BothAdded`); + a binary both touched (`Binary`); an unknown extension edited by both + (`Text` only, listed in `unsupported`). +- **FR-002.** Tests: symmetry of B-8 on every fixture; double evaluation + yields identical canonical bytes; `line_hunks` on a golden pair matches + recorded hunks so an `imara-diff` bump is visible; the sort order of + B-8 on a synthetic report with every kind. +- **FR-003.** A test pins the exact `imara-diff` version against + `Cargo.lock`, mirroring 025 FR-002. +- **FR-004.** `lib.rs` re-exports `semantic_conflicts`, `ConflictReport`, + `Conflict`, `Severity`, `Side`, and `TextConflictKind`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked conflict` passes with + every fixture in FR-001. +- **AC-2.** The two named cases of the plan hold: different functions in + one file report nothing; a changed signature the other side calls + reports `SignatureChangedAndCalled` at `Blocking`. + +## 6. Out of scope + +Performing or resolving a merge (031; 076 for the queue); attesting a +report (076, or a later spec registering a predicate); cross-file and +cross-repo symbol resolution (083, 084); conflicts among more than two +revisions (076 evaluates candidates pairwise and in batches); rendering +(095); languages beyond Rust and TypeScript (a later spec per language). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked +``` diff --git a/specs/060-identity-and-key-rotation/spec.md b/specs/060-identity-and-key-rotation/spec.md new file mode 100644 index 0000000..7b36b6c --- /dev/null +++ b/specs/060-identity-and-key-rotation/spec.md @@ -0,0 +1,170 @@ +--- +id: "060-identity-and-key-rotation" +title: "Identity as a keypair with a rotation chain recorded in the ledger" +status: approved +kind: "kernel" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 4 +depends_on: + - "019-facts-and-derived-state" +establishes: + - "crates/hqgit-trust/Cargo.toml" + - "crates/hqgit-trust/src/lib.rs" + - "crates/hqgit-trust/src/identity.rs" + - "crates/hqgit-trust/src/rotation.rs" + - "crates/hqgit-trust/src/resolver.rs" + - "crates/hqgit-trust/tests/" + - "crates/hqgit-trust/testdata/identity/" +summary: > + Authentication and identity are different problems (thesis §4.5). Login is + OIDC (061); durable identity is a keypair whose rotation history is a + sequence of signed facts in the ledger, so a signature made under a key + that was later rotated or revoked still verifies at the time it was made. + This spec founds hqgit-trust and fixes the three identity facts + (identity.created, identity.key_rotated, identity.key_revoked), the + IdentityId that every Principal id in 010 resolves to, the IdentityView + fold that answers key_valid_at, and the RotationAwareResolver that plugs + into the ledger's IssuerResolver seam (017) so chain verification stops + trusting a static key table. Constitution XI: trust is checkable, not + decorative. +--- + +# 060: Identity and key rotation + +## 1. Purpose + +Design doc §1.1 point 5: attribution today is an email string and signing +is decorative. hqgit makes identity a cryptographic object with history: a +`Principal` (010 B-6) is the hash of the fact that created it, its current +key is whatever the rotation chain says it is, and the chain lives in the +same ledger as everything it signs. Without this spec, every signature in +the corpus is verified against a static table (017's `StaticResolver`), +which cannot express rotation, revocation, or compromise windows. With it, +"who signed this, and was that key theirs at the time" is a fold over +facts, replayable by any replica. + +## 2. Territory + +`crates/hqgit-trust` as founded here: the manifest (workspace dependencies +`hqgit-types` and `hqgit-ledger` only), `lib.rs`, `identity.rs` (the facts, +`IdentityId`, `IdentityView`), `rotation.rs` (the chain rules and the +cosignature), `resolver.rs` (`RotationAwareResolver`), the `tests/` subtree, +and the frozen chain vectors under `testdata/identity/`. OIDC binding is +061, the transparency log is 062, keyless certificates are 063, and full +attestation verification is 064; each extends this crate. + +## 3. Behavior + +- **B-1 (fact kinds).** Three fact kinds, registered with the 019 + `FactRegistry` by `register_trust(registry)`, with frozen kind strings: + `identity.created`, `identity.key_rotated`, `identity.key_revoked`. Each + body is a `Canonical` struct (011) with an `extra` map for unknown-field + preservation. +- **B-2 (`identity.created`).** Body `{ kind: PrincipalKind, initial_key: + PublicKey, display: String, extra }`. The entry carrying it MUST be signed + by `initial_key` (issuer equals `KeyId::of(initial_key)`): an identity is + self-certifying. `IdentityId = Hash::of(canonical bytes of the fact + envelope)`; the 010 ids (`HumanId`, `AgentId`, `ServiceId`, `OrgId`) are + `IdentityId`s tagged by `kind`. `display` is descriptive only and never + enters any trust decision. +- **B-3 (`identity.key_rotated`).** Body `{ identity: IdentityId, prev: + KeyId, next: KeyId, next_pub: PublicKey, effective: Hlc, cosign: + Signature, extra }`. The entry MUST be signed by `prev`, which MUST be the + identity's active key at `effective`; `cosign` MUST be `next`'s signature + under `SignDomain("identity.rotation")` over the canonical bytes of the + body with `cosign` absent (proof of possession of the next key). + `effective` MUST be greater than the previous chain fact's `effective`. + A rotation makes `prev` `Rotated` from `effective` onward and `next` + `Valid` from `effective` onward. +- **B-4 (`identity.key_revoked`).** Body `{ identity: IdentityId, key: + KeyId, reason: Revocation, effective: Hlc, since: Option<Hlc>, extra }` + with `Revocation` a closed enum `Compromise | Retired | Lost`. The entry + MUST be signed by the identity's active key at `effective` or by any key + that succeeds `key` in the chain (a compromised key is revoked by its + successor). `since`, when present, MUST be less than or equal to + `effective` and marks the start of the compromise window: signatures at or + after `since` are invalid. Revoking the only active key freezes the + identity: no further facts for it are accepted. +- **B-5 (`IdentityView`).** A `DerivedState` (019) folding the three kinds + in total order (018). `fn key_valid_at(&self, key: &KeyId, at: &Hlc) -> + KeyValidity` with `KeyValidity` a closed enum `Valid | NotYet(effective) | + Rotated(effective) | Revoked { effective, since } | Unknown`. A `Rotated` + key is `Valid` for `at` before its rotation (historical signatures keep + verifying); a `Revoked` key is invalid for `at` at or after `since` + (or `effective` when `since` is absent). `fn identity_of(&self, key) -> + Option<IdentityId>`, `fn active_key(&self, identity, at) -> + Option<(KeyId, PublicKey)>`, `fn chain(&self, identity) -> Vec<ChainLink>`. +- **B-6 (chain defects are recorded, never applied).** A fact that violates + B-2 through B-4 (issuer is not the active key, missing or wrong cosign, + non-monotonic `effective`, unknown identity, a rotation after a freeze) + MUST NOT alter the view; it is recorded as a `ChainDefect { entry, rule, + detail }` on the view so a verifier (064) and an operator can see the + attempt. The fold never panics on a malformed body: it records + `ChainDefect` with rule `Malformed`. +- **B-7 (resolver).** `RotationAwareResolver { view: IdentityView }` + implements 017's `IssuerResolver`: `verifier_for(key, at)` returns an + `Ed25519Verifier` when `key_valid_at(key, at)` is `Valid`, otherwise + `Error::Crypto` naming the validity state and the identity. It is the + resolver the CLI (034) and the server (090) MUST use once this spec + lands; `StaticResolver` remains for tests and for bootstrapping a repo + whose identity facts are being verified for the first time (the fold + validates its own chain, B-2 to B-4, so no external key table is needed). +- **B-8 (no ambient input).** Nothing in this crate reads a clock or the + environment; `effective` values are supplied by the caller (the CLI's + `HlcGenerator`, 018). `BTreeMap` is the only map type (010 B-11). + +## 4. Functional requirements + +- **FR-001.** Every function in `identity.rs`, `rotation.rs`, and + `resolver.rs` is pure over its arguments; the only trait objects are the + 010 `Signer` and `Verifier` seams and 017's `IssuerResolver`. +- **FR-002.** Golden chain vectors under `testdata/identity/`: + `created.json`, `rotated-once.json`, `rotated-twice.json`, + `revoked-compromise.json`, `forged-rotation.json`, each carrying seeds, + the fact bodies, the expected canonical bytes and entry hashes, and the + expected `KeyValidity` answers at named `Hlc` probes. A test re-derives + every field. These vectors are frozen (constitution VIII). +- **FR-003.** Tests cover: a signature made before rotation verifies through + the resolver and one made after fails; a revocation with `since` earlier + than `effective` invalidates the window; a rotation without a valid + `cosign` is a `ChainDefect` and leaves the view unchanged; a revocation + signed by a key outside the chain is a defect; the frozen identity accepts + no further facts; permutation of entry arrival order yields the same view. +- **FR-004.** `IdentityView` folds a ledger of 10,000 identity facts in + linear time; the chain per identity is stored as a `Vec<ChainLink>` + indexed by `IdentityId` in a `BTreeMap`. +- **FR-005.** The crate depends on `hqgit-types` and `hqgit-ledger` only + within the workspace, and its manifest carries + `[package.metadata.spec-spine] spec = "060-identity-and-key-rotation"`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-trust --locked` passes, vectors included. +- **AC-2.** `spec-spine index` discovers `hqgit-trust` bound to this spec + and `index coverage --fail-on-untraced` exits 0. +- **AC-3.** Ledger verification (017 B-7) of a fixture DAG containing a + rotation passes with `RotationAwareResolver` and fails with a + `StaticResolver` holding only the initial key, proving the resolver is + load-bearing. + +## 6. Out of scope + +OIDC login and subject binding (061); the transparency log (062); keyless +short-lived certificates (063); end-to-end attestation verification (064); +agent registration and delegation (100, 101); any UI for key management. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-trust --locked +cargo clippy -p hqgit-trust --all-targets --locked -- -D warnings +``` diff --git a/specs/061-oidc-login/spec.md b/specs/061-oidc-login/spec.md new file mode 100644 index 0000000..b9a8c7d --- /dev/null +++ b/specs/061-oidc-login/spec.md @@ -0,0 +1,167 @@ +--- +id: "061-oidc-login" +title: "OIDC login: authorization code with PKCE, device flow, and subject binding" +status: approved +kind: "feature" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 4 +depends_on: + - "060-identity-and-key-rotation" +establishes: + - "crates/hqgit-trust/src/oidc.rs" + - "crates/hqgit-trust/src/session.rs" + - "crates/hqgit-trust/tests/oidc.rs" + - "crates/hqgit-trust/testdata/oidc/" +extends: + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/src/lib.rs", nature: additive } + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Login is OIDC; identity is a keypair (060). This spec adds the OIDC + client (discovery, authorization code with PKCE, the device authorization + grant for the CLI) with an injected HTTP seam so every flow is testable + against a mock issuer, and the binding that ties an OIDC subject to a + ledger identity: an identity.binding_added fact signed by the identity's + active key, so a login proves control of a subject the issuer vouches for + and the fact proves control of a key the ledger already knows. Workforce + federation is a per-repository allow-list of issuers, with Rauthy as the + self-hosted reference. No password is handled anywhere in the crate. +--- + +# 061: OIDC login + +## 1. Purpose + +Thesis §4.5 separates authentication from identity: OIDC (Rauthy as the +self-hosted answer) handles login and workforce federation; the ledger +holds the identity. The two meet at a binding fact. This spec supplies the +client half and the binding so that a human at a browser or a terminal can +prove to a server (090 onward) and to a certificate issuer (063) that a +given OIDC subject controls a given identity. Nothing here decides +authorization; it establishes who is speaking. + +## 2. Territory + +Two modules added to `crates/hqgit-trust`: `oidc.rs` (issuer configuration, +discovery, the two grants, token validation) and `session.rs` (the binding +facts, `BindingView`, and the in-memory `Session`), plus their tests and +the mock-issuer fixtures under `testdata/oidc/`. Adds the `openidconnect` +crate (pinned) to the workspace dependency table and to the crate manifest, +and re-exports through `lib.rs`. + +## 3. Behavior + +- **B-1 (issuer configuration).** `IssuerConfig { issuer_url: String, + client_id: String, client_secret: Option<String>, scopes: Vec<String>, + allowed_audiences: Vec<String>, extra }` and `FederationConfig { issuers: + Vec<IssuerConfig> }` read from the repository config (021 `.hq/config.toml` + table `[trust.oidc]`). A login against an issuer not in the list MUST be + refused with `Error::Config` before any network call. The spec body + carries a reference configuration for Rauthy (issuer URL shape, a public + client for the CLI, a confidential client for the server). +- **B-2 (HTTP seam).** Every network interaction goes through an injected + `HttpClient` trait (`fn request(&self, req: HttpRequest) -> + Result<HttpResponse, Error>`); the production implementation is a thin + wrapper the binary supplies, and tests use a `MockIssuer` replaying the + fixtures under `testdata/oidc/` (a discovery document, a JWKS with a + fixture signing key, recorded token responses). No test touches the + network. +- **B-3 (discovery).** `discover(cfg, http) -> Result<IssuerMetadata>` + fetches `<issuer>/.well-known/openid-configuration`, requires `issuer` to + equal `cfg.issuer_url` exactly, requires `code` and + `urn:ietf:params:oauth:grant-type:device_code` among the grant types + the config will use, and caches the JWKS by `kid`. +- **B-4 (authorization code with PKCE).** `AuthCodeFlow::start(cfg, meta, + redirect_uri, entropy: [u8; 32]) -> (AuthorizeUrl, PendingAuth)` builds + the request with `code_challenge_method=S256`, a `state`, and a `nonce` + derived from the caller-supplied entropy (the crate reads no randomness + itself); `AuthCodeFlow::finish(pending, callback_params, http) -> + Result<OidcSubject>` exchanges the code and validates the id token (B-6). + `state` mismatch is `Error::Crypto`. +- **B-5 (device authorization grant).** `DeviceFlow::start(cfg, meta, http) + -> Result<DeviceCode { user_code, verification_uri, interval, expires_in + }>`; `DeviceFlow::poll(pending, http) -> Result<PollState>` with + `PollState` a closed enum `Pending | SlowDown | Complete(OidcSubject) | + Denied | Expired`. The caller owns the sleep; the crate never sleeps or + reads a clock. This is the CLI's flow (032's config and 093's remote + login consume it). +- **B-6 (token validation).** An id token is accepted only when: the JWS + signature verifies against a JWKS key with the token's `kid`; `iss` + equals the configured issuer; `aud` contains the client id or an allowed + audience; `exp` is later than the caller-supplied `now: u64` seconds + (injected, never read); `nonce` matches when a nonce was sent; `sub` is + non-empty. The result is `OidcSubject { issuer: String, sub: String, + email: Option<String>, claims_hash: Hash }` where `claims_hash` is the + hash of the canonical encoding of the verified claim set, never the raw + token. Access and refresh tokens are returned to the caller as opaque + strings and never written to the ledger or to disk by this crate. +- **B-7 (binding facts).** Two fact kinds registered by `register_trust` + (060): `identity.binding_added` with body `{ identity: IdentityId, + issuer: String, sub: String, claims_hash: Hash, bound_at: Hlc, extra }` + and `identity.binding_removed` with body `{ identity, issuer, sub, + removed_at: Hlc, extra }`. Both entries MUST be signed by the identity's + active key at the fact's `Hlc` (060 B-5); a binding signed by any other + key is a `ChainDefect` and never applied. A binding fact is produced only + by `bind(subject: &OidcSubject, identity, signer, hlc)` after a completed + flow, so the fact exists only when both proofs were made in one session. +- **B-8 (`BindingView`).** A `DerivedState` fold answering `identity_for( + issuer, sub) -> Option<IdentityId>` (the most recent unremoved binding) + and `bindings_of(identity) -> Vec<Binding>`. One subject binds to at most + one identity at a time; binding an already-bound subject to a second + identity is recorded as a defect until the first is removed. An identity + MAY hold bindings from several issuers (workforce federation). +- **B-9 (`Session`).** `Session { subject: OidcSubject, identity: + IdentityId, established_at: Hlc, expires_at: u64 }` is an in-memory value + the server (090) and the certificate issuer (063) consume; it is never + persisted by this crate and never becomes a fact. +- **B-10 (no passwords).** No type in this crate holds a password, and no + flow other than B-4 and B-5 exists. Resource-owner password credentials + are refused at the type level (there is no constructor). + +## 4. Functional requirements + +- **FR-001.** All flow state machines are pure over `(config, metadata, + injected entropy, injected now, responses)`; the `HttpClient` seam is the + only I/O boundary. +- **FR-002.** Fixtures under `testdata/oidc/`: `discovery.json`, + `jwks.json`, the fixture signing key, and recorded responses for a + successful code exchange, a successful device flow, a `slow_down`, an + `authorization_pending`, an `access_denied`, and an expired token. +- **FR-003.** Tests cover: discovery issuer mismatch refused; PKCE + challenge equals `S256(verifier)`; `state` mismatch; nonce mismatch; a + token signed by an unknown `kid`; `aud` mismatch; `exp` in the past; + device flow state transitions including `SlowDown` back-off signal; + binding signed by a rotated-out key recorded as a defect; a subject + bound twice; federation refusal for an unlisted issuer. +- **FR-004.** `claims_hash` is stable across two validations of the same + token (canonical claim ordering) and differs when any claim differs. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-trust --locked oidc` passes against the + mock issuer with no network access (the test binary is run with + networking disabled in CI where the runner supports it). +- **AC-2.** A fixture repository gains an `identity.binding_added` fact + through `bind`, and `BindingView::identity_for` returns the identity for + the fixture subject. + +## 6. Out of scope + +Serving OIDC (hqgit is a relying party, never an issuer); browser and +server session cookies (093, 095); the certificate issuance that consumes +a session (063); agent credentials, which are never OIDC-derived (100). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-trust --locked oidc +``` diff --git a/specs/062-transparency-log/spec.md b/specs/062-transparency-log/spec.md new file mode 100644 index 0000000..11355b7 --- /dev/null +++ b/specs/062-transparency-log/spec.md @@ -0,0 +1,160 @@ +--- +id: "062-transparency-log" +title: "Transparency log: Merkle log, inclusion and consistency proofs, signed checkpoints" +status: approved +kind: "kernel" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 4 +depends_on: + - "060-identity-and-key-rotation" +establishes: + - "crates/hqgit-trust/src/tlog/mod.rs" + - "crates/hqgit-trust/src/tlog/merkle.rs" + - "crates/hqgit-trust/src/tlog/proofs.rs" + - "crates/hqgit-trust/src/tlog/client.rs" + - "crates/hqgit-trust/tests/tlog.rs" + - "crates/hqgit-trust/testdata/tlog/" +extends: + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/src/lib.rs", nature: additive } +summary: > + An append-only Merkle log in the RFC 6962 shape over BLAKE3, holding + attestation ids and identity facts, with inclusion proofs, consistency + proofs, and checkpoints signed by the log key and cosignable by + witnesses. A signature proves who; the log proves that the signed thing + was published and cannot later be quietly withdrawn or forked. This spec + fixes the leaf and node hashing, the proof shapes, the checkpoint + envelope, and the TlogClient seam with an in-process implementation; + keyless signing (063) logs its certificates here and attestation + verification (064) requires an inclusion proof when the repository policy + demands one. The served log is a later extension. +--- + +# 062: Transparency log + +## 1. Purpose + +Thesis §4.5 names transparency-log inclusion proofs as part of the default +signing shape, and design doc §1.1 point 5 lists a transparency log among +the things that turn trust from decorative into checkable. A signature +alone lets a signer deny having published; a log with consistency proofs +makes publication a public, append-only fact that any verifier can hold the +log operator to. This spec builds that log as a library so a host (090 and +later) can serve it and any client can verify against it offline. + +## 2. Territory + +The `tlog` module tree inside `crates/hqgit-trust`: `merkle.rs` (leaf and +node hashing, the in-memory tree, roots at any size), `proofs.rs` +(inclusion and consistency proof types and their pure verifiers), +`client.rs` (the `TlogClient` trait, `Checkpoint`, cosignatures, and the +`InProcessLog`), `mod.rs` (re-exports and `LogEntry`), the tests, and the +frozen proof vectors under `testdata/tlog/`. Serving the log over the +network and persisting it durably belong to a later spec that extends +`client.rs`. + +## 3. Behavior + +- **B-1 (leaves).** `LogEntry` is a closed enum `Attestation(AttestationId) + | IdentityFact(EntryHash) | Certificate(Hash)` (the third is for 063), + encoded canonically (011) as `[tag: u8, hash]`. The leaf hash is + `Hash::of(0x00 || canonical_bytes)`; an interior node hash is + `Hash::of(0x01 || left || right)`. The empty tree root is + `Hash::of(b"")`. These are frozen (constitution VIII). +- **B-2 (tree).** `MerkleTree` holds leaves in append order and MUST + answer `root_at(size) -> Hash` for any `size <= len` in `O(log n)` using + the RFC 6962 split rule (the largest power of two strictly less than + `size` as the left subtree). `append(leaf) -> u64` returns the leaf + index. Leaves are never removed or reordered. +- **B-3 (inclusion proofs).** `InclusionProof { leaf_index: u64, tree_size: + u64, path: Vec<Hash> }`; `prove_inclusion(tree, index, size)` and the + pure `verify_inclusion(proof, leaf_hash, root) -> Result<(), Error>` + recompute the root from the leaf and the path and compare; a mismatch, + an index at or beyond `tree_size`, or a path of the wrong length is + `Error::Crypto`. +- **B-4 (consistency proofs).** `ConsistencyProof { old_size: u64, + new_size: u64, path: Vec<Hash> }`; `prove_consistency(tree, old, new)` + and the pure `verify_consistency(proof, old_root, new_root) -> + Result<(), Error>` per RFC 6962 §2.1.2. A verifier that has trusted a + checkpoint at `old_size` MUST require a consistency proof before trusting + one at `new_size` from the same log; failure is evidence of a fork. +- **B-5 (checkpoints).** `Checkpoint { origin: String, tree_size: u64, + root: Hash, at: Hlc, sig: Signature, cosigs: Vec<Cosignature>, extra }` + where `origin` is the log's identity (an `IdentityId` from 060, rendered + as hex), `sig` is the log key's signature under + `SignDomain("tlog.checkpoint")` over the canonical bytes with `sig` and + `cosigs` absent, and `Cosignature { witness: KeyId, sig: Signature }` is + a witness's signature over the same bytes under + `SignDomain("tlog.cosign")`. `verify_checkpoint(cp, log_verifier, + witness_policy: &WitnessPolicy) -> Result<(), Error>` requires the log + signature and at least `witness_policy.threshold` valid cosignatures + from `witness_policy.witnesses`. +- **B-6 (client seam).** `trait TlogClient { fn submit(&mut self, entry: + LogEntry) -> Result<Receipt, Error>; fn inclusion(&self, leaf_hash: &Hash) + -> Result<(InclusionProof, Checkpoint), Error>; fn latest_checkpoint(&self) + -> Result<Checkpoint, Error>; fn consistency(&self, old_size: u64, + new_size: u64) -> Result<ConsistencyProof, Error>; }` with `Receipt { + index: u64, leaf_hash: Hash, checkpoint: Checkpoint }`. Submitting a leaf + already present returns the existing index (idempotent). +- **B-7 (in-process log).** `InProcessLog { tree, signer: Box<dyn Signer>, + origin, witnesses: Vec<Box<dyn Signer>> }` implements `TlogClient` over a + `LogStore` trait (`append`, `len`, `leaf(i)`, `checkpoint`) with + `MemoryLogStore`; it issues a fresh checkpoint on every append, `at` + supplied by an injected `HlcGenerator` (018). It is the log the CLI (034 + onward) and tests use; a served, durable `LogStore` is the later spec's + extension. +- **B-8 (trusted checkpoints).** `TrustedCheckpoints { by_origin: + BTreeMap<String, Checkpoint> }` is the verifier-side record (064 holds + one per repository config): `advance(new_cp, consistency_proof) -> + Result<(), Error>` accepts a later checkpoint only with a valid + consistency proof from the recorded one; a smaller `tree_size` or a + failed proof is `Error::Crypto("tlog fork")`. +- **B-9 (no ambient input).** The tree and proofs read no clock; checkpoint + times come from the injected generator; `BTreeMap` only. + +## 4. Functional requirements + +- **FR-001.** `merkle.rs` and `proofs.rs` are pure; `client.rs` isolates + the only mutation behind `LogStore`. +- **FR-002.** Frozen vectors under `testdata/tlog/`: `roots.json` (roots for + sizes 0 through 8 over fixed leaves), `inclusion.json` (proofs for every + leaf at sizes 1 through 8), `consistency.json` (every `old <= new` pair + up to 8), `checkpoint.json` (a signed checkpoint with one cosignature and + its canonical bytes). A test re-derives every field. +- **FR-003.** Tests cover: root recomputation matches vectors; inclusion + proof for every leaf at every size; a tampered path element, a wrong + index, and a wrong `tree_size` each fail; consistency across ten + appends; a fork (two different trees at the same size) fails + `advance`; checkpoint signature and cosignature threshold; idempotent + submit; `TrustedCheckpoints` refuses a smaller size. +- **FR-004.** Property tests: for random leaf sets up to 1,000, every + inclusion proof verifies and every consistency pair verifies. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-trust --locked tlog` passes, vectors + included. +- **AC-2.** An `InProcessLog` seeded with the fixtures produces the frozen + `checkpoint.json` bytes exactly when driven with the fixture signer and + `Hlc`. + +## 6. Out of scope + +Serving the log over HTTP or gRPC and durable log storage (a later spec +extending `client.rs`, with the endpoint in 090's app); the certificates +logged as leaves (063); the verifier that demands inclusion (064); gossip +between witnesses (a federation concern, 112). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-trust --locked tlog +``` diff --git a/specs/063-keyless-signing/spec.md b/specs/063-keyless-signing/spec.md new file mode 100644 index 0000000..48c51fe --- /dev/null +++ b/specs/063-keyless-signing/spec.md @@ -0,0 +1,214 @@ +--- +id: "063-keyless-signing" +title: "Keyless signing: short-lived certificates bound to an OIDC identity, the signature bundle, and bundle verification" +status: approved +kind: "feature" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 4 +depends_on: + - "061-oidc-login" + - "062-transparency-log" +establishes: + - "crates/hqgit-trust/src/keyless.rs" + - "crates/hqgit-trust/src/bundle.rs" + - "crates/hqgit-trust/tests/keyless.rs" + - "crates/hqgit-trust/testdata/keyless/" +extends: + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/src/lib.rs", nature: additive } + # The `keyless` feature and the two optional dependencies join the manifest. + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The Sigstore shape, self-hosted: a session proven by OIDC login (061) is + exchanged for an X.509 certificate over an ephemeral ed25519 key that is + valid for ten minutes, the certificate is published to the transparency + log (062), and the attestation signed with that key carries a + SignatureBundle (certificate chain plus the certificate's inclusion + proof) in its extra map, so a verifier can check it long after the key + is gone. This spec fixes the certificate profile, the issuer seam with an + in-process reference issuer, the bundle's canonical form, keyless + issuance, and the pure bundle verifier that spec 064 calls as one stage + of its chain. The identity's long-lived key (060) remains the offline + signing path; keyless is the default when a session exists. +--- + +# 063: Keyless signing + +## 1. Purpose + +Thesis §4.5 and decision D11: signing defaults to the Sigstore shape, +short-lived certificates bound to an OIDC identity with transparency-log +inclusion proofs, run in-house when sovereignty matters. Design doc §1.1 +point 5 counts it among the things that make trust checkable rather than +decorative (constitution XI). Spec 061 proves who is logged in and spec +060 holds the durable key; this spec joins them for the common case where +a human signs from a machine that should never hold a long-lived secret. +A session (061 B-9) buys a certificate over a key that lives ten minutes, +the certificate is logged, and the attestation carries what a verifier +needs. Nothing about the long-lived key path changes. + +## 2. Territory + +`keyless.rs` (the ephemeral key, the `CertificateIssuer` seam, the +`InProcessIssuer`, `sign_keyless`, `verify_bundle`) and `bundle.rs` +(`SignatureBundle`, `TrustRoots`, the Sigstore bundle reader) inside +`crates/hqgit-trust`, both behind the `keyless` cargo feature; +`tests/keyless.rs`; and the fixtures under `testdata/keyless/`. Additively: +`lib.rs` re-exports, the crate manifest (the feature and two optional +dependencies), and the workspace table (`sigstore` and `x509-cert`, pinned +exact). Consuming the bundle inside the verification chain is spec 064; +obtaining a session is spec 061; the log is spec 062. + +## 3. Behavior + +- **B-1 (feature).** A cargo feature `keyless`, on by default, gates both + modules and makes `sigstore` and `x509-cert` optional dependencies. + `cargo build -p hqgit-trust --no-default-features --locked` MUST build. + Spec 064 compiles its keyless stage under the same feature. +- **B-2 (ephemeral key).** `EphemeralKey::generate(entropy: [u8; 32]) -> + EphemeralKey` wraps a 010 `Ed25519Signer`; `public() -> PublicKey`, + `key_id() -> KeyId`; it implements `Signer`. The seed is zeroized on + drop, has no `Debug` or serde, and is never written to disk by this + crate. `pop(&self, session: &Session) -> Signature` is the proof of + possession: a signature under `SignDomain("keyless.pop")` over + `session.subject.claims_hash || public key bytes`. +- **B-3 (issuer seam).** `CertRequest { session: Session, public: + PublicKey, proof: Signature, not_before: u64 }` (seconds since epoch, + injected). `trait CertificateIssuer { fn issue(&mut self, req: + &CertRequest) -> Result<CertChain, Error>; }`. An issuer MUST refuse an + invalid proof (`Error::Crypto`), `session.expires_at <= not_before` + (`Error::Validation`), and a `session.subject.issuer` outside its + allow-list (`Error::Config`). `CertChain { leaf: Vec<u8>, chain: + Vec<Vec<u8>>, inclusion: InclusionProof, checkpoint: Checkpoint }` holds + DER, leaf first, the root excluded. +- **B-4 (certificate profile).** X.509 v3; ed25519 SubjectPublicKeyInfo + (OID `1.3.101.112`); serial from the issuer's monotonic counter; + `notBefore = not_before`, `notAfter = not_before + 600`; empty subject; + SubjectAltName with exactly one `otherName` of type + `1.3.6.1.4.1.57264.1.7` carrying `sub` as UTF8String, plus an + `rfc822Name` when the session has an email; extension + `1.3.6.1.4.1.57264.1.8` carrying the OIDC issuer URL as a DER + UTF8String; KeyUsage `digitalSignature` (critical); ExtendedKeyUsage + `codeSigning`; BasicConstraints `CA = false`. The intermediate is ed25519 + with `pathLen = 0`; the root is ed25519. These OIDs are Sigstore's so + Fulcio-issued and hqgit-issued certificates are read by one parser. +- **B-5 (in-process issuer).** `InProcessIssuer { ca: CaMaterial, + allowed_issuers: Vec<String>, tlog: Box<dyn TlogClient>, serial: u64 }` + with `CaMaterial { root: Vec<u8>, intermediate: Vec<u8>, signer: Box<dyn + Signer> }` (the intermediate's key signs leaves). `issue` builds the + leaf per B-4, signs it, submits `LogEntry::Certificate(Hash::of(leaf))` + to the log (062 B-6), and returns the chain with the receipt's inclusion + proof and checkpoint. It is the reference issuer tests and the CLI use; + a served issuer is a later extension in the server (090 onward). +- **B-6 (`SignatureBundle`).** `SignatureBundle { v: u16, leaf: Vec<u8>, + chain: Vec<Vec<u8>>, inclusion: InclusionProof, checkpoint: Checkpoint, + extra }`, `v = 1`, a `Canonical` (011) envelope with kind + `"keyless.bundle"`; `to_value()` and `from_value()`. It rides in + `Attestation.extra["bundle"]` (027 B-1), so the attestation's signature + and id cover it. It carries the certificate's inclusion proof and MUST + NOT carry the attestation's own: the attestation id does not exist + before signing. Spec 064 fetches that proof from the log by leaf hash. +- **B-7 (`sign_keyless`).** `sign_keyless(unsigned: UnsignedAttestation, + key: &EphemeralKey, chain: &CertChain) -> Result<Attestation, Error>` + refuses an `extra["bundle"]` already present (`Error::Validation`), a + leaf whose key is not `key.public()` (`Error::Crypto`), and an + `unsigned.at.wall_ms / 1000` outside the leaf's validity window + (`Error::Validation`); it inserts the bundle and calls 027 + `Attestation::issue(unsigned, key)`, so `issuer_key` is the ephemeral + key id while `issuer` stays the session's ledger identity. + `log_attestation(att: &Attestation, tlog: &mut dyn TlogClient) -> + Result<Receipt, Error>` submits `LogEntry::Attestation(id)`; the caller + invokes it immediately after issuance. +- **B-8 (`verify_bundle`).** `verify_bundle(bundle: &SignatureBundle, + roots: &TrustRoots, issuer_key: &KeyId, at_secs: u64, trusted: + &TrustedCheckpoints) -> Result<KeylessIdentity, BundleFailure>` runs in + this fixed order and stops at the first failure: (1) DER parsing, + `Malformed`; (2) chain building from leaf through `chain` to a member of + `roots.roots` with every link's signature checked, `UntrustedChain`, or + `UnsupportedAlgorithm` for any non-ed25519 link; (3) `at_secs` inside + the leaf window and the window at most 600 s, `OutsideValidity`; (4) + KeyUsage, ExtendedKeyUsage, and `CA = false` per B-4, `BadLeafProfile`; + (5) `KeyId::of(leaf key) == issuer_key`, `KeyMismatch`; (6) the SAN + `otherName` and the issuer extension both present, `MissingSubject`; + (7) `verify_checkpoint(checkpoint, roots.log_key, roots.witness_policy)` + (062 B-5), `BadCheckpoint`; a trusted checkpoint of the same origin and + `tree_size` with a different root, `LogForked`; `verify_inclusion` of + the certificate leaf hash against `checkpoint.root`, `BadInclusion`. + Success is `KeylessIdentity { issuer: String, sub: String, leaf_hash: + Hash, not_before: u64, not_after: u64 }`. Mapping the subject to an + `IdentityId` through 061's `BindingView` is spec 064's stage. +- **B-9 (`TrustRoots`).** `TrustRoots { roots: Vec<Vec<u8>>, log_key: + PublicKey, witness_policy: WitnessPolicy, extra }`. The binary reads it + from `[trust.keyless]` in `.hq/config.toml` (021); this crate never reads + configuration. `TrustRoots::from_ca(&CaMaterial, log_key, + witness_policy)` builds one for tests. +- **B-10 (Sigstore interop).** `read_sigstore_bundle(json: &[u8]) -> + Result<ForeignBundle, Error>` parses the + `application/vnd.dev.sigstore.bundle.v0.3+json` media type through the + `sigstore` crate's bundle types into `ForeignBundle { leaf, chain, + signature: Vec<u8>, subject: Option<(String, String)>, rekor: Value }`. + Rekor entries hash with SHA-256, are carried opaquely, and are never + accepted as hqgit log proofs; a foreign ECDSA chain is + `UnsupportedAlgorithm` under B-8. The reader exists so the B-4 profile + stays readable by Sigstore tooling and vice versa. +- **B-11 (no ambient input).** Entropy, `not_before`, and `at_secs` are + injected; the crate reads no clock or randomness; `BTreeMap` only. The + identity-key path (027 `Attestation::issue` with the 060 key) is + untouched: offline signing needs neither an issuer nor a log. + +## 4. Functional requirements + +- **FR-001.** Every function is pure over its arguments; the only trait + objects are `Signer`, `Verifier`, `TlogClient`, and `CertificateIssuer`. +- **FR-002.** Fixtures under `testdata/keyless/`: `ca/` (root and + intermediate DER with their seeds), `leaf-valid.der`, and the bundles + `valid.json`, `expired.json`, `wrong-key.json`, `untrusted-root.json`, + `bad-inclusion.json`, `forked.json`, each recording the expected + `verify_bundle` answer; plus `sigstore-bundle-v0.3.json` produced by + sigstore-rs with its expected subject. +- **FR-003.** Tests cover: proof of possession accept and reject; each + issuer refusal of B-3; an issued leaf parsed back matches B-4 field by + field; the certificate leaf is included in the log; each `sign_keyless` + refusal; the bundle is covered by the signature (flipping one byte of it + fails 027 `verify_signature`); every `BundleFailure` variant; ordering + (a bundle that is both expired and key-mismatched reports + `OutsideValidity`); the Sigstore fixture yields its subject and + `UnsupportedAlgorithm`. +- **FR-004.** `EphemeralKey` has no `Debug` or `Serialize` impl (a + compile-fail test) and its seed is zeroized (a drop test on a copy of + the memory is not required; the `zeroize` derive is asserted by source). +- **FR-005.** The manifest declares `sigstore` with default features off + and only its bundle-parsing feature, and `x509-cert` with its `builder` + feature, both `optional = true` behind `keyless`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-trust --locked keyless` passes. +- **AC-2.** `cargo build -p hqgit-trust --no-default-features --locked` + passes. +- **AC-3.** An attestation signed with `sign_keyless` against the fixture + CA and an `InProcessLog` (062 B-7) round-trips through 027's canonical + codec with `extra["bundle"]` intact and its id unchanged. + +## 6. Out of scope + +The verification chain that consumes the bundle (064); the CLI login and +keyless-signing verbs (the wave 6 CLI extension beside 093's remote +login); serving the issuer and the log (090 onward); ECDSA chains; +namespace-key distribution (a wave 4 amendment to 060). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-trust --locked keyless +cargo build -p hqgit-trust --no-default-features --locked +``` diff --git a/specs/064-attestation-verification/spec.md b/specs/064-attestation-verification/spec.md new file mode 100644 index 0000000..4b86f05 --- /dev/null +++ b/specs/064-attestation-verification/spec.md @@ -0,0 +1,192 @@ +--- +id: "064-attestation-verification" +title: "Attestation verification: the fixed pipeline from signature to verified set" +status: approved +kind: "kernel" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 4 +depends_on: + - "060-identity-and-key-rotation" + - "062-transparency-log" + - "027-attestation-primitive" +establishes: + - "crates/hqgit-trust/src/verify.rs" + - "crates/hqgit-trust/src/verified.rs" + - "crates/hqgit-trust/src/verify_policy.rs" + - "crates/hqgit-trust/tests/verify.rs" + - "crates/hqgit-trust/testdata/verify/" +extends: + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/src/lib.rs", nature: additive } + # hqgit-domain joins the trust crate's dependencies (the Attestation type). + - { spec: "060-identity-and-key-rotation", unit: "crates/hqgit-trust/Cargo.toml", nature: additive } +summary: > + The one path by which an attestation becomes evidence. Spec 027 checks a + signature against a verifier it is handed; this spec fixes the whole + chain in one order: signature, issuer key valid at the attestation's + clock (060), keyless bundle when present (063), transparency-log + inclusion when the verify policy requires it (062), and claim shape + against the predicate registry (027). The output is VerifiedAttestation, + a type nothing outside this module can construct, gathered into a + VerifiedAttestationSet that carries a per-item verdict and never hides a + failure inside a partial success. That set is the only shape the policy + engine (065), the action cache (071), quarantine promotion (094), and the + projections (081) accept: verification happens here or it did not happen. +--- + +# 064: Attestation verification + +## 1. Purpose + +Constitution XI: trust is checkable, not decorative. Spec 027 made every +form of evidence one primitive; spec 060 made identity a keypair with a +rotation chain; spec 062 made issuance publicly logged; spec 063 made +short-lived keys usable. Each of those verifies one thing. A consumer that +composes them by hand will get the order wrong, skip a stage under load, or +accept a failed stage as a warning. This spec removes that freedom: there is +one pipeline, its stages run in one order, each stage's failure is a named +variant, and the result is a type that proves the pipeline ran. Thesis §4.5 +and design doc §1.1 point 5 describe the property; this spec is where it +becomes unavoidable. + +## 2. Territory + +`crates/hqgit-trust/src/verify.rs` (the pipeline, `verify_one`, +`verify_set`), `src/verified.rs` (`VerifiedAttestation`, +`VerifiedAttestationSet`, `Verdict`, the sealed constructor, the test +builders), `src/verify_policy.rs` (`VerifyPolicy`, `TrustAnchors`), the +`tests/verify.rs` file, and fixtures under `testdata/verify/`. Additively: +the crate's `lib.rs` re-exports and its `Cargo.toml` (the `hqgit-domain` +dependency, which keeps the dependency direction of thesis §5: +`hqgit-trust` sits above `hqgit-domain`, never the reverse). Policy +evaluation (065) consumes the set; nothing here evaluates policy. + +## 3. Behavior + +- **B-1 (`VerifyPolicy`).** `VerifyPolicy { require_tlog: bool, + require_keyless_for: BTreeSet<PrincipalKind>, max_clock_skew_ms: u64, + registry: PredicateRegistry }` and `TrustAnchors { identities: + IdentityView, roots: Option<TrustRoots>, checkpoints: Vec<Checkpoint> }`. + Both are plain data supplied by the caller; the pipeline reads no clock, + no environment, and no network. `VerifyPolicy::strict()` sets + `require_tlog = true` and requires keyless bundles for `Human` + principals; `VerifyPolicy::offline()` sets `require_tlog = false` (the + wave 1 CLI posture, 034). +- **B-2 (the pipeline, in order).** `verify_one(att: &Attestation, claim: + &Value, policy: &VerifyPolicy, anchors: &TrustAnchors, tlog: Option<&dyn + TlogClient>) -> Verdict` runs exactly these stages and stops at the first + failure: + 1. **Structure.** `att.subject`, `att.claim`, and `att.issuer_key` are + well-formed; `att.at` is not more than `max_clock_skew_ms` ahead of the + newest checkpoint in `anchors` when one exists. Failure: + `VerifyFailure::Malformed(String)`. + 2. **Signature.** `027::verify_signature` with an `Ed25519Verifier` for + `att.issuer_key`, taking the public key from `anchors.identities` + (060) or, when the attestation carries a keyless bundle in `extra`, + from the bundle's leaf certificate. Failure: + `VerifyFailure::BadSignature`. + 3. **Issuer.** `anchors.identities.key_valid_at(&att.issuer_key, + &att.at)` (060 B-5) is `Valid` and the identity that owns the key is + `att.issuer`. Failure: `VerifyFailure::KeyNotValidAt { key, at, + reason }` or `VerifyFailure::IssuerMismatch`. + 4. **Bundle.** When `extra` carries a `SignatureBundle` (063 B-6), + `063::verify_bundle` against `anchors.roots` must pass, and the + certificate's OIDC subject must bind to `att.issuer` through an + `identity.binding_added` fact (061). When the policy requires keyless + for the issuer's kind and no bundle is present: + `VerifyFailure::BundleRequired`. Failure otherwise: + `VerifyFailure::BadBundle(String)`. + 5. **Transparency.** When `policy.require_tlog` is true, `tlog` MUST be + `Some`, and `tlog.inclusion(&leaf_hash(LogEntry::Attestation(id)))` + (062 B-6) must return a proof that verifies against a checkpoint in + `anchors.checkpoints` or against `tlog.latest_checkpoint()` after a + consistency proof from a trusted one. Failure: + `VerifyFailure::NotLogged` or `VerifyFailure::LogUntrusted`. + 6. **Claim.** `027::verify_claim(att, claim, &policy.registry)`. An + unknown predicate passes this stage with `claim_checked: false` + recorded on the verified item (027 B-5 preserves unknown predicates); + a known predicate whose claim fails validation is + `VerifyFailure::BadClaim(String)`. + A stage never runs before every earlier stage passed; the order is a + frozen constant `STAGES: [&str; 6]` that tests assert against. +- **B-3 (`Verdict`).** `enum Verdict { Ok(VerifiedAttestation), + Failed { id: AttestationId, stage: &'static str, failure: VerifyFailure + } }`. `VerifyFailure` is a closed enum with the variants named in B-2 + plus `Erased` (the claim object is a tombstone, 020) and `Unavailable` + (the claim object is missing from the store). A verdict is never a + warning: it is `Ok` or it names the stage. +- **B-4 (`VerifiedAttestation`).** A struct holding the attestation, its + id, the decoded claim, `claim_checked: bool`, `logged: bool`, `keyless: + bool`, and `verified_under: VerifyPolicyDigest` (the hash of the policy + and anchors used, so a consumer can tell a strict verification from an + offline one). Its only constructor is private to `verify.rs`; the type is + `Clone` and serde-serializable so it can be journaled, but + deserialization yields an `UnverifiedRecord`, never a + `VerifiedAttestation`. Test builders (`verified::testing::verified(att, + claim)`) exist behind `#[cfg(feature = "testing")]` and are what 071, + 081, and 094 use in fixtures. +- **B-5 (`VerifiedAttestationSet`).** `verify_set(atts: &[(Attestation, + Value)], policy, anchors, tlog) -> VerifiedAttestationSet` runs + `verify_one` on every item and returns `{ ok: Vec<VerifiedAttestation>, + failed: Vec<Verdict>, policy_digest }`, both halves sorted by + attestation id. `is_clean()` is true iff `failed` is empty; `by_predicate + (&PredicateType)`, `by_subject(&Hash)`, and `issuers()` query the `ok` + half only. A consumer that wants "all approvals" gets only verified ones + and can see, separately, how many failed and why. +- **B-6 (determinism).** `verify_one` and `verify_set` are pure functions + of their arguments; the `TlogClient` is the only seam that may perform + I/O, and an in-process client makes every test hermetic. Two runs over + identical inputs yield identical verdicts and an identical + `policy_digest`. +- **B-7 (erasure).** A claim whose object has been erased (020) verifies + the signature and issuer stages (the commitment is intact) and then fails + at stage 6 with `Erased`; the attestation is thereby excluded from the + `ok` half. An erased approval is not an approval. + +## 4. Functional requirements + +- **FR-001.** Every `VerifyFailure` variant is reachable by a test and the + stage name reported with it matches `STAGES`. +- **FR-002.** Tests cover: a valid offline attestation; a valid strict + attestation with an in-process transparency log; a signature made before + a rotation still verifying and one made after a revocation failing at + stage 3; a keyless attestation passing with a good bundle and failing with + a tampered certificate; `require_tlog` refusing an unlogged attestation; + an unknown predicate passing with `claim_checked = false`; a known + predicate with a malformed claim failing at stage 6; an erased claim + failing with `Erased`; `verify_set` sorting and `is_clean`. +- **FR-003.** A compile-fail test (`trybuild`) asserts that + `VerifiedAttestation` cannot be constructed outside the crate. +- **FR-004.** `policy_digest` changes when any field of `VerifyPolicy` or + the anchor set changes, and is stable otherwise (golden fixture). +- **FR-005.** The crate depends on `hqgit-types`, `hqgit-ledger`, and + `hqgit-domain` only within the workspace. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-trust --locked --test verify` passes. +- **AC-2.** On the spec 033 fixture ledger, verifying the approval + attestation under `VerifyPolicy::offline()` yields `Ok`, and under + `VerifyPolicy::strict()` with an empty log yields `Failed` at + `transparency`. + +## 6. Out of scope + +Policy evaluation over the verified set (065); deciding which policy +applies (068); capability derivation from verified attestations (094); +the executor trust tier (071); a served transparency log (a later +extension of 062 on the server). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-trust --locked +``` diff --git a/specs/065-policy-engine/spec.md b/specs/065-policy-engine/spec.md new file mode 100644 index 0000000..a80e1b1 --- /dev/null +++ b/specs/065-policy-engine/spec.md @@ -0,0 +1,175 @@ +--- +id: "065-policy-engine" +title: "Policy engine: deterministic WASM merge predicates, hash-pinned" +status: approved +kind: "kernel" +domain: "l6-policy" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 4 +depends_on: + - "027-attestation-primitive" + - "024-change-and-revision" +establishes: + - "crates/hqgit-policy/Cargo.toml" + - "crates/hqgit-policy/src/lib.rs" + - "crates/hqgit-policy/src/engine.rs" + - "crates/hqgit-policy/src/abi.rs" + - "crates/hqgit-policy/src/module.rs" + - "crates/hqgit-policy/src/input.rs" + - "crates/hqgit-policy/tests/" + - "crates/hqgit-policy/testdata/policies/" +extends: + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The merge predicate as the thesis states it: f(change, attestation_set, + policy_version) -> Allow | Deny(reasons), deterministic and side-effect + free. This spec founds hqgit-policy: a policy is a WebAssembly module + stored as a content-addressed object whose Cid is its version; the engine + is wasmtime configured for determinism (no clocks, no randomness, no + filesystem, no network, no threads, fuel and memory limits, canonical + NaNs); the ABI is one exported function taking the canonical bytes of a + PolicyInput and returning the canonical bytes of a Verdict; and the input + is built from domain types plus the attestation summaries of a verified + set. Same input bytes and same module hash give the same output bytes, + which is what makes a verdict replayable years later (067). Writing + policies is the SDK's job (066); pinning them to a repository is 068's. +--- + +# 065: Policy engine + +## 1. Purpose + +Thesis §4.6 and decision D13: "requires two approvals" must stop being UI +state and become a checkable predicate over the evidence graph, expressed +as versioned executable code. Constitution XI forbids repository settings +as mutable toggles. That requires a policy to be a thing with a hash, an +evaluator that cannot smuggle in a clock or a network call, and an input +whose bytes are reproducible from the ledger. This spec supplies all three +and nothing else: it does not decide which policy governs a repository +(068) and does not record what a policy decided (067). + +## 2. Territory + +`crates/hqgit-policy` as founded here: the manifest (`[package.metadata. +spec-spine] spec = "065-policy-engine"`), `lib.rs`, `engine.rs` (the +wasmtime host), `abi.rs` (the wire contract and the `Verdict` type), +`module.rs` (loading, validating, and hashing a policy module), `input.rs` +(building a `PolicyInput` from domain types), the `tests/` subtree, and +prebuilt policy modules under `testdata/policies/` with their source +recorded beside them. Additively: `wasmtime` joins the workspace +dependency table. The crate depends on `hqgit-types` and `hqgit-domain` +only; it never depends on `hqgit-trust`, because it consumes the verified +set as data (064 B-5) and the caller is responsible for having verified it. + +## 3. Behavior + +- **B-1 (`PolicyModule`).** A policy is a core WebAssembly module (not a + component) compiled for `wasm32-unknown-unknown`, stored as a `Raw` + object (013) so `PolicyId = Cid` is its version. `PolicyModule::load + (bytes) -> Result<PolicyModule, Error>` validates the binary, refuses any + import (`Error::Policy("policy imports are forbidden: <name>")`), and + requires exactly the exports of B-3. The Cid is recomputed from the bytes + on load and compared to the caller's expectation when given. +- **B-2 (engine configuration).** `Engine::new(limits: Limits) -> Engine` + builds a `wasmtime::Config` with: `consume_fuel(true)`, + `cranelift_nan_canonicalization(true)`, `wasm_threads(false)`, + `wasm_simd(false)`, `wasm_relaxed_simd(false)`, `wasm_bulk_memory(true)`, + `wasm_reference_types(false)`, `epoch_interruption(false)`, `static + memory maximum = limits.memory_bytes`, and no WASI at all: no clock, no + randomness, no filesystem, no network, no environment reach the module. + `Limits { fuel: u64 (default 50_000_000), memory_bytes: u64 (default + 64 MiB), output_bytes: u32 (default 1 MiB) }`. The configuration is + exposed as `Engine::config_digest()`, a hash of every knob, so a verdict + records the engine it ran under. +- **B-3 (ABI).** The module MUST export `memory`, `alloc(len: i32) -> + i32`, and `evaluate(ptr: i32, len: i32) -> i64`. The host writes the + canonical bytes (011) of `PolicyInput` into memory obtained from + `alloc`, calls `evaluate`, and reads the result as a packed + `(ptr: u32 << 32) | len: u32` pointing at the canonical bytes of a + `Verdict`. An `evaluate` that traps, exhausts fuel, exceeds + `output_bytes`, or returns bytes that do not decode as a `Verdict` yields + `Verdict::Deny { reasons: ["engine: <cause>"] }`, never an error to the + caller: a policy that cannot run cannot allow. +- **B-4 (`PolicyInput`).** `PolicyInput { v: u16, change: ChangeSummary, + revision: RevisionSummary, attestations: Vec<AttestationSummary>, + policy: Cid, policy_version: u16, now: Hlc }` where + `ChangeSummary { id, opened_by, state, stack_depth }`, `RevisionSummary { + id, number, tree, base, submitted_by, at }`, and `AttestationSummary { + id, subject, predicate, issuer, issuer_kind: PrincipalKind, at, + claim: Value, claim_checked: bool, logged: bool, keyless: bool }`. + `now` is the HLC of the fact that triggers the evaluation, never a clock + read. `input.rs` builds it: `build_input(change: &Change, revision: + &Revision, verified: &VerifiedAttestationSetView, policy: Cid, now: Hlc) + -> PolicyInput`, where `VerifiedAttestationSetView` is the plain-data + projection of 064's set (its `ok` half only) that `hqgit-domain` can + express without depending on `hqgit-trust`. `attestations` MUST be + sorted by id; `input_hash(&PolicyInput) -> Hash` is the hash of its + canonical bytes. +- **B-5 (`Verdict`).** `enum Verdict { Allow, Deny { reasons: Vec<String> + } }` with canonical encoding as the map `{ "verdict": "allow" }` or `{ + "verdict": "deny", "reasons": [...] }`; `reasons` non-empty and each + reason at most 512 bytes (longer reasons are truncated by the host with a + trailing marker, never dropped). An empty `reasons` on `Deny` decodes as + `Deny { reasons: ["policy gave no reason"] }`. +- **B-6 (`evaluate`).** `Engine::evaluate(&self, module: &PolicyModule, + input: &PolicyInput) -> Evaluation { verdict: Verdict, fuel_used: u64, + input_hash: Hash, policy: Cid, engine_config: Hash }`. Each call + instantiates a fresh store: no state survives between evaluations, and + the same `(module, input)` pair yields byte-identical verdict bytes. +- **B-7 (sample policies).** `testdata/policies/` holds prebuilt modules + with their SDK source beside them (066 builds them; this spec commits the + binaries and their sha256 so the engine tests need no wasm toolchain): + `allow_all.wasm`, `deny_all.wasm`, `two_approvals.wasm` (Allow iff at + least two `hqgit/approval/v1` attestations from distinct `Human` issuers + whose subject is the revision), `deny_unattested.wasm` (Deny unless a + `hqgit/provenance/v1` attestation covers the revision tree), and + `burn_fuel.wasm` (loops forever, for the fuel test). +- **B-8 (no ambient input).** The crate reads no clock and no environment; + `now` comes from the caller as data. The source guard of 010 FR-003 + applies. + +## 4. Functional requirements + +- **FR-001.** Tests cover: determinism (two evaluations of one input give + identical bytes and identical `input_hash`); the ABI round trip with + each sample policy; a module with an import refused at load; fuel + exhaustion yielding `Deny` with an `engine:` reason; an oversized output + yielding `Deny`; a trapping module yielding `Deny`; `attestations` + ordering independence (permuted input yields the same `input_hash` + because the builder sorts); `Verdict` canonical encoding vectors. +- **FR-002.** `Engine::config_digest()` is stable across runs and changes + when a limit changes (golden fixture). +- **FR-003.** The sample module binaries carry recorded sha256 values in + `testdata/policies/MANIFEST.txt` and a test verifies them, so a rebuilt + module that differs is noticed. +- **FR-004.** The crate depends on `hqgit-types` and `hqgit-domain` only + within the workspace; a test asserts `hqgit-trust` is not in `cargo + metadata`'s resolve for this package. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-policy --locked` passes. +- **AC-2.** `two_approvals.wasm` evaluated over a fixture input with one + approval yields `Deny` naming the shortfall, and with two approvals from + distinct humans yields `Allow`; the two verdict byte strings match the + recorded fixtures. + +## 6. Out of scope + +Authoring policies (066); recording a verdict as an attestation and +replaying it (067); which policy is active for a repository or path +(068); capability-based authorization for agents (101); any WASI surface. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-policy --locked +``` diff --git a/specs/066-policy-sdk/spec.md b/specs/066-policy-sdk/spec.md new file mode 100644 index 0000000..4babdb7 --- /dev/null +++ b/specs/066-policy-sdk/spec.md @@ -0,0 +1,166 @@ +--- +id: "066-policy-sdk" +title: "Policy SDK: typed Rust policies compiled to WASM, testable natively" +status: approved +kind: "feature" +domain: "l6-policy" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 4 +depends_on: + - "065-policy-engine" +establishes: + - "crates/hqgit-policy-sdk/Cargo.toml" + - "crates/hqgit-policy-sdk/src/lib.rs" + - "crates/hqgit-policy-sdk/src/prelude.rs" + - "crates/hqgit-policy-sdk/src/evidence.rs" + - "crates/hqgit-policy-sdk/src/export.rs" + - "crates/hqgit-policy-sdk/src/testing.rs" + - "crates/hqgit-policy-sdk/examples/allow_all.rs" + - "crates/hqgit-policy-sdk/examples/two_approvals.rs" + - "crates/hqgit-policy-sdk/examples/provenance_required.rs" + - "crates/hqgit-policy-sdk/examples/no_self_approval.rs" + - "crates/hqgit-policy-sdk/tests/" +extends: + # The sample binaries under 065's testdata are rebuilt from these examples. + - { spec: "065-policy-engine", unit: "crates/hqgit-policy/testdata/policies/", nature: additive } +summary: > + Policies must be unit-testable code, not settings, and a policy author + should never touch the WASM ABI by hand. This spec founds + hqgit-policy-sdk: a no_std-capable library that a policy crate depends on + to write `fn evaluate(&PolicyInput) -> Verdict` in plain Rust, an + `hqgit_policy!` macro that emits spec 065's exports around it, typed + query helpers over the attestation summaries (approvals by distinct + humans, provenance present, issuer kinds, predicate presence) with no + floating point anywhere, and a native test harness so the same function + runs under `cargo test` on the host and under wasmtime in the engine with + identical verdicts. Four example policies are the fixtures the engine + tests and later specs (103) reuse. +--- + +# 066: Policy SDK + +## 1. Purpose + +Thesis §4.6: compile policies to WASM with a typed SDK so they are +unit-testable, hash-pinned, and evaluable locally before push. Spec 065 +fixed the engine and the ABI; without an SDK every policy would hand-roll +memory allocation, canonical decoding, and result packing, and the first +subtle divergence between two policies' decoders would make verdicts +depend on which policy was asked. The SDK makes the decoder one shared +implementation, makes the policy body a pure Rust function, and makes +testing it a matter of calling that function. + +## 2. Territory + +`crates/hqgit-policy-sdk` as founded here: the manifest (`[package. +metadata.spec-spine] spec = "066-policy-sdk"`, `crate-type = ["rlib"]`, +no default features that require std so a policy crate can target +`wasm32-unknown-unknown`), `lib.rs`, `prelude.rs` (the re-exports a policy +imports with one `use`), `evidence.rs` (query helpers), `export.rs` (the +`hqgit_policy!` macro and the allocator shim), `testing.rs` (the native +harness), the four examples, and the `tests/` subtree. Additively, the +rebuilt sample binaries in 065's `testdata/policies/`. The engine's types +(`PolicyInput`, `Verdict`, `AttestationSummary`) are re-exported from +`hqgit-policy` behind its `types-only` feature so the SDK never links +wasmtime. + +## 3. Behavior + +- **B-1 (the policy function).** A policy crate declares + `hqgit_policy!(evaluate);` where `fn evaluate(input: &PolicyInput) -> + Verdict` is a plain function. The macro emits `#[no_mangle] extern "C" + fn alloc(len: i32) -> i32`, `#[no_mangle] extern "C" fn evaluate(ptr: + i32, len: i32) -> i64`, and the `memory` export, exactly the surface 065 + B-3 requires and nothing else. The generated `evaluate` decodes the + input with the shared canonical decoder, calls the user function, encodes + the verdict canonically, and packs the pointer and length. A decode + failure returns `Verdict::Deny { reasons: ["sdk: input did not decode: + <cause>"] }`. +- **B-2 (no ambient input).** The SDK exposes no clock, no randomness, and + no I/O to a policy; the only time is `input.now` (065 B-4). A policy that + needs entropy or the wall clock cannot be written with this SDK, on + purpose. `#![forbid(unsafe_code)]` applies except for the single + allocator shim in `export.rs`, which is the one audited `unsafe` block in + the workspace and is documented as such in `Cargo.toml` lints overrides + (010 B-1's `forbid` becomes `deny` for this crate only, with the + exception named). +- **B-3 (`evidence.rs`).** Pure helpers over `&PolicyInput`, all integer + arithmetic: `approvals(&PolicyInput) -> impl Iterator<Item = + &AttestationSummary>` (predicate `hqgit/approval/v1`, subject the + revision id, `claim_checked` true); `approvals_by_distinct_humans(& + PolicyInput) -> u32`; `has_provenance_for_tree(&PolicyInput) -> bool` + (predicate `hqgit/provenance/v1`, subject the revision tree); `issued_by + (&PolicyInput, kind: PrincipalKind) -> impl Iterator`; + `predicates_present(&PolicyInput) -> BTreeSet<PredicateType>`; + `missing(&PolicyInput, required: &[&str]) -> Vec<String>` (the required + predicates not present, in the given order); `submitted_by_agent(& + PolicyInput) -> bool`; `self_approved(&PolicyInput) -> bool` (an approval + whose issuer equals `revision.submitted_by`); `logged_only(&PolicyInput) + -> impl Iterator` (attestations with `logged` true). Every helper ignores + attestations whose `claim_checked` is false unless named `_unchecked`. +- **B-4 (`testing.rs`).** `evaluate_native(policy: fn(&PolicyInput) -> + Verdict, input: &PolicyInput) -> Verdict` runs the function on the host; + `input_fixture()` builders (`InputBuilder::new(change, revision) + .approval_by(principal).provenance().build()`) produce inputs without a + ledger; `assert_same_verdict(policy_fn, wasm_bytes, input)` runs both the + native function and the compiled module through 065's engine and asserts + byte-identical verdicts, which is the test every example ships with. +- **B-5 (examples).** `allow_all` (always `Allow`); `two_approvals` + (`Allow` iff `approvals_by_distinct_humans >= 2`, else `Deny` naming the + count); `provenance_required` (`Deny` unless `has_provenance_for_tree`); + `no_self_approval` (`Deny` when `self_approved`, else the two-approvals + rule). Each example is a policy crate in miniature: `hqgit_policy!` plus + a `#[cfg(test)]` module using B-4. +- **B-6 (build recipe).** `cargo build -p hqgit-policy-sdk --examples + --target wasm32-unknown-unknown --release --locked` produces + `target/wasm32-unknown-unknown/release/examples/<name>.wasm`; the + release profile for the wasm target sets `opt-level = "z"`, `lto = true`, + `panic = "abort"`, and `strip = true` so the module has no imports and + loads under 065 B-1. A `scripts/`-free rule: the recipe lives in this + spec and in `hq policy build` (067), never in an ad-hoc script. +- **B-7 (reproducible modules).** Given the pinned toolchain (010 B-2) the + compiled bytes of each example are reproducible; their sha256 values are + recorded in 065's `testdata/policies/MANIFEST.txt`, and rebuilding them + is how that manifest changes. + +## 4. Functional requirements + +- **FR-001.** Each example passes `assert_same_verdict` on at least two + inputs (one `Allow`, one `Deny`). +- **FR-002.** Helper tests cover: approvals counted per distinct human, + not per attestation; an agent-issued approval never counts as a human; + `claim_checked = false` ignored; `missing` preserves order; + `self_approved` true only on issuer equality. +- **FR-003.** A test builds the examples for `wasm32-unknown-unknown` when + the target is installed and skips with a named reason otherwise; CI + installs the target. +- **FR-004.** `cargo tree -p hqgit-policy-sdk --target + wasm32-unknown-unknown` contains no `wasmtime` and no `std`-only crate. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-policy-sdk --locked` passes. +- **AC-2.** `cargo build -p hqgit-policy-sdk --examples --target + wasm32-unknown-unknown --release --locked` succeeds and the four modules + load under 065's `PolicyModule::load` with no imports. + +## 6. Out of scope + +Evaluating and recording verdicts (067); policy pinning (068); a policy +language other than Rust (a datalog or Rego front end would compile to the +same ABI as a later spec); the evidence-required example (103). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-policy-sdk --locked +cargo build -p hqgit-policy-sdk --examples --target wasm32-unknown-unknown --release --locked +``` diff --git a/specs/067-policy-evaluation-attestation/spec.md b/specs/067-policy-evaluation-attestation/spec.md new file mode 100644 index 0000000..8d23f73 --- /dev/null +++ b/specs/067-policy-evaluation-attestation/spec.md @@ -0,0 +1,171 @@ +--- +id: "067-policy-evaluation-attestation" +title: "Policy evaluation as evidence: the policy-eval attestation, replay, and hq policy" +status: approved +kind: "kernel" +domain: "l6-policy" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 4 +depends_on: + - "065-policy-engine" + - "034-cli-attest-and-verify" +establishes: + - "crates/hqgit-policy/src/evaluate.rs" + - "crates/hqgit-policy/src/replay.rs" + - "crates/hqgit-cli/src/cmd_policy.rs" + - "crates/hqgit-cli/tests/policy.rs" +extends: + - { spec: "065-policy-engine", unit: "crates/hqgit-policy/src/lib.rs", nature: additive } + - { spec: "027-attestation-primitive", unit: "crates/hqgit-domain/src/predicate.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } +summary: > + A merge decision is evidence, not a moment. Every engine evaluation + (065) is recorded as an Attestation with predicate hqgit/policy-eval/v1 + whose claim names the policy Cid, the input hash, the engine + configuration digest, and the verdict, signed by the evaluating + principal and appended as a fact. A gate consumes that attestation, + never a live evaluation, so the decision that let a revision merge can be + produced years later and replayed: this spec's replay rebuilds the input + from the ledger, re-runs the pinned policy, and reports Match, Mismatch + with the differing bytes named, or Unavailable when an input was erased. + The CLI grows hq policy eval, replay, and build so the whole loop runs + offline on a laptop before anything is pushed. +--- + +# 067: Policy evaluation as evidence + +## 1. Purpose + +Constitution XI: every merge decision is replayable against the exact +policy that produced it. Thesis §4.6 makes the mechanism explicit: policy +evaluation emits an attestation. Spec 065 can evaluate; this spec makes the +evaluation durable and honest. It also closes the loop that constitution +IX promised: a policy verdict is the same primitive as a human approval or +a build's provenance, stored, verified (064), and queried through one path. +The merge queue (076), the git endpoint (092), and the API (093) gate on +the presence of a verified `policy-eval` attestation with `Allow`; none of +them evaluates anything themselves. + +## 2. Territory + +`evaluate.rs` (`evaluate_and_attest`, the claim schema builder) and +`replay.rs` (`replay`, `ReplayResult`) in `crates/hqgit-policy`; +`cmd_policy.rs` and `tests/policy.rs` in `crates/hqgit-cli`. Additively: +the `hqgit/policy-eval/v1` claim validator registered in 027's +`predicate.rs`, the crate re-exports, and the CLI frame (032's `main.rs`, +`cli.rs`, and manifest for the `hqgit-policy` dependency). Which policy is +active for a revision is 068's question; this spec takes a policy Cid as +input. + +## 3. Behavior + +- **B-1 (claim schema).** The `hqgit/policy-eval/v1` claim is the map + `PolicyEvalClaim { policy: Cid, policy_version: u16, input_hash: Hash, + engine_config: Hash, sdk_abi: u16, verdict: Verdict, fuel_used: u64, + attestations_considered: Vec<AttestationId> }`, canonical (011). + `attestations_considered` is the sorted id list of the verified set the + input was built from, so a replay can detect a set that has since gained + or lost members. The 027 validator for this predicate checks every field + is present and `verdict` decodes. +- **B-2 (`evaluate_and_attest`).** `evaluate_and_attest(engine, module, + input: PolicyInput, signer: &impl Signer, issuer: Principal, store: &mut + impl ObjectStore) -> Result<(Attestation, Evaluation), Error>`: runs + 065 B-6, builds the claim, puts the claim object (013), and signs an + Attestation whose `subject` is the revision id from `input.revision.id` + (the thing being gated) and whose `issuer` is the evaluating principal + (a `Service` on a server, the local human or agent identity on the CLI). + The caller appends the resulting `attestation.issued` fact (023). A + `Deny` is attested exactly like an `Allow`: refusals are evidence too. +- **B-3 (`replay`).** `replay(att: &Attestation, claim: &PolicyEvalClaim, + ledger: &impl LedgerRead, store: &impl ObjectStore, engine: &Engine) -> + ReplayResult` where `ReplayResult` is `Match { verdict }`, `Mismatch { + recorded: Verdict, recomputed: Verdict, input_hash_recorded: Hash, + input_hash_recomputed: Hash, first_difference: String }`, or + `Unavailable { missing: Vec<Cid> }`. Replay loads the policy module by + its recorded Cid (a different module hash is `Mismatch` at once), rebuilds + the `PolicyInput` from the ledger's state at the attestation's `at` + (the change and revision as of that HLC, and exactly the attestations in + `attestations_considered`, re-summarized), compares `input_hash`, and + re-evaluates only when the hashes match. An erased claim object (020) or + a missing policy object is `Unavailable`, never a mismatch. +- **B-4 (engine pinning).** When `claim.engine_config` differs from the + replaying engine's `config_digest()`, replay proceeds but the result + carries `engine_differs: true`; a `Mismatch` under a different engine is + reported as such rather than as a policy defect. `sdk_abi` mismatch is a + hard `Unavailable` with the reason named (the ABI is frozen at 1 in this + wave). +- **B-5 (`hq policy eval`).** `hq policy eval <change> [--revision <n>] + --policy <cid|file.wasm> [--strict|--offline] [--json]`: loads the + revision, verifies the attestations on it through 064 under the chosen + `VerifyPolicy` (offline by default on the CLI), builds the input with + `now` = the newest entry's HLC, evaluates, attests with the local + identity, appends the fact, and prints the verdict with its reasons. + Exit `0` on `Allow`, `1` on `Deny` (the verdict is the result, not an + error), `3` on I/O or a module that fails to load. `--dry-run` evaluates + without attesting. +- **B-6 (`hq policy replay`).** `hq policy replay <attestation-id> + [--json]` prints `match`, `mismatch` with the first difference, or + `unavailable` with the missing Cids. Exit `0` on match, `1` on mismatch, + `2` on unavailable. +- **B-7 (`hq policy build`).** `hq policy build <crate-dir> [--out + <file>]` runs the 066 B-6 recipe through `cargo` for the named policy + crate, verifies the output loads under 065 with no imports, prints the + module's Cid, and with `--store` puts it into the repository's object + store so `--policy <cid>` resolves. It shells out to `cargo` only; it + never fetches. +- **B-8 (gates consume, never evaluate).** A consumer that needs a + decision asks the ledger for a verified `hqgit/policy-eval/v1` + attestation on the revision whose `policy` equals the active policy + (068) and whose verdict is `Allow`; absence is a `Deny` for gating + purposes. This rule is stated here and referenced by 076, 092, and 093; + none of them may call `Engine::evaluate` directly. + +## 4. Functional requirements + +- **FR-001.** Tests cover: the claim round trip and validator; evaluate + then replay on an unchanged ledger is `Match`; amending the verified set + (one more approval appended after the evaluation) is a `Mismatch` naming + `attestations_considered`; erasing the claim object is `Unavailable`; a + substituted policy module is `Mismatch` at the module stage; a `Deny` + is attested with its reasons intact. +- **FR-002.** CLI tests drive `hq policy eval` on the spec 033 fixture + ledger with `two_approvals.wasm`: one approval exits `1` with the reason, + two approvals exit `0`, and `hq attest list <revision>` then shows the + policy-eval attestation; `hq policy replay` on it exits `0`. +- **FR-003.** `--json` output is byte-identical across two runs of the + same command on the same ledger. +- **FR-004.** `hq policy build` is tested only when a `wasm32-unknown- + unknown` target is installed; otherwise the test skips with the reason + printed. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-policy --locked` and `cargo test -p + hqgit-cli --locked --test policy` pass. +- **AC-2.** On the 033 fixture, `hq policy eval <change> --policy + two_approvals.wasm` records a `Deny`, a second approval is issued, a + second eval records an `Allow`, and `hq policy replay` on both + attestations reports `match`. + +## 6. Out of scope + +Choosing the active policy (068); server-side evaluation triggers (093); +the merge queue's use of verdicts (076); evaluating policies inside the +web UI (095 renders verdicts it reads). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-policy --locked +cargo test -p hqgit-cli --locked --test policy +``` diff --git a/specs/068-policy-in-repo/spec.md b/specs/068-policy-in-repo/spec.md new file mode 100644 index 0000000..70f9456 --- /dev/null +++ b/specs/068-policy-in-repo/spec.md @@ -0,0 +1,159 @@ +--- +id: "068-policy-in-repo" +title: "Policy in the repository: pinned by fact, resolved by clock, no settings table" +status: approved +kind: "feature" +domain: "l6-policy" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 4 +depends_on: + - "067-policy-evaluation-attestation" + - "021-local-repository" +establishes: + - "crates/hqgit-policy/src/pin.rs" + - "crates/hqgit-policy/src/scope.rs" + - "crates/hqgit-policy/tests/pin.rs" +extends: + - { spec: "065-policy-engine", unit: "crates/hqgit-policy/src/lib.rs", nature: additive } + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } + - { spec: "020-commitments-and-tombstones", unit: "crates/hqgit-ledger/src/tombstone.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cmd_policy.rs", nature: additive } +summary: > + Where a repository's policy lives: in the ledger, as facts. A + policy.pinned fact names a policy module Cid, a scope (a namespace or a + path pattern), the clock from which it applies, and who pinned it; + policy.unpinned retires one. The PolicyPinView fold answers + active_policy(repo, at, scope): the policy that governs a revision is the + one active at the revision's submission clock, so a decision recorded in + wave 4 replays identically in wave 8 against the pin that was in force. + There is no settings table to toggle, which is constitution XI made + structural. The same fold is what lets a policy verdict mint the erase + capability of spec 020 and what the agent authority of spec 100 compares + its policy version against. +--- + +# 068: Policy in the repository + +## 1. Purpose + +Thesis §4.6 ends with the anti-pattern the design makes impossible: +repository settings as mutable UI toggles. A toggle is a row somewhere +outside the log, which constitution VI forbids, and it has no history, +which makes "why was this allowed to merge" unanswerable. This spec puts +the policy binding itself into the ledger, so the active policy at any +clock is a fold over facts, changing it is a signed fact by a principal +the repository recognizes, and the question of which policy governed a +revision has exactly one deterministic answer. + +## 2. Territory + +`pin.rs` (the facts' semantics, `PolicyPinView`, `active_policy`) and +`scope.rs` (`PolicyScope`, precedence) in `crates/hqgit-policy`, with +`tests/pin.rs`. Additively: the two fact kinds in 023's vocabulary, the +`EraseCapability::from_verdict` implementation hook in 020's +`tombstone.rs`, `hq policy pin|unpin|show` in 067's `cmd_policy.rs`, and +the crate re-exports. + +## 3. Behavior + +- **B-1 (facts).** `policy.pinned { policy: Cid, scope: PolicyScope, + effective_from: Hlc, pinned_by: Principal, note: String }` and + `policy.unpinned { pin: PinId, unpinned_by: Principal, note: String }` + where `PinId = Hash` of the pinning fact's canonical bytes (023's + content-derived id rule). Kind strings are frozen. The pinning + principal MUST hold the `pin-policy` capability (094's capability + vocabulary; on the wave 1 CLI the repository owner holds it by + construction, 021) and the fact's signer MUST be that principal; a + pin by anyone else is folded as `rejected` with the reason, never + silently dropped. +- **B-2 (`PolicyScope`).** `enum PolicyScope { Namespace(Hash), + Path(PathPattern) }` where `PathPattern` is a glob over tree paths + (`crates/hqgit-ledger/**`) evaluated against the files a revision + touches (024's tree delta). Precedence when several pins are active at + one clock: the most specific path pattern wins over a less specific one + (longest literal prefix, then fewest wildcards), and any path pin wins + over the namespace pin; ties are broken by the later `effective_from`, + then by `PinId`. Precedence is a pure function and its rules are a + frozen vector. +- **B-3 (`PolicyPinView`).** A `DerivedState` (019) folding the two kinds + in total order (018): `pins: BTreeMap<PinId, Pin>` with `Pin { policy, + scope, effective_from, pinned_by, retired: Option<Hlc> }`. `active_policy + (&self, at: &Hlc, scope: &ScopeQuery) -> Option<ActivePin>` returns the + pin in force at `at` for the query (a namespace plus the touched paths), + `None` when nothing is pinned, and `ActivePin { pin_id, policy, scope, + effective_from }`. A pin is in force from `effective_from` until its + retirement clock, exclusive. +- **B-4 (which clock).** The policy that governs a revision is + `active_policy(revision.at, ...)`: the pin in force when the revision was + submitted, not when the evaluation runs. Re-pinning a policy therefore + never changes the verdict a shipped revision replays to (067 B-3), and a + newly pinned policy applies to the next revision, never retroactively. + Tests pin this with a revision submitted between two pins. +- **B-5 (no pin, no allow).** With `None` from `active_policy`, a gate + (076, 092, 093) treats the revision as `Deny { reasons: ["no policy + pinned for this scope"] }`. An unpinned repository cannot merge through a + gate; the wave 1 CLI (`hq init`, 032) pins `allow_all.wasm` by default + so local review is unaffected, and `hq status` shows the active pin. +- **B-6 (erase by verdict).** `EraseCapability::from_verdict(att: + &AttestationId, view: &PolicyPinView, verified: &VerifiedAttestationSet) + -> Result<EraseCapability, Error>` succeeds only when the attestation is + a verified `hqgit/policy-eval/v1` with verdict `Allow`, whose policy Cid + is the active pin for scope `Namespace(target namespace)` at the + attestation's clock, and whose claim's `attestations_considered` + includes an `hqgit/erasure-request/v1` attestation naming the target + Cid. This is the second and last constructor of the capability (020 + B-6): erasure is either the owner's act or a policy's verdict. +- **B-7 (CLI).** `hq policy pin <cid|file.wasm> [--scope <ns|glob>] + [--from <hlc>] [--note]` appends the fact (storing the module when given + as a file); `hq policy unpin <pin-id>`; `hq policy show [--at <hlc>] + [--json]` prints the active pin for the default namespace and every pin + with its state. Exit codes per 032. +- **B-8 (determinism and honesty).** The fold is a pure function of the + ordered facts; `active_policy` reads no clock. A rejected pin (B-1) is + visible in `hq policy show` as `rejected: <reason>` so an operator sees + the attempt, never a silent no-op. + +## 4. Functional requirements + +- **FR-001.** Tests cover: pin, unpin, and the in-force window; precedence + vectors for path over namespace and specificity ordering; a revision + between two pins governed by the earlier one; a pin by a principal + without the capability folded as rejected; `from_verdict` succeeding on + a matching verified verdict and failing on an unverified one, on a + `Deny`, and on a policy that is not the active pin; fold-order + independence (permuted facts, one state). +- **FR-002.** The default pin at `hq init` is a fact like any other, + visible in `hq log` and replaceable by `hq policy pin`. +- **FR-003.** `hq policy show --json` is byte-identical across two runs. +- **FR-004.** The crate still depends only on `hqgit-types`, + `hqgit-domain`, `hqgit-ledger`, and `hqgit-object` within the workspace + (the `hqgit-ledger` dependency is new here, for the fold and the + capability hook, and stays below `hqgit-trust`). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-policy --locked --test pin` passes. +- **AC-2.** On the 033 fixture, `hq policy pin two_approvals.wasm` then + `hq policy eval <change>` records a `Deny`, `hq policy show` names the + pin, and after `hq policy unpin` a new eval is refused with `no policy + pinned`. + +## 6. Out of scope + +Server-side capability checks for `pin-policy` (094); agent-scoped policy +versions in tokens (100, 101); policy inheritance across federated peers +(112); a UI for pins (095 shows the active pin on a change). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-policy --locked +``` diff --git a/specs/070-reapi-types/spec.md b/specs/070-reapi-types/spec.md new file mode 100644 index 0000000..43898b0 --- /dev/null +++ b/specs/070-reapi-types/spec.md @@ -0,0 +1,181 @@ +--- +id: "070-reapi-types" +title: "Remote Execution API types: vendored protos, BLAKE3 digests, actions, the CAS adapter" +status: approved +kind: "kernel" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 5 +depends_on: + - "013-object-store" +establishes: + - "crates/hqgit-eval/Cargo.toml" + - "crates/hqgit-eval/build.rs" + - "crates/hqgit-eval/proto/build/bazel/remote/execution/v2/remote_execution.proto" + - "crates/hqgit-eval/proto/build/bazel/semver/semver.proto" + - "crates/hqgit-eval/proto/google/" + - "crates/hqgit-eval/src/lib.rs" + - "crates/hqgit-eval/src/digest.rs" + - "crates/hqgit-eval/src/action.rs" + - "crates/hqgit-eval/src/cas_adapter.rs" + - "crates/hqgit-eval/tests/" + - "crates/hqgit-eval/testdata/reapi/" +extends: + # prost, prost-types, tonic, tonic-build, protox: the wire substrate of L3. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The evaluation plane speaks the Bazel Remote Execution API rather than a + protocol of its own (thesis D9), so existing executors, clients, and + caches interoperate on day one. This spec founds hqgit-eval with the + vendored REAPI v2 protos compiled at build time, a BLAKE3 digest function + that coincides with the hqgit object hash for raw blobs and carries an + explicit mapping where the two encodings differ (Directory versus Tree), + the pure construction of an Action from (repository state, target, + toolchain) whose digest is the key the cache and the merge queue share, + and the CAS adapter that presents the spec 013 object store as REAPI + content-addressable storage. No service listens yet (072) and nothing + executes (073): this is the typed vocabulary the rest of wave 5 shares. +--- + +# 070: Remote Execution API types + +## 1. Purpose + +Thesis §4.4 fixes the evaluation plane as `eval(repo_state_hash, target, +toolchain_hash) -> output_hash`, cached globally on input hash, and thesis +D9 refuses to invent the protocol: the Bazel Remote Execution API (REAPI) +already has executors, clients, and caches. What hqgit adds is the +identity of the inputs: the repository state is a spec 013 tree, the +toolchain is a hash, and the action digest is therefore a pure function of +canonical, content-addressed inputs. This spec is where REAPI's digests and +hqgit's Cids are reconciled once, in code, with a table for every place the +two byte encodings differ, so no later spec has to guess which hash it is +holding. + +## 2. Territory + +`crates/hqgit-eval` as founded here: the manifest (workspace dependencies +on `hqgit-types`, `hqgit-object`, `prost`, `prost-types`, `tonic`), the +`build.rs` that compiles the vendored protos through `tonic-build` with +`protox` (no `protoc` binary is required, so `--locked` builds are +hermetic), the vendored proto tree, `lib.rs`, `digest.rs` (the digest +function and the mapping table), `action.rs` (Action, Command, and the +action key), `cas_adapter.rs` (the object store seen as CAS), the `tests/` +subtree, and recorded REAPI fixtures under `testdata/reapi/`. The gRPC +services are spec 072; the cache is spec 071; provenance is spec 074; the +build graph is spec 075. + +## 3. Behavior + +- **B-1 (vendored protos).** The crate vendors + `build/bazel/remote/execution/v2/remote_execution.proto` and + `build/bazel/semver/semver.proto` at REAPI v2.3 or later (the first + release whose `DigestFunction.Value` enumerates `BLAKE3 = 9`), plus the + `google/api`, `google/rpc`, `google/longrunning`, and `google/bytestream` + protos they import, byte for byte from upstream with the upstream commit + recorded in `proto/VERSION`. `build.rs` generates `build.bazel.remote. + execution.v2`, `google.longrunning`, `google.bytestream`, and `google.rpc` + modules with server and client stubs; generation MUST be deterministic + (sorted inputs, no timestamps in output). +- **B-2 (digest function).** `Digest { hash: Hash, size_bytes: u64 }` is + the typed form of the REAPI `Digest` message; the wire `hash` field is + the 64-character lowercase hex of spec 010's `Hash`, and `DigestFunction` + is always `BLAKE3`. `digest_of(bytes) -> Digest` is `Hash::of(bytes)` + with the byte length. A server or client advertising any other digest + function is refused with `Error::Validation` naming the function. +- **B-3 (the coincidence rule).** For a raw blob, the REAPI digest hash + equals the hash of the spec 013 `Cid { codec: Raw }` of the same bytes, + so a blob has one identity in both worlds. For structured messages the + encodings differ: a REAPI `Directory` is a deterministically serialized + protobuf, a hqgit `Tree` is DAG-CBOR. `digest.rs` therefore carries an + explicit `DirectoryMap` (`BTreeMap<Cid, Digest>` plus the inverse) built + by `tree_to_directory(tree: &Tree, store) -> Result<(Directory, Digest), + Error>` and `directory_to_tree(dir: &Directory, store) -> Result<Cid, + Error>`; both conversions are pure given the store, and both are tested to + round-trip. Tree entry modes map to `FileNode.is_executable`, + `DirectoryNode`, and `SymlinkNode`; a mode with no REAPI counterpart is + `Error::Validation`. `Tree` chunked blobs (spec 014 `BlobManifest`) are + presented as one REAPI blob with the whole-content hash recorded in the + manifest, never as their chunks. +- **B-4 (canonical proto bytes).** Every message hqgit digests + (`Directory`, `Command`, `Action`) is serialized in REAPI's required + canonical form: fields in field-number order, repeated entries sorted + where the API mandates it (`Directory.files` by name, `Command. + environment_variables` by name, `Command.output_paths` lexicographic), no + unknown fields, no default-valued scalars emitted. `canonical_bytes(msg)` + is the one serialization path and is what `digest_of` receives. +- **B-5 (action key).** `action.rs` defines `ActionInput { input_root: Cid, + target: TargetId, command: Command, toolchain: Hash, platform: + BTreeMap<String, String>, timeout_s: Option<u32>, do_not_cache: bool }` + and `action_key(input: &ActionInput, store) -> Result<ActionKey, Error>` + where `ActionKey(Digest)` is the digest of the REAPI `Action` built from + the `Command` digest, the `Directory` digest of `input_root` (B-3), the + timeout, `do_not_cache`, and the platform properties. The toolchain hash + enters the action as the platform property `hqgit.toolchain` and the + target as `hqgit.target`, so two evaluations differing only in toolchain + or target never share a key. `salt` is never used. The key is the + `output_hash` of the thesis's `eval` signature: what the cache (071) is + indexed by and what the merge queue (076) speculates over. +- **B-6 (CAS adapter).** `CasAdapter<S: ObjectStore>` implements a + `ContentAddressable` trait: `find_missing(&[Digest]) -> Vec<Digest>`, + `read(&Digest) -> Result<Bytes, Error>`, `write(bytes) -> Result<Digest, + Error>`, and `read_directory_tree(root: &Digest) -> Result<Vec<Directory>, + Error>` (the `GetTree` shape), all over spec 013's `ObjectStore` with + blobs stored as `Raw` and directories stored as their canonical proto + bytes as `Raw` with the `DirectoryMap` updated. Every read verifies the + hash on the way out (013 B-x); a size mismatch between the requested + `size_bytes` and the stored length is `Error::Validation`. +- **B-7 (no ambient input).** Nothing in this crate reads a clock, the + environment, or a `HashMap`; spec 010 FR-003's source guard applies. The + generated code is excluded from that guard by path. + +## 4. Functional requirements + +- **FR-001.** `build.rs` compiles the vendored protos with `protox` and + `tonic-build`; generated output lands under `OUT_DIR`, never in the + source tree; a checked-in `proto/VERSION` names the upstream commit. +- **FR-002.** Tests cover: `digest_of` against recorded REAPI fixtures + under `testdata/reapi/` (a blob, a `Directory`, a `Command`, an `Action`, + each with its expected BLAKE3 digest produced by an independent REAPI + implementation); the coincidence rule for raw blobs; `Tree` to + `Directory` and back for a nested fixture with an executable and a + symlink; a chunked blob presented as one REAPI blob; `action_key` + stability across two builds and sensitivity to each input field; refusal + of a non-BLAKE3 digest function. +- **FR-003.** The `ContentAddressable` trait is object-safe and the adapter + is tested against spec 013's `MemoryStore`: find-missing, write-read + round trip, size mismatch refusal, `read_directory_tree` breadth-first + order. +- **FR-004.** The crate depends on `hqgit-types` and `hqgit-object` only + within the workspace; `tonic` server features are enabled but no service + is implemented here. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-eval --locked` passes, fixtures included. +- **AC-2.** `cargo build -p hqgit-eval --locked` succeeds on a machine with + no `protoc` installed. +- **AC-3.** `spec-spine index` discovers `hqgit-eval` bound to this spec and + `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The action cache and its trust rules (071); the gRPC services and the +scheduler (072); executors and sandboxes (073); provenance attestations +(074); the build manifest and affected-target selection (075); the merge +queue (076). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-eval --locked +cargo clippy -p hqgit-eval --all-targets --locked -- -D warnings +``` diff --git a/specs/071-action-cache/spec.md b/specs/071-action-cache/spec.md new file mode 100644 index 0000000..16bb6c2 --- /dev/null +++ b/specs/071-action-cache/spec.md @@ -0,0 +1,157 @@ +--- +id: "071-action-cache" +title: "Action cache as a trust boundary: attested entries, gating versus advisory lookups" +status: approved +kind: "kernel" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 5 +depends_on: + - "070-reapi-types" + - "064-attestation-verification" +establishes: + - "crates/hqgit-eval/src/cache.rs" + - "crates/hqgit-eval/src/executor_identity.rs" + - "crates/hqgit-eval/tests/cache.rs" +extends: + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/src/lib.rs", nature: additive } + # redb for the persistent cache backend, hqgit-trust for the verified set. + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/Cargo.toml", nature: additive } + # The executor registration fact joins the domain vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + A poisoned action cache forges provenance for arbitrary code, so the cache + is a trust boundary (thesis D10, constitution XI). Entries are immutable + and keyed by both the action digest and the executor that produced them, + so two executors' results coexist instead of overwriting; every entry + names its executor's Service identity and the provenance attestation that + vouches for it; and a lookup declares its purpose. A gating lookup, the + kind a merge decision consumes, treats an entry whose attestation is + absent or fails spec 064 verification as a miss. An advisory lookup may + serve the same entry, flagged, for developer feedback. Executors are + registered Service identities with a trust tier, and a quorum rule can + demand that several of them agree before an output counts. +--- + +# 071: Action cache as a trust boundary + +## 1. Purpose + +Thesis §4.4 ends with the sentence this spec implements: the cache is a +trust boundary; cache entries must carry executor identity and be +verifiable; unattested cache hits are cache misses for anything that gates +a merge. Under content addressing, a cache hit is indistinguishable from an +execution, which is what makes global caching affordable (076) and exactly +what makes a forged entry dangerous. The defense is not secrecy but +evidence: the entry itself is worthless without the provenance attestation +(074) an identified executor signed over it, verified through the trust +plane (064). + +## 2. Territory + +`cache.rs` (the `ActionCache` trait, the entry shape, the lookup rule, the +in-memory and redb backends) and `executor_identity.rs` (executor +registration, trust tiers, the quorum rule) inside `crates/hqgit-eval`, +plus `tests/cache.rs`. Additively: the `eval.executor_registered` fact kind +in spec 023's vocabulary, the crate manifest (redb, `hqgit-trust`), and the +crate's re-exports. The service that fronts this cache over gRPC is spec +072; the attestation an entry references is minted by spec 074. + +## 3. Behavior + +- **B-1 (entry).** `CacheEntry { key: ActionKey, executor: ServiceId, + executor_key: KeyId, result: ActionResult, attestation: + Option<AttestationId>, sandbox_tier: Option<SandboxTier>, entry_hash: + Hash }` where `ActionResult` is the typed form of the REAPI message + (output files, directories, symlinks as digests, exit code, stdout and + stderr digests, `ExecutedActionMetadata`). `entry_hash` is + `Hash::of(canonical bytes of the entry without entry_hash)`; a read + recomputes it and a mismatch is `Error::Crypto`, never a served entry. +- **B-2 (immutability).** The store key is `(ActionKey, ServiceId)`. `put` + with a new key stores; `put` with an existing key and identical + `entry_hash` is a no-op `Ok`; `put` with an existing key and a different + `entry_hash` is `Error::Validation("cache entry is immutable")`. There is + no update and no delete; an executor that wants to supersede its result + registers a new identity or the operator evicts through a journaled + operator command outside this crate. +- **B-3 (purpose).** `enum Purpose { Gating, Advisory }`. `lookup(key, + purpose, verified: &VerifiedAttestationSet) -> Lookup` returns + `Lookup::Hit { entry, grade: Grade::{Attested, Unattested} }` or + `Lookup::Miss(MissReason::{Absent, Unattested, VerificationFailed(String), + QuorumUnmet})`. For `Gating`, an entry is a hit only when `attestation` + is `Some(id)`, `verified` contains that id with `Verdict::Ok`, the + attestation's subject equals the `ActionKey` hash, its predicate is + `hqgit/provenance/v1`, and the claim's byproduct digests equal the + entry's output digests; any other state is a `Miss`. For `Advisory`, an + entry with a missing or unverified attestation is served as `Hit { grade: + Unattested }`, and callers MUST render the grade. The verified set is an + input, never computed here: this crate never verifies a signature. +- **B-4 (coexistence and quorum).** `lookup_all(key) -> Vec<CacheEntry>` + returns every executor's entry for a key. `QuorumRule { required: u8, + tiers: BTreeSet<TrustTier> }` and `quorum(key, rule, verified) -> Lookup` + produce a gating hit only when at least `required` attested entries from + executors of an allowed tier agree on the output digests; disagreement is + `Miss(QuorumUnmet)` with every entry hash listed, so a divergent executor + is visible rather than averaged away. +- **B-5 (executor identity).** `ExecutorIdentity { service: ServiceId, + key: KeyId, tier: TrustTier::{Trusted, Untrusted}, sandbox_tiers: + BTreeSet<SandboxTier> }` registered by an `eval.executor_registered` + fact signed by the operator identity; `ExecutorRegistry` folds those + facts (and `eval.executor_revoked`) and answers `executor(service, at)`. + A `put` from a `ServiceId` the registry does not know at the entry's + attestation time is `Error::Validation`. +- **B-6 (backends).** `MemoryActionCache` (BTreeMap) and + `RedbActionCache` at `<data>/eval/action-cache.redb` with one table + keyed by the 64 bytes `action hash || service id` and a secondary table + by action hash; iteration is key-ordered; writes are fsynced. +- **B-7 (`do_not_cache`).** An `ActionResult` for an action whose + `do_not_cache` was set is never stored; `put` returns `Ok` and records a + counter so the refusal is observable. +- **B-8 (no ambient input).** The cache reads no clock; `stored_at` is not + a field. Freshness is a property of the attestation's `at`, judged by the + caller. + +## 4. Functional requirements + +- **FR-001.** `ActionCache` is an object-safe trait with `put`, `lookup`, + `lookup_all`, and `quorum`; both backends implement it and share one test + suite parameterized over the backend. +- **FR-002.** Tests cover: an unattested entry is a gating miss and an + advisory hit flagged `Unattested`; a verified attestation over a different + subject is a miss; byproduct digests disagreeing with the entry's outputs + is a miss; a tampered stored entry is `Error::Crypto`; overwrite with a + different hash is refused while an identical put is a no-op; two + executors' entries coexist and `quorum(required: 2)` hits only when they + agree; a revoked executor's put is refused; `do_not_cache` never stores. +- **FR-003.** The verified attestation set arrives as spec 064's + `VerifiedAttestationSet` value; tests build it through 064's test + constructor, never by signing inside this crate. +- **FR-004.** The redb backend survives reopen with identical lookups and + key order. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-eval --locked cache` passes. +- **AC-2.** A fixture where the only entry for a key is unattested yields + `Miss(Unattested)` for `Gating` and `Hit { Unattested }` for `Advisory`. + +## 6. Out of scope + +Minting the provenance attestation (074); serving the cache over gRPC and +authenticating executors on the wire (072); eviction policy and quotas (a +later operator spec); cross-host cache replication (rides on federation, +112). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-eval --locked cache +``` diff --git a/specs/072-execution-service/spec.md b/specs/072-execution-service/spec.md new file mode 100644 index 0000000..e2002c2 --- /dev/null +++ b/specs/072-execution-service/spec.md @@ -0,0 +1,186 @@ +--- +id: "072-execution-service" +title: "Execution service: REAPI Capabilities, CAS, ByteStream, ActionCache, Execution, and the scheduler" +status: approved +kind: "feature" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 5 +depends_on: + - "071-action-cache" +establishes: + - "crates/hqgit-eval/proto/hqgit/eval/v1/worker.proto" + - "crates/hqgit-eval/src/service/mod.rs" + - "crates/hqgit-eval/src/service/capabilities.rs" + - "crates/hqgit-eval/src/service/cas.rs" + - "crates/hqgit-eval/src/service/bytestream.rs" + - "crates/hqgit-eval/src/service/action_cache.rs" + - "crates/hqgit-eval/src/service/execution.rs" + - "crates/hqgit-eval/src/service/worker.rs" + - "crates/hqgit-eval/src/scheduler.rs" + - "crates/hqgit-eval/tests/service.rs" +extends: + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/src/lib.rs", nature: additive } + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/build.rs", nature: additive } + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/Cargo.toml", nature: additive } + # tokio, tokio-stream, uuid: the async runtime and operation ids. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The listening half of the evaluation plane: tonic implementations of the + four REAPI services (Capabilities, ContentAddressableStorage with + ByteStream, ActionCache, Execution with long-running operations) over the + spec 070 adapter and the spec 071 cache, plus the scheduler that turns an + Execute request into a leased unit of work for an external worker. + Nothing executes in this process: workers (073) register, lease, heartbeat, + and complete through a small hqgit-owned Worker service, because REAPI + standardizes the client side and not the worker side. Identical in-flight + actions coalesce, lost leases requeue, the sandbox tier is a platform + property the scheduler matches, and every completion passes through a + hook seam that spec 074 fills with provenance before the cache sees it. +--- + +# 072: Execution service + +## 1. Purpose + +Thesis D9 says interoperate on day one: any REAPI client (Bazel, Buck2, +`reclient`, a `hq` CLI) must be able to point at hqgit and get execution +and caching without a translation layer. This spec is that surface. It +also draws the line thesis D14 requires: the Rust side schedules and +stores, the Go side (073) executes, and the two meet at a worker protocol +that carries the REAPI messages unchanged. + +## 2. Territory + +The `service/` module of `crates/hqgit-eval`: `capabilities.rs`, `cas.rs`, +`bytestream.rs`, `action_cache.rs`, `execution.rs`, `worker.rs`, and the +`mod.rs` that assembles them into a `tonic::transport::Server` router; +`scheduler.rs`; the hqgit-owned `hqgit.eval.v1.Worker` proto and its build +step; `tests/service.rs`. Additively: the crate manifest (tokio, +tokio-stream, uuid) and `build.rs`. Executors themselves are spec 073; the +provenance hook implementation is spec 074; the merge queue that drives +this service is spec 076; embedding the router into `hqgit-server` is +wired by spec 090's app when both exist. + +## 3. Behavior + +- **B-1 (Capabilities).** `GetCapabilities` returns `ServerCapabilities` + with `cache_capabilities.digest_functions = [BLAKE3]`, + `action_cache_update_capabilities.update_enabled = true`, + `max_batch_total_size_bytes = 4 MiB`, `symlink_absolute_path_strategy = + DISALLOWED`, `execution_capabilities.digest_function = BLAKE3`, + `exec_enabled = true`, `execution_priority_capabilities` covering `-10` + to `10`, `supported_node_properties = []`, and `low_api_version = + high_api_version = 2.3`. A request naming another instance than the + configured ones is `NOT_FOUND`. +- **B-2 (CAS).** `FindMissingBlobs`, `BatchUpdateBlobs`, + `BatchReadBlobs`, and `GetTree` delegate to spec 070's + `ContentAddressable`; a blob whose bytes do not hash to the declared + digest is `INVALID_ARGUMENT` and never stored; batch limits are enforced + from B-1; `GetTree` pages breadth-first with an opaque cursor. +- **B-3 (ByteStream).** `Read` streams `{instance}/blobs/{hash}/{size}` + in 64 KiB chunks from the store, honoring `read_offset` and + `read_limit`; `Write` accepts + `{instance}/uploads/{uuid}/blobs/{hash}/{size}`, buffers to a spooled + temporary file, verifies the hash at `finish_write`, and commits through + the adapter; `QueryWriteStatus` reports committed size. A digest whose + size exceeds the configured maximum (default 2 GiB) is + `RESOURCE_EXHAUSTED`. +- **B-4 (ActionCache service).** `GetActionResult` performs a spec 071 + `lookup` with `Purpose::Advisory` and the caller-presented verified set + (empty unless the request metadata carries attestation ids the server + can verify through the 064 seam), returning `NOT_FOUND` on a miss; the + response `ActionResult` carries the grade in + `execution_metadata.auxiliary_metadata` as a `hqgit.eval.v1.CacheGrade` + message so a client can see `Unattested`. `UpdateActionResult` is + accepted only from a caller authenticated as a registered executor + (`ExecutorRegistry`, 071 B-5) and stores through `put`; any other caller + is `PERMISSION_DENIED`. Gating lookups never happen over this service: + the merge queue (076) calls the cache in process. +- **B-5 (Execution).** `Execute` validates the `Action` (digests present + in CAS, platform property `hqgit.sandbox-tier` present and one of + `trusted`, `untrusted`), consults the cache with `Advisory` unless + `skip_cache_lookup`, and otherwise enqueues, returning a stream of + `google.longrunning.Operation` whose metadata is + `ExecuteOperationMetadata` with stages `CACHE_CHECK`, `QUEUED`, + `EXECUTING`, `COMPLETED`; `WaitExecution` reattaches to an operation by + name. Identical action digests already queued or executing coalesce onto + one execution; every attached operation completes with the same result. +- **B-6 (scheduler).** `Scheduler` is a pure state machine over + `SchedulerEvent`s (`Enqueue`, `RegisterWorker`, `Lease`, `Heartbeat`, + `Complete`, `Tick`) with a `Clock` seam for lease expiry: a queue ordered + by `(priority, enqueue sequence)`; workers register with an + `ExecutorIdentity` (071) and platform properties; `lease(worker)` hands + out the highest-priority queued action whose platform properties are all + satisfied by the worker's, with a lease of 60 s renewed by heartbeat; a + lease that misses two heartbeats is revoked and the action requeued at + the head with an `attempt` counter; the third failed attempt completes + the operation with `FAILED_PRECONDITION` and the last worker's error. + The tier property is matched exactly: an `untrusted` action never leases + to a worker that registered only `trusted`. +- **B-7 (Worker service).** `hqgit.eval.v1.Worker` (`worker.proto`) has + `Register(RegisterRequest) returns (RegisterResponse)`, + `Lease(LeaseRequest) returns (stream LeaseAssignment)`, + `Heartbeat(HeartbeatRequest) returns (HeartbeatResponse)`, and + `Complete(CompleteRequest) returns (CompleteResponse)`; assignments carry + the REAPI `Action` digest and instance name only (the worker fetches the + rest from CAS); `Complete` carries the REAPI `ActionResult` and a + `SandboxReport` (073) and is authenticated as the leasing executor. +- **B-8 (completion hook).** `trait CompletionHook { fn on_complete(&self, + ctx: &CompletionContext, result: ActionResult) -> Result<CacheEntry, + Error> }` receives the action key, the executor identity, the sandbox + report, and the result, and returns the entry to store. The default + `UnattestedHook` builds an entry with `attestation: None` (advisory + only); spec 074 supplies the hook that mints provenance first. A hook + error fails the operation with the reason and stores nothing. +- **B-9 (observability and limits).** Every state transition is a + structured log line with operation id, action key, worker id, and stage; + queue depth and lease counts are gauges; a configured maximum queue depth + refuses new work with `RESOURCE_EXHAUSTED`. + +## 4. Functional requirements + +- **FR-001.** Every service is generic over the store, cache, registry, + and hook seams, and tests instantiate them with in-memory + implementations and a `FakeWorker` that speaks the Worker service in + process. +- **FR-002.** Scheduler tests, pure over events: priority order, platform + matching including tier exactness, coalescing of identical digests, + lease expiry and requeue, third-attempt failure, heartbeat renewal. +- **FR-003.** Service tests: a full Execute round trip through the fake + worker ending in a stored advisory entry; `UpdateActionResult` refused + for a non-executor; ByteStream write with a bad hash refused; + `GetActionResult` surfacing the `Unattested` grade; `WaitExecution` + reattachment. +- **FR-004.** An interop smoke test drives the server with an + independent REAPI client binary when one is on `PATH`, marked + `#[ignore]` otherwise. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-eval --locked service` passes. +- **AC-2.** In the fixture flow, two concurrent `Execute` calls for one + action digest produce exactly one lease and two completed operations + with identical results. + +## 6. Out of scope + +Executing anything (073); provenance minting (074); the build manifest +and affected targets (075); the merge queue (076); authentication of human +principals on this listener (the API in 093 fronts humans; this listener +serves clients and executors); multi-node scheduling (091 places repos, +not workers, in v1). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-eval --locked service +``` diff --git a/specs/073-sandbox-executor/spec.md b/specs/073-sandbox-executor/spec.md new file mode 100644 index 0000000..eea7191 --- /dev/null +++ b/specs/073-sandbox-executor/spec.md @@ -0,0 +1,190 @@ +--- +id: "073-sandbox-executor" +title: "Sandbox executor: the Go worker with namespace and microVM tiers" +status: approved +kind: "feature" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 5 +depends_on: + - "072-execution-service" +establishes: + - "executor/go.mod" + - "executor/go.sum" + - "executor/Makefile" + - "executor/cmd/hq-executor/main.go" + - "executor/internal/reapi/worker.go" + - "executor/internal/reapi/cas.go" + - "executor/internal/sandbox/tier.go" + - "executor/internal/sandbox/report.go" + - "executor/internal/sandbox/namespaces_linux.go" + - "executor/internal/sandbox/seccomp_linux.go" + - "executor/internal/sandbox/microvm.go" + - "executor/internal/sandbox/sandbox_test.go" + - "executor/proto/" + - "crates/hqgit-eval/src/sandbox.rs" +extends: + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/src/lib.rs", nature: additive } + # A guarded `go` job joins the CI gate for the executor module. + - { spec: "001-agentic-harness", unit: ".github/workflows/govern.yml", nature: additive } +summary: > + The one language boundary in the platform (thesis D14): a Go worker that + leases actions from the execution service (072), stages their inputs + from CAS, runs them in one of two sandbox tiers, uploads the outputs, and + reports exactly which isolation it applied. The Trusted tier is Linux + namespaces plus a seccomp allowlist, no network, read-only inputs, an + unprivileged uid. The Untrusted tier is a microVM (Firecracker or Cloud + Hypervisor behind one driver interface) booted from a rootfs whose hash + is pinned, with no network device unless the action declared one. Fork + contributions and agent principals always get the Untrusted tier; the + Rust side owns that rule and the tier vocabulary, so the domain model + never crosses into Go. The executor's sandbox report is what spec 074 + attests, which is why a missing report fails the action. +--- + +# 073: Sandbox executor + +## 1. Purpose + +Thesis §4.4 names two sandbox tiers and the rule that fork contributions +and agent execution get the stronger one. The design analysis (§5) +concedes the execution plane to Go, where the container and microVM SDKs +live, on the condition that the domain model stays on the Rust side. This +spec draws that seam precisely: Go receives REAPI messages and a tier +name, Rust decides which tier a principal deserves and validates the +report that comes back. The report matters because provenance (074) is +only as true as the isolation it describes; an executor that cannot say +what it did cannot have its results trusted. + +## 2. Territory + +The `executor/` Go module (`github.com/statecrafting/hqgit/executor`): +the binary under `cmd/hq-executor`, the REAPI and Worker clients under +`internal/reapi`, the sandbox tiers under `internal/sandbox`, vendored +proto descriptors under `executor/proto/` generated from spec 070's and +072's protos, and the module's `Makefile` (`build`, `vet`, `test`, +`lint`). On the Rust side, `crates/hqgit-eval/src/sandbox.rs`: the +`SandboxTier` vocabulary, the tier assignment rule, and the report parser. +Additively: a guarded `go` job in the CI workflow. The scheduler that +hands out leases is spec 072; the attestation over the report is spec 074. + +## 3. Behavior + +- **B-1 (tier vocabulary, Rust).** `enum SandboxTier { Trusted, Untrusted + }` with `Ord` (`Untrusted > Trusted`) and the wire spellings `trusted` + and `untrusted` used in the platform property `hqgit.sandbox-tier`. + `SandboxDeclaration { tier, network: NetworkPolicy::{None, + Declared(BTreeSet<String>)}, rootfs: Option<Hash> }` is what an action + declares (075 fills it from the build manifest). +- **B-2 (assignment rule, Rust).** `required_tier(principal: PrincipalKind, + origin: ChangeOrigin::{Member, Fork}) -> SandboxTier` returns `Untrusted` + for `PrincipalKind::Agent` and for `ChangeOrigin::Fork` regardless of + principal, and `Trusted` otherwise. `check_lease(worker_tiers, required) + -> Result<(), Error>` refuses a lease when the worker's registered tiers + (071 B-5) do not include the required one. Nothing in Go may lower a + tier: the tier travels in the action's platform properties and the + worker either honors it or refuses the lease. +- **B-3 (report, both sides).** `SandboxReport { tier: SandboxTier, + isolation: Vec<String> (the primitives actually applied, e.g. + `user-ns`, `mount-ns`, `pid-ns`, `net-ns-empty`, `seccomp:v1`, + `firecracker:1.x`), rootfs: Option<Hash>, network: NetworkPolicy, + executor_version: String }` is serialized by Go as the + `hqgit.eval.v1.SandboxReport` message into + `ExecutedActionMetadata.auxiliary_metadata` and parsed on the Rust side + by `parse_report(result: &ActionResult) -> Result<SandboxReport, Error>`; + a result without a report, or whose reported tier is lower than the + action's required tier, is `Error::Validation` and the completion is + failed (072 B-8 hook error path). +- **B-4 (worker loop, Go).** `hq-executor` registers with the Worker + service (072 B-7) presenting its executor identity (a Service key issued + by the operator, stored on disk with mode 0600) and its supported tiers, + then loops: lease, fetch the `Action`, `Command`, and input `Directory` + tree from CAS into a fresh staging directory (files read-only, symlinks + refused when absolute, executables marked), run the command under the + tier, collect declared output paths, upload outputs and stdout/stderr + to CAS, and `Complete` with the `ActionResult` and the report. + Heartbeats run on a ticker at half the lease interval. Any failure is + reported as a completion with a non-zero exit and the error in + `stderr`, never a silent drop. +- **B-5 (Trusted tier, Linux).** `namespaces_linux.go` creates a child in + new user, mount, pid, net, ipc, and uts namespaces (`CLONE_NEW*`), maps + the caller to an unprivileged uid inside, mounts the staging directory + read-only at `/work/in`, a tmpfs at `/work/out` and `/tmp` with a size + cap, `/proc` fresh, and nothing else from the host; the net namespace + has only loopback and stays down; all capabilities are dropped; + `no_new_privs` is set; rlimits bound cpu time, memory, file size, and + process count from the action's timeout and platform properties. + `seccomp_linux.go` installs a default-deny BPF allowlist (via + `libseccomp-golang`) of the syscalls a toolchain needs, explicitly + denying `ptrace`, `mount`, `umount2`, `pivot_root`, `reboot`, `kexec_*`, + `bpf`, `io_uring_*`, `perf_event_open`, `keyctl`, `add_key`, and + `userfaultfd`; the profile is versioned (`seccomp:v1`) and its hash is + part of the report's `isolation`. +- **B-6 (Untrusted tier).** `microvm.go` defines `type VmmDriver interface + { Boot(ctx, spec) (VM, error) }` with Firecracker (`firecracker-go-sdk`) + and Cloud Hypervisor (its REST API over a unix socket) implementations + selected by configuration; the rootfs image is opened, hashed with + BLAKE3, and compared to the action's declared `rootfs` (or the + executor's configured default) before boot, a mismatch refusing the + lease; the VM gets a vsock agent that receives the staged inputs as a + read-only virtio block image, runs the command, and returns outputs as + a block image; no network device is attached unless the action's + `NetworkPolicy` is `Declared`, in which case a tap with an egress + allowlist of the declared hosts is attached and the report records it. + The VM is destroyed after every action; nothing persists between + actions. +- **B-7 (platform and portability).** The Linux tier files carry + `//go:build linux` tags; on other platforms the worker builds, refuses + to register any tier, and exits 3, so a misconfigured host cannot + silently run unsandboxed. There is no tier that runs a command on the + host directly. +- **B-8 (Makefile and CI).** `executor/Makefile` targets `build`, `vet`, + `test`, `lint` (`staticcheck` when present); the CI workflow gains a + `go` job guarded on `hashFiles('executor/go.mod') != ''` that runs `go + vet ./...` and `go test ./...` with `-race`. + +## 4. Functional requirements + +- **FR-001.** `sandbox_test.go` runs without root: tier parsing, report + serialization round trip through the proto, seccomp profile compiles + and its hash is stable, rootfs hash check refuses a mismatch, the + no-tier platform path exits 3. Tests that need namespaces are tagged + and skipped unless `HQ_SANDBOX_TESTS=1` and the caller is root. +- **FR-002.** Rust tests in `hqgit-eval` cover `required_tier` for every + principal kind and origin, `check_lease`, `parse_report` on a fixture + result, and refusal of a report whose tier is below the requirement. +- **FR-003.** The Go module vendors no Rust; its only contract with the + workspace is the proto descriptors under `executor/proto/`, regenerated + by `make -C executor proto` from `crates/hqgit-eval/proto/`. +- **FR-004.** `go.mod` pins Go 1.23 or later and every dependency by + version; `go.sum` is committed. + +## 5. Acceptance criteria + +- **AC-1.** `cd executor && go vet ./... && go test ./...` passes. +- **AC-2.** `cargo test -p hqgit-eval --locked sandbox` passes. +- **AC-3.** Against the 072 fixture server, `hq-executor` in Trusted mode + on a Linux host completes an `echo` action with a report naming + `user-ns`, `net-ns-empty`, and `seccomp:v1`. + +## 6. Out of scope + +Provenance over the report (074); the build manifest that declares +network hatches (075); executor fleet management and autoscaling; Windows +and macOS sandbox tiers (the worker refuses on those hosts, B-7); GPU +passthrough. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cd executor && go vet ./... && go test ./... +cargo test -p hqgit-eval --locked sandbox +``` diff --git a/specs/074-execution-provenance/spec.md b/specs/074-execution-provenance/spec.md new file mode 100644 index 0000000..d487567 --- /dev/null +++ b/specs/074-execution-provenance/spec.md @@ -0,0 +1,155 @@ +--- +id: "074-execution-provenance" +title: "Execution provenance: every completed action emits a signed SLSA attestation" +status: approved +kind: "kernel" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 5 +depends_on: + - "072-execution-service" + - "064-attestation-verification" +establishes: + - "crates/hqgit-eval/src/provenance.rs" + - "crates/hqgit-eval/tests/provenance.rs" + - "crates/hqgit-eval/testdata/slsa/" +extends: + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/src/lib.rs", nature: additive } + # The provenance claim schema is SLSA Provenance v1, registered as a predicate. + - { spec: "027-attestation-primitive", unit: "crates/hqgit-domain/src/predicate.rs", nature: additive } +summary: > + CI results are not a separate system; they are evidence (thesis §4.4). + This spec closes that loop: every completed execution emits an + Attestation with predicate hqgit/provenance/v1 whose claim is a SLSA + Provenance v1 predicate (builder = the executor's Service identity, + buildType = the REAPI action, resolved dependencies = the input tree, + command, and toolchain hashes, byproducts = the output digests, plus the + sandbox report), signed by the executor and appended to the repository + ledger as an attestation.issued fact. The cache entry (071) references + that attestation, and verifying it through the trust plane (064) is + exactly what turns a cache hit into a gating-grade hit. An execution + whose sandbox report is missing gets no attestation and no cache entry. +--- + +# 074: Execution provenance + +## 1. Purpose + +Constitution IX: there is one evidence primitive, and a build result is +not exempt. Design §1.1 point 4 asks for every result to become an +attestation over (input hash, toolchain hash, output hash), and point 5 +asks for in-toto/SLSA provenance linking source to artifact. Both are the +same object once the claim is SLSA Provenance v1 and the subject is the +action key. This spec is the hook (072 B-8) that mints it, and the reason +the action cache (071) can be a trust boundary rather than a hope. + +## 2. Territory + +`provenance.rs` in `crates/hqgit-eval` (the claim builder, the +`ProvenanceHook`, and the ledger append) and `tests/provenance.rs`, with +the SLSA v1 schema and expected statements vendored under +`testdata/slsa/`. Additively: the `hqgit/provenance/v1` claim validator in +spec 027's predicate registry and the crate's re-exports. The executor's +report is spec 073; the verification this attestation passes through is +spec 064. + +## 3. Behavior + +- **B-1 (predicate).** `PredicateType("hqgit/provenance/v1")` is + registered with a claim validator; the claim object is the SLSA + Provenance v1 predicate body, so `Attestation::to_in_toto()` (027) + yields a statement any SLSA verifier reads with `predicateType = + "https://slsa.dev/provenance/v1"`. The in-toto subject is the + `ActionKey` hash under the name `hqgit:action`. +- **B-2 (claim mapping).** `build_claim(ctx: &CompletionContext, result: + &ActionResult, report: &SandboxReport) -> ProvenanceClaim` fills: + `buildDefinition.buildType = "https://hqgit.dev/buildtype/reapi-action/v1"`; + `externalParameters = { action: <action digest>, target: <TargetId>, + toolchain: <toolchain hash>, platform: <sorted properties> }`; + `internalParameters = { sandbox_tier, isolation, rootfs, network, + executor_version }` from the report; `resolvedDependencies = [ { name: + "input-root", digest: { blake3: <tree cid hash> } }, { name: "command", + digest }, { name: "toolchain", digest } ]` sorted by name; + `runDetails.builder.id = "hqgit:service:<ServiceId hex>"` with + `builder.version = { "hq-executor": <executor_version> }`; + `runDetails.metadata.invocationId = <operation id>`, `startedOn` and + `finishedOn` copied from `ExecutedActionMetadata.worker_start_timestamp` + and `worker_completed_timestamp` (executor-supplied evidence, never a + clock read in this crate); `runDetails.byproducts = [ every output file, + directory, and symlink digest, then stdout and stderr digests ]` sorted + by name; and `exit_code` as a top-level extension field. All digests are + `{ "blake3": <hex> }`. +- **B-3 (signing and issuing).** `ProvenanceHook { signer: Box<dyn + Signer>, executor: ExecutorIdentity, ledger: Box<dyn LedgerAppend> }` + implements 072's `CompletionHook`: it validates the report (073 B-3), + builds the claim, stores the claim as a DagCbor object in the repository + object store, signs the `Attestation { subject: action key hash, + predicate, issuer: Principal::Service(executor.service), issuer_key: + executor.key, claim: Cid, at: <the completion Hlc from the ledger clock>, + sig }` under `SignDomain("attestation")`, appends an + `attestation.issued` fact through the `LedgerAppend` seam (which 021's + `Repository::append_fact` implements, taking the namespace from the + action's `hqgit.namespace` platform property), and returns the 071 + `CacheEntry` with `attestation: Some(id)` and `sandbox_tier` set. +- **B-4 (refusals).** A missing or under-tier sandbox report, an executor + not present in the registry at completion time, a claim the predicate + validator rejects, or a ledger append failure each return `Err` from the + hook; the operation fails with the reason (072 B-8) and no cache entry is + written. There is no unattested fallback in this hook: an operator who + wants advisory-only results configures 072's `UnattestedHook` instead, + visibly. +- **B-5 (gating grade).** The attestation's subject is the action key and + its byproduct digests equal the entry's outputs, which is precisely what + 071 B-3 checks for a `Gating` hit once 064 has verified the executor's + signature and identity. `gating_verified(entry, verified) -> bool` is + provided here as the single implementation both the cache and the merge + queue (076) call. +- **B-6 (determinism).** `build_claim` is a pure function of its + arguments; two calls with the same inputs produce byte-identical claim + objects and therefore one Cid. The only non-reproducible fields are the + timestamps and operation id, which are inputs. + +## 4. Functional requirements + +- **FR-001.** `testdata/slsa/` holds the SLSA Provenance v1 JSON schema + and an expected in-toto statement for a fixture completion; a test + validates the emitted statement against the schema and compares it to + the expectation field by field. +- **FR-002.** Tests cover: the hook mints, signs, appends, and returns an + attested entry through a fake `LedgerAppend`; the attestation verifies + through 064's verifier with a static resolver; `gating_verified` is true + for the fixture and false when an output digest is altered; each B-4 + refusal fails the completion with no ledger append and no entry; claim + determinism across two builds. +- **FR-003.** The `LedgerAppend` seam is a one-method trait (`fn + append(&self, namespace: &Hash, fact: DomainFact) -> Result<EntryHash, + Error>`) so tests never open a repository. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-eval --locked provenance` passes. +- **AC-2.** In the 072 fixture flow with the `ProvenanceHook` installed, a + completed action yields a cache entry whose attestation verifies and + whose `Gating` lookup is a hit; removing the report from the completion + yields a failed operation and no entry. + +## 6. Out of scope + +Verification itself (064); transparency-log submission of the attestation +(062's client is wired by the server in 090 and later); provenance for +artifacts published outside the repository (release provenance is a later +spec); agent-action provenance (102). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-eval --locked provenance +``` diff --git a/specs/075-build-graph/spec.md b/specs/075-build-graph/spec.md new file mode 100644 index 0000000..a055971 --- /dev/null +++ b/specs/075-build-graph/spec.md @@ -0,0 +1,220 @@ +--- +id: "075-build-graph" +title: "Build graph: the hq-build.toml manifest, pinned toolchains, and affected-target selection" +status: approved +kind: "feature" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 5 +depends_on: + - "070-reapi-types" +establishes: + - "crates/hqgit-eval/src/graph/mod.rs" + - "crates/hqgit-eval/src/graph/manifest.rs" + - "crates/hqgit-eval/src/graph/toolchain.rs" + - "crates/hqgit-eval/src/graph/affected.rs" + - "crates/hqgit-eval/tests/graph.rs" + - "crates/hqgit-eval/testdata/graph/" +extends: + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/src/lib.rs", nature: additive } + # toml and globset join the crate's dependencies. + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/Cargo.toml", nature: additive } + # globset is new to the workspace; toml was pinned by 032. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Design doc §1.1 point 4: CI is a pure function of repository state. This + spec gives that function its argument list. A repository declares its + targets in hq-build.toml (inputs as globs over the tree, an argv command, + a pinned toolchain, declared outputs, target dependencies, and the escape + hatches network and nondeterministic), a toolchain is the content hash of + a pinned image or a nix-style closure, and a target's identity is a pure + function of the subtree its globs select. Affected-target selection is a + comparison of those identities between two tree cids, transitive over + dependencies, with no execution. Every declaration becomes an action + platform property (070), so an escape hatch is visible in provenance + (074) rather than hidden in a script, and a target that declares nothing + cannot read anything it did not declare in the Trusted tier (073). +--- + +# 075: Build graph + +## 1. Purpose + +Thesis §4.4 fixes `eval(repo_state_hash, target, toolchain_hash) -> +output_hash` and thesis §8 names the standing risk: hermetic builds tax +ergonomics, and the escape hatches are where the model leaks, so they must +be attested too. This spec is the manifest that makes `target` and +`toolchain_hash` well-defined, the hermeticity rule that makes the input +hash honest, and the affected-target algorithm the merge queue (076) uses +to know which results it may reuse. It executes nothing. + +## 2. Territory + +The `graph/` module of `crates/hqgit-eval`: `mod.rs` (the `BuildGraph` +type and the action builder), `manifest.rs` (the `hq-build.toml` grammar, +parser, and validation), `toolchain.rs` (toolchain sources and hashing), +`affected.rs` (target keys and affected selection); `tests/graph.rs`; and +recorded selection vectors under `testdata/graph/`. Additively: the crate +manifest (`toml`, `globset`) and `lib.rs`. Executing an action is 072 and +073; attesting it is 074; deciding to merge on the results is 076. + +## 3. Behavior + +- **B-1 (manifest location and schema).** The manifest is the file + `hq-build.toml` at the root of a spec 013 `Tree`. `Manifest::parse(bytes) + -> Result<Manifest, Error>` returns `Error::Parse` on invalid TOML, + `Error::Schema` on a `schema` MAJOR other than `1`, and `Error::Config` + naming the key on any unknown key at any level (no silent extras). The + grammar, every key listed: + + ```toml + schema = "1.0.0" # required + + [toolchain.<name>] # one table per toolchain + image = "blake3:<64 hex>" # OCI image digest, or + closure = "blake3:<64 hex>" # nix-style closure hash + reference = "ghcr.io/org/img:tag" # optional, descriptive only + + [target.<name>] # one table per target + inputs = ["src/**/*.rs", "Cargo.toml"] # globs over the tree, required + deps = ["other-target"] # target dependencies, default [] + command = ["cargo", "test", "--locked"]# argv, no shell, required + env = { RUSTFLAGS = "-D warnings" } # default {} + toolchain = "rust" # a [toolchain.*] name, required + outputs = ["target/report.json"] # declared output paths, default [] + timeout_s = 600 # default 900, max 86400 + tier = "trusted" # "trusted" | "untrusted", default "trusted" + gate = true # participates in merge gating, default true + + [target.<name>.escape] # optional; the escape hatches + network = ["index.crates.io"] # egress hosts, default [] + nondeterministic = false # outputs not a pure function, default false + ``` + + Exactly one of `image` and `closure` MUST be present per toolchain. + Names match `^[a-z][a-z0-9_-]*(/[a-z][a-z0-9_-]*)*$`; `TargetId` (070 + B-5) is the validated target name. Globs use `globset` syntax, are + relative to the tree root, MUST NOT begin with `/` or contain `..`, and + the manifest itself is an implicit input of every target. `env` keys + match `^[A-Z_][A-Z0-9_]*$` and are emitted sorted. +- **B-2 (validation).** `Manifest::validate(&self) -> Result<BuildGraph, + Error>` returns `Error::Validation` naming the target for: a `deps` entry + that is not a target, a `toolchain` that is not declared, a dependency + cycle (the cycle is listed in order), an output path equal to or nested + inside another target's output path, an `outputs` entry that is absolute + or contains `..`, and an empty `command`. `BuildGraph { targets: + BTreeMap<TargetId, Target>, toolchains: BTreeMap<String, Toolchain>, + order: Vec<TargetId> }` where `order` is the dependency order with ties + broken by name, so iteration is deterministic. +- **B-3 (toolchain).** `Toolchain { name: String, source: ToolchainSource, + reference: Option<String> }` with `ToolchainSource` a closed enum `Image + { digest: Hash } | Closure { hash: Hash }`. `toolchain_hash(&Toolchain) + -> Hash` is `Hash::of(b"hqgit/v1/toolchain/image" || digest)` or + `Hash::of(b"hqgit/v1/toolchain/closure" || hash)`; `reference` never + enters the hash. This hash is the `toolchain` field of 070's + `ActionInput` and, for an `Image` source, the `rootfs` the Untrusted tier + (073 B-6) pins. +- **B-4 (input selection).** `select_inputs(tree: &Cid, target: &Target, + store: &dyn ObjectStore) -> Result<InputSet, Error>` walks the tree (013 + B-3 order), matches every file and symlink path against the target's + globs, and returns `InputSet { entries: BTreeMap<String, (EntryMode, + Cid)> }` plus the manifest entry. A glob matching nothing is allowed + (generated paths may be absent); an `InputSet` that is empty apart from + the manifest is `Error::Validation`. `input_root(set, store) -> + Result<Cid, Error>` builds and stores the pruned spec 013 `Tree` holding + exactly those entries at their original paths, so the input root contains + nothing the target did not declare. That pruned tree is what 073 B-5 + mounts read-only: hermeticity is a property of the input root, not of + a sandbox rule. +- **B-5 (target key).** `target_key(tree: &Cid, target: &TargetId, graph: + &BuildGraph, store) -> Result<Hash, Error>` is `Hash::of(canonical + DagCbor of { input_root, command, env, toolchain: toolchain_hash, tier, + escape, timeout_s, deps: [target_key(dep) for dep in deps sorted] })`. + A dependency's key enters its dependents' keys, so a change anywhere + upstream changes every downstream key without executing anything. + `TargetKeys = BTreeMap<TargetId, Hash>` for a whole tree is computed in + `order` with memoization, in one pass. +- **B-6 (affected).** `affected(old: &Cid, new: &Cid, graph: &BuildGraph, + store) -> Result<Affected, Error>` returns `Affected { changed: + BTreeSet<TargetId>, unchanged: BTreeSet<TargetId>, added: + BTreeSet<TargetId>, removed: BTreeSet<TargetId> }` by comparing + `TargetKeys` of both trees, where `graph` is parsed from `new`'s manifest + and `old`'s manifest is parsed separately (a target present in only one + is `added` or `removed`). A target is `changed` when its key differs; + because keys are transitive (B-5), no separate closure step exists. The + function is pure over its arguments and never consults a cache. +- **B-7 (action construction).** `to_action_input(tree: &Cid, target: + &TargetId, graph, dep_outputs: &BTreeMap<TargetId, Cid>, namespace: + &Hash, store) -> Result<ActionInput, Error>` builds 070 B-5's + `ActionInput` with `input_root` = the pruned tree of B-4 plus each + dependency's output tree grafted at `.hq-deps/<dep name>/` (`dep_outputs` + MUST cover every dependency; a missing one is `Error::NotFound`), + `command` from `command`, `env`, and `outputs` (`Command.output_paths`), + `timeout_s`, `do_not_cache = escape.nondeterministic`, and platform + properties `hqgit.target`, `hqgit.toolchain`, `hqgit.sandbox-tier` + (`tier` spelled as 073 B-1), `hqgit.rootfs` (image digest hex, `Image` + sources only), `hqgit.namespace`, `hqgit.network` (declared hosts joined + by `,`, present only when non-empty), and `hqgit.nondeterministic` + (`"true"`, present only when set). A nondeterministic target therefore + never enters the cache (071 B-7) and is executed on every evaluation; + every escape hatch is an external parameter of the provenance claim (074 + B-2) by construction. +- **B-8 (tier floor).** `tier` in the manifest is a floor, never a ceiling: + 073 B-2's assignment rule may raise an action to `untrusted` (fork + origin, agent principal) and nothing in this module may lower it. A + target declaring `network` non-empty with `tier = "trusted"` is + `Error::Validation`: the Trusted tier has no network (073 B-5), so the + declaration would be a lie. +- **B-9 (no ambient input).** No clock, environment, working directory, + or `HashMap`; the tree is read only through the `ObjectStore` seam. + +## 4. Functional requirements + +- **FR-001.** `testdata/graph/<case>/` holds `hq-build.toml`, `before/` + and `after/` directory snapshots, and `expected.json` listing `changed`, + `unchanged`, `added`, `removed`, and each target's platform properties. + Cases: a leaf edit affecting one target and its dependents, a + manifest-only edit affecting every target, a dependency-only change, a + toolchain digest bump, an added and a removed target, an edit outside + every glob affecting nothing, and an escape-hatch target. +- **FR-002.** Tests cover: every B-1 parse and B-2 validation refusal with + the named key or target; `toolchain_hash` vectors for both sources with + `reference` ignored; `select_inputs` excluding undeclared paths and + including the manifest; `input_root` reproducing a fixture tree cid; + `target_key` stability across two computations and sensitivity to each + field; `affected` against every FR-001 case; `to_action_input` platform + properties, `do_not_cache` for a nondeterministic target, and the dep + graft path; the B-8 refusals. +- **FR-003.** Computing `TargetKeys` for a graph of 1,000 targets in a + chain visits each target once (asserted through a counting store). +- **FR-004.** The module exposes no function that executes a command or + opens a network connection; `cargo tree -p hqgit-eval` gains only `toml` + and `globset`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-eval --locked graph` passes, vectors + included. +- **AC-2.** For every FR-001 case, `affected(before, after)` equals + `expected.json` and `affected(after, after)` reports everything + `unchanged`. + +## 6. Out of scope + +Executing targets (072, 073); the provenance claim the properties land in +(074); the merge queue that consumes `affected` (076); a `hq build` verb +(a later CLI spec); per-language dependency inference (targets declare). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-eval --locked graph +``` diff --git a/specs/076-merge-queue/spec.md b/specs/076-merge-queue/spec.md new file mode 100644 index 0000000..6a7c176 --- /dev/null +++ b/specs/076-merge-queue/spec.md @@ -0,0 +1,198 @@ +--- +id: "076-merge-queue" +title: "Merge queue: speculative evaluation over candidate merge states, batching, and bisection" +status: approved +kind: "feature" +domain: "l3-evaluation" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 5 +depends_on: + - "075-build-graph" + - "074-execution-provenance" + - "067-policy-evaluation-attestation" +establishes: + - "crates/hqgit-eval/src/queue.rs" + - "crates/hqgit-eval/src/speculate.rs" + - "crates/hqgit-eval/tests/queue.rs" +extends: + - { spec: "070-reapi-types", unit: "crates/hqgit-eval/src/lib.rs", nature: additive } + # The queue's own facts (enqueued, dequeued, merged) join the vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + Thesis §4.4: merge queues become speculative evaluation over candidate + merge states. A candidate is a change whose latest revision carries a + verified policy-eval attestation (067) with verdict Allow; the queue + never runs a policy live (constitution XI). The speculator builds the + merge tree of trunk plus the candidates in order, computes the affected + targets (075), and evaluates every gating target through the execution + service with gating cache lookups (071), so a target whose key is + unchanged since trunk costs a verified cache hit and nothing else. A + green batch merges by appending facts; a red batch bisects until each + failure is pinned on one candidate, which is dequeued with the evidence. + Queue state is facts, so the queue is a fold any replica can rebuild. +--- + +# 076: Merge queue + +## 1. Purpose + +Design doc §1.1 point 4 promises merge-queue correctness falling out as a +property, not as a feature. It does, once three things hold: the trunk's +gating results are attested cache entries keyed by target keys (075 B-5), +a candidate's merge tree yields the same key for every target it does not +touch, and a gating lookup refuses anything unattested (071 B-3, thesis +D10). Then evaluating a batch is exactly the cost of what the batch +changed, and merging on green is sound because every result consumed was +either produced under provenance (074) or verified as identical input. +This spec is the driver that composes those parts and the facts that +record what it did. + +## 2. Territory + +`queue.rs` (the queue facts, the `QueueView` fold, candidate admission, +the trunk view) and `speculate.rs` (merge trees, batch planning, +evaluation through the seams, bisection) in `crates/hqgit-eval`, plus +`tests/queue.rs`. Additively: three fact kinds in spec 023's vocabulary +and the crate's re-exports. Running the driver on a schedule and exposing +it over the API is the server's (a later edge spec over 093); the CLI +verb that enqueues is a later CLI spec. + +## 3. Behavior + +- **B-1 (facts).** Three kinds join 023 B-2: `queue.enqueued { change: + ChangeId, revision: RevisionId, policy_eval: AttestationId, enqueued_by: + Principal }`; `queue.dequeued { change, revision, reason: Dequeue }` with + `Dequeue` a closed enum `Withdrawn | Superseded(RevisionId) | + Conflict(Vec<String>) | Failed(Vec<TargetId>) | PolicyRevoked`; and + `queue.merged { batch: Hash, base: Cid, trunk: Cid, merged: Vec<(ChangeId, + RevisionId)>, evidence: Vec<AttestationId> }` where `evidence` lists + the provenance attestation of every gating target's consumed entry. A + `queue.merged` fact is accompanied, in the same append sequence, by one + `change.merged` (023) per merged change so the change view (024) agrees. +- **B-2 (trunk).** `TrunkView` is a `DerivedState` (019) folding + `queue.merged` into `Trunk { tree: Cid, since: EntryHash }`, the tree of + the latest merge in total order (018). A repository with no merge yet + has trunk `= QueueConfig.initial_trunk` (a tree cid the operator names; + for a git-backed repository, 031's mapping of the default branch tree). + The first batch evaluates every gating target because nothing is + cached; every later batch pays only for what changed. +- **B-3 (candidates).** `QueueView` folds `queue.enqueued` and + `queue.dequeued` into an ordered `Vec<Queued>` by the enqueue fact's + `(hlc, hash)`. `admit(queued, changes: &ChangeView, verified: + &VerifiedAttestationSet, config) -> Result<Candidate, Dequeue>` requires: + the change is `Open` and `revision` is its latest (else `Superseded`); + `policy_eval` is in `verified` with `Verdict::Ok`, subject equal to the + revision id, predicate `hqgit/policy-eval/v1`, claim `verdict = Allow`, + and claim `policy` equal to `config.policy` (else `PolicyRevoked`). The + verified set is an input built by 064; this crate never evaluates a + policy and never verifies a signature (constitution XI: the gate consumes + the attestation). +- **B-4 (merge tree).** `merge_tree(store, base: &Cid, ours: &Cid, theirs: + &Cid) -> Result<Merged, Error>` is a path-level three-way merge of spec + 013 trees: an entry changed on one side only takes that side; changed + identically on both is taken; changed differently on both, or changed on + one side and removed on the other, is a conflict. `Merged::Clean(Cid)` + stores the result; `Merged::Conflict(Vec<String>)` lists the paths. There + is no textual or semantic merge here (052 owns that); a file-level + conflict rejects the candidate with `Dequeue::Conflict`. +- **B-5 (batch).** `plan_batch(trunk: &Trunk, candidates: &[Candidate], + config, store) -> Batch` folds candidates in queue order, up to + `config.max_batch` (default 8): each candidate's revision tree is merged + onto the running tree with `base = revision.base`; a conflicting + candidate is set aside with its paths and the fold continues. `Batch { + id: Hash, base: Cid, members: Vec<Candidate>, tree: Cid, rejected: + Vec<(Candidate, Dequeue)> }` with `id = Hash::of(b"hqgit/v1/batch" || + base || member revision ids in order)`. +- **B-6 (evaluation).** `speculate(batch, graph: &BuildGraph, seams: + &Seams) -> Result<BatchResult, Error>` where `Seams { evaluator: &dyn + Evaluator, cache: &dyn ActionCache, verifier: &dyn GateVerifier, store }` + and `trait Evaluator { fn evaluate(&self, input: &ActionInput) -> + Result<ActionKey, Error>; }` (072's Execute, blocking until complete; + results reach the cache through 074's hook), `trait GateVerifier { fn + verify(&self, ids: &[AttestationId]) -> VerifiedAttestationSet; }` + (064). For every target with `gate = true` in dependency order (075 + B-2): build the `ActionInput` (075 B-7) with dependency outputs taken + from earlier results; `lookup(key, Gating, verified)`; on `Hit` record + `Reused`; on any `Miss` call `evaluate`, refresh `verified` for the new + entry's attestation, and look up again; a second miss is + `TargetOutcome::Unattested` and counts as red. A hit whose result has + `exit_code != 0` is `Red`. `BatchResult { batch, targets: + BTreeMap<TargetId, TargetOutcome::{Green { key, reused: bool, + attestation }, Red { key, attestation }, Unattested { key }}>, reused: + u32, executed: u32 }`. `affected(trunk.tree, batch.tree)` (075 B-6) is + computed first and recorded on the result; a test asserts that no target + in `unchanged` is ever executed, which is the content-addressing + guarantee made observable. +- **B-7 (bisection).** `resolve(batch, result, ...) -> Resolution`: an + all-green batch is `Resolution::Merge(batch)`. A red batch of one member + is `Resolution::Reject(member, Dequeue::Failed(red targets))`. A red + batch of `n > 1` members splits into the prefix `[0, n/2)` and the + suffix `[n/2, n)`, re-plans each as a batch on the same base in the same + order (the prefix's tree is a prefix of the fold, the suffix is re-merged + onto trunk), evaluates the prefix first, merges it if green, then + evaluates the suffix on the new trunk; recursion continues until every + member is merged or rejected. Total evaluations are `O(k log n)` for + `k` failing members; a test pins the count for the fixtures. +- **B-8 (commit).** `Driver::step(&mut self) -> Result<StepReport, Error>` + performs one plan, speculate, resolve cycle and appends, through the + 074 `LedgerAppend` seam in this order: `queue.dequeued` for every + rejected candidate, then for a merge `change.merged` per member + (024 `mark_merged`) followed by one `queue.merged`. A merge appends only + when every gating target is `Green`; `evidence` lists their attestation + ids so the merge is replayable evidence, not a log line. A concurrent + `queue.merged` observed after planning (the trunk moved) discards the + plan and the next step re-plans; nothing is appended twice. +- **B-9 (no ambient input).** The driver reads no clock; ordering comes + from entry `Hlc`s and all time-dependent judgment (attestation + freshness) lives in the verifier seam. + +## 4. Functional requirements + +- **FR-001.** Every function in `speculate.rs` is pure over its arguments + and the seams; `tests/queue.rs` implements `Evaluator` with a + `FakeExecution` that maps action keys to scripted outcomes, counts + calls, and writes attested entries through an in-memory 071 cache using + 064's test constructor for the verified set. +- **FR-002.** Tests cover: the three facts round-trip canonically and + `QueueView` orders by `(hlc, hash)`; admission refuses a superseded + revision, a `Deny` verdict, a policy cid mismatch, and an unverified + attestation; `merge_tree` for one-sided, identical, conflicting, and + removed-versus-modified entries; a clean batch of three merges with one + `queue.merged` and three `change.merged`; a batch with one failing + member of eight bisects to exactly that member with the pinned + evaluation count; a nondeterministic target executes on every step; an + unattested execution never merges; reuse counts equal the `unchanged` + set size on a second step with no new candidates; a moved trunk discards + the plan. +- **FR-003.** `Driver` holds no state beyond what `QueueView` and + `TrunkView` fold; a test rebuilds both from zero mid-run and continues + with identical results. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-eval --locked queue` passes. +- **AC-2.** In the fixture with two candidates touching disjoint targets, + the second step executes zero targets from `unchanged` and its + `queue.merged.evidence` verifies through the 064 verifier. + +## 6. Out of scope + +Semantic or textual conflict resolution (052); ordering by stack +dependencies (050's `StackView` is consulted by a later amendment); +scheduling the driver and exposing queue status over the API (a server +spec over 093); the `hq queue` verbs (a later CLI spec); cross-host queue +coordination (a queue runs on the repository's control-plane leader, 091). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-eval --locked queue +``` diff --git a/specs/080-projection-framework/spec.md b/specs/080-projection-framework/spec.md new file mode 100644 index 0000000..2398209 --- /dev/null +++ b/specs/080-projection-framework/spec.md @@ -0,0 +1,194 @@ +--- +id: "080-projection-framework" +title: "Projection framework: disposable read models folded from the total order" +status: approved +kind: "kernel" +domain: "l5-projection" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 6 +depends_on: + - "021-local-repository" + - "032-cli-skeleton" +establishes: + - "crates/hqgit-projection/Cargo.toml" + - "crates/hqgit-projection/src/lib.rs" + - "crates/hqgit-projection/src/projection.rs" + - "crates/hqgit-projection/src/runner.rs" + - "crates/hqgit-projection/src/sqlite.rs" + - "crates/hqgit-projection/src/checkpoint.rs" + - "crates/hqgit-projection/src/registry.rs" + - "crates/hqgit-projection/tests/" + - "crates/hqgit-cli/src/cmd_projection.rs" +extends: + # The `hq projection` verb rides in the CLI 032 founded. + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } + # rusqlite (bundled) joins the shared dependency table. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Constitution VI in code: every index, timeline, queue, and dashboard is a + projection that can be rebuilt from zero, and no projection is ever read + as authority. This spec founds hqgit-projection: the Projection trait, a + runner that folds the ledger in the deterministic total order (018) from + a crash-consistent checkpoint, SQLite-backed storage under .hq/projections + where the apply and the checkpoint commit in one transaction, a registry + that later specs (081 to 085) plug their read models into, the rule that + every read answers "as of entry N", the rule that erased payloads render + as erased, and the `hq projection` verbs to inspect and rebuild. It owns + no concrete view. +--- + +# 080: Projection framework + +## 1. Purpose + +Thesis §3: canonical state is the signed DAG; everything else is derived +and must be rebuildable from zero. Thesis §4.7 places projections in L5, +the disposable layer, and constitution XIII forbids them from writing +authoritatively. Holding that line is a mechanism, not a habit: this spec +is the one place a read model is allowed to be built, the one fold that +feeds it, and the one checkpoint that says how far it has read. Specs 081 +through 085 are instances of the trait defined here; the API (093) and the +UI (095) query them and repeat, on every answer, which ledger entry the +answer is projected as of. + +## 2. Territory + +`crates/hqgit-projection` as founded here: the manifest, `lib.rs`, +`projection.rs` (the trait and the fold step types), `runner.rs` (the fold +driver), `sqlite.rs` (the storage seam and its SQLite implementation), +`checkpoint.rs` (the cursor), `registry.rs` (named projections by +factory), and the `tests/` subtree. The `hq projection` verb lives in +`cmd_projection.rs` and extends the CLI frame of 032. The crate depends on +`hqgit-types`, `hqgit-object`, `hqgit-ledger`, and `hqgit-domain` only, +and never on `hqgit-cli` or `hqgit-server`. + +## 3. Behavior + +- **B-1 (the trait).** `trait Projection { const NAME: &'static str; + const SCHEMA_VERSION: u32; fn init(&mut self, tx: &mut Tx) -> + Result<(), Error>; fn apply(&mut self, tx: &mut Tx, step: &FoldStep) -> + Result<(), Error>; fn on_tombstone(&mut self, tx: &mut Tx, target: &Cid) + -> Result<(), Error>; fn reset(&mut self, tx: &mut Tx) -> Result<(), + Error>; }`. `NAME` matches `^[a-z][a-z0-9-]*$` and names the storage + file. `FoldStep { ordinal: u64, entry_hash: EntryHash, entry: Entry, + fact: FactView, cursor: Cursor }` where `FactView` is `Present(FactEnvelope) + | Opaque { kind: FactKind, bytes: Vec<u8> } | Erased { target: Cid } | + Missing { target: Cid }` (spec 020 `resolve_payload` mapped one to one; + `Opaque` is a fact kind the 019 registry does not know, passed through + and counted, never dropped). +- **B-2 (the runner).** `Runner::new(entries: &dyn EntryReader, objects: + &dyn ObjectStore, registry: &FactRegistry, storage: &dyn + ProjectionStorage)`; `run<P: Projection>(&self, p: &mut P) -> + Result<RunReport, Error>` reads the checkpoint (B-3), iterates + `order_from(cursor)` (018) over the ledger, resolves each payload (020), + decodes it through the registry, and calls `apply` once per entry inside + one storage transaction that also writes the new checkpoint. `rebuild` + is `reset` followed by `run` from ordinal zero in one call. `RunReport { + name, from_ordinal, to_ordinal, applied, opaque, erased, missing, + reset_reason: Option<String> }`. +- **B-3 (checkpoint).** `Cursor { ordinal: u64, entry: EntryHash, + heads_hash: Hash }` where `heads_hash` is `Hash::of` of the sorted head + set at the time of the step. Stored in the projection's own storage in + table `_hq_checkpoint(name TEXT PRIMARY KEY, schema_version INTEGER, + ordinal INTEGER, entry_hash BLOB, heads_hash BLOB)`. On open, a stored + `schema_version` different from `SCHEMA_VERSION` MUST trigger `reset` + (the projection is disposable; a schema bump is the migration), recorded + in `reset_reason`. A stored `entry_hash` the ledger does not contain is + `Error::Stale` and the CLI's advice is `rebuild`; the runner never guesses + a resume point. +- **B-4 (storage).** `trait ProjectionStorage { fn open(&self, name: &str) + -> Result<Box<dyn Store>, Error>; }` and `trait Store { fn begin(&mut + self) -> Result<Tx, Error>; }` with `Tx` exposing `execute`, `query`, and + `commit`. `SqliteStorage` keeps one database per projection at + `<repo>/.hq/projections/<name>.db` (rusqlite, bundled SQLite, WAL mode, + `synchronous=FULL`). `MemoryStorage` (in-memory SQLite) backs tests. The + apply for one step and the checkpoint update commit in the same + transaction, so a crash leaves either both or neither: replay after a + crash starts from the last committed cursor, and every `apply` MUST be + idempotent for the same ordinal (writes keyed by fact-derived ids with + upsert semantics), which the framework checks in tests by applying a + step twice. +- **B-5 (never authority).** Every query type this crate and its + instances expose returns `AsOf<T> { value: T, cursor: Cursor }`, and + every CLI rendering prints `projected as of <entry-hash-short> (ordinal + N)`. The crate holds no `Signer`, appends nothing to any ledger, and + exposes no write to an object store; a projection that needs to record + something records nothing (constitution XIII). +- **B-6 (erasure).** A `FactView::Erased` step reaches `apply` so the + projection can record that something existed and is gone; the runner + additionally calls `on_tombstone(target)` when the step's fact is a + `ledger.tombstone` (020), and every instance MUST remove any previously + projected content derived from that `Cid` and render it as erased + thereafter. Content is never copied into a projection when a `Cid` + suffices (constitution X): views store the `Cid` and an `erased` flag. +- **B-7 (registry).** `ProjectionRegistry` holds `Box<dyn + ProjectionFactory>` entries keyed by `NAME`; `register`, `names()` + sorted, `build(name) -> Result<Box<dyn DynProjection>, Error::NotFound>`. + `DynProjection` is the object-safe form of B-1 the CLI and the server + drive. Specs 081 to 085 register their instances in `register_all`. +- **B-8 (the verb).** `hq projection status [--json]` lists every + registered projection with `name`, `schema_version`, `ordinal`, + `entry`, and `behind` (the ledger's ordinal count minus the cursor). + `hq projection run <name> | --all` catches up. `hq projection rebuild + <name> | --all` resets and runs from zero. Exit codes through + `Error::exit_code` (010 B-9); `--json` output is sorted-key canonical + JSON (032). +- **B-9 (no ambient input).** Projections read no clock; every timestamp + a view stores is an `Hlc` from an entry, encoded as the fixed-width + sortable text `<wall_ms:020><logical:010><node-hex>` so SQL `ORDER BY` + reproduces `Hlc` order. `BTreeMap` is the only map type in the crate. + +## 4. Functional requirements + +- **FR-001.** The runner is a pure function of `(entry reader, object + store, fact registry, storage, projection)`; `EntryReader` is the read + seam of 021's `EntryStore` and tests supply an in-memory one. +- **FR-002.** Tests cover: rebuild-from-zero equals incremental (the two + SQLite dumps are byte-identical after the checkpoint row is excluded); + a storage that fails after `apply` and before `commit` leaves the + cursor unchanged and the replay applies the same ordinal once more with + no duplicate rows; a schema bump resets and reports the reason; an + erased fact reaches `apply` and `on_tombstone` removes prior content; an + unknown fact kind is passed as `Opaque` and counted; `Error::Stale` on a + foreign cursor. +- **FR-003.** A fixture projection (`tests/fixtures/counter.rs`) counting + facts per kind is the reference instance the framework tests run. +- **FR-004.** CLI tests with `assert_cmd`: `status` on a fresh repo lists + the registered names at ordinal zero; `rebuild --all` then `status` + shows `behind 0`; an unknown name exits 1. +- **FR-005.** The crate depends on `hqgit-types`, `hqgit-object`, + `hqgit-ledger`, and `hqgit-domain` only within the workspace. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-projection --locked` passes. +- **AC-2.** Against a fixture repo with fifty facts, `hq projection + rebuild counter` twice produces identical dumps, and `hq projection + status --json` reports `behind: 0` with the cursor naming the last + entry. +- **AC-3.** `spec-spine index` discovers `hqgit-projection` bound to this + spec and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The concrete views (081), full-text search (082), the code graph (083), +the ecosystem graph (084), feeds (085), serving projections over the API +(093), and replicating projections between nodes (091 replicates the +ledger; a node rebuilds its own projections). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-projection --locked +cargo test -p hqgit-cli --locked projection +``` diff --git a/specs/081-change-and-review-views/spec.md b/specs/081-change-and-review-views/spec.md new file mode 100644 index 0000000..2903abd --- /dev/null +++ b/specs/081-change-and-review-views/spec.md @@ -0,0 +1,182 @@ +--- +id: "081-change-and-review-views" +title: "Change and review views: the SQL read models for changes, threads, attestations, stacks" +status: approved +kind: "feature" +domain: "l5-projection" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 6 +depends_on: + - "080-projection-framework" + - "026-review-threads" + - "027-attestation-primitive" + - "050-stacked-changes" +establishes: + - "crates/hqgit-projection/src/views/mod.rs" + - "crates/hqgit-projection/src/views/changes.rs" + - "crates/hqgit-projection/src/views/threads.rs" + - "crates/hqgit-projection/src/views/attestations.rs" + - "crates/hqgit-projection/src/views/stacks.rs" + - "crates/hqgit-projection/tests/views.rs" + - "crates/hqgit-projection/testdata/views/" +extends: + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/lib.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/registry.rs", nature: additive } +summary: > + The four read models the API (093) and the review UI (095) query: changes + with their revisions and stack position, threads with their per-revision + anchor resolution (exact, moved, text, lost), attestations by subject and + predicate with a verification column a composing binary fills, and + stacks. Each is a Projection instance (080) with its own schema version, + every write is an upsert keyed by a fact-derived id so replay is + idempotent, comment bodies stay in the object store as a Cid plus an + erased flag, and every query answers as of a ledger entry. Nothing here + is authority: a rebuild from zero reproduces every row. +--- + +# 081: Change and review views + +## 1. Purpose + +The domain folds of 024, 026, 027, and 050 answer questions one change at +a time from the ledger. A list of open changes sorted by activity, the +threads of a change re-anchored against its latest revision, or every +attestation over a subject are set queries, and answering them by folding +the whole ledger on each request is the wrong shape. This spec materializes +those folds as SQL tables through the 080 framework, keeping the constraint +that makes them safe (constitution VI): the tables are disposable, and the +answers carry the ledger entry they were computed from. + +## 2. Territory + +The `views` module of `hqgit-projection`: `changes.rs`, `threads.rs`, +`attestations.rs`, `stacks.rs`, their `mod.rs` with the shared query +types, the `tests/views.rs` suite, and the fixture ledgers under +`testdata/views/`. Additively: the `lib.rs` re-exports and the +`register_all` entry in `registry.rs`. The view of issues is a later spec +(§6); search is 082. + +## 3. Behavior + +- **B-1 (changes).** `ChangesProjection` (`NAME = "changes"`, + `SCHEMA_VERSION = 1`) owns `changes(change_id TEXT PRIMARY KEY, + opened_by TEXT NOT NULL, title TEXT, state TEXT NOT NULL CHECK (state IN + ('open','merged','abandoned')), latest_revision INTEGER NOT NULL, + latest_revision_id TEXT, latest_tree TEXT, opened_at TEXT NOT NULL, + updated_at TEXT NOT NULL, stack_id TEXT, stack_position INTEGER)` and + `revisions(revision_id TEXT PRIMARY KEY, change_id TEXT NOT NULL, + number INTEGER NOT NULL, tree_cid TEXT NOT NULL, base_cid TEXT NOT NULL, + parent_revision_id TEXT, submitted_by TEXT NOT NULL, at TEXT NOT NULL, + git_commit TEXT, UNIQUE(change_id, number))`, folded from the 024 facts + (`change.opened`, `change.revision_submitted`, `change.abandoned`, and + the merge facts 076 appends). `title` and `state` follow the 024 + `ChangeView` LWW rules, replayed here on the fact's `Hlc`; `updated_at` + is the greatest `Hlc` of any fact about the change or its threads. All + `Hlc` columns use the 080 B-9 fixed-width text. +- **B-2 (threads).** `ThreadsProjection` (`NAME = "threads"`, + `SCHEMA_VERSION = 1`) owns `threads(thread_id TEXT PRIMARY KEY, + change_id TEXT NOT NULL, opened_at_revision TEXT, anchor_path TEXT, + anchor_json TEXT, resolved INTEGER NOT NULL DEFAULT 0, resolved_at + TEXT)`, `comments(comment_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, + author TEXT NOT NULL, body_cid TEXT NOT NULL, at TEXT NOT NULL, erased + INTEGER NOT NULL DEFAULT 0)`, and `thread_positions(thread_id TEXT, + revision_id TEXT, resolution TEXT NOT NULL CHECK (resolution IN + ('exact','moved','text','lost','unavailable')), start_line INTEGER, + end_line INTEGER, PRIMARY KEY (thread_id, revision_id))`. On every + `change.revision_submitted` the projection re-resolves each open + anchored thread of that change against the new revision's tree through + 025 `resolve`, reading the tree from the object store; a tree the store + does not hold yields `unavailable`, never a guess. Comment bodies are + never copied: `body_cid` plus `erased`, and `on_tombstone(cid)` sets + `erased = 1` for every comment whose `body_cid` matches. +- **B-3 (attestations).** `AttestationsProjection` (`NAME = + "attestations"`, `SCHEMA_VERSION = 1`) owns `attestations(attestation_id + TEXT PRIMARY KEY, subject TEXT NOT NULL, predicate TEXT NOT NULL, issuer + TEXT NOT NULL, issuer_kind TEXT NOT NULL CHECK (issuer_kind IN + ('human','agent','service','org')), issuer_key TEXT NOT NULL, claim_cid + TEXT NOT NULL, at TEXT NOT NULL, entry_hash TEXT NOT NULL, verification + TEXT NOT NULL DEFAULT 'unverified' CHECK (verification IN + ('unverified','ok','failed')), verification_reason TEXT, verified_at + TEXT)` with indexes on `(subject)` and `(predicate, subject)`, folded + from `attestation.issued` (027). The projection crate does not depend on + `hqgit-trust`; the composing binary (CLI or server) that holds a 064 + verifier calls `record_verification(tx, attestation_id, verdict, at)`, + the one write this view accepts from outside the fold, and a rebuild + resets the column to `unverified` (verification is re-derived, never + trusted from a prior table). +- **B-4 (stacks).** `StacksProjection` (`NAME = "stacks"`, + `SCHEMA_VERSION = 1`) owns `stacks(stack_id TEXT PRIMARY KEY, + root_change_id TEXT NOT NULL, depth INTEGER NOT NULL, state TEXT NOT + NULL CHECK (state IN ('valid','invalid')), reason TEXT)` and + `stack_members(stack_id TEXT, change_id TEXT, position INTEGER, + depends_on_change_id TEXT, PRIMARY KEY (stack_id, change_id))`, folded + from `change.depends_on` (050) with `stack_id = Hash::of` of the sorted + member set's root; a dependency cycle 050 refuses is recorded as + `invalid` with the cycle path in `reason` rather than omitted. The + `changes` view's `stack_id` and `stack_position` are filled from this + view's rows in the same fold (both projections run in one runner pass + when registered together; each remains rebuildable alone). +- **B-5 (queries).** `mod.rs` defines `ChangesQuery { state: + Option<ChangeState>, author: Option<Principal>, cursor: Option<PageCursor>, + limit: u32 }` and the functions `list_changes(store, q) -> + AsOf<Page<ChangeRow>>`, `change_detail(store, id) -> + AsOf<Option<ChangeDetail>>` (change, revisions, stack position), + `threads_for(store, change, revision) -> AsOf<Vec<ThreadRow>>` with the + position for that revision, `attestations_for(store, subject) -> + AsOf<Vec<AttestationRow>>`, `attestations_by_predicate(store, predicate, + cursor, limit)`, and `stack_of(store, change) -> AsOf<Option<StackRow>>`. + `PageCursor` is `(updated_at, change_id)`, opaque to callers, and + ordering is `updated_at DESC, change_id ASC` so pages are stable across + refreshes. +- **B-6 (idempotent apply).** Every write is `INSERT ... ON CONFLICT DO + UPDATE` keyed by the fact-derived id (023 `ids.rs`), so re-applying an + ordinal after a crash (080 B-4) changes no row. +- **B-7 (as of).** Every function of B-5 returns `AsOf<T>` carrying the + view's cursor; the composing binary renders 080 B-5's line. + +## 4. Functional requirements + +- **FR-001.** Each view is a separate `Projection` registered by name; a + rebuild of one never touches another's database. +- **FR-002.** Tests cover, from the fixture ledgers: expected rows for a + change with three revisions and an abandonment; thread positions across + a revision that moves the anchored function and one that deletes it + (`moved`, then `lost`); a tombstoned comment body flagged `erased`; + attestation rows per subject and predicate with `record_verification` + round trip and reset on rebuild; a valid stack and an invalid (cyclic) + one; pagination stability across two pages with an insert between them; + applying one ordinal twice yields the same dump. +- **FR-003.** Rebuild equivalence: for every fixture, the incremental dump + equals the rebuild-from-zero dump (080 FR-002's check, run per view). +- **FR-004.** No comment body text is stored in any table (a test greps + the dump for a fixture body string and asserts absence). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-projection --locked views` passes. +- **AC-2.** On the offline-review fixture of 033, `hq projection rebuild + --all` followed by `list_changes` through the crate returns the change + with `latest_revision` equal to the revision count and every thread + positioned for the latest revision. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Issue read models (a later feature spec adds `views/issues.rs`), full-text +search (082), feeds (085), serving these views over the API (093), and the +review UI (095). Verification itself is 064; this spec only records its +verdict. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-projection --locked views +``` diff --git a/specs/082-search-index/spec.md b/specs/082-search-index/spec.md new file mode 100644 index 0000000..c989b2c --- /dev/null +++ b/specs/082-search-index/spec.md @@ -0,0 +1,151 @@ +--- +id: "082-search-index" +title: "Search index: a tantivy projection over changes, comments, issues, and attestations" +status: approved +kind: "feature" +domain: "l5-projection" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 6 +depends_on: + - "080-projection-framework" +establishes: + - "crates/hqgit-projection/src/search.rs" + - "crates/hqgit-projection/tests/search.rs" +extends: + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/lib.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/registry.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Full-text search as one more disposable projection: a tantivy index over + change titles, comment bodies resolved from the object store, issue + fields, and attestation predicates, keyed by stable document ids so + replay is idempotent, with erased bodies never indexed and removed on + tombstone, quarantine documents excluded by default, and a query API + whose pages carry the ledger cursor they were computed from. Because + tantivy is not SQLite, this spec fixes the commit ordering that keeps the + index crash-consistent with the 080 checkpoint. Rebuild wipes the + directory; nothing here is authority. +--- + +# 082: Search index + +## 1. Purpose + +Thesis §4.7 names search as a projection; constitution VI says it must +therefore be rebuildable from zero and never read as authority. Search is +also where the erasure rule (constitution X) is easiest to break by +accident: a copied comment body in an inverted index outlives the blob it +came from. This spec builds the index inside the 080 framework, resolves +every body from the object store at apply time, and makes the tombstone +hook delete by content id, so the search surface forgets exactly when the +ledger does. + +## 2. Territory + +`search.rs` in `hqgit-projection` (the `SearchProjection`, its schema, the +commit discipline, and the query API) and `tests/search.rs`. Additively: +the `lib.rs` re-export, the `register_all` entry, the crate manifest +(tantivy), and the workspace dependency table. A CLI verb is not in this +spec (§6). + +## 3. Behavior + +- **B-1 (the projection).** `SearchProjection` (`NAME = "search"`, + `SCHEMA_VERSION = 1`) keeps a tantivy index directory at + `<repo>/.hq/projections/search/index/` and its 080 checkpoint in the + ordinary `search.db` SQLite file. Because the index is not inside the + SQLite transaction, `apply` MUST order its effects: tantivy writer + commit first, then the checkpoint commit. A crash between the two + replays the ordinal; every document is keyed by a stable `doc_id` + (B-2) and re-indexing is delete-by-term then add, so the replay is + idempotent. +- **B-2 (schema).** Fields: `doc_id` (STRING, stored, indexed as a single + term: `<kind>:<id>`), `kind` (STRING, one of `change`, `comment`, + `issue`, `attestation`), `subject_id` (STRING: the change, issue, or + attestation subject), `change_id` (STRING, optional), `namespace` + (STRING), `title` (TEXT, default tokenizer), `body` (TEXT), `predicate` + (STRING), `author` (STRING), `at` (STRING fast field in the 080 B-9 + fixed-width form), `ordinal` (U64 fast field). Stored fields are `doc_id`, + `kind`, `subject_id`, `change_id`, `namespace`, `title`, `predicate`, + `author`, `at`, `ordinal`; `body` is indexed but not stored (the object + store is the source of the text). +- **B-3 (content resolution).** For `review.comment_posted` the body is + read from the object store by `Cid` at apply time; a `FactView::Erased` + or `Missing` body is never indexed and the document is written with an + empty `body`. `on_tombstone(cid)` deletes every document whose indexed + body came from that `Cid` (the projection keeps a `body_cids(doc_id + TEXT PRIMARY KEY, body_cid TEXT)` table in `search.db` for that + reverse lookup) and re-adds the document without a body. +- **B-4 (what is indexed).** `change.opened` and title changes index the + change (`title`); `review.comment_posted` indexes the comment (`body`, + `change_id`); `issue.opened` and `issue.field_set` index the issue + (`title`, `body` from the description field); `attestation.issued` + indexes the attestation (`predicate`, `subject_id`, `author`). Facts in + the quarantine namespace (021) are indexed with `namespace` set and are + excluded by default queries (B-5). +- **B-5 (query API).** `search(store, q: &str, filter: SearchFilter { + kinds: Vec<Kind>, namespaces: Vec<Namespace> (empty means main only), + since_ordinal: Option<u64> }, mode: SearchMode::{Relevance, Recent}, + cursor: Option<SearchCursor>, limit: u32) -> Result<AsOf<SearchPage>, + Error>`. `Recent` orders by `(ordinal DESC, doc_id ASC)` and its cursor + is `(ordinal, doc_id)`. `Relevance` orders by BM25 with ties broken by + `(ordinal DESC, doc_id ASC)` and its cursor is the offset within the + same query string; a cursor from a different query is + `Error::Validation`. Results expose rank position, never the float + score: floats never leave tantivy (constitution VIII applies to hashed + paths; here it is defense in depth). A query string that fails to parse + is `Error::Parse`. +- **B-6 (disposable).** `reset` deletes the index directory and the + `body_cids` table; `rebuild` re-indexes from ordinal zero and yields an + index whose `Recent` results are identical to the incremental one. +- **B-7 (bounds).** Indexed text per document is capped at 1 MiB (a + longer body is truncated and the document flagged `truncated` in a + stored boolean field); the index writer heap is a constant in the + module. + +## 4. Functional requirements + +- **FR-001.** Tests cover: index a change, a comment, an issue, and an + attestation from a fixture ledger and find each by a distinguishing + term; a tombstoned comment body is unfindable and the document still + exists without a body; a quarantine document is excluded by default and + included when its namespace is named; `Recent` pagination across two + pages is stable; `Relevance` cursor reuse across query strings is + refused; applying one ordinal twice leaves one document; rebuild equals + incremental for `Recent` results. +- **FR-002.** The crash-ordering test simulates a failure after the + tantivy commit and before the checkpoint commit and asserts the replay + leaves exactly one document per id. +- **FR-003.** No comment body text is stored in `search.db` or in a + tantivy stored field (a test reads the stored fields of every document + and asserts `body` is absent). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-projection --locked search` passes. +- **AC-2.** On the 033 offline-review fixture, a query for a word that + appears only in one comment returns that comment's `doc_id`, and after + `hq` erases that comment's body (020) the same query returns nothing. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +An `hq search` CLI verb and the API route (093 exposes the query through +the Connect surface and the CLI client), highlighting and snippets, +search over code (083), and cross-repository search (each repository's +index is local; a server that wants federated search queries each). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-projection --locked search +``` diff --git a/specs/083-code-graph/spec.md b/specs/083-code-graph/spec.md new file mode 100644 index 0000000..64a1106 --- /dev/null +++ b/specs/083-code-graph/spec.md @@ -0,0 +1,209 @@ +--- +id: "083-code-graph" +title: "Code graph: a cross-repository SCIP symbol projection keyed by tree" +status: approved +kind: "feature" +domain: "l5-projection" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 6 +depends_on: + - "080-projection-framework" + - "025-semantic-anchors" +establishes: + - "crates/hqgit-projection/src/codegraph/mod.rs" + - "crates/hqgit-projection/src/codegraph/scip.rs" + - "crates/hqgit-projection/src/codegraph/store.rs" + - "crates/hqgit-projection/tests/codegraph.rs" + - "crates/hqgit-projection/testdata/scip/" +extends: + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/lib.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/registry.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/Cargo.toml", nature: additive } + # The `scip` protobuf bindings (and prost, if 070 has not yet added it) join the table. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } + # An index is evidence about a tree: it arrives under a registered predicate. + - { spec: "027-attestation-primitive", unit: "crates/hqgit-domain/src/predicate.rs", nature: additive } +summary: > + Design §1.1 point 6: there is no ecosystem graph, and the first half of + one is a type-aware, cross-repository code index. This spec ingests SCIP + indexes, produced per revision tree by external indexers and delivered + as attestations under the predicate hqgit/code-index/v1, into a + projection keyed by (namespace, tree cid): symbols with a stable + content-derived id, definitions, references, and relationships, in one + shared SQLite file that spans repositories so a reference in one + namespace resolves to a definition in another. It is the one legitimately + centralized component (thesis §8) and therefore the most carefully + non-authoritative: nothing originates here, every answer names the tree + and ledger entry it was computed from, and any namespace's rows can be + dropped and rebuilt from its own ledger alone. +--- + +# 083: Code graph + +## 1. Purpose + +Thesis §4.7 places the SCIP-class cross-repo index in L5 and names it, +with the package graph (084), the one component that is legitimately +centralized. Thesis §8 lists it as a standing risk for exactly that +reason: a central index is where authority leaks in. This spec builds the +index inside the 080 framework so the leak is impossible by construction: +indexes enter only as evidence appended to a repository's ledger, the +store answers "as of" a tree and an entry, and a namespace's rows are a +pure function of that namespace's facts. Anchors (025) are the bridge from +a review location to the symbols under it. + +## 2. Territory + +The `codegraph` module of `hqgit-projection`: `mod.rs` (the projection, +symbol ids, query API), `scip.rs` (decoding and normalizing a SCIP +index), `store.rs` (the shared cross-namespace storage), `tests/codegraph.rs`, +and fixture indexes with expectations under `testdata/scip/`. Additively: +the `lib.rs` re-exports, the `register_all` entry, the crate manifest, the +workspace dependency table (`scip`, pinned exact, and `prost`), and the +`hqgit/code-index/v1` claim validator in 027's registry. Producing indexes +is not here (§6). + +## 3. Behavior + +- **B-1 (the predicate).** `PredicateType("hqgit/code-index/v1")` is + registered with claim schema `{ format: "scip", index: Cid (codec Raw), + tree: Cid (codec DagCbor), language: String, indexer: String, + indexer_version: String }`; the attestation's `subject` MUST equal + `tree.hash`. The issuer is whoever ran the indexer (a Service under 075 + later, a human running `hq attest` today); the projection records the + issuer and never judges it (verification is 064's, rendered by 081 B-3). +- **B-2 (ingestion).** `CodeGraphProjection { namespace: Hash }` (`NAME = + "codegraph"`, `SCHEMA_VERSION = 1`) applies `attestation.issued` facts + whose predicate is B-1: it reads the claim object and the Raw index from + the object store, decodes the index with the `scip` crate + (`scip::types::Index`), and writes B-4's rows in the same transaction as + the checkpoint. An index over 256 MiB, a decode failure, a claim whose + `subject` disagrees with `tree`, or more than 8,000,000 occurrences is + recorded with `status = 'rejected'` and a `reason`; an absent object is + `'missing'`; the fold never fails on an index. Every other fact kind is + ignored by this projection. +- **B-3 (symbol ids).** SCIP symbols are `<scheme> <manager> <package> + <version> <descriptors>` or `local <id>`. `SymbolId = Hash::of( + b"hqgit/codegraph/symbol/v1" || 0x00 || symbol_with_version_dot)` where + the version field is replaced by `.` so the same item at two versions + shares an id and the observed version is stored beside each occurrence. + A local symbol's id is `Hash::of(b"hqgit/codegraph/local/v1" || 0x00 || + namespace || tree cid hash || path || 0x00 || symbol)` and is flagged + `local = 1`. `scip.rs` exposes `parse_symbol(&str) -> Result<ScipSymbol, + Error>` and the role bitmask constants `DEFINITION = 0x1`, `IMPORT = + 0x2`, `WRITE = 0x4`, `READ = 0x8`, `GENERATED = 0x10`, `TEST = 0x20`, + `FORWARD_DEFINITION = 0x40`. A three-element SCIP range expands to + `(line, start, line, end)`; all positions are 0-based as SCIP records. +- **B-4 (tables).** `code_indexes(namespace TEXT, tree_cid TEXT, + index_cid TEXT, attestation_id TEXT NOT NULL, issuer TEXT NOT NULL, + language TEXT NOT NULL, indexer TEXT NOT NULL, indexer_version TEXT NOT + NULL, ordinal INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN + ('ingested','missing','rejected','erased')), reason TEXT, documents + INTEGER NOT NULL DEFAULT 0, symbols INTEGER NOT NULL DEFAULT 0, + occurrences INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (namespace, + tree_cid, index_cid))`; `code_symbols(symbol_id TEXT PRIMARY KEY, + scheme TEXT NOT NULL, manager TEXT NOT NULL, package TEXT NOT NULL, + descriptors TEXT NOT NULL, display_name TEXT, kind INTEGER NOT NULL + DEFAULT 0, local INTEGER NOT NULL DEFAULT 0)`; + `code_definitions(namespace, tree_cid, index_cid, symbol_id, path TEXT, + start_line INTEGER, start_col INTEGER, end_line INTEGER, end_col + INTEGER, version TEXT, roles INTEGER NOT NULL, PRIMARY KEY (namespace, + tree_cid, index_cid, symbol_id, path, start_line, start_col))`; + `code_references` with the same columns and `PRIMARY KEY (namespace, + tree_cid, index_cid, path, start_line, start_col, symbol_id)`; + `code_relationships(namespace, tree_cid, index_cid, symbol_id, + related_symbol_id, relation TEXT NOT NULL CHECK (relation IN + ('reference','implementation','type-definition','definition')), PRIMARY + KEY (namespace, tree_cid, index_cid, symbol_id, related_symbol_id, + relation))`. Indexes on `code_definitions(symbol_id)`, + `code_references(symbol_id)`, and `(namespace, tree_cid, path)` on both. + Every write is an upsert keyed by the primary key (080 B-4). +- **B-5 (shared store).** `store.rs` provides `SharedStorage::open(path) + -> SharedStorage`, an implementation of 080's `ProjectionStorage` over + one SQLite file that many namespaces share. Its checkpoint row name is + `<NAME>@<namespace-hex>` so each repository resumes independently, and + `reset` for one namespace deletes only rows `WHERE namespace = ?` plus + `code_symbols` rows no definition or reference still names. The CLI + passes `<repo>/.hq/projections/codegraph.db` (a one-namespace file); + the server passes `<data>/projections/codegraph.db`. The file is never + attached to another projection's database and no projection other than + 084 reads it; the operator may delete it and rebuild every namespace. +- **B-6 (queries).** `Scope::{Tree { namespace, tree_cid }, Namespace(Hash), + All}`; for `Namespace` and `All` the answer comes from each namespace's + latest ingested index (greatest `ordinal`), and every returned + `Location { namespace, tree_cid, path, range: Range { start_line, + start_col, end_line, end_col }, roles: u32, version: Option<String> }` + names its tree. `definitions(store, symbol, scope) -> AsOf<Vec<Location>>`; + `references(store, symbol, scope, roles: Option<u32>, cursor, limit) -> + AsOf<Page<Location>>` with cursor `(namespace, tree_cid, path, + start_line, start_col)` and that ordering; `relationships(store, symbol, + relation, scope)`; `symbol_by_scip(store, &str) -> AsOf<Option<SymbolRow>>`; + `latest_indexed_tree(store, namespace) -> AsOf<Option<Cid>>`. Local + symbols are answered only under `Scope::Tree` and never cross a + namespace. `symbols_in(store, objects, namespace, tree_cid, anchor: + &Anchor) -> AsOf<SymbolsAt>` resolves the anchor against the tree's file + through 025 `resolve`, converts the byte range to lines with 025 + `line_range`, and returns `SymbolsAt::Found { resolution, occurrences }` + for occurrences intersecting the range, `Unindexed` when the tree has no + ingested index, `Unavailable` when the object store lacks the file, or + `Lost` when the anchor does not resolve. +- **B-7 (erasure).** `on_tombstone(cid)` deletes every definition, + reference, and relationship row whose `index_cid` is that cid and sets + the `code_indexes` row to `'erased'` (constitution X): the store never + keeps content an erased index carried, and never copies source text. +- **B-8 (no ambient input).** No clock, no `HashMap`, no floats; ranges + and counts are integers. Rows are a pure function of the namespace's + facts and the object store, so a rebuild is byte-identical (080 FR-002). + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/scip/`: `rust-two-crates/{a,b}.scip` + (crate `b` references a symbol crate `a` defines), `ts-single/index.scip`, + `local-only.scip`, `malformed.scip`, `symbol-ids.json` (frozen SCIP + symbol to `SymbolId` vectors, including a local one), and `expected/` + JSON per query. +- **FR-002.** Tests cover: ingest `a.scip` and query its definitions; + `b.scip` under a second namespace resolves references to `a`'s symbol + under `Scope::All`; a local symbol is invisible outside its tree; a + malformed and an oversized index are `'rejected'` with a reason and the + next ordinal still applies; a missing object is `'missing'`; a + tombstone on the index cid erases its rows; `Namespace` scope answers + from the newest index after two are ingested; `symbols_in` over a 025 + fixture anchor returns the occurrence at the anchored node and + `Unindexed` before ingestion; two namespaces in one file checkpoint + independently and a reset of one leaves the other's rows; rebuild equals + incremental for both. +- **FR-003.** The symbol id vectors are frozen (constitution VIII): a + change to `symbol-ids.json` is an amendment to this spec. +- **FR-004.** The `scip` crate is pinned exact in `[workspace.dependencies]` + and a test asserts `Cargo.lock` agrees. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-projection --locked codegraph` passes. +- **AC-2.** With `a.scip` attested into one repository and `b.scip` into + another, both projected into one shared file, `references` for the + shared symbol under `Scope::All` returns `b`'s call site naming `b`'s + namespace and tree, and `definitions` returns `a`'s. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Running indexers (a 075 build target that emits the B-1 attestation is a +later feature spec); the package dependency graph and impact analysis +(084); serving queries over the API (093); code search (082 indexes prose, +not symbols); languages beyond what an external SCIP indexer supports. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-projection --locked codegraph +``` diff --git a/specs/084-ecosystem-graph/spec.md b/specs/084-ecosystem-graph/spec.md new file mode 100644 index 0000000..a15186f --- /dev/null +++ b/specs/084-ecosystem-graph/spec.md @@ -0,0 +1,205 @@ +--- +id: "084-ecosystem-graph" +title: "Ecosystem graph: package dependencies joined to the code graph for downstream impact" +status: approved +kind: "feature" +domain: "l5-projection" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 6 +depends_on: + - "083-code-graph" + - "075-build-graph" + - "051-semantic-deltas" +establishes: + - "crates/hqgit-projection/src/ecosystem/mod.rs" + - "crates/hqgit-projection/src/ecosystem/deps.rs" + - "crates/hqgit-projection/src/ecosystem/impact.rs" + - "crates/hqgit-projection/tests/ecosystem.rs" + - "crates/hqgit-projection/testdata/ecosystem/" +extends: + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/lib.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/registry.rs", nature: additive } + # toml and serde_json (already in the workspace table) join this crate's manifest. + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/Cargo.toml", nature: additive } +summary: > + The second half of design §1.1 point 6: the package dependency graph, + read from manifests and lockfiles in each revision tree for Cargo, npm, + Go, and Python, joined to the code graph (083) through the SCIP package + coordinates so that "who depends on this package" becomes "which call + sites in which repositories use the symbols this change removed or + re-signed". It answers downstream impact for an API-surface delta (051) + before the API breaks, plans crater-style downstream test runs as data + the evaluation plane materializes through the build manifest (075), and + reports usage with honest denominators: how many dependents are known, + how many are indexed, how many are hit. It shares the 083 file and its + isolation: non-authoritative, per-namespace rebuildable, integer-only. +--- + +# 084: Ecosystem graph + +## 1. Purpose + +Thesis §4.7: the cross-repo code index joined to the package dependency +graph is the one legitimately centralized component, isolated and +non-authoritative. The code graph alone answers "where is this symbol +used" inside repositories that happen to be indexed; the package graph +says which repositories are downstream at all, so a usage count can carry +its denominator instead of implying one. This spec builds the package +side, the join, the impact query, and the run planner, and it keeps the +crate topology intact: `hqgit-projection` does not depend on `hqgit-eval` +(080 FR-005), so runs are planned as data and executed elsewhere. + +## 2. Territory + +The `ecosystem` module of `hqgit-projection`: `mod.rs` (the projection, +tables, dependents query), `deps.rs` (manifest and lockfile readers per +ecosystem), `impact.rs` (delta to symbols to call sites, usage counts, run +planning), `tests/ecosystem.rs`, and fixtures under `testdata/ecosystem/`. +Additively: the `lib.rs` re-exports, the `register_all` entry, and the +crate manifest (`toml`, `serde_json`). The build manifest format is 075's; +the delta types are 051's; both are read, never redefined. + +## 3. Behavior + +- **B-1 (ecosystems).** `Ecosystem::{Cargo, Npm, Go, Python}` with the + SCIP manager tokens it joins on: `cargo`, `npm`, `gomod`, and `pip` + (aliases `pypi`, `python`). Sources per tree, found by file name at any + depth except under `node_modules/`, `target/`, `vendor/`, `.git/`, and + `.hq/`, each file at most 4 MiB: + + | ecosystem | manifests | lockfiles | recorded as `unsupported` | + |---|---|---|---| + | Cargo | `Cargo.toml` (workspace members expanded) | `Cargo.lock` | | + | Npm | `package.json` (workspaces expanded) | `package-lock.json` v2, v3 | `pnpm-lock.yaml`, `yarn.lock` | + | Go | `go.mod` | `go.sum` | | + | Python | `pyproject.toml` (`project.dependencies`, `tool.poetry.dependencies`), `requirements*.txt` | `poetry.lock`, `uv.lock` | `Pipfile.lock` | + + An unsupported lockfile is a row, not silence, so a missing resolution + is visible. +- **B-2 (the projection).** `EcosystemProjection { namespace: Hash }` + (`NAME = "ecosystem"`, `SCHEMA_VERSION = 1`) stores in the 083 + `SharedStorage` file with checkpoint row `ecosystem@<namespace-hex>`. + On `change.revision_submitted` it walks the revision's tree from the + object store and applies B-3 for that tree; on `change.merged` it marks + the merged revision's tree `head = 1` (the newest such tree by ordinal + is the namespace's head; before any merge the newest parsed tree is). + A tree the store does not hold yields `eco_trees.status = 'unavailable'`. +- **B-3 (tables).** `eco_trees(namespace, tree_cid, ordinal INTEGER NOT + NULL, head INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL CHECK + (status IN ('parsed','partial','unavailable')), PRIMARY KEY (namespace, + tree_cid))`; `eco_manifests(namespace, tree_cid, path TEXT, blob_cid + TEXT NOT NULL, ecosystem TEXT NOT NULL, kind TEXT NOT NULL CHECK (kind + IN ('manifest','lockfile','unsupported')), status TEXT NOT NULL CHECK + (status IN ('parsed','malformed','unsupported','erased')), reason TEXT, + PRIMARY KEY (namespace, tree_cid, path))`; `eco_packages(namespace, + tree_cid, ecosystem, name TEXT, version TEXT, manifest_path TEXT NOT + NULL, PRIMARY KEY (namespace, tree_cid, ecosystem, name))`; + `eco_dependencies(namespace, tree_cid, ecosystem, package TEXT, + dep_name TEXT, dep_kind TEXT NOT NULL CHECK (dep_kind IN + ('normal','dev','build','optional')), requirement TEXT NOT NULL, + resolved TEXT, source TEXT NOT NULL CHECK (source IN + ('registry','git','path','workspace','unknown')), locked INTEGER NOT + NULL DEFAULT 0, PRIMARY KEY (namespace, tree_cid, ecosystem, package, + dep_name, dep_kind))` with an index on `(ecosystem, dep_name)`. + `deps.rs` exposes one `read_<ecosystem>(files: &[(path, bytes)]) -> + Result<Parsed { packages, dependencies, manifests }, Error>` per + ecosystem; a malformed file marks its row `'malformed'` with the parser + message, the tree `'partial'`, and the fold continues. +- **B-4 (dependents).** `PackageRef { ecosystem, name }`. + `dependents(store, pkg) -> AsOf<Vec<Dependent { namespace, tree_cid, + package, dep_kind, requirement, resolved, source }>>` answers over each + namespace's head tree only, sorted by `(namespace, package, dep_kind)`. + `packages_of(store, namespace) -> AsOf<Vec<PackageRow>>` lists a head + tree's packages. +- **B-5 (impact).** `downstream_impact(store, origin: PackageRef, delta: + &ApiSurfaceDelta) -> AsOf<ImpactReport>`. Each delta item 051 marks + removed or signature-changed carries a path and an item name; it maps + to the `code_definitions` rows (083) of the origin namespace's latest + indexed tree at that path whose `code_symbols.display_name` equals the + name or whose `descriptors` end in the name followed by `#`, `().`, or + `.`. Zero matches list the item under `unmapped`; several include all. + For every dependent of B-4 whose head tree has an ingested 083 index, + the mapped symbols' `code_references` rows in that namespace and tree + are its call sites. `ImpactReport { origin, delta_items: u32, + mapped: Vec<SymbolId>, unmapped: Vec<UnmappedItem { path, name }>, + dependents: Vec<DependentImpact { namespace, package, tree_cid, + symbols_used: Vec<SymbolId>, call_sites: Vec<Location> }>, + denominators: Denominators { known: u32, indexed: u32, impacted: u32 } + }`, every list sorted, dependents without call sites omitted from + `dependents` but counted in `known` and `indexed`. +- **B-6 (honest counts).** `usage(store, symbol: &SymbolId) -> + AsOf<Usage { users: u32, call_sites: u32, of_indexed_dependents: u32, + of_known_dependents: u32 }>` where `users` counts dependents with at + least one reference. `Usage::percent_of_indexed(&self) -> Option<u32>` + is `users * 100 / of_indexed_dependents`, `None` when the denominator is + zero; there is no float anywhere and no rendering prints a percentage + without both numbers beside it. +- **B-7 (planning runs).** `plan_downstream_runs(store, objects, report: + &ImpactReport) -> AsOf<Vec<DownstreamRun { namespace, package, + tree_cid, targets: Vec<String>, status: Planned | NoManifest | + NoTestTargets, because: Vec<SymbolId> }>>` reads `hq-build.toml` at the + dependent's tree root and selects the targets whose `kind = "test"` and + whose `inputs` globs match at least one call-site path. The reader + extracts only target names, kinds, and input globs; the manifest's full + semantics (toolchains, escape hatches, affected selection) stay with + 075, and the consumer (076 or an operator) materializes each run as a + 070 action through 075's manifest module. Sorted by `(namespace, + package, target)`. +- **B-8 (erasure and idempotence).** `on_tombstone(cid)` deletes the + packages and dependencies derived from any manifest whose `blob_cid` + is that cid and marks the row `'erased'`. Every write is an upsert on + its primary key; a rebuild is byte-identical to the incremental fold. + No clock, no `HashMap`. + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/ecosystem/`: `cargo-workspace/`, + `npm-lock-v3/`, `go-mod/`, `python-pyproject/`, `python-requirements/`, + `unsupported-pnpm/`, `malformed-cargo/`, each with an `expected.json` + of rows; and `impact/` pairing 083's `rust-two-crates` indexes with the + two crates' manifests, a 051 delta fixture removing one function, and + `expected-impact.json`, `expected-runs.json`, and an `hq-build.toml` + for the dependent. +- **FR-002.** Tests cover: every parser fixture yields its expected rows; + a malformed manifest marks `'malformed'` and `'partial'` and the fold + continues; an unsupported lockfile is a visible row; `dependents` + across two namespaces uses head trees only and follows a merge; + `downstream_impact` maps the removed function to the dependent's call + sites and lists an unmapped item; denominators count a dependent that + lacks an index as known but not indexed; `percent_of_indexed` on zero + is `None`; `plan_downstream_runs` selects the test target whose inputs + match and reports `NoManifest` for a dependent without `hq-build.toml`; + a tombstoned manifest erases its rows; rebuild equals incremental. +- **FR-003.** The crate manifest gains no dependency outside the + workspace table, and `hqgit-eval` is not among its dependencies (a test + reads `Cargo.toml` and asserts it). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-projection --locked ecosystem` passes. +- **AC-2.** On the `impact/` fixture, the report names crate `b` with one + call site, `denominators = { known: 1, indexed: 1, impacted: 1 }`, and + the plan names `b`'s test target. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Executing planned runs (076 consumes the plan); proposing codemods to +dependents as changes (a later feature spec over 093); lockfile formats +listed as unsupported in B-1 (each a later additive extension of +`deps.rs`); vulnerability or license data joins (a later spec over the +license and static-finding predicates); serving over the API (093). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-projection --locked ecosystem +``` diff --git a/specs/085-attention-feeds/spec.md b/specs/085-attention-feeds/spec.md new file mode 100644 index 0000000..55e0171 --- /dev/null +++ b/specs/085-attention-feeds/spec.md @@ -0,0 +1,185 @@ +--- +id: "085-attention-feeds" +title: "Attention feeds: a per-principal ranked feed where every item carries its reason" +status: approved +kind: "feature" +domain: "l5-projection" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: low +wave: 6 +depends_on: + - "081-change-and-review-views" +establishes: + - "crates/hqgit-projection/src/feeds.rs" + - "crates/hqgit-projection/tests/feeds.rs" + - "crates/hqgit-projection/testdata/feeds/" +extends: + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/lib.rs", nature: additive } + - { spec: "080-projection-framework", unit: "crates/hqgit-projection/src/registry.rs", nature: additive } + # Requests and mutes are facts: three `attention.*` kinds join the vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + Design §1.1 point 8 names attention management as an email firehose + with no prioritization. This spec replaces the firehose with a + projection: for each principal, a feed of items derived from facts, + ranked by integer weights for blocking someone's merge, an ownership + match with its SLA when spec 104 supplies one, staleness measured in + ledger time rather than wall-clock time, stack depth, and explicit + requests, with the reasons listed on every item so there is never a + notification without a reason. Requesting attention and muting are + facts, so a rebuild reproduces the feed and a mute survives every + replica. The feed is disposable, as-of a ledger entry, and never a + source of anything. +--- + +# 085: Attention feeds + +## 1. Purpose + +Thesis §4.7 lists feeds among the projections; constitution VI makes them +rebuildable and non-authoritative. A feed is also where a product grows +hidden state fastest (read markers, snoozes, per-user rules in a settings +table), which constitution XI forbids. This spec keeps every input a fact +and every output a pure fold: the rank is arithmetic over integers the +ledger already holds, the reasons are the terms of that arithmetic, and +"now" is the newest entry the fold has seen, not a clock. + +## 2. Territory + +`feeds.rs` in `hqgit-projection` (the `FeedsProjection`, the rank +function, the query API) and `tests/feeds.rs`, with ranking vectors under +`testdata/feeds/`. Additively: the `lib.rs` re-export, the `register_all` +entry, and three fact kinds in 023's vocabulary. Ownership facts are +consumed through a seam (B-5) because their body is fixed by the +higher-numbered spec 104. + +## 3. Behavior + +- **B-1 (facts).** Three kinds join `DomainFact` with frozen strings: + `attention.requested { change: ChangeId, from: Principal, to: Principal, + note: Option<String>, extra }`; `attention.muted { principal: + Principal, target: MuteTarget, until: Option<Hlc>, extra }`; + `attention.unmuted { principal: Principal, target: MuteTarget, extra }`. + `MuteTarget` is a closed enum `Change(ChangeId) | Stack(Hash) | + Author(Principal) | Path(String)` (a path pattern as 104 will spell + it). A mute or unmute MUST be issued by `principal` (023 B-6: the trust + plane verifies issuer against principal; the fold records a mismatch as + a defect and applies nothing). Concurrent mute and unmute converge by + the greater `Hlc` (constitution VII, an LWW register per target). +- **B-2 (the projection).** `FeedsProjection` (`NAME = "feeds"`, + `SCHEMA_VERSION = 1`) folds `change.opened`, `change.revision_submitted`, + `change.merged`, `change.abandoned`, `change.depends_on`, + `review.thread_opened`, `review.comment_posted`, + `review.thread_resolved`, `review.approval_issued`, the three B-1 + kinds, and `ownership.declared` / `ownership.revoked` through the B-5 + seam. It keeps its own small tables so it is rebuildable alone and + reads no other projection's file; the 081 dependency supplies the + shared `AsOf`, `Page`, and `PageCursor` types and the change rows the + composing binary joins for display. Tables: `feed_items(principal TEXT, + item_id TEXT, kind TEXT NOT NULL CHECK (kind IN + ('requested','reply','comment-on-change','owned-path','blocking')), + change_id TEXT NOT NULL, thread_id TEXT, static_score INTEGER NOT NULL, + reasons TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT + NULL, resolved INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (principal, + item_id))`, `feed_mutes(principal TEXT, target TEXT, until TEXT, + PRIMARY KEY (principal, target))`, `feed_changes(change_id TEXT + PRIMARY KEY, opened_by TEXT, state TEXT, stack_depth INTEGER NOT NULL + DEFAULT 0, open_dependents INTEGER NOT NULL DEFAULT 0, touched_paths + TEXT, updated_at TEXT)`, and `feed_meta(key TEXT PRIMARY KEY, value + TEXT)` holding `latest_hlc`, the greatest `Hlc` applied so far. + `item_id = Hash::of(kind || 0x00 || change_id || 0x00 || thread_id?)`. +- **B-3 (items and reasons).** An item exists for a principal only when + at least one reason does; `Reason` is a closed enum `Requested { by } + | ReplyToYou { thread } | CommentOnYourChange { thread } | OwnsPath { + path, sla_hours: Option<u32> } | BlocksMerge { count } | StackDepth { + depth } | Stale { hours }`, stored as sorted-key JSON in `reasons`; a + write with an empty list is refused by a `CHECK (reasons <> '[]')`. + `requested` arises from `attention.requested` naming the principal as + `to`; `reply` from a `review.comment_posted` whose `reply_to` the + principal authored or on a thread the principal opened; `comment-on-change` + from a comment on a change the principal opened; `owned-path` from a + revision whose tree touches a path the principal owns (B-5); + `blocking` from `change.depends_on` when an open change by another + principal depends on one the principal opened. An item is `resolved` + when its change merges or is abandoned, its thread resolves, or the + principal approves the revision; resolved items are excluded from the + feed and kept for rebuild equality. +- **B-4 (rank, integers only).** `static_score` is written at apply time: + `400` for `Requested`, `150` for `ReplyToYou`, `100` for + `CommentOnYourChange`, `200` for `OwnsPath`, `300 * min(count, 3)` for + `BlocksMerge`, `25 * min(depth, 4)` for `StackDepth`, summed over the + item's reasons. The time terms are computed at query time from + `feed_meta.latest_hlc` so the stored rows stay a pure function of the + facts: `age_hours = (latest.wall_ms - updated_at.wall_ms) / + 3_600_000`, `stale = min(age_hours, 168)`, and, when an `OwnsPath` + reason carries `sla_hours = H`, `urgency = min(age_hours * 200 / H, + 400)`. `score = static_score + stale + urgency`, all `u64` integer + arithmetic. Order is `score DESC, updated_at ASC, item_id ASC`. +- **B-5 (ownership seam).** `trait OwnershipSource { fn owners_of(&self, + path: &str, at: &Hlc) -> Vec<Owner { principal, sla_hours: + Option<u32> }>; }`. `FeedsProjection::new(ownership: Box<dyn + OwnershipSource>)`; this spec ships `NoOwnership` (always empty), so + `owned-path` items and `urgency` are absent until 104 registers its + `OwnershipView` as the source. The seam takes facts the projection + hands it, never a file or a clock. +- **B-6 (mutes).** A mute row suppresses items whose change, stack, + author, or touched path matches `target`, until `until` is less than + `latest_hlc`; an unmute deletes the row. Mutes never delete items, so + unmuting restores them without a rebuild. +- **B-7 (query).** `feed_for(store, principal, cursor: Option<PageCursor>, + limit: u32) -> AsOf<Page<FeedItem { item_id, kind, change_id, + thread_id, score: u64, reasons: Vec<Reason>, updated_at: Hlc }>>` + applies B-4 and B-6 in SQL and returns the reasons on every item; + `mutes_for(store, principal) -> AsOf<Vec<MuteRow>>`. +- **B-8 (discipline).** No clock, no `HashMap`, no float; every write is + an upsert on `(principal, item_id)` (080 B-4); no comment body text is + stored (constitution X); a rebuild equals the incremental fold. + +## 4. Functional requirements + +- **FR-001.** `testdata/feeds/ranking.json` holds vectors: a list of + items with reasons, `updated_at`, and `latest_hlc`, and the expected + `score` and order. A test evaluates the rank function against each. +- **FR-002.** Tests cover, from fixture ledgers: an explicit request + creates a `requested` item with `Requested` in its reasons; a reply to + the principal's comment creates `reply`; a dependent change raises + `BlocksMerge` with the right count and the score changes when a second + dependent opens; stack depth capped at 4; staleness capped at 168 and + computed from `latest_hlc`, so appending an unrelated fact later raises + the score without touching the row; a merge resolves the item; a mute + hides and an unmute restores; a mute with `until` in the past does not + hide; a mute issued for another principal is a defect and ignored; with + a fixture `OwnershipSource` an `owned-path` item appears with + `sla_hours` and `urgency` follows B-4; rebuild equals incremental. +- **FR-003.** A test asserts no item row exists whose `reasons` is empty + after folding every fixture (the CHECK is exercised, not assumed). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-projection --locked feeds` passes with + every vector in FR-001. +- **AC-2.** On the 033 offline-review fixture with one added + `attention.requested`, `feed_for` returns that change first with + `Requested` as a reason and `projected as of` the last entry. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The ownership fold and its SLA field (104 supplies the `OwnershipSource`); +delivery (email, push, and a `hq feed` verb belong to 093 and 095 as +clients of this query); read markers (an item resolves by facts, not by +being seen); cross-repository feeds (each repository's feed is local; a +server concatenates). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-projection --locked feeds +``` diff --git a/specs/090-server-skeleton/spec.md b/specs/090-server-skeleton/spec.md new file mode 100644 index 0000000..86cccbf --- /dev/null +++ b/specs/090-server-skeleton/spec.md @@ -0,0 +1,197 @@ +--- +id: "090-server-skeleton" +title: "The hqgit-server binary: one listener for HTTP and gRPC, config, the repo registry, health, shutdown" +status: approved +kind: "kernel" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 6 +depends_on: + - "021-local-repository" +establishes: + - "crates/hqgit-server/Cargo.toml" + - "crates/hqgit-server/src/main.rs" + - "crates/hqgit-server/src/lib.rs" + - "crates/hqgit-server/src/config.rs" + - "crates/hqgit-server/src/app.rs" + - "crates/hqgit-server/src/health.rs" + - "crates/hqgit-server/src/repos.rs" + - "crates/hqgit-server/src/auth.rs" + - "crates/hqgit-server/src/telemetry.rs" + - "crates/hqgit-server/tests/" +extends: + # axum, tonic, tonic-health, tokio, tower, tower-http, tracing, and + # tracing-subscriber join the shared dependency table, pinned. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The hosted edge begins here. This spec founds hqgit-server, the binary + that serves every later L7 surface (the git endpoint 092, the Connect API + 093, the review UI 095, federation 112) from one listener, and fixes the + frame those specs hang on: TOML plus environment configuration, one axum + router that multiplexes HTTP/1.1 and gRPC on a single port, a registry + that opens spec 021 repositories lazily under a fixed data-directory + layout, a server identity of kind Service, the caller and namespace + routing seams every request passes through, health and readiness + endpoints, structured logs, graceful shutdown, and exit codes through + Error::exit_code. The server runs the same ledger implementation as the + CLI (thesis §5) and never depends on hqgit-cli (constitution XIII). +--- + +# 090: The hqgit-server binary + +## 1. Purpose + +Thesis §4.7 and §5: the edge is a set of interchangeable clients over the +same signed history, and the server is one of them, embedding the very +`Repository` (021) the `hq` binary (032) writes to. Nothing in this spec +interprets a fact; it is the process boundary, the port, the data +directory, and the seams (who is calling, where does a write go) that the +git endpoint, the API, the quarantine, and the UI all need to agree on. +Founding those once, before any route exists, is what keeps 092 through +095 from inventing four authentication shapes. + +## 2. Territory + +`crates/hqgit-server` as founded here: the manifest (binary and library +targets, both named `hqgit-server`, workspace dependencies `hqgit-types`, +`hqgit-object`, `hqgit-ledger`, and `hqgit-domain`; never `hqgit-cli`), +`main.rs` (argument parsing, the single exit), `lib.rs`, `config.rs`, +`app.rs` (the router builder and `AppState`), `health.rs`, `repos.rs` (the +registry and the server identity), `auth.rs` (the `Caller` type and the two +seams), `telemetry.rs` (logging), and the `tests/` subtree with the +ephemeral-server harness later specs reuse. The control plane is 091, the +git protocol 092, the API 093, quarantine 094, static files 095. + +## 3. Behavior + +- **B-1 (config).** `Config { bind: SocketAddr, data_dir: PathBuf, node: + Option<NodeId>, oidc: OidcSection { issuer: Option<String>, audience: + Option<String> }, log: LogSection { format: Json | Text, level: String }, + limits: LimitsSection { max_body_bytes: u64, request_timeout_secs: u64 }, + shutdown_grace_secs: u64, anonymous_read: bool }`. Defaults: bind + `127.0.0.1:7410`, `max_body_bytes` 64 MiB, timeout 60, grace 20, + `anonymous_read = true`, log `json` at `info`. Load order: the file named + by `--config <path>` or `HQGIT_SERVER_CONFIG`, then the environment + (`HQGIT_BIND`, `HQGIT_DATA_DIR`, `HQGIT_OIDC_ISSUER`, `HQGIT_LOG_FORMAT`, + `HQGIT_LOG_LEVEL`), then defaults. An unknown key is `Error::Config` + naming it. `oidc` is a placeholder 061 reads; no secret ever lives in the + file. +- **B-2 (one listener).** `app.rs` exposes `AppState { config: Arc<Config>, + repos: Arc<RepoRegistry>, auth: Arc<dyn Authenticator>, router: Arc<dyn + NamespaceRouter>, identity: Arc<ServerIdentity> }` and `AppBuilder::new( + state).with_http(Router<AppState>).with_grpc(service).build() -> Router`. + `build` merges every HTTP router, mounts every tonic service through + `tonic::service::Routes`, and dispatches by `Content-Type` + (`application/grpc*` to tonic, everything else to axum) so HTTP/1.1, + Connect JSON (093), and HTTP/2 gRPC share `config.bind`. Layers, in + order: request id (`x-request-id`, generated when absent), tracing span, + body limit, timeout. Later specs add routes only through the builder. +- **B-3 (repo registry).** `RepoRegistry::new(data_dir, identity, + registrars: Vec<Registrar>)` where a `Registrar` is a fact-registry + install function (019 B-2: `register_domain`, later `register_trust`), + applied to every repository on open. Layout: `<data>/repos/<namespace-hex>/ + .hq` (64 lowercase hex characters, the working directory of a spec 021 + `Repository`). `open(&self, ns: &Hash) -> Result<Arc<RepoHandle>, Error>` + opens lazily, memoizes in a `Mutex<BTreeMap<Hash, Arc<RepoHandle>>>`, and + returns the same `Arc` for the same namespace; `create(&self) -> + Result<Arc<RepoHandle>, Error>` runs `Repository::init` (021 B-4) with the + server identity; `list() -> Result<Vec<Hash>, Error>` scans the directory + sorted; `close_idle(older_than)` drops handles. `RepoHandle { namespace: + Hash, repo: RwLock<Repository> }`: reads take the read lock, appends the + write lock, so the 021 append path stays the only write path. +- **B-4 (server identity).** `ServerIdentity` is a spec 021 `LocalIdentity` + stored at `<data>/identity/seed` (mode `0600`), generated on first boot, + with `Principal::Service(ServiceId)` derived as 060 will (an + `identity.created` fact appended to each repository this identity + creates). It signs every entry the server itself issues; an entry the + server appends on behalf of a caller carries `extra["on_behalf_of"] = + <principal>` and `extra["via"] = "<surface>"` (092, 093 fill the surface). +- **B-5 (caller seam).** `auth.rs`: `Caller { principal: Option<Principal>, + trust: CallerTrust }` with `CallerTrust` a closed enum `Anonymous | + Bearer { token_hash: Hash } | Verified { identity: IdentityId, key: KeyId + }`. `trait Authenticator: Send + Sync { fn authenticate(&self, headers: + &HeaderMap) -> Result<Caller, Error>; }` with two implementations here: + `AnonymousAuthenticator` and `StaticTokenAuthenticator(BTreeMap<Hash, + Principal>)` keyed by `Hash::of(token)` for tests. `Caller` is an axum + extractor and a tonic interceptor extension. 061 and 100 supply + production authenticators; nothing here validates a credential. +- **B-6 (namespace routing seam).** `trait NamespaceRouter: Send + Sync { + fn target_for(&self, caller: &Caller, repo: &RepoHandle) -> Result<Hash, + Error>; }` answers which namespace a write lands in. The default here, + `TrustRouter`, returns `main` for `CallerTrust::Verified` and the repo's + quarantine namespace (021 B-6) for everything else, so constitution XV + holds from the first write; 094 replaces it with the capability-checked + router. No surface may append without consulting the router. +- **B-7 (health).** `GET /healthz` answers `200 { "status": "ok", + "version": <crate version> }` whenever the process serves; `GET /readyz` + answers `200 { "ready": true }` when the data directory is writable and + the identity is loaded, else `503 { "ready": false, "reasons": [...] }`. + Neither opens a repository. The gRPC health service (`tonic-health`) + reports `SERVING` on the same port for `hqgit.v1`. +- **B-8 (shutdown and exit).** `SIGTERM` or `SIGINT` stops accepting, + drains in-flight requests for `shutdown_grace_secs`, drops every + `RepoHandle` (redb commits are already durable, 021 B-2), and exits `0`. + A startup failure exits through spec 010 `Error::exit_code` in the one + `std::process::exit` call in `main.rs`; `hqgit-server --check-config` + loads the config and exits without binding. +- **B-9 (logs).** `telemetry.rs` installs `tracing-subscriber` with JSON + lines (`ts`, `level`, `target`, `request_id`, `method`, `path`, `status`, + `latency_ms`, `namespace`) or text per config. No log line ever carries a + bearer token, a request body, or object content; a test greps for both. +- **B-10 (same ledger, no ambient input on hashed paths).** The crate + never spawns `git` or `hq`; it reaches the ledger only through 021 + `Repository`. The wall clock enters only through one `SystemClock` + implementing 018's `ClockSource`, constructed in `repos.rs` and handed to + each repository's `HlcGenerator`; no handler formats `SystemTime` into a + response. `BTreeMap` is the only map type. + +## 4. Functional requirements + +- **FR-001.** `tests/harness.rs` exposes `TestServer::start(config_overrides) + -> TestServer { addr, data_dir: TempDir, state }` binding port `0`, used + by every later server test; drop shuts the server down. +- **FR-002.** Tests cover: config precedence file, then environment, then + defaults; an unknown key exits `3` through the binary; `/healthz` 200 and + `/readyz` 200 on a fresh data directory, 503 after the directory is made + read-only; the gRPC health check `SERVING`; `create` then `open` returns + the same `Arc` twice and the layout of B-3 exists on disk; a repository + created by the server opens with `hq status` (032) from its directory; + `StaticTokenAuthenticator` yields `Bearer` for a known token and + `Anonymous` for none; `TrustRouter` sends an anonymous write to + quarantine; a request in flight completes during graceful shutdown; logs + contain `request_id` and never the test bearer. +- **FR-003.** `main.rs` contains exactly one `process::exit`; a contract + test drives every `Error` variant through `--check-config` fixtures. +- **FR-004.** The manifest carries `[package.metadata.spec-spine] spec = + "090-server-skeleton"` and `cargo tree -p hqgit-server` shows no + `hqgit-cli`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-server --locked` passes. +- **AC-2.** `hqgit-server --data-dir <tmp>` boots, answers `/healthz`, and + exits `0` on `SIGTERM` within the grace period. +- **AC-3.** `spec-spine index` discovers `hqgit-server` bound to this spec + and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Replicated append order and placement (091), the git smart protocol (092), +the typed API and its CLI client (093), capability checks and quarantine +limits (094), static files for the UI (095), OIDC sessions (061), Biscuit +authentication (102), and federation (112). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-server --locked +cargo run -p hqgit-server --locked -- --check-config +``` diff --git a/specs/091-per-repo-control-plane/spec.md b/specs/091-per-repo-control-plane/spec.md new file mode 100644 index 0000000..2df67e1 --- /dev/null +++ b/specs/091-per-repo-control-plane/spec.md @@ -0,0 +1,190 @@ +--- +id: "091-per-repo-control-plane" +title: "Per-repo control plane: one Raft group per repository over hiqlite, proposal, apply, placement" +status: approved +kind: "kernel" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 6 +depends_on: + - "090-server-skeleton" +establishes: + - "crates/hqgit-server/src/control/mod.rs" + - "crates/hqgit-server/src/control/raft.rs" + - "crates/hqgit-server/src/control/placement.rs" + - "crates/hqgit-server/src/control/apply.rs" + - "crates/hqgit-server/tests/control.rs" +extends: + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } + # hiqlite (embedded Raft-replicated SQLite) joins the dependency table, pinned. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + Thesis §4.2, last paragraph: repositories are independent consistency + domains, so the hosted server replicates each one in its own Raft group + rather than through a shared database tier. This spec adds the control + plane to hqgit-server: a ControlPlane seam with a hiqlite-backed + implementation and an in-process one for tests; a per-namespace group + whose replicated state is the append order of signed entries and the + head set, never payload content and never a signature of the cluster's + own; a proposal path that validates before forwarding and commits + through the leader; an applier that feeds committed entries into the + local spec 021 store in order with the payload present first; and + consistent-hash placement of groups onto nodes with rebalancing as an + operator command. A single node is a one-member group on the same path. +--- + +# 091: Per-repo control plane + +## 1. Purpose + +Two servers hosting one repository must agree on the order in which +concurrent appends arrived, or their head sets diverge and every client +sees a different "latest". The ledger itself tolerates that (017 D4: a +DAG, not a log), and federation (112) reconciles it across hosts, but +inside one hosting cluster a client expects linearizable appends: propose, +get a hash back, and every node serves it. Raft provides exactly that +ordering, and because repositories never share state the group can be as +small as one repository. Raft is not the truth. The signed DAG is; Raft is +how a cluster of hqgit-server nodes stops racing on it (constitution VI, +XIII). + +## 2. Territory + +The `control` module of `hqgit-server`: `mod.rs` (the `ControlPlane` +trait, `LocalControlPlane`, the cluster config), `raft.rs` (the hiqlite +group per namespace and the `_cluster` meta group), `placement.rs` +(the ring and the rebalance plan), `apply.rs` (the applier and the +`PayloadFetcher` seam), and `tests/control.rs`. Additively: `lib.rs` and +the crate manifest (090). The remote object backend a cluster shares is +016; reconciliation between clusters is 110 and 112. + +## 3. Behavior + +- **B-1 (groups).** A `GroupId` is a namespace `Hash` (021 B-6). Every + namespace of every hosted repository is its own Raft group; a `main` and + a `quarantine` namespace of one repository are two groups. One extra + group, `_cluster`, has every node as a member and holds the node table + and the placement version (B-6). There is no transaction across groups. +- **B-2 (replicated state).** Each group's state machine is a hiqlite + database with tables `log(seq INTEGER PRIMARY KEY, entry_hash BLOB + UNIQUE, entry_bytes BLOB NOT NULL, proposer TEXT NOT NULL)`, `heads( + entry_hash BLOB PRIMARY KEY)`, and `meta(key TEXT PRIMARY KEY, value + BLOB)` holding `namespace`, `genesis`, and `schema`. `entry_bytes` are + spec 017 canonical bytes with the issuer's signature intact; the cluster + signs nothing and payload objects are never replicated through Raft + (they travel through the object store, 013 and 016, or B-5's fetcher). + A group can be rebuilt from any member's 021 store by replaying + `iter_all` in total order (018), and a 021 store from the group log, so + neither is the sole copy. +- **B-3 (seam).** `trait ControlPlane: Send + Sync { fn propose(&self, + group: &GroupId, append: ProposedAppend) -> Result<Committed, Error>; + fn heads(&self, group: &GroupId) -> Result<(Vec<EntryHash>, u64), + Error>; fn status(&self, group: &GroupId) -> Result<GroupStatus, Error>; + fn ensure_group(&self, group: &GroupId, genesis: &Entry) -> Result<(), + Error>; }` with `ProposedAppend { entry: Entry, payload_present: bool }`, + `Committed { seq: u64, hash: EntryHash }`, and `GroupStatus { leader: + Option<NodeId>, term: u64, members: Vec<NodeId>, commit_seq: u64, + applied_seq: u64 }`. `HiqliteControlPlane` is the production + implementation; `LocalControlPlane` (in-memory, one member, same + validation and apply path) is what 092 through 094 test against. +- **B-4 (proposal).** The receiving node runs the 017 B-6 checks against + its local DAG and the signature through the repository's resolver (060 + B-7 once present, `StaticResolver` before) BEFORE forwarding; a failure is + the caller's error and never reaches the leader. The leader re-checks + against the replicated `heads` and `log` (every parent in `log`, hash not + already present) and commits. A duplicate hash is `Ok(Committed)` with + the existing `seq`. A parent the local node lacks but the log holds is + `Error::Stale` with the missing hash; the caller retries after the + applier catches up. `propose` returns only after the entry is committed + and applied locally (B-5), so a subsequent local read observes it. + Proposal timeout is `cluster.propose_timeout_ms` (default 5000). +- **B-5 (apply).** `apply.rs` consumes each group's committed sequence in + `seq` order and calls the local 021 `EntryStore::append`. Per 021 B-3 + the payload object MUST be present before the entry is appended: the + applier checks the store, else fetches through `trait PayloadFetcher { + fn fetch(&self, cid: &Cid, hint: &NodeId) -> Result<Vec<u8>, Error>; }` + (the proposer node, then any member, verified by hash on receipt, 013 + B-4), else parks the entry as `pending_payload` and retries with backoff + while later entries wait (order is never skipped). `applied_seq` is + written in the same 021 transaction as the append, so a restart resumes + at the last applied entry and a re-apply is a no-op. +- **B-6 (placement).** `Placement { version: u64, nodes: BTreeMap<NodeId, + NodeAddr>, replication_factor: u8 }` with a consistent-hash ring of 64 + virtual points per node at `Hash::of(b"hqgit/v1/ring" || node_id || + point_index)`. `members_for(&self, group: &GroupId) -> Vec<NodeId>` is + the first `replication_factor` distinct nodes clockwise from the group's + hash, deterministic for a given `Placement`. `plan(from: &Placement, to: + &Placement) -> RebalancePlan` lists per group the members to add (as + learners first) and remove; `apply_plan` promotes a learner only after + it has caught up and never removes a member while the group would drop + below quorum. Placement changes are committed to `_cluster` and only + through the operator subcommands `hqgit-server placement show | plan + --add <id>=<addr> | --remove <id> | apply <plan.json>`. +- **B-7 (config and single node).** `[cluster] node_id = <u64>, raft_addr, + api_addr, peers = [{ id, raft_addr, api_addr }], replication_factor = 3, + election_timeout_ms = 1500, heartbeat_ms = 300, propose_timeout_ms` with + the shared secret from `HQGIT_CLUSTER_SECRET` only. With no `[cluster]` + table the server is node `1`, every group has one member, and the same + `propose` and apply path runs, so single-node and clustered deployments + differ in membership only. +- **B-8 (independence).** A group's leader loss, election, or stalled + applier affects no other group; the applier is one task per group and + the hiqlite instances are one per group under `<data>/control/<group-hex>/`. +- **B-9 (no ambient input).** Raft timers are the only clock reads and + never touch an entry; every `Hlc` is the issuer's. `BTreeMap` only. + +## 4. Functional requirements + +- **FR-001.** `tests/control.rs` starts three nodes in one process on + ephemeral ports with distinct temp data directories and covers: a + proposal on a follower commits and is readable on all three; the leader + is killed and a proposal on a survivor succeeds within twice the election + timeout; twelve concurrent proposers across the three nodes on one group + yield an identical `log` on every node, every parent preceding its + child, and one 018 total order; partitioning group A's leader leaves + proposals on group B unaffected; a duplicate proposal is idempotent; an + entry with a missing parent is refused before forwarding; an entry + whose payload is absent on a follower is fetched before append, and with + the fetcher failing is parked and applied after the object arrives; a + node restarted mid-sequence resumes from `applied_seq` with no duplicate + rows; `LocalControlPlane` passes the same single-group assertions. +- **FR-002.** Placement tests: `members_for` is stable across two + constructions; adding a node to `N` moves at most `ceil(groups / (N+1))` + groups; a plan never proposes a removal that breaks quorum. +- **FR-003.** No function in `placement.rs` performs I/O; `apply.rs` and + `raft.rs` reach the ledger only through 021 `EntryStore` and + `Repository`. +- **FR-004.** `hiqlite` is pinned exact in `[workspace.dependencies]` and + the manifest gains it behind the default feature `cluster`; `cargo test + -p hqgit-server --no-default-features` still builds with + `LocalControlPlane` alone. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-server --locked control` passes. +- **AC-2.** With the three-node fixture, `hqgit-server placement show` + on each node prints the same membership for every group. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Reconciliation between clusters and with CLIs (110, 112), sharing objects +between nodes beyond the fetch seam (016 provides the shared backend), +authorization of a proposal (094), projections on each node (080 rebuilds +locally), and any multi-group transaction. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-server --locked control +cargo test -p hqgit-server --locked +``` diff --git a/specs/092-git-endpoint/spec.md b/specs/092-git-endpoint/spec.md new file mode 100644 index 0000000..f7ccdbc --- /dev/null +++ b/specs/092-git-endpoint/spec.md @@ -0,0 +1,190 @@ +--- +id: "092-git-endpoint" +title: "Git smart HTTP endpoint: refs as a projection of changes, push as revision submission, gated main" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 6 +depends_on: + - "090-server-skeleton" + - "031-git-object-bridge" + - "024-change-and-revision" + - "068-policy-in-repo" +establishes: + - "crates/hqgit-server/src/git/mod.rs" + - "crates/hqgit-server/src/git/refs.rs" + - "crates/hqgit-server/src/git/upload_pack.rs" + - "crates/hqgit-server/src/git/receive_pack.rs" + - "crates/hqgit-server/src/git/gate.rs" + - "crates/hqgit-server/tests/git_endpoint.rs" +extends: + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/app.rs", nature: additive } + # hqgit-git, hqgit-policy, and flate2 join the server's dependencies. + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } +summary: > + Git compatibility is a hard requirement (constitution XIV), so the + server speaks the git smart HTTP protocol without becoming a git host: + refs are computed from the ledger, packs are built from the object + store through the spec 031 bridge, and a push is a revision submission + rather than a pointer move. Each open change exposes refs/changes/<id>/ + <n>; main is the merged lineage rendered as deterministic commits; a + push to a change ref imports the tree and appends + change.revision_submitted; a push to main is accepted only when a + policy-eval attestation with an Allow verdict exists for that revision + under the policy pinned in the repository, and then appends + change.merged. Every fact lands where the namespace router says + (quarantine for unverified pushers), the endpoint never spawns git, + and the tests drive a real git client against the ephemeral server. +--- + +# 092: Git smart HTTP endpoint + +## 1. Purpose + +Design §1.1 point 2: the mutable branch pointer is the wrong unit of +change, but every developer's tools speak git. This spec makes the hosted +ledger reachable from `git clone` and `git push` by translating at the +boundary: what git calls a ref is a view over Changes and Revisions +(024), what git calls a push is a fact append, and what git calls a merge +is a verdict already recorded as evidence (067, 068; constitution XI). No +branch pointer is stored anywhere; the refs advertised are a projection +and can be recomputed from zero (constitution VI). + +## 2. Territory + +The `git` module of `hqgit-server`: `mod.rs` (routes, content types, +the per-repo git object database), `refs.rs` (the ref projection and +synthetic commits), `upload_pack.rs` (protocol v2 `ls-refs` and +`fetch`), `receive_pack.rs` (pack ingestion and per-ref commands), +`gate.rs` (the pure merge gate), and `tests/git_endpoint.rs`. Additively: +the router mount in `app.rs`, `lib.rs`, and the manifest (090). The bridge +itself is 031; the merge queue that merges through evaluation is 076. + +## 3. Behavior + +- **B-1 (routes).** `GET /{ns}.git/info/refs?service=git-upload-pack| + git-receive-pack`, `POST /{ns}.git/git-upload-pack`, `POST /{ns}.git/ + git-receive-pack`, where `{ns}` is the 64-hex namespace of a hosted + repository (090 B-3); any other `{ns}` is `404`. Content types follow + the smart HTTP protocol (`application/x-git-<service>-advertisement`, + `-request`, `-result`); gzip request bodies are accepted. Upload-pack + honors `Git-Protocol: version=2` and serves v2 only (a v0 fetch is + answered `400` with the header named); receive-pack speaks v1 with + capabilities `report-status`, `side-band-64k`, `ofs-delta`, `quiet`. +- **B-2 (git odb is a cache).** Each hosted repository keeps a bare gix + object database at `<data>/repos/<ns>/git/objects` populated on demand + by 031 `export_tree` and the synthetic commits of B-3, with the 031 + `GitMap` at `<data>/repos/<ns>/.hq/gitmap.redb`. Deleting the directory + costs a re-export, never correctness; nothing in it is authority. +- **B-3 (refs are a projection).** `refs.rs` computes, from the 024 + `ChangeView` fold of the namespace the caller may read (main, plus + quarantine for a caller the router of 094 admits): `refs/changes/<change- + id-hex>/<n>` for every revision `n` of every open change, and + `refs/heads/main` as the merged lineage: one synthetic commit per + `change.merged` in total order (018), each with tree `export_tree( + revision.tree)`, parent the previous merged commit, author `<display> + <<principal-hex>@hqgit>` from the submitting principal, committer the + server identity (090 B-4), both timestamps the fact's `hlc.wall_ms` in + UTC, and message the revision message plus a trailer `Hqgit-Revision: + <id>`. When the revision's `extra["git.commit"]` names a commit whose + object the odb holds, that commit is advertised instead, so a tree that + entered through git keeps its oid. Two requests against an unchanged + ledger advertise byte-identical ref lists. `HEAD` is a symref to + `refs/heads/main`, absent until the first merge. +- **B-4 (upload-pack).** `ls-refs` (with `ref-prefix` and `symrefs`) and + `fetch` (with `want`, `have`, `done`, `thin-pack` off, `ofs-delta`) are + served by gix's pack generation over the odb; a `want` outside the + advertised set is refused per protocol. Reads are allowed for + `CallerTrust::Anonymous` only when the config `anonymous_read` (090 B-1) + is true; otherwise `401` with `WWW-Authenticate: Bearer`. +- **B-5 (receive-pack).** The pack is indexed into the odb through gix + (thin packs resolved against the odb; a missing base is `ng` for every + ref in the command list); packs above `[git] max_pack_bytes` (default + 512 MiB) are `413`. Commands are handled independently, each with its + own `ok <ref>` or `ng <ref> <reason>` in `report-status`: a push to + `refs/for/<change-id-hex>` or `refs/changes/<change-id-hex>/new` imports + the commit through 031 `import_commit` and appends + `change.revision_submitted` (024 B-3 `submit_revision`) with `extra + ["git.commit"]`, `extra["via"] = "git-receive-pack"`, and `extra + ["on_behalf_of"]` the caller principal, signed by the server identity + (090 B-4) into the namespace the router returns (090 B-6); a push to + `refs/for/main` with no change id opens a change (`change.opened`, + title the first line of the commit message) and submits revision 1; a + push to `refs/heads/main` runs B-6; a delete command, a tag, or any other + ref is `ng <ref> unsupported`. Non-fast-forward is not a concept here: a + forced push to a change ref is the next revision (design §1.1 point 2) + and nothing is destroyed. A pusher without a principal is `401`. +- **B-6 (the gate).** `gate.rs`: `fn gate(revision: &Revision, active: + &Cid, attestations: &[Attestation]) -> GateVerdict` is pure. It finds an + attestation with predicate `hqgit/policy-eval/v1` (067), subject the + `RevisionId`, claim `policy == active`, and verdict `Allow`, returning + `GateVerdict::Allow { attestation: AttestationId }`; otherwise + `Deny(GateReason::NoVerdict | PolicyMismatch { found: Cid } | + Denied(reasons) | SubjectMismatch)`. `receive_pack` resolves the pushed + commit to a revision through the `GitMap` (tree cid) and the + `ChangeView`, takes `active` from 068 `active_policy(repo, revision.at, + main)`, passes only attestations whose signature verifies through the + repository's resolver, and on `Allow` appends `change.merged` (024 + `mark_merged`) in main, then answers `ok refs/heads/main`. Any `Deny` + answers `ng refs/heads/main policy: <reason>`. The endpoint never + evaluates a policy itself (constitution XI): the verdict is evidence it + reads. A repository with no pinned policy denies with `NoVerdict`. +- **B-7 (no shelling out, no ambient input).** The module never spawns + `git`; every pack, tree, and ignore decision goes through gix and 031. + Commit timestamps come from ledger `Hlc` values, never the wall clock. + +## 4. Functional requirements + +- **FR-001.** `tests/git_endpoint.rs` skips (printing `git absent`) when + no `git` binary is on `PATH`; otherwise it drives the real client + against 090's `TestServer` with `LocalControlPlane` (091) and a + `StaticTokenAuthenticator`, covering: clone of a fresh repository + yields no refs; push to `refs/for/main` creates a change and revision 1 + visible in the `ChangeView`; a second push of an amended commit to + `refs/changes/<id>/new` yields revision 2 and both refs advertise; push + to `refs/heads/main` without a verdict is `ng ... NoVerdict`; after a + fixture `policy.pinned` fact (068) and a signed `hqgit/policy-eval/v1` + Allow attestation for revision 2, the push is `ok` and `change.merged` + appears in main; a clone after the merge checks out a tree byte- + identical to the pushed one; an Allow verdict for a different policy + cid is `PolicyMismatch`; a Deny verdict lists its reasons; an anonymous + push is `401`; an anonymous clone with `anonymous_read = false` is + `401`; a pack over the cap is `413`; two `info/refs` responses are + byte-identical; an unverified pusher's facts carry the quarantine tag + (021 B-6) while a `Verified` pusher's land in main. +- **FR-002.** `gate` has unit tests for every `GateReason` with no server. +- **FR-003.** Deleting `<data>/repos/<ns>/git/` and cloning again + reproduces the same ref list and tree oids. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-server --locked git_endpoint` passes + (or skips with `git absent`). +- **AC-2.** `git clone http://<addr>/<ns>.git` followed by `git push origin + HEAD:refs/for/main` prints `ok` in report-status and `hq change list` + (033) on the server's repository directory shows the change. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +SSH transport, protocol v0 and v1 fetch, LFS endpoints (chunked blobs are +already the general path, 014), tags, merge through speculative +evaluation (076), capability-checked routing and quarantine limits (094), +and credential validation (061, 102). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-server --locked git_endpoint +cargo test -p hqgit-server --locked +``` diff --git a/specs/093-connect-api/spec.md b/specs/093-connect-api/spec.md new file mode 100644 index 0000000..32024a6 --- /dev/null +++ b/specs/093-connect-api/spec.md @@ -0,0 +1,225 @@ +--- +id: "093-connect-api" +title: "The typed API: gRPC and Connect JSON services for changes, reviews, attestations, policy, repos; the CLI remote client" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 6 +depends_on: + - "090-server-skeleton" + - "026-review-threads" + - "027-attestation-primitive" + - "067-policy-evaluation-attestation" + - "032-cli-skeleton" + - "081-change-and-review-views" +establishes: + - "proto/hqgit/v1/common.proto" + - "proto/hqgit/v1/changes.proto" + - "proto/hqgit/v1/reviews.proto" + - "proto/hqgit/v1/attestations.proto" + - "proto/hqgit/v1/policy.proto" + - "proto/hqgit/v1/repos.proto" + - "crates/hqgit-server/build.rs" + - "crates/hqgit-server/src/api/mod.rs" + - "crates/hqgit-server/src/api/connect.rs" + - "crates/hqgit-server/src/api/read_model.rs" + - "crates/hqgit-server/src/api/changes.rs" + - "crates/hqgit-server/src/api/reviews.rs" + - "crates/hqgit-server/src/api/attestations.rs" + - "crates/hqgit-server/src/api/policy.rs" + - "crates/hqgit-server/src/api/repos.rs" + - "crates/hqgit-server/tests/api.rs" + - "crates/hqgit-cli/build.rs" + - "crates/hqgit-cli/src/client.rs" + - "crates/hqgit-cli/src/cmd_remote.rs" + - "crates/hqgit-cli/tests/remote.rs" +extends: + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/app.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + # The remotes table joins the CLI config; tonic and prost join its manifest. + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/config.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } + # prost, tonic-build, protox, pbjson, pbjson-build, and tower-http cors, pinned. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The one typed API of the edge (thesis §2, L7): five services defined in + protobuf, served as gRPC and as Connect-compatible JSON over plain HTTP + on the same listener so browsers and CLIs use one contract. Every write + is a fact append through the spec 021 path and the control plane (091), + never a table update; approvals and attestations arrive already signed + by their issuer and are verified before append; server-mediated writes + record who they were made for. Reads come from the spec 081 projections + when present and from a domain fold otherwise, through one ReadModel + seam, and every response says which ledger entry it is projected as of. + Pagination is by total-order cursor. The CLI gains a generated client + and hq remote so every existing verb can target a hosted repository. +--- + +# 093: The Connect API + +## 1. Purpose + +Thesis §4.7: the edge is interchangeable clients over the same signed +history, which is only true if there is one contract those clients share. +This spec is that contract. It refuses two shortcuts that would recreate +the incumbent shape (thesis §1): an API that updates rows (every write +here appends a fact, constitution VI), and an API that answers from an +index as if it were truth (every read carries `as_of`, 080 B-5). The CLI +client exists so that the offline verbs of wave 1 and a hosted repository +are the same verbs with a `--remote` flag (constitution XIII). + +## 2. Territory + +The protobuf package `hqgit.v1` under `proto/`; in `hqgit-server`, +`build.rs` (tonic-build plus pbjson-build over `proto/`), the `api` module +(`mod.rs` mounts, `connect.rs` the JSON transport, `read_model.rs` the +seam, one file per service), and `tests/api.rs`; in `hqgit-cli`, +`build.rs` (the client side of the same protos, generated independently +because the CLI never depends on the server), `client.rs`, `cmd_remote.rs`, +and `tests/remote.rs`. Additively: the server's `app.rs`, `lib.rs`, and +manifest (090); the CLI's `main.rs`, `cli.rs`, `config.rs`, and manifest +(032). The review UI that consumes the JSON form is 095. + +## 3. Behavior + +- **B-1 (common shapes).** `common.proto`: `Hash { string hex }`, `Cid { + string codec; string hex }`, `Hlc { uint64 wall_ms; uint32 logical; + string node }`, `Principal { string kind; string hex }`, `AsOf { Hash + entry; uint64 ordinal }`, `Page { string cursor; uint32 limit }` (limit + 1..=200, default 50), `Namespace { Hash id; string name }`, and + `ErrorDetail { string kind; string message }`. Hashes and cids render + exactly as 032 B-3 renders them. +- **B-2 (services).** `Changes { ListChanges, GetChange, GetRevisionDiff, + OpenChange, SubmitRevision, RetitleChange, AbandonChange }`; `Reviews { + ListThreads, OpenThread, PostComment, ResolveThread, ReopenThread, + Approve }`; `Attestations { ListAttestations, GetAttestation, Issue }`; + `Policy { Evaluate, ListVerdicts, Replay }`; `Repos { ListRepos, + CreateRepo, GetRepo, ListEntries, Append, PutObjects, GetObject }`. All + RPCs are unary; every list takes `Page` and returns `next_cursor` plus + `AsOf`; every read returns `AsOf`. `ListThreads` returns each thread's + position for the requested revision (081 B-2 resolutions) and, with + `include_bodies`, comment text resolved from the object store or + `erased = true`. `GetRevisionDiff(revision, against: PREVIOUS | BASE)` + returns per-file unified hunks computed from the two trees. +- **B-3 (two transports, one listener).** Every RPC is reachable as gRPC + and as Connect unary JSON: `POST /hqgit.v1.<Service>/<Method>` with + `Content-Type: application/json`, body the proto3 JSON of the request + (pbjson-generated serde, never hand-written), errors as `{ "code": + "<connect code>", "message", "details": [ErrorDetail] }` with Connect's + HTTP status mapping, CORS from `[api] cors_origins`. `connect.rs` is one + generic handler that deserializes, calls the same service object tonic + calls, and serializes, so the two transports cannot drift. +- **B-4 (writes are appends).** Every write RPC ends in exactly one call: + `RepoHandle::append(fact, target)` which builds the entry through 021 + `append_fact` and commits through the 091 `ControlPlane` (`propose`), + with `target` from the 090 `NamespaceRouter`. Server-mediated writes + (`OpenChange`, `SubmitRevision`, `RetitleChange`, `AbandonChange`, + `OpenThread`, `PostComment`, `ResolveThread`, `ReopenThread`) are signed + by the server identity with `extra["on_behalf_of"]` the caller and + `extra["via"] = "connect-api"` (090 B-4). `Approve`, `Attestations.Issue`, + and `Policy.Evaluate`'s result are attestations, and an attestation is + signed by its issuer or it is nothing (027, constitution XI): `Approve` + and `Issue` take `bytes attestation` (027 canonical bytes) plus the + claim object, verify the signature through the repository's resolver + before append, and refuse a server-signed substitute; `Evaluate` runs + 067 on the server and the resulting `hqgit/policy-eval/v1` attestation + is issued by the server's Service principal. `Repos.Append` accepts a + client-built entry (`bytes entry`, `bytes payload`) and runs the 017 B-6 + checks; it is how the CLI submits entries it signed locally. There is + no RPC that updates any row anywhere. +- **B-5 (reads through one seam).** `read_model.rs`: `trait ReadModel { + fn list_changes(..) -> Result<AsOf<Page<ChangeRow>>, Error>; fn change_ + detail(..); fn threads_for(..); fn attestations_for(..); fn verdicts_ + for(..); fn stack_of(..); }` with `ProjectedReadModel` over the 081 + views when their checkpoint exists for the repository and `FoldReadModel` + over the 024, 026, 027, and 050 folds otherwise; the choice is per + request and logged. A cursor is the base64 of 081's `PageCursor`; a + cursor from another query is `invalid_argument`. Rows are never + re-sorted after the read model orders them. +- **B-6 (auth).** The 090 `Caller` reaches every handler; reads are open + to `Anonymous` when `anonymous_read` is set, writes require a principal + (`unauthenticated` otherwise), and an `Agent` principal on `Approve` is + `permission_denied` (constitution XII; 102 refines). Objects: `PutObjects` + verifies each object's hash on receipt (013 B-4) and caps the batch at + `[api] max_objects_per_call` (default 1000) and `max_body_bytes` (090). +- **B-7 (errors).** `Error` maps to `invalid_argument` (Validation, + Crypto), `not_found`, `failed_precondition` (Stale, Drift), + `permission_denied` (Policy), `internal` (Io, Parse, Schema, Config), + each with `ErrorDetail.kind` the lowercase variant; the client maps back + so 010 B-9's exit codes hold end to end. +- **B-8 (CLI remote).** `hq remote add <name> <url> --namespace <hex>`, + `hq remote remove | list | status <name>` store `[remotes.<name>] url, + namespace` in the user config (032 B-4) and read the token from + `HQ_TOKEN` or `[remotes.<name>].token_env`. A global `--remote <name>` + makes `status`, `log`, `change list|show|new|submit`, `review show| + comment|reply|resolve|approve`, and `attest list|show|<issue>` target the + remote: reads through the API; `submit` uploads the snapshot's objects + with `PutObjects` then calls `SubmitRevision`; `approve` and `attest` + sign locally (033 B-6, 034 B-1) and submit through `Approve` and + `Issue`. `client.rs` is tonic over HTTP/2 with rustls (`--insecure` for + `http://` in tests). `--json` output is byte-identical to the local + verb's for the same ledger (032 FR-004). +- **B-9 (no ambient input).** No handler formats the wall clock; every + timestamp in a response is a ledger `Hlc` or an `AsOf`. + +## 4. Functional requirements + +- **FR-001.** `tests/api.rs` runs against 090's `TestServer` with + `LocalControlPlane` (091) through the generated tonic client and a + plain HTTP client for Connect JSON, covering: `CreateRepo` then + `GetRepo` heads; `OpenChange`, `PutObjects`, `SubmitRevision`, and + `GetChange` as of the new entry; `ListChanges` pagination stable across + an insert between two pages; `PostComment` and `ListThreads` with + bodies and an erased body; `Approve` with a locally signed attestation + accepted and with a tampered signature `invalid_argument`; `Issue` of + an unknown predicate carried verbatim; `Evaluate` on a fixture policy + and `ListVerdicts`, then `Replay` reporting `Match`; every `Error` + variant's code through a fixture handler; anonymous write + `unauthenticated`; an Agent `Approve` `permission_denied`; the same + `ListChanges` page from `FoldReadModel` and `ProjectedReadModel` + compared equal; Connect JSON request and gRPC request for one method + yielding the same response body. +- **FR-002.** `tests/remote.rs` drives the `hq` binary with `assert_cmd` + against the same server: `remote add`, `change list --remote`, `review + comment --remote`, `review approve --remote`, and `status --remote`. +- **FR-003.** Both `build.rs` files compile only `proto/hqgit/v1/*.proto` + through `protox` (no `protoc` on the host) and fail the build on a + proto change that removes or renumbers a field (a golden descriptor set + under each crate's `tests/`). +- **FR-004.** `cargo tree -p hqgit-cli` shows no `hqgit-server` and vice + versa. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-server --locked api` and `cargo test -p + hqgit-cli --locked --test remote` pass. +- **AC-2.** `curl -X POST http://<addr>/hqgit.v1.Repos/ListRepos -H + 'content-type: application/json' -d '{}'` returns a JSON page with + `asOf`. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The browser client (095), OIDC sessions and device-code login (061), +Biscuit-authenticated agents (102), capability-checked routing and +quarantine reads (094), server-side streaming, and any RPC over issues +(a later feature spec adds `issues.proto` beside these). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-server --locked api +cargo test -p hqgit-cli --locked --test remote +``` diff --git a/specs/094-quarantine-and-promotion/spec.md b/specs/094-quarantine-and-promotion/spec.md new file mode 100644 index 0000000..4d3cd78 --- /dev/null +++ b/specs/094-quarantine-and-promotion/spec.md @@ -0,0 +1,208 @@ +--- +id: "094-quarantine-and-promotion" +title: "Quarantine and promotion: untrusted writes land quarantined, capabilities are attestations, promotion is a fact" +status: approved +kind: "kernel" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 6 +depends_on: + - "091-per-repo-control-plane" + - "064-attestation-verification" +establishes: + - "crates/hqgit-server/src/quarantine.rs" + - "crates/hqgit-server/src/capability.rs" + - "crates/hqgit-server/tests/quarantine.rs" + - "crates/hqgit-server/testdata/capabilities/" +extends: + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + # The capability router replaces TrustRouter in the app state. + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/app.rs", nature: additive } + # hqgit-trust joins the server's dependencies. + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } + # The namespace.promoted variant joins the domain vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + Constitution XV in code. Every write reaching the server from a + principal that is not verified and not capable (anonymous pushers, fork + contributors, the mirror's Service principal, any agent) is appended to + the repository's quarantine namespace, never to main. A capability is an + attestation with predicate hqgit/capability/v1 over a principal, rooted + at the repository's genesis identity, verified end to end by spec 064, + and evaluated by pure functions over the verified set at the Hlc of the + write in question. Promotion is a namespace.promoted fact signed by a + holder of the promote capability; main's fold is main's facts plus what + main has promoted, so a promoted fact keeps its original issuer and + signature. Per-principal rate and size limits bound abuse of the + append-only store, and quarantine content is served only to holders of + read-quarantine. +--- + +# 094: Quarantine and promotion + +## 1. Purpose + +Thesis §8 names abuse in an append-only replicated store as a standing +risk, and constitution XV answers it: untrusted by default, promoted by +capability. Spec 021 created the quarantine namespace and tagged its +entries; 090 routed unverified writes there provisionally; 040 put every +mirrored fact there. This spec supplies what those relied on: a definition +of "capable" that is evidence rather than a settings table (constitution +IX, XI), a promotion path that is itself a signed fact, and the limits +that keep an open endpoint from being filled. Because the checks are pure +over verified attestations, any replica reaches the same answer. + +## 2. Territory + +`capability.rs` (the predicate, its claim schema and validator, the pure +`CapabilitySet` and its checks), `quarantine.rs` (the router, the +promotion path, the promotion-aware fact source, limits, the object +visibility index, the purge subcommand), `tests/quarantine.rs`, and +frozen vectors under `testdata/capabilities/`. Additively: `app.rs`, +`lib.rs`, and the manifest (090) and the `namespace.promoted` variant in +023's `facts.rs`. Delegation chains for agents are 101; federation peers +holding `promote` are 112. + +## 3. Behavior + +- **B-1 (capabilities are attestations).** `Capability` is a closed enum + `WriteMain | Promote | ReadQuarantine | Grant`. A grant is an + `Attestation` (027) with predicate `hqgit/capability/v1`, subject the + grantee's `IdentityId` (060), issuer the grantor, and claim `{ + namespace: Hash, action: Grant { capabilities: Vec<Capability>, expires: + Option<Hlc>, delegable: bool } | Revoke { grant: AttestationId } }`. + `capability.rs` registers the predicate and a `ClaimValidator` with the + 027 registry at server start (`register_capability(&mut registry)`); + the kind string and claim schema are frozen by a vector. +- **B-2 (root of authority).** The principal recorded in the repository's + `RepoGenesis` (013 B-2, `created_by`) holds every capability on every + namespace of that repository, implicitly and irrevocably. Everyone else + holds exactly what a chain of grants proves: each link is a + `VerifiedAttestation` (064; an unverified or failed one contributes + nothing), its issuer is the root or holds `Grant` with `delegable` at + the link's `at`, it is not expired at the check `Hlc`, and no verified + `Revoke` names it. Only the root and holders of `Grant` may issue a + `Revoke`, and only for a grant they issued or one beneath them in the + chain. +- **B-3 (pure checks).** `CapabilitySet::build(root: &Principal, + verified: &VerifiedAttestationSet, at: &Hlc) -> CapabilitySet` and + `fn check(&self, principal: &Principal, namespace: &Hash, cap: + Capability) -> CapabilityCheck` where `CapabilityCheck` is `Granted { + via: Vec<AttestationId> } | Denied(DenyReason)` and `DenyReason` is + `NoGrant | Expired { at: Hlc } | Revoked { by: AttestationId } | + IssuerLackedGrant { issuer: Principal } | NotDelegable`. No I/O, no + clock: `at` is the `Hlc` of the write being judged, so replaying the + decision years later yields the same answer. Permuting the verified set + never changes a result (a test proves it). +- **B-4 (the router).** `QuarantineRouter` implements 090 B-6 and replaces + `TrustRouter` in `AppState`. `target_for` returns `main` only when the + caller is `CallerTrust::Verified`, its principal kind is `Human` or + `Org`, and `check(principal, main, WriteMain)` is `Granted`; a `Service` + principal that is the server identity itself qualifies for the facts it + issues on its own behalf, but a fact carrying `on_behalf_of` (090 B-4) + is judged by the behalf principal; the mirror principal (040 B-2) and + every `Agent` principal are quarantined regardless of grants (101 and + 102 extend this rule with the delegation chain); everything else is + quarantined. Quarantined entries carry 021 B-6's `extra["namespace"]` + plus `extra["submitted_by"]` and `extra["trust"]` (`anonymous`, + `unbound`, `no-capability`, `agent`, `mirror`). +- **B-5 (promotion).** `namespace.promoted` (023, frozen here) body `{ + from: Hash, to: Hash, entries: Vec<EntryHash> (sorted, deduplicated), + promoted_by: Principal, reason: Option<String>, extra }`, appended to + `to`. A promotion MUST arrive signed by the promoter's own key (through + 093 `Repos.Append` or the CLI); the server refuses to mint one on a + caller's behalf. Before `propose` (091) the server checks: every entry + exists in `from`, `from` is a quarantine namespace and `to` is not, and + `check(promoted_by, to, Promote)` is `Granted` at the fact's `Hlc`; + otherwise `Error::Policy` naming the reason. Facts never move (021 + B-6): the promoted entries keep their issuer, signature, and hash. +- **B-6 (main's fold).** `PromotedView` is a `DerivedState` (019) over + `namespace.promoted` answering `promoted(entry) -> Option<EntryHash>` + and recording a `PromotionDefect { entry, reason }` for any promotion + whose promoter lacked `Promote` at its `Hlc` (mirroring 060 B-6: a + defect alters nothing, so a rogue node cannot inject through the store). + `PromotionAwareFactSource` wraps 021's `FactSource`: the fact set of + main is main's entries plus every entry a non-defective promotion names, + ordered by 018 over the union. Every server-side fold, the 080 runner + when driven by the server, and the 093 read models MUST read main + through it; a rebuild from zero reproduces the same set. +- **B-7 (reading quarantine).** Listing or fetching quarantine entries + (093 with `namespace = quarantine`, 092 change refs from quarantine) + requires `check(caller, quarantine, ReadQuarantine)` or the caller being + the entry's own `submitted_by`; otherwise `Error::Policy`. An object is + served by cid only when the visibility index says a main-visible entry + references it or the caller may read quarantine; the index lives in + `<repo>/.hq/quarantine.redb`, is rebuilt by `hqgit-server quarantine + reindex`, and is never authority. +- **B-8 (limits).** Per principal, or per `token_hash`, or per hashed + remote address for `Anonymous`: `[quarantine] writes_per_hour = 60`, + `bytes_per_day = 268435456`, `max_payload_bytes = 4194304`, + `max_objects_per_write = 1000`, enforced by a token bucket persisted in + `quarantine.redb`; exceeding one is `Error::Policy` with kind + `rate-limited` (093 maps it to `resource_exhausted`). Holders of + `WriteMain` are exempt from per-principal buckets but bound by 090's + body limit. +- **B-9 (purge).** `hqgit-server quarantine purge <ns> <cid> --reason + <Moderation|Legal>` tombstones through 021 `erase` with the server + identity's owner capability (021 B-7) when the server identity is the + repository root, and refuses otherwise. + +## 4. Functional requirements + +- **FR-001.** `testdata/capabilities/` holds frozen vectors: `grant-direct + .json`, `grant-delegated.json`, `grant-non-delegable.json`, `revoked + .json`, `expired.json`, `forged-issuer.json`, each with seeds, the + claims, canonical bytes, attestation ids, and expected `CapabilityCheck` + answers at named `Hlc` probes. +- **FR-002.** `tests/quarantine.rs` covers, against 090's `TestServer` with + `LocalControlPlane`: an anonymous write lands in quarantine with the + `extra` tags; a `Verified` human without a grant lands in quarantine + with `trust = no-capability`; with a root-issued `WriteMain` grant the + write lands in main; the mirror principal and an agent land in + quarantine despite grants; a promotion by a principal without `Promote` + is `Error::Policy`; with an expired or revoked grant likewise; a + delegated chain root to A (`delegable`) to B succeeds and a non- + delegable chain fails with `NotDelegable`; a promotion naming an entry + absent from quarantine is refused; after a valid promotion the promoted + change appears in the 024 fold over `PromotionAwareFactSource` and in + the 081 `changes` view after rebuild, with its original issuer; a + promotion injected at the store level by a promoter lacking capability + is a `PromotionDefect` and folds nothing; quarantine listing without + `ReadQuarantine` is refused and with it succeeds; the sixty-first write + in an hour is `rate-limited`; a payload over the cap is refused; the + same `CapabilitySet` answers identically for two permutations of the + verified set; `purge` tombstones and the chain still verifies (020 B-7). +- **FR-003.** `capability.rs` contains no I/O and reads no clock; a source + guard test asserts it (010 FR-003 pattern). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-server --locked quarantine` passes, + vectors included. +- **AC-2.** On a repository imported through 040, `hq log --namespace + quarantine` lists the mirrored facts, and after a root-signed promotion + submitted with `hq --remote`, `hq change list --remote` shows the + promoted change without any fact having been re-signed. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Delegation chains that let an agent's writes reach main (101, 102), +federation peers holding `promote` (112), OIDC binding that produces +`Verified` callers (061), erasure granted by a policy verdict (068), and +any moderation UI. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-server --locked quarantine +cargo test -p hqgit-server --locked +``` diff --git a/specs/095-web-review-ui/spec.md b/specs/095-web-review-ui/spec.md new file mode 100644 index 0000000..0f0ccc9 --- /dev/null +++ b/specs/095-web-review-ui/spec.md @@ -0,0 +1,198 @@ +--- +id: "095-web-review-ui" +title: "The web review client: change list, change detail with semantic deltas first, threads, the evidence panel" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 6 +depends_on: + - "093-connect-api" + - "081-change-and-review-views" +establishes: + - "web/package.json" + - "web/tsconfig.json" + - "web/vite.config.ts" + - "web/index.html" + - "web/src/main.tsx" + - "web/src/api.ts" + - "web/src/format.ts" + - "web/src/views/ChangeList.tsx" + - "web/src/views/ChangeDetail.tsx" + - "web/src/views/DeltaPanel.tsx" + - "web/src/views/LineDiff.tsx" + - "web/src/views/EvidencePanel.tsx" + - "web/src/views/ThreadPanel.tsx" + - "web/test/" + - "crates/hqgit-server/src/static_files.rs" +extends: + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/app.rs", nature: additive } + # include_dir (embedded dist behind a feature) joins the manifest and the table. + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The minimal review client (thesis §9: a UI beyond review is a later + client). A TypeScript, React, and Vite single-page application under + web/ that speaks only the Connect JSON form of the spec 093 API and + renders four things: a paginated change list; a change detail whose + order is the argument of design §1.1 point 3 (policy verdict, then the + semantic delta views for API surface, dependencies, and capabilities, + then threads, then the line diff last and collapsed); threads with + their per-revision exact, moved, text, lost, and unavailable markers; + and an evidence panel listing every attestation over the revision with + its verification status. Every view prints which ledger entry it is + projected as of. The server serves web/dist from static_files.rs, the + package carries its spec-spine binding, and no tsconfig exists at the + repository root (spec 001 D-1). +--- + +# 095: The web review client + +## 1. Purpose + +Design §1.1 point 3: the line diff is the lowest-value view of a change, +and point 7: a human should review the argument rather than the diff. +Every incumbent UI puts the diff first because its data model has nothing +else. This client exists to prove the ledger has something else, and to +be honest about it: when no semantic delta has been attested for a +revision the panel says so, when an attestation failed verification the +badge says why, and when a thread's anchor was lost the marker says lost. +It is deliberately small: a review surface, served by the same binary, +reading the same API the CLI uses (constitution XIII). + +## 2. Territory + +The whole of `web/` as founded here: `package.json` (with `"spec-spine": +{ "spec": "095-web-review-ui" }`), `tsconfig.json` (the only one in the +repository), `vite.config.ts` (build and vitest config), `index.html`, +`src/main.tsx` (router and shell), `src/api.ts` (the typed Connect JSON +client), `src/format.ts` (hash, cid, principal, and Hlc rendering), the +six views, and the `test/` subtree with recorded fixtures. On the Rust +side, `static_files.rs` in `hqgit-server` and, additively, `app.rs`, +`lib.rs`, and the manifest (090). Login is 061; in-browser signing is a +later spec. + +## 3. Behavior + +- **B-1 (toolchain).** TypeScript in `strict` mode, React 18, Vite 6, + vitest with jsdom and Testing Library, `engines.node = ">=22"`, every + dependency pinned exact, `package-lock.json` committed, `npm ci` the + only install. Scripts: `build` (`tsc --noEmit && vite build` into + `web/dist`), `test` (`vitest run`), `dev`. No tsconfig, package.json, + or lockfile is placed at the repository root (001 D-1; spec-spine.toml + lists `web` as a standalone npm package). +- **B-2 (the client).** `api.ts` exposes one function per RPC this UI + uses (`listChanges`, `getChange`, `getRevisionDiff`, `listThreads`, + `postComment`, `resolveThread`, `reopenThread`, `listAttestations`, + `listVerdicts`, `replay`, `getRepo`, `listRepos`) as `fetch` calls to + `POST /hqgit.v1.<Service>/<Method>` with `application/json` (093 B-3), + an optional bearer from session storage sent as `Authorization`, and + the TypeScript types of the messages it uses declared beside them. A + Connect error body becomes `ApiError { code, message, details }`. Every + response type carries `asOf`; the client never strips it. A drift-guard + test parses each recorded fixture with the declared types. +- **B-3 (change list).** Route `/` (and `/repos/<ns>`): rows from + `listChanges` in the order the API returns them (never re-sorted), with + id prefix, title, state, latest revision number, stack position when + present, open thread count, and the latest verdict badge; a state + filter; cursor pagination with `next`; a footer `projected as of + <entry-prefix> (ordinal N)` (080 B-5) on every page. +- **B-4 (change detail).** Route `/changes/<id>` with a revision selector + (latest by default). Sections in this DOM order and no other: (1) the + verdict header from `listVerdicts` (Allow, or Deny with every reason + listed, or `no policy evaluation recorded`); (2) `DeltaPanel`: API + surface, dependency, and capability deltas read from + `hqgit/semantic-delta/v1` attestations (051) over the selected revision, + each rendered as added, removed, and changed lists, an `unsupported` + delta rendered as such, and a missing delta rendered as `no semantic + delta attested for this revision`; (3) `ThreadPanel`; (4) + `EvidencePanel`; (5) `LineDiff`, collapsed by default, fetching + `getRevisionDiff` only when expanded, against the previous revision by + default and the base on request, with an erased blob rendered as + `erased`. A test asserts the order by DOM position. +- **B-5 (threads).** `ThreadPanel` lists threads for the selected revision + with the 081 B-2 position marker (`exact`, `moved` with confidence, + `text`, `lost` with the original path and line, `unavailable`), comments + in the order returned with author kind badges (human, agent, service, + org), `erased` bodies rendered as such, and a comment form plus resolve + and reopen actions that call the server-mediated writes of 093 B-4; + after a write the panel refetches and shows the new `asOf`. The panel + renders the routing outcome the API reports (main or quarantine) so an + unverified viewer sees that their comment is quarantined. +- **B-6 (evidence).** `EvidencePanel` lists every attestation whose + subject is the selected revision, grouped by predicate, each with + issuer principal and kind, `at`, and a verification badge from 081 + B-3's column (`ok`, `failed` with reason, `unverified`), the claim + summarized per known predicate (approval verdict, test counts, finding + counts, mirror source) and shown as raw JSON otherwise; the policy-eval + rows offer `replay`, showing 067's `Match | Mismatch | Unavailable`. + There is no approve button: an approval is signed by the approver's key + (027, constitution XI) and the panel prints the `hq review approve + --remote` invocation instead. +- **B-7 (rendering rules).** `format.ts` renders hashes and cids as a + 12-character prefix with the full value on hover and copy, principals + as `<kind>:<prefix>`, and `Hlc` as UTC ISO-8601 from `wall_ms` with the + logical counter on hover; nothing reads `Date.now()` to describe ledger + data. Comment bodies and diff text are rendered as text nodes, never as + HTML. +- **B-8 (serving).** `static_files.rs` mounts `GET /`, `GET /ui`, and + `GET /ui/*` to `index.html` (SPA fallback) and `GET /assets/*` to the + hashed bundles with `Cache-Control: public, max-age=31536000, + immutable`; `index.html` is `no-cache`. Files come from the embedded + `web/dist` when the crate is built with feature `web` (`include_dir`) + and from `[web] dist_dir` otherwise; with neither, `/ui` answers `404 + { "hint": "build web/ or set web.dist_dir" }`. The mount never shadows + `/hqgit.v1.*`, `/*.git/*`, `/healthz`, or `/readyz`. Responses carry + `Content-Security-Policy: default-src 'self'; connect-src 'self'; + img-src 'self' data:` and `X-Content-Type-Options: nosniff`. + +## 4. Functional requirements + +- **FR-001.** `web/test/fixtures/` holds recorded Connect JSON responses + from the 093 fixtures (a change with two revisions, threads in every + position state, attestations in every verification state, an Allow and + a Deny verdict, a semantic delta and a revision without one). +- **FR-002.** `web/test/` covers with vitest: `api.ts` request shape and + error mapping; the drift guard of B-2; `ChangeList` rows, filter, + pagination, and the as-of footer; `ChangeDetail` section order; the + `DeltaPanel` absent and unsupported renderings; every `ThreadPanel` + marker and the erased body; every `EvidencePanel` badge, Deny reasons, + and the replay result; `format.ts` against fixed inputs; a comment body + containing markup rendered as text. +- **FR-003.** `cargo test -p hqgit-server --locked static_files` covers + the SPA fallback, asset caching headers, the no-dist hint, the CSP + header, and that `/hqgit.v1.Repos/ListRepos` still reaches the API with + the UI mounted. +- **FR-004.** `npm run build` produces `web/dist` with hashed asset names + and no source maps in production; `web/dist` is gitignored. + +## 5. Acceptance criteria + +- **AC-1.** `cd web && npm ci && npm run build && npm test` exits 0. +- **AC-2.** `cargo test -p hqgit-server --locked static_files` passes. +- **AC-3.** Against a server built with `--features web` holding the 033 + fixture ledger, `/ui/changes/<id>` renders the change with the delta + section above the line diff and the evidence panel listing the approval. + +## 6. Out of scope + +Login and sessions (061), in-browser signing of approvals and attestations +(a later spec over 063), issue views, search (082), feeds (085), stack +visualization beyond the position column (a later client), and any UI +for repository administration or key management. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cd web && npm ci && npm run build && npm test +cargo test -p hqgit-server --locked static_files +``` diff --git a/specs/100-agent-principals/spec.md b/specs/100-agent-principals/spec.md new file mode 100644 index 0000000..9cdd500 --- /dev/null +++ b/specs/100-agent-principals/spec.md @@ -0,0 +1,214 @@ +--- +id: "100-agent-principals" +title: "Agent principals: registration facts, Biscuit tokens, and the caveat vocabulary" +status: approved +kind: "kernel" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 7 +depends_on: + - "060-identity-and-key-rotation" + - "068-policy-in-repo" +establishes: + - "crates/hqgit-agent/Cargo.toml" + - "crates/hqgit-agent/src/lib.rs" + - "crates/hqgit-agent/src/token.rs" + - "crates/hqgit-agent/src/principal.rs" + - "crates/hqgit-agent/src/caveats.rs" + - "crates/hqgit-agent/tests/" + - "crates/hqgit-agent/testdata/tokens/" +extends: + # biscuit-auth (pinned exact) joins the shared dependency table. + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } + # agent.registered, agent.deregistered, agent.token_revoked join the vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + Constitution XII and thesis D12: an agent is a distinct principal class + and never authenticates as a human. This spec founds hqgit-agent and + fixes how an agent becomes one: an identity of kind Agent (060) that an + operator, a human or an org, registers with a fact naming the runtime + and the declared sandbox; a Biscuit token whose authority block is + signed by the operator's identity key and names the agent, the + registration entry, the pinned policy (068), the namespaces, the + capabilities, and an expiry; attenuation that can only add caveats, + from a frozen datalog vocabulary over namespace, path, predicate, + revision size, operation, and time window; offline verification against + the operator's key as the 060 fold knew it at issuance; and revocation + by fact. The chain of delegations and the authorize function are 101. +--- + +# 100: Agent principals + +## 1. Purpose + +Design §1.1 point 7: agents authenticate as humans holding human tokens, +so nothing about their scope, sandbox, or accountability is recorded. +Thesis §4.5 answers with Biscuit: the delegation chain lives in the +token, attenuation is monotonic and verifiable offline, and caveats are +datalog. The `Principal::Agent` variant has existed since 010 B-6 so the +type system could refuse a human token on an agent path from the first +line of code; this spec supplies the credential and the registration +that variant was waiting for. Tokens are bearer secrets and never enter +the ledger; the facts that make them meaningful do. + +## 2. Territory + +`crates/hqgit-agent` as founded here: the manifest (workspace deps +`hqgit-types`, `hqgit-object`, `hqgit-ledger`, `hqgit-domain`, +`hqgit-trust`, `hqgit-policy`, and `biscuit-auth`), `lib.rs`, +`token.rs` (issue, attenuate, parse, check), `principal.rs` (the +registration facts, `AgentView`, credential resolution), `caveats.rs` +(the vocabulary and its datalog), the `tests/` subtree, and frozen datalog +vectors under `testdata/tokens/`. Additively: the fact variants in 023's +`facts.rs` and the dependency table. Delegation facts and `authorize` are +101; the sandbox type and provenance are 102. + +## 3. Behavior + +- **B-1 (facts).** Frozen kinds: `agent.registered { agent: AgentId, + operator: Principal, runtime: String, sandbox: Value, extra }`, + `agent.deregistered { agent: AgentId, operator: Principal, reason: + String, extra }`, `agent.token_revoked { agent: AgentId, revocation_id: + Hash, reason: String, extra }`. `operator.kind()` MUST be `Human` or + `Org`. The agent MUST already exist as an `identity.created` of kind + `Agent` (060 B-2) in the same ledger. `agent.registered` and + `agent.deregistered` MUST be signed by the operator's active key at the + entry's `hlc` (060 B-5); `agent.token_revoked` by the operator's or the + agent's. `sandbox` is an opaque canonical `Value` here; 102 gives it a + type and a rule. +- **B-2 (`AgentView`).** A `DerivedState` (019) folding B-1 in total + order: `registration(agent) -> Option<Registration { agent, operator, + runtime, sandbox: Value, entry: EntryHash, at: Hlc }>` (the latest by + `Hlc` wins, an LWW register), `is_active(agent, at) -> bool` (registered + and not deregistered at `at`), `revoked(agent) -> BTreeSet<Hash>` of + revocation ids, and `defects() -> &[ChainDefect]` in 060 B-6's shape: a + fact violating B-1 is recorded and never applied. +- **B-3 (root key).** The operator's identity key is the Biscuit root + key: `token.rs` converts a 010 `Ed25519Signer` seed to a + `biscuit_auth::KeyPair` and a 010 `PublicKey` to a + `biscuit_auth::PublicKey`, 32 bytes each way, with no second key + material. A token is verified against `IdentityView::key_valid_at( + issuer_key, issued_at)` (060): a rotation after issuance keeps the + token valid until it expires; a revocation whose `since` precedes + `issued_at` invalidates it. +- **B-4 (authority block, frozen).** `Authority { agent: AgentId, + operator: Principal, issuer_key: KeyId, registration: EntryHash, + policy: Cid, namespaces: BTreeSet<Hash>, capabilities: + BTreeSet<Operation>, issued_at_ms: u64, expires_ms: u64 }` renders as + block 0, one fact per line in this order: + `hq_version(1); agent("<hex>"); operator("<kind>:<hex>"); + issuer_key("<hex>"); registration("<hex>"); policy("<cid>"); + namespace("<hex>");` (one per namespace, sorted), `capability("<op>");` + (one per operation, sorted), `issued_at(<ms>); expires(<ms>); + check if now($t), $t >= <issued_at>, $t < <expires>;`. + `Operation` is a closed enum `Read | Append | Attest | ReadQuarantine` + with wire strings `read`, `append`, `attest`, `read-quarantine`. + `expires_ms - issued_at_ms` MUST be at most 30 days (`Error::Validation` + on issue). The authorizer's policies are `allow if operation($o), + capability($o);` then `deny if true;`; because Biscuit scopes rules to + the authority and authorizer facts, a `capability` fact added in a + later block is invisible: attenuation cannot widen. +- **B-5 (caveats, frozen).** `Caveat` is a closed enum whose + `to_datalog()` is one check each, over the authorizer facts `now(ms)`, + `operation(str)`, `namespace(hex)`, `path(str)` (one per file touched; + `path("")` when none), `predicate(str)` (`""` when none), + `revision_bytes(u64)` (`0` when none), and `fact_kind(str)` (`""` when + none), every fact always supplied so no caveat fails vacuously: + `Namespace(h)` is `check if namespace("<h>");`; `PathPrefix(p)` is + `check all path($p), $p == "" || $p.starts_with("<p>");`; + `PathPattern(re)` is `check all path($p), $p == "" || $p.matches("<re>");`; + `PredicateIn(list)` is `check if predicate($p), $p == "" || [<list>].contains($p);`; + `MaxRevisionBytes(n)` is `check if revision_bytes($s), $s <= <n>;`; + `TimeWindow { from_ms, until_ms }` is `check if now($t), $t >= <from>, $t < <until>;`; + `OperationIn(list)` is `check if operation($o), [<list>].contains($o);`; + `FactKindIn(list)` is `check if fact_kind($k), $k == "" || [<list>].contains($k);`. + Strings are escaped as Biscuit requires; lists are sorted. `Caveat` + derives `Canonical` (011) so a caveat set can live in a fact (101). +- **B-6 (token API).** `AgentToken(Biscuit)` with `issue(root: + &KeyPair, authority: &Authority) -> Result<AgentToken, Error>`, + `attenuate(&self, caveats: &[Caveat]) -> Result<AgentToken, Error>` + (one new block whose checks are B-5's, in order), `parse(bytes, root: + &PublicKey) -> Result<AgentToken, Error::Crypto>`, `authority(&self) -> + Result<Authority, Error>` (decoded from block 0; a missing fact is + `Error::Validation` naming it; a token without `agent` is + `TokenDeny::NotAgentToken`), `revocation_ids(&self) -> Vec<Hash>` (each + `Hash::of` of a block's Biscuit revocation identifier, authority first), + and `check(&self, facts: &AuthorizerFacts, revoked: &BTreeSet<Hash>) + -> Result<(), TokenDeny>`. `TokenDeny` is `NotAgentToken | Signature | + Expired { expires_ms, now_ms } | NotYetValid { issued_at_ms, now_ms } | + Revoked(Hash) | CaveatFailed(String) | CapabilityMissing(Operation)`. + `AgentCredential { token: Vec<u8>, issuer_key: KeyId }` has the text + form `hqa1.<base64url of its canonical bytes>`; `AgentCredential::parse` + rejects any other prefix with `NotAgentToken`. +- **B-7 (resolution).** `resolve_agent(cred, identities: &IdentityView, + agents: &AgentView, at: &Hlc) -> Result<ResolvedAgent { agent, + operator, registration, token }, ResolveError>` in a fixed order: + `issuer_key` known to an identity (`UnknownIssuerKey`); valid at + `issued_at` (`IssuerKeyInvalid(KeyValidity)`); token signature + (`Signature`); authority decodes; `agent` registered and active at `at` + (`NotRegistered`, `Deregistered`); `registration` equals the view's + entry hash and `operator` equals the registration's operator and the + issuer key's identity (`OperatorMismatch`). The function never returns + a `Principal::Human`; a 061 session is not an `AgentCredential` and + fails at B-6's prefix check (102 enforces the converse on routes). +- **B-8 (policy pin).** `authority.policy` is compared with 068's + `active_policy(repo, at, scope)` by 101; here `Authority::policy_matches( + &self, active: &Cid) -> bool` is the one comparison both use. +- **B-9 (no ambient input).** `now` is an `Hlc` the caller supplies (the + server's 018 generator); the crate reads no clock and no environment; + seeds are never `Debug`-printed; `BTreeMap` and `BTreeSet` only. + +## 4. Functional requirements + +- **FR-001.** Vectors under `testdata/tokens/`: `authority.datalog` (the + rendered block 0 for a fixture `Authority`), `caveats.json` (every + `Caveat` variant and its frozen datalog), `seeds.json` (operator and + agent seeds, fixture `Hlc`s). Token bytes are not frozen (Biscuit + block signatures are randomized); the rendered datalog is. +- **FR-002.** Tests cover: issue then parse under the operator key; + parse under another key fails; an attenuation adding `capability( + "append")` to a read-only token still denies `append`; each caveat + admits its fixture and rejects its counterexample; expiry and + not-yet-valid; a revocation id recorded by `agent.token_revoked` denies + the token and its attenuations; rotation after issuance still + resolves; revocation with `since` before issuance does not; a + deregistered agent does not resolve; `OperatorMismatch`; a registration + signed by a non-operator key is a defect; a 30-day-plus lifetime is + refused; permutation of fact arrival yields the same `AgentView`. +- **FR-003.** `register_agent_facts(registry)` registers exactly the + three B-1 kinds; 023's `domain/fact-kinds.json` gains them (a visible + vector change, 023 FR-002). +- **FR-004.** The manifest carries `[package.metadata.spec-spine] spec = + "100-agent-principals"` and depends on no crate above L6. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-agent --locked` passes, vectors included. +- **AC-2.** `spec-spine index` discovers `hqgit-agent` bound to this spec + and `index coverage --fail-on-untraced` exits 0. +- **AC-3.** A token issued for `Read` and attenuated to `PathPrefix( + "crates/hqgit-agent/")` passes `check` for a read of that path and fails + for a read of `crates/hqgit-trust/` and for any `Append`. + +## 6. Out of scope + +Delegation facts, chain resolution, and `authorize` (101); the sandbox +type, agent-action provenance, and the server's credential handling +(102); evidence bundles (103); OIDC sessions for humans (061); third-party +Biscuit blocks signed by external keys (not used: a sub-delegation is a +new token rooted in the delegator's key, 101). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-agent --locked +cargo clippy -p hqgit-agent --all-targets --locked -- -D warnings +``` diff --git a/specs/101-delegation-chain/spec.md b/specs/101-delegation-chain/spec.md new file mode 100644 index 0000000..18dd53f --- /dev/null +++ b/specs/101-delegation-chain/spec.md @@ -0,0 +1,188 @@ +--- +id: "101-delegation-chain" +title: "Delegation chain: delegation facts, chain resolution to a human root, and authorize" +status: approved +kind: "kernel" +domain: "l4-trust" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: critical +wave: 7 +depends_on: + - "100-agent-principals" +establishes: + - "crates/hqgit-agent/src/delegation.rs" + - "crates/hqgit-agent/src/authorize.rs" + - "crates/hqgit-agent/tests/delegation.rs" + - "crates/hqgit-agent/testdata/delegation/" +extends: + - { spec: "100-agent-principals", unit: "crates/hqgit-agent/src/lib.rs", nature: additive } + # agent.delegated, agent.delegation_revoked, agent.action_denied join the vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + The accountability chain as a cryptographic object. Every token an + agent holds is mirrored by an agent.delegated fact naming who delegated, + to which agent, under which pinned policy, with which caveats, expiring + when, so the chain is queryable long after the bearer token is gone. A + sub-delegation is a new token rooted in the delegating agent's identity + key with a caveat set at least as strict as its parent's. This spec + fixes the facts, the DelegationView fold, delegation_chain resolving an + agent to its human or org root within a bounded depth, and authorize: + a pure function combining Biscuit verification (100) with the ledger's + registrations, revocations, and policy pin (068), returning Allow with + the chain hash or Deny with a closed reason that the caller journals as + an agent.action_denied fact. +--- + +# 101: Delegation chain + +## 1. Purpose + +Constitution XII: an agent's credential carries its delegation chain, +which human, under which policy version, expiring when. A Biscuit token +carries that chain to whoever holds it, but tokens expire, are lost, and +are never appended to a ledger. Audit needs the chain to survive the +token, and revocation needs a place a holder cannot suppress. This spec +puts both in facts, and it makes the authorization decision one function +with one fixed order of checks, so every replica and every test reaches +the same verdict from the same ledger. + +## 2. Territory + +`delegation.rs` (the facts, `DelegationId`, `DelegationView`, +`delegation_chain`, `Chain::hash`) and `authorize.rs` (`ActionRequest`, +`AuthzContext`, `authorize`, `DenyReason`, the journal fact) in +`crates/hqgit-agent`, `tests/delegation.rs`, and chain fixtures under +`testdata/delegation/`. Additively: the crate's `lib.rs` and three fact +variants in 023's `facts.rs`. Sandbox validation and the server seam are +102. + +## 3. Behavior + +- **B-1 (facts).** Frozen kinds: `agent.delegated { from: Principal, to: + AgentId, policy: Cid, scope: Vec<Caveat>, expires: Option<Hlc>, + revocation_id: Hash, nonce: Nonce, extra }` whose `DelegationId = + Hash::of(canonical body bytes)` (023 B-3 style); `agent.delegation_revoked + { delegation: DelegationId, by: Principal, reason: String, extra }`; + `agent.action_denied { agent: Option<AgentId>, operation: String, + namespace: Hash, reason: String, detail: String, request_hash: Hash, + extra }`. `from.kind()` MUST be `Human`, `Org`, or `Agent`, never + `Service`; `to` MUST be registered (100 B-2). `agent.delegated` MUST be + signed by `from`'s active key at the entry `hlc`; a revocation by + `from`, by any principal above it in the chain, or by `to`'s operator; + `agent.action_denied` by the authorizing service. `revocation_id` is the + authority block's id (100 B-6) of the token this fact mirrors, so a + `delegation_revoked` also revokes that token. +- **B-2 (`DelegationView`).** A `DerivedState` fold: `links_to(agent) -> + Vec<Link>` sorted by `at` descending, `link(id) -> Option<Link>`, + `is_revoked(id) -> bool`, `revoked_ids() -> BTreeSet<Hash>` (union of + every revoked delegation's `revocation_id`), and `defects()`. `Link { + id: DelegationId, from, to, policy, scope: Vec<Caveat>, expires, at: + Hlc, entry: EntryHash }`. A malformed or mis-signed fact is a defect, + never applied (060 B-6). +- **B-3 (chain).** `delegation_chain(view, agents: &AgentView, agent, + at: &Hlc) -> Result<Chain, ChainError>` picks, for `agent`, the newest + link valid at `at` (not revoked, not expired, issued at or before + `at`) and, while `from` is an `Agent`, repeats for it; `MAX_DEPTH = 8`. + `Chain { links: Vec<Link> (leaf first), root: Principal }`; `root` is + the last link's `from`. `ChainError` is `NoDelegation(AgentId) | + Revoked(DelegationId) | Expired(DelegationId) | TooDeep(u8) | + NoHumanRoot | ScopeWidened { link: DelegationId } | NotRegistered(AgentId)`. + Monotonicity: every caveat of a parent link MUST appear, structurally + equal, in its child link's `scope`, else `ScopeWidened`. `Chain::hash() + = Hash::of(canonical bytes of the Vec<DelegationId> leaf first)`, the + value 102's claim carries. Chain resolution is pure and deterministic. +- **B-4 (`authorize`).** `pub fn authorize(req: &ActionRequest, cred: + &AgentCredential, ctx: &AuthzContext<'_>) -> Decision` with + `ActionRequest { operation: Operation, namespace: Hash, paths: + Vec<String>, predicate: Option<PredicateType>, revision_bytes: + Option<u64>, fact_kind: Option<String> }` (sorted, deduplicated paths), + `AuthzContext { now: Hlc, identities: &IdentityView, agents: + &AgentView, delegations: &DelegationView, active_policy: Cid, sandbox: + &dyn SandboxCheck, service: Principal }` where `trait SandboxCheck { fn + check(&self, registration: &Registration) -> Result<(), String>; }` + (102 supplies the real one; `AcceptAll` ships here for tests), and + `Decision::{Allow(Grant { agent, chain: Chain, chain_hash: Hash, root: + Principal, operation, namespace }), Deny(Denial { agent: + Option<AgentId>, reason: DenyReason, request_hash: Hash })}` with + `request_hash = Hash::of(canonical ActionRequest)`. Checks run in this + order and the first failure is the reason: + 1. `resolve_agent` (100 B-7): `NotAgentToken`, `IssuerKeyUnknown`, + `IssuerKeyInvalid(KeyValidity)`, `TokenInvalid`, `NotRegistered`, + `Deregistered`, `OperatorMismatch`; + 2. `ctx.sandbox.check(registration)`: `SandboxUndeclared(String)`; + 3. the leaf link: a link `to = agent` whose `from` is the identity of + `cred.issuer_key`, valid at `now`: `NoDelegation`; + 4. `delegation_chain`: `DelegationRevoked`, `DelegationExpired`, + `ChainTooDeep`, `NoHumanRoot`, `ScopeWidened`; + 5. `authority.policy` versus `ctx.active_policy` and every link's + `policy`: `PolicyMismatch { token: Cid, active: Cid }`; + 6. `req.namespace` in the authority's namespaces: `NamespaceNotGranted`; + 7. the token's revocation ids against `agents.revoked(agent)` and + `delegations.revoked_ids()`: `TokenRevoked(Hash)`; + 8. `token.check` with the B-5 (100) facts built from `req` and `now`: + `Expired`, `NotYetValid`, `CaveatFailed(String)`, + `CapabilityMissing(Operation)`. + `DenyReason` is that closed enum plus `HumanSessionPresented` (raised by + 102 when a human session reaches an agent path) with `wire_name() -> + &'static str` in kebab-case (`policy-mismatch`, `chain-too-deep`). +- **B-5 (journal).** `Denial::to_fact(&self, req: &ActionRequest) -> + DomainFact` builds the `agent.action_denied` fact with `reason = + wire_name()` and `detail` the human text. Every caller of `authorize` + that holds a ledger (the server, 102) MUST append it for every `Deny`; + `authorize` itself appends nothing and reads no clock. +- **B-6 (allow is narrow).** A `Grant` names exactly one operation and + one namespace; a request spanning namespaces is two requests. The grant + carries the chain so 094 can tag the write and 102 can check provenance + against `chain_hash` without a second resolution. + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/delegation/`: `chains.json` with + seeds, fixture `Hlc`s, the fact bodies for a three-link chain (human + to agent A to agent B to agent C), and the expected `DelegationId`s + and `Chain::hash` (frozen, constitution VIII); `deny-table.json` + listing every `DenyReason` wire name. +- **FR-002.** Tests cover: a depth-8 chain resolves and depth 9 is + `ChainTooDeep`; revoking the middle link denies the leaf and leaves the + middle agent's own chain intact; an expired link; a link whose `scope` + drops a parent caveat is `ScopeWidened`; a chain rooted in a `Service` + is `NoHumanRoot`; `PolicyMismatch` when 068's active pin moves; a + namespace outside the grant; a revoked delegation also revokes its + token; each ordered check in B-4 is reached by a fixture that passes + every earlier one; every `DenyReason` produces a journal fact whose + `reason` equals its wire name; permutation of fact arrival yields the + same view; the allow path returns the frozen `chain_hash`. +- **FR-003.** `authorize` is a pure function of its arguments; a test + calls it twice on the same inputs and asserts equal `Decision`s. +- **FR-004.** `register_agent_facts` (100 FR-003) registers B-1's three + kinds as well; the fact-kinds vector gains them. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-agent --locked delegation` passes, + fixtures included. +- **AC-2.** For the fixture chain, `authorize` of an `Append` under agent + C's token returns `Allow` whose `root` is the fixture human and whose + `chain_hash` equals `chains.json`; after `agent.delegation_revoked` on + A's link it returns `Deny(DelegationRevoked)`. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The sandbox check implementation and the agent-action claim (102); the +server's credential extraction, route classes, and the actual journaling +append (102); quarantine tagging of agent writes (094); ownership-based +delegation of review duty (104, a different chain). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-agent --locked delegation +``` diff --git a/specs/102-agent-sandbox-and-provenance/spec.md b/specs/102-agent-sandbox-and-provenance/spec.md new file mode 100644 index 0000000..aac1549 --- /dev/null +++ b/specs/102-agent-sandbox-and-provenance/spec.md @@ -0,0 +1,202 @@ +--- +id: "102-agent-sandbox-and-provenance" +title: "Agent sandbox and provenance: declared sandboxes, the agent-action attestation, and the server seam" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 7 +depends_on: + - "101-delegation-chain" + - "074-execution-provenance" + - "090-server-skeleton" +establishes: + - "crates/hqgit-agent/src/sandbox.rs" + - "crates/hqgit-agent/src/provenance.rs" + - "crates/hqgit-agent/tests/provenance.rs" + - "crates/hqgit-server/src/agent_auth.rs" + - "crates/hqgit-server/tests/agent_auth.rs" +extends: + - { spec: "100-agent-principals", unit: "crates/hqgit-agent/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/app.rs", nature: additive } + # hqgit-agent joins the server's manifest. + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } + # The agent-action claim schema fills the slot 027 reserved. + - { spec: "027-attestation-primitive", unit: "crates/hqgit-domain/src/predicate.rs", nature: additive } +summary: > + Constitution XII's last two clauses: an agent's sandbox is declared and + every artifact it produces carries provenance. This spec types the + sandbox an agent registration names (always the Untrusted tier of 073 + for execution, with declared tools and network), fills the + hqgit/agent-action/v1 claim slot 027 reserved with a schema binding the + agent, its delegation chain hash (101), its sandbox, its inputs, a + summary of its tool calls, and the execution provenance (074) of + anything it ran, signed by the agent's own key; fixes the rule that an + agent-submitted revision without such an attestation is refused; and + builds the server seam that accepts Biscuit credentials, refuses a + human session on an agent route and the reverse, journals every denial + (101), and tags every agent write so quarantine (094) applies. +--- + +# 102: Agent sandbox and provenance + +## 1. Purpose + +Design §1.1 point 7 asks for a declared sandbox, mandatory provenance on +every artifact, and an explicit delegation chain; 100 and 101 delivered +the credential and the chain, and this spec attaches the other two to +the artifacts themselves. Thesis §4.4 fixes that agent execution gets the +microVM tier; 073 B-2 encodes that rule on the Rust side. Here the +registration cannot claim otherwise, the claim an agent signs over its +work names the chain and sandbox the ledger can recompute, and the server +is the one place a bearer credential is turned into a principal. + +## 2. Territory + +In `crates/hqgit-agent`: `sandbox.rs` (`DeclaredSandbox`, validation, the +101 `SandboxCheck` implementation), `provenance.rs` (the claim, issuing, +claim-level verification, the mandatory-provenance rule), and +`tests/provenance.rs`. In `crates/hqgit-server`: `agent_auth.rs` (the +credential layer, route classes, write tagging, journaling) and +`tests/agent_auth.rs`. Additively: both crates' `lib.rs`, the server's +`app.rs` (the layer is installed) and manifest, and the claim validator +in 027's registry. The API routes themselves are 093's; quarantine is +094's. + +## 3. Behavior + +- **B-1 (declared sandbox).** `DeclaredSandbox { tier: DeclaredTier, + runtime: String, tools: BTreeSet<String>, network: NetworkAllowance, + rootfs: Option<Hash>, extra }` with `DeclaredTier` a closed enum whose + only variant is `Untrusted` (wire string `untrusted`, equal to 073's + spelling; the crate does not depend on `hqgit-eval`, and the server + maps the wire string to 073's `SandboxTier` at the seam) and + `NetworkAllowance::{None, Hosts(BTreeSet<String>)}`. `from_value` and + `to_value` type the opaque `sandbox` of 100 B-1. `validate(reg: + &Registration) -> Result<DeclaredSandbox, SandboxError::{Malformed(String), + TierNotUntrusted(String), EmptyRuntime}>`; `RegisteredSandbox` + implements 101's `SandboxCheck` by calling it, so a registration that + names any other tier denies every action with `SandboxUndeclared`. +- **B-2 (claim schema).** `PredicateType("hqgit/agent-action/v1")` gains + the validator for `AgentActionClaim { agent: AgentId, chain_hash: Hash, + root: Principal, policy: Cid, sandbox: DeclaredSandbox, inputs: + Vec<InputRef { kind: InputKind::{Tree, Revision, Attestation, Object, + Prompt}, hash: Hash }>, tool_calls: ToolCallSummary { count: u32, + by_tool: BTreeMap<String, u32>, transcript: Option<Cid>, + transcript_hash: Hash }, executions: Vec<AttestationId>, runtime: + String, model: Option<String>, extra }`. `inputs` and `executions` MUST + be sorted and deduplicated; `executions` name 074 provenance + attestations for every action the agent ran; `transcript` is an + erasable object (constitution X) and `transcript_hash` is its cid hash, + so erasure leaves the commitment; `by_tool` values MUST sum to `count`. + The attestation's `subject` is the revision id (or other subject hash) + the agent submits and its `issuer` MUST be `Principal::Agent(agent)`. +- **B-3 (issue).** `issue_agent_action(signer: &impl Signer, input: + &AgentActionInput, at: Hlc) -> Result<(Attestation, Value), Error>` + builds the claim, validates it through the registry, and signs through + 027 B-3 with `issuer = Principal::Agent(input.agent)`; the caller stores + the claim object and appends `attestation.issued`. `issuer_key` MUST be + the agent identity's active key (060) at `at`. +- **B-4 (verify).** `verify_agent_action(att: &Attestation, claim: &Value, + ctx: &ProvenanceContext { chain: &Chain, registration: &Registration, + active_policy: &Cid }) -> Result<(), ProvenanceError>` runs after 064's + signature and identity chain: `NotAgentIssuer`, `AgentMismatch` + (claim agent, issuer, and registration disagree), `ChainHashMismatch { + claimed, computed }` (`ctx.chain.hash()` recomputed by 101 at `att.at`), + `SandboxMismatch` (claim sandbox differs from the registration's + validated one), `PolicyMismatch`, `ClaimInvalid(String)`, in that order. +- **B-5 (mandatory provenance).** `require_provenance(subject: &Hash, + submitted_by: &Principal, attestations: &[Attestation]) -> Result<(), + MissingProvenance>`: when `submitted_by.kind() == Agent`, an + `hqgit/agent-action/v1` attestation over `subject` issued by that agent + MUST be present. It applies to `change.revision_submitted` (subject the + revision id) and to every attestation an agent issues under another + predicate (subject the same). A human or service submitter passes + without one. +- **B-6 (server credential layer).** `AgentAuthLayer { state: + AgentAuthState { identities, agents, delegations, policy: Box<dyn + ActivePolicy>, journal: Box<dyn LedgerAppend>, service: Principal, clock: + HlcGenerator } }` is a tower layer installed in `app.rs`. `Authorization: + Bearer hqa1.<...>` is an `AgentCredential`; any other bearer or a 061 + session cookie is a human credential; both present is `400 + mixed-credentials`. `trait RouteClassifier { fn classify(&self, method: + &Method, path: &str) -> RouteClass::{Human, Agent, Either}; }` with + `PrefixClassifier` (`/api/agent/` Agent, `/login` and `/session` Human, + everything else Either) as the default 093 replaces with its route + table. A human credential on an `Agent` route is `403 + human-session-on-agent-route` and is journaled with reason + `human-session-presented`; an agent credential on a `Human` route is + `403 agent-on-human-route`. +- **B-7 (authorize and tag).** For an agent credential the layer builds + the 101 `ActionRequest` (operation from the method and route, namespace + from the repository, paths and sizes from the body when the handler + declares them through `RequestFacts`), calls `authorize`, appends + `Denial::to_fact` through `journal` on every `Deny` (101 B-5), and + answers `403 { "error": "agent-denied", "reason": <wire name>, + "request_hash": <hex> }`. On `Allow` it inserts `WritePrincipal::Agent + { agent, chain_hash, root }` into the request extensions; every write + handler MUST read `WritePrincipal` and pass it to 094's quarantine + path, and a handler that appends without one is a defect the fixture + handler in FR-003 asserts against. +- **B-8 (provenance at the write seam).** For an agent write carrying a + revision or an attestation, the layer's `check_agent_write` runs B-5 + and B-4 on the attestations in the same request: absence is `422 + agent-provenance-required`; a claim failing B-4 is `403 + agent-provenance-invalid` with the `ProvenanceError` name; both are + journaled. The write never reaches the ledger. +- **B-9 (no ambient input in the crate).** `hqgit-agent` reads no clock; + the server supplies `now` from its 018 generator. Rate limits and size + caps are 094's and are not duplicated here. + +## 4. Functional requirements + +- **FR-001.** Agent-crate tests cover: the validator accepts the fixture + claim and rejects one missing `chain_hash`, one with unsorted `inputs`, + and one whose `by_tool` does not sum; `validate` refuses a `trusted` + tier and an empty runtime; issue then `verify_agent_action` on a 101 + fixture chain passes; each `ProvenanceError` in B-4 order; `require_provenance` + fails for an agent without the attestation, passes with it, and passes + for a human; erasing the transcript object leaves verification intact. +- **FR-002.** Server tests boot the 090 skeleton on an ephemeral port + with a fixture router of three routes (one per `RouteClass`) and a fake + `LedgerAppend`: an agent credential on the `Agent` route reaches the + handler with `WritePrincipal::Agent`; the same on the `Human` route is + 403; a human session on the `Agent` route is 403 and journaled; a + denied agent yields 403 with the wire reason and one journaled fact; + mixed credentials 400; a revision write without provenance 422; one + with a wrong chain hash 403 `agent-provenance-invalid`. +- **FR-003.** The fixture write handler panics if `WritePrincipal` is + absent, so the layer's tagging is exercised, not assumed. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-agent --locked provenance` and `cargo + test -p hqgit-server --locked agent_auth` pass. +- **AC-2.** Against the ephemeral server, a scripted agent with the 101 + fixture chain submits a revision with a valid agent-action attestation + and receives 200 with the write tagged; the same submission without the + attestation receives 422 and appends nothing. + +## 6. Out of scope + +Quarantine placement, rate limits, and promotion of agent writes (094); +the API routes and their classifier table (093); the evidence bundle an +agent-authored change carries (103); running agents (the runtime is an +external client of the API); executing agent-requested actions (072, +073; the tier rule is theirs). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-agent --locked provenance +cargo test -p hqgit-server --locked agent_auth +``` diff --git a/specs/103-evidence-carrying-changes/spec.md b/specs/103-evidence-carrying-changes/spec.md new file mode 100644 index 0000000..ad28c88 --- /dev/null +++ b/specs/103-evidence-carrying-changes/spec.md @@ -0,0 +1,217 @@ +--- +id: "103-evidence-carrying-changes" +title: "Evidence-carrying changes: the bundle, the argument view, and the evidence_required policy" +status: approved +kind: "feature" +domain: "l6-policy" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 7 +depends_on: + - "102-agent-sandbox-and-provenance" + - "067-policy-evaluation-attestation" + - "051-semantic-deltas" +establishes: + - "crates/hqgit-domain/src/evidence.rs" + - "crates/hqgit-domain/tests/evidence.rs" + - "crates/hqgit-domain/testdata/evidence/" + - "crates/hqgit-policy-sdk/examples/evidence_required.rs" + - "crates/hqgit-cli/src/cmd_evidence.rs" + - "crates/hqgit-cli/tests/evidence.rs" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + # evidence.attached joins the vocabulary. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } + # property-test, invariant, and spec-conformance claim schemas register. + - { spec: "027-attestation-primitive", unit: "crates/hqgit-domain/src/predicate.rs", nature: additive } + # Query helpers the example policy uses. + - { spec: "066-policy-sdk", unit: "crates/hqgit-policy-sdk/src/evidence.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + # hqgit-trust and hqgit-agent join the CLI's manifest for verification. + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } +summary: > + Design §1.1 point 7: as authoring cost approaches zero, trusted review + capacity is the binding constraint, so a change should arrive carrying + machine-checkable evidence and a human should review the argument + rather than the diff. This spec names that argument: an EvidenceBundle + is the set of attestations a revision carries (provenance, test and + property-test results, invariant checks, semantic deltas, policy + evaluations, agent actions, approvals, spec conformance), declared by + an evidence.attached fact, each item classified by whether a machine or + a human must check it and carrying its verification verdict. The CLI + renders the argument with a throughput line (how much verified itself, + how much needs a person), completeness is computed against a required + predicate set, and the SDK example policy evidence_required denies a + revision lacking one. Nothing here is a new noun: every item is an + attestation (constitution IX). +--- + +# 103: Evidence-carrying changes + +## 1. Purpose + +Thesis §8 says the design optimizes verification throughput, and +constitution XII repeats it for agents. Throughput is a ratio a reviewer +can only act on if it is computed: of the claims this revision makes, how +many did the system already verify, how many failed, and how many wait on +a human. Specs 027 through 102 produced the claims; this spec assembles +them into one object with one honest count, puts the assembly on the +ledger as a fact so two reviewers see the same argument, and gives policy +a way to demand it before merge. + +## 2. Territory + +`evidence.rs` in `hqgit-domain` (the fact, `EvidenceBundle`, +checkability, throughput, completeness, the argument ordering), +`tests/evidence.rs` and fixtures under `testdata/evidence/`; the SDK +example `evidence_required.rs`; and `cmd_evidence.rs` with `tests/evidence.rs` +in the CLI. Additively: 023's `lib.rs` and `facts.rs`, three claim +schemas in 027's registry, helpers in 066's `evidence.rs`, and the CLI +frame and manifest. Verification itself stays in 064, 067, and 102; this +spec composes their verdicts. + +## 3. Behavior + +- **B-1 (predicates).** Three claim schemas register: + `hqgit/property-test/v1 { suite: String, properties: Vec<{ name: + String, cases: u32, passed: bool, counterexample: Option<String> }>, + seed: Option<u64>, log: Option<Cid> }`; `hqgit/invariant/v1 { checker: + String, invariants: Vec<{ name: String, holds: bool, detail: + Option<String> }>, log: Option<Cid> }`; `hqgit/spec-conformance/v1 { + specs: Vec<{ id: String, acceptance: Vec<String>, evidence: + Vec<AttestationId> }> }` where `id` MUST match `^[0-9]{3}-[a-z0-9-]+$` + and `acceptance` entries `^AC-[0-9]+$`. Lists are sorted by name or id. +- **B-2 (fact).** Frozen kind `evidence.attached { change: ChangeId, + revision: RevisionId, attestations: Vec<AttestationId>, attached_by: + Principal, extra }`: `attestations` sorted, deduplicated, non-empty, at + most 256; every named attestation's `subject` MUST equal `revision` + (validated by the CLI before append and by the bundle on assembly, where + a mismatch is listed under `rejected`). Several facts for one revision + merge by set union (constitution VII); there is no detach: a withdrawn + claim stays visible with its verdict. +- **B-3 (checkability).** `Checkability::{Machine, Human}` by predicate: + `provenance`, `test-result`, `property-test`, `invariant`, + `semantic-delta`, `policy-eval`, `agent-action`, `static-finding`, + `license`, `mirror`, and `code-index` are `Machine` (their verification + is 064's chain plus, where one exists, a recomputation: 074 + `gating_verified`, 051's deterministic delta, 067 `replay`, 102 + `verify_agent_action`); `approval` and `spec-conformance` are `Human`; + an unregistered predicate is `Human`. `checkability(&PredicateType) -> + Checkability` is the one table. +- **B-4 (bundle).** `EvidenceBundle::assemble(revision: RevisionId, + submitted_by: Principal, attached: &[EvidenceAttached], attestations: + &BTreeMap<AttestationId, (Attestation, Value)>, registry: + &PredicateRegistry) -> EvidenceBundle { revision, submitted_by, items: + Vec<EvidenceItem { id, predicate, issuer, issuer_kind, checkability, + claim_verdict: ClaimVerdict, verification: Verification, summary: + String }>, rejected: Vec<(AttestationId, String)>, unavailable: + Vec<AttestationId> }` is pure: `Verification` starts `Unverified` for + every item; the composing binary calls `with_verdicts(&mut self, + verdicts: &BTreeMap<AttestationId, Verification::{Ok, Failed(String)}>)` + with 064's results and recomputation outcomes (081 B-3's pattern). + `summary` is a fixed rendering per predicate (`"12 properties, 12 + passed"`, `"3 public items removed, 1 signature changed"`, `"chain + root human:<hex8>"`), never claim text verbatim. +- **B-5 (throughput and completeness).** `Throughput { total: u32, + machine_verified: u32, machine_failed: u32, needs_human: u32, + unverified: u32 }` where `machine_verified` counts `Machine` items with + `Ok`, `machine_failed` counts any `Failed`, `needs_human` counts + `Human` items, and `unverified` the rest; the five sum to `total`. + `Requirement { predicates: BTreeSet<PredicateType>, min_approvals: + u32, agent_action_if_agent: bool }` with `Requirement::default_for( + submitted_by)` = `{ provenance, semantic-delta, one of test-result or + property-test }`, `min_approvals = 1`, and `agent_action_if_agent = + true`. `completeness(&self, req) -> Completeness { satisfied: + Vec<PredicateType>, missing: Vec<PredicateType>, approvals: u32, + failed: Vec<AttestationId> }`; `complete()` is `missing` empty, enough + approvals, and `failed` empty. Integers only. +- **B-6 (the argument).** `argument(&self) -> Vec<&EvidenceItem>` orders + items by predicate rank (`provenance`, `agent-action`, + `semantic-delta`, `property-test`, `invariant`, `test-result`, + `policy-eval`, `static-finding`, `license`, `spec-conformance`, + `approval`, then others alphabetically), then issuer, then id, so the + deltas precede the line diff (design §1.1 point 3) and human claims + come last. +- **B-7 (SDK).** 066's `evidence.rs` gains `predicates_present(&PolicyInput) + -> BTreeSet<PredicateType>`, `missing(&PolicyInput, required: &[&str]) + -> Vec<String>`, and `submitted_by_agent(&PolicyInput) -> bool`. The + example `evidence_required` requires `hqgit/provenance/v1`, + `hqgit/semantic-delta/v1`, one of `hqgit/test-result/v1` or + `hqgit/property-test/v1`, and `hqgit/agent-action/v1` when the revision + was submitted by an agent, returning `Deny` with one reason per missing + predicate (`missing evidence: <predicate>`) and `Allow` otherwise. It + builds natively and to wasm32 per 066. +- **B-8 (CLI).** `hq evidence attach <change> [--revision N] + <attestation-id>...` checks every id resolves and its subject is the + revision (`Error::Validation` naming the id), then appends B-2 signed by + the local identity and prints the fact's entry hash. `hq evidence show + <change> [--revision N] [--require <pred>[,<pred>...]] [--json]` + assembles the bundle from the ledger and object store, verifies every + item through 064 with the `RotationAwareResolver` (060 B-7), recomputes + the agent-action chain hash through 101 when the submitter is an agent, + applies `with_verdicts`, and renders the argument one line per item + (`<status> <predicate> <issuer-kind>:<hex8> <summary>`) followed by + `evidence: <total> attached; machine-verified <n>, failed <n>, needs + human <n>, unverified <n>; missing: <list or none>`. `--require` + replaces the default requirement. Exit `0` when `complete()`, else + `Error::Policy` (exit `1`); `--json` per 032 B-3. +- **B-9 (discipline).** `evidence.rs` reads no clock and performs no + I/O; claim objects are passed in. No `HashMap`, no float. + +## 4. Functional requirements + +- **FR-001.** Fixtures under `testdata/evidence/`: a revision with eight + attestations across the B-3 table (one agent-action with a fixture + chain hash, one approval, one spec-conformance), two `evidence.attached` + facts overlapping in ids, one attestation over another subject, and + `expected-bundle.json`, `expected-argument.json`, `expected-throughput.json`. +- **FR-002.** Domain tests cover: union and deduplication across the two + facts; the foreign-subject attestation lands in `rejected`; a missing + claim object lands in `unavailable`; throughput sums to `total` before + and after `with_verdicts`; `default_for` differs for an agent submitter + and `completeness` reports `agent-action` missing; argument order + equals the fixture; each B-1 validator accepts its fixture and rejects + a malformed one (bad spec id, unsorted list, `passed` absent). +- **FR-003.** SDK tests run `evidence_required` natively and as wasm + against fixtures: complete human revision allows; agent revision without + agent-action denies with exactly that reason; one missing predicate per + reason line. +- **FR-004.** CLI tests with `assert_cmd` on the 033 fixture: `attach` + refuses a foreign-subject id; `attach` then `show` lists the items and + exits 1 with `missing:` naming `hqgit/provenance/v1`; `show --require + hqgit/approval/v1` exits 0 after `hq review approve`; `--json` output is + byte-identical across two runs. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked evidence`, `cargo test -p + hqgit-policy-sdk --locked evidence_required`, and `cargo test -p + hqgit-cli --locked evidence` pass. +- **AC-2.** On the fixture, `hq evidence show --json` reports `{ + "machine_verified": 5, "machine_failed": 0, "needs_human": 2, + "unverified": 0, "total": 7 }` after every machine item verifies. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Producing the evidence (051, 067, 074, 102, and the test runners that +issue test-result and property-test attestations from 075 targets, a +later feature spec); rendering the argument in the review UI (095 +consumes the bundle through 093); pinning `evidence_required` as a +repository policy (068's fact, an operator act); ownership-based +requirements (104 with 065). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked evidence +cargo test -p hqgit-cli --locked evidence +``` diff --git a/specs/104-ownership-and-sla/spec.md b/specs/104-ownership-and-sla/spec.md new file mode 100644 index 0000000..2aeaec1 --- /dev/null +++ b/specs/104-ownership-and-sla/spec.md @@ -0,0 +1,215 @@ +--- +id: "104-ownership-and-sla" +title: "Ownership with delegation, expiry, and SLA: facts that replace CODEOWNERS" +status: approved +kind: "feature" +domain: "l2-domain" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: medium +wave: 7 +depends_on: + - "028-issues-and-derived-state" + - "060-identity-and-key-rotation" + - "025-semantic-anchors" +establishes: + - "crates/hqgit-domain/src/ownership.rs" + - "crates/hqgit-domain/tests/ownership.rs" + - "crates/hqgit-domain/testdata/ownership/" +extends: + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/lib.rs", nature: additive } + # The reserved ownership.declared and ownership.revoked bodies get their shape here. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + Ownership today is a text file with no SLA, delegation, or expiry + (design §1.1 point 8). Here it is a set of facts: ownership.declared + names a scope (a path pattern, a semantic anchor, or a crate), an + owner, an optional delegator, an optional expiry, and an optional + review SLA; ownership.revoked ends a declaration. OwnershipView folds + them and answers owners_of(path, at) with expiry honored, revocations + applied, delegation chains resolved to an accountable Human or Org + root, and the most specific scope winning by an integer weight. Agents + and services may hold delegated ownership but never a root, which is + the other half of the accountability chain wave 7 builds. A CODEOWNERS + import is a helper that emits facts; feeds (085) and policy (065) read + the view, never the file. +--- + +# 104: Ownership and SLA + +## 1. Purpose + +Constitution XII makes an agent a distinct principal whose credential +carries its delegation chain; ownership is where that chain meets the +tree. Who must review a change to `crates/hqgit-trust/` is today a line +in CODEOWNERS with no notion of "until when", "on whose authority", or +"how fast". This spec makes ownership a fold over facts (constitution +VII) so that a delegation to an agent expires, a revoked human takes +every delegate down with them, and an SLA is data a feed can rank on. +Thesis §4.5 fixes identity as a keypair with a chain (060); this spec +consumes that identity model without importing the trust crate +(constitution XIII: `hqgit-domain` sits below `hqgit-trust`). + +## 2. Territory + +`ownership.rs` in `crates/hqgit-domain` (scopes and patterns, the +declaration and view, delegation and SLA resolution, the builders, the +CODEOWNERS import), `tests/ownership.rs`, and fixtures under +`testdata/ownership/`. Additively: `lib.rs` re-exports and the typed +bodies of `ownership.declared` and `ownership.revoked` in `facts.rs`, +replacing the opaque `Value` 023 reserved. + +## 3. Behavior + +- **B-1 (facts).** `ownership.declared { scope: OwnershipScope, owner: + Principal, delegated_by: Option<Principal>, expires: Option<Hlc>, sla: + Option<Sla>, nonce: Nonce, extra }` mints `DeclarationId = + Hash::of(canonical body)` (023 B-3, a new newtype in `ownership.rs`). + `ownership.revoked { declaration: DeclarationId, reason: String, extra + }`. `Sla { review_within_hours: u32, extra }`. Validation (023 B-4 + extended): a `Path` pattern non-empty, free of NUL and `[`, and at most + 1024 bytes; a `Crate` name matching `^[A-Za-z0-9_-]+$`; + `review_within_hours` in `1..=8760`; `delegated_by != owner`. +- **B-2 (scopes and patterns).** `OwnershipScope` is `Path(PathPattern) | + Anchor(Anchor) | Crate(String)`. `PathPattern(String)` uses CODEOWNERS + syntax over repo-relative POSIX paths: `/` splits segments, `**` matches + zero or more segments, `*` any run within a segment, `?` one character; + a pattern with no `/` matches in any directory (`*.rs` is `**/*.rs`); a + trailing `/` means the directory and everything under it; a leading `/` + anchors at the root and is stripped. `matches(&self, path: &str) -> + bool` is a hand-written matcher (no regex crate). `Crate(name)` resolves + through a caller-supplied `CrateIndex(BTreeMap<String, String>)` (crate + name to directory) to the pattern `<dir>/**`; the index is content + derived from a tree, so the caller builds it (033 or 090 through 051's + manifest parser) and the view never reads a tree. +- **B-3 (specificity weight).** `weight(scope, crates) -> Option<u32>`: + `Path`: `1_000 * literal_segments + min(literal_bytes, 999)` where a + literal segment contains no wildcard; `Crate`: the weight of its + resolved pattern minus 1 (an explicit path declaration beats the crate + declaration for the same directory), `None` when unresolved; `Anchor`: + `10_000_000 + node_path.len()`. Integer arithmetic only. +- **B-4 (`OwnershipView`).** Implements `DerivedState` (019 B-5): + `declarations: BTreeMap<DeclarationId, Declaration>`, `by_owner: + BTreeMap<Principal, BTreeSet<DeclarationId>>`, `warnings: + Vec<OwnershipWarning>`. `Declaration { id, scope, owner, delegated_by, + declared_at: Hlc, declared_by_key: KeyId, expires, sla, revoked: + Option<(Hlc, String)> }`; `declared_by_key` is the fact's issuer (019 + B-1), recorded so policy and the UI can show who declared, while the + authority of that key over `owner` or `delegated_by` remains the trust + plane's check (023 B-6). A revocation of an unknown declaration is + `OwnershipWarning::RevokeUnknown`; a second revocation is ignored and + the first in total order stands; `expires <= declared_at` is + `OwnershipWarning::ExpiresBeforeDeclared` and the declaration is never + active. The fold rejects nothing. +- **B-5 (activity and delegation).** A declaration is active at `at` + iff `declared_at <= at`, `at < expires` when set, `at < revoked.0` when + revoked, and its chain is valid at `at`. A root declaration + (`delegated_by: None`) MUST have a `Human` or `Org` owner (010 B-6); + otherwise `OwnershipWarning::RootNotAccountable` and never active. A + delegated declaration is valid iff its delegator held, at + `declared_at`, an active declaration whose scope covers the delegated + scope (`covers(outer, inner)`: identical, or `outer` ends in `**` and + `inner`'s literal prefix matches it, or `inner` is an `Anchor` whose + `path` `outer` matches; conservative, `false` when unsure); that + declaration is the parent link, chosen by greatest weight then lowest + id among candidates; the chain is re-checked at every query `at`, so + revoking or expiring a parent deactivates the subtree from that `Hlc`. + Depth is capped at `MAX_DELEGATION_DEPTH = 8` + (`OwnershipWarning::DelegationTooDeep`). `Agent` and `Service` owners + may only appear as delegates. +- **B-6 (`PrincipalStatus` seam).** `trait PrincipalStatus { fn + active_at(&self, principal: &Principal, at: &Hlc) -> bool; }` with + `AlwaysActive` provided. The trust plane's `IdentityView` (060 B-5) + satisfies it through an adapter that lives with the wiring in the CLI + (034) and the server (090), so a frozen or unknown identity is skipped + without inverting the crate direction. An inactive principal anywhere + in a chain deactivates the chain at `at`. +- **B-7 (queries).** `owners_of(&self, path: &str, at: &Hlc, crates: + &CrateIndex, status: &dyn PrincipalStatus) -> Vec<Owner>` returns every + active matching declaration as `Owner { principal, declaration: + DeclarationId, root: Principal, chain: Vec<DeclarationId>, weight: u32, + expires: Option<Hlc>, sla: Option<Sla> }` sorted by `(weight desc, + principal)`; `expires` is the minimum over the chain and `sla` is the + owner's own or the nearest ancestor's. `primary_owners` keeps the + maximum weight only (most specific scope wins). `owners_of_anchor(anchor, + ..)` ranks `Anchor` scopes whose `path` equals and whose `node_path` is + a prefix of the anchor's above every path scope, else falls back to + `owners_of(anchor.path)`. `explain(path, at, crates, status) -> + Vec<(DeclarationId, Disposition)>` lists every declaration with + `Disposition` `Applied(u32) | NoMatch | NotYet | Expired | Revoked | + DelegationInvalid(String) | CrateUnresolved | PrincipalInactive`, so no + answer is silent. `sla_deadline(owner: &Owner, requested_at: &Hlc) -> + Option<Hlc>` is `wall_ms + hours * 3_600_000` (saturating), `logical + 0`, the request's node. +- **B-8 (builders and write-path checks).** `declare(scope, owner, + delegated_by, expires, sla, nonce) -> (DeclarationId, DomainFact)` and + `revoke(declaration, reason) -> DomainFact` are pure. `check_declaration + (view, at, scope, owner, delegated_by, crates, status) -> Result<(), + Error>` runs B-5 ahead of appending so the CLI refuses an unaccountable + root, a delegation outside scope, or excess depth with + `Error::Validation`; the fold still accepts whatever arrives. +- **B-9 (CODEOWNERS import).** `import_codeowners(text: &str, resolve: + &dyn Fn(&str) -> Option<Principal>, nonces: &mut dyn FnMut() -> Nonce) + -> ImportReport { facts: Vec<DomainFact>, unresolved: Vec<(u32, String)> + }` parses comments, blank lines, one pattern per line followed by + handles (`@user`, `@org/team`, an email), emits one root declaration per + `(pattern, owner)` with no expiry and no SLA, and lists every handle + `resolve` returned `None` for by line, never guessing. Precedence + differs from GitHub's last-match rule (B-3 is most-specific); the + report's `precedence_note: String` says so verbatim. +- **B-10 (no ambient input).** No clock, environment, randomness, + `HashMap`, or float; the caller supplies `at`, `nonce`, and the crate + index (010 B-11, 023 B-7). + +## 4. Functional requirements + +- **FR-001.** Tests: `at == expires` is inactive and `at == expires - 1 + logical` is active; a delegation chain of depth 8 resolves and depth 9 + warns; `primary_owners` picks `crates/x/src/**` over `crates/**` over + `Crate("x")`; an `Anchor` scope beats every path scope for its node and + not for a sibling; delegation outside the delegator's scope is + `DelegationInvalid`; a root owned by an `Agent` is `RootNotAccountable`; + effective `expires` is the chain minimum and SLA inherits; revoking the + root deactivates delegates from the revocation `Hlc` and not before; + `PrincipalInactive` through a stub `PrincipalStatus`; `sla_deadline` + arithmetic and saturation; `explain` names a disposition for every + declaration; CODEOWNERS fixtures under `testdata/ownership/codeowners/` + produce golden facts and unresolved lines. +- **FR-002.** `PathPattern` matcher vectors in + `testdata/ownership/patterns.json` (pattern, path, expected) covering + each rule of B-2. +- **FR-003.** A property test (`proptest`) folds a random declaration + and revocation history through two total-order-preserving permutations + and asserts equal views and equal `owners_of` answers at random probes. +- **FR-004.** `lib.rs` re-exports `OwnershipView`, `OwnershipScope`, + `PathPattern`, `CrateIndex`, `Owner`, `Sla`, `PrincipalStatus`, + `AlwaysActive`, `Disposition`, the builders, and `import_codeowners`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-domain --locked ownership` passes, + fixtures and vectors included. +- **AC-2.** The golden ownership fixture in `tests/ownership.rs` (two + roots, one agent delegate, one expiry, one revocation) answers the + recorded `owners_of` table at four probe `Hlc`s from every tested + permutation. + +## 6. Out of scope + +Enforcing ownership at merge (a policy, 065 and 066); ranking owned +work in feeds (085); binding the declaring key to the owner identity +(064); agent registration and the token-side delegation chain (100, +101); mirroring GitHub CODEOWNERS on sync (040 may call B-9 later); +team membership as a noun (an `Org` is opaque here). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-domain --locked +``` diff --git a/specs/110-set-reconciliation/spec.md b/specs/110-set-reconciliation/spec.md new file mode 100644 index 0000000..92b06d7 --- /dev/null +++ b/specs/110-set-reconciliation/spec.md @@ -0,0 +1,204 @@ +--- +id: "110-set-reconciliation" +title: "Range-based set reconciliation over entry hashes: fingerprints, the split protocol, and ingest" +status: approved +kind: "kernel" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 8 +depends_on: + - "021-local-repository" +establishes: + - "crates/hqgit-sync/Cargo.toml" + - "crates/hqgit-sync/src/lib.rs" + - "crates/hqgit-sync/src/reconcile.rs" + - "crates/hqgit-sync/src/fingerprint.rs" + - "crates/hqgit-sync/src/protocol.rs" + - "crates/hqgit-sync/tests/" + - "crates/hqgit-sync/testdata/protocol/" +extends: + # The repository gains the verified ingest path foreign entries take. + - { spec: "021-local-repository", unit: "crates/hqgit-ledger/src/repo.rs", nature: additive } +summary: > + Thesis §4.2: replication is range-based set reconciliation, with the + Willow protocol as the prior art. Two replicas of one repository hold + sets of entry hashes ordered by (hlc, hash); each side fingerprints a + range, and where fingerprints differ the range is split until the + difference is small enough to exchange item lists. Because an entry's + hlc is strictly greater than its parents' (017), that same order is a + topological order, so missing entries append in order with no extra + sort. This spec founds hqgit-sync with the fingerprint, the pure + reconciler state machine, the canonical message vocabulary, a + transport-agnostic Channel seam, and the ingest path that verifies every + foreign entry before it touches the ledger. Payload objects are fetched + lazily and verified through spec 015. The transport is spec 111. +--- + +# 110: Set reconciliation + +## 1. Purpose + +Facts merge by set union (019 B-3), so replication is set reconciliation +and nothing more: neither side needs the other's history, only the +difference. Range fingerprinting converges in a number of rounds +logarithmic in the set size regardless of how the difference is +distributed, works over any transport, and needs no per-peer log +position, which is what makes federation (112) between hosts that have +never met tractable. Everything received is verified through the ledger's +own rules before it is stored, so a lying peer can withhold but never +inject. + +## 2. Territory + +`crates/hqgit-sync` as founded here: the manifest (workspace dependencies +`hqgit-types`, `hqgit-object`, `hqgit-ledger`; `proptest` as a dev +dependency), `lib.rs`, `fingerprint.rs`, `reconcile.rs`, `protocol.rs`, +the `tests/` subtree, and frozen message vectors under +`testdata/protocol/`. Additively: `Repository::ingest_entry` in spec +021's `repo.rs`. QUIC, peer identity, and the object source are spec 111; +peer registries and the sync verbs are spec 112. + +## 3. Behavior + +- **B-1 (key space).** An item is `Item { hlc: Hlc, hash: EntryHash }`, + ordered lexicographically (010 B-8's `Ord` on `Hlc`, then hash bytes). + `Bound` is the same pair used as a range endpoint; `Bound::MIN` is all + zero bytes and `Bound::MAX` all `0xFF`. A `Range { lo: Bound, hi: Bound + }` is half-open `[lo, hi)`; the whole space is `[MIN, MAX)`. serde + encodes a bound as the two-element array `[hlc, hash]`. +- **B-2 (fingerprint).** `Fingerprint { xor: [u8; 32], count: u64 }` + where `xor` is the bytewise XOR of every item's hash in the range and + `count` the item count; the empty range is all zero. `Fingerprint::of( + items)` and `combine(&self, other)` (XOR and add) make a range's + fingerprint the combination of its sub-ranges', which is what lets a + split be answered without re-hashing. The threat model is stated, not + hidden: a peer who controls its own item set can withhold entries + regardless of fingerprinting, so the fingerprint drives which ranges + are exchanged and never what is accepted (B-7 accepts only verified + entries). Withholding is defended by syncing with more than one peer + (112), not by the fingerprint. +- **B-3 (messages).** `protocol.rs` defines `Message`, each variant a + spec 011 `Envelope` with `kind = "sync.<variant>"`, `v = 1`, and an + `extra` map: `RangeFingerprint { id: u32, range, fp: Fingerprint }`; + `RangeItems { id: u32, range, items: Vec<Item>, want: bool }` (the + sender's items in the range; `want` asks for the receiver's items in + return); `Done { fp: Fingerprint }` (the sender's fingerprint of the + whole space, sent when it has no open ranges); `WantEntries { hashes: + Vec<EntryHash> }`; `Entries { entries: Vec<Vec<u8>> }` (canonical entry + bytes, 017 B-2); and `Abort { reason: String }`. Constants: + `ITEM_THRESHOLD = 32` (a range with at most this many local items is + answered with `RangeItems`, never split), `SPLIT = 4` (a larger range + splits into this many sub-ranges at local item-count quantiles), + `MAX_ITEMS = 4096` per `RangeItems`, `MAX_ENTRIES = 256` per `Entries`, + `MAX_ROUNDS = 64`. Ids are per-session sequence numbers. +- **B-4 (`EntryIndex`).** `trait EntryIndex { fn fingerprint(&self, range: + &Range) -> Fingerprint; fn items(&self, range: &Range, limit: usize) -> + Vec<Item>; fn split(&self, range: &Range, parts: u8) -> Vec<Range>; fn + has(&self, hash: &EntryHash) -> bool; }`. `MemoryIndex(BTreeSet<Item>)` + implements it and is what `LedgerIndex::load(store: &dyn EntryStore) + -> Result<MemoryIndex, Error>` builds from 021's `iter_all` and `get` + at session start (a persistent by-hlc index is a later optimization, + noted in 021's territory). +- **B-5 (reconciler).** `Reconciler::new(index: &dyn EntryIndex, role: + Role::{Initiator, Responder})`; `start(&mut self) -> Vec<Message>` (the + initiator sends `RangeFingerprint` for the whole space; the responder + sends nothing); `handle(&mut self, msg: &Message) -> Result<Step, + Error>` with `Step { send: Vec<Message>, missing: Vec<Item>, done: bool + }`. On `RangeFingerprint`: equal local fingerprint yields no reply for + that range; otherwise if local items in the range are at most + `ITEM_THRESHOLD` reply `RangeItems { want: true }`, else reply one + `RangeFingerprint` per sub-range of `split`. On `RangeItems`: every + received item not in the index is added to `missing`; if `want`, reply + `RangeItems { want: false }` with local items in the range. `done` + becomes true when both sides have sent `Done` and the two whole-space + fingerprints, each combined with the items that side is about to + receive, agree; disagreement is `Error::Validation("reconciliation + did not converge")`. A peer exceeding `MAX_ROUNDS`, sending an id it + never received, a range outside the one it was asked about, or a + `RangeItems` whose items do not reproduce the fingerprint it claimed + for that range earlier is `Error::Validation` naming the rule, and the + session aborts. The reconciler is a pure state machine: no I/O, no + clock. +- **B-6 (`Channel` and the runner).** `trait Channel { fn send(&mut self, + msg: &Message) -> Result<(), Error>; fn recv(&mut self) -> Result< + Option<Message>, Error>; }`. `run(reconciler, channel: &mut dyn + Channel) -> Result<Outcome, Error>` drives the exchange to `done`, then + requests missing entries in batches of `MAX_ENTRIES` with `WantEntries` + in `(hlc, hash)` order and ingests each `Entries` reply (B-7) before + requesting the next, returning `Outcome { rounds: u32, sent: u64, + received: u64, ingested: u32, missing_payloads: u32 }`. `MemoryChannel + ::pair()` gives two connected in-process channels for tests and for 111 + to wrap. +- **B-7 (ingest).** `Repository::ingest_entry(&mut self, bytes: &[u8], + resolver: &dyn IssuerResolver) -> Result<Ingested, Error>` (additive on + 021) decodes the entry (017 B-1, rejecting a malformed shape), checks + that the recomputed hash equals the hash advertised in the item and the + `hlc` equals the advertised one (`Error::Validation`), verifies the + signature through the resolver (`Error::Crypto`), requires every parent + to be present (`Error::NotFound` naming it; the runner requests parents + first because `(hlc, hash)` order lists them first), and appends + through 021's `EntryStore` inside one transaction. `Ingested::{Appended, + AlreadyPresent}`. It never puts a payload object: an entry whose + payload resolves `Missing` (020 B-1) is a legitimate replica state. +- **B-8 (payloads).** `fetch_payloads(repo, entries: &[EntryHash], source: + &dyn ObjectSource) -> Result<u32, Error>` fetches each entry's payload + through 015 `fetch_into`, skipping cids the `TombstoneSet` (020 B-5) + marks erased and cids already present, and returns the count still + missing. Verification is 015's: nothing unverified enters the store. +- **B-9 (no ambient input).** The crate reads no clock, no environment, + no randomness; `BTreeMap` and `BTreeSet` are the only collections on + the ordered paths. + +## 4. Functional requirements + +- **FR-001.** `testdata/protocol/` holds one vector per message variant + (`{ description, input, canonical_hex, hash }`, spec 011's layout) and + a recorded two-party transcript; a test replays the transcript through + two reconcilers and asserts every emitted message byte for byte. These + vectors are frozen (constitution VIII). +- **FR-002.** Tests cover: fingerprint combination equals whole-range + computation; equal sets finish in one round with no items exchanged; + two random sets of `n` items differing in `d` complete with rounds at + most `2 * ceil(log4(n)) + 2` for `n` in {1, 100, 10,000} and `d` in + {1, n/2, n} (proptest over seeds); every B-5 refusal, including a peer + whose `RangeItems` contradict its earlier fingerprint; `run` over + `MemoryChannel::pair` ingests in parent-first order against two 021 + repositories initialized from one genesis; ingest refuses a bad + signature, a wrong advertised hlc, and a missing parent, leaving the + store unchanged; `fetch_payloads` skips erased cids. +- **FR-003.** 021's FR-003 public-API test gains `ingest_entry` as the + fourth write method; no other mutable path is added. +- **FR-004.** The crate depends on `hqgit-types`, `hqgit-object`, and + `hqgit-ledger` only within the workspace; its manifest carries + `[package.metadata.spec-spine] spec = "110-set-reconciliation"`. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-sync --locked` passes, vectors included. +- **AC-2.** `cargo test -p hqgit-ledger --locked repo` passes with the + ingest path added. +- **AC-3.** `spec-spine index` discovers `hqgit-sync` bound to this spec + and `index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Transport, peer identity, and capability checks (111); peer discovery, +registries, scheduling, and the `hq sync` verbs (112); quarantine +admission of foreign entries (112 over 094); a persistent by-hlc index +(a 021 amendment when measured to matter); encrypted namespace keys (020 +`KeyProvider`; ciphertext replicates as bytes). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-sync --locked +cargo test -p hqgit-ledger --locked repo +``` diff --git a/specs/111-quic-transport/spec.md b/specs/111-quic-transport/spec.md new file mode 100644 index 0000000..9583fd3 --- /dev/null +++ b/specs/111-quic-transport/spec.md @@ -0,0 +1,200 @@ +--- +id: "111-quic-transport" +title: "QUIC transport: identity-bound endpoints, the sync session, and the remote object source" +status: approved +kind: "feature" +domain: "l1-ledger" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 8 +depends_on: + - "110-set-reconciliation" + - "060-identity-and-key-rotation" +establishes: + - "crates/hqgit-sync/src/transport.rs" + - "crates/hqgit-sync/src/session.rs" + - "crates/hqgit-sync/src/object_source.rs" + - "crates/hqgit-sync/tests/transport.rs" +extends: + - { spec: "110-set-reconciliation", unit: "crates/hqgit-sync/src/lib.rs", nature: additive } + # iroh, quinn, tokio, and hqgit-trust join the crate's dependencies. + - { spec: "110-set-reconciliation", unit: "crates/hqgit-sync/Cargo.toml", nature: additive } + - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +summary: > + The wire under reconciliation: QUIC through iroh and quinn (thesis + §4.2), where the peer's TLS key is its hqgit identity key, so a + handshake is an identity proof checked against the rotation chain (060) + and nothing else names a peer. One ALPN, one framed message stream per + session, and one stream per object fetch. The session runs heads + exchange, reconciliation (110) per namespace, entry ingest, and payload + fetch through a RemoteObjectSource that implements spec 015's + ObjectSource, so every byte from a peer arrives with a range proof and + a tampered slice is refused before it exists locally. Reads are gated + per namespace by an Authorizer seam and quarantine is never pulled + unless asked for; an interrupted session leaves a valid replica and the + next one resumes from the last completed watermark. +--- + +# 111: QUIC transport + +## 1. Purpose + +Spec 110 made replication a pure state machine over messages; this spec +gives it a network. Two properties are load-bearing. First, identity: a +peer is the key it proves possession of in the TLS handshake, and that +key resolves through the identity fold (060 B-5), so a revoked key cannot +sync even if the operator's peer list still names it (constitution XI). +Second, verification: content flows only through 015's proof-carrying +seam, so the transport can serve an untrusted client and accept from an +untrusted server without either trusting the other (thesis §4.1, D2). + +## 2. Territory + +`transport.rs` (endpoint, connection, framing, `QuicChannel`), +`session.rs` (the handshake, phases, authorization seam, resume state), +`object_source.rs` (`RemoteObjectSource` and its serving half) in +`crates/hqgit-sync`, plus `tests/transport.rs`. Additively: the crate's +`lib.rs` and manifest (`iroh`, `quinn`, `tokio`, `hqgit-trust`), and the +workspace dependency table. The peer registry, capability attestations, +scheduling, and the CLI verbs are spec 112; the resume file lives under +`.hq/sync.redb`, an additive entry in 021 B-1's layout. + +## 3. Behavior + +- **B-1 (endpoint and identity).** `Endpoint::bind(identity: + &LocalIdentity, config: TransportConfig) -> Result<Endpoint, Error>` + builds an `iroh::Endpoint` whose secret key is the identity's ed25519 + seed (021 B-4), so the TLS certificate presented is a raw public key + (RFC 7250) equal to the identity's public key and `iroh`'s node id is + that key. The ALPN is exactly `b"hqgit/sync/1"`; a connection + negotiating any other ALPN is refused. `TransportConfig { bind: + SocketAddr, relay: Option<String>, idle_timeout_s: u32 (default 30), + max_frame: u32 (default 16 MiB), max_object_streams: u8 (default 8) }`. + `PeerAddr { key: PublicKey, addrs: Vec<SocketAddr>, relay: + Option<String> }` names a peer; `Endpoint::connect(&PeerAddr) -> + Result<Connection, Error>` fails with `Error::Crypto` when the + handshake's key differs from `PeerAddr.key`; `Endpoint::accept() -> + Result<Connection, Error>`. `Connection::peer_key() -> PublicKey`. +- **B-2 (peer verification).** `verify_peer(key: &PublicKey, view: + &IdentityView, now: &Hlc, authorizer: &dyn Authorizer) -> + Result<PeerIdentity, Error>`: `key_valid_at(KeyId::of(key), now)` (060 + B-5) `Valid` yields `PeerIdentity::Known(identity)`; `Unknown` yields + `PeerIdentity::Pinned(KeyId)` only when `authorizer.knows_key(key)` + (an operator-pinned peer whose identity facts have not replicated + yet), else `Error::Crypto("unknown peer key")`; `Rotated`, `Revoked`, + and `NotYet` are `Error::Crypto` naming the state. `now` is the + caller's `HlcGenerator` value (018); this crate reads no clock. +- **B-3 (framing and channel).** The first bidirectional stream a + connection opens is the session stream; every message is a `u32` + big-endian length followed by the canonical bytes of a 110 `Message` or + a session message of B-4; a frame above `max_frame` or that fails + canonical decode is `Error::Validation` and closes the connection with + QUIC error code `0x01`. `QuicChannel` wraps the session stream and + implements 110's `Channel`. Object fetches (B-6) each open their own + bidirectional stream, at most `max_object_streams` in flight. +- **B-4 (session messages).** Kinds `sync.hello { v: 1, namespaces: + Vec<Hash>, heads: BTreeMap<Hash, Vec<EntryHash>>, resume: + Option<Bound> }`, `sync.begin { namespace: Hash, range: Range }`, + `sync.end { namespace: Hash, outcome: Outcome }`, and `sync.bye { + stats: SessionStats }`. `namespaces` lists the requested namespace ids; + the default request is `[main]` and the quarantine namespace (021 B-6) + is included only when the caller asks for it explicitly. `heads` are + the sender's 017 heads per namespace; equal heads on both sides skip + reconciliation for that namespace. +- **B-5 (`Authorizer` and phases).** `trait Authorizer { fn may_read(&self, + peer: &PeerIdentity, namespace: &Hash) -> bool; fn may_write(&self, + peer: &PeerIdentity, namespace: &Hash) -> bool; fn knows_key(&self, + key: &PublicKey) -> bool; }` with `OwnerOnly` (the local identity and + nothing else) and `StaticAuthorizer(BTreeMap<KeyId, BTreeSet<(Hash, + Right)>>)` with `Right::{Read, Write}` shipped here; 112 supplies the + capability-backed one. `Session::run(conn, repo, view, authorizer, + now, request: SyncRequest) -> Result<SessionStats, Error>` proceeds: + hello exchange (a namespace the peer may not read is answered with + `sync.end { outcome: Denied }` and `Error::Policy("read denied for + namespace <hex>")` on the requesting side; a peer with `may_write` + false is served read-only, meaning its `RangeItems` are consumed for + fingerprinting but no `WantEntries` is sent for its items); then per + namespace `sync.begin`, 110 `run` over `QuicChannel` with an + `EntryIndex` restricted to entries whose `extra["namespace"]` is that + namespace (absent meaning `main`), ingest through `ingest_entry` (110 + B-7) with the 060 `RotationAwareResolver`, `sync.end`; then payload + fetch (110 B-8) through B-6; then `sync.bye`. `SyncRequest { + namespaces: Vec<Hash>, pull: bool, push: bool, fetch_payloads: bool }`. +- **B-6 (`RemoteObjectSource`).** Object stream messages: `obj.slice { + cid, range }`, `obj.whole { cid }`, `obj.size { cid }`, and replies + `obj.slice_ok { proof: SliceProof, bytes }`, `obj.whole_ok { bytes }`, + `obj.size_ok { len }`, `obj.absent`. `RemoteObjectSource { conn }` + implements 015 B-4's `ObjectSource`; the serving half answers from + `LocalSource` over the repository store with proofs from the stored + outboard (015 B-1). Serving requires `may_read` for at least one + namespace of the session; objects are not partitioned by namespace + (content addressing does not know namespaces) and encrypted namespaces + protect content by ciphertext (020 B-3), not by transport. Every slice + is verified by 015 `verify_slice` against the requested cid before it + is returned, so a peer serving altered bytes produces `Error::Crypto` + naming the chunk group and nothing enters the store. +- **B-7 (resume).** `.hq/sync.redb` holds `peers: (peer KeyId, namespace) + -> ResumeState { completed_to: Bound, last_heads: Vec<EntryHash> }`, + written only after `sync.end { Ok }`. A session with a resume state + first reconciles `[completed_to, MAX)`; if 110's final fingerprint + check fails for that range the session falls back to the whole space + once. Because ingest is idempotent and parent-first, an interrupted + session at any frame leaves a valid ledger with at worst `Missing` + payloads, and no partial object (015 B-5). +- **B-8 (limits and refusals).** Per connection: one session stream, at + most `max_object_streams` object streams, idle close after + `idle_timeout_s`, an `Abort` on any 110 B-5 violation. Every refusal + maps to one `Error` variant: `Crypto` (identity, tamper), `Policy` + (authorization), `Validation` (protocol), `Io` (transport). The + serving side never reveals why beyond the variant name. + +## 4. Functional requirements + +- **FR-001.** `tests/transport.rs` binds two endpoints on ephemeral + loopback ports, each over a 021 repository initialized from one + genesis with its own identity facts, and asserts: a full sync converges + both ledgers and object stores; a second sync exchanges no entries and + resumes from `completed_to`; a peer holding only `Read` receives entries + and sends none; a namespace without `may_read` is `Error::Policy`; + quarantine is untouched by a default request and synced when + requested with the right. +- **FR-002.** Tests cover: ALPN mismatch refused; a connection whose + handshake key differs from `PeerAddr.key`; every `KeyValidity` state + in `verify_peer`, including `Pinned`; an oversized frame; a serving + endpoint wrapped to flip one byte in a slice or a proof yielding + `Error::Crypto` with the store unchanged; an interrupted session + (connection dropped mid-`Entries`) followed by a successful resume. +- **FR-003.** The `bao`-verified path is the only path from the network + to the object store; a test enumerates `object_source.rs` for calls to + `ObjectStore::put` and asserts each is preceded by `fetch_into` or + `verify_slice`. +- **FR-004.** `iroh` and `quinn` are pinned exact in the workspace table; + `hqgit-sync` gains `hqgit-trust` and no other workspace crate. + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-sync --locked transport` passes. +- **AC-2.** `cargo test -p hqgit-sync --locked` passes in full. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +The peer registry, capability attestations as authorization, server +scheduling, and the CLI verbs (112); quarantine admission of foreign +entries (112 over 094); relay operation and NAT traversal policy beyond +what `iroh` provides by configuration; bandwidth shaping; git protocol +compatibility (092). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-sync --locked transport +cargo test -p hqgit-sync --locked +``` diff --git a/specs/112-federation/spec.md b/specs/112-federation/spec.md new file mode 100644 index 0000000..e1cea47 --- /dev/null +++ b/specs/112-federation/spec.md @@ -0,0 +1,206 @@ +--- +id: "112-federation" +title: "Federation: peer registries as facts, the server sync driver, holds for unpromoted peers, and hq sync" +status: approved +kind: "feature" +domain: "l7-edge" +created: "2026-09-02" +authors: ["Bartek Kus"] +implementation: pending +risk: high +wave: 8 +depends_on: + - "111-quic-transport" + - "094-quarantine-and-promotion" + - "032-cli-skeleton" +establishes: + - "crates/hqgit-server/src/federation.rs" + - "crates/hqgit-server/src/peers.rs" + - "crates/hqgit-cli/src/cmd_sync.rs" + - "crates/hqgit-server/tests/federation.rs" + - "crates/hqgit-cli/tests/sync.rs" +extends: + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/lib.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/app.rs", nature: additive } + # The [federation] config table and hqgit-sync as a server dependency. + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/src/config.rs", nature: additive } + - { spec: "090-server-skeleton", unit: "crates/hqgit-server/Cargo.toml", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/main.rs", nature: additive } + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/src/cli.rs", nature: additive } + # hqgit-sync joins the CLI's dependencies. + - { spec: "032-cli-skeleton", unit: "crates/hqgit-cli/Cargo.toml", nature: additive } + # federation.peer_added, federation.peer_removed, federation.received. + - { spec: "023-domain-fact-vocabulary", unit: "crates/hqgit-domain/src/facts.rs", nature: additive } +summary: > + Thesis §6 step 8: hosts replicate signed history, not authority. A + repository's peers are facts (identity, transport key, endpoints, + namespaces, and the capability attestation that says what the peer may + do here), folded into a registry that implements the transport's + Authorizer with spec 094's capability checks. The server runs a per + repository driver over 111 that pulls from every peer and pushes to the + peers it is configured to push to; entries arriving from a peer without + the promote capability go into a per-peer hold that no fold reads until + a promotion fact (094) admits them, so a foreign host can contribute + history but never decide what this host treats as main (constitution + XV). The CLI is a peer like any other: hq sync, hq push, and hq pull + speak the same protocol against a server or another laptop. +--- + +# 112: Federation + +## 1. Purpose + +The thesis's last risk (002 §8): decentralized state raises discovery +cost, which is why centralization keeps winning. hqgit answers with a +central index that has no lock-in (wave 6) and, here, replication of the +same signed history between hosts, so leaving a host is a sync rather +than a migration (constitution XIV). What is deliberately not replicated +is authority: which peers a host trusts, and which foreign entries it +admits to `main`, are local decisions recorded as local facts, verifiable +by anyone and binding on no one else. + +## 2. Territory + +`peers.rs` (the peer facts, `PeerRegistry`, `CapabilityAuthorizer`) and +`federation.rs` (the hold, admission, the driver, the status route) in +`crates/hqgit-server`; `cmd_sync.rs` in `crates/hqgit-cli`; and the two +test files. Additively: the server's `lib.rs`, `app.rs` (QUIC listener +and the status route), `config.rs` (`[federation]`), and manifest; the +CLI's dispatch, clap tree, and manifest; three fact kinds in spec 023's +vocabulary. The transport and session are 111; the capability predicate +and the promotion fact are 094. + +## 3. Behavior + +- **B-1 (peer facts).** `federation.peer_added { peer: Principal, key: + PublicKey, endpoints: Vec<String>, relay: Option<String>, namespaces: + Vec<Hash>, mode: PeerMode, capability: Option<AttestationId> }` with + `PeerMode` a closed enum `Pull | Push | Both`; `federation.peer_removed + { peer: Principal }`; `federation.received { peer: Principal, namespace: + Hash, session: Hash, entries: Vec<EntryHash> }` (at most 4,096 hashes + per fact, chunked). Endpoints are `host:port` strings; `key` is the + peer's transport key (111 B-1), so a peer whose identity facts have not + replicated yet is still pinned (111 B-2 `Pinned`). Peer facts are + written to the repository's `main` namespace by the host's own Service + identity (090); `federation.received` is written to the quarantine + namespace (021 B-6). +- **B-2 (registry and rights).** `PeerRegistry` is a `DerivedState` (019) + folding B-1 into `BTreeMap<Principal, Peer>` (last `peer_added` in total + order wins; `peer_removed` deletes). Rights come from the referenced + capability attestation, verified through 094's `capability.rs` over a + 064 verified set: `read` (pull `main`), `read-quarantine` (pull the + quarantine namespace and this host's holds), `write` (push into a + hold), `promote` (push admitted directly). `CapabilityAuthorizer { + registry, capabilities }` implements 111 B-5's `Authorizer`: `may_read + (peer, ns)` is `read` for `main` and `read-quarantine` for quarantine; + `may_write` is `write` or `promote`; `knows_key` is membership of the + key in the registry. A peer with no capability attestation has no + rights at all; the registry entry alone grants nothing. +- **B-3 (hold).** `Hold` is a per-peer `EntryStore` (021 B-2, redb) at + `<data>/repos/<namespace-hex>/.hq/hold/<peer-hex>.redb`. An incoming + session from a peer whose rights include `write` but not `promote` + ingests into the hold through 110 B-7's checks with the hold as the + store (parents may resolve from either the ledger or the hold); on + `sync.end { Ok }` the host appends `federation.received` naming the + session and the held hashes. A peer with `promote` ingests into the + ledger directly. No fold, projection, or API read ever opens a hold + except `read-quarantine` reads and the promotion path. +- **B-4 (admission).** `promote_hold(repo, peer, session: Hash, verified) + -> Result<Admitted, Error>` is what 094's promotion path calls when a + `namespace.promoted` fact whose subject is a `federation.received` + entry hash is appended by a principal holding `promote`: every held + entry named by that fact is ingested into the ledger in `(hlc, hash)` + order, payloads fetched from the hold's object store, and the hold + entries deleted; `Admitted { entries: u32, payloads_missing: u32 }`. + A `namespace.promoted` without the capability is refused by 094 before + this function runs. Nothing is retagged: the entries keep their + issuers, signatures, and namespace markers, and 020's tombstones apply + to them as to any other. +- **B-5 (outgoing sessions).** `FederationDriver::new(repos: &RepoRegistry + (090), endpoint: Endpoint (111), config)` runs one task per repository: + every `interval_s` (default 60) it folds the registry and, for each + peer in `Pull` or `Both`, opens a session with `SyncRequest { pull: + true, push: mode == Both, namespaces: peer.namespaces }`; for `Push` + peers, `pull: false, push: true`. The remote host applies its own + authorizer to what we push (B-2 on its side). Failures back off per + peer (base 5 s, factor 2, cap 900 s) and are recorded on + `PeerStatus { last_ok: Option<Hlc>, last_error: Option<String>, + consecutive_failures: u32 }`; the driver never busy-loops. +- **B-6 (incoming sessions).** `app.rs` binds the QUIC endpoint from + `[federation] bind = "0.0.0.0:4433"` (`enabled = false` skips all of + this) with the host's Service identity and accepts connections into + `Session::run` (111 B-5) with the `CapabilityAuthorizer` of the + repository the hello names; the repository is located by its namespace + id through 090's `RepoRegistry`; an unknown namespace is + `Error::NotFound` after the handshake and never before (no existence + oracle for unauthenticated keys). `GET /federation/status` returns + `{ "enabled", "peers": [{ "peer", "mode", "rights", "status" }] }` + per repository for the operator; it is read-only and served from the + registry fold. +- **B-7 (CLI verbs).** `hq peer add <name> --key <hex> --addr <host:port> + [--relay <url>] [--namespace <name>]... [--mode pull|push|both]` + records `[[sync.peers]]` in `.hq/config.toml`; `hq peer list` and + `hq peer remove <name>`. `hq sync [<name>] [--namespace <name>] + [--no-payloads]` runs a `Both` session against the named peer or every + configured peer; `hq pull [<name>]` and `hq push [<name>]` are `pull` + only and `push` only. The CLI's local authorizer is 111's `OwnerOnly` + extended with the configured peers holding `promote` for pulls: a + laptop trusts the peers its owner configured, and its ingest goes + straight to the ledger. The CLI's identity key (021 B-1) is the + transport key. Output in JSON mode is `{ "peer", "namespaces": [{ + "namespace", "received", "sent", "payloads_missing", "rounds" }], + "resumed": bool }` per peer; exit `0` when every session completed, + `1` on a policy or identity refusal, `3` on transport failure. +- **B-8 (no ambient input on hashed paths).** The driver's interval and + backoff use `tokio` timers outside every hashing path; `now` for 111 + B-2 comes from each repository's `HlcGenerator` (018). + +## 4. Functional requirements + +- **FR-001.** `tests/federation.rs` boots two servers on ephemeral ports, + each hosting one repository replicated from one genesis, registers each + as the other's peer with `Both` and `promote`, appends distinct facts on + each, runs one driver tick on both, and asserts identical total orders + (018) and object stores; it then starts an `hq` process (assert_cmd) + with the first server as a `pull` peer and asserts the CLI ledger + matches. +- **FR-002.** Tests cover: a peer without a capability attestation is + refused with `Error::Policy` and nothing is ingested; a peer with + `write` only lands its entries in the hold, `main`'s fold (024) does + not see them, `federation.received` names them, and `promote_hold` + after a `namespace.promoted` admits them with the fold updated; a peer + with `read` only never receives our `WantEntries`; a removed peer is + refused on the next tick; backoff schedule after two failures; the + status route reflects the registry. +- **FR-003.** `tests/sync.rs` drives `hq peer add`, `hq sync --json`, `hq + pull`, and `hq push` against a server started in-process by the test, + asserting the JSON shape, the exit codes of B-7, and that a second + `hq sync` reports `received = 0` and `resumed = true`. +- **FR-004.** Neither crate gains a dependency on the other; `hqgit-sync` + is the shared crate (constitution XIII). + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-server --locked federation` passes. +- **AC-2.** `cargo test -p hqgit-cli --locked sync` passes. +- **AC-3.** `spec-spine index coverage --fail-on-untraced` exits 0. + +## 6. Out of scope + +Peer discovery through the ecosystem index (084 lists repositories; a +later spec joins them to peers); promotion policy beyond 094's +capability check; replicating the action cache (071) between hosts; +cross-host merge-queue coordination (076 runs on one host's 091 leader); +mirroring to GitHub (040 to 042); web UI for peers (095). + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-server --locked federation +cargo test -p hqgit-cli --locked sync +``` diff --git a/standards/spec/constitution.md b/standards/spec/constitution.md new file mode 100644 index 0000000..f35f39f --- /dev/null +++ b/standards/spec/constitution.md @@ -0,0 +1,171 @@ +# hqgit constitution + +Durable principles that govern this corpus. This document is **tier 2**: it is +subordinate to the bootstrap spec (`specs/000-hqgit-bootstrap/spec.md`), whose +`unamendable` anchors it may not contradict, and it governs all ordinary specs +(`001`+). + +**Normative hierarchy (highest wins):** + +1. `specs/000-hqgit-bootstrap/spec.md`: the bootstrap spec. Non-overridable. +2. `standards/spec/constitution.md`: this document. +3. `standards/spec/contract.md`: a normative summary of the bootstrap spec. +4. `specs/002-platform-thesis/spec.md`: the architectural thesis. It owns no + code; it fixes the layer model and the build order every other spec + operates inside. +5. Ordinary specs (`010`+): feature-level claims within this envelope. + +When two specs conflict, resolve in this order, then by the typed authority +graph. + +The first five principles are spec-spine's own and apply to the corpus. The +principles from VI onward are hqgit's and apply to the system the corpus +describes. Both halves bind every spec and every build session equally. + +--- + +## I. Markdown-only authored truth + +Authored truth lives only in markdown with YAML frontmatter. There is no +authoritative hand-authored JSON, YAML, or TOML data file that governs the +system. If a fact governs the system, it is written in a `spec.md` (or a +`standards/` document), never in a derived artifact. *(Bootstrap anchor: +`markdown-truth-boundary`.)* + +## II. Compiler-owned JSON machine truth + +All machine-consumable truth about the corpus is emitted by `spec-spine` into +`.derived/` and is read only through `spec-spine` subcommands. Hand-editing a +derived artifact is a workflow violation; ad-hoc parsing of one is equally +forbidden. *(Bootstrap anchor: `json-truth-boundary`.)* + +## III. Spec-first development + +A change to behavior begins with a change to a spec. The spec defines the +territory (the units it owns) and the relationships (the typed edges) before +the code is written. The coupling gate enforces this at PR time. The escape +valve is a named, scoped waiver in the PR body, never a silent edit to an +owner spec. In this repository the ownership ratchet is on: every source file +inside a crate must be specifically claimed by a spec, so a build session +that adds a file adds it to the `establishes` list of the spec it is +implementing, in the same change. + +## IV. Determinism and validation + +Every artifact-producing function in the corpus toolchain is a pure function +of `(config, file contents)`. Validation is mechanical. *(Bootstrap anchor: +`determinism-requirement`.)* The same principle is the design rule of the +system itself (see VIII). + +## V. Legacy as evidence + +Code that predates a governing spec is evidence, not a violation. hqgit is +greenfield, so only the bootstrap spec carries `origin.retroactive: true`; +every ordinary spec is a forward claim. Should the corpus ever adopt code it +did not author (a vendored executor, a mirrored protocol implementation), that +code is specced as found, defects recorded under a `## Known defects` heading, +never blessed. + +--- + +## VI. Canonical objects; everything else is derived + +The canonical state of a repository is a set of signed, content-addressed +objects forming a per-repository DAG that covers code, collaboration, and +evidence (layers L0 through L4 in spec 002). Every index, timeline, search +result, dashboard, queue, and feed is a projection that MUST be rebuildable +from zero. No projection may hold an authoritative row that is not in the +log. One violation of this principle recreates the system this project +exists to replace. *(Bootstrap anchor: `canonical-derived-boundary`.)* + +## VII. Facts are immutable; only derived state converges + +A fact is an immutable, signed event: a revision was submitted, an attestation +was issued, a comment was anchored. Facts never conflict; concurrent facts +merge by set union under one deterministic total order. Only derived state +(open or closed, labels, assignee, title) needs convergence, and last-writer-wins +over a hybrid logical clock is the default instrument. Sequence CRDTs are +reserved for genuinely collaborative text and nothing else. A spec that moves a +noun from the fact side to the CRDT side must say why in its body. *(Bootstrap +anchor: `facts-immutable`.)* + +## VIII. Hash stability is the one unrecoverable boundary + +The canonical encoding of every L0 and L1 object (spec 011) and the hash of +every ledger entry (spec 017) are frozen the moment the first signed entry +exists. Serialization is canonical and deterministic; unknown fields are +preserved, never dropped; fields are never reordered or retyped inside a +schema MAJOR; and no clock, environment read, or map iteration order reaches +a hashed byte. Golden vectors under `crates/hqgit-types/testdata/vectors/` +are the frozen record: a change that alters any vector's hash is a schema +MAJOR, a spec amendment, and a human decision, in that order. *(Bootstrap +anchor: `hash-stability`.)* + +## IX. One evidence primitive + +Every form of evidence (human approval, build provenance, test result, static +finding, license scan, policy evaluation, agent action, mirrored external +state) is an `Attestation { subject, predicate, issuer, claim, sig }` (spec +027). One storage path, one verification path, one policy input, one audit +trail. Requests to special-case a predicate are refused; new evidence kinds +register a predicate, never a new noun. *(Bootstrap anchor: +`single-evidence-primitive`.)* + +## X. Erasure by tombstone, never by rewrite + +The signed log holds commitments (content identifiers), never user content. +Content lives in the object store, encrypted per namespace where required. +Deletion removes the blob and appends a tombstone over the commitment; the +chain stays verifiable and the content is genuinely gone. Signed history is +never rewritten. *(Bootstrap anchor: `erasure-by-tombstone`.)* + +## XI. Trust is checkable, not decorative + +Identity is a keypair with a rotation chain recorded in the ledger. Approvals +are attestations over a specific revision hash. "Requires two approvals" is a +predicate over the evidence graph, evaluated by a hash-pinned, deterministic +policy module whose verdict is itself an attestation. Repository settings as +mutable toggles are an anti-pattern the design makes impossible. A cache that +gates a merge is a trust boundary: an unattested cache hit is a miss. + +## XII. Agents are a distinct principal class + +`Principal` is `Human | Agent | Service | Org` at the type level. An agent +never authenticates as a human holding a human token. Its credential carries +its delegation chain (which human, under which policy version, expiring +when), its sandbox is declared, and every artifact it produces carries +provenance. Verification throughput, not authoring convenience, is what the +system optimizes for. *(Bootstrap anchor: `agent-principal-class`.)* + +## XIII. Layer boundaries are one-directional + +L5 and above read from L0 through L4 and never write authoritatively. +Crates depend downward only: `hqgit-types` has no workspace dependency, and +`hqgit-server` and `hqgit-cli` depend on everything beneath them but on each +other never. The CLI and the server run the same ledger implementation; that +is what makes offline-first true rather than aspirational. *(Bootstrap anchor: +`layer-direction`.)* + +## XIV. Absorption over replacement + +Git compatibility is a hard requirement and the GitHub mirror is the wedge. +hqgit builds the verification and review plane over existing repositories +and lets hosting commoditize underneath. A spec that requires a user to +migrate before receiving value must justify that in its Purpose section. + +## XV. Untrusted by default + +Contributions from unverified principals, mirrored external state, and agent +output land in a quarantine namespace and are promoted by capability, never +accepted by default. Abuse in an append-only replicated store is designed +against from the first spec, not patched later. + +--- + +## Amendment + +This constitution may be amended by an ordinary spec that `amends` it and is +approved, **provided** the amendment does not contradict a `specs/000` +`unamendable` anchor. The bootstrap spec's freeze surface is the hard +boundary; everything else in this document is revisable through the normal +governed flow. diff --git a/standards/spec/contract.md b/standards/spec/contract.md new file mode 100644 index 0000000..7f68192 --- /dev/null +++ b/standards/spec/contract.md @@ -0,0 +1,72 @@ +# hqgit spec contract (normative summary) + +A one-page operational summary of the bootstrap spec +(`specs/000-hqgit-bootstrap/spec.md`) and the corpus conventions the thesis +(`specs/002-platform-thesis/spec.md`) fixes. The bootstrap spec and the +constitution are authoritative; where this summary is terser, they govern. + +## Inputs (authored truth: markdown only) + +- `specs/NNN-slug/spec.md`: one spec per directory; directory name equals + `id`; `NNN` is a unique three-digit ordinal that is also the build order. +- `standards/spec/`: the constitution, this contract, and templates. +- `spec-spine.toml`: the repo's configuration (owned by spec 001). +- `docs/design/`: design analysis cited from specs with non-owning + `references` edges. Prose, not authority. + +## Outputs (machine truth: compiler-owned JSON, read via `spec-spine` only) + +- `.derived/spec-registry/by-spec/<id>.json`: spec-as-source shards. +- `.derived/codebase-index/by-spec/<id>.json` and `.../by-package/<slug>.json`: + code-as-source shards. +- `.derived/**/build-meta.json`: wall-clock metadata, gitignored. + +Both shard trees are committed. `spec-spine compile --check` and +`spec-spine index check` compare the working tree against them. + +## Required frontmatter + +`id`, `title`, `status` (`draft` / `approved` / `superseded` / `retired`), +`created` (`YYYY-MM-DD`), `summary`. In this corpus every ordinary spec also +carries `kind` (closed enum), `domain` (the layer, closed enum), +`implementation`, `risk`, `authors`, `wave` (build-order wave, 1 to 8), and +`depends_on` (never empty except on the bootstrap spec). + +## Typed edges (8; `references` is the only non-owning one) + +`establishes`, `extends`, `refines`, `supersedes`, `amends`, `co_authority`, +`constrains`, `references`. `origin` is a bootstrap marker, not an edge. + +## Authority units + +`file` (bare string shorthand; trailing slash is a subtree), `section` +(`{file, anchor}`), `symbol` (`{id}`), `directory`, `crate`, `module`. +This corpus claims a crate's manifest and each source file explicitly; test +and fixture directories are claimed as subtrees. + +## Lifecycle as scheduling + +- `status: approved` + `implementation: pending`: a work order. The + orchestrator (claude-observatory) schedules the lowest-numbered one whose + `depends_on` are all shipped. +- `status: draft`: never schedulable, visible as a blocker. Approval is a + human act. +- `implementation: n-a` (thesis, bootstrap) and `complete` count as shipped, + pinned at the sha256 of their `spec.md`. Amending a shipped spec invalidates + every transitive dependent until it re-verifies. +- `depends_on` MUST be acyclic and MUST only name lower-numbered specs. + +## The gate chain + +`compile` → `index` → `lint --fail-on-warn` → `couple`, plus +`index coverage --fail-on-untraced` in CI. `[coupling] require_ownership` is +on: a changed source file no spec specifically claims is `C-002`. Cargo +gates (`build`, `test`, `clippy`, `fmt`, `deny`) run whenever `Cargo.toml` +exists. A spec's `## Verification` block is what the verify stage runs after +merge. + +## Determinism + +Pure function of `(config, file contents)`; byte-identical output; staleness +by content hash alone. The same rule governs the system's own L0/L1 encoding +(constitution VIII). diff --git a/standards/spec/templates/constitution-template.md b/standards/spec/templates/constitution-template.md new file mode 100644 index 0000000..98fc1c3 --- /dev/null +++ b/standards/spec/templates/constitution-template.md @@ -0,0 +1,7 @@ +# Constitution (tier 2): template + +Replace these with your project's durable principles. Keep them subordinate to +the bootstrap spec and few in number. + +1. **<principle>**: <one sentence>. +2. **<principle>**: <one sentence>. diff --git a/standards/spec/templates/spec-template.md b/standards/spec/templates/spec-template.md new file mode 100644 index 0000000..0c3e9d1 --- /dev/null +++ b/standards/spec/templates/spec-template.md @@ -0,0 +1,74 @@ +--- +id: "NNN-slug" # MUST equal the directory name; NNN = unique 3-digit ordinal = build order +title: "Short imperative title" +status: draft # draft | approved | superseded | retired; only approved specs schedule +kind: kernel # constitutional-bootstrap | thesis | governance | kernel | feature | tooling +domain: l1-ledger # governance | l0-objects | l1-ledger | l2-domain | l3-evaluation | l4-trust | l5-projection | l6-policy | l7-edge +created: "YYYY-MM-DD" +authors: ["Bartek Kus"] +implementation: pending # pending | in-progress | complete | n-a | deferred +risk: medium # low | medium | high | critical (critical = touches hashed bytes or trust decisions) +wave: 1 # build-order wave, 1..8 (spec 002 §6) +depends_on: + - "NNN-lower-numbered" # every dependency is lower-numbered; the graph is a DAG +summary: > + One short paragraph: what territory this spec claims and why it exists. +# --- typed edges (declare territory + relationships) --- +# establishes: +# - "crates/hqgit-x/Cargo.toml" # a crate-founding spec claims its manifest +# - "crates/hqgit-x/src/lib.rs" +# - "crates/hqgit-x/src/thing.rs" # every source file, explicitly +# - "crates/hqgit-x/tests/thing.rs" +# - "crates/hqgit-x/testdata/thing/" # fixtures as a subtree +# extends: +# - { spec: "NNN-founder", unit: "crates/hqgit-x/src/lib.rs", nature: additive } # re-exports +# - { spec: "NNN-founder", unit: "crates/hqgit-x/Cargo.toml", nature: additive } # new deps +# - { spec: "010-workspace-and-core-types", unit: { kind: section, file: "Cargo.toml", anchor: "workspace.dependencies" }, nature: additive } +# constrains: +# - { flavor: invariant-freeze, unit: "crates/hqgit-types/testdata/vectors/", note: "golden vectors are frozen" } +# references: +# - { unit: { kind: file, path: "docs/design/00-architecture.md" }, role: context } +--- + +# NNN: Title + +## 1. Purpose + +What problem this spec solves, and which thesis section (spec 002) or +constitutional principle it serves. One or two paragraphs. + +## 2. Territory + +The units this spec claims, in prose (mirrors the frontmatter). Name the +crate, the modules, and the seams it exposes to later specs. + +## 3. Behavior + +- **B-1 (name).** What the governed code MUST do. Use MUST/SHOULD/MAY. +- **B-2 (name).** ... + +## 4. Functional requirements + +- **FR-001.** Testable requirement, including the seams (traits, injected + readers) that keep the core pure and the tests fixture-driven. +- **FR-002.** ... + +## 5. Acceptance criteria + +- **AC-1.** `cargo test -p hqgit-x --locked` passes. +- **AC-2.** A concrete observable outcome against a fixture. + +## 6. Out of scope + +What this spec deliberately does not cover, and which later spec covers it. + +## 7. Resolved decisions + +None yet. The build session records D-n entries here for choices this spec +is silent on (date, provenance, the decision, the alternative rejected). + +## Verification + +```verify:cli +cargo test -p hqgit-x --locked +``` From 6417bf5a9da259e6bb1bf3767e9fa0b6ffb98374 Mon Sep 17 00:00:00 2001 From: Bartek Kus <7887446+bartekus@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:57:21 -0600 Subject: [PATCH 2/2] fix(001): CI starts, the cargo gates guard on a job output not hashFiles GitHub allows hashFiles only inside a step, so the job-level guards on Cargo.toml and deny.toml failed the workflow at startup: the run reported as "no checks" on the pull request instead of as a gate result. The spine job now probes the tree after checkout and publishes has_cargo and has_deny for the cargo and deny jobs to gate on. Recorded as D-3 on 001. --- .../by-spec/001-agentic-harness.json | 2 +- .../by-spec/001-agentic-harness.json | 2 +- .github/workflows/govern.yml | 25 +++++++++++++++++-- specs/001-agentic-harness/spec.md | 10 ++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.derived/codebase-index/by-spec/001-agentic-harness.json b/.derived/codebase-index/by-spec/001-agentic-harness.json index 2ebae66..3168181 100644 --- a/.derived/codebase-index/by-spec/001-agentic-harness.json +++ b/.derived/codebase-index/by-spec/001-agentic-harness.json @@ -266,5 +266,5 @@ "specStatus": "approved" }, "schemaVersion": "1.1.0", - "shardHash": "52107d99fd1a9ff7ea30558c8cca03bc9b8e83f2ef0b2443e781eb21546f6e01" + "shardHash": "0e52b423daad11e9dea5d09a12c395ace5d86f4589394cdbd79e967964fff0a1" } diff --git a/.derived/spec-registry/by-spec/001-agentic-harness.json b/.derived/spec-registry/by-spec/001-agentic-harness.json index efd5982..f82ccb3 100644 --- a/.derived/spec-registry/by-spec/001-agentic-harness.json +++ b/.derived/spec-registry/by-spec/001-agentic-harness.json @@ -93,6 +93,6 @@ "summary": "The governed-development loop every human and every driven session runs inside: the cross-agent New Sessions protocol and the Working the backlog protocol in AGENTS.md, the Claude Code skills (init, setup, next, build, verify, spec, commit, code-review, ship, shepherd, validate-and-fix, cleanup, implement-plan, research, refactor-claude-md), the six agents (architect, explorer, implementer, reviewer, ledger-guardian, trust-reviewer), the standing and path-scoped rules, the hooks that keep the derived artifacts fresh and block an ungated PR, the Makefile that is the one source of truth for what CI validates, and the CI workflow that re-runs the same gate. The harness is what makes the corpus buildable by claude-observatory: the orchestrator reads AGENTS.md's backlog section into every build prompt, drives the repo's own /ship, watches the CI this spec wires, and runs each spec's Verification block after merge.\n", "title": "Agentic engineering harness: session protocol, skills, agents, hooks, gate" }, - "shardHash": "6a0c492d251bd7da4fbf98541e02aa267937ed388fd03ecd86cb756e93ad32ed", + "shardHash": "2809311581694ac71649f72b3c16773b6631ffaf37face0fb1b283b9abce8659", "specVersion": "1.1.0" } diff --git a/.github/workflows/govern.yml b/.github/workflows/govern.yml index 7b1c201..af78f30 100644 --- a/.github/workflows/govern.yml +++ b/.github/workflows/govern.yml @@ -18,10 +18,31 @@ jobs: spine: name: spec-spine gate runs-on: ubuntu-latest + outputs: + has_cargo: ${{ steps.tree.outputs.has_cargo }} + has_deny: ${{ steps.tree.outputs.has_deny }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + # `hashFiles` is not available in a job-level `if` (its contexts are + # github, needs, vars and inputs only), and a job-level `if` is + # evaluated before any checkout anyway. The guard the Makefile applies + # locally is therefore published as an output of this job. + - name: Probe the tree + id: tree + run: | + set -euo pipefail + if [ -f Cargo.toml ]; then + echo "has_cargo=true" >> "$GITHUB_OUTPUT" + else + echo "has_cargo=false" >> "$GITHUB_OUTPUT" + fi + if [ -f deny.toml ]; then + echo "has_deny=true" >> "$GITHUB_OUTPUT" + else + echo "has_deny=false" >> "$GITHUB_OUTPUT" + fi - name: Install spec-spine run: | set -euo pipefail @@ -54,7 +75,7 @@ jobs: cargo: name: cargo gate needs: spine - if: ${{ hashFiles('Cargo.toml') != '' }} + if: needs.spine.outputs.has_cargo == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -69,7 +90,7 @@ jobs: deny: name: supply chain (cargo-deny) needs: spine - if: ${{ hashFiles('deny.toml') != '' }} + if: needs.spine.outputs.has_deny == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/specs/001-agentic-harness/spec.md b/specs/001-agentic-harness/spec.md index 5f9c461..a78b3a1 100644 --- a/specs/001-agentic-harness/spec.md +++ b/specs/001-agentic-harness/spec.md @@ -171,6 +171,16 @@ floor and is deliberately not claimed here: it changes only through a spec that `amends` it, which is the governed path the constitution's own Amendment section names. +D-3 (2026-09-03, first CI run). B-3's "cargo gates when a workspace +exists" is guarded by an output of the `spine` job, not by `hashFiles` in +a job-level `if`. GitHub allows `hashFiles` only inside a step (a +job-level `if` is evaluated before any checkout), and the workflow fails +at startup with `calling function "hashFiles" is not allowed here`, which +reports as a run with no checks rather than as a failed gate. The `spine` +job probes for `Cargo.toml` and `deny.toml` after its checkout and +publishes `has_cargo` and `has_deny`; the `cargo` and `deny` jobs gate on +those. The guard's meaning is unchanged. + ## Verification ```verify:cli