From fe469919a0d83d449b9f343cb05d9a1f46ca971c Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 8 Sep 2026 15:50:07 +0200 Subject: [PATCH] [ticket-094] fix: adopt published integration-base validation --- .governance/error/GOV-WORKTREE-OVERLAP.md | 13 +++- .governance/governance_check.py | 25 +++++++ .governance/manifest.base.json | 2 +- .governance/manifest.json | 2 +- .governance/manifest.lock.json | 18 ++--- .governance/worktree_overlap_check.py | 40 ++++++++-- .governance/worktree_path_check.py | 20 +++-- .governance/worktrees.lock.json | 8 +- .governance/worktrees.schema.json | 3 +- project/ticket-094/README.md | 18 +++++ project/ticket-094/intent.json | 90 +++++++++++++++++++++++ pyproject.toml | 4 +- 12 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 project/ticket-094/README.md create mode 100644 project/ticket-094/intent.json diff --git a/.governance/error/GOV-WORKTREE-OVERLAP.md b/.governance/error/GOV-WORKTREE-OVERLAP.md index 395740a..953aae4 100644 --- a/.governance/error/GOV-WORKTREE-OVERLAP.md +++ b/.governance/error/GOV-WORKTREE-OVERLAP.md @@ -13,9 +13,16 @@ równoległe worktree są dozwolone, nachodzące zmiany nie. ## Meaning `001` — rzeczywisty konkurencyjny wkład: brudne zmiany wobec brudnych zmian -lub nowych commitów drugiej strony od wspólnego przodka; dla rozbieżnych -commitów sprawdzany jest merge. Wspólna historia nie jest drugim wkładem. -Brak możliwości ustalenia przodka zachowuje konserwatywne sprawdzanie. +lub nowych commitów drugiej strony. Gdy oba checkouty widzą ten sam SHA +`origin/`, wkład każdego jest liczony od jego wspólnego przodka z +tym SHA. Zmiany odziedziczone z main nie należą do nowego writera. +Dla rozbieżnych commitów sprawdzany jest merge; konflikt przypisuje się parze +tylko na ścieżce, na której obie strony wnoszą wkład. Konflikt starej gałęzi +z main pozostaje widoczny w jej stanie, ale nie blokuje niezależnego writera. +Brak zgodnych refów albo odczytu obiektów zachowuje konserwatywne sprawdzanie +od wspólnego przodka pary. Tak samo traktowane są historie ze zmianą nazwy: +Git może zgłosić konflikt pod inną ścieżką niż pierwotna edycja. Checker nie +pobiera refów i nie zatwierdza merge'a. `002` — dwa `IN_PROGRESS` intent.json w różnych worktree deklarują nachodzące `allowedPaths` i żadne nie wymienia drugiego w `conflictsWith`. `003` — audyt nie dał się bezpiecznie dokończyć. diff --git a/.governance/governance_check.py b/.governance/governance_check.py index 7b2af30..133ab65 100755 --- a/.governance/governance_check.py +++ b/.governance/governance_check.py @@ -2601,6 +2601,26 @@ def check_delivery_timebox( ) +def is_published_integration(root: Path, target: str, supplied_base: str | None) -> bool: + """Recognize one clean integration already observed on the target branch. + + The supplied first parent is the target before this integration. Comparing + the accepted base with the published HEAD would include the ticket's own + changes as intervening drift. Ambiguous ranges and dirty trees retain the + conservative target comparison; this observation never grants approval. + """ + if not supplied_base: + return False + try: + return ( + git_output(root, ["rev-parse", "HEAD"]).decode().strip() == target + and git_output(root, ["rev-parse", f"{target}^1"]).decode().strip() == supplied_base + and not git_output(root, ["status", "--porcelain"]) + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + def check_delivery_base( root: Path, policy: dict[str, Any], @@ -2649,6 +2669,11 @@ def check_delivery_base( current_target = git_output(root, ["rev-parse", "--verify", f"{target_ref}^{{commit}}"]).decode().strip() except (subprocess.CalledProcessError, FileNotFoundError): continue + supplied_base = next((sha for source, sha in observed if source == "suppliedBase"), None) + if is_published_integration(root, current_target, supplied_base): + # The accepted-to-supplied-base check below still detects changes + # that landed on the target before this ticket was integrated. + break observed.append((target_ref, current_target)) break diff --git a/.governance/manifest.base.json b/.governance/manifest.base.json index e36299e..2c36174 100644 --- a/.governance/manifest.base.json +++ b/.governance/manifest.base.json @@ -110,7 +110,7 @@ "stacks": [], "standard": { "id": "wellmanifest/new-project", - "version": "0.20.13" + "version": "0.20.16" }, "ticket": { "activeStatuses": [ diff --git a/.governance/manifest.json b/.governance/manifest.json index 014aed4..f7760e8 100644 --- a/.governance/manifest.json +++ b/.governance/manifest.json @@ -194,7 +194,7 @@ ], "standard": { "id": "wellmanifest/new-project", - "version": "0.20.13" + "version": "0.20.16" }, "ticket": { "activeStatuses": [ diff --git a/.governance/manifest.lock.json b/.governance/manifest.lock.json index b10cd2f..2a6da86 100644 --- a/.governance/manifest.lock.json +++ b/.governance/manifest.lock.json @@ -36,13 +36,13 @@ ".governance/error/GOV-TICKET-ALLOCATION.md": "09f92cac24bbbbe5c2967221497fb6b68b02bcd3bf4f56afe36d36ac7d7b0a58", ".governance/error/GOV-WORK-CONTINUITY.md": "de46a4adc51e8dd5880a83a6d9585ecdbbea4ae30a44ee6506c207ea2e8bc8eb", ".governance/error/GOV-WORKSPACE-LIFECYCLE.md": "72436684572d784327a340d90ca96d9fd2927519e02334036fab402e9c1d8262", - ".governance/error/GOV-WORKTREE-OVERLAP.md": "3f0170924de9c316cfc7313b3f0d37635e47181714ce5542fdeac054fd08124e", + ".governance/error/GOV-WORKTREE-OVERLAP.md": "65a2533f13e63d6ebeeb63c07adc0794ea9e04075a91e873eff2d79910239b0e", ".governance/error/README.md": "e8486dd29f52ca3fee96ed6881a62c38141864cde5aa1adea2b16d22b2feefaa", ".governance/generate_required_checks.py": "aa44610d8ec26aaf4993cec194ce16338d458b567f7d06a2c34b470b9b76f6cc", - ".governance/governance_check.py": "305fc2fbf1c01a8a05e514346aa244977efabedd348379ebffd7e6562949fa17", + ".governance/governance_check.py": "290f8dee1c2e6b24c3a195ae548bedf796f5ea3447b2518cc2a737e8437f71d6", ".governance/intent.schema.json": "9755f20cd189efe2205629781a6cf6b613bd93e9368416793c4de53256b0a9c0", ".governance/lock.schema.json": "ad80c98f800a4a3310870336dcdaf0aa689cc4988f71084d25d76bea2df1242f", - ".governance/manifest.base.json": "763a046bb830631e3a6fb94f6a35403620918a02d4956d7573a8dcd03e4ba1ab", + ".governance/manifest.base.json": "87fb6c9a351c4893cfa7ba15d39f00079dafe366ef26dd9bf435000719bfc665", ".governance/manifest.schema.json": "5aa2ccd3f6898834d4e39a78342448145490be56aa132e16ac7c9d64acef8f73", ".governance/package-manifest.json": "a2ea77aca16d59282e1b60105521270ae112ebc52ddb3e08af3eeec26942dc3f", ".governance/precommit_standard_update.py": "c91e2bf9ae9d6ccc77bce0e61450c818a5961edee5bfde3b60426da88e296b0f", @@ -69,10 +69,10 @@ ".governance/work_continuity.py": "2efb720b530cc295b45413a2bc7384a426bd29b5d9b20452035e5943294c18e4", ".governance/workspace_lifecycle_check.py": "f196f33c4884120ca216e65473f32b163f6c6d262624309cf0cbdc37063d181f", ".governance/worktree_guard.py": "b154f6e67626770ec11c9544d31a27b32d9c215ef73ffc9eb8f52e9c3a9b051b", - ".governance/worktree_overlap_check.py": "dfb6463889617352578576ad699f8baa4246c013cf943e1b427a76176d7e19a5", - ".governance/worktree_path_check.py": "d3309b76e2c91dd7f046b00322ec8cb48ef744b8b7908d70e475fedf95e5e196", - ".governance/worktrees.lock.json": "9ed607dd339fd4ed30263d7de62231249447568e8e35c8b19fdc0231fca39fde", - ".governance/worktrees.schema.json": "9cc10d126e06cafc87cc117f11b8b095676f461de4d168d2a1c2bb959f0fccd5", + ".governance/worktree_overlap_check.py": "582fb82588aa1cf40af5213e9e931f06ae90824e59b5ec5deb16ae7d6beb09b8", + ".governance/worktree_path_check.py": "4d3e8457023eccd417a472be9c46a975dbc46c04d38c204eecd92a8583bb996e", + ".governance/worktrees.lock.json": "319349217c525d28def89d155dade1cfcee637f536dd852dc87287612c47731f", + ".governance/worktrees.schema.json": "bb5989c19ee33d9beafa34576ef568ef70384a664ccf763ac2e29dde3a464756", ".subactor/.gitignore": "dd223aed5e053f94c6808ac434368c16eeca7e77218f8426e8cf5e46ae441d03", ".subactor/manifest.json": "ab8b1cbe4052a6f0005a3c33a43fa2a70e8837cd32c18b4183d0d448c3a8cee2", "AGENTS.md": "12641a33dcfc24c353834e95d09d4784c42ec035dcf350dd153b55eb02182364", @@ -92,7 +92,7 @@ "id": "wellmanifest/new-project", "publicationStatus": "published", "sourceRepository": "wellmanifest/new-project", - "sourceRevision": "824625238d697511f58be2b73c0aa6e15c228a9a", - "version": "0.20.13" + "sourceRevision": "6d2da011088b69ebe1636f3bf681e5ec21a062ab", + "version": "0.20.16" } } diff --git a/.governance/worktree_overlap_check.py b/.governance/worktree_overlap_check.py index 0be2b0a..e2ba694 100755 --- a/.governance/worktree_overlap_check.py +++ b/.governance/worktree_overlap_check.py @@ -482,14 +482,15 @@ def contested_paths( ) -> tuple[str, ...]: """Paths these two checkouts genuinely contend for. - Compare each dirty delta with the peer's contribution since their shared - history, not with everything inherited from the default branch. An inert - snapshot at the same HEAD contributes no competing committed change. - Unknown ancestry retains the conservative path-intersection fallback. + Prefer each writer's contribution relative to the same observed origin + default-branch revision. A pair's older common ancestor includes main's + history in a fresh writer, even when that writer edits unrelated files. + Missing or divergent observations retain the common-ancestor fallback. """ first_dirty = set(first.dirty_paths) - pending_main_imports(first.path) second_dirty = set(second.dirty_paths) - pending_main_imports(second.path) first_changes, second_changes = set(first.changed_paths), set(second.changed_paths) + shared_default = False if first.head and second.head: base = first.head if first.head == second.head else merge_base(first.path, first.head, second.head) if base: @@ -502,6 +503,30 @@ def contested_paths( second_changes = second_committed | second_dirty except AuditError: pass + try: + first_default = run_git(first.path, "rev-parse", "--verify", + f"refs/remotes/origin/{default_branch(first.path)}^{{commit}}") + second_default = run_git(second.path, "rev-parse", "--verify", + f"refs/remotes/origin/{default_branch(second.path)}^{{commit}}") + if first_default == second_default: + first_base = run_git(first.path, "merge-base", first.head, first_default) + second_base = run_git(second.path, "merge-base", second.head, second_default) + first_committed = set(run_git(first.path, "diff", "--name-only", "--no-renames", first_base, first.head).splitlines()) + second_committed = set(run_git(second.path, "diff", "--name-only", "--no-renames", second_base, second.head).splitlines()) + first_renames = run_git(first.path, "diff", "--name-only", "--find-renames", + "--diff-filter=R", first_base, first.head) + second_renames = run_git(second.path, "diff", "--name-only", "--find-renames", + "--diff-filter=R", second_base, second.head) + # Replace both sides only after every strict Git read succeeds. + # Rename/directory-rename conflicts can be reported at a path + # edited under another name. Preserve the conservative path + # model until attribution can follow those identities too. + if not first_renames and not second_renames: + first_changes = first_committed | first_dirty + second_changes = second_committed | second_dirty + shared_default = True + except AuditError: + pass dirty_overlap = (first_dirty & second_changes) | (second_dirty & first_changes) conflicts: set[str] = set() if first.head and second.head and first.head != second.head: @@ -511,9 +536,14 @@ def contested_paths( reported = merge_tree_conflicts(first.path, first.head, second.head) if reported is None: # No usable merge-tree: fall back to the path-intersection proxy. - conflicts = set(first.changed_paths) & set(second.changed_paths) + conflicts = first_changes & second_changes else: conflicts = set(reported) + if shared_default: + # A branch can conflict with main without contending with + # this particular peer. Keep the conflict in its inventory, + # but require contributions from both writers for pairing. + conflicts &= first_changes & second_changes return tuple( sorted( name diff --git a/.governance/worktree_path_check.py b/.governance/worktree_path_check.py index a509631..5566a33 100755 --- a/.governance/worktree_path_check.py +++ b/.governance/worktree_path_check.py @@ -34,6 +34,16 @@ def _validate_segment(label: str, value: str) -> None: raise ValueError(f"{label} must contain lowercase ASCII words separated by hyphens") +def _validate_repository_name(repository_name: str) -> None: + if ( + not isinstance(repository_name, str) + or not repository_name + or repository_name in {".", ".."} + or any(character in repository_name for character in ("/", "\\", "\0")) + ): + raise ValueError("repositoryName must be an observed repository basename") + + def plan( *, repository: str, @@ -44,7 +54,7 @@ def plan( path_style: str = "posix", ) -> dict[str, str]: """Return the canonical v5 layout record for one delivery unit.""" - _validate_segment("repositoryName", repository_name) + _validate_repository_name(repository_name) _validate_segment("slug", slug) ticket_match = TICKET_RE.fullmatch(ticket) if not ticket_match: @@ -303,13 +313,7 @@ def inventory( path_style: str = "posix", ) -> dict[str, Any]: """Build a deterministic, observation-only inventory record.""" - if ( - not isinstance(repository_name, str) - or not repository_name - or repository_name in {".", ".."} - or any(character in repository_name for character in ("/", "\\", "\0")) - ): - raise ValueError("repositoryName must be an observed repository basename") + _validate_repository_name(repository_name) path_type = _path_type(path_style) primary = path_type(primary_checkout) if not primary.is_absolute(): diff --git a/.governance/worktrees.lock.json b/.governance/worktrees.lock.json index 75cfd1b..d9acb40 100644 --- a/.governance/worktrees.lock.json +++ b/.governance/worktrees.lock.json @@ -2,22 +2,22 @@ "schema": "new-project.worktrees-lock/v1", "dependency": { "id": "wellmanifest/worktrees", - "version": "0.5.1", + "version": "0.5.2", "sourceRepository": "wellmanifest/worktrees", - "sourceRevision": "81e0d750f18ecace4436706250bf5deb190a000a" + "sourceRevision": "44f1686dd041554649720e171d690944afa49586" }, "artifacts": [ { "sourcePath": "models/worktrees.schema.json", "packageSourcePath": "subprojects/worktrees/worktrees.schema.json", "managedTargetPath": ".governance/worktrees.schema.json", - "sourceSha256": "9cc10d126e06cafc87cc117f11b8b095676f461de4d168d2a1c2bb959f0fccd5" + "sourceSha256": "bb5989c19ee33d9beafa34576ef568ef70384a664ccf763ac2e29dde3a464756" }, { "sourcePath": "operations/conformance.py", "packageSourcePath": "subprojects/worktrees/conformance.py", "managedTargetPath": ".governance/worktree_path_check.py", - "sourceSha256": "d3309b76e2c91dd7f046b00322ec8cb48ef744b8b7908d70e475fedf95e5e196" + "sourceSha256": "4d3e8457023eccd417a472be9c46a975dbc46c04d38c204eecd92a8583bb996e" } ], "installation": { diff --git a/.governance/worktrees.schema.json b/.governance/worktrees.schema.json index e0d5d7c..1212c65 100644 --- a/.governance/worktrees.schema.json +++ b/.governance/worktrees.schema.json @@ -8,8 +8,7 @@ ], "$defs": { "repositoryName": { - "type": "string", - "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" + "$ref": "#/$defs/observedRepositoryName" }, "observedRepositoryName": { "type": "string", diff --git a/project/ticket-094/README.md b/project/ticket-094/README.md new file mode 100644 index 0000000..9f84f90 --- /dev/null +++ b/project/ticket-094/README.md @@ -0,0 +1,18 @@ +# Ticket 094: Adopt published integration-base validation + +- **Owner**: codex +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION + +SESSION_EXECUTION_AUTHORIZATION: the user requests continuation, fixes, tests and GitHub publication. Wait for the independently approved immutable standard release 0.20.16, then adopt it through Goal and verify the post-merge CI result. This does not wait for another chat approval. + +## Acceptance criteria + +- [x] AC-01: Adopt published standard 0.20.16 while preserving target extensions and immutable provenance. +- [ ] AC-02: Full tests and required checks pass, protected publication succeeds, and the main-branch CI verifies the integrated result without GOV-BASE-002. + +Existing primary metadata and other agents' workspaces remain preserved. + +Published dependency: wellmanifest/new-project v0.20.16, immutable source `6d2da011088b69ebe1636f3bf681e5ec21a062ab`, approved and merged in PR #309. + +Validation before publication: 735 tests passed, 2 pre-existing skips; all 10 published integration-base regression cases passed against digest-verified adopted code. Governance passed with 0 errors and 0 warnings; immutable adoption, active host hooks, local pin and Compose validation passed. Package standard/revision declarations were regenerated from the lock; product package version and dependencies remain unchanged. AC-02 remains pending protected merge and the main-branch CI run. diff --git a/project/ticket-094/intent.json b/project/ticket-094/intent.json new file mode 100644 index 0000000..994877b --- /dev/null +++ b/project/ticket-094/intent.json @@ -0,0 +1,90 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-094", + "summary": "Adopt the immutable published fix for integration-base validation", + "workstream": "governance", + "classification": { + "kind": "BUG", + "priority": "P1", + "origin": "regression" + }, + "allowedPaths": [ + ".governance/**", + "pyproject.toml", + "project/ticket-094/**" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [ + "python", + "docker" + ], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "c684b082618c8b7ba0cd54ca03d04aeff1f771b3", + "targetBranch": "main", + "outcome": "Adopt approved standard 0.20.16, restore green post-merge main CI, and verify the actual local Goal installation.", + "nonGoals": [ + "Change product CLI behavior beyond the published governance update.", + "Publish a new Goal package version." + ], + "complexity": "L", + "estimatedMinutes": 30, + "budgets": { + "maxImplementationFiles": 15, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Use the immutable published standard through Goal and preserve all existing target-owned extensions. Update only the standard/revision package declaration from the adoption lock, through the standard atomic-adoption binding.", + "components": [ + { + "name": "governance-adoption", + "paths": [ + ".governance/**", + "pyproject.toml" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Re-adopt the previous published source through protected delivery." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "goal governance adopt --check", + "./scripts/install-agent-hosts.sh --check" + ], + "evidence": "Published immutable source revision and managed payload digests." + }, + { + "criterion": "AC-02", + "commands": [ + "./project/governance-check.sh", + "python3 -m pytest tests/ -q", + "docker compose config --quiet" + ], + "evidence": "Required Python CI on the PR and after integration; fresh trusted merge and deployment readback." + } + ], + "standardAdoption": { + "sourceRepository": "wellmanifest/new-project", + "fromRevision": "824625238d697511f58be2b73c0aa6e15c228a9a", + "toRevision": "6d2da011088b69ebe1636f3bf681e5ec21a062ab" + } + } +} diff --git a/pyproject.toml b/pyproject.toml index 3f715de..984c7aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,8 +71,8 @@ python_files = ["test_*.py"] addopts = "-p wellmanifest_governance" [tool.wellmanifest] -standard = "0.20.13" -revision = "824625238d697511f58be2b73c0aa6e15c228a9a" +standard = "0.20.16" +revision = "6d2da011088b69ebe1636f3bf681e5ec21a062ab" gate = "project/governance-check.sh" [tool.pfix]