From 8d306819ae2358bf5c572e8a4916abcf1b056fe6 Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Mon, 27 Jul 2026 14:28:39 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ 4b31ab388751 --- CONTRIBUTING.md | 2 +- UPSTREAM.json | 21 +- boatstack/references/artifacts.md | 42 ++- boatstack/statemap.go | 243 ++++++++++++++++ boatstack/statemap_conformance_test.go | 264 ++++++++++++++++++ docs/evidence-engineered-coding.md | 4 +- docs/public-claims.json | 24 +- labs/diagram-json/plan.lock.json | 2 +- .../2026-07-27-state-ownership-map.md | 7 + 9 files changed, 583 insertions(+), 26 deletions(-) create mode 100644 boatstack/statemap.go create mode 100644 boatstack/statemap_conformance_test.go create mode 100644 release-notes/2026-07-27-state-ownership-map.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7780ff7..fe25f19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ # Contributing -Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/039454bde99f8059e1a8ee0356ef433f7837cd74/labs/12-product-engineering-loop). +Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/4b31ab38875160d6ef71a85c65efcdb4bd7ad91b/labs/12-product-engineering-loop). The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR. diff --git a/UPSTREAM.json b/UPSTREAM.json index 92bdcd3..1de88b0 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 80947, - "estimated_tokens": 20237, + "characters": 83659, + "estimated_tokens": 20915, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "583ad367e1375a741b32879d96dbf084a8c6b3ed8edc6d701342f7b384fc27f1", + "CONTRIBUTING.md": "9361609e7207a8b0dc8b9257e180798e08e4ec88b9e39b00f93453d9699efb9f", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -149,7 +149,7 @@ "boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b", "boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e", "boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742", - "boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59", + "boatstack/references/artifacts.md": "9589849796553dfb433c3d49f2d75f49fd2b50e03068f481c41776f1d4cef066", "boatstack/references/config-schema.md": "eff8586850eca9941df1cea29ac7edb779277ed9bd6d938a1980db5028d28cf4", "boatstack/references/failure-moves.md": "b65ef72035afa6ad0dce589a0b38f84bc40cde3864c9ecf973f08fc687f001c3", "boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d", @@ -171,6 +171,8 @@ "boatstack/safety_update_publisher_test.go": "ed3f8187036623694dfe7c395cdae00fdae14609bab6124d1fdfc6fe73fa2196", "boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e", "boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb", + "boatstack/statemap.go": "39ff3a7a7254ec5fde8340551fa92a82aac3f00a48bea34c94192823dcb41337", + "boatstack/statemap_conformance_test.go": "38950377d10b97f4223b79cdb16b17c29a6334f8f2a9a4d826ce12cbd292aa57", "boatstack/supervisory_control_test.go": "c7ea4bcd678e8ec211dac772c834981c4e21762914be2770a5e181bc24605e06", "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", "boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975", @@ -193,10 +195,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0", - "docs/evidence-engineered-coding.md": "c81d462de78afc834b04acc99e6a816f97ac6e05c98f065a35167eb9feb4fce7", + "docs/evidence-engineered-coding.md": "e4eb592093db7fb1ce18f41fe7dd1c89efbaa76760fd618c3375f27fa507b684", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "79ddc45aebfbbaba1054a731413183a47da0d1c6c299e444869d6986a27f7498", + "docs/public-claims.json": "a243fcd3b9a12ef51f491b77b4646db6f8d4b7d435d9b5f0402b97941d73821a", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -210,7 +212,7 @@ "labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d", "labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71", "labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39", - "labs/diagram-json/plan.lock.json": "5807b7d5140a41e9db5a20256e4bd4adebb2820778db2f378803059dd111ea45", + "labs/diagram-json/plan.lock.json": "06b5298ea7a2f9a3db31eb3b1a9bee41a74168a2f3ff02bf5d8a00183705392d", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -333,12 +335,13 @@ "release-notes/2026-07-27-invalid-delivery-block-actionable.md": "8fac8e3921e2285291703efa46e624b72cb5bac1b8492beca4c4b633abb5ba16", "release-notes/2026-07-27-prescriptive-planning-closure.md": "e544408e1c3cceb0cb1979833ea120853c38020933439b39e7f009f454b9661e", "release-notes/2026-07-27-read-only-inspection-pipelines.md": "0963286371e9a12592915c23a958dd013bf2a35e9fca6921691bc8bb3c3d8dc8", - "release-notes/2026-07-27-sandboxed-migration-grading.md": "03cebc372bbdfed37cc70d18f3b6374d1aa5e585bafefa073dbcced58bd0336a" + "release-notes/2026-07-27-sandboxed-migration-grading.md": "03cebc372bbdfed37cc70d18f3b6374d1aa5e585bafefa073dbcced58bd0336a", + "release-notes/2026-07-27-state-ownership-map.md": "d032547aafc1a4acbeb520f6cbb59d7757de4f33fe824701d7b5ea8cd8c8b9e7" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "039454bde99f8059e1a8ee0356ef433f7837cd74", + "commit": "4b31ab38875160d6ef71a85c65efcdb4bd7ad91b", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/references/artifacts.md b/boatstack/references/artifacts.md index bbc2d86..d29c492 100644 --- a/boatstack/references/artifacts.md +++ b/boatstack/references/artifacts.md @@ -95,7 +95,7 @@ ledger while the publisher rechecks the matching receipts. The generated host hook fragments and launchers are committed installation infrastructure. Their policy is immutable in project configuration. Cursor pre/post native, shell, and MCP events; Claude and Codex `PreToolUse`/`PostToolUse`; and Gemini `BeforeTool`/`AfterTool` project into one classifier and completion observer. The machine-local helper is ignored and restored by the installer. Safety evidence belongs in the feature evidence ledger: target identity, failure behavior, independent oracle, operational-diff scan, and the operator-only recovery boundary. A source edit is reviewable evidence, not permission to execute it. -Operation receipts live under Git-common `boatstack/operations/v1`, never in Git history. They distinguish prepared, executing, unknown, retryable, and terminal work across turns and linked worktrees. Receipts contain hashes and bounded observations rather than commands, tool payloads, responses, credentials, or autonomous workflow intent. Terminal identities remain long enough to consume delayed duplicate events; old detail is compacted. +Operation receipts live under the current worktree's Git directory at `boatstack/operations/v2`, never in Git history. (The Git-common `operations/v1` ledger is the orphaned pre-isolation layout; `doctor` prunes it.) They distinguish prepared, executing, unknown, retryable, and terminal work across turns and linked worktrees. Receipts contain hashes and bounded observations rather than commands, tool payloads, responses, credentials, or autonomous workflow intent. Terminal identities remain long enough to consume delayed duplicate events; old detail is compacted. Installation repair receipts and backups live under Git-common `boatstack/updates/` and `boatstack/repair-backups/`. The checksum-verified target helper owns this recovery plane. Exact installed fragments migrate automatically; `--repair` covers only a displayed fingerprinted owned-state package. User-owned or ambiguous state is never converted into repair authority. @@ -103,6 +103,46 @@ Installation repair receipts and backups live under Git-common `boatstack/update When `workflow.pr_visual_evidence` is enabled, the approved plan records whether screenshots are relevant and names no more than three review scenarios. PNG bytes and capability receipts live under Git-common Boatstack state; committed ledgers retain only compact metadata and hashes. PR schema v3 binds the policy, status, count, and manifest fingerprint to the preview. Screenshots are human-review evidence rather than mechanical correctness proof. +## State ownership + +Every tree Boatstack manages has one declared owner, class, and partition. The +authoritative registry is `StateRegistry` in the runtime; this table mirrors it +and a conformance test holds the two together, so neither can drift silently. +Partitions: `checkout` lives in the working tree, `per-worktree` under the +worktree's own Git directory, `git-common` shared by every worktree of the +clone, `external` outside the repository (Detached Supervision). + +| Name | Class | Partition | Owned by | +| --- | --- | --- | --- | +| project-config | committed-generated | checkout | init, update, export | +| source-config | committed-generated | checkout | init, migrate-config, update | +| generated-references | committed-generated | checkout | init, update, export | +| guard-hooks | committed-generated | checkout | init, update, export | +| generated-lock | committed-generated | checkout | init, update, export | +| planning-artifacts | committed-planning | checkout | planning-write | +| approval-receipt | committed-planning | checkout | record-approval | +| plan-lock | committed-planning | checkout | activate-plan | +| compiled-artifacts | committed-planning | checkout | activate-plan | +| pr-preview | committed-planning | checkout | ship-gate, publish-pr | +| change-ledger | committed-planning | checkout | record-change | +| discard-archive | committed-planning | checkout | discard-delivery | +| pr-briefs | committed-planning | checkout | pr-context | +| verified-boundaries | committed-planning | checkout | record-delivery-gate | +| worktree-helper | checkout-runtime | checkout | init, update, hydrate-runtime | +| managed-worktrees | checkout-runtime | checkout | workspace-cut, workspace-cleanup, workspace-reap | +| delivery-state | runtime-worktree | per-worktree | delivery transitions | +| operation-ledger | runtime-worktree | per-worktree | run-preflight, publishers | +| flow-logs | runtime-worktree | per-worktree | flow | +| runtime-slots | runtime-shared | git-common | init, update, hydrate-runtime | +| mutation-receipts | runtime-shared | git-common | activate-plan, undo | +| update-previews | runtime-shared | git-common | prepare-update-pr, publish-update-pr | +| repair-receipts | runtime-shared | git-common | update | +| visual-evidence | runtime-shared | git-common | evidence verbs | +| quarantine | runtime-shared | git-common | repair-state | +| host-hook-config | host-activation | checkout | activation merge only | +| detached-registry | detached | external | attach, detach | +| detached-repositories | detached | external | attach, detach, activate | + ## Templates Copy only the templates required for the current slice from `assets/templates/`. Do not create empty ceremony. The feature spec, question ledger, test plan, gap ledger, and evidence ledger are the usual minimum for material product work. diff --git a/boatstack/statemap.go b/boatstack/statemap.go new file mode 100644 index 0000000..7f1d078 --- /dev/null +++ b/boatstack/statemap.go @@ -0,0 +1,243 @@ +package boatstack + +import "path/filepath" + +// The state-ownership map: one declared registry of every filesystem tree +// Boatstack manages — its class, partition, owning verbs, and whether the +// guard protects it from raw mutation. Before this map the same knowledge was +// scattered across path resolvers, guard regexes, and prose, and every +// state-partitioning defect (a clone-shared ledger, a worktree-local delivery +// state blocking a sibling, binary-vs-pin drift) was discovered by failing. +// The conformance suite in statemap_conformance_test.go holds the map, the +// WorkspaceContext resolvers, and the guard's path classifiers to each other, +// so the next divergence fails a test instead of a user. +// control-law: every-managed-path-has-a-declared-owner + +// PathClass names what kind of state a managed tree holds. +type PathClass string + +const ( + // ClassCommittedGenerated is the exported, hash-manifested bundle Boatstack + // owns wholesale and regenerates (project.json, hooks, references). + ClassCommittedGenerated PathClass = "committed-generated" + // ClassCommittedPlanning is the durable feature evidence authored through + // owned verbs and committed with the product (plans, approvals, locks, PRs). + ClassCommittedPlanning PathClass = "committed-planning" + // ClassCheckoutRuntime is reinstallable machine state living inside the + // checkout but gitignored (the pinned helper binary, managed worktrees). + ClassCheckoutRuntime PathClass = "checkout-runtime" + // ClassRuntimeWorktree is mutable control state partitioned per worktree + // under the worktree's own Git directory. + ClassRuntimeWorktree PathClass = "runtime-worktree" + // ClassRuntimeShared is control state shared by every worktree of a clone, + // under the Git common directory. + ClassRuntimeShared PathClass = "runtime-shared" + // ClassHostActivation is host-owned configuration Boatstack merges into + // (never owns wholesale): hook fragments in .claude/.cursor/.codex/.gemini. + ClassHostActivation PathClass = "host-activation" + // ClassDetached is the external per-user control root used by Detached + // Supervision (repositories/, registry.json, shared runtimes). + ClassDetached PathClass = "detached" +) + +// StateEntry declares one managed tree. +type StateEntry struct { + Name string + Class PathClass + // Partition names the isolation domain: "checkout" (inside the worktree's + // working tree), "per-worktree" (the worktree's Git dir), "git-common" + // (shared by all worktrees), or "external" (outside the repository). + Partition string + // OwnerVerbs are the helper verbs/transitions that may create or mutate the + // tree. Empty means host-owned (Boatstack only merges fragments in). + OwnerVerbs []string + Gitignored bool + // GuardProtected marks trees only Boatstack transitions may name in a raw + // mutation — exactly the set deliveryStatePathPattern denies. + GuardProtected bool + // Sample resolves one concrete representative path for conformance checks. + Sample func(w WorkspaceContext) (string, error) +} + +func staticSample(path string) func(WorkspaceContext) (string, error) { + return func(WorkspaceContext) (string, error) { return path, nil } +} + +func generatedSample(parts ...string) func(WorkspaceContext) (string, error) { + return func(w WorkspaceContext) (string, error) { + return filepath.Join(append([]string{w.GeneratedRoot()}, parts...)...), nil + } +} + +// StateRegistry returns the declared ownership map. It is a function (not a +// package variable) so entries can never be mutated by a caller. +func StateRegistry() []StateEntry { + return []StateEntry{ + { + Name: "project-config", Class: ClassCommittedGenerated, Partition: "checkout", + OwnerVerbs: []string{"init", "update", "export"}, + Sample: func(w WorkspaceContext) (string, error) { return w.ProjectConfigPath(), nil }, + }, + { + Name: "source-config", Class: ClassCommittedGenerated, Partition: "checkout", + OwnerVerbs: []string{"init", "migrate-config", "update"}, + Sample: func(w WorkspaceContext) (string, error) { return w.SourceConfigPath(), nil }, + }, + { + Name: "generated-references", Class: ClassCommittedGenerated, Partition: "checkout", + OwnerVerbs: []string{"init", "update", "export"}, + Sample: generatedSample("workflow.md"), + }, + { + Name: "guard-hooks", Class: ClassCommittedGenerated, Partition: "checkout", + OwnerVerbs: []string{"init", "update", "export"}, + Sample: generatedSample("hooks", "guard.sh"), + }, + { + Name: "generated-lock", Class: ClassCommittedGenerated, Partition: "checkout", + OwnerVerbs: []string{"init", "update", "export"}, + Sample: generatedSample("generated.lock.json"), + }, + { + Name: "planning-artifacts", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"planning-write"}, + Sample: generatedSample("features", "sample-feature", "plan.md"), + }, + { + Name: "approval-receipt", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"record-approval"}, + Sample: generatedSample("features", "sample-feature", "approval.md"), + }, + { + Name: "plan-lock", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"activate-plan"}, + Sample: generatedSample("features", "sample-feature", "plan.lock.json"), + }, + { + Name: "compiled-artifacts", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"activate-plan"}, + Sample: generatedSample("features", "sample-feature", "compiled", "tasks.json"), + }, + { + Name: "pr-preview", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"ship-gate", "publish-pr"}, + Sample: generatedSample("features", "sample-feature", "pr.md"), + }, + { + Name: "change-ledger", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"record-change"}, + Sample: generatedSample("features", "sample-feature", "changes.md"), + }, + { + Name: "discard-archive", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"discard-delivery"}, + Sample: generatedSample("features", ".discarded", "sample-feature", "plan.md"), + }, + { + Name: "pr-briefs", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"pr-context"}, + Sample: generatedSample("pr-briefs", "sample-branch", "pr.md"), + }, + { + Name: "verified-boundaries", Class: ClassCommittedPlanning, Partition: "checkout", + OwnerVerbs: []string{"record-delivery-gate"}, + Sample: generatedSample("verified-boundaries.md"), + }, + { + Name: "worktree-helper", Class: ClassCheckoutRuntime, Partition: "checkout", Gitignored: true, + OwnerVerbs: []string{"init", "update", "hydrate-runtime"}, + Sample: generatedSample("bin", "install.lock.json"), + }, + { + Name: "managed-worktrees", Class: ClassCheckoutRuntime, Partition: "checkout", Gitignored: true, + OwnerVerbs: []string{"workspace-cut", "workspace-cleanup", "workspace-reap"}, + Sample: generatedSample("worktrees", "sample-branch"), + }, + { + Name: "delivery-state", Class: ClassRuntimeWorktree, Partition: "per-worktree", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"activate-plan", "record-delivery-gate", "record-change", "publish-pr", "repair-state", "discard-delivery"}, + Sample: func(w WorkspaceContext) (string, error) { + base, err := w.DeliveryDir() + if err != nil { + return "", err + } + return filepath.Join(base, "sample-feature", "state.json"), nil + }, + }, + { + Name: "operation-ledger", Class: ClassRuntimeWorktree, Partition: "per-worktree", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"run-preflight", "publish-pr", "publish-update-pr"}, + Sample: func(w WorkspaceContext) (string, error) { + base, err := w.OperationDir() + if err != nil { + return "", err + } + return filepath.Join(base, "sample-operation.json"), nil + }, + }, + { + Name: "flow-logs", Class: ClassRuntimeWorktree, Partition: "per-worktree", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"flow"}, + Sample: func(w WorkspaceContext) (string, error) { + base, err := w.FlowDir() + if err != nil { + return "", err + } + return filepath.Join(base, "trajectory.jsonl"), nil + }, + }, + { + Name: "runtime-slots", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"init", "update", "hydrate-runtime"}, + Sample: func(w WorkspaceContext) (string, error) { + base, err := w.RuntimeDir("v0.0.0", "0000000") + if err != nil { + return "", err + } + return filepath.Join(base, "runtime.lock.json"), nil + }, + }, + { + Name: "mutation-receipts", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"activate-plan", "undo"}, + Sample: staticSample(filepath.FromSlash(".git/boatstack/mutations/v1/sample.json")), + }, + { + Name: "update-previews", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"prepare-update-pr", "publish-update-pr"}, + Sample: staticSample(filepath.FromSlash(".git/boatstack/updates/v0.0.0/pr-preview.json")), + }, + { + Name: "repair-receipts", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"update"}, + Sample: staticSample(filepath.FromSlash(".git/boatstack/updates/v0.0.0/repair.json")), + }, + { + Name: "visual-evidence", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"record-pr-visual-evidence", "capture-evidence", "record-pr-visual-publication"}, + Sample: staticSample(filepath.FromSlash(".git/boatstack/visual-evidence/sample/manifest.json")), + }, + { + Name: "quarantine", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, + OwnerVerbs: []string{"repair-state"}, + Sample: staticSample(filepath.FromSlash(".git/boatstack/quarantine/sample-feature/receipt.json")), + }, + { + Name: "host-hook-config", Class: ClassHostActivation, Partition: "checkout", + // Host-owned files Boatstack merges hook fragments into; never owned + // wholesale, so no owner verbs beyond the activation merge. + OwnerVerbs: []string{"init", "update", "activate"}, + Sample: staticSample(filepath.FromSlash(".claude/settings.json")), + }, + { + Name: "detached-registry", Class: ClassDetached, Partition: "external", GuardProtected: true, + OwnerVerbs: []string{"attach", "detach"}, + Sample: staticSample(filepath.FromSlash("state-root/boatstack/registry.json")), + }, + { + Name: "detached-repositories", Class: ClassDetached, Partition: "external", GuardProtected: true, + OwnerVerbs: []string{"attach", "detach", "activate"}, + Sample: staticSample(filepath.FromSlash("state-root/boatstack/repositories/sample/binding.json")), + }, + } +} diff --git a/boatstack/statemap_conformance_test.go b/boatstack/statemap_conformance_test.go new file mode 100644 index 0000000..dad8864 --- /dev/null +++ b/boatstack/statemap_conformance_test.go @@ -0,0 +1,264 @@ +package boatstack + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" +) + +// control-law: every-managed-path-has-a-declared-owner +// +// The state-ownership map (statemap.go) is the single declaration of every +// tree Boatstack manages: class, partition, owning verbs, guard protection. +// These tests hold the declaration, the WorkspaceContext resolvers, the +// guard's path classifiers, the exported ownership doc, and the package's +// hand-joined path literals to each other — so the next state-partitioning +// divergence fails here instead of surfacing as a live defect. + +func statemapContext(t *testing.T) WorkspaceContext { + t.Helper() + repo := safetyTestRepo(t) + w, err := ResolveWorkspaceContext(repo) + if err != nil { + t.Fatalf("resolve workspace context: %v", err) + } + return w +} + +func entrySample(t *testing.T, w WorkspaceContext, entry StateEntry) string { + t.Helper() + sample, err := entry.Sample(w) + if err != nil { + t.Fatalf("entry %s sample: %v", entry.Name, err) + } + if sample == "" { + t.Fatalf("entry %s resolves an empty sample", entry.Name) + } + return filepath.ToSlash(sample) +} + +// Positive: every WorkspaceContext resolver's output is owned by exactly one +// registry entry of the expected class — the resolvers and the declaration +// cannot drift apart. +func TestEveryWorkspaceResolverIsDeclared(t *testing.T) { + w := statemapContext(t) + registry := StateRegistry() + + resolve := func(f func() (string, error)) string { + path, err := f() + if err != nil { + t.Fatal(err) + } + return filepath.ToSlash(path) + } + resolverOutputs := map[string]struct { + path string + wantClass PathClass + }{ + "GeneratedRoot": {filepath.ToSlash(w.GeneratedRoot()), ClassCommittedGenerated}, + "ProjectConfigPath": {filepath.ToSlash(w.ProjectConfigPath()), ClassCommittedGenerated}, + "SourceConfigPath": {filepath.ToSlash(w.SourceConfigPath()), ClassCommittedGenerated}, + "DeliveryDir": {resolve(w.DeliveryDir), ClassRuntimeWorktree}, + "OperationDir": {resolve(w.OperationDir), ClassRuntimeWorktree}, + "FlowDir": {resolve(w.FlowDir), ClassRuntimeWorktree}, + "RuntimeDir": {resolve(func() (string, error) { + return w.RuntimeDir("v0.0.0", "0000000") + }), ClassRuntimeShared}, + } + + for name, want := range resolverOutputs { + owners := 0 + var ownerClass PathClass + for _, entry := range registry { + sample := entrySample(t, w, entry) + // A resolver is "declared" when some entry's sample lives at or under + // its output (the entry is the concrete artifact inside the tree). + if sample == want.path || strings.HasPrefix(sample, want.path+"/") { + if entry.Class != ownerClass { + owners++ + ownerClass = entry.Class + } + } + } + if owners == 0 { + t.Errorf("resolver %s (%s) has no declared owner in the state registry", name, want.path) + continue + } + if ownerClass != want.wantClass && name != "GeneratedRoot" { + t.Errorf("resolver %s owned by class %s, want %s", name, ownerClass, want.wantClass) + } + } + // GeneratedRoot hosts multiple classes by design (generated bundle, planning, + // checkout runtime) — assert it is covered, which the loop above did. +} + +// Negative: the declaration is well-formed — unique names, non-empty owners, +// and no two entries of different classes resolve the same sample path. +func TestStateRegistryIsWellFormed(t *testing.T) { + w := statemapContext(t) + seenNames := map[string]bool{} + seenSamples := map[string]PathClass{} + for _, entry := range StateRegistry() { + if seenNames[entry.Name] { + t.Errorf("duplicate entry name %s", entry.Name) + } + seenNames[entry.Name] = true + if len(entry.OwnerVerbs) == 0 { + t.Errorf("entry %s declares no owner", entry.Name) + } + if entry.Partition == "" || entry.Class == "" { + t.Errorf("entry %s missing partition or class: %+v", entry.Name, entry) + } + sample := entrySample(t, w, entry) + if class, dup := seenSamples[sample]; dup && class != entry.Class { + t.Errorf("sample %s claimed by two classes: %s and %s", sample, class, entry.Class) + } + seenSamples[sample] = entry.Class + } +} + +// Relation: the guard's path classifiers agree with the declaration exactly. +// deliveryStatePathPattern denies precisely the GuardProtected trees, and the +// planning first-write latch covers precisely the features/-resident +// committed-planning trees. +func TestGuardClassifiersMatchDeclaredOwnership(t *testing.T) { + w := statemapContext(t) + repoRoot := filepath.ToSlash(w.RepoRoot) + for _, entry := range StateRegistry() { + sample := entrySample(t, w, entry) + if got := deliveryStatePathPattern.MatchString(sample); got != entry.GuardProtected { + t.Errorf("guard pattern(%s)=%t but declaration says GuardProtected=%t (sample %s)", entry.Name, got, entry.GuardProtected, sample) + } + if entry.Class == ClassCommittedPlanning { + relative := strings.TrimPrefix(strings.TrimPrefix(sample, repoRoot), "/") + underFeatures := strings.HasPrefix(relative, ".product-loop/features/") + if featureScopedPath(relative) != underFeatures { + t.Errorf("first-write latch scope disagrees for %s (%s)", entry.Name, relative) + } + } + } +} + +// Bypass: no NEW file may hand-join ".product-loop" paths without consciously +// extending this allowlist — the frozen inventory of declaring files. Growth +// pressure should flow toward WorkspaceContext/statemap, not new literals. +func TestProductLoopLiteralsStayInDeclaredFiles(t *testing.T) { + allowed := map[string]bool{ + "activation.go": true, "delivery.go": true, "export.go": true, + "flow_control.go": true, "flow_tasks.go": true, "hooks.go": true, + "init.go": true, "installation_repair.go": true, "mutation_undo.go": true, + "next.go": true, "paths.go": true, "planning.go": true, "pr.go": true, + "recovery.go": true, "runtime_cache.go": true, "safety.go": true, + "update.go": true, "update_publication.go": true, + "workspace.go": true, + // denial.go names .product-loop/features/ only in user-facing denial + // copy (the owned-channel guidance), never as a joined path. + "denial.go": true, + } + + fset := token.NewFileSet() + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + offenders := map[string]bool{} + scanned := 0 + for _, item := range entries { + name := item.Name() + if item.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + file, err := parser.ParseFile(fset, name, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", name, err) + } + scanned++ + ast.Inspect(file, func(n ast.Node) bool { + lit, ok := n.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + value, err := strconv.Unquote(lit.Value) + if err != nil { + return true + } + if strings.Contains(value, ".product-loop") && !allowed[name] { + offenders[name] = true + } + return true + }) + } + if scanned < 20 { + t.Fatalf("scanned only %d files; the literal guarantee would be vacuous", scanned) + } + if len(offenders) > 0 { + names := make([]string, 0, len(offenders)) + for name := range offenders { + names = append(names, name) + } + sort.Strings(names) + t.Fatalf("new hand-joined .product-loop literals outside the declared files: %v — route the path through WorkspaceContext/statemap or consciously extend the allowlist", names) + } + // Reverse check: a file on the allowlist that no longer carries a literal is + // stale — shrink the list so the freeze stays honest. + for name := range allowed { + content, err := os.ReadFile(name) + if err != nil { + t.Fatalf("allowlisted file %s unreadable: %v", name, err) + } + if !strings.Contains(string(content), ".product-loop") { + t.Errorf("allowlist entry %s is stale — it no longer names .product-loop", name) + } + } +} + +// Failure-state / doc drift: the exported ownership table mirrors the registry +// exactly, and the stale pre-isolation ledger path is gone from the doc. +func TestOwnershipDocMirrorsRegistry(t *testing.T) { + content, err := os.ReadFile(filepath.Join("references", "artifacts.md")) + if err != nil { + t.Fatal(err) + } + doc := string(content) + if strings.Contains(doc, "Git-common `boatstack/operations/v1`") { + t.Fatal("artifacts.md still documents the orphaned Git-common operations/v1 ledger as current") + } + + documented := map[string]bool{} + inTable := false + for _, line := range strings.Split(doc, "\n") { + if strings.HasPrefix(line, "## State ownership") { + inTable = true + continue + } + if inTable && strings.HasPrefix(line, "## ") { + break + } + if !inTable || !strings.HasPrefix(line, "| ") || strings.HasPrefix(line, "| ---") || strings.HasPrefix(line, "| Name") { + continue + } + cells := strings.Split(line, "|") + if len(cells) > 1 { + documented[strings.TrimSpace(cells[1])] = true + } + } + + registry := map[string]bool{} + for _, entry := range StateRegistry() { + registry[entry.Name] = true + if !documented[entry.Name] { + t.Errorf("registry entry %s missing from the artifacts.md ownership table", entry.Name) + } + } + for name := range documented { + if !registry[name] { + t.Errorf("artifacts.md documents %s which is not in the registry", name) + } + } +} diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 523a2f5..f40674f 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -96,7 +96,7 @@ subject to acceptance criteria pass approval is current ``` -That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **20237 estimated tokens**, while host adapters point to one operation at a time. +That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **20915 estimated tokens**, while host adapters point to one operation at a time. ## Control appears at transitions @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`039454bde99f8059e1a8ee0356ef433f7837cd74`](https://github.com/operatorstack/intelligence-flow/tree/039454bde99f8059e1a8ee0356ef433f7837cd74/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`4b31ab38875160d6ef71a85c65efcdb4bd7ad91b`](https://github.com/operatorstack/intelligence-flow/tree/4b31ab38875160d6ef71a85c65efcdb4bd7ad91b/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index fe35641..2889e98 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "039454bde99f8059e1a8ee0356ef433f7837cd74", + "source_commit": "4b31ab38875160d6ef71a85c65efcdb4bd7ad91b", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:039454bde99f8059e1a8ee0356ef433f7837cd74" + "last_verified_version": "source:4b31ab38875160d6ef71a85c65efcdb4bd7ad91b" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 59dc000..07f8a2e 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "039454bde99f8059e1a8ee0356ef433f7837cd74", + "source_commit": "4b31ab38875160d6ef71a85c65efcdb4bd7ad91b", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-07-27-state-ownership-map.md b/release-notes/2026-07-27-state-ownership-map.md new file mode 100644 index 0000000..df7285d --- /dev/null +++ b/release-notes/2026-07-27-state-ownership-map.md @@ -0,0 +1,7 @@ +### Every managed path now has a declared owner + +Boatstack keeps state in several places — committed feature evidence under `.product-loop/`, per-worktree control state under the worktree's Git directory, clone-shared runtime slots and receipts under the Git common directory, and an external root for detached supervision. Until now that layout lived implicitly in path resolvers, guard patterns, and prose, and each partitioning defect (a clone-shared ledger, one worktree's state blocking another, a stale binary next to a fresh pin) was discovered by hitting it. + +The layout is now a single declared registry: every managed tree names its class, its partition, the verbs that own it, and whether the guard protects it from raw mutation. A conformance suite holds the registry, the path resolvers, the guard's classifiers, and the exported ownership table in `artifacts.md` to each other — none of them can drift silently. A frozen inventory also pins which source files may hand-join managed paths, so new code is steered through the shared resolvers instead of new string literals. + +The documentation was corrected along the way: operation receipts live under the current worktree's Git directory (`operations/v2`), not the old clone-shared location the docs still described.