diff --git a/.github/workflows/new-project-governance.yml b/.github/workflows/new-project-governance.yml new file mode 100644 index 0000000..bc2dbed --- /dev/null +++ b/.github/workflows/new-project-governance.yml @@ -0,0 +1,73 @@ +name: new-project-governance + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +jobs: + remote-lifecycle: + name: governance / remote lifecycle + runs-on: ubuntu-latest + steps: + - name: Check out governed repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Acquire GitHub branch lifecycle snapshot + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + BRANCH_LIFECYCLE_SNAPSHOT: ${{ runner.temp }}/new-project-branch-lifecycle.json + with: + script: | + const fs = require('fs'); + const repository = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + const settings = await github.graphql(` + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { deleteBranchOnMerge } + } + `, {owner: context.repo.owner, repo: context.repo.repo}); + const branches = await github.paginate(github.rest.repos.listBranches, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }); + const pulls = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + const snapshot = { + schema: 'new-project.branch-lifecycle-snapshot/v1', + repository: `${context.repo.owner}/${context.repo.repo}`, + defaultBranch: repository.data.default_branch, + deleteBranchOnMerge: settings.repository.deleteBranchOnMerge, + branches: branches.map(branch => branch.name).sort(), + openPullRequests: pulls.map(pull => ({ + number: pull.number, + headRepository: pull.head.repo?.full_name ?? null, + headRef: pull.head.ref, + })).sort((left, right) => left.number - right.number), + }; + fs.writeFileSync( + process.env.BRANCH_LIFECYCLE_SNAPSHOT, + `${JSON.stringify(snapshot)}\n`, + {encoding: 'utf8', mode: 0o600}, + ); + - name: Validate remote branch lifecycle + shell: bash + env: + BRANCH_LIFECYCLE_SNAPSHOT: ${{ runner.temp }}/new-project-branch-lifecycle.json + run: | + python3 .governance/branch_lifecycle_check.py \ + --snapshot "$BRANCH_LIFECYCLE_SNAPSHOT" \ + --expected-repository "$GITHUB_REPOSITORY" \ + --format text diff --git a/.governance/approval-evidence.schema.json b/.governance/approval-evidence.schema.json new file mode 100644 index 0000000..b22e8d0 --- /dev/null +++ b/.governance/approval-evidence.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/approval-evidence.schema.json", + "title": "new-project trusted merge approval evidence", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "source", + "repository", + "pullRequest", + "headSha", + "ticket", + "actor", + "verification" + ], + "properties": { + "schema": { "const": "new-project.approval-evidence/v1" }, + "source": { + "enum": ["github-review", "github-app-review", "signed-attestation"] + }, + "repository": { + "type": "string", + "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" + }, + "pullRequest": { "type": "integer", "minimum": 1 }, + "headSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "ticket": { "type": "string", "pattern": "^ticket-[0-9]{3}$" }, + "actor": { + "type": "object", + "additionalProperties": false, + "required": ["login", "type"], + "properties": { + "login": { "type": "string", "minLength": 1 }, + "type": { "enum": ["User", "Bot", "Workflow"] } + } + }, + "verification": { + "type": "object", + "additionalProperties": false, + "required": ["method", "verified"], + "properties": { + "method": { + "enum": ["github-api-allowlist", "github-attestation", "sigstore"] + }, + "verified": { "const": true }, + "issuer": { "type": "string", "minLength": 1 }, + "predicateType": { "type": "string", "minLength": 1 } + } + } + }, + "allOf": [ + { + "if": { "properties": { "source": { "const": "github-review" } } }, + "then": { + "properties": { + "actor": { "properties": { "type": { "const": "User" } } }, + "verification": { + "properties": { "method": { "const": "github-api-allowlist" } } + } + } + } + }, + { + "if": { "properties": { "source": { "const": "github-app-review" } } }, + "then": { + "properties": { + "actor": { "properties": { "type": { "const": "Bot" } } }, + "verification": { + "properties": { "method": { "const": "github-api-allowlist" } } + } + } + } + }, + { + "if": { "properties": { "source": { "const": "signed-attestation" } } }, + "then": { + "properties": { + "verification": { + "required": ["method", "verified", "issuer", "predicateType"], + "properties": { + "method": { "enum": ["github-attestation", "sigstore"] } + } + } + } + } + } + ] +} diff --git a/.governance/branch_lifecycle_check.py b/.governance/branch_lifecycle_check.py new file mode 100755 index 0000000..2f4afe3 --- /dev/null +++ b/.governance/branch_lifecycle_check.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Validate a versioned GitHub branch lifecycle snapshot without network access.""" + +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + + +SNAPSHOT_SCHEMA = "new-project.branch-lifecycle-snapshot/v1" +REPORT_SCHEMA = "new-project.branch-lifecycle-report/v1" +MAX_ITEMS = 10_000 +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") + + +@dataclass(order=True) +class Finding: + code: str + severity: str + message: str + remediation: str + evidence: dict[str, Any] + + +class SnapshotError(ValueError): + """A closed snapshot contract or its internal consistency is invalid.""" + + +def require_exact_fields(value: dict[str, Any], fields: set[str], label: str) -> None: + observed = set(value) + if observed != fields: + missing = sorted(fields - observed) + extra = sorted(observed - fields) + raise SnapshotError(f"{label} fields are invalid (missing={missing}, extra={extra})") + + +def require_repository(value: Any, label: str, *, nullable: bool = False) -> str | None: + if value is None and nullable: + return None + if not isinstance(value, str) or not REPOSITORY_RE.fullmatch(value): + raise SnapshotError(f"{label} must be an owner/repository identifier") + return value + + +def require_ref(value: Any, label: str) -> str: + if ( + not isinstance(value, str) + or not value + or len(value) > 255 + or any(ord(character) < 32 or ord(character) == 127 for character in value) + ): + raise SnapshotError(f"{label} must be a non-empty bounded Git ref name") + return value + + +def parse_snapshot(value: Any, expected_repository: str | None) -> dict[str, Any]: + if not isinstance(value, dict): + raise SnapshotError("snapshot root must be an object") + require_exact_fields( + value, + { + "schema", + "repository", + "defaultBranch", + "deleteBranchOnMerge", + "branches", + "openPullRequests", + }, + "snapshot", + ) + if value["schema"] != SNAPSHOT_SCHEMA: + raise SnapshotError(f"unsupported snapshot schema: {value['schema']!r}") + repository = require_repository(value["repository"], "repository") + if expected_repository is not None and repository.lower() != expected_repository.lower(): + raise SnapshotError( + f"snapshot repository {repository!r} differs from expected {expected_repository!r}" + ) + default_branch = require_ref(value["defaultBranch"], "defaultBranch") + if not isinstance(value["deleteBranchOnMerge"], bool): + raise SnapshotError("deleteBranchOnMerge must be a boolean") + + branches_value = value["branches"] + if not isinstance(branches_value, list) or len(branches_value) > MAX_ITEMS: + raise SnapshotError(f"branches must be an array with at most {MAX_ITEMS} items") + branches = [require_ref(item, f"branches[{index}]") for index, item in enumerate(branches_value)] + if len(branches) != len(set(branches)): + raise SnapshotError("branches must not contain duplicate names") + if default_branch not in branches: + raise SnapshotError("defaultBranch is missing from branches") + + pulls_value = value["openPullRequests"] + if not isinstance(pulls_value, list) or len(pulls_value) > MAX_ITEMS: + raise SnapshotError(f"openPullRequests must be an array with at most {MAX_ITEMS} items") + pulls: list[dict[str, Any]] = [] + numbers: set[int] = set() + for index, item in enumerate(pulls_value): + if not isinstance(item, dict): + raise SnapshotError(f"openPullRequests[{index}] must be an object") + require_exact_fields(item, {"number", "headRepository", "headRef"}, f"openPullRequests[{index}]") + number = item["number"] + if not isinstance(number, int) or isinstance(number, bool) or number < 1: + raise SnapshotError(f"openPullRequests[{index}].number must be a positive integer") + if number in numbers: + raise SnapshotError("openPullRequests must not contain duplicate numbers") + numbers.add(number) + pulls.append( + { + "number": number, + "headRepository": require_repository( + item["headRepository"], + f"openPullRequests[{index}].headRepository", + nullable=True, + ), + "headRef": require_ref(item["headRef"], f"openPullRequests[{index}].headRef"), + } + ) + return { + "repository": repository, + "defaultBranch": default_branch, + "deleteBranchOnMerge": value["deleteBranchOnMerge"], + "branches": branches, + "openPullRequests": pulls, + } + + +def evaluate(snapshot: dict[str, Any]) -> list[Finding]: + findings: list[Finding] = [] + repository = snapshot["repository"] + if not snapshot["deleteBranchOnMerge"]: + findings.append(Finding( + code="GOV-BRANCH-LIFECYCLE-001", + severity="error", + message="GitHub automatic head-branch deletion after merge is disabled.", + remediation="Set repository delete_branch_on_merge to true.", + evidence={"repository": repository, "deleteBranchOnMerge": False}, + )) + + branch_set = set(snapshot["branches"]) + internal_heads = { + item["headRef"] + for item in snapshot["openPullRequests"] + if item["headRepository"] is not None + and item["headRepository"].lower() == repository.lower() + } + missing_heads = sorted(internal_heads - branch_set) + if missing_heads: + findings.append(Finding( + code="GOV-BRANCH-LIFECYCLE-003", + severity="error", + message="The snapshot is inconsistent: an internal open PR head is missing.", + remediation="Re-acquire one atomic snapshot and verify the open PR head branches.", + evidence={"repository": repository, "missingInternalHeads": missing_heads}, + )) + + allowed = {snapshot["defaultBranch"], *internal_heads} + orphaned = sorted(branch_set - allowed) + if orphaned: + findings.append(Finding( + code="GOV-BRANCH-LIFECYCLE-002", + severity="error", + message="Remote branches exist without ownership by an open pull request.", + remediation=( + "Open a bounded ticket pull request for each branch or obtain an explicit owner " + "decision to discard the unmerged branch." + ), + evidence={"repository": repository, "orphanedBranches": orphaned}, + )) + return sorted(findings) + + +def report_payload(findings: list[Finding]) -> dict[str, Any]: + return { + "schema": REPORT_SCHEMA, + "status": "passed" if not findings else "failed", + "summary": {"errors": len(findings), "warnings": 0, "findings": len(findings)}, + "findings": [asdict(item) for item in findings], + } + + +def render_text(payload: dict[str, Any]) -> str: + lines: list[str] = [] + for finding in payload["findings"]: + evidence = json.dumps(finding["evidence"], ensure_ascii=False, sort_keys=True, separators=(",", ":")) + lines.append(f"{finding['code']} ERROR: {finding['message']} [{evidence}]") + lines.append(f" remediation: {finding['remediation']}") + summary = payload["summary"] + label = "GOV-BRANCH-PASS" if payload["status"] == "passed" else "GOV-BRANCH-FAIL" + lines.append( + f"{label}: {payload['status']} ({summary['errors']} errors, {summary['warnings']} warnings)" + ) + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--snapshot", required=True, type=Path) + parser.add_argument("--expected-repository") + parser.add_argument("--format", choices=("text", "json"), default="text") + args = parser.parse_args(argv) + + expected_repository: str | None = None + if args.expected_repository is not None: + try: + expected_repository = require_repository(args.expected_repository, "expected repository") + except SnapshotError as error: + parser.error(str(error)) + + findings: list[Finding] + try: + with args.snapshot.open("r", encoding="utf-8") as handle: + raw = json.load(handle) + snapshot = parse_snapshot(raw, expected_repository) + findings = evaluate(snapshot) + except (OSError, json.JSONDecodeError, SnapshotError) as error: + findings = [Finding( + code="GOV-BRANCH-LIFECYCLE-003", + severity="error", + message="The branch lifecycle snapshot is missing, malformed or inconsistent.", + remediation="Re-acquire the snapshot from the protected GitHub workflow.", + evidence={"reason": str(error)}, + )] + + payload = report_payload(findings) + if args.format == "json": + print(json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))) + else: + print(render_text(payload)) + return 0 if payload["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.governance/change-evaluation.schema.json b/.governance/change-evaluation.schema.json new file mode 100644 index 0000000..262b3ed --- /dev/null +++ b/.governance/change-evaluation.schema.json @@ -0,0 +1,357 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/change-evaluation.schema.json", + "title": "Canonical change evaluation contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "subject", + "contract", + "actors", + "changeSet", + "criteriaEvaluation", + "gates", + "dimensions", + "approval", + "findings", + "contribution", + "verdict", + "confidence", + "provenance" + ], + "properties": { + "schemaVersion": { "const": "t2c.change-evaluation/v1" }, + "subject": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "event", "baseSha", "headSha", "mergeBaseSha", "evaluatedAt"], + "properties": { + "repository": { "$ref": "#/$defs/repository" }, + "event": { "enum": ["commit", "push", "pull_request", "merge_group"] }, + "pullRequest": { "type": ["integer", "null"], "minimum": 1 }, + "baseSha": { "$ref": "#/$defs/sha" }, + "headSha": { "$ref": "#/$defs/sha" }, + "mergeBaseSha": { "$ref": "#/$defs/sha" }, + "evaluatedAt": { "type": "string", "format": "date-time" } + }, + "allOf": [ + { + "if": { + "properties": { "event": { "enum": ["pull_request", "merge_group"] } }, + "required": ["event"] + }, + "then": { "required": ["pullRequest"] } + } + ] + }, + "contract": { + "type": "object", + "additionalProperties": false, + "required": [ + "ticket", + "workstream", + "criteria", + "intentHash", + "policyHash", + "manifestLockHash", + "approvalScopeHash" + ], + "properties": { + "ticket": { "type": "string", "pattern": "^ticket-[0-9]{3}$" }, + "workstream": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "criteria": { + "type": "array", + "items": { "$ref": "#/$defs/criterion" }, + "minItems": 1, + "uniqueItems": true + }, + "intentHash": { "$ref": "#/$defs/digest" }, + "policyHash": { "$ref": "#/$defs/digest" }, + "manifestLockHash": { "$ref": "#/$defs/digest" }, + "approvalScopeHash": { "$ref": "#/$defs/digest" } + } + }, + "actors": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "role", "contributionTypes"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "role": { + "enum": [ + "author", + "last-push-author", + "implementation-assistant", + "reviewer", + "tester", + "decision-owner" + ] + }, + "contributionTypes": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1, + "uniqueItems": true + } + } + } + }, + "changeSet": { + "type": "object", + "additionalProperties": false, + "required": ["commits", "changedPaths", "changedSymbols", "publicApiChanges", "dependencyChanges"], + "properties": { + "commits": { + "type": "array", + "items": { "$ref": "#/$defs/sha" }, + "minItems": 1, + "uniqueItems": true + }, + "changedPaths": { + "type": "array", + "items": { "$ref": "#/$defs/path" }, + "minItems": 1, + "uniqueItems": true + }, + "changedSymbols": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "publicApiChanges": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "dependencyChanges": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + } + }, + "criteriaEvaluation": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "criterion", + "status", + "implementationEvidence", + "validationEvidence", + "missingEvidence", + "confidence" + ], + "properties": { + "criterion": { "$ref": "#/$defs/criterion" }, + "status": { "enum": ["SATISFIED", "PARTIAL", "FAILED", "UNKNOWN", "NOT_APPLICABLE"] }, + "implementationEvidence": { + "type": "array", + "items": { "$ref": "#/$defs/evidence" } + }, + "validationEvidence": { + "type": "array", + "items": { "$ref": "#/$defs/evidence" } + }, + "missingEvidence": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 } + }, + "allOf": [ + { + "if": { "properties": { "status": { "const": "SATISFIED" } }, "required": ["status"] }, + "then": { + "properties": { + "implementationEvidence": { "minItems": 1 }, + "validationEvidence": { "minItems": 1 }, + "missingEvidence": { "maxItems": 0 } + } + } + } + ] + } + }, + "gates": { + "type": "object", + "additionalProperties": false, + "required": [ + "governance", + "scope", + "secrets", + "tests", + "regression", + "documentation", + "approval", + "evidenceCompleteness" + ], + "properties": { + "governance": { "$ref": "#/$defs/gateStatus" }, + "scope": { "$ref": "#/$defs/gateStatus" }, + "secrets": { "$ref": "#/$defs/gateStatus" }, + "tests": { "$ref": "#/$defs/gateStatus" }, + "regression": { "$ref": "#/$defs/gateStatus" }, + "documentation": { "$ref": "#/$defs/gateStatus" }, + "approval": { "$ref": "#/$defs/gateStatus" }, + "evidenceCompleteness": { "$ref": "#/$defs/gateStatus" } + } + }, + "dimensions": { + "type": "object", + "additionalProperties": false, + "required": [ + "governanceCompliance", + "intentAlignment", + "implementationCorrectness", + "projectDirection", + "changeReasonableness", + "contributionValue", + "evidenceConfidence" + ], + "properties": { + "governanceCompliance": { "$ref": "#/$defs/dimensionStatus" }, + "intentAlignment": { "$ref": "#/$defs/dimensionStatus" }, + "implementationCorrectness": { "$ref": "#/$defs/dimensionStatus" }, + "projectDirection": { "$ref": "#/$defs/dimensionStatus" }, + "changeReasonableness": { "$ref": "#/$defs/dimensionStatus" }, + "contributionValue": { "$ref": "#/$defs/dimensionStatus" }, + "evidenceConfidence": { "$ref": "#/$defs/dimensionStatus" } + } + }, + "approval": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "source", + "actor", + "actorRole", + "verificationMethod", + "headSha", + "approvalScopeHash", + "evidenceDigest" + ], + "properties": { + "status": { "enum": ["VERIFIED", "WAITING", "REJECTED"] }, + "source": { "enum": ["github-review", "github-app-review", "signed-attestation", "none"] }, + "actor": { "type": "string", "minLength": 1 }, + "actorRole": { "enum": ["human", "validator-app", "attestation-issuer", "unresolved"] }, + "verificationMethod": { "enum": ["github-api-allowlist", "signed-attestation", "none"] }, + "headSha": { "oneOf": [{ "$ref": "#/$defs/sha" }, { "type": "null" }] }, + "approvalScopeHash": { "oneOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }] }, + "evidenceDigest": { "oneOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }] } + } + }, + "findings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["code", "severity", "message", "evidence", "remediation", "blocks"], + "properties": { + "code": { "type": "string", "pattern": "^(GOV|COM|INT|EVD|REG|DIR|DOC|APR|CTR|SEC)-[A-Z0-9-]+$" }, + "severity": { "enum": ["BLOCKING", "REVIEW_REQUIRED", "INFO"] }, + "criterion": { "$ref": "#/$defs/criterion" }, + "message": { "type": "string", "minLength": 1 }, + "evidence": { "type": "array", "items": { "$ref": "#/$defs/evidence" }, "minItems": 1 }, + "remediation": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1 }, + "blocks": { + "type": "object", + "additionalProperties": false, + "required": ["merge", "completion"], + "properties": { + "merge": { "type": "boolean" }, + "completion": { "type": "boolean" } + } + } + } + } + }, + "contribution": { + "type": "object", + "additionalProperties": false, + "required": ["claims"], + "properties": { + "claims": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["actor", "type", "evidence"], + "properties": { + "actor": { "type": "string", "minLength": 1 }, + "type": { "enum": ["implementation", "test", "diagnosis", "review", "decision", "documentation"] }, + "criterion": { "$ref": "#/$defs/criterion" }, + "finding": { "type": "string", "minLength": 1 }, + "evidence": { "type": "array", "items": { "$ref": "#/$defs/evidence" }, "minItems": 1 } + } + } + } + } + }, + "verdict": { + "type": "object", + "additionalProperties": false, + "required": ["merge", "completion", "reasonCodes", "requiredHumanDecisions"], + "properties": { + "merge": { "enum": ["BLOCKED", "REVIEW_REQUIRED", "ALLOWED"] }, + "completion": { "enum": ["NOT_DONE", "CANDIDATE", "ACCEPTED"] }, + "reasonCodes": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }, + "requiredHumanDecisions": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true } + } + }, + "confidence": { + "type": "object", + "additionalProperties": false, + "required": ["overall", "unknowns"], + "properties": { + "overall": { "type": "number", "minimum": 0, "maximum": 1 }, + "unknowns": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true } + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["evaluatorVersion", "generatedByWorkflow"], + "properties": { + "evaluatorVersion": { "type": "string", "minLength": 1 }, + "generatedByWorkflow": { "type": "string", "minLength": 1 }, + "evaluationDigest": { "$ref": "#/$defs/digest" } + } + } + }, + "$defs": { + "sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "repository": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" }, + "path": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$" + }, + "criterion": { "type": "string", "pattern": "^AC-[0-9]+$" }, + "gateStatus": { "enum": ["PASS", "FAILED", "UNKNOWN", "WAITING", "NOT_APPLICABLE"] }, + "dimensionStatus": { "enum": ["PASS", "REVIEW_REQUIRED", "FAILED", "INSUFFICIENT_EVIDENCE", "NOT_APPLICABLE"] }, + "evidence": { + "type": "object", + "minProperties": 2, + "required": ["type"], + "properties": { + "type": { "type": "string", "minLength": 1 }, + "reference": { "type": "string", "minLength": 1 }, + "path": { "$ref": "#/$defs/path" }, + "symbol": { "type": "string", "minLength": 1 }, + "revision": { "$ref": "#/$defs/sha" }, + "result": { "type": "string", "minLength": 1 } + } + } + } +} diff --git a/.governance/check_required_checks.py b/.governance/check_required_checks.py new file mode 100755 index 0000000..23eead3 --- /dev/null +++ b/.governance/check_required_checks.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Compare governance/required-checks.json to jobs published by CI workflow. + +Single source of truth for required check *names* is +``governance/required-checks.json``. This gate fails when: + +* a required name is missing from the workflow job map, or +* a top-level workflow job is not listed in requiredCheckNames + (every published job name is part of the protected surface for this hub). + +Circular governance checks ignored by the external validator are recorded in +the same file but are not expected as workflow jobs here. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +SOURCE_REL = Path("governance/required-checks.json") +SCHEMA = "new-project.required-checks/v1" +JOB_LINE = re.compile(r"^ ([A-Za-z0-9][A-Za-z0-9_-]*):\s*(?:#.*)?$") + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def load_source(path: Path) -> dict: + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict) or data.get("schema") != SCHEMA: + raise SystemExit(f"unsupported required-checks schema in {path}") + names = data.get("requiredCheckNames") + if not isinstance(names, list) or not names or not all(isinstance(n, str) and n.strip() for n in names): + raise SystemExit(f"requiredCheckNames missing or empty in {path}") + workflow = data.get("workflowFile") + if not isinstance(workflow, str) or not workflow.strip(): + raise SystemExit(f"workflowFile missing in {path}") + return data + + +def workflow_job_names(workflow_path: Path) -> list[str]: + text = workflow_path.read_text(encoding="utf-8") + lines = text.splitlines() + in_jobs = False + jobs: list[str] = [] + for line in lines: + if re.match(r"^jobs:\s*(?:#.*)?$", line): + in_jobs = True + continue + if not in_jobs: + continue + # next top-level key ends the jobs block + if line and not line.startswith(" ") and not line.startswith("\t") and line.strip() and not line.lstrip().startswith("#"): + break + match = JOB_LINE.match(line) + if match: + jobs.append(match.group(1)) + if not jobs: + raise SystemExit(f"no jobs parsed from {workflow_path}") + return jobs + + +def compare(required: list[str], published: list[str]) -> list[str]: + errors: list[str] = [] + req_set = set(required) + pub_set = set(published) + for name in required: + if name not in pub_set: + errors.append( + f"required check {name!r} is missing from workflow jobs " + f"(published={sorted(pub_set)})" + ) + for name in published: + if name not in req_set: + errors.append( + f"workflow job {name!r} is not listed in requiredCheckNames " + f"(required={required})" + ) + if list(required) != sorted(required, key=required.index): + pass # order preserved as declared; no error + # stable order check: duplicates + if len(required) != len(set(required)): + errors.append(f"requiredCheckNames contains duplicates: {required}") + if len(published) != len(set(published)): + errors.append(f"workflow jobs contain duplicates: {published}") + return errors + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--root", + type=Path, + default=None, + help="repository root (default: parent of scripts/)", + ) + parser.add_argument( + "--source", + type=Path, + default=None, + help="override path to required-checks.json", + ) + parser.add_argument( + "--workflow", + type=Path, + default=None, + help="override path to workflow YAML", + ) + args = parser.parse_args(argv) + root = args.root.resolve() if args.root else repo_root() + source_path = args.source if args.source else root / SOURCE_REL + data = load_source(source_path) + workflow_path = args.workflow if args.workflow else root / data["workflowFile"] + if not workflow_path.is_file(): + print(f"workflow file not found: {workflow_path}", file=sys.stderr) + return 2 + required = list(data["requiredCheckNames"]) + published = workflow_job_names(workflow_path) + errors = compare(required, published) + if errors: + print("required-checks gate FAILED:", file=sys.stderr) + for err in errors: + print(f" - {err}", file=sys.stderr) + return 1 + print( + "required-checks gate OK: " + f"source={source_path.relative_to(root) if source_path.is_relative_to(root) else source_path} " + f"required={required} published={published}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.governance/decision-record.schema.json b/.governance/decision-record.schema.json new file mode 100644 index 0000000..eb6a440 --- /dev/null +++ b/.governance/decision-record.schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/decision-record.schema.json", + "title": "Recomputable autonomous decision record", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "decisionId", + "ticket", + "headSha", + "correlationId", + "actor", + "appliedRule", + "inputs", + "verdict", + "verdictAuthority", + "rejected", + "assertions" + ], + "properties": { + "schema": { "const": "new-project.decision-record/v1" }, + "decisionId": { + "type": "string", + "pattern": "^D-[0-9]{3}-[0-9]{4,}$" + }, + "ticket": { + "type": "string", + "pattern": "^ticket-[0-9]{3}$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "correlationId": { + "type": "string", + "minLength": 8, + "maxLength": 200 + }, + "actor": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "appliedRule": { + "type": "string", + "pattern": "^[A-Z]+-[A-Z0-9]+-[0-9]{3}$|^P-CORE-[0-9]{3}$|^C-[A-Z]+-[0-9]{3}$" + }, + "inputs": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": ["string", "number", "boolean", "array", "object", "null"] + } + }, + "verdict": { + "type": "string", + "enum": ["APPROVE", "REQUEST_CHANGES", "BLOCKED", "SKIP"] + }, + "verdictAuthority": { + "type": "string", + "enum": ["DETERMINISTIC", "ADVISORY"] + }, + "rejected": { + "type": "object", + "additionalProperties": false, + "required": ["alternative", "because"], + "properties": { + "alternative": { "type": "string", "minLength": 1 }, + "because": { "type": "string", "minLength": 1 } + } + }, + "advisory": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "llmVerdict": { "type": "string" }, + "model": { "type": "string" } + } + }, + "assertions": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "derivedFrom": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "changeEvaluationSchema": { "const": "t2c.change-evaluation/v1" }, + "evaluationPath": { "type": "string" } + } + } + } +} diff --git a/.governance/decision_record.py b/.governance/decision_record.py new file mode 100755 index 0000000..7149a80 --- /dev/null +++ b/.governance/decision_record.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 +"""Parse, serialize, replay and append-only-check decision records (ticket-031). + +DSL form and JSON (governance/decision-record.schema.json) are mutually +derivable. Verdicts with authority ADVISORY are never trusted: replay always +recomputes from INPUT + APPLIED_RULE. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + +SCHEMA = "new-project.decision-record/v1" +DECISION_START = re.compile(r"^DECISION\s+(D-\d{3}-\d{4,})\s*$") +FIELD = re.compile(r"^([A-Z][A-Z0-9_]*)\s+(.+)$") +INPUT_LINE = re.compile(r"^INPUT\s+([A-Za-z0-9_]+)\s*=\s*(.+)$") +VERDICT_LINE = re.compile( + r"^VERDICT\s+(\S+)\s+AUTHORITY\s+(DETERMINISTIC|ADVISORY)\s*$" +) +REJECTED_LINE = re.compile(r"^REJECTED\s+(\S+)\s+BECAUSE\s+(.+)$") +ADVISORY_LINE = re.compile( + r'^ADVISORY\s+llm_verdict\s*=\s*"([^"]*)"\s+MODEL\s+"([^"]*)"\s*$' +) +ASSERT_LINE = re.compile(r"^ASSERT\s+(.+)$") + + +def parse_value(raw: str) -> Any: + raw = raw.strip() + try: + return json.loads(raw) + except json.JSONDecodeError: + return raw + + +def format_value(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + + +def decision_body(text: str) -> str: + body = text.strip() + if body.startswith("```"): + lines = body.splitlines() + if lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + body = "\n".join(lines).strip() + return body + + +def apply_named_field(record: dict[str, Any], key: str, value: str) -> bool: + destinations = { + "TICKET": "ticket", + "HEAD_SHA": "headSha", + "CORRELATION_ID": "correlationId", + "ACTOR": "actor", + "APPLIED_RULE": "appliedRule", + } + destination = destinations.get(key) + if destination is None: + return False + record[destination] = value + return True + + +def apply_decision_line(record: dict[str, Any], line: str) -> bool: + match = DECISION_START.match(line) + if match: + record["decisionId"] = match.group(1) + return True + match = INPUT_LINE.match(line) + if match: + record["inputs"][match.group(1)] = parse_value(match.group(2)) + return True + match = VERDICT_LINE.match(line) + if match: + record["verdict"] = match.group(1) + record["verdictAuthority"] = match.group(2) + return True + match = REJECTED_LINE.match(line) + if match: + record["rejected"] = { + "alternative": match.group(1), + "because": match.group(2).strip(), + } + return True + match = ADVISORY_LINE.match(line) + if match: + record["advisory"] = { + "llmVerdict": match.group(1), + "model": match.group(2), + } + return True + match = ASSERT_LINE.match(line) + if match: + record["assertions"].append(match.group(1).strip()) + return True + match = FIELD.match(line) + return bool(match and apply_named_field(record, match.group(1), match.group(2).strip())) + + +def require_decision_fields(record: dict[str, Any]) -> None: + required = [ + "decisionId", + "ticket", + "headSha", + "correlationId", + "actor", + "appliedRule", + "verdict", + "verdictAuthority", + "rejected", + ] + missing = [key for key in required if key not in record] + if missing: + raise ValueError(f"decision record missing fields: {missing}") + if not record["inputs"]: + raise ValueError("decision record has no INPUT lines") + if not record["assertions"]: + raise ValueError("decision record has no ASSERT lines") + + +def parse_dsl_record(text: str) -> dict[str, Any]: + record: dict[str, Any] = { + "schema": SCHEMA, + "inputs": {}, + "assertions": [], + "advisory": None, + "derivedFrom": None, + } + for line in decision_body(text).splitlines(): + line = line.rstrip() + if not line or line.startswith("#"): + continue + if not apply_decision_line(record, line): + raise ValueError(f"unrecognized decision-record line: {line}") + require_decision_fields(record) + return record + + +def to_dsl(record: dict[str, Any]) -> str: + lines = [ + f"DECISION {record['decisionId']}", + f"TICKET {record['ticket']}", + f"HEAD_SHA {record['headSha']}", + f"CORRELATION_ID {record['correlationId']}", + f"ACTOR {record['actor']}", + f"APPLIED_RULE {record['appliedRule']}", + ] + for key in sorted(record["inputs"]): + lines.append(f"INPUT {key} = {format_value(record['inputs'][key])}") + lines.append( + f"VERDICT {record['verdict']} AUTHORITY {record['verdictAuthority']}" + ) + rejected = record["rejected"] + lines.append( + f"REJECTED {rejected['alternative']} BECAUSE {rejected['because']}" + ) + adv = record.get("advisory") + if adv: + lines.append( + f'ADVISORY llm_verdict = "{adv.get("llmVerdict", "")}" ' + f'MODEL "{adv.get("model", "")}"' + ) + for assertion in record.get("assertions") or []: + lines.append(f"ASSERT {assertion}") + return "\n".join(lines) + "\n" + + +def record_content_hash(record: dict[str, Any]) -> str: + # Hash the canonical DSL without relying on insertion order of free text. + canonical = to_dsl(record) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def replay_verdict(record: dict[str, Any]) -> str: + """Recompute verdict from INPUT + APPLIED_RULE without reading ADVISORY.""" + if record.get("verdictAuthority") == "ADVISORY": + raise ValueError("GOV-DECISION-003: verdict authority must not be ADVISORY") + rule = record["appliedRule"] + inputs = record["inputs"] + + # P-CORE-015 / check-gate family: required checks must all PASS. + if rule in {"P-CORE-015", "C-CI-001", "C-DECISION-GATE"} or rule.startswith( + "P-CORE-01" + ): + required = inputs.get("required_checks") + observed = inputs.get("observed_checks") + if not isinstance(required, list) or not isinstance(observed, list): + raise ValueError( + "GOV-DECISION-002: check-gate rules require " + "required_checks and observed_checks arrays" + ) + status: dict[str, str] = {} + for item in observed: + if not isinstance(item, str) or "=" not in item: + raise ValueError( + f"GOV-DECISION-002: observed_checks entry not name=STATUS: {item!r}" + ) + name, st = item.split("=", 1) + status[name] = st.upper() + for name in required: + st = status.get(str(name)) + if st != "PASS" and st != "SUCCESS": + return "REQUEST_CHANGES" + unsafe = inputs.get("unsafe_change_reasons") or [] + if unsafe: + return "REQUEST_CHANGES" + return "APPROVE" + + # Default deterministic gate: explicit expected_verdict in inputs for tests + # of custom rules without encoding every POLICY rule here. + if "expected_verdict_from_rule" in inputs: + return str(inputs["expected_verdict_from_rule"]) + + raise ValueError( + f"GOV-DECISION-002: no deterministic replay for APPLIED_RULE {rule}" + ) + + +def validate_record(record: dict[str, Any]) -> list[str]: + errors: list[str] = [] + if record.get("schema") != SCHEMA: + errors.append("GOV-DECISION-002: unsupported schema") + if record.get("verdictAuthority") != "DETERMINISTIC": + errors.append("GOV-DECISION-003: VERDICT_AUTHORITY must be DETERMINISTIC") + for assertion in record.get("assertions") or []: + if ( + "VERDICT_AUTHORITY" in assertion + and "ADVISORY" in assertion + and record.get("verdictAuthority") == "ADVISORY" + ): + errors.append("GOV-DECISION-003: assertion forbids ADVISORY authority") + try: + recomputed = replay_verdict(record) + except ValueError as exc: + errors.append(str(exc)) + return errors + if recomputed != record.get("verdict"): + errors.append( + "GOV-DECISION-004: replayed verdict " + f"{recomputed!r} != recorded {record.get('verdict')!r}" + ) + return errors + + +def split_decision_blocks(markdown: str) -> list[str]: + """Extract fenced ```dsl DECISION ... blocks or bare DECISION sequences.""" + blocks: list[str] = [] + fence = re.findall(r"```dsl\n(.*?)```", markdown, flags=re.DOTALL) + for body in fence: + if "DECISION " in body: + # may contain multiple DECISION records + parts = re.split(r"(?=^DECISION\s+D-)", body.strip(), flags=re.MULTILINE) + for part in parts: + part = part.strip() + if part.startswith("DECISION "): + blocks.append(part) + if blocks: + return blocks + parts = re.split(r"(?=^DECISION\s+D-)", markdown.strip(), flags=re.MULTILINE) + return [p.strip() for p in parts if p.strip().startswith("DECISION ")] + + +def check_append_only(previous_markdown: str, current_markdown: str) -> list[str]: + """Fail if any earlier decision record was modified or removed.""" + prev_blocks = split_decision_blocks(previous_markdown) + curr_blocks = split_decision_blocks(current_markdown) + errors: list[str] = [] + if len(curr_blocks) < len(prev_blocks): + errors.append( + "GOV-DECISION-001: decision log shrank " + f"({len(prev_blocks)} -> {len(curr_blocks)}); append-only violated" + ) + return errors + for idx, prev in enumerate(prev_blocks): + prev_rec = parse_dsl_record(prev) + curr_rec = parse_dsl_record(curr_blocks[idx]) + if record_content_hash(prev_rec) != record_content_hash(curr_rec): + errors.append( + "GOV-DECISION-001: earlier decision " + f"{prev_rec.get('decisionId')} was modified (append-only)" + ) + return errors + + +def from_change_evaluation(evaluation: dict[str, Any], **meta: str) -> dict[str, Any]: + """Derive a decision record from t2c.change-evaluation/v1 (no dual truth).""" + if evaluation.get("schemaVersion") != "t2c.change-evaluation/v1": + raise ValueError("expected t2c.change-evaluation/v1") + subject = evaluation["subject"] + contract = evaluation["contract"] + verdict_map = { + "allow": "APPROVE", + "deny": "REQUEST_CHANGES", + "approve": "APPROVE", + "request_changes": "REQUEST_CHANGES", + } + raw = str(evaluation.get("verdict", "")).lower() + verdict = verdict_map.get(raw, "BLOCKED") + gates = evaluation.get("gates") or {} + observed = [] + if isinstance(gates, dict): + for name, state in gates.items(): + observed.append(f"{name}={str(state).upper()}") + record = { + "schema": SCHEMA, + "decisionId": meta["decisionId"], + "ticket": contract["ticket"], + "headSha": subject["headSha"], + "correlationId": meta["correlationId"], + "actor": meta.get("actor", "agent:validator"), + "appliedRule": meta.get("appliedRule", "P-CORE-015"), + "inputs": { + "required_checks": meta.get("required_checks") + or json.loads(Path("governance/required-checks.json").read_text()).get( + "requiredCheckNames", ["test"] + ), + "observed_checks": observed + or meta.get("observed_checks", ["test=PASS"]), + "evaluation_verdict": evaluation.get("verdict"), + }, + "verdict": verdict if verdict != "BLOCKED" else "REQUEST_CHANGES", + "verdictAuthority": "DETERMINISTIC", + "rejected": { + "alternative": "APPROVE" + if verdict != "APPROVE" + else "REQUEST_CHANGES", + "because": meta.get( + "because", + "DERIVED_FROM_CHANGE_EVALUATION", + ), + }, + "advisory": None, + "assertions": ['VERDICT_AUTHORITY != "ADVISORY"'], + "derivedFrom": { + "changeEvaluationSchema": "t2c.change-evaluation/v1", + "evaluationPath": meta.get("evaluationPath", "change-evaluation.json"), + }, + } + return record + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="cmd", required=True) + + p_val = sub.add_parser("validate-dsl", help="validate one DSL decision record") + p_val.add_argument("path", type=Path) + + p_rep = sub.add_parser("replay", help="print recomputed verdict") + p_rep.add_argument("path", type=Path) + + p_app = sub.add_parser( + "check-append-only", + help="compare previous and current decision log markdown", + ) + p_app.add_argument("previous", type=Path) + p_app.add_argument("current", type=Path) + + args = parser.parse_args(argv) + if args.cmd == "validate-dsl": + record = parse_dsl_record(args.path.read_text(encoding="utf-8")) + errors = validate_record(record) + if errors: + print("FAIL", file=sys.stderr) + for e in errors: + print(e, file=sys.stderr) + return 1 + print("OK", record["decisionId"], record["verdict"]) + return 0 + if args.cmd == "replay": + record = parse_dsl_record(args.path.read_text(encoding="utf-8")) + print(replay_verdict(record)) + return 0 + if args.cmd == "check-append-only": + errors = check_append_only( + args.previous.read_text(encoding="utf-8"), + args.current.read_text(encoding="utf-8"), + ) + if errors: + for e in errors: + print(e, file=sys.stderr) + return 1 + print("append-only OK") + return 0 + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.governance/diagnostics.json b/.governance/diagnostics.json new file mode 100644 index 0000000..1aeadb9 --- /dev/null +++ b/.governance/diagnostics.json @@ -0,0 +1,320 @@ +{ + "schema": "new-project.diagnostics/v2", + "codes": { + "GOV-APPROVAL-001": { + "message": "Implementation lacks approval from a trusted external source.", + "remediation": "Obtain an exact-head approval from an allowlisted human, trusted Validator App or verified signed attestation.", + "documentation": null + }, + "GOV-APPROVAL-002": { + "message": "Approval refers to a different ticket.", + "remediation": "Approve the current ticket after reviewing its latest intent and exact implementation head.", + "documentation": null + }, + "GOV-APPROVAL-003": { + "message": "Approval evidence is missing, repository-controlled or structurally invalid.", + "remediation": "Create v1 approval evidence outside the PR checkout through a protected verifier.", + "documentation": null + }, + "GOV-APPROVAL-004": { + "message": "Approval evidence is bound to another repository, pull request or commit.", + "remediation": "Regenerate protected evidence for the exact repository, PR, HEAD and ticket tuple.", + "documentation": null + }, + "GOV-APPROVAL-005": { + "message": "Approval actor or verification method is not trusted for the claimed source.", + "remediation": "Use the type-specific protected allowlist or verify a signed attestation with a trusted issuer.", + "documentation": null + }, + "GOV-ARCHITECTURE-001": { + "message": "Architecture ownership, UI/data impact or component mapping is unresolved.", + "remediation": "Complete the accepted architecture block in intent.json before implementation.", + "documentation": null + }, + "GOV-BASE-001": { + "message": "The target branch or base SHA differs from the approved delivery contract.", + "remediation": "Rebase or rebuild from the accepted base, then update intent only through an authorized scope review.", + "documentation": null + }, + "GOV-BOOT-001": { + "message": "A required target-repository file is missing.", + "remediation": "Create the target-owned prerequisite or adopt the managed file through the pinned standard package as applicable.", + "documentation": null + }, + "GOV-BRANCH-LIFECYCLE-001": { + "message": "GitHub is not configured to delete merged head branches.", + "remediation": "Set delete_branch_on_merge=true in repository settings.", + "documentation": null + }, + "GOV-BRANCH-LIFECYCLE-002": { + "message": "The remote branch snapshot is inconsistent with open pull requests.", + "remediation": "Acquire one fresh protected snapshot and verify each open PR head branch.", + "documentation": null + }, + "GOV-BRANCH-LIFECYCLE-003": { + "message": "A quiescent repository has a remote branch other than its default branch.", + "remediation": "Preserve unmerged work, then delete only a verified merged or explicitly discarded remote branch.", + "documentation": null + }, + "GOV-BUDGET-001": { + "message": "The actual implementation diff exceeds its approved budget.", + "remediation": "Reduce the slice or obtain fresh authorization for a larger bounded intent.", + "documentation": null + }, + "GOV-CLASS-000": { + "message": "The work-classification contract is unavailable.", + "remediation": "Restore the managed work-classification DSL from the pinned package before allocating a ticket.", + "documentation": null + }, + "GOV-CLASS-001": { + "message": "A requested work classification value is not declared.", + "remediation": "Choose kind, priority and origin values declared by the managed classification contract.", + "documentation": null + }, + "GOV-CONFLICT-001": { + "message": "Tickets declared as conflicting are active at the same time.", + "remediation": "Move one ticket to BACKLOG, PLAN or BLOCKED until the conflicting work reaches a terminal state.", + "documentation": null + }, + "GOV-DECISION-001": { + "message": "A repository-changing autonomous decision lacks an append-only record or rewrites earlier evidence.", + "remediation": "Append a new recomputable decision record; never edit an earlier record.", + "documentation": null + }, + "GOV-DECISION-002": { + "message": "A decision record is not recomputable.", + "remediation": "Store deterministic inputs verbatim and name a replayable applied rule.", + "documentation": null + }, + "GOV-DECISION-003": { + "message": "A decision record treats advisory LLM output as verdict authority.", + "remediation": "Keep LLM output ADVISORY and derive the verdict from deterministic rules and evidence.", + "documentation": null + }, + "GOV-DECISION-004": { + "message": "Replaying decision inputs diverges from the recorded verdict.", + "remediation": "Correct the new decision record or underlying inputs; do not rewrite historical evidence.", + "documentation": null + }, + "GOV-DELIVERY-001": { + "message": "The implementation slice lacks or exceeds its approved delivery contract.", + "remediation": "Declare a valid bounded delivery block or split the work into a smaller authorized slice.", + "documentation": null + }, + "GOV-DELIVERY-002": { + "message": "The implementation slice reached its pre-stop checkpoint.", + "remediation": "Stop, record evidence and replan the remaining bounded work before continuing.", + "documentation": null + }, + "GOV-DEPENDENCY-001": { + "message": "The ticket dependency graph contains a cycle or self-reference.", + "remediation": "Rewrite dependsOn as an acyclic graph with no ticket depending on itself.", + "documentation": null + }, + "GOV-DEPENDENCY-002": { + "message": "An active ticket depends on a missing or unfinished ticket.", + "remediation": "Complete the dependency or move the dependent ticket out of IN_PROGRESS.", + "documentation": null + }, + "GOV-DIAGNOSTIC-001": { + "message": "The stable diagnostic catalog is malformed or differs from emitted runtime codes.", + "remediation": "Register every emitted code exactly once with a non-empty canonical message and remediation, and remove stale entries.", + "documentation": null + }, + "GOV-DIAGNOSTIC-002": { + "message": "A linked diagnostic runbook is missing, unsafe or structurally incomplete.", + "remediation": "Restore the linked error/*.md page with all required fail-closed sections and a relative path.", + "documentation": null + }, + "GOV-DIFF-001": { + "message": "The changed-path set or commit history could not be determined safely.", + "remediation": "Restore valid Git metadata and provide an explicit base and head before evaluating scope.", + "documentation": null + }, + "GOV-DOCKER-001": { + "message": "The required Docker runtime declaration is incomplete.", + "remediation": "Declare the Docker marker and explicit governed Dockerfile/Compose paths, or truthfully disable the stack.", + "documentation": null + }, + "GOV-DOCKER-002": { + "message": "A Dockerfile or Compose image reference is not pinned to an immutable SHA-256 digest.", + "remediation": "Replace every external image tag with registry/image@sha256 followed by 64 lowercase hex characters.", + "documentation": null + }, + "GOV-ENV-001": { + "message": "Governance environment resolution failed or exposed an undeclared value.", + "remediation": "Use governance_env.py with declared variables, relative ENV_FILE paths and redacted required secrets.", + "documentation": null + }, + "GOV-INTEGRATION-001": { + "message": "A shared contract path lacks valid routing through an integration ticket.", + "remediation": "Route the shared path through the manifest-declared integration workstream without transferring path ownership.", + "documentation": null + }, + "GOV-INTENT-001": { + "message": "Implementation changed before the ticket entered an implementation state.", + "remediation": "Record authorization and move the active ticket to EDIT, VALIDATION or PUBLICATION before changing implementation.", + "documentation": null + }, + "GOV-INTENT-002": { + "message": "Ticket intent is missing or malformed.", + "remediation": "Create a schema-valid intent.json with bounded allowedPaths and required delivery evidence.", + "documentation": null + }, + "GOV-INTENT-003": { + "message": "Ticket intent was not committed before the first implementation commit.", + "remediation": "Rebuild the branch so the approved intent commit precedes implementation without rewriting trusted published history.", + "documentation": null + }, + "GOV-MANIFEST-001": { + "message": "Manifest or managed governance contract is missing, unreadable or structurally invalid.", + "remediation": "Restore the complete pinned governance package and validate its JSON contracts.", + "documentation": null + }, + "GOV-OWNER-001": { + "message": "An untrusted actor changed a human-owned participant file.", + "remediation": "Revert the agent-authored human file change and obtain input from the human owner or trusted intake boundary.", + "documentation": null + }, + "GOV-PATH-001": { + "message": "A committed governance artifact contains a machine-local absolute path.", + "remediation": "Replace the local path with a repository-relative reference and sanitize committed logs.", + "documentation": null + }, + "GOV-REMEDIATION-001": { + "message": "A diagnostic remediation intent is malformed, unresolved or semantically unsafe.", + "remediation": "Correct the target-owned remediation-intent DSL and pass deterministic schema and semantic validation before LLM planning.", + "documentation": "error/GOV-REMEDIATION-INTENT.md" + }, + "GOV-REMEDIATION-002": { + "message": "A todo2code plan conflicts with accepted remediation scope, criteria, priority or user-state safety.", + "remediation": "Reject or regenerate the conflicting plan; obtain a fresh bounded intent before any material scope or authority expansion.", + "documentation": "error/GOV-REMEDIATION-INTENT.md" + }, + "GOV-REMEDIATION-003": { + "message": "The todo2code advisory overlay is stale for the current remediation intent.", + "remediation": "Discard the overlay and rerun deterministic todo2code analysis against the current authority-bearing intent digest.", + "documentation": "error/GOV-REMEDIATION-INTENT.md" + }, + "GOV-SCOPE-001": { + "message": "A changed implementation path is outside approved intent scope.", + "remediation": "Remove the unrelated change or obtain a fresh bounded intent before editing that path.", + "documentation": null + }, + "GOV-SECRET-001": { + "message": "A changed file contains a probable secret assignment.", + "remediation": "Stop publication, remove and rotate the secret through a trusted boundary, then rescan the exact diff.", + "documentation": null + }, + "GOV-STACK-001": { + "message": "The declared technology stack lacks its required project marker.", + "remediation": "Add a truthful root marker or remove the incorrect stack declaration; do not create a synthetic marker only to pass the gate.", + "documentation": null + }, + "GOV-STATUS-001": { + "message": "A ticket status is missing or not declared by the governance manifest.", + "remediation": "Use one declared active, non-active or closed status and a compatible workflow state.", + "documentation": null + }, + "GOV-SYNC-001": { + "message": "A managed governance file does not match its pinned SHA-256 digest.", + "remediation": "Adopt or upgrade the complete immutable package through Goal; do not patch managed payload files manually.", + "documentation": null + }, + "GOV-TICKET-001": { + "message": "Implementation changed without exactly one active ticket.", + "remediation": "Keep the owning ticket IN_PROGRESS through implementation publication and use a governance-only closure after trusted merge.", + "documentation": "error/GOV-TICKET-001.md" + }, + "GOV-TICKET-002": { + "message": "More than one active ticket exists where the manifest permits only one.", + "remediation": "Continue the matching ticket and move unrelated waiting work to BACKLOG, PLAN or BLOCKED.", + "documentation": null + }, + "GOV-TICKET-003": { + "message": "An active ticket is malformed or missing a required governance file.", + "remediation": "Restore its README, preprompt, changelog, intent and typed agent participant files before implementation.", + "documentation": null + }, + "GOV-TICKET-004": { + "message": "Executable source, test or research content is stored in a ticket directory.", + "remediation": "Move executable material to its normal source, scripts or tests directory and keep only evidence in the ticket.", + "documentation": null + }, + "GOV-TICKET-005": { + "message": "Implementation paths do not resolve to exactly one active ticket.", + "remediation": "Split unrelated paths or correct non-overlapping allowedPaths and workstream ownership.", + "documentation": null + }, + "GOV-TICKET-ALLOCATION-001": { + "message": "A ticket claim is outside a valid clone-wide high-water reservation.", + "remediation": "Preserve the worktree, classify ownership and allocate a fresh ID only through project/new-ticket.sh.", + "documentation": "error/GOV-TICKET-ALLOCATION.md" + }, + "GOV-TICKET-ALLOCATION-002": { + "message": "Linked worktrees assign different intents to the same ticket ID.", + "remediation": "Stop both writers, preserve both heads and reallocate the later intent through project/new-ticket.sh before rebuilding its branch.", + "documentation": "error/GOV-TICKET-ALLOCATION.md" + }, + "GOV-TICKET-LOCK-001": { + "message": "Another ticket allocation holds the clone-wide lock.", + "remediation": "Wait for the allocator; remove a stale lock only after proving no allocation process is active.", + "documentation": "error/GOV-TICKET-ALLOCATION.md" + }, + "GOV-TICKET-LOCK-002": { + "message": "The clone-wide ticket high-water state is invalid.", + "remediation": "Preserve ticket worktrees and repair the shared numeric reservation before assigning another ID.", + "documentation": "error/GOV-TICKET-ALLOCATION.md" + }, + "GOV-TICKET-LOCK-003": { + "message": "The ticket directory selected by the allocator already exists.", + "remediation": "Stop and classify the existing claim; never overwrite or rename it automatically.", + "documentation": "error/GOV-TICKET-ALLOCATION.md" + }, + "GOV-TICKET-LOCK-004": { + "message": "Remote ticket refs could not be refreshed before allocation.", + "remediation": "Restore origin connectivity and retry; do not allocate from stale refs.", + "documentation": "error/GOV-TICKET-ALLOCATION.md" + }, + "GOV-WORKSPACE-LIFECYCLE-001": { + "message": "A terminal workspace still contains a linked worktree.", + "remediation": "Verify dirty state and HEAD reachability, then remove only the exact disposable worktree through Git.", + "documentation": "error/GOV-WORKSPACE-LIFECYCLE.md" + }, + "GOV-WORKSPACE-LIFECYCLE-002": { + "message": "A terminal workspace still contains a duplicate clone.", + "remediation": "Verify it has no unique data, then move the exact duplicate checkout to recoverable trash.", + "documentation": "error/GOV-WORKSPACE-LIFECYCLE.md" + }, + "GOV-WORKSPACE-LIFECYCLE-003": { + "message": "The local workspace audit could not be completed safely.", + "remediation": "Repair repository metadata or narrow the explicit workspace root before cleanup.", + "documentation": "error/GOV-WORKSPACE-LIFECYCLE.md" + }, + "GOV-WORKSPACE-LIFECYCLE-004": { + "message": "A terminal workspace still contains a non-default local branch.", + "remediation": "Classify its HEAD, preserve unique history, release any worktree and delete only the exact disposable local ref.", + "documentation": "error/GOV-WORKSPACE-LIFECYCLE.md" + }, + "GOV-WORKSTREAM-001": { + "message": "An active ticket declares a missing or unknown workstream.", + "remediation": "Choose a workstream declared by the current governance manifest.", + "documentation": null + }, + "GOV-WORKSTREAM-002": { + "message": "A workstream exceeds its active-ticket limit.", + "remediation": "Keep one implementation owner active and release waiting reservations.", + "documentation": null + }, + "GOV-WORKSTREAM-003": { + "message": "A changed path is not owned by the ticket workstream.", + "remediation": "Move the path to the owning workstream or correct the manifest through an authorized integration change.", + "documentation": null + }, + "GOV-WORKSTREAM-004": { + "message": "Active ticket write scopes overlap on a concrete repository path.", + "remediation": "Serialize the work or narrow allowedPaths until each changed path has exactly one owner.", + "documentation": null + } + } +} diff --git a/.governance/diagnostics.schema.json b/.governance/diagnostics.schema.json new file mode 100644 index 0000000..2b975e5 --- /dev/null +++ b/.governance/diagnostics.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://wellmanifest.dev/schemas/new-project-diagnostics-v2.json", + "title": "new-project stable diagnostic catalog", + "type": "object", + "additionalProperties": false, + "required": ["schema", "codes"], + "properties": { + "schema": {"const": "new-project.diagnostics/v2"}, + "codes": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^GOV-[A-Z]+(?:-[A-Z]+)*-[0-9]{3}$" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": ["message", "remediation", "documentation"], + "properties": { + "message": {"type": "string", "minLength": 1}, + "remediation": {"type": "string", "minLength": 1}, + "documentation": { + "oneOf": [ + {"type": "null"}, + {"type": "string", "pattern": "^error/[^/]+\\.md$"} + ] + } + } + } + } + } +} diff --git a/.governance/error/GOV-REMEDIATION-INTENT.md b/.governance/error/GOV-REMEDIATION-INTENT.md new file mode 100644 index 0000000..c9b982c --- /dev/null +++ b/.governance/error/GOV-REMEDIATION-INTENT.md @@ -0,0 +1,48 @@ +# GOV-REMEDIATION-001/002/003 — invalid or inconsistent remediation intent + +## Situation + +`GOV-REMEDIATION-001` means the target-owned remediation intent is malformed or +semantically unsafe. `GOV-REMEDIATION-002` means a todo2code plan conflicts with +accepted scope, criteria, priority or preservation constraints. +`GOV-REMEDIATION-003` means the advisory overlay no longer matches the +authority-bearing intent digest. + +## Meaning + +The deterministic boundary cannot prove that the proposed refactoring still +implements the accepted diagnostic intent. LLM and todo2code output remains +advisory and cannot repair that authority gap by assertion. + +## Safe resolution + +1. Open the populated `remediation-intent.dsl.json` in the affected target + repository ticket; do not copy it into the Governance Hub. +2. For `GOV-REMEDIATION-001`, resolve every reported field, path, dependency, + applicability signal and verification, then validate again. +3. For `GOV-REMEDIATION-002`, reject or regenerate plans outside accepted scope. + If the objective truly changed, record a fresh bounded intent and authority. +4. For `GOV-REMEDIATION-003`, discard the stale advisory overlay and rerun + todo2code analysis against the current intent. +5. Keep unknown ownership explicit and preserve dirty worktrees or other user + state until a human classifies them. + +## Verification + +Run `python3 .governance/remediation_intent.py validate ` and require a +zero exit status. Regenerated analyzed intents must bind current intent, +diagnostics and plan digests and must contain no blocking todo2code finding +before implementation proceeds. + +## Do not + +Do not edit digests by hand, suppress applicability uncertainty, infer missing +owners, widen paths from an LLM suggestion, or authorize deletion merely to +make validation pass. Do not use a target ticket or incident log as a reusable +runbook. + +## Related rules + +`C-DIAGNOSTIC-001`, `C-DIAGNOSTIC-002`, `C-DIAGNOSTIC-003`, +`C-REMEDIATION-001`, `C-REMEDIATION-002`, `C-REMEDIATION-003`, +`C-REMEDIATION-004`, `P-CORE-008`, `P-CORE-020`. diff --git a/.governance/error/GOV-TICKET-001.md b/.governance/error/GOV-TICKET-001.md new file mode 100644 index 0000000..9bf0803 --- /dev/null +++ b/.governance/error/GOV-TICKET-001.md @@ -0,0 +1,41 @@ +# GOV-TICKET-001: brak aktywnego właściciela implementacji + +## Situation + +Kod pojawia się, gdy diff implementacyjny nie ma dokładnie jednego ticketu ze +statusem `IN_PROGRESS`. Typowy przypadek to ustawienie `DONE / DONE` na branchu +PR przed trusted merge. + +## Meaning + +Zamknięty ticket nie udziela już uprawnienia do swojego `allowedPaths`. +Implementacyjny PR musi zachować `IN_PROGRESS / PUBLICATION` aż exact-head +review zostanie zintegrowany z gałęzią domyślną. + +## Safe resolution + +1. Sprawdź, czy PR nadal wskazuje oczekiwany HEAD i dokładnie jeden ticket. +2. Jeżeli implementacja nie została scalona, przywróć ticket do + `IN_PROGRESS / PUBLICATION` na tym samym branchu i ponów bramę. +3. Po trusted merge utwórz nowy governance-only closure z aktualnego `main`. +4. W closure ustaw `DONE / DONE` i zapisz merge SHA oraz post-merge checks. + +## Verification + +- Diff implementacyjnego PR zawiera aktywny ticket i przechodzi governance + gate. +- Diff closure nie zawiera implementacji, a zapisany merge SHA jest przodkiem + bieżącego `main`. +- Zdalny branch implementacyjny znika dopiero po merge. + +## Do not + +- Nie osłabiaj `activeStatuses` i nie zezwalaj `DONE` na autoryzowanie diffu. +- Nie dopisuj closure do niescalonego full-diff branchu. +- Nie traktuj lokalnego statusu Markdown jako trusted approval. + +## Related rules + +- `P-CORE-014`, `P-CORE-023` +- `C-TICKET-017` +- `C-PUBLISH-003`, `C-PUBLISH-009` diff --git a/.governance/error/GOV-TICKET-ALLOCATION.md b/.governance/error/GOV-TICKET-ALLOCATION.md new file mode 100644 index 0000000..433027c --- /dev/null +++ b/.governance/error/GOV-TICKET-ALLOCATION.md @@ -0,0 +1,47 @@ +# GOV-TICKET-ALLOCATION i GOV-TICKET-LOCK + +## Situation + +Runbook obejmuje `GOV-TICKET-ALLOCATION-001`, +`GOV-TICKET-ALLOCATION-002` oraz `GOV-TICKET-LOCK-001`–`004`. Kody oznaczają +aktywny lub uszkodzony lock, nieczytelny high-water, istniejący katalog, +nieodświeżone zdalne refy, ticket poza rezerwacją albo dwa różne intenty z tym +samym numerem. + +## Meaning + +Numer ticketu jest zasobem całego klonu, a nie pojedynczego worktree. +`project/new-ticket.sh` musi odświeżyć refy, zdobyć wspólny lock i podnieść +high-water przed utworzeniem katalogu. Ręczne `mkdir` lub skopiowanie +`project/ticket-{NNN}` nie tworzy tej rezerwacji. + +## Safe resolution + +1. Zatrzymaj nowych writerów i zinwentaryzuj wszystkie linked worktree. +2. Zachowaj dirty state i oba HEAD-y; nie wybieraj zwycięzcy po nazwie. +3. Ustal wcześniejszą prawidłową rezerwację z refów, high-water i historii + allocatora. +4. Pozostaw wcześniejszy intent przy jego numerze. Dla drugiej pracy uruchom + `project/new-ticket.sh` i odtwórz branch wyłącznie z jej własnych zmian. +5. Przy błędzie refów napraw łączność z `origin` i ponów allocator. Przy + aktywnym locku zaczekaj; usuwaj stale lock tylko po potwierdzeniu braku + procesu. + +## Verification + +- `git worktree list --porcelain` pokazuje każdy sklasyfikowany checkout. +- Wspólny high-water jest nie mniejszy od najwyższego niescalonego claimu. +- Każdy numer ma jedną tożsamość `ticket + summary + workstream`. +- Ponowne uruchomienie workspace checkera nie emituje kodów allocation. + +## Do not + +- Nie zmieniaj numeru przez ręczne `mv` i nie kopiuj historii obu branchy. +- Nie usuwaj dirty/unreachable worktree ani locka bez identyfikacji procesu. +- Nie przydzielaj numeru offline ze starych refów. + +## Related rules + +- `P-CORE-022` +- `C-CONCURRENCY-001`, `C-CONCURRENCY-002`, `C-CONCURRENCY-003` +- `P-WORKSPACE-001`, `C-WORKSPACE-001` diff --git a/.governance/error/GOV-WORKSPACE-LIFECYCLE.md b/.governance/error/GOV-WORKSPACE-LIFECYCLE.md new file mode 100644 index 0000000..687a667 --- /dev/null +++ b/.governance/error/GOV-WORKSPACE-LIFECYCLE.md @@ -0,0 +1,49 @@ +# GOV-WORKSPACE-LIFECYCLE + +## Situation + +Kody `GOV-WORKSPACE-LIFECYCLE-001`–`004` oznaczają pozostały linked worktree, +duplikat klonu, audyt, którego nie da się bezpiecznie zakończyć, albo +non-defaultowy lokalny branch pozostawiony w `refs/heads`. + +## Meaning + +Stan terminalny wymaga jednego podstawowego checkoutu, lecz żaden checker nie +ma prawa automatycznie niszczyć nieznanych danych. Lokalny filesystem i zdalny +GitHub są osobnymi granicami dowodu. + +## Safe resolution + +1. Dla każdego checkoutu zapisz dirty state, branch, HEAD i tożsamość remote. +2. Potwierdź, że HEAD jest zintegrowany albo że właściciel jawnie porzucił + unmerged pilot. +3. Linked worktree usuń przez `git worktree remove `, potem + `git worktree prune` i dopiero wtedy usuń zwolniony lokalny branch. +4. Zweryfikowany duplikat klonu przenieś do odzyskiwalnego kosza. +5. Dla kodu `004` sprawdź wskazane `branch`, `head`, `defaultBranch`, `checkout` + i `primary`. Jeśli commit nie jest zintegrowany, zachowaj go pod opisanym, + zdalnie zweryfikowanym tagiem/refem albo uzyskaj jawną decyzję właściciela. + Dopiero po zwolnieniu worktree usuń dokładny lokalny ref. W czasie aktywnej + pracy można zwolnić branch z findingu wyłącznie przez dokładną ścieżkę + checkoutu przekazaną jako `--allow`; wzorce i sama nazwa brancha nie są + wyjątkiem. + +## Verification + +- Lokalny workspace checker kończy się `GOV-WORKSPACE-PASS` bez + nieallowlistowanych checkoutów. +- Osobny workflow GitHub potwierdza tylko `main`, brak otwartych PR i + `delete_branch_on_merge=true`. + +## Do not + +- Nie używaj globów rekurencyjnych ani nie usuwaj primary worktree. +- Nie uznawaj zielonego CI za dowód stanu lokalnego dysku. +- Nie usuwaj danych dirty lub unreachable bez decyzji właściciela. +- Nie traktuj `GOV-WORKSPACE-PASS` jako uprawnienia do usuwania refów; checker + jest wyłącznie read-only. + +## Related rules + +- `P-WORKSPACE-001`–`004` +- `C-WORKSPACE-001`–`004` diff --git a/.governance/error/README.md b/.governance/error/README.md new file mode 100644 index 0000000..40cf8e9 --- /dev/null +++ b/.governance/error/README.md @@ -0,0 +1,24 @@ +# Kanoniczne rozwiązania diagnostyk + +Katalog `error/` zawiera runbooki dla stabilnych kodów `GOV-*`, których +rozwiązanie jest wieloetapowe, wymaga klasyfikacji danych albo może prowadzić +do destrukcyjnej operacji. Krótka, maszynowa remediacja zawsze pozostaje w +`governance/diagnostics.json`; pole `documentation` wskazuje ten katalog. + +Runbook nie jest wyjątkiem od polityki. W razie konfliktu obowiązuje kolejno +`POLICY.md`, `CONTRIBUTING.md`, finding z bieżącego uruchomienia i dopiero +procedura pomocnicza. Historyczne pliki ticketów wyjaśniają, dlaczego standard +się zmienił, ale nie są instrukcją operacyjną dla kolejnych zdarzeń. + +Każdy podlinkowany runbook musi zawierać dokładnie rozpoznawalne sekcje: + +- `Situation` — kiedy kod występuje; +- `Meaning` — który invariant został naruszony; +- `Safe resolution` — niedestrukcyjne kroki naprawy; +- `Verification` — deterministyczne sprawdzenie wyniku; +- `Do not` — zabronione skróty i ryzyka; +- `Related rules` — stabilne identyfikatory reguł. + +Nazwy plików są stabilne i mogą grupować rodzinę kodów, np. +`GOV-TICKET-ALLOCATION.md`. Linki muszą być względne i pozostawać wewnątrz +`error/`. diff --git a/.governance/governance_check.py b/.governance/governance_check.py new file mode 100755 index 0000000..d404f4a --- /dev/null +++ b/.governance/governance_check.py @@ -0,0 +1,2836 @@ +#!/usr/bin/env python3 +"""Deterministic policy-as-code validator for new-project target repositories.""" + +from __future__ import annotations + +import argparse +import fnmatch +import hashlib +import json +import os +import re +import stat +import subprocess +import sys +from collections.abc import Iterable +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any + +RUNTIME_VERSION = "0.10.0" +ACTIVE_DEFAULT = {"IN_PROGRESS"} +EXECUTABLE_SUFFIXES = { + ".bat", ".c", ".cc", ".cmd", ".cpp", ".go", ".java", ".js", ".jsx", + ".mjs", ".php", ".ps1", ".py", ".rb", ".rs", ".sh", ".ts", ".tsx", +} +SECRET_RE = re.compile( + r"(?i)(api[_-]?key|access[_-]?key|client[_-]?secret|password|private[_-]?key|token)" + r"[ \t]*[:=][ \t]*['\"]?([A-Za-z0-9_./+=-]{12,})" +) +SAFE_SECRET_VALUES = re.compile(r"(?i)^(example|placeholder|changeme|your[_-]|\$\{|<|xxx|test)") +LOCAL_PATH_RE = re.compile(r"(?:[A-Za-z]:[\\/](?:Users|Documents|Desktop)[\\/]|/(?:home|Users)/[^/\s]+/)") +IMMUTABLE_IMAGE_RE = re.compile(r"^[^@\s]+@sha256:[a-f0-9]{64}$") +COMPOSE_IMAGE_RE = re.compile( + r"^\s*image\s*:\s*(?:\"([^\"]+)\"|'([^']+)'|([^\s#]+))" +) + + +@dataclass(order=True) +class Finding: + code: str + severity: str + message: str + remediation: str + paths: list[str] = field(default_factory=list, compare=False) + evidence: dict[str, Any] = field(default_factory=dict, compare=False) + + +@dataclass +class TicketRecord: + directory: Path + status: str | None + workflow: str | None + intent: dict[str, Any] | None + intent_error: str | None + + +class Report: + def __init__(self, root: Path) -> None: + self.root = root + self.findings: list[Finding] = [] + + def add( + self, + code: str, + message: str, + remediation: str, + paths: Iterable[str] = (), + evidence: dict[str, Any] | None = None, + severity: str = "error", + ) -> None: + self.findings.append(Finding( + code=code, + severity=severity, + message=message, + remediation=remediation, + paths=sorted(set(paths)), + evidence=evidence or {}, + )) + + @property + def errors(self) -> int: + return sum(item.severity == "error" for item in self.findings) + + def payload(self) -> dict[str, Any]: + findings = sorted(self.findings) + return { + "schema": "new-project.governance-report/v1", + "runtimeVersion": RUNTIME_VERSION, + "root": ".", + "status": "passed" if self.errors == 0 else "failed", + "summary": { + "errors": self.errors, + "warnings": sum(item.severity == "warning" for item in findings), + "findings": len(findings), + }, + "findings": [asdict(item) for item in findings], + } + + +def load_json(path: Path) -> Any: + with path.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def work_classification_header_error(value: Any) -> str | None: + fields = {"$schema", "schema", "dimensions", "ordering", "priorityDerivation", "evaluation", "rules"} + if not isinstance(value, dict) or set(value) != fields: + return "work classification contract fields are invalid" + if value.get("$schema") != "./work-classification.schema.json": + return "work classification schema reference drifted" + if value.get("schema") != "new-project.work-classification/v1": + return "unsupported work classification schema" + if value.get("dimensions") != { + "kind": ["BUG", "FEATURE", "SERVICE"], + "priority": ["P0", "P1", "P2", "P3"], + "origin": ["regression", "requested", "health"], + }: + return "work classification dimensions or order drifted" + ordering = value.get("ordering") + if ordering != { + "precedence": ["dependencies", "kind", "priority", "stableId"], + "kindOrder": ["BUG", "FEATURE", "SERVICE"], + "priorityOrder": ["P0", "P1", "P2", "P3"], + "dependencyPolicy": "topological-before-ranking", + "stableIdPolicy": "lexicographic", + }: + return "work classification precedence drifted" + if value.get("priorityDerivation") != { + "impact": {"critical": "P0", "high": "P1", "medium": "P2", "low": "P3"}, + "declaredPolicy": "require-valid-priority", + "serviceDefault": "P2", + }: + return "work classification priority derivation drifted" + evaluation = value.get("evaluation") + if not isinstance(evaluation, dict) or evaluation != { + "mode": "first-match", "unmatchedPolicy": "reject", "llmRole": "advisory-only", + }: + return "work classification evaluation policy drifted" + return None + + +def complexity_rule_assignment(when: dict[str, Any]) -> tuple[tuple[str, str] | None, str | None]: + if when.get("baseline") == "measured" and ( + when.get("delta") == "increased" or when.get("threshold") == "crossed" + ): + return ("BUG", "regression"), "impact" + if when == { + "signal": "cyclomatic-complexity", + "baseline": "pre-existing", + "delta": "not-increased", + }: + return ("SERVICE", "health"), "service-default" + return None, None + + +def expected_rule_assignment(when: dict[str, Any]) -> tuple[tuple[str, str] | None, str | None]: + signal = when.get("signal") + if signal == "defect" and when.get("impact") in {"outage-or-security", "functional"}: + return ("BUG", "regression"), "impact" + if signal == "cyclomatic-complexity": + return complexity_rule_assignment(when) + if signal == "work-request" and when.get("request") == "new-behavior": + return ("FEATURE", "requested"), "declared" + if signal == "work-request" and when.get("request") == "maintenance": + return ("SERVICE", "health"), "service-default" + return None, None + + +def work_classification_rule_error(rule: dict[str, Any]) -> str | None: + if set(rule) != {"id", "when", "assign", "prioritySource"}: + return "work classification rule fields are invalid" + when = rule.get("when") + assignment = rule.get("assign") + if not isinstance(when, dict) or not isinstance(assignment, dict): + return "work classification rule condition or assignment is invalid" + expected_when_fields = { + "defect": [{"signal", "impact"}], + "cyclomatic-complexity": [ + {"signal", "baseline", "delta"}, + {"signal", "baseline", "threshold"}, + ], + "work-request": [{"signal", "request"}], + }.get(when.get("signal")) + if expected_when_fields is None or set(when) not in expected_when_fields: + return f"work classification rule {rule['id']} mixes incompatible signal fields" + expected_assignment, expected_priority_source = expected_rule_assignment(when) + if expected_assignment is None: + return f"work classification rule {rule['id']} has invalid condition values" + if set(assignment) != {"kind", "origin"}: + return f"work classification rule {rule['id']} has an invalid assignment" + actual_assignment = assignment.get("kind"), assignment.get("origin") + if actual_assignment != expected_assignment: + return f"work classification rule {rule['id']} has an invalid assignment" + if rule.get("prioritySource") != expected_priority_source: + return f"work classification rule {rule['id']} has an invalid priority source" + return None + + +def work_classification_error(value: Any) -> str | None: + header_error = work_classification_header_error(value) + if header_error: + return header_error + assert isinstance(value, dict) + rules = value.get("rules") + if not isinstance(rules, list) or len(rules) != 7: + return "work classification must contain exactly seven rules" + identifiers = [rule.get("id") for rule in rules if isinstance(rule, dict)] + expected_identifiers = [f"W-CLASS-{index:03d}" for index in range(1, 8)] + if identifiers != expected_identifiers: + return "work classification rule identifiers or first-match order drifted" + for rule in rules: + assert isinstance(rule, dict) + rule_error = work_classification_rule_error(rule) + if rule_error: + return rule_error + return None + + +def load_work_classification( + root: Path, + report: Report, + raw_path: str = ".governance/work-classification.dsl.json", +) -> dict[str, Any] | None: + try: + path = safe_repo_path(root, raw_path) + value = load_json(path) + error = work_classification_error(value) + if error: + raise ValueError(error) + except (OSError, ValueError, json.JSONDecodeError) as error: + report.add( + "GOV-MANIFEST-001", + f"Work classification contract is invalid: {error}", + "Restore the managed work-classification DSL from the pinned standard release.", + [raw_path], + ) + return None + return value + + +def rel(root: Path, path: Path) -> str: + return path.relative_to(root).as_posix() + + +def safe_repo_path(root: Path, raw: str) -> Path: + candidate = (root / raw).resolve() + try: + candidate.relative_to(root) + except ValueError as error: + raise ValueError(f"path escapes repository: {raw}") from error + return candidate + + +def string_list(value: Any, *, nonempty: bool = False) -> bool: + return ( + isinstance(value, list) + and (not nonempty or bool(value)) + and all(isinstance(item, str) and bool(item) for item in value) + and len(value) == len(set(value)) + ) + + +def relative_pattern(value: str) -> bool: + normalized = value.replace("\\", "/") + return ( + not normalized.startswith("/") + and not re.match(r"^[A-Za-z]:/", normalized) + and ".." not in normalized.split("/") + ) + + +def approval_evidence_config_valid(value: Any) -> bool: + if value is None: + return True + return ( + isinstance(value, dict) + and set(value) == { + "schema", "requiredBindings", "reviewVerificationMethod", + "signedAttestationPredicateType", + } + and value.get("schema") == "new-project.approval-evidence/v1" + and value.get("requiredBindings") == [ + "repository", "pullRequest", "headSha", "ticket", "actor", + ] + and value.get("reviewVerificationMethod") == "github-api-allowlist" + and value.get("signedAttestationPredicateType") + == "https://wellmanifest.dev/attestations/validator/v1" + ) + + +def branch_name(value: Any) -> bool: + return ( + isinstance(value, str) + and bool(value) + and not value.startswith("/") + and re.search(r"(?:\.\.|//|@\{|[~^:?*\[\\])", value) is None + ) + + +def integer_fields_valid(value: dict[str, Any], fields: Iterable[str]) -> bool: + return all( + isinstance(value.get(name), int) and not isinstance(value[name], bool) + for name in fields + ) + + +def relative_pattern_list(value: Any, *, nonempty: bool = False) -> bool: + return string_list(value, nonempty=nonempty) and all(relative_pattern(item) for item in value) + + +def delivery_limits_valid(value: dict[str, Any]) -> bool: + return all([ + isinstance(value.get("requiredForImplementation"), bool), + 1 <= value["maxActiveMinutes"] <= 30, + 1 <= value["checkpointMinutes"] < value["maxActiveMinutes"], + value["maxImplementationFiles"] >= 1, + value["maxAffectedComponents"] >= 1, + value["maxPublicInterfaceChanges"] >= 0, + value["maxRuntimeDependencies"] >= 0, + ]) + + +def delivery_policy_valid(value: Any) -> bool: + fields = { + "requiredForImplementation", "maxActiveMinutes", "checkpointMinutes", + "allowedComplexityClasses", "maxImplementationFiles", + "maxAffectedComponents", "maxPublicInterfaceChanges", + "maxRuntimeDependencies", "targetBranches", "publicInterfacePaths", + "dependencyManifestPaths", + } + if not isinstance(value, dict) or set(value) != fields: + return False + integer_limits = ( + "maxActiveMinutes", "checkpointMinutes", "maxImplementationFiles", + "maxAffectedComponents", "maxPublicInterfaceChanges", + "maxRuntimeDependencies", + ) + if not integer_fields_valid(value, integer_limits): + return False + limits_valid = delivery_limits_valid(value) + classes_valid = ( + string_list(value.get("allowedComplexityClasses"), nonempty=True) + and set(value["allowedComplexityClasses"]) <= {"XS", "S"} + ) + targets_valid = ( + string_list(value.get("targetBranches"), nonempty=True) + and all(branch_name(item) for item in value["targetBranches"]) + ) + paths_valid = relative_pattern_list(value.get("publicInterfacePaths")) and relative_pattern_list( + value.get("dependencyManifestPaths") + ) + return limits_valid and classes_valid and targets_valid and paths_valid + + +def delivery_header_error(value: dict[str, Any]) -> str | None: + if not isinstance(value.get("acceptedBaseSha"), str) or re.fullmatch(r"[0-9a-f]{40}", value["acceptedBaseSha"]) is None: + return "delivery acceptedBaseSha must be a full lowercase commit SHA" + if not branch_name(value.get("targetBranch")): + return "delivery targetBranch is invalid" + if not isinstance(value.get("outcome"), str) or not value["outcome"].strip(): + return "delivery outcome is blank" + if not string_list(value.get("nonGoals"), nonempty=True): + return "delivery nonGoals must be an explicit non-empty list" + if value.get("complexity") not in {"XS", "S"}: + return "delivery complexity must be XS or S" + minutes = value.get("estimatedMinutes") + if not isinstance(minutes, int) or isinstance(minutes, bool) or not 1 <= minutes <= 30: + return "delivery estimatedMinutes must be between 1 and 30" + return None + + +def delivery_budgets_error(budgets: Any) -> str | None: + fields = { + "maxImplementationFiles", "maxAffectedComponents", + "maxPublicInterfaceChanges", "maxRuntimeDependencies", + } + if not isinstance(budgets, dict) or set(budgets) != fields: + return "delivery budgets are incomplete" + if not integer_fields_valid(budgets, fields): + return "delivery budgets must be integers" + if budgets["maxImplementationFiles"] < 1 or budgets["maxAffectedComponents"] < 1: + return "delivery file and component budgets must be positive" + if budgets["maxPublicInterfaceChanges"] < 0 or budgets["maxRuntimeDependencies"] < 0: + return "delivery interface and dependency budgets cannot be negative" + return None + + +def delivery_components_error(components: Any) -> str | None: + if not isinstance(components, list) or not components: + return "delivery architecture requires at least one component" + names: list[str] = [] + for component in components: + if not isinstance(component, dict) or set(component) != {"name", "paths"}: + return "delivery component must contain name and paths" + if not isinstance(component.get("name"), str) or not component["name"].strip(): + return "delivery component name is blank" + if not relative_pattern_list(component.get("paths"), nonempty=True): + return "delivery component paths must be repository-relative patterns" + names.append(component["name"]) + return "delivery component names must be unique" if len(names) != len(set(names)) else None + + +def delivery_ui_error(ui: Any) -> str | None: + if not isinstance(ui, dict) or set(ui) != {"impact", "states", "evidence"}: + return "delivery UI decision is incomplete" + if ui.get("impact") not in {"none", "single-state", "multi-state"}: + return "delivery UI impact is invalid" + if not string_list(ui.get("states")) or not set(ui["states"]) <= {"loading", "empty", "error", "success"}: + return "delivery UI states are invalid" + if not string_list(ui.get("evidence")): + return "delivery UI evidence must be a unique string list" + return delivery_ui_impact_error(ui["impact"], ui["states"], ui["evidence"]) + + +def delivery_ui_impact_error(impact: str, states: list[str], evidence: list[str]) -> str | None: + if impact == "none" and (states or evidence): + return "delivery UI states/evidence must be empty when impact is none" + if impact == "single-state" and (len(states) != 1 or not evidence): + return "single-state UI work requires one state and planned evidence" + if impact == "multi-state" and (len(states) < 2 or not evidence): + return "multi-state UI work requires at least two states and planned evidence" + return None + + +def delivery_architecture_error(architecture: Any) -> str | None: + fields = { + "status", "decision", "components", "responsibilityChanges", + "interfaceChanges", "dataChanges", "ui", "rollback", + } + if not isinstance(architecture, dict) or set(architecture) != fields: + return "delivery architecture decision is incomplete" + if architecture.get("status") != "accepted": + return "delivery architecture status must be accepted before implementation" + for name in ("decision", "rollback"): + if not isinstance(architecture.get(name), str) or not architecture[name].strip(): + return f"delivery architecture {name} is blank" + if not isinstance(architecture.get("responsibilityChanges"), bool): + return "delivery responsibilityChanges must be boolean" + for name in ("interfaceChanges", "dataChanges"): + if not string_list(architecture.get(name)): + return f"delivery architecture {name} must be a unique string list" + return delivery_components_error(architecture.get("components")) or delivery_ui_error(architecture.get("ui")) + + +def delivery_validation_error(validation: Any) -> str | None: + if not isinstance(validation, list) or not validation: + return "delivery validation must map at least one acceptance criterion" + criteria: list[str] = [] + for item in validation: + if not isinstance(item, dict) or set(item) != {"criterion", "commands", "evidence"}: + return "delivery validation entry is incomplete" + if not isinstance(item.get("criterion"), str) or re.fullmatch(r"AC-[0-9]+", item["criterion"]) is None: + return "delivery validation criterion is invalid" + if not string_list(item.get("commands"), nonempty=True): + return "delivery validation commands cannot be empty" + if not isinstance(item.get("evidence"), str) or not item["evidence"].strip(): + return "delivery validation evidence is blank" + criteria.append(item["criterion"]) + return "delivery validation criteria must be unique" if len(criteria) != len(set(criteria)) else None + + +def standard_adoption_error(value: Any) -> str | None: + fields = {"sourceRepository", "fromRevision", "toRevision"} + if not isinstance(value, dict) or set(value) != fields: + return "delivery standardAdoption fields are invalid" + if value.get("sourceRepository") != "wellmanifest/new-project": + return "delivery standardAdoption sourceRepository is invalid" + from_revision = value.get("fromRevision") + to_revision = value.get("toRevision") + if from_revision is not None and ( + not isinstance(from_revision, str) + or re.fullmatch(r"[0-9a-f]{40}", from_revision) is None + ): + return "delivery standardAdoption revisions must be full lowercase commit SHAs" + if not isinstance(to_revision, str) or re.fullmatch(r"[0-9a-f]{40}", to_revision) is None: + return "delivery standardAdoption revisions must be full lowercase commit SHAs" + if from_revision == to_revision: + return "delivery standardAdoption revisions must differ" + return None + + +def delivery_intent_error(value: Any) -> str | None: + required_fields = { + "acceptedBaseSha", "targetBranch", "outcome", "nonGoals", + "complexity", "estimatedMinutes", "budgets", "architecture", + "runtimeDependencies", "validation", + } + if not isinstance(value, dict) or set(value) not in { + frozenset(required_fields), frozenset({*required_fields, "standardAdoption"}), + }: + return "delivery must contain exactly the bounded-delivery fields" + error = delivery_header_error(value) or delivery_budgets_error(value.get("budgets")) + if error: + return error + error = delivery_architecture_error(value.get("architecture")) + if error: + return error + if not string_list(value.get("runtimeDependencies")): + return "delivery runtimeDependencies must be a unique string list" + if "standardAdoption" in value: + error = standard_adoption_error(value["standardAdoption"]) + if error: + return error + return delivery_validation_error(value.get("validation")) + + +def matches(path: str, patterns: Iterable[str]) -> bool: + path_parts = path.replace("\\", "/").strip("/").split("/") + + def match_pattern(pattern: str) -> bool: + pattern_parts = pattern.replace("\\", "/").strip("/").split("/") + memo: dict[tuple[int, int], bool] = {} + + def visit(path_index: int, pattern_index: int) -> bool: + key = (path_index, pattern_index) + if key in memo: + return memo[key] + if pattern_index == len(pattern_parts): + result = path_index == len(path_parts) + elif pattern_parts[pattern_index] == "**": + result = visit(path_index, pattern_index + 1) or ( + path_index < len(path_parts) and visit(path_index + 1, pattern_index) + ) + else: + result = ( + path_index < len(path_parts) + and fnmatch.fnmatchcase(path_parts[path_index], pattern_parts[pattern_index]) + and visit(path_index + 1, pattern_index + 1) + ) + memo[key] = result + return result + + return visit(0, 0) + + return any(match_pattern(pattern) for pattern in patterns) + + +def segment_literal_prefix(pattern: str) -> str: + index = min((pattern.find(char) for char in "*?[" if char in pattern), default=len(pattern)) + return pattern[:index] + + +def segment_literal_suffix(pattern: str) -> str: + indexes = [pattern.rfind(char) for char in "*?]" if char in pattern] + return pattern[max(indexes, default=-1) + 1:] + + +def segments_may_overlap(first: str, second: str) -> bool: + first_magic = any(char in first for char in "*?[") + second_magic = any(char in second for char in "*?[") + if not first_magic and not second_magic: + return first == second + if not first_magic: + return fnmatch.fnmatchcase(first, second) + if not second_magic: + return fnmatch.fnmatchcase(second, first) + first_prefix = segment_literal_prefix(first) + second_prefix = segment_literal_prefix(second) + if first_prefix and second_prefix and not ( + first_prefix.startswith(second_prefix) or second_prefix.startswith(first_prefix) + ): + return False + first_suffix = segment_literal_suffix(first) + second_suffix = segment_literal_suffix(second) + return not ( + first_suffix + and second_suffix + and not ( + first_suffix.endswith(second_suffix) or second_suffix.endswith(first_suffix) + ) + ) + + +def patterns_may_overlap(first: str, second: str) -> bool: + first_parts = first.replace("\\", "/").strip("/").split("/") + second_parts = second.replace("\\", "/").strip("/").split("/") + memo: dict[tuple[int, int], bool] = {} + + def remaining_are_globstars(parts: list[str], index: int) -> bool: + return all(part == "**" for part in parts[index:]) + + def visit(first_index: int, second_index: int) -> bool: + key = (first_index, second_index) + if key in memo: + return memo[key] + if first_index == len(first_parts) and second_index == len(second_parts): + result = True + elif first_index == len(first_parts): + result = remaining_are_globstars(second_parts, second_index) + elif second_index == len(second_parts): + result = remaining_are_globstars(first_parts, first_index) + elif first_parts[first_index] == "**": + result = visit(first_index + 1, second_index) or visit(first_index, second_index + 1) + elif second_parts[second_index] == "**": + result = visit(first_index, second_index + 1) or visit(first_index + 1, second_index) + else: + result = segments_may_overlap(first_parts[first_index], second_parts[second_index]) and visit( + first_index + 1, second_index + 1 + ) + memo[key] = result + return result + + return visit(0, 0) + + +def segment_pattern_covered_by(pattern: str, owner_pattern: str) -> bool: + if pattern == owner_pattern: + return True + if not any(char in pattern for char in "*?["): + return fnmatch.fnmatchcase(pattern, owner_pattern) + if owner_pattern == "*": + return True + if "?" in owner_pattern or "[" in owner_pattern or owner_pattern.count("*") != 1: + return False + owner_prefix, owner_suffix = owner_pattern.split("*", 1) + first_magic = min( + (pattern.find(char) for char in "*?[" if char in pattern), + default=len(pattern), + ) + last_magic = max(pattern.rfind(char) for char in "*?[") + pattern_prefix = pattern[:first_magic] + pattern_suffix = pattern[last_magic + 1:] + return pattern_prefix.startswith(owner_prefix) and pattern_suffix.endswith(owner_suffix) + + +def pattern_covered_by(pattern: str, owner_pattern: str) -> bool: + if pattern == owner_pattern: + return True + if not any(char in pattern for char in "*?["): + return matches(pattern, [owner_pattern]) + pattern_parts = pattern.replace("\\", "/").strip("/").split("/") + owner_parts = owner_pattern.replace("\\", "/").strip("/").split("/") + if owner_parts and owner_parts[-1] == "**" and len(pattern_parts) >= len(owner_parts) - 1: + prefix = owner_parts[:-1] + return all( + segment_pattern_covered_by(allowed, owned) + for allowed, owned in zip(pattern_parts, prefix) + ) + if len(pattern_parts) == len(owner_parts) and "**" not in owner_parts: + return all( + segment_pattern_covered_by(allowed, owned) + for allowed, owned in zip(pattern_parts, owner_parts) + ) + return False + + +def git_output(root: Path, args: list[str]) -> bytes: + return subprocess.run( + ["git", *args], cwd=root, check=True, capture_output=True, + ).stdout + + +def changed_paths(root: Path, base: str | None, head: str, explicit: list[str]) -> list[str]: + if explicit: + normalized = sorted({path.replace("\\", "/").removeprefix("./") for path in explicit if path}) + for path in normalized: + safe_repo_path(root, path) + return normalized + try: + if base: + raw = git_output(root, ["diff", "--name-only", "-z", f"{base}...{head}"]) + paths = raw.decode("utf-8", "surrogateescape").split("\0") + else: + tracked = git_output(root, ["diff", "--name-only", "-z", "HEAD"]) + untracked = git_output(root, ["ls-files", "--others", "--exclude-standard", "-z"]) + paths = (tracked + untracked).decode("utf-8", "surrogateescape").split("\0") + return sorted({path for path in paths if path}) + except (subprocess.CalledProcessError, FileNotFoundError) as error: + raise RuntimeError("Git could not determine the changed-path set") from error + + +def check_history_order( + root: Path, + base: str | None, + head: str, + ticket_name: str, + ticket_root: str, + intent_path: str, + governance_patterns: list[str], + report: Report, +) -> None: + if not base: + return + try: + commits = git_output(root, ["rev-list", "--reverse", f"{base}..{head}"]).decode().splitlines() + except (subprocess.CalledProcessError, FileNotFoundError): + report.add( + "GOV-DIFF-001", "Git could not enumerate commits for history-order validation.", + "Fetch the complete base/head history and rerun the governance gate.", + evidence={"base": base, "head": head}, + ) + return + first_implementation: tuple[int, str] | None = None + for index, commit in enumerate(commits): + try: + raw = git_output(root, ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", "-z", commit]) + except subprocess.CalledProcessError: + report.add( + "GOV-DIFF-001", f"Git could not inspect commit {commit}.", + "Fetch complete commit objects and rerun the governance gate.", + evidence={"commit": commit}, + ) + return + paths = [path for path in raw.decode("utf-8", "surrogateescape").split("\0") if path] + if any(not matches(path, governance_patterns) for path in paths): + first_implementation = (index, commit) + break + if first_implementation is None: + return + index, commit = first_implementation + parent = f"{commit}^" if index > 0 else base + ticket_intent = f"{ticket_root.rstrip('/')}/{ticket_name}/{intent_path}" + try: + subprocess.run( + ["git", "cat-file", "-e", f"{parent}:{ticket_intent}"], cwd=root, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + except subprocess.CalledProcessError: + report.add( + "GOV-INTENT-003", + f"{ticket_intent} did not exist before the first implementation commit.", + "Commit the plan-only ticket and intent first; start implementation in a later commit after review.", + [ticket_intent], {"firstImplementationCommit": commit}, + ) + + +def standard_policy_valid(standard: Any) -> bool: + return ( + isinstance(standard, dict) + and set(standard) == {"id", "version"} + and standard.get("id") == "wellmanifest/new-project" + and isinstance(standard.get("version"), str) + and re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", standard["version"]) is not None + ) + + +def ticket_policy_valid(ticket: Any) -> bool: + base_fields = { + "root", "directoryPattern", "requiredFiles", "requiredAgentFiles", + "activeStatuses", "closedStatuses", "implementationStates", "intentFile", + } + if not isinstance(ticket, dict) or set(ticket) not in { + frozenset(base_fields), frozenset({*base_fields, "nonActiveStatuses"}), + }: + return False + values_valid = ticket_scalar_policy_valid(ticket) and ticket_list_policy_valid(ticket) + if not values_valid: + return False + try: + re.compile(ticket["directoryPattern"]) + except re.error: + return False + return True + + +def ticket_scalar_policy_valid(ticket: dict[str, Any]) -> bool: + return all([ + isinstance(ticket.get("root"), str) and bool(ticket["root"]) and relative_pattern(ticket["root"]), + isinstance(ticket.get("directoryPattern"), str) and bool(ticket["directoryPattern"]), + isinstance(ticket.get("intentFile"), str) and bool(ticket["intentFile"]) and relative_pattern(ticket["intentFile"]), + ]) + + +def ticket_list_policy_valid(ticket: dict[str, Any]) -> bool: + status_groups = [ + set(ticket.get(name, [])) + for name in ("activeStatuses", "nonActiveStatuses", "closedStatuses") + ] + return all([ + relative_pattern_list(ticket.get("requiredFiles")), + relative_pattern_list(ticket.get("requiredAgentFiles")), + string_list(ticket.get("activeStatuses"), nonempty=True), + "nonActiveStatuses" not in ticket or string_list(ticket.get("nonActiveStatuses"), nonempty=True), + string_list(ticket.get("closedStatuses"), nonempty=True), + string_list(ticket.get("implementationStates"), nonempty=True), + all( + left.isdisjoint(right) + for index, left in enumerate(status_groups) + for right in status_groups[index + 1:] + ), + ]) + + +def docker_policy_valid(docker: Any) -> bool: + return ( + isinstance(docker, dict) + and set(docker) == {"required", "dockerfiles", "composeFiles"} + and isinstance(docker.get("required"), bool) + and relative_pattern_list(docker.get("dockerfiles"), nonempty=True) + and relative_pattern_list(docker.get("composeFiles"), nonempty=True) + ) + + +def workstreams_policy_valid(workstreams: Any) -> bool: + if not isinstance(workstreams, dict) or not workstreams: + return False + valid_name = re.compile(r"[a-z0-9][a-z0-9-]*").fullmatch + return all( + isinstance(name, str) + and valid_name(name) is not None + and isinstance(item, dict) + and set(item) == {"ownedPaths"} + and relative_pattern_list(item.get("ownedPaths"), nonempty=True) + for name, item in workstreams.items() + ) + + +def integration_policy_valid(integration: Any, workstreams: dict[str, Any]) -> bool: + return ( + isinstance(integration, dict) + and set(integration) == {"workstream", "requiredForPaths"} + and isinstance(integration.get("workstream"), str) + and relative_pattern_list(integration.get("requiredForPaths")) + and integration["workstream"] in workstreams + ) + + +def coordination_policy_valid(coordination: Any) -> bool: + fields = { + "mode", "maxActiveTicketsPerWorkstream", "rejectActiveScopeOverlap", + "workstreams", "integration", + } + if not isinstance(coordination, dict) or set(coordination) != fields: + return False + limit = coordination.get("maxActiveTicketsPerWorkstream") + settings_valid = ( + coordination.get("mode") == "workstreams" + and isinstance(limit, int) + and not isinstance(limit, bool) + and limit >= 1 + and isinstance(coordination.get("rejectActiveScopeOverlap"), bool) + ) + workstreams = coordination.get("workstreams") + return ( + settings_valid + and workstreams_policy_valid(workstreams) + and integration_policy_valid(coordination.get("integration"), workstreams) + ) + + +def common_manifest_policy_valid(manifest: dict[str, Any]) -> bool: + approvals = manifest.get("trustedApprovalSources") + return ( + standard_policy_valid(manifest.get("standard")) + and relative_pattern_list(manifest.get("requiredFiles")) + and relative_pattern_list(manifest.get("governancePaths")) + and string_list(approvals, nonempty=True) + and set(approvals) <= { + "github-review", "github-app-review", "signed-attestation", + } + and approval_evidence_config_valid(manifest.get("approvalEvidence")) + and ticket_policy_valid(manifest.get("ticket")) + and docker_policy_valid(manifest.get("docker")) + ) + + +def basic_manifest_valid(manifest: Any) -> bool: + if not isinstance(manifest, dict) or manifest.get("schema") not in { + "new-project.governance/v1", "new-project.governance/v2", + }: + return False + common_valid = common_manifest_policy_valid(manifest) + if not common_valid or manifest["schema"] == "new-project.governance/v1": + return common_valid + if "nonActiveStatuses" not in manifest["ticket"]: + return False + allowed_root_keys = { + "$schema", "schema", "standard", "requiredFiles", "governancePaths", + "trustedApprovalSources", "approvalEvidence", "ticket", "docker", + "coordination", "delivery", "stacks", + } + coordination = manifest.get("coordination") + delivery = manifest.get("delivery") + return ( + set(manifest) <= allowed_root_keys + and string_list(manifest.get("stacks", [])) + and set(manifest.get("stacks", [])) <= {"node", "python", "go", "rust", "java", "docker", "frontend", "terraform", "kubernetes"} + and coordination_policy_valid(coordination) + and (delivery is None or delivery_policy_valid(delivery)) + ) + + +def lock_standard_valid(standard: Any, expected_version: str) -> bool: + return isinstance(standard, dict) and ( + set(standard) == {"id", "version", "sourceRepository", "sourceRevision", "publicationStatus"} + and standard.get("id") == "wellmanifest/new-project" + and standard.get("version") == expected_version + and standard.get("sourceRepository") == "wellmanifest/new-project" + and isinstance(standard.get("sourceRevision"), str) + and re.fullmatch(r"[0-9a-f]{40}", standard["sourceRevision"]) is not None + and standard.get("publicationStatus") == "published" + ) + + +def load_managed_lock(lock_path: Path, manifest: dict[str, Any]) -> dict[str, str]: + lock = load_json(lock_path) + managed = lock["managedFiles"] + if ( + lock.get("schema") != "new-project.lock/v1" + or set(lock) != {"schema", "standard", "managedFiles"} + or not isinstance(managed, dict) + ): + raise ValueError("unsupported lock schema") + if not lock_standard_valid(lock["standard"], manifest["standard"]["version"]): + raise ValueError("lock must identify the published immutable standard revision") + if not all( + isinstance(raw_path, str) + and relative_pattern(raw_path) + and isinstance(digest, str) + and re.fullmatch(r"[a-f0-9]{64}", digest) + for raw_path, digest in managed.items() + ): + raise ValueError("managedFiles must map repository-relative paths to lowercase SHA-256 digests") + return managed + + +def check_managed_file(root: Path, raw_path: str, expected: str, report: Report) -> None: + try: + path = safe_repo_path(root, raw_path) + except ValueError as error: + report.add("GOV-SYNC-001", str(error), "Use repository-relative managed paths.", [raw_path]) + return + actual = hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None + if actual != expected: + report.add( + "GOV-SYNC-001", f"Managed governance file digest differs: {raw_path}", + "Restore the pinned file or perform an explicit standard upgrade and regenerate the lock.", + [raw_path], {"expectedSha256": expected, "actualSha256": actual}, + ) + + +def extension_error(required: Any, candidate: Any, path: str = "$") -> str | None: + if isinstance(required, dict): + if not isinstance(candidate, dict): + return f"{path} must remain an object" + for key, value in required.items(): + if key not in candidate: + return f"{path}/{key} is required by the managed base" + error = extension_error(value, candidate[key], f"{path}/{key}") + if error: + return error + return None + if isinstance(required, list): + if not isinstance(candidate, list): + return f"{path} must remain an array" + for value in required: + if value not in candidate: + return f"{path} removed a value required by the managed base" + return None + if candidate != required: + return f"{path} differs from the managed base" + return None + + +def check_lock( + root: Path, + lock_path: Path | None, + manifest: dict[str, Any], + report: Report, +) -> None: + if lock_path is None: + return + if not lock_path.is_file(): + report.add( + "GOV-SYNC-001", "Governance lock file is missing.", + "Copy the versioned manifest lock from the approved standard adoption.", + [rel(root, lock_path)] if lock_path.is_relative_to(root) else [], + ) + return + try: + managed = load_managed_lock(lock_path, manifest) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as error: + report.add("GOV-SYNC-001", f"Governance lock is invalid: {error}", "Regenerate the lock from a trusted standard release.", [rel(root, lock_path)]) + return + for raw_path, expected in sorted(managed.items()): + check_managed_file(root, raw_path, expected, report) + package_path = root / ".governance/package-manifest.json" + if not package_path.is_file(): + return + try: + strategies = package_strategies(package_path.read_bytes()) + except (OSError, TypeError, ValueError, json.JSONDecodeError) as error: + report.add( + "GOV-SYNC-001", f"Governance package manifest is invalid: {error}", + "Restore the pinned package manifest through an explicit standard upgrade.", + [rel(root, package_path)], + ) + return + manifest_target = ".governance/manifest.json" + base_target = ".governance/manifest.base.json" + if strategies.get(manifest_target) != "extendable": + return + if strategies.get(base_target) != "managed" or base_target not in managed: + report.add( + "GOV-SYNC-001", "Extendable governance manifest has no hash-bound managed base.", + "Adopt the complete published package including manifest.base.json.", + [base_target, manifest_target], + ) + return + try: + base = load_json(safe_repo_path(root, base_target)) + error = extension_error(base, manifest) + except (OSError, ValueError, json.JSONDecodeError) as load_error: + error = f"managed manifest base is invalid: {load_error}" + if error: + report.add( + "GOV-SYNC-001", f"Target governance manifest violates its managed base: {error}", + "Restore standard-owned values; keep target changes inside the declared extension fields.", + [base_target, manifest_target], + ) + + +def parse_ticket_state(readme: Path) -> tuple[str | None, str | None]: + try: + text = readme.read_text(encoding="utf-8") + except OSError: + return None, None + status_match = re.search(r"(?mi)^-[ \t]+\*\*Status\*\*:[ \t]*([A-Z_]+)[ \t]*$", text) + state_match = re.search(r"(?mi)^-[ \t]+\*\*Workflow state\*\*:[ \t]*([A-Z_]+)[ \t]*$", text) + return ( + status_match.group(1).upper() if status_match else None, + state_match.group(1).upper() if state_match else None, + ) + + +def ticket_directories(root: Path, config: dict[str, Any]) -> list[Path]: + ticket_root = safe_repo_path(root, config["root"]) + pattern = re.compile(config["directoryPattern"]) + if not ticket_root.is_dir(): + return [] + return sorted( + path for path in ticket_root.iterdir() + if path.is_dir() and not path.is_symlink() and pattern.fullmatch(path.name) + ) + + +def intent_common_error(intent: dict[str, Any], ticket_name: str) -> str | None: + if intent.get("ticket") != ticket_name: + return "intent schema or ticket identity differs" + if not isinstance(intent.get("summary"), str) or not intent["summary"].strip(): + return "intent summary is blank" + for field_name in ("allowedPaths", "forbiddenPaths", "stacks"): + if not string_list(intent.get(field_name)): + return f"intent {field_name} must be a list of non-blank strings" + if not intent["allowedPaths"]: + return "intent allowedPaths is empty" + for field_name in ("allowedPaths", "forbiddenPaths"): + if not all(relative_pattern(value) for value in intent[field_name]): + return f"intent {field_name} must contain repository-relative patterns" + return None + + +def ticket_id_list_error(intent: dict[str, Any], field_name: str) -> str | None: + values = intent.get(field_name) + if not isinstance(values, list) or not all( + isinstance(value, str) and re.fullmatch(r"ticket-[0-9]{3}", value) + for value in values + ): + return f"intent {field_name} must contain ticket IDs" + return f"intent {field_name} contains duplicates" if len(values) != len(set(values)) else None + + +def intent_v2_error(intent: dict[str, Any], ticket_name: str) -> str | None: + workstream = intent.get("workstream") + if not isinstance(workstream, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]*", workstream): + return "intent workstream is invalid" + for field_name in ("dependsOn", "conflictsWith"): + error = ticket_id_list_error(intent, field_name) + if error: + return error + integration = intent.get("integrationTicket") + if integration is not None and (not isinstance(integration, str) or not re.fullmatch(r"ticket-[0-9]{3}", integration)): + return "intent integrationTicket must be null or a ticket ID" + if integration == ticket_name: + return "intent integrationTicket cannot reference its own ticket" + return delivery_intent_error(intent["delivery"]) if "delivery" in intent else None + + +def intent_classification_error(value: Any) -> str | None: + if not isinstance(value, dict) or set(value) != {"kind", "priority", "origin"}: + return "intent classification must contain kind, priority and origin" + if value.get("kind") not in {"BUG", "FEATURE", "SERVICE"}: + return "intent classification kind is invalid" + if value.get("priority") not in {"P0", "P1", "P2", "P3"}: + return "intent classification priority is invalid" + if value.get("origin") not in {"regression", "requested", "health"}: + return "intent classification origin is invalid" + return None + + +def intent_fields_error(intent: Any) -> str | None: + v1_fields = {"schema", "ticket", "summary", "allowedPaths", "forbiddenPaths", "stacks"} + v2_fields = v1_fields | {"workstream", "dependsOn", "conflictsWith", "integrationTicket"} + if not isinstance(intent, dict) or intent.get("schema") not in { + "new-project.intent/v1", "new-project.intent/v2", "new-project.intent/v3", + }: + return "unsupported intent schema" + expected = v1_fields if intent["schema"] == "new-project.intent/v1" else v2_fields + if intent["schema"] == "new-project.intent/v3": + expected |= {"classification"} + allowed = [expected] if intent["schema"] == "new-project.intent/v1" else [expected, expected | {"delivery"}] + if set(intent) not in allowed: + return f"intent must contain exactly the {intent['schema'].rsplit('/', 1)[-1]} fields" + return None + + +def validate_intent(path: Path, ticket_name: str) -> tuple[dict[str, Any] | None, str | None]: + try: + intent = load_json(path) + except (OSError, json.JSONDecodeError) as error: + return None, str(error) + error = intent_fields_error(intent) + if error: + return None, error + assert isinstance(intent, dict) + error = intent_common_error(intent, ticket_name) + if error: + return None, error + if intent["schema"] in {"new-project.intent/v2", "new-project.intent/v3"}: + error = intent_v2_error(intent, ticket_name) + if error: + return None, error + if intent["schema"] == "new-project.intent/v3": + error = intent_classification_error(intent.get("classification")) + if error: + return None, error + return intent, None + + +def load_ticket_records(directories: list[Path], config: dict[str, Any]) -> list[TicketRecord]: + records = [] + for directory in directories: + status, workflow = parse_ticket_state(directory / "README.md") + intent, error = validate_intent(directory / config["intentFile"], directory.name) + records.append(TicketRecord(directory, status, workflow, intent, error)) + return records + + +def repository_files(root: Path, changed: list[str]) -> list[str]: + try: + raw = git_output(root, ["ls-files", "-co", "--exclude-standard", "-z"]) + files = raw.decode("utf-8", "surrogateescape").split("\0") + except (subprocess.CalledProcessError, FileNotFoundError): + files = [rel(root, path) for path in root.rglob("*") if path.is_file() and ".git" not in path.parts] + return sorted({*files, *changed} - {""}) + + +def valid_active_tickets( + root: Path, + config: dict[str, Any], + active: list[TicketRecord], + workstreams: dict[str, Any], + report: Report, +) -> list[TicketRecord]: + valid: list[TicketRecord] = [] + for record in active: + intent_path = rel(root, record.directory / config["intentFile"]) + if record.intent_error: + report.add( + "GOV-INTENT-002", f"Ticket intent is invalid: {record.intent_error}", + "Create a valid new-project.intent/v3 file before implementation.", [intent_path], + ) + continue + assert record.intent is not None + if record.intent["schema"] != "new-project.intent/v3": + report.add( + "GOV-INTENT-002", f"Active ticket {record.directory.name} lacks deterministic intent/v3 classification.", + "Migrate the active ticket to intent/v3 and declare kind, priority and origin; archived v1/v2 tickets remain readable.", [intent_path], + ) + continue + workstream = record.intent["workstream"] + if workstream not in workstreams: + report.add( + "GOV-WORKSTREAM-001", f"Active ticket {record.directory.name} declares unknown workstream '{workstream}'.", + "Choose a workstream declared in the pinned governance manifest and obtain fresh plan approval.", [intent_path], + {"workstream": workstream, "knownWorkstreams": sorted(workstreams)}, + ) + continue + valid.append(record) + return valid + + +def check_workstream_limits( + root: Path, + valid_active: list[TicketRecord], + limit: int, + report: Report, +) -> None: + grouped: dict[str, list[TicketRecord]] = {} + for record in valid_active: + grouped.setdefault(record.intent["workstream"], []).append(record) # type: ignore[index] + for workstream, members in sorted(grouped.items()): + if len(members) > limit: + report.add( + "GOV-WORKSTREAM-002", f"Workstream '{workstream}' has {len(members)} active tickets; limit is {limit}.", + "Keep one active implementation ticket in this workstream or close/block-route the competing scope.", + [rel(root, member.directory) for member in members], + {"workstream": workstream, "tickets": [member.directory.name for member in members], "limit": limit}, + ) + + +def dependency_graph( + root: Path, + records: list[TicketRecord], + config: dict[str, Any], + report: Report, +) -> dict[str, list[str]]: + graph: dict[str, list[str]] = {} + for record in records: + if record.intent and record.intent.get("schema") in {"new-project.intent/v2", "new-project.intent/v3"}: + graph[record.directory.name] = list(record.intent["dependsOn"]) + if record.directory.name in record.intent["dependsOn"] or record.directory.name in record.intent["conflictsWith"]: + report.add( + "GOV-DEPENDENCY-001", f"Ticket {record.directory.name} references itself as a dependency or conflict.", + "Remove the self-reference and keep only directed edges to other tickets.", [rel(root, record.directory / config["intentFile"])], + ) + return graph + + +def find_dependency_cycle(graph: dict[str, list[str]]) -> list[str]: + visiting: set[str] = set() + visited: set[str] = set() + cycle: list[str] = [] + + def visit(name: str, trail: list[str]) -> bool: + if name in visiting: + cycle.extend(trail[trail.index(name):] + [name]) + return True + if name in visited: + return False + visiting.add(name) + for dependency in graph.get(name, []): + if dependency in graph and visit(dependency, [*trail, dependency]): + return True + visiting.remove(name) + visited.add(name) + return False + + for name in sorted(graph): + if visit(name, [name]): + return cycle + return [] + + +def check_dependency_cycle(graph: dict[str, list[str]], report: Report) -> None: + cycle = find_dependency_cycle(graph) + if cycle: + report.add( + "GOV-DEPENDENCY-001", "Ticket dependency graph contains a cycle.", + "Break the cycle by choosing a directed implementation order or an explicit integration ticket.", + [f"project/{item}/intent.json" for item in sorted(set(cycle))], {"cycle": cycle}, + ) + + +def integration_reference_valid(record: TicketRecord | None, required_workstream: str) -> bool: + return bool( + record is not None + and record.intent is not None + and record.intent.get("schema") in {"new-project.intent/v2", "new-project.intent/v3"} + and record.intent.get("workstream") == required_workstream + and record.status != "CANCELLED" + ) + + +def check_active_relationships( + root: Path, + config: dict[str, Any], + coordination: dict[str, Any], + records: list[TicketRecord], + active: list[TicketRecord], + valid_active: list[TicketRecord], + report: Report, +) -> None: + closed_statuses = set(config.get("closedStatuses", [])) + by_name = {record.directory.name: record for record in records} + active_names = {record.directory.name for record in active} + conflict_pairs: set[tuple[str, str]] = set() + integration_config = coordination["integration"] + for record in valid_active: + assert record.intent is not None + for dependency in record.intent["dependsOn"]: + prerequisite = by_name.get(dependency) + if prerequisite is None or prerequisite.status not in closed_statuses: + report.add( + "GOV-DEPENDENCY-002", f"Active ticket {record.directory.name} has unfinished or missing dependency {dependency}.", + "Complete the prerequisite or return the dependent ticket to a non-active planning backlog.", + [rel(root, record.directory / config["intentFile"])], + {"ticket": record.directory.name, "dependency": dependency, "dependencyStatus": prerequisite.status if prerequisite else None}, + ) + for conflict in record.intent["conflictsWith"]: + if conflict in active_names: + conflict_pairs.add(tuple(sorted((record.directory.name, conflict)))) + integration_name = record.intent["integrationTicket"] + if integration_name is not None: + integration_record = by_name.get(integration_name) + valid_integration = integration_reference_valid(integration_record, integration_config["workstream"]) + if not valid_integration: + report.add( + "GOV-INTEGRATION-001", + f"Ticket {record.directory.name} references an invalid integration ticket {integration_name}.", + "Reference an existing, non-cancelled ticket in the manifest-declared integration workstream.", + [rel(root, record.directory / config["intentFile"])], + {"ticket": record.directory.name, "integrationTicket": integration_name, "requiredWorkstream": integration_config["workstream"]}, + ) + for first, second in sorted(conflict_pairs): + report.add( + "GOV-CONFLICT-001", f"Conflicting tickets {first} and {second} are active together.", + "Serialize the tickets or resolve the conflict through an approved integration plan.", + [f"project/{first}/intent.json", f"project/{second}/intent.json"], + ) + + +def check_workstream_claims( + root: Path, + config: dict[str, Any], + workstreams: dict[str, Any], + governance_patterns: list[str], + files: list[str], + valid_active: list[TicketRecord], + report: Report, +) -> None: + for record in valid_active: + assert record.intent is not None + owned_paths = workstreams[record.intent["workstream"]]["ownedPaths"] + implementation_patterns = [ + pattern for pattern in record.intent["allowedPaths"] + if not matches(pattern, governance_patterns) + ] + unowned_patterns = [ + pattern for pattern in implementation_patterns + if not any(pattern_covered_by(pattern, owned) for owned in owned_paths) + ] + unowned_claims = [ + path for path in files + if not matches(path, governance_patterns) + and matches(path, record.intent["allowedPaths"]) + and not matches(path, record.intent["forbiddenPaths"]) + and not matches(path, owned_paths) + ] + if unowned_patterns or unowned_claims: + report.add( + "GOV-WORKSTREAM-003", f"Ticket {record.directory.name} claims paths outside workstream '{record.intent['workstream']}'.", + "Narrow allowedPaths or route the paths to their owning workstream/integration ticket and obtain fresh approval.", + sorted({*unowned_patterns, *unowned_claims})[:20], + { + "ticket": record.directory.name, + "workstream": record.intent["workstream"], + "ownedPaths": owned_paths, + "unownedPatterns": unowned_patterns, + "concretePathCount": len(unowned_claims), + }, + ) + + +def ticket_shared_files( + first: TicketRecord, + second: TicketRecord, + files: list[str], + governance_patterns: list[str], +) -> list[str]: + assert first.intent is not None and second.intent is not None + return [ + path for path in files + if not matches(path, governance_patterns) + and matches(path, first.intent["allowedPaths"]) + and not matches(path, first.intent["forbiddenPaths"]) + and matches(path, second.intent["allowedPaths"]) + and not matches(path, second.intent["forbiddenPaths"]) + ] + + +def ticket_overlapping_patterns( + first: TicketRecord, + second: TicketRecord, + governance_patterns: list[str], +) -> list[str]: + assert first.intent is not None and second.intent is not None + first_patterns = [pattern for pattern in first.intent["allowedPaths"] if not matches(pattern, governance_patterns)] + second_patterns = [pattern for pattern in second.intent["allowedPaths"] if not matches(pattern, governance_patterns)] + return sorted({ + f"{first_pattern} <-> {second_pattern}" + for first_pattern in first_patterns + for second_pattern in second_patterns + if patterns_may_overlap(first_pattern, second_pattern) + }) + + +def check_scope_overlaps( + valid_active: list[TicketRecord], + files: list[str], + governance_patterns: list[str], + report: Report, +) -> None: + for index, first in enumerate(valid_active): + for second in valid_active[index + 1:]: + shared_files = ticket_shared_files(first, second, files, governance_patterns) + overlapping_patterns = ticket_overlapping_patterns(first, second, governance_patterns) + if shared_files or overlapping_patterns: + report.add( + "GOV-WORKSTREAM-004", + f"Active ticket scopes overlap: {first.directory.name} and {second.directory.name}.", + "Narrow one allowedPaths declaration, serialize the work, or route the shared contract through integration.", + shared_files[:20], + {"tickets": [first.directory.name, second.directory.name], "overlappingPatterns": overlapping_patterns, "concretePathCount": len(shared_files)}, + ) + + +def check_ticket_statuses( + root: Path, + config: dict[str, Any], + records: list[TicketRecord], + report: Report, +) -> None: + allowed = set(config.get("activeStatuses", ACTIVE_DEFAULT)) + allowed.update(config.get("nonActiveStatuses", [])) + allowed.update(config.get("closedStatuses", [])) + for record in records: + if record.status not in allowed: + report.add( + "GOV-STATUS-001", + f"Ticket {record.directory.name} has unknown status '{record.status or 'MISSING'}'.", + "Use a status declared in activeStatuses, nonActiveStatuses or closedStatuses.", + [rel(root, record.directory / "README.md")], + { + "ticket": record.directory.name, + "status": record.status, + "allowedStatuses": sorted(allowed), + }, + ) + + +def check_coordination( + root: Path, + manifest: dict[str, Any], + records: list[TicketRecord], + changed: list[str], + report: Report, +) -> None: + coordination = manifest.get("coordination") + if not isinstance(coordination, dict): + return + config = manifest["ticket"] + check_ticket_statuses(root, config, records, report) + active = [record for record in records if record.status in set(config.get("activeStatuses", ACTIVE_DEFAULT))] + workstreams = coordination["workstreams"] + valid_active = valid_active_tickets(root, config, active, workstreams, report) + check_workstream_limits(root, valid_active, coordination["maxActiveTicketsPerWorkstream"], report) + check_dependency_cycle(dependency_graph(root, records, config, report), report) + check_active_relationships(root, config, coordination, records, active, valid_active, report) + files = repository_files(root, changed) + governance_patterns = manifest["governancePaths"] + check_workstream_claims(root, config, workstreams, governance_patterns, files, valid_active, report) + if coordination["rejectActiveScopeOverlap"]: + check_scope_overlaps(valid_active, files, governance_patterns, report) + + +def check_required_files(root: Path, manifest: dict[str, Any], report: Report) -> None: + missing = [] + for raw in manifest["requiredFiles"]: + try: + if not safe_repo_path(root, raw).exists(): + missing.append(raw) + except ValueError: + missing.append(raw) + if missing: + report.add("GOV-BOOT-001", "Required target-repository files are missing.", "Run the approved new-project bootstrap before implementation.", missing) + + docker = manifest["docker"] + if docker["required"]: + def first_repo_file(names: list[str]) -> str | None: + for name in names: + try: + if safe_repo_path(root, name).is_file(): + return name + except ValueError: + continue + return None + + dockerfile = first_repo_file(docker["dockerfiles"]) + compose = first_repo_file(docker["composeFiles"]) + if dockerfile is None or compose is None: + report.add( + "GOV-DOCKER-001", "Required Dockerfile or Compose declaration is missing.", + "Add a pinned Docker runtime and validate its Compose configuration.", + [*([] if dockerfile else docker["dockerfiles"]), *([] if compose else docker["composeFiles"])], + ) + + +def immutable_image_reference(reference: str) -> bool: + return reference == "scratch" or IMMUTABLE_IMAGE_RE.fullmatch(reference) is not None + + +def dockerfile_image_references(path: Path) -> list[tuple[int, str]]: + references: list[tuple[int, str]] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + tokens = line.strip().split() + if not tokens or tokens[0].upper() != "FROM": + continue + image = next((token for token in tokens[1:] if not token.startswith("--")), "") + references.append((line_number, image)) + return references + + +def compose_image_references(path: Path) -> list[tuple[int, str]]: + references: list[tuple[int, str]] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + match = COMPOSE_IMAGE_RE.match(line) + if match: + references.append((line_number, next(value for value in match.groups() if value is not None))) + return references + + +def check_docker_image_references(root: Path, manifest: dict[str, Any], report: Report) -> None: + docker = manifest["docker"] + if not docker["required"]: + return + invalid: list[tuple[str, int, str]] = [] + for raw_path in docker["dockerfiles"]: + path = safe_repo_path(root, raw_path) + if path.is_file(): + invalid.extend( + (raw_path, line_number, reference) + for line_number, reference in dockerfile_image_references(path) + if not immutable_image_reference(reference) + ) + for raw_path in docker["composeFiles"]: + path = safe_repo_path(root, raw_path) + if path.is_file(): + invalid.extend( + (raw_path, line_number, reference) + for line_number, reference in compose_image_references(path) + if not immutable_image_reference(reference) + ) + if invalid: + report.add( + "GOV-DOCKER-002", + "Docker image references are not pinned to immutable SHA-256 digests.", + "Pin external images as name@sha256:<64 lowercase hex>; for a local-only Compose build, omit image so no mutable tag can be pulled.", + [f"{path}:{line_number}" for path, line_number, _ in invalid], + {"references": [reference for _, _, reference in invalid]}, + ) + + +def check_stacks(root: Path, manifest: dict[str, Any], profiles_path: Path | None, report: Report) -> None: + stacks = manifest.get("stacks", []) + if not stacks or profiles_path is None: + return + try: + profiles = load_json(profiles_path)["profiles"] + if not isinstance(profiles, dict): + raise TypeError("profiles must be an object") + except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError): + report.add("GOV-MANIFEST-001", "Stack profile catalog is unreadable.", "Restore the pinned stack profile catalog.", []) + return + for stack in stacks: + profile = profiles.get(stack) + if not isinstance(profile, dict): + report.add("GOV-STACK-001", f"Unknown stack profile: {stack}", "Declare a profile published by the pinned governance standard.", []) + continue + markers = profile.get("anyFiles", []) + if not string_list(markers) or not all(relative_pattern(marker) for marker in markers): + report.add("GOV-MANIFEST-001", f"Stack profile '{stack}' has invalid markers.", "Restore the pinned stack profile catalog.", []) + continue + if markers and not any(safe_repo_path(root, marker).exists() for marker in markers): + report.add("GOV-STACK-001", f"Declared stack '{stack}' has no recognized project marker.", "Add the stack marker or remove the inaccurate stack declaration.", markers) + + +def check_ticket_content(root: Path, directories: list[Path], config: dict[str, Any], report: Report) -> None: + for directory in directories: + status, _ = parse_ticket_state(directory / "README.md") + if status in set(config["activeStatuses"]): + missing = [rel(root, directory / item) for item in config["requiredFiles"] if not (directory / item).is_file()] + for pattern in config["requiredAgentFiles"]: + if not any(directory.glob(pattern)): + missing.append(rel(root, directory / pattern)) + if missing: + report.add("GOV-TICKET-003", f"Active ticket {directory.name} is missing required governance files.", "Complete the ticket scaffold before implementation.", missing) + for path in directory.rglob("*"): + if not path.is_file(): + continue + mode_executable = bool(path.stat().st_mode & 0o111) + if path.suffix.lower() in EXECUTABLE_SUFFIXES or mode_executable: + report.add( + "GOV-TICKET-004", f"Executable content is forbidden in ticket directory: {rel(root, path)}", + "Move implementation to the repository's normal source, test or scripts directory.", [rel(root, path)], + ) + + +def probable_secret_fields(text: str) -> list[str]: + fields = [] + for match in SECRET_RE.finditer(text): + shell_assignment = text[match.end(2):].startswith("=") + environment_reference = re.match(r"^[A-Z][A-Z0-9_]*=", match.group(2)) + if not shell_assignment and not environment_reference and not SAFE_SECRET_VALUES.match(match.group(2)): + fields.append(match.group(1)) + return sorted(set(fields)) + + +def check_changed_file(root: Path, raw: str, report: Report) -> None: + try: + path = safe_repo_path(root, raw) + except ValueError: + return + if not path.is_file() or path.stat().st_size > 1_000_000: + return + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + return + secrets = probable_secret_fields(text) + if secrets: + report.add( + "GOV-SECRET-001", f"Probable secret assignment detected in {raw}.", + "Remove and rotate the secret; keep only placeholders in tracked files.", [raw], {"fieldNames": secrets}, + ) + if raw.startswith(("project/ticket-", ".governance/")) and LOCAL_PATH_RE.search(text): + report.add( + "GOV-PATH-001", f"Machine-local absolute path detected in governed artifact: {raw}", + "Replace it with a repository-relative path before publication.", [raw], + ) + if fnmatch.fnmatchcase(raw, "project/ticket-*/decisions.md"): + check_decision_log_file(root, raw, text, report) + + +def check_decision_log_file(root: Path, raw: str, text: str, report: Report) -> None: + """Validate recomputable decision records (C-DECISION / ticket-031).""" + scripts_dir = Path(__file__).resolve().parent + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from decision_record import parse_dsl_record, split_decision_blocks, validate_record + except ImportError: + report.add( + "GOV-DECISION-002", + f"Cannot import decision_record helper while validating {raw}.", + "Keep scripts/decision_record.py next to governance_check.py.", + [raw], + ) + return + blocks = split_decision_blocks(text) + if not blocks: + report.add( + "GOV-DECISION-001", + f"Decision log {raw} has no DECISION records.", + "Append a fenced ```dsl DECISION record or remove the empty log.", + [raw], + ) + return + for block in blocks: + try: + record = parse_dsl_record(block) + except ValueError as error: + report.add( + "GOV-DECISION-002", + f"Decision record in {raw} is not parseable: {error}", + "Store deterministic INPUT lines and a complete DECISION shape.", + [raw], + ) + continue + for error in validate_record(record): + code = "GOV-DECISION-002" + if "GOV-DECISION-003" in error or "ADVISORY" in error: + code = "GOV-DECISION-003" + elif "GOV-DECISION-004" in error or "replayed verdict" in error: + code = "GOV-DECISION-004" + elif "GOV-DECISION-001" in error: + code = "GOV-DECISION-001" + report.add( + code, + f"Decision record in {raw}: {error}", + "Fix the record so INPUT + APPLIED_RULE recompute VERDICT with DETERMINISTIC authority.", + [raw], + ) + + +def check_changed_content(root: Path, changed: list[str], actor: str, trusted_human_change: bool, report: Report) -> None: + human_paths = [path for path in changed if fnmatch.fnmatchcase(path, "project/ticket-*/user-*.md")] + if human_paths and (actor != "human" or not trusted_human_change): + report.add( + "GOV-OWNER-001", "Human-owned participant content changed without trusted human intake evidence.", + "Revert the agent edit or have the human owner submit it through the trusted intake boundary.", human_paths, + ) + for raw in changed: + check_changed_file(root, raw, report) + + +def check_declared_delivery_budget( + policy: dict[str, Any], + delivery: dict[str, Any], + record: TicketRecord, + intent_path: str, + report: Report, +) -> None: + complexity_limit = 10 if delivery["complexity"] == "XS" else policy["maxActiveMinutes"] + declared_limits = delivery["budgets"] + policy_limits = { + "maxImplementationFiles": policy["maxImplementationFiles"], + "maxAffectedComponents": policy["maxAffectedComponents"], + "maxPublicInterfaceChanges": policy["maxPublicInterfaceChanges"], + "maxRuntimeDependencies": policy["maxRuntimeDependencies"], + } + violations = { + name: {"declared": declared_limits[name], "policy": limit} + for name, limit in policy_limits.items() + if declared_limits[name] > limit + } + if ( + delivery["complexity"] not in policy["allowedComplexityClasses"] + or delivery["estimatedMinutes"] > policy["maxActiveMinutes"] + or delivery["estimatedMinutes"] > complexity_limit + or violations + ): + report.add( + "GOV-DELIVERY-001", + f"Ticket {record.directory.name} exceeds the approved delivery class or policy budget.", + "Split the outcome into dependent XS/S slices; do not widen the current ticket or PR.", + [intent_path], + { + "complexity": delivery["complexity"], + "estimatedMinutes": delivery["estimatedMinutes"], + "maxActiveMinutes": policy["maxActiveMinutes"], + "budgetViolations": violations, + }, + ) + + +def check_delivery_timebox( + policy: dict[str, Any], + record: TicketRecord, + intent_path: str, + elapsed_minutes: int | None, + report: Report, +) -> None: + if elapsed_minutes is not None: + if elapsed_minutes >= policy["maxActiveMinutes"]: + report.add( + "GOV-DELIVERY-001", + f"Ticket {record.directory.name} reached its {policy['maxActiveMinutes']}-minute implementation timebox.", + "Stop implementation, preserve evidence and plan unfinished work as an explicit dependent slice.", + [intent_path], {"elapsedMinutes": elapsed_minutes}, + ) + elif elapsed_minutes >= policy["checkpointMinutes"]: + report.add( + "GOV-DELIVERY-002", + f"Ticket {record.directory.name} reached its delivery checkpoint.", + "Record completed and remaining scope now; stop at the hard timebox instead of expanding the diff.", + [intent_path], {"elapsedMinutes": elapsed_minutes, "stopAtMinutes": policy["maxActiveMinutes"]}, + severity="warning", + ) + + +def check_delivery_base( + root: Path, + policy: dict[str, Any], + delivery: dict[str, Any], + record: TicketRecord, + intent_path: str, + base: str | None, + report: Report, +) -> None: + if delivery["targetBranch"] not in policy["targetBranches"]: + report.add( + "GOV-BASE-001", + f"Ticket {record.directory.name} targets unapproved branch '{delivery['targetBranch']}'.", + "Choose a manifest-approved target branch and obtain fresh approval for its exact base SHA.", + [intent_path], {"allowedTargets": policy["targetBranches"]}, + ) + + accepted_sha = delivery["acceptedBaseSha"] + observed_base = None + if base: + try: + observed_base = git_output(root, ["rev-parse", f"{base}^{{commit}}"]).decode().strip() + except (subprocess.CalledProcessError, FileNotFoundError): + report.add( + "GOV-BASE-001", "The supplied base revision cannot be resolved.", + "Fetch the complete target history and rerun against the exact accepted base SHA.", + [intent_path], {"suppliedBase": base, "acceptedBaseSha": accepted_sha}, + ) + if observed_base and observed_base != accepted_sha: + report.add( + "GOV-BASE-001", f"Ticket {record.directory.name} approval is bound to a stale or different base SHA.", + "Refresh the branch, update architecture/scope evidence and obtain fresh approval before continuing.", + [intent_path], {"acceptedBaseSha": accepted_sha, "observedBaseSha": observed_base}, + ) + + target_refs = [ + f"refs/remotes/origin/{delivery['targetBranch']}", + f"refs/heads/{delivery['targetBranch']}", + ] + for target_ref in target_refs: + try: + current_target = git_output(root, ["rev-parse", "--verify", f"{target_ref}^{{commit}}"]).decode().strip() + except (subprocess.CalledProcessError, FileNotFoundError): + continue + if current_target != accepted_sha: + report.add( + "GOV-BASE-001", f"Target branch '{delivery['targetBranch']}' moved after ticket approval.", + "Refresh from the target, re-run conflict and validation checks, then obtain fresh approval if intent or architecture changed.", + [intent_path], {"acceptedBaseSha": accepted_sha, "currentTargetSha": current_target, "targetRef": target_ref}, + ) + break + + +def map_implementation_components( + implementation: list[str], + components: list[dict[str, Any]], +) -> tuple[list[str], list[str], set[str]]: + unmapped: list[str] = [] + multiply_mapped: list[str] = [] + touched_components: set[str] = set() + for path in implementation: + owners = [component["name"] for component in components if matches(path, component["paths"])] + if not owners: + unmapped.append(path) + elif len(owners) > 1: + multiply_mapped.append(path) + else: + touched_components.add(owners[0]) + return unmapped, multiply_mapped, touched_components + + +def check_delivery_architecture( + policy: dict[str, Any], + delivery: dict[str, Any], + record: TicketRecord, + implementation: list[str], + intent_path: str, + report: Report, +) -> set[str]: + declared_limits = delivery["budgets"] + architecture = delivery["architecture"] + components = architecture["components"] + component_overflow = len(components) > min( + declared_limits["maxAffectedComponents"], policy["maxAffectedComponents"], + ) + interface_overflow = len(architecture["interfaceChanges"]) > min( + declared_limits["maxPublicInterfaceChanges"], policy["maxPublicInterfaceChanges"], + ) + dependency_overflow = len(delivery["runtimeDependencies"]) > min( + declared_limits["maxRuntimeDependencies"], policy["maxRuntimeDependencies"], + ) + unmapped, multiply_mapped, touched_components = map_implementation_components(implementation, components) + if component_overflow or unmapped or multiply_mapped: + report.add( + "GOV-ARCHITECTURE-001", + f"Ticket {record.directory.name} has unresolved or ambiguous component ownership.", + "Decide component ownership before EDIT; map every changed implementation path to exactly one approved component.", + [intent_path, *unmapped, *multiply_mapped], + { + "declaredComponents": [component["name"] for component in components], + "touchedComponents": sorted(touched_components), + "unmappedPaths": unmapped, + "multiplyMappedPaths": multiply_mapped, + }, + ) + check_actual_delivery_budget( + policy, delivery, record, implementation, touched_components, + interface_overflow, dependency_overflow, report, + ) + return touched_components + + +def check_actual_delivery_budget( + policy: dict[str, Any], + delivery: dict[str, Any], + record: TicketRecord, + implementation: list[str], + touched_components: set[str], + interface_overflow: bool, + dependency_overflow: bool, + report: Report, +) -> None: + declared_limits = delivery["budgets"] + implementation_limit = min(declared_limits["maxImplementationFiles"], policy["maxImplementationFiles"]) + public_paths = [path for path in implementation if matches(path, policy["publicInterfacePaths"])] + dependency_paths = [path for path in implementation if path in policy["dependencyManifestPaths"]] + if ( + len(implementation) > implementation_limit + or len(touched_components) > declared_limits["maxAffectedComponents"] + or interface_overflow + or dependency_overflow + or len(public_paths) > declared_limits["maxPublicInterfaceChanges"] + ): + report.add( + "GOV-BUDGET-001", + f"Actual diff for {record.directory.name} exceeds its approved complexity budget.", + "Stop and split the remaining outcome into an explicitly dependent ticket; do not enlarge the current PR.", + implementation, + { + "implementationFiles": len(implementation), + "implementationFileLimit": implementation_limit, + "touchedComponents": sorted(touched_components), + "publicInterfacePaths": public_paths, + "dependencyManifestPaths": dependency_paths, + "declaredRuntimeDependencies": delivery["runtimeDependencies"], + }, + ) + + +def check_integration_ownership( + manifest: dict[str, Any], + delivery: dict[str, Any], + record: TicketRecord, + intent_path: str, + report: Report, +) -> None: + integration_workstream = manifest["coordination"]["integration"]["workstream"] + architecture = delivery["architecture"] + if (architecture["responsibilityChanges"] or architecture["dataChanges"]) and record.intent["workstream"] != integration_workstream: + report.add( + "GOV-ARCHITECTURE-001", + "Responsibility or persistent-data movement is not owned by an integration slice.", + "Create and approve a <=30-minute integration-workstream slice before changing component ownership or persistent data.", + [intent_path], + {"workstream": record.intent["workstream"], "requiredWorkstream": integration_workstream}, + ) + + +def check_delivery_gate( + root: Path, + manifest: dict[str, Any], + record: TicketRecord, + implementation: list[str], + base: str | None, + elapsed_minutes: int | None, + report: Report, +) -> None: + policy = manifest.get("delivery") + if not isinstance(policy, dict) or not policy.get("requiredForImplementation"): + return + assert record.intent is not None + delivery = record.intent.get("delivery") + intent_path = rel(root, record.directory / manifest["ticket"]["intentFile"]) + if not isinstance(delivery, dict): + report.add( + "GOV-DELIVERY-001", + f"Implementation ticket {record.directory.name} has no bounded delivery contract.", + "Return to WAIT_FOR_APPROVAL, declare one <=30-minute XS/S outcome with architecture and validation evidence, then obtain fresh approval.", + [intent_path], + ) + return + check_declared_delivery_budget(policy, delivery, record, intent_path, report) + check_delivery_timebox(policy, record, intent_path, elapsed_minutes, report) + check_delivery_base(root, policy, delivery, record, intent_path, base, report) + check_delivery_architecture(policy, delivery, record, implementation, intent_path, report) + check_integration_ownership(manifest, delivery, record, intent_path, report) + + +def ticket_owns_implementation(record: TicketRecord, implementation: list[str]) -> bool: + return bool( + record.intent is not None + and record.intent.get("schema") in {"new-project.intent/v2", "new-project.intent/v3"} + and all( + matches(path, record.intent["allowedPaths"]) + and not matches(path, record.intent["forbiddenPaths"]) + for path in implementation + ) + ) + + +def ticket_path_owners(active: list[TicketRecord], implementation: list[str]) -> dict[str, list[str]]: + return { + path: [ + record.directory.name for record in active + if record.intent is not None + and matches(path, record.intent["allowedPaths"]) + and not matches(path, record.intent["forbiddenPaths"]) + ] + for path in implementation + } + + +def select_change_ticket( + root: Path, + active: list[TicketRecord], + coordination: Any, + implementation: list[str], + report: Report, +) -> TicketRecord | None: + if not active: + report.add( + "GOV-TICKET-001", "Implementation paths changed without an active ticket.", + "Create the next target-repository ticket, publish its plan and obtain approval before editing implementation.", implementation, + ) + return None + if not isinstance(coordination, dict): + if len(active) > 1: + report.add( + "GOV-TICKET-002", "More than one active ticket exists.", + "Continue the existing ticket or close/cancel it before creating another.", + [rel(root, item.directory) for item in active], {"tickets": [item.directory.name for item in active]}, + ) + return None + return active[0] + candidates = [record for record in active if ticket_owns_implementation(record, implementation)] + if len(candidates) == 1: + return candidates[0] + if not candidates and len(active) == 1: + return active[0] + path_owners = ticket_path_owners(active, implementation) + report.add( + "GOV-TICKET-005", "Implementation diff does not resolve to exactly one active ticket.", + "Use one ticket per branch/PR, narrow allowedPaths, or create an approved integration ticket for the combined diff.", + implementation, {"candidateTickets": [record.directory.name for record in candidates], "pathOwners": path_owners}, + ) + return None + + +def check_selected_ticket_state( + root: Path, + config: dict[str, Any], + selected: TicketRecord, + implementation: list[str], + base: str | None, + head: str, + governance_patterns: list[str], + report: Report, +) -> None: + directory = selected.directory + workflow = selected.workflow + check_history_order( + root, base=base, head=head, ticket_name=directory.name, + ticket_root=config["root"], + intent_path=config["intentFile"], governance_patterns=governance_patterns, + report=report, + ) + if workflow not in set(config["implementationStates"]): + report.add( + "GOV-INTENT-001", f"Ticket {directory.name} is in workflow state {workflow or 'UNKNOWN'}, not an implementation state.", + "Keep the change plan-only until explicit approval moves the ticket to EDIT.", implementation, + ) + + +def check_workstream_change_scope( + records: list[TicketRecord], + coordination: dict[str, Any], + selected: TicketRecord, + implementation: list[str], + report: Report, +) -> None: + intent = selected.intent + assert intent is not None + workstream = coordination["workstreams"].get(intent["workstream"]) + if isinstance(workstream, dict): + unowned = [path for path in implementation if not matches(path, workstream["ownedPaths"])] + if unowned: + report.add( + "GOV-WORKSTREAM-003", f"Changed paths are not owned by workstream '{intent['workstream']}'.", + "Move the change to its owning workstream or create and approve an integration ticket; do not widen ownership retroactively.", + unowned, {"ticket": selected.directory.name, "workstream": intent["workstream"], "ownedPaths": workstream["ownedPaths"]}, + ) + integration = coordination["integration"] + shared = [path for path in implementation if matches(path, integration["requiredForPaths"])] + if shared and intent["workstream"] != integration["workstream"]: + integration_name = intent["integrationTicket"] + integration_record = next((record for record in records if record.directory.name == integration_name), None) + report.add( + "GOV-INTEGRATION-001", "Shared contract paths must be changed by the integration-workstream ticket.", + "Move the shared-path diff to the referenced integration ticket's branch; integrationTicket coordinates work but does not transfer path ownership.", + shared, + { + "ticket": selected.directory.name, + "integrationTicket": integration_name, + "validIntegrationReference": integration_reference_valid(integration_record, integration["workstream"]), + "requiredWorkstream": integration["workstream"], + }, + ) + + +def check_selected_ticket_intent( + root: Path, + manifest: dict[str, Any], + records: list[TicketRecord], + selected: TicketRecord, + implementation: list[str], + base: str | None, + elapsed_minutes: int | None, + report: Report, +) -> None: + directory = selected.directory + config = manifest["ticket"] + intent_path = directory / config["intentFile"] + intent, error = selected.intent, selected.intent_error + if error: + report.add("GOV-INTENT-002", f"Ticket intent is invalid: {error}", "Create a valid intent file before implementation.", [rel(root, intent_path)]) + else: + outside = [path for path in implementation if not matches(path, intent["allowedPaths"]) or matches(path, intent["forbiddenPaths"])] + if outside: + report.add( + "GOV-SCOPE-001", "Changed implementation paths are outside the ticket intent.", + "Revert the paths or return to PLAN, expand allowedPaths and obtain fresh approval.", outside, + {"ticket": directory.name, "allowedPaths": intent["allowedPaths"]}, + ) + coordination = manifest.get("coordination") + if isinstance(coordination, dict) and intent.get("schema") in {"new-project.intent/v2", "new-project.intent/v3"}: + check_workstream_change_scope(records, coordination, selected, implementation, report) + if intent is not None: + check_delivery_gate(root, manifest, selected, implementation, base, elapsed_minutes, report) + + +def approval_subject_valid(evidence: Any) -> bool: + required = { + "schema", "source", "repository", "pullRequest", "headSha", "ticket", + "actor", "verification", + } + return ( + isinstance(evidence, dict) + and set(evidence) == required + and evidence.get("schema") == "new-project.approval-evidence/v1" + and evidence.get("source") in { + "github-review", "github-app-review", "signed-attestation", + } + and isinstance(evidence.get("repository"), str) + and re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", evidence["repository"]) is not None + and isinstance(evidence.get("pullRequest"), int) + and not isinstance(evidence.get("pullRequest"), bool) + and evidence["pullRequest"] >= 1 + and isinstance(evidence.get("headSha"), str) + and re.fullmatch(r"[0-9a-f]{40}", evidence["headSha"]) is not None + and isinstance(evidence.get("ticket"), str) + and re.fullmatch(r"ticket-[0-9]{3}", evidence["ticket"]) is not None + ) + + +def approval_actor_valid(actor: Any) -> bool: + return ( + isinstance(actor, dict) + and set(actor) == {"login", "type"} + and isinstance(actor.get("login"), str) + and bool(actor["login"]) + and actor.get("type") in {"User", "Bot", "Workflow"} + ) + + +def approval_verification_valid(verification: Any) -> bool: + return ( + isinstance(verification, dict) + and {"method", "verified"} <= set(verification) + and set(verification) <= {"method", "verified", "issuer", "predicateType"} + and verification.get("method") in { + "github-api-allowlist", "github-attestation", "sigstore", + } + and verification.get("verified") is True + ) + + +def approval_authority_valid( + evidence: dict[str, Any], + manifest: dict[str, Any], +) -> bool: + source = evidence["source"] + actor = evidence["actor"] + verification = evidence["verification"] + method = verification["method"] + if source == "github-review": + return actor["type"] == "User" and method == "github-api-allowlist" + if source == "github-app-review": + return ( + actor["type"] == "Bot" + and actor["login"].endswith("[bot]") + and method == "github-api-allowlist" + ) + approval_config = manifest.get("approvalEvidence") or {} + expected_predicate = approval_config.get( + "signedAttestationPredicateType", + "https://wellmanifest.dev/attestations/validator/v1", + ) + return ( + actor["type"] in {"Bot", "Workflow"} + and method in {"github-attestation", "sigstore"} + and isinstance(verification.get("issuer"), str) + and bool(verification["issuer"]) + and verification.get("predicateType") == expected_predicate + ) + + +def approval_binding_mismatches( + evidence: dict[str, Any], + expected_repository: str | None, + expected_pull_request: int | None, + expected_head: str | None, +) -> tuple[bool, dict[str, dict[str, Any]]]: + missing = ( + expected_repository is None + or expected_pull_request is None + or expected_head is None + or re.fullmatch(r"[0-9a-f]{40}", expected_head or "") is None + ) + expected = { + "repository": expected_repository, + "pullRequest": expected_pull_request, + "headSha": expected_head, + } + mismatches = { + name: {"evidence": evidence[name], "expected": value} + for name, value in expected.items() + if evidence[name] != value + } + return missing, mismatches + + +def load_external_approval_evidence( + root: Path, + raw_path: str | None, + report: Report, +) -> Any | None: + if not raw_path: + return None + expanded = Path(raw_path).expanduser() + if not expanded.is_absolute(): + expanded = Path.cwd() / expanded + try: + path = expanded.parent.resolve(strict=True) / expanded.name + except OSError as error: + report.add( + "GOV-APPROVAL-003", f"Approval evidence path is unreadable: {error}", + "Have the protected approval resolver create a valid v1 evidence document outside the checkout.", + ) + return None + if path.is_relative_to(root): + report.add( + "GOV-APPROVAL-003", + "Approval evidence is controlled by the pull-request checkout.", + "Create evidence outside the checkout from a protected workflow after API or signature verification.", + [rel(root, path)], + ) + return None + no_follow = getattr(os, "O_NOFOLLOW", None) + if no_follow is None: + report.add( + "GOV-APPROVAL-003", + "Approval evidence cannot be opened safely on this platform.", + "Use a validator platform that supports no-follow file opens for external approval evidence.", + ) + return None + descriptor = -1 + try: + flags = os.O_RDONLY | no_follow | getattr(os, "O_CLOEXEC", 0) + descriptor = os.open(path, flags) + metadata = os.fstat(descriptor) + if not stat.S_ISREG(metadata.st_mode): + raise OSError("approval evidence is not a regular file") + with os.fdopen(descriptor, "r", encoding="utf-8") as handle: + descriptor = -1 + evidence = json.load(handle) + except (OSError, json.JSONDecodeError) as error: + report.add( + "GOV-APPROVAL-003", f"Approval evidence is unreadable: {error}", + "Have the protected approval resolver create a valid v1 evidence document outside the checkout.", + ) + return None + finally: + if descriptor >= 0: + os.close(descriptor) + return evidence + + +def approval_evidence( + root: Path, + raw_path: str | None, + manifest: dict[str, Any], + expected_repository: str | None, + expected_pull_request: int | None, + expected_head: str | None, + report: Report, +) -> dict[str, Any] | None: + evidence = load_external_approval_evidence(root, raw_path, report) + if evidence is None: + return None + actor = evidence.get("actor") if isinstance(evidence, dict) else None + verification = evidence.get("verification") if isinstance(evidence, dict) else None + if not ( + approval_subject_valid(evidence) + and approval_actor_valid(actor) + and approval_verification_valid(verification) + ): + report.add( + "GOV-APPROVAL-003", "Approval evidence does not conform to new-project.approval-evidence/v1.", + "Regenerate evidence with the protected resolver and the pinned approval-evidence schema.", + ) + return None + missing, mismatches = approval_binding_mismatches( + evidence, expected_repository, expected_pull_request, expected_head, + ) + if missing or mismatches: + report.add( + "GOV-APPROVAL-004", + "Approval evidence is not bound to the current repository, pull request and HEAD.", + "Pass the current protected event bindings and request a fresh approval for the exact HEAD.", + evidence={"missingExpectedBinding": missing, "mismatches": mismatches}, + ) + return None + if not approval_authority_valid(evidence, manifest): + report.add( + "GOV-APPROVAL-005", + "Approval actor or verification method is not valid for the claimed source.", + "Use an allowlisted User, an allowlisted GitHub App bot login, or a signature-verified trusted attestation issuer.", + evidence={ + "source": evidence["source"], + "actor": evidence["actor"], + "verification": evidence["verification"], + }, + ) + return None + return evidence + + +def check_change_approval( + root: Path, + manifest: dict[str, Any], + selected: TicketRecord, + approval_source: str | None, + approved_ticket: str | None, + report: Report, +) -> None: + directory = selected.directory + trusted = set(manifest["trustedApprovalSources"]) + if approval_source not in trusted: + report.add( + "GOV-APPROVAL-001", "No trusted external approval was supplied for implementation.", + "Require an approving CODEOWNER GitHub review or signed attestation; Markdown status alone is not trusted.", + [rel(root, directory / "README.md")], {"suppliedSource": approval_source, "trustedSources": sorted(trusted)}, + ) + approved_tickets = set((approved_ticket or "").split(",")) - {""} + if directory.name not in approved_tickets: + report.add( + "GOV-APPROVAL-002", "Trusted approval does not identify the active ticket.", + "Approve the current ticket after reviewing its latest intent and implementation diff.", + [rel(root, directory)], {"activeTicket": directory.name, "approvedTickets": sorted(approved_tickets)}, + ) + + +def resolve_change_approval( + root: Path, + manifest: dict[str, Any], + selected: TicketRecord, + approval_source: str | None, + approved_ticket: str | None, + approval_evidence_path: str | None, + expected_repository: str | None, + expected_pull_request: int | None, + expected_head: str | None, + report: Report, +) -> None: + supplied = approval_evidence( + root, approval_evidence_path, manifest, expected_repository, + expected_pull_request, expected_head, report, + ) + if supplied is not None: + approval_source = supplied["source"] + approved_ticket = supplied["ticket"] + elif approval_source in {"github-app-review", "signed-attestation"}: + report.add( + "GOV-APPROVAL-003", + f"Approval source {approval_source} requires external v1 evidence.", + "Create bound evidence outside the checkout after allowlist or signature verification.", + ) + check_change_approval( + root, manifest, selected, approval_source, approved_ticket, report, + ) + + +def git_revision_file(root: Path, revision: str, raw_path: str) -> bytes | None: + try: + return git_output(root, ["show", f"{revision}:{raw_path}"]) + except (subprocess.CalledProcessError, FileNotFoundError): + return None + + +def package_entry(item: Any) -> tuple[str, str, str]: + if not isinstance(item, dict) or set(item) != {"source", "target", "strategy", "executable"}: + raise ValueError("package manifest entry fields are invalid") + source, target = item.get("source"), item.get("target") + if not isinstance(source, str) or not isinstance(target, str): + raise TypeError("package manifest entry is invalid") + if not relative_pattern(source) or not relative_pattern(target): + raise ValueError("package manifest entry is invalid") + if item.get("strategy") not in {"managed", "seed", "extendable"}: + raise ValueError("package manifest entry is invalid") + if not isinstance(item.get("executable"), bool): + raise TypeError("package manifest entry is invalid") + if item.get("strategy") == "extendable" and ( + source != "governance/manifest.default.json" + or target != ".governance/manifest.json" + or item.get("executable") + ): + raise ValueError("package manifest extendable target is invalid") + return source, target, item["strategy"] + + +def package_strategies(content: bytes) -> dict[str, str]: + document = json.loads(content) + if not isinstance(document, dict) or set(document) != {"schema", "files"}: + raise ValueError("package manifest fields are invalid") + if document.get("schema") != "new-project.package-manifest/v1" or not isinstance(document.get("files"), list): + raise ValueError("package manifest schema is invalid") + strategies: dict[str, str] = {} + for item in document["files"]: + _source, target, strategy = package_entry(item) + if target in strategies: + raise ValueError("package manifest targets must be unique") + strategies[target] = strategy + if not strategies: + raise ValueError("package manifest is empty") + return strategies + + +def adoption_standard_binding_is_valid(document: dict[str, Any], expected_revision: str) -> bool: + standard = document.get("standard") + if document.get("schema") != "new-project.lock/v1" or not isinstance(standard, dict): + return False + fields = {"id", "version", "sourceRepository", "sourceRevision", "publicationStatus"} + if set(standard) != fields: + return False + expected = { + "id": "wellmanifest/new-project", + "sourceRepository": "wellmanifest/new-project", + "sourceRevision": expected_revision, + "publicationStatus": "published", + } + if any(standard.get(key) != value for key, value in expected.items()): + return False + version = standard.get("version") + return isinstance(version, str) and re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version) is not None + + +def adoption_lock(content: bytes, expected_revision: str) -> dict[str, str]: + document = json.loads(content) + if not isinstance(document, dict) or set(document) != {"schema", "standard", "managedFiles"}: + raise ValueError("adoption lock fields are invalid") + managed = document.get("managedFiles") + if not adoption_standard_binding_is_valid(document, expected_revision) or not isinstance(managed, dict): + raise ValueError("adoption lock standard binding is invalid") + if not all( + isinstance(path, str) + and relative_pattern(path) + and isinstance(digest, str) + and re.fullmatch(r"[a-f0-9]{64}", digest) is not None + for path, digest in managed.items() + ): + raise ValueError("adoption lock managed hashes are invalid") + return managed + + +def content_digest(content: bytes) -> str: + return hashlib.sha256(content).hexdigest() + + +def standard_adoption_records(active: list[TicketRecord]) -> list[TicketRecord]: + return [ + record for record in active + if record.intent is not None + and isinstance(record.intent.get("delivery"), dict) + and "standardAdoption" in record.intent["delivery"] + ] + + +def load_standard_adoption_evidence( + root: Path, + base: str, + adoption: dict[str, Any], +) -> tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str], bool]: + base_package_content = git_revision_file(root, base, ".governance/package-manifest.json") + base_lock_content = git_revision_file(root, base, ".governance/manifest.lock.json") + head_package_path = safe_repo_path(root, ".governance/package-manifest.json") + head_lock_path = safe_repo_path(root, ".governance/manifest.lock.json") + if not head_package_path.is_file() or not head_lock_path.is_file(): + raise ValueError("head package manifest or lock is missing") + initial = adoption["fromRevision"] is None + if initial: + if base_package_content is not None or base_lock_content is not None: + raise ValueError("initial adoption base already contains a package manifest or lock") + base_strategies: dict[str, str] = {} + base_hashes: dict[str, str] = {} + else: + if base_package_content is None or base_lock_content is None: + raise ValueError("upgrade base package manifest or lock is missing") + base_strategies = package_strategies(base_package_content) + base_hashes = adoption_lock(base_lock_content, adoption["fromRevision"]) + head_strategies = package_strategies(head_package_path.read_bytes()) + head_hashes = adoption_lock(head_lock_path.read_bytes(), adoption["toRevision"]) + base_managed = {path for path, strategy in base_strategies.items() if strategy == "managed"} + head_managed = {path for path, strategy in head_strategies.items() if strategy == "managed"} + if frozenset(base_hashes) not in {frozenset(base_strategies), frozenset(base_managed)}: + raise ValueError("base package targets and lock targets differ") + if set(head_hashes) != head_managed: + raise ValueError("package targets and lock targets differ") + return base_strategies, head_strategies, base_hashes, head_hashes, initial + + +def verify_changed_managed_paths( + root: Path, + base: str, + changed: list[str], + base_strategies: dict[str, str], + head_strategies: dict[str, str], + base_hashes: dict[str, str], + head_hashes: dict[str, str], + initial: bool, +) -> set[str]: + exempt: set[str] = set() + for raw_path in changed: + if head_strategies.get(raw_path) != "managed": + continue + head_path = safe_repo_path(root, raw_path) + if not head_path.is_file() or content_digest(head_path.read_bytes()) != head_hashes[raw_path]: + raise ValueError(f"head managed hash differs: {raw_path}") + base_content = git_revision_file(root, base, raw_path) + if raw_path in base_strategies: + if base_strategies[raw_path] != "managed" or base_content is None: + raise ValueError(f"managed strategy continuity differs: {raw_path}") + if content_digest(base_content) != base_hashes[raw_path]: + raise ValueError(f"base managed hash differs: {raw_path}") + elif base_content is not None: + if initial: + # Installing the standard does not erase target ownership. + # A replaced path remains an ordinary implementation change. + continue + raise ValueError(f"new managed target already existed at base: {raw_path}") + exempt.add(raw_path) + if not exempt: + raise ValueError("no changed managed payload was verified") + return exempt + + +def atomic_standard_adoption_paths( + root: Path, + base: str | None, + changed: list[str], + active: list[TicketRecord], + report: Report, +) -> set[str]: + records = standard_adoption_records(active) + if not records: + return set() + evidence_paths = [".governance/manifest.lock.json", ".governance/package-manifest.json"] + if len(records) != 1: + report.add( + "GOV-SYNC-001", + "Atomic standard adoption must resolve to exactly one active ticket.", + "Keep one approved adoption ticket active and serialize every other adoption.", + [rel(root, record.directory / "intent.json") for record in records], + ) + return set() + record = records[0] + assert record.intent is not None + adoption = record.intent["delivery"]["standardAdoption"] + error = standard_adoption_error(adoption) + if error or base is None or ".governance/manifest.lock.json" not in changed: + report.add( + "GOV-SYNC-001", + f"Atomic standard adoption preconditions are invalid: {error or 'base and changed lock are required'}.", + "Declare null-to-SHA bootstrap or distinct immutable upgrade revisions, compare against the approved Git base and regenerate the complete lock through Goal.", + evidence_paths, + ) + return set() + try: + evidence = load_standard_adoption_evidence(root, base, adoption) + return verify_changed_managed_paths(root, base, changed, *evidence) + except (OSError, TypeError, ValueError, KeyError, json.JSONDecodeError) as error: + report.add( + "GOV-SYNC-001", + f"Atomic standard adoption is inconsistent: {error}.", + "Restore the base, install the complete published managed set through Goal and regenerate its lock before review.", + evidence_paths, + {"ticket": record.directory.name, "base": base}, + ) + return set() + + +def check_change_gate( + root: Path, + manifest: dict[str, Any], + records: list[TicketRecord], + changed: list[str], + base: str | None, + head: str, + approval_source: str | None, + approved_ticket: str | None, + approval_evidence_path: str | None, + expected_repository: str | None, + expected_pull_request: int | None, + expected_head: str | None, + enforce_approval: bool, + elapsed_minutes: int | None, + report: Report, +) -> str | None: + governance_patterns = manifest["governancePaths"] + config = manifest["ticket"] + active = [record for record in records if record.status in set(config.get("activeStatuses", ACTIVE_DEFAULT))] + adoption_paths = atomic_standard_adoption_paths(root, base, changed, active, report) + implementation = [ + path for path in changed + if not matches(path, governance_patterns) and path not in adoption_paths + ] + if not implementation: + return None + selected = select_change_ticket(root, active, manifest.get("coordination"), implementation, report) + if selected is None: + return None + check_selected_ticket_state(root, config, selected, implementation, base, head, governance_patterns, report) + check_selected_ticket_intent(root, manifest, records, selected, implementation, base, elapsed_minutes, report) + if enforce_approval: + resolve_change_approval( + root, manifest, selected, approval_source, approved_ticket, + approval_evidence_path, expected_repository, expected_pull_request, + expected_head, report, + ) + return selected.directory.name + + +def sarif(payload: dict[str, Any]) -> dict[str, Any]: + findings = payload["findings"] + rules = {} + results = [] + for item in findings: + rules[item["code"]] = { + "id": item["code"], + "shortDescription": {"text": item["message"]}, + "help": {"text": item["remediation"]}, + } + result: dict[str, Any] = { + "ruleId": item["code"], + "level": "error" if item["severity"] == "error" else "warning", + "message": {"text": item["message"]}, + } + if item["paths"]: + result["locations"] = [{ + "physicalLocation": {"artifactLocation": {"uri": item["paths"][0]}}, + }] + results.append(result) + return { + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [{ + "tool": {"driver": {"name": "new-project-governance", "version": RUNTIME_VERSION, "rules": [rules[key] for key in sorted(rules)]}}, + "results": results, + }], + } + + +def render_text(payload: dict[str, Any]) -> str: + lines = [] + for item in payload["findings"]: + paths = f" [{', '.join(item['paths'])}]" if item["paths"] else "" + lines.append(f"{item['code']} {item['severity'].upper()}: {item['message']}{paths}") + lines.append(f" remediation: {item['remediation']}") + summary = payload["summary"] + code = "GOV-PASS" if payload["status"] == "passed" else "GOV-FAIL" + lines.append(f"{code}: {payload['status']} ({summary['errors']} errors, {summary['warnings']} warnings)") + return "\n".join(lines) + "\n" + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=".") + parser.add_argument("--manifest", default=".governance/manifest.json") + parser.add_argument("--lock", default=None) + parser.add_argument("--stack-profiles", default=None) + parser.add_argument( + "--work-classification", + default=".governance/work-classification.dsl.json", + ) + parser.add_argument("--base") + parser.add_argument("--head", default="HEAD") + parser.add_argument("--changed-file", action="append", default=[]) + parser.add_argument("--actor", choices=["agent", "human", "ci"], default="agent") + parser.add_argument("--trusted-human-change", action="store_true") + parser.add_argument("--enforce-approval", action="store_true") + parser.add_argument("--approval-source") + parser.add_argument("--approved-ticket") + parser.add_argument("--approval-evidence") + parser.add_argument("--expected-repository") + parser.add_argument("--expected-pull-request", type=int) + parser.add_argument("--expected-head") + parser.add_argument("--resolved-ticket-output") + parser.add_argument("--elapsed-minutes", type=int) + parser.add_argument("--format", choices=["text", "json", "sarif"], default="text") + parser.add_argument("--output") + return parser.parse_args(argv) + + +def load_manifest(root: Path, raw_path: str, report: Report) -> dict[str, Any] | None: + try: + manifest_path = safe_repo_path(root, raw_path) + except ValueError as error: + report.add("GOV-MANIFEST-001", str(error), "Use a repository-relative manifest path.") + return None + try: + manifest = load_json(manifest_path) + if not basic_manifest_valid(manifest): + raise ValueError("required manifest fields are missing or invalid") + except (OSError, ValueError, json.JSONDecodeError) as error: + report.add("GOV-MANIFEST-001", f"Governance manifest is invalid: {error}", "Restore a manifest conforming to the pinned governance schema.", [raw_path]) + return None + return manifest + + +def optional_repo_path( + root: Path, + raw_path: str | None, + code: str, + label: str, + report: Report, +) -> Path | None: + if not raw_path: + return None + try: + return safe_repo_path(root, raw_path) + except ValueError as error: + report.add(code, str(error), f"Use a repository-relative {label} path.", [raw_path]) + return None + + +def resolve_changed_paths( + args: argparse.Namespace, + root: Path, + base: str | None, + report: Report, +) -> list[str]: + try: + return changed_paths(root, base, args.head, args.changed_file) + except (RuntimeError, ValueError) as error: + report.add( + "GOV-DIFF-001", str(error), + "Use repository-relative changed paths and fetch the complete base/head history before retrying.", + evidence={"base": base, "head": args.head}, + ) + return [] + + +def resolve_validation_base( + supplied_base: str | None, + records: list[TicketRecord], + config: dict[str, Any], +) -> str | None: + if supplied_base is not None: + return supplied_base + active_statuses = set(config.get("activeStatuses", ACTIVE_DEFAULT)) + active = [record for record in records if record.status in active_statuses] + adoption_records = standard_adoption_records(active) + if len(adoption_records) != 1: + return None + record = adoption_records[0] + assert record.intent is not None + return record.intent["delivery"]["acceptedBaseSha"] + + +def run_governance_checks( + args: argparse.Namespace, + root: Path, + manifest: dict[str, Any], + report: Report, +) -> str | None: + lock_path = optional_repo_path(root, args.lock, "GOV-SYNC-001", "governance lock", report) + profiles_path = optional_repo_path(root, args.stack_profiles, "GOV-MANIFEST-001", "stack-profile", report) + directories = ticket_directories(root, manifest["ticket"]) + records = load_ticket_records(directories, manifest["ticket"]) + base = resolve_validation_base(args.base, records, manifest["ticket"]) + changed = resolve_changed_paths(args, root, base, report) + load_work_classification(root, report, args.work_classification) + check_lock(root, lock_path, manifest, report) + check_required_files(root, manifest, report) + check_docker_image_references(root, manifest, report) + check_stacks(root, manifest, profiles_path, report) + check_ticket_content(root, directories, manifest["ticket"], report) + check_coordination(root, manifest, records, changed, report) + check_changed_content(root, changed, args.actor, args.trusted_human_change, report) + return check_change_gate( + root, manifest, records, changed, base, args.head, args.approval_source, + args.approved_ticket, args.approval_evidence, args.expected_repository, + args.expected_pull_request, args.expected_head, args.enforce_approval, + args.elapsed_minutes, report, + ) + + +def formatted_report(payload: dict[str, Any], output_format: str) -> str: + if output_format == "json": + return json.dumps(payload, indent=2, sort_keys=True) + "\n" + if output_format == "sarif": + return json.dumps(sarif(payload), indent=2, sort_keys=True) + "\n" + return render_text(payload) + + +def write_report(output_path: Path | None, output: str) -> None: + if output_path is None: + sys.stdout.write(output) + return + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(output, encoding="utf-8") + + +def write_resolved_ticket( + root: Path, + raw_path: str | None, + selected_ticket: str | None, + report: Report, +) -> None: + if not raw_path or not selected_ticket or report.errors: + return + path = Path(raw_path).expanduser().resolve() + if path.is_relative_to(root): + report.add( + "GOV-PATH-001", "Resolved ticket output must be outside the repository checkout.", + "Write ephemeral approval context to runner.temp or another protected directory.", + [rel(root, path)], + ) + return + try: + path.write_text(f"{selected_ticket}\n", encoding="utf-8") + except OSError as error: + report.add( + "GOV-PATH-001", f"Could not write resolved ticket output: {error}", + "Use a writable protected directory outside the checkout.", + ) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv or sys.argv[1:]) + root = Path(args.root).resolve() + report = Report(root) + manifest = load_manifest(root, args.manifest, report) + selected_ticket: str | None = None + + if manifest is not None: + selected_ticket = run_governance_checks(args, root, manifest, report) + write_resolved_ticket(root, args.resolved_ticket_output, selected_ticket, report) + output_path = optional_repo_path(root, args.output, "GOV-PATH-001", "report output", report) + payload = report.payload() + write_report(output_path, formatted_report(payload, args.format)) + return 0 if report.errors == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.governance/intent.schema.json b/.governance/intent.schema.json new file mode 100644 index 0000000..3a2f91e --- /dev/null +++ b/.governance/intent.schema.json @@ -0,0 +1,206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/intent.schema.json", + "title": "new-project ticket intent", + "type": "object", + "additionalProperties": false, + "required": ["schema", "ticket", "summary", "workstream", "allowedPaths", "forbiddenPaths", "stacks", "dependsOn", "conflictsWith", "integrationTicket"], + "properties": { + "schema": { "enum": ["new-project.intent/v2", "new-project.intent/v3"] }, + "ticket": { "type": "string", "pattern": "^ticket-[0-9]{3}$" }, + "summary": { "type": "string", "minLength": 1 }, + "workstream": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "allowedPaths": { "type": "array", "items": { "$ref": "#/$defs/glob" }, "minItems": 1, "uniqueItems": true }, + "forbiddenPaths": { "type": "array", "items": { "$ref": "#/$defs/glob" }, "uniqueItems": true }, + "stacks": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }, + "dependsOn": { "type": "array", "items": { "type": "string", "pattern": "^ticket-[0-9]{3}$" }, "uniqueItems": true }, + "conflictsWith": { "type": "array", "items": { "type": "string", "pattern": "^ticket-[0-9]{3}$" }, "uniqueItems": true }, + "integrationTicket": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "pattern": "^ticket-[0-9]{3}$" } + ] + }, + "classification": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "priority", "origin"], + "properties": { + "kind": { "enum": ["BUG", "FEATURE", "SERVICE"] }, + "priority": { "enum": ["P0", "P1", "P2", "P3"] }, + "origin": { "enum": ["regression", "requested", "health"] } + } + }, + "delivery": { + "type": "object", + "additionalProperties": false, + "required": [ + "acceptedBaseSha", + "targetBranch", + "outcome", + "nonGoals", + "complexity", + "estimatedMinutes", + "budgets", + "architecture", + "runtimeDependencies", + "validation" + ], + "properties": { + "acceptedBaseSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "targetBranch": { "$ref": "#/$defs/branch" }, + "outcome": { "type": "string", "minLength": 1 }, + "nonGoals": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1, + "uniqueItems": true + }, + "complexity": { "enum": ["XS", "S"] }, + "estimatedMinutes": { "type": "integer", "minimum": 1, "maximum": 30 }, + "standardAdoption": { + "type": "object", + "additionalProperties": false, + "required": ["sourceRepository", "fromRevision", "toRevision"], + "properties": { + "sourceRepository": { "const": "wellmanifest/new-project" }, + "fromRevision": { + "oneOf": [ + { "$ref": "#/$defs/sha" }, + { "type": "null" } + ] + }, + "toRevision": { "$ref": "#/$defs/sha" } + } + }, + "budgets": { + "type": "object", + "additionalProperties": false, + "required": [ + "maxImplementationFiles", + "maxAffectedComponents", + "maxPublicInterfaceChanges", + "maxRuntimeDependencies" + ], + "properties": { + "maxImplementationFiles": { "type": "integer", "minimum": 1 }, + "maxAffectedComponents": { "type": "integer", "minimum": 1 }, + "maxPublicInterfaceChanges": { "type": "integer", "minimum": 0 }, + "maxRuntimeDependencies": { "type": "integer", "minimum": 0 } + } + }, + "architecture": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "decision", + "components", + "responsibilityChanges", + "interfaceChanges", + "dataChanges", + "ui", + "rollback" + ], + "properties": { + "status": { "const": "accepted" }, + "decision": { "type": "string", "minLength": 1 }, + "components": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "paths"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "paths": { + "type": "array", + "items": { "$ref": "#/$defs/glob" }, + "minItems": 1, + "uniqueItems": true + } + } + } + }, + "responsibilityChanges": { "type": "boolean" }, + "interfaceChanges": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "dataChanges": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "ui": { + "type": "object", + "additionalProperties": false, + "required": ["impact", "states", "evidence"], + "properties": { + "impact": { "enum": ["none", "single-state", "multi-state"] }, + "states": { + "type": "array", + "items": { "enum": ["loading", "empty", "error", "success"] }, + "uniqueItems": true + }, + "evidence": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + } + }, + "rollback": { "type": "string", "minLength": 1 } + } + }, + "runtimeDependencies": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "validation": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["criterion", "commands", "evidence"], + "properties": { + "criterion": { "type": "string", "pattern": "^AC-[0-9]+$" }, + "commands": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1, + "uniqueItems": true + }, + "evidence": { "type": "string", "minLength": 1 } + } + } + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { "schema": { "const": "new-project.intent/v3" } }, + "required": ["schema"] + }, + "then": { "required": ["classification"] } + }, + { + "if": { + "properties": { "schema": { "const": "new-project.intent/v2" } }, + "required": ["schema"] + }, + "then": { "not": { "required": ["classification"] } } + } + ], + "$defs": { + "glob": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$" }, + "branch": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:\\.\\.|//|@\\{|[~^:?*\\[\\\\])).+$" }, + "sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" } + } +} diff --git a/.governance/lock.schema.json b/.governance/lock.schema.json new file mode 100644 index 0000000..4f41584 --- /dev/null +++ b/.governance/lock.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/lock.schema.json", + "title": "new-project governance lock", + "type": "object", + "additionalProperties": false, + "required": ["schema", "standard", "managedFiles"], + "properties": { + "schema": { "const": "new-project.lock/v1" }, + "standard": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version", "sourceRepository", "sourceRevision", "publicationStatus"], + "properties": { + "id": { "const": "wellmanifest/new-project" }, + "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "sourceRepository": { "const": "wellmanifest/new-project" }, + "sourceRevision": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "publicationStatus": { + "enum": ["published", "unpublished-test"] + } + } + }, + "managedFiles": { + "type": "object", + "minProperties": 1, + "propertyNames": { "pattern": "^(?!/)(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$" }, + "additionalProperties": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + } + } +} diff --git a/.governance/manifest.base.json b/.governance/manifest.base.json new file mode 100644 index 0000000..7ac5882 --- /dev/null +++ b/.governance/manifest.base.json @@ -0,0 +1,115 @@ +{ + "approvalEvidence": { + "requiredBindings": [ + "repository", + "pullRequest", + "headSha", + "ticket", + "actor" + ], + "reviewVerificationMethod": "github-api-allowlist", + "schema": "new-project.approval-evidence/v1", + "signedAttestationPredicateType": "https://wellmanifest.dev/attestations/validator/v1" + }, + "coordination": { + "integration": { + "workstream": "integration" + }, + "maxActiveTicketsPerWorkstream": 1, + "mode": "workstreams", + "rejectActiveScopeOverlap": true + }, + "delivery": { + "allowedComplexityClasses": [ + "XS", + "S" + ], + "checkpointMinutes": 25, + "dependencyManifestPaths": [ + "package.json", + "pyproject.toml", + "go.mod", + "Cargo.toml", + "pom.xml" + ], + "maxActiveMinutes": 30, + "maxAffectedComponents": 2, + "maxImplementationFiles": 5, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0, + "targetBranches": [ + "main" + ] + }, + "docker": { + "composeFiles": [ + "compose.yml", + "compose.yaml", + "docker-compose.yml", + "docker-compose.yaml", + "compose.e2e.yml" + ], + "dockerfiles": [ + "Dockerfile", + "Dockerfile.e2e" + ] + }, + "governancePaths": [ + "TODO.md", + "project/TICKETS.md", + "project/ticket-*/**" + ], + "requiredFiles": [ + "README.md", + "VERSION", + "CHANGELOG.md", + "TODO.md", + "AGENTS.md", + "project/TICKETS.md", + "project/new-ticket.sh", + "project/readme.sh" + ], + "schema": "new-project.governance/v2", + "stacks": [], + "standard": { + "id": "wellmanifest/new-project", + "version": "0.16.2" + }, + "ticket": { + "activeStatuses": [ + "IN_PROGRESS" + ], + "closedStatuses": [ + "DONE", + "CANCELLED" + ], + "directoryPattern": "^ticket-[0-9]{3}$", + "implementationStates": [ + "EDIT", + "VALIDATION", + "PUBLICATION" + ], + "intentFile": "intent.json", + "nonActiveStatuses": [ + "BACKLOG", + "PLAN", + "BLOCKED" + ], + "requiredAgentFiles": [ + "ai-*.md", + "ai-*-logs.txt" + ], + "requiredFiles": [ + "README.md", + "preprompt.md", + "changelog.md", + "intent.json" + ], + "root": "project" + }, + "trustedApprovalSources": [ + "github-review", + "github-app-review", + "signed-attestation" + ] +} diff --git a/.governance/manifest.json b/.governance/manifest.json new file mode 100644 index 0000000..36094ab --- /dev/null +++ b/.governance/manifest.json @@ -0,0 +1,184 @@ +{ + "$schema": "./manifest.schema.json", + "approvalEvidence": { + "requiredBindings": [ + "repository", + "pullRequest", + "headSha", + "ticket", + "actor" + ], + "reviewVerificationMethod": "github-api-allowlist", + "schema": "new-project.approval-evidence/v1", + "signedAttestationPredicateType": "https://wellmanifest.dev/attestations/validator/v1" + }, + "coordination": { + "integration": { + "requiredForPaths": [ + "package.json", + "pyproject.toml", + "go.mod", + "Cargo.toml", + "pom.xml" + ], + "workstream": "integration" + }, + "maxActiveTicketsPerWorkstream": 1, + "mode": "workstreams", + "rejectActiveScopeOverlap": true, + "workstreams": { + "application": { + "ownedPaths": [ + "src/**", + "app/**", + "lib/**", + "test/**", + "tests/**" + ] + }, + "governance": { + "ownedPaths": [ + ".github/workflows/new-project-governance.yml", + ".governance/**", + "AGENTS.md", + "README.md", + "TODO.md", + "CHANGELOG.md", + ".env.example", + "goal.yaml", + "project.sh", + "project.bat", + "project/**", + "scripts/runtime.sh" + ] + }, + "infrastructure": { + "ownedPaths": [ + "Dockerfile*", + "compose*.yml", + "compose*.yaml", + "infra/**", + ".github/**" + ] + }, + "integration": { + "ownedPaths": [ + "VERSION", + "package.json", + "pyproject.toml", + "go.mod", + "Cargo.toml", + "pom.xml", + "docs/**" + ] + }, + "interfaces": { + "ownedPaths": [ + "api/**", + "sdk/**", + "clients/**" + ] + } + } + }, + "delivery": { + "allowedComplexityClasses": [ + "XS", + "S" + ], + "checkpointMinutes": 25, + "dependencyManifestPaths": [ + "package.json", + "pyproject.toml", + "go.mod", + "Cargo.toml", + "pom.xml" + ], + "maxActiveMinutes": 30, + "maxAffectedComponents": 2, + "maxImplementationFiles": 5, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0, + "publicInterfacePaths": [ + "api/**", + "sdk/**", + "src/public/**" + ], + "requiredForImplementation": true, + "targetBranches": [ + "main" + ] + }, + "docker": { + "composeFiles": [ + "compose.yml", + "compose.yaml", + "docker-compose.yml", + "docker-compose.yaml", + "compose.e2e.yml" + ], + "dockerfiles": [ + "Dockerfile", + "Dockerfile.e2e" + ], + "required": false + }, + "governancePaths": [ + "TODO.md", + "project/TICKETS.md", + "project/ticket-*/**" + ], + "requiredFiles": [ + "README.md", + "VERSION", + "CHANGELOG.md", + "TODO.md", + "AGENTS.md", + "project/TICKETS.md", + "project/new-ticket.sh", + "project/readme.sh" + ], + "schema": "new-project.governance/v2", + "stacks": [], + "standard": { + "id": "wellmanifest/new-project", + "version": "0.16.2" + }, + "ticket": { + "activeStatuses": [ + "IN_PROGRESS" + ], + "closedStatuses": [ + "DONE", + "CANCELLED" + ], + "directoryPattern": "^ticket-[0-9]{3}$", + "implementationStates": [ + "EDIT", + "VALIDATION", + "PUBLICATION" + ], + "intentFile": "intent.json", + "nonActiveStatuses": [ + "BACKLOG", + "PLAN", + "BLOCKED" + ], + "requiredAgentFiles": [ + "ai-*.md", + "ai-*-logs.txt" + ], + "requiredFiles": [ + "README.md", + "preprompt.md", + "changelog.md", + "intent.json" + ], + "root": "project" + }, + "trustedApprovalSources": [ + "github-review", + "github-app-review", + "signed-attestation" + ] +} diff --git a/.governance/manifest.lock.json b/.governance/manifest.lock.json new file mode 100644 index 0000000..50f30df --- /dev/null +++ b/.governance/manifest.lock.json @@ -0,0 +1,46 @@ +{ + "managedFiles": { + ".github/workflows/new-project-governance.yml": "9dd307f48368fea9438bb607e28e48cffcfe0ae4f231d77df86c384745bb68e9", + ".governance/approval-evidence.schema.json": "488dee5a4bfbf221206acc45947fce5283eb5e80614ec0ef478b5d618cc4eb83", + ".governance/branch_lifecycle_check.py": "02e80224fb659feccf39ffa6afebd053d68f0f44137e842ea9cff6e7170b8c9d", + ".governance/change-evaluation.schema.json": "d16c9cbc9646ad7d98b7a5ecd56594e4572df968a0ee0c63e969b2a8c3b9490a", + ".governance/check_required_checks.py": "c763e14ee118af591b80f3186fa6d6f16c4f525b222935605458dc349928ba17", + ".governance/decision-record.schema.json": "08278322846b6da5e8306c0661c23fd1bcdf4a003f8ef75aac0d33299a4cd357", + ".governance/decision_record.py": "ffbdf4f8d1184e210b5dc824c7b889e56523ebd6351b323b14e7ff23abb063f5", + ".governance/diagnostics.json": "8db26b4b1f5c18eb6975ea15a7e36f8d2f577f0cf309ae32dd705bc6f69c3454", + ".governance/diagnostics.schema.json": "8704209cfe05575b3690f9d96b15b076a1dbb5ecef495e85e52bbc850d7ca0f6", + ".governance/error/GOV-REMEDIATION-INTENT.md": "5f5a36b50c6e2508acbe611b6f1ded3a611d8557bb60c1e13e84885ef1309665", + ".governance/error/GOV-TICKET-001.md": "5ddc3c05b8bc2193f0b1796cd5fb383f62151464ab9539aec854eca9bfa3d2a5", + ".governance/error/GOV-TICKET-ALLOCATION.md": "2da9115e3dcac3c5e84b04bf0fc2c6a73975526272f45405a4fe0c05fe51c772", + ".governance/error/GOV-WORKSPACE-LIFECYCLE.md": "72436684572d784327a340d90ca96d9fd2927519e02334036fab402e9c1d8262", + ".governance/error/README.md": "e8486dd29f52ca3fee96ed6881a62c38141864cde5aa1adea2b16d22b2feefaa", + ".governance/governance_check.py": "cc7c3a378a6d0f6f058875945237906e41fc15ff0b6152d0e16df83f90653dad", + ".governance/intent.schema.json": "ca0f818ff72e0a766425c83fbb45f326f1ddbc31118838f1c94159dabbb6655e", + ".governance/lock.schema.json": "ad80c98f800a4a3310870336dcdaf0aa689cc4988f71084d25d76bea2df1242f", + ".governance/manifest.base.json": "2abab4bcbdb1f68e9b19df0701ab5e796187fcb9882dfdf5f0044fa314802c84", + ".governance/manifest.schema.json": "d48f258e3397ac2d8c5010e5ccdeef9eaf87b0b015d56798d11d5916c5f70a18", + ".governance/package-manifest.json": "c7202c4642077569ba41f6e74502f725a4a5d44cf9a1dbcde7327da7dd9c6b83", + ".governance/remediation-intent.schema.json": "67ca198e9090fd571f631331df314cbdfd766aec88ee70416b22e20aa9a94299", + ".governance/remediation-intent.template.dsl.json": "a3eb01c54fe678f3fcebb88103ac4eb02f5dd24016b2ba9552814b5e442dfb34", + ".governance/remediation_intent.py": "b2f3d65f731ba747104e3fcb8fc19668956e262041fdf1be011f22eaa1ca57eb", + ".governance/required-checks.json": "579e008e2dba9110ce45d34fdd96768fa28c003a339cf338378d28fe00fb83a2", + ".governance/stack-profiles.json": "47a3b899553968dfc5e0565c0de525f13aadde5dacae4556614572a731054f0e", + ".governance/work-classification.dsl.json": "3a947c41938c0b8ef1717957f313ff9248764252735182de30b1f2d6878748b6", + ".governance/work-classification.schema.json": "f5c2b518238543589e4f8d3805cc6455e19d6919643644aaeae034abf472a467", + ".governance/workspace_lifecycle_check.py": "7a4a8696b31711e325c82e79f38fb06962a3e8535e1ab656bcbfd6d18e7e9bdb", + "AGENTS.md": "8fa1365cc7ded2b4b0c95c1043e3b5a762c4f6d0f8ffb9a6fa0f9f905ce380e8", + "project/governance-check.bat": "2f0091eba580c0a24b5d1a127fb82963521a4b8512ff9d3b85e679b66b5e361c", + "project/governance-check.sh": "158ca61531b8e51ba484de8eb6f91f4e4fbba908ae3c8db678b63bf6bab49923", + "project/new-ticket.sh": "e44778eaa8c66b40d75ddf44e4845f47156f92d0a6522e9b81470baec0b19c8a", + "project/readme.sh": "b41a9c88374e6de0439284a4561fb11b1b482039bc5ba1bcf6683fd59b1a3968", + "scripts/runtime.sh": "8d5c91808d3c126fc84018a12f0b39dd49194fb8b01c5e024932ff9cf2a7a7e2" + }, + "schema": "new-project.lock/v1", + "standard": { + "id": "wellmanifest/new-project", + "publicationStatus": "published", + "sourceRepository": "wellmanifest/new-project", + "sourceRevision": "63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac", + "version": "0.16.2" + } +} diff --git a/.governance/manifest.schema.json b/.governance/manifest.schema.json new file mode 100644 index 0000000..efb92fb --- /dev/null +++ b/.governance/manifest.schema.json @@ -0,0 +1,171 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/manifest.schema.json", + "title": "new-project governance manifest", + "type": "object", + "additionalProperties": false, + "required": ["schema", "standard", "requiredFiles", "ticket", "docker", "governancePaths", "trustedApprovalSources", "coordination"], + "properties": { + "$schema": { "type": "string", "minLength": 1 }, + "schema": { "const": "new-project.governance/v2" }, + "standard": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version"], + "properties": { + "id": { "const": "wellmanifest/new-project" }, + "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" } + } + }, + "requiredFiles": { "type": "array", "items": { "$ref": "#/$defs/path" }, "uniqueItems": true }, + "governancePaths": { "type": "array", "items": { "$ref": "#/$defs/glob" }, "uniqueItems": true }, + "trustedApprovalSources": { + "type": "array", + "items": { "enum": ["github-review", "github-app-review", "signed-attestation"] }, + "minItems": 1, + "uniqueItems": true + }, + "approvalEvidence": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "requiredBindings", "reviewVerificationMethod", "signedAttestationPredicateType"], + "properties": { + "schema": { "const": "new-project.approval-evidence/v1" }, + "requiredBindings": { + "type": "array", + "prefixItems": [ + { "const": "repository" }, + { "const": "pullRequest" }, + { "const": "headSha" }, + { "const": "ticket" }, + { "const": "actor" } + ], + "items": false, + "minItems": 5, + "maxItems": 5 + }, + "reviewVerificationMethod": { "const": "github-api-allowlist" }, + "signedAttestationPredicateType": { + "const": "https://wellmanifest.dev/attestations/validator/v1" + } + } + }, + "ticket": { + "type": "object", + "additionalProperties": false, + "required": ["root", "directoryPattern", "requiredFiles", "requiredAgentFiles", "activeStatuses", "nonActiveStatuses", "closedStatuses", "implementationStates", "intentFile"], + "properties": { + "root": { "$ref": "#/$defs/path" }, + "directoryPattern": { "type": "string", "minLength": 1 }, + "requiredFiles": { "type": "array", "items": { "$ref": "#/$defs/path" }, "uniqueItems": true }, + "requiredAgentFiles": { "type": "array", "items": { "$ref": "#/$defs/glob" }, "uniqueItems": true }, + "activeStatuses": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, + "nonActiveStatuses": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, + "closedStatuses": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, + "implementationStates": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, + "intentFile": { "$ref": "#/$defs/path" } + } + }, + "docker": { + "type": "object", + "additionalProperties": false, + "required": ["required", "dockerfiles", "composeFiles"], + "properties": { + "required": { "type": "boolean" }, + "dockerfiles": { "type": "array", "items": { "$ref": "#/$defs/path" }, "minItems": 1 }, + "composeFiles": { "type": "array", "items": { "$ref": "#/$defs/path" }, "minItems": 1 } + } + }, + "coordination": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "maxActiveTicketsPerWorkstream", "rejectActiveScopeOverlap", "workstreams", "integration"], + "properties": { + "mode": { "const": "workstreams" }, + "maxActiveTicketsPerWorkstream": { "type": "integer", "minimum": 1 }, + "rejectActiveScopeOverlap": { "type": "boolean" }, + "workstreams": { + "type": "object", + "minProperties": 1, + "propertyNames": { "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": ["ownedPaths"], + "properties": { + "ownedPaths": { "type": "array", "items": { "$ref": "#/$defs/glob" }, "minItems": 1, "uniqueItems": true } + } + } + }, + "integration": { + "type": "object", + "additionalProperties": false, + "required": ["workstream", "requiredForPaths"], + "properties": { + "workstream": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "requiredForPaths": { "type": "array", "items": { "$ref": "#/$defs/glob" }, "uniqueItems": true } + } + } + } + }, + "delivery": { + "type": "object", + "additionalProperties": false, + "required": [ + "requiredForImplementation", + "maxActiveMinutes", + "checkpointMinutes", + "allowedComplexityClasses", + "maxImplementationFiles", + "maxAffectedComponents", + "maxPublicInterfaceChanges", + "maxRuntimeDependencies", + "targetBranches", + "publicInterfacePaths", + "dependencyManifestPaths" + ], + "properties": { + "requiredForImplementation": { "type": "boolean" }, + "maxActiveMinutes": { "type": "integer", "minimum": 1, "maximum": 30 }, + "checkpointMinutes": { "type": "integer", "minimum": 1, "maximum": 29 }, + "allowedComplexityClasses": { + "type": "array", + "items": { "enum": ["XS", "S"] }, + "minItems": 1, + "uniqueItems": true + }, + "maxImplementationFiles": { "type": "integer", "minimum": 1 }, + "maxAffectedComponents": { "type": "integer", "minimum": 1 }, + "maxPublicInterfaceChanges": { "type": "integer", "minimum": 0 }, + "maxRuntimeDependencies": { "type": "integer", "minimum": 0 }, + "targetBranches": { + "type": "array", + "items": { "$ref": "#/$defs/branch" }, + "minItems": 1, + "uniqueItems": true + }, + "publicInterfacePaths": { + "type": "array", + "items": { "$ref": "#/$defs/glob" }, + "uniqueItems": true + }, + "dependencyManifestPaths": { + "type": "array", + "items": { "$ref": "#/$defs/path" }, + "uniqueItems": true + } + } + }, + "stacks": { + "type": "array", + "items": { "enum": ["node", "python", "go", "rust", "java", "docker", "frontend", "terraform", "kubernetes"] }, + "uniqueItems": true, + "default": [] + } + }, + "$defs": { + "path": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$" }, + "glob": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$" }, + "branch": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:\\.\\.|//|@\\{|[~^:?*\\[\\\\])).+$" } + } +} diff --git a/.governance/package-manifest.json b/.governance/package-manifest.json new file mode 100644 index 0000000..055ed82 --- /dev/null +++ b/.governance/package-manifest.json @@ -0,0 +1,42 @@ +{ + "schema": "new-project.package-manifest/v1", + "files": [ + { "source": "template/files/AGENTS.template.md", "target": "AGENTS.md", "strategy": "managed", "executable": false }, + { "source": "project.sh", "target": "project.sh", "strategy": "seed", "executable": true }, + { "source": "project.bat", "target": "project.bat", "strategy": "seed", "executable": false }, + { "source": "governance/approval-evidence.schema.json", "target": ".governance/approval-evidence.schema.json", "strategy": "managed", "executable": false }, + { "source": "governance/change-evaluation.schema.json", "target": ".governance/change-evaluation.schema.json", "strategy": "managed", "executable": false }, + { "source": "governance/diagnostics.json", "target": ".governance/diagnostics.json", "strategy": "managed", "executable": false }, + { "source": "governance/diagnostics.schema.json", "target": ".governance/diagnostics.schema.json", "strategy": "managed", "executable": false }, + { "source": "governance/remediation-intent.schema.json", "target": ".governance/remediation-intent.schema.json", "strategy": "managed", "executable": false }, + { "source": "template/files/remediation-intent.template.dsl.json", "target": ".governance/remediation-intent.template.dsl.json", "strategy": "managed", "executable": false }, + { "source": "error/README.md", "target": ".governance/error/README.md", "strategy": "managed", "executable": false }, + { "source": "error/GOV-REMEDIATION-INTENT.md", "target": ".governance/error/GOV-REMEDIATION-INTENT.md", "strategy": "managed", "executable": false }, + { "source": "error/GOV-TICKET-001.md", "target": ".governance/error/GOV-TICKET-001.md", "strategy": "managed", "executable": false }, + { "source": "error/GOV-TICKET-ALLOCATION.md", "target": ".governance/error/GOV-TICKET-ALLOCATION.md", "strategy": "managed", "executable": false }, + { "source": "error/GOV-WORKSPACE-LIFECYCLE.md", "target": ".governance/error/GOV-WORKSPACE-LIFECYCLE.md", "strategy": "managed", "executable": false }, + { "source": "governance/required-checks.json", "target": ".governance/required-checks.json", "strategy": "managed", "executable": false }, + { "source": "governance/decision-record.schema.json", "target": ".governance/decision-record.schema.json", "strategy": "managed", "executable": false }, + { "source": "scripts/check_required_checks.py", "target": ".governance/check_required_checks.py", "strategy": "managed", "executable": true }, + { "source": "scripts/decision_record.py", "target": ".governance/decision_record.py", "strategy": "managed", "executable": true }, + { "source": "scripts/remediation_intent.py", "target": ".governance/remediation_intent.py", "strategy": "managed", "executable": true }, + { "source": "scripts/branch_lifecycle_check.py", "target": ".governance/branch_lifecycle_check.py", "strategy": "managed", "executable": true }, + { "source": "scripts/workspace_lifecycle_check.py", "target": ".governance/workspace_lifecycle_check.py", "strategy": "managed", "executable": true }, + { "source": "governance/intent.schema.json", "target": ".governance/intent.schema.json", "strategy": "managed", "executable": false }, + { "source": "governance/lock.schema.json", "target": ".governance/lock.schema.json", "strategy": "managed", "executable": false }, + { "source": "governance/manifest.schema.json", "target": ".governance/manifest.schema.json", "strategy": "managed", "executable": false }, + { "source": "governance/package-manifest.json", "target": ".governance/package-manifest.json", "strategy": "managed", "executable": false }, + { "source": "governance/stack-profiles.json", "target": ".governance/stack-profiles.json", "strategy": "managed", "executable": false }, + { "source": "governance/work-classification.dsl.json", "target": ".governance/work-classification.dsl.json", "strategy": "managed", "executable": false }, + { "source": "governance/work-classification.schema.json", "target": ".governance/work-classification.schema.json", "strategy": "managed", "executable": false }, + { "source": "scripts/governance_check.py", "target": ".governance/governance_check.py", "strategy": "managed", "executable": true }, + { "source": "template/files/new-project-governance.workflow.yml", "target": ".github/workflows/new-project-governance.yml", "strategy": "managed", "executable": false }, + { "source": "scripts/runtime.sh", "target": "scripts/runtime.sh", "strategy": "managed", "executable": true }, + { "source": "project/governance-check.sh", "target": "project/governance-check.sh", "strategy": "managed", "executable": true }, + { "source": "project/governance-check.bat", "target": "project/governance-check.bat", "strategy": "managed", "executable": false }, + { "source": "project/new-ticket.sh", "target": "project/new-ticket.sh", "strategy": "managed", "executable": true }, + { "source": "project/readme.sh", "target": "project/readme.sh", "strategy": "managed", "executable": true }, + { "source": "governance/manifest.default.json", "target": ".governance/manifest.base.json", "strategy": "managed", "executable": false }, + { "source": "governance/manifest.default.json", "target": ".governance/manifest.json", "strategy": "extendable", "executable": false } + ] +} diff --git a/.governance/remediation-intent.schema.json b/.governance/remediation-intent.schema.json new file mode 100644 index 0000000..880075a --- /dev/null +++ b/.governance/remediation-intent.schema.json @@ -0,0 +1,451 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://wellmanifest.dev/schemas/new-project-remediation-intent-v1.json", + "title": "Target-owned diagnostic remediation intent DSL", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "intentId", + "ticket", + "repository", + "ownerRoute", + "status", + "source", + "objective", + "scope", + "findings", + "actions", + "verifications", + "acceptanceCriteria", + "llmGuidance", + "todo2code" + ], + "properties": { + "schema": {"const": "new-project.remediation-intent/v1"}, + "intentId": {"type": "string", "pattern": "^RI-[A-Z0-9][A-Z0-9-]*$"}, + "ticket": {"type": "string", "pattern": "^ticket-[0-9]{3}$"}, + "repository": {"$ref": "#/$defs/repository"}, + "ownerRoute": {"$ref": "#/$defs/nonempty"}, + "status": {"enum": ["DRAFT", "READY", "ANALYZED"]}, + "source": {"$ref": "#/$defs/source"}, + "objective": {"$ref": "#/$defs/objective"}, + "scope": {"$ref": "#/$defs/scope"}, + "findings": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/finding"} + }, + "actions": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/action"} + }, + "verifications": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/verification"} + }, + "acceptanceCriteria": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/acceptanceCriterion"} + }, + "llmGuidance": {"$ref": "#/$defs/llmGuidance"}, + "todo2code": {"$ref": "#/$defs/todo2code"}, + "advisoryAnalysis": {"$ref": "#/$defs/advisoryAnalysis"} + }, + "allOf": [ + { + "if": { + "properties": {"status": {"const": "ANALYZED"}}, + "required": ["status"] + }, + "then": {"required": ["advisoryAnalysis"]} + } + ], + "$defs": { + "nonempty": {"type": "string", "minLength": 1}, + "digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "repository": { + "type": "string", + "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" + }, + "path": { + "type": "string", + "minLength": 1, + "pattern": "^(?!unresolved:)(?!/)(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$" + }, + "pathOrUnresolved": { + "oneOf": [ + {"$ref": "#/$defs/path"}, + {"const": "unresolved:agent"} + ] + }, + "stringList": { + "type": "array", + "items": {"$ref": "#/$defs/nonempty"}, + "uniqueItems": true + }, + "nonemptyStringList": { + "type": "array", + "items": {"$ref": "#/$defs/nonempty"}, + "minItems": 1, + "uniqueItems": true + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["producer", "observedAt", "reportDigest"], + "properties": { + "producer": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": {"$ref": "#/$defs/nonempty"}, + "version": {"$ref": "#/$defs/nonempty"} + } + }, + "observedAt": {"type": "string", "format": "date-time"}, + "reportDigest": { + "oneOf": [ + {"$ref": "#/$defs/digest"}, + {"const": "unresolved:agent"} + ] + } + } + }, + "objective": { + "type": "object", + "additionalProperties": false, + "required": ["outcome", "nonGoals", "constraints"], + "properties": { + "outcome": {"$ref": "#/$defs/nonempty"}, + "nonGoals": {"$ref": "#/$defs/nonemptyStringList"}, + "constraints": {"$ref": "#/$defs/nonemptyStringList"} + } + }, + "scope": { + "type": "object", + "additionalProperties": false, + "required": ["allowedPaths", "forbiddenPaths", "preservePaths"], + "properties": { + "allowedPaths": { + "type": "array", + "items": {"$ref": "#/$defs/pathOrUnresolved"}, + "minItems": 1, + "uniqueItems": true + }, + "forbiddenPaths": { + "type": "array", + "items": {"$ref": "#/$defs/path"}, + "uniqueItems": true + }, + "preservePaths": { + "type": "array", + "items": {"$ref": "#/$defs/path"}, + "uniqueItems": true + } + } + }, + "diagnosticExpectation": { + "type": "object", + "additionalProperties": false, + "required": ["code", "current", "required"], + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9]*(?:[_-][A-Z0-9]+)*$" + }, + "current": {"enum": ["EMITTED", "MISSING", "FALSE_POSITIVE", "DRIFT"]}, + "required": {"enum": ["EMIT", "SUPPRESS", "REFINE", "PRESERVE"]} + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["ref", "observation"], + "properties": { + "ref": {"$ref": "#/$defs/nonempty"}, + "observation": {"$ref": "#/$defs/nonempty"} + } + }, + "applicability": { + "type": "object", + "additionalProperties": false, + "required": ["requiredSignals", "excludedSignals", "unknownOutcome"], + "properties": { + "requiredSignals": {"$ref": "#/$defs/nonemptyStringList"}, + "excludedSignals": {"$ref": "#/$defs/stringList"}, + "unknownOutcome": {"enum": ["BLOCK", "REPORT"]} + } + }, + "finding": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "category", + "status", + "priority", + "summary", + "diagnostic", + "evidence", + "applicability", + "desiredOutcome", + "affectedPaths", + "dependsOn", + "acceptanceCriteria" + ], + "properties": { + "id": {"type": "string", "pattern": "^F-[A-Z0-9][A-Z0-9-]*$"}, + "category": { + "enum": [ + "FALSE_POSITIVE", + "SILENT_OMISSION", + "AMBIGUOUS_HEURISTIC", + "CONTRACT_DRIFT", + "MISSING_INVENTORY", + "STATE_RISK", + "OTHER" + ] + }, + "status": {"enum": ["CONFIRMED", "PROPOSED", "DEFERRED"]}, + "priority": {"enum": ["P0", "P1", "P2", "P3"]}, + "summary": {"$ref": "#/$defs/nonempty"}, + "diagnostic": {"$ref": "#/$defs/diagnosticExpectation"}, + "evidence": { + "type": "array", + "items": {"$ref": "#/$defs/evidence"}, + "minItems": 1 + }, + "applicability": {"$ref": "#/$defs/applicability"}, + "desiredOutcome": {"$ref": "#/$defs/nonempty"}, + "affectedPaths": { + "type": "array", + "items": {"$ref": "#/$defs/pathOrUnresolved"}, + "minItems": 1, + "uniqueItems": true + }, + "dependsOn": { + "type": "array", + "items": {"type": "string", "pattern": "^F-[A-Z0-9][A-Z0-9-]*$"}, + "uniqueItems": true + }, + "acceptanceCriteria": { + "type": "array", + "items": {"type": "string", "pattern": "^AC-[0-9]+$"}, + "minItems": 1, + "uniqueItems": true + } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "required": ["level", "authorization", "automation", "preservesUserData"], + "properties": { + "level": {"enum": ["READ_ONLY", "REVERSIBLE_WRITE", "DESTRUCTIVE"]}, + "authorization": { + "enum": ["NOT_APPLICABLE", "SESSION_EXECUTION_AUTHORIZATION", "EXPLICIT_HUMAN"] + }, + "automation": {"enum": ["ALLOWED", "PROHIBITED"]}, + "preservesUserData": {"type": "boolean"} + } + }, + "action": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "findingIds", + "operation", + "description", + "paths", + "dependsOn", + "verificationIds", + "risk" + ], + "properties": { + "id": {"type": "string", "pattern": "^A-[A-Z0-9][A-Z0-9-]*$"}, + "findingIds": { + "type": "array", + "items": {"type": "string", "pattern": "^F-[A-Z0-9][A-Z0-9-]*$"}, + "minItems": 1, + "uniqueItems": true + }, + "operation": { + "enum": ["CLASSIFY", "IMPLEMENT", "TEST", "DOCUMENT", "RELEASE", "TRIAGE", "PRESERVE"] + }, + "description": {"$ref": "#/$defs/nonempty"}, + "paths": { + "type": "array", + "items": {"$ref": "#/$defs/pathOrUnresolved"}, + "minItems": 1, + "uniqueItems": true + }, + "dependsOn": { + "type": "array", + "items": {"type": "string", "pattern": "^A-[A-Z0-9][A-Z0-9-]*$"}, + "uniqueItems": true + }, + "verificationIds": { + "type": "array", + "items": {"type": "string", "pattern": "^V-[A-Z0-9][A-Z0-9-]*$"}, + "minItems": 1, + "uniqueItems": true + }, + "risk": {"$ref": "#/$defs/risk"} + } + }, + "verification": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "command", "expected", "deterministic", "covers"], + "properties": { + "id": {"type": "string", "pattern": "^V-[A-Z0-9][A-Z0-9-]*$"}, + "type": {"enum": ["COMMAND", "ASSERTION", "EXTERNAL_EVIDENCE"]}, + "command": { + "oneOf": [ + {"$ref": "#/$defs/nonempty"}, + {"type": "null"} + ] + }, + "expected": {"$ref": "#/$defs/nonempty"}, + "deterministic": {"type": "boolean"}, + "covers": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:F|A)-[A-Z0-9][A-Z0-9-]*$" + }, + "minItems": 1, + "uniqueItems": true + } + } + }, + "acceptanceCriterion": { + "type": "object", + "additionalProperties": false, + "required": ["id", "statement", "findingIds", "verificationIds"], + "properties": { + "id": {"type": "string", "pattern": "^AC-[0-9]+$"}, + "statement": {"$ref": "#/$defs/nonempty"}, + "findingIds": { + "type": "array", + "items": {"type": "string", "pattern": "^F-[A-Z0-9][A-Z0-9-]*$"}, + "minItems": 1, + "uniqueItems": true + }, + "verificationIds": { + "type": "array", + "items": {"type": "string", "pattern": "^V-[A-Z0-9][A-Z0-9-]*$"}, + "minItems": 1, + "uniqueItems": true + } + } + }, + "llmGuidance": { + "type": "object", + "additionalProperties": false, + "required": ["role", "mustPreserve", "forbiddenAssumptions", "planningOrder", "openQuestions"], + "properties": { + "role": {"$ref": "#/$defs/nonempty"}, + "mustPreserve": {"$ref": "#/$defs/nonemptyStringList"}, + "forbiddenAssumptions": {"$ref": "#/$defs/nonemptyStringList"}, + "planningOrder": { + "type": "array", + "items": {"type": "string", "pattern": "^A-[A-Z0-9][A-Z0-9-]*$"}, + "minItems": 1, + "uniqueItems": true + }, + "openQuestions": {"$ref": "#/$defs/stringList"} + } + }, + "todo2code": { + "type": "object", + "additionalProperties": false, + "required": ["enabled", "taskPath", "todoPath", "planSchema", "requiredDiagnosticCodes"], + "properties": { + "enabled": {"type": "boolean"}, + "taskPath": {"$ref": "#/$defs/path"}, + "todoPath": {"$ref": "#/$defs/path"}, + "planSchema": {"const": "t2c.code-change-plan/v1"}, + "requiredDiagnosticCodes": { + "type": "array", + "items": { + "enum": [ + "AMBIGUOUS_REQUIREMENT", + "HUMAN_AGENT_CONFLICT", + "HUMAN_COMMUNICATION_CONFLICT", + "PLANNED_NOT_IMPLEMENTED" + ] + }, + "minItems": 4, + "uniqueItems": true + } + } + }, + "analysisFinding": { + "type": "object", + "additionalProperties": false, + "required": ["code", "severity", "message", "references", "llmHint"], + "properties": { + "code": { + "enum": [ + "T2C_AMBIGUOUS_INTENT", + "T2C_CONFLICT", + "T2C_CRITERION_GAP", + "T2C_PLAN_GAP", + "T2C_PRIORITY_DRIFT", + "T2C_SCOPE_EXPANSION", + "T2C_UNAUTHORIZED_DELETION" + ] + }, + "severity": {"enum": ["BLOCKING", "REVIEW", "INFO"]}, + "message": {"$ref": "#/$defs/nonempty"}, + "references": {"$ref": "#/$defs/nonemptyStringList"}, + "llmHint": {"$ref": "#/$defs/nonempty"} + } + }, + "advisoryAnalysis": { + "type": "object", + "additionalProperties": false, + "required": [ + "authority", + "producer", + "analyzedAt", + "intentDigest", + "diagnosticsDigest", + "plansDigest", + "planIds", + "findings", + "llmHints" + ], + "properties": { + "authority": {"const": "ADVISORY"}, + "producer": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version", "mode"], + "properties": { + "name": {"const": "todo2code"}, + "version": {"$ref": "#/$defs/nonempty"}, + "mode": {"const": "deterministic"} + } + }, + "analyzedAt": {"type": "string", "format": "date-time"}, + "intentDigest": {"$ref": "#/$defs/digest"}, + "diagnosticsDigest": {"$ref": "#/$defs/digest"}, + "plansDigest": {"$ref": "#/$defs/digest"}, + "planIds": {"$ref": "#/$defs/stringList"}, + "findings": { + "type": "array", + "items": {"$ref": "#/$defs/analysisFinding"} + }, + "llmHints": {"$ref": "#/$defs/stringList"} + } + } + } +} diff --git a/.governance/remediation-intent.template.dsl.json b/.governance/remediation-intent.template.dsl.json new file mode 100644 index 0000000..ad06b4d --- /dev/null +++ b/.governance/remediation-intent.template.dsl.json @@ -0,0 +1,144 @@ +{ + "schema": "new-project.remediation-intent/v1", + "intentId": "RI-UNRESOLVED", + "ticket": "ticket-000", + "repository": "owner/repository", + "ownerRoute": "unresolved:human", + "status": "DRAFT", + "source": { + "producer": { + "name": "diagnostic-tool", + "version": "unresolved:agent" + }, + "observedAt": "1970-01-01T00:00:00Z", + "reportDigest": "unresolved:agent" + }, + "objective": { + "outcome": "Replace this draft outcome with one observable desired state.", + "nonGoals": [ + "Do not expand scope beyond the owning ticket." + ], + "constraints": [ + "Preserve user-owned and unclassified data." + ] + }, + "scope": { + "allowedPaths": [ + "unresolved:agent" + ], + "forbiddenPaths": [ + ".env" + ], + "preservePaths": [] + }, + "findings": [ + { + "id": "F-UNRESOLVED", + "category": "OTHER", + "status": "PROPOSED", + "priority": "P2", + "summary": "Replace this draft with an evidence-backed finding.", + "diagnostic": { + "code": "UNRESOLVED_FINDING", + "current": "MISSING", + "required": "EMIT" + }, + "evidence": [ + { + "ref": "unresolved:agent", + "observation": "Capture the exact report path, code and observed state." + } + ], + "applicability": { + "requiredSignals": [ + "Define the signal that proves this finding applies." + ], + "excludedSignals": [], + "unknownOutcome": "BLOCK" + }, + "desiredOutcome": "Define a deterministic postcondition.", + "affectedPaths": [ + "unresolved:agent" + ], + "dependsOn": [], + "acceptanceCriteria": [ + "AC-01" + ] + } + ], + "actions": [ + { + "id": "A-UNRESOLVED", + "findingIds": [ + "F-UNRESOLVED" + ], + "operation": "CLASSIFY", + "description": "Resolve the affected path and implementation action.", + "paths": [ + "unresolved:agent" + ], + "dependsOn": [], + "verificationIds": [ + "V-UNRESOLVED" + ], + "risk": { + "level": "READ_ONLY", + "authorization": "NOT_APPLICABLE", + "automation": "ALLOWED", + "preservesUserData": true + } + } + ], + "verifications": [ + { + "id": "V-UNRESOLVED", + "type": "ASSERTION", + "command": null, + "expected": "Replace with a deterministic verification.", + "deterministic": true, + "covers": [ + "F-UNRESOLVED", + "A-UNRESOLVED" + ] + } + ], + "acceptanceCriteria": [ + { + "id": "AC-01", + "statement": "The finding has a bounded implementation path and deterministic verification.", + "findingIds": [ + "F-UNRESOLVED" + ], + "verificationIds": [ + "V-UNRESOLVED" + ] + } + ], + "llmGuidance": { + "role": "Plan a bounded refactoring; do not implement or approve it.", + "mustPreserve": [ + "Accepted ticket scope and user-owned data." + ], + "forbiddenAssumptions": [ + "Do not infer missing paths, ownership or authorization." + ], + "planningOrder": [ + "A-UNRESOLVED" + ], + "openQuestions": [ + "Which exact source and test paths implement this diagnostic?" + ] + }, + "todo2code": { + "enabled": true, + "taskPath": "project/ticket-000/REMEDIATION.task.md", + "todoPath": "project/ticket-000/REMEDIATION.todo.md", + "planSchema": "t2c.code-change-plan/v1", + "requiredDiagnosticCodes": [ + "AMBIGUOUS_REQUIREMENT", + "HUMAN_AGENT_CONFLICT", + "HUMAN_COMMUNICATION_CONFLICT", + "PLANNED_NOT_IMPLEMENTED" + ] + } +} diff --git a/.governance/remediation_intent.py b/.governance/remediation_intent.py new file mode 100755 index 0000000..7efc90a --- /dev/null +++ b/.governance/remediation_intent.py @@ -0,0 +1,1589 @@ +#!/usr/bin/env python3 +"""Validate and project target-owned diagnostic remediation intent DSL files.""" + +from __future__ import annotations + +import argparse +from copy import deepcopy +from datetime import datetime, timezone +from fnmatch import fnmatchcase +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +import sys +from typing import Any + + +INTENT_SCHEMA = "new-project.remediation-intent/v1" +VALIDATION_SCHEMA = "new-project.remediation-validation/v1" +T2C_DIAGNOSTICS_SCHEMA = "t2c.diagnostics/v1" +T2C_PLAN_SET_SCHEMA = "t2c.code-change-plan-set/v1" +T2C_PLAN_SCHEMA = "t2c.code-change-plan/v1" +MALFORMED_CODE = "GOV-REMEDIATION-001" +T2C_CODE = "GOV-REMEDIATION-002" +STALE_CODE = "GOV-REMEDIATION-003" + +INTENT_ID = re.compile(r"RI-[A-Z0-9][A-Z0-9-]*") +TICKET_ID = re.compile(r"ticket-[0-9]{3}") +REPOSITORY = re.compile(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+") +FINDING_ID = re.compile(r"F-[A-Z0-9][A-Z0-9-]*") +ACTION_ID = re.compile(r"A-[A-Z0-9][A-Z0-9-]*") +VERIFICATION_ID = re.compile(r"V-[A-Z0-9][A-Z0-9-]*") +CRITERION_ID = re.compile(r"AC-[0-9]+") +DIAGNOSTIC_CODE = re.compile(r"[A-Z][A-Z0-9]*(?:[_-][A-Z0-9]+)*") +DIGEST = re.compile(r"[0-9a-f]{64}") + +FINDING_CATEGORIES = { + "FALSE_POSITIVE", + "SILENT_OMISSION", + "AMBIGUOUS_HEURISTIC", + "CONTRACT_DRIFT", + "MISSING_INVENTORY", + "STATE_RISK", + "OTHER", +} +FINDING_STATUSES = {"CONFIRMED", "PROPOSED", "DEFERRED"} +PRIORITIES = {"P0", "P1", "P2", "P3"} +DIAGNOSTIC_STATES = {"EMITTED", "MISSING", "FALSE_POSITIVE", "DRIFT"} +DIAGNOSTIC_OUTCOMES = {"EMIT", "SUPPRESS", "REFINE", "PRESERVE"} +OPERATIONS = { + "CLASSIFY", + "IMPLEMENT", + "TEST", + "DOCUMENT", + "RELEASE", + "TRIAGE", + "PRESERVE", +} +RISK_LEVELS = {"READ_ONLY", "REVERSIBLE_WRITE", "DESTRUCTIVE"} +AUTHORIZATIONS = { + "NOT_APPLICABLE", + "SESSION_EXECUTION_AUTHORIZATION", + "EXPLICIT_HUMAN", +} +AUTOMATION_VALUES = {"ALLOWED", "PROHIBITED"} +VERIFICATION_TYPES = {"COMMAND", "ASSERTION", "EXTERNAL_EVIDENCE"} +ANALYSIS_CODES = { + "T2C_AMBIGUOUS_INTENT", + "T2C_CONFLICT", + "T2C_CRITERION_GAP", + "T2C_PLAN_GAP", + "T2C_PRIORITY_DRIFT", + "T2C_SCOPE_EXPANSION", + "T2C_UNAUTHORIZED_DELETION", +} +REQUIRED_T2C_DIAGNOSTIC_CODES = { + "AMBIGUOUS_REQUIREMENT", + "HUMAN_AGENT_CONFLICT", + "HUMAN_COMMUNICATION_CONFLICT", + "PLANNED_NOT_IMPLEMENTED", +} + + +def _issue(code: str, path: str, message: str) -> dict[str, str]: + return {"code": code, "path": path, "message": message} + + +def _load_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise ValueError(f"cannot read JSON {path}: {error}") from error + if not isinstance(value, dict): + raise ValueError(f"JSON root must be an object: {path}") + return value + + +def _canonical(value: Any) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _digest(value: Any) -> str: + return hashlib.sha256(_canonical(value)).hexdigest() + + +def intent_projection(document: dict[str, Any]) -> dict[str, Any]: + """Return the authority-bearing projection used by advisory bindings.""" + projection = deepcopy(document) + projection.pop("advisoryAnalysis", None) + if projection.get("status") == "ANALYZED": + projection["status"] = "READY" + return projection + + +def intent_digest(document: dict[str, Any]) -> str: + return _digest(intent_projection(document)) + + +def _expect_object( + value: Any, + path: str, + errors: list[dict[str, str]], +) -> dict[str, Any]: + if not isinstance(value, dict): + errors.append(_issue(MALFORMED_CODE, path, "must be an object")) + return {} + return value + + +def _exact_fields( + value: dict[str, Any], + path: str, + required: set[str], + optional: set[str], + errors: list[dict[str, str]], +) -> None: + missing = sorted(required - set(value)) + unknown = sorted(set(value) - required - optional) + if missing: + errors.append( + _issue(MALFORMED_CODE, path, f"missing fields: {', '.join(missing)}") + ) + if unknown: + errors.append( + _issue(MALFORMED_CODE, path, f"unknown fields: {', '.join(unknown)}") + ) + + +def _nonempty_text( + value: Any, + path: str, + errors: list[dict[str, str]], +) -> str: + if not isinstance(value, str) or not value.strip(): + errors.append(_issue(MALFORMED_CODE, path, "must be a non-empty string")) + return "" + return value.strip() + + +def _enum( + value: Any, + allowed: set[str], + path: str, + errors: list[dict[str, str]], +) -> str: + result = _nonempty_text(value, path, errors) + if result and result not in allowed: + errors.append( + _issue( + MALFORMED_CODE, + path, + f"must be one of: {', '.join(sorted(allowed))}", + ) + ) + return result + + +def _string_list( + value: Any, + path: str, + errors: list[dict[str, str]], + *, + minimum: int = 0, +) -> list[str]: + if not isinstance(value, list): + errors.append(_issue(MALFORMED_CODE, path, "must be an array")) + return [] + result: list[str] = [] + for index, item in enumerate(value): + text = _nonempty_text(item, f"{path}[{index}]", errors) + if text: + result.append(text) + if len(result) < minimum: + errors.append( + _issue(MALFORMED_CODE, path, f"must contain at least {minimum} item(s)") + ) + if len(result) != len(set(result)): + errors.append(_issue(MALFORMED_CODE, path, "must contain unique items")) + return result + + +def _pattern( + value: Any, + pattern: re.Pattern[str], + path: str, + errors: list[dict[str, str]], +) -> str: + result = _nonempty_text(value, path, errors) + if result and pattern.fullmatch(result) is None: + errors.append(_issue(MALFORMED_CODE, path, "has an invalid identifier")) + return result + + +def _safe_path(value: str) -> bool: + if value == "unresolved:agent": + return True + if not value or "\\" in value or value.startswith("/"): + return False + if re.match(r"^[A-Za-z]:", value): + return False + path = PurePosixPath(value) + return ".." not in path.parts and not value.startswith("unresolved:") + + +def _path_list( + value: Any, + path: str, + errors: list[dict[str, str]], + *, + minimum: int = 0, +) -> list[str]: + result = _string_list(value, path, errors, minimum=minimum) + for index, item in enumerate(result): + if not _safe_path(item): + errors.append( + _issue(MALFORMED_CODE, f"{path}[{index}]", "must be a safe relative path") + ) + return result + + +def _duplicate_ids( + items: list[Any], + path: str, + errors: list[dict[str, str]], +) -> None: + seen: set[str] = set() + for index, item in enumerate(items): + if not isinstance(item, dict) or not isinstance(item.get("id"), str): + continue + item_id = item["id"] + if item_id in seen: + errors.append( + _issue(MALFORMED_CODE, f"{path}[{index}].id", f"duplicate id: {item_id}") + ) + seen.add(item_id) + + +def _cycles(graph: dict[str, list[str]]) -> list[list[str]]: + cycles: list[list[str]] = [] + visited: set[str] = set() + active: list[str] = [] + active_set: set[str] = set() + + def visit(node: str) -> None: + if node in active_set: + start = active.index(node) + cycles.append(active[start:] + [node]) + return + if node in visited: + return + visited.add(node) + active.append(node) + active_set.add(node) + for dependency in graph.get(node, []): + if dependency in graph: + visit(dependency) + active.pop() + active_set.remove(node) + + for node in graph: + visit(node) + return cycles + + +def _matches(path: str, patterns: list[str]) -> bool: + return any(fnmatchcase(path, pattern) for pattern in patterns) + + +def _ancestors(action_id: str, graph: dict[str, list[str]]) -> set[str]: + result: set[str] = set() + pending = list(graph.get(action_id, [])) + while pending: + candidate = pending.pop() + if candidate in result: + continue + result.add(candidate) + pending.extend(graph.get(candidate, [])) + return result + + +def _validate_source( + document: dict[str, Any], + errors: list[dict[str, str]], + warnings: list[dict[str, str]], +) -> None: + source = _expect_object(document.get("source"), "source", errors) + _exact_fields(source, "source", {"producer", "observedAt", "reportDigest"}, set(), errors) + producer = _expect_object(source.get("producer"), "source.producer", errors) + _exact_fields(producer, "source.producer", {"name", "version"}, set(), errors) + _nonempty_text(producer.get("name"), "source.producer.name", errors) + _nonempty_text(producer.get("version"), "source.producer.version", errors) + observed = _nonempty_text(source.get("observedAt"), "source.observedAt", errors) + if observed: + try: + datetime.fromisoformat(observed.replace("Z", "+00:00")) + except ValueError: + errors.append( + _issue(MALFORMED_CODE, "source.observedAt", "must be an ISO-8601 date-time") + ) + report_digest = _nonempty_text( + source.get("reportDigest"), "source.reportDigest", errors + ) + if report_digest and report_digest != "unresolved:agent" and DIGEST.fullmatch(report_digest) is None: + errors.append( + _issue(MALFORMED_CODE, "source.reportDigest", "must be a SHA-256 digest") + ) + if report_digest == "unresolved:agent": + warnings.append( + _issue(MALFORMED_CODE, "source.reportDigest", "report digest is unresolved") + ) + + +def _validate_objective_scope( + document: dict[str, Any], + errors: list[dict[str, str]], +) -> tuple[list[str], list[str], list[str]]: + objective = _expect_object(document.get("objective"), "objective", errors) + _exact_fields(objective, "objective", {"outcome", "nonGoals", "constraints"}, set(), errors) + _nonempty_text(objective.get("outcome"), "objective.outcome", errors) + _string_list(objective.get("nonGoals"), "objective.nonGoals", errors, minimum=1) + _string_list(objective.get("constraints"), "objective.constraints", errors, minimum=1) + + scope = _expect_object(document.get("scope"), "scope", errors) + _exact_fields(scope, "scope", {"allowedPaths", "forbiddenPaths", "preservePaths"}, set(), errors) + allowed = _path_list(scope.get("allowedPaths"), "scope.allowedPaths", errors, minimum=1) + forbidden = _path_list(scope.get("forbiddenPaths"), "scope.forbiddenPaths", errors) + preserve = _path_list(scope.get("preservePaths"), "scope.preservePaths", errors) + return allowed, forbidden, preserve + + +def _validate_findings( + document: dict[str, Any], + errors: list[dict[str, str]], +) -> tuple[list[dict[str, Any]], dict[str, dict[str, Any]]]: + raw = document.get("findings") + if not isinstance(raw, list) or not raw: + errors.append(_issue(MALFORMED_CODE, "findings", "must be a non-empty array")) + return [], {} + _duplicate_ids(raw, "findings", errors) + findings: list[dict[str, Any]] = [] + for index, candidate in enumerate(raw): + path = f"findings[{index}]" + finding = _expect_object(candidate, path, errors) + _exact_fields( + finding, + path, + { + "id", + "category", + "status", + "priority", + "summary", + "diagnostic", + "evidence", + "applicability", + "desiredOutcome", + "affectedPaths", + "dependsOn", + "acceptanceCriteria", + }, + set(), + errors, + ) + finding_id = _pattern(finding.get("id"), FINDING_ID, f"{path}.id", errors) + category = _enum(finding.get("category"), FINDING_CATEGORIES, f"{path}.category", errors) + _enum(finding.get("status"), FINDING_STATUSES, f"{path}.status", errors) + _enum(finding.get("priority"), PRIORITIES, f"{path}.priority", errors) + _nonempty_text(finding.get("summary"), f"{path}.summary", errors) + diagnostic = _expect_object(finding.get("diagnostic"), f"{path}.diagnostic", errors) + _exact_fields(diagnostic, f"{path}.diagnostic", {"code", "current", "required"}, set(), errors) + code = _pattern(diagnostic.get("code"), DIAGNOSTIC_CODE, f"{path}.diagnostic.code", errors) + current = _enum(diagnostic.get("current"), DIAGNOSTIC_STATES, f"{path}.diagnostic.current", errors) + required = _enum(diagnostic.get("required"), DIAGNOSTIC_OUTCOMES, f"{path}.diagnostic.required", errors) + + evidence = finding.get("evidence") + if not isinstance(evidence, list) or not evidence: + errors.append(_issue(MALFORMED_CODE, f"{path}.evidence", "must be a non-empty array")) + else: + for evidence_index, evidence_candidate in enumerate(evidence): + evidence_path = f"{path}.evidence[{evidence_index}]" + item = _expect_object(evidence_candidate, evidence_path, errors) + _exact_fields(item, evidence_path, {"ref", "observation"}, set(), errors) + _nonempty_text(item.get("ref"), f"{evidence_path}.ref", errors) + _nonempty_text(item.get("observation"), f"{evidence_path}.observation", errors) + + applicability = _expect_object(finding.get("applicability"), f"{path}.applicability", errors) + _exact_fields( + applicability, + f"{path}.applicability", + {"requiredSignals", "excludedSignals", "unknownOutcome"}, + set(), + errors, + ) + required_signals = _string_list( + applicability.get("requiredSignals"), + f"{path}.applicability.requiredSignals", + errors, + minimum=1, + ) + excluded_signals = _string_list( + applicability.get("excludedSignals"), + f"{path}.applicability.excludedSignals", + errors, + ) + _enum( + applicability.get("unknownOutcome"), + {"BLOCK", "REPORT"}, + f"{path}.applicability.unknownOutcome", + errors, + ) + _nonempty_text(finding.get("desiredOutcome"), f"{path}.desiredOutcome", errors) + _path_list(finding.get("affectedPaths"), f"{path}.affectedPaths", errors, minimum=1) + _string_list(finding.get("dependsOn"), f"{path}.dependsOn", errors) + _string_list( + finding.get("acceptanceCriteria"), + f"{path}.acceptanceCriteria", + errors, + minimum=1, + ) + + if category == "FALSE_POSITIVE": + if current != "FALSE_POSITIVE" or required not in {"REFINE", "SUPPRESS"}: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.diagnostic", + "FALSE_POSITIVE requires current=FALSE_POSITIVE and required=REFINE|SUPPRESS", + ) + ) + if not required_signals or not excluded_signals: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.applicability", + "FALSE_POSITIVE requires both positive and excluded signals", + ) + ) + if category in {"SILENT_OMISSION", "MISSING_INVENTORY"} and ( + current != "MISSING" or required != "EMIT" + ): + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.diagnostic", + f"{category} requires current=MISSING and required=EMIT", + ) + ) + if finding_id and code: + findings.append(finding) + finding_by_id = {item["id"]: item for item in findings} + graph: dict[str, list[str]] = {} + for index, finding in enumerate(findings): + dependencies = finding.get("dependsOn", []) + graph[finding["id"]] = dependencies if isinstance(dependencies, list) else [] + for dependency in graph[finding["id"]]: + if dependency not in finding_by_id: + errors.append( + _issue( + MALFORMED_CODE, + f"findings[{index}].dependsOn", + f"unknown finding dependency: {dependency}", + ) + ) + for cycle in _cycles(graph): + errors.append( + _issue(MALFORMED_CODE, "findings", f"dependency cycle: {' -> '.join(cycle)}") + ) + return findings, finding_by_id + + +def _validate_verifications( + document: dict[str, Any], + errors: list[dict[str, str]], +) -> tuple[list[dict[str, Any]], dict[str, dict[str, Any]]]: + raw = document.get("verifications") + if not isinstance(raw, list) or not raw: + errors.append( + _issue(MALFORMED_CODE, "verifications", "must be a non-empty array") + ) + return [], {} + _duplicate_ids(raw, "verifications", errors) + result: list[dict[str, Any]] = [] + for index, candidate in enumerate(raw): + path = f"verifications[{index}]" + item = _expect_object(candidate, path, errors) + _exact_fields( + item, + path, + {"id", "type", "command", "expected", "deterministic", "covers"}, + set(), + errors, + ) + verification_id = _pattern(item.get("id"), VERIFICATION_ID, f"{path}.id", errors) + verification_type = _enum(item.get("type"), VERIFICATION_TYPES, f"{path}.type", errors) + command = item.get("command") + if verification_type == "COMMAND": + _nonempty_text(command, f"{path}.command", errors) + elif command is not None: + _nonempty_text(command, f"{path}.command", errors) + _nonempty_text(item.get("expected"), f"{path}.expected", errors) + if not isinstance(item.get("deterministic"), bool): + errors.append(_issue(MALFORMED_CODE, f"{path}.deterministic", "must be boolean")) + _string_list(item.get("covers"), f"{path}.covers", errors, minimum=1) + if verification_id: + result.append(item) + return result, {item["id"]: item for item in result} + + +def _validate_actions( + document: dict[str, Any], + finding_by_id: dict[str, dict[str, Any]], + verification_by_id: dict[str, dict[str, Any]], + allowed: list[str], + forbidden: list[str], + preserve: list[str], + errors: list[dict[str, str]], +) -> tuple[list[dict[str, Any]], dict[str, list[str]]]: + raw = document.get("actions") + if not isinstance(raw, list) or not raw: + errors.append(_issue(MALFORMED_CODE, "actions", "must be a non-empty array")) + return [], {} + _duplicate_ids(raw, "actions", errors) + actions: list[dict[str, Any]] = [] + for index, candidate in enumerate(raw): + path = f"actions[{index}]" + item = _expect_object(candidate, path, errors) + _exact_fields( + item, + path, + { + "id", + "findingIds", + "operation", + "description", + "paths", + "dependsOn", + "verificationIds", + "risk", + }, + set(), + errors, + ) + action_id = _pattern(item.get("id"), ACTION_ID, f"{path}.id", errors) + finding_ids = _string_list(item.get("findingIds"), f"{path}.findingIds", errors, minimum=1) + operation = _enum(item.get("operation"), OPERATIONS, f"{path}.operation", errors) + _nonempty_text(item.get("description"), f"{path}.description", errors) + paths = _path_list(item.get("paths"), f"{path}.paths", errors, minimum=1) + dependencies = _string_list(item.get("dependsOn"), f"{path}.dependsOn", errors) + verification_ids = _string_list( + item.get("verificationIds"), f"{path}.verificationIds", errors, minimum=1 + ) + risk = _expect_object(item.get("risk"), f"{path}.risk", errors) + _exact_fields( + risk, + f"{path}.risk", + {"level", "authorization", "automation", "preservesUserData"}, + set(), + errors, + ) + level = _enum(risk.get("level"), RISK_LEVELS, f"{path}.risk.level", errors) + authorization = _enum( + risk.get("authorization"), + AUTHORIZATIONS, + f"{path}.risk.authorization", + errors, + ) + automation = _enum( + risk.get("automation"), AUTOMATION_VALUES, f"{path}.risk.automation", errors + ) + preserves_user_data = risk.get("preservesUserData") + if not isinstance(preserves_user_data, bool): + errors.append( + _issue(MALFORMED_CODE, f"{path}.risk.preservesUserData", "must be boolean") + ) + for finding_id in finding_ids: + if finding_id not in finding_by_id: + errors.append( + _issue(MALFORMED_CODE, f"{path}.findingIds", f"unknown finding: {finding_id}") + ) + verification_coverage: set[str] = set() + for verification_id in verification_ids: + verification = verification_by_id.get(verification_id) + if verification is None: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.verificationIds", + f"unknown verification: {verification_id}", + ) + ) + elif verification.get("deterministic") is not True: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.verificationIds", + f"action requires deterministic verification: {verification_id}", + ) + ) + else: + verification_coverage.update(verification.get("covers", [])) + missing_coverage = sorted( + {action_id, *finding_ids} - verification_coverage + ) + if missing_coverage: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.verificationIds", + "selected verifications do not cover: " + + ", ".join(missing_coverage), + ) + ) + for action_path in paths: + if action_path == "unresolved:agent": + continue + if not _matches(action_path, allowed): + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.paths", + f"path is outside scope.allowedPaths: {action_path}", + ) + ) + if _matches(action_path, forbidden): + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.paths", + f"path matches scope.forbiddenPaths: {action_path}", + ) + ) + if level == "DESTRUCTIVE" and ( + authorization != "EXPLICIT_HUMAN" or automation != "PROHIBITED" + ): + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.risk", + "DESTRUCTIVE action requires EXPLICIT_HUMAN and PROHIBITED automation", + ) + ) + state_risk = any( + finding_by_id.get(finding_id, {}).get("category") == "STATE_RISK" + for finding_id in finding_ids + ) + if state_risk and ( + operation != "PRESERVE" + or automation != "PROHIBITED" + or preserves_user_data is not True + ): + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.risk", + "STATE_RISK requires a PRESERVE action with prohibited automation and preserved user data", + ) + ) + if operation == "PRESERVE" and preserve and not any( + _matches(action_path, preserve) + for action_path in paths + if action_path != "unresolved:agent" + ): + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.paths", + "PRESERVE action must reference scope.preservePaths", + ) + ) + if action_id: + actions.append(item) + + action_ids = {item["id"] for item in actions} + graph: dict[str, list[str]] = {} + for index, action in enumerate(actions): + dependencies = action.get("dependsOn", []) + graph[action["id"]] = dependencies if isinstance(dependencies, list) else [] + for dependency in graph[action["id"]]: + if dependency not in action_ids: + errors.append( + _issue( + MALFORMED_CODE, + f"actions[{index}].dependsOn", + f"unknown action dependency: {dependency}", + ) + ) + for cycle in _cycles(graph): + errors.append( + _issue(MALFORMED_CODE, "actions", f"dependency cycle: {' -> '.join(cycle)}") + ) + + blocking_actions = { + action["id"] + for action in actions + if action.get("operation") != "RELEASE" + and any( + finding_by_id.get(finding_id, {}).get("priority") in {"P0", "P1"} + and finding_by_id.get(finding_id, {}).get("status") != "DEFERRED" + for finding_id in action.get("findingIds", []) + ) + } + for index, action in enumerate(actions): + if action.get("operation") != "RELEASE": + continue + missing = sorted(blocking_actions - _ancestors(action["id"], graph)) + if missing: + errors.append( + _issue( + MALFORMED_CODE, + f"actions[{index}].dependsOn", + "RELEASE must depend transitively on P0/P1 repair actions: " + + ", ".join(missing), + ) + ) + return actions, graph + + +def _validate_criteria_guidance_t2c( + document: dict[str, Any], + finding_by_id: dict[str, dict[str, Any]], + action_graph: dict[str, list[str]], + verification_by_id: dict[str, dict[str, Any]], + errors: list[dict[str, str]], +) -> None: + raw_criteria = document.get("acceptanceCriteria") + if not isinstance(raw_criteria, list) or not raw_criteria: + errors.append( + _issue(MALFORMED_CODE, "acceptanceCriteria", "must be a non-empty array") + ) + criteria: list[dict[str, Any]] = [] + else: + _duplicate_ids(raw_criteria, "acceptanceCriteria", errors) + criteria = [] + for index, candidate in enumerate(raw_criteria): + path = f"acceptanceCriteria[{index}]" + item = _expect_object(candidate, path, errors) + _exact_fields( + item, + path, + {"id", "statement", "findingIds", "verificationIds"}, + set(), + errors, + ) + criterion_id = _pattern(item.get("id"), CRITERION_ID, f"{path}.id", errors) + _nonempty_text(item.get("statement"), f"{path}.statement", errors) + finding_ids = _string_list(item.get("findingIds"), f"{path}.findingIds", errors, minimum=1) + verification_ids = _string_list( + item.get("verificationIds"), f"{path}.verificationIds", errors, minimum=1 + ) + for finding_id in finding_ids: + if finding_id not in finding_by_id: + errors.append( + _issue(MALFORMED_CODE, f"{path}.findingIds", f"unknown finding: {finding_id}") + ) + for verification_id in verification_ids: + if verification_id not in verification_by_id: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.verificationIds", + f"unknown verification: {verification_id}", + ) + ) + verification_coverage = { + covered + for verification_id in verification_ids + for covered in verification_by_id.get(verification_id, {}).get( + "covers", [] + ) + } + uncovered_findings = sorted( + set(finding_ids) - verification_coverage + ) + if uncovered_findings: + errors.append( + _issue( + MALFORMED_CODE, + f"{path}.verificationIds", + "criterion verifications do not cover findings: " + + ", ".join(uncovered_findings), + ) + ) + if criterion_id: + criteria.append(item) + + criterion_ids = {item["id"] for item in criteria} + for finding_id, finding in finding_by_id.items(): + for criterion_id in finding.get("acceptanceCriteria", []): + if criterion_id not in criterion_ids: + errors.append( + _issue( + MALFORMED_CODE, + f"finding:{finding_id}.acceptanceCriteria", + f"unknown acceptance criterion: {criterion_id}", + ) + ) + elif finding_id not in next( + item["findingIds"] for item in criteria if item["id"] == criterion_id + ): + errors.append( + _issue( + MALFORMED_CODE, + f"finding:{finding_id}.acceptanceCriteria", + f"criterion does not bind this finding: {criterion_id}", + ) + ) + + guidance = _expect_object(document.get("llmGuidance"), "llmGuidance", errors) + _exact_fields( + guidance, + "llmGuidance", + {"role", "mustPreserve", "forbiddenAssumptions", "planningOrder", "openQuestions"}, + set(), + errors, + ) + _nonempty_text(guidance.get("role"), "llmGuidance.role", errors) + _string_list(guidance.get("mustPreserve"), "llmGuidance.mustPreserve", errors, minimum=1) + _string_list( + guidance.get("forbiddenAssumptions"), + "llmGuidance.forbiddenAssumptions", + errors, + minimum=1, + ) + planning_order = _string_list( + guidance.get("planningOrder"), "llmGuidance.planningOrder", errors, minimum=1 + ) + _string_list(guidance.get("openQuestions"), "llmGuidance.openQuestions", errors) + if set(planning_order) != set(action_graph): + errors.append( + _issue( + MALFORMED_CODE, + "llmGuidance.planningOrder", + "must contain every action id exactly once", + ) + ) + position = {action_id: index for index, action_id in enumerate(planning_order)} + for action_id, dependencies in action_graph.items(): + for dependency in dependencies: + if position.get(dependency, -1) >= position.get(action_id, -1): + errors.append( + _issue( + MALFORMED_CODE, + "llmGuidance.planningOrder", + f"dependency order violated: {dependency} before {action_id}", + ) + ) + + todo2code = _expect_object(document.get("todo2code"), "todo2code", errors) + _exact_fields( + todo2code, + "todo2code", + {"enabled", "taskPath", "todoPath", "planSchema", "requiredDiagnosticCodes"}, + set(), + errors, + ) + if not isinstance(todo2code.get("enabled"), bool): + errors.append(_issue(MALFORMED_CODE, "todo2code.enabled", "must be boolean")) + _path_list([todo2code.get("taskPath")], "todo2code.taskPath", errors, minimum=1) + _path_list([todo2code.get("todoPath")], "todo2code.todoPath", errors, minimum=1) + if todo2code.get("planSchema") != T2C_PLAN_SCHEMA: + errors.append( + _issue(MALFORMED_CODE, "todo2code.planSchema", f"must be {T2C_PLAN_SCHEMA}") + ) + diagnostic_codes = _string_list( + todo2code.get("requiredDiagnosticCodes"), + "todo2code.requiredDiagnosticCodes", + errors, + minimum=1, + ) + missing_diagnostic_codes = sorted( + REQUIRED_T2C_DIAGNOSTIC_CODES - set(diagnostic_codes) + ) + unknown_diagnostic_codes = sorted( + set(diagnostic_codes) - REQUIRED_T2C_DIAGNOSTIC_CODES + ) + if missing_diagnostic_codes: + errors.append( + _issue( + MALFORMED_CODE, + "todo2code.requiredDiagnosticCodes", + "missing required consistency diagnostics: " + + ", ".join(missing_diagnostic_codes), + ) + ) + if unknown_diagnostic_codes: + errors.append( + _issue( + MALFORMED_CODE, + "todo2code.requiredDiagnosticCodes", + "unsupported consistency diagnostics: " + + ", ".join(unknown_diagnostic_codes), + ) + ) + + +def _validate_analysis( + document: dict[str, Any], + errors: list[dict[str, str]], +) -> None: + status = document.get("status") + analysis = document.get("advisoryAnalysis") + if status == "ANALYZED" and not isinstance(analysis, dict): + errors.append( + _issue(MALFORMED_CODE, "advisoryAnalysis", "ANALYZED status requires advisoryAnalysis") + ) + return + if analysis is None: + return + analysis = _expect_object(analysis, "advisoryAnalysis", errors) + _exact_fields( + analysis, + "advisoryAnalysis", + { + "authority", + "producer", + "analyzedAt", + "intentDigest", + "diagnosticsDigest", + "plansDigest", + "planIds", + "findings", + "llmHints", + }, + set(), + errors, + ) + if analysis.get("authority") != "ADVISORY": + errors.append( + _issue(MALFORMED_CODE, "advisoryAnalysis.authority", "must be ADVISORY") + ) + producer = _expect_object(analysis.get("producer"), "advisoryAnalysis.producer", errors) + _exact_fields(producer, "advisoryAnalysis.producer", {"name", "version", "mode"}, set(), errors) + if producer.get("name") != "todo2code" or producer.get("mode") != "deterministic": + errors.append( + _issue( + MALFORMED_CODE, + "advisoryAnalysis.producer", + "must identify deterministic todo2code", + ) + ) + _nonempty_text(producer.get("version"), "advisoryAnalysis.producer.version", errors) + _nonempty_text(analysis.get("analyzedAt"), "advisoryAnalysis.analyzedAt", errors) + for field in ("intentDigest", "diagnosticsDigest", "plansDigest"): + value = _nonempty_text(analysis.get(field), f"advisoryAnalysis.{field}", errors) + if value and DIGEST.fullmatch(value) is None: + errors.append( + _issue(MALFORMED_CODE, f"advisoryAnalysis.{field}", "must be SHA-256") + ) + if analysis.get("intentDigest") != intent_digest(document): + errors.append( + _issue( + STALE_CODE, + "advisoryAnalysis.intentDigest", + "analysis is stale for the authority-bearing intent projection", + ) + ) + _string_list(analysis.get("planIds"), "advisoryAnalysis.planIds", errors) + _string_list(analysis.get("llmHints"), "advisoryAnalysis.llmHints", errors) + findings = analysis.get("findings") + if not isinstance(findings, list): + errors.append(_issue(MALFORMED_CODE, "advisoryAnalysis.findings", "must be an array")) + else: + for index, candidate in enumerate(findings): + path = f"advisoryAnalysis.findings[{index}]" + item = _expect_object(candidate, path, errors) + _exact_fields(item, path, {"code", "severity", "message", "references", "llmHint"}, set(), errors) + _enum(item.get("code"), ANALYSIS_CODES, f"{path}.code", errors) + _enum(item.get("severity"), {"BLOCKING", "REVIEW", "INFO"}, f"{path}.severity", errors) + _nonempty_text(item.get("message"), f"{path}.message", errors) + _string_list(item.get("references"), f"{path}.references", errors, minimum=1) + _nonempty_text(item.get("llmHint"), f"{path}.llmHint", errors) + + +def validate_document(document: dict[str, Any]) -> dict[str, Any]: + errors: list[dict[str, str]] = [] + warnings: list[dict[str, str]] = [] + _exact_fields( + document, + "$", + { + "schema", + "intentId", + "ticket", + "repository", + "ownerRoute", + "status", + "source", + "objective", + "scope", + "findings", + "actions", + "verifications", + "acceptanceCriteria", + "llmGuidance", + "todo2code", + }, + {"advisoryAnalysis"}, + errors, + ) + if document.get("schema") != INTENT_SCHEMA: + errors.append(_issue(MALFORMED_CODE, "schema", f"must be {INTENT_SCHEMA}")) + _pattern(document.get("intentId"), INTENT_ID, "intentId", errors) + _pattern(document.get("ticket"), TICKET_ID, "ticket", errors) + _pattern(document.get("repository"), REPOSITORY, "repository", errors) + owner_route = _nonempty_text(document.get("ownerRoute"), "ownerRoute", errors) + status = _enum(document.get("status"), {"DRAFT", "READY", "ANALYZED"}, "status", errors) + _validate_source(document, errors, warnings) + allowed, forbidden, preserve = _validate_objective_scope(document, errors) + findings, finding_by_id = _validate_findings(document, errors) + for finding in findings: + for affected_path in finding.get("affectedPaths", []): + if affected_path == "unresolved:agent": + continue + if not _matches(affected_path, allowed): + errors.append( + _issue( + MALFORMED_CODE, + f"finding:{finding['id']}.affectedPaths", + f"path is outside scope.allowedPaths: {affected_path}", + ) + ) + if _matches(affected_path, forbidden): + errors.append( + _issue( + MALFORMED_CODE, + f"finding:{finding['id']}.affectedPaths", + f"path matches scope.forbiddenPaths: {affected_path}", + ) + ) + verifications, verification_by_id = _validate_verifications(document, errors) + actions, action_graph = _validate_actions( + document, + finding_by_id, + verification_by_id, + allowed, + forbidden, + preserve, + errors, + ) + action_finding_ids = { + finding_id + for action in actions + for finding_id in action.get("findingIds", []) + } + for finding in findings: + if finding.get("status") != "DEFERRED" and finding["id"] not in action_finding_ids: + errors.append( + _issue( + MALFORMED_CODE, + f"finding:{finding['id']}", + "active finding must be resolved by at least one action", + ) + ) + _validate_criteria_guidance_t2c( + document, finding_by_id, action_graph, verification_by_id, errors + ) + _validate_analysis(document, errors) + + unresolved_paths: list[str] = [] + if ( + status in {"READY", "ANALYZED"} + and document.get("source", {}).get("reportDigest") == "unresolved:agent" + ): + unresolved_paths.append("source.reportDigest") + if owner_route in {"unresolved:human", "unresolved:agent"}: + unresolved_paths.append("ownerRoute") + for path in allowed: + if path == "unresolved:agent": + unresolved_paths.append("scope.allowedPaths") + for finding in findings: + if "unresolved:agent" in finding.get("affectedPaths", []): + unresolved_paths.append(f"finding:{finding['id']}.affectedPaths") + if any(item.get("ref") == "unresolved:agent" for item in finding.get("evidence", [])): + unresolved_paths.append(f"finding:{finding['id']}.evidence") + for action in actions: + if "unresolved:agent" in action.get("paths", []): + unresolved_paths.append(f"action:{action['id']}.paths") + if unresolved_paths: + target = errors if status in {"READY", "ANALYZED"} else warnings + for path in unresolved_paths: + target.append( + _issue( + MALFORMED_CODE, + path, + "unresolved path is allowed only while status=DRAFT", + ) + ) + + finding_ids = {item["id"] for item in findings} + action_ids = {item["id"] for item in actions} + for verification in verifications: + for covered in verification.get("covers", []): + if covered not in finding_ids | action_ids: + errors.append( + _issue( + MALFORMED_CODE, + f"verification:{verification['id']}.covers", + f"unknown covered id: {covered}", + ) + ) + + return { + "schema": VALIDATION_SCHEMA, + "intentId": document.get("intentId"), + "intentDigest": intent_digest(document), + "findings": len(findings), + "actions": len(actions), + "errors": errors, + "warnings": warnings, + "ok": not errors, + } + + +def _require_valid(document: dict[str, Any], *, ready: bool = False) -> dict[str, Any]: + report = validate_document(document) + if ready and document.get("status") == "DRAFT": + report["errors"].append( + _issue(MALFORMED_CODE, "status", "todo2code projection requires READY or ANALYZED") + ) + report["ok"] = False + if not report["ok"]: + raise ValueError(json.dumps(report, ensure_ascii=False, indent=2)) + return report + + +def _criterion_map(document: dict[str, Any]) -> dict[str, dict[str, Any]]: + return { + criterion["id"]: criterion + for criterion in document.get("acceptanceCriteria", []) + if isinstance(criterion, dict) and isinstance(criterion.get("id"), str) + } + + +def render_llm(document: dict[str, Any]) -> str: + report = _require_valid(document, ready=True) + criteria = _criterion_map(document) + lines = [ + f"# Remediation planning brief: {document['intentId']}", + "", + f"- Ticket: `{document['ticket']}`", + f"- Repository: `{document['repository']}`", + f"- Owner route: `{document['ownerRoute']}`", + f"- Status: `{document['status']}`", + f"- Intent digest: `{report['intentDigest']}`", + "- Authority: accepted intent and deterministic governance; LLM/todo2code are advisory.", + "", + "## Objective", + "", + document["objective"]["outcome"], + "", + "### Non-goals", + "", + ] + lines.extend(f"- {item}" for item in document["objective"]["nonGoals"]) + lines.extend(["", "### Constraints", ""]) + lines.extend(f"- {item}" for item in document["objective"]["constraints"]) + lines.extend(["", "## Findings", ""]) + for finding in document["findings"]: + diagnostic = finding["diagnostic"] + lines.extend( + [ + f"### {finding['id']} — {diagnostic['code']} ({finding['priority']})", + "", + f"- Category/state: `{finding['category']}` / `{finding['status']}`", + f"- Diagnostic transition: `{diagnostic['current']} -> {diagnostic['required']}`", + f"- Observation: {finding['summary']}", + f"- Desired outcome: {finding['desiredOutcome']}", + "- Required signals:", + ] + ) + lines.extend(f" - {item}" for item in finding["applicability"]["requiredSignals"]) + lines.append("- Excluded signals:") + excluded = finding["applicability"]["excludedSignals"] + lines.extend(f" - {item}" for item in excluded or ["(none declared)"]) + lines.append("- Evidence:") + lines.extend( + f" - `{item['ref']}` — {item['observation']}" for item in finding["evidence"] + ) + lines.append("- Acceptance:") + lines.extend( + f" - [{criterion_id}] {criteria[criterion_id]['statement']}" + for criterion_id in finding["acceptanceCriteria"] + if criterion_id in criteria + ) + lines.append("") + lines.extend(["## Required planning order", ""]) + actions = {item["id"]: item for item in document["actions"]} + for index, action_id in enumerate(document["llmGuidance"]["planningOrder"], start=1): + action = actions[action_id] + paths = ", ".join(f"`{path}`" for path in action["paths"]) + lines.append( + f"{index}. [{action_id}/{action['operation']}] {action['description']} Paths: {paths}." + ) + lines.extend(["", "## LLM guardrails", "", f"Role: {document['llmGuidance']['role']}", ""]) + lines.append("Must preserve:") + lines.extend(f"- {item}" for item in document["llmGuidance"]["mustPreserve"]) + lines.append("") + lines.append("Forbidden assumptions:") + lines.extend( + f"- {item}" for item in document["llmGuidance"]["forbiddenAssumptions"] + ) + analysis = document.get("advisoryAnalysis") + if isinstance(analysis, dict): + lines.extend(["", "## Digest-bound todo2code hints (ADVISORY)", ""]) + lines.extend(f"- {hint}" for hint in analysis.get("llmHints", [])) + return "\n".join(lines).rstrip() + "\n" + + +def render_todo2code(document: dict[str, Any]) -> tuple[str, str]: + report = _require_valid(document, ready=True) + if document["todo2code"]["enabled"] is not True: + raise ValueError("todo2code projection is disabled by the accepted intent") + criteria = _criterion_map(document) + task_lines = [ + f"# Refactoring task {document['intentId']}", + "", + f"Ticket: {document['ticket']}", + f"Repository: {document['repository']}", + f"Intent-Digest: {report['intentDigest']}", + "", + "## Outcome", + "", + document["objective"]["outcome"], + "", + "## Required changes", + "", + ] + finding_by_id = {item["id"]: item for item in document["findings"]} + action_by_id = {item["id"]: item for item in document["actions"]} + todo_lines = [ + f"# TODO for {document['intentId']}", + "", + f"Intent-Digest: {report['intentDigest']}", + "", + ] + for action_id in document["llmGuidance"]["planningOrder"]: + action = action_by_id[action_id] + finding_labels = [] + criterion_labels: list[str] = [] + for finding_id in action["findingIds"]: + finding = finding_by_id[finding_id] + finding_labels.append( + f"{finding_id}/{finding['diagnostic']['code']}/{finding['priority']}" + ) + criterion_labels.extend(finding["acceptanceCriteria"]) + paths = ", ".join(f"`{path}`" for path in action["paths"]) + criterion_text = "; ".join( + f"[{criterion_id}] {criteria[criterion_id]['statement']}" + for criterion_id in dict.fromkeys(criterion_labels) + if criterion_id in criteria + ) + line = ( + f"[{action_id}] Implement {action['description']} " + f"Findings: {', '.join(finding_labels)}. Paths: {paths}. " + f"Acceptance: {criterion_text}" + ) + task_lines.extend([f"### {action_id}", "", line, ""]) + todo_lines.append(f"- [ ] {line}") + task_lines.extend(["## Constraints", ""]) + task_lines.extend(f"- {item}" for item in document["objective"]["constraints"]) + task_lines.extend(["", "## Non-goals", ""]) + task_lines.extend(f"- {item}" for item in document["objective"]["nonGoals"]) + return "\n".join(task_lines).rstrip() + "\n", "\n".join(todo_lines).rstrip() + "\n" + + +def _analysis_finding( + code: str, + severity: str, + message: str, + references: list[str], + hint: str, +) -> dict[str, Any]: + return { + "code": code, + "severity": severity, + "message": message, + "references": list(dict.fromkeys(references)), + "llmHint": hint, + } + + +def _plan_corpus(plan: dict[str, Any]) -> str: + return json.dumps(plan, ensure_ascii=False, sort_keys=True).lower() + + +def _plan_paths(plan: dict[str, Any]) -> list[str]: + target = plan.get("target") + if not isinstance(target, dict) or not isinstance(target.get("paths"), list): + return [] + return [path for path in target["paths"] if isinstance(path, str)] + + +def analyze_todo2code( + document: dict[str, Any], + diagnostics: dict[str, Any], + plans: dict[str, Any], +) -> tuple[dict[str, Any], bool]: + _require_valid(document, ready=True) + if diagnostics.get("schemaVersion") != T2C_DIAGNOSTICS_SCHEMA or not isinstance( + diagnostics.get("diagnostics"), list + ): + raise ValueError(f"diagnostics must use {T2C_DIAGNOSTICS_SCHEMA}") + if plans.get("schemaVersion") != T2C_PLAN_SET_SCHEMA or not isinstance( + plans.get("plans"), list + ): + raise ValueError(f"plans must use {T2C_PLAN_SET_SCHEMA}") + plan_items = [item for item in plans["plans"] if isinstance(item, dict)] + for index, plan in enumerate(plan_items): + if plan.get("schemaVersion") != T2C_PLAN_SCHEMA: + raise ValueError(f"plans[{index}] must use {T2C_PLAN_SCHEMA}") + + scope = document["scope"] + allowed = scope["allowedPaths"] + forbidden = scope["forbiddenPaths"] + finding_by_id = {item["id"]: item for item in document["findings"]} + action_by_id = {item["id"]: item for item in document["actions"]} + plan_corpora = {str(plan.get("id", f"plan-{index}")): _plan_corpus(plan) for index, plan in enumerate(plan_items)} + findings: list[dict[str, Any]] = [] + + for plan in plan_items: + plan_id = str(plan.get("id", "unknown-plan")) + for path in _plan_paths(plan): + if not _safe_path(path) or not _matches(path, allowed) or _matches(path, forbidden): + findings.append( + _analysis_finding( + "T2C_SCOPE_EXPANSION", + "BLOCKING", + f"todo2code plan {plan_id} targets path outside accepted scope: {path}", + [plan_id, path], + f"Remove `{path}` from the refactoring plan or obtain a fresh bounded intent before implementation.", + ) + ) + changes = plan.get("changes", []) + if isinstance(changes, list): + for change in changes: + if not isinstance(change, dict) or change.get("action") != "delete": + continue + path = str(change.get("path", "")) + authorized = any( + path in action.get("paths", []) + and action.get("risk", {}).get("level") == "DESTRUCTIVE" + and action.get("risk", {}).get("authorization") == "EXPLICIT_HUMAN" + for action in action_by_id.values() + ) + if not authorized: + findings.append( + _analysis_finding( + "T2C_UNAUTHORIZED_DELETION", + "BLOCKING", + f"todo2code proposes deletion without explicit-human destructive authorization: {path}", + [plan_id, path], + "Replace deletion with preservation/read-only triage or request explicit human authority in a fresh intent.", + ) + ) + + for finding_id, finding in finding_by_id.items(): + if finding.get("status") == "DEFERRED": + continue + code = finding["diagnostic"]["code"].lower() + affected = [path.lower() for path in finding.get("affectedPaths", [])] + matched = [ + plan_id + for plan_id, corpus in plan_corpora.items() + if finding_id.lower() in corpus + or code in corpus + or any(path != "unresolved:agent" and path in corpus for path in affected) + ] + if not matched: + findings.append( + _analysis_finding( + "T2C_PLAN_GAP", + "REVIEW", + f"no todo2code plan is grounded in active finding {finding_id}", + [finding_id, finding["diagnostic"]["code"]], + f"Add an explicit action/path link for {finding_id}; do not guess a path from the diagnostic name.", + ) + ) + continue + expected_priority = finding["priority"] + for plan in plan_items: + plan_id = str(plan.get("id", "unknown-plan")) + if plan_id not in matched: + continue + plan_priority = plan.get("priority") + if plan_priority in PRIORITIES and int(plan_priority[1]) > int(expected_priority[1]): + findings.append( + _analysis_finding( + "T2C_PRIORITY_DRIFT", + "REVIEW", + f"plan {plan_id} lowers {finding_id} from {expected_priority} to {plan_priority}", + [finding_id, plan_id], + f"Preserve the accepted {expected_priority} priority or record why a fresh intent changes it.", + ) + ) + + all_plan_text = "\n".join(plan_corpora.values()) + for criterion in document["acceptanceCriteria"]: + if criterion["id"].lower() not in all_plan_text and criterion["statement"].lower() not in all_plan_text: + findings.append( + _analysis_finding( + "T2C_CRITERION_GAP", + "REVIEW", + f"todo2code plans do not preserve acceptance criterion {criterion['id']}", + [criterion["id"]], + f"Add `{criterion['id']}` and its deterministic verification to the implementation plan.", + ) + ) + + for diagnostic in diagnostics["diagnostics"]: + if not isinstance(diagnostic, dict): + continue + code = diagnostic.get("code") + diagnostic_id = str(diagnostic.get("id", "unknown-diagnostic")) + action = str(diagnostic.get("suggestedAction", "Review the todo2code diagnostic.")) + detail = str(diagnostic.get("detail", diagnostic.get("title", code or "diagnostic"))) + if code == "AMBIGUOUS_REQUIREMENT": + findings.append( + _analysis_finding( + "T2C_AMBIGUOUS_INTENT", + "REVIEW", + detail, + [diagnostic_id], + action, + ) + ) + elif code in {"HUMAN_AGENT_CONFLICT", "HUMAN_COMMUNICATION_CONFLICT"}: + findings.append( + _analysis_finding( + "T2C_CONFLICT", + "BLOCKING", + detail, + [diagnostic_id], + action, + ) + ) + + unique_findings: list[dict[str, Any]] = [] + seen: set[bytes] = set() + for finding in findings: + key = _canonical(finding) + if key not in seen: + unique_findings.append(finding) + seen.add(key) + llm_hints = list(dict.fromkeys(item["llmHint"] for item in unique_findings)) + runtime_version = plans.get("generation", {}).get("runtimeVersion") + if not isinstance(runtime_version, str) or not runtime_version: + runtime_version = "unresolved-version" + + result = deepcopy(document) + result["status"] = "ANALYZED" + result["advisoryAnalysis"] = { + "authority": "ADVISORY", + "producer": { + "name": "todo2code", + "version": runtime_version, + "mode": "deterministic", + }, + "analyzedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "intentDigest": intent_digest(document), + "diagnosticsDigest": _digest(diagnostics), + "plansDigest": _digest(plans), + "planIds": [str(plan.get("id")) for plan in plan_items if plan.get("id")], + "findings": unique_findings, + "llmHints": llm_hints, + } + validation = validate_document(result) + if not validation["ok"]: + raise ValueError(json.dumps(validation, ensure_ascii=False, indent=2)) + blocking = any(item["severity"] == "BLOCKING" for item in unique_findings) + return result, blocking + + +def _write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _print_validation(report: dict[str, Any], output_format: str) -> None: + if output_format == "json": + print(json.dumps(report, ensure_ascii=False, indent=2)) + return + print( + f"remediation-intent: {report['findings']} findings, " + f"{report['actions']} actions, {len(report['errors'])} errors, " + f"{len(report['warnings'])} warnings" + ) + for issue in [*report["errors"], *report["warnings"]]: + print(f"{issue['code']}: {issue['path']}: {issue['message']}") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + validate_parser = subparsers.add_parser("validate", help="validate one remediation intent") + validate_parser.add_argument("intent", type=Path) + validate_parser.add_argument("--format", choices=("text", "json"), default="text") + + digest_parser = subparsers.add_parser("digest", help="print the authority-bearing intent digest") + digest_parser.add_argument("intent", type=Path) + + llm_parser = subparsers.add_parser("render-llm", help="render a canonical LLM planning brief") + llm_parser.add_argument("intent", type=Path) + llm_parser.add_argument("--out", type=Path) + + todo_parser = subparsers.add_parser( + "render-todo2code", help="render deterministic todo2code task and TODO inputs" + ) + todo_parser.add_argument("intent", type=Path) + todo_parser.add_argument("--task-out", type=Path, required=True) + todo_parser.add_argument("--todo-out", type=Path, required=True) + + analyze_parser = subparsers.add_parser( + "analyze-todo2code", help="bind todo2code diagnostics/plans as an advisory overlay" + ) + analyze_parser.add_argument("intent", type=Path) + analyze_parser.add_argument("--diagnostics", type=Path, required=True) + analyze_parser.add_argument("--plans", type=Path, required=True) + analyze_parser.add_argument("--out", type=Path, required=True) + + args = parser.parse_args() + try: + document = _load_json(args.intent) + if args.command == "validate": + report = validate_document(document) + _print_validation(report, args.format) + return 0 if report["ok"] else 1 + if args.command == "digest": + _require_valid(document) + print(intent_digest(document)) + return 0 + if args.command == "render-llm": + content = render_llm(document) + if args.out: + _write(args.out, content) + else: + print(content, end="") + return 0 + if args.command == "render-todo2code": + task, todo = render_todo2code(document) + _write(args.task_out, task) + _write(args.todo_out, todo) + return 0 + if args.command == "analyze-todo2code": + result, blocking = analyze_todo2code( + document, + _load_json(args.diagnostics), + _load_json(args.plans), + ) + _write(args.out, json.dumps(result, ensure_ascii=False, indent=2) + "\n") + if blocking: + print( + f"{T2C_CODE}: todo2code analysis contains blocking inconsistencies", + file=sys.stderr, + ) + return 1 if blocking else 0 + except ValueError as error: + print(f"{MALFORMED_CODE}: {error}", file=sys.stderr) + return 2 + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.governance/required-checks.json b/.governance/required-checks.json new file mode 100644 index 0000000..fc2248b --- /dev/null +++ b/.governance/required-checks.json @@ -0,0 +1,33 @@ +{ + "schema": "new-project.required-checks/v1", + "version": 1, + "repository": "wellmanifest/new-project", + "workflowFile": ".github/workflows/ci.yml", + "requiredCheckNames": [ + "test", + "windows-governance" + ], + "circularGovernanceChecksIgnoredByValidator": [ + "governance / enforce", + "governance / governance / enforce" + ], + "externalConsumers": [ + { + "name": "validator-agent", + "repository": "subactor/validator-agent", + "mustRead": "governance/required-checks.json#/requiredCheckNames", + "bindings": [ + "DIRECT_PR_REQUIRED_CHECKS", + "DIRECT_PR_SCAN_CONFIG[\"wellmanifest/new-project\"].required_checks", + "bin/dispatch-direct-pr.sh resolve_required_checks" + ], + "note": "External consumers must load requiredCheckNames from this file (or an equivalent generated export). Hardcoding a subset is a governance defect of the same class as ticket-025." + }, + { + "name": "github-ruleset", + "id": "main-governance-protection", + "mustMatch": "requiredCheckNames", + "note": "Ruleset lives outside the repository and cannot be read from CI without admin API. Align it manually with requiredCheckNames; CI gates the workflow side only." + } + ] +} diff --git a/.governance/stack-profiles.json b/.governance/stack-profiles.json new file mode 100644 index 0000000..3d58322 --- /dev/null +++ b/.governance/stack-profiles.json @@ -0,0 +1,14 @@ +{ + "schema": "new-project.stack-profiles/v1", + "profiles": { + "node": { "anyFiles": ["package.json"], "recommended": ["npm ci", "lint", "typecheck", "test", "dependency audit"] }, + "python": { "anyFiles": ["pyproject.toml", "requirements.txt"], "recommended": ["ruff", "mypy", "pytest", "bandit", "pip-audit"] }, + "go": { "anyFiles": ["go.mod"], "recommended": ["gofmt", "go vet", "go test -race", "staticcheck", "govulncheck"] }, + "rust": { "anyFiles": ["Cargo.toml"], "recommended": ["cargo fmt --check", "cargo clippy -- -D warnings", "cargo test", "cargo deny"] }, + "java": { "anyFiles": ["pom.xml", "build.gradle", "build.gradle.kts"], "recommended": ["wrapper verify", "tests", "Checkstyle or SpotBugs", "dependency audit"] }, + "docker": { "anyFiles": ["Dockerfile", "Dockerfile.e2e", "compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"], "recommended": ["hadolint", "docker compose config", "build", "Trivy", "SBOM"] }, + "frontend": { "anyFiles": ["playwright.config.ts", "playwright.config.js", "cypress.config.ts", "cypress.config.js"], "recommended": ["browser E2E", "accessibility", "pinned browser image"] }, + "terraform": { "anyFiles": ["main.tf", "versions.tf"], "recommended": ["terraform fmt -check", "terraform validate", "tflint", "checkov"] }, + "kubernetes": { "anyFiles": ["Chart.yaml", "kustomization.yaml"], "recommended": ["helm lint", "kubeconform", "OPA/Conftest"] } + } +} diff --git a/.governance/work-classification.dsl.json b/.governance/work-classification.dsl.json new file mode 100644 index 0000000..80c09f8 --- /dev/null +++ b/.governance/work-classification.dsl.json @@ -0,0 +1,120 @@ +{ + "$schema": "./work-classification.schema.json", + "schema": "new-project.work-classification/v1", + "dimensions": { + "kind": ["BUG", "FEATURE", "SERVICE"], + "priority": ["P0", "P1", "P2", "P3"], + "origin": ["regression", "requested", "health"] + }, + "ordering": { + "precedence": ["dependencies", "kind", "priority", "stableId"], + "kindOrder": ["BUG", "FEATURE", "SERVICE"], + "priorityOrder": ["P0", "P1", "P2", "P3"], + "dependencyPolicy": "topological-before-ranking", + "stableIdPolicy": "lexicographic" + }, + "priorityDerivation": { + "impact": { + "critical": "P0", + "high": "P1", + "medium": "P2", + "low": "P3" + }, + "declaredPolicy": "require-valid-priority", + "serviceDefault": "P2" + }, + "evaluation": { + "mode": "first-match", + "unmatchedPolicy": "reject", + "llmRole": "advisory-only" + }, + "rules": [ + { + "id": "W-CLASS-001", + "when": { + "signal": "defect", + "impact": "outage-or-security" + }, + "assign": { + "kind": "BUG", + "origin": "regression" + }, + "prioritySource": "impact" + }, + { + "id": "W-CLASS-002", + "when": { + "signal": "cyclomatic-complexity", + "baseline": "measured", + "delta": "increased" + }, + "assign": { + "kind": "BUG", + "origin": "regression" + }, + "prioritySource": "impact" + }, + { + "id": "W-CLASS-003", + "when": { + "signal": "cyclomatic-complexity", + "baseline": "measured", + "threshold": "crossed" + }, + "assign": { + "kind": "BUG", + "origin": "regression" + }, + "prioritySource": "impact" + }, + { + "id": "W-CLASS-004", + "when": { + "signal": "cyclomatic-complexity", + "baseline": "pre-existing", + "delta": "not-increased" + }, + "assign": { + "kind": "SERVICE", + "origin": "health" + }, + "prioritySource": "service-default" + }, + { + "id": "W-CLASS-005", + "when": { + "signal": "work-request", + "request": "new-behavior" + }, + "assign": { + "kind": "FEATURE", + "origin": "requested" + }, + "prioritySource": "declared" + }, + { + "id": "W-CLASS-006", + "when": { + "signal": "work-request", + "request": "maintenance" + }, + "assign": { + "kind": "SERVICE", + "origin": "health" + }, + "prioritySource": "service-default" + }, + { + "id": "W-CLASS-007", + "when": { + "signal": "defect", + "impact": "functional" + }, + "assign": { + "kind": "BUG", + "origin": "regression" + }, + "prioritySource": "impact" + } + ] +} diff --git a/.governance/work-classification.schema.json b/.governance/work-classification.schema.json new file mode 100644 index 0000000..6c19271 --- /dev/null +++ b/.governance/work-classification.schema.json @@ -0,0 +1,180 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/wellmanifest/new-project/governance/work-classification.schema.json", + "title": "new-project work classification DSL", + "type": "object", + "additionalProperties": false, + "required": ["$schema", "schema", "dimensions", "ordering", "priorityDerivation", "evaluation", "rules"], + "properties": { + "$schema": {"const": "./work-classification.schema.json"}, + "schema": {"const": "new-project.work-classification/v1"}, + "dimensions": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "priority", "origin"], + "properties": { + "kind": { + "type": "array", + "prefixItems": [{"const": "BUG"}, {"const": "FEATURE"}, {"const": "SERVICE"}], + "items": false, + "minItems": 3, + "maxItems": 3, + "uniqueItems": true + }, + "priority": { + "type": "array", + "prefixItems": [{"const": "P0"}, {"const": "P1"}, {"const": "P2"}, {"const": "P3"}], + "items": false, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true + }, + "origin": { + "type": "array", + "prefixItems": [{"const": "regression"}, {"const": "requested"}, {"const": "health"}], + "items": false, + "minItems": 3, + "maxItems": 3, + "uniqueItems": true + } + } + }, + "ordering": { + "type": "object", + "additionalProperties": false, + "required": ["precedence", "kindOrder", "priorityOrder", "dependencyPolicy", "stableIdPolicy"], + "properties": { + "precedence": { + "type": "array", + "prefixItems": [ + {"const": "dependencies"}, + {"const": "kind"}, + {"const": "priority"}, + {"const": "stableId"} + ], + "items": false, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true + }, + "kindOrder": { + "type": "array", + "prefixItems": [{"const": "BUG"}, {"const": "FEATURE"}, {"const": "SERVICE"}], + "items": false, + "minItems": 3, + "maxItems": 3, + "uniqueItems": true + }, + "priorityOrder": { + "type": "array", + "prefixItems": [{"const": "P0"}, {"const": "P1"}, {"const": "P2"}, {"const": "P3"}], + "items": false, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true + }, + "dependencyPolicy": {"const": "topological-before-ranking"}, + "stableIdPolicy": {"const": "lexicographic"} + } + }, + "priorityDerivation": { + "type": "object", + "additionalProperties": false, + "required": ["impact", "declaredPolicy", "serviceDefault"], + "properties": { + "impact": { + "type": "object", + "additionalProperties": false, + "required": ["critical", "high", "medium", "low"], + "properties": { + "critical": {"const": "P0"}, + "high": {"const": "P1"}, + "medium": {"const": "P2"}, + "low": {"const": "P3"} + } + }, + "declaredPolicy": {"const": "require-valid-priority"}, + "serviceDefault": {"enum": ["P0", "P1", "P2", "P3"]} + } + }, + "evaluation": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "unmatchedPolicy", "llmRole"], + "properties": { + "mode": {"const": "first-match"}, + "unmatchedPolicy": {"const": "reject"}, + "llmRole": {"const": "advisory-only"} + } + }, + "rules": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "uniqueItems": true, + "items": {"$ref": "#/$defs/rule"} + } + }, + "$defs": { + "rule": { + "type": "object", + "additionalProperties": false, + "required": ["id", "when", "assign", "prioritySource"], + "properties": { + "id": {"type": "string", "pattern": "^W-CLASS-[0-9]{3}$"}, + "when": { + "type": "object", + "additionalProperties": false, + "required": ["signal"], + "properties": { + "signal": {"enum": ["defect", "cyclomatic-complexity", "work-request"]}, + "impact": {"enum": ["outage-or-security", "functional"]}, + "baseline": {"enum": ["measured", "pre-existing"]}, + "delta": {"enum": ["increased", "not-increased"]}, + "threshold": {"const": "crossed"}, + "request": {"enum": ["new-behavior", "maintenance"]} + }, + "allOf": [ + { + "if": { + "properties": {"signal": {"const": "defect"}}, + "required": ["signal"] + }, + "then": {"required": ["impact"]} + }, + { + "if": { + "properties": {"signal": {"const": "cyclomatic-complexity"}}, + "required": ["signal"] + }, + "then": { + "required": ["baseline"], + "anyOf": [ + {"required": ["delta"]}, + {"required": ["threshold"]} + ] + } + }, + { + "if": { + "properties": {"signal": {"const": "work-request"}}, + "required": ["signal"] + }, + "then": {"required": ["request"]} + } + ] + }, + "assign": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "origin"], + "properties": { + "kind": {"enum": ["BUG", "FEATURE", "SERVICE"]}, + "origin": {"enum": ["regression", "requested", "health"]} + } + }, + "prioritySource": {"enum": ["impact", "declared", "service-default"]} + } + } + } +} diff --git a/.governance/workspace_lifecycle_check.py b/.governance/workspace_lifecycle_check.py new file mode 100755 index 0000000..80dfc6d --- /dev/null +++ b/.governance/workspace_lifecycle_check.py @@ -0,0 +1,555 @@ +#!/usr/bin/env python3 +"""Audit a workspace root for temporary checkouts and orphan local branches.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + + +REPORT_SCHEMA = "new-project.workspace-lifecycle-report/v1" +MAX_REPOSITORIES = 10_000 +SCP_REMOTE_RE = re.compile(r"^(?:[^@/]+@)?([^:/]+):(.+)$") +TICKET_DIRECTORY_RE = re.compile(r"^ticket-([0-9]+)$") + + +@dataclass(order=True) +class Finding: + code: str + severity: str + message: str + remediation: str + evidence: dict[str, Any] + + +@dataclass(frozen=True) +class TicketClaim: + number: int + ticket: str | None + summary: str | None + workstream: str | None + path: str + + +@dataclass(frozen=True) +class LocalBranch: + name: str + head: str + + +@dataclass(frozen=True) +class Checkout: + path: Path + common_git_dir: Path + identity: str + head: str | None + branch: str | None + dirty: bool + tickets: tuple[TicketClaim, ...] + + +class AuditError(RuntimeError): + """The local workspace could not be audited safely.""" + + +def run_git(root: Path, *arguments: str) -> str: + try: + result = subprocess.run( + ["git", "-C", str(root), *arguments], + capture_output=True, + check=False, + text=True, + timeout=15, + ) + except (OSError, subprocess.TimeoutExpired) as error: + raise AuditError(f"git failed for {root}: {error}") from error + if result.returncode != 0: + detail = (result.stderr or result.stdout).strip() + raise AuditError(f"git {' '.join(arguments)} failed for {root}: {detail}") + return result.stdout.strip() + + +def local_remote_path(root: Path, remote: str) -> Path | None: + if remote.startswith("file://"): + parsed = urlparse(remote) + return Path(parsed.path).resolve() + candidate = Path(remote).expanduser() + if candidate.is_absolute() or remote.startswith(("./", "../")): + if not candidate.is_absolute(): + candidate = root / candidate + return candidate.resolve() + return None + + +def normalized_network_remote(remote: str) -> str: + value = remote.strip().rstrip("/") + parsed = urlparse(value) + if parsed.scheme and parsed.hostname: + path = parsed.path.lstrip("/") + host = parsed.hostname.lower() + else: + match = SCP_REMOTE_RE.fullmatch(value) + if not match: + return f"remote:{value.removesuffix('.git').lower()}" + host, path = match.groups() + host = host.lower() + return f"remote:{host}/{path.removesuffix('.git').lower()}" + + +def repository_identity(root: Path, seen: set[Path] | None = None) -> str: + resolved = root.resolve() + visited = set() if seen is None else set(seen) + if resolved in visited: + raise AuditError(f"local origin cycle detected at {resolved}") + visited.add(resolved) + try: + remote = run_git(resolved, "remote", "get-url", "origin") + except AuditError as error: + if "No such remote" not in str(error): + raise + return f"local-repository:{resolved}" + local = local_remote_path(resolved, remote) + if local is not None and (local / ".git").exists(): + return repository_identity(local, visited) + if local is not None: + return f"local:{local}" + return normalized_network_remote(remote) + + +def checkout_head(path: Path) -> str | None: + try: + return run_git(path, "rev-parse", "--verify", "HEAD") + except AuditError: + status = run_git( + path, + "status", + "--porcelain=v2", + "--branch", + "--untracked-files=no", + ) + if "# branch.oid (initial)" in status.splitlines(): + return None + raise + + +def inspect_checkout(path: Path) -> Checkout: + common = Path( + run_git(path, "rev-parse", "--path-format=absolute", "--git-common-dir") + ).resolve() + head = checkout_head(path) + branch = run_git(path, "branch", "--show-current") or None + dirty = bool(run_git(path, "status", "--porcelain=v1", "--untracked-files=all")) + identity = repository_identity(path) + tickets = ticket_claims(path) + return Checkout( + path=path.resolve(), + common_git_dir=common, + identity=identity, + head=head, + branch=branch, + dirty=dirty, + tickets=tickets, + ) + + +def ticket_claims(root: Path) -> tuple[TicketClaim, ...]: + project = root / "project" + if not project.is_dir(): + return () + claims: list[TicketClaim] = [] + for directory in sorted(project.iterdir(), key=lambda item: item.name): + match = TICKET_DIRECTORY_RE.fullmatch(directory.name) + if not directory.is_dir() or match is None: + continue + intent_path = directory / "intent.json" + intent: dict[str, Any] = {} + try: + value = json.loads(intent_path.read_text(encoding="utf-8")) + if isinstance(value, dict): + intent = value + except (OSError, json.JSONDecodeError): + pass + claims.append(TicketClaim( + number=int(match.group(1)), + ticket=intent.get("ticket") if isinstance(intent.get("ticket"), str) else None, + summary=intent.get("summary") if isinstance(intent.get("summary"), str) else None, + workstream=( + intent.get("workstream") + if isinstance(intent.get("workstream"), str) + else None + ), + path=str(directory.resolve()), + )) + return tuple(claims) + + +def highest_ref_ticket(root: Path) -> int: + highest = 0 + refs = run_git( + root, + "for-each-ref", + "--format=%(refname)", + "refs/heads", + "refs/remotes", + ).splitlines() + for ref in refs: + paths = run_git(root, "ls-tree", "-d", "-r", "--name-only", ref, "--", "project") + for raw_path in paths.splitlines(): + match = re.fullmatch(r"project/ticket-([0-9]+)", raw_path) + if match: + highest = max(highest, int(match.group(1))) + return highest + + +def allocation_high_water(common_git_dir: Path) -> tuple[int | None, str | None]: + state = common_git_dir / "new-project-ticket-high-water" + if not state.exists(): + return None, None + try: + raw = state.read_text(encoding="utf-8").strip() + except OSError as error: + return None, str(error) + if not raw.isdigit(): + return None, "high-water state is not a decimal ticket number" + return int(raw), None + + +def allocation_findings(checkouts: list[Checkout]) -> list[Finding]: + findings: list[Finding] = [] + groups: dict[Path, list[Checkout]] = {} + for checkout in checkouts: + groups.setdefault(checkout.common_git_dir, []).append(checkout) + + for common_git_dir, group in sorted(groups.items(), key=lambda item: str(item[0])): + primary = min(group, key=lambda item: str(item.path)) + ref_highest = highest_ref_ticket(primary.path) + high_water, state_error = allocation_high_water(common_git_dir) + reserved_highest = max(ref_highest, high_water or 0) + if state_error: + findings.append(Finding( + code="GOV-TICKET-ALLOCATION-001", + severity="error", + message="The clone-wide ticket allocation reservation is unreadable.", + remediation=( + "Stop allocators, preserve every ticket worktree and repair the shared " + "high-water state through the managed allocator before assigning a number." + ), + evidence={"reason": state_error}, + )) + + claims_by_number: dict[int, list[TicketClaim]] = {} + for checkout in group: + for claim in checkout.tickets: + claims_by_number.setdefault(claim.number, []).append(claim) + if claim.number > reserved_highest: + findings.append(Finding( + code="GOV-TICKET-ALLOCATION-001", + severity="error", + message="A ticket directory is outside the clone-wide reservation.", + remediation=( + "Do not reuse or rename it automatically. Preserve the worktree, " + "classify ownership, then allocate through project/new-ticket.sh." + ), + evidence={ + "path": claim.path, + "refHighest": ref_highest, + "reservedHighWater": high_water, + "ticket": f"ticket-{claim.number:03d}", + }, + )) + + for number, claims in sorted(claims_by_number.items()): + identities = { + (claim.ticket, claim.summary, claim.workstream) + for claim in claims + } + if len(identities) <= 1: + continue + findings.append(Finding( + code="GOV-TICKET-ALLOCATION-002", + severity="error", + message="Linked worktrees assign different intents to the same ticket ID.", + remediation=( + "Stop both writers and preserve both heads. Keep the earlier reserved " + "identity, allocate a new ID through project/new-ticket.sh for the other " + "workstream, then rebuild its branch without mixing histories." + ), + evidence={ + "claims": [asdict(claim) for claim in sorted(claims, key=lambda item: item.path)], + "ticket": f"ticket-{number:03d}", + }, + )) + return findings + + +def registered_worktrees(path: Path) -> list[Path]: + worktrees: list[Path] = [] + for line in run_git(path, "worktree", "list", "--porcelain").splitlines(): + if line.startswith("worktree "): + worktrees.append(Path(line.removeprefix("worktree ")).resolve()) + return worktrees + + +def local_branches(path: Path) -> tuple[LocalBranch, ...]: + branches: list[LocalBranch] = [] + output = run_git( + path, + "for-each-ref", + "--format=%(refname:short)\t%(objectname)", + "refs/heads", + ) + for line in output.splitlines(): + name, separator, head = line.partition("\t") + if not separator or not name or not head: + raise AuditError(f"local branch inventory is malformed for {path}") + branches.append(LocalBranch(name=name, head=head)) + return tuple(sorted(branches, key=lambda item: item.name)) + + +def default_branch(path: Path, branches: tuple[LocalBranch, ...]) -> str | None: + if not branches: + return None + try: + remote_head = run_git( + path, + "symbolic-ref", + "--quiet", + "--short", + "refs/remotes/origin/HEAD", + ) + except AuditError: + remote_head = "" + if remote_head.startswith("origin/"): + return remote_head.removeprefix("origin/") + + names = {branch.name for branch in branches} + for conventional in ("main", "master"): + if conventional in names: + return conventional + if len(branches) == 1: + return branches[0].name + raise AuditError( + f"default branch cannot be resolved without origin/HEAD for {path}" + ) + + +def choose_primary(checkouts: list[Checkout]) -> Checkout: + slug = checkouts[0].identity.rsplit("/", 1)[-1] + named = [checkout for checkout in checkouts if checkout.path.name.lower() == slug] + if len(named) == 1: + return named[0] + common_owners = [ + checkout + for checkout in checkouts + if checkout.common_git_dir == checkout.path / ".git" + ] + return min( + common_owners or checkouts, + key=lambda item: (len(item.path.parts), str(item.path)), + ) + + +def local_branch_findings( + checkouts: list[Checkout], allowed: set[Path] +) -> list[Finding]: + findings: list[Finding] = [] + clone_groups: dict[Path, list[Checkout]] = {} + for checkout in checkouts: + clone_groups.setdefault(checkout.common_git_dir, []).append(checkout) + + for _, group in sorted(clone_groups.items(), key=lambda item: str(item[0])): + primary = choose_primary(group) + branches = local_branches(primary.path) + default = default_branch(primary.path, branches) + checkout_by_branch = { + checkout.branch: checkout + for checkout in group + if checkout.branch is not None + } + for branch in branches: + if branch.name == default: + continue + active_checkout = checkout_by_branch.get(branch.name) + if active_checkout is not None and active_checkout.path in allowed: + continue + findings.append(Finding( + code="GOV-WORKSPACE-LIFECYCLE-004", + severity="error", + message="A terminal workspace still contains a non-default local branch.", + remediation=( + "Classify the branch HEAD and preserve unique history. After releasing " + "its worktree, delete only this exact disposable local ref; never let " + "the checker delete it automatically." + ), + evidence={ + "branch": branch.name, + "checkout": ( + str(active_checkout.path) + if active_checkout is not None + else None + ), + "defaultBranch": default, + "head": branch.head, + "identity": primary.identity, + "primary": str(primary.path), + }, + )) + return findings + + +def evaluate(workspace_root: Path, allowed: set[Path]) -> list[Finding]: + if not workspace_root.is_dir(): + raise AuditError(f"workspace root is not a directory: {workspace_root}") + candidates: list[Path] = [] + for child in workspace_root.iterdir(): + if not child.is_dir(): + continue + if (child / ".git").exists(): + candidates.append(child) + continue + for grandchild in child.iterdir(): + if grandchild.is_dir() and (grandchild / ".git").exists(): + candidates.append(grandchild) + candidate_paths = {candidate.resolve() for candidate in candidates} + if len(candidate_paths) > MAX_REPOSITORIES: + raise AuditError(f"workspace contains more than {MAX_REPOSITORIES} repositories") + + pending = sorted(candidate_paths, key=str) + inspected: set[Path] = set() + while pending: + candidate = pending.pop(0) + if candidate in inspected: + continue + inspected.add(candidate) + discovered = { + worktree + for worktree in registered_worktrees(candidate) + if worktree not in candidate_paths + } + candidate_paths.update(discovered) + if len(candidate_paths) > MAX_REPOSITORIES: + raise AuditError( + f"workspace contains more than {MAX_REPOSITORIES} repositories" + ) + pending.extend(sorted(discovered, key=str)) + checkouts = [ + inspect_checkout(candidate) for candidate in sorted(candidate_paths, key=str) + ] + groups: dict[str, list[Checkout]] = {} + for checkout in checkouts: + groups.setdefault(checkout.identity, []).append(checkout) + + findings: list[Finding] = [] + findings.extend(allocation_findings(checkouts)) + findings.extend(local_branch_findings(checkouts, allowed)) + for identity in sorted(groups): + group = groups[identity] + if len(group) < 2: + continue + primary = choose_primary(group) + for checkout in sorted(group, key=lambda item: str(item.path)): + if checkout == primary or checkout.path in allowed: + continue + linked = checkout.common_git_dir == primary.common_git_dir + kind = "linked worktree" if linked else "duplicate clone" + findings.append(Finding( + code=( + "GOV-WORKSPACE-LIFECYCLE-001" + if linked + else "GOV-WORKSPACE-LIFECYCLE-002" + ), + severity="error", + message=f"A terminal workspace still contains a {kind}.", + remediation=( + "Verify dirty state and HEAD reachability. Preserve unknown or unique data; " + "then remove this exact workspace and its disposable local branch." + ), + evidence={ + "branch": checkout.branch, + "dirty": checkout.dirty, + "head": checkout.head, + "identity": identity, + "path": str(checkout.path), + "primary": str(primary.path), + }, + )) + return sorted( + findings, + key=lambda item: ( + item.code, + json.dumps(item.evidence, ensure_ascii=False, sort_keys=True), + ), + ) + + +def report_payload(findings: list[Finding]) -> dict[str, Any]: + return { + "schema": REPORT_SCHEMA, + "status": "passed" if not findings else "failed", + "summary": {"errors": len(findings), "warnings": 0, "findings": len(findings)}, + "findings": [asdict(item) for item in findings], + } + + +def render_text(payload: dict[str, Any]) -> str: + lines: list[str] = [] + for finding in payload["findings"]: + evidence = json.dumps( + finding["evidence"], + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + lines.append(f"{finding['code']} ERROR: {finding['message']} [{evidence}]") + lines.append(f" remediation: {finding['remediation']}") + summary = payload["summary"] + label = "GOV-WORKSPACE-PASS" if payload["status"] == "passed" else "GOV-WORKSPACE-FAIL" + lines.append( + f"{label}: {payload['status']} " + f"({summary['errors']} errors, {summary['warnings']} warnings)" + ) + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workspace-root", required=True, type=Path) + parser.add_argument( + "--allow", + action="append", + default=[], + type=Path, + help="Exact active secondary checkout allowed during this non-terminal audit.", + ) + parser.add_argument("--format", choices=("text", "json"), default="text") + args = parser.parse_args(argv) + + findings: list[Finding] + try: + allowed = {path.expanduser().resolve() for path in args.allow} + findings = evaluate(args.workspace_root.expanduser().resolve(), allowed) + except AuditError as error: + findings = [Finding( + code="GOV-WORKSPACE-LIFECYCLE-003", + severity="error", + message="The local workspace audit could not be completed safely.", + remediation="Repair repository metadata or narrow the explicit workspace root.", + evidence={"reason": str(error)}, + )] + + payload = report_payload(findings) + if args.format == "json": + print(json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))) + else: + print(render_text(payload)) + return 0 if payload["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d993bf7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,73 @@ +# AGENTS.md + +This target repository follows `wellmanifest/new-project` policy-as-code. + +Before any multi-step implementation, an agent must: + +1. Read `.governance/manifest.json`, `TODO.md`, `project/TICKETS.md` and the + active ticket. +2. Reuse an unfinished ticket whose workstream and scope match. A second active + ticket is allowed only in a distinct workstream with no write-scope overlap. + Otherwise run `./project/new-ticket.sh --title "..." --agent "..." + --workstream "..."`. +3. Complete the ticket `README.md`, owned `ai-*.md`, `intent.json` and `TODO.md`. +4. Treat a user request that already says to execute or work autonomously as + `SESSION_EXECUTION_AUTHORIZATION`; record it in the agent-owned ticket file. +5. Move to `EDIT` without a second confirmation and stay inside `intent.json` + `allowedPaths`. Ask for new authority only for destructive action, secret + access, new external coordination, or material objective expansion. +6. Never create or edit `project/ticket-*/user-*.md`; only its human owner or a + trusted intake boundary may do so. +7. Keep executable source/tests/scripts outside ticket directories. +8. Run the managed `./project/governance-check.sh` (or + `project\governance-check.bat` on Windows) plus the stack checks before + reporting completion. Root `project.sh` / `project.bat` are optional + target-owned seed aliases and must not be assumed to contain the gate. +9. Serialize ticket-ID allocation before branching, then use a separate + branch/worktree per implementation ticket. Each diff must resolve to exactly + one active ticket. Shared contract paths are edited only by the declared + integration workstream; `integrationTicket` coordinates work but does not + transfer path ownership. +10. Only `IN_PROGRESS` reserves a workstream and write scope. `BACKLOG`, `PLAN` + and `BLOCKED` retain evidence without blocking another implementation; + transition back to `IN_PROGRESS` before changing source or tests. +11. Treat GitHub review as trusted only when it targets the current HEAD and + either a `User` login is in protected `trusted-reviewers` or a `Bot` login + is in the separate protected `trusted-validator-apps` input. Never trust an + arbitrary Bot review. +12. Require merge approval evidence to bind repository, PR, current HEAD, + active ticket and actor. The protected resolver creates that evidence + outside the PR checkout; repository-authored evidence is untrusted. +13. A signed attestation is trusted only after a protected verifier validates + its signature, issuer, predicate type and subject bindings. +14. Validator-agent examples use + `LLM_MODEL_VALIDATOR=openrouter/z-ai/glm-5.2`; model findings stay advisory. +15. Configure GitHub with `delete_branch_on_merge=true`. A merged ticket branch + must disappear after merge. A PR closed without merge keeps its branch until + the owner explicitly discards that unmerged work. When no PR is open, the + only remote branch is the default branch. +16. At merge, publication or explicit pilot discard, inventory temporary linked + worktrees, duplicate clones and non-default local branches. Verify dirty state and HEAD reachability + before removal; preserve unknown or unique data. Remove an exact linked + worktree through Git, prune its metadata and only then delete its released + disposable branch. Prefer recoverable trash for a verified duplicate clone. + The checker is read-only; during active work exempt a branch only through + the exact allowlisted checkout path, never a pattern or branch name. Run the + adopted workspace lifecycle checker through Goal for the terminal audit. CI + validates GitHub state separately and cannot inspect a developer filesystem. +17. Allocate every ticket ID only through `./project/new-ticket.sh` after + fetching/pruning. Never create or copy `project/ticket-{NNN}` manually; the + clone-wide lock and high-water reservation must exist before commit. +18. Keep an implementation ticket `IN_PROGRESS / PUBLICATION` through + exact-head review and trusted merge. Set `DONE / DONE` only in a + governance-only closure based on the integrated default branch. +19. Resolve `GOV-*` findings through `.governance/diagnostics.json` and its + linked `.governance/error/*.md` runbook when present. Ticket logs are + historical evidence and never authorize bypassing a fail-closed gate. +20. Keep each incident-specific `remediation-intent.dsl.json` in its target + ticket. Validate it before LLM planning and treat todo2code/LLM results as + digest-bound advisory input; never let either expand the accepted intent. + +Markdown approval is an audit note, not trusted merge approval. Required +merge approval comes from the repository's protected review, attestation and +ruleset boundary. diff --git a/TODO.md b/TODO.md index 10dc7c1..0fbfbd5 100644 --- a/TODO.md +++ b/TODO.md @@ -15,6 +15,13 @@ stwórz fodler w sciezce: i sklonuj tam ten zasob +## Governance delivery + +- [x] [ticket-003](project/ticket-003/README.md): adopt immutable new-project + v0.16.2 and repair the Python, Node, and Rust publish strategies. +- [ ] Validate governance, package tests, delivery policy, exact-head review, + merge, and terminal workspace cleanup. + # TODO diff --git a/goal.yaml b/goal.yaml index ab3a861..a50e17f 100644 --- a/goal.yaml +++ b/goal.yaml @@ -220,11 +220,22 @@ git: enabled: true prefix: v format: '{prefix}{version}' +governance: + delivery: + require_goal_a: true + default_mode: pull-request + allowed_modes: + - pull-request + - publish-only + - direct-main + remote: origin + base_branch: main + require_clean_governance: true strategies: python: test: pytest tests/ -v build: python -m build - publish: twine upload dist/glon-{version}* + publish: twine upload --skip-existing dist/glon-{version}* publish_enabled: true dependencies: file: requirements.txt @@ -232,7 +243,7 @@ strategies: nodejs: test: npm test build: npm run build - publish: twine upload dist/glon-{version}* + publish: npm publish publish_enabled: true dependencies: file: package-lock.json @@ -240,7 +251,7 @@ strategies: rust: test: cargo test build: cargo build --release - publish: twine upload dist/glon-{version}* + publish: cargo publish publish_enabled: true dependencies: file: Cargo.lock diff --git a/project.bat b/project.bat new file mode 100644 index 0000000..6cfb2cc --- /dev/null +++ b/project.bat @@ -0,0 +1,35 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +set "REPO_ROOT=%~dp0" + +if not exist "%REPO_ROOT%.governance\manifest.json" ( + echo GOV-MANIFEST-001: .governance\manifest.json is not installed in this target repository. 1>&2 + echo remediation: bootstrap the pinned governance package before implementation. 1>&2 + exit /b 1 +) +if not exist "%REPO_ROOT%project\governance-check.bat" ( + echo GOV-BOOT-001: project\governance-check.bat is missing. 1>&2 + exit /b 1 +) + +call "%REPO_ROOT%project\governance-check.bat" %* +set "GOVERNANCE_EXIT=%ERRORLEVEL%" +if not "%GOVERNANCE_EXIT%"=="0" exit /b %GOVERNANCE_EXIT% + +if not "%NEW_PROJECT_ANALYSIS_IMAGE%"=="" ( + powershell -NoProfile -Command "if ($env:NEW_PROJECT_ANALYSIS_IMAGE -notmatch '@sha256:[a-f0-9]{64}$') { exit 1 }" + if errorlevel 1 ( + echo GOV-STACK-001: NEW_PROJECT_ANALYSIS_IMAGE must be pinned by sha256 digest. 1>&2 + exit /b 1 + ) + docker info >nul 2>&1 + if errorlevel 1 ( + echo GOV-DOCKER-001: Docker engine is unavailable. 1>&2 + exit /b 1 + ) + docker run --rm --network none --mount "type=bind,src=%REPO_ROOT%,dst=/workspace" --workdir /workspace "%NEW_PROJECT_ANALYSIS_IMAGE%" + set "DOCKER_EXIT=!ERRORLEVEL!" + exit /b !DOCKER_EXIT! +) + +exit /b 0 diff --git a/project/TICKETS.md b/project/TICKETS.md new file mode 100644 index 0000000..bafa8cb --- /dev/null +++ b/project/TICKETS.md @@ -0,0 +1,10 @@ +# Ticket index (`project/`) + +This file indexes governance tickets without taking ownership of +`project/README.md`, which may belong to an analysis generator. + + +| Ticket ID | Spec | Preprompt | Human input | Agent plans | Agent logs | Changelog | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| **ticket-003** | [`README.md`](./ticket-003/README.md) | [`preprompt.md`](./ticket-003/preprompt.md) | - | [`ai-codex.md`](./ticket-003/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-003/ai-codex-logs.txt) | [`changelog.md`](./ticket-003/changelog.md) | + diff --git a/project/governance-check.bat b/project/governance-check.bat new file mode 100644 index 0000000..3929963 --- /dev/null +++ b/project/governance-check.bat @@ -0,0 +1,11 @@ +@echo off +setlocal +set "REPO_ROOT=%~dp0.." +where python >nul 2>&1 +if errorlevel 1 ( + echo GOV-BOOT-001: python is unavailable on PATH. 1>&2 + exit /b 1 +) +python "%REPO_ROOT%\.governance\governance_check.py" --root "%REPO_ROOT%" --manifest .governance/manifest.json --lock .governance/manifest.lock.json --stack-profiles .governance/stack-profiles.json %* +set "GOVERNANCE_EXIT=%ERRORLEVEL%" +exit /b %GOVERNANCE_EXIT% diff --git a/project/governance-check.sh b/project/governance-check.sh new file mode 100755 index 0000000..7b119b6 --- /dev/null +++ b/project/governance-check.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +python3 "$repo_root/.governance/governance_check.py" \ + --root "$repo_root" \ + --manifest .governance/manifest.json \ + --lock .governance/manifest.lock.json \ + --stack-profiles .governance/stack-profiles.json \ + "$@" diff --git a/project/new-ticket.sh b/project/new-ticket.sh new file mode 100755 index 0000000..6524dca --- /dev/null +++ b/project/new-ticket.sh @@ -0,0 +1,442 @@ +#!/usr/bin/env bash +# Universal ticket scaffolder for target System X repositories. + +set -euo pipefail + +TITLE="New Task Ticket" +USERS="" +AGENT="antigravity" +WORKSTREAM="" +FORCE_NEW=false + +# Work classification for intent/v3. The defaults are the contract's own answer +# for an unclassified new ticket: rule W-CLASS-006 (work-request / maintenance) +# assigns SERVICE and health, and priorityDerivation.serviceDefault is P2. +# Declare --kind/--priority/--origin when the ticket is a defect or new behavior. +KIND="SERVICE" +PRIORITY="P2" +ORIGIN="health" + +usage() { + cat <<'EOF' +Usage: ./project/new-ticket.sh [options] + + -t, --title TITLE Ticket title + -a, --agent ID Agent provider/id used for ai-{ID}.md + -w, --workstream ID Required workstream declared in the governance manifest + -u, --users IDS Compatibility input only; human files are not created + -k, --kind KIND Work kind; default SERVICE + -p, --priority P Work priority; default P2 + -o, --origin ORIGIN Work origin; default health + --force-new Create a new ticket despite an unfinished ticket + -h, --help Show this help + +Accepted classification values are read from the work classification contract, +not hardcoded here. The defaults are that contract's own answer for an +unclassified new ticket (rule W-CLASS-006 plus the service priority default); +declare the three explicitly for a defect or new behavior. + +Only a human may authorize --force-new. Human-owned user-*.md files must be +created and written by that human or by a trusted intake boundary. +EOF +} + +require_value() { + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "Missing value for $1" >&2 + usage >&2 + exit 2 + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--title) + require_value "$@" + TITLE="$2" + shift 2 + ;; + -u|--users) + require_value "$@" + USERS="$2" + shift 2 + ;; + -a|--agent) + require_value "$@" + AGENT="$2" + shift 2 + ;; + -w|--workstream) + require_value "$@" + WORKSTREAM="$2" + shift 2 + ;; + -k|--kind) + require_value "$@" + KIND="$2" + shift 2 + ;; + -p|--priority) + require_value "$@" + PRIORITY="$2" + shift 2 + ;; + -o|--origin) + require_value "$@" + ORIGIN="$2" + shift 2 + ;; + --force-new) + FORCE_NEW=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ "$TITLE" == *$'\n'* || "$TITLE" == *$'\r'* ]]; then + echo "Ticket title must fit on one line" >&2 + exit 2 +fi + +AGENT="$(printf '%s' "$AGENT" | tr '[:upper:]' '[:lower:]')" +if [[ ! "$AGENT" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then + echo "Agent id must match [a-z0-9][a-z0-9._-]*" >&2 + exit 2 +fi + +if [[ -z "$WORKSTREAM" ]]; then + echo "Workstream is required; choose an id declared in .governance/manifest.json" >&2 + exit 2 +fi + +WORKSTREAM="$(printf '%s' "$WORKSTREAM" | tr '[:upper:]' '[:lower:]')" +if [[ ! "$WORKSTREAM" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then + echo "Workstream id must match [a-z0-9][a-z0-9-]*" >&2 + exit 2 +fi + +is_active_ticket() { + local readme="$1/README.md" + [[ -f "$readme" ]] && grep -Eiq '^-[[:space:]]+\*\*Status\*\*:[[:space:]]*IN_PROGRESS([[:space:]]|$)' "$readme" +} + +# The dimension vocabularies live in the work classification contract, which is +# shipped to targets as .governance/ and kept at governance/ in the hub. Reading +# them keeps this script from drifting away from the contract it must satisfy. +classification_dsl() { + local candidate + for candidate in .governance/work-classification.dsl.json governance/work-classification.dsl.json; do + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + done + return 1 +} + +require_classification_value() { + local dimension="$1" value="$2" dsl + if ! dsl="$(classification_dsl)"; then + echo "GOV-CLASS-000: work classification contract not found; cannot validate --$dimension." >&2 + echo " remediation: restore .governance/work-classification.dsl.json from the pinned package." >&2 + exit 1 + fi + local allowed + allowed="$(python3 -c 'import json,sys +data = json.load(open(sys.argv[1])) +print("\n".join(data["dimensions"][sys.argv[2]]))' "$dsl" "$dimension")" + if ! printf '%s\n' "$allowed" | grep -Fxq -- "$value"; then + echo "GOV-CLASS-001: '$value' is not a declared $dimension in $dsl." >&2 + echo " accepted: $(printf '%s' "$allowed" | tr '\n' ' ')" >&2 + exit 1 + fi +} + +require_classification_value kind "$KIND" +require_classification_value priority "$PRIORITY" +require_classification_value origin "$ORIGIN" + +# Serialize allocation across every worktree sharing this clone. The high-water +# mark reserves a number even before its ticket is committed and therefore +# remains visible when another worktree cannot see the new directory. +allocation_lock="" +allocation_state="" +release_allocation_lock() { + if [[ -n "$allocation_lock" ]]; then + rmdir "$allocation_lock" 2>/dev/null || true + fi +} +if git_common_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"; then + allocation_lock="$git_common_dir/new-project-ticket-allocation.lock" + allocation_state="$git_common_dir/new-project-ticket-high-water" + if ! mkdir "$allocation_lock" 2>/dev/null; then + echo "GOV-TICKET-LOCK-001: another ticket allocation is active in this clone." >&2 + echo " remediation: wait for it to finish; remove a stale lock only after confirming no allocator is running." >&2 + exit 4 + fi + trap release_allocation_lock EXIT INT TERM +fi + +# The allocator owns the freshness requirement. Relying on a caller to fetch +# recreates the same partial view that clone-wide locking is meant to avoid. +if git rev-parse --git-dir >/dev/null 2>&1 \ + && git remote get-url origin >/dev/null 2>&1; then + if ! git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' >/dev/null 2>&1; then + echo "GOV-TICKET-LOCK-004: remote ticket refs could not be refreshed safely." >&2 + echo " remediation: restore origin connectivity and retry; do not allocate a number from stale refs." >&2 + exit 4 + fi +fi + +# A ticket number taken on a branch is invisible on disk in another worktree. +# Consult every local and fetched remote branch known to this clone. +refs_highest() { + local highest_ref=0 ref number decimal + while read -r ref; do + [[ -n "$ref" ]] || continue + while read -r number; do + decimal=$((10#$number)) + (( decimal > highest_ref )) && highest_ref=$decimal + done < <( + git ls-tree -d -r --name-only "$ref" -- project 2>/dev/null \ + | sed -nE 's|^project/ticket-([0-9]+)$|\1|p' + ) + done < <(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null) + printf '%s' "$highest_ref" +} + +highest=0 +conflicting_ticket="" +if git rev-parse --git-dir >/dev/null 2>&1; then + highest="$(refs_highest)" + if [[ -n "$allocation_state" && -f "$allocation_state" ]]; then + read -r reserved_highest < "$allocation_state" + if [[ ! "$reserved_highest" =~ ^[0-9]+$ ]]; then + echo "GOV-TICKET-LOCK-002: ticket allocation state is invalid." >&2 + exit 4 + fi + reserved_decimal=$((10#$reserved_highest)) + (( reserved_decimal > highest )) && highest=$reserved_decimal + fi +fi +if [[ -d project ]]; then + for dir in project/ticket-*; do + [[ -d "$dir" ]] || continue + number="${dir##*-}" + [[ "$number" =~ ^[0-9]+$ ]] || continue + decimal=$((10#$number)) + (( decimal > highest )) && highest=$decimal + if is_active_ticket "$dir"; then + active_workstream="$(sed -nE 's/^[[:space:]]*"workstream"[[:space:]]*:[[:space:]]*"([a-z0-9-]+)".*/\1/p' "$dir/intent.json" 2>/dev/null | head -n 1)" + if [[ -z "$active_workstream" || "$active_workstream" == "unresolved" || "$WORKSTREAM" == "unresolved" || "$active_workstream" == "$WORKSTREAM" ]]; then + conflicting_ticket="$dir" + fi + fi + done +fi + +if [[ -n "$conflicting_ticket" && "$FORCE_NEW" != true ]]; then + echo "Active ticket conflicts with workstream '$WORKSTREAM': $conflicting_ticket" >&2 + echo "Continue it, choose a distinct declared workstream, close/cancel it, or use --force-new after an explicit human decision." >&2 + exit 3 +fi + +next_num=$((highest + 1)) +ticket_num="$(printf '%03d' "$next_num")" +ticket_id="ticket-$ticket_num" +ticket_dir="project/$ticket_id" +timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +date_only="${timestamp%%T*}" +agent_file="ai-$AGENT.md" +agent_log="ai-$AGENT-logs.txt" + +if ! mkdir "$ticket_dir" 2>/dev/null; then + echo "GOV-TICKET-LOCK-003: ticket directory already exists: $ticket_dir" >&2 + exit 4 +fi +if [[ -n "$allocation_state" ]]; then + allocation_state_tmp="$allocation_state.$$" + printf '%s\n' "$next_num" > "$allocation_state_tmp" + mv "$allocation_state_tmp" "$allocation_state" +fi + +escape_sed() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//&/\\&}" + value="${value//|/\\|}" + printf '%s' "$value" +} + +render_template() { + local source="$1" + local target="$2" + sed \ + -e "s|{TICKET_ID}|$(escape_sed "$ticket_id")|g" \ + -e "s|{NNN}|$(escape_sed "$ticket_num")|g" \ + -e "s|{SHORT_TITLE}|$(escape_sed "$TITLE")|g" \ + -e "s|{TIMESTAMP}|$(escape_sed "$timestamp")|g" \ + -e "s|{YYYY-MM-DD}|$(escape_sed "$date_only")|g" \ + -e "s|{OWNER_NAME}|unresolved:human|g" \ + -e "s|{PROVIDER}|$(escape_sed "$AGENT")|g" \ + -e "s|{WORKSTREAM}|$(escape_sed "$WORKSTREAM")|g" \ + "$source" > "$target" +} + +json_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + value="${value//$'\t'/\\t}" + printf '%s' "$value" +} + +render_json_template() { + local source="$1" + local target="$2" + sed \ + -e "s|{TICKET_ID}|$(escape_sed "$(json_escape "$ticket_id")")|g" \ + -e "s|{NNN}|$(escape_sed "$(json_escape "$ticket_num")")|g" \ + -e "s|{SHORT_TITLE}|$(escape_sed "$(json_escape "$TITLE")")|g" \ + -e "s|{TIMESTAMP}|$(escape_sed "$(json_escape "$timestamp")")|g" \ + -e "s|{YYYY-MM-DD}|$(escape_sed "$(json_escape "$date_only")")|g" \ + -e "s|{PROVIDER}|$(escape_sed "$(json_escape "$AGENT")")|g" \ + -e "s|{WORKSTREAM}|$(escape_sed "$(json_escape "$WORKSTREAM")")|g" \ + -e "s|{KIND}|$(escape_sed "$(json_escape "$KIND")")|g" \ + -e "s|{PRIORITY}|$(escape_sed "$(json_escape "$PRIORITY")")|g" \ + -e "s|{ORIGIN}|$(escape_sed "$(json_escape "$ORIGIN")")|g" \ + "$source" > "$target" +} + +if [[ -f template/files/ticket.template.md ]]; then + render_template template/files/ticket.template.md "$ticket_dir/README.md" +else + cat > "$ticket_dir/README.md" < "$ticket_dir/preprompt.md" < "$ticket_dir/intent.json" < "$ticket_dir/$agent_file" < "$ticket_dir/$agent_log" + +cat > "$ticket_dir/changelog.md" <&2 +fi + +if [[ -f project/readme.sh ]]; then + bash ./project/readme.sh +fi + +echo "Successfully scaffolded $ticket_dir for '$TITLE'." diff --git a/project/readme.sh b/project/readme.sh new file mode 100755 index 0000000..4dd4720 --- /dev/null +++ b/project/readme.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Universal ticket index generator for target System X repositories. + +set -euo pipefail + +index_file="${T2C_TICKET_INDEX_FILE:-project/TICKETS.md}" +case "$index_file" in + project/*) ;; + *) + echo "Ticket index must stay under project/: $index_file" >&2 + exit 2 + ;; +esac + +if [[ "$index_file" == *".."* ]]; then + echo "Ticket index cannot contain parent traversal: $index_file" >&2 + exit 2 +fi + +mkdir -p project +if [[ ! -f "$index_file" ]]; then + if [[ -f template/files/project.template.md ]]; then + timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + sed "s/{TIMESTAMP}/$timestamp/g" template/files/project.template.md > "$index_file" + else + cat > "$index_file" <<'EOF' +# Ticket index (`project/`) + +This file indexes governance tickets without taking ownership of +`project/README.md`, which may belong to an analysis generator. + + + +EOF + fi +fi + +start_count="$(grep -c '^$' "$index_file" || true)" +end_count="$(grep -c '^$' "$index_file" || true)" +if [[ "$start_count" != 1 || "$end_count" != 1 ]]; then + echo "$index_file must contain exactly one ticket-index marker pair" >&2 + exit 2 +fi + +table_file="$(mktemp "${TMPDIR:-/tmp}/new-project-ticket-table.XXXXXX")" +index_dir="$(dirname "$index_file")" +output_file="$(mktemp "$index_dir/.ticket-index.XXXXXX")" +cleanup() { + rm -f "$table_file" "$output_file" +} +trap cleanup EXIT INT TERM + +printf '%s\n' \ + '| Ticket ID | Spec | Preprompt | Human input | Agent plans | Agent logs | Changelog |' \ + '| :--- | :--- | :--- | :--- | :--- | :--- | :--- |' > "$table_file" + +for dir in project/ticket-*; do + [[ -d "$dir" ]] || continue + # A ticket git does not track yet belongs to work in flight, often somebody + # else's. Indexing it writes rows whose links resolve in no commit but that + # author's working tree, so whoever regenerates the index next ships broken + # links. An untracked ticket appears in the index once it is committed. + if git rev-parse --git-dir >/dev/null 2>&1 && [[ -z "$(git ls-files -- "$dir")" ]]; then + printf 'skipping untracked %s; commit it to have it indexed\n' "$dir" >&2 + continue + fi + ticket_name="$(basename "$dir")" + spec='-' + preprompt='-' + changelog='-' + [[ -f "$dir/README.md" ]] && spec="[\`README.md\`](./$ticket_name/README.md)" + [[ -f "$dir/preprompt.md" ]] && preprompt="[\`preprompt.md\`](./$ticket_name/preprompt.md)" + [[ -f "$dir/changelog.md" ]] && changelog="[\`changelog.md\`](./$ticket_name/changelog.md)" + + humans="" + for file in "$dir"/user-*.md; do + [[ -f "$file" ]] || continue + name="$(basename "$file")" + humans+=" [\`$name\`](./$ticket_name/$name)" + done + [[ -n "$humans" ]] || humans='-' + + agents="" + for file in "$dir"/ai-*.md; do + [[ -f "$file" ]] || continue + name="$(basename "$file")" + agents+=" [\`$name\`](./$ticket_name/$name)" + done + [[ -n "$agents" ]] || agents='-' + + logs="" + for file in "$dir"/ai-*-logs.txt; do + [[ -f "$file" ]] || continue + name="$(basename "$file")" + logs+=" [\`$name\`](./$ticket_name/$name)" + done + [[ -n "$logs" ]] || logs='-' + + printf '| **%s** | %s | %s | %s | %s | %s | %s |\n' \ + "$ticket_name" "$spec" "$preprompt" "$humans" "$agents" "$logs" "$changelog" >> "$table_file" +done + +awk -v table="$table_file" ' + /^$/ { + print + while ((getline line < table) > 0) print line + close(table) + inside = 1 + next + } + /^$/ { + inside = 0 + print + next + } + !inside { print } +' "$index_file" > "$output_file" + +chmod 0644 "$output_file" +mv "$output_file" "$index_file" +echo "Updated $index_file ticket index successfully." diff --git a/project/ticket-003/README.md b/project/ticket-003/README.md new file mode 100644 index 0000000..95a2506 --- /dev/null +++ b/project/ticket-003/README.md @@ -0,0 +1,67 @@ +# Ticket 003: Adopt governance and repair publish strategies + +- **ID**: ticket-003 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-08-12 + +## Goal and scope + +Adopt the immutable `wellmanifest/new-project` v0.16.2 governance package and +repair the publish commands corrupted by the historical Goal PY013 autofix. + +The implementation is limited to the standard-managed adoption payload, +ticket evidence, and `goal.yaml`. Application source, tests, package metadata, +the generated analysis under `project/`, and the target-owned `project.sh` +must remain unchanged. + +## Acceptance criteria + +- [x] AC-01: The adoption lock identifies published commit + `63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac` and version `0.16.2`. +- [x] AC-02: Existing `project.sh` remains byte-identical and is not listed as + a managed adoption file. +- [x] AC-03: Python publishing uses `twine upload --skip-existing`, Node uses + `npm publish`, and Rust uses `cargo publish`. +- [x] AC-04: Goal delivery defaults to `pull-request`, requires `goal -a`, and + permits release modes only after trusted merge. +- [x] AC-05: The deterministic governance gate passes with zero errors. +- [x] AC-06: Existing Python tests, Ruff and Black pass without changing + application source or tests; current mypy output is byte-for-byte equivalent + after path normalization to the `origin/main` baseline. +- [ ] AC-07: Delivery uses a ticket-bound PR and an independent current-head + approval before merge. +- [x] AC-08: User changes in the primary checkout are not included. + +## Risks and controls + +- A governance retrofit touches many managed files. Their hashes and source + revision are controlled by `.governance/manifest.lock.json`. +- The old PY013 fix changed all publisher types. Tests inspect the three YAML + values structurally, not by a broad text replacement. +- The primary checkout is dirty. Work happens in this dedicated worktree from + `origin/main`; no shared index is mutated. +- The v0.16.2 package installs a governance workflow under `.github`, while + its default manifest assigns all `.github/**` paths to infrastructure. The + local target manifest assigns this exact managed workflow to governance; + the upstream ownership mismatch remains a standard finding to report. +- The bounded budget is four local files outside ticket evidence. This is the + validator's measured minimum for this atomic adoption: target manifest, + adoption lock, optional Windows seed, and `goal.yaml`. +- Session execution authorization comes from the user's request to continue. + It is not trusted merge approval. + +## Known baseline limitation + +The current dependency set resolves a mypy release that no longer supports the +configured Python 3.9 target and reports the existing `ChoicesCompleter` +annotation in `glon/cli.py`. The exact same two diagnostics occur on +`origin/main`; ticket-003 neither fixes nor suppresses them. They require a +separate application/integration ticket because both `glon/**` and +`pyproject.toml` are forbidden here. + +## Participants + +- Human participant: unresolved; no user-* file was created by this script. +- Agent participant: [ai-codex.md](ai-codex.md) diff --git a/project/ticket-003/ai-codex-logs.txt b/project/ticket-003/ai-codex-logs.txt new file mode 100644 index 0000000..29e89b2 --- /dev/null +++ b/project/ticket-003/ai-codex-logs.txt @@ -0,0 +1,95 @@ +2026-08-12T19:05:00Z COMMAND goal governance adopt --source-revision 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac --target-root . --check +CREATE 37 standard-managed paths +MISSING target prerequisite project/TICKETS.md +drift detected: 37 change(s) required +exit=1 + +2026-08-12T19:06:00Z COMMAND goal governance adopt --source-revision 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac --target-root . +adopted wellmanifest/new-project 0.16.2 at 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac +MISSING target prerequisite project/TICKETS.md +exit=0 + +2026-08-12T19:20:00Z COMMAND ./project/governance-check.sh --actor agent +GOV-ARCHITECTURE-001 ERROR: Responsibility or persistent-data movement is not owned by an integration slice. [project/ticket-003/intent.json] +GOV-BUDGET-001 ERROR: Actual diff for ticket-003 exceeds its approved complexity budget. [.governance/manifest.json, .governance/manifest.lock.json, goal.yaml, project.bat] +GOV-FAIL: failed (2 errors, 0 warnings) +exit=1 + +2026-08-12T19:06:11Z COMMAND ./project/new-ticket.sh --title "Adopt governance and repair publish strategies" --agent codex --workstream governance --kind BUG --priority P1 --origin regression +skipping untracked project/ticket-003; commit it to have it indexed +Updated project/TICKETS.md ticket index successfully. +Successfully scaffolded project/ticket-003 for 'Adopt governance and repair publish strategies'. +exit=0 + +2026-08-12T19:12:00Z COMMAND ./project/governance-check.sh --actor agent +GOV-DELIVERY-001 ERROR: Implementation ticket ticket-003 has no bounded delivery contract. [project/ticket-003/intent.json] +GOV-WORKSTREAM-003 ERROR: Changed paths are not owned by workstream 'governance'. [.github/workflows/new-project-governance.yml] +GOV-WORKSTREAM-003 ERROR: Ticket ticket-003 claims paths outside workstream 'governance'. [.github/workflows/new-project-governance.yml] +GOV-FAIL: failed (3 errors, 0 warnings) +exit=1 + +2026-08-12T19:12:00Z COMMAND goal governance adopt --source-revision 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac --target-root . --check +up-to-date wellmanifest/new-project 0.16.2 at 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac +exit=0 + +2026-08-12T19:24:00Z COMMAND ./project/governance-check.sh --actor agent +GOV-PASS: passed (0 errors, 0 warnings) +exit=0 + +2026-08-12T19:25:00Z COMMAND uv run --frozen --extra dev pytest -q +57 passed in 2.49s +exit=0 + +2026-08-12T19:25:10Z COMMAND uv run --frozen --extra dev ruff check glon tests +All checks passed! +exit=0 + +2026-08-12T19:25:10Z COMMAND uv run --frozen --extra dev black --check glon tests +All done! 8 files would be left unchanged. +exit=0 + +2026-08-12T19:25:10Z COMMAND uv run --frozen --extra dev mypy glon +pyproject.toml: [mypy]: python_version: Python 3.9 is not supported (must be 3.10 or higher) +glon/cli.py:686: error: Argument 1 to "ChoicesCompleter" has incompatible type "Sequence[str]"; expected "Mapping[str, str | bytes]" [arg-type] +Found 1 error in 1 file (checked 4 source files) +exit=1 + +2026-08-12T19:27:00Z COMMAND compare normalized mypy output against isolated origin/main archive +base_mypy_rc=1 +head_mypy_rc=1 +mypy_baseline_comparison=PASS +exit=0 + +2026-08-12T19:25:10Z COMMAND goal governance adopt --source-revision 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac --target-root . --check +up-to-date wellmanifest/new-project 0.16.2 at 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac +exit=0 + +2026-08-12T19:25:00Z COMMAND goal doctor --fix +Goal v2.1.300 +No issues found +doctor_idempotent=PASS +exit=0 + +2026-08-12T19:24:30Z COMMAND goal governance verify-delivery --delivery-mode pull-request +enabled=true +hookInstalled=true +mode=pull-request +require_goal_a=true +require_clean_governance=true +serverEnforcementRequired=true +exit=0 + +2026-08-12T19:31:00Z COMMAND goal -a --delivery-mode pull-request --no-publish push --ticket ticket-003 +Goal v2.1.300 +Governed pull-request mode: registry publish waits for merge. +Detected project types: python +Error: pull-request resume refuses commits not bound to ticket-003: docs(governance): record ticket-003 validation +exit=1 + +2026-08-12T19:33:00Z COMMAND reword unpublished local candidate subjects with exact [ticket-003] prefix +old_head=23b9eceeeec5918d4bf9648c6222ad2daf7f49ea +new_head=b09ba4acd6d20900e5254273a6077b5afd2782ca +old_tree=ec1bcc2d21082539f9917b8b1322c8b32fa462a9 +new_tree=ec1bcc2d21082539f9917b8b1322c8b32fa462a9 +GOV-PASS: passed (0 errors, 0 warnings) +exit=0 diff --git a/project/ticket-003/ai-codex.md b/project/ticket-003/ai-codex.md new file mode 100644 index 0000000..b62273e --- /dev/null +++ b/project/ticket-003/ai-codex.md @@ -0,0 +1,70 @@ +--- +participant-id: agent:codex +participant: codex +role: agent +ticket: ticket-003 +--- +# Participant: codex (AI agent) + +## Understanding + +The current task continues the live Glon pilot requested by the user. The +first pilot exposed bootstrap collisions and over-strict defaults. The +published v0.16.2 candidate now treats root scripts as target-owned seeds, +makes Docker optional for a library with no declared container stack, and +adds lifecycle checks. In parallel, public Goal 2.1.300 proved on an isolated +clone that PY013 now updates only the Python strategy. + +This ticket adopts that immutable standard and fixes the three publish values +in the real repository through a protected pull-request workflow. It does not +change application behavior or publish a new package version. + +## Execution plan + +1. Record and commit this bounded plan separately from implementation. +2. Verify the adoption lock and preserve the existing root `project.sh`. +3. Configure governed Goal delivery and repair the three publish commands. +4. Run the deterministic gate, structural configuration assertions, package + tests, lint/type checks, and adoption drift check. +5. Deliver only a ticket-bound PR with public Goal 2.1.300. +6. Require independent current-head review, merge exact head, retest `main`, + then create a governance-only closure and clean disposable workspaces. + +## Actual changes + +- Initialized the bounded ticket and recorded SESSION_EXECUTION_AUTHORIZATION + from the request to execute this work. +- Adopted the immutable v0.16.2 package in a dedicated worktree; implementation + and validation evidence remain pending. +- Added the bounded delivery contract required by the v0.16.2 validator. +- Assigned only the exact standard-managed GitHub workflow to the local + governance workstream after the first gate exposed an upstream ownership + mismatch between the package and default manifest. +- Corrected the unchanged outcome's measured budget from two to four files; + no allowed path or implementation was added. No Glon component ownership or + persistent application data moves as part of the retrofit. +- Verified 57 tests, Ruff and Black. Mypy remains nonzero for two pre-existing + baseline diagnostics; an isolated archive of `origin/main` produced exactly + the same normalized output with the same tool and lockfile. +- Verified adoption drift, delivery policy, lock provenance, unchanged + `project.sh`, forbidden-path exclusion, and governance `0/0`; the candidate + is ready for PR publication while AC-07 remains open through review/merge. +- Goal's resume parser requires every subject to start with `[ticket-003] `. + The still-unpublished local subjects were reworded under a temporary backup + ref; pre/post tree hashes were identical and governance remained green. + +## Blockers + +- None inside the recorded intent; proceed without a second confirmation. +- New authority remains required for destructive action, secret access, new + external coordination, material objective expansion and trusted merge. +- The pre-existing mypy debt is explicitly outside this governance workstream + and does not represent a regression from this ticket. + +## Authorization + +- `SESSION_EXECUTION_AUTHORIZATION`: recorded from the user's instruction + `kontynuuj`, continuing the previously authorized Glon pilot and publication + workflow. +- This authorization covers edits and PR delivery inside `intent.json` only. +- It does not count as trusted merge approval. diff --git a/project/ticket-003/changelog.md b/project/ticket-003/changelog.md new file mode 100644 index 0000000..4dfc719 --- /dev/null +++ b/project/ticket-003/changelog.md @@ -0,0 +1,18 @@ +# Ticket Changelog (ticket-003) + +## [0.1.0] - 2026-08-12 + +- Initial governance scaffold created. +- No human participant identity or content was generated. +- Recorded the immutable adoption revision, bounded paths, acceptance criteria, + regression risk, and PR-only delivery plan. +- Added the bounded delivery contract and the minimal local ownership rule for + the standard-managed governance workflow. +- Aligned the bounded budget and architecture flags with the validator's + measured atomic-adoption diff without expanding scope. +- Recorded green tests/lint/format and the unchanged nonzero mypy baseline as + a separate, out-of-scope limitation. +- Advanced the ticket to `PUBLICATION` after all local acceptance checks except + trusted current-head PR review and merge completed. +- Reworded only unpublished local commit subjects to Goal's exact ticket-prefix + contract; candidate content remained byte-identical. diff --git a/project/ticket-003/intent.json b/project/ticket-003/intent.json new file mode 100644 index 0000000..d8e7087 --- /dev/null +++ b/project/ticket-003/intent.json @@ -0,0 +1,138 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-003", + "summary": "Adopt governance and repair publish strategies", + "workstream": "governance", + "classification": { + "kind": "BUG", + "priority": "P1", + "origin": "regression" + }, + "allowedPaths": [ + ".github/workflows/new-project-governance.yml", + ".governance/**", + "AGENTS.md", + "TODO.md", + "goal.yaml", + "project.bat", + "project/TICKETS.md", + "project/governance-check.bat", + "project/governance-check.sh", + "project/new-ticket.sh", + "project/readme.sh", + "project/ticket-003/**", + "scripts/runtime.sh" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md", + "glon/**", + "tests/**", + "project.sh", + "pyproject.toml", + "VERSION" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "ae7ea3533f0e0c1decffda78a25aed3a6931a63a", + "targetBranch": "main", + "outcome": "Glon adopts immutable new-project v0.16.2 and restores language-correct retry-safe publish strategies through governed Goal pull-request delivery", + "nonGoals": [ + "No application source, test, package metadata, generated analysis or target-owned project.sh change", + "No dependency, public API, package version, registry, tag or GitHub Release change", + "No direct push, force push, self-approval or merge without current-head trusted review", + "No deletion of unclassified local branches, worktrees or user changes" + ], + "complexity": "S", + "estimatedMinutes": 30, + "standardAdoption": { + "sourceRepository": "wellmanifest/new-project", + "fromRevision": null, + "toRevision": "63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac" + }, + "budgets": { + "maxImplementationFiles": 4, + "maxAffectedComponents": 2, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Install the hash-locked governance package without replacing target automation, assign its exact protected workflow to the local governance workstream, and repair only the three corrupted publisher values plus governed delivery policy", + "components": [ + { + "name": "governance-adoption", + "paths": [ + ".github/workflows/new-project-governance.yml", + ".governance/**", + "AGENTS.md", + "project.bat", + "project/governance-check.bat", + "project/governance-check.sh", + "project/new-ticket.sh", + "project/readme.sh", + "scripts/runtime.sh" + ] + }, + { + "name": "delivery-configuration", + "paths": ["goal.yaml"] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert the unmerged ticket branch or the exact merge commit; keep origin/main and package version unchanged" + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": ["goal governance adopt --source-revision 63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac --target-root . --check"], + "evidence": "Adoption reports up-to-date and the lock binds published v0.16.2" + }, + { + "criterion": "AC-02", + "commands": ["sha256sum project.sh", "validate .governance/manifest.lock.json"], + "evidence": "The baseline hash is unchanged and project.sh is absent from managedFiles" + }, + { + "criterion": "AC-03", + "commands": ["parse goal.yaml and assert all three strategies"], + "evidence": "Structured YAML values match Twine retry-safe, npm and Cargo publishers" + }, + { + "criterion": "AC-04", + "commands": ["goal governance verify-delivery --delivery-mode pull-request"], + "evidence": "Resolved policy requires goal -a and server-side enforcement" + }, + { + "criterion": "AC-05", + "commands": ["./project/governance-check.sh --actor agent"], + "evidence": "Deterministic validator reports GOV-PASS with zero errors" + }, + { + "criterion": "AC-06", + "commands": ["uv run --frozen --extra dev pytest -q", "uv run --frozen --extra dev ruff check glon tests", "uv run --frozen --extra dev black --check glon tests", "compare normalized uv run --frozen --extra dev mypy glon output with an origin/main archive"], + "evidence": "Tests, lint and formatting pass; the nonzero mypy diagnostics are exactly identical to the accepted-base baseline and introduce no regression" + }, + { + "criterion": "AC-07", + "commands": ["goal --delivery-mode pull-request --no-publish -a push --ticket ticket-003", "protected GitHub governance check", "independent current-head Validator App review"], + "evidence": "One ticket-bound PR is reviewed at its exact head before merge" + }, + { + "criterion": "AC-08", + "commands": ["git diff --name-only ae7ea3533f0e0c1decffda78a25aed3a6931a63a...HEAD", "git status --short in primary checkout"], + "evidence": "Forbidden application paths and primary-checkout user changes are absent from the ticket" + } + ] + } +} diff --git a/project/ticket-003/preprompt.md b/project/ticket-003/preprompt.md new file mode 100644 index 0000000..5e36ee9 --- /dev/null +++ b/project/ticket-003/preprompt.md @@ -0,0 +1,23 @@ +# Ticket preprompt + +- **Task ID**: ticket-003 +- **Task title**: Adopt governance and repair publish strategies +- **Created**: 2026-08-12T19:06:11Z + +Keep executable implementation outside this governance/evidence directory. +Read a human-owned user-*.md file only when one exists. +The request to execute this work creates SESSION_EXECUTION_AUTHORIZATION; +proceed within the recorded intent without a redundant confirmation prompt. +Require new authority for destructive action, secrets, external coordination, +material objective expansion and trusted merge approval. + +## Technical directives + +- Standard source: `wellmanifest/new-project` v0.16.2 at immutable commit + `63a03d0c2ec417f8eab9a6edb3c4ed654937a1ac`. +- Delivery runtime: public Goal 2.1.300, imported outside its source checkout. +- Preserve the target-owned root `project.sh` byte-for-byte. +- Treat `.governance/manifest.lock.json` as the managed-file hash source. +- Do not edit application source, tests, generated code-analysis artifacts, + package version, or human participant files. +- Publish implementation only through a ticket-bound pull request. diff --git a/scripts/runtime.sh b/scripts/runtime.sh new file mode 100755 index 0000000..4cdd534 --- /dev/null +++ b/scripts/runtime.sh @@ -0,0 +1,858 @@ +#!/usr/bin/env bash +set -euo pipefail + +runtime_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if ! command -v node >/dev/null 2>&1; then + echo "EVD-RUNTIME-001: Node.js 20 or newer is required" >&2 + exit 2 +fi + +node_major="$(node -p 'Number(process.versions.node.split(".")[0])')" +if [[ ! "$node_major" =~ ^[0-9]+$ ]] || (( node_major < 20 )); then + echo "EVD-RUNTIME-001: Node.js 20 or newer is required" >&2 + exit 2 +fi + +# The body is valid TypeScript and executable JavaScript. Keeping it inside the +# Bash entrypoint avoids a transpiler/runtime dependency while preserving one +# portable file for adopted TypeScript repositories. +exec node --input-type=commonjs - "$runtime_root" "$@" <<'TYPESCRIPT' +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const argv = process.argv.slice(2); +const packagedRoot = path.resolve(argv.shift() || "."); +const command = argv.shift() || "help"; + +function usage(message) { + if (message) process.stderr.write(`EVD-RUNTIME-002: ${message}\n`); + process.stderr.write( + "Usage:\n" + + " bash scripts/runtime.sh policy [--policy CONTRIBUTING.md]\n" + + " bash scripts/runtime.sh validate --evaluation FILE --intent FILE " + + "--manifest-lock FILE [--policy FILE] [--repository-root DIR] " + + "[--json-out FILE] [--markdown-out FILE]\n", + ); + process.exit(message ? 2 : 0); +} + +function parseOptions(items) { + const options = new Map(); + for (let index = 0; index < items.length; index += 1) { + const key = items[index]; + if (!key.startsWith("--")) usage(`unexpected argument ${key}`); + if (options.has(key)) usage(`option ${key} was repeated`); + const value = items[index + 1]; + if (value === undefined || value.startsWith("--")) usage(`option ${key} requires a value`); + options.set(key, value); + index += 1; + } + return options; +} + +function sortDeep(value) { + if (Array.isArray(value)) return value.map(sortDeep); + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortDeep(value[key])]), + ); + } + return value; +} + +function canonical(value) { + return JSON.stringify(sortDeep(value)); +} + +function sha256Bytes(value) { + return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`; +} + +function sha256File(filePath) { + return sha256Bytes(fs.readFileSync(filePath)); +} + +function readText(filePath, label) { + try { + return fs.readFileSync(filePath, "utf8"); + } catch (error) { + throw new Error(`${label} is unreadable: ${error.message}`); + } +} + +function readJson(filePath, label) { + try { + return JSON.parse(readText(filePath, label)); + } catch (error) { + if (error.message.startsWith(`${label} is unreadable:`)) throw error; + throw new Error(`${label} is not valid JSON: ${error.message}`); + } +} + +function diagnostic(code, message, evidence, remediation) { + return { + code, + severity: "BLOCKING", + message, + evidence: Array.isArray(evidence) ? evidence : [evidence].filter(Boolean), + remediation: Array.isArray(remediation) ? remediation : [remediation].filter(Boolean), + }; +} + +const requiredEvaluationRules = Array.from( + { length: 10 }, + (_, index) => `C-EVALUATION-${String(index + 1).padStart(3, "0")}`, +); + +function validatePolicyText(policyText) { + const diagnostics = []; + const counts = new Map(); + for (const match of policyText.matchAll(/^\s*RULE\s+(C-EVALUATION-\d{3})\b/gm)) { + counts.set(match[1], (counts.get(match[1]) || 0) + 1); + } + for (const rule of requiredEvaluationRules) { + if (counts.get(rule) !== 1) { + diagnostics.push( + diagnostic( + "EVD-POLICY-001", + `${rule} must occur exactly once in the policy`, + `observed=${counts.get(rule) || 0}`, + `restore the canonical ${rule} block from wellmanifest/new-project`, + ), + ); + } + } + const requiredClauses = [ + "CHANGE_EVALUATION_SCHEMA = \"t2c.change-evaluation/v1\"", + "PUBLICATION_MODE = PULL_REQUEST_REQUIRED_FOR_IMPLEMENTATION", + "DIRECT_PUSH = FORBIDDEN_FOR_IMPLEMENTATION", + "FORBID COMPENSATE_REQUIRED_GATE_WITH_NUMERIC_SCORE", + "FORBID LLM_OUTPUT_AS_TRUSTED_APPROVAL", + ]; + for (const clause of requiredClauses) { + if (!policyText.includes(clause)) { + diagnostics.push( + diagnostic( + "EVD-POLICY-002", + `required policy clause is missing: ${clause}`, + "CONTRIBUTING.md", + "restore the canonical CHANGE EVALUATION contract", + ), + ); + } + } + return diagnostics; +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isSha(value) { + return typeof value === "string" && /^[0-9a-f]{40}$/.test(value); +} + +function isDigest(value) { + return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value); +} + +function validateMinimumShape(evaluation) { + const diagnostics = []; + const requiredObjects = [ + "subject", + "contract", + "changeSet", + "gates", + "dimensions", + "approval", + "contribution", + "verdict", + "confidence", + "provenance", + ]; + if (!isObject(evaluation) || evaluation.schemaVersion !== "t2c.change-evaluation/v1") { + diagnostics.push( + diagnostic( + "EVD-SCHEMA-001", + "schemaVersion must equal t2c.change-evaluation/v1", + "change-evaluation.json", + "generate the report with the published v1 schema", + ), + ); + return diagnostics; + } + for (const key of requiredObjects) { + if (!isObject(evaluation[key])) { + diagnostics.push( + diagnostic("EVD-SCHEMA-001", `${key} must be an object`, key, "provide the required v1 object"), + ); + } + } + for (const key of ["actors", "criteriaEvaluation", "findings"]) { + if (!Array.isArray(evaluation[key])) { + diagnostics.push( + diagnostic("EVD-SCHEMA-001", `${key} must be an array`, key, "provide the required v1 array"), + ); + } + } + const allowedTopLevel = new Set([ + "schemaVersion", + "subject", + "contract", + "actors", + "changeSet", + "criteriaEvaluation", + "gates", + "dimensions", + "approval", + "findings", + "contribution", + "verdict", + "confidence", + "provenance", + ]); + for (const key of Object.keys(evaluation)) { + if (!allowedTopLevel.has(key)) { + diagnostics.push( + diagnostic( + "EVD-SCHEMA-001", + `unsupported top-level property: ${key}`, + key, + "remove the property or publish a new schema version", + ), + ); + } + } + if (isObject(evaluation.subject)) { + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(evaluation.subject.repository || "")) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "subject.repository is invalid", "subject.repository", "use owner/repository")); + } + if (!["commit", "push", "pull_request", "merge_group"].includes(evaluation.subject.event)) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "subject.event is invalid", "subject.event", "use a v1 event")); + } + if ( + ["pull_request", "merge_group"].includes(evaluation.subject.event) && + (!Number.isInteger(evaluation.subject.pullRequest) || evaluation.subject.pullRequest < 1) + ) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "subject.pullRequest is required", "subject.pullRequest", "provide the PR number")); + } + } + if (isObject(evaluation.contract)) { + if (!/^ticket-[0-9]{3}$/.test(evaluation.contract.ticket || "")) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "contract.ticket is invalid", "contract.ticket", "use ticket-NNN")); + } + if (!Array.isArray(evaluation.contract.criteria) || evaluation.contract.criteria.length === 0) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "contract.criteria must not be empty", "contract.criteria", "declare required criteria")); + } + } + if (isObject(evaluation.changeSet)) { + for (const key of ["commits", "changedPaths", "changedSymbols", "publicApiChanges", "dependencyChanges"]) { + if (!Array.isArray(evaluation.changeSet[key])) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", `changeSet.${key} must be an array`, `changeSet.${key}`, "provide the v1 field")); + } + } + } + if (Array.isArray(evaluation.actors)) { + for (const actor of evaluation.actors) { + if (!isObject(actor) || typeof actor.id !== "string" || !Array.isArray(actor.contributionTypes)) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "actor entry is invalid", "actors", "provide id, role and contributionTypes")); + } + } + } + if (Array.isArray(evaluation.criteriaEvaluation)) { + const statuses = ["SATISFIED", "PARTIAL", "FAILED", "UNKNOWN", "NOT_APPLICABLE"]; + for (const criterion of evaluation.criteriaEvaluation) { + if ( + !isObject(criterion) || + !/^AC-[0-9]+$/.test(criterion.criterion || "") || + !statuses.includes(criterion.status) || + !Array.isArray(criterion.implementationEvidence) || + !Array.isArray(criterion.validationEvidence) || + !Array.isArray(criterion.missingEvidence) || + typeof criterion.confidence !== "number" || + criterion.confidence < 0 || + criterion.confidence > 1 + ) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "criterion evaluation entry is invalid", "criteriaEvaluation", "conform to the v1 criterion contract")); + } + } + } + if (isObject(evaluation.contribution) && !Array.isArray(evaluation.contribution.claims)) { + diagnostics.push(diagnostic("EVD-SCHEMA-001", "contribution.claims must be an array", "contribution.claims", "provide evidence-backed claims")); + } + return diagnostics; +} + +function findNumericScore(value, prefix = "") { + const paths = []; + if (Array.isArray(value)) { + value.forEach((item, index) => paths.push(...findNumericScore(item, `${prefix}[${index}]`))); + } else if (isObject(value)) { + for (const [key, item] of Object.entries(value)) { + const current = prefix ? `${prefix}.${key}` : key; + if (/score$/i.test(key) && typeof item === "number") paths.push(current); + paths.push(...findNumericScore(item, current)); + } + } + return paths; +} + +function globToRegExp(glob) { + let expression = "^"; + for (let index = 0; index < glob.length; index += 1) { + const char = glob[index]; + if (char === "*" && glob[index + 1] === "*") { + expression += ".*"; + index += 1; + } else if (char === "*") { + expression += "[^/]*"; + } else if (char === "?") { + expression += "[^/]"; + } else { + expression += char.replace(/[\\^$+?.()|{}\[\]]/g, "\\$&"); + } + } + return new RegExp(`${expression}$`); +} + +function pathAllowed(changedPath, patterns) { + return patterns.some((pattern) => typeof pattern === "string" && globToRegExp(pattern).test(changedPath)); +} + +function git(repositoryRoot, args) { + const result = spawnSync("git", ["-C", repositoryRoot, ...args], { + encoding: "utf8", + shell: false, + }); + if (result.status !== 0) { + throw new Error((result.stderr || result.stdout || "git command failed").trim()); + } + return result.stdout; +} + +function exactStringSet(left, right) { + const normalize = (items) => [...new Set(items)].sort(); + return canonical(normalize(left)) === canonical(normalize(right)); +} + +function approvalScopeDigest(evaluation) { + return sha256Bytes( + canonical({ + actor: evaluation.approval.actor, + headSha: evaluation.subject.headSha, + pullRequest: evaluation.subject.pullRequest ?? null, + repository: evaluation.subject.repository, + ticket: evaluation.contract.ticket, + }), + ); +} + +function expectedVerdict(evaluation) { + const blockingGate = Object.values(evaluation.gates).some((status) => + ["FAILED", "UNKNOWN", "WAITING"].includes(status), + ); + const blockingDimension = Object.values(evaluation.dimensions).some((status) => + ["FAILED", "INSUFFICIENT_EVIDENCE"].includes(status), + ); + const blockingCriterion = evaluation.criteriaEvaluation.some((criterion) => + ["PARTIAL", "FAILED", "UNKNOWN"].includes(criterion.status), + ); + const blockingFinding = evaluation.findings.some((finding) => finding.severity === "BLOCKING"); + const approvalMissing = evaluation.approval.status !== "VERIFIED"; + if (blockingGate || blockingDimension || blockingCriterion || blockingFinding || approvalMissing) { + return "BLOCKED"; + } + const reviewFinding = evaluation.findings.some((finding) => finding.severity === "REVIEW_REQUIRED"); + if (Object.values(evaluation.dimensions).includes("REVIEW_REQUIRED") || reviewFinding) { + return "REVIEW_REQUIRED"; + } + return "ALLOWED"; +} + +function validateEvaluation(evaluation, intent, paths, policyText) { + const diagnostics = [...validatePolicyText(policyText), ...validateMinimumShape(evaluation)]; + if (diagnostics.some((item) => item.code === "EVD-SCHEMA-001")) return diagnostics; + + const scorePaths = findNumericScore(evaluation); + if (scorePaths.length > 0) { + diagnostics.push( + diagnostic( + "EVD-SCORE-001", + "numeric score fields are forbidden because they can compensate hard failures", + scorePaths, + "use independent status dimensions and non-compensable gates", + ), + ); + } + + const subject = evaluation.subject; + const contract = evaluation.contract; + for (const [label, value] of [ + ["baseSha", subject.baseSha], + ["headSha", subject.headSha], + ["mergeBaseSha", subject.mergeBaseSha], + ]) { + if (!isSha(value)) { + diagnostics.push( + diagnostic("INT-BINDING-001", `${label} must be a full lowercase commit SHA`, label, "record the exact Git SHA"), + ); + } + } + if (contract.ticket !== intent.ticket || contract.workstream !== intent.workstream) { + diagnostics.push( + diagnostic( + "INT-BINDING-002", + "evaluation ticket/workstream does not match the approved intent", + [`evaluation=${contract.ticket}/${contract.workstream}`, `intent=${intent.ticket}/${intent.workstream}`], + "regenerate the report for the active ticket intent", + ), + ); + } + if ( + isObject(intent.delivery) && + isSha(intent.delivery.acceptedBaseSha) && + intent.delivery.acceptedBaseSha !== subject.baseSha + ) { + diagnostics.push( + diagnostic( + "INT-BASE-001", + "subject.baseSha does not match the base accepted in ticket intent", + [`evaluation=${subject.baseSha}`, `intent=${intent.delivery.acceptedBaseSha}`], + "rebase the change or obtain approval for a refreshed intent base", + ), + ); + } + + const expectedHashes = { + intentHash: sha256File(paths.intent), + policyHash: sha256File(paths.policy), + manifestLockHash: sha256File(paths.manifestLock), + }; + for (const [key, expected] of Object.entries(expectedHashes)) { + if (!isDigest(contract[key]) || contract[key] !== expected) { + diagnostics.push( + diagnostic( + "INT-HASH-001", + `${key} does not match the evaluated source`, + [`expected=${expected}`, `observed=${contract[key]}`], + "regenerate the evaluation after loading the current contract files", + ), + ); + } + } + + const declaredPaths = Array.isArray(evaluation.changeSet.changedPaths) + ? evaluation.changeSet.changedPaths + : []; + const allowedPaths = Array.isArray(intent.allowedPaths) ? intent.allowedPaths : []; + for (const changedPath of declaredPaths) { + if (!pathAllowed(changedPath, allowedPaths)) { + diagnostics.push( + diagnostic( + "GOV-SCOPE-001", + `changed path is outside intent.allowedPaths: ${changedPath}`, + changedPath, + "remove the path or obtain a fresh approved intent", + ), + ); + } + } + + if (paths.repositoryRoot) { + try { + git(paths.repositoryRoot, ["cat-file", "-e", `${subject.baseSha}^{commit}`]); + git(paths.repositoryRoot, ["cat-file", "-e", `${subject.headSha}^{commit}`]); + const observedMergeBase = git(paths.repositoryRoot, [ + "merge-base", + subject.baseSha, + subject.headSha, + ]).trim(); + if (observedMergeBase !== subject.mergeBaseSha) { + diagnostics.push( + diagnostic( + "COM-GIT-001", + "mergeBaseSha does not match Git", + [`expected=${observedMergeBase}`, `observed=${subject.mergeBaseSha}`], + "regenerate the report for the current branch base and head", + ), + ); + } + const observedPaths = git(paths.repositoryRoot, [ + "diff", + "--name-only", + "-z", + subject.mergeBaseSha, + subject.headSha, + "--", + ]) + .split("\0") + .filter(Boolean); + if (!exactStringSet(observedPaths, declaredPaths)) { + diagnostics.push( + diagnostic( + "COM-GIT-002", + "changeSet.changedPaths does not match the exact Git range", + [`git=${observedPaths.sort().join(",")}`, `report=${[...declaredPaths].sort().join(",")}`], + "extract changed paths from mergeBaseSha..headSha", + ), + ); + } + const observedCommits = git(paths.repositoryRoot, [ + "rev-list", + "--reverse", + `${subject.mergeBaseSha}..${subject.headSha}`, + ]) + .split("\n") + .filter(Boolean); + const declaredCommits = Array.isArray(evaluation.changeSet.commits) + ? evaluation.changeSet.commits + : []; + if (!exactStringSet(observedCommits, declaredCommits)) { + diagnostics.push( + diagnostic( + "COM-GIT-003", + "changeSet.commits does not match the exact Git range", + [`git=${observedCommits.join(",")}`, `report=${declaredCommits.join(",")}`], + "extract every commit from mergeBaseSha..headSha", + ), + ); + } + } catch (error) { + diagnostics.push( + diagnostic( + "COM-GIT-001", + `exact Git range could not be verified: ${error.message}`, + paths.repositoryRoot, + "provide an existing repository and reachable full SHAs", + ), + ); + } + } + + const requiredCriteria = Array.isArray(contract.criteria) ? contract.criteria : []; + const evaluations = Array.isArray(evaluation.criteriaEvaluation) + ? evaluation.criteriaEvaluation + : []; + const evaluatedCriteria = evaluations.map((item) => item.criterion); + if (!exactStringSet(requiredCriteria, evaluatedCriteria)) { + diagnostics.push( + diagnostic( + "EVD-CRITERION-001", + "criteriaEvaluation must cover every declared criterion exactly once", + [`required=${requiredCriteria.join(",")}`, `evaluated=${evaluatedCriteria.join(",")}`], + "add one evidence record for each required acceptance criterion", + ), + ); + } + if (new Set(evaluatedCriteria).size !== evaluatedCriteria.length) { + diagnostics.push( + diagnostic( + "EVD-CRITERION-001", + "criteriaEvaluation contains duplicate criteria", + evaluatedCriteria, + "keep exactly one record per criterion", + ), + ); + } + for (const criterion of evaluations) { + if ( + criterion.status === "SATISFIED" && + (!Array.isArray(criterion.implementationEvidence) || + criterion.implementationEvidence.length === 0 || + !Array.isArray(criterion.validationEvidence) || + criterion.validationEvidence.length === 0) + ) { + diagnostics.push( + diagnostic( + "EVD-CRITERION-002", + `${criterion.criterion} is SATISFIED without implementation and validation evidence`, + criterion.criterion, + "attach both evidence classes or lower the criterion status", + ), + ); + } + } + + const requiredGates = [ + "governance", + "scope", + "secrets", + "tests", + "regression", + "documentation", + "approval", + "evidenceCompleteness", + ]; + for (const gate of requiredGates) { + if (!["PASS", "FAILED", "UNKNOWN", "WAITING", "NOT_APPLICABLE"].includes(evaluation.gates[gate])) { + diagnostics.push( + diagnostic("EVD-GATE-001", `required gate ${gate} has no valid status`, gate, "set an explicit gate status"), + ); + } + } + const dimensionStatuses = [ + "PASS", + "REVIEW_REQUIRED", + "FAILED", + "INSUFFICIENT_EVIDENCE", + "NOT_APPLICABLE", + ]; + const requiredDimensions = [ + "governanceCompliance", + "intentAlignment", + "implementationCorrectness", + "projectDirection", + "changeReasonableness", + "contributionValue", + "evidenceConfidence", + ]; + for (const dimension of requiredDimensions) { + if (!dimensionStatuses.includes(evaluation.dimensions[dimension])) { + diagnostics.push( + diagnostic( + "EVD-DIMENSION-001", + `required dimension ${dimension} has no valid status`, + dimension, + "set an explicit independent dimension status", + ), + ); + } + } + + const approval = evaluation.approval; + if (approval.status === "VERIFIED") { + const sourceContract = { + "github-review": ["human", "github-api-allowlist"], + "github-app-review": ["validator-app", "github-api-allowlist"], + "signed-attestation": ["attestation-issuer", "signed-attestation"], + }[approval.source]; + if (!sourceContract || approval.actorRole !== sourceContract[0] || approval.verificationMethod !== sourceContract[1]) { + diagnostics.push( + diagnostic( + "APR-AUTHORITY-001", + "approval source, actor role and verification method are inconsistent", + `${approval.source}/${approval.actorRole}/${approval.verificationMethod}`, + "use the protected source-specific approval resolver", + ), + ); + } + if (approval.headSha !== subject.headSha) { + diagnostics.push( + diagnostic( + "APR-STALE-001", + "approval is bound to a previous headSha", + [`approval=${approval.headSha}`, `head=${subject.headSha}`], + "obtain a new independent approval for the exact current head", + ), + ); + } + const expectedScope = approvalScopeDigest(evaluation); + if ( + approval.approvalScopeHash !== expectedScope || + contract.approvalScopeHash !== expectedScope + ) { + diagnostics.push( + diagnostic( + "APR-BINDING-001", + "approvalScopeHash does not match repository, PR, head, ticket and actor", + [ + `expected=${expectedScope}`, + `approval=${approval.approvalScopeHash}`, + `contract=${contract.approvalScopeHash}`, + ], + "recreate approval evidence in the protected verifier", + ), + ); + } + if (!isDigest(approval.evidenceDigest)) { + diagnostics.push( + diagnostic( + "APR-BINDING-002", + "verified approval requires a SHA-256 evidence digest", + approval.evidenceDigest, + "bind the protected approval evidence artifact by digest", + ), + ); + } + const authors = evaluation.actors + .filter((actor) => actor.role === "author" || actor.role === "last-push-author") + .map((actor) => actor.id); + if (authors.includes(approval.actor)) { + diagnostics.push( + diagnostic( + "APR-INDEPENDENCE-001", + "approval actor is also an author or last-push author", + approval.actor, + "obtain review from an independent trusted authority", + ), + ); + } + if (evaluation.gates.approval !== "PASS") { + diagnostics.push( + diagnostic( + "APR-GATE-001", + "verified approval requires gates.approval=PASS", + evaluation.gates.approval, + "reconcile the approval gate with protected evidence", + ), + ); + } + } else if (evaluation.gates.approval === "PASS") { + diagnostics.push( + diagnostic( + "APR-GATE-001", + "approval gate cannot pass without VERIFIED approval", + approval.status, + "attach exact-head protected approval evidence", + ), + ); + } + + const derivedMerge = expectedVerdict(evaluation); + if (evaluation.verdict.merge !== derivedMerge) { + diagnostics.push( + diagnostic( + "INT-VERDICT-001", + "declared merge verdict does not match hard gates, criteria, findings and dimensions", + [`expected=${derivedMerge}`, `observed=${evaluation.verdict.merge}`], + "use the deterministic derived verdict; do not average failures", + ), + ); + } + const criteriaComplete = evaluations.every((item) => + ["SATISFIED", "NOT_APPLICABLE"].includes(item.status), + ); + const expectedCompletion = + derivedMerge === "ALLOWED" && criteriaComplete + ? "ACCEPTED" + : derivedMerge === "REVIEW_REQUIRED" && criteriaComplete + ? "CANDIDATE" + : "NOT_DONE"; + if (evaluation.verdict.completion !== expectedCompletion) { + diagnostics.push( + diagnostic( + "INT-COMPLETION-001", + "declared completion does not match accepted evidence and merge verdict", + [`expected=${expectedCompletion}`, `observed=${evaluation.verdict.completion}`], + "mark incomplete work NOT_DONE until every required criterion is accepted", + ), + ); + } + return diagnostics; +} + +function markdownReport(valid, evaluation, diagnostics, evaluationDigest) { + const lines = ["# Change Evaluation", ""]; + lines.push(`Validation: ${valid ? "PASS" : "FAILED"}`); + if (evaluation && isObject(evaluation.subject) && isObject(evaluation.contract)) { + lines.push(`Merge verdict: ${evaluation.verdict?.merge || "UNKNOWN"}`); + lines.push(`Completion: ${evaluation.verdict?.completion || "UNKNOWN"}`); + lines.push(`Ticket: ${evaluation.contract.ticket || "UNKNOWN"}`); + lines.push(`Base: ${evaluation.subject.baseSha || "UNKNOWN"}`); + lines.push(`Head: ${evaluation.subject.headSha || "UNKNOWN"}`); + lines.push(`Intent hash: ${evaluation.contract.intentHash || "UNKNOWN"}`); + lines.push(`Policy hash: ${evaluation.contract.policyHash || "UNKNOWN"}`); + } + lines.push(`Evaluation digest: ${evaluationDigest || "UNAVAILABLE"}`, ""); + lines.push(`Blocking diagnostics: ${diagnostics.length}`); + if (diagnostics.length > 0) { + lines.push("", "## Diagnostics", ""); + for (const item of diagnostics) lines.push(`- ${item.code}: ${item.message}`); + } + return `${lines.join("\n")}\n`; +} + +function writeResult(options, envelope, markdown) { + const json = `${JSON.stringify(sortDeep(envelope), null, 2)}\n`; + if (options.has("--json-out")) fs.writeFileSync(path.resolve(options.get("--json-out")), json); + if (options.has("--markdown-out")) { + fs.writeFileSync(path.resolve(options.get("--markdown-out")), markdown); + } + process.stdout.write(json); +} + +if (command === "help" || command === "--help" || command === "-h") usage(); + +const options = parseOptions(argv); +const policyPath = path.resolve(options.get("--policy") || path.join(packagedRoot, "CONTRIBUTING.md")); + +if (command === "policy") { + const policyText = readText(policyPath, "policy"); + const diagnostics = validatePolicyText(policyText).sort((left, right) => + `${left.code}:${left.message}`.localeCompare(`${right.code}:${right.message}`), + ); + const result = { + schemaVersion: "t2c.change-evaluation-policy-validation/v1", + valid: diagnostics.length === 0, + policyHash: sha256File(policyPath), + ruleIds: requiredEvaluationRules, + diagnostics, + }; + process.stdout.write(`${JSON.stringify(sortDeep(result), null, 2)}\n`); + process.exit(result.valid ? 0 : 1); +} + +if (command !== "validate") usage(`unknown command ${command}`); +for (const required of ["--evaluation", "--intent", "--manifest-lock"]) { + if (!options.has(required)) usage(`${required} is required`); +} + +const paths = { + evaluation: path.resolve(options.get("--evaluation")), + intent: path.resolve(options.get("--intent")), + manifestLock: path.resolve(options.get("--manifest-lock")), + policy: policyPath, + repositoryRoot: options.has("--repository-root") + ? path.resolve(options.get("--repository-root")) + : null, +}; + +let evaluation; +let intent; +let diagnostics = []; +try { + evaluation = readJson(paths.evaluation, "evaluation"); + intent = readJson(paths.intent, "intent"); + readJson(paths.manifestLock, "manifest lock"); + const policyText = readText(paths.policy, "policy"); + diagnostics = validateEvaluation(evaluation, intent, paths, policyText); +} catch (error) { + diagnostics = [ + diagnostic( + "EVD-INPUT-001", + error.message, + "runtime input", + "provide readable, valid contract inputs", + ), + ]; +} + +diagnostics.sort((left, right) => + `${left.code}:${left.message}`.localeCompare(`${right.code}:${right.message}`), +); +let evaluationDigest = null; +if (evaluation !== undefined) { + const digestSubject = JSON.parse(JSON.stringify(evaluation)); + if (isObject(digestSubject.provenance)) delete digestSubject.provenance.evaluationDigest; + evaluationDigest = sha256Bytes(canonical(digestSubject)); +} +const valid = diagnostics.length === 0; +const mergeAllowed = valid && evaluation?.verdict?.merge === "ALLOWED"; +const envelope = { + schemaVersion: "t2c.change-evaluation-validation/v1", + valid, + mergeAllowed, + evaluationDigest, + verdict: evaluation?.verdict || null, + diagnostics, +}; +writeResult(options, envelope, markdownReport(valid, evaluation, diagnostics, evaluationDigest)); +process.exit(mergeAllowed ? 0 : 1); +TYPESCRIPT