diff --git a/.github/scripts/build_codex_github_review.py b/.github/scripts/build_codex_github_review.py new file mode 100644 index 0000000..ae97c4c --- /dev/null +++ b/.github/scripts/build_codex_github_review.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Build a GitHub review while preserving findings without valid diff anchors.""" + +from __future__ import annotations + +import argparse +import json +import os +import posixpath +import re +import subprocess +from pathlib import Path + + +HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@") + + +def normalize_path(value: str, workspace: str) -> str | None: + candidate = value.replace("\\", "/") + root = workspace.replace("\\", "/").rstrip("/") + if candidate.startswith(root + "/"): + candidate = candidate[len(root) + 1 :] + while candidate.startswith("./"): + candidate = candidate[2:] + candidate = posixpath.normpath(candidate) + if ( + not candidate + or candidate == "." + or candidate.startswith("/") + or candidate == ".." + or candidate.startswith("../") + or "/../" in candidate + ): + return None + return candidate + + +def header_path(line: str) -> str | None: + value = line[4:].split("\t", 1)[0] + if value == "/dev/null": + return None + if value.startswith(("a/", "b/")): + value = value[2:] + return value + + +def changed_lines(diff: str) -> dict[str, set[tuple[str, int]]]: + allowed: dict[str, set[tuple[str, int]]] = {"LEFT": set(), "RIGHT": set()} + old_path: str | None = None + new_path: str | None = None + old_line = 0 + new_line = 0 + in_hunk = False + for line in diff.splitlines(): + if line.startswith("diff --git "): + old_path = new_path = None + in_hunk = False + continue + if not in_hunk and line.startswith("--- "): + old_path = header_path(line) + continue + if not in_hunk and line.startswith("+++ "): + new_path = header_path(line) + continue + match = HUNK.match(line) + if match: + old_line = int(match.group(1)) + new_line = int(match.group(3)) + in_hunk = True + continue + if not in_hunk or line.startswith("\\"): + continue + if line.startswith("-"): + if old_path is not None: + allowed["LEFT"].add((old_path, old_line)) + old_line += 1 + elif line.startswith("+"): + if new_path is not None: + allowed["RIGHT"].add((new_path, new_line)) + new_line += 1 + elif line.startswith(" "): + old_line += 1 + new_line += 1 + else: + in_hunk = False + return allowed + + +def finding_body(finding: dict[str, object]) -> str: + return ( + f"[P{finding['priority']}] {finding['title']}\n\n" + f"{finding['body']}\n\nConfidence: {finding['confidence_score']}" + ) + + +def build_review( + review: dict[str, object], + *, + commit: str, + workspace: str, + allowed: dict[str, set[tuple[str, int]]], +) -> dict[str, object]: + body = ( + "Codex automated review\n\n" + f"Verdict: {review['overall_correctness']}\n" + f"Confidence: {review['overall_confidence_score']}\n\n" + f"{review['overall_explanation']}" + ) + comments: list[dict[str, object]] = [] + unanchored: list[str] = [] + for finding in review["findings"]: # type: ignore[index] + location = finding["code_location"] + path = normalize_path(location["absolute_file_path"], workspace) + side = location["side"] + start = location["line_range"]["start"] + end = location["line_range"]["end"] + valid_anchor = ( + path is not None + and side in allowed + and start <= end + and all((path, line) in allowed[side] for line in range(start, end + 1)) + ) + text = finding_body(finding) + if valid_anchor: + comment: dict[str, object] = { + "path": path, + "line": end, + "side": side, + "body": text, + } + if start != end: + comment["start_line"] = start + comment["start_side"] = side + comments.append(comment) + continue + display_path = path or "unresolved-path" + unanchored.append(f"{text}\n\nLocation: {display_path}:{start}-{end} ({side})") + if unanchored: + body += "\n\nFindings without inline diff anchors\n\n" + "\n\n---\n\n".join(unanchored) + return {"commit_id": commit, "event": "COMMENT", "body": body, "comments": comments} + + +def pull_request_diff(workspace: str, base: str, head: str) -> str: + merge_base = subprocess.run( + ["git", "merge-base", base, head], + cwd=workspace, + check=True, + text=True, + capture_output=True, + ).stdout.strip() + return subprocess.run( + [ + "git", + "-c", + "core.quotePath=false", + "diff", + "--no-ext-diff", + "--no-renames", + "--unified=0", + merge_base, + head, + ], + cwd=workspace, + check=True, + text=True, + capture_output=True, + ).stdout + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--input", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--workspace", required=True) + parser.add_argument("--base", required=True) + parser.add_argument("--head", required=True) + args = parser.parse_args() + diff = pull_request_diff(args.workspace, args.base, args.head) + review = json.loads(Path(args.input).read_text()) + payload = build_review( + review, + commit=args.head, + workspace=os.path.abspath(args.workspace), + allowed=changed_lines(diff), + ) + Path(args.output).write_text(json.dumps(payload, indent=2) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/tests/boatstack_test_support.py b/.github/tests/boatstack_test_support.py index 3e3ed0b..ff5c56b 100644 --- a/.github/tests/boatstack_test_support.py +++ b/.github/tests/boatstack_test_support.py @@ -14,12 +14,16 @@ def prescription_cli_arguments( correlation, "--prescription-id", str(prescription["id"]), + "--expected-instance-id", + str(prescription["expected_instance_id"]), "--expected-state-revision", str(prescription["expected_state_revision"]), "--expected-program-fingerprint", str(prescription["expected_program_fingerprint"]), "--expected-snapshot-fingerprint", str(prescription["expected_snapshot_fingerprint"]), + "--expected-objective-binding-fingerprint", + str(prescription["expected_objective_binding_fingerprint"]), "--authority-fingerprint", str(prescription["authority_fingerprint"]), ] diff --git a/.github/tests/test_codex_review_publish.py b/.github/tests/test_codex_review_publish.py new file mode 100644 index 0000000..df6bc87 --- /dev/null +++ b/.github/tests/test_codex_review_publish.py @@ -0,0 +1,112 @@ +import importlib.util +import subprocess +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).parents[1] / "scripts" / "build_codex_github_review.py" +SPEC = importlib.util.spec_from_file_location("build_codex_github_review", SCRIPT) +MODULE = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(MODULE) + + +class CodexReviewPublishTests(unittest.TestCase): + def test_invalid_inline_location_is_preserved_in_review_body(self) -> None: + diff = """diff --git a/review.go b/review.go +--- a/review.go ++++ b/review.go +@@ -2 +2 @@ +-old ++new +""" + review = { + "findings": [ + self.finding("anchored", 2), + self.finding("not changed", 9), + ], + "overall_correctness": "patch is incorrect", + "overall_explanation": "One location is outside the pull request diff.", + "overall_confidence_score": 0.9, + } + payload = MODULE.build_review( + review, + commit="head", + workspace="/workspace", + allowed=MODULE.changed_lines(diff), + ) + self.assertEqual(len(payload["comments"]), 1) + self.assertEqual(payload["comments"][0]["line"], 2) + self.assertIn("Findings without inline diff anchors", payload["body"]) + self.assertIn("not changed", payload["body"]) + self.assertIn("review.go:9-9 (RIGHT)", payload["body"]) + + def test_outside_workspace_path_cannot_become_inline_comment(self) -> None: + review = { + "findings": [self.finding("outside", 2, path="/tmp/other.go")], + "overall_correctness": "patch is incorrect", + "overall_explanation": "The path is not repository-relative.", + "overall_confidence_score": 0.8, + } + payload = MODULE.build_review( + review, + commit="head", + workspace="/workspace", + allowed={"LEFT": set(), "RIGHT": {("review.go", 2)}}, + ) + self.assertEqual(payload["comments"], []) + self.assertIn("unresolved-path", payload["body"]) + + def test_pull_request_diff_excludes_base_only_changes(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repository = Path(directory) + self.git(repository, "init", "-q") + self.git(repository, "config", "user.email", "review@example.invalid") + self.git(repository, "config", "user.name", "Review Test") + (repository / "base.txt").write_text("original\n") + (repository / "head.txt").write_text("original\n") + self.git(repository, "add", ".") + self.git(repository, "commit", "-q", "-m", "initial") + common = self.git(repository, "rev-parse", "HEAD") + + (repository / "base.txt").write_text("base only\n") + self.git(repository, "commit", "-q", "-am", "base") + base = self.git(repository, "rev-parse", "HEAD") + + self.git(repository, "checkout", "-q", "--detach", common) + (repository / "head.txt").write_text("head only\n") + self.git(repository, "commit", "-q", "-am", "head") + head = self.git(repository, "rev-parse", "HEAD") + + diff = MODULE.pull_request_diff(str(repository), base, head) + self.assertIn("head.txt", diff) + self.assertNotIn("base.txt", diff) + + @staticmethod + def finding(title: str, line: int, path: str = "/workspace/review.go") -> dict: + return { + "title": title, + "body": "Finding detail.", + "confidence_score": 0.95, + "priority": 1, + "code_location": { + "absolute_file_path": path, + "side": "RIGHT", + "line_range": {"start": line, "end": line}, + }, + } + + @staticmethod + def git(repository: Path, *args: str) -> str: + return subprocess.run( + ["git", *args], + cwd=repository, + check=True, + text=True, + capture_output=True, + ).stdout.strip() + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/tests/test_detached_supervision.py b/.github/tests/test_detached_supervision.py index 5cc689b..602cec9 100644 --- a/.github/tests/test_detached_supervision.py +++ b/.github/tests/test_detached_supervision.py @@ -116,16 +116,16 @@ def porcelain(self, repository: Path | None = None) -> str: ).stdout.strip() @staticmethod - def goal_flags() -> tuple[str, ...]: + def objective_flags() -> tuple[str, ...]: return ( - "--goal-id", "bootstrap", "--goal-kind", "approved-plan", + "--objective-id", "bootstrap", "--objective-kind", "approved-plan", "--delivery", "bootstrap", ) def attach(self, repository: Path | None = None) -> dict: repository = repository or self.repo return self.helper_json( - "attach", "--repo", repository, *self.goal_flags(), "--human", "contract", + "attach", "--repo", repository, *self.objective_flags(), "--human", "contract", "--param", "topology=detached", "--param", "config_authority=repository", cwd=repository, ) @@ -142,7 +142,7 @@ def test_attach_and_detach_transfer_only_controller_authority(self) -> None: self.assertEqual(binding["topology"], "detached") detached = self.helper_json( - "detach", "--repo", self.repo, *self.goal_flags(), "--human", "contract" + "detach", "--repo", self.repo, "--human", "contract" ) self.assertEqual(detached["snapshot"]["invocation"]["topology"], "embedded") self.assertEqual(detached["receipt"]["transition_id"], "repository.detach") @@ -215,13 +215,13 @@ def test_detached_installation_and_engaged_guard_use_the_same_kernel(self) -> No ) self.apply_prescribed( - "goal.configure", "--repo", self.repo, - *self.goal_flags(), "--human", "contract", - "--param", "goal_kind=approved-plan", "--param", "delivery_id=bootstrap", + "objective.bind", "--repo", self.repo, + *self.objective_flags(), "--human", "contract", + "--param", "objective_kind=approved-plan", "--param", "delivery_id=bootstrap", ) self.apply_prescribed( "engagement.begin", "--repo", self.repo, - *self.goal_flags(), "--repository-authority", + *self.objective_flags(), "--repository-authority", ) ordinary = self.helper_json( "guard", "--repo", self.repo, "--command", "go test ./..." @@ -240,14 +240,14 @@ def test_detached_installation_and_engaged_guard_use_the_same_kernel(self) -> No def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) -> None: # control-law: codex-mode-authority-survives-observation-and-effects - goal = ( - "--goal-id", "codex-driver-authority-triggers", - "--goal-kind", "open-or-updated-pr", + objective = ( + "--objective-id", "codex-driver-authority-triggers", + "--objective-kind", "open-or-updated-pr", "--delivery", "codex-driver-authority-triggers", ) flow = ("--flow", "flow-codex-driver-authority-triggers") self.helper_json( - "attach", "--repo", self.repo, *goal, *flow, "--human", "contract", + "attach", "--repo", self.repo, *objective, *flow, "--human", "contract", "--param", "topology=detached", "--param", "config_authority=repository", ) config = Path(self.work.name) / "driver-project.json" @@ -262,28 +262,28 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - ) ) self.helper_json( - "init", "--repo", self.repo, *goal, *flow, "--human", "contract", + "init", "--repo", self.repo, *objective, *flow, "--human", "contract", "--param", f"config_path={config}", ) self.apply_prescribed( - "goal.configure", "--repo", self.repo, - *goal, *flow, "--human", "contract", - "--param", "goal_kind=open-or-updated-pr", + "objective.bind", "--repo", self.repo, + *objective, *flow, "--human", "contract", + "--param", "objective_kind=open-or-updated-pr", "--param", "delivery_id=codex-driver-authority-triggers", ) self.apply_prescribed( "engagement.begin", "--repo", self.repo, - *goal, *flow, "--repository-authority", + *objective, *flow, "--repository-authority", ) before = self.porcelain() - diagnostic = self.helper_json("status", "--repo", self.repo, *goal, *flow) + diagnostic = self.helper_json("status", "--repo", self.repo, *objective, *flow) self.assertEqual(diagnostic["decision"]["kind"], "FRONTIER") self.assertIn("plan.create", diagnostic["decision"]["candidates"]) self.assertEqual(self.porcelain(), before) progressing = self.helper_json( - "next", "--repo", self.repo, *goal, *flow, + "next", "--repo", self.repo, *objective, *flow, "--human", "contract", "--repository-authority", ) self.assertEqual(progressing["decision"]["kind"], "CANDIDATE") @@ -297,13 +297,13 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - ) prescribed = self.helper_json( "next", "--repo", self.repo, "--transition", "plan.create", - *goal, *flow, "--human", "contract", *parameters, + *objective, *flow, "--human", "contract", *parameters, ) self.assertEqual(prescribed["decision"]["kind"], "PRESCRIBED") self.assertEqual(prescribed["decision"]["transition"]["id"], "plan.create") applied_process = self.run_helper( - "plan-create", "--repo", self.repo, *goal, *flow, + "plan-create", "--repo", self.repo, *objective, *flow, "--human", "contract", *parameters, ) applied = json.loads(applied_process.stdout) @@ -323,27 +323,27 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - self.assertIn(field, applied_process.stdout) resolved = self.helper_json( - "next", "--repo", self.repo, *goal, *flow, "--repository-authority", + "next", "--repo", self.repo, *objective, *flow, "--repository-authority", ) self.assertEqual(resolved["decision"]["kind"], "PRESCRIBED") self.assertEqual(resolved["decision"]["transition"]["id"], "plan.validate") def test_one_delivery_context_rematerializes_repository_authority_after_initialization(self) -> None: # control-law: retained-repository-source-crosses-maintenance-receipt-once - goal = ( - "--goal-id", "preserve-repository-authority-context", - "--goal-kind", "open-or-updated-pr", + objective = ( + "--objective-id", "preserve-repository-authority-context", + "--objective-kind", "open-or-updated-pr", "--delivery", "preserve-repository-authority-context", ) flow = ("--flow", "flow-preserve-repository-authority-context") actor = ("--human", "contract") self.helper_json( - "attach", "--repo", self.repo, *goal, *flow, *actor, + "attach", "--repo", self.repo, *objective, *flow, *actor, "--param", "topology=detached", "--param", "config_authority=repository", ) prescribed = self.helper_json( - "next", "--repo", self.repo, *goal, *flow, *actor, + "next", "--repo", self.repo, *objective, *flow, *actor, ) self.assertEqual(prescribed["decision"]["kind"], "CANDIDATE") self.assertEqual( @@ -375,7 +375,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali ).hexdigest() bound_initialization = self.helper_json( "next", "--repo", self.repo, - "--transition", "installation.initialize", *goal, *flow, *actor, + "--transition", "installation.initialize", *objective, *flow, *actor, "--param", f"source_revision={self._git(self.repo, 'rev-parse', 'HEAD').stdout.strip()}", "--param", f"runtime_version={self.run_helper('version').stdout.strip()}", "--param", f"runtime_sha256={hashlib.sha256(self.binary.read_bytes()).hexdigest()}", @@ -388,7 +388,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali "installation.initialize", ) initialized_process = self.run_helper( - "init", "--repo", self.repo, *goal, *flow, *actor, + "init", "--repo", self.repo, *objective, *flow, *actor, "--param", f"config_path={config}", ) initialized = json.loads(initialized_process.stdout) @@ -399,15 +399,15 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali self.assertIn(field, initialized_process.stdout) configured = self.apply_prescribed( - "goal.configure", "--repo", self.repo, - *goal, *flow, *actor, - "--param", "goal_kind=open-or-updated-pr", + "objective.bind", "--repo", self.repo, + *objective, *flow, *actor, + "--param", "objective_kind=open-or-updated-pr", "--param", "delivery_id=preserve-repository-authority-context", ) - self.assertEqual(configured["receipt"]["transition_id"], "goal.configure") + self.assertEqual(configured["receipt"]["transition_id"], "objective.bind") engagement = self.helper_json( - "next", "--repo", self.repo, *goal, *flow, *actor, + "next", "--repo", self.repo, *objective, *flow, *actor, "--repository-authority", ) self.assertEqual(engagement["decision"]["kind"], "PRESCRIBED") @@ -415,7 +415,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali engaged = self.apply_prescribed( "engagement.begin", "--repo", self.repo, - *goal, *flow, *actor, "--repository-authority", + *objective, *flow, *actor, "--repository-authority", ) self.assertEqual(engaged["receipt"]["transition_id"], "engagement.begin") self.assertEqual(engaged["receipt"]["flow_id"], flow[1]) @@ -428,7 +428,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali self.assertIn(field, engaged_output) plan = self.helper_json( - "next", "--repo", self.repo, *goal, *flow, *actor, + "next", "--repo", self.repo, *objective, *flow, *actor, "--repository-authority", ) self.assertEqual(plan["decision"]["kind"], "CANDIDATE") @@ -438,7 +438,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali plan_source.write_text("# Retained authority\n\nContinue in one operation context.\n") bound = self.helper_json( "next", "--repo", self.repo, "--transition", "plan.create", - *goal, *flow, *actor, "--repository-authority", + *objective, *flow, *actor, "--repository-authority", "--param", f"source_path={plan_source}", "--param", "delivery_id=preserve-repository-authority-context", ) @@ -458,8 +458,8 @@ def test_repository_authority_rematerialization_fails_closed_without_verified_co before = self.porcelain(root) result = self.run_helper( "next", "--repo", root, - "--goal-id", "unverified-authority", - "--goal-kind", "open-or-updated-pr", + "--objective-id", "unverified-authority", + "--objective-kind", "open-or-updated-pr", "--delivery", "unverified-authority", "--flow", "flow-unverified-authority", "--human", "contract", "--repository-authority", diff --git a/.github/tests/test_prescription_projection.py b/.github/tests/test_prescription_projection.py index b23846b..9b5379c 100644 --- a/.github/tests/test_prescription_projection.py +++ b/.github/tests/test_prescription_projection.py @@ -11,9 +11,11 @@ class PrescriptionProjectionContract(unittest.TestCase): def setUp(self) -> None: self.prescription = { "id": "prx-example", + "expected_instance_id": "repo-example", "expected_state_revision": 41, "expected_program_fingerprint": "a" * 64, "expected_snapshot_fingerprint": "snapshot-example", + "expected_objective_binding_fingerprint": "objective-binding-example", "authority_fingerprint": "auth-example", "required_capabilities": ["command.execute", "repository.write"], "effective_capabilities": ["command.execute", "repository.write"], @@ -25,9 +27,11 @@ def test_projection_preserves_complete_current_identity(self) -> None: ( "--correlation", "correlation-example", "--prescription-id", "prx-example", + "--expected-instance-id", "repo-example", "--expected-state-revision", "41", "--expected-program-fingerprint", "a" * 64, "--expected-snapshot-fingerprint", "snapshot-example", + "--expected-objective-binding-fingerprint", "objective-binding-example", "--authority-fingerprint", "auth-example", "--required-capability", "command.execute", "--required-capability", "repository.write", diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py index 0358c3e..2f23e33 100644 --- a/.github/tests/test_repository_contract.py +++ b/.github/tests/test_repository_contract.py @@ -167,7 +167,10 @@ def test_codex_review_is_secret_scoped_read_only_and_structured(self) -> None: location = finding["properties"]["code_location"] self.assertIn("side", location["required"]) self.assertEqual(location["properties"]["side"]["enum"], ["LEFT", "RIGHT"]) - self.assertIn("side: .code_location.side", workflow) + self.assertIn("build_codex_github_review.py", workflow) + publisher = (REPO / ".github" / "scripts" / "build_codex_github_review.py").read_text() + self.assertIn('"side": side', publisher) + self.assertIn("Findings without inline diff anchors", publisher) self.assertFalse((REPO / "UPSTREAM.json").exists()) def test_release_builds_six_checksum_bound_v2_runtimes(self) -> None: @@ -279,7 +282,7 @@ def test_operation_skills_are_three_distinct_authority_preserving_surfaces(self) "carry the same human authority\nthrough that rollback", ): self.assertIn(contract, update) - self.assertIn("Untargeted resolution selects\nonly a transition that advances the configured goal", readme) + self.assertIn("Untargeted resolution selects\nonly a transition that advances the configured objective", readme) self.assertIn("exactly three operation skills", readme) def test_document_links_claims_and_assets_are_valid(self) -> None: @@ -367,6 +370,53 @@ def test_public_tree_excludes_private_context_and_v1_operating_guidance(self) -> for token in deprecated: self.assertNotIn(token, value, document) + def test_general_kernel_is_domain_neutral_and_owns_shared_control_laws(self) -> None: + kernel = REPO / "boatstack" / "kernel" + source = "\n".join(path.read_text() for path in sorted(kernel.glob("*.go"))) + for token in ( + "git", "repository", "worktree", "branch", "pull request", + "coding agent", "publication", + ): + self.assertNotIn(token, source.lower(), token) + + runtime = (kernel / "runtime.go").read_text() + software_relation = ( + REPO / "boatstack" / "internal" / "softwaredelivery" / + "supervisor" / "supervisor.go" + ).read_text() + software_prescription = ( + REPO / "boatstack" / "internal" / "softwaredelivery" / + "protocol" / "prescription.go" + ).read_text() + self.assertIn("Relate(RelationInput", runtime) + self.assertIn("general.Relate(general.RelationInput", software_relation) + self.assertIn("general.Freshness", software_prescription) + self.assertIn("general.NewFreshness", software_prescription) + + implementation = "\n".join( + path.read_text() for path in sorted((REPO / "boatstack").rglob("*.go")) + ) + for retired in ( + "boatstack/control", "internal/kernel", "internal/effects", + "internal/plant", "internal/surfaces", "goal.configure", + "GOAL_REQUIRED", 'json:"goal', + ): + self.assertNotIn(retired, implementation) + + component_ci = (REPO / ".github" / "workflows" / "ci.yml").read_text() + for current in ( + "./kernel", "./delivery", "./internal/softwaredelivery/protocol", + "./internal/softwaredelivery/surfaces", + "./internal/softwaredelivery/plant", + "./internal/softwaredelivery/effects", + ): + self.assertIn(current, component_ci) + for retired in ( + "./control", "./internal/kernel", "./internal/plant", + "./internal/effects", "./internal/surfaces", + ): + self.assertNotIn(retired, component_ci) + def test_documented_cli_verbs_are_registered_v2_surfaces(self) -> None: documents = [ REPO / "README.md", @@ -379,7 +429,7 @@ def test_documented_cli_verbs_are_registered_v2_surfaces(self) -> None: "events", "catalog", "guard", "rpc", "retro", "version", "init", "update", "attach", "detach", "hydrate-runtime", "configure", "reconcile-update", - "goal-configure", "plan-create", "plan-validate", "plan-approve", + "objective-bind", "plan-create", "plan-validate", "plan-approve", "plan-activate", "plan-amend", "workspace-cut", "workspace-sync", "workspace-cleanup", "workspace-reap", "record-build", "record-test", "record-review", "record-change", "record-journey", @@ -561,17 +611,17 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) - self.assertEqual(doctor["doctor"]["transition_count"], 63) self.assertEqual(doctor["snapshot"]["runtime"]["value"], "verified") - goal = ( - "--goal-id", "bootstrap", "--goal-kind", "approved-plan", + objective = ( + "--objective-id", "bootstrap", "--objective-kind", "approved-plan", "--delivery", "bootstrap", ) self.apply_prescribed( - launcher, "goal.configure", "--repo", repository, *goal, - "--human", "contract", "--param", "goal_kind=approved-plan", + launcher, "objective.bind", "--repo", repository, *objective, + "--human", "contract", "--param", "objective_kind=approved-plan", "--param", "delivery_id=bootstrap", env=env, ) self.apply_prescribed( - launcher, "engagement.begin", "--repo", repository, *goal, + launcher, "engagement.begin", "--repo", repository, *objective, "--repository-authority", env=env, ) ordinary = json.loads( @@ -677,9 +727,9 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No "catalog.reconcile", "--human", "contract", - "--goal-id", + "--objective-id", "bootstrap", - "--goal-kind", + "--objective-kind", "approved-plan", "--delivery", "bootstrap", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16c9d2a..8397274 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,17 +21,17 @@ jobs: matrix: include: - name: kernel-mechanism - packages: ./internal/kernel/model ./internal/kernel/catalog ./internal/kernel/supervisor ./internal/kernel/engine + packages: ./kernel - name: control-program-compiler - packages: ./control ./core + packages: ./delivery ./core - name: standard-flow - packages: ./flow/standard ./internal/kernel/protocol + packages: ./flow/standard ./internal/softwaredelivery/protocol - name: extension-conformance packages: ./extension/... ./distribution - name: surface-parity - packages: ./internal/surfaces ./sdk ./cmd/boatstack-helper + packages: ./internal/softwaredelivery/surfaces ./sdk ./cmd/boatstack-helper - name: plant-integration - packages: ./internal/plant ./internal/effects + packages: ./internal/softwaredelivery/plant ./internal/softwaredelivery/effects ./internal/softwaredelivery/engine runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/codex-review.yml b/.github/workflows/codex-review.yml index 1c53ce6..ec8d072 100644 --- a/.github/workflows/codex-review.yml +++ b/.github/workflows/codex-review.yml @@ -131,28 +131,23 @@ jobs: (.overall_confidence_score >= 0 and .overall_confidence_score <= 1) ' "$RUNNER_TEMP/codex-review-output.json" >/dev/null + - name: Build GitHub review payload + if: steps.configuration.outputs.enabled == 'true' && steps.policy.outputs.enabled == 'true' + shell: bash + run: | + python3 .github/scripts/build_codex_github_review.py \ + --input "$RUNNER_TEMP/codex-review-output.json" \ + --output "$RUNNER_TEMP/codex-github-review.json" \ + --workspace "$GITHUB_WORKSPACE" \ + --base "$BASE_SHA" \ + --head "$HEAD_SHA" + - name: Publish GitHub review if: steps.configuration.outputs.enabled == 'true' && steps.policy.outputs.enabled == 'true' env: GH_TOKEN: ${{ github.token }} shell: bash run: | - jq \ - --arg commit "$HEAD_SHA" \ - --arg workspace "$GITHUB_WORKSPACE" \ - '{ - commit_id: $commit, - event: "COMMENT", - body: ("Codex automated review\n\nVerdict: " + .overall_correctness + "\nConfidence: " + (.overall_confidence_score | tostring) + "\n\n" + .overall_explanation), - comments: [.findings[] | { - path: (.code_location.absolute_file_path | ltrimstr($workspace + "/") | ltrimstr("./")), - line: .code_location.line_range.end, - side: .code_location.side, - start_line: (if .code_location.line_range.start == .code_location.line_range.end then null else .code_location.line_range.start end), - start_side: (if .code_location.line_range.start == .code_location.line_range.end then null else .code_location.side end), - body: ("[P" + (.priority | tostring) + "] " + .title + "\n\n" + .body + "\n\nConfidence: " + (.confidence_score | tostring)) - } | with_entries(select(.value != null))] - }' "$RUNNER_TEMP/codex-review-output.json" > "$RUNNER_TEMP/codex-github-review.json" jq -e 'all(.comments[]; ((.path | length) > 0) and (((.path | startswith("/")) or (.path | startswith("../")) or (.path | contains("/../"))) | not) and diff --git a/README.md b/README.md index aa1bf8d..636b579 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Alpha · active development · expect breaking changes

-Boatstack turns software delivery into a controlled state-transition system. -An agent can propose what happens next; a deterministic Kernel decides what is -admissible, executes registered effects, verifies the result from fresh -evidence, and records a durable receipt. +Boatstack is a programmable supervisory runtime over state-changing operators. +Software delivery is its first production domain: an agent can propose what +happens next, while a deterministic kernel decides what is admissible, executes +registered effects, verifies fresh evidence, and records a durable receipt. ```text agent intent @@ -79,9 +79,10 @@ control graph. The complete list is generated from the registry in the | Surface | Shipped functionality | | --- | --- | -| **Control Programs** | One immutable `ControlProgram` compiled from the CoreSystem, one Program Runtime, and optional conservative extensions. Canonical fingerprints bind executable semantics, goal contracts, resource ownership, and program-qualified transition IDs. | -| **StandardFlow** | A first-party product-delivery Flow covering installation, repository attachment, configuration, goals, planning, worktrees, build/test/review evidence, publication, cleanup, and recovery. | -| **Deterministic supervisor** | Targeted and untargeted resolution, explicit terminal goals, transition priorities, prerequisite selection, and typed `PRESCRIBED`, `CANDIDATE`, `FRONTIER`, `BLOCKED`, `REFUSED`, `UNRESOLVED`, and `TERMINAL` decisions. | +| **General kernel** | Domain-neutral programs, control instances, objective bindings, observations, capabilities, operators, verification, recovery, marked states, and receipts. The kernel does not require Git or a coding agent. | +| **Control Programs** | One immutable program fingerprint binds executable semantics, objective contracts, resource ownership, capabilities, and transition IDs. | +| **StandardFlow** | A first-party product-delivery Flow covering installation, repository attachment, configuration, objectives, planning, worktrees, build/test/review evidence, publication, cleanup, and recovery. | +| **Deterministic supervisor** | Targeted and untargeted resolution, explicit accepted objectives, transition priorities, prerequisite selection, and typed `PRESCRIBED`, `CANDIDATE`, `FRONTIER`, `BLOCKED`, `REFUSED`, `UNRESOLVED`, and `TERMINAL` decisions. | | **Authority and capabilities** | Separate human, autonomy, repository-policy, and external-provider receipts. Programs declare a maximum capability surface but cannot grant themselves authority. | | **Transactional effects** | Prescriptions bind the exact state revision, program fingerprint, snapshot fingerprint, transition, and correlation. Apply rechecks that compare-and-swap boundary under a repository lock before any managed effect. | | **Durable state ownership** | Installation, program, control, and product state are separate facets. A transition fails closed if it attempts to mutate a facet outside its Kernel-owned policy. | @@ -102,7 +103,7 @@ The public protocol is deliberately small: ```sh # Observe or resolve. These commands do not mutate managed state. boatstack status --repo . --format json -boatstack next --repo . --goal-id --goal-kind \ +boatstack next --repo . --objective-id --objective-kind \ --delivery --format json # Inspect the exact program and transition surface. @@ -122,10 +123,10 @@ commands such as `plan-create`, `workspace-cut`, `record-test`, and `publish-pr` resolve and consume one exact prescription in the same invocation. Untargeted resolution selects -only a transition that advances the configured goal. Maintenance, correction, +only a transition that advances the configured objective. Maintenance, correction, abandonment, provider actions, and merge authority are never invented as a way around a frontier. After an operation is selected, generated host drivers keep -one command-scoped goal, repository, worktree, flow, actor, and authority +one command-scoped objective, repository, worktree, flow, actor, and authority context through every resolution, effect, recovery, and re-resolution. ## Internals @@ -139,29 +140,33 @@ Boatstack separates inference, control, execution, and verification: └──────────────────────────────┬───────────────────────────────┘ │ versioned request ┌──────────────────────────────▼───────────────────────────────┐ -│ Kernel │ -│ observe → snapshot → supervise → admit → execute → verify │ +│ General kernel │ +│ observe → relate → prescribe → admit → execute → verify │ └───────────────┬──────────────────────────────┬───────────────┘ │ │ ┌───────────────▼──────────────┐ ┌────────────▼───────────────┐ -│ Control Program │ │ Plant and effect boundary │ -│ Core + Flow + extensions │ │ Git · files · processes │ -│ transitions · goals · laws │ │ provider outcomes │ +│ Control Program │ │ Software-delivery domain │ +│ transitions · objectives │ │ Git · files · processes │ +│ laws · marked states │ │ plans · tests · PRs │ └──────────────────────────────┘ └────────────────────────────┘ ``` -The Kernel owns mechanism. A Control Program owns delivery policy. The product -calls a complete Control Program a **Flow**; the rules encoded by it are its -**control law**. +The kernel owns mechanism. A Control Program owns policy. The product calls a +complete Control Program a **Flow**; the rules encoded by it are its **control +law**. See the [general kernel boundary](docs/architecture/general-supervisory-kernel.md). The current authoring boundary already includes: - a strict JSON [Control Program ABI](docs/architecture/control-program-abi.md); -- public Go contracts in `boatstack/control`; -- `sdk.New(...)` for StandardFlow and `sdk.NewKernel(...)` for an explicit +- the domain-neutral Go runtime in `boatstack/kernel`; +- software-delivery contracts in `boatstack/delivery`; +- `sdk.New(...)` for StandardFlow and `sdk.NewProgramClient(...)` for an explicit trusted Program Runtime; - canonical program identity and runtime compatibility checks; -- program-qualified transitions, goal contracts, resource ownership, +- one kernel Program fingerprint binding the complete software-domain ABI; +- one transition relation and freshness envelope shared by the generic runtime + and the software-delivery adapter; +- program-qualified transitions, objective contracts, resource ownership, capabilities, effects, verifiers, recovery, and context predicates; - a protocol execution boundary for repository-authored transitions. @@ -192,12 +197,12 @@ for the exact contracts. ## Repository map ```text -boatstack/control/ Control Program authoring and compilation -boatstack/core/ Kernel-owned operational transitions +boatstack/kernel/ Domain-neutral supervisory runtime +boatstack/delivery/ Software-delivery program contracts +boatstack/core/ Software-delivery operational transitions boatstack/flow/standard/ First-party StandardFlow -boatstack/internal/kernel/ model, catalog, supervisor, admission, engine -boatstack/internal/plant/ read-only repository observation -boatstack/internal/effects/ transactional effects, receipts, and recovery +boatstack/internal/softwaredelivery/ + repository model, observation, effects, and recovery boatstack/internal/runtime/ immutable runtime selection and dispatch boatstack/sdk/ public Go protocol client docs/architecture/ executable contracts and generated evidence diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 190024f..5a32bd7 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -20,11 +20,12 @@ import ( "github.com/operatorstack/boatstack/boatstack/analysis" "github.com/operatorstack/boatstack/boatstack/distribution" "github.com/operatorstack/boatstack/boatstack/internal/buildinfo" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" + general "github.com/operatorstack/boatstack/boatstack/kernel" ) type stringList []string @@ -36,30 +37,32 @@ func (s *stringList) Set(value string) error { } type commandOptions struct { - repository string - format string - goalID string - goalKind string - deliveryID string - flowID string - transitionID string - correlationID string - prescriptionID string - expectedStateRevision uint64 - expectedProgramFingerprint string - expectedSnapshotFingerprint string - authorityFingerprint string - requiredCapabilities stringList - effectiveCapabilities stringList - idempotencyKey string - humanActor string - repositoryPolicy bool - acceptProgramChange bool - parameters stringList - authorityReceipts stringList - follow bool - host string - command string + repository string + format string + objectiveID string + objectiveKind string + deliveryID string + flowID string + transitionID string + correlationID string + prescriptionID string + expectedInstanceID string + expectedStateRevision uint64 + expectedProgramFingerprint string + expectedSnapshotFingerprint string + expectedObjectiveBindingFingerprint string + authorityFingerprint string + requiredCapabilities stringList + effectiveCapabilities stringList + idempotencyKey string + humanActor string + repositoryPolicy bool + acceptProgramChange bool + parameters stringList + authorityReceipts stringList + follow bool + host string + command string } func main() { @@ -207,7 +210,7 @@ func runRetrospective(arguments []string) error { func classifyCommand(command string) (surfaces.Operation, catalog.TransitionID, map[string]string, error) { aliases := map[string]catalog.TransitionID{ "init": "installation.initialize", "update": "installation.update", "reconcile-update": "installation.reconcile-update", "attach": "repository.attach", "detach": "repository.detach", - "hydrate-runtime": "runtime.hydrate", "configure": "configuration.mutate", "goal-configure": "goal.configure", + "hydrate-runtime": "runtime.hydrate", "configure": "configuration.mutate", "objective-bind": "objective.bind", "plan-create": "plan.create", "plan-validate": "plan.validate", "plan-approve": "plan.approve", "plan-activate": "plan.activate", "plan-amend": "plan.amend", "workspace-cut": "workspace.cut", "workspace-sync": "workspace.sync", "workspace-cleanup": "workspace.cleanup", "workspace-reap": "workspace.reap", "record-build": "gate.build.record", "record-test": "gate.test.record", "record-review": "gate.review.record", "record-change": "gate.change.record", "record-journey": "gate.journey.record", @@ -241,20 +244,22 @@ func parseOptions(command string, arguments []string, transition catalog.Transit flags.SetOutput(os.Stderr) options := commandOptions{format: "json", transitionID: string(transition), host: "cli"} if defaults != nil { - options.goalKind, options.deliveryID, options.goalID = defaults["goal-kind"], defaults["delivery"], defaults["goal-id"] + options.objectiveKind, options.deliveryID, options.objectiveID = defaults["objective-kind"], defaults["delivery"], defaults["objective-id"] } flags.StringVar(&options.repository, "repo", ".", "explicit invoking repository or worktree") flags.StringVar(&options.format, "format", options.format, "json, text, or jsonl") - flags.StringVar(&options.goalID, "goal-id", options.goalID, "configured goal identity") - flags.StringVar(&options.goalKind, "goal-kind", options.goalKind, "approved-plan, verified-implementation, open-or-updated-pr, merged-delivery, or safely-abandoned") + flags.StringVar(&options.objectiveID, "objective-id", options.objectiveID, "configured objective identity") + flags.StringVar(&options.objectiveKind, "objective-kind", options.objectiveKind, "approved-plan, verified-implementation, open-or-updated-pr, merged-delivery, or safely-abandoned") flags.StringVar(&options.deliveryID, "delivery", options.deliveryID, "delivery identity") flags.StringVar(&options.flowID, "flow", "", "flow identity") flags.StringVar(&options.transitionID, "transition", options.transitionID, "stable semantic transition id") flags.StringVar(&options.correlationID, "correlation", "", "command-scoped correlation identity from resolution") flags.StringVar(&options.prescriptionID, "prescription-id", "", "exact prescription identity from resolution") + flags.StringVar(&options.expectedInstanceID, "expected-instance-id", "", "exact control instance identity observed during resolution") flags.Uint64Var(&options.expectedStateRevision, "expected-state-revision", 0, "exact durable state revision observed during resolution") flags.StringVar(&options.expectedProgramFingerprint, "expected-program-fingerprint", "", "exact executable control-program fingerprint observed during resolution") flags.StringVar(&options.expectedSnapshotFingerprint, "expected-snapshot-fingerprint", "", "exact admission-relevant snapshot fingerprint observed during resolution") + flags.StringVar(&options.expectedObjectiveBindingFingerprint, "expected-objective-binding-fingerprint", "", "exact objective binding fingerprint observed during resolution") flags.StringVar(&options.authorityFingerprint, "authority-fingerprint", "", "exact authority projection fingerprint from resolution") flags.Var(&options.requiredCapabilities, "required-capability", "required capability from resolution (repeatable)") flags.Var(&options.effectiveCapabilities, "effective-capability", "effective capability from resolution (repeatable)") @@ -297,7 +302,7 @@ func parseOptions(command string, arguments []string, transition catalog.Transit return options, nil } -func standardKernel(ctx context.Context, request surfaces.Request) (boatstack.Kernel, error) { +func standardKernel(ctx context.Context, request surfaces.Request) (boatstack.DeliveryController, error) { programRequest := distribution.RepositoryProgramRequest{ Repository: request.Repository, Host: request.Host, CorrelationID: request.CorrelationID, } @@ -307,12 +312,12 @@ func standardKernel(ctx context.Context, request surfaces.Request) (boatstack.Ke } program, err := distribution.StandardProgramForRepository(ctx, programRequest) if err != nil { - return boatstack.Kernel{}, err + return boatstack.DeliveryController{}, err } - return boatstack.NewKernel("", program) + return boatstack.NewDeliveryController("", program) } -func followEvents(kernel boatstack.Kernel, request surfaces.Request) error { +func followEvents(kernel boatstack.DeliveryController, request surfaces.Request) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() encoder := json.NewEncoder(os.Stdout) @@ -439,10 +444,10 @@ func buildRequest(operation surfaces.Operation, options commandOptions) (surface if correlation == "" { correlation = fmt.Sprintf("cli-%d-%d", os.Getpid(), now.UnixNano()) } - goal := model.Goal{} - if options.goalKind != "" || options.goalID != "" || options.deliveryID != "" { - goal = model.Goal{ID: options.goalID, Kind: model.GoalKind(options.goalKind), DeliveryID: options.deliveryID} - if err := goal.Validate(); err != nil { + objective := model.Objective{} + if options.objectiveKind != "" || options.objectiveID != "" || options.deliveryID != "" { + objective = model.Objective{ID: options.objectiveID, Kind: model.ObjectiveKind(options.objectiveKind), DeliveryID: options.deliveryID} + if err := objective.Validate(); err != nil { return surfaces.Request{}, err } } @@ -450,7 +455,7 @@ func buildRequest(operation surfaces.Operation, options commandOptions) (surface if err != nil { return surfaces.Request{}, err } - authority, err := loadAuthority(options, correlation, goal, now) + authority, err := loadAuthority(options, correlation, objective, now) if err != nil { return surfaces.Request{}, err } @@ -463,19 +468,21 @@ func buildRequest(operation surfaces.Operation, options commandOptions) (surface return surfaces.Request{}, err } flowID := options.flowID - if flowID == "" && goal.ID != "" { - flowID = "flow-" + goal.ID + if flowID == "" && objective.ID != "" { + flowID = "flow-" + objective.ID } if flowID == "" && (operation == surfaces.OperationApply || operation == surfaces.OperationRecover) { flowID = "flow-" + correlation } return surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: operation, Repository: options.repository, Host: options.host, CorrelationID: correlation, - FlowID: flowID, Goal: goal, TransitionID: catalog.TransitionID(options.transitionID), Authority: authority, Parameters: parameters, + FlowID: flowID, Objective: objective, TransitionID: catalog.TransitionID(options.transitionID), Authority: authority, Parameters: parameters, Prescription: protocol.Prescription{SchemaVersion: protocol.PrescriptionSchemaVersion, ID: options.prescriptionID, - TransitionID: catalog.TransitionID(options.transitionID), ExpectedStateRevision: options.expectedStateRevision, - ExpectedProgramFingerprint: options.expectedProgramFingerprint, ExpectedSnapshotFingerprint: options.expectedSnapshotFingerprint, - AuthorityFingerprint: options.authorityFingerprint, RequiredCapabilities: requiredCapabilities, EffectiveCapabilities: effectiveCapabilities}, + TransitionID: catalog.TransitionID(options.transitionID), Freshness: general.Freshness{ + ExpectedInstanceID: options.expectedInstanceID, ExpectedStateRevision: options.expectedStateRevision, ExpectedProgramFingerprint: options.expectedProgramFingerprint, + ExpectedSnapshotFingerprint: options.expectedSnapshotFingerprint, ExpectedObjectiveBindingFingerprint: options.expectedObjectiveBindingFingerprint, + AuthorityFingerprint: options.authorityFingerprint, + }, RequiredCapabilities: requiredCapabilities, EffectiveCapabilities: effectiveCapabilities}, RepositoryAuthority: options.repositoryPolicy, IdempotencyKey: options.idempotencyKey, Command: options.command, }, nil } @@ -528,7 +535,7 @@ func isPathParameter(name string) bool { } } -func loadAuthority(options commandOptions, correlation string, goal model.Goal, now time.Time) (protocol.AuthorityBundle, error) { +func loadAuthority(options commandOptions, correlation string, objective model.Objective, now time.Time) (protocol.AuthorityBundle, error) { bundle := protocol.AuthorityBundle{} for _, path := range options.authorityReceipts { raw, err := os.ReadFile(path) @@ -548,7 +555,7 @@ func loadAuthority(options commandOptions, correlation string, goal model.Goal, bundle.Receipts = append(bundle.Receipts, receipt) } if options.humanActor != "" { - fingerprint := hash([]byte(strings.Join([]string{correlation, goal.ID, options.transitionID, options.humanActor}, "\x00"))) + fingerprint := hash([]byte(strings.Join([]string{correlation, objective.ID, options.transitionID, options.humanActor}, "\x00"))) bundle.Receipts = append(bundle.Receipts, protocol.AuthorityReceipt{ ID: "human-" + fingerprint[:16], Class: catalog.AuthorityHuman, Subject: options.humanActor, Fingerprint: fingerprint, IssuedAt: now, ExpiresAt: now.Add(5 * time.Minute), diff --git a/boatstack/cmd/boatstack-helper/main_test.go b/boatstack/cmd/boatstack-helper/main_test.go index 2188c2d..85a0ba9 100644 --- a/boatstack/cmd/boatstack-helper/main_test.go +++ b/boatstack/cmd/boatstack-helper/main_test.go @@ -7,16 +7,16 @@ import ( "time" "github.com/operatorstack/boatstack/boatstack/internal/buildinfo" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) func TestEveryFriendlyMutationAliasMapsToOneRegistryTransition(t *testing.T) { // control-law: cli-verbs-are-adapters-not-transition-authority registry := testprogram.StandardRegistry() - commands := []string{"init", "update", "attach", "detach", "hydrate-runtime", "configure", "goal-configure", "plan-create", "plan-validate", "plan-approve", "plan-activate", "plan-amend", "workspace-cut", "workspace-sync", "workspace-cleanup", "workspace-reap", "record-build", "record-test", "record-review", "record-change", "record-journey", "publication-preview", "publish-pr", "observe-pr", "correct-pr", "abandon"} + commands := []string{"init", "update", "attach", "detach", "hydrate-runtime", "configure", "objective-bind", "plan-create", "plan-validate", "plan-approve", "plan-activate", "plan-amend", "workspace-cut", "workspace-sync", "workspace-cleanup", "workspace-reap", "record-build", "record-test", "record-review", "record-change", "record-journey", "publication-preview", "publish-pr", "observe-pr", "correct-pr", "abandon"} for _, command := range commands { operation, transitionID, _, err := classifyCommand(command) if err != nil { @@ -38,13 +38,13 @@ func TestParametersRejectMissingEquals(t *testing.T) { } } -func TestApplyWithoutRestatedGoalGetsCommandScopedFlowIdentity(t *testing.T) { +func TestApplyWithoutRestatedObjectiveGetsCommandScopedFlowIdentity(t *testing.T) { request, err := buildRequest(surfaces.OperationApply, commandOptions{transitionID: "installation.update", host: "cli", repository: "."}) if err != nil { t.Fatal(err) } - if request.FlowID == "" || request.Goal.ID != "" { - t.Fatalf("request did not preserve configured-goal lookup with a generated flow: %#v", request) + if request.FlowID == "" || request.Objective.ID != "" { + t.Fatalf("request did not preserve configured-objective lookup with a generated flow: %#v", request) } } @@ -83,7 +83,7 @@ func TestTransitionReceiptCannotBeLoadedAsAuthority(t *testing.T) { if err := os.WriteFile(path, []byte(`{"schema_version":5,"id":"trc-old"}`), 0o600); err != nil { t.Fatal(err) } - _, err := loadAuthority(commandOptions{authorityReceipts: stringList{path}}, "correlation", model.Goal{}, time.Now().UTC()) + _, err := loadAuthority(commandOptions{authorityReceipts: stringList{path}}, "correlation", model.Objective{}, time.Now().UTC()) if err == nil { t.Fatal("transition receipt was accepted as authority") } diff --git a/boatstack/control/program_manifest_test.go b/boatstack/control/program_manifest_test.go deleted file mode 100644 index 9c8bfcc..0000000 --- a/boatstack/control/program_manifest_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package control_test - -import ( - "bytes" - "encoding/json" - "errors" - "sort" - "strings" - "testing" - - boatstack "github.com/operatorstack/boatstack/boatstack" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" -) - -func TestProgramManifestCanonicalFingerprintContract(t *testing.T) { - // control-law: validated-executable-semantics-exactly-determine-program-fingerprint - base := programFixture() - one := loadManifest(t, base) - - equivalent := programFixture() - equivalent.ProgramVersion = "99" - equivalent.RequiresRuntime = ">=0.9.0" - reverse(equivalent.Capabilities.Effects) - reverse(equivalent.Capabilities.Verifiers) - reverse(equivalent.Capabilities.CapabilitySurface) - reverse(equivalent.OwnedResources) - reverse(equivalent.Transitions) - reverse(equivalent.Transitions[0].RequiredIdentity) - reverse(equivalent.Transitions[0].SourceConditions) - two := loadManifest(t, equivalent) - if one.Fingerprint() != two.Fingerprint() { - t.Fatalf("representation-only or author-version changes changed executable fingerprint: %s != %s", one.Fingerprint(), two.Fingerprint()) - } - - raw, err := json.MarshalIndent(base, "", " ") - if err != nil { - t.Fatal(err) - } - three, err := control.LoadProgram(bytes.NewReader(raw), runtimeFixture()) - if err != nil { - t.Fatal(err) - } - if one.Fingerprint() != three.Fingerprint() { - t.Fatal("whitespace changed executable fingerprint") - } - reordered := reorderTopLevelObject(t, raw) - four, err := control.LoadProgram(bytes.NewReader(reordered), runtimeFixture()) - if err != nil { - t.Fatal(err) - } - if one.Fingerprint() != four.Fingerprint() { - t.Fatal("JSON object key order changed executable fingerprint") - } - - mutations := map[string]func(*control.ProgramManifest){ - "program-id": func(value *control.ProgramManifest) { value.ProgramID = "alternate-program" }, - "source-phase": func(value *control.ProgramManifest) { - value.Transitions[0].SourcePhases = []control.ProtocolPhase{control.PhaseObserved} - }, - "target-phase": func(value *control.ProgramManifest) { - value.Transitions[0].TargetPhases = []control.ProtocolPhase{control.PhaseFrontier} - }, - "authority": func(value *control.ProgramManifest) { - value.Transitions[0].Authority = []control.AuthorityClass{control.AuthorityHuman} - }, - "capability-surface": func(value *control.ProgramManifest) { - value.Capabilities.CapabilitySurface = append(value.Capabilities.CapabilitySurface, control.CapabilityHumanApprove) - }, - "effect": func(value *control.ProgramManifest) { - value.Capabilities.Effects[0] = "alternate.effect" - value.Transitions[0].Effect = "alternate.effect" - value.Transitions[0].LocalEffects = []control.EffectID{"alternate.effect"} - }, - "verifier": func(value *control.ProgramManifest) { - value.Capabilities.Verifiers[1] = "alternate.verifier" - value.Transitions[0].Verifier = "alternate.verifier" - }, - "postcondition": func(value *control.ProgramManifest) { - value.Transitions[0].TargetConditions[0].Values = []string{"alternate"} - }, - "recovery": func(value *control.ProgramManifest) { value.Transitions[0].Interruption.Recovery = "alternate.recover" }, - "priority": func(value *control.ProgramManifest) { value.Transitions[0].Priority++ }, - } - for name, mutate := range mutations { - t.Run(name, func(t *testing.T) { - candidate := programFixture() - mutate(&candidate) - if name == "recovery" { - recovery := candidate.Transitions[1] - recovery.ID = "alternate.recover" - recovery.Interruption.Recovery = "alternate.recover" - candidate.Transitions = append(candidate.Transitions, recovery) - } - program := loadManifest(t, candidate) - if program.Fingerprint() == one.Fingerprint() { - t.Fatalf("%s semantic change did not change fingerprint", name) - } - }) - } -} - -func TestProgramManifestNamespaceAndCompatibilityBoundary(t *testing.T) { - // control-law: only-compatible-validated-programs-reach-the-runtime-registry - first := programFixture() - first.ProgramID = "first-program" - second := programFixture() - second.ProgramID = "second-program" - one := loadManifest(t, first) - two := loadManifest(t, second) - if one.Transitions()[0].ID == two.Transitions()[0].ID { - t.Fatal("equal local transition IDs collided across programs") - } - for _, transition := range one.Transitions() { - if !strings.HasPrefix(string(transition.ID), "first-program/") { - t.Fatalf("transition is not program-qualified: %s", transition.ID) - } - if !transition.RuntimeExecution { - t.Fatalf("repository-authored transition %s escaped protocol-runtime classification", transition.ID) - } - } - - cases := []struct { - name string - mutate func(*control.ProgramManifest) - runtime control.RuntimeCompatibility - code control.ProgramErrorCode - }{ - {"unsupported-schema", func(value *control.ProgramManifest) { value.SchemaVersion = control.ProgramSchemaVersion + 1 }, runtimeFixture(), control.ProgramSchemaUnsupported}, - {"invalid-schema", func(value *control.ProgramManifest) { value.SchemaVersion = 0 }, runtimeFixture(), control.ProgramInvalid}, - {"runtime-too-old", func(value *control.ProgramManifest) { value.RequiresRuntime = ">=2.0.0" }, runtimeFixture(), control.RuntimeTooOld}, - {"malformed-runtime", func(value *control.ProgramManifest) { value.RequiresRuntime = "^1" }, runtimeFixture(), control.ProgramInvalid}, - {"malformed-program-id", func(value *control.ProgramManifest) { value.ProgramID = "../program" }, runtimeFixture(), control.ProgramInvalid}, - {"ambiguous-transition-id", func(value *control.ProgramManifest) { value.Transitions[0].ID = "other/advance" }, runtimeFixture(), control.ProgramInvalid}, - {"duplicate-transition", func(value *control.ProgramManifest) { - value.Transitions = append(value.Transitions, value.Transitions[0]) - }, runtimeFixture(), control.ProgramInvalid}, - {"duplicate-capability", func(value *control.ProgramManifest) { - value.Capabilities.Effects = append(value.Capabilities.Effects, value.Capabilities.Effects[0]) - }, runtimeFixture(), control.ProgramInvalid}, - {"unknown-authority-capability", func(value *control.ProgramManifest) { - value.Capabilities.CapabilitySurface = []control.Capability{"production.nuke"} - }, runtimeFixture(), control.ProgramInvalid}, - {"under-declared-kernel-effect", func(value *control.ProgramManifest) { - value.Capabilities.CapabilitySurface = []control.Capability{control.CapabilityRepositoryWrite} - }, runtimeFixture(), control.ProgramInvalid}, - {"duplicate-condition", func(value *control.ProgramManifest) { - value.Transitions[0].SourceConditions = append(value.Transitions[0].SourceConditions, value.Transitions[0].SourceConditions[0]) - }, runtimeFixture(), control.ProgramInvalid}, - {"missing-runtime-capability", func(*control.ProgramManifest) {}, control.RuntimeCompatibility{Version: "v1.2.3"}, control.ProgramInvalid}, - } - for _, test := range cases { - t.Run(test.name, func(t *testing.T) { - manifest := programFixture() - test.mutate(&manifest) - _, err := control.ValidateProgram(manifest, test.runtime) - var programErr control.ProgramError - if !errors.As(err, &programErr) || programErr.Code != test.code { - t.Fatalf("error = %v, want %s", err, test.code) - } - }) - } - - exact := programFixture() - exact.RequiresRuntime = ">=1.2.3" - if _, err := control.ValidateProgram(exact, runtimeFixture()); err != nil { - t.Fatalf("exact minimum was rejected: %v", err) - } - above := programFixture() - above.RequiresRuntime = ">=1.0.0" - if _, err := control.ValidateProgram(above, runtimeFixture()); err != nil { - t.Fatalf("runtime above minimum was rejected: %v", err) - } - prerelease := programFixture() - prerelease.RequiresRuntime = ">=1.2.3" - candidateRuntime := runtimeFixture() - candidateRuntime.Version = "v1.2.3-dev" - if _, err := control.ValidateProgram(prerelease, candidateRuntime); err == nil { - t.Fatal("prerelease runtime incorrectly satisfied the matching stable minimum") - } -} - -func TestProgramSourceParserFailsClosed(t *testing.T) { - // control-law: uninterpreted-source-cannot-reach-the-executable-program - manifest := programFixture() - raw, err := json.Marshal(manifest) - if err != nil { - t.Fatal(err) - } - unknown := bytes.Replace(raw, []byte(`"program_id"`), []byte(`"requires_human":true,"program_id"`), 1) - if _, err := control.LoadProgram(bytes.NewReader(unknown), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "unknown field") { - t.Fatalf("unknown control-law field was not rejected: %v", err) - } - unknownTransition := bytes.Replace(raw, []byte(`"priority":1`), []byte(`"requires_human":true,"priority":1`), 1) - if _, err := control.LoadProgram(bytes.NewReader(unknownTransition), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "unknown field") { - t.Fatalf("unknown transition field was not rejected: %v", err) - } - duplicate := bytes.Replace(raw, []byte(`"program_id":"test-program"`), []byte(`"program_id":"test-program","program_id":"weaker-program"`), 1) - if _, err := control.LoadProgram(bytes.NewReader(duplicate), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "duplicate JSON field") { - t.Fatalf("duplicate JSON field was not rejected: %v", err) - } - if _, err := control.LoadProgram(bytes.NewReader(append(raw, []byte(` {}`)...)), runtimeFixture()); err == nil { - t.Fatal("trailing JSON was accepted") - } -} - -func TestValidatedProgramIsTheKernelRegistry(t *testing.T) { - // control-law: kernel-consumes-the-exact-validated-fingerprinted-registry - program := loadManifest(t, programFixture()) - kernel, err := boatstack.NewKernel(t.TempDir(), program) - if err != nil { - t.Fatal(err) - } - response, err := kernel.Handle(t.Context(), surfaces.Request{SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationCatalog}) - if err != nil { - t.Fatal(err) - } - if len(response.Catalog) != len(programFixture().Transitions) { - t.Fatalf("kernel catalog count = %d", len(response.Catalog)) - } - for _, transition := range response.Catalog { - if !strings.HasPrefix(string(transition.ID), "test-program/") || transition.Origin.ManifestFingerprint != program.Fingerprint() { - t.Fatalf("kernel reached around validated identity: %+v", transition) - } - } -} - -func programFixture() control.ProgramManifest { - recovery := control.ProgramTransition{ - ID: "recover", Version: 1, SelectionClass: control.SelectionProgramRecovery, Class: control.EventRecovery, - SourcePhases: []control.ProtocolPhase{control.PhaseRecovery}, TargetPhases: []control.ProtocolPhase{control.PhaseActive}, - RequiredIdentity: []string{"repository-id"}, Authority: []control.AuthorityClass{control.AuthorityRepository}, RequiredCapabilities: []control.Capability{control.CapabilityRepositoryWrite}, RequiredEvidence: []string{"snapshot"}, - OwnedResources: []string{"program.state"}, Effect: "program.recover", LocalEffects: []control.EffectID{"program.recover"}, Idempotent: true, - Prescription: control.Prescription{Operation: "recover", ExpectedPostcondition: "active"}, SourcePredicate: "recovery-required", - SourceConditions: []control.FacetCondition{control.KnownCondition(control.FacetRecovery, "required")}, AdmissionPredicate: "exact-admission", - TargetPredicate: "active", TargetConditions: []control.FacetCondition{control.KnownCondition(control.FacetProgram, "current")}, Verifier: "program.current", - Interruption: interruption("recover"), Reversibility: control.Reversible, TerminalEffect: "none", - PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "local", Priority: 1, - } - advance := recovery - advance.ID = "advance" - advance.SelectionClass = control.SelectionProgramProgress - advance.Class = control.EventOwnedLocal - advance.SourcePhases = []control.ProtocolPhase{control.PhaseActive} - advance.TargetPhases = []control.ProtocolPhase{control.PhaseTerminal} - advance.GoalKinds = []control.GoalKind{control.GoalVerified} - advance.Authority = []control.AuthorityClass{control.AuthorityHuman, control.AuthorityRepository} - advance.Effect = "program.advance" - advance.LocalEffects = []control.EffectID{"program.advance"} - advance.Prescription = control.Prescription{Operation: "advance", Arguments: []string{"--exact"}, ExpectedPostcondition: "terminal"} - advance.SourcePredicate = "active" - advance.SourceConditions = []control.FacetCondition{control.KnownCondition(control.FacetProgram, "current"), control.KnownCondition(control.FacetDelivery, "active")} - advance.TargetPredicate = "terminal" - advance.TargetConditions = []control.FacetCondition{control.KnownCondition(control.FacetDelivery, "terminal")} - advance.Verifier = "program.terminal" - return control.ProgramManifest{ - SchemaVersion: control.ProgramSchemaVersion, ProgramID: "test-program", ProgramVersion: "1", RequiresRuntime: ">=1.0.0", - Capabilities: control.ProgramCapabilities{ - Effects: []string{"program.advance", "program.recover"}, Verifiers: []string{"program.current", "program.terminal"}, - CapabilitySurface: []control.Capability{control.CapabilityRepositoryWrite, control.CapabilityCommandExecute}, - }, - OwnedResources: []string{"program.state"}, GoalContracts: []control.GoalContract{{GoalKind: control.GoalVerified, Conditions: []control.FacetCondition{control.KnownCondition(control.FacetDelivery, "terminal")}}}, - Transitions: []control.ProgramTransition{advance, recovery}, - } -} - -func runtimeFixture() control.RuntimeCompatibility { - return control.RuntimeCompatibility{Version: "v1.2.3", Effects: []string{"program.advance", "program.recover", "alternate.effect"}, Verifiers: []string{"program.current", "program.terminal", "alternate.verifier"}, Capabilities: []control.Capability{control.CapabilityRepositoryWrite, control.CapabilityCommandExecute, control.CapabilityHumanApprove}} -} - -func loadManifest(t *testing.T, manifest control.ProgramManifest) control.ControlProgram { - t.Helper() - program, err := control.ValidateProgram(manifest, runtimeFixture()) - if err != nil { - t.Fatal(err) - } - return program -} - -func interruption(recovery control.TransitionID) control.InterruptionContract { - return control.InterruptionContract{Points: []string{"after-effect"}, PartialState: []string{"effect-may-exist"}, Detection: "fresh-observation", ResumeContract: "resume", RollbackContract: "rollback", CompensationContract: "compensate", Recovery: recovery, RecoveryAuthority: "repository-policy", ResumptionPredicate: "exact-state"} -} - -func reverse[T any](values []T) { - for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 { - values[left], values[right] = values[right], values[left] - } -} - -func reorderTopLevelObject(t *testing.T, raw []byte) []byte { - t.Helper() - var fields map[string]json.RawMessage - if err := json.Unmarshal(raw, &fields); err != nil { - t.Fatal(err) - } - keys := make([]string, 0, len(fields)) - for key := range fields { - keys = append(keys, key) - } - sort.Sort(sort.Reverse(sort.StringSlice(keys))) - var result bytes.Buffer - result.WriteByte('{') - for index, key := range keys { - if index != 0 { - result.WriteByte(',') - } - encodedKey, _ := json.Marshal(key) - result.Write(encodedKey) - result.WriteByte(':') - result.Write(fields[key]) - } - result.WriteByte('}') - return result.Bytes() -} diff --git a/boatstack/core/system.go b/boatstack/core/system.go index 1db38c7..807d234 100644 --- a/boatstack/core/system.go +++ b/boatstack/core/system.go @@ -10,7 +10,7 @@ import ( "fmt" "io" - "github.com/operatorstack/boatstack/boatstack/control" + "github.com/operatorstack/boatstack/boatstack/delivery" ) const ( @@ -23,25 +23,25 @@ type system struct{} //go:embed transitions.json var transitionDeclarations []byte -func System() control.CoreSystemDefinition { return system{} } +func System() delivery.CoreSystemDefinition { return system{} } -func (system) CoreManifest(context.Context) (control.CoreSystemManifest, error) { +func (system) CoreManifest(context.Context) (delivery.CoreSystemManifest, error) { transitions, err := decodeTransitions() if err != nil { - return control.CoreSystemManifest{}, err + return delivery.CoreSystemManifest{}, err } - var capabilities []control.Capability + var capabilities []delivery.Capability for index := range transitions { - transitions[index].RequiredCapabilities = control.KernelEffectCapabilities(transitions[index]) - capabilities = control.UnionCapabilities(capabilities, transitions[index].RequiredCapabilities) + transitions[index].RequiredCapabilities = delivery.KernelEffectCapabilities(transitions[index]) + capabilities = delivery.UnionCapabilities(capabilities, transitions[index].RequiredCapabilities) } - return control.CoreSystemManifest{ID: ID, Version: Version, Capabilities: capabilities, Transitions: transitions}, nil + return delivery.CoreSystemManifest{ID: ID, Version: Version, Capabilities: capabilities, Transitions: transitions}, nil } -func decodeTransitions() ([]control.Transition, error) { +func decodeTransitions() ([]delivery.Transition, error) { decoder := json.NewDecoder(bytes.NewReader(transitionDeclarations)) decoder.DisallowUnknownFields() - var transitions []control.Transition + var transitions []delivery.Transition if err := decoder.Decode(&transitions); err != nil { return nil, fmt.Errorf("decode CoreSystem transitions: %w", err) } diff --git a/boatstack/core/system_test.go b/boatstack/core/system_test.go index 6bf2298..a6c3f65 100644 --- a/boatstack/core/system_test.go +++ b/boatstack/core/system_test.go @@ -19,7 +19,7 @@ func TestManifestOwnsOnlyOperationalCapabilities(t *testing.T) { } for _, transition := range manifest.Transitions { id := string(transition.ID) - if !hasPrefix(id, "engagement.", "invocation.", "repository.", "runtime.", "configuration.", "installation.", "catalog.", "goal.", "recovery.", "external.") { + if !hasPrefix(id, "engagement.", "invocation.", "repository.", "runtime.", "configuration.", "installation.", "catalog.", "objective.", "recovery.", "external.") { t.Errorf("CoreSystem owns delivery-flow transition %s", id) } } diff --git a/boatstack/core/transitions.json b/boatstack/core/transitions.json index 023f63b..0ea528f 100644 --- a/boatstack/core/transitions.json +++ b/boatstack/core/transitions.json @@ -9,7 +9,7 @@ "manifest_fingerprint": "" }, "owner": "", - "selection_class": "GOAL_REQUIRED", + "selection_class": "OBJECTIVE_REQUIRED", "class": "authority", "source_phases": [ "DORMANT", @@ -19,7 +19,7 @@ "OBSERVED", "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -46,9 +46,9 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:engagement", - "facet:goal", + "facet:objective", "facet:program", "facet:recovery", "facet:transaction", @@ -81,7 +81,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -182,7 +182,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 10 }, { @@ -203,7 +205,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -231,9 +233,9 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:engagement", - "facet:goal", + "facet:objective", "facet:program", "facet:recovery", "facet:transaction", @@ -265,7 +267,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -366,7 +368,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 70 }, { @@ -388,7 +392,7 @@ "target_phases": [ "DORMANT" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -415,9 +419,9 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:engagement", - "facet:goal", + "facet:objective", "facet:program", "facet:recovery", "facet:transaction", @@ -451,7 +455,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -552,7 +556,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 95 }, { @@ -574,7 +580,7 @@ "target_phases": [ "OBSERVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -601,7 +607,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:topology", "facet:program", "facet:recovery", @@ -703,7 +709,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 15, "allows_identity_rebind": true }, @@ -726,7 +734,7 @@ "target_phases": [ "OBSERVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -753,7 +761,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:topology", "facet:program", "facet:recovery", @@ -874,7 +882,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 12, "allows_identity_rebind": true }, @@ -898,7 +908,7 @@ "target_phases": [ "DORMANT" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -925,7 +935,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:topology", "facet:program", "facet:recovery", @@ -1034,7 +1044,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 96, "allows_identity_rebind": true }, @@ -1048,7 +1060,7 @@ "manifest_fingerprint": "" }, "owner": "", - "selection_class": "GOAL_REQUIRED", + "selection_class": "OBJECTIVE_REQUIRED", "class": "owned-local", "source_phases": [ "OBSERVED", @@ -1060,7 +1072,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1087,7 +1099,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:runtime", "facet:program", "facet:recovery", @@ -1214,11 +1226,13 @@ "resumption_predicate": "recovery-contract-for:runtime.hydrate" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 20 }, { @@ -1241,7 +1255,7 @@ "OBSERVED", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1269,7 +1283,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:runtime", "facet:program", "facet:recovery", @@ -1396,11 +1410,13 @@ "resumption_predicate": "recovery-contract-for:runtime.replace" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 25 }, { @@ -1424,7 +1440,7 @@ "FRONTIER", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1451,7 +1467,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:program" ], @@ -1553,11 +1569,13 @@ "resumption_predicate": "recovery-contract-for:runtime.reconcile" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 4 }, { @@ -1570,7 +1588,7 @@ "manifest_fingerprint": "" }, "owner": "", - "selection_class": "GOAL_REQUIRED", + "selection_class": "OBJECTIVE_REQUIRED", "class": "owned-local", "source_phases": [ "OBSERVED" @@ -1579,7 +1597,7 @@ "OBSERVED", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1607,7 +1625,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:configuration", "facet:program", "facet:recovery", @@ -1726,11 +1744,13 @@ "resumption_predicate": "recovery-contract-for:configuration.initialize" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 22 }, { @@ -1756,7 +1776,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1784,14 +1804,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:configuration", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:runtime" ], "owned_resources": [ @@ -1881,7 +1901,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -1932,11 +1952,13 @@ "resumption_predicate": "recovery-contract-for:configuration.mutate" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 60 }, { @@ -1960,7 +1982,7 @@ "FRONTIER", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1988,7 +2010,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:program" ], @@ -2075,11 +2097,13 @@ "resumption_predicate": "recovery-contract-for:configuration.reconcile" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 3 }, { @@ -2092,7 +2116,7 @@ "manifest_fingerprint": "" }, "owner": "", - "selection_class": "GOAL_REQUIRED", + "selection_class": "OBJECTIVE_REQUIRED", "class": "owned-local", "source_phases": [ "DORMANT", @@ -2101,7 +2125,7 @@ "target_phases": [ "OBSERVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2128,7 +2152,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:runtime", "facet:program", "facet:recovery", @@ -2274,7 +2298,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 11 }, { @@ -2305,7 +2331,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2333,14 +2359,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:runtime", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration" ], "owned_resources": [ @@ -2429,7 +2455,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known", "absent" @@ -2481,23 +2507,44 @@ "resumption_predicate": "recovery-contract-for:installation.update" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 65 }, { "id": "installation.reconcile-update", "version": 1, - "origin": {"kind": "", "id": "", "version": "", "manifest_fingerprint": ""}, + "origin": { + "kind": "", + "id": "", + "version": "", + "manifest_fingerprint": "" + }, "owner": "", "selection_class": "EXPLICIT_ONLY", "class": "owned-local", - "source_phases": ["DORMANT", "OBSERVED", "ACTIVE", "FRONTIER", "TERMINAL", "ABANDONED"], - "target_phases": ["DORMANT", "OBSERVED", "ACTIVE", "FRONTIER", "TERMINAL", "ABANDONED"], - "goal_kinds": [ + "source_phases": [ + "DORMANT", + "OBSERVED", + "ACTIVE", + "FRONTIER", + "TERMINAL", + "ABANDONED" + ], + "target_phases": [ + "DORMANT", + "OBSERVED", + "ACTIVE", + "FRONTIER", + "TERMINAL", + "ABANDONED" + ], + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2518,28 +2565,50 @@ "host", "correlation-id" ], - "authority": ["human"], + "authority": [ + "human" + ], "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:runtime", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", - "facet:goal", + "facet:objective", "facet:configuration" ], - "owned_resources": ["installation"], + "owned_resources": [ + "installation" + ], "effect": "installation.reconcile-update", - "local_effects": ["installation.reconcile-update"], + "local_effects": [ + "installation.reconcile-update" + ], "idempotent": true, "parameters": [ - {"name": "source_revision", "required": true, "secret": false}, - {"name": "runtime_version", "required": true, "secret": false}, - {"name": "runtime_sha256", "required": true, "secret": false}, - {"name": "accept_obligation_change", "required": true, "secret": false} + { + "name": "source_revision", + "required": true, + "secret": false + }, + { + "name": "runtime_version", + "required": true, + "secret": false + }, + { + "name": "runtime_sha256", + "required": true, + "secret": false + }, + { + "name": "accept_obligation_change", + "required": true, + "secret": false + } ], "prescription": { "operation": "installation.reconcile-update", @@ -2550,26 +2619,110 @@ "source_conditions": [ { "facet": "runtime", - "statuses": ["known"], - "values": ["absent", "verified", "stale", "invalid", "conflicting", "wrong-source", "partially-published"] + "statuses": [ + "known" + ], + "values": [ + "absent", + "verified", + "stale", + "invalid", + "conflicting", + "wrong-source", + "partially-published" + ] + }, + { + "facet": "program", + "statuses": [ + "known" + ], + "values": [ + "drift" + ] + }, + { + "facet": "recovery", + "statuses": [ + "known" + ], + "values": [ + "none" + ] + }, + { + "facet": "transaction", + "statuses": [ + "known" + ], + "values": [ + "none" + ] + }, + { + "facet": "terminal", + "statuses": [ + "known" + ], + "values": [ + "nonterminal", + "stale", + "established" + ] + }, + { + "facet": "objective", + "statuses": [ + "known", + "absent" + ] }, - {"facet": "program", "statuses": ["known"], "values": ["drift"]}, - {"facet": "recovery", "statuses": ["known"], "values": ["none"]}, - {"facet": "transaction", "statuses": ["known"], "values": ["none"]}, - {"facet": "terminal", "statuses": ["known"], "values": ["nonterminal", "stale", "established"]}, - {"facet": "goal", "statuses": ["known", "absent"]}, - {"facet": "configuration", "statuses": ["known"], "values": ["verified"]} + { + "facet": "configuration", + "statuses": [ + "known" + ], + "values": [ + "verified" + ] + } ], "admission_predicate": "predicate:exact-admission:installation.reconcile-update", "target_predicate": "predicate:target-phase:installation.reconcile-update", "target_conditions": [ - {"facet": "runtime", "statuses": ["known"], "values": ["verified"]}, - {"facet": "program", "statuses": ["known"], "values": ["current"]} + { + "facet": "runtime", + "statuses": [ + "known" + ], + "values": [ + "verified" + ] + }, + { + "facet": "program", + "statuses": [ + "known" + ], + "values": [ + "current" + ] + } ], "verifier": "verifier:fresh-observation:installation.reconcile-update", "interruption": { - "points": ["after-lock", "after-stage", "after-effect", "before-receipt"], - "partial_state": ["journal-begun", "effect-staged", "effect-possibly-installed", "postcondition-unreceipted"], + "points": [ + "after-lock", + "after-stage", + "after-effect", + "before-receipt" + ], + "partial_state": [ + "journal-begun", + "effect-staged", + "effect-possibly-installed", + "postcondition-unreceipted" + ], "detection": "pending-journal-plus-fresh-canonical-observation", "resume_contract": "never-activate-a-partial-program-update", "rollback_contract": "restore-runtime-launcher-program-state-and-generated-artifacts-together", @@ -2583,7 +2736,10 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"reconciles_program": true, "goal_scope": "optional-preserve"}, + "policy": { + "reconciles_program": true, + "objective_scope": "optional-preserve" + }, "priority": 1 }, { @@ -2618,7 +2774,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2645,7 +2801,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:program", "facet:program", "facet:recovery", @@ -2768,12 +2924,12 @@ "cost_class": "declared-neutral", "policy": { "reconciles_program": true, - "goal_scope": "optional-preserve" + "objective_scope": "optional-preserve" }, "priority": 1 }, { - "id": "goal.configure", + "id": "objective.bind", "version": 1, "origin": { "kind": "", @@ -2782,7 +2938,7 @@ "manifest_fingerprint": "" }, "owner": "", - "selection_class": "GOAL_REQUIRED", + "selection_class": "OBJECTIVE_REQUIRED", "class": "authority", "source_phases": [ "OBSERVED", @@ -2797,7 +2953,7 @@ "ACTIVE", "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2825,21 +2981,21 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", - "facet:goal", + "objective", + "facet:objective", "facet:program" ], "owned_resources": [ - "goal" + "objective" ], - "effect": "goal.configure", + "effect": "objective.bind", "local_effects": [ - "goal.configure" + "objective.bind" ], "idempotent": true, "parameters": [ { - "name": "goal_kind", + "name": "objective_kind", "required": true, "secret": false }, @@ -2850,13 +3006,13 @@ } ], "prescription": { - "operation": "goal.configure", - "expected_postcondition": "predicate:target-phase:goal.configure" + "operation": "objective.bind", + "expected_postcondition": "predicate:target-phase:objective.bind" }, - "source_predicate": "predicate:source-phase:goal.configure", + "source_predicate": "predicate:source-phase:objective.bind", "source_conditions": [ { - "facet": "goal", + "facet": "objective", "statuses": [ "known", "absent" @@ -2873,17 +3029,17 @@ ] } ], - "admission_predicate": "predicate:exact-admission:goal.configure", - "target_predicate": "predicate:target-phase:goal.configure", + "admission_predicate": "predicate:exact-admission:objective.bind", + "target_predicate": "predicate:target-phase:objective.bind", "target_conditions": [ { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] } ], - "verifier": "verifier:fresh-observation:goal.configure", + "verifier": "verifier:fresh-observation:objective.bind", "interruption": { "points": [ "after-lock", @@ -2903,7 +3059,7 @@ "compensation_contract": "not-required-for-owned-local-effects", "recovery": "recovery.resume", "recovery_authority": "declared-by:recovery.resume", - "resumption_predicate": "recovery-contract-for:goal.configure" + "resumption_predicate": "recovery-contract-for:objective.bind" }, "reversibility": "reversible", "terminal_effect": "none", @@ -2911,7 +3067,8 @@ "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "binds_requested_goal": true + "binds_requested_objective": true, + "objective_scope": "none" }, "priority": 30 }, @@ -2938,7 +3095,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2967,7 +3124,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:program" ], @@ -3058,7 +3215,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 2 }, { @@ -3084,7 +3243,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3112,7 +3271,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:program" ], @@ -3203,7 +3362,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 3 }, { @@ -3225,7 +3386,7 @@ "target_phases": [ "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3252,7 +3413,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:program" ], @@ -3343,7 +3504,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"goal_scope": "optional-preserve"}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 5 }, { @@ -3369,7 +3532,7 @@ "target_phases": [ "OBSERVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3396,7 +3559,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:verification" ], "idempotent": false, @@ -3443,7 +3606,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -3469,7 +3634,7 @@ "target_phases": [ "OBSERVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3496,7 +3661,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:verification" ], "idempotent": false, @@ -3543,7 +3708,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -3569,7 +3736,7 @@ "target_phases": [ "OBSERVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3596,7 +3763,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace" ], "idempotent": false, @@ -3643,7 +3810,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -3670,7 +3839,7 @@ "OBSERVED", "RECOVERY" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3697,7 +3866,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:runtime" ], "idempotent": false, @@ -3752,7 +3921,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -3779,7 +3950,7 @@ "OBSERVED", "UNRESOLVED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3806,7 +3977,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:configuration" ], "idempotent": false, @@ -3861,7 +4032,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -3888,7 +4061,7 @@ "DORMANT", "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3915,7 +4088,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:engagement" ], "idempotent": false, @@ -3970,7 +4143,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -3996,7 +4171,7 @@ "target_phases": [ "RECOVERY" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4023,7 +4198,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:transaction", "facet:transaction-info" ], @@ -4083,7 +4258,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -4111,7 +4288,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4138,7 +4315,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:verification" ], "idempotent": false, @@ -4185,7 +4362,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -4213,7 +4392,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4240,7 +4419,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication" ], "idempotent": false, @@ -4290,7 +4469,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -4318,7 +4499,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4345,7 +4526,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication" ], "idempotent": false, @@ -4395,7 +4576,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -4423,7 +4606,7 @@ "ACTIVE", "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4450,7 +4633,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication" ], "idempotent": false, @@ -4501,7 +4684,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -4529,7 +4714,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4556,7 +4741,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication" ], "idempotent": false, @@ -4606,7 +4791,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 }, { @@ -4633,7 +4820,7 @@ "UNRESOLVED", "RECOVERY" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4660,7 +4847,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication" ], "idempotent": false, @@ -4710,7 +4897,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 100 } ] diff --git a/boatstack/control/control.go b/boatstack/delivery/control.go similarity index 79% rename from boatstack/control/control.go rename to boatstack/delivery/control.go index 04dddda..4ffbe8d 100644 --- a/boatstack/control/control.go +++ b/boatstack/delivery/control.go @@ -1,6 +1,7 @@ -// Package control defines the stable authoring and compilation contracts for -// Boatstack control programs. It contains no default flow or product surface. -package control +// Package delivery defines the software-delivery domain's authoring and +// compilation contracts. The domain is Boatstack's first production use of +// the general kernel, not part of the kernel itself. +package delivery import ( "context" @@ -13,8 +14,9 @@ import ( "sort" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + general "github.com/operatorstack/boatstack/boatstack/kernel" "github.com/santhosh-tekuri/jsonschema/v6" ) @@ -25,15 +27,15 @@ type AuthorityClass = catalog.AuthorityClass type Capability = catalog.Capability type FacetCondition = catalog.FacetCondition type SelectionClass = catalog.SelectionClass -type GoalContract = catalog.GoalContract -type GoalScope = catalog.GoalScope +type ObjectiveContract = catalog.ObjectiveContract +type ObjectiveScope = catalog.ObjectiveScope type EffectID = catalog.EffectID type Prescription = catalog.Prescription type ParameterSpec = catalog.ParameterSpec type InterruptionContract = catalog.InterruptionContract type PolicyContract = catalog.PolicyContract type Reversibility = catalog.Reversibility -type GoalKind = model.GoalKind +type ObjectiveKind = model.ObjectiveKind type ProtocolPhase = model.ProtocolPhase type FactStatus = model.FactStatus type FacetName = model.FacetName @@ -61,18 +63,20 @@ const ( SelectionSystemRecovery = catalog.SelectionSystemRecovery SelectionProgramRecovery = catalog.SelectionProgramRecovery SelectionExtensionRecovery = catalog.SelectionExtensionRecovery - SelectionGoalRequired = catalog.SelectionGoalRequired + SelectionObjectiveRequired = catalog.SelectionObjectiveRequired SelectionProgramProgress = catalog.SelectionProgramProgress SelectionExplicitOnly = catalog.SelectionExplicitOnly SelectionObservedExternal = catalog.SelectionObservedExternal - GoalScopeOptionalPreserve = catalog.GoalScopeOptionalPreserve + ObjectiveScopeNone = catalog.ObjectiveScopeNone + ObjectiveScopeOptionalPreserve = catalog.ObjectiveScopeOptionalPreserve + ObjectiveScopeBoundExact = catalog.ObjectiveScopeBoundExact - GoalApprovedPlan = model.GoalApprovedPlan - GoalVerified = model.GoalVerified - GoalOpenPR = model.GoalOpenPR - GoalMerged = model.GoalMerged - GoalAbandoned = model.GoalAbandoned + ObjectiveApprovedPlan = model.ObjectiveApprovedPlan + ObjectiveVerified = model.ObjectiveVerified + ObjectiveOpenPR = model.ObjectiveOpenPR + ObjectiveMerged = model.ObjectiveMerged + ObjectiveAbandoned = model.ObjectiveAbandoned PhaseDormant = model.PhaseDormant PhaseObserved = model.PhaseObserved @@ -111,10 +115,10 @@ const ( FacetRecoveryInfo = model.FacetRecoveryInfo FacetTransactionInfo = model.FacetTransactionInfo FacetTerminal = model.FacetTerminal - FacetGoal = model.FacetGoal + FacetObjective = model.FacetObjective ) -const ProgramSchemaVersion = 2 +const ProgramSchemaVersion = 3 func KernelEffectCapabilities(transition Transition) []Capability { return catalog.KernelEffectCapabilities(transition) @@ -157,48 +161,48 @@ type CoreSystemManifest struct { } type ProgramRuntimeManifest struct { - ID string `json:"id"` - Version string `json:"version"` - ProtocolVersion int `json:"protocol_version"` - RuntimeMode ProgramRuntimeMode `json:"runtime_mode"` - SupportedGoals []GoalKind `json:"supported_goals"` - GoalContracts []GoalContract `json:"goal_contracts"` - Transitions []Transition `json:"transitions"` - Facts []string `json:"facts,omitempty"` - OwnedResources []string `json:"owned_resources"` - Effects []string `json:"effects"` - Verifiers []string `json:"verifiers"` - Capabilities []Capability `json:"capabilities"` - RecoveryTransitions []TransitionID `json:"recovery_transitions"` - Settings json.RawMessage `json:"settings,omitempty"` - ConfigurationSchema json.RawMessage `json:"configuration_schema,omitempty"` - PrivacyClassification string `json:"privacy_classification"` - TelemetryClassification string `json:"telemetry_classification"` + ID string `json:"id"` + Version string `json:"version"` + ProtocolVersion int `json:"protocol_version"` + RuntimeMode ProgramRuntimeMode `json:"runtime_mode"` + SupportedObjectives []ObjectiveKind `json:"supported_objectives"` + ObjectiveContracts []ObjectiveContract `json:"objective_contracts"` + Transitions []Transition `json:"transitions"` + Facts []string `json:"facts,omitempty"` + OwnedResources []string `json:"owned_resources"` + Effects []string `json:"effects"` + Verifiers []string `json:"verifiers"` + Capabilities []Capability `json:"capabilities"` + RecoveryTransitions []TransitionID `json:"recovery_transitions"` + Settings json.RawMessage `json:"settings,omitempty"` + ConfigurationSchema json.RawMessage `json:"configuration_schema,omitempty"` + PrivacyClassification string `json:"privacy_classification"` + TelemetryClassification string `json:"telemetry_classification"` } -type GoalConstraint struct { - GoalKind GoalKind `json:"goal_kind"` - Conditions []FacetCondition `json:"conditions"` +type ObjectiveConstraint struct { + ObjectiveKind ObjectiveKind `json:"objective_kind"` + Conditions []FacetCondition `json:"conditions"` } type ExtensionManifest struct { - ID string `json:"id"` - Version string `json:"version"` - ProtocolVersion int `json:"protocol_version"` - ExecutableSHA256 string `json:"executable_sha256,omitempty"` - Settings json.RawMessage `json:"settings,omitempty"` - SettingsSchema json.RawMessage `json:"settings_schema"` - Facts []string `json:"facts,omitempty"` - Transitions []Transition `json:"transitions,omitempty"` - GoalConstraints []GoalConstraint `json:"goal_constraints,omitempty"` - OwnedResources []string `json:"owned_resources,omitempty"` - Effects []string `json:"effects,omitempty"` - Verifiers []string `json:"verifiers,omitempty"` - Capabilities []Capability `json:"capabilities"` - RecoveryTransitions []TransitionID `json:"recovery_transitions,omitempty"` - PrivacyClassification string `json:"privacy_classification"` - TelemetryClassification string `json:"telemetry_classification"` - Dependencies []string `json:"dependencies,omitempty"` + ID string `json:"id"` + Version string `json:"version"` + ProtocolVersion int `json:"protocol_version"` + ExecutableSHA256 string `json:"executable_sha256,omitempty"` + Settings json.RawMessage `json:"settings,omitempty"` + SettingsSchema json.RawMessage `json:"settings_schema"` + Facts []string `json:"facts,omitempty"` + Transitions []Transition `json:"transitions,omitempty"` + ObjectiveConstraints []ObjectiveConstraint `json:"objective_constraints,omitempty"` + OwnedResources []string `json:"owned_resources,omitempty"` + Effects []string `json:"effects,omitempty"` + Verifiers []string `json:"verifiers,omitempty"` + Capabilities []Capability `json:"capabilities"` + RecoveryTransitions []TransitionID `json:"recovery_transitions,omitempty"` + PrivacyClassification string `json:"privacy_classification"` + TelemetryClassification string `json:"telemetry_classification"` + Dependencies []string `json:"dependencies,omitempty"` } type ComponentIdentity struct { @@ -227,8 +231,9 @@ type ProgramSummary struct { // copies; the runtime registry remains the one executable graph. type ControlProgram struct { summary ProgramSummary + supervisoryProgram general.Program registry catalog.Registry - goalContracts catalog.GoalContracts + objectiveContracts catalog.ObjectiveContracts resourceOwnership map[string]string settingsFingerprint string extensions []compiledExtension @@ -298,10 +303,19 @@ func (p ControlProgram) ProgramRuntime() CompiledProgramRuntime { return CompiledProgramRuntime{Manifest: cloneRuntimeManifest(p.programRuntime.manifest), Identity: p.programRuntime.identity, Runtime: p.programRuntime.runtime} } -// RuntimeRegistry and RuntimeGoalContracts are for the Boatstack mechanism. +// RuntimeRegistry and RuntimeObjectiveContracts are for the Boatstack mechanism. // External applications should use Transitions and Summary. -func (p ControlProgram) RuntimeRegistry() catalog.Registry { return p.registry } -func (p ControlProgram) RuntimeGoalContracts() catalog.GoalContracts { return p.goalContracts.Clone() } +func (p ControlProgram) RuntimeRegistry() catalog.Registry { return p.registry } +func (p ControlProgram) RuntimeObjectiveContracts() catalog.ObjectiveContracts { + return p.objectiveContracts.Clone() +} + +// SupervisoryProgram returns the domain-neutral executable program consumed +// by the kernel. The software manifest fingerprint remains bound into this +// program identity. +func (p ControlProgram) SupervisoryProgram() general.Program { + return p.supervisoryProgram.Clone() +} type CompileRequest struct { KernelVersion string @@ -389,7 +403,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error return ControlProgram{}, err } - extensionConditions := map[model.GoalKind][]catalog.FacetCondition{} + extensionConditions := map[model.ObjectiveKind][]catalog.FacetCondition{} compiledExtensions := make([]compiledExtension, 0, len(request.Extensions)) extensionIdentities := make([]ComponentIdentity, 0, len(request.Extensions)) extensionCount := 0 @@ -432,8 +446,8 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error return ControlProgram{}, fmt.Errorf("extension %q declares runtime behavior without an ExtensionRuntime", manifest.ID) } compiledExtensions = append(compiledExtensions, compiledExtension{manifest: manifest, identity: identity, runtime: runtime}) - for _, constraint := range manifest.GoalConstraints { - extensionConditions[constraint.GoalKind] = append(extensionConditions[constraint.GoalKind], constraint.Conditions...) + for _, constraint := range manifest.ObjectiveConstraints { + extensionConditions[constraint.ObjectiveKind] = append(extensionConditions[constraint.ObjectiveKind], constraint.Conditions...) } for _, resource := range manifest.OwnedResources { if !strings.HasPrefix(resource, manifest.ID+".") { @@ -464,7 +478,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error item.SelectionClass = catalog.SelectionExplicitOnly } } - if item.SelectionClass != catalog.SelectionGoalRequired && item.SelectionClass != catalog.SelectionExtensionRecovery && + if item.SelectionClass != catalog.SelectionObjectiveRequired && item.SelectionClass != catalog.SelectionExtensionRecovery && item.SelectionClass != catalog.SelectionExplicitOnly && item.SelectionClass != catalog.SelectionObservedExternal { return ControlProgram{}, fmt.Errorf("extension %q transition %q uses forbidden selection class %q", manifest.ID, item.ID, item.SelectionClass) } @@ -498,21 +512,21 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error if err := validateDependencies(compiledExtensions); err != nil { return ControlProgram{}, err } - for goal, conditions := range extensionConditions { + for objective, conditions := range extensionConditions { sort.SliceStable(conditions, func(i, j int) bool { left, _ := json.Marshal(conditions[i]) right, _ := json.Marshal(conditions[j]) return string(left) < string(right) }) - extensionConditions[goal] = conditions + extensionConditions[objective] = conditions } registry, err := catalog.New(transitions) if err != nil { return ControlProgram{}, fmt.Errorf("compile transition registry: %w", err) } - contracts, err := catalog.NewGoalContracts(flow.GoalContracts, extensionConditions) + contracts, err := catalog.NewObjectiveContracts(flow.ObjectiveContracts, extensionConditions) if err != nil { - return ControlProgram{}, fmt.Errorf("compile goal contracts: %w", err) + return ControlProgram{}, fmt.Errorf("compile objective contracts: %w", err) } sort.Slice(extensionIdentities, func(i, j int) bool { return extensionIdentities[i].ID < extensionIdentities[j].ID }) programIdentity := struct { @@ -523,7 +537,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error Extensions []ComponentIdentity SettingsFingerprint string Transitions []Transition - GoalContracts []catalog.GoalContract + ObjectiveContracts []catalog.ObjectiveContract Resources map[string]string }{ ProgramSchemaVersion, request.KernelVersion, @@ -531,24 +545,62 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error ComponentIdentity{ID: flow.ID, Version: flow.Version, Fingerprint: flowFingerprint}, extensionIdentities, settingsFingerprint, registry.All(), contracts.All(), resources, } - programFingerprint, err := fingerprint(programIdentity) + domainContractFingerprint, err := fingerprint(programIdentity) if err != nil { return ControlProgram{}, err } + supervisoryProgram, err := compileSupervisoryProgram(flow, request.KernelVersion, domainContractFingerprint, registry.All()) + if err != nil { + return ControlProgram{}, fmt.Errorf("compile supervisory program: %w", err) + } summary := ProgramSummary{ SchemaVersion: ProgramSchemaVersion, KernelVersion: request.KernelVersion, ProgramID: flow.ID, ProgramVersion: flow.Version, Core: programIdentity.Core, Runtime: programIdentity.Runtime, Extensions: extensionIdentities, CoreTransitionCount: len(core.Transitions), RuntimeTransitionCount: len(flow.Transitions), - ExtensionTransitionCount: extensionCount, TotalTransitionCount: registry.Len(), ProgramFingerprint: programFingerprint, + ExtensionTransitionCount: extensionCount, TotalTransitionCount: registry.Len(), ProgramFingerprint: supervisoryProgram.Fingerprint, } return ControlProgram{ - summary: summary, registry: registry, goalContracts: contracts, resourceOwnership: resources, + summary: summary, supervisoryProgram: supervisoryProgram, registry: registry, objectiveContracts: contracts, resourceOwnership: resources, settingsFingerprint: settingsFingerprint, extensions: compiledExtensions, programRuntime: compiledProgramRuntime{manifest: cloneRuntimeManifest(flow), identity: programIdentity.Runtime, runtime: flowRuntime}, }, nil } +func compileSupervisoryProgram(runtime ProgramRuntimeManifest, compatibility, domainContractFingerprint string, transitions []Transition) (general.Program, error) { + recoveredBy := make(map[TransitionID][]string) + for _, transition := range transitions { + if transition.Controllable() && transition.Interruption.Recovery != "" { + recoveredBy[transition.Interruption.Recovery] = append(recoveredBy[transition.Interruption.Recovery], string(transition.ID)) + } + } + projected := make([]general.Transition, 0, len(transitions)) + for _, transition := range transitions { + if !transition.Controllable() { + continue + } + capabilities := make([]general.Capability, 0, len(transition.RequiredCapabilities)+1) + for _, capability := range transition.RequiredCapabilities { + capabilities = append(capabilities, general.Capability(capability)) + } + facets := append([]string(nil), transition.OwnedResources...) + mutation := general.PreserveObjective + if transition.Policy.BindsRequestedObjective { + mutation = general.BindObjectiveMutation + capabilities = append(capabilities, general.Capability("objective.bind")) + facets = append(facets, "supervisor.objective") + } + projected = append(projected, general.Transition{ + ID: string(transition.ID), SourceModes: []string{"software-delivery"}, TargetMode: "software-delivery", + ObjectiveScope: transition.Policy.ObjectiveScope, ObjectiveMutation: mutation, + RequiredCapabilities: capabilities, OwnedFacets: facets, + Operation: string(transition.ID), Priority: transition.SelectionClass.Rank()*1000 + transition.Priority, + Recovers: recoveredBy[transition.ID], + }) + } + return general.CompileDomainProgram(runtime.ID, runtime.Version, compatibility, domainContractFingerprint, "software-delivery", []string{"software-delivery-marked"}, projected) +} + func validateCore(manifest CoreSystemManifest) error { if !componentID.MatchString(manifest.ID) || manifest.Version == "" || len(manifest.Transitions) == 0 || len(manifest.Capabilities) == 0 { return fmt.Errorf("CoreSystem requires semantic id, version, and transitions") @@ -562,11 +614,11 @@ func validateCore(manifest CoreSystemManifest) error { func validateProgramRuntime(manifest ProgramRuntimeManifest) error { if !componentID.MatchString(manifest.ID) || manifest.Version == "" || manifest.ProtocolVersion != ProgramRuntimeProtocolVersion || (manifest.RuntimeMode != ProgramRuntimeNative && manifest.RuntimeMode != ProgramRuntimeProtocol) || - len(manifest.Transitions) == 0 || len(manifest.SupportedGoals) == 0 || + len(manifest.Transitions) == 0 || len(manifest.SupportedObjectives) == 0 || len(manifest.Capabilities) == 0 || !validJSONObject(manifest.ConfigurationSchema) || manifest.PrivacyClassification == "" || manifest.TelemetryClassification == "" { - return fmt.Errorf("ProgramRuntime requires semantic id, version, configuration schema, goals, and transitions") + return fmt.Errorf("ProgramRuntime requires semantic id, version, configuration schema, objectives, and transitions") } declaredCapabilities, err := catalog.NormalizeCapabilities("ProgramRuntime "+manifest.ID+" capabilities", manifest.Capabilities) if err != nil { @@ -575,21 +627,21 @@ func validateProgramRuntime(manifest ProgramRuntimeManifest) error { if err := validateDeclaredSchema(manifest.ConfigurationSchema, manifest.Settings, "ProgramRuntime "+manifest.ID+" configuration"); err != nil { return err } - supported := map[GoalKind]bool{} - for _, goal := range manifest.SupportedGoals { - if !goal.Valid() || supported[goal] { - return fmt.Errorf("ProgramRuntime has invalid or duplicate goal %q", goal) + supported := map[ObjectiveKind]bool{} + for _, objective := range manifest.SupportedObjectives { + if !objective.Valid() || supported[objective] { + return fmt.Errorf("ProgramRuntime has invalid or duplicate objective %q", objective) } - supported[goal] = true + supported[objective] = true } - for _, contract := range manifest.GoalContracts { - if !supported[contract.GoalKind] { - return fmt.Errorf("ProgramRuntime goal contract %q is not supported", contract.GoalKind) + for _, contract := range manifest.ObjectiveContracts { + if !supported[contract.ObjectiveKind] { + return fmt.Errorf("ProgramRuntime objective contract %q is not supported", contract.ObjectiveKind) } - delete(supported, contract.GoalKind) + delete(supported, contract.ObjectiveKind) } if len(supported) != 0 { - return fmt.Errorf("ProgramRuntime does not define every supported goal contract") + return fmt.Errorf("ProgramRuntime does not define every supported objective contract") } for _, values := range [][]string{manifest.Facts, manifest.OwnedResources, manifest.Effects, manifest.Verifiers} { if duplicate := duplicateString(values); duplicate != "" { @@ -745,24 +797,24 @@ func validateExtension(manifest ExtensionManifest, seen, reserved map[string]boo return fmt.Errorf("extension %q cannot depend on itself", manifest.ID) } } - constrainedFacets := map[GoalKind]map[FacetName][]FacetCondition{} - for _, constraint := range manifest.GoalConstraints { - if !constraint.GoalKind.Valid() || len(constraint.Conditions) == 0 { - return fmt.Errorf("extension %q has invalid goal constraint", manifest.ID) + constrainedFacets := map[ObjectiveKind]map[FacetName][]FacetCondition{} + for _, constraint := range manifest.ObjectiveConstraints { + if !constraint.ObjectiveKind.Valid() || len(constraint.Conditions) == 0 { + return fmt.Errorf("extension %q has invalid objective constraint", manifest.ID) } for _, condition := range constraint.Conditions { if !condition.Facet.Valid() || len(condition.Statuses) == 0 || condition.Facet == model.FacetTerminal { - return fmt.Errorf("extension %q has invalid or terminal-reporting goal condition", manifest.ID) + return fmt.Errorf("extension %q has invalid or terminal-reporting objective condition", manifest.ID) } for _, status := range condition.Statuses { if !status.Valid() { - return fmt.Errorf("extension %q has invalid goal-condition status %q", manifest.ID, status) + return fmt.Errorf("extension %q has invalid objective-condition status %q", manifest.ID, status) } } - if constrainedFacets[constraint.GoalKind] == nil { - constrainedFacets[constraint.GoalKind] = map[FacetName][]FacetCondition{} + if constrainedFacets[constraint.ObjectiveKind] == nil { + constrainedFacets[constraint.ObjectiveKind] = map[FacetName][]FacetCondition{} } - constrainedFacets[constraint.GoalKind][condition.Facet] = append(constrainedFacets[constraint.GoalKind][condition.Facet], condition) + constrainedFacets[constraint.ObjectiveKind][condition.Facet] = append(constrainedFacets[constraint.ObjectiveKind][condition.Facet], condition) } } declaredRecovery := map[TransitionID]bool{} @@ -791,19 +843,19 @@ func validateExtension(manifest ExtensionManifest, seen, reserved map[string]boo return fmt.Errorf("extension transition %q targets undeclared fact %q", transition.ID, condition.Facet) } } - if transition.SelectionClass == SelectionGoalRequired { - if len(transition.GoalKinds) == 0 { - return fmt.Errorf("extension transition %q is implicitly selectable without an explicit constrained goal", transition.ID) + if transition.SelectionClass == SelectionObjectiveRequired { + if len(transition.ObjectiveKinds) == 0 { + return fmt.Errorf("extension transition %q is implicitly selectable without an explicit constrained objective", transition.ID) } - for _, goal := range transition.GoalKinds { + for _, objective := range transition.ObjectiveKinds { discharges := false for _, target := range transition.TargetConditions { - for _, obligation := range constrainedFacets[goal][target.Facet] { + for _, obligation := range constrainedFacets[objective][target.Facet] { discharges = discharges || conditionImplies(target, obligation) } } if !discharges { - return fmt.Errorf("extension transition %q is implicitly selectable for goal %q without discharging an active obligation", transition.ID, goal) + return fmt.Errorf("extension transition %q is implicitly selectable for objective %q without discharging an active obligation", transition.ID, objective) } } } @@ -977,7 +1029,7 @@ func validateDependencies(extensions []compiledExtension) error { func cloneTransition(value Transition) Transition { value.SourcePhases = append([]model.ProtocolPhase(nil), value.SourcePhases...) value.TargetPhases = append([]model.ProtocolPhase(nil), value.TargetPhases...) - value.GoalKinds = append([]model.GoalKind(nil), value.GoalKinds...) + value.ObjectiveKinds = append([]model.ObjectiveKind(nil), value.ObjectiveKinds...) value.RequiredIdentity = append([]string(nil), value.RequiredIdentity...) value.Authority = append([]catalog.AuthorityClass(nil), value.Authority...) value.AuthorityAll = append([]catalog.AuthorityClass(nil), value.AuthorityAll...) @@ -1013,10 +1065,10 @@ func cloneCoreManifest(value CoreSystemManifest) CoreSystemManifest { } func cloneRuntimeManifest(value ProgramRuntimeManifest) ProgramRuntimeManifest { - value.SupportedGoals = append([]GoalKind(nil), value.SupportedGoals...) - value.GoalContracts = append([]GoalContract(nil), value.GoalContracts...) - for index := range value.GoalContracts { - value.GoalContracts[index].Conditions = cloneConditions(value.GoalContracts[index].Conditions) + value.SupportedObjectives = append([]ObjectiveKind(nil), value.SupportedObjectives...) + value.ObjectiveContracts = append([]ObjectiveContract(nil), value.ObjectiveContracts...) + for index := range value.ObjectiveContracts { + value.ObjectiveContracts[index].Conditions = cloneConditions(value.ObjectiveContracts[index].Conditions) } value.Transitions = cloneTransitions(value.Transitions) value.Facts = append([]string(nil), value.Facts...) @@ -1035,9 +1087,9 @@ func cloneExtensionManifest(value ExtensionManifest) ExtensionManifest { value.SettingsSchema = append(json.RawMessage(nil), value.SettingsSchema...) value.Facts = append([]string(nil), value.Facts...) value.Transitions = cloneTransitions(value.Transitions) - value.GoalConstraints = append([]GoalConstraint(nil), value.GoalConstraints...) - for index := range value.GoalConstraints { - value.GoalConstraints[index].Conditions = cloneConditions(value.GoalConstraints[index].Conditions) + value.ObjectiveConstraints = append([]ObjectiveConstraint(nil), value.ObjectiveConstraints...) + for index := range value.ObjectiveConstraints { + value.ObjectiveConstraints[index].Conditions = cloneConditions(value.ObjectiveConstraints[index].Conditions) } value.OwnedResources = append([]string(nil), value.OwnedResources...) value.Effects = append([]string(nil), value.Effects...) diff --git a/boatstack/control/control_test.go b/boatstack/delivery/control_test.go similarity index 65% rename from boatstack/control/control_test.go rename to boatstack/delivery/control_test.go index b33070e..3ec27ba 100644 --- a/boatstack/control/control_test.go +++ b/boatstack/delivery/control_test.go @@ -1,4 +1,4 @@ -package control_test +package delivery_test import ( "context" @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/distribution" "github.com/operatorstack/boatstack/boatstack/extension" "github.com/operatorstack/boatstack/boatstack/extension/releasenote" @@ -15,10 +15,10 @@ import ( ) type staticProgramRuntimeDefinition struct { - manifest control.ProgramRuntimeManifest + manifest delivery.ProgramRuntimeManifest } -func (f staticProgramRuntimeDefinition) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) { +func (f staticProgramRuntimeDefinition) RuntimeManifest(context.Context) (delivery.ProgramRuntimeManifest, error) { return f.manifest, nil } @@ -36,6 +36,19 @@ func TestStandardProgramHasExplicitStableComposition(t *testing.T) { t.Fatalf("identical compilation drifted: %s != %s", one.Fingerprint(), two.Fingerprint()) } summary := one.Summary() + supervisory := one.SupervisoryProgram() + if err := supervisory.Validate(); err != nil || supervisory.Fingerprint != summary.ProgramFingerprint || supervisory.DomainContractFingerprint == "" { + t.Fatalf("software program is not bound to one kernel identity: program=%+v summary=%+v err=%v", supervisory.Identity(), summary, err) + } + controllable := 0 + for _, transition := range one.Transitions() { + if transition.Controllable() { + controllable++ + } + } + if len(supervisory.Transitions) != controllable { + t.Fatalf("kernel program transition count = %d, want %d controllable software transitions", len(supervisory.Transitions), controllable) + } if summary.CoreTransitionCount != 33 || summary.RuntimeTransitionCount != 30 || summary.ExtensionTransitionCount != 0 || summary.TotalTransitionCount != 63 { t.Fatalf("compiled counts = %+v", summary) } @@ -62,7 +75,7 @@ func TestProgramFingerprintBindsCompositionAndPolicyInputs(t *testing.T) { if err != nil { t.Fatal(err) } - variants := map[string]control.ExtensionManifest{} + variants := map[string]delivery.ExtensionManifest{} version := cloneManifest(t, manifest) version.Version = "1.0.1" variants["extension-version"] = version @@ -87,19 +100,19 @@ func TestProgramFingerprintBindsCompositionAndPolicyInputs(t *testing.T) { recovery := cloneManifest(t, manifest) recoveryTransition := manifest.Transitions[0] recoveryTransition.ID = "boatstack.release-note.recover" - recoveryTransition.Class = control.EventRecovery - recoveryTransition.SelectionClass = control.SelectionExtensionRecovery - recoveryTransition.SourcePhases = []control.ProtocolPhase{control.PhaseRecovery} - recoveryTransition.TargetPhases = []control.ProtocolPhase{control.PhaseActive} + recoveryTransition.Class = delivery.EventRecovery + recoveryTransition.SelectionClass = delivery.SelectionExtensionRecovery + recoveryTransition.SourcePhases = []delivery.ProtocolPhase{delivery.PhaseRecovery} + recoveryTransition.TargetPhases = []delivery.ProtocolPhase{delivery.PhaseActive} recoveryTransition.Effect = "boatstack.release-note.recover-effect" - recoveryTransition.LocalEffects = []control.EffectID{recoveryTransition.Effect} + recoveryTransition.LocalEffects = []delivery.EffectID{recoveryTransition.Effect} recoveryTransition.Verifier = "boatstack.release-note.recover-verifier" recoveryTransition.Interruption.Recovery = "recovery.escalate" recoveryTransition.Priority = 0 recovery.Transitions = append(recovery.Transitions, recoveryTransition) recovery.Effects = append(recovery.Effects, string(recoveryTransition.Effect)) recovery.Verifiers = append(recovery.Verifiers, recoveryTransition.Verifier) - recovery.RecoveryTransitions = []control.TransitionID{recoveryTransition.ID} + recovery.RecoveryTransitions = []delivery.TransitionID{recoveryTransition.ID} variants["recovery-declaration"] = recovery for name, variant := range variants { @@ -118,11 +131,11 @@ func TestProgramFingerprintBindsCompositionAndPolicyInputs(t *testing.T) { }) } - policyOne, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: standard.Definition(), Settings: map[string]any{"policy": "one"}}) + policyOne, err := delivery.Compile(context.Background(), delivery.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: standard.Definition(), Settings: map[string]any{"policy": "one"}}) if err != nil { t.Fatal(err) } - policyTwo, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: standard.Definition(), Settings: map[string]any{"policy": "two"}}) + policyTwo, err := delivery.Compile(context.Background(), delivery.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: standard.Definition(), Settings: map[string]any{"policy": "two"}}) if err != nil { t.Fatal(err) } @@ -166,7 +179,7 @@ func TestCompileEnforcesDeclaredComponentSchemas(t *testing.T) { } flow.ConfigurationSchema = json.RawMessage(`{"type":"object","required":["mode"],"additionalProperties":false}`) flow.Settings = json.RawMessage(`{}`) - if _, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: staticProgramRuntimeDefinition{manifest: flow}}); err == nil { + if _, err := delivery.Compile(context.Background(), delivery.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: staticProgramRuntimeDefinition{manifest: flow}}); err == nil { t.Fatal("ProgramRuntime settings that violate ConfigurationSchema compiled") } } @@ -178,7 +191,7 @@ func TestComponentsMustDeclareTheirOwnSelectionSemantics(t *testing.T) { t.Fatal(err) } flow.Transitions[0].SelectionClass = "" - if _, err := control.Compile(context.Background(), control.CompileRequest{ + if _, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: "kernel", Core: core.System(), Runtime: staticFlow{manifest: flow}, }); err == nil || !strings.Contains(err.Error(), "selection class") { t.Fatalf("flow without an explicit selection class was accepted: %v", err) @@ -188,8 +201,8 @@ func TestComponentsMustDeclareTheirOwnSelectionSemantics(t *testing.T) { if err != nil { t.Fatal(err) } - flow.Transitions[0].SelectionClass = control.SelectionSystemRecovery - if _, err := control.Compile(context.Background(), control.CompileRequest{ + flow.Transitions[0].SelectionClass = delivery.SelectionSystemRecovery + if _, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: "kernel", Core: core.System(), Runtime: staticFlow{manifest: flow}, }); err == nil || !strings.Contains(err.Error(), "SYSTEM_RECOVERY") { t.Fatalf("flow claimed CoreSystem recovery precedence: %v", err) @@ -218,7 +231,7 @@ func TestProgramRuntimeCannotClaimCoreSystemResources(t *testing.T) { } flow.OwnedResources = append(flow.OwnedResources, coreResource) flow.Transitions[0].OwnedResources = append(flow.Transitions[0].OwnedResources, coreResource) - if _, err := control.Compile(context.Background(), control.CompileRequest{ + if _, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: "kernel", Core: core.System(), Runtime: staticFlow{manifest: flow}, }); err == nil || !strings.Contains(err.Error(), "overlapping owners") { t.Fatalf("ProgramRuntime claimed CoreSystem resource %q: %v", coreResource, err) @@ -232,38 +245,38 @@ func TestExtensionCompilationRejectsBoundaryViolations(t *testing.T) { if err != nil { t.Fatal(err) } - cases := map[string]func(*control.ExtensionManifest){ - "reserved-component-id": func(value *control.ExtensionManifest) { value.ID = standard.ID }, - "unnamespaced-fact": func(value *control.ExtensionManifest) { value.Facts[0] = "present" }, - "raw-priority": func(value *control.ExtensionManifest) { value.Transitions[0].Priority = 7 }, - "undeclared-effect": func(value *control.ExtensionManifest) { value.Effects = nil }, - "undeclared-verifier": func(value *control.ExtensionManifest) { value.Verifiers = nil }, - "phantom-recovery": func(value *control.ExtensionManifest) { - value.RecoveryTransitions = []control.TransitionID{"boatstack.release-note.missing-recovery"} + cases := map[string]func(*delivery.ExtensionManifest){ + "reserved-component-id": func(value *delivery.ExtensionManifest) { value.ID = standard.ID }, + "unnamespaced-fact": func(value *delivery.ExtensionManifest) { value.Facts[0] = "present" }, + "raw-priority": func(value *delivery.ExtensionManifest) { value.Transitions[0].Priority = 7 }, + "undeclared-effect": func(value *delivery.ExtensionManifest) { value.Effects = nil }, + "undeclared-verifier": func(value *delivery.ExtensionManifest) { value.Verifiers = nil }, + "phantom-recovery": func(value *delivery.ExtensionManifest) { + value.RecoveryTransitions = []delivery.TransitionID{"boatstack.release-note.missing-recovery"} }, - "invalid-goal-status": func(value *control.ExtensionManifest) { - value.GoalConstraints[0].Conditions[0].Statuses = []control.FactStatus{"invented"} + "invalid-objective-status": func(value *delivery.ExtensionManifest) { + value.ObjectiveConstraints[0].Conditions[0].Statuses = []delivery.FactStatus{"invented"} }, - "goal-selection-without-matching-obligation": func(value *control.ExtensionManifest) { - value.Transitions[0].GoalKinds = []control.GoalKind{control.GoalVerified} + "objective-selection-without-matching-obligation": func(value *delivery.ExtensionManifest) { + value.Transitions[0].ObjectiveKinds = []delivery.ObjectiveKind{delivery.ObjectiveVerified} }, - "goal-selection-does-not-discharge-obligation": func(value *control.ExtensionManifest) { + "objective-selection-does-not-discharge-obligation": func(value *delivery.ExtensionManifest) { value.Transitions[0].TargetConditions[0].Values = []string{"missing"} }, - "recovery-selection-on-progress": func(value *control.ExtensionManifest) { - value.Transitions[0].SelectionClass = control.SelectionExtensionRecovery + "recovery-selection-on-progress": func(value *delivery.ExtensionManifest) { + value.Transitions[0].SelectionClass = delivery.SelectionExtensionRecovery }, - "program-reconciliation-claim": func(value *control.ExtensionManifest) { + "program-reconciliation-claim": func(value *delivery.ExtensionManifest) { value.Transitions[0].Policy.ReconcilesProgram = true }, - "requested-goal-binding-claim": func(value *control.ExtensionManifest) { - value.Transitions[0].Policy.BindsRequestedGoal = true + "requested-objective-binding-claim": func(value *delivery.ExtensionManifest) { + value.Transitions[0].Policy.BindsRequestedObjective = true }, - "foreign-target": func(value *control.ExtensionManifest) { - value.Transitions[0].TargetConditions = []control.FacetCondition{control.KnownCondition(control.FacetPlan, "approved")} + "foreign-target": func(value *delivery.ExtensionManifest) { + value.Transitions[0].TargetConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetPlan, "approved")} }, - "undeclared-owned-target": func(value *control.ExtensionManifest) { - value.Transitions[0].TargetConditions = []control.FacetCondition{control.KnownCondition(control.FacetName(value.ID+".phantom"), "verified")} + "undeclared-owned-target": func(value *delivery.ExtensionManifest) { + value.Transitions[0].TargetConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetName(value.ID+".phantom"), "verified")} }, } for name, mutate := range cases { @@ -295,21 +308,21 @@ func TestControlProgramAccessorsCannotMutateCompiledBytes(t *testing.T) { } originalFingerprint := program.Fingerprint() transitions := program.Transitions() - transitions[0].SourcePhases[0] = control.PhaseAbandoned + transitions[0].SourcePhases[0] = delivery.PhaseAbandoned transitions[0].SourceConditions[0].Values = []string{"mutated"} extensions := program.Extensions() extensions[0].Manifest.Facts[0] = "mutated.fact.id" extensions[0].Manifest.Settings = json.RawMessage(`{"mutated":true}`) flow := program.ProgramRuntime() - flow.Manifest.GoalContracts[0].Conditions[0].Values = []string{"mutated"} + flow.Manifest.ObjectiveContracts[0].Conditions[0].Values = []string{"mutated"} - if program.Fingerprint() != originalFingerprint || program.Transitions()[0].SourcePhases[0] == control.PhaseAbandoned || - program.Extensions()[0].Manifest.Facts[0] == "mutated.fact.id" || program.ProgramRuntime().Manifest.GoalContracts[0].Conditions[0].Values[0] == "mutated" { + if program.Fingerprint() != originalFingerprint || program.Transitions()[0].SourcePhases[0] == delivery.PhaseAbandoned || + program.Extensions()[0].Manifest.Facts[0] == "mutated.fact.id" || program.ProgramRuntime().Manifest.ObjectiveContracts[0].Conditions[0].Values[0] == "mutated" { t.Fatal("public accessor mutated the compiled ControlProgram") } } -func TestExtensionGoalConditionsAreConjunctive(t *testing.T) { +func TestExtensionObjectiveConditionsAreConjunctive(t *testing.T) { // control-law: extension-terminal-set-is-a-subset-of-control-program-terminal-set base, err := distribution.StandardProgram(context.Background()) if err != nil { @@ -319,39 +332,39 @@ func TestExtensionGoalConditionsAreConjunctive(t *testing.T) { if err != nil { t.Fatal(err) } - conditionCount := func(program control.ControlProgram) int { - for _, contract := range program.RuntimeGoalContracts().All() { - if contract.GoalKind == control.GoalVerified { + conditionCount := func(program delivery.ControlProgram) int { + for _, contract := range program.RuntimeObjectiveContracts().All() { + if contract.ObjectiveKind == delivery.ObjectiveVerified { return len(contract.Conditions) } } return 0 } if conditionCount(extended) != conditionCount(base) { - t.Fatalf("release-note extension unexpectedly changed the verified goal") + t.Fatalf("release-note extension unexpectedly changed the verified objective") } - for _, goal := range []control.GoalKind{control.GoalOpenPR, control.GoalMerged} { + for _, objective := range []delivery.ObjectiveKind{delivery.ObjectiveOpenPR, delivery.ObjectiveMerged} { baseCount, extendedCount := 0, 0 - for _, contract := range base.RuntimeGoalContracts().All() { - if contract.GoalKind == goal { + for _, contract := range base.RuntimeObjectiveContracts().All() { + if contract.ObjectiveKind == objective { baseCount = len(contract.Conditions) } } - for _, contract := range extended.RuntimeGoalContracts().All() { - if contract.GoalKind == goal { + for _, contract := range extended.RuntimeObjectiveContracts().All() { + if contract.ObjectiveKind == objective { extendedCount = len(contract.Conditions) } } if extendedCount != baseCount+1 { - t.Fatalf("goal %s conditions: base=%d extended=%d", goal, baseCount, extendedCount) + t.Fatalf("objective %s conditions: base=%d extended=%d", objective, baseCount, extendedCount) } } } func TestExtensionOrderDoesNotChangeProgramIdentity(t *testing.T) { // control-law: extension-observation-and-compilation-order-is-deterministic - left := declarationOnlyExtension{id: "example.left", goalConditions: []control.FacetCondition{control.KnownCondition(control.FacetPlan, "locked")}} - right := declarationOnlyExtension{id: "example.right", goalConditions: []control.FacetCondition{control.KnownCondition(control.FacetConfiguration, "verified")}} + left := declarationOnlyExtension{id: "example.left", objectiveConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetPlan, "locked")}} + right := declarationOnlyExtension{id: "example.right", objectiveConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetConfiguration, "verified")}} one, err := distribution.StandardProgram(context.Background(), left, right) if err != nil { t.Fatal(err) @@ -366,41 +379,41 @@ func TestExtensionOrderDoesNotChangeProgramIdentity(t *testing.T) { } type declarationOnlyExtension struct { - id string - dependencies []string - goalConditions []control.FacetCondition + id string + dependencies []string + objectiveConditions []delivery.FacetCondition } type staticFlow struct { - manifest control.ProgramRuntimeManifest + manifest delivery.ProgramRuntimeManifest } -func (s staticFlow) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) { +func (s staticFlow) RuntimeManifest(context.Context) (delivery.ProgramRuntimeManifest, error) { return s.manifest, nil } -func cloneManifest(t *testing.T, value control.ExtensionManifest) control.ExtensionManifest { +func cloneManifest(t *testing.T, value delivery.ExtensionManifest) delivery.ExtensionManifest { t.Helper() raw, err := json.Marshal(value) if err != nil { t.Fatal(err) } - var result control.ExtensionManifest + var result delivery.ExtensionManifest if err := json.Unmarshal(raw, &result); err != nil { t.Fatal(err) } return result } -func (e declarationOnlyExtension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) { - var constraints []control.GoalConstraint - if len(e.goalConditions) != 0 { - constraints = []control.GoalConstraint{{GoalKind: control.GoalOpenPR, Conditions: append([]control.FacetCondition(nil), e.goalConditions...)}} +func (e declarationOnlyExtension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, error) { + var constraints []delivery.ObjectiveConstraint + if len(e.objectiveConditions) != 0 { + constraints = []delivery.ObjectiveConstraint{{ObjectiveKind: delivery.ObjectiveOpenPR, Conditions: append([]delivery.FacetCondition(nil), e.objectiveConditions...)}} } - return control.ExtensionManifest{ - ID: e.id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion, + return delivery.ExtensionManifest{ + ID: e.id, Version: "1.0.0", ProtocolVersion: delivery.ExtensionProtocolVersion, SettingsSchema: json.RawMessage(`{"type":"object"}`), PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", - Dependencies: append([]string(nil), e.dependencies...), GoalConstraints: constraints, + Dependencies: append([]string(nil), e.dependencies...), ObjectiveConstraints: constraints, }, nil } diff --git a/boatstack/control/extension.go b/boatstack/delivery/extension.go similarity index 99% rename from boatstack/control/extension.go rename to boatstack/delivery/extension.go index 1112ab2..d9b9742 100644 --- a/boatstack/control/extension.go +++ b/boatstack/delivery/extension.go @@ -1,4 +1,4 @@ -package control +package delivery import ( "context" diff --git a/boatstack/control/program_manifest.go b/boatstack/delivery/program_manifest.go similarity index 89% rename from boatstack/control/program_manifest.go rename to boatstack/delivery/program_manifest.go index 68d8618..0ae51a5 100644 --- a/boatstack/control/program_manifest.go +++ b/boatstack/delivery/program_manifest.go @@ -1,4 +1,4 @@ -package control +package delivery import ( "bytes" @@ -10,15 +10,17 @@ import ( "strconv" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" ) type ProgramErrorCode string const ( - ProgramInvalid ProgramErrorCode = "PROGRAM_INVALID" - ProgramSchemaUnsupported ProgramErrorCode = "PROGRAM_SCHEMA_UNSUPPORTED" - RuntimeTooOld ProgramErrorCode = "RUNTIME_TOO_OLD" + ProgramInvalid ProgramErrorCode = "PROGRAM_INVALID" + ProgramSchemaUnsupported ProgramErrorCode = "PROGRAM_SCHEMA_UNSUPPORTED" + RuntimeTooOld ProgramErrorCode = "RUNTIME_TOO_OLD" + manifestProgramVersion = "manifest" + manifestKernelCompatibility = "general-kernel" ) type ProgramError struct { @@ -49,7 +51,7 @@ type ProgramTransition struct { Class EventClass `json:"class"` SourcePhases []ProtocolPhase `json:"source_phases"` TargetPhases []ProtocolPhase `json:"target_phases"` - GoalKinds []GoalKind `json:"goal_kinds,omitempty"` + ObjectiveKinds []ObjectiveKind `json:"objective_kinds,omitempty"` RequiredIdentity []string `json:"required_identity"` Authority []AuthorityClass `json:"authority"` AuthorityAll []AuthorityClass `json:"authority_all,omitempty"` @@ -85,14 +87,14 @@ type ProgramTransition struct { // ProgramManifest is the complete source representation of one Control // Program. Product surfaces may call the complete program a Flow. type ProgramManifest struct { - SchemaVersion int `json:"schema_version"` - ProgramID string `json:"program_id"` - ProgramVersion string `json:"program_version"` - RequiresRuntime string `json:"requires_runtime"` - Capabilities ProgramCapabilities `json:"capabilities"` - OwnedResources []string `json:"owned_resources"` - GoalContracts []GoalContract `json:"goal_contracts"` - Transitions []ProgramTransition `json:"transitions"` + SchemaVersion int `json:"schema_version"` + ProgramID string `json:"program_id"` + ProgramVersion string `json:"program_version"` + RequiresRuntime string `json:"requires_runtime"` + Capabilities ProgramCapabilities `json:"capabilities"` + OwnedResources []string `json:"owned_resources"` + ObjectiveContracts []ObjectiveContract `json:"objective_contracts"` + Transitions []ProgramTransition `json:"transitions"` } // RuntimeCompatibility is verified runtime evidence. Declaring a capability @@ -228,7 +230,7 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co } source.Interruption.Recovery = TransitionID(manifest.ProgramID + "/" + string(source.Interruption.Recovery)) } - if source.Policy.BindsRequestedGoal || source.Policy.ReconcilesProgram { + if source.Policy.BindsRequestedObjective || source.Policy.ReconcilesProgram { return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].policy", index), "runtime-reserved program mutation policy is not repository-declarable") } if source.Controllable() && !containsDeclaration(effects, string(source.Effect)) { @@ -265,17 +267,17 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co return ControlProgram{}, invalidProgram("owned_resources", fmt.Sprintf("unused declaration %q", extra)) } - contracts := append([]GoalContract(nil), manifest.GoalContracts...) + contracts := append([]ObjectiveContract(nil), manifest.ObjectiveContracts...) for index := range contracts { contracts[index].Conditions, err = normalizeConditionSet(contracts[index].Conditions) if err != nil { - return ControlProgram{}, invalidProgram(fmt.Sprintf("goal_contracts[%d].conditions", index), err.Error()) + return ControlProgram{}, invalidProgram(fmt.Sprintf("objective_contracts[%d].conditions", index), err.Error()) } } - sort.Slice(contracts, func(i, j int) bool { return contracts[i].GoalKind < contracts[j].GoalKind }) - goalContracts, err := catalog.NewGoalContracts(contracts, nil) + sort.Slice(contracts, func(i, j int) bool { return contracts[i].ObjectiveKind < contracts[j].ObjectiveKind }) + objectiveContracts, err := catalog.NewObjectiveContracts(contracts, nil) if err != nil { - return ControlProgram{}, invalidProgram("goal_contracts", err.Error()) + return ControlProgram{}, invalidProgram("objective_contracts", err.Error()) } canonicalTransitions := cloneTransitionsForProgram(normalized) @@ -285,18 +287,28 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co } sort.Slice(canonicalTransitions, func(i, j int) bool { return canonicalTransitions[i].ID < canonicalTransitions[j].ID }) canonical := struct { - ProgramID string - Capabilities ProgramCapabilities - OwnedResources []string - GoalContracts []GoalContract - Transitions []Transition - }{manifest.ProgramID, ProgramCapabilities{Effects: effects, Verifiers: verifiers, CapabilitySurface: declaredCapabilities}, resources, goalContracts.All(), canonicalTransitions} - fingerprint, err := fingerprint(canonical) + ProgramID string + Capabilities ProgramCapabilities + OwnedResources []string + ObjectiveContracts []ObjectiveContract + Transitions []Transition + }{manifest.ProgramID, ProgramCapabilities{Effects: effects, Verifiers: verifiers, CapabilitySurface: declaredCapabilities}, resources, objectiveContracts.All(), canonicalTransitions} + domainContractFingerprint, err := fingerprint(canonical) if err != nil { return ControlProgram{}, invalidProgram("", "fingerprint canonical program: "+err.Error()) } + supervisoryProgram, err := compileSupervisoryProgram( + ProgramRuntimeManifest{ID: manifest.ProgramID, Version: manifestProgramVersion}, + manifestKernelCompatibility, + domainContractFingerprint, + normalized, + ) + if err != nil { + return ControlProgram{}, invalidProgram("transitions", "compile supervisory program: "+err.Error()) + } + programFingerprint := supervisoryProgram.Fingerprint for index := range normalized { - normalized[index].Origin.ManifestFingerprint = fingerprint + normalized[index].Origin.ManifestFingerprint = programFingerprint } registry, err := catalog.New(normalized) if err != nil { @@ -306,14 +318,14 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co for _, resource := range resources { ownership[resource] = manifest.ProgramID } - identity := ComponentIdentity{ID: manifest.ProgramID, Version: manifest.ProgramVersion, Fingerprint: fingerprint} + identity := ComponentIdentity{ID: manifest.ProgramID, Version: manifest.ProgramVersion, Fingerprint: programFingerprint} return ControlProgram{ summary: ProgramSummary{ SchemaVersion: ProgramSchemaVersion, KernelVersion: runtime.Version, ProgramID: manifest.ProgramID, ProgramVersion: manifest.ProgramVersion, RequiresRuntime: manifest.RequiresRuntime, - Runtime: identity, RuntimeTransitionCount: registry.Len(), TotalTransitionCount: registry.Len(), ProgramFingerprint: fingerprint, + Runtime: identity, RuntimeTransitionCount: registry.Len(), TotalTransitionCount: registry.Len(), ProgramFingerprint: programFingerprint, }, - registry: registry, goalContracts: goalContracts, resourceOwnership: ownership, + supervisoryProgram: supervisoryProgram, registry: registry, objectiveContracts: objectiveContracts, resourceOwnership: ownership, programRuntime: compiledProgramRuntime{identity: identity}, }, nil } @@ -321,7 +333,7 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co func (value ProgramTransition) runtimeTransition() Transition { return Transition{ ID: value.ID, Version: value.Version, SelectionClass: value.SelectionClass, Class: value.Class, - SourcePhases: value.SourcePhases, TargetPhases: value.TargetPhases, GoalKinds: value.GoalKinds, + SourcePhases: value.SourcePhases, TargetPhases: value.TargetPhases, ObjectiveKinds: value.ObjectiveKinds, RequiredIdentity: value.RequiredIdentity, Authority: value.Authority, AuthorityAll: value.AuthorityAll, RequiredCapabilities: value.RequiredCapabilities, RequiredEvidence: value.RequiredEvidence, OwnedResources: value.OwnedResources, Effect: value.Effect, @@ -433,7 +445,7 @@ func normalizeProgramTransition(value Transition) (Transition, error) { if err != nil { return Transition{}, err } - value.GoalKinds, err = uniqueSorted(value.GoalKinds, func(v GoalKind) string { return string(v) }) + value.ObjectiveKinds, err = uniqueSorted(value.ObjectiveKinds, func(v ObjectiveKind) string { return string(v) }) if err != nil { return Transition{}, err } diff --git a/boatstack/delivery/program_manifest_test.go b/boatstack/delivery/program_manifest_test.go new file mode 100644 index 0000000..40f5e40 --- /dev/null +++ b/boatstack/delivery/program_manifest_test.go @@ -0,0 +1,323 @@ +package delivery_test + +import ( + "bytes" + "encoding/json" + "errors" + "sort" + "strings" + "testing" + + boatstack "github.com/operatorstack/boatstack/boatstack" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" +) + +func TestProgramManifestCanonicalFingerprintContract(t *testing.T) { + // control-law: validated-executable-semantics-exactly-determine-program-fingerprint + base := programFixture() + one := loadManifest(t, base) + + equivalent := programFixture() + equivalent.ProgramVersion = "99" + equivalent.RequiresRuntime = ">=0.9.0" + reverse(equivalent.Capabilities.Effects) + reverse(equivalent.Capabilities.Verifiers) + reverse(equivalent.Capabilities.CapabilitySurface) + reverse(equivalent.OwnedResources) + reverse(equivalent.Transitions) + reverse(equivalent.Transitions[0].RequiredIdentity) + reverse(equivalent.Transitions[0].SourceConditions) + two := loadManifest(t, equivalent) + if one.Fingerprint() != two.Fingerprint() { + t.Fatalf("representation-only or author-version changes changed executable fingerprint: %s != %s", one.Fingerprint(), two.Fingerprint()) + } + + raw, err := json.MarshalIndent(base, "", " ") + if err != nil { + t.Fatal(err) + } + three, err := delivery.LoadProgram(bytes.NewReader(raw), runtimeFixture()) + if err != nil { + t.Fatal(err) + } + if one.Fingerprint() != three.Fingerprint() { + t.Fatal("whitespace changed executable fingerprint") + } + if err := three.SupervisoryProgram().Validate(); err != nil { + t.Fatalf("validated manifest exposed an invalid supervisory program: %v", err) + } + if three.SupervisoryProgram().Fingerprint != three.Fingerprint() { + t.Fatal("manifest and supervisory program fingerprints diverged") + } + reordered := reorderTopLevelObject(t, raw) + four, err := delivery.LoadProgram(bytes.NewReader(reordered), runtimeFixture()) + if err != nil { + t.Fatal(err) + } + if one.Fingerprint() != four.Fingerprint() { + t.Fatal("JSON object key order changed executable fingerprint") + } + + mutations := map[string]func(*delivery.ProgramManifest){ + "program-id": func(value *delivery.ProgramManifest) { value.ProgramID = "alternate-program" }, + "source-phase": func(value *delivery.ProgramManifest) { + value.Transitions[0].SourcePhases = []delivery.ProtocolPhase{delivery.PhaseObserved} + }, + "target-phase": func(value *delivery.ProgramManifest) { + value.Transitions[0].TargetPhases = []delivery.ProtocolPhase{delivery.PhaseFrontier} + }, + "authority": func(value *delivery.ProgramManifest) { + value.Transitions[0].Authority = []delivery.AuthorityClass{delivery.AuthorityHuman} + }, + "capability-surface": func(value *delivery.ProgramManifest) { + value.Capabilities.CapabilitySurface = append(value.Capabilities.CapabilitySurface, delivery.CapabilityHumanApprove) + }, + "effect": func(value *delivery.ProgramManifest) { + value.Capabilities.Effects[0] = "alternate.effect" + value.Transitions[0].Effect = "alternate.effect" + value.Transitions[0].LocalEffects = []delivery.EffectID{"alternate.effect"} + }, + "verifier": func(value *delivery.ProgramManifest) { + value.Capabilities.Verifiers[1] = "alternate.verifier" + value.Transitions[0].Verifier = "alternate.verifier" + }, + "postcondition": func(value *delivery.ProgramManifest) { + value.Transitions[0].TargetConditions[0].Values = []string{"alternate"} + }, + "recovery": func(value *delivery.ProgramManifest) { + value.Transitions[0].Interruption.Recovery = "alternate.recover" + }, + "priority": func(value *delivery.ProgramManifest) { value.Transitions[0].Priority++ }, + } + for name, mutate := range mutations { + t.Run(name, func(t *testing.T) { + candidate := programFixture() + mutate(&candidate) + if name == "recovery" { + recovery := candidate.Transitions[1] + recovery.ID = "alternate.recover" + recovery.Interruption.Recovery = "alternate.recover" + candidate.Transitions = append(candidate.Transitions, recovery) + } + program := loadManifest(t, candidate) + if program.Fingerprint() == one.Fingerprint() { + t.Fatalf("%s semantic change did not change fingerprint", name) + } + }) + } +} + +func TestProgramManifestNamespaceAndCompatibilityBoundary(t *testing.T) { + // control-law: only-compatible-validated-programs-reach-the-runtime-registry + first := programFixture() + first.ProgramID = "first-program" + second := programFixture() + second.ProgramID = "second-program" + one := loadManifest(t, first) + two := loadManifest(t, second) + if one.Transitions()[0].ID == two.Transitions()[0].ID { + t.Fatal("equal local transition IDs collided across programs") + } + for _, transition := range one.Transitions() { + if !strings.HasPrefix(string(transition.ID), "first-program/") { + t.Fatalf("transition is not program-qualified: %s", transition.ID) + } + if !transition.RuntimeExecution { + t.Fatalf("repository-authored transition %s escaped protocol-runtime classification", transition.ID) + } + } + + cases := []struct { + name string + mutate func(*delivery.ProgramManifest) + runtime delivery.RuntimeCompatibility + code delivery.ProgramErrorCode + }{ + {"unsupported-schema", func(value *delivery.ProgramManifest) { value.SchemaVersion = delivery.ProgramSchemaVersion + 1 }, runtimeFixture(), delivery.ProgramSchemaUnsupported}, + {"invalid-schema", func(value *delivery.ProgramManifest) { value.SchemaVersion = 0 }, runtimeFixture(), delivery.ProgramInvalid}, + {"runtime-too-old", func(value *delivery.ProgramManifest) { value.RequiresRuntime = ">=2.0.0" }, runtimeFixture(), delivery.RuntimeTooOld}, + {"malformed-runtime", func(value *delivery.ProgramManifest) { value.RequiresRuntime = "^1" }, runtimeFixture(), delivery.ProgramInvalid}, + {"malformed-program-id", func(value *delivery.ProgramManifest) { value.ProgramID = "../program" }, runtimeFixture(), delivery.ProgramInvalid}, + {"ambiguous-transition-id", func(value *delivery.ProgramManifest) { value.Transitions[0].ID = "other/advance" }, runtimeFixture(), delivery.ProgramInvalid}, + {"duplicate-transition", func(value *delivery.ProgramManifest) { + value.Transitions = append(value.Transitions, value.Transitions[0]) + }, runtimeFixture(), delivery.ProgramInvalid}, + {"duplicate-capability", func(value *delivery.ProgramManifest) { + value.Capabilities.Effects = append(value.Capabilities.Effects, value.Capabilities.Effects[0]) + }, runtimeFixture(), delivery.ProgramInvalid}, + {"unknown-authority-capability", func(value *delivery.ProgramManifest) { + value.Capabilities.CapabilitySurface = []delivery.Capability{"production.nuke"} + }, runtimeFixture(), delivery.ProgramInvalid}, + {"under-declared-kernel-effect", func(value *delivery.ProgramManifest) { + value.Capabilities.CapabilitySurface = []delivery.Capability{delivery.CapabilityRepositoryWrite} + }, runtimeFixture(), delivery.ProgramInvalid}, + {"duplicate-condition", func(value *delivery.ProgramManifest) { + value.Transitions[0].SourceConditions = append(value.Transitions[0].SourceConditions, value.Transitions[0].SourceConditions[0]) + }, runtimeFixture(), delivery.ProgramInvalid}, + {"missing-runtime-capability", func(*delivery.ProgramManifest) {}, delivery.RuntimeCompatibility{Version: "v1.2.3"}, delivery.ProgramInvalid}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + manifest := programFixture() + test.mutate(&manifest) + _, err := delivery.ValidateProgram(manifest, test.runtime) + var programErr delivery.ProgramError + if !errors.As(err, &programErr) || programErr.Code != test.code { + t.Fatalf("error = %v, want %s", err, test.code) + } + }) + } + + exact := programFixture() + exact.RequiresRuntime = ">=1.2.3" + if _, err := delivery.ValidateProgram(exact, runtimeFixture()); err != nil { + t.Fatalf("exact minimum was rejected: %v", err) + } + above := programFixture() + above.RequiresRuntime = ">=1.0.0" + if _, err := delivery.ValidateProgram(above, runtimeFixture()); err != nil { + t.Fatalf("runtime above minimum was rejected: %v", err) + } + prerelease := programFixture() + prerelease.RequiresRuntime = ">=1.2.3" + candidateRuntime := runtimeFixture() + candidateRuntime.Version = "v1.2.3-dev" + if _, err := delivery.ValidateProgram(prerelease, candidateRuntime); err == nil { + t.Fatal("prerelease runtime incorrectly satisfied the matching stable minimum") + } +} + +func TestProgramSourceParserFailsClosed(t *testing.T) { + // control-law: uninterpreted-source-cannot-reach-the-executable-program + manifest := programFixture() + raw, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + unknown := bytes.Replace(raw, []byte(`"program_id"`), []byte(`"requires_human":true,"program_id"`), 1) + if _, err := delivery.LoadProgram(bytes.NewReader(unknown), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "unknown field") { + t.Fatalf("unknown control-law field was not rejected: %v", err) + } + unknownTransition := bytes.Replace(raw, []byte(`"priority":1`), []byte(`"requires_human":true,"priority":1`), 1) + if _, err := delivery.LoadProgram(bytes.NewReader(unknownTransition), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "unknown field") { + t.Fatalf("unknown transition field was not rejected: %v", err) + } + duplicate := bytes.Replace(raw, []byte(`"program_id":"test-program"`), []byte(`"program_id":"test-program","program_id":"weaker-program"`), 1) + if _, err := delivery.LoadProgram(bytes.NewReader(duplicate), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "duplicate JSON field") { + t.Fatalf("duplicate JSON field was not rejected: %v", err) + } + if _, err := delivery.LoadProgram(bytes.NewReader(append(raw, []byte(` {}`)...)), runtimeFixture()); err == nil { + t.Fatal("trailing JSON was accepted") + } +} + +func TestValidatedProgramIsTheKernelRegistry(t *testing.T) { + // control-law: kernel-consumes-the-exact-validated-fingerprinted-registry + program := loadManifest(t, programFixture()) + kernel, err := boatstack.NewDeliveryController(t.TempDir(), program) + if err != nil { + t.Fatal(err) + } + response, err := kernel.Handle(t.Context(), surfaces.Request{SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationCatalog}) + if err != nil { + t.Fatal(err) + } + if len(response.Catalog) != len(programFixture().Transitions) { + t.Fatalf("kernel catalog count = %d", len(response.Catalog)) + } + for _, transition := range response.Catalog { + if !strings.HasPrefix(string(transition.ID), "test-program/") || transition.Origin.ManifestFingerprint != program.Fingerprint() { + t.Fatalf("kernel reached around validated identity: %+v", transition) + } + } +} + +func programFixture() delivery.ProgramManifest { + recovery := delivery.ProgramTransition{ + ID: "recover", Version: 1, SelectionClass: delivery.SelectionProgramRecovery, Class: delivery.EventRecovery, + SourcePhases: []delivery.ProtocolPhase{delivery.PhaseRecovery}, TargetPhases: []delivery.ProtocolPhase{delivery.PhaseActive}, + RequiredIdentity: []string{"repository-id"}, Authority: []delivery.AuthorityClass{delivery.AuthorityRepository}, RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite}, RequiredEvidence: []string{"snapshot"}, + OwnedResources: []string{"program.state"}, Effect: "program.recover", LocalEffects: []delivery.EffectID{"program.recover"}, Idempotent: true, + Prescription: delivery.Prescription{Operation: "recover", ExpectedPostcondition: "active"}, SourcePredicate: "recovery-required", + SourceConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetRecovery, "required")}, AdmissionPredicate: "exact-admission", + TargetPredicate: "active", TargetConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetProgram, "current")}, Verifier: "program.current", + Interruption: interruption("recover"), Reversibility: delivery.Reversible, TerminalEffect: "none", + PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "local", Policy: delivery.PolicyContract{ObjectiveScope: delivery.ObjectiveScopeOptionalPreserve}, Priority: 1, + } + advance := recovery + advance.ID = "advance" + advance.SelectionClass = delivery.SelectionProgramProgress + advance.Class = delivery.EventOwnedLocal + advance.SourcePhases = []delivery.ProtocolPhase{delivery.PhaseActive} + advance.TargetPhases = []delivery.ProtocolPhase{delivery.PhaseTerminal} + advance.ObjectiveKinds = []delivery.ObjectiveKind{delivery.ObjectiveVerified} + advance.Authority = []delivery.AuthorityClass{delivery.AuthorityHuman, delivery.AuthorityRepository} + advance.Effect = "program.advance" + advance.LocalEffects = []delivery.EffectID{"program.advance"} + advance.Prescription = delivery.Prescription{Operation: "advance", Arguments: []string{"--exact"}, ExpectedPostcondition: "terminal"} + advance.SourcePredicate = "active" + advance.SourceConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetProgram, "current"), delivery.KnownCondition(delivery.FacetDelivery, "active")} + advance.TargetPredicate = "terminal" + advance.TargetConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, "terminal")} + advance.Verifier = "program.terminal" + advance.Policy.ObjectiveScope = delivery.ObjectiveScopeBoundExact + return delivery.ProgramManifest{ + SchemaVersion: delivery.ProgramSchemaVersion, ProgramID: "test-program", ProgramVersion: "1", RequiresRuntime: ">=1.0.0", + Capabilities: delivery.ProgramCapabilities{ + Effects: []string{"program.advance", "program.recover"}, Verifiers: []string{"program.current", "program.terminal"}, + CapabilitySurface: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, + }, + OwnedResources: []string{"program.state"}, ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, "terminal")}}}, + Transitions: []delivery.ProgramTransition{advance, recovery}, + } +} + +func runtimeFixture() delivery.RuntimeCompatibility { + return delivery.RuntimeCompatibility{Version: "v1.2.3", Effects: []string{"program.advance", "program.recover", "alternate.effect"}, Verifiers: []string{"program.current", "program.terminal", "alternate.verifier"}, Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityHumanApprove}} +} + +func loadManifest(t *testing.T, manifest delivery.ProgramManifest) delivery.ControlProgram { + t.Helper() + program, err := delivery.ValidateProgram(manifest, runtimeFixture()) + if err != nil { + t.Fatal(err) + } + return program +} + +func interruption(recovery delivery.TransitionID) delivery.InterruptionContract { + return delivery.InterruptionContract{Points: []string{"after-effect"}, PartialState: []string{"effect-may-exist"}, Detection: "fresh-observation", ResumeContract: "resume", RollbackContract: "rollback", CompensationContract: "compensate", Recovery: recovery, RecoveryAuthority: "repository-policy", ResumptionPredicate: "exact-state"} +} + +func reverse[T any](values []T) { + for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 { + values[left], values[right] = values[right], values[left] + } +} + +func reorderTopLevelObject(t *testing.T, raw []byte) []byte { + t.Helper() + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil { + t.Fatal(err) + } + keys := make([]string, 0, len(fields)) + for key := range fields { + keys = append(keys, key) + } + sort.Sort(sort.Reverse(sort.StringSlice(keys))) + var result bytes.Buffer + result.WriteByte('{') + for index, key := range keys { + if index != 0 { + result.WriteByte(',') + } + encodedKey, _ := json.Marshal(key) + result.Write(encodedKey) + result.WriteByte(':') + result.Write(fields[key]) + } + result.WriteByte('}') + return result.Bytes() +} diff --git a/boatstack/control/program_runtime.go b/boatstack/delivery/program_runtime.go similarity index 99% rename from boatstack/control/program_runtime.go rename to boatstack/delivery/program_runtime.go index 5d5aa34..6898497 100644 --- a/boatstack/control/program_runtime.go +++ b/boatstack/delivery/program_runtime.go @@ -1,4 +1,4 @@ -package control +package delivery import ( "context" diff --git a/boatstack/control/runtime_contract_test.go b/boatstack/delivery/runtime_contract_test.go similarity index 99% rename from boatstack/control/runtime_contract_test.go rename to boatstack/delivery/runtime_contract_test.go index 8c1cfba..92e8772 100644 --- a/boatstack/control/runtime_contract_test.go +++ b/boatstack/delivery/runtime_contract_test.go @@ -1,4 +1,4 @@ -package control +package delivery import ( "encoding/json" diff --git a/boatstack/kernel.go b/boatstack/delivery_controller.go similarity index 77% rename from boatstack/kernel.go rename to boatstack/delivery_controller.go index 41b2d3e..945fc47 100644 --- a/boatstack/kernel.go +++ b/boatstack/delivery_controller.go @@ -8,17 +8,17 @@ import ( "os" "time" - "github.com/operatorstack/boatstack/boatstack/control" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/internal/buildinfo" - "github.com/operatorstack/boatstack/boatstack/internal/effects" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/engine" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" - "github.com/operatorstack/boatstack/boatstack/internal/plant" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/engine" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" ) var ( @@ -27,11 +27,11 @@ var ( ChecksumsSHA256 = buildinfo.ChecksumsSHA256 ) -// Kernel is the deterministic mechanism facade over one immutable compiled +// DeliveryController is the deterministic mechanism facade over one immutable compiled // ControlProgram and its concrete plant/effect ports. It owns no independent // delivery-flow policy or durable lifecycle state. -type Kernel struct { - program control.ControlProgram +type DeliveryController struct { + program delivery.ControlProgram registry catalog.Registry resolver plant.Resolver observer ports.Observer @@ -39,47 +39,47 @@ type Kernel struct { clock effects.Clock } -func NewKernel(externalStateRoot string, program control.ControlProgram) (Kernel, error) { +func NewDeliveryController(externalStateRoot string, program delivery.ControlProgram) (DeliveryController, error) { if program.Fingerprint() == "" || program.TransitionCount() == 0 { - return Kernel{}, fmt.Errorf("Kernel requires an immutable compiled ControlProgram") + return DeliveryController{}, fmt.Errorf("DeliveryController requires an immutable compiled ControlProgram") } clock := effects.Clock{} resolver, err := plant.NewResolver(externalStateRoot) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } baseObserver, err := plant.NewObserver(resolver, clock) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } observer := programObserver{base: baseObserver, program: program} locker, err := effects.NewLocker(resolver) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } journal, err := effects.NewJournal(resolver, clock) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } receipts, err := effects.NewReceiptStore(resolver, clock) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } baseDriver, err := effects.NewProgramDriver(resolver, clock, effects.NewNativeBoundary(), program.ResourceOwnership()) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } driver := programEffectDriver{base: baseDriver, program: program, resolver: resolver, clock: clock} registry := program.RuntimeRegistry() summary := program.Summary() - runtimeEngine, err := engine.New(registry, program.RuntimeGoalContracts(), protocol.ProgramIdentity{ID: summary.ProgramID, Version: summary.ProgramVersion, Fingerprint: summary.ProgramFingerprint}, observer, clock, locker, journal, driver, receipts) + runtimeEngine, err := engine.New(registry, program.RuntimeObjectiveContracts(), protocol.ProgramIdentity{ID: summary.ProgramID, Version: summary.ProgramVersion, Fingerprint: summary.ProgramFingerprint}, observer, clock, locker, journal, driver, receipts) if err != nil { - return Kernel{}, err + return DeliveryController{}, err } - return Kernel{program: program, registry: registry, resolver: resolver, observer: observer, engine: runtimeEngine, clock: clock}, nil + return DeliveryController{program: program, registry: registry, resolver: resolver, observer: observer, engine: runtimeEngine, clock: clock}, nil } -func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces.Response, error) { +func (k DeliveryController) Handle(ctx context.Context, request surfaces.Request) (surfaces.Response, error) { response := surfaces.Response{SchemaVersion: surfaces.SchemaVersion, Operation: request.Operation} if err := request.Validate(k.clock.Now()); err != nil { response.Error = err.Error() @@ -111,8 +111,8 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces. } switch request.Operation { case surfaces.OperationResolve: - resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Parameters: request.Parameters, Requested: request.TransitionID}) - response.Goal, response.Decision = resolution.Goal, &resolution.Decision + resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Objective: request.Objective, Authority: request.Authority, Parameters: request.Parameters, Requested: request.TransitionID}) + response.Objective, response.Decision = resolution.Objective, &resolution.Decision if resolution.Prescription.ID != "" { response.Prescription = &resolution.Prescription response.Admission = &resolution.Admission @@ -128,11 +128,11 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces. return response, nil case surfaces.OperationApply, surfaces.OperationRecover: result, applyErr := k.engine.Apply(ctx, engine.ApplyRequest{ - ResolveRequest: engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Requested: request.TransitionID}, + ResolveRequest: engine.ResolveRequest{Invocation: invocation, Objective: request.Objective, Authority: request.Authority, Requested: request.TransitionID}, FlowID: request.FlowID, Prescription: request.Prescription, Parameters: request.Parameters, IdempotencyKey: request.IdempotencyKey, AdmissionLifetime: 2 * time.Minute, }) response.Prescription = &request.Prescription - response.Goal = result.Goal + response.Objective = result.Objective if result.Target.Fingerprint != "" { response.Snapshot = &result.Target } else if result.Source.Fingerprint != "" { @@ -194,7 +194,7 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces. } report.Healthy = k.registry.Len() == summary.TotalTransitionCount && !report.UnresolvedProgramDrift && report.RuntimeHealthy && report.UpdateReady && !report.RecoveryRequired report.Snapshot = snapshot.Fingerprint - report.Detail = "Kernel, observation, and compiled control program are valid" + report.Detail = "DeliveryController, observation, and compiled control program are valid" if report.UnresolvedProgramDrift { report.Detail = supervisor.ReasonProgramDrift } else if report.RecoveryRequired { @@ -230,7 +230,7 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces. return response, canonicalErr } intent := surfaces.ClassifyCommandIntent(request.Command) - guard := supervisor.New(k.registry, k.program.RuntimeGoalContracts()).Guard(snapshot, intent) + guard := supervisor.New(k.registry, k.program.RuntimeObjectiveContracts()).Guard(snapshot, intent) response.Snapshot, response.Guard = &snapshot, &guard return response, nil default: @@ -252,10 +252,10 @@ func programChangeFor(snapshot *model.Snapshot) *surfaces.ProgramChange { } } -func (k Kernel) deriveRepositoryAuthority(ctx context.Context, invocation model.InvocationContext, bundle protocol.AuthorityBundle) (protocol.AuthorityBundle, error) { +func (k DeliveryController) deriveRepositoryAuthority(ctx context.Context, invocation model.InvocationContext, bundle protocol.AuthorityBundle) (protocol.AuthorityBundle, error) { for _, receipt := range bundle.Receipts { if receipt.Class == catalog.AuthorityRepository { - return protocol.AuthorityBundle{}, fmt.Errorf("repository authority must be derived once by Kernel") + return protocol.AuthorityBundle{}, fmt.Errorf("repository authority must be derived once by DeliveryController") } } observation, err := k.observer.Observe(ctx, ports.ObservationRequest{Invocation: invocation, Capabilities: bundle.GrantedCapabilities(k.clock.Now())}) diff --git a/boatstack/kernel_test.go b/boatstack/delivery_controller_test.go similarity index 76% rename from boatstack/kernel_test.go rename to boatstack/delivery_controller_test.go index 13529f1..b628596 100644 --- a/boatstack/kernel_test.go +++ b/boatstack/delivery_controller_test.go @@ -8,10 +8,10 @@ import ( boatstack "github.com/operatorstack/boatstack/boatstack" "github.com/operatorstack/boatstack/boatstack/distribution" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" ) func TestRecoverSurfaceConsumesCompiledRegistryInsteadOfFixedProgramIDs(t *testing.T) { @@ -20,7 +20,7 @@ func TestRecoverSurfaceConsumesCompiledRegistryInsteadOfFixedProgramIDs(t *testi if err != nil { t.Fatal(err) } - kernel, err := boatstack.NewKernel(t.TempDir(), program) + kernel, err := boatstack.NewDeliveryController(t.TempDir(), program) if err != nil { t.Fatal(err) } @@ -29,7 +29,7 @@ func TestRecoverSurfaceConsumesCompiledRegistryInsteadOfFixedProgramIDs(t *testi Repository: "/repository-is-not-consulted", Host: "cli", CorrelationID: "compiled-recovery", FlowID: "flow", TransitionID: "plan.create", } - prescriptionSnapshot := model.Snapshot{Observation: model.Observation{StateRevision: 1, ProgramFingerprint: program.Fingerprint()}, Fingerprint: strings.Repeat("a", 64)} + prescriptionSnapshot := model.Snapshot{Observation: model.Observation{Invocation: model.InvocationContext{RepositoryID: "repo-fixture"}, StateRevision: 1, ProgramFingerprint: program.Fingerprint()}, Fingerprint: strings.Repeat("a", 64)} projection := protocol.CapabilityProjection{AuthorityFingerprint: "auth-test", Required: []catalog.Capability{catalog.CapabilityRepositoryWrite}, Effective: []catalog.Capability{catalog.CapabilityRepositoryWrite}} request.Prescription, err = protocol.NewPrescription(prescriptionSnapshot, catalog.Transition{ID: request.TransitionID}, projection) if err != nil { diff --git a/boatstack/distribution/standard.go b/boatstack/distribution/standard.go index 7cb943a..86bb3a2 100644 --- a/boatstack/distribution/standard.go +++ b/boatstack/distribution/standard.go @@ -12,16 +12,16 @@ import ( "time" boatstack "github.com/operatorstack/boatstack/boatstack" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/extension/subprocess" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) -func StandardProgram(ctx context.Context, extensions ...control.Extension) (control.ControlProgram, error) { - return control.Compile(ctx, control.CompileRequest{ +func StandardProgram(ctx context.Context, extensions ...delivery.Extension) (delivery.ControlProgram, error) { + return delivery.Compile(ctx, delivery.CompileRequest{ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: extensions, Settings: programSettings{}, @@ -43,7 +43,7 @@ type RepositoryProgramRequest struct { ExternalStateRoot string Host string CorrelationID string - Extensions []control.Extension + Extensions []delivery.Extension ConfigurationPath string ConfigurationFingerprint string } @@ -52,14 +52,14 @@ type RepositoryProgramRequest struct { // checksum-verified subprocess extensions selected by the repository's strict // project configuration. A new value is returned per call, so concurrent // repositories never share mutable program state. -func StandardProgramForRepository(ctx context.Context, request RepositoryProgramRequest) (control.ControlProgram, error) { +func StandardProgramForRepository(ctx context.Context, request RepositoryProgramRequest) (delivery.ControlProgram, error) { configured, settings, err := ConfiguredExtensions(ctx, request) if err != nil { - return control.ControlProgram{}, err + return delivery.ControlProgram{}, err } - extensions := append([]control.Extension(nil), request.Extensions...) + extensions := append([]delivery.Extension(nil), request.Extensions...) extensions = append(extensions, configured...) - return control.Compile(ctx, control.CompileRequest{ + return delivery.Compile(ctx, delivery.CompileRequest{ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: extensions, Settings: settings, }) @@ -68,7 +68,7 @@ func StandardProgramForRepository(ctx context.Context, request RepositoryProgram // ConfiguredExtensions resolves only additive subprocess extensions. The // returned settings identity binds every repository policy byte that can // affect the compiled program. -func ConfiguredExtensions(ctx context.Context, request RepositoryProgramRequest) ([]control.Extension, any, error) { +func ConfiguredExtensions(ctx context.Context, request RepositoryProgramRequest) ([]delivery.Extension, any, error) { if request.Repository == "" { return nil, programSettings{}, nil } @@ -113,12 +113,12 @@ func ConfiguredExtensions(ctx context.Context, request RepositoryProgramRequest) if request.ConfigurationFingerprint != "" && request.ConfigurationFingerprint != fingerprint { return nil, nil, fmt.Errorf("candidate repository program configuration fingerprint mismatch") } - configured := make([]control.Extension, 0, len(configuration.Extensions)) + configured := make([]delivery.Extension, 0, len(configuration.Extensions)) for _, declaration := range configuration.Extensions { extension, extensionErr := subprocess.New(subprocess.Config{ ID: declaration.ID, Version: declaration.Version, Executable: declaration.Executable, SHA256: declaration.SHA256, Manifest: declaration.Manifest, Settings: declaration.Settings, - Limits: control.SubprocessLimits{Deadline: time.Duration(declaration.DeadlineMillis) * time.Millisecond, StdoutBytes: declaration.StdoutBytes, StderrBytes: declaration.StderrBytes}, + Limits: delivery.SubprocessLimits{Deadline: time.Duration(declaration.DeadlineMillis) * time.Millisecond, StdoutBytes: declaration.StdoutBytes, StderrBytes: declaration.StderrBytes}, }) if extensionErr != nil { return nil, nil, fmt.Errorf("verify configured subprocess extension %q: %w", declaration.ID, extensionErr) diff --git a/boatstack/distribution/standard_test.go b/boatstack/distribution/standard_test.go index 7b5de3f..9ed691a 100644 --- a/boatstack/distribution/standard_test.go +++ b/boatstack/distribution/standard_test.go @@ -13,8 +13,8 @@ import ( "sync" "testing" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func TestRepositoryScopedProgramsAreIndependentUnderConcurrency(t *testing.T) { @@ -44,10 +44,10 @@ func TestRepositoryScopedProgramsAreIndependentUnderConcurrency(t *testing.T) { t.Fatal(err) } digest := sha256.Sum256(content) - manifest, err := json.Marshal(control.ExtensionManifest{ - ID: id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion, + manifest, err := json.Marshal(delivery.ExtensionManifest{ + ID: id, Version: "1.0.0", ProtocolVersion: delivery.ExtensionProtocolVersion, SettingsSchema: json.RawMessage(`{"type":"object"}`), Facts: []string{id + ".present"}, - Capabilities: []control.Capability{control.CapabilityCommandExecute}, + Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}, PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", }) if err != nil { diff --git a/boatstack/examples/control_program_test.go b/boatstack/examples/control_program_test.go index e0eeecf..8eaa95c 100644 --- a/boatstack/examples/control_program_test.go +++ b/boatstack/examples/control_program_test.go @@ -4,19 +4,19 @@ import ( "context" "fmt" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/extension/releasenote" "github.com/operatorstack/boatstack/boatstack/flow/standard" "github.com/operatorstack/boatstack/boatstack/sdk" ) func Example_standardFlowWithReleaseNoteExtension() { - program, err := control.Compile(context.Background(), control.CompileRequest{ + program, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: "example-kernel", Core: core.System(), Runtime: standard.Definition(), - Extensions: []control.Extension{releasenote.Definition()}, + Extensions: []delivery.Extension{releasenote.Definition()}, }) if err != nil { panic(err) @@ -28,7 +28,7 @@ func Example_standardFlowWithReleaseNoteExtension() { } func Example_sdkCustomKernel() { - _, err := sdk.NewKernel("", + _, err := sdk.NewProgramClient("", sdk.WithProgramRuntime(standard.Definition()), sdk.WithExtension(releasenote.Definition()), ) diff --git a/boatstack/extension/inprocess.go b/boatstack/extension/inprocess.go index 40eb13e..97dccd4 100644 --- a/boatstack/extension/inprocess.go +++ b/boatstack/extension/inprocess.go @@ -6,23 +6,23 @@ import ( "context" "fmt" - "github.com/operatorstack/boatstack/boatstack/control" + "github.com/operatorstack/boatstack/boatstack/delivery" ) type InProcess struct { - manifest control.ExtensionManifest - runtime control.ExtensionRuntime + manifest delivery.ExtensionManifest + runtime delivery.ExtensionRuntime } -func NewInProcess(manifest control.ExtensionManifest, runtime control.ExtensionRuntime) (*InProcess, error) { +func NewInProcess(manifest delivery.ExtensionManifest, runtime delivery.ExtensionRuntime) (*InProcess, error) { if runtime == nil { return nil, fmt.Errorf("in-process extension requires a runtime") } return &InProcess{manifest: manifest, runtime: runtime}, nil } -func (e *InProcess) ExtensionManifest(context.Context) (control.ExtensionManifest, error) { +func (e *InProcess) ExtensionManifest(context.Context) (delivery.ExtensionManifest, error) { return e.manifest, nil } -func (e *InProcess) Runtime() control.ExtensionRuntime { return e.runtime } +func (e *InProcess) Runtime() delivery.ExtensionRuntime { return e.runtime } diff --git a/boatstack/extension/releasenote/releasenote.go b/boatstack/extension/releasenote/releasenote.go index bac4be0..308c443 100644 --- a/boatstack/extension/releasenote/releasenote.go +++ b/boatstack/extension/releasenote/releasenote.go @@ -1,5 +1,5 @@ // Package releasenote is a deterministic reference extension that adds a -// conservative release-note evidence obligation to PR and merged goals. +// conservative release-note evidence obligation to PR and merged objectives. package releasenote import ( @@ -14,9 +14,9 @@ import ( "sort" "strings" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) const ( @@ -31,16 +31,16 @@ const ( type Extension struct{} -func Definition() control.RuntimeExtension { return Extension{} } +func Definition() delivery.RuntimeExtension { return Extension{} } -func (Extension) Runtime() control.ExtensionRuntime { return Extension{} } +func (Extension) Runtime() delivery.ExtensionRuntime { return Extension{} } -func (Extension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) { +func (Extension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, error) { transition := catalog.Transition{ - ID: Transition, Version: 1, Class: catalog.EventOwnedLocal, SelectionClass: catalog.SelectionGoalRequired, + ID: Transition, Version: 1, Class: catalog.EventOwnedLocal, SelectionClass: catalog.SelectionObjectiveRequired, SourcePhases: []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, TargetPhases: []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, - GoalKinds: []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id", "controller-id", "topology", "host", "correlation-id"}, - Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot-fingerprint", "goal", "facet:" + FactID}, + ObjectiveKinds: []model.ObjectiveKind{model.ObjectiveOpenPR, model.ObjectiveMerged}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id", "controller-id", "topology", "host", "correlation-id"}, + Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot-fingerprint", "objective", "facet:" + FactID}, OwnedResources: []string{Resource}, Effect: Effect, LocalEffects: []catalog.EffectID{Effect}, Idempotent: true, Prescription: catalog.Prescription{Operation: Transition, ExpectedPostcondition: "release-note evidence is verified"}, SourcePredicate: "reference-release-note-missing", AdmissionPredicate: "exact-extension-admission", TargetPredicate: "reference-release-note-verified", Verifier: Verifier, @@ -56,56 +56,57 @@ func (Extension) ExtensionManifest(context.Context) (control.ExtensionManifest, Detection: "fresh extension observation", ResumeContract: "re-observe exact namespaced evidence", RollbackContract: "restore prior namespaced bytes", CompensationContract: "not-required", Recovery: "recovery.escalate", RecoveryAuthority: "repository-policy", ResumptionPredicate: "program and extension evidence are current", }, - Reversibility: catalog.Reversible, TerminalEffect: "conjunctive-goal-obligation", + Reversibility: catalog.Reversible, TerminalEffect: "conjunctive-objective-obligation", PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "local-verification", + Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, } transition.RequiredCapabilities = []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute} - constraint := func(goal model.GoalKind) control.GoalConstraint { - return control.GoalConstraint{GoalKind: goal, Conditions: []catalog.FacetCondition{known(model.FacetName(FactID), "verified", "not-required")}} + constraint := func(objective model.ObjectiveKind) delivery.ObjectiveConstraint { + return delivery.ObjectiveConstraint{ObjectiveKind: objective, Conditions: []catalog.FacetCondition{known(model.FacetName(FactID), "verified", "not-required")}} } - return control.ExtensionManifest{ - ID: ID, Version: Version, ProtocolVersion: control.ExtensionProtocolVersion, + return delivery.ExtensionManifest{ + ID: ID, Version: Version, ProtocolVersion: delivery.ExtensionProtocolVersion, SettingsSchema: json.RawMessage(`{"type":"object","additionalProperties":false}`), - Facts: []string{FactID}, Transitions: []control.Transition{transition}, - Capabilities: []control.Capability{control.CapabilityRepositoryWrite, control.CapabilityCommandExecute}, - GoalConstraints: []control.GoalConstraint{constraint(model.GoalOpenPR), constraint(model.GoalMerged)}, - OwnedResources: []string{Resource}, Effects: []string{Effect}, Verifiers: []string{Verifier}, + Facts: []string{FactID}, Transitions: []delivery.Transition{transition}, + Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, + ObjectiveConstraints: []delivery.ObjectiveConstraint{constraint(model.ObjectiveOpenPR), constraint(model.ObjectiveMerged)}, + OwnedResources: []string{Resource}, Effects: []string{Effect}, Verifiers: []string{Verifier}, PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", }, nil } -func (Extension) Invoke(_ context.Context, request control.ExtensionRequest) (control.ExtensionResponse, error) { - response := control.ExtensionResponse{ - ProtocolVersion: control.ExtensionProtocolVersion, Operation: request.Operation, +func (Extension) Invoke(_ context.Context, request delivery.ExtensionRequest) (delivery.ExtensionResponse, error) { + response := delivery.ExtensionResponse{ + ProtocolVersion: delivery.ExtensionProtocolVersion, Operation: request.Operation, ExtensionID: ID, ExtensionVersion: Version, CorrelationID: request.CorrelationID, } switch request.Operation { - case control.ExtensionObserveOperation: + case delivery.ExtensionObserveOperation: fact, err := observe(request.RepositoryRoot, request.ProgramFingerprint) if err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } - response.Facts = []control.ExtensionFact{fact} - case control.ExtensionPlanLocalEffectOperation: + response.Facts = []delivery.ExtensionFact{fact} + case delivery.ExtensionPlanLocalEffectOperation: if request.TransitionID != Transition { - return control.ExtensionResponse{}, fmt.Errorf("release-note extension received an unknown transition") + return delivery.ExtensionResponse{}, fmt.Errorf("release-note extension received an unknown transition") } content, err := evidenceBytes(request.RepositoryRoot, request.ProgramFingerprint) if err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } - response.Writes = []control.ResourceWrite{{ + response.Writes = []delivery.ResourceWrite{{ Resource: Resource, Path: evidencePath(request.RepositoryRoot), Content: content, SHA256: digest(content), Mode: 0o600, }} - case control.ExtensionVerifyOperation: + case delivery.ExtensionVerifyOperation: fact, err := observe(request.RepositoryRoot, request.ProgramFingerprint) if err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } verified := fact.Status == model.FactKnown && fact.Value == "verified" response.Verified = &verified default: - return control.ExtensionResponse{}, fmt.Errorf("release-note extension does not support %q", request.Operation) + return delivery.ExtensionResponse{}, fmt.Errorf("release-note extension does not support %q", request.Operation) } return response, nil } @@ -116,28 +117,28 @@ type evidence struct { ProgramFingerprint string `json:"program_fingerprint"` } -func observe(repository, programFingerprint string) (control.ExtensionFact, error) { +func observe(repository, programFingerprint string) (delivery.ExtensionFact, error) { releaseDigest, relevant, err := releaseNotesDigest(repository) if err != nil { - return control.ExtensionFact{}, err + return delivery.ExtensionFact{}, err } if !relevant { - return control.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "not-required", Fingerprint: digest([]byte("not-required"))}, nil + return delivery.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "not-required", Fingerprint: digest([]byte("not-required"))}, nil } raw, err := os.ReadFile(evidencePath(repository)) if err != nil { if os.IsNotExist(err) { - return control.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "missing", Fingerprint: releaseDigest}, nil + return delivery.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "missing", Fingerprint: releaseDigest}, nil } - return control.ExtensionFact{}, err + return delivery.ExtensionFact{}, err } var record evidence decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(&record); err != nil || record.SchemaVersion != 1 || record.ReleaseNotesSHA256 != releaseDigest || record.ProgramFingerprint != programFingerprint { - return control.ExtensionFact{ID: FactID, Status: model.FactStale, Detail: "release-note evidence is stale", Fingerprint: digest(raw)}, nil + return delivery.ExtensionFact{ID: FactID, Status: model.FactStale, Detail: "release-note evidence is stale", Fingerprint: digest(raw)}, nil } - return control.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "verified", Fingerprint: digest(raw)}, nil + return delivery.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "verified", Fingerprint: digest(raw)}, nil } func evidenceBytes(repository, programFingerprint string) ([]byte, error) { diff --git a/boatstack/extension/releasenote/releasenote_test.go b/boatstack/extension/releasenote/releasenote_test.go index 126934e..6b5f7ac 100644 --- a/boatstack/extension/releasenote/releasenote_test.go +++ b/boatstack/extension/releasenote/releasenote_test.go @@ -6,10 +6,10 @@ import ( "path/filepath" "testing" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/effects" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) const testProgram = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" @@ -24,12 +24,12 @@ func TestReferenceExtensionPlansVerifiesAndInvalidatesNamespacedEvidence(t *test t.Fatal(err) } runtime := Extension{} - request := control.ExtensionRequest{ - ProtocolVersion: control.ExtensionProtocolVersion, ExtensionID: ID, ExtensionVersion: Version, + request := delivery.ExtensionRequest{ + ProtocolVersion: delivery.ExtensionProtocolVersion, ExtensionID: ID, ExtensionVersion: Version, ProgramFingerprint: testProgram, CorrelationID: "reference", RepositoryRoot: repository, - Capabilities: []control.Capability{control.CapabilityRepositoryWrite, control.CapabilityCommandExecute}, + Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, } - request.Operation = control.ExtensionObserveOperation + request.Operation = delivery.ExtensionObserveOperation observed, err := runtime.Invoke(context.Background(), request) if err != nil { t.Fatal(err) @@ -37,7 +37,7 @@ func TestReferenceExtensionPlansVerifiesAndInvalidatesNamespacedEvidence(t *test if len(observed.Facts) != 1 || observed.Facts[0].Status != model.FactKnown || observed.Facts[0].Value != "missing" { t.Fatalf("initial fact = %#v", observed.Facts) } - request.Operation, request.TransitionID = control.ExtensionPlanLocalEffectOperation, Transition + request.Operation, request.TransitionID = delivery.ExtensionPlanLocalEffectOperation, Transition planned, err := runtime.Invoke(context.Background(), request) if err != nil { t.Fatal(err) @@ -46,7 +46,7 @@ func TestReferenceExtensionPlansVerifiesAndInvalidatesNamespacedEvidence(t *test if err != nil { t.Fatal(err) } - admission := protocol.Admission{EffectiveCapabilities: append([]control.Capability(nil), request.Capabilities...)} + admission := protocol.Admission{EffectiveCapabilities: append([]delivery.Capability(nil), request.Capabilities...)} prepared, err := effects.NewExtensionLocalPrepared(repository, ID, planned.Writes, admission, manifest.Transitions[0]) if err != nil { t.Fatal(err) @@ -54,7 +54,7 @@ func TestReferenceExtensionPlansVerifiesAndInvalidatesNamespacedEvidence(t *test if _, err := prepared.Execute(context.Background()); err != nil { t.Fatal(err) } - request.Operation = control.ExtensionVerifyOperation + request.Operation = delivery.ExtensionVerifyOperation verified, err := runtime.Invoke(context.Background(), request) if err != nil { t.Fatal(err) diff --git a/boatstack/extension/subprocess/subprocess.go b/boatstack/extension/subprocess/subprocess.go index 46fa0ca..c6e1ad2 100644 --- a/boatstack/extension/subprocess/subprocess.go +++ b/boatstack/extension/subprocess/subprocess.go @@ -17,8 +17,8 @@ import ( "path/filepath" "time" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/effects" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects" ) const maxRequestBytes = 1 << 20 @@ -30,7 +30,7 @@ type Config struct { SHA256 string Manifest json.RawMessage Settings json.RawMessage - Limits control.SubprocessLimits + Limits delivery.SubprocessLimits } type Extension struct { @@ -70,7 +70,7 @@ func New(config Config) (*Extension, error) { if err != nil { return nil, fmt.Errorf("decode declarative subprocess extension manifest: %w", err) } - if manifest.ID != config.ID || manifest.Version != config.Version || manifest.ProtocolVersion != control.ExtensionProtocolVersion { + if manifest.ID != config.ID || manifest.Version != config.Version || manifest.ProtocolVersion != delivery.ExtensionProtocolVersion { return nil, fmt.Errorf("subprocess extension manifest identity mismatch") } if manifest.ExecutableSHA256 != "" && manifest.ExecutableSHA256 != config.SHA256 { @@ -89,29 +89,29 @@ func New(config Config) (*Extension, error) { return extension, nil } -func (e *Extension) Runtime() control.ExtensionRuntime { return e } +func (e *Extension) Runtime() delivery.ExtensionRuntime { return e } -func (e *Extension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) { +func (e *Extension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, error) { return decodeManifest(e.manifestRaw) } -func (e *Extension) Invoke(ctx context.Context, request control.ExtensionRequest) (control.ExtensionResponse, error) { - if request.ProtocolVersion != control.ExtensionProtocolVersion || request.ExtensionID != e.config.ID || request.ExtensionVersion != e.config.Version || request.CorrelationID == "" { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension request identity mismatch") +func (e *Extension) Invoke(ctx context.Context, request delivery.ExtensionRequest) (delivery.ExtensionResponse, error) { + if request.ProtocolVersion != delivery.ExtensionProtocolVersion || request.ExtensionID != e.config.ID || request.ExtensionVersion != e.config.Version || request.CorrelationID == "" { + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension request identity mismatch") } switch request.Operation { - case control.ExtensionManifestOperation, control.ExtensionObserveOperation, control.ExtensionPlanLocalEffectOperation, - control.ExtensionExecuteExternalOperation, control.ExtensionVerifyOperation, control.ExtensionRecoverOperation: + case delivery.ExtensionManifestOperation, delivery.ExtensionObserveOperation, delivery.ExtensionPlanLocalEffectOperation, + delivery.ExtensionExecuteExternalOperation, delivery.ExtensionVerifyOperation, delivery.ExtensionRecoverOperation: default: - return control.ExtensionResponse{}, fmt.Errorf("unsupported subprocess extension operation %q", request.Operation) + return delivery.ExtensionResponse{}, fmt.Errorf("unsupported subprocess extension operation %q", request.Operation) } executable, err := e.verifiedExecutable() if err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } stagedPath, cleanup, err := effects.StageVerifiedExecutable(e.config.Executable, executable) if err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } defer cleanup() if e.beforeStart != nil { @@ -119,10 +119,10 @@ func (e *Extension) Invoke(ctx context.Context, request control.ExtensionRequest } raw, err := json.Marshal(request) if err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } if len(raw) > maxRequestBytes { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension request exceeds 1 MiB") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension request exceeds 1 MiB") } deadlineContext, cancel := context.WithTimeout(ctx, e.config.Limits.Deadline) defer cancel() @@ -134,32 +134,32 @@ func (e *Extension) Invoke(ctx context.Context, request control.ExtensionRequest command.Stdout, command.Stderr = stdout, stderr if err := command.Run(); err != nil { if stdout.exceeded || stderr.exceeded { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension output exceeded its bound") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension output exceeded its bound") } if errors.Is(deadlineContext.Err(), context.DeadlineExceeded) { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension deadline exceeded") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension deadline exceeded") } - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension process failed") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension process failed") } if stdout.exceeded || stderr.exceeded { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension output exceeded its bound") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension output exceeded its bound") } - var response control.ExtensionResponse + var response delivery.ExtensionResponse decoder := json.NewDecoder(bytes.NewReader(stdout.Bytes())) decoder.DisallowUnknownFields() if err := decoder.Decode(&response); err != nil { - return control.ExtensionResponse{}, fmt.Errorf("decode subprocess extension response: %w", err) + return delivery.ExtensionResponse{}, fmt.Errorf("decode subprocess extension response: %w", err) } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension response contains trailing JSON") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension response contains trailing JSON") } - if response.ProtocolVersion != control.ExtensionProtocolVersion || response.Operation != request.Operation || + if response.ProtocolVersion != delivery.ExtensionProtocolVersion || response.Operation != request.Operation || response.ExtensionID != e.config.ID || response.ExtensionVersion != e.config.Version || response.CorrelationID != request.CorrelationID { - return control.ExtensionResponse{}, fmt.Errorf("subprocess extension response identity mismatch") + return delivery.ExtensionResponse{}, fmt.Errorf("subprocess extension response identity mismatch") } - if err := control.ValidateExtensionOperationResponse(request.Operation, response); err != nil { - return control.ExtensionResponse{}, err + if err := delivery.ValidateExtensionOperationResponse(request.Operation, response); err != nil { + return delivery.ExtensionResponse{}, err } return response, nil } @@ -193,16 +193,16 @@ func (e *Extension) verifiedExecutable() ([]byte, error) { return raw, nil } -func decodeManifest(raw json.RawMessage) (control.ExtensionManifest, error) { - var manifest control.ExtensionManifest +func decodeManifest(raw json.RawMessage) (delivery.ExtensionManifest, error) { + var manifest delivery.ExtensionManifest decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(&manifest); err != nil { - return control.ExtensionManifest{}, err + return delivery.ExtensionManifest{}, err } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { - return control.ExtensionManifest{}, fmt.Errorf("manifest contains trailing JSON") + return delivery.ExtensionManifest{}, fmt.Errorf("manifest contains trailing JSON") } return manifest, nil } diff --git a/boatstack/extension/subprocess/subprocess_test.go b/boatstack/extension/subprocess/subprocess_test.go index 23195c8..f2932f0 100644 --- a/boatstack/extension/subprocess/subprocess_test.go +++ b/boatstack/extension/subprocess/subprocess_test.go @@ -13,17 +13,17 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/flow/standard" ) func fixtureManifest(t *testing.T, id string) json.RawMessage { t.Helper() - raw, err := json.Marshal(control.ExtensionManifest{ - ID: id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion, + raw, err := json.Marshal(delivery.ExtensionManifest{ + ID: id, Version: "1.0.0", ProtocolVersion: delivery.ExtensionProtocolVersion, SettingsSchema: json.RawMessage(`{"type":"object"}`), Facts: []string{id + ".present"}, - Capabilities: []control.Capability{control.CapabilityCommandExecute}, + Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}, PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", }) if err != nil { @@ -52,7 +52,7 @@ func fixtureExtension(t *testing.T) *Extension { extension, err := New(Config{ ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo"), - Limits: control.SubprocessLimits{Deadline: 30 * time.Second}, + Limits: delivery.SubprocessLimits{Deadline: 30 * time.Second}, }) if err != nil { t.Fatal(err) @@ -60,12 +60,12 @@ func fixtureExtension(t *testing.T) *Extension { return extension } -func fixtureObserveRequest(correlationID string) control.ExtensionRequest { - return control.ExtensionRequest{ - ProtocolVersion: control.ExtensionProtocolVersion, - Operation: control.ExtensionObserveOperation, +func fixtureObserveRequest(correlationID string) delivery.ExtensionRequest { + return delivery.ExtensionRequest{ + ProtocolVersion: delivery.ExtensionProtocolVersion, + Operation: delivery.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: correlationID, - Capabilities: []control.Capability{control.CapabilityCommandExecute}, + Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}, } } @@ -87,7 +87,7 @@ func pythonFixture(t *testing.T, mutate func(string) string) *Extension { extension, err := New(Config{ ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo"), - Limits: control.SubprocessLimits{Deadline: 30 * time.Second}, + Limits: delivery.SubprocessLimits{Deadline: 30 * time.Second}, }) if err != nil { t.Fatal(err) @@ -189,8 +189,8 @@ func TestDeclarativeManifestDoesNotStartExecutable(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := control.Compile(context.Background(), control.CompileRequest{ - KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{extension}, + if _, err := delivery.Compile(context.Background(), delivery.CompileRequest{ + KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), Extensions: []delivery.Extension{extension}, }); err != nil { t.Fatal(err) } @@ -251,7 +251,7 @@ func TestDeadlineAndOutputBoundsFailClosed(t *testing.T) { t.Fatal(err) } digest := sha256.Sum256([]byte(fixture.body)) - extension, err := New(Config{ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo"), Limits: control.SubprocessLimits{Deadline: fixture.deadline, StdoutBytes: fixture.stdout, StderrBytes: 64}}) + extension, err := New(Config{ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo"), Limits: delivery.SubprocessLimits{Deadline: fixture.deadline, StdoutBytes: fixture.stdout, StderrBytes: 64}}) if err != nil { t.Fatal(err) } diff --git a/boatstack/flow/standard/completeness_test.go b/boatstack/flow/standard/completeness_test.go index d98b44e..7ede78d 100644 --- a/boatstack/flow/standard/completeness_test.go +++ b/boatstack/flow/standard/completeness_test.go @@ -12,8 +12,8 @@ import ( "strings" "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) @@ -49,7 +49,7 @@ func TestEveryControllingFacetAndEventIsClassifiedByTheRuntimeCatalog(t *testing t.Errorf("controlling facet %s is absent from executable predicates", facet) } } - want := map[string]int{"invocation-engagement": 6, "installation-runtime-configuration": 9, "catalog": 1, "goal-plan": 9, "workspace": 8, "gate-evidence-delivery": 8, "publication": 6, "recovery": 3, "external": 13} + want := map[string]int{"invocation-engagement": 6, "installation-runtime-configuration": 9, "catalog": 1, "objective-plan": 9, "workspace": 8, "gate-evidence-delivery": 8, "publication": 6, "recovery": 3, "external": 13} for family, count := range want { if families[family] != count { t.Errorf("family %s=%d, want %d", family, families[family], count) @@ -75,8 +75,8 @@ func familyFor(id catalog.TransitionID) string { return "installation-runtime-configuration" case strings.HasPrefix(value, "catalog."): return "catalog" - case strings.HasPrefix(value, "goal."), strings.HasPrefix(value, "plan."): - return "goal-plan" + case strings.HasPrefix(value, "objective."), strings.HasPrefix(value, "plan."): + return "objective-plan" case strings.HasPrefix(value, "workspace."): return "workspace" case strings.HasPrefix(value, "gate."), strings.HasPrefix(value, "evidence."), strings.HasPrefix(value, "delivery."): @@ -142,17 +142,17 @@ func TestSourceInventoryHasNoWriterOrLifecycleAuthorityOutsideOwnedPackages(t *t return true } importPath := imports[owner.Name] - if importPath == "os" && writerCalls[selector.Sel.Name] && !strings.HasPrefix(relative, "internal/effects/") && !strings.HasPrefix(relative, "internal/runtime/") { + if importPath == "os" && writerCalls[selector.Sel.Name] && !strings.HasPrefix(relative, "internal/softwaredelivery/effects/") && !strings.HasPrefix(relative, "internal/runtime/") { t.Errorf("managed writer os.%s escaped effects package in %s", selector.Sel.Name, relative) } if importPath == "os/exec" && (selector.Sel.Name == "Command" || selector.Sel.Name == "CommandContext") { - if relative != "internal/effects/command_boundary.go" && relative != "internal/plant/resolver.go" && relative != "extension/subprocess/subprocess.go" && relative != "internal/runtime/exec_windows.go" { + if relative != "internal/softwaredelivery/effects/command_boundary.go" && relative != "internal/softwaredelivery/plant/resolver.go" && relative != "extension/subprocess/subprocess.go" && relative != "internal/runtime/exec_windows.go" { t.Errorf("unclassified command boundary in %s", relative) } } - if strings.HasSuffix(importPath, "/internal/kernel/model") && lifecycleSelector(selector.Sel.Name) && - !strings.HasPrefix(relative, "internal/kernel/") && !strings.HasPrefix(relative, "internal/effects/") && !strings.HasPrefix(relative, "internal/plant/") && - !strings.HasPrefix(relative, "control/") && !strings.HasPrefix(relative, "flow/") && !strings.HasPrefix(relative, "extension/") { + if strings.HasSuffix(importPath, "/internal/softwaredelivery/model") && lifecycleSelector(selector.Sel.Name) && + !strings.HasPrefix(relative, "internal/softwaredelivery/") && !strings.HasPrefix(relative, "internal/softwaredelivery/effects/") && !strings.HasPrefix(relative, "internal/softwaredelivery/plant/") && + !strings.HasPrefix(relative, "delivery/") && !strings.HasPrefix(relative, "flow/") && !strings.HasPrefix(relative, "extension/") { t.Errorf("lifecycle selector model.%s escaped kernel/plant/effects in %s", selector.Sel.Name, relative) } return true @@ -174,7 +174,7 @@ func TestSourceInventoryHasNoWriterOrLifecycleAuthorityOutsideOwnedPackages(t *t func TestEveryControllableRuntimeEventHasAnExecutableStateReducer(t *testing.T) { // control-law: registry-entry-cannot-exist-without-runtime-effect-reduction - path := filepath.Join(sourceRoot(t), "internal", "effects", "state_reducer.go") + path := filepath.Join(sourceRoot(t), "internal", "softwaredelivery", "effects", "state_reducer.go") parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) if err != nil { t.Fatal(err) @@ -216,10 +216,10 @@ func TestEveryControllableRuntimeEventHasAnExecutableStateReducer(t *testing.T) } func TestPackageImportsPreserveControlProgramDependencyDirection(t *testing.T) { - // control-law: kernel-mechanism-cannot-depend-on-standard-flow-or-product-surfaces + // control-law: general-kernel-cannot-depend-on-software-delivery root := sourceRoot(t) - forbiddenKernel := []string{"/flow/standard", "/distribution", "/sdk", "/cmd/boatstack-helper"} - forbiddenFlow := []string{"/distribution", "/sdk", "/cmd/boatstack-helper", "/internal/surfaces"} + forbiddenKernel := []string{"/internal/softwaredelivery", "/flow/standard", "/distribution", "/sdk", "/cmd/boatstack-helper"} + forbiddenFlow := []string{"/distribution", "/sdk", "/cmd/boatstack-helper", "/internal/softwaredelivery/surfaces"} err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr @@ -232,7 +232,7 @@ func TestPackageImportsPreserveControlProgramDependencyDirection(t *testing.T) { return err } relative = filepath.ToSlash(relative) - kernelOwned := relative == "kernel.go" || relative == "program_effects.go" || relative == "program_observer.go" || strings.HasPrefix(relative, "internal/kernel/") + kernelOwned := strings.HasPrefix(relative, "kernel/") flowOwned := strings.HasPrefix(relative, "flow/") if !kernelOwned && !flowOwned { return nil @@ -262,13 +262,12 @@ func TestPackageImportsPreserveControlProgramDependencyDirection(t *testing.T) { } func classifiedProductionFile(relative string) bool { - return relative == "kernel.go" || relative == "program_effects.go" || relative == "program_observer.go" || strings.HasPrefix(relative, "cmd/boatstack-helper/") || - strings.HasPrefix(relative, "control/") || strings.HasPrefix(relative, "core/") || + return relative == "delivery_controller.go" || relative == "program_effects.go" || relative == "program_observer.go" || strings.HasPrefix(relative, "cmd/boatstack-helper/") || + strings.HasPrefix(relative, "delivery/") || strings.HasPrefix(relative, "core/") || strings.HasPrefix(relative, "flow/") || strings.HasPrefix(relative, "distribution/") || strings.HasPrefix(relative, "extension/") || - strings.HasPrefix(relative, "internal/kernel/") || strings.HasPrefix(relative, "internal/plant/") || - strings.HasPrefix(relative, "internal/effects/") || strings.HasPrefix(relative, "internal/surfaces/") || + strings.HasPrefix(relative, "internal/softwaredelivery/") || strings.HasPrefix(relative, "internal/buildinfo/") || strings.HasPrefix(relative, "internal/runtime/") || - strings.HasPrefix(relative, "internal/retromine/") || strings.HasPrefix(relative, "internal/testprogram/") || strings.HasPrefix(relative, "sdk/") || + strings.HasPrefix(relative, "internal/retromine/") || strings.HasPrefix(relative, "internal/testprogram/") || strings.HasPrefix(relative, "kernel/") || strings.HasPrefix(relative, "sdk/") || strings.HasPrefix(relative, "analysis/") } diff --git a/boatstack/flow/standard/historical_test.go b/boatstack/flow/standard/historical_test.go index 179a46a..b2419a5 100644 --- a/boatstack/flow/standard/historical_test.go +++ b/boatstack/flow/standard/historical_test.go @@ -13,18 +13,18 @@ import ( "time" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) -func historicalGoalContracts() catalog.GoalContracts { +func historicalObjectiveContracts() catalog.ObjectiveContracts { manifest, err := standard.Definition().RuntimeManifest(context.Background()) if err != nil { panic(err) } - contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil) + contracts, err := catalog.NewObjectiveContracts(manifest.ObjectiveContracts, nil) if err != nil { panic(err) } @@ -41,7 +41,7 @@ type historicalFixture struct { Name string `json:"name"` InitialPlantFacts map[string]string `json:"initial_plant_facts"` CanonicalObservation map[string]string `json:"canonical_observation"` - RequestedGoal model.Goal `json:"requested_goal"` + RequestedObjective model.Objective `json:"requested_objective"` Event catalog.TransitionID `json:"event"` Authority []catalog.AuthorityClass `json:"authority"` ExpectedDecision supervisor.DecisionKind `json:"expected_decision"` @@ -87,7 +87,7 @@ func snapshotFromFixture(t *testing.T, fixture historicalFixture) model.Snapshot Runtime: model.Known(model.RuntimeState(facts["runtime"]), evidence), Publication: model.Known(model.PublicationState(facts["publication"]), evidence), Verification: model.Known(model.VerificationState(facts["verification"]), evidence), Recovery: model.Known(model.RecoveryState(facts["recovery"]), evidence), Transaction: model.Known(model.TransactionState(facts["transaction"]), evidence), Terminal: model.Known(model.TerminalStatus(facts["terminal"]), evidence), - Goal: model.Known(fixture.RequestedGoal, evidence), RecoveryInfo: model.Absent[model.RecoveryContext]("none", evidence), + Objective: model.Known(fixture.RequestedObjective, evidence), RecoveryInfo: model.Absent[model.RecoveryContext]("none", evidence), TransactionInfo: model.Absent[model.TransactionContext]("none", evidence), ObservedAt: time.Unix(100, 0).UTC(), } if observation.Phase.Value == model.PhaseRecovery { @@ -116,7 +116,7 @@ func TestHistoricalFailureCorpusUsesTheRuntimeControlLaw(t *testing.T) { // control-law: historical failures bind to executable catalog predicates corpus := loadHistoricalCorpus(t) registry := testprogram.StandardRegistry() - control := supervisor.New(registry, historicalGoalContracts()) + control := supervisor.New(registry, historicalObjectiveContracts()) seenNames := map[string]bool{} for _, fixture := range corpus.Fixtures { fixture := fixture @@ -136,8 +136,8 @@ func TestHistoricalFailureCorpusUsesTheRuntimeControlLaw(t *testing.T) { return } snapshot := snapshotFromFixture(t, fixture) - one := control.Resolve(snapshot, fixture.RequestedGoal, authoritySet(fixture.Authority), fixture.Event) - two := control.Resolve(snapshot, fixture.RequestedGoal, authoritySet(fixture.Authority), fixture.Event) + one := control.Resolve(snapshot, fixture.RequestedObjective, authoritySet(fixture.Authority), fixture.Event) + two := control.Resolve(snapshot, fixture.RequestedObjective, authoritySet(fixture.Authority), fixture.Event) if !reflect.DeepEqual(one, two) { t.Fatalf("resolution is nondeterministic: %#v != %#v", one, two) } diff --git a/boatstack/flow/standard/standard.go b/boatstack/flow/standard/standard.go index f566ef8..2987356 100644 --- a/boatstack/flow/standard/standard.go +++ b/boatstack/flow/standard/standard.go @@ -10,8 +10,8 @@ import ( "fmt" "io" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) const ( @@ -24,43 +24,43 @@ type definition struct{} //go:embed transitions.json var transitionDeclarations []byte -func Definition() control.ProgramRuntimeDefinition { return definition{} } +func Definition() delivery.ProgramRuntimeDefinition { return definition{} } -func (definition) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) { +func (definition) RuntimeManifest(context.Context) (delivery.ProgramRuntimeManifest, error) { transitions, err := decodeTransitions() if err != nil { - return control.ProgramRuntimeManifest{}, err + return delivery.ProgramRuntimeManifest{}, err } resources, effects, verifiers, recoveries := declarations(transitions) - capabilities := []control.Capability{control.CapabilityHumanApprove} + capabilities := []delivery.Capability{delivery.CapabilityHumanApprove} for index := range transitions { - transitions[index].RequiredCapabilities = control.KernelEffectCapabilities(transitions[index]) - capabilities = control.UnionCapabilities(capabilities, transitions[index].RequiredCapabilities) + transitions[index].RequiredCapabilities = delivery.KernelEffectCapabilities(transitions[index]) + capabilities = delivery.UnionCapabilities(capabilities, transitions[index].RequiredCapabilities) } - return control.ProgramRuntimeManifest{ - ID: ID, Version: Version, ProtocolVersion: control.ProgramRuntimeProtocolVersion, RuntimeMode: control.ProgramRuntimeNative, - SupportedGoals: []control.GoalKind{ - model.GoalApprovedPlan, model.GoalVerified, model.GoalOpenPR, - model.GoalMerged, model.GoalAbandoned, + return delivery.ProgramRuntimeManifest{ + ID: ID, Version: Version, ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, RuntimeMode: delivery.ProgramRuntimeNative, + SupportedObjectives: []delivery.ObjectiveKind{ + model.ObjectiveApprovedPlan, model.ObjectiveVerified, model.ObjectiveOpenPR, + model.ObjectiveMerged, model.ObjectiveAbandoned, }, - GoalContracts: []control.GoalContract{ - contract(model.GoalApprovedPlan, + ObjectiveContracts: []delivery.ObjectiveContract{ + contract(model.ObjectiveApprovedPlan, known(model.FacetPlan, string(model.PlanApproved))), - contract(model.GoalVerified, + contract(model.ObjectiveVerified, known(model.FacetVerification, string(model.VerificationCurrent)), known(model.FacetConfiguration, string(model.ConfigurationVerified)), known(model.FacetRuntime, string(model.RuntimeVerified)), known(model.FacetDelivery, string(model.DeliveryTerminal))), - contract(model.GoalOpenPR, + contract(model.ObjectiveOpenPR, known(model.FacetVerification, string(model.VerificationCurrent)), known(model.FacetConfiguration, string(model.ConfigurationVerified)), known(model.FacetRuntime, string(model.RuntimeVerified)), known(model.FacetPublication, string(model.PublicationOpen))), - contract(model.GoalMerged, + contract(model.ObjectiveMerged, known(model.FacetPublication, string(model.PublicationMerged)), known(model.FacetDelivery, string(model.DeliveryTerminal)), known(model.FacetWorkspace, string(model.WorkspaceLanded), string(model.WorkspaceAbsent))), - contract(model.GoalAbandoned, + contract(model.ObjectiveAbandoned, known(model.FacetDelivery, string(model.DeliveryDiscarded)), known(model.FacetWorkspace, string(model.WorkspaceAbandoned), string(model.WorkspaceAbsent))), }, @@ -71,10 +71,10 @@ func (definition) RuntimeManifest(context.Context) (control.ProgramRuntimeManife }, nil } -func decodeTransitions() ([]control.Transition, error) { +func decodeTransitions() ([]delivery.Transition, error) { decoder := json.NewDecoder(bytes.NewReader(transitionDeclarations)) decoder.DisallowUnknownFields() - var transitions []control.Transition + var transitions []delivery.Transition if err := decoder.Decode(&transitions); err != nil { return nil, fmt.Errorf("decode StandardFlow transitions: %w", err) } @@ -85,10 +85,10 @@ func decodeTransitions() ([]control.Transition, error) { return transitions, nil } -func declarations(transitions []control.Transition) ([]string, []string, []string, []control.TransitionID) { +func declarations(transitions []delivery.Transition) ([]string, []string, []string, []delivery.TransitionID) { var resources, effects, verifiers []string - var recoveries []control.TransitionID - seenResources, seenEffects, seenVerifiers, seenRecoveries := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[control.TransitionID]bool{} + var recoveries []delivery.TransitionID + seenResources, seenEffects, seenVerifiers, seenRecoveries := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[delivery.TransitionID]bool{} for _, transition := range transitions { for _, resource := range transition.OwnedResources { if !seenResources[resource] { @@ -101,17 +101,17 @@ func declarations(transitions []control.Transition) ([]string, []string, []strin if transition.Verifier != "" && !seenVerifiers[transition.Verifier] { seenVerifiers[transition.Verifier], verifiers = true, append(verifiers, transition.Verifier) } - if transition.Class == control.EventRecovery && !seenRecoveries[transition.ID] { + if transition.Class == delivery.EventRecovery && !seenRecoveries[transition.ID] { seenRecoveries[transition.ID], recoveries = true, append(recoveries, transition.ID) } } return resources, effects, verifiers, recoveries } -func contract(goal model.GoalKind, conditions ...control.FacetCondition) control.GoalContract { - return control.GoalContract{GoalKind: goal, Conditions: conditions} +func contract(objective model.ObjectiveKind, conditions ...delivery.FacetCondition) delivery.ObjectiveContract { + return delivery.ObjectiveContract{ObjectiveKind: objective, Conditions: conditions} } -func known(facet model.FacetName, values ...string) control.FacetCondition { - return control.FacetCondition{Facet: facet, Statuses: []model.FactStatus{model.FactKnown}, Values: values} +func known(facet model.FacetName, values ...string) delivery.FacetCondition { + return delivery.FacetCondition{Facet: facet, Statuses: []model.FactStatus{model.FactKnown}, Values: values} } diff --git a/boatstack/flow/standard/standard_test.go b/boatstack/flow/standard/standard_test.go index 7c0c6d2..c25e98f 100644 --- a/boatstack/flow/standard/standard_test.go +++ b/boatstack/flow/standard/standard_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/operatorstack/boatstack/boatstack/control" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/flow/standard" ) @@ -24,16 +24,16 @@ func TestManifestOwnsOnlyStandardDeliverySemantics(t *testing.T) { t.Errorf("StandardFlow owns non-delivery transition %s", id) } } - if len(manifest.GoalContracts) != 5 { - t.Fatalf("goal contracts = %d, want 5", len(manifest.GoalContracts)) + if len(manifest.ObjectiveContracts) != 5 { + t.Fatalf("objective contracts = %d, want 5", len(manifest.ObjectiveContracts)) } - for _, goal := range []control.GoalKind{control.GoalApprovedPlan, control.GoalVerified, control.GoalOpenPR, control.GoalMerged, control.GoalAbandoned} { + for _, objective := range []delivery.ObjectiveKind{delivery.ObjectiveApprovedPlan, delivery.ObjectiveVerified, delivery.ObjectiveOpenPR, delivery.ObjectiveMerged, delivery.ObjectiveAbandoned} { found := false - for _, contract := range manifest.GoalContracts { - found = found || contract.GoalKind == goal + for _, contract := range manifest.ObjectiveContracts { + found = found || contract.ObjectiveKind == objective } if !found { - t.Errorf("missing goal contract %s", goal) + t.Errorf("missing objective contract %s", objective) } } } diff --git a/boatstack/flow/standard/supervisor_parity_test.go b/boatstack/flow/standard/supervisor_parity_test.go index b774daa..f8d359c 100644 --- a/boatstack/flow/standard/supervisor_parity_test.go +++ b/boatstack/flow/standard/supervisor_parity_test.go @@ -7,18 +7,18 @@ import ( "time" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - . "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + . "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) -func testGoalContracts() catalog.GoalContracts { +func testObjectiveContracts() catalog.ObjectiveContracts { manifest, err := standard.Definition().RuntimeManifest(context.Background()) if err != nil { panic(err) } - contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil) + contracts, err := catalog.NewObjectiveContracts(manifest.ObjectiveContracts, nil) if err != nil { panic(err) } @@ -38,7 +38,7 @@ func snapshotFor(t *testing.T, phase model.ProtocolPhase, terminal model.Termina Publication: model.Known(model.PublicationNone, e), Verification: model.Known(model.VerificationUnverified, e), Recovery: model.Known(model.RecoveryNone, e), Transaction: model.Known(model.TransactionNone, e), RecoveryInfo: model.Absent[model.RecoveryContext]("none", e), TransactionInfo: model.Absent[model.TransactionContext]("none", e), - Terminal: model.Known(terminal, e), Goal: model.Known(goalFor(), e), ObservedAt: time.Unix(10, 0).UTC(), + Terminal: model.Known(terminal, e), Objective: model.Known(objectiveFor(), e), ObservedAt: time.Unix(10, 0).UTC(), } if phase == model.PhaseRecovery { o.Recovery = model.Known(model.RecoveryReconcile, e) @@ -57,8 +57,8 @@ func snapshotFor(t *testing.T, phase model.ProtocolPhase, terminal model.Termina return result } -func goalFor() model.Goal { - return model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"} +func objectiveFor() model.Objective { + return model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} } func recanonicalize(t *testing.T, snapshot model.Snapshot) model.Snapshot { @@ -70,12 +70,12 @@ func recanonicalize(t *testing.T, snapshot model.Snapshot) model.Snapshot { return result } -func openPRSnapshot(t *testing.T, recordedGates ...string) (model.Snapshot, model.Goal) { +func openPRSnapshot(t *testing.T, recordedGates ...string) (model.Snapshot, model.Objective) { t.Helper() snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) - goal := model.Goal{ID: "goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} evidence := snapshot.Verification.Evidence[0] - snapshot.Goal = model.Known(goal, evidence) + snapshot.Objective = model.Known(objective, evidence) snapshot.Plan = model.Known(model.PlanLocked, evidence) snapshot.Publication = model.Known(model.PublicationCandidate, evidence) snapshot.Verification = model.Known(model.VerificationCurrent, evidence) @@ -84,13 +84,13 @@ func openPRSnapshot(t *testing.T, recordedGates ...string) (model.Snapshot, mode gateEvidence.Source = "gate-evidence:" + gate + ":/fixture/" + gate + ".json" snapshot.Verification.Evidence = append(snapshot.Verification.Evidence, gateEvidence) } - return recanonicalize(t, snapshot), goal + return recanonicalize(t, snapshot), objective } -func TestTerminalGoalOutranksLocalTransitions(t *testing.T) { +func TestTerminalObjectiveOutranksLocalTransitions(t *testing.T) { // control-law: configured-terminal-outranks-local-lifecycle - s := New(testprogram.StandardRegistry(), testGoalContracts()) - decision := s.Resolve(snapshotFor(t, model.PhaseTerminal, model.TerminalEstablished), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + s := New(testprogram.StandardRegistry(), testObjectiveContracts()) + decision := s.Resolve(snapshotFor(t, model.PhaseTerminal, model.TerminalEstablished), objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") if decision.Kind != DecisionTerminal || decision.Transition != nil { t.Fatalf("decision = %#v, want terminal without transition", decision) } @@ -105,58 +105,58 @@ func TestExplicitPostTerminalCleanupRemainsAdmissible(t *testing.T) { if err != nil { t.Fatal(err) } - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(canonical, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "workspace.cleanup") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(canonical, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "workspace.cleanup") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "workspace.cleanup" { t.Fatalf("decision = %#v, want explicit post-terminal cleanup", decision) } } -func TestTerminalEvidenceForOldGoalDoesNotTerminateNewGoal(t *testing.T) { - // control-law: terminal-evidence-is-bound-to-exact-goal-not-local-phase - s := New(testprogram.StandardRegistry(), testGoalContracts()) - newGoal := model.Goal{ID: "next-goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"} - decision := s.Resolve(snapshotFor(t, model.PhaseTerminal, model.TerminalEstablished), newGoal, catalog.AuthoritySet{catalog.AuthorityHuman: true}, "goal.configure") - if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "goal.configure" { - t.Fatalf("decision=%#v, want exact new-goal configuration", decision) +func TestTerminalEvidenceForOldObjectiveDoesNotTerminateNewObjective(t *testing.T) { + // control-law: terminal-evidence-is-bound-to-exact-objective-not-local-phase + s := New(testprogram.StandardRegistry(), testObjectiveContracts()) + newObjective := model.Objective{ID: "next-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + decision := s.Resolve(snapshotFor(t, model.PhaseTerminal, model.TerminalEstablished), newObjective, catalog.AuthoritySet{catalog.AuthorityHuman: true}, "objective.bind") + if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "objective.bind" { + t.Fatalf("decision=%#v, want exact new-objective configuration", decision) } } -func TestUntargetedResolutionReconfiguresDifferentGoalAndSkipsSatisfiedGoal(t *testing.T) { - // control-law: untargeted-resolution-must-advance-the-exact-goal +func TestUntargetedResolutionReconfiguresDifferentObjectiveAndSkipsSatisfiedObjective(t *testing.T) { + // control-law: untargeted-resolution-must-advance-the-exact-objective snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} - newGoal := model.Goal{ID: "new-goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"} - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, newGoal, authority, "") - if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "goal.configure" { - t.Fatalf("different-goal decision = %#v, want goal.configure", decision) + newObjective := model.Objective{ID: "new-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, newObjective, authority, "") + if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "objective.bind" { + t.Fatalf("different-objective decision = %#v, want objective.bind", decision) } snapshot.Plan = model.Known(model.PlanValid, snapshot.Plan.Evidence[0]) snapshot = recanonicalize(t, snapshot) - decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "") + decision = New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), authority, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "plan.approve" { - t.Fatalf("exact-goal decision = %#v, want plan.approve without goal.configure stutter", decision) + t.Fatalf("exact-objective decision = %#v, want plan.approve without objective.bind stutter", decision) } } -func TestDormantBootstrapGoalReconfiguresBeforeEngagement(t *testing.T) { - // control-law: a retained bootstrap goal cannot be bypassed by engagement +func TestDormantBootstrapObjectiveReconfiguresBeforeEngagement(t *testing.T) { + // control-law: a retained bootstrap objective cannot be bypassed by engagement snapshot := snapshotFor(t, model.PhaseDormant, model.TerminalNonterminal) - requested := model.Goal{ID: "basic-project", Kind: model.GoalApprovedPlan, DeliveryID: "basic-project"} + requested := model.Objective{ID: "basic-project", Kind: model.ObjectiveApprovedPlan, DeliveryID: "basic-project"} authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} - untargeted := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, "") - if untargeted.Kind != DecisionPrescribed || untargeted.Transition == nil || untargeted.Transition.ID != "goal.configure" { - t.Fatalf("untargeted decision = %#v, want goal.configure", untargeted) + untargeted := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, requested, authority, "") + if untargeted.Kind != DecisionPrescribed || untargeted.Transition == nil || untargeted.Transition.ID != "objective.bind" { + t.Fatalf("untargeted decision = %#v, want objective.bind", untargeted) } - targeted := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, untargeted.Transition.ID) + targeted := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, requested, authority, untargeted.Transition.ID) if targeted.Kind != DecisionPrescribed || targeted.Transition == nil || targeted.Transition.ID != untargeted.Transition.ID { t.Fatalf("targeted decision = %#v, want parity with %#v", targeted, untargeted) } - engagement := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, "engagement.begin") + engagement := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, requested, authority, "engagement.begin") if engagement.Kind != DecisionRefused { - t.Fatalf("engagement decision = %#v, want refusal until goal.configure", engagement) + t.Fatalf("engagement decision = %#v, want refusal until objective.bind", engagement) } } @@ -165,7 +165,7 @@ func TestDisabledHostIsRefusedBeforeUntargetedSelection(t *testing.T) { snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) snapshot.Invocation.Host = "codex" snapshot = recanonicalize(t, snapshot) - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") if decision.Kind != DecisionRefused || decision.Transition != nil { t.Fatalf("disabled-host decision = %#v, want REFUSED", decision) } @@ -173,9 +173,9 @@ func TestDisabledHostIsRefusedBeforeUntargetedSelection(t *testing.T) { func TestPublicationObservationRemainsSelectableForVolatileExternalState(t *testing.T) { // control-law: a nonterminal provider observation is evidence, not permanent progress - snapshot, goal := openPRSnapshot(t, "build", "test", "review", "change", "journey") - goal.Kind = model.GoalMerged - snapshot.Goal = model.Known(goal, snapshot.Goal.Evidence[0]) + snapshot, objective := openPRSnapshot(t, "build", "test", "review", "change", "journey") + objective.Kind = model.ObjectiveMerged + snapshot.Objective = model.Known(objective, snapshot.Objective.Evidence[0]) snapshot.Publication = model.Known(model.PublicationOpen, snapshot.Publication.Evidence[0]) snapshot = recanonicalize(t, snapshot) var transitions []catalog.Transition @@ -188,7 +188,7 @@ func TestPublicationObservationRemainsSelectableForVolatileExternalState(t *test if err != nil { t.Fatal(err) } - decision := New(registry, testGoalContracts()).Resolve(snapshot, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + decision := New(registry, testObjectiveContracts()).Resolve(snapshot, objective, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "publication.observe" { t.Fatalf("volatile publication decision = %#v, want publication.observe", decision) } @@ -202,15 +202,15 @@ func TestUntargetedResolutionExcludesExplicitControlTransitions(t *testing.T) { snapshot = recanonicalize(t, snapshot) authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), authority, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "gate.build.record" { t.Fatalf("untargeted decision = %#v, want gate.build.record", decision) } - decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "plan.invalidate") + decision = New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), authority, "plan.invalidate") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "plan.invalidate" { t.Fatalf("explicit invalidation decision = %#v, want requested plan.invalidate", decision) } - decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "delivery.slice.advance") + decision = New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), authority, "delivery.slice.advance") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "delivery.slice.advance" { t.Fatalf("explicit slice-marker decision = %#v, want requested delivery.slice.advance", decision) } @@ -222,7 +222,7 @@ func TestSelectionClassOutranksComponentLocalPriority(t *testing.T) { for index := range transitions { switch transitions[index].ID { case "gate.build.record": - transitions[index].SelectionClass = catalog.SelectionGoalRequired + transitions[index].SelectionClass = catalog.SelectionObjectiveRequired transitions[index].Priority = 999 case "gate.test.record": transitions[index].SelectionClass = catalog.SelectionProgramProgress @@ -236,7 +236,7 @@ func TestSelectionClassOutranksComponentLocalPriority(t *testing.T) { snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) snapshot.Plan = model.Known(model.PlanLocked, snapshot.Plan.Evidence[0]) snapshot = recanonicalize(t, snapshot) - decision := New(registry, testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + decision := New(registry, testObjectiveContracts()).Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "gate.build.record" { t.Fatalf("decision = %#v, want higher selection class despite lower-layer numeric priority", decision) } @@ -245,19 +245,19 @@ func TestSelectionClassOutranksComponentLocalPriority(t *testing.T) { func TestUntargetedResolutionUsesCurrentGateEvidenceForProgress(t *testing.T) { // control-law: verified-gate-progress-is-derived-from-canonical-evidence authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} - snapshot, goal := openPRSnapshot(t, "build") + snapshot, objective := openPRSnapshot(t, "build") snapshot.Publication = model.Known(model.PublicationNone, snapshot.Publication.Evidence[0]) snapshot = recanonicalize(t, snapshot) - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goal, authority, "") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objective, authority, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "gate.test.record" { t.Fatalf("one-gate decision = %#v, want gate.test.record", decision) } - snapshot, goal = openPRSnapshot(t, "build", "test", "review") + snapshot, objective = openPRSnapshot(t, "build", "test", "review") snapshot.Publication = model.Known(model.PublicationNone, snapshot.Publication.Evidence[0]) snapshot = recanonicalize(t, snapshot) - decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goal, authority, "") + decision = New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objective, authority, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "publication.preview" { t.Fatalf("complete-gates decision = %#v, want publication.preview", decision) } @@ -265,16 +265,16 @@ func TestUntargetedResolutionUsesCurrentGateEvidenceForProgress(t *testing.T) { func TestUntargetedResolutionStopsAtSelectedProviderBoundary(t *testing.T) { // control-law: unavailable-authority-cannot-be-skipped-for-a-lower-priority-effect - snapshot, goal := openPRSnapshot(t, "build", "test", "review") - supervisor := New(testprogram.StandardRegistry(), testGoalContracts()) + snapshot, objective := openPRSnapshot(t, "build", "test", "review") + supervisor := New(testprogram.StandardRegistry(), testObjectiveContracts()) authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} - decision := supervisor.Resolve(snapshot, goal, authority, "") + decision := supervisor.Resolve(snapshot, objective, authority, "") if decision.Kind != DecisionFrontier || len(decision.Candidates) != 1 || decision.Candidates[0] != "publication.execute" { t.Fatalf("provider-free decision = %#v, want publication.execute FRONTIER", decision) } authority[catalog.AuthorityProvider] = true - decision = supervisor.Resolve(snapshot, goal, authority, "") + decision = supervisor.Resolve(snapshot, objective, authority, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "publication.execute" { t.Fatalf("provider-authorized decision = %#v, want publication.execute", decision) } @@ -282,8 +282,8 @@ func TestUntargetedResolutionStopsAtSelectedProviderBoundary(t *testing.T) { func TestRequestedTransitionRequiresExactAuthority(t *testing.T) { // control-law: useful-action-is-not-effect-authority - s := New(testprogram.StandardRegistry(), testGoalContracts()) - decision := s.Resolve(snapshotFor(t, model.PhaseActive, model.TerminalNonterminal), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "plan.approve") + s := New(testprogram.StandardRegistry(), testObjectiveContracts()) + decision := s.Resolve(snapshotFor(t, model.PhaseActive, model.TerminalNonterminal), objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "plan.approve") if decision.Kind != DecisionFrontier { t.Fatalf("decision = %s, want FRONTIER", decision.Kind) } @@ -292,7 +292,7 @@ func TestRequestedTransitionRequiresExactAuthority(t *testing.T) { func TestPlanApprovalPolicyDistinguishesHumanFromAutonomyAuthority(t *testing.T) { snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) autonomy := catalog.AuthoritySet{catalog.AuthorityAutonomy: true} - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), autonomy, "plan.approve") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), autonomy, "plan.approve") if decision.Kind != DecisionFrontier { t.Fatalf("human-only policy decision = %#v, want FRONTIER", decision) } @@ -301,7 +301,7 @@ func TestPlanApprovalPolicyDistinguishesHumanFromAutonomyAuthority(t *testing.T) if err != nil { t.Fatal(err) } - decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), autonomy, "plan.approve") + decision = New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), autonomy, "plan.approve") if decision.Kind != DecisionPrescribed { t.Fatalf("autonomy-enabled policy decision = %#v, want PRESCRIBED", decision) } @@ -314,7 +314,7 @@ func TestDisabledHostCannotRequestManagedTransition(t *testing.T) { if err != nil { t.Fatal(err) } - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "plan.approve") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "plan.approve") if decision.Kind != DecisionRefused { t.Fatalf("disabled host decision = %#v, want REFUSED", decision) } @@ -329,12 +329,12 @@ func TestHighRiskReviewPolicyRequiresHumanAuthority(t *testing.T) { if err != nil { t.Fatal(err) } - supervisor := New(testprogram.StandardRegistry(), testGoalContracts()) - decision := supervisor.Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "gate.review.record") + supervisor := New(testprogram.StandardRegistry(), testObjectiveContracts()) + decision := supervisor.Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "gate.review.record") if decision.Kind != DecisionFrontier { t.Fatalf("repository-only high-risk review = %#v, want FRONTIER", decision) } - decision = supervisor.Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "gate.review.record") + decision = supervisor.Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "gate.review.record") if decision.Kind != DecisionPrescribed { t.Fatalf("human high-risk review = %#v, want PRESCRIBED", decision) } @@ -347,7 +347,7 @@ func TestVisualEvidenceOffRefusesAttachment(t *testing.T) { if err != nil { t.Fatal(err) } - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "evidence.visual.attach") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "evidence.visual.attach") if decision.Kind != DecisionRefused { t.Fatalf("visual-off decision = %#v, want REFUSED", decision) } @@ -355,8 +355,8 @@ func TestVisualEvidenceOffRefusesAttachment(t *testing.T) { func TestRecoveryModeOnlyPrescribesRecoveryTransition(t *testing.T) { // control-law: recovery-outranks-slice-position - s := New(testprogram.StandardRegistry(), testGoalContracts()) - decision := s.Resolve(snapshotFor(t, model.PhaseRecovery, model.TerminalNonterminal), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "recovery.resume") + s := New(testprogram.StandardRegistry(), testObjectiveContracts()) + decision := s.Resolve(snapshotFor(t, model.PhaseRecovery, model.TerminalNonterminal), objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "recovery.resume") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.Class != catalog.EventRecovery { t.Fatalf("decision = %#v, want a recovery prescription", decision) } @@ -364,7 +364,7 @@ func TestRecoveryModeOnlyPrescribesRecoveryTransition(t *testing.T) { func TestRecoveryModeRejectsARecoveryEventOutsideExactJournalContract(t *testing.T) { snapshot := snapshotFor(t, model.PhaseRecovery, model.TerminalNonterminal) - decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "recovery.rollback") + decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, objectiveFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "recovery.rollback") if decision.Kind != DecisionRefused { t.Fatalf("unpermitted recovery decision = %#v, want REFUSED", decision) } @@ -372,15 +372,15 @@ func TestRecoveryModeRejectsARecoveryEventOutsideExactJournalContract(t *testing func TestUncontrollableEventCannotBeRequested(t *testing.T) { // control-law: surfaces-cannot-assert-external-facts - s := New(testprogram.StandardRegistry(), testGoalContracts()) - decision := s.Resolve(snapshotFor(t, model.PhaseActive, model.TerminalNonterminal), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "external.pr-merged") + s := New(testprogram.StandardRegistry(), testObjectiveContracts()) + decision := s.Resolve(snapshotFor(t, model.PhaseActive, model.TerminalNonterminal), objectiveFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "external.pr-merged") if decision.Kind != DecisionRefused { t.Fatalf("decision = %s, want REFUSED", decision.Kind) } } func TestGuardDeniesDestructionAndRoutesManagedBypassThroughAdmission(t *testing.T) { - s := New(testprogram.StandardRegistry(), testGoalContracts()) + s := New(testprogram.StandardRegistry(), testObjectiveContracts()) snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) destructive := s.Guard(snapshot, CommandIntent{Class: IntentDestructive, Operation: "git.reset-hard", Fingerprint: "fingerprint"}) if destructive.Allowed { diff --git a/boatstack/flow/standard/transitions.json b/boatstack/flow/standard/transitions.json index ed62296..a184380 100644 --- a/boatstack/flow/standard/transitions.json +++ b/boatstack/flow/standard/transitions.json @@ -18,7 +18,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -46,14 +46,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -143,7 +143,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -207,7 +207,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 35 }, { @@ -230,7 +232,7 @@ "ACTIVE", "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -257,14 +259,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -340,7 +342,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -404,7 +406,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 40 }, { @@ -427,7 +431,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -455,14 +459,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime", "facet:configuration-policy" @@ -550,7 +554,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -616,12 +620,13 @@ "resumption_predicate": "recovery-contract-for:plan.approve" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "authority_rule": "plan-approval" + "authority_rule": "plan-approval", + "objective_scope": "bound-exact" }, "priority": 45 }, @@ -644,7 +649,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "verified-implementation", "open-or-updated-pr", "merged-delivery" @@ -670,14 +675,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -752,7 +757,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -816,7 +821,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 50 }, { @@ -838,7 +845,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -866,14 +873,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -964,7 +971,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -1028,7 +1035,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 42 }, { @@ -1050,7 +1059,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1078,14 +1087,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime", "facet:configuration-policy" @@ -1173,7 +1182,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -1239,12 +1248,13 @@ "resumption_predicate": "recovery-contract-for:plan.approve-amendment" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "authority_rule": "plan-approval" + "authority_rule": "plan-approval", + "objective_scope": "bound-exact" }, "priority": 46 }, @@ -1267,7 +1277,7 @@ "target_phases": [ "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1294,14 +1304,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal" + "facet:objective" ], "owned_resources": [ "plan-evidence" @@ -1379,7 +1389,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -1425,7 +1435,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 41 }, { @@ -1448,7 +1460,7 @@ "target_phases": [ "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "safely-abandoned" ], "required_identity": [ @@ -1471,12 +1483,12 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:delivery", "facet:program", "facet:terminal", "facet:engagement", - "facet:goal" + "facet:objective" ], "owned_resources": [ "plan" @@ -1539,7 +1551,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -1603,7 +1615,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 90 }, { @@ -1625,7 +1639,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1653,14 +1667,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -1752,7 +1766,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -1816,7 +1830,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 52, "allows_worktree_transfer": true }, @@ -1839,7 +1855,7 @@ "ACTIVE", "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1867,14 +1883,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -1957,7 +1973,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -2021,7 +2037,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 58 }, { @@ -2043,7 +2061,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2070,14 +2088,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -2160,7 +2178,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -2224,7 +2242,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 53 }, { @@ -2245,7 +2265,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "open-or-updated-pr", "merged-delivery" ], @@ -2269,14 +2289,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -2358,7 +2378,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -2422,7 +2442,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 75 }, { @@ -2448,7 +2470,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2476,14 +2498,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -2567,7 +2589,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -2627,14 +2649,15 @@ "resumption_predicate": "recovery-contract-for:workspace.cleanup" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { "managed_operations": [ "workspace.remove" - ] + ], + "objective_scope": "bound-exact" }, "priority": 92, "allows_worktree_transfer": true @@ -2661,7 +2684,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2688,14 +2711,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -2779,7 +2802,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -2839,11 +2862,13 @@ "resumption_predicate": "recovery-contract-for:workspace.reap" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 98, "allows_worktree_transfer": true }, @@ -2866,7 +2891,7 @@ "target_phases": [ "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "safely-abandoned" ], "required_identity": [ @@ -2889,12 +2914,12 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:workspace", "facet:program", "facet:terminal", "facet:engagement", - "facet:goal" + "facet:objective" ], "owned_resources": [ "workspace" @@ -2959,7 +2984,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -3023,7 +3048,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 91 }, { @@ -3050,7 +3077,7 @@ "TERMINAL", "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3078,11 +3105,11 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:program", "facet:engagement", - "facet:goal" + "facet:objective" ], "owned_resources": [ "workspace" @@ -3132,7 +3159,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -3187,7 +3214,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 2, "allows_worktree_transfer": true }, @@ -3209,7 +3238,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3236,7 +3265,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:delivery", "facet:program", @@ -3244,7 +3273,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -3347,7 +3376,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -3407,12 +3436,13 @@ "resumption_predicate": "recovery-contract-for:gate.build.record" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "current_evidence_prefix": "gate-evidence:build:" + "current_evidence_prefix": "gate-evidence:build:", + "objective_scope": "bound-exact" }, "priority": 61, "binds_source_revision": true @@ -3436,7 +3466,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3463,7 +3493,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:delivery", "facet:program", @@ -3471,7 +3501,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -3574,7 +3604,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -3634,12 +3664,13 @@ "resumption_predicate": "recovery-contract-for:gate.test.record" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "current_evidence_prefix": "gate-evidence:test:" + "current_evidence_prefix": "gate-evidence:test:", + "objective_scope": "bound-exact" }, "priority": 62, "binds_source_revision": true @@ -3663,7 +3694,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3691,7 +3722,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:delivery", "facet:program", @@ -3699,7 +3730,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime", "facet:configuration-policy" @@ -3803,7 +3834,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -3869,13 +3900,14 @@ "resumption_predicate": "recovery-contract-for:gate.review.record" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { "authority_rule": "independent-high-risk-review", - "current_evidence_prefix": "gate-evidence:review:" + "current_evidence_prefix": "gate-evidence:review:", + "objective_scope": "bound-exact" }, "priority": 63, "binds_source_revision": true @@ -3898,7 +3930,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3925,7 +3957,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:delivery", "facet:program", @@ -3933,7 +3965,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -4036,7 +4068,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -4096,12 +4128,13 @@ "resumption_predicate": "recovery-contract-for:gate.change.record" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "current_evidence_prefix": "gate-evidence:change:" + "current_evidence_prefix": "gate-evidence:change:", + "objective_scope": "bound-exact" }, "priority": 64, "binds_source_revision": true @@ -4124,7 +4157,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4151,7 +4184,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:delivery", "facet:program", @@ -4159,7 +4192,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -4262,7 +4295,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -4322,12 +4355,13 @@ "resumption_predicate": "recovery-contract-for:gate.journey.record" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "current_evidence_prefix": "gate-evidence:journey:" + "current_evidence_prefix": "gate-evidence:journey:", + "objective_scope": "bound-exact" }, "priority": 64, "binds_source_revision": true @@ -4351,7 +4385,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4379,14 +4413,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:delivery", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime", "facet:configuration-policy" @@ -4482,7 +4516,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -4545,14 +4579,15 @@ "resumption_predicate": "recovery-contract-for:evidence.visual.attach" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { "required_when": "visual-evidence-required", "availability_rule": "visual-evidence-enabled", - "current_evidence_prefix": "visual-evidence:" + "current_evidence_prefix": "visual-evidence:", + "objective_scope": "bound-exact" }, "priority": 66, "binds_source_revision": true @@ -4576,7 +4611,7 @@ "target_phases": [ "FRONTIER" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4603,14 +4638,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal" + "facet:objective" ], "owned_resources": [ "approval" @@ -4684,7 +4719,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -4730,7 +4765,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 44 }, { @@ -4752,7 +4789,7 @@ "ACTIVE", "TERMINAL" ], - "goal_kinds": [ + "objective_kinds": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4780,14 +4817,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:delivery", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -4874,7 +4911,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -4938,7 +4975,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 68, "binds_source_revision": true }, @@ -4960,7 +4999,7 @@ "target_phases": [ "ACTIVE" ], - "goal_kinds": [ + "objective_kinds": [ "open-or-updated-pr", "merged-delivery" ], @@ -4984,7 +5023,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:plan", "facet:verification", "facet:workspace", @@ -4993,7 +5032,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -5104,7 +5143,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -5168,7 +5207,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 72 }, { @@ -5190,7 +5231,7 @@ "ACTIVE", "RECOVERY" ], - "goal_kinds": [ + "objective_kinds": [ "open-or-updated-pr", "merged-delivery" ], @@ -5218,7 +5259,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication", "facet:verification", "facet:workspace", @@ -5227,7 +5268,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -5328,7 +5369,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -5395,7 +5436,8 @@ "managed_operations": [ "publication.create", "publication.push" - ] + ], + "objective_scope": "bound-exact" }, "priority": 76, "authority_fingerprint_parameter": "preview_fingerprint" @@ -5424,7 +5466,7 @@ "FRONTIER", "UNRESOLVED" ], - "goal_kinds": [ + "objective_kinds": [ "open-or-updated-pr", "merged-delivery" ], @@ -5448,14 +5490,14 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication", "facet:program", "facet:recovery", "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -5542,7 +5584,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -5606,12 +5648,13 @@ "resumption_predicate": "recovery-contract-for:publication.observe" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "rechecks_external_state": true + "rechecks_external_state": true, + "objective_scope": "bound-exact" }, "priority": 77 }, @@ -5637,7 +5680,7 @@ "FRONTIER", "UNRESOLVED" ], - "goal_kinds": [ + "objective_kinds": [ "open-or-updated-pr", "merged-delivery" ], @@ -5662,12 +5705,12 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:recovery-info", "facet:publication", "facet:program", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -5730,7 +5773,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -5803,11 +5846,13 @@ "resumption_predicate": "recovery-contract-for:publication.reconcile" }, "reversibility": "reversible", - "terminal_effect": "may-establish-configured-goal-after-fresh-observation", + "terminal_effect": "may-establish-configured-objective-after-fresh-observation", "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "optional-preserve" + }, "priority": 1, "authority_fingerprint_parameter": "publication_id" }, @@ -5832,7 +5877,7 @@ "ACTIVE", "RECOVERY" ], - "goal_kinds": [ + "objective_kinds": [ "open-or-updated-pr", "merged-delivery" ], @@ -5860,7 +5905,7 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:publication", "facet:verification", "facet:program", @@ -5868,7 +5913,7 @@ "facet:transaction", "facet:terminal", "facet:engagement", - "facet:goal", + "facet:objective", "facet:configuration", "facet:runtime" ], @@ -5971,7 +6016,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -6039,7 +6084,8 @@ "publication.edit", "publication.ready", "publication.api-write" - ] + ], + "objective_scope": "bound-exact" }, "priority": 80, "authority_fingerprint_parameter": "body_sha256" @@ -6063,7 +6109,7 @@ "target_phases": [ "ABANDONED" ], - "goal_kinds": [ + "objective_kinds": [ "safely-abandoned" ], "required_identity": [ @@ -6086,12 +6132,12 @@ "required_evidence": [ "invocation-context", "snapshot-fingerprint", - "goal", + "objective", "facet:delivery", "facet:program", "facet:terminal", "facet:engagement", - "facet:goal" + "facet:objective" ], "owned_resources": [ "publication" @@ -6154,7 +6200,7 @@ ] }, { - "facet": "goal", + "facet": "objective", "statuses": [ "known" ] @@ -6218,7 +6264,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "objective_scope": "bound-exact" + }, "priority": 93 } ] diff --git a/boatstack/internal/kernel/catalog/goal_contract.go b/boatstack/internal/kernel/catalog/goal_contract.go deleted file mode 100644 index 87d6d2d..0000000 --- a/boatstack/internal/kernel/catalog/goal_contract.go +++ /dev/null @@ -1,85 +0,0 @@ -package catalog - -import ( - "fmt" - "sort" - - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" -) - -// GoalContract is the compiled terminal law supplied by the program runtime. -// Extension conditions are conjunctive and therefore can only narrow the -// terminal set. -type GoalContract struct { - GoalKind model.GoalKind `json:"goal_kind"` - Conditions []FacetCondition `json:"conditions"` -} - -type GoalContracts map[model.GoalKind]GoalContract - -func (c GoalContracts) Clone() GoalContracts { - result := make(GoalContracts, len(c)) - for goal, contract := range c { - contract.Conditions = cloneConditions(contract.Conditions) - result[goal] = contract - } - return result -} - -func NewGoalContracts(base []GoalContract, extension map[model.GoalKind][]FacetCondition) (GoalContracts, error) { - contracts := make(GoalContracts, len(base)) - for _, contract := range base { - if !contract.GoalKind.Valid() || len(contract.Conditions) == 0 { - return nil, fmt.Errorf("goal contract requires a valid goal and conditions") - } - if _, exists := contracts[contract.GoalKind]; exists { - return nil, fmt.Errorf("duplicate goal contract %q", contract.GoalKind) - } - conditions := append([]FacetCondition(nil), contract.Conditions...) - conditions = append(conditions, extension[contract.GoalKind]...) - for _, condition := range conditions { - if !condition.Facet.Valid() || len(condition.Statuses) == 0 { - return nil, fmt.Errorf("goal %q has invalid terminal condition", contract.GoalKind) - } - for _, status := range condition.Statuses { - if !status.Valid() { - return nil, fmt.Errorf("goal %q has invalid terminal status %q", contract.GoalKind, status) - } - } - } - contract.Conditions = conditions - contracts[contract.GoalKind] = contract - } - for goal := range extension { - if _, exists := contracts[goal]; !exists { - return nil, fmt.Errorf("extension constrains unsupported goal %q", goal) - } - } - return contracts, nil -} - -func (c GoalContracts) Matches(snapshot model.Snapshot, goal model.Goal) bool { - if snapshot.Goal.Status != model.FactKnown || snapshot.Goal.Value != goal { - return false - } - contract, ok := c[goal.Kind] - if !ok { - return false - } - for _, condition := range contract.Conditions { - if !condition.Matches(snapshot) { - return false - } - } - return true -} - -func (c GoalContracts) All() []GoalContract { - result := make([]GoalContract, 0, len(c)) - for _, contract := range c { - contract.Conditions = append([]FacetCondition(nil), contract.Conditions...) - result = append(result, contract) - } - sort.Slice(result, func(i, j int) bool { return result[i].GoalKind < result[j].GoalKind }) - return result -} diff --git a/boatstack/internal/kernel/engine/maintenance_replay_test.go b/boatstack/internal/kernel/engine/maintenance_replay_test.go deleted file mode 100644 index 85a233c..0000000 --- a/boatstack/internal/kernel/engine/maintenance_replay_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package engine - -import ( - "testing" - - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" -) - -func TestMaintenanceReplayBindsDurableGoalState(t *testing.T) { - // control-law: maintenance-replay-preserves-verified-product-goal-state - configured := model.Goal{ID: "configured", Kind: model.GoalOpenPR, DeliveryID: "delivery"} - commandGoal := model.Goal{ID: "command", Kind: model.GoalApprovedPlan, DeliveryID: "other"} - request := ApplyRequest{ResolveRequest: ResolveRequest{Goal: commandGoal}, FlowID: "flow"} - - tests := []struct { - name string - receipt protocol.TransitionReceipt - fact model.Fact[model.Goal] - wantErr bool - }{ - {name: "absent survives command goal and retry", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactAbsent}, fact: model.Fact[model.Goal]{Status: model.FactAbsent}}, - {name: "known survives conflicting command goal and retry", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactKnown, GoalID: configured.ID, GoalKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}}, - {name: "absent cannot replay after product goal appears", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactAbsent}, fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}, wantErr: true}, - {name: "known cannot replay after product goal changes", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactKnown, GoalID: configured.ID, GoalKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: commandGoal}, wantErr: true}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - test.receipt.FlowID = request.FlowID - test.receipt.Program = syntheticProgram - if err := validateReplayRequest(test.receipt, request, syntheticProgramFingerprint); err != nil { - t.Fatalf("command goal affected maintenance replay identity: %v", err) - } - err := validateReplayGoalState(test.receipt, model.Snapshot{Observation: model.Observation{Goal: test.fact}}) - if test.wantErr && err == nil { - t.Fatal("changed durable product-goal state was accepted for replay") - } - if !test.wantErr && err != nil { - t.Fatalf("unchanged durable product-goal state rejected: %v", err) - } - }) - } -} diff --git a/boatstack/internal/kernel/model/goal.go b/boatstack/internal/kernel/model/goal.go deleted file mode 100644 index 39ff2e5..0000000 --- a/boatstack/internal/kernel/model/goal.go +++ /dev/null @@ -1,45 +0,0 @@ -package model - -import ( - "fmt" - "regexp" -) - -type GoalKind string - -const ( - GoalApprovedPlan GoalKind = "approved-plan" - GoalVerified GoalKind = "verified-implementation" - GoalOpenPR GoalKind = "open-or-updated-pr" - GoalMerged GoalKind = "merged-delivery" - GoalAbandoned GoalKind = "safely-abandoned" -) - -func (k GoalKind) Valid() bool { - switch k { - case GoalApprovedPlan, GoalVerified, GoalOpenPR, GoalMerged, GoalAbandoned: - return true - default: - return false - } -} - -type Goal struct { - ID string `json:"id"` - Kind GoalKind `json:"kind"` - DeliveryID string `json:"delivery_id"` - EvidenceFingerprint string `json:"evidence_fingerprint,omitempty"` - FrontierIsStop bool `json:"frontier_is_stop,omitempty"` -} - -var safeGoalIdentity = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) - -func (g Goal) Validate() error { - if !safeGoalIdentity.MatchString(g.ID) || !safeGoalIdentity.MatchString(g.DeliveryID) { - return fmt.Errorf("goal: id and delivery identity must be safe semantic segments") - } - if !g.Kind.Valid() { - return fmt.Errorf("goal: invalid kind %q", g.Kind) - } - return nil -} diff --git a/boatstack/internal/kernel/protocol/maintenance_goal_test.go b/boatstack/internal/kernel/protocol/maintenance_goal_test.go deleted file mode 100644 index 8a5e7b4..0000000 --- a/boatstack/internal/kernel/protocol/maintenance_goal_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package protocol - -import ( - "testing" - - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" -) - -func TestMaintenanceGoalBindingUsesOnlyDurableProductState(t *testing.T) { - // control-law: maintenance-admission-is-independent-from-command-product-intent - transition := catalog.Transition{ID: "installation.update", Policy: catalog.PolicyContract{GoalScope: catalog.GoalScopeOptionalPreserve}} - configured := model.Goal{ID: "configured", Kind: model.GoalOpenPR, DeliveryID: "delivery"} - conflicting := model.Goal{ID: "command", Kind: model.GoalApprovedPlan, DeliveryID: "other"} - - tests := []struct { - name string - fact model.Fact[model.Goal] - request model.Goal - want model.Goal - wantFail bool - }{ - {name: "goal absent", fact: model.Fact[model.Goal]{Status: model.FactAbsent}, request: conflicting}, - {name: "goal known and preserved", fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}, want: configured}, - {name: "conflicting command-scoped goal ignored", fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}, request: conflicting, want: configured}, - {name: "unknown goal fails closed", fact: model.Fact[model.Goal]{Status: model.FactUnknown}, request: conflicting, wantFail: true}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, err := GoalForTransition(model.Snapshot{Observation: model.Observation{Goal: test.fact}}, test.request, transition) - if test.wantFail { - if err == nil { - t.Fatalf("unknown product-goal evidence produced %#v", got) - } - return - } - if err != nil || got != test.want { - t.Fatalf("goal binding = %#v, %v; want %#v", got, err, test.want) - } - }) - } -} diff --git a/boatstack/internal/kernel/protocol/prescription.go b/boatstack/internal/kernel/protocol/prescription.go deleted file mode 100644 index 6208f37..0000000 --- a/boatstack/internal/kernel/protocol/prescription.go +++ /dev/null @@ -1,99 +0,0 @@ -package protocol - -import ( - "fmt" - - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" -) - -const PrescriptionSchemaVersion = 2 - -// Prescription is the immutable compare-and-swap binding emitted by -// resolution and required by apply. It carries no reusable authority or -// credential material, only the content identity of the admitted projection. -type Prescription struct { - SchemaVersion int `json:"schema_version"` - ID string `json:"id"` - TransitionID catalog.TransitionID `json:"transition_id"` - ExpectedStateRevision uint64 `json:"expected_state_revision"` - ExpectedProgramFingerprint string `json:"expected_program_fingerprint"` - ExpectedSnapshotFingerprint string `json:"expected_snapshot_fingerprint"` - AuthorityFingerprint string `json:"authority_fingerprint"` - RequiredCapabilities []catalog.Capability `json:"required_capabilities"` - EffectiveCapabilities []catalog.Capability `json:"effective_capabilities"` -} - -func NewPrescription(snapshot model.Snapshot, transition catalog.Transition, capabilities CapabilityProjection) (Prescription, error) { - prescription := Prescription{ - SchemaVersion: PrescriptionSchemaVersion, - TransitionID: transition.ID, - ExpectedStateRevision: snapshot.StateRevision, - ExpectedProgramFingerprint: snapshot.ProgramFingerprint, - ExpectedSnapshotFingerprint: snapshot.Fingerprint, - AuthorityFingerprint: capabilities.AuthorityFingerprint, - RequiredCapabilities: append([]catalog.Capability(nil), capabilities.Required...), - EffectiveCapabilities: append([]catalog.Capability(nil), capabilities.Effective...), - } - if err := prescription.validateFields(); err != nil { - return Prescription{}, err - } - identity := prescription - identity.ID = "" - var err error - prescription.ID, err = contentID("prx-", identity) - if err != nil { - return Prescription{}, err - } - return prescription, nil -} - -func (p Prescription) Validate() error { - if err := p.validateFields(); err != nil { - return err - } - identity := p - want := identity.ID - identity.ID = "" - got, err := contentID("prx-", identity) - if err != nil { - return err - } - if want == "" || got != want { - return fmt.Errorf("prescription failed content identity verification") - } - return nil -} - -func (p Prescription) validateFields() error { - if p.SchemaVersion != PrescriptionSchemaVersion || p.TransitionID == "" || p.ExpectedStateRevision == 0 || p.AuthorityFingerprint == "" || - len(p.RequiredCapabilities) == 0 || len(p.EffectiveCapabilities) == 0 || - len(p.ExpectedProgramFingerprint) != 64 || len(p.ExpectedSnapshotFingerprint) != 64 { - return fmt.Errorf("prescription has invalid schema, transition, state revision, program, or snapshot identity") - } - return nil -} - -func (p Prescription) ValidateCurrent(snapshot model.Snapshot, transition catalog.Transition, capabilities CapabilityProjection) error { - if err := p.Validate(); err != nil { - return err - } - if p.TransitionID != transition.ID { - return fmt.Errorf("prescription %q is bound to transition %q, not %q", p.ID, p.TransitionID, transition.ID) - } - if p.ExpectedStateRevision != snapshot.StateRevision { - return fmt.Errorf("prescription %q expected state revision %d, observed %d", p.ID, p.ExpectedStateRevision, snapshot.StateRevision) - } - if p.ExpectedProgramFingerprint != snapshot.ProgramFingerprint { - return fmt.Errorf("prescription %q expected control program %s, observed %s", p.ID, p.ExpectedProgramFingerprint, snapshot.ProgramFingerprint) - } - if p.ExpectedSnapshotFingerprint != snapshot.Fingerprint { - return fmt.Errorf("prescription %q expected snapshot %s, observed %s", p.ID, p.ExpectedSnapshotFingerprint, snapshot.Fingerprint) - } - if p.AuthorityFingerprint != capabilities.AuthorityFingerprint || - !sameCapabilities(p.RequiredCapabilities, capabilities.Required) || - !sameCapabilities(p.EffectiveCapabilities, capabilities.Effective) { - return fmt.Errorf("prescription %q is bound to a different authority or capability context", p.ID) - } - return nil -} diff --git a/boatstack/internal/retromine/classify.go b/boatstack/internal/retromine/classify.go index 37a5984..0075bee 100644 --- a/boatstack/internal/retromine/classify.go +++ b/boatstack/internal/retromine/classify.go @@ -8,7 +8,7 @@ import "strings" // // missing_observation — the operator keeps asking what the system could show // missing_verb — the operator keeps describing an action to take -// missing_setpoint — the operator keeps restating a goal or condition to +// missing_setpoint — the operator keeps restating a objective or condition to // pursue ("until", "every time", "at least") // missing_guard — the operator keeps warning what must not happen // @@ -85,7 +85,7 @@ func SuggestedShape(gapType string) string { case GapVerb: return "Add or prescribe a typed verb: a deterministic command the flow names at the right state." case GapSetpoint: - return "Add a typed setpoint: a persisted goal or condition (like delivery.terminal) the flow pursues so this stops being restated." + return "Add a typed setpoint: a persisted objective or condition (like delivery.terminal) the flow pursues so this stops being restated." case GapGuard: return "Add a typed guard: an enforced precondition or denial (a gate or policy) instead of a remembered warning." default: diff --git a/boatstack/internal/kernel/catalog/capability.go b/boatstack/internal/softwaredelivery/catalog/capability.go similarity index 97% rename from boatstack/internal/kernel/catalog/capability.go rename to boatstack/internal/softwaredelivery/catalog/capability.go index b05fb9a..473d1fd 100644 --- a/boatstack/internal/kernel/catalog/capability.go +++ b/boatstack/internal/softwaredelivery/catalog/capability.go @@ -137,7 +137,7 @@ func KernelEffectCapabilities(transition Transition) []Capability { "publication.observe", "publication.reconcile", "publication.execute", "publication.correct": required[CapabilityCommandExecute] = true } - if strings.HasPrefix(id, "goal.") || strings.HasPrefix(id, "plan.") || strings.HasPrefix(id, "workspace.") || + if strings.HasPrefix(id, "objective.") || strings.HasPrefix(id, "plan.") || strings.HasPrefix(id, "workspace.") || strings.HasPrefix(id, "gate.") || strings.HasPrefix(id, "evidence.") || strings.HasPrefix(id, "delivery.") || strings.HasPrefix(id, "publication.") { required[CapabilityProductMutate] = true diff --git a/boatstack/internal/kernel/catalog/capability_test.go b/boatstack/internal/softwaredelivery/catalog/capability_test.go similarity index 100% rename from boatstack/internal/kernel/catalog/capability_test.go rename to boatstack/internal/softwaredelivery/catalog/capability_test.go diff --git a/boatstack/internal/softwaredelivery/catalog/objective_contract.go b/boatstack/internal/softwaredelivery/catalog/objective_contract.go new file mode 100644 index 0000000..3fb3770 --- /dev/null +++ b/boatstack/internal/softwaredelivery/catalog/objective_contract.go @@ -0,0 +1,85 @@ +package catalog + +import ( + "fmt" + "sort" + + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" +) + +// ObjectiveContract is the compiled terminal law supplied by the program runtime. +// Extension conditions are conjunctive and therefore can only narrow the +// terminal set. +type ObjectiveContract struct { + ObjectiveKind model.ObjectiveKind `json:"objective_kind"` + Conditions []FacetCondition `json:"conditions"` +} + +type ObjectiveContracts map[model.ObjectiveKind]ObjectiveContract + +func (c ObjectiveContracts) Clone() ObjectiveContracts { + result := make(ObjectiveContracts, len(c)) + for objective, contract := range c { + contract.Conditions = cloneConditions(contract.Conditions) + result[objective] = contract + } + return result +} + +func NewObjectiveContracts(base []ObjectiveContract, extension map[model.ObjectiveKind][]FacetCondition) (ObjectiveContracts, error) { + contracts := make(ObjectiveContracts, len(base)) + for _, contract := range base { + if !contract.ObjectiveKind.Valid() || len(contract.Conditions) == 0 { + return nil, fmt.Errorf("objective contract requires a valid objective and conditions") + } + if _, exists := contracts[contract.ObjectiveKind]; exists { + return nil, fmt.Errorf("duplicate objective contract %q", contract.ObjectiveKind) + } + conditions := append([]FacetCondition(nil), contract.Conditions...) + conditions = append(conditions, extension[contract.ObjectiveKind]...) + for _, condition := range conditions { + if !condition.Facet.Valid() || len(condition.Statuses) == 0 { + return nil, fmt.Errorf("objective %q has invalid terminal condition", contract.ObjectiveKind) + } + for _, status := range condition.Statuses { + if !status.Valid() { + return nil, fmt.Errorf("objective %q has invalid terminal status %q", contract.ObjectiveKind, status) + } + } + } + contract.Conditions = conditions + contracts[contract.ObjectiveKind] = contract + } + for objective := range extension { + if _, exists := contracts[objective]; !exists { + return nil, fmt.Errorf("extension constrains unsupported objective %q", objective) + } + } + return contracts, nil +} + +func (c ObjectiveContracts) Matches(snapshot model.Snapshot, objective model.Objective) bool { + if snapshot.Objective.Status != model.FactKnown || snapshot.Objective.Value != objective { + return false + } + contract, ok := c[objective.Kind] + if !ok { + return false + } + for _, condition := range contract.Conditions { + if !condition.Matches(snapshot) { + return false + } + } + return true +} + +func (c ObjectiveContracts) All() []ObjectiveContract { + result := make([]ObjectiveContract, 0, len(c)) + for _, contract := range c { + contract.Conditions = append([]FacetCondition(nil), contract.Conditions...) + result = append(result, contract) + } + sort.Slice(result, func(i, j int) bool { return result[i].ObjectiveKind < result[j].ObjectiveKind }) + return result +} diff --git a/boatstack/internal/kernel/catalog/state_facet.go b/boatstack/internal/softwaredelivery/catalog/state_facet.go similarity index 97% rename from boatstack/internal/kernel/catalog/state_facet.go rename to boatstack/internal/softwaredelivery/catalog/state_facet.go index 6082576..1399e23 100644 --- a/boatstack/internal/kernel/catalog/state_facet.go +++ b/boatstack/internal/softwaredelivery/catalog/state_facet.go @@ -3,7 +3,7 @@ package catalog import ( "fmt" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) type StateFacetPolicy struct { @@ -69,7 +69,7 @@ func durableStateWritesForID(id TransitionID) ([]model.StateFacet, bool) { return append([]model.StateFacet(nil), programStateFacets...), true case "invocation.rebind", "configuration.initialize", "configuration.mutate", "configuration.reconcile", "recovery.escalate": return append([]model.StateFacet(nil), controlStateFacets...), true - case "engagement.begin", "engagement.renew", "engagement.release", "repository.detach", "goal.configure", + case "engagement.begin", "engagement.renew", "engagement.release", "repository.detach", "objective.bind", "plan.create", "plan.validate", "plan.approve", "plan.activate", "plan.amend", "plan.approve-amendment", "plan.invalidate", "plan.abandon", "workspace.cut", "workspace.sync", "workspace.activate", "workspace.publish", "workspace.cleanup", "workspace.reap", "workspace.abandon", "workspace.reconcile", "gate.build.record", "gate.test.record", "gate.review.record", "gate.change.record", "gate.journey.record", diff --git a/boatstack/internal/kernel/catalog/state_facet_test.go b/boatstack/internal/softwaredelivery/catalog/state_facet_test.go similarity index 92% rename from boatstack/internal/kernel/catalog/state_facet_test.go rename to boatstack/internal/softwaredelivery/catalog/state_facet_test.go index 46fab7d..abb62b1 100644 --- a/boatstack/internal/kernel/catalog/state_facet_test.go +++ b/boatstack/internal/softwaredelivery/catalog/state_facet_test.go @@ -4,7 +4,7 @@ import ( "slices" "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func TestKernelOwnsDurableStateFacetPolicies(t *testing.T) { @@ -15,7 +15,7 @@ func TestKernelOwnsDurableStateFacetPolicies(t *testing.T) { {"installation.update", []model.StateFacet{model.StateFacetControl, model.StateFacetInstallation}}, {"installation.reconcile-update", []model.StateFacet{model.StateFacetControl, model.StateFacetInstallation, model.StateFacetProgram}}, {"catalog.reconcile", []model.StateFacet{model.StateFacetControl, model.StateFacetProgram}}, - {"goal.configure", []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}}, + {"objective.bind", []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}}, {"recovery.escalate", []model.StateFacet{model.StateFacetControl}}, } for _, fixture := range fixtures { diff --git a/boatstack/internal/kernel/catalog/transition.go b/boatstack/internal/softwaredelivery/catalog/transition.go similarity index 87% rename from boatstack/internal/kernel/catalog/transition.go rename to boatstack/internal/softwaredelivery/catalog/transition.go index 12d5558..e7fe05e 100644 --- a/boatstack/internal/kernel/catalog/transition.go +++ b/boatstack/internal/softwaredelivery/catalog/transition.go @@ -5,7 +5,8 @@ import ( "regexp" "sort" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + general "github.com/operatorstack/boatstack/boatstack/kernel" ) type TransitionID string @@ -41,7 +42,7 @@ const ( SelectionSystemRecovery SelectionClass = "SYSTEM_RECOVERY" SelectionProgramRecovery SelectionClass = "PROGRAM_RECOVERY" SelectionExtensionRecovery SelectionClass = "EXTENSION_RECOVERY" - SelectionGoalRequired SelectionClass = "GOAL_REQUIRED" + SelectionObjectiveRequired SelectionClass = "OBJECTIVE_REQUIRED" SelectionProgramProgress SelectionClass = "PROGRAM_PROGRESS" SelectionExplicitOnly SelectionClass = "EXPLICIT_ONLY" SelectionObservedExternal SelectionClass = "OBSERVED_EXTERNAL" @@ -49,7 +50,7 @@ const ( func (c SelectionClass) Valid() bool { switch c { - case SelectionSystemRecovery, SelectionProgramRecovery, SelectionExtensionRecovery, SelectionGoalRequired, + case SelectionSystemRecovery, SelectionProgramRecovery, SelectionExtensionRecovery, SelectionObjectiveRequired, SelectionProgramProgress, SelectionExplicitOnly, SelectionObservedExternal: return true default: @@ -57,7 +58,7 @@ func (c SelectionClass) Valid() bool { } } -func (c SelectionClass) rank() int { +func (c SelectionClass) Rank() int { switch c { case SelectionSystemRecovery: return 1 @@ -65,7 +66,7 @@ func (c SelectionClass) rank() int { return 2 case SelectionExtensionRecovery: return 3 - case SelectionGoalRequired: + case SelectionObjectiveRequired: return 4 case SelectionProgramProgress: return 5 @@ -176,22 +177,26 @@ type InterruptionContract struct { ResumptionPredicate string `json:"resumption_predicate"` } -type GoalScope string +// ObjectiveScope is the software-delivery projection of the kernel's general +// objective-scope law. The transition catalog must state the scope explicitly. +type ObjectiveScope = general.ObjectiveScope -const GoalScopeOptionalPreserve GoalScope = "optional-preserve" - -func (s GoalScope) Valid() bool { return s == "" || s == GoalScopeOptionalPreserve } +const ( + ObjectiveScopeNone = general.ObjectiveNone + ObjectiveScopeOptionalPreserve = general.ObjectiveOptionalPreserve + ObjectiveScopeBoundExact = general.ObjectiveBoundExact +) type PolicyContract struct { - RequiredWhen string `json:"required_when,omitempty"` - AuthorityRule string `json:"authority_rule,omitempty"` - AvailabilityRule string `json:"availability_rule,omitempty"` - CurrentEvidencePrefix string `json:"current_evidence_prefix,omitempty"` - ManagedOperations []string `json:"managed_operations,omitempty"` - BindsRequestedGoal bool `json:"binds_requested_goal,omitempty"` - ReconcilesProgram bool `json:"reconciles_program,omitempty"` - RechecksExternalState bool `json:"rechecks_external_state,omitempty"` - GoalScope GoalScope `json:"goal_scope,omitempty"` + RequiredWhen string `json:"required_when,omitempty"` + AuthorityRule string `json:"authority_rule,omitempty"` + AvailabilityRule string `json:"availability_rule,omitempty"` + CurrentEvidencePrefix string `json:"current_evidence_prefix,omitempty"` + ManagedOperations []string `json:"managed_operations,omitempty"` + BindsRequestedObjective bool `json:"binds_requested_objective,omitempty"` + ReconcilesProgram bool `json:"reconciles_program,omitempty"` + RechecksExternalState bool `json:"rechecks_external_state,omitempty"` + ObjectiveScope ObjectiveScope `json:"objective_scope,omitempty"` } // FacetCondition is an executable, serializable predicate over one canonical @@ -239,7 +244,7 @@ type Transition struct { Class EventClass `json:"class"` SourcePhases []model.ProtocolPhase `json:"source_phases"` TargetPhases []model.ProtocolPhase `json:"target_phases"` - GoalKinds []model.GoalKind `json:"goal_kinds,omitempty"` + ObjectiveKinds []model.ObjectiveKind `json:"objective_kinds,omitempty"` RequiredIdentity []string `json:"required_identity"` Authority []AuthorityClass `json:"authority"` AuthorityAll []AuthorityClass `json:"authority_all,omitempty"` @@ -279,25 +284,25 @@ func (t Transition) Controllable() bool { return t.Class.Controllable() } // ImplicitlySelectable reports whether an untargeted resolution may choose the // transition as delivery progress. Maintenance, correction, abandonment, and // caller-defined markers remain available through an explicit requested -// transition, but cannot outrank the configured goal by merely being +// transition, but cannot outrank the configured objective by merely being // admissible from the same snapshot. func (t Transition) ImplicitlySelectable() bool { return t.SelectionClass == SelectionSystemRecovery || t.SelectionClass == SelectionProgramRecovery || t.SelectionClass == SelectionExtensionRecovery || - t.SelectionClass == SelectionGoalRequired || + t.SelectionClass == SelectionObjectiveRequired || t.SelectionClass == SelectionProgramProgress } -func (t Transition) SupportsGoal(goal model.Goal) bool { - if t.Policy.GoalScope == GoalScopeOptionalPreserve { +func (t Transition) SupportsObjective(objective model.Objective) bool { + if t.Policy.ObjectiveScope == ObjectiveScopeOptionalPreserve { return true } - if len(t.GoalKinds) == 0 { + if len(t.ObjectiveKinds) == 0 { return true } - for _, kind := range t.GoalKinds { - if kind == goal.Kind { + for _, kind := range t.ObjectiveKinds { + if kind == objective.Kind { return true } } @@ -322,7 +327,7 @@ func (t Transition) SourceMatches(snapshot model.Snapshot) bool { return false } } - if t.Policy.GoalScope == GoalScopeOptionalPreserve && snapshot.Goal.Status != model.FactKnown && snapshot.Goal.Status != model.FactAbsent { + if t.Policy.ObjectiveScope == ObjectiveScopeOptionalPreserve && snapshot.Objective.Status != model.FactKnown && snapshot.Objective.Status != model.FactAbsent { return false } return true @@ -381,8 +386,8 @@ func New(transitions []Transition) (Registry, error) { } } sort.SliceStable(registry.ordered, func(i, j int) bool { - if registry.ordered[i].SelectionClass.rank() != registry.ordered[j].SelectionClass.rank() { - return registry.ordered[i].SelectionClass.rank() < registry.ordered[j].SelectionClass.rank() + if registry.ordered[i].SelectionClass.Rank() != registry.ordered[j].SelectionClass.Rank() { + return registry.ordered[i].SelectionClass.Rank() < registry.ordered[j].SelectionClass.Rank() } if registry.ordered[i].Priority != registry.ordered[j].Priority { return registry.ordered[i].Priority < registry.ordered[j].Priority @@ -425,12 +430,12 @@ func validateTransition(t Transition) error { return fmt.Errorf("%s: invalid phase %q", t.ID, phase) } } - goalKinds := map[model.GoalKind]bool{} - for _, goal := range t.GoalKinds { - if !goal.Valid() || goalKinds[goal] { - return fmt.Errorf("%s: goal kinds must be valid and unique", t.ID) + objectiveKinds := map[model.ObjectiveKind]bool{} + for _, objective := range t.ObjectiveKinds { + if !objective.Valid() || objectiveKinds[objective] { + return fmt.Errorf("%s: objective kinds must be valid and unique", t.ID) } - goalKinds[goal] = true + objectiveKinds[objective] = true } if len(t.Authority) == 0 || len(t.RequiredEvidence) == 0 { return fmt.Errorf("%s: authority and evidence declarations are required", t.ID) @@ -518,14 +523,14 @@ func validateTransition(t Transition) error { } managedOperations[operation] = true } - if !t.Policy.GoalScope.Valid() { - return fmt.Errorf("%s: invalid goal scope %q", t.ID, t.Policy.GoalScope) + if !t.Policy.ObjectiveScope.Valid() { + return fmt.Errorf("%s: invalid objective scope %q", t.ID, t.Policy.ObjectiveScope) } - if t.Policy.GoalScope == GoalScopeOptionalPreserve && t.Policy.BindsRequestedGoal { - return fmt.Errorf("%s: optional-preserve maintenance cannot bind a requested product goal", t.ID) + if t.Policy.ObjectiveScope == ObjectiveScopeOptionalPreserve && t.Policy.BindsRequestedObjective { + return fmt.Errorf("%s: optional-preserve maintenance cannot bind a requested product objective", t.ID) } - if t.Policy.BindsRequestedGoal && (t.Origin.Kind != OriginCoreSystem || !conditionNamesFacet(t.TargetConditions, model.FacetGoal)) { - return fmt.Errorf("%s: requested-goal binding requires a CoreSystem goal target", t.ID) + if t.Policy.BindsRequestedObjective && (t.Origin.Kind != OriginCoreSystem || !conditionNamesFacet(t.TargetConditions, model.FacetObjective)) { + return fmt.Errorf("%s: requested-objective binding requires a CoreSystem objective target", t.ID) } if t.Policy.ReconcilesProgram && (t.Origin.Kind != OriginCoreSystem || !conditionNamesFacet(t.TargetConditions, model.FacetProgram)) { return fmt.Errorf("%s: program reconciliation requires a CoreSystem program target", t.ID) @@ -602,10 +607,10 @@ func (r Registry) ManagedTransition(operation string) (Transition, bool) { return r.Lookup(id) } -func (r Registry) Admissible(snapshot model.Snapshot, goal model.Goal) []Transition { +func (r Registry) Admissible(snapshot model.Snapshot, objective model.Objective) []Transition { var result []Transition for _, transition := range r.ordered { - if transition.Controllable() && transition.SourceMatches(snapshot) && transition.SupportsGoal(goal) { + if transition.Controllable() && transition.SourceMatches(snapshot) && transition.SupportsObjective(objective) { result = append(result, cloneTransition(transition)) } } @@ -623,7 +628,7 @@ func cloneTransitions(values []Transition) []Transition { func cloneTransition(value Transition) Transition { value.SourcePhases = append([]model.ProtocolPhase(nil), value.SourcePhases...) value.TargetPhases = append([]model.ProtocolPhase(nil), value.TargetPhases...) - value.GoalKinds = append([]model.GoalKind(nil), value.GoalKinds...) + value.ObjectiveKinds = append([]model.ObjectiveKind(nil), value.ObjectiveKinds...) value.RequiredIdentity = append([]string(nil), value.RequiredIdentity...) value.Authority = append([]AuthorityClass(nil), value.Authority...) value.AuthorityAll = append([]AuthorityClass(nil), value.AuthorityAll...) diff --git a/boatstack/internal/kernel/durable/binding.go b/boatstack/internal/softwaredelivery/durable/binding.go similarity index 95% rename from boatstack/internal/kernel/durable/binding.go rename to boatstack/internal/softwaredelivery/durable/binding.go index d074900..fb6c11c 100644 --- a/boatstack/internal/kernel/durable/binding.go +++ b/boatstack/internal/softwaredelivery/durable/binding.go @@ -7,7 +7,7 @@ import ( "io" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) const BindingSchemaVersion = 1 diff --git a/boatstack/internal/kernel/durable/state.go b/boatstack/internal/softwaredelivery/durable/state.go similarity index 97% rename from boatstack/internal/kernel/durable/state.go rename to boatstack/internal/softwaredelivery/durable/state.go index 0d80c73..590fd60 100644 --- a/boatstack/internal/kernel/durable/state.go +++ b/boatstack/internal/softwaredelivery/durable/state.go @@ -8,11 +8,11 @@ import ( "sort" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) -const StateSchemaVersion = 2 +const StateSchemaVersion = 3 type GateEvidence struct { Gate string `json:"gate"` @@ -39,7 +39,7 @@ type State struct { Recovery model.RecoveryState `json:"recovery"` Transaction model.TransactionState `json:"transaction"` Terminal model.TerminalStatus `json:"terminal"` - Goal model.Goal `json:"goal"` + Objective model.Objective `json:"objective"` SourceRevision string `json:"source_revision,omitempty"` WorktreeFingerprint string `json:"worktree_fingerprint,omitempty"` ConfigFingerprint string `json:"config_fingerprint,omitempty"` diff --git a/boatstack/internal/kernel/durable/state_facet.go b/boatstack/internal/softwaredelivery/durable/state_facet.go similarity index 95% rename from boatstack/internal/kernel/durable/state_facet.go rename to boatstack/internal/softwaredelivery/durable/state_facet.go index 8fbee15..a92a2c8 100644 --- a/boatstack/internal/kernel/durable/state_facet.go +++ b/boatstack/internal/softwaredelivery/durable/state_facet.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) var stateFieldFacets = map[string]model.StateFacet{ @@ -16,7 +16,7 @@ var stateFieldFacets = map[string]model.StateFacet{ "Runtime": model.StateFacetInstallation, "Publication": model.StateFacetProduct, "Verification": model.StateFacetProduct, "Recovery": model.StateFacetControl, "Transaction": model.StateFacetControl, - "Terminal": model.StateFacetProduct, "Goal": model.StateFacetProduct, + "Terminal": model.StateFacetProduct, "Objective": model.StateFacetProduct, "SourceRevision": model.StateFacetProduct, "WorktreeFingerprint": model.StateFacetProduct, "ConfigFingerprint": model.StateFacetControl, "PlanApprovalPolicy": model.StateFacetControl, "VisualEvidencePolicy": model.StateFacetControl, "ExternalEffectPolicy": model.StateFacetControl, "IndependentReview": model.StateFacetControl, "EnabledHosts": model.StateFacetControl, diff --git a/boatstack/internal/kernel/durable/state_facet_test.go b/boatstack/internal/softwaredelivery/durable/state_facet_test.go similarity index 83% rename from boatstack/internal/kernel/durable/state_facet_test.go rename to boatstack/internal/softwaredelivery/durable/state_facet_test.go index 82211f2..75d707d 100644 --- a/boatstack/internal/kernel/durable/state_facet_test.go +++ b/boatstack/internal/softwaredelivery/durable/state_facet_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func facetFixture() State { @@ -25,7 +25,7 @@ func TestChangedFacetsPreservesExactDomainValues(t *testing.T) { before := facetFixture() before.ProgramFingerprint = "program-a" before.RuntimeVersion = "runtime-a" - before.Goal = model.Goal{ID: "goal-a", Kind: model.GoalOpenPR, DeliveryID: "delivery-a"} + before.Objective = model.Objective{ID: "objective-a", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery-a"} after := before after.RuntimeVersion = "runtime-b" after.Revision++ @@ -37,7 +37,7 @@ func TestChangedFacetsPreservesExactDomainValues(t *testing.T) { if !reflect.DeepEqual(facets, want) { t.Fatalf("changed facets=%v, want %v", facets, want) } - if after.ProgramFingerprint != before.ProgramFingerprint || after.Goal != before.Goal { + if after.ProgramFingerprint != before.ProgramFingerprint || after.Objective != before.Objective { t.Fatal("facet classification changed legacy program or product values") } } diff --git a/boatstack/internal/kernel/durable/state_revision_test.go b/boatstack/internal/softwaredelivery/durable/state_revision_test.go similarity index 100% rename from boatstack/internal/kernel/durable/state_revision_test.go rename to boatstack/internal/softwaredelivery/durable/state_revision_test.go diff --git a/boatstack/internal/effects/artifacts.go b/boatstack/internal/softwaredelivery/effects/artifacts.go similarity index 96% rename from boatstack/internal/effects/artifacts.go rename to boatstack/internal/softwaredelivery/effects/artifacts.go index 5b44d22..bb8975e 100644 --- a/boatstack/internal/effects/artifacts.go +++ b/boatstack/internal/softwaredelivery/effects/artifacts.go @@ -9,11 +9,11 @@ import ( "path/filepath" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func prepareAttachBinding(layout ports.ControllerLayout, admission protocol.Admission, now time.Time) (ports.ResourceMutation, error) { @@ -92,7 +92,7 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio var deliveryID string if transitionUsesDeliveryArtifacts(transition.ID) { var err error - deliveryID, err = safeSegment(admission.Goal.DeliveryID, "delivery identity") + deliveryID, err = safeSegment(admission.Objective.DeliveryID, "delivery identity") if err != nil { return nil, err } @@ -348,7 +348,7 @@ func loadPublicationPreview(path string) (publicationPreview, error) { } func validatePublicationPreviewForAdmission(layout ports.ControllerLayout, admission protocol.Admission, preview publicationPreview) error { - deliveryID, err := safeSegment(admission.Goal.DeliveryID, "delivery identity") + deliveryID, err := safeSegment(admission.Objective.DeliveryID, "delivery identity") if err != nil { return err } diff --git a/boatstack/internal/effects/cas_integration_test.go b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go similarity index 84% rename from boatstack/internal/effects/cas_integration_test.go rename to boatstack/internal/softwaredelivery/effects/cas_integration_test.go index 89b53d7..f0817f6 100644 --- a/boatstack/internal/effects/cas_integration_test.go +++ b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go @@ -11,17 +11,17 @@ import ( "time" boatstack "github.com/operatorstack/boatstack/boatstack" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/extension/releasenote" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/engine" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/plant" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/engine" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" ) type concurrentApplyResult struct { @@ -35,11 +35,11 @@ func TestConcurrentApplyConsumesOneRevisionExactlyOnce(t *testing.T) { repository := testRepository(t) externalRoot := t.TempDir() program := testProgram() - kernelA, err := boatstack.NewKernel(externalRoot, program) + kernelA, err := boatstack.NewDeliveryController(externalRoot, program) if err != nil { t.Fatal(err) } - kernelB, err := boatstack.NewKernel(externalRoot, program) + kernelB, err := boatstack.NewDeliveryController(externalRoot, program) if err != nil { t.Fatal(err) } @@ -58,10 +58,10 @@ func TestConcurrentApplyConsumesOneRevisionExactlyOnce(t *testing.T) { ID: "cas-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "cas-human-proof", IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour), }}} - goal := model.Goal{ID: "cas-goal", Kind: model.GoalApprovedPlan, DeliveryID: "cas-delivery"} + objective := model.Objective{ID: "cas-objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "cas-delivery"} request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "cas-concurrent", - FlowID: "flow-cas", Goal: goal, TransitionID: "installation.initialize", Authority: human, + FlowID: "flow-cas", Objective: objective, TransitionID: "installation.initialize", Authority: human, Parameters: protocol.Parameters{ {Name: "source_revision", Value: "cas-fixture"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, @@ -89,7 +89,7 @@ func TestConcurrentApplyConsumesOneRevisionExactlyOnce(t *testing.T) { start := make(chan struct{}) results := make(chan concurrentApplyResult, 3) - for _, kernel := range []boatstack.Kernel{kernelA, kernelA, kernelB} { + for _, kernel := range []boatstack.DeliveryController{kernelA, kernelA, kernelB} { kernel := kernel go func() { <-start @@ -168,14 +168,14 @@ func TestProgramChangeInvalidatesPriorPrescriptionBeforeEffects(t *testing.T) { repository := testRepository(t) externalRoot := t.TempDir() programP := testProgram() - programQ, err := control.Compile(ctx, control.CompileRequest{ - KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()}, + programQ, err := delivery.Compile(ctx, delivery.CompileRequest{ + KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []delivery.Extension{releasenote.Definition()}, }) if err != nil { t.Fatal(err) } - kernelP, _ := boatstack.NewKernel(externalRoot, programP) - kernelQ, _ := boatstack.NewKernel(externalRoot, programQ) + kernelP, _ := boatstack.NewDeliveryController(externalRoot, programP) + kernelQ, _ := boatstack.NewDeliveryController(externalRoot, programQ) executable, _ := os.Executable() executable, _ = filepath.Abs(executable) executable, _ = filepath.EvalSymlinks(executable) @@ -189,7 +189,7 @@ func TestProgramChangeInvalidatesPriorPrescriptionBeforeEffects(t *testing.T) { now := time.Now().UTC() request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-cas", - FlowID: "flow-program-cas", Goal: model.Goal{ID: "program-cas", Kind: model.GoalApprovedPlan, DeliveryID: "program-cas"}, TransitionID: "installation.initialize", + FlowID: "flow-program-cas", Objective: model.Objective{ID: "program-cas", Kind: model.ObjectiveApprovedPlan, DeliveryID: "program-cas"}, TransitionID: "installation.initialize", Authority: protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ID: "program-cas-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "human", IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)}}}, Parameters: protocol.Parameters{ {Name: "source_revision", Value: "program-cas"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, diff --git a/boatstack/internal/effects/clock.go b/boatstack/internal/softwaredelivery/effects/clock.go similarity index 100% rename from boatstack/internal/effects/clock.go rename to boatstack/internal/softwaredelivery/effects/clock.go diff --git a/boatstack/internal/effects/command_boundary.go b/boatstack/internal/softwaredelivery/effects/command_boundary.go similarity index 95% rename from boatstack/internal/effects/command_boundary.go rename to boatstack/internal/softwaredelivery/effects/command_boundary.go index bdd8581..866b369 100644 --- a/boatstack/internal/effects/command_boundary.go +++ b/boatstack/internal/softwaredelivery/effects/command_boundary.go @@ -10,13 +10,13 @@ import ( "runtime" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" ) type NativeCommandRunner interface { @@ -225,7 +225,7 @@ func (b NativeBoundary) Execute(ctx context.Context, admission protocol.Admissio return ports.EffectResult{Settlement: ports.EffectUnknown, Detail: strings.TrimSpace(string(output))}, nil } case "publication.execute": - deliveryID, err := safeSegment(admission.Goal.DeliveryID, "delivery identity") + deliveryID, err := safeSegment(admission.Objective.DeliveryID, "delivery identity") if err != nil { return settled, err } diff --git a/boatstack/internal/effects/command_boundary_test.go b/boatstack/internal/softwaredelivery/effects/command_boundary_test.go similarity index 94% rename from boatstack/internal/effects/command_boundary_test.go rename to boatstack/internal/softwaredelivery/effects/command_boundary_test.go index e41f3d3..fa67eb9 100644 --- a/boatstack/internal/effects/command_boundary_test.go +++ b/boatstack/internal/softwaredelivery/effects/command_boundary_test.go @@ -10,11 +10,11 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) diff --git a/boatstack/internal/effects/dirsync_unix.go b/boatstack/internal/softwaredelivery/effects/dirsync_unix.go similarity index 100% rename from boatstack/internal/effects/dirsync_unix.go rename to boatstack/internal/softwaredelivery/effects/dirsync_unix.go diff --git a/boatstack/internal/effects/dirsync_windows.go b/boatstack/internal/softwaredelivery/effects/dirsync_windows.go similarity index 100% rename from boatstack/internal/effects/dirsync_windows.go rename to boatstack/internal/softwaredelivery/effects/dirsync_windows.go diff --git a/boatstack/internal/effects/driver.go b/boatstack/internal/softwaredelivery/effects/driver.go similarity index 97% rename from boatstack/internal/effects/driver.go rename to boatstack/internal/softwaredelivery/effects/driver.go index 013277f..bc821e0 100644 --- a/boatstack/internal/effects/driver.go +++ b/boatstack/internal/softwaredelivery/effects/driver.go @@ -9,12 +9,12 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type CommandBoundary interface { @@ -372,7 +372,7 @@ func parkedSourceState(state durable.State, resultingRevision uint64, transition state.Recovery = model.RecoveryNone state.Transaction = model.TransactionNone state.Terminal = model.TerminalNonterminal - state.Goal = model.Goal{} + state.Objective = model.Objective{} state.SourceRevision = "" state.WorktreeFingerprint = "" state.PlanFingerprint = "" diff --git a/boatstack/internal/effects/executable.go b/boatstack/internal/softwaredelivery/effects/executable.go similarity index 100% rename from boatstack/internal/effects/executable.go rename to boatstack/internal/softwaredelivery/effects/executable.go diff --git a/boatstack/internal/effects/extensions.go b/boatstack/internal/softwaredelivery/effects/extensions.go similarity index 86% rename from boatstack/internal/effects/extensions.go rename to boatstack/internal/softwaredelivery/effects/extensions.go index 4c06411..188c7d2 100644 --- a/boatstack/internal/effects/extensions.go +++ b/boatstack/internal/softwaredelivery/effects/extensions.go @@ -9,26 +9,26 @@ import ( "path/filepath" "strings" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) // NewExtensionLocalPrepared turns a declarative extension write plan into the // same reversible prepared-effect contract used by first-party effects. -func NewExtensionLocalPrepared(repositoryRoot, extensionID string, writes []control.ResourceWrite, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { +func NewExtensionLocalPrepared(repositoryRoot, extensionID string, writes []delivery.ResourceWrite, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { return newNamespacedLocalPrepared(repositoryRoot, filepath.Join("extensions", extensionID), extensionID, "extension", writes, admission, transition) } // NewFlowLocalPrepared constrains a protocol-backed program runtime to its own // repository-local namespace while retaining the normal reversible effect // contract. -func NewFlowLocalPrepared(repositoryRoot, flowID string, writes []control.ResourceWrite, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { +func NewFlowLocalPrepared(repositoryRoot, flowID string, writes []delivery.ResourceWrite, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { return newNamespacedLocalPrepared(repositoryRoot, filepath.Join("flows", flowID), flowID, "program runtime", writes, admission, transition) } -func newNamespacedLocalPrepared(repositoryRoot, namespace, owner, kind string, writes []control.ResourceWrite, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { +func newNamespacedLocalPrepared(repositoryRoot, namespace, owner, kind string, writes []delivery.ResourceWrite, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { if len(writes) == 0 { return nil, fmt.Errorf("%s %q planned no local writes", kind, owner) } diff --git a/boatstack/internal/effects/extensions_test.go b/boatstack/internal/softwaredelivery/effects/extensions_test.go similarity index 86% rename from boatstack/internal/effects/extensions_test.go rename to boatstack/internal/softwaredelivery/effects/extensions_test.go index 78bceaa..ad83531 100644 --- a/boatstack/internal/effects/extensions_test.go +++ b/boatstack/internal/softwaredelivery/effects/extensions_test.go @@ -8,9 +8,9 @@ import ( "runtime" "testing" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func TestNamespacedExtensionWriteRejectsSymlinkEscapeWithoutMutation(t *testing.T) { @@ -31,7 +31,7 @@ func TestNamespacedExtensionWriteRejectsSymlinkEscapeWithoutMutation(t *testing. digest := sha256.Sum256(content) transition := catalog.Transition{ID: "example.guard/write", Effect: "example.guard.write", Class: catalog.EventOwnedLocal, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, DeclaredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}} admission := protocol.Admission{EffectiveCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}} - _, err := NewExtensionLocalPrepared(repository, "example.guard", []control.ResourceWrite{{ + _, err := NewExtensionLocalPrepared(repository, "example.guard", []delivery.ResourceWrite{{ Resource: "example.guard.evidence", Path: filepath.Join(link, "evidence.json"), Content: content, SHA256: hex.EncodeToString(digest[:]), }}, admission, transition) if err == nil { diff --git a/boatstack/internal/effects/filelock_unix.go b/boatstack/internal/softwaredelivery/effects/filelock_unix.go similarity index 100% rename from boatstack/internal/effects/filelock_unix.go rename to boatstack/internal/softwaredelivery/effects/filelock_unix.go diff --git a/boatstack/internal/effects/filelock_windows.go b/boatstack/internal/softwaredelivery/effects/filelock_windows.go similarity index 100% rename from boatstack/internal/effects/filelock_windows.go rename to boatstack/internal/softwaredelivery/effects/filelock_windows.go diff --git a/boatstack/internal/effects/host_skills.go b/boatstack/internal/softwaredelivery/effects/host_skills.go similarity index 98% rename from boatstack/internal/effects/host_skills.go rename to boatstack/internal/softwaredelivery/effects/host_skills.go index eb592b4..fb10f34 100644 --- a/boatstack/internal/effects/host_skills.go +++ b/boatstack/internal/softwaredelivery/effects/host_skills.go @@ -8,7 +8,7 @@ import ( "sort" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" ) const hostSkillManifestSchema = 1 @@ -94,7 +94,7 @@ Run `+ "`boatstack status --repo . --format json`"+` once for observation. An authority-free `+"`FRONTIER`"+` from status is diagnostic only and cannot terminate this selected operation. -Bind one command-scoped context containing the exact goal, delivery, repository, +Bind one command-scoped context containing the exact objective, delivery, repository, worktree, flow, actor, and supplied authority receipts. Preserve that context through every `+"`next`"+`, `+"`apply`"+`, `+"`recover`"+`, and re-resolution. Never synthesize missing authority or infer it from authentication, files, branches, or prior conversation. @@ -119,7 +119,7 @@ Re-resolve with the same context after every complete receipt. Evaluate a frontier only after every requested authority source is materialized or conclusively rejected against the post-receipt state. Stop only on an authority-bearing `+"`FRONTIER`"+`, `+"`BLOCKED`"+`, `+"`REFUSED`"+`, or -`+"`UNRESOLVED`"+` result for this operation. Treat `+"`TERMINAL`"+` as exact goal evidence. +`+"`UNRESOLVED`"+` result for this operation. Treat `+"`TERMINAL`"+` as exact objective evidence. If recovery is active, use only a transition in `+"`recovery_info.permitted`"+` and the exact transaction ID. Never choose maintenance, correction, abandonment, merge, provider, or destructive authority as an escape from a frontier. diff --git a/boatstack/internal/effects/host_skills_test.go b/boatstack/internal/softwaredelivery/effects/host_skills_test.go similarity index 98% rename from boatstack/internal/effects/host_skills_test.go rename to boatstack/internal/softwaredelivery/effects/host_skills_test.go index ca3ca4f..e845540 100644 --- a/boatstack/internal/effects/host_skills_test.go +++ b/boatstack/internal/softwaredelivery/effects/host_skills_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" ) func TestHostSkillProjectionExposesExactlyThreeOperationsPerInteractiveHost(t *testing.T) { diff --git a/boatstack/internal/effects/integration_test.go b/boatstack/internal/softwaredelivery/effects/integration_test.go similarity index 83% rename from boatstack/internal/effects/integration_test.go rename to boatstack/internal/softwaredelivery/effects/integration_test.go index 2d0038d..44e0731 100644 --- a/boatstack/internal/effects/integration_test.go +++ b/boatstack/internal/softwaredelivery/effects/integration_test.go @@ -14,20 +14,20 @@ import ( "time" boatstack "github.com/operatorstack/boatstack/boatstack" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/extension/releasenote" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/effects" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/engine" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" - "github.com/operatorstack/boatstack/boatstack/internal/plant" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/engine" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) @@ -37,20 +37,20 @@ const testProgramFingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa var testProgramIdentity = protocol.ProgramIdentity{ID: "standard", Version: "test", Fingerprint: testProgramFingerprint} -func testGoalContracts() catalog.GoalContracts { +func testObjectiveContracts() catalog.ObjectiveContracts { manifest, err := standard.Definition().RuntimeManifest(context.Background()) if err != nil { panic(err) } - contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil) + contracts, err := catalog.NewObjectiveContracts(manifest.ObjectiveContracts, nil) if err != nil { panic(err) } return contracts } -func testProgram() control.ControlProgram { - program, err := control.Compile(context.Background(), control.CompileRequest{ +func testProgram() delivery.ControlProgram { + program, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), }) if err != nil { @@ -74,7 +74,7 @@ func prescribeEngine(t *testing.T, ctx context.Context, kernel engine.Engine, re return request } -func prescribeSurface(t *testing.T, ctx context.Context, kernel boatstack.Kernel, request surfaces.Request) surfaces.Request { +func prescribeSurface(t *testing.T, ctx context.Context, kernel boatstack.DeliveryController, request surfaces.Request) surfaces.Request { t.Helper() resolve := request resolve.Operation = surfaces.OperationResolve @@ -160,17 +160,17 @@ func TestConcreteBoundaryAppliesAndReceiptsOneTransition(t *testing.T) { if err != nil { t.Fatal(err) } - kernel, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramIdentity, observer, clock, locker, journal, driver, receipts) + kernel, err := engine.New(testprogram.StandardRegistry(), testObjectiveContracts(), testProgramIdentity, observer, clock, locker, journal, driver, receipts) if err != nil { t.Fatal(err) } - goal := model.Goal{ID: "goal-1", Kind: model.GoalVerified, DeliveryID: "delivery-1"} + objective := model.Objective{ID: "objective-1", Kind: model.ObjectiveVerified, DeliveryID: "delivery-1"} authority := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "authority-1", Class: catalog.AuthorityHuman, Subject: invocation.RepositoryID, Fingerprint: "human-fingerprint", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), }}} request := engine.ApplyRequest{ - ResolveRequest: engine.ResolveRequest{Invocation: invocation, Goal: goal, Authority: authority, Requested: "repository.attach"}, + ResolveRequest: engine.ResolveRequest{Invocation: invocation, Objective: objective, Authority: authority, Requested: "repository.attach"}, FlowID: "flow-1", Parameters: protocol.Parameters{{Name: "topology", Value: string(model.TopologyDetached)}, {Name: "config_authority", Value: "repository"}}, AdmissionLifetime: time.Minute, } result, err := kernel.Apply(ctx, prescribeEngine(t, ctx, kernel, request)) @@ -196,11 +196,11 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing ctx := context.Background() repository := testRepository(t) externalRoot := t.TempDir() - kernel, err := boatstack.NewKernel(externalRoot, testProgram()) + kernel, err := boatstack.NewDeliveryController(externalRoot, testProgram()) if err != nil { t.Fatal(err) } - goal := model.Goal{ID: "external-config-goal", Kind: model.GoalApprovedPlan, DeliveryID: "external-config"} + objective := model.Objective{ID: "external-config-objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "external-config"} now := time.Now().UTC() human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "external-config-human", Class: catalog.AuthorityHuman, Subject: "integration", Fingerprint: "explicit-human", @@ -210,7 +210,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing t.Helper() request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", - CorrelationID: "external-config-" + string(id), FlowID: "flow-external-config", Goal: goal, TransitionID: id, + CorrelationID: "external-config-" + string(id), FlowID: "flow-external-config", Objective: objective, TransitionID: id, Authority: authority, RepositoryAuthority: repositoryAuthority, Parameters: parameters, } response, handleErr := kernel.Handle(ctx, prescribeSurface(t, ctx, kernel, request)) @@ -233,7 +233,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing {Name: "source_revision", Value: "external-config-fixture"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: initialPath}, {Name: "config_sha256", Value: configFingerprint(t, initialConfig)}, }) - apply("goal.configure", human, false, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) + apply("objective.bind", human, false, protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) apply("repository.attach", human, false, protocol.Parameters{{Name: "topology", Value: "detached"}, {Name: "config_authority", Value: "external"}}) resolver, err := plant.NewResolver(externalRoot) if err != nil { @@ -284,17 +284,17 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { ctx := context.Background() repository := testRepository(t) externalRoot := t.TempDir() - oldProgram, err := control.Compile(ctx, control.CompileRequest{ - KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()}, + oldProgram, err := delivery.Compile(ctx, delivery.CompileRequest{ + KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []delivery.Extension{releasenote.Definition()}, }) if err != nil { t.Fatal(err) } - oldKernel, err := boatstack.NewKernel(externalRoot, oldProgram) + oldKernel, err := boatstack.NewDeliveryController(externalRoot, oldProgram) if err != nil { t.Fatal(err) } - goal := model.Goal{ID: "program-drift", Kind: model.GoalApprovedPlan, DeliveryID: "program-drift"} + objective := model.Objective{ID: "program-drift", Kind: model.ObjectiveApprovedPlan, DeliveryID: "program-drift"} now := time.Now().UTC() human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "program-drift-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "explicit-program-reconciliation", @@ -312,7 +312,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { } initializeRequest := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-old", - FlowID: "flow-program-drift", Goal: goal, TransitionID: "installation.initialize", Authority: human, + FlowID: "flow-program-drift", Objective: objective, TransitionID: "installation.initialize", Authority: human, Parameters: protocol.Parameters{ {Name: "source_revision", Value: "program-old"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, @@ -325,17 +325,17 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { if initialized.Receipt == nil || initialized.Receipt.Program.Fingerprint != oldProgram.Fingerprint() { t.Fatalf("initial receipt did not freeze old program: %#v", initialized.Receipt) } - if initialized.Snapshot == nil || initialized.Snapshot.Goal.Status != model.FactAbsent || initialized.Receipt.GoalStatus != model.FactAbsent || initialized.Receipt.GoalID != "" { + if initialized.Snapshot == nil || initialized.Snapshot.Objective.Status != model.FactAbsent || initialized.Receipt.ObjectiveStatus != model.FactAbsent || initialized.Receipt.ObjectiveID != "" { t.Fatalf("installation initialization invented product intent: %#v", initialized) } newProgram := testProgram() - newKernel, err := boatstack.NewKernel(externalRoot, newProgram) + newKernel, err := boatstack.NewDeliveryController(externalRoot, newProgram) if err != nil { t.Fatal(err) } resolved, err := newKernel.Handle(ctx, surfaces.Request{ - SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli", CorrelationID: "program-drift-resolve", Goal: goal, + SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli", CorrelationID: "program-drift-resolve", Objective: objective, }) if err != nil { t.Fatal(err) @@ -362,7 +362,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { } request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-drift-reconcile", - FlowID: "flow-program-drift", Goal: goal, TransitionID: "installation.reconcile-update", + FlowID: "flow-program-drift", Objective: objective, TransitionID: "installation.reconcile-update", Parameters: protocol.Parameters{ {Name: "source_revision", Value: "program-new"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "accept_obligation_change", Value: "true"}, @@ -399,7 +399,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { reconciled.Snapshot.Program.Value != model.ProgramCurrent || reconciled.Snapshot.Phase.Value != model.PhaseObserved { t.Fatalf("reconciliation did not establish exact program identity: %#v", reconciled) } - if reconciled.Snapshot.Goal.Status != model.FactAbsent || reconciled.Receipt.GoalStatus != model.FactAbsent || reconciled.Receipt.GoalID != "" { + if reconciled.Snapshot.Objective.Status != model.FactAbsent || reconciled.Receipt.ObjectiveStatus != model.FactAbsent || reconciled.Receipt.ObjectiveID != "" { t.Fatalf("reconcile-update invented product intent: %#v", reconciled) } pinRaw, err := os.ReadFile(boatstackruntime.PinPath(repository)) @@ -429,7 +429,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { } updateRequest := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-current-update", - FlowID: "flow-program-drift", Goal: model.Goal{ID: "ignored-command-goal", Kind: model.GoalOpenPR, DeliveryID: "ignored"}, + FlowID: "flow-program-drift", Objective: model.Objective{ID: "ignored-command-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "ignored"}, TransitionID: "installation.update", Authority: human, Parameters: protocol.Parameters{ {Name: "source_revision", Value: "program-current"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, @@ -439,7 +439,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { if err != nil { t.Fatalf("current-program update after reconciliation: %v", err) } - if updated.Snapshot == nil || updated.Snapshot.Goal.Status != model.FactAbsent || updated.Receipt == nil || updated.Receipt.GoalStatus != model.FactAbsent || updated.Receipt.GoalID != "" { + if updated.Snapshot == nil || updated.Snapshot.Objective.Status != model.FactAbsent || updated.Receipt == nil || updated.Receipt.ObjectiveStatus != model.FactAbsent || updated.Receipt.ObjectiveID != "" { t.Fatalf("reconcile to update composition invented product intent: %#v", updated) } } @@ -454,17 +454,17 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test if err := os.WriteFile(filepath.Join(repository, "release-notes", "extension.md"), []byte("### Extension\n\nVerifiable user impact.\n"), 0o644); err != nil { t.Fatal(err) } - program, err := control.Compile(ctx, control.CompileRequest{ - KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()}, + program, err := delivery.Compile(ctx, delivery.CompileRequest{ + KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []delivery.Extension{releasenote.Definition()}, }) if err != nil { t.Fatal(err) } - kernel, err := boatstack.NewKernel(t.TempDir(), program) + kernel, err := boatstack.NewDeliveryController(t.TempDir(), program) if err != nil { t.Fatal(err) } - goal := model.Goal{ID: "extension-receipt", Kind: model.GoalOpenPR, DeliveryID: "extension-receipt"} + objective := model.Objective{ID: "extension-receipt", Kind: model.ObjectiveOpenPR, DeliveryID: "extension-receipt"} now := time.Now().UTC() authority := func(class catalog.AuthorityClass) protocol.AuthorityBundle { fingerprint, subject := "explicit-human", "integration" @@ -485,7 +485,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test t.Helper() request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", - CorrelationID: "extension-" + string(id), FlowID: "flow-extension-receipt", Goal: goal, TransitionID: id, + CorrelationID: "extension-" + string(id), FlowID: "flow-extension-receipt", Objective: objective, TransitionID: id, Authority: authorization, Parameters: parameters, } response, applyErr := kernel.Handle(ctx, prescribeSurface(t, ctx, kernel, request)) @@ -511,14 +511,14 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test if initialized.Receipt == nil || initialized.Receipt.AuthorityFingerprint == "" || len(initialized.Receipt.AuthoritySources) != 1 || len(initialized.Receipt.RequiredCapabilities) == 0 || len(initialized.Receipt.GrantedCapabilities) == 0 || len(initialized.Receipt.ExercisedCapabilities) != 0 || len(initialized.Receipt.CommittedEffects) == 0 || initialized.Receipt.Verification.Result != protocol.VerificationSatisfied { t.Fatalf("receipt lost capability or authority provenance: %#v", initialized.Receipt) } - apply("goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) + apply("objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) apply("engagement.begin", authority(catalog.AuthorityRepository), nil) planPath := filepath.Join(t.TempDir(), "plan.md") planRaw := []byte("# Extension plan\n") if err := os.WriteFile(planPath, planRaw, 0o600); err != nil { t.Fatal(err) } - apply("plan.create", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "source_path", Value: planPath}, {Name: "delivery_id", Value: goal.DeliveryID}}) + apply("plan.create", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "source_path", Value: planPath}, {Name: "delivery_id", Value: objective.DeliveryID}}) apply("plan.validate", authority(catalog.AuthorityRepository), nil) apply("plan.approve", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "plan_fingerprint", Value: digestBytes(planRaw)}, {Name: "actor", Value: "integration"}}) apply("plan.activate", authority(catalog.AuthorityHuman), nil) @@ -542,7 +542,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test } next, err := kernel.Handle(ctx, surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli", - CorrelationID: "extension-next", Goal: goal, Authority: authority(catalog.AuthorityRepository), + CorrelationID: "extension-next", Objective: objective, Authority: authority(catalog.AuthorityRepository), }) if err != nil { t.Fatal(err) @@ -557,7 +557,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test } after, err := kernel.Handle(ctx, surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli", - CorrelationID: "extension-after", Goal: goal, Authority: authority(catalog.AuthorityRepository), + CorrelationID: "extension-after", Objective: objective, Authority: authority(catalog.AuthorityRepository), }) if err != nil || after.Decision == nil { t.Fatalf("verified extension did not return control to ProgramRuntime: %#v error=%v", after.Decision, err) @@ -572,8 +572,8 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test } } -func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing.T) { - // control-law: successful-writes-remain-independently-verifiable-and-goal-specific +func TestConcreteWorkflowPreservesConfigurationProofAndObjectiveTerminals(t *testing.T) { + // control-law: successful-writes-remain-independently-verifiable-and-objective-specific ctx := context.Background() repository := testRepository(t) clock := fixedClock{value: time.Unix(2000, 0).UTC()} @@ -590,7 +590,7 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. journal, _ := effects.NewJournal(resolver, clock) receipts, _ := effects.NewReceiptStore(resolver, clock) driver, _ := effects.NewDriver(resolver, clock, effects.NewNativeBoundary()) - kernel, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramIdentity, observer, clock, locker, journal, driver, receipts) + kernel, err := engine.New(testprogram.StandardRegistry(), testObjectiveContracts(), testProgramIdentity, observer, clock, locker, journal, driver, receipts) if err != nil { t.Fatal(err) } @@ -610,10 +610,10 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), }}} } - apply := func(goal model.Goal, id catalog.TransitionID, auth protocol.AuthorityBundle, parameters protocol.Parameters) engine.ApplyResult { + apply := func(objective model.Objective, id catalog.TransitionID, auth protocol.AuthorityBundle, parameters protocol.Parameters) engine.ApplyResult { t.Helper() request := engine.ApplyRequest{ - ResolveRequest: engine.ResolveRequest{Invocation: invocation, Goal: goal, Authority: auth, Requested: id}, + ResolveRequest: engine.ResolveRequest{Invocation: invocation, Objective: objective, Authority: auth, Requested: id}, FlowID: "flow-workflow", Parameters: parameters, AdmissionLifetime: time.Minute, } result, applyErr := kernel.Apply(ctx, prescribeEngine(t, ctx, kernel, request)) @@ -636,20 +636,20 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. if err := os.WriteFile(configPath, configRaw, 0o600); err != nil { t.Fatal(err) } - approvedGoal := model.Goal{ID: "goal-approved", Kind: model.GoalApprovedPlan, DeliveryID: "delivery-workflow"} - apply(approvedGoal, "installation.initialize", authority(catalog.AuthorityHuman), protocol.Parameters{ + approvedObjective := model.Objective{ID: "objective-approved", Kind: model.ObjectiveApprovedPlan, DeliveryID: "delivery-workflow"} + apply(approvedObjective, "installation.initialize", authority(catalog.AuthorityHuman), protocol.Parameters{ {Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, }) - apply(approvedGoal, "goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(approvedGoal.Kind)}, {Name: "delivery_id", Value: approvedGoal.DeliveryID}}) - apply(approvedGoal, "engagement.begin", authority(catalog.AuthorityRepository), nil) + apply(approvedObjective, "objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "objective_kind", Value: string(approvedObjective.Kind)}, {Name: "delivery_id", Value: approvedObjective.DeliveryID}}) + apply(approvedObjective, "engagement.begin", authority(catalog.AuthorityRepository), nil) updatedConfigPath := filepath.Join(t.TempDir(), "project-v2-updated.json") updatedConfig := []byte("{\"schema_version\":2,\"project\":{\"name\":\"integration-updated\",\"default_branch\":\"main\",\"commands\":{\"build\":\"go version\",\"test\":\"go version\"}},\"policy\":{\"plan_approval\":\"human\",\"visual_evidence\":\"optional\"},\"hosts\":[\"cli\"]}\n") if err := os.WriteFile(updatedConfigPath, updatedConfig, 0o600); err != nil { t.Fatal(err) } - configResult := apply(approvedGoal, "configuration.mutate", authority(catalog.AuthorityHuman), protocol.Parameters{ + configResult := apply(approvedObjective, "configuration.mutate", authority(catalog.AuthorityHuman), protocol.Parameters{ {Name: "config_path", Value: updatedConfigPath}, {Name: "config_sha256", Value: configFingerprint(t, updatedConfig)}, }) if configResult.Target.Configuration.Value != model.ConfigurationVerified { @@ -661,16 +661,16 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. if err := os.WriteFile(planPath, planRaw, 0o600); err != nil { t.Fatal(err) } - apply(approvedGoal, "plan.create", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "source_path", Value: planPath}, {Name: "delivery_id", Value: approvedGoal.DeliveryID}}) - apply(approvedGoal, "plan.validate", authority(catalog.AuthorityRepository), nil) - approved := apply(approvedGoal, "plan.approve", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "plan_fingerprint", Value: digestBytes(planRaw)}, {Name: "actor", Value: "integration-human"}}) + apply(approvedObjective, "plan.create", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "source_path", Value: planPath}, {Name: "delivery_id", Value: approvedObjective.DeliveryID}}) + apply(approvedObjective, "plan.validate", authority(catalog.AuthorityRepository), nil) + approved := apply(approvedObjective, "plan.approve", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "plan_fingerprint", Value: digestBytes(planRaw)}, {Name: "actor", Value: "integration-human"}}) if approved.Target.Terminal.Value != model.TerminalEstablished || approved.Target.Plan.Value != model.PlanApproved { t.Fatalf("approved-plan terminal not established: %#v", approved.Target) } - verifiedGoal := model.Goal{ID: "goal-verified", Kind: model.GoalVerified, DeliveryID: approvedGoal.DeliveryID} - apply(verifiedGoal, "goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(verifiedGoal.Kind)}, {Name: "delivery_id", Value: verifiedGoal.DeliveryID}}) - apply(verifiedGoal, "plan.activate", authority(catalog.AuthorityHuman), nil) + verifiedObjective := model.Objective{ID: "objective-verified", Kind: model.ObjectiveVerified, DeliveryID: approvedObjective.DeliveryID} + apply(verifiedObjective, "objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "objective_kind", Value: string(verifiedObjective.Kind)}, {Name: "delivery_id", Value: verifiedObjective.DeliveryID}}) + apply(verifiedObjective, "plan.activate", authority(catalog.AuthorityHuman), nil) head := strings.TrimSpace(commandOutput(t, repository, "git", "rev-parse", "HEAD")) gateParameters := func(name string) protocol.Parameters { evidenceRaw, marshalErr := json.Marshal(map[string]any{ @@ -689,13 +689,13 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. {Name: "evidence_fingerprint", Value: digestBytes(evidenceRaw)}, } } - apply(verifiedGoal, "gate.build.record", authority(catalog.AuthorityRepository), gateParameters("build")) - apply(verifiedGoal, "gate.test.record", authority(catalog.AuthorityRepository), gateParameters("test")) - verified := apply(verifiedGoal, "gate.review.record", authority(catalog.AuthorityRepository), gateParameters("review")) + apply(verifiedObjective, "gate.build.record", authority(catalog.AuthorityRepository), gateParameters("build")) + apply(verifiedObjective, "gate.test.record", authority(catalog.AuthorityRepository), gateParameters("test")) + verified := apply(verifiedObjective, "gate.review.record", authority(catalog.AuthorityRepository), gateParameters("review")) if verified.Target.Terminal.Value != model.TerminalEstablished || verified.Target.Verification.Value != model.VerificationCurrent || verified.Target.Delivery.Value != model.DeliveryTerminal { t.Fatalf("verified terminal not established: %#v", verified.Target) } - if err := os.WriteFile(filepath.Join(repository, ".boatstack", "evidence", verifiedGoal.DeliveryID, "review.json"), []byte("tampered\n"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(repository, ".boatstack", "evidence", verifiedObjective.DeliveryID, "review.json"), []byte("tampered\n"), 0o644); err != nil { t.Fatal(err) } tamperedObservation, err := observer.Observe(ctx, ports.ObservationRequest{Invocation: invocation}) @@ -712,7 +712,7 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. if tampered.Phase.Value != model.PhaseActive || tampered.Delivery.Value != model.DeliveryActive { t.Fatalf("stale terminal had no re-verification path: phase=%s delivery=%s", tampered.Phase.Value, tampered.Delivery.Value) } - reverified := apply(verifiedGoal, "gate.review.record", authority(catalog.AuthorityRepository), gateParameters("review")) + reverified := apply(verifiedObjective, "gate.review.record", authority(catalog.AuthorityRepository), gateParameters("review")) if reverified.Target.Terminal.Value != model.TerminalEstablished || reverified.Target.Verification.Value != model.VerificationCurrent { t.Fatalf("repaired evidence did not re-establish the exact terminal: %#v", reverified.Target) } @@ -736,11 +736,11 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) journal, _ := effects.NewJournal(resolver, clock) receipts, _ := effects.NewReceiptStore(resolver, clock) driver, _ := effects.NewDriver(resolver, clock, effects.NewNativeBoundary()) - kernel, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramIdentity, observer, clock, locker, journal, driver, receipts) + kernel, err := engine.New(testprogram.StandardRegistry(), testObjectiveContracts(), testProgramIdentity, observer, clock, locker, journal, driver, receipts) if err != nil { t.Fatal(err) } - goal := model.Goal{ID: "goal-workspace", Kind: model.GoalMerged, DeliveryID: "delivery-workspace"} + objective := model.Objective{ID: "objective-workspace", Kind: model.ObjectiveMerged, DeliveryID: "delivery-workspace"} human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "human-workspace", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "human-workspace-proof", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), @@ -748,7 +748,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) apply := func(invocation model.InvocationContext, id catalog.TransitionID, authority protocol.AuthorityBundle, parameters protocol.Parameters) engine.ApplyResult { t.Helper() request := engine.ApplyRequest{ - ResolveRequest: engine.ResolveRequest{Invocation: invocation, Goal: goal, Authority: authority, Requested: id}, + ResolveRequest: engine.ResolveRequest{Invocation: invocation, Objective: objective, Authority: authority, Requested: id}, FlowID: "flow-workspace", Parameters: parameters, AdmissionLifetime: time.Minute, } result, applyErr := kernel.Apply(ctx, prescribeEngine(t, ctx, kernel, request)) @@ -774,7 +774,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) {Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configSource}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, }) - apply(sourceInvocation, "goal.configure", human, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) + apply(sourceInvocation, "objective.bind", human, protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) run(t, repository, "git", "add", ".boatstack/project.json") run(t, repository, "git", "commit", "-q", "-m", "install V2 configuration") repositoryAuthority := func(path string) protocol.AuthorityBundle { @@ -809,7 +809,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) if err != nil { t.Fatal(err) } - if sourceObservation.Phase.Value != model.PhaseDormant || sourceObservation.Engagement.Value != model.EngagementDormant || sourceObservation.Goal.Status != model.FactAbsent { + if sourceObservation.Phase.Value != model.PhaseDormant || sourceObservation.Engagement.Value != model.EngagementDormant || sourceObservation.Objective.Status != model.FactAbsent { t.Fatalf("source checkout retained ambient workflow authority: %#v", sourceObservation) } @@ -831,7 +831,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) if err != nil { t.Fatal(err) } - if destinationObservation.Workspace.Value != model.WorkspaceCut || destinationObservation.Goal.Value != goal { + if destinationObservation.Workspace.Value != model.WorkspaceCut || destinationObservation.Objective.Value != objective { t.Fatalf("destination did not receive exact controller state: %#v", destinationObservation) } activated := apply(destinationInvocation, "workspace.activate", repositoryAuthority(canonicalDestination), protocol.Parameters{{Name: "branch", Value: "feature/v2-workspace-transfer"}}) @@ -841,8 +841,8 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) if err := os.WriteFile(destinationConfigPath, destinationConfig, 0o600); err != nil { t.Fatal(err) } - goal = model.Goal{ID: "goal-workspace-abandon", Kind: model.GoalAbandoned, DeliveryID: "delivery-workspace"} - apply(destinationInvocation, "goal.configure", human, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) + objective = model.Objective{ID: "objective-workspace-abandon", Kind: model.ObjectiveAbandoned, DeliveryID: "delivery-workspace"} + apply(destinationInvocation, "objective.bind", human, protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) abandoned := apply(destinationInvocation, "workspace.abandon", human, protocol.Parameters{{Name: "branch", Value: "feature/v2-workspace-transfer"}}) if abandoned.Target.Terminal.Value != model.TerminalEstablished || abandoned.Target.Workspace.Value != model.WorkspaceAbandoned { t.Fatalf("workspace abandonment did not establish its configured terminal: %#v", abandoned.Target) diff --git a/boatstack/internal/effects/io.go b/boatstack/internal/softwaredelivery/effects/io.go similarity index 100% rename from boatstack/internal/effects/io.go rename to boatstack/internal/softwaredelivery/effects/io.go diff --git a/boatstack/internal/effects/journal.go b/boatstack/internal/softwaredelivery/effects/journal.go similarity index 95% rename from boatstack/internal/effects/journal.go rename to boatstack/internal/softwaredelivery/effects/journal.go index 876db87..4c32560 100644 --- a/boatstack/internal/effects/journal.go +++ b/boatstack/internal/softwaredelivery/effects/journal.go @@ -12,10 +12,10 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type Journal struct { @@ -129,8 +129,8 @@ func readJournal(path string) (journalRecord, error) { if receipt.PrescriptionID != admission.PrescriptionID || receipt.TransitionVersion != admission.TransitionVersion || receipt.Program.Fingerprint != admission.ExpectedProgramFingerprint || receipt.PriorStateRevision != admission.ExpectedStateRevision || receipt.SourceFingerprint != admission.ExpectedSnapshotFingerprint || receipt.AuthorityFingerprint != admission.AuthorityFingerprint || !slices.Equal(receipt.RequiredCapabilities, admission.RequiredCapabilities) || - !slices.Equal(receipt.GrantedCapabilities, admission.GrantedCapabilities) || receipt.GoalID != admission.Goal.ID || receipt.GoalKind != admission.Goal.Kind || - receipt.DeliveryID != admission.Goal.DeliveryID || receipt.GoalScope != admission.GoalScope || receipt.GoalStatus != admission.GoalStatus { + !slices.Equal(receipt.GrantedCapabilities, admission.GrantedCapabilities) || receipt.ObjectiveID != admission.Objective.ID || receipt.ObjectiveKind != admission.Objective.Kind || + receipt.DeliveryID != admission.Objective.DeliveryID || receipt.ObjectiveScope != admission.ObjectiveScope || receipt.ObjectiveStatus != admission.ObjectiveStatus { return journalRecord{}, fmt.Errorf("committed transition fact in %s does not match its exact admission", path) } if err := validateCommittedMutationFacts(record.TransitionClass, record.Mutations, receipt.ChangedStateFacets, receipt.CommittedEffects); err != nil { diff --git a/boatstack/internal/effects/locker.go b/boatstack/internal/softwaredelivery/effects/locker.go similarity index 95% rename from boatstack/internal/effects/locker.go rename to boatstack/internal/softwaredelivery/effects/locker.go index f3275dc..bf5269a 100644 --- a/boatstack/internal/effects/locker.go +++ b/boatstack/internal/softwaredelivery/effects/locker.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" ) type Locker struct{ resolver ports.InvocationResolver } diff --git a/boatstack/internal/effects/locker_test.go b/boatstack/internal/softwaredelivery/effects/locker_test.go similarity index 96% rename from boatstack/internal/effects/locker_test.go rename to boatstack/internal/softwaredelivery/effects/locker_test.go index 43e50b2..b98e7af 100644 --- a/boatstack/internal/effects/locker_test.go +++ b/boatstack/internal/softwaredelivery/effects/locker_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" ) func TestKernelLockUsesProcessScopedHandleNotFilePresence(t *testing.T) { diff --git a/boatstack/internal/effects/mutex.go b/boatstack/internal/softwaredelivery/effects/mutex.go similarity index 100% rename from boatstack/internal/effects/mutex.go rename to boatstack/internal/softwaredelivery/effects/mutex.go diff --git a/boatstack/internal/effects/prepared.go b/boatstack/internal/softwaredelivery/effects/prepared.go similarity index 94% rename from boatstack/internal/effects/prepared.go rename to boatstack/internal/softwaredelivery/effects/prepared.go index a84b62f..9926dfb 100644 --- a/boatstack/internal/effects/prepared.go +++ b/boatstack/internal/softwaredelivery/effects/prepared.go @@ -8,10 +8,10 @@ import ( "os" "sort" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type boundaryCall func(context.Context) (ports.EffectResult, error) diff --git a/boatstack/internal/effects/prepared_test.go b/boatstack/internal/softwaredelivery/effects/prepared_test.go similarity index 96% rename from boatstack/internal/effects/prepared_test.go rename to boatstack/internal/softwaredelivery/effects/prepared_test.go index 3420af0..693dcca 100644 --- a/boatstack/internal/effects/prepared_test.go +++ b/boatstack/internal/softwaredelivery/effects/prepared_test.go @@ -6,9 +6,9 @@ import ( "path/filepath" "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func TestPreparedEffectNeverAcceptsMixedEpochAtWriteBoundary(t *testing.T) { diff --git a/boatstack/internal/effects/receipts.go b/boatstack/internal/softwaredelivery/effects/receipts.go similarity index 91% rename from boatstack/internal/effects/receipts.go rename to boatstack/internal/softwaredelivery/effects/receipts.go index 0b12096..f0c1cc4 100644 --- a/boatstack/internal/effects/receipts.go +++ b/boatstack/internal/softwaredelivery/effects/receipts.go @@ -10,10 +10,10 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type ReceiptStore struct { @@ -140,9 +140,9 @@ type processEvent struct { FlowID string `json:"flow_id"` Sequence uint64 `json:"sequence"` Timestamp time.Time `json:"timestamp"` - GoalID string `json:"goal_id"` - GoalScope string `json:"goal_scope,omitempty"` - GoalStatus string `json:"goal_status,omitempty"` + ObjectiveID string `json:"objective_id"` + ObjectiveScope string `json:"objective_scope,omitempty"` + ObjectiveStatus string `json:"objective_status,omitempty"` TransitionID string `json:"transition_id"` ProgramID string `json:"program_id"` ProgramVersion string `json:"program_version"` @@ -200,8 +200,8 @@ func (s *ReceiptStore) Project(ctx context.Context, receipt protocol.TransitionR return err } event := processEvent{ - SchemaVersion: 5, FlowID: receipt.FlowID, Sequence: receipt.Sequence, Timestamp: s.clock.Now().UTC(), GoalID: receipt.GoalID, - GoalScope: string(receipt.GoalScope), GoalStatus: string(receipt.GoalStatus), + SchemaVersion: 5, FlowID: receipt.FlowID, Sequence: receipt.Sequence, Timestamp: s.clock.Now().UTC(), ObjectiveID: receipt.ObjectiveID, + ObjectiveScope: string(receipt.ObjectiveScope), ObjectiveStatus: string(receipt.ObjectiveStatus), TransitionID: string(receipt.TransitionID), ProgramID: receipt.Program.ID, ProgramVersion: receipt.Program.Version, ProgramFingerprint: receipt.Program.Fingerprint, PrescriptionID: receipt.PrescriptionID, PriorStateRevision: receipt.PriorStateRevision, ResultingStateRevision: receipt.ResultingStateRevision, SourceFingerprint: receipt.SourceFingerprint, TargetFingerprint: receipt.TargetFingerprint, diff --git a/boatstack/internal/effects/recovery.go b/boatstack/internal/softwaredelivery/effects/recovery.go similarity index 96% rename from boatstack/internal/effects/recovery.go rename to boatstack/internal/softwaredelivery/effects/recovery.go index 9b08b3e..da6357c 100644 --- a/boatstack/internal/effects/recovery.go +++ b/boatstack/internal/softwaredelivery/effects/recovery.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func (d Driver) prepareRecoveryReplay(ctx context.Context, layout ports.ControllerLayout, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { diff --git a/boatstack/internal/effects/recovery_test.go b/boatstack/internal/softwaredelivery/effects/recovery_test.go similarity index 84% rename from boatstack/internal/effects/recovery_test.go rename to boatstack/internal/softwaredelivery/effects/recovery_test.go index 0cc5eb0..c5bb5be 100644 --- a/boatstack/internal/effects/recovery_test.go +++ b/boatstack/internal/softwaredelivery/effects/recovery_test.go @@ -10,14 +10,14 @@ import ( "github.com/operatorstack/boatstack/boatstack/flow/standard" "github.com/operatorstack/boatstack/boatstack/internal/buildinfo" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/engine" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/plant" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/engine" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) @@ -27,12 +27,12 @@ const testProgramFingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa var testProgramIdentity = protocol.ProgramIdentity{ID: "standard", Version: "test", Fingerprint: testProgramFingerprint} -func testGoalContracts() catalog.GoalContracts { +func testObjectiveContracts() catalog.ObjectiveContracts { manifest, err := standard.Definition().RuntimeManifest(context.Background()) if err != nil { panic(err) } - contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil) + contracts, err := catalog.NewObjectiveContracts(manifest.ObjectiveContracts, nil) if err != nil { panic(err) } @@ -90,7 +90,7 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. if err != nil { t.Fatal(err) } - goal := model.Goal{ID: "goal-recovery", Kind: model.GoalVerified, DeliveryID: "delivery-recovery"} + objective := model.Objective{ID: "objective-recovery", Kind: model.ObjectiveVerified, DeliveryID: "delivery-recovery"} authority := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "recovery-human", Class: catalog.AuthorityHuman, Subject: "fixture", Fingerprint: "recovery-human-fingerprint", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), @@ -127,7 +127,7 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. if err != nil { t.Fatal(err) } - admission, err := protocol.NewAdmission(initial, goal, transition, prescription, authority, parameters, clock.Now(), time.Minute) + admission, err := protocol.NewAdmission(initial, objective, transition, prescription, authority, parameters, clock.Now(), time.Minute) if err != nil { t.Fatal(err) } @@ -172,12 +172,12 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. locker, _ := NewLocker(resolver) journalAfterRestart, _ := NewJournal(resolver, clock) receipts, _ := NewReceiptStore(resolver, clock) - restartedEngine, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramIdentity, observer, clock, locker, journalAfterRestart, driver, receipts) + restartedEngine, err := engine.New(testprogram.StandardRegistry(), testObjectiveContracts(), testProgramIdentity, observer, clock, locker, journalAfterRestart, driver, receipts) if err != nil { t.Fatal(err) } recoveryRequest := engine.ApplyRequest{ - ResolveRequest: engine.ResolveRequest{Invocation: restartedInvocation, Goal: goal, Authority: authority, Requested: "recovery.rollback"}, + ResolveRequest: engine.ResolveRequest{Invocation: restartedInvocation, Objective: objective, Authority: authority, Requested: "recovery.rollback"}, FlowID: "flow-recovery", Parameters: protocol.Parameters{{Name: "transaction_id", Value: admission.ID}}, AdmissionLifetime: time.Minute, } recoveryResolve := recoveryRequest.ResolveRequest @@ -195,8 +195,8 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. result.Receipt.ResultingStateRevision != resolvedRecovery.Prescription.ExpectedStateRevision+1 { t.Fatalf("recovery receipt did not advance exactly once: %#v", result.Receipt) } - if result.Target.Phase.Value != model.PhaseDormant || result.Target.Recovery.Value != model.RecoveryNone || result.Target.Goal.Status != model.FactAbsent || - result.Receipt.ID == "" || result.Receipt.GoalStatus != model.FactAbsent || result.Receipt.GoalID != "" { + if result.Target.Phase.Value != model.PhaseDormant || result.Target.Recovery.Value != model.RecoveryNone || result.Target.Objective.Status != model.FactAbsent || + result.Receipt.ID == "" || result.Receipt.ObjectiveStatus != model.FactAbsent || result.Receipt.ObjectiveID != "" { t.Fatalf("rollback target=%#v receipt=%q", result.Target, result.Receipt.ID) } layout, _, _ := resolver.ResolveLayout(ctx, restartedInvocation) @@ -216,7 +216,7 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. if err != nil || recoveredState.Revision != result.Receipt.ResultingStateRevision { t.Fatalf("rollback state revision is not receipt-bound: state=%#v err=%v receipt=%#v", recoveredState, err, result.Receipt) } - if recoveredState.Goal.Validate() == nil || recoveredState.Runtime != model.RuntimeAbsent || recoveredState.Configuration != model.ConfigurationUnsupported { + if recoveredState.Objective.Validate() == nil || recoveredState.Runtime != model.RuntimeAbsent || recoveredState.Configuration != model.ConfigurationUnsupported { t.Fatalf("rollback created product intent or retained initialized state: %#v", recoveredState) } if _, err := os.Stat(boatstackruntime.PinPath(repository)); !os.IsNotExist(err) { diff --git a/boatstack/internal/effects/replace_unix.go b/boatstack/internal/softwaredelivery/effects/replace_unix.go similarity index 100% rename from boatstack/internal/effects/replace_unix.go rename to boatstack/internal/softwaredelivery/effects/replace_unix.go diff --git a/boatstack/internal/effects/replace_windows.go b/boatstack/internal/softwaredelivery/effects/replace_windows.go similarity index 100% rename from boatstack/internal/effects/replace_windows.go rename to boatstack/internal/softwaredelivery/effects/replace_windows.go diff --git a/boatstack/internal/effects/revision.go b/boatstack/internal/softwaredelivery/effects/revision.go similarity index 86% rename from boatstack/internal/effects/revision.go rename to boatstack/internal/softwaredelivery/effects/revision.go index 0bf850e..dfc3e57 100644 --- a/boatstack/internal/effects/revision.go +++ b/boatstack/internal/softwaredelivery/effects/revision.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) const ( diff --git a/boatstack/internal/effects/runtime_boundary_test.go b/boatstack/internal/softwaredelivery/effects/runtime_boundary_test.go similarity index 92% rename from boatstack/internal/effects/runtime_boundary_test.go rename to boatstack/internal/softwaredelivery/effects/runtime_boundary_test.go index 614a485..f931895 100644 --- a/boatstack/internal/effects/runtime_boundary_test.go +++ b/boatstack/internal/softwaredelivery/effects/runtime_boundary_test.go @@ -8,9 +8,9 @@ import ( "testing" "github.com/operatorstack/boatstack/boatstack/internal/buildinfo" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) diff --git a/boatstack/internal/effects/standard_adapter.go b/boatstack/internal/softwaredelivery/effects/standard_adapter.go similarity index 85% rename from boatstack/internal/effects/standard_adapter.go rename to boatstack/internal/softwaredelivery/effects/standard_adapter.go index ee132cb..0ffcf48 100644 --- a/boatstack/internal/effects/standard_adapter.go +++ b/boatstack/internal/softwaredelivery/effects/standard_adapter.go @@ -3,7 +3,7 @@ package effects import ( "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" ) // standardGateName belongs to the trusted StandardFlow native adapter. The diff --git a/boatstack/internal/effects/state_facet.go b/boatstack/internal/softwaredelivery/effects/state_facet.go similarity index 92% rename from boatstack/internal/effects/state_facet.go rename to boatstack/internal/softwaredelivery/effects/state_facet.go index 1f54967..3c84a80 100644 --- a/boatstack/internal/effects/state_facet.go +++ b/boatstack/internal/softwaredelivery/effects/state_facet.go @@ -4,10 +4,10 @@ import ( "fmt" "path/filepath" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" ) func validateTransitionStateFacets(transition catalog.Transition, changed []model.StateFacet) ([]model.StateFacet, error) { diff --git a/boatstack/internal/effects/state_facet_test.go b/boatstack/internal/softwaredelivery/effects/state_facet_test.go similarity index 78% rename from boatstack/internal/effects/state_facet_test.go rename to boatstack/internal/softwaredelivery/effects/state_facet_test.go index e6d644d..2767cbf 100644 --- a/boatstack/internal/effects/state_facet_test.go +++ b/boatstack/internal/softwaredelivery/effects/state_facet_test.go @@ -6,11 +6,11 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func ownershipState() durable.State { @@ -39,7 +39,7 @@ func requireOwnedChange(t *testing.T, transition catalog.Transition, before, aft func TestStateFacetIsolationMatrix(t *testing.T) { base := ownershipState() - knownGoal := model.Goal{ID: "goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"} + knownObjective := model.Objective{ID: "objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} fixtures := []struct { name string transition catalog.Transition @@ -48,15 +48,15 @@ func TestStateFacetIsolationMatrix(t *testing.T) { }{ {"installation owns installation", transitionFixture("installation.update", catalog.OriginCoreSystem, false), func(s *durable.State) { s.RuntimeVersion = "v2" }, false}, {"installation owns control", transitionFixture("installation.update", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Revision++ }, false}, - {"installation cannot mutate product", transitionFixture("installation.update", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Goal = knownGoal }, true}, + {"installation cannot mutate product", transitionFixture("installation.update", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Objective = knownObjective }, true}, {"program reconcile owns program", transitionFixture("catalog.reconcile", catalog.OriginCoreSystem, false), func(s *durable.State) { s.ProgramFingerprint = strings.Repeat("b", 64) }, false}, {"program reconcile owns control", transitionFixture("catalog.reconcile", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Revision++ }, false}, - {"program reconcile cannot mutate product", transitionFixture("catalog.reconcile", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Goal = knownGoal }, true}, - {"product owns product", transitionFixture("goal.configure", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Goal = knownGoal }, false}, - {"product owns control", transitionFixture("goal.configure", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Revision++ }, false}, - {"product cannot mutate installation", transitionFixture("goal.configure", catalog.OriginCoreSystem, false), func(s *durable.State) { s.RuntimeVersion = "v2" }, true}, + {"program reconcile cannot mutate product", transitionFixture("catalog.reconcile", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Objective = knownObjective }, true}, + {"product owns product", transitionFixture("objective.bind", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Objective = knownObjective }, false}, + {"product owns control", transitionFixture("objective.bind", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Revision++ }, false}, + {"product cannot mutate installation", transitionFixture("objective.bind", catalog.OriginCoreSystem, false), func(s *durable.State) { s.RuntimeVersion = "v2" }, true}, {"control cannot synthesize engagement", transitionFixture("recovery.escalate", catalog.OriginCoreSystem, false), func(s *durable.State) { s.Engagement = model.EngagementActive }, true}, - {"repository write cannot bypass product ownership", transitionFixture("repository-program/write", catalog.OriginControlProgram, true), func(s *durable.State) { s.Goal = knownGoal }, true}, + {"repository write cannot bypass product ownership", transitionFixture("repository-program/write", catalog.OriginControlProgram, true), func(s *durable.State) { s.Objective = knownObjective }, true}, } for _, fixture := range fixtures { t.Run(fixture.name, func(t *testing.T) { @@ -67,22 +67,22 @@ func TestStateFacetIsolationMatrix(t *testing.T) { } } -func TestMaintenancePreservesAbsentAndKnownGoalExactly(t *testing.T) { +func TestMaintenancePreservesAbsentAndKnownObjectiveExactly(t *testing.T) { transition := transitionFixture("installation.update", catalog.OriginCoreSystem, false) - transition.Policy.GoalScope = catalog.GoalScopeOptionalPreserve + transition.Policy.ObjectiveScope = catalog.ObjectiveScopeOptionalPreserve transition.TargetPhases = []model.ProtocolPhase{model.PhaseDormant} - for _, goal := range []model.Goal{{}, {ID: "known", Kind: model.GoalOpenPR, DeliveryID: "delivery"}} { + for _, objective := range []model.Objective{{}, {ID: "known", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"}} { state := ownershipState() - state.Goal = goal - admission := protocol.Admission{GoalStatus: model.FactAbsent} - if goal.Validate() == nil { - admission.Goal, admission.GoalStatus = goal, model.FactKnown + state.Objective = objective + admission := protocol.Admission{ObjectiveStatus: model.FactAbsent} + if objective.Validate() == nil { + admission.Objective, admission.ObjectiveStatus = objective, model.FactKnown } if err := applyStateTransition(&state, admission, transition); err != nil { t.Fatal(err) } - if state.Goal != goal { - t.Fatalf("goal changed from %#v to %#v", goal, state.Goal) + if state.Objective != objective { + t.Fatalf("objective changed from %#v to %#v", objective, state.Objective) } } } @@ -90,7 +90,7 @@ func TestMaintenancePreservesAbsentAndKnownGoalExactly(t *testing.T) { func TestRecoveryCannotReplayFacetOutsideInterruptedTransition(t *testing.T) { before := ownershipState() after := before - after.Goal = model.Goal{ID: "invented", Kind: model.GoalApprovedPlan, DeliveryID: "invented"} + after.Objective = model.Objective{ID: "invented", Kind: model.ObjectiveApprovedPlan, DeliveryID: "invented"} prior, _ := durable.EncodeState(before) target, _ := durable.EncodeState(after) record := journalRecord{TransitionID: "installation.update", Mutations: []ports.ResourceMutation{{Path: "/controller/state.json", PriorExists: true, Prior: prior, Target: target, StateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}}}} diff --git a/boatstack/internal/effects/state_reducer.go b/boatstack/internal/softwaredelivery/effects/state_reducer.go similarity index 87% rename from boatstack/internal/effects/state_reducer.go rename to boatstack/internal/softwaredelivery/effects/state_reducer.go index 66d122a..d96075e 100644 --- a/boatstack/internal/effects/state_reducer.go +++ b/boatstack/internal/softwaredelivery/effects/state_reducer.go @@ -3,31 +3,31 @@ package effects import ( "fmt" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) func applyStateTransition(state *durable.State, admission protocol.Admission, transition catalog.Transition) error { - configured := state.Goal.Validate() == nil - if transition.Policy.GoalScope == catalog.GoalScopeOptionalPreserve { - if configured && state.Goal != admission.Goal { - return fmt.Errorf("transition %q must preserve the exact configured product goal", transition.ID) + configured := state.Objective.Validate() == nil + if transition.Policy.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve { + if configured && state.Objective != admission.Objective { + return fmt.Errorf("transition %q must preserve the exact configured product objective", transition.ID) } - if !configured && admission.Goal.Validate() == nil { + if !configured && admission.Objective.Validate() == nil { return fmt.Errorf("transition %q cannot create product intent from verified absence", transition.ID) } - } else if transition.ID == "goal.configure" { - state.Goal = admission.Goal + } else if transition.ID == "objective.bind" { + state.Objective = admission.Objective configured = true } else { - if configured && state.Goal != admission.Goal { - return fmt.Errorf("transition %q cannot replace configured goal; use goal.configure", transition.ID) + if configured && state.Objective != admission.Objective { + return fmt.Errorf("transition %q cannot replace configured objective; use objective.bind", transition.ID) } } state.LastTransition = transition.ID - if transition.ID == "goal.configure" { + if transition.ID == "objective.bind" { state.Terminal = model.TerminalNonterminal } switch transition.ID { @@ -91,12 +91,12 @@ func applyStateTransition(state *durable.State, admission protocol.Admission, tr state.RuntimeSource, _ = admission.Parameters.Get("source_revision") state.ConfigFingerprint, _ = admission.Parameters.Get("config_sha256") state.Phase = model.PhaseObserved - case "goal.configure": + case "objective.bind": wasActive := state.Phase == model.PhaseActive - kind, _ := admission.Parameters.Get("goal_kind") + kind, _ := admission.Parameters.Get("objective_kind") delivery, _ := admission.Parameters.Get("delivery_id") - if kind != string(admission.Goal.Kind) || delivery != admission.Goal.DeliveryID { - return fmt.Errorf("goal parameters do not match admitted goal") + if kind != string(admission.Objective.Kind) || delivery != admission.Objective.DeliveryID { + return fmt.Errorf("objective parameters do not match admitted objective") } state.Phase = model.PhaseObserved if state.Recovery == model.RecoveryEscalated { @@ -110,7 +110,7 @@ func applyStateTransition(state *durable.State, admission protocol.Admission, tr state.Plan, state.Phase, state.Terminal = model.PlanValid, model.PhaseActive, model.TerminalNonterminal case "plan.approve": state.Plan, state.Delivery, state.Phase = model.PlanApproved, model.DeliveryApproved, model.PhaseActive - if admission.Goal.Kind == model.GoalApprovedPlan { + if admission.Objective.Kind == model.ObjectiveApprovedPlan { establishTerminal(state, model.PhaseTerminal) } case "plan.activate": @@ -166,7 +166,7 @@ func applyStateTransition(state *durable.State, admission protocol.Admission, tr state.SourceRevision, state.WorktreeFingerprint = admission.SourceRevision, admission.WorktreeFingerprint state.Terminal, state.Delivery = model.TerminalNonterminal, model.DeliveryActive state.Verification, state.Phase = model.VerificationCurrent, model.PhaseActive - if verifiedGoalSatisfied(*state, admission.Goal) { + if verifiedObjectiveSatisfied(*state, admission.Objective) { state.Delivery = model.DeliveryGatesPassed establishTerminal(state, model.PhaseTerminal) } @@ -176,11 +176,11 @@ func applyStateTransition(state *durable.State, admission protocol.Admission, tr upsertGate(state, durable.GateEvidence{Gate: "visual", Revision: revision, Fingerprint: fingerprint}) state.SourceRevision, state.WorktreeFingerprint = admission.SourceRevision, admission.WorktreeFingerprint state.Terminal = model.TerminalNonterminal - if admission.Goal.Kind == model.GoalVerified { + if admission.Objective.Kind == model.ObjectiveVerified { state.Delivery = model.DeliveryActive } state.Verification, state.Phase = model.VerificationCurrent, model.PhaseActive - if verifiedGoalSatisfied(*state, admission.Goal) { + if verifiedObjectiveSatisfied(*state, admission.Objective) { state.Delivery = model.DeliveryGatesPassed establishTerminal(state, model.PhaseTerminal) } @@ -200,9 +200,9 @@ func applyStateTransition(state *durable.State, admission protocol.Admission, tr state.Phase = model.PhaseUnresolved } else if state.Publication == model.PublicationClosedUnmerged { state.Phase = model.PhaseFrontier - } else if admission.Goal.Kind == model.GoalOpenPR && state.Publication == model.PublicationOpen { + } else if admission.Objective.Kind == model.ObjectiveOpenPR && state.Publication == model.PublicationOpen { establishTerminal(state, model.PhaseTerminal) - } else if admission.Goal.Kind == model.GoalMerged && state.Publication == model.PublicationMerged { + } else if admission.Objective.Kind == model.ObjectiveMerged && state.Publication == model.PublicationMerged { state.Workspace, state.Delivery = model.WorkspaceLanded, model.DeliveryTerminal establishTerminal(state, model.PhaseTerminal) } else { @@ -300,8 +300,8 @@ func hasGates(state durable.State, names ...string) bool { return true } -func verifiedGoalSatisfied(state durable.State, goal model.Goal) bool { - if goal.Kind != model.GoalVerified || !hasGates(state, "build", "test", "review") { +func verifiedObjectiveSatisfied(state durable.State, objective model.Objective) bool { + if objective.Kind != model.ObjectiveVerified || !hasGates(state, "build", "test", "review") { return false } return state.VisualEvidencePolicy != "required" || hasGates(state, "visual") diff --git a/boatstack/internal/effects/state_reducer_test.go b/boatstack/internal/softwaredelivery/effects/state_reducer_test.go similarity index 69% rename from boatstack/internal/effects/state_reducer_test.go rename to boatstack/internal/softwaredelivery/effects/state_reducer_test.go index c9f1ed4..4fab182 100644 --- a/boatstack/internal/effects/state_reducer_test.go +++ b/boatstack/internal/softwaredelivery/effects/state_reducer_test.go @@ -3,22 +3,22 @@ package effects import ( "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) { // control-law: repository-visual-policy-is-terminal-authority-not-decoration - goal := model.Goal{ID: "visual-goal", Kind: model.GoalVerified, DeliveryID: "visual-delivery"} + objective := model.Objective{ID: "visual-objective", Kind: model.ObjectiveVerified, DeliveryID: "visual-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseActive, Engagement: model.EngagementActive, Delivery: model.DeliveryActive, Workspace: model.WorkspaceActive, Plan: model.PlanLocked, Configuration: model.ConfigurationVerified, Runtime: model.RuntimeVerified, Publication: model.PublicationNone, Verification: model.VerificationUnverified, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalNonterminal, - Goal: goal, ConfigFingerprint: "config", PlanApprovalPolicy: "human", VisualEvidencePolicy: "required", + Objective: objective, ConfigFingerprint: "config", PlanApprovalPolicy: "human", VisualEvidencePolicy: "required", ExternalEffectPolicy: "human-or-autonomy-plus-provider", EnabledHosts: []string{"cli"}, } apply := func(id catalog.TransitionID, parameters protocol.Parameters) { @@ -27,7 +27,7 @@ func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) { if !ok { t.Fatalf("missing transition %s", id) } - if err := applyStateTransition(&state, protocol.Admission{Goal: goal, Parameters: parameters, SourceRevision: "revision", WorktreeFingerprint: "worktree"}, transition); err != nil { + if err := applyStateTransition(&state, protocol.Admission{Objective: objective, Parameters: parameters, SourceRevision: "revision", WorktreeFingerprint: "worktree"}, transition); err != nil { t.Fatalf("apply %s: %v", id, err) } } @@ -49,16 +49,16 @@ func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) { func TestPublicationCorrectionRequiresIndependentObservationForTerminal(t *testing.T) { // control-law: external-writer-cannot-self-certify-provider-state - goal := model.Goal{ID: "publication-goal", Kind: model.GoalOpenPR, DeliveryID: "publication-delivery"} + objective := model.Objective{ID: "publication-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "publication-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseTerminal, Engagement: model.EngagementActive, Delivery: model.DeliveryTerminal, Workspace: model.WorkspacePublished, Plan: model.PlanLocked, Configuration: model.ConfigurationVerified, Runtime: model.RuntimeVerified, Publication: model.PublicationOpen, Verification: model.VerificationCurrent, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalEstablished, - Goal: goal, + Objective: objective, } correct, _ := testprogram.StandardRegistry().Lookup("publication.correct") - admission := protocol.Admission{Goal: goal, Parameters: protocol.Parameters{{Name: "publication_id", Value: "7"}, {Name: "body_path", Value: "/body"}}} + admission := protocol.Admission{Objective: objective, Parameters: protocol.Parameters{{Name: "publication_id", Value: "7"}, {Name: "body_path", Value: "/body"}}} if err := applyStateTransition(&state, admission, correct); err != nil { t.Fatal(err) } @@ -77,53 +77,53 @@ func TestPublicationCorrectionRequiresIndependentObservationForTerminal(t *testi func TestWorkspaceReapPreservesEstablishedTerminalPhase(t *testing.T) { for _, fixture := range []struct { - goalKind model.GoalKind - delivery model.DeliveryState - phase model.ProtocolPhase + objectiveKind model.ObjectiveKind + delivery model.DeliveryState + phase model.ProtocolPhase }{ - {goalKind: model.GoalMerged, delivery: model.DeliveryTerminal, phase: model.PhaseTerminal}, - {goalKind: model.GoalAbandoned, delivery: model.DeliveryDiscarded, phase: model.PhaseAbandoned}, + {objectiveKind: model.ObjectiveMerged, delivery: model.DeliveryTerminal, phase: model.PhaseTerminal}, + {objectiveKind: model.ObjectiveAbandoned, delivery: model.DeliveryDiscarded, phase: model.PhaseAbandoned}, } { - goal := model.Goal{ID: "cleanup-goal", Kind: fixture.goalKind, DeliveryID: "cleanup-delivery"} + objective := model.Objective{ID: "cleanup-objective", Kind: fixture.objectiveKind, DeliveryID: "cleanup-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: fixture.phase, Engagement: model.EngagementActive, Delivery: fixture.delivery, Workspace: model.WorkspaceAbandoned, Plan: model.PlanLocked, Configuration: model.ConfigurationVerified, Runtime: model.RuntimeVerified, Publication: model.PublicationMerged, - Verification: model.VerificationCurrent, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalEstablished, Goal: goal, + Verification: model.VerificationCurrent, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalEstablished, Objective: objective, } transition, _ := testprogram.StandardRegistry().Lookup("workspace.reap") - if err := applyStateTransition(&state, protocol.Admission{Goal: goal}, transition); err != nil { + if err := applyStateTransition(&state, protocol.Admission{Objective: objective}, transition); err != nil { t.Fatal(err) } if state.Workspace != model.WorkspaceAbsent || state.Phase != fixture.phase || state.Terminal != model.TerminalEstablished { - t.Fatalf("reap lost %s terminal: %#v", fixture.goalKind, state) + t.Fatalf("reap lost %s terminal: %#v", fixture.objectiveKind, state) } } } func TestEscalatedRecoveryCanOnlyBeReconfiguredTowardExplicitAbandonment(t *testing.T) { - original := model.Goal{ID: "delivery-goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"} - abandoned := model.Goal{ID: "abandon-goal", Kind: model.GoalAbandoned, DeliveryID: "delivery"} + original := model.Objective{ID: "delivery-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + abandoned := model.Objective{ID: "abandon-objective", Kind: model.ObjectiveAbandoned, DeliveryID: "delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseFrontier, Engagement: model.EngagementActive, Delivery: model.DeliveryPublished, Workspace: model.WorkspacePublished, Plan: model.PlanLocked, Configuration: model.ConfigurationVerified, Runtime: model.RuntimeVerified, Publication: model.PublicationUnavailable, Verification: model.VerificationCurrent, Recovery: model.RecoveryEscalated, Transaction: model.TransactionNone, Terminal: model.TerminalStale, - Goal: original, TransactionID: "adm-interrupted", RecoveryCause: "provider unknown", RecoverySourcePhase: model.PhaseActive, + Objective: original, TransactionID: "adm-interrupted", RecoveryCause: "provider unknown", RecoverySourcePhase: model.PhaseActive, RecoveryResumption: model.PhaseFrontier, RecoveryBudget: 0, } - configure, _ := testprogram.StandardRegistry().Lookup("goal.configure") - configureAdmission := protocol.Admission{Goal: abandoned, Parameters: protocol.Parameters{ - {Name: "goal_kind", Value: string(abandoned.Kind)}, {Name: "delivery_id", Value: abandoned.DeliveryID}, + configure, _ := testprogram.StandardRegistry().Lookup("objective.bind") + configureAdmission := protocol.Admission{Objective: abandoned, Parameters: protocol.Parameters{ + {Name: "objective_kind", Value: string(abandoned.Kind)}, {Name: "delivery_id", Value: abandoned.DeliveryID}, }} if err := applyStateTransition(&state, configureAdmission, configure); err != nil { t.Fatal(err) } if state.Phase != model.PhaseFrontier || state.Recovery != model.RecoveryEscalated { - t.Fatalf("goal reconfiguration bypassed escalated recovery: %#v", state) + t.Fatalf("objective reconfiguration bypassed escalated recovery: %#v", state) } abandon, _ := testprogram.StandardRegistry().Lookup("plan.abandon") - if err := applyStateTransition(&state, protocol.Admission{Goal: abandoned}, abandon); err != nil { + if err := applyStateTransition(&state, protocol.Admission{Objective: abandoned}, abandon); err != nil { t.Fatal(err) } if state.Phase != model.PhaseAbandoned || state.Recovery != model.RecoveryNone || state.TransactionID != "" || state.Terminal != model.TerminalEstablished { diff --git a/boatstack/internal/kernel/engine/engine.go b/boatstack/internal/softwaredelivery/engine/engine.go similarity index 77% rename from boatstack/internal/kernel/engine/engine.go rename to boatstack/internal/softwaredelivery/engine/engine.go index 7eb04f1..fa90a91 100644 --- a/boatstack/internal/kernel/engine/engine.go +++ b/boatstack/internal/softwaredelivery/engine/engine.go @@ -7,11 +7,12 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" + general "github.com/operatorstack/boatstack/boatstack/kernel" ) type Engine struct { @@ -26,9 +27,9 @@ type Engine struct { receipts ports.ReceiptStore } -func New(registry catalog.Registry, contracts catalog.GoalContracts, program protocol.ProgramIdentity, observer ports.Observer, clock ports.Clock, locker ports.Locker, journal ports.Journal, effects ports.EffectDriver, receipts ports.ReceiptStore) (Engine, error) { +func New(registry catalog.Registry, contracts catalog.ObjectiveContracts, program protocol.ProgramIdentity, observer ports.Observer, clock ports.Clock, locker ports.Locker, journal ports.Journal, effects ports.EffectDriver, receipts ports.ReceiptStore) (Engine, error) { if registry.Len() == 0 || len(contracts) == 0 || program.Validate() != nil || observer == nil || clock == nil || locker == nil || journal == nil || effects == nil || receipts == nil { - return Engine{}, fmt.Errorf("kernel engine requires registry, goal contracts, observer, clock, locker, journal, effects, and receipt store") + return Engine{}, fmt.Errorf("kernel engine requires registry, objective contracts, observer, clock, locker, journal, effects, and receipt store") } return Engine{registry: registry, control: supervisor.New(registry, contracts), program: program, observer: observer, clock: clock, locker: locker, journal: journal, effects: effects, receipts: receipts}, nil } @@ -39,7 +40,7 @@ func (e Engine) canonicalize(observation model.Observation) (model.Snapshot, err type ResolveRequest struct { Invocation model.InvocationContext - Goal model.Goal + Objective model.Objective Authority protocol.AuthorityBundle Parameters protocol.Parameters Requested catalog.TransitionID @@ -47,7 +48,7 @@ type ResolveRequest struct { type Resolution struct { Snapshot model.Snapshot - Goal model.Goal + Objective model.Objective Decision supervisor.Decision Prescription protocol.Prescription Admission protocol.Admission @@ -55,7 +56,7 @@ type Resolution struct { func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution, error) { if err := request.Invocation.Validate(false); err != nil { - return unresolvedResolution(request.Goal, "invocation identity is invalid"), err + return unresolvedResolution(request.Objective, "invocation identity is invalid"), err } now := e.clock.Now() if err := request.Authority.Validate(now); err != nil { @@ -63,41 +64,41 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution } observation, err := e.observer.Observe(ctx, ports.ObservationRequest{Invocation: request.Invocation, Capabilities: request.Authority.GrantedCapabilities(now)}) if err != nil { - return unresolvedResolution(request.Goal, "required observation failed"), fmt.Errorf("observe plant: %w", err) + return unresolvedResolution(request.Objective, "required observation failed"), fmt.Errorf("observe plant: %w", err) } snapshot, err := e.canonicalize(observation) if err != nil { - return unresolvedResolution(request.Goal, "canonical observation is invalid"), fmt.Errorf("canonicalize observation: %w", err) + return unresolvedResolution(request.Objective, "canonical observation is invalid"), fmt.Errorf("canonicalize observation: %w", err) } if snapshot.Invocation != request.Invocation { return Resolution{}, fmt.Errorf("observer returned a different invocation identity") } - goal := request.Goal - if transition, requested := e.registry.Lookup(request.Requested); requested && transition.Policy.GoalScope == catalog.GoalScopeOptionalPreserve { - goal, err = protocol.GoalForTransition(snapshot, request.Goal, transition) + objective := request.Objective + if transition, requested := e.registry.Lookup(request.Requested); requested && transition.Policy.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve { + objective, err = protocol.ObjectiveForTransition(snapshot, request.Objective, transition) if err != nil { return Resolution{}, err } - } else if err := goal.Validate(); err != nil { - switch snapshot.Goal.Status { + } else if err := objective.Validate(); err != nil { + switch snapshot.Objective.Status { case model.FactKnown: - goal = snapshot.Goal.Value + objective = snapshot.Objective.Value case model.FactAbsent: - goal = model.Goal{} + objective = model.Objective{} default: - return Resolution{}, fmt.Errorf("no valid requested or configured goal: %w", err) + return Resolution{}, fmt.Errorf("no valid requested or configured objective: %w", err) } } - decision := e.control.Resolve(snapshot, goal, request.Authority.Set(now), request.Requested) + decision := e.control.Resolve(snapshot, objective, request.Authority.Set(now), request.Requested) if decision.Kind == supervisor.DecisionPrescribed && decision.Transition != nil { - goal, err = protocol.GoalForTransition(snapshot, goal, *decision.Transition) + objective, err = protocol.ObjectiveForTransition(snapshot, objective, *decision.Transition) if err != nil { decision.Kind = supervisor.DecisionRefused decision.Reason = err.Error() decision.Transition = nil - return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil + return Resolution{Snapshot: snapshot, Objective: objective, Decision: decision}, nil } - if applicabilityErr := protocol.ValidateApplicability(snapshot, goal, *decision.Transition, request.Authority, request.Parameters, now); applicabilityErr != nil { + if applicabilityErr := protocol.ValidateApplicability(snapshot, objective, *decision.Transition, request.Authority, request.Parameters, now); applicabilityErr != nil { if protocol.IsMissingParameter(applicabilityErr) { decision.Kind = supervisor.DecisionCandidate decision.Reason = applicabilityErr.Error() + "; bind the declared parameters and re-resolve this transition" @@ -113,16 +114,16 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution decision.Kind = supervisor.DecisionRefused decision.Reason = capabilityErr.Error() decision.Transition = nil - return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil + return Resolution{Snapshot: snapshot, Objective: objective, Decision: decision}, nil } prescription, prescriptionErr := protocol.NewPrescription(snapshot, *decision.Transition, capabilities) if prescriptionErr != nil { decision.Kind = supervisor.DecisionUnresolved decision.Reason = prescriptionErr.Error() decision.Transition = nil - return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil + return Resolution{Snapshot: snapshot, Objective: objective, Decision: decision}, nil } - admission, admissionErr := protocol.NewAdmission(snapshot, goal, *decision.Transition, prescription, request.Authority, request.Parameters, now, 2*time.Minute) + admission, admissionErr := protocol.NewAdmission(snapshot, objective, *decision.Transition, prescription, request.Authority, request.Parameters, now, 2*time.Minute) if admissionErr != nil { decision.Kind = supervisor.DecisionUnresolved decision.Reason = admissionErr.Error() @@ -132,15 +133,15 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution decision.Reason = fmt.Sprintf("transition %q failed deterministic effect preflight: %v", admission.TransitionID, preflightErr) decision.Transition = nil } else { - return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision, Prescription: prescription, Admission: admission}, nil + return Resolution{Snapshot: snapshot, Objective: objective, Decision: decision, Prescription: prescription, Admission: admission}, nil } } } - return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil + return Resolution{Snapshot: snapshot, Objective: objective, Decision: decision}, nil } -func unresolvedResolution(goal model.Goal, reason string) Resolution { - return Resolution{Goal: goal, Decision: supervisor.Decision{Kind: supervisor.DecisionUnresolved, Reason: reason}} +func unresolvedResolution(objective model.Objective, reason string) Resolution { + return Resolution{Objective: objective, Decision: supervisor.Decision{Kind: supervisor.DecisionUnresolved, Reason: reason}} } type ApplyRequest struct { @@ -155,7 +156,7 @@ type ApplyRequest struct { type ApplyResult struct { Source model.Snapshot Target model.Snapshot - Goal model.Goal + Objective model.Objective Decision supervisor.Decision Admission protocol.Admission Receipt protocol.TransitionReceipt @@ -180,6 +181,7 @@ type StalePrescriptionError struct { ExpectedProgramFingerprint string ObservedProgramFingerprint string SnapshotChanged bool + ObjectiveBindingChanged bool AuthorityChanged bool } @@ -194,6 +196,9 @@ func (e StalePrescriptionError) Error() string { if e.SnapshotChanged { facets = append(facets, "admission-relevant snapshot changed") } + if e.ObjectiveBindingChanged { + facets = append(facets, "objective binding changed") + } if e.AuthorityChanged { facets = append(facets, "authority or capability context changed") } @@ -251,15 +256,15 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if canonicalErr != nil { return result, canonicalErr } - if err := validateReplayGoalState(prior, snapshot); err != nil { + if err := validateReplayObjectiveState(prior, snapshot); err != nil { return result, err } if !replayStateSettled(snapshot) { return result, ReplayRecoveryError{ReceiptID: prior.ID} } - result.Source, result.Target, result.Goal, result.Receipt, result.Replayed = snapshot, snapshot, request.Goal, prior, true - if result.Goal.Validate() != nil && snapshot.Goal.Status == model.FactKnown { - result.Goal = snapshot.Goal.Value + result.Source, result.Target, result.Objective, result.Receipt, result.Replayed = snapshot, snapshot, request.Objective, prior, true + if result.Objective.Validate() != nil && snapshot.Objective.Status == model.FactKnown { + result.Objective = snapshot.Objective.Value } return result, nil } @@ -269,7 +274,7 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe } request.ResolveRequest.Parameters = request.Parameters resolution, err := e.Resolve(ctx, request.ResolveRequest) - result.Source, result.Goal, result.Decision = resolution.Snapshot, resolution.Goal, resolution.Decision + result.Source, result.Objective, result.Decision = resolution.Snapshot, resolution.Objective, resolution.Decision if err != nil { return result, err } @@ -284,13 +289,13 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe AuthorityChanged: true, } } - request.Goal = resolution.Goal + request.Objective = resolution.Objective if resolution.Decision.Kind != supervisor.DecisionPrescribed || resolution.Decision.Transition == nil { return result, DecisionError{Decision: resolution.Decision} } transition := *resolution.Decision.Transition now := e.clock.Now() - admission, err := protocol.NewAdmission(resolution.Snapshot, request.Goal, transition, request.Prescription, request.Authority, request.Parameters, now, request.AdmissionLifetime) + admission, err := protocol.NewAdmission(resolution.Snapshot, request.Objective, transition, request.Prescription, request.Authority, request.Parameters, now, request.AdmissionLifetime) if err != nil { return result, err } @@ -315,7 +320,7 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if canonicalErr != nil { return result, canonicalErr } - if err := validateReplayGoalState(prior, current); err != nil { + if err := validateReplayObjectiveState(prior, current); err != nil { return result, err } if !replayStateSettled(current) { @@ -356,7 +361,7 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if err := validateReplayRequest(prior, request, e.program.Fingerprint); err != nil { return result, err } - if err := validateReplayGoalState(prior, lockedSnapshot); err != nil { + if err := validateReplayObjectiveState(prior, lockedSnapshot); err != nil { return result, err } if !replayStateSettled(lockedSnapshot) { @@ -366,7 +371,7 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe return result, nil } } - if err := admission.ValidateCurrent(lockedSnapshot, request.Goal, transition, e.clock.Now()); err != nil { + if err := admission.ValidateCurrent(lockedSnapshot, request.Objective, transition, e.clock.Now()); err != nil { return result, StaleAdmissionError{Err: err} } if err := protocol.ValidateEffectCapabilities(admission, transition); err != nil { @@ -473,11 +478,19 @@ func validatePrescriptionCurrent(prescription protocol.Prescription, snapshot mo if err := prescription.Validate(); err != nil { return err } - snapshotChanged := prescription.ExpectedSnapshotFingerprint != snapshot.Fingerprint - if prescription.ExpectedStateRevision == snapshot.StateRevision && - prescription.ExpectedProgramFingerprint == snapshot.ProgramFingerprint && !snapshotChanged { + objectiveBindingFingerprint, err := protocol.ObjectiveBindingFingerprint(snapshot) + if err != nil { + return err + } + current, err := general.NewFreshness(snapshot.Invocation.RepositoryID, snapshot.StateRevision, snapshot.ProgramFingerprint, snapshot.Fingerprint, objectiveBindingFingerprint, prescription.AuthorityFingerprint) + if err != nil { + return err + } + if prescription.Freshness.Check(current) == nil { return nil } + snapshotChanged := prescription.ExpectedSnapshotFingerprint != snapshot.Fingerprint + objectiveBindingChanged := prescription.ExpectedObjectiveBindingFingerprint != objectiveBindingFingerprint return StalePrescriptionError{ PrescriptionID: prescription.ID, ExpectedStateRevision: prescription.ExpectedStateRevision, @@ -485,6 +498,7 @@ func validatePrescriptionCurrent(prescription protocol.Prescription, snapshot mo ExpectedProgramFingerprint: prescription.ExpectedProgramFingerprint, ObservedProgramFingerprint: snapshot.ProgramFingerprint, SnapshotChanged: snapshotChanged, + ObjectiveBindingChanged: objectiveBindingChanged, } } @@ -498,9 +512,9 @@ func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyReques if prior.PrescriptionID != request.Prescription.ID { return fmt.Errorf("idempotency receipt belongs to a different prescription") } - if prior.GoalScope != catalog.GoalScopeOptionalPreserve && request.Goal.Validate() == nil { - if prior.GoalID != request.Goal.ID || prior.GoalKind != request.Goal.Kind || prior.DeliveryID != request.Goal.DeliveryID { - return fmt.Errorf("idempotency receipt belongs to a different configured goal") + if prior.ObjectiveScope != catalog.ObjectiveScopeOptionalPreserve && request.Objective.Validate() == nil { + if prior.ObjectiveID != request.Objective.ID || prior.ObjectiveKind != request.Objective.Kind || prior.DeliveryID != request.Objective.DeliveryID { + return fmt.Errorf("idempotency receipt belongs to a different configured objective") } } if request.Requested != "" && prior.TransitionID != request.Requested { @@ -509,22 +523,22 @@ func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyReques return nil } -func validateReplayGoalState(prior protocol.TransitionReceipt, snapshot model.Snapshot) error { - if prior.GoalScope != catalog.GoalScopeOptionalPreserve { +func validateReplayObjectiveState(prior protocol.TransitionReceipt, snapshot model.Snapshot) error { + if prior.ObjectiveScope != catalog.ObjectiveScopeOptionalPreserve { return nil } - switch prior.GoalStatus { + switch prior.ObjectiveStatus { case model.FactKnown: - if snapshot.Goal.Status != model.FactKnown || snapshot.Goal.Value.ID != prior.GoalID || - snapshot.Goal.Value.Kind != prior.GoalKind || snapshot.Goal.Value.DeliveryID != prior.DeliveryID { - return fmt.Errorf("idempotency receipt product-goal binding no longer matches current state") + if snapshot.Objective.Status != model.FactKnown || snapshot.Objective.Value.ID != prior.ObjectiveID || + snapshot.Objective.Value.Kind != prior.ObjectiveKind || snapshot.Objective.Value.DeliveryID != prior.DeliveryID { + return fmt.Errorf("idempotency receipt objective binding no longer matches current state") } case model.FactAbsent: - if snapshot.Goal.Status != model.FactAbsent { - return fmt.Errorf("idempotency receipt preserved an absent product goal, but current state is %q", snapshot.Goal.Status) + if snapshot.Objective.Status != model.FactAbsent { + return fmt.Errorf("idempotency receipt preserved an absent product objective, but current state is %q", snapshot.Objective.Status) } default: - return fmt.Errorf("idempotency receipt has invalid preserved product-goal status %q", prior.GoalStatus) + return fmt.Errorf("idempotency receipt has invalid preserved objective status %q", prior.ObjectiveStatus) } return nil } diff --git a/boatstack/internal/kernel/engine/engine_test.go b/boatstack/internal/softwaredelivery/engine/engine_test.go similarity index 87% rename from boatstack/internal/kernel/engine/engine_test.go rename to boatstack/internal/softwaredelivery/engine/engine_test.go index b491197..c4349ba 100644 --- a/boatstack/internal/kernel/engine/engine_test.go +++ b/boatstack/internal/softwaredelivery/engine/engine_test.go @@ -10,11 +10,11 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" ) type fixedClock struct{ now time.Time } @@ -23,11 +23,11 @@ const syntheticProgramFingerprint = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb var syntheticProgram = protocol.ProgramIdentity{ID: "test.synthetic", Version: "1.0.0", Fingerprint: syntheticProgramFingerprint} -func syntheticGoalContracts(t *testing.T) catalog.GoalContracts { +func syntheticObjectiveContracts(t *testing.T) catalog.ObjectiveContracts { t.Helper() - contracts, err := catalog.NewGoalContracts([]catalog.GoalContract{{ - GoalKind: model.GoalVerified, - Conditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, + contracts, err := catalog.NewObjectiveContracts([]catalog.ObjectiveContract{{ + ObjectiveKind: model.ObjectiveVerified, + Conditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, }}, nil) if err != nil { t.Fatal(err) @@ -192,7 +192,7 @@ func observation(phase model.ProtocolPhase, fingerprint string) model.Observatio ConfigurationPolicy: model.Known(model.ConfigurationPolicy{PlanApproval: "human", VisualEvidence: "optional", ExternalEffectAuthority: "human-or-autonomy-plus-provider", Hosts: []string{"cli"}}, configurationEvidence), Publication: model.Known(model.PublicationNone, e), Verification: model.Known(model.VerificationUnverified, e), Recovery: model.Known(model.RecoveryNone, e), Transaction: model.Known(model.TransactionNone, e), RecoveryInfo: model.Absent[model.RecoveryContext]("none", e), TransactionInfo: model.Absent[model.TransactionContext]("none", e), - Terminal: model.Known(model.TerminalNonterminal, e), Goal: model.Known(model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"}, e), ObservedAt: time.Unix(20, 0).UTC(), + Terminal: model.Known(model.TerminalNonterminal, e), Objective: model.Known(model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"}, e), ObservedAt: time.Unix(20, 0).UTC(), ProgramFacts: map[string]model.Fact[string]{"test.synthetic.stage": model.Known(stage, e)}, } } @@ -243,7 +243,7 @@ func testRegistryWithAdvanceClass(t *testing.T, class catalog.EventClass) catalo SourceConditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"start"}}}, TargetConditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, Interruption: interruption("test.recover"), Reversibility: catalog.Reversible, TerminalEffect: "none", - PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Priority: 1, + PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, Priority: 1, }, { ID: "test.recover", Version: 1, Class: catalog.EventRecovery, Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: syntheticProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionProgramRecovery, @@ -253,7 +253,7 @@ func testRegistryWithAdvanceClass(t *testing.T, class catalog.EventClass) catalo SourceConditions: []catalog.FacetCondition{{Facet: model.FacetRecovery, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.RecoveryReconcile)}}}, TargetConditions: []catalog.FacetCondition{{Facet: model.FacetRecovery, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.RecoveryEscalated)}}}, Interruption: interruption("test.recover"), Reversibility: catalog.Reversible, TerminalEffect: "none", - PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Priority: 2, + PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, Priority: 2, }}) if err != nil { t.Fatal(err) @@ -279,7 +279,7 @@ func request(t *testing.T, now time.Time) ApplyRequest { t.Fatal(err) } return ApplyRequest{ResolveRequest: ResolveRequest{ - Invocation: invocation, Goal: model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"}, Requested: "test.advance", + Invocation: invocation, Objective: model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"}, Requested: "test.advance", Authority: authorityBundle, }, FlowID: "flow", Prescription: prescription, AdmissionLifetime: time.Minute} } @@ -289,7 +289,7 @@ func TestRequiredObserverFailureReturnsTypedUnresolvedDecision(t *testing.T) { now := time.Unix(30, 0).UTC() journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{} kernel, err := New( - testRegistry(t), syntheticGoalContracts(t), syntheticProgram, + testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, failingObserver{err: errors.New("observer unavailable")}, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts, ) @@ -331,7 +331,7 @@ func TestResolutionDoesNotPrescribeBeforeRequiredParametersAreBound(t *testing.T t.Fatal(err) } observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}} - kernel, err := New(registry, syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}) + kernel, err := New(registry, syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}) if err != nil { t.Fatal(err) } @@ -361,7 +361,7 @@ func TestResolutionDoesNotPrescribeAnEffectThatDeterministicPreflightRejects(t * effects := &fakeEffects{prepareErr: errors.New("malformed artifact")} observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source")}} journal := &fakeJournal{} - kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, &memoryReceipts{}) + kernel, err := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, &memoryReceipts{}) if err != nil { t.Fatal(err) } @@ -382,7 +382,7 @@ func TestApplyCrossesAdmissionEffectVerificationAndReceiptBoundary(t *testing.T) now := time.Unix(30, 0).UTC() observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseActive, "target"), observation(model.PhaseActive, "target")}} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{} - kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, err := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) if err != nil { t.Fatal(err) } @@ -410,7 +410,7 @@ func TestCommitFailureCannotProjectSuccessfulTransitionFact(t *testing.T) { observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseActive, "target")}} journal := &fakeJournal{commitErr: errors.New("injected canonical commit failure")} effects, receipts := &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, receipts) result, err := kernel.Apply(context.Background(), request(t, now)) if err == nil || !strings.Contains(err.Error(), "canonical commit failure") { t.Fatalf("error=%v, want canonical commit failure", err) @@ -427,7 +427,7 @@ func TestProjectionFailureCannotUndoCanonicalTransitionFact(t *testing.T) { journal := &fakeJournal{} effects := &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}} receipts := &memoryReceipts{projectErr: errors.New("projection unavailable")} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, receipts) result, err := kernel.Apply(context.Background(), request(t, now)) if err != nil || result.Receipt.ID == "" || journal.committed != 1 || journal.recovery != 0 || len(receipts.values) != 0 { t.Fatalf("passive projection changed commit result: result=%#v err=%v journal=%+v", result.Receipt, err, journal) @@ -436,18 +436,18 @@ func TestProjectionFailureCannotUndoCanonicalTransitionFact(t *testing.T) { func TestSyntheticStartVerifyTerminalContractNeedsNoStandardFlowFacet(t *testing.T) { // control-law: kernel-terminal-is-defined-only-by-the-compiled-control-program-contract - goal := model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} source, err := model.CanonicalizeForProgram(observation(model.PhaseObserved, "source"), syntheticProgramFingerprint) if err != nil { t.Fatal(err) } - syntheticSupervisor := supervisor.New(testRegistry(t), syntheticGoalContracts(t)) - one := syntheticSupervisor.Resolve(source, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") - two := syntheticSupervisor.Resolve(source, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + syntheticSupervisor := supervisor.New(testRegistry(t), syntheticObjectiveContracts(t)) + one := syntheticSupervisor.Resolve(source, objective, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + two := syntheticSupervisor.Resolve(source, objective, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") if !reflect.DeepEqual(one, two) || one.Kind != supervisor.DecisionPrescribed || one.Transition == nil || one.Transition.ID != "test.advance" { t.Fatalf("synthetic resolution is not deterministic: one=%+v two=%+v", one, two) } - outside := syntheticSupervisor.Resolve(source, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "not.compiled") + outside := syntheticSupervisor.Resolve(source, objective, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "not.compiled") if outside.Kind != supervisor.DecisionRefused || outside.Transition != nil { t.Fatalf("transition outside compiled program was not refused: %+v", outside) } @@ -461,13 +461,13 @@ func TestSyntheticStartVerifyTerminalContractNeedsNoStandardFlowFacet(t *testing if err != nil { t.Fatal(err) } - decision := syntheticSupervisor.Resolve(target, goal, nil, "") + decision := syntheticSupervisor.Resolve(target, objective, nil, "") if decision.Kind != supervisor.DecisionTerminal { t.Fatalf("synthetic terminal decision = %+v", decision) } - withoutContract := supervisor.New(testRegistry(t), catalog.GoalContracts{}).Resolve(target, goal, nil, "") + withoutContract := supervisor.New(testRegistry(t), catalog.ObjectiveContracts{}).Resolve(target, objective, nil, "") if withoutContract.Kind == supervisor.DecisionTerminal { - t.Fatalf("synthetic state terminated without a compiled flow goal contract: %+v", withoutContract) + t.Fatalf("synthetic state terminated without a compiled flow objective contract: %+v", withoutContract) } if target.Terminal.Value != model.TerminalNonterminal || target.Plan.Value != model.PlanAbsent || target.Publication.Value != model.PublicationNone { t.Fatalf("fixture unexpectedly relied on StandardFlow terminal state: %+v", target) @@ -489,8 +489,8 @@ func TestExactPermittedRecoveryRemainsReachableAcrossProgramDrift(t *testing.T) if snapshot.Program.Value != model.ProgramDrift { t.Fatalf("program state = %s, want drift", snapshot.Program.Value) } - control := supervisor.New(testRegistry(t), syntheticGoalContracts(t)) - decision := control.Resolve(snapshot, snapshot.Goal.Value, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "test.recover") + control := supervisor.New(testRegistry(t), syntheticObjectiveContracts(t)) + decision := control.Resolve(snapshot, snapshot.Objective.Value, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "test.recover") if decision.Kind != supervisor.DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "test.recover" { t.Fatalf("permitted recovery across drift = %+v", decision) } @@ -500,7 +500,7 @@ func TestExactPermittedRecoveryRemainsReachableAcrossProgramDrift(t *testing.T) if err != nil { t.Fatal(err) } - blocked := control.Resolve(snapshot, snapshot.Goal.Value, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "test.recover") + blocked := control.Resolve(snapshot, snapshot.Objective.Value, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "test.recover") if blocked.Kind != supervisor.DecisionUnresolved { t.Fatalf("unpermitted recovery crossed program drift: %+v", blocked) } @@ -514,7 +514,7 @@ func TestIdempotencyReceiptCannotHideUncommittedRecoveryJournal(t *testing.T) { recoveryObservation("recovery"), }} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{} - kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, err := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) if err != nil { t.Fatal(err) } @@ -539,7 +539,7 @@ func TestApplyRejectsSnapshotDriftBeforeEffect(t *testing.T) { now := time.Unix(30, 0).UTC() observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "drifted")}} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) _, err := kernel.Apply(context.Background(), request(t, now)) var stale StalePrescriptionError if !errors.As(err, &stale) { @@ -557,7 +557,7 @@ func TestApplyRejectsHumanRevisionAdvanceBeforeEffect(t *testing.T) { advanced.StateRevision = 2 observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), advanced}} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) _, err := kernel.Apply(context.Background(), request(t, now)) var stale StalePrescriptionError if !errors.As(err, &stale) || stale.ExpectedStateRevision != 1 || stale.ObservedStateRevision != 2 { @@ -573,7 +573,7 @@ func TestApplyRollsBackFailedPostconditionAndDoesNotReceipt(t *testing.T) { now := time.Unix(30, 0).UTC() observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "unchanged")}} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) _, err := kernel.Apply(context.Background(), request(t, now)) var postcondition PostconditionError if !errors.As(err, &postcondition) { @@ -590,7 +590,7 @@ func TestApplyRequiresRecoveryWhenJournalFailsAfterEffect(t *testing.T) { observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}} journal := &fakeJournal{failMark: "verifying"} effects, receipts, lock := &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) _, err := kernel.Apply(context.Background(), request(t, now)) if err == nil || !strings.Contains(err.Error(), "injected journal mark failure") { t.Fatalf("error=%v, want injected post-effect journal failure", err) @@ -605,7 +605,7 @@ func TestApplyPreservesUnknownExternalOutcomeForReconciliation(t *testing.T) { now := time.Unix(30, 0).UTC() observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "unchanged")}} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectUnknown}}, &memoryReceipts{}, &fakeLock{} - kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, _ := New(testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) _, err := kernel.Apply(context.Background(), request(t, now)) var unknown ExternalOutcomeUnknownError if !errors.As(err, &unknown) { @@ -622,7 +622,7 @@ func TestOwnedExternalExecutionErrorRequiresRecoveryWithoutRollback(t *testing.T registry := testRegistryWithAdvanceClass(t, catalog.EventOwnedExternal) observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}} journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{err: context.DeadlineExceeded}, &memoryReceipts{}, &fakeLock{} - kernel, err := New(registry, syntheticGoalContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) + kernel, err := New(registry, syntheticObjectiveContracts(t), syntheticProgram, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts) if err != nil { t.Fatal(err) } diff --git a/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go b/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go new file mode 100644 index 0000000..a2fa41d --- /dev/null +++ b/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go @@ -0,0 +1,44 @@ +package engine + +import ( + "testing" + + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" +) + +func TestMaintenanceReplayBindsDurableObjectiveState(t *testing.T) { + // control-law: maintenance-replay-preserves-verified-objective-state + configured := model.Objective{ID: "configured", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + commandObjective := model.Objective{ID: "command", Kind: model.ObjectiveApprovedPlan, DeliveryID: "other"} + request := ApplyRequest{ResolveRequest: ResolveRequest{Objective: commandObjective}, FlowID: "flow"} + + tests := []struct { + name string + receipt protocol.TransitionReceipt + fact model.Fact[model.Objective] + wantErr bool + }{ + {name: "absent survives command objective and retry", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactAbsent}, fact: model.Fact[model.Objective]{Status: model.FactAbsent}}, + {name: "known survives conflicting command objective and retry", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactKnown, ObjectiveID: configured.ID, ObjectiveKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}}, + {name: "absent cannot replay after product objective appears", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactAbsent}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}, wantErr: true}, + {name: "known cannot replay after product objective changes", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactKnown, ObjectiveID: configured.ID, ObjectiveKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: commandObjective}, wantErr: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.receipt.FlowID = request.FlowID + test.receipt.Program = syntheticProgram + if err := validateReplayRequest(test.receipt, request, syntheticProgramFingerprint); err != nil { + t.Fatalf("command objective affected maintenance replay identity: %v", err) + } + err := validateReplayObjectiveState(test.receipt, model.Snapshot{Observation: model.Observation{Objective: test.fact}}) + if test.wantErr && err == nil { + t.Fatal("changed durable objective state was accepted for replay") + } + if !test.wantErr && err != nil { + t.Fatalf("unchanged durable objective state rejected: %v", err) + } + }) + } +} diff --git a/boatstack/internal/kernel/model/facet.go b/boatstack/internal/softwaredelivery/model/facet.go similarity index 93% rename from boatstack/internal/kernel/model/facet.go rename to boatstack/internal/softwaredelivery/model/facet.go index 6b502b5..578b5f1 100644 --- a/boatstack/internal/kernel/model/facet.go +++ b/boatstack/internal/softwaredelivery/model/facet.go @@ -30,14 +30,14 @@ const ( FacetRecoveryInfo FacetName = "recovery-info" FacetTransactionInfo FacetName = "transaction-info" FacetTerminal FacetName = "terminal" - FacetGoal FacetName = "goal" + FacetObjective FacetName = "objective" ) var controllingFacets = []FacetName{ FacetPhase, FacetProgram, FacetTopology, FacetEngagement, FacetDelivery, FacetWorkspace, FacetPlan, FacetConfiguration, FacetConfigurationPolicy, FacetRuntime, FacetPublication, FacetVerification, FacetRecovery, FacetTransaction, FacetRecoveryInfo, - FacetTransactionInfo, FacetTerminal, FacetGoal, + FacetTransactionInfo, FacetTerminal, FacetObjective, } var namespacedFacet = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+){2,}$`) @@ -101,9 +101,9 @@ func (s Snapshot) Facet(name FacetName) (FactStatus, string, bool) { return s.TransactionInfo.Status, strings.Join([]string{value.ID, value.TransitionID, value.Status, strings.Join(resources, ","), fmt.Sprint(value.ExternalPossible)}, "|"), true case FacetTerminal: return s.Terminal.Status, string(s.Terminal.Value), true - case FacetGoal: - value := s.Goal.Value - return s.Goal.Status, strings.Join([]string{value.ID, string(value.Kind), value.DeliveryID, value.EvidenceFingerprint, fmt.Sprint(value.FrontierIsStop)}, "|"), true + case FacetObjective: + value := s.Objective.Value + return s.Objective.Status, strings.Join([]string{value.ID, string(value.Kind), value.DeliveryID, value.EvidenceFingerprint, fmt.Sprint(value.FrontierIsStop)}, "|"), true default: if fact, ok := s.ProgramFacts[string(name)]; ok { return fact.Status, fact.Value, true diff --git a/boatstack/internal/kernel/model/fact.go b/boatstack/internal/softwaredelivery/model/fact.go similarity index 100% rename from boatstack/internal/kernel/model/fact.go rename to boatstack/internal/softwaredelivery/model/fact.go diff --git a/boatstack/internal/kernel/model/health.go b/boatstack/internal/softwaredelivery/model/health.go similarity index 100% rename from boatstack/internal/kernel/model/health.go rename to boatstack/internal/softwaredelivery/model/health.go diff --git a/boatstack/internal/kernel/model/identity.go b/boatstack/internal/softwaredelivery/model/identity.go similarity index 100% rename from boatstack/internal/kernel/model/identity.go rename to boatstack/internal/softwaredelivery/model/identity.go diff --git a/boatstack/internal/softwaredelivery/model/objective.go b/boatstack/internal/softwaredelivery/model/objective.go new file mode 100644 index 0000000..cc37d72 --- /dev/null +++ b/boatstack/internal/softwaredelivery/model/objective.go @@ -0,0 +1,45 @@ +package model + +import ( + "fmt" + "regexp" +) + +type ObjectiveKind string + +const ( + ObjectiveApprovedPlan ObjectiveKind = "approved-plan" + ObjectiveVerified ObjectiveKind = "verified-implementation" + ObjectiveOpenPR ObjectiveKind = "open-or-updated-pr" + ObjectiveMerged ObjectiveKind = "merged-delivery" + ObjectiveAbandoned ObjectiveKind = "safely-abandoned" +) + +func (k ObjectiveKind) Valid() bool { + switch k { + case ObjectiveApprovedPlan, ObjectiveVerified, ObjectiveOpenPR, ObjectiveMerged, ObjectiveAbandoned: + return true + default: + return false + } +} + +type Objective struct { + ID string `json:"id"` + Kind ObjectiveKind `json:"kind"` + DeliveryID string `json:"delivery_id"` + EvidenceFingerprint string `json:"evidence_fingerprint,omitempty"` + FrontierIsStop bool `json:"frontier_is_stop,omitempty"` +} + +var safeObjectiveIdentity = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) + +func (g Objective) Validate() error { + if !safeObjectiveIdentity.MatchString(g.ID) || !safeObjectiveIdentity.MatchString(g.DeliveryID) { + return fmt.Errorf("objective: id and delivery identity must be safe semantic segments") + } + if !g.Kind.Valid() { + return fmt.Errorf("objective: invalid kind %q", g.Kind) + } + return nil +} diff --git a/boatstack/internal/kernel/model/state.go b/boatstack/internal/softwaredelivery/model/state.go similarity index 97% rename from boatstack/internal/kernel/model/state.go rename to boatstack/internal/softwaredelivery/model/state.go index 0db00c1..66fe73e 100644 --- a/boatstack/internal/kernel/model/state.go +++ b/boatstack/internal/softwaredelivery/model/state.go @@ -9,7 +9,7 @@ import ( "time" ) -const SnapshotSchemaVersion = 3 +const SnapshotSchemaVersion = 4 type ProtocolPhase string @@ -409,7 +409,7 @@ type Observation struct { RecoveryInfo Fact[RecoveryContext] `json:"recovery_info"` TransactionInfo Fact[TransactionContext] `json:"transaction_info"` Terminal Fact[TerminalStatus] `json:"terminal"` - Goal Fact[Goal] `json:"goal"` + Objective Fact[Objective] `json:"objective"` ProgramFacts map[string]Fact[string] `json:"flow_facts,omitempty"` ExtensionFacts map[string]Fact[string] `json:"extension_facts,omitempty"` ObservedAt time.Time `json:"observed_at"` @@ -481,7 +481,7 @@ func Canonicalize(observation Observation) (Snapshot, error) { {"recovery_info", observation.RecoveryInfo.Validate("recovery_info")}, {"transaction_info", observation.TransactionInfo.Validate("transaction_info")}, {"terminal", observation.Terminal.Validate("terminal")}, - {"goal", observation.Goal.Validate("goal")}, + {"objective", observation.Objective.Validate("objective")}, } for _, check := range checks { if check.err != nil { @@ -539,8 +539,8 @@ func Canonicalize(observation Observation) (Snapshot, error) { return Snapshot{}, fmt.Errorf("snapshot: established terminal evidence requires terminal or abandoned phase") } if observation.Terminal.Status == FactKnown && observation.Terminal.Value == TerminalEstablished { - if observation.Goal.Status != FactKnown || observation.Goal.Value.Validate() != nil { - return Snapshot{}, fmt.Errorf("snapshot: established terminal evidence requires an exact configured goal") + if observation.Objective.Status != FactKnown || observation.Objective.Value.Validate() != nil { + return Snapshot{}, fmt.Errorf("snapshot: established terminal evidence requires an exact configured objective") } if observation.Delivery.Status == FactKnown && observation.Delivery.Value != DeliveryTerminal && observation.Delivery.Value != DeliveryDiscarded { return Snapshot{}, fmt.Errorf("snapshot: established terminal evidence requires terminal or discarded delivery") @@ -552,9 +552,9 @@ func Canonicalize(observation Observation) (Snapshot, error) { if observation.Phase.Status == FactKnown && observation.Phase.Value == PhaseActive && observation.Engagement.Status == FactKnown && observation.Engagement.Value == EngagementDormant { return Snapshot{}, fmt.Errorf("snapshot: active protocol phase cannot have dormant engagement") } - if observation.Goal.Status == FactKnown { - if err := observation.Goal.Value.Validate(); err != nil { - return Snapshot{}, fmt.Errorf("snapshot: invalid goal fact: %w", err) + if observation.Objective.Status == FactKnown { + if err := observation.Objective.Value.Validate(); err != nil { + return Snapshot{}, fmt.Errorf("snapshot: invalid objective fact: %w", err) } } for id, fact := range observation.ProgramFacts { @@ -608,7 +608,7 @@ func Canonicalize(observation Observation) (Snapshot, error) { zeroEvidenceTimes(&projection.RecoveryInfo) zeroEvidenceTimes(&projection.TransactionInfo) zeroEvidenceTimes(&projection.Terminal) - zeroEvidenceTimes(&projection.Goal) + zeroEvidenceTimes(&projection.Objective) for id, fact := range projection.ProgramFacts { zeroEvidenceTimes(&fact) projection.ProgramFacts[id] = fact diff --git a/boatstack/internal/kernel/model/state_facet.go b/boatstack/internal/softwaredelivery/model/state_facet.go similarity index 100% rename from boatstack/internal/kernel/model/state_facet.go rename to boatstack/internal/softwaredelivery/model/state_facet.go diff --git a/boatstack/internal/kernel/model/state_test.go b/boatstack/internal/softwaredelivery/model/state_test.go similarity index 95% rename from boatstack/internal/kernel/model/state_test.go rename to boatstack/internal/softwaredelivery/model/state_test.go index 949a639..ed7c4ec 100644 --- a/boatstack/internal/kernel/model/state_test.go +++ b/boatstack/internal/softwaredelivery/model/state_test.go @@ -34,7 +34,7 @@ func testObservation(phase ProtocolPhase) Observation { Publication: Known(PublicationNone, evidence), Verification: Known(VerificationUnverified, evidence), Recovery: Known(RecoveryNone, evidence), Transaction: Known(TransactionNone, evidence), RecoveryInfo: Absent[RecoveryContext]("none", evidence), TransactionInfo: Absent[TransactionContext]("none", evidence), - Terminal: Known(TerminalNonterminal, evidence), Goal: Absent[Goal]("not configured", evidence), ObservedAt: time.Unix(100, 0).UTC(), + Terminal: Known(TerminalNonterminal, evidence), Objective: Absent[Objective]("not configured", evidence), ObservedAt: time.Unix(100, 0).UTC(), } } @@ -88,7 +88,7 @@ func TestInvocationContextRejectsEffectWithoutExactRuntimeIdentity(t *testing.T) } func TestCanonicalizeRejectsEstablishedTerminalInActivePhase(t *testing.T) { - // control-law: terminal-is-an-evidence-backed-goal-state + // control-law: terminal-is-an-evidence-backed-objective-state observation := testObservation(PhaseActive) observation.Terminal.Value = TerminalEstablished if _, err := Canonicalize(observation); err == nil { @@ -135,8 +135,8 @@ func TestEveryControllingFacetChangesCanonicalIdentity(t *testing.T) { o.TransactionInfo = Unknown[TransactionContext](FactStale, "stale context", evidence) }, FacetTerminal: func(o *Observation) { o.Terminal = Known(TerminalStale, evidence) }, - FacetGoal: func(o *Observation) { - o.Goal = Known(Goal{ID: "goal", Kind: GoalVerified, DeliveryID: "delivery"}, evidence) + FacetObjective: func(o *Observation) { + o.Objective = Known(Objective{ID: "objective", Kind: ObjectiveVerified, DeliveryID: "delivery"}, evidence) }, } for _, facet := range ControllingFacets() { diff --git a/boatstack/internal/plant/observer.go b/boatstack/internal/softwaredelivery/plant/observer.go similarity index 96% rename from boatstack/internal/plant/observer.go rename to boatstack/internal/softwaredelivery/plant/observer.go index 86f93b5..e2a46d4 100644 --- a/boatstack/internal/plant/observer.go +++ b/boatstack/internal/softwaredelivery/plant/observer.go @@ -16,12 +16,12 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type TimeSource interface{ Now() time.Time } @@ -191,21 +191,21 @@ func (o Observer) Observe(ctx context.Context, request ports.ObservationRequest) recoveryInfoFact := model.Absent[model.RecoveryContext]("no recovery context", stateEvidence...) transactionInfoFact := model.Absent[model.TransactionContext]("no active transaction", stateEvidence...) terminal := artifactTerminal - requiresCurrentImplementation := state.Goal.Kind == model.GoalVerified || state.Goal.Kind == model.GoalOpenPR + requiresCurrentImplementation := state.Objective.Kind == model.ObjectiveVerified || state.Objective.Kind == model.ObjectiveOpenPR currentDeliveryInvalid := verification != model.VerificationCurrent || configuration != model.ConfigurationVerified || runtimeState != model.RuntimeVerified if requiresCurrentImplementation && (terminal == model.TerminalStale || (terminal == model.TerminalEstablished && currentDeliveryInvalid)) { terminal, phase, delivery = model.TerminalStale, model.PhaseActive, model.DeliveryActive if runtimeState == model.RuntimeAbsent { phase = model.PhaseObserved } - } else if state.Goal.Kind == model.GoalApprovedPlan && terminal == model.TerminalStale { + } else if state.Objective.Kind == model.ObjectiveApprovedPlan && terminal == model.TerminalStale { phase, delivery = model.PhaseActive, model.DeliveryPlanning } - if state.Goal.Kind == model.GoalMerged && state.Publication == model.PublicationMerged && state.Delivery == model.DeliveryTerminal && + if state.Objective.Kind == model.ObjectiveMerged && state.Publication == model.PublicationMerged && state.Delivery == model.DeliveryTerminal && (state.Workspace == model.WorkspaceLanded || state.Workspace == model.WorkspaceAbsent) { terminal, phase = model.TerminalEstablished, model.PhaseTerminal } - if state.Goal.Kind == model.GoalAbandoned && state.Delivery == model.DeliveryDiscarded && + if state.Objective.Kind == model.ObjectiveAbandoned && state.Delivery == model.DeliveryDiscarded && (state.Workspace == model.WorkspaceAbandoned || state.Workspace == model.WorkspaceAbsent) { terminal, phase = model.TerminalEstablished, model.PhaseAbandoned } @@ -254,9 +254,9 @@ func (o Observer) Observe(ctx context.Context, request ports.ObservationRequest) if configEvidence.Source != "" { configurationEvidence = append(append([]model.Evidence(nil), stateEvidence...), configEvidence) } - goalFact := model.Absent[model.Goal]("no configured V2 goal", stateEvidence...) - if state.Goal.Validate() == nil { - goalFact = model.Fact[model.Goal]{Status: model.FactKnown, Value: state.Goal, Evidence: stateEvidence} + objectiveFact := model.Absent[model.Objective]("no configured V2 objective", stateEvidence...) + if state.Objective.Validate() == nil { + objectiveFact = model.Fact[model.Objective]{Status: model.FactKnown, Value: state.Objective, Evidence: stateEvidence} } return model.Observation{ SchemaVersion: model.SnapshotSchemaVersion, StateRevision: state.Revision, RecordedProgramFingerprint: recordedProgramFingerprint, Invocation: current, @@ -275,7 +275,7 @@ func (o Observer) Observe(ctx context.Context, request ports.ObservationRequest) RecoveryInfo: recoveryInfoFact, TransactionInfo: transactionInfoFact, Terminal: model.Fact[model.TerminalStatus]{Status: model.FactKnown, Value: terminal, Evidence: terminalEvidence}, - Goal: goalFact, + Objective: objectiveFact, ObservedAt: now, }, nil } @@ -502,10 +502,10 @@ func decodeStrictJSON[T any](raw []byte, value *T) error { func observeRepositoryArtifacts(layout ports.ControllerLayout, state durable.State, now time.Time) (model.PlanState, model.VerificationState, model.TerminalStatus, []model.Evidence, []model.Evidence, error) { plan, verification, terminal := state.Plan, state.Verification, state.Terminal var planEvidence, verificationEvidence []model.Evidence - if state.Goal.Validate() != nil { + if state.Objective.Validate() != nil { return plan, verification, terminal, planEvidence, verificationEvidence, nil } - deliveryID := state.Goal.DeliveryID + deliveryID := state.Objective.DeliveryID if state.Plan != model.PlanAbsent { path := filepath.Join(layout.RepositoryRoot, ".boatstack", "plans", deliveryID+".source") evidence, fingerprint, exists, err := fileEvidence(path, "plan", now) @@ -600,7 +600,7 @@ func observeRepositoryArtifacts(layout ports.ControllerLayout, state durable.Sta verification, terminal = model.VerificationStale, model.TerminalStale } } - if terminal == model.TerminalEstablished && state.Goal.Kind == model.GoalVerified && state.VisualEvidencePolicy == "required" && !hasVisual { + if terminal == model.TerminalEstablished && state.Objective.Kind == model.ObjectiveVerified && state.VisualEvidencePolicy == "required" && !hasVisual { verification, terminal = model.VerificationUnresolved, model.TerminalStale } return plan, verification, terminal, planEvidence, verificationEvidence, nil diff --git a/boatstack/internal/plant/observer_test.go b/boatstack/internal/softwaredelivery/plant/observer_test.go similarity index 97% rename from boatstack/internal/plant/observer_test.go rename to boatstack/internal/softwaredelivery/plant/observer_test.go index 5bb8808..a3d393e 100644 --- a/boatstack/internal/plant/observer_test.go +++ b/boatstack/internal/softwaredelivery/plant/observer_test.go @@ -10,11 +10,11 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type observerClock struct{ now time.Time } @@ -195,7 +195,7 @@ func TestDoubleStarMatchesRootAndNestedPaths(t *testing.T) { want bool }{ {pattern: "**/*.go", name: "main.go", want: true}, - {pattern: "**/*.go", name: "internal/kernel/main.go", want: true}, + {pattern: "**/*.go", name: "internal/softwaredelivery/main.go", want: true}, {pattern: "migrations/**", name: "migrations/001.sql", want: true}, {pattern: "migrations/**", name: "docs/migrations/001.sql", want: false}, } { diff --git a/boatstack/internal/plant/resolver.go b/boatstack/internal/softwaredelivery/plant/resolver.go similarity index 97% rename from boatstack/internal/plant/resolver.go rename to boatstack/internal/softwaredelivery/plant/resolver.go index c4a5db6..a2b95e2 100644 --- a/boatstack/internal/plant/resolver.go +++ b/boatstack/internal/softwaredelivery/plant/resolver.go @@ -12,9 +12,9 @@ import ( "strings" "github.com/operatorstack/boatstack/boatstack/internal/buildinfo" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/durable" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" ) const stateRootEnvironment = "BOATSTACK_STATE_ROOT" diff --git a/boatstack/internal/kernel/ports/ports.go b/boatstack/internal/softwaredelivery/ports/ports.go similarity index 93% rename from boatstack/internal/kernel/ports/ports.go rename to boatstack/internal/softwaredelivery/ports/ports.go index 0192b29..4763b63 100644 --- a/boatstack/internal/kernel/ports/ports.go +++ b/boatstack/internal/softwaredelivery/ports/ports.go @@ -4,9 +4,9 @@ import ( "context" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type Observer interface { diff --git a/boatstack/internal/kernel/protocol/admission.go b/boatstack/internal/softwaredelivery/protocol/admission.go similarity index 68% rename from boatstack/internal/kernel/protocol/admission.go rename to boatstack/internal/softwaredelivery/protocol/admission.go index 49db778..2bb4759 100644 --- a/boatstack/internal/kernel/protocol/admission.go +++ b/boatstack/internal/softwaredelivery/protocol/admission.go @@ -5,49 +5,50 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) -const AdmissionSchemaVersion = 4 +const AdmissionSchemaVersion = 5 type Admission struct { - SchemaVersion int `json:"schema_version"` - ID string `json:"id"` - PrescriptionID string `json:"prescription_id"` - TransitionID catalog.TransitionID `json:"transition_id"` - TransitionVersion int `json:"transition_version"` - ExpectedStateRevision uint64 `json:"expected_state_revision"` - ExpectedProgramFingerprint string `json:"expected_program_fingerprint"` - PriorProgramFingerprint string `json:"prior_program_fingerprint,omitempty"` - ProgramDeltaFingerprint string `json:"program_delta_fingerprint,omitempty"` - ExpectedSnapshotFingerprint string `json:"expected_snapshot_fingerprint"` - SourceRevision string `json:"source_revision,omitempty"` - WorktreeFingerprint string `json:"worktree_fingerprint,omitempty"` - SourcePhase model.ProtocolPhase `json:"source_phase"` - Invocation model.InvocationContext `json:"invocation"` - Goal model.Goal `json:"goal"` - GoalScope catalog.GoalScope `json:"goal_scope,omitempty"` - GoalStatus model.FactStatus `json:"goal_status,omitempty"` - Authority AuthorityBundle `json:"authority"` - AuthorityFingerprint string `json:"authority_fingerprint"` - RequiredCapabilities []catalog.Capability `json:"required_capabilities"` - GrantedCapabilities []catalog.Capability `json:"granted_capabilities"` - EffectiveCapabilities []catalog.Capability `json:"effective_capabilities"` - Parameters Parameters `json:"parameters,omitempty"` - Evidence []string `json:"evidence"` - IdempotencyKey string `json:"idempotency_key"` - IssuedAt time.Time `json:"issued_at"` - ExpiresAt time.Time `json:"expires_at"` + SchemaVersion int `json:"schema_version"` + ID string `json:"id"` + PrescriptionID string `json:"prescription_id"` + TransitionID catalog.TransitionID `json:"transition_id"` + TransitionVersion int `json:"transition_version"` + ExpectedStateRevision uint64 `json:"expected_state_revision"` + ExpectedProgramFingerprint string `json:"expected_program_fingerprint"` + PriorProgramFingerprint string `json:"prior_program_fingerprint,omitempty"` + ProgramDeltaFingerprint string `json:"program_delta_fingerprint,omitempty"` + ExpectedSnapshotFingerprint string `json:"expected_snapshot_fingerprint"` + ExpectedObjectiveBindingFingerprint string `json:"expected_objective_binding_fingerprint"` + SourceRevision string `json:"source_revision,omitempty"` + WorktreeFingerprint string `json:"worktree_fingerprint,omitempty"` + SourcePhase model.ProtocolPhase `json:"source_phase"` + Invocation model.InvocationContext `json:"invocation"` + Objective model.Objective `json:"objective"` + ObjectiveScope catalog.ObjectiveScope `json:"objective_scope,omitempty"` + ObjectiveStatus model.FactStatus `json:"objective_status,omitempty"` + Authority AuthorityBundle `json:"authority"` + AuthorityFingerprint string `json:"authority_fingerprint"` + RequiredCapabilities []catalog.Capability `json:"required_capabilities"` + GrantedCapabilities []catalog.Capability `json:"granted_capabilities"` + EffectiveCapabilities []catalog.Capability `json:"effective_capabilities"` + Parameters Parameters `json:"parameters,omitempty"` + Evidence []string `json:"evidence"` + IdempotencyKey string `json:"idempotency_key"` + IssuedAt time.Time `json:"issued_at"` + ExpiresAt time.Time `json:"expires_at"` } -func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, prescription Prescription, authority AuthorityBundle, parameters Parameters, now time.Time, lifetime time.Duration) (Admission, error) { +func NewAdmission(snapshot model.Snapshot, objective model.Objective, transition catalog.Transition, prescription Prescription, authority AuthorityBundle, parameters Parameters, now time.Time, lifetime time.Duration) (Admission, error) { var err error - goal, err = GoalForTransition(snapshot, goal, transition) + objective, err = ObjectiveForTransition(snapshot, objective, transition) if err != nil { return Admission{}, err } - if err := ValidateApplicability(snapshot, goal, transition, authority, parameters, now); err != nil { + if err := ValidateApplicability(snapshot, objective, transition, authority, parameters, now); err != nil { return Admission{}, err } capabilities, err := ProjectCapabilities(snapshot, transition, authority, now) @@ -64,14 +65,15 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T a := Admission{ SchemaVersion: AdmissionSchemaVersion, PrescriptionID: prescription.ID, TransitionID: transition.ID, TransitionVersion: transition.Version, ExpectedStateRevision: prescription.ExpectedStateRevision, ExpectedProgramFingerprint: prescription.ExpectedProgramFingerprint, - ExpectedSnapshotFingerprint: prescription.ExpectedSnapshotFingerprint, SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint, - SourcePhase: snapshot.Phase.Value, Invocation: snapshot.Invocation, Goal: goal, GoalScope: transition.Policy.GoalScope, Authority: authority.canonical(), + ExpectedSnapshotFingerprint: prescription.ExpectedSnapshotFingerprint, ExpectedObjectiveBindingFingerprint: prescription.ExpectedObjectiveBindingFingerprint, + SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint, + SourcePhase: snapshot.Phase.Value, Invocation: snapshot.Invocation, Objective: objective, ObjectiveScope: transition.Policy.ObjectiveScope, Authority: authority.canonical(), AuthorityFingerprint: capabilities.AuthorityFingerprint, RequiredCapabilities: capabilities.Required, GrantedCapabilities: capabilities.Granted, EffectiveCapabilities: capabilities.Effective, Evidence: append([]string(nil), transition.RequiredEvidence...), Parameters: parameters.Canonical(), IssuedAt: now.UTC(), ExpiresAt: now.Add(lifetime).UTC(), } - if transition.Policy.GoalScope == catalog.GoalScopeOptionalPreserve { - a.GoalStatus = snapshot.Goal.Status + if transition.Policy.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve { + a.ObjectiveStatus = snapshot.Objective.Status } if snapshot.RecordedProgramFingerprint != "" && snapshot.RecordedProgramFingerprint != snapshot.ProgramFingerprint { a.PriorProgramFingerprint = snapshot.RecordedProgramFingerprint @@ -85,9 +87,9 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T Transition catalog.TransitionID `json:"transition"` Snapshot string `json:"snapshot"` Invocation model.InvocationContext `json:"invocation"` - Goal model.Goal `json:"goal"` + Objective model.Objective `json:"objective"` Parameters Parameters `json:"parameters"` - }{transition.ID, snapshot.Fingerprint, snapshot.Invocation, goal, parameters.Canonical()}) + }{transition.ID, snapshot.Fingerprint, snapshot.Invocation, objective, parameters.Canonical()}) if err != nil { return Admission{}, err } @@ -101,32 +103,32 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T return a, nil } -// GoalForTransition binds maintenance to verified durable product-goal state. +// ObjectiveForTransition binds maintenance to verified durable objective state. // Command-scoped product intent is deliberately irrelevant to maintenance. -func GoalForTransition(snapshot model.Snapshot, requested model.Goal, transition catalog.Transition) (model.Goal, error) { - if transition.Policy.GoalScope != catalog.GoalScopeOptionalPreserve { +func ObjectiveForTransition(snapshot model.Snapshot, requested model.Objective, transition catalog.Transition) (model.Objective, error) { + if transition.Policy.ObjectiveScope != catalog.ObjectiveScopeOptionalPreserve { if err := requested.Validate(); err != nil { - return model.Goal{}, err + return model.Objective{}, err } return requested, nil } - switch snapshot.Goal.Status { + switch snapshot.Objective.Status { case model.FactKnown: - if err := snapshot.Goal.Value.Validate(); err != nil { - return model.Goal{}, fmt.Errorf("transition %q has invalid configured product-goal evidence: %w", transition.ID, err) + if err := snapshot.Objective.Value.Validate(); err != nil { + return model.Objective{}, fmt.Errorf("transition %q has invalid configured objective evidence: %w", transition.ID, err) } - return snapshot.Goal.Value, nil + return snapshot.Objective.Value, nil case model.FactAbsent: - return model.Goal{}, nil + return model.Objective{}, nil default: - return model.Goal{}, fmt.Errorf("transition %q requires known or verified-absent product goal evidence", transition.ID) + return model.Objective{}, fmt.Errorf("transition %q requires known or verified-absent product objective evidence", transition.ID) } } // ValidateApplicability is the deterministic transition law shared by // resolution and admission. A transition that fails here must never be // reported as prescribed for the same snapshot and context. -func ValidateApplicability(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, authority AuthorityBundle, parameters Parameters, now time.Time) error { +func ValidateApplicability(snapshot model.Snapshot, objective model.Objective, transition catalog.Transition, authority AuthorityBundle, parameters Parameters, now time.Time) error { if !transition.Controllable() { return fmt.Errorf("transition %q is uncontrollable and cannot be admitted", transition.ID) } @@ -134,15 +136,15 @@ func ValidateApplicability(snapshot model.Snapshot, goal model.Goal, transition return err } var err error - goal, err = GoalForTransition(snapshot, goal, transition) + objective, err = ObjectiveForTransition(snapshot, objective, transition) if err != nil { return err } - if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) { + if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsObjective(objective) { return fmt.Errorf("transition %q is not admissible from snapshot %q", transition.ID, snapshot.Fingerprint) } - if snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal && !transition.Policy.BindsRequestedGoal { - return fmt.Errorf("transition %q cannot replace configured goal; goal.configure is required", transition.ID) + if snapshot.Objective.Status == model.FactKnown && snapshot.Objective.Value != objective && !transition.Policy.BindsRequestedObjective { + return fmt.Errorf("transition %q cannot replace configured objective; objective.bind is required", transition.ID) } if err := authority.Validate(now); err != nil { return err @@ -175,7 +177,7 @@ func ValidateApplicability(snapshot model.Snapshot, goal model.Goal, transition return nil } -func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, now time.Time) error { +func (a Admission) ValidateCurrent(snapshot model.Snapshot, objective model.Objective, transition catalog.Transition, now time.Time) error { if err := a.ValidateIdentity(); err != nil { return err } @@ -185,8 +187,8 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra if a.TransitionID != transition.ID || a.TransitionVersion != transition.Version { return fmt.Errorf("admission %q is bound to a different transition", a.ID) } - if a.GoalScope != transition.Policy.GoalScope { - return fmt.Errorf("admission %q is bound to a different product-goal scope", a.ID) + if a.ObjectiveScope != transition.Policy.ObjectiveScope { + return fmt.Errorf("admission %q is bound to a different objective scope", a.ID) } if a.ExpectedStateRevision != snapshot.StateRevision { return fmt.Errorf("admission %q is stale: state revision changed", a.ID) @@ -194,6 +196,13 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra if a.ExpectedSnapshotFingerprint != snapshot.Fingerprint { return fmt.Errorf("admission %q is stale: snapshot changed", a.ID) } + objectiveBindingFingerprint, err := ObjectiveBindingFingerprint(snapshot) + if err != nil { + return err + } + if a.ExpectedObjectiveBindingFingerprint != objectiveBindingFingerprint { + return fmt.Errorf("admission %q is stale: objective binding changed", a.ID) + } if a.ExpectedProgramFingerprint != snapshot.ProgramFingerprint { return fmt.Errorf("admission %q is bound to a different control program", a.ID) } @@ -216,11 +225,11 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra if a.Invocation != snapshot.Invocation { return fmt.Errorf("admission %q is bound to a different invocation", a.ID) } - if a.Goal != goal { - return fmt.Errorf("admission %q is bound to a different goal", a.ID) + if a.Objective != objective { + return fmt.Errorf("admission %q is bound to a different objective", a.ID) } - if a.GoalScope == catalog.GoalScopeOptionalPreserve && (a.GoalStatus != snapshot.Goal.Status || (a.GoalStatus == model.FactKnown && a.Goal != snapshot.Goal.Value)) { - return fmt.Errorf("admission %q is stale: product goal changed", a.ID) + if a.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve && (a.ObjectiveStatus != snapshot.Objective.Status || (a.ObjectiveStatus == model.FactKnown && a.Objective != snapshot.Objective.Value)) { + return fmt.Errorf("admission %q is stale: product objective changed", a.ID) } if err := a.Authority.Validate(now); err != nil { return err @@ -364,7 +373,7 @@ func validateAuthorityEvidence(snapshot model.Snapshot, authority AuthorityBundl } func (a Admission) ValidateIdentity() error { - if a.SchemaVersion != AdmissionSchemaVersion || a.ID == "" || a.PrescriptionID == "" || a.TransitionID == "" || a.TransitionVersion < 1 || a.ExpectedStateRevision == 0 || len(a.ExpectedProgramFingerprint) != 64 || len(a.ExpectedSnapshotFingerprint) != 64 || a.AuthorityFingerprint == "" || len(a.RequiredCapabilities) == 0 || len(a.EffectiveCapabilities) == 0 || !a.SourcePhase.Valid() || a.IdempotencyKey == "" || a.IssuedAt.IsZero() || a.ExpiresAt.Before(a.IssuedAt) { + if a.SchemaVersion != AdmissionSchemaVersion || a.ID == "" || a.PrescriptionID == "" || a.TransitionID == "" || a.TransitionVersion < 1 || a.ExpectedStateRevision == 0 || len(a.ExpectedProgramFingerprint) != 64 || len(a.ExpectedSnapshotFingerprint) != 64 || len(a.ExpectedObjectiveBindingFingerprint) != 64 || a.AuthorityFingerprint == "" || len(a.RequiredCapabilities) == 0 || len(a.EffectiveCapabilities) == 0 || !a.SourcePhase.Valid() || a.IdempotencyKey == "" || a.IssuedAt.IsZero() || a.ExpiresAt.Before(a.IssuedAt) { return fmt.Errorf("admission: invalid schema, identity, source, or lifetime") } fingerprint, err := a.Authority.Fingerprint() @@ -404,23 +413,23 @@ func (a Admission) ValidateIdentity() error { if err := a.Invocation.Validate(true); err != nil { return err } - if !a.GoalScope.Valid() { - return fmt.Errorf("admission has invalid product-goal scope %q", a.GoalScope) + if !a.ObjectiveScope.Valid() { + return fmt.Errorf("admission has invalid objective scope %q", a.ObjectiveScope) } - if a.GoalScope == catalog.GoalScopeOptionalPreserve { - switch a.GoalStatus { + if a.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve { + switch a.ObjectiveStatus { case model.FactKnown: - if err := a.Goal.Validate(); err != nil { + if err := a.Objective.Validate(); err != nil { return err } case model.FactAbsent: - if a.Goal.Validate() == nil { + if a.Objective.Validate() == nil { return fmt.Errorf("maintenance admission cannot bind product intent to verified absence") } default: - return fmt.Errorf("maintenance admission requires known or verified-absent product goal status") + return fmt.Errorf("maintenance admission requires known or verified-absent product objective status") } - } else if err := a.Goal.Validate(); err != nil { + } else if err := a.Objective.Validate(); err != nil { return err } identity := a diff --git a/boatstack/internal/kernel/protocol/authority.go b/boatstack/internal/softwaredelivery/protocol/authority.go similarity index 95% rename from boatstack/internal/kernel/protocol/authority.go rename to boatstack/internal/softwaredelivery/protocol/authority.go index 522f2c4..ce5201f 100644 --- a/boatstack/internal/kernel/protocol/authority.go +++ b/boatstack/internal/softwaredelivery/protocol/authority.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) type AuthorityReceipt struct { diff --git a/boatstack/internal/kernel/protocol/capability.go b/boatstack/internal/softwaredelivery/protocol/capability.go similarity index 94% rename from boatstack/internal/kernel/protocol/capability.go rename to boatstack/internal/softwaredelivery/protocol/capability.go index 3490dc9..c97f85f 100644 --- a/boatstack/internal/kernel/protocol/capability.go +++ b/boatstack/internal/softwaredelivery/protocol/capability.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) type CapabilityProjection struct { diff --git a/boatstack/internal/kernel/protocol/capability_test.go b/boatstack/internal/softwaredelivery/protocol/capability_test.go similarity index 97% rename from boatstack/internal/kernel/protocol/capability_test.go rename to boatstack/internal/softwaredelivery/protocol/capability_test.go index c392834..18d98fc 100644 --- a/boatstack/internal/kernel/protocol/capability_test.go +++ b/boatstack/internal/softwaredelivery/protocol/capability_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func capabilityAuthority(now time.Time, class catalog.AuthorityClass, id string) AuthorityBundle { diff --git a/boatstack/internal/kernel/protocol/config.go b/boatstack/internal/softwaredelivery/protocol/config.go similarity index 99% rename from boatstack/internal/kernel/protocol/config.go rename to boatstack/internal/softwaredelivery/protocol/config.go index bf38d3b..5b2fb3d 100644 --- a/boatstack/internal/kernel/protocol/config.go +++ b/boatstack/internal/softwaredelivery/protocol/config.go @@ -11,7 +11,7 @@ import ( "regexp" "sort" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) const ConfigSchemaVersion = 2 diff --git a/boatstack/internal/kernel/protocol/config_test.go b/boatstack/internal/softwaredelivery/protocol/config_test.go similarity index 100% rename from boatstack/internal/kernel/protocol/config_test.go rename to boatstack/internal/softwaredelivery/protocol/config_test.go diff --git a/boatstack/internal/kernel/protocol/hash.go b/boatstack/internal/softwaredelivery/protocol/hash.go similarity index 100% rename from boatstack/internal/kernel/protocol/hash.go rename to boatstack/internal/softwaredelivery/protocol/hash.go diff --git a/boatstack/internal/kernel/protocol/journal.go b/boatstack/internal/softwaredelivery/protocol/journal.go similarity index 80% rename from boatstack/internal/kernel/protocol/journal.go rename to boatstack/internal/softwaredelivery/protocol/journal.go index 97a806a..4931b64 100644 --- a/boatstack/internal/kernel/protocol/journal.go +++ b/boatstack/internal/softwaredelivery/protocol/journal.go @@ -2,4 +2,4 @@ package protocol // JournalSchemaVersion identifies the transaction record that embeds an exact // prescription-bound admission. -const JournalSchemaVersion = 6 +const JournalSchemaVersion = 7 diff --git a/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go b/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go new file mode 100644 index 0000000..63e7678 --- /dev/null +++ b/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go @@ -0,0 +1,42 @@ +package protocol + +import ( + "testing" + + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" +) + +func TestMaintenanceObjectiveBindingUsesOnlyDurableProductState(t *testing.T) { + // control-law: maintenance-admission-is-independent-from-command-product-intent + transition := catalog.Transition{ID: "installation.update", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve}} + configured := model.Objective{ID: "configured", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + conflicting := model.Objective{ID: "command", Kind: model.ObjectiveApprovedPlan, DeliveryID: "other"} + + tests := []struct { + name string + fact model.Fact[model.Objective] + request model.Objective + want model.Objective + wantFail bool + }{ + {name: "objective absent", fact: model.Fact[model.Objective]{Status: model.FactAbsent}, request: conflicting}, + {name: "objective known and preserved", fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}, want: configured}, + {name: "conflicting command-scoped objective ignored", fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}, request: conflicting, want: configured}, + {name: "unknown objective fails closed", fact: model.Fact[model.Objective]{Status: model.FactUnknown}, request: conflicting, wantFail: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ObjectiveForTransition(model.Snapshot{Observation: model.Observation{Objective: test.fact}}, test.request, transition) + if test.wantFail { + if err == nil { + t.Fatalf("unknown objective evidence produced %#v", got) + } + return + } + if err != nil || got != test.want { + t.Fatalf("objective binding = %#v, %v; want %#v", got, err, test.want) + } + }) + } +} diff --git a/boatstack/internal/kernel/protocol/parameters.go b/boatstack/internal/softwaredelivery/protocol/parameters.go similarity index 97% rename from boatstack/internal/kernel/protocol/parameters.go rename to boatstack/internal/softwaredelivery/protocol/parameters.go index a523f38..11bbc99 100644 --- a/boatstack/internal/kernel/protocol/parameters.go +++ b/boatstack/internal/softwaredelivery/protocol/parameters.go @@ -6,7 +6,7 @@ import ( "path/filepath" "sort" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" ) type Parameter struct { diff --git a/boatstack/internal/kernel/protocol/parameters_test.go b/boatstack/internal/softwaredelivery/protocol/parameters_test.go similarity index 100% rename from boatstack/internal/kernel/protocol/parameters_test.go rename to boatstack/internal/softwaredelivery/protocol/parameters_test.go diff --git a/boatstack/internal/kernel/protocol/policy_test.go b/boatstack/internal/softwaredelivery/protocol/policy_test.go similarity index 95% rename from boatstack/internal/kernel/protocol/policy_test.go rename to boatstack/internal/softwaredelivery/protocol/policy_test.go index 7abaa0b..3160007 100644 --- a/boatstack/internal/kernel/protocol/policy_test.go +++ b/boatstack/internal/softwaredelivery/protocol/policy_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" ) diff --git a/boatstack/internal/softwaredelivery/protocol/prescription.go b/boatstack/internal/softwaredelivery/protocol/prescription.go new file mode 100644 index 0000000..4967b62 --- /dev/null +++ b/boatstack/internal/softwaredelivery/protocol/prescription.go @@ -0,0 +1,111 @@ +package protocol + +import ( + "fmt" + + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + general "github.com/operatorstack/boatstack/boatstack/kernel" +) + +const PrescriptionSchemaVersion = 4 + +// Prescription is the immutable compare-and-swap binding emitted by +// resolution and required by apply. It carries no reusable authority or +// credential material, only the content identity of the admitted projection. +type Prescription struct { + SchemaVersion int `json:"schema_version"` + ID string `json:"id"` + TransitionID catalog.TransitionID `json:"transition_id"` + general.Freshness + RequiredCapabilities []catalog.Capability `json:"required_capabilities"` + EffectiveCapabilities []catalog.Capability `json:"effective_capabilities"` +} + +func NewPrescription(snapshot model.Snapshot, transition catalog.Transition, capabilities CapabilityProjection) (Prescription, error) { + objectiveBindingFingerprint, err := ObjectiveBindingFingerprint(snapshot) + if err != nil { + return Prescription{}, err + } + freshness, err := general.NewFreshness(snapshot.Invocation.RepositoryID, snapshot.StateRevision, snapshot.ProgramFingerprint, snapshot.Fingerprint, objectiveBindingFingerprint, capabilities.AuthorityFingerprint) + if err != nil { + return Prescription{}, err + } + prescription := Prescription{ + SchemaVersion: PrescriptionSchemaVersion, + TransitionID: transition.ID, + Freshness: freshness, + RequiredCapabilities: append([]catalog.Capability(nil), capabilities.Required...), + EffectiveCapabilities: append([]catalog.Capability(nil), capabilities.Effective...), + } + if err := prescription.validateFields(); err != nil { + return Prescription{}, err + } + identity := prescription + identity.ID = "" + prescription.ID, err = contentID("prx-", identity) + if err != nil { + return Prescription{}, err + } + return prescription, nil +} + +func (p Prescription) Validate() error { + if err := p.validateFields(); err != nil { + return err + } + identity := p + want := identity.ID + identity.ID = "" + got, err := contentID("prx-", identity) + if err != nil { + return err + } + if want == "" || got != want { + return fmt.Errorf("prescription failed content identity verification") + } + return nil +} + +func (p Prescription) validateFields() error { + if p.SchemaVersion != PrescriptionSchemaVersion || p.TransitionID == "" || p.Freshness.Validate() != nil || + len(p.RequiredCapabilities) == 0 || len(p.EffectiveCapabilities) == 0 { + return fmt.Errorf("prescription has invalid schema, transition, state revision, program, or snapshot identity") + } + return nil +} + +func (p Prescription) ValidateCurrent(snapshot model.Snapshot, transition catalog.Transition, capabilities CapabilityProjection) error { + if err := p.Validate(); err != nil { + return err + } + if p.TransitionID != transition.ID { + return fmt.Errorf("prescription %q is bound to transition %q, not %q", p.ID, p.TransitionID, transition.ID) + } + objectiveBindingFingerprint, err := ObjectiveBindingFingerprint(snapshot) + if err != nil { + return err + } + current, err := general.NewFreshness(snapshot.Invocation.RepositoryID, snapshot.StateRevision, snapshot.ProgramFingerprint, snapshot.Fingerprint, objectiveBindingFingerprint, capabilities.AuthorityFingerprint) + if err != nil { + return err + } + if err := p.Freshness.Check(current); err != nil { + return fmt.Errorf("prescription %q is stale: %w", p.ID, err) + } + if p.AuthorityFingerprint != capabilities.AuthorityFingerprint || + !sameCapabilities(p.RequiredCapabilities, capabilities.Required) || + !sameCapabilities(p.EffectiveCapabilities, capabilities.Effective) { + return fmt.Errorf("prescription %q is bound to a different authority or capability context", p.ID) + } + return nil +} + +// ObjectiveBindingFingerprint projects only the durable binding status and +// value. Observation evidence may be refreshed without changing the binding. +func ObjectiveBindingFingerprint(snapshot model.Snapshot) (string, error) { + return general.Fingerprint(struct { + Status model.FactStatus `json:"status"` + Objective model.Objective `json:"objective"` + }{snapshot.Objective.Status, snapshot.Objective.Value}) +} diff --git a/boatstack/internal/kernel/protocol/prescription_test.go b/boatstack/internal/softwaredelivery/protocol/prescription_test.go similarity index 67% rename from boatstack/internal/kernel/protocol/prescription_test.go rename to boatstack/internal/softwaredelivery/protocol/prescription_test.go index 47139c6..8e91ce7 100644 --- a/boatstack/internal/kernel/protocol/prescription_test.go +++ b/boatstack/internal/softwaredelivery/protocol/prescription_test.go @@ -4,16 +4,23 @@ import ( "strings" "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func TestPrescriptionContentIdentityBindsTransitionStateProgramAndSnapshot(t *testing.T) { // control-law: resolution emits one immutable state-program CAS identity base := model.Snapshot{ - Observation: model.Observation{StateRevision: 41, ProgramFingerprint: strings.Repeat("a", 64)}, + Observation: model.Observation{Invocation: model.InvocationContext{RepositoryID: "repo-fixture"}, StateRevision: 41, ProgramFingerprint: strings.Repeat("a", 64)}, Fingerprint: strings.Repeat("b", 64), } + snapshot := func(revision uint64, program, fingerprint string) model.Snapshot { + value := base + value.StateRevision = revision + value.ProgramFingerprint = program + value.Fingerprint = fingerprint + return value + } transition := catalog.Transition{ID: "program/advance"} capabilities := CapabilityProjection{AuthorityFingerprint: "auth-test", Required: []catalog.Capability{catalog.CapabilityRepositoryWrite}, Effective: []catalog.Capability{catalog.CapabilityRepositoryWrite}} one, err := NewPrescription(base, transition, capabilities) @@ -28,9 +35,10 @@ func TestPrescriptionContentIdentityBindsTransitionStateProgramAndSnapshot(t *te snapshot model.Snapshot transition catalog.Transition }{ - {name: "state", snapshot: model.Snapshot{Observation: model.Observation{StateRevision: 42, ProgramFingerprint: strings.Repeat("a", 64)}, Fingerprint: strings.Repeat("b", 64)}, transition: transition}, - {name: "program", snapshot: model.Snapshot{Observation: model.Observation{StateRevision: 41, ProgramFingerprint: strings.Repeat("c", 64)}, Fingerprint: strings.Repeat("b", 64)}, transition: transition}, - {name: "snapshot", snapshot: model.Snapshot{Observation: model.Observation{StateRevision: 41, ProgramFingerprint: strings.Repeat("a", 64)}, Fingerprint: strings.Repeat("d", 64)}, transition: transition}, + {name: "instance", snapshot: func() model.Snapshot { value := base; value.Invocation.RepositoryID = "repo-other"; return value }(), transition: transition}, + {name: "state", snapshot: snapshot(42, strings.Repeat("a", 64), strings.Repeat("b", 64)), transition: transition}, + {name: "program", snapshot: snapshot(41, strings.Repeat("c", 64), strings.Repeat("b", 64)), transition: transition}, + {name: "snapshot", snapshot: snapshot(41, strings.Repeat("a", 64), strings.Repeat("d", 64)), transition: transition}, {name: "transition", snapshot: base, transition: catalog.Transition{ID: "program/other"}}, } for _, mutation := range mutations { diff --git a/boatstack/internal/kernel/protocol/program_change.go b/boatstack/internal/softwaredelivery/protocol/program_change.go similarity index 100% rename from boatstack/internal/kernel/protocol/program_change.go rename to boatstack/internal/softwaredelivery/protocol/program_change.go diff --git a/boatstack/internal/kernel/protocol/program_change_test.go b/boatstack/internal/softwaredelivery/protocol/program_change_test.go similarity index 100% rename from boatstack/internal/kernel/protocol/program_change_test.go rename to boatstack/internal/softwaredelivery/protocol/program_change_test.go diff --git a/boatstack/internal/kernel/protocol/receipt.go b/boatstack/internal/softwaredelivery/protocol/receipt.go similarity index 74% rename from boatstack/internal/kernel/protocol/receipt.go rename to boatstack/internal/softwaredelivery/protocol/receipt.go index 9bc76fd..7789f74 100644 --- a/boatstack/internal/kernel/protocol/receipt.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt.go @@ -8,11 +8,11 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) -const ReceiptSchemaVersion = 7 +const ReceiptSchemaVersion = 8 type TransitionFactKind string @@ -90,45 +90,46 @@ func (v VerificationFact) Validate() error { // TransitionReceipt is the immutable fact for one committed transition. It is // not a request, prescription, admission, refusal, or recovery authorization. type TransitionReceipt struct { - SchemaVersion int `json:"schema_version"` - Kind TransitionFactKind `json:"kind"` - ID string `json:"id"` - FlowID string `json:"flow_id"` - Sequence uint64 `json:"sequence"` - Program ProgramIdentity `json:"program"` - TransitionID catalog.TransitionID `json:"transition_id"` - TransitionVersion int `json:"transition_version"` - PriorProgramFingerprint string `json:"prior_program_fingerprint,omitempty"` - ProgramDeltaFingerprint string `json:"program_delta_fingerprint,omitempty"` - ProgramChangeAccepted bool `json:"program_change_accepted,omitempty"` - RuntimeVersion string `json:"runtime_version,omitempty"` - RuntimeFingerprint string `json:"runtime_fingerprint,omitempty"` - RuntimeSourceRevision string `json:"runtime_source_revision,omitempty"` - PrescriptionID string `json:"prescription_id"` - AdmissionID string `json:"admission_id"` - PriorStateRevision uint64 `json:"prior_state_revision"` - ResultingStateRevision uint64 `json:"resulting_state_revision"` - GoalID string `json:"goal_id"` - GoalKind model.GoalKind `json:"goal_kind"` - DeliveryID string `json:"delivery_id"` - GoalScope catalog.GoalScope `json:"goal_scope,omitempty"` - GoalStatus model.FactStatus `json:"goal_status,omitempty"` - SourceFingerprint string `json:"source_fingerprint"` - TargetFingerprint string `json:"target_fingerprint"` - AuthorityFingerprint string `json:"authority_fingerprint"` - AuthoritySources []AuthoritySource `json:"authority_sources"` - RequiredCapabilities []catalog.Capability `json:"required_capabilities"` - GrantedCapabilities []catalog.Capability `json:"granted_capabilities"` - ExercisedCapabilities []catalog.Capability `json:"exercised_capabilities,omitempty"` - CommittedEffects []EffectFact `json:"committed_effects"` - ChangedStateFacets []model.StateFacet `json:"changed_state_facets"` - Verification VerificationFact `json:"verification"` - IdempotencyKey string `json:"idempotency_key"` - Recovery catalog.TransitionID `json:"recovery,omitempty"` - Terminal model.TerminalStatus `json:"terminal"` - StartedAt time.Time `json:"started_at"` - CommittedAt time.Time `json:"committed_at"` - DurationNanoseconds int64 `json:"duration_nanoseconds"` + SchemaVersion int `json:"schema_version"` + Kind TransitionFactKind `json:"kind"` + ID string `json:"id"` + FlowID string `json:"flow_id"` + Sequence uint64 `json:"sequence"` + Program ProgramIdentity `json:"program"` + TransitionID catalog.TransitionID `json:"transition_id"` + TransitionVersion int `json:"transition_version"` + PriorProgramFingerprint string `json:"prior_program_fingerprint,omitempty"` + ProgramDeltaFingerprint string `json:"program_delta_fingerprint,omitempty"` + ProgramChangeAccepted bool `json:"program_change_accepted,omitempty"` + RuntimeVersion string `json:"runtime_version,omitempty"` + RuntimeFingerprint string `json:"runtime_fingerprint,omitempty"` + RuntimeSourceRevision string `json:"runtime_source_revision,omitempty"` + PrescriptionID string `json:"prescription_id"` + AdmissionID string `json:"admission_id"` + PriorStateRevision uint64 `json:"prior_state_revision"` + ResultingStateRevision uint64 `json:"resulting_state_revision"` + ObjectiveID string `json:"objective_id"` + ObjectiveKind model.ObjectiveKind `json:"objective_kind"` + DeliveryID string `json:"delivery_id"` + ObjectiveScope catalog.ObjectiveScope `json:"objective_scope,omitempty"` + ObjectiveStatus model.FactStatus `json:"objective_status,omitempty"` + ObjectiveBindingFingerprint string `json:"objective_binding_fingerprint"` + SourceFingerprint string `json:"source_fingerprint"` + TargetFingerprint string `json:"target_fingerprint"` + AuthorityFingerprint string `json:"authority_fingerprint"` + AuthoritySources []AuthoritySource `json:"authority_sources"` + RequiredCapabilities []catalog.Capability `json:"required_capabilities"` + GrantedCapabilities []catalog.Capability `json:"granted_capabilities"` + ExercisedCapabilities []catalog.Capability `json:"exercised_capabilities,omitempty"` + CommittedEffects []EffectFact `json:"committed_effects"` + ChangedStateFacets []model.StateFacet `json:"changed_state_facets"` + Verification VerificationFact `json:"verification"` + IdempotencyKey string `json:"idempotency_key"` + Recovery catalog.TransitionID `json:"recovery,omitempty"` + Terminal model.TerminalStatus `json:"terminal"` + StartedAt time.Time `json:"started_at"` + CommittedAt time.Time `json:"committed_at"` + DurationNanoseconds int64 `json:"duration_nanoseconds"` } type AuthoritySource struct { @@ -154,6 +155,10 @@ func NewReceipt(flowID string, sequence uint64, program ProgramIdentity, admissi if admission.ExpectedStateRevision == ^uint64(0) || target.StateRevision != admission.ExpectedStateRevision+1 { return TransitionReceipt{}, fmt.Errorf("receipt target revision must advance exactly once from the prescribed revision") } + resultingObjectiveBindingFingerprint, err := ObjectiveBindingFingerprint(target) + if err != nil { + return TransitionReceipt{}, fmt.Errorf("receipt target objective binding: %w", err) + } if len(effects) == 0 { return TransitionReceipt{}, fmt.Errorf("committed transition requires kernel-observed effect facts") } @@ -176,9 +181,10 @@ func NewReceipt(flowID string, sequence uint64, program ProgramIdentity, admissi TransitionID: transition.ID, TransitionVersion: transition.Version, PrescriptionID: admission.PrescriptionID, AdmissionID: admission.ID, PriorStateRevision: admission.ExpectedStateRevision, ResultingStateRevision: target.StateRevision, - GoalID: admission.Goal.ID, GoalKind: admission.Goal.Kind, DeliveryID: admission.Goal.DeliveryID, - GoalScope: admission.GoalScope, GoalStatus: admission.GoalStatus, - SourceFingerprint: admission.ExpectedSnapshotFingerprint, TargetFingerprint: target.Fingerprint, + ObjectiveID: admission.Objective.ID, ObjectiveKind: admission.Objective.Kind, DeliveryID: admission.Objective.DeliveryID, + ObjectiveScope: admission.ObjectiveScope, ObjectiveStatus: admission.ObjectiveStatus, + ObjectiveBindingFingerprint: resultingObjectiveBindingFingerprint, + SourceFingerprint: admission.ExpectedSnapshotFingerprint, TargetFingerprint: target.Fingerprint, AuthorityFingerprint: admission.AuthorityFingerprint, AuthoritySources: sources, RequiredCapabilities: append([]catalog.Capability(nil), admission.RequiredCapabilities...), GrantedCapabilities: append([]catalog.Capability(nil), admission.GrantedCapabilities...), @@ -212,7 +218,7 @@ func NewReceipt(flowID string, sequence uint64, program ProgramIdentity, admissi } func (r TransitionReceipt) Validate() error { - if r.SchemaVersion != ReceiptSchemaVersion || r.Kind != TransitionCommitted || r.ID == "" || r.FlowID == "" || r.Sequence == 0 || r.TransitionID == "" || r.TransitionVersion < 1 || r.PrescriptionID == "" || r.AdmissionID == "" || r.PriorStateRevision == 0 || r.PriorStateRevision == ^uint64(0) || r.ResultingStateRevision != r.PriorStateRevision+1 || !validSHA256(r.SourceFingerprint) || !validSHA256(r.TargetFingerprint) || r.AuthorityFingerprint == "" || len(r.RequiredCapabilities) == 0 || r.IdempotencyKey == "" || len(r.CommittedEffects) == 0 || len(r.ChangedStateFacets) == 0 { + if r.SchemaVersion != ReceiptSchemaVersion || r.Kind != TransitionCommitted || r.ID == "" || r.FlowID == "" || r.Sequence == 0 || r.TransitionID == "" || r.TransitionVersion < 1 || r.PrescriptionID == "" || r.AdmissionID == "" || r.PriorStateRevision == 0 || r.PriorStateRevision == ^uint64(0) || r.ResultingStateRevision != r.PriorStateRevision+1 || !validSHA256(r.SourceFingerprint) || !validSHA256(r.TargetFingerprint) || !validSHA256(r.ObjectiveBindingFingerprint) || r.AuthorityFingerprint == "" || len(r.RequiredCapabilities) == 0 || r.IdempotencyKey == "" || len(r.CommittedEffects) == 0 || len(r.ChangedStateFacets) == 0 { return fmt.Errorf("receipt has incomplete committed-transition identity or evidence") } canonicalFacets, err := model.NormalizeStateFacets("receipt.changed_state_facets", r.ChangedStateFacets) @@ -277,24 +283,24 @@ func (r TransitionReceipt) Validate() error { if !sameCapabilities(r.GrantedCapabilities, catalog.AuthorityCapabilities(authoritySet).Sorted()) { return fmt.Errorf("receipt granted capabilities do not match authority provenance") } - if !r.GoalScope.Valid() { - return fmt.Errorf("receipt has invalid product-goal scope %q", r.GoalScope) + if !r.ObjectiveScope.Valid() { + return fmt.Errorf("receipt has invalid objective scope %q", r.ObjectiveScope) } - if r.GoalScope == catalog.GoalScopeOptionalPreserve { - switch r.GoalStatus { + if r.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve { + switch r.ObjectiveStatus { case model.FactKnown: - if r.GoalID == "" || !r.GoalKind.Valid() || r.DeliveryID == "" { - return fmt.Errorf("maintenance receipt has incomplete known product-goal binding") + if r.ObjectiveID == "" || !r.ObjectiveKind.Valid() || r.DeliveryID == "" { + return fmt.Errorf("maintenance receipt has incomplete known objective binding") } case model.FactAbsent: - if r.GoalID != "" || r.GoalKind != "" || r.DeliveryID != "" { + if r.ObjectiveID != "" || r.ObjectiveKind != "" || r.DeliveryID != "" { return fmt.Errorf("maintenance receipt invents product intent from verified absence") } default: - return fmt.Errorf("maintenance receipt requires known or verified-absent product-goal status") + return fmt.Errorf("maintenance receipt requires known or verified-absent objective status") } - } else if r.GoalID == "" || !r.GoalKind.Valid() || r.DeliveryID == "" { - return fmt.Errorf("receipt has incomplete product-goal identity") + } else if r.ObjectiveID == "" || !r.ObjectiveKind.Valid() || r.DeliveryID == "" { + return fmt.Errorf("receipt has incomplete objective identity") } if r.StartedAt.IsZero() || r.CommittedAt.Before(r.StartedAt) || r.DurationNanoseconds != r.CommittedAt.Sub(r.StartedAt).Nanoseconds() || r.Verification.VerifiedAt.After(r.CommittedAt) { return fmt.Errorf("receipt has invalid timing evidence") diff --git a/boatstack/internal/kernel/protocol/receipt_capability_test.go b/boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go similarity index 75% rename from boatstack/internal/kernel/protocol/receipt_capability_test.go rename to boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go index 7525e18..f399f31 100644 --- a/boatstack/internal/kernel/protocol/receipt_capability_test.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func TestReceiptRejectsRehashedAuthorityProvenanceTampering(t *testing.T) { @@ -19,14 +19,16 @@ func TestReceiptRejectsRehashedAuthorityProvenanceTampering(t *testing.T) { admission := Admission{ ID: "admission", PrescriptionID: "prescription", ExpectedStateRevision: 1, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), - Goal: model.Goal{ID: "goal", Kind: model.GoalApprovedPlan, DeliveryID: "delivery"}, - Authority: authority, AuthorityFingerprint: authorityFingerprint, + ExpectedObjectiveBindingFingerprint: strings.Repeat("d", 64), + Objective: model.Objective{ID: "objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "delivery"}, + ObjectiveScope: catalog.ObjectiveScopeBoundExact, + Authority: authority, AuthorityFingerprint: authorityFingerprint, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, GrantedCapabilities: authority.GrantedCapabilities(now), EffectiveCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, IdempotencyKey: "idempotency", } - transition := catalog.Transition{ID: "program/write", Version: 1, Owner: "program", Effect: "program.write", TargetPredicate: "program.written", Verifier: "program.written"} + transition := catalog.Transition{ID: "program/write", Version: 1, Owner: "program", Effect: "program.write", TargetPredicate: "program.written", Verifier: "program.written", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}} effects := []EffectFact{{Kind: EffectResourceMutation, EffectID: transition.Effect, Owner: transition.Owner, Resource: "program.state", Target: "/state", Operation: "update", PriorFingerprint: strings.Repeat("1", 64), ResultingFingerprint: strings.Repeat("2", 64)}} receipt, err := NewReceipt("flow", 1, ProgramIdentity{ID: "program", Version: "1.0.0", Fingerprint: admission.ExpectedProgramFingerprint}, admission, transition, model.Snapshot{Observation: model.Observation{StateRevision: 2}, Fingerprint: strings.Repeat("c", 64)}, []model.StateFacet{model.StateFacetControl}, effects, nil, now, now.Add(time.Second)) if err != nil { diff --git a/boatstack/internal/kernel/protocol/receipt_fact_test.go b/boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go similarity index 83% rename from boatstack/internal/kernel/protocol/receipt_fact_test.go rename to boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go index 9e14258..79fa85d 100644 --- a/boatstack/internal/kernel/protocol/receipt_fact_test.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func committedReceiptFixture(t *testing.T) (TransitionReceipt, Admission, catalog.Transition, model.Snapshot, time.Time) { @@ -21,14 +21,17 @@ func committedReceiptFixture(t *testing.T) (TransitionReceipt, Admission, catalo admission := Admission{ ID: "adm-fixture", PrescriptionID: "prx-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), - Goal: model.Goal{ID: "goal", Kind: model.GoalApprovedPlan, DeliveryID: "delivery"}, - Authority: authority, AuthorityFingerprint: authorityFingerprint, + ExpectedObjectiveBindingFingerprint: strings.Repeat("d", 64), + Objective: model.Objective{ID: "objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "delivery"}, + ObjectiveScope: catalog.ObjectiveScopeBoundExact, + Authority: authority, AuthorityFingerprint: authorityFingerprint, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, GrantedCapabilities: authority.GrantedCapabilities(now), EffectiveCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, IdempotencyKey: "idem-fixture", } transition := catalog.Transition{ ID: "product-delivery/build.begin", Version: 3, Owner: "product-delivery", Effect: "build.begin", TargetPredicate: "build is active", Verifier: "product-delivery.build-active", + Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, } target := model.Snapshot{Observation: model.Observation{StateRevision: 42}, Fingerprint: strings.Repeat("c", 64)} effects := []EffectFact{ @@ -81,6 +84,25 @@ func TestCommittedTransitionFactBindsProgramTransitionStateAuthorityEffectsAndVe } } +func TestObjectiveBindReceiptRecordsResultingObjectiveBinding(t *testing.T) { + _, admission, transition, target, now := committedReceiptFixture(t) + transition.ID = "objective.bind" + transition.Policy.BindsRequestedObjective = true + target.Objective = model.Fact[model.Objective]{Status: model.FactKnown, Value: admission.Objective} + want, err := ObjectiveBindingFingerprint(target) + if err != nil { + t.Fatal(err) + } + effects := []EffectFact{{Kind: EffectResourceMutation, EffectID: transition.Effect, Owner: transition.Owner, Resource: "objective", Target: "/state", Operation: "update", PriorFingerprint: strings.Repeat("1", 64), ResultingFingerprint: strings.Repeat("2", 64)}} + receipt, err := NewReceipt("flow", 8, ProgramIdentity{ID: "product-delivery", Version: "2.1.0", Fingerprint: admission.ExpectedProgramFingerprint}, admission, transition, target, []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}, effects, nil, now, now.Add(time.Second)) + if err != nil { + t.Fatal(err) + } + if receipt.ObjectiveBindingFingerprint != want || receipt.ObjectiveBindingFingerprint == admission.ExpectedObjectiveBindingFingerprint { + t.Fatalf("objective binding fingerprint = %q, want resulting %q", receipt.ObjectiveBindingFingerprint, want) + } +} + func TestCommittedTransitionFactRejectsNonSuccessSemantics(t *testing.T) { receipt, _, _, _, _ := committedReceiptFixture(t) receipt.Kind = "transition-refused" diff --git a/boatstack/internal/kernel/protocol/values.go b/boatstack/internal/softwaredelivery/protocol/values.go similarity index 100% rename from boatstack/internal/kernel/protocol/values.go rename to boatstack/internal/softwaredelivery/protocol/values.go diff --git a/boatstack/internal/kernel/supervisor/classify.go b/boatstack/internal/softwaredelivery/supervisor/classify.go similarity index 100% rename from boatstack/internal/kernel/supervisor/classify.go rename to boatstack/internal/softwaredelivery/supervisor/classify.go diff --git a/boatstack/internal/kernel/supervisor/guard.go b/boatstack/internal/softwaredelivery/supervisor/guard.go similarity index 94% rename from boatstack/internal/kernel/supervisor/guard.go rename to boatstack/internal/softwaredelivery/supervisor/guard.go index 6f99ac2..e1c5101 100644 --- a/boatstack/internal/kernel/supervisor/guard.go +++ b/boatstack/internal/softwaredelivery/supervisor/guard.go @@ -3,8 +3,8 @@ package supervisor import ( "fmt" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) type IntentClass string diff --git a/boatstack/internal/kernel/supervisor/guard_test.go b/boatstack/internal/softwaredelivery/supervisor/guard_test.go similarity index 89% rename from boatstack/internal/kernel/supervisor/guard_test.go rename to boatstack/internal/softwaredelivery/supervisor/guard_test.go index 928fc7b..15b7fb8 100644 --- a/boatstack/internal/kernel/supervisor/guard_test.go +++ b/boatstack/internal/softwaredelivery/supervisor/guard_test.go @@ -3,8 +3,8 @@ package supervisor import ( "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func TestManagedCommandRoutingComesOnlyFromCompiledProgram(t *testing.T) { @@ -22,7 +22,7 @@ func TestManagedCommandRoutingComesOnlyFromCompiledProgram(t *testing.T) { Engagement: model.Fact[model.EngagementState]{Status: model.FactKnown, Value: model.EngagementActive}, }, } - supervisor := New(registry, catalog.GoalContracts{}) + supervisor := New(registry, catalog.ObjectiveContracts{}) managed := supervisor.Guard(active, CommandIntent{Class: IntentManagedBypass, Operation: "artifact.publish", Fingerprint: "command"}) if managed.Allowed || managed.RequiredTransition != "synthetic.publish" || managed.Intent.Transition != "synthetic.publish" { t.Fatalf("compiled managed operation was not routed through admission: %#v", managed) @@ -34,7 +34,7 @@ func TestManagedCommandRoutingComesOnlyFromCompiledProgram(t *testing.T) { } func syntheticManagedTransition(id catalog.TransitionID, class catalog.EventClass, selection catalog.SelectionClass, recovery catalog.TransitionID) catalog.Transition { - policy := catalog.PolicyContract{} + policy := catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact} if id == "synthetic.publish" { policy.ManagedOperations = []string{"artifact.publish"} } @@ -43,7 +43,7 @@ func syntheticManagedTransition(id catalog.TransitionID, class catalog.EventClas Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: "manifest"}, Owner: "test.synthetic", SelectionClass: selection, Class: class, SourcePhases: []model.ProtocolPhase{model.PhaseActive}, TargetPhases: []model.ProtocolPhase{model.PhaseActive}, - GoalKinds: []model.GoalKind{model.GoalVerified}, RequiredIdentity: []string{"repository-id"}, + ObjectiveKinds: []model.ObjectiveKind{model.ObjectiveVerified}, RequiredIdentity: []string{"repository-id"}, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, DeclaredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"test.synthetic.state"}, Effect: catalog.EffectID(id), LocalEffects: []catalog.EffectID{catalog.EffectID(id)}, Idempotent: true, diff --git a/boatstack/internal/kernel/supervisor/supervisor.go b/boatstack/internal/softwaredelivery/supervisor/supervisor.go similarity index 54% rename from boatstack/internal/kernel/supervisor/supervisor.go rename to boatstack/internal/softwaredelivery/supervisor/supervisor.go index 872e4d7..cc9b8f6 100644 --- a/boatstack/internal/kernel/supervisor/supervisor.go +++ b/boatstack/internal/softwaredelivery/supervisor/supervisor.go @@ -4,8 +4,9 @@ import ( "fmt" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + general "github.com/operatorstack/boatstack/boatstack/kernel" ) type DecisionKind string @@ -31,18 +32,18 @@ type Decision struct { type Supervisor struct { registry catalog.Registry - contracts catalog.GoalContracts + contracts catalog.ObjectiveContracts } -func New(registry catalog.Registry, contracts catalog.GoalContracts) Supervisor { +func New(registry catalog.Registry, contracts catalog.ObjectiveContracts) Supervisor { return Supervisor{registry: registry, contracts: contracts} } -func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority catalog.AuthoritySet, requested catalog.TransitionID) Decision { +func (s Supervisor) Resolve(snapshot model.Snapshot, objective model.Objective, authority catalog.AuthoritySet, requested catalog.TransitionID) Decision { base := Decision{SnapshotFingerprint: snapshot.Fingerprint} - goalAbsent := snapshot.Goal.Status == model.FactAbsent - if (goal.Validate() != nil && !goalAbsent) || snapshot.Fingerprint == "" { - base.Kind, base.Reason = DecisionUnresolved, "goal or canonical snapshot is invalid" + objectiveAbsent := snapshot.Objective.Status == model.FactAbsent + if (objective.Validate() != nil && !objectiveAbsent) || snapshot.Fingerprint == "" { + base.Kind, base.Reason = DecisionUnresolved, "objective or canonical snapshot is invalid" return base } if snapshot.Terminal.Status != model.FactKnown || snapshot.Phase.Status != model.FactKnown { @@ -64,15 +65,12 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority base.Kind, base.Reason = DecisionRefused, fmt.Sprintf("host %q is not enabled by repository policy", snapshot.Invocation.Host) return base } - if requested == "" && s.contracts.Matches(snapshot, goal) { - base.Kind, base.Reason = DecisionTerminal, "configured terminal is established by current evidence" - return base - } - admissible := s.registry.Admissible(snapshot, goal) - if snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal { + marked := s.contracts.Matches(snapshot, objective) + admissible := s.registry.Admissible(snapshot, objective) + if snapshot.Objective.Status == model.FactKnown && snapshot.Objective.Value != objective { filtered := admissible[:0] for _, candidate := range admissible { - if candidate.Policy.BindsRequestedGoal { + if candidate.Policy.BindsRequestedObjective { filtered = append(filtered, candidate) } } @@ -93,68 +91,103 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority base.Kind, base.Reason = DecisionRefused, fmt.Sprintf("transition %q is not a controllable registry event", requested) return base } - found := false - for _, candidate := range admissible { - if candidate.ID == requested { - transition, found = candidate, true - break - } - } - if !found { - base.Kind, base.Reason = DecisionRefused, fmt.Sprintf("transition %q is not admissible from current snapshot", requested) - return base - } - if allowed, reason := policyAllows(snapshot, transition); !allowed { - base.Kind, base.Reason = DecisionRefused, reason - return base - } - if !authoritySatisfies(snapshot, transition, authority) { - base.Kind, base.Reason = DecisionFrontier, fmt.Sprintf("transition %q requires unavailable authority", requested) - base.Candidates = []catalog.TransitionID{requested} - return base - } - base.Kind, base.Reason, base.Transition = DecisionPrescribed, "requested transition is admissible", &transition - return base } - selectable := make([]catalog.Transition, 0, len(admissible)) - for _, candidate := range admissible { - if !candidate.ImplicitlySelectable() || targetAlreadySatisfied(snapshot, goal, candidate) { + byID := make(map[string]catalog.Transition, len(admissible)) + candidates := make([]general.RelationCandidate, 0, len(admissible)) + for _, transition := range admissible { + if allowed, _ := policyAllows(snapshot, transition); !allowed { continue } - if allowed, _ := policyAllows(snapshot, candidate); allowed { - selectable = append(selectable, candidate) - } + all, any := relationAuthority(snapshot, transition) + id := string(transition.ID) + byID[id] = transition + candidates = append(candidates, general.RelationCandidate{ + ID: id, Rank: transition.SelectionClass.Rank(), Priority: transition.Priority, + Selectable: transition.ImplicitlySelectable() && !targetAlreadySatisfied(snapshot, objective, transition), + RequiredAll: all, RequiredAny: any, + }) } - if len(selectable) == 0 { - if snapshot.Phase.Value == model.PhaseRecovery || snapshot.Phase.Value == model.PhaseUnresolved { - base.Kind, base.Reason = DecisionBlocked, "no registered recovery transition is admissible" - return base + noCandidate := general.Unresolved + if snapshot.Phase.Value == model.PhaseRecovery || snapshot.Phase.Value == model.PhaseUnresolved { + noCandidate = general.Blocked + } + relation := general.Relate(general.RelationInput{ + Requested: string(requested), Marked: marked, NoCandidate: noCandidate, + Candidates: candidates, Available: availableAuthority(authority), + }) + base.Kind = mapDecisionKind(relation.Kind) + base.Reason = relation.Reason + for _, candidate := range relation.Candidates { + base.Candidates = append(base.Candidates, catalog.TransitionID(candidate)) + } + if relation.Kind == general.Prescribed { + transition := byID[relation.Transition] + base.Transition = &transition + } + return base +} + +func mapDecisionKind(kind general.DecisionKind) DecisionKind { + switch kind { + case general.Prescribed: + return DecisionPrescribed + case general.Marked: + return DecisionTerminal + case general.Frontier: + return DecisionFrontier + case general.Blocked: + return DecisionBlocked + case general.Refused: + return DecisionRefused + default: + return DecisionUnresolved + } +} + +func availableAuthority(authority catalog.AuthoritySet) []general.Capability { + result := make([]general.Capability, 0, len(authority)) + for class, present := range authority { + if present && class != catalog.AuthorityNone { + result = append(result, authorityCapability(class)) } - base.Kind, base.Reason = DecisionUnresolved, "no goal-progressing transition is safely selectable from current evidence" - return base } - topClass := selectable[0].SelectionClass - topPriority := selectable[0].Priority - var top []catalog.Transition - for _, candidate := range selectable { - if candidate.SelectionClass == topClass && candidate.Priority == topPriority { - top = append(top, candidate) + return result +} + +func relationAuthority(snapshot model.Snapshot, transition catalog.Transition) (all, any []general.Capability) { + unrestricted := false + for _, class := range transition.Authority { + if class == catalog.AuthorityNone { + unrestricted = true + continue } + any = append(any, authorityCapability(class)) } - if len(top) != 1 { - base.Kind, base.Reason = DecisionFrontier, "several equally preferred transitions remain admissible" - for _, candidate := range top { - base.Candidates = append(base.Candidates, candidate.ID) + if unrestricted { + any = nil + } + for _, class := range transition.AuthorityAll { + if class != catalog.AuthorityNone { + all = append(all, authorityCapability(class)) } - return base } - if !authoritySatisfies(snapshot, top[0], authority) { - base.Kind, base.Reason = DecisionFrontier, "next goal-progressing transition requires unavailable authority" - base.Candidates = []catalog.TransitionID{top[0].ID} - return base + if transition.Policy.AuthorityRule != "" && snapshot.ConfigurationPolicy.Status != model.FactKnown { + all = append(all, general.Capability("authority.verified-configuration-policy")) } - base.Kind, base.Reason, base.Transition = DecisionPrescribed, "deterministic highest-priority transition", &top[0] - return base + if transition.Policy.AuthorityRule == "plan-approval" && snapshot.ConfigurationPolicy.Status == model.FactKnown && snapshot.ConfigurationPolicy.Value.PlanApproval == "human" { + all = append(all, authorityCapability(catalog.AuthorityHuman)) + } + if transition.Policy.AuthorityRule == "independent-high-risk-review" && snapshot.ConfigurationPolicy.Status == model.FactKnown { + policy := snapshot.ConfigurationPolicy.Value + if policy.IndependentReviewForHighRisk && policy.HighRiskChange { + all = append(all, authorityCapability(catalog.AuthorityHuman)) + } + } + return all, any +} + +func authorityCapability(class catalog.AuthorityClass) general.Capability { + return general.Capability("authority." + string(class)) } func permittedProgramDriftRecovery(snapshot model.Snapshot, transition catalog.Transition) bool { @@ -169,12 +202,12 @@ func permittedProgramDriftRecovery(snapshot model.Snapshot, transition catalog.T return false } -func targetAlreadySatisfied(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition) bool { +func targetAlreadySatisfied(snapshot model.Snapshot, objective model.Objective, transition catalog.Transition) bool { if transition.Policy.RechecksExternalState { return false } - if transition.Policy.BindsRequestedGoal { - return snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value == goal + if transition.Policy.BindsRequestedObjective { + return snapshot.Objective.Status == model.FactKnown && snapshot.Objective.Value == objective } if transition.Policy.RequiredWhen == "visual-evidence-required" && (snapshot.ConfigurationPolicy.Status != model.FactKnown || snapshot.ConfigurationPolicy.Value.VisualEvidence != "required") { @@ -207,30 +240,6 @@ func hostEnabled(hosts []string, host string) bool { return false } -func authoritySatisfies(snapshot model.Snapshot, transition catalog.Transition, authority catalog.AuthoritySet) bool { - if !authority.Satisfies(transition.Authority, transition.AuthorityAll) { - return false - } - if transition.Policy.AuthorityRule == "plan-approval" { - if snapshot.ConfigurationPolicy.Status != model.FactKnown { - return false - } - if snapshot.ConfigurationPolicy.Value.PlanApproval == "human" && !authority[catalog.AuthorityHuman] { - return false - } - } - if transition.Policy.AuthorityRule == "independent-high-risk-review" { - if snapshot.ConfigurationPolicy.Status != model.FactKnown { - return false - } - policy := snapshot.ConfigurationPolicy.Value - if policy.IndependentReviewForHighRisk && policy.HighRiskChange && !authority[catalog.AuthorityHuman] { - return false - } - } - return true -} - func policyAllows(snapshot model.Snapshot, transition catalog.Transition) (bool, string) { if transition.Class == catalog.EventRecovery && snapshot.RecoveryInfo.Status == model.FactKnown { permitted := false diff --git a/boatstack/internal/surfaces/artifacts_external_test.go b/boatstack/internal/softwaredelivery/surfaces/artifacts_external_test.go similarity index 92% rename from boatstack/internal/surfaces/artifacts_external_test.go rename to boatstack/internal/softwaredelivery/surfaces/artifacts_external_test.go index 52ac1cf..6736519 100644 --- a/boatstack/internal/surfaces/artifacts_external_test.go +++ b/boatstack/internal/softwaredelivery/surfaces/artifacts_external_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/operatorstack/boatstack/boatstack/distribution" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" ) func TestCheckedArchitectureArtifactsMatchCompiledStandardProgram(t *testing.T) { @@ -37,7 +37,7 @@ func TestCheckedArchitectureArtifactsMatchCompiledStandardProgram(t *testing.T) if !ok { t.Fatal("cannot locate checked architecture artifacts") } - repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "..")) + repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "..", "..")) for name, expected := range checks { actual, err := os.ReadFile(filepath.Join(repositoryRoot, "docs", "architecture", name)) if err != nil { diff --git a/boatstack/internal/surfaces/catalog_render.go b/boatstack/internal/softwaredelivery/surfaces/catalog_render.go similarity index 97% rename from boatstack/internal/surfaces/catalog_render.go rename to boatstack/internal/softwaredelivery/surfaces/catalog_render.go index afce4a7..e0a47db 100644 --- a/boatstack/internal/surfaces/catalog_render.go +++ b/boatstack/internal/softwaredelivery/surfaces/catalog_render.go @@ -5,8 +5,8 @@ import ( "sort" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) // RenderCatalogMarkdown projects the executable registry into a reviewable diff --git a/boatstack/internal/surfaces/guard.go b/boatstack/internal/softwaredelivery/surfaces/guard.go similarity index 73% rename from boatstack/internal/surfaces/guard.go rename to boatstack/internal/softwaredelivery/surfaces/guard.go index 55df3ce..ac7392a 100644 --- a/boatstack/internal/surfaces/guard.go +++ b/boatstack/internal/softwaredelivery/surfaces/guard.go @@ -1,6 +1,6 @@ package surfaces -import "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" +import "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" // ClassifyCommandIntent is the host-facing projection of the kernel's one // consumer-neutral command classifier. diff --git a/boatstack/internal/surfaces/locus_render.go b/boatstack/internal/softwaredelivery/surfaces/locus_render.go similarity index 80% rename from boatstack/internal/surfaces/locus_render.go rename to boatstack/internal/softwaredelivery/surfaces/locus_render.go index c3e7275..49be866 100644 --- a/boatstack/internal/surfaces/locus_render.go +++ b/boatstack/internal/softwaredelivery/surfaces/locus_render.go @@ -4,8 +4,8 @@ import ( "encoding/json" "sort" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) type locusEvidence struct { @@ -87,14 +87,14 @@ func renderCatalogLocus(transitions []catalog.Transition, safety bool) (string, ID: "boatstack-v2-executable-catalog-liveness-v1", Subject: "Finite stable-phase abstraction generated from the compiled Boatstack ControlProgram registry. It contains one event for every runtime entry and expands each declared source and target phase set. The 18-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.", Evidence: []locusEvidence{ - {Path: "boatstack/control/control.go", Note: "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry."}, + {Path: "boatstack/delivery/delivery.go", Note: "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry."}, {Path: "docs/architecture/boatstack-v2-transition-catalog.md", Note: "Generated readable projection from the same runtime registry."}, - {Path: "boatstack/internal/kernel/protocol/admission.go", Note: "Exact admission, authority, parameter, source-revision, provider-request, expiry, and stale-snapshot checks."}, - {Path: "boatstack/internal/kernel/engine/engine.go", Note: "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery."}, - {Path: "boatstack/internal/effects/state_reducer.go", Note: "Admitted native effects reduce every controllable Standard distribution transition through one state adapter."}, + {Path: "boatstack/internal/softwaredelivery/protocol/admission.go", Note: "Exact admission, authority, parameter, source-revision, provider-request, expiry, and stale-snapshot checks."}, + {Path: "boatstack/internal/softwaredelivery/engine/engine.go", Note: "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery."}, + {Path: "boatstack/internal/softwaredelivery/effects/state_reducer.go", Note: "Admitted native effects reduce every controllable Standard distribution transition through one state adapter."}, {Path: "boatstack/flow/standard/completeness_test.go", Note: "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests."}, - {Path: "boatstack/internal/kernel/engine/engine_test.go", Note: "Exact-admission, stale-snapshot, postcondition, interruption, idempotency, and unknown-outcome tests."}, - {Path: "boatstack/internal/effects/prepared.go", Note: "Staged effect ordering, atomic resource application, rollback, and external settlement boundary."}, + {Path: "boatstack/internal/softwaredelivery/engine/engine_test.go", Note: "Exact-admission, stale-snapshot, postcondition, interruption, idempotency, and unknown-outcome tests."}, + {Path: "boatstack/internal/softwaredelivery/effects/prepared.go", Note: "Staged effect ordering, atomic resource application, rollback, and external settlement boundary."}, {Path: "boatstack/flow/standard/historical_test.go", Note: "Historical incidents resolved through the executable runtime supervisor."}, }, Spec: locusSpec{ diff --git a/boatstack/internal/surfaces/protocol.go b/boatstack/internal/softwaredelivery/surfaces/protocol.go similarity index 91% rename from boatstack/internal/surfaces/protocol.go rename to boatstack/internal/softwaredelivery/surfaces/protocol.go index a0d89e5..c95994e 100644 --- a/boatstack/internal/surfaces/protocol.go +++ b/boatstack/internal/softwaredelivery/surfaces/protocol.go @@ -5,13 +5,13 @@ import ( "strings" "time" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" ) -const SchemaVersion = 4 +const SchemaVersion = 5 type Operation string @@ -41,7 +41,7 @@ type Request struct { Host string `json:"host"` CorrelationID string `json:"correlation_id"` FlowID string `json:"flow_id,omitempty"` - Goal model.Goal `json:"goal,omitempty"` + Objective model.Objective `json:"objective,omitempty"` TransitionID catalog.TransitionID `json:"transition_id,omitempty"` Prescription protocol.Prescription `json:"prescription,omitempty"` Authority protocol.AuthorityBundle `json:"authority,omitempty"` @@ -125,7 +125,7 @@ type ProgramChange struct { type Response struct { SchemaVersion int `json:"schema_version"` Operation Operation `json:"operation"` - Goal model.Goal `json:"goal,omitempty"` + Objective model.Objective `json:"objective,omitempty"` Snapshot *model.Snapshot `json:"snapshot,omitempty"` Decision *supervisor.Decision `json:"decision,omitempty"` Prescription *protocol.Prescription `json:"prescription,omitempty"` diff --git a/boatstack/internal/surfaces/protocol_test.go b/boatstack/internal/softwaredelivery/surfaces/protocol_test.go similarity index 100% rename from boatstack/internal/surfaces/protocol_test.go rename to boatstack/internal/softwaredelivery/surfaces/protocol_test.go diff --git a/boatstack/internal/surfaces/render.go b/boatstack/internal/softwaredelivery/surfaces/render.go similarity index 80% rename from boatstack/internal/surfaces/render.go rename to boatstack/internal/softwaredelivery/surfaces/render.go index 45eb2a4..1843953 100644 --- a/boatstack/internal/surfaces/render.go +++ b/boatstack/internal/softwaredelivery/surfaces/render.go @@ -5,9 +5,9 @@ import ( "strconv" "strings" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type Shell string @@ -25,12 +25,14 @@ type CommandAST struct { Arguments []string } -func PrescriptionCommand(transition catalog.Transition, prescription protocol.Prescription, correlation, repository string, goal model.Goal, flowID string, parameters protocol.Parameters) CommandAST { +func PrescriptionCommand(transition catalog.Transition, prescription protocol.Prescription, correlation, repository string, objective model.Objective, flowID string, parameters protocol.Parameters) CommandAST { arguments := []string{"apply", "--repo", repository, "--transition", string(transition.ID), "--flow", flowID, "--correlation", correlation, "--prescription-id", prescription.ID, + "--expected-instance-id", prescription.ExpectedInstanceID, "--expected-state-revision", strconv.FormatUint(prescription.ExpectedStateRevision, 10), "--expected-program-fingerprint", prescription.ExpectedProgramFingerprint, "--expected-snapshot-fingerprint", prescription.ExpectedSnapshotFingerprint, + "--expected-objective-binding-fingerprint", prescription.ExpectedObjectiveBindingFingerprint, "--authority-fingerprint", prescription.AuthorityFingerprint} for _, capability := range prescription.RequiredCapabilities { arguments = append(arguments, "--required-capability", string(capability)) @@ -38,8 +40,8 @@ func PrescriptionCommand(transition catalog.Transition, prescription protocol.Pr for _, capability := range prescription.EffectiveCapabilities { arguments = append(arguments, "--effective-capability", string(capability)) } - if goal.Validate() == nil { - arguments = append(arguments, "--goal-kind", string(goal.Kind), "--delivery", goal.DeliveryID, "--goal-id", goal.ID) + if objective.Validate() == nil { + arguments = append(arguments, "--objective-kind", string(objective.Kind), "--delivery", objective.DeliveryID, "--objective-id", objective.ID) } canonical := parameters.Canonical() for _, parameter := range canonical { @@ -75,7 +77,7 @@ type HostPrescription struct { // ProjectHostPrescription changes host capability metadata only. Every host // consumes the same semantic command, authority prompt, and postcondition. -func ProjectHostPrescription(host string, transition catalog.Transition, prescription protocol.Prescription, correlation, repository string, goal model.Goal, flowID string, parameters protocol.Parameters) (HostPrescription, error) { +func ProjectHostPrescription(host string, transition catalog.Transition, prescription protocol.Prescription, correlation, repository string, objective model.Objective, flowID string, parameters protocol.Parameters) (HostPrescription, error) { known := false for _, candidate := range CanonicalHostNames() { if host == candidate { @@ -87,7 +89,7 @@ func ProjectHostPrescription(host string, transition catalog.Transition, prescri return HostPrescription{}, fmt.Errorf("unsupported host %q", host) } return HostPrescription{ - Host: host, TransitionID: string(transition.ID), Command: PrescriptionCommand(transition, prescription, correlation, repository, goal, flowID, parameters), + Host: host, TransitionID: string(transition.ID), Command: PrescriptionCommand(transition, prescription, correlation, repository, objective, flowID, parameters), AuthorityPrompt: transition.Prescription.AuthorityPrompt, ExpectedPostcondition: transition.Prescription.ExpectedPostcondition, }, nil } diff --git a/boatstack/internal/surfaces/render_test.go b/boatstack/internal/softwaredelivery/surfaces/render_test.go similarity index 80% rename from boatstack/internal/surfaces/render_test.go rename to boatstack/internal/softwaredelivery/surfaces/render_test.go index 379d1a0..faaf493 100644 --- a/boatstack/internal/surfaces/render_test.go +++ b/boatstack/internal/softwaredelivery/surfaces/render_test.go @@ -6,11 +6,12 @@ import ( "strings" "testing" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" "github.com/operatorstack/boatstack/boatstack/internal/testprogram" + general "github.com/operatorstack/boatstack/boatstack/kernel" ) func TestShellRenderersConsumeOneCommandAST(t *testing.T) { @@ -19,16 +20,16 @@ func TestShellRenderersConsumeOneCommandAST(t *testing.T) { if !ok { t.Fatal("missing plan.create") } - goal := model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} parameters := protocol.Parameters{{Name: "source_path", Value: "/tmp/O'Brien plan.md"}, {Name: "delivery_id", Value: "delivery"}} prescription := protocol.Prescription{ - ID: "prx-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), - AuthorityFingerprint: "auth-fixture", RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute}, + ID: "prx-fixture", Freshness: general.Freshness{ExpectedInstanceID: "repo-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), ExpectedObjectiveBindingFingerprint: strings.Repeat("c", 64), AuthorityFingerprint: "auth-fixture"}, + RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute}, EffectiveCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute}, } - command := PrescriptionCommand(transition, prescription, "corr-1", "/repo with space", goal, "flow", parameters) + command := PrescriptionCommand(transition, prescription, "corr-1", "/repo with space", objective, "flow", parameters) joined := strings.Join(command.Arguments, " ") - for _, binding := range []string{"--correlation corr-1", "--prescription-id prx-fixture", "--expected-state-revision 41", "--expected-program-fingerprint", "--expected-snapshot-fingerprint", "--authority-fingerprint auth-fixture", "--required-capability repository.write", "--required-capability command.execute", "--effective-capability repository.write", "--effective-capability command.execute"} { + for _, binding := range []string{"--correlation corr-1", "--prescription-id prx-fixture", "--expected-instance-id repo-fixture", "--expected-state-revision 41", "--expected-program-fingerprint", "--expected-snapshot-fingerprint", "--expected-objective-binding-fingerprint", "--authority-fingerprint auth-fixture", "--required-capability repository.write", "--required-capability command.execute", "--effective-capability repository.write", "--effective-capability command.execute"} { if !strings.Contains(joined, binding) { t.Fatalf("prescription command omitted CAS binding %q: %s", binding, joined) } @@ -117,8 +118,8 @@ func TestLocusModelsAreGeneratedFromEveryRuntimeTransition(t *testing.T) { } func TestEveryHostConsumesOneSemanticPrescription(t *testing.T) { - goal := model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"} - prescription := protocol.Prescription{ID: "prx-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64)} + objective := model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} + prescription := protocol.Prescription{ID: "prx-fixture", Freshness: general.Freshness{ExpectedInstanceID: "repo-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), ExpectedObjectiveBindingFingerprint: strings.Repeat("c", 64)}} for _, transition := range testprogram.StandardRegistry().All() { if !transition.Controllable() { continue @@ -129,7 +130,7 @@ func TestEveryHostConsumesOneSemanticPrescription(t *testing.T) { } var canonical HostPrescription for index, host := range CanonicalHostNames() { - projection, err := ProjectHostPrescription(host, transition, prescription, "corr-1", "/repo", goal, "flow", parameters) + projection, err := ProjectHostPrescription(host, transition, prescription, "corr-1", "/repo", objective, "flow", parameters) if err != nil { t.Fatal(err) } diff --git a/boatstack/internal/testprogram/standard.go b/boatstack/internal/testprogram/standard.go index 604ea24..be1d732 100644 --- a/boatstack/internal/testprogram/standard.go +++ b/boatstack/internal/testprogram/standard.go @@ -5,16 +5,16 @@ package testprogram import ( "context" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" ) // StandardRegistry compiles the real CoreSystem and StandardFlow declaration // bytes. It panics only in tests when a checked first-party manifest is invalid. func StandardRegistry() catalog.Registry { - program, err := control.Compile(context.Background(), control.CompileRequest{ + program, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), diff --git a/boatstack/kernel/freshness.go b/boatstack/kernel/freshness.go new file mode 100644 index 0000000..e9f68a7 --- /dev/null +++ b/boatstack/kernel/freshness.go @@ -0,0 +1,50 @@ +package kernel + +import "fmt" + +// Freshness is the domain-independent compare-and-swap identity shared by +// every prescription. Snapshot means the exact canonical domain observation; +// ObjectiveBindingFingerprint identifies either the exact bound revision or +// verified absence. +type Freshness struct { + ExpectedInstanceID string `json:"expected_instance_id"` + ExpectedStateRevision uint64 `json:"expected_state_revision"` + ExpectedProgramFingerprint string `json:"expected_program_fingerprint"` + ExpectedSnapshotFingerprint string `json:"expected_snapshot_fingerprint"` + ExpectedObjectiveBindingFingerprint string `json:"expected_objective_binding_fingerprint"` + AuthorityFingerprint string `json:"authority_fingerprint"` +} + +func NewFreshness(instanceID string, stateRevision uint64, programFingerprint, snapshotFingerprint, objectiveBindingFingerprint, authorityFingerprint string) (Freshness, error) { + value := Freshness{ + ExpectedInstanceID: instanceID, ExpectedStateRevision: stateRevision, ExpectedProgramFingerprint: programFingerprint, + ExpectedSnapshotFingerprint: snapshotFingerprint, + ExpectedObjectiveBindingFingerprint: objectiveBindingFingerprint, + AuthorityFingerprint: authorityFingerprint, + } + return value, value.Validate() +} + +func (f Freshness) Validate() error { + if f.ExpectedInstanceID == "" || f.ExpectedStateRevision == 0 || len(f.ExpectedProgramFingerprint) != 64 || len(f.ExpectedSnapshotFingerprint) != 64 || len(f.ExpectedObjectiveBindingFingerprint) != 64 || f.AuthorityFingerprint == "" { + return fmt.Errorf("freshness requires exact instance, state, program, snapshot, objective binding, and authority identities") + } + return nil +} + +func (f Freshness) Check(current Freshness) error { + if err := f.Validate(); err != nil { + return err + } + if err := current.Validate(); err != nil { + return err + } + if f != current { + return fmt.Errorf("instance, state, program, snapshot, objective binding, or authority changed") + } + return nil +} + +// Fingerprint returns the canonical JSON content identity used at kernel +// boundaries. Domain adapters use it to bind their exact objective projection. +func Fingerprint(value any) (string, error) { return contentHash(value) } diff --git a/boatstack/kernel/program.go b/boatstack/kernel/program.go new file mode 100644 index 0000000..e3f1a1a --- /dev/null +++ b/boatstack/kernel/program.go @@ -0,0 +1,264 @@ +package kernel + +import ( + "fmt" + "sort" +) + +type Transition struct { + ID string `json:"id"` + SourceModes []string `json:"source_modes"` + TargetMode string `json:"target_mode"` + ObjectiveScope ObjectiveScope `json:"objective_scope"` + ObjectiveMutation ObjectiveMutation `json:"objective_mutation"` + RequiredCapabilities []Capability `json:"required_capabilities"` + OwnedFacets []string `json:"owned_facets"` + Operation string `json:"operation"` + Priority int `json:"priority"` + Recovers []string `json:"recovers,omitempty"` +} + +type ObjectiveMutation string + +const ( + PreserveObjective ObjectiveMutation = "preserve" + BindObjectiveMutation ObjectiveMutation = "bind" + ClearObjectiveMutation ObjectiveMutation = "clear" +) + +func (m ObjectiveMutation) valid() bool { + return m == PreserveObjective || m == BindObjectiveMutation || m == ClearObjectiveMutation +} + +func (t Transition) validate() error { + if !qualifiedSemanticID.MatchString(t.ID) || len(t.SourceModes) == 0 || t.TargetMode == "" || !t.ObjectiveScope.Valid() || !t.ObjectiveMutation.valid() || !qualifiedSemanticID.MatchString(t.Operation) || t.Priority < 1 { + return fmt.Errorf("transition %q has incomplete identity, modes, objective scope, operation, or priority", t.ID) + } + if len(t.RequiredCapabilities) == 0 || len(t.OwnedFacets) == 0 { + return fmt.Errorf("transition %q requires explicit capabilities and owned facets", t.ID) + } + if _, err := normalizeCapabilities(t.RequiredCapabilities); err != nil { + return fmt.Errorf("transition %q: %w", t.ID, err) + } + seen := map[string]bool{} + for _, facet := range t.OwnedFacets { + if !semanticID.MatchString(facet) || seen[facet] { + return fmt.Errorf("transition %q has invalid or duplicate owned facet %q", t.ID, facet) + } + seen[facet] = true + } + if t.ObjectiveMutation != PreserveObjective && (t.ObjectiveScope != ObjectiveNone || !contains(t.OwnedFacets, "supervisor.objective") || !containsCapability(t.RequiredCapabilities, "objective.bind")) { + return fmt.Errorf("transition %q objective mutation requires NONE scope, supervisor.objective ownership, and objective.bind capability", t.ID) + } + for _, recovered := range t.Recovers { + if !qualifiedSemanticID.MatchString(recovered) { + return fmt.Errorf("transition %q has invalid recovery target %q", t.ID, recovered) + } + } + return nil +} + +func (t Transition) canonical() (Transition, error) { + copy := t + var err error + if copy.SourceModes, err = canonicalIDs(copy.SourceModes, "source mode"); err != nil { + return Transition{}, fmt.Errorf("transition %q: %w", copy.ID, err) + } + if copy.RequiredCapabilities, err = normalizeCapabilities(copy.RequiredCapabilities); err != nil { + return Transition{}, fmt.Errorf("transition %q: %w", copy.ID, err) + } + if copy.OwnedFacets, err = canonicalIDs(copy.OwnedFacets, "owned facet"); err != nil { + return Transition{}, fmt.Errorf("transition %q: %w", copy.ID, err) + } + if copy.Recovers, err = canonicalQualifiedIDs(copy.Recovers, "recovery target"); err != nil { + return Transition{}, fmt.Errorf("transition %q: %w", copy.ID, err) + } + return copy, nil +} + +type Program struct { + SchemaVersion int `json:"schema_version"` + ID string `json:"id"` + Version string `json:"version"` + RuntimeCompatibility string `json:"runtime_compatibility"` + DomainContractFingerprint string `json:"domain_contract_fingerprint,omitempty"` + InitialMode string `json:"initial_mode"` + MarkedModes []string `json:"marked_modes"` + Transitions []Transition `json:"transitions"` + Fingerprint string `json:"fingerprint"` + byID map[string]Transition +} + +func CompileProgram(id, version, runtimeCompatibility, initialMode string, markedModes []string, transitions []Transition) (Program, error) { + return CompileDomainProgram(id, version, runtimeCompatibility, "", initialMode, markedModes, transitions) +} + +// CompileDomainProgram binds the complete domain contract fingerprint into the +// same executable Program identity used by the kernel. Domain-specific ABI +// fields remain outside the kernel, but changing any of them changes the +// program fingerprint and invalidates prior prescriptions. +func CompileDomainProgram(id, version, runtimeCompatibility, domainContractFingerprint, initialMode string, markedModes []string, transitions []Transition) (Program, error) { + program := Program{SchemaVersion: ProgramSchemaVersion, ID: id, Version: version, RuntimeCompatibility: runtimeCompatibility, DomainContractFingerprint: domainContractFingerprint, InitialMode: initialMode, MarkedModes: append([]string(nil), markedModes...), Transitions: append([]Transition(nil), transitions...)} + if err := program.prepare(); err != nil { + return Program{}, err + } + identity := program + identity.Fingerprint = "" + identity.byID = nil + fingerprint, err := contentHash(identity) + if err != nil { + return Program{}, err + } + program.Fingerprint = fingerprint + return program, nil +} + +func (p *Program) prepare() error { + if p.SchemaVersion != ProgramSchemaVersion || !semanticID.MatchString(p.ID) || p.Version == "" || p.RuntimeCompatibility == "" || p.InitialMode == "" || len(p.MarkedModes) == 0 || len(p.Transitions) == 0 { + return fmt.Errorf("program requires schema, identity, compatibility, initial mode, marked modes, and transitions") + } + if p.DomainContractFingerprint != "" && len(p.DomainContractFingerprint) != 64 { + return fmt.Errorf("program domain contract fingerprint must be an exact sha256 identity") + } + markedModes, err := canonicalIDs(p.MarkedModes, "marked mode") + if err != nil { + return err + } + p.MarkedModes = markedModes + p.Transitions = append([]Transition(nil), p.Transitions...) + for index, transition := range p.Transitions { + canonical, err := transition.canonical() + if err != nil { + return err + } + p.Transitions[index] = canonical + } + sort.Slice(p.Transitions, func(i, j int) bool { + if p.Transitions[i].Priority != p.Transitions[j].Priority { + return p.Transitions[i].Priority < p.Transitions[j].Priority + } + return p.Transitions[i].ID < p.Transitions[j].ID + }) + p.byID = make(map[string]Transition, len(p.Transitions)) + for _, transition := range p.Transitions { + if err := transition.validate(); err != nil { + return err + } + if _, exists := p.byID[transition.ID]; exists { + return fmt.Errorf("program duplicates transition %q", transition.ID) + } + p.byID[transition.ID] = transition + } + for _, transition := range p.Transitions { + if len(transition.Recovers) != 0 && (transition.ObjectiveScope == ObjectiveBoundExact || transition.ObjectiveMutation != PreserveObjective) { + return fmt.Errorf("recovery transition %q must preserve objective state without requiring an exact objective", transition.ID) + } + for _, recovered := range transition.Recovers { + recoveredTransition, exists := p.byID[recovered] + if !exists { + return fmt.Errorf("transition %q recovers unknown transition %q", transition.ID, recovered) + } + for _, sourceMode := range recoveredTransition.SourceModes { + if !contains(transition.SourceModes, sourceMode) { + return fmt.Errorf("transition %q cannot recover %q from source mode %q", transition.ID, recovered, sourceMode) + } + } + } + } + recovered := make(map[string]bool, len(p.Transitions)) + for _, transition := range p.Transitions { + for _, recoveredID := range transition.Recovers { + recovered[recoveredID] = true + } + } + for _, transition := range p.Transitions { + if len(transition.Recovers) == 0 && !recovered[transition.ID] { + return fmt.Errorf("transition %q has no declared recovery", transition.ID) + } + } + return nil +} + +func canonicalIDs(values []string, label string) ([]string, error) { + result := append([]string(nil), values...) + sort.Strings(result) + for index, value := range result { + if !semanticID.MatchString(value) { + return nil, fmt.Errorf("%s %q is not a semantic identifier", label, value) + } + if index > 0 && value == result[index-1] { + return nil, fmt.Errorf("%s %q is duplicated", label, value) + } + } + return result, nil +} + +func canonicalQualifiedIDs(values []string, label string) ([]string, error) { + result := append([]string(nil), values...) + sort.Strings(result) + for index, value := range result { + if !qualifiedSemanticID.MatchString(value) { + return nil, fmt.Errorf("%s %q is not a qualified semantic identifier", label, value) + } + if index > 0 && value == result[index-1] { + return nil, fmt.Errorf("%s %q is duplicated", label, value) + } + } + return result, nil +} + +func (p Program) Validate() error { + copy := p + want := copy.Fingerprint + copy.Fingerprint = "" + copy.byID = nil + if err := copy.prepare(); err != nil { + return err + } + got, err := contentHash(copy) + if err != nil || want == "" || got != want { + return fmt.Errorf("program fingerprint does not identify its canonical executable representation") + } + return nil +} + +func (p Program) Identity() ProgramIdentity { + return ProgramIdentity{ID: p.ID, Version: p.Version, Fingerprint: p.Fingerprint} +} + +func (p Program) Transition(id string) (Transition, bool) { + if p.byID == nil { + copy := p + if copy.prepare() != nil { + return Transition{}, false + } + return copy.Transition(id) + } + transition, ok := p.byID[id] + return transition, ok +} + +func (p Program) Marked(mode string) bool { + for _, candidate := range p.MarkedModes { + if candidate == mode { + return true + } + } + return false +} + +func (p Program) Clone() Program { + copy := p + copy.MarkedModes = append([]string(nil), p.MarkedModes...) + copy.Transitions = append([]Transition(nil), p.Transitions...) + copy.byID = make(map[string]Transition, len(copy.Transitions)) + for index, transition := range copy.Transitions { + transition.SourceModes = append([]string(nil), transition.SourceModes...) + transition.RequiredCapabilities = append([]Capability(nil), transition.RequiredCapabilities...) + transition.OwnedFacets = append([]string(nil), transition.OwnedFacets...) + transition.Recovers = append([]string(nil), transition.Recovers...) + copy.Transitions[index] = transition + copy.byID[transition.ID] = transition + } + return copy +} diff --git a/boatstack/kernel/program_test.go b/boatstack/kernel/program_test.go new file mode 100644 index 0000000..5609afb --- /dev/null +++ b/boatstack/kernel/program_test.go @@ -0,0 +1,84 @@ +package kernel + +import ( + "strings" + "testing" +) + +func TestProgramFingerprintCanonicalizesSemanticSets(t *testing.T) { + left, err := CompileProgram("canonical", "1", "kernel-v1", "idle", []string{"done", "closed"}, []Transition{ + { + ID: "advance", SourceModes: []string{"ready", "idle"}, TargetMode: "done", + ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, + RequiredCapabilities: []Capability{"state.write", "state.inspect"}, + OwnedFacets: []string{"counter.value", "counter.audit"}, + Operation: "counter.advance", Priority: 1, + }, + {ID: "recover", SourceModes: []string{"idle", "ready"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.recover", Priority: 2, Recovers: []string{"advance"}}, + }) + if err != nil { + t.Fatal(err) + } + right, err := CompileProgram("canonical", "1", "kernel-v1", "idle", []string{"closed", "done"}, []Transition{ + { + ID: "advance", SourceModes: []string{"idle", "ready"}, TargetMode: "done", + ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, + RequiredCapabilities: []Capability{"state.inspect", "state.write"}, + OwnedFacets: []string{"counter.audit", "counter.value"}, + Operation: "counter.advance", Priority: 1, + }, + {ID: "recover", SourceModes: []string{"ready", "idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.recover", Priority: 2, Recovers: []string{"advance"}}, + }) + if err != nil { + t.Fatal(err) + } + if left.Fingerprint != right.Fingerprint { + t.Fatalf("semantic reordering changed fingerprint: %s != %s", left.Fingerprint, right.Fingerprint) + } +} + +func TestProgramRejectsRecoveryThatCannotRunFromRecoveredSourceMode(t *testing.T) { + _, err := CompileProgram("blocked-recovery", "1", "kernel-v1", "one", []string{"done"}, []Transition{ + {ID: "increment", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 1}, + {ID: "recover", SourceModes: []string{"zero"}, TargetMode: "zero", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"increment"}}, + }) + if err == nil || !strings.Contains(err.Error(), `cannot recover "increment" from source mode "one"`) { + t.Fatalf("compile error = %v", err) + } +} + +func TestProgramRejectsObjectiveDependentRecovery(t *testing.T) { + _, err := CompileProgram("blocked-objective-recovery", "1", "kernel-v1", "idle", []string{"done"}, []Transition{ + {ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 1}, + {ID: "recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"advance"}}, + }) + if err == nil || !strings.Contains(err.Error(), `recovery transition "recover" must preserve objective state without requiring an exact objective`) { + t.Fatalf("compile error = %v", err) + } +} + +func TestProgramRejectsTransitionWithoutDeclaredRecovery(t *testing.T) { + _, err := CompileProgram("unrecoverable", "1", "kernel-v1", "idle", []string{"done"}, []Transition{{ + ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, + RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 1, + }}) + if err == nil || !strings.Contains(err.Error(), `transition "advance" has no declared recovery`) { + t.Fatalf("compile error = %v", err) + } +} + +func TestProgramAcceptsQualifiedTransitionIdentities(t *testing.T) { + program, err := CompileProgram("qualified", "1", "kernel-v1", "idle", []string{"done"}, []Transition{ + {ID: "example-program/advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "example-program/advance", Priority: 1}, + {ID: "example-program/recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "example-program/recover", Priority: 2, Recovers: []string{"example-program/advance", "example-program/recover"}}, + }) + if err != nil { + t.Fatal(err) + } + if err := program.Validate(); err != nil { + t.Fatal(err) + } + if _, ok := program.Transition("example-program/advance"); !ok { + t.Fatal("qualified transition identity was not retained") + } +} diff --git a/boatstack/kernel/relation.go b/boatstack/kernel/relation.go new file mode 100644 index 0000000..46e56fe --- /dev/null +++ b/boatstack/kernel/relation.go @@ -0,0 +1,122 @@ +package kernel + +import ( + "fmt" + "sort" +) + +// RelationCandidate is a domain-independent projection of one transition that +// has already satisfied its program and domain predicates. Rank and Priority +// are ordered ascending. Authority remains data: only Relate compares the +// required capabilities with the externally admitted capability set. +type RelationCandidate struct { + ID string + Rank int + Priority int + Selectable bool + RequiredAll []Capability + RequiredAny []Capability +} + +type RelationInput struct { + Requested string + Marked bool + NoCandidate DecisionKind + Candidates []RelationCandidate + Available []Capability +} + +// Relate is the kernel's canonical transition-selection relation. Domains +// decide whether a transition satisfies domain predicates; the kernel alone +// applies target selection, ordering, ambiguity, and authority. +func Relate(input RelationInput) Decision { + if input.Marked && input.Requested == "" { + return Decision{Kind: Marked, Reason: "program-defined marked state is established"} + } + candidates := append([]RelationCandidate(nil), input.Candidates...) + if input.Requested != "" { + filtered := candidates[:0] + for _, candidate := range candidates { + if candidate.ID == input.Requested { + filtered = append(filtered, candidate) + } + } + candidates = filtered + } + if len(candidates) == 0 { + if input.Requested != "" { + return Decision{Kind: Refused, Transition: input.Requested, Reason: "requested transition is not admissible under the canonical relation"} + } + kind := input.NoCandidate + if kind == "" { + kind = Unresolved + } + return Decision{Kind: kind, Reason: "no transition is admissible under the canonical relation"} + } + if input.Requested == "" { + filtered := candidates[:0] + for _, candidate := range candidates { + if candidate.Selectable { + filtered = append(filtered, candidate) + } + } + candidates = filtered + if len(candidates) == 0 { + kind := input.NoCandidate + if kind == "" { + kind = Unresolved + } + return Decision{Kind: kind, Reason: "no transition is selectable under the canonical relation"} + } + } + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].Rank != candidates[j].Rank { + return candidates[i].Rank < candidates[j].Rank + } + if candidates[i].Priority != candidates[j].Priority { + return candidates[i].Priority < candidates[j].Priority + } + return candidates[i].ID < candidates[j].ID + }) + top := candidates[0] + var equal []string + for _, candidate := range candidates { + if candidate.Rank == top.Rank && candidate.Priority == top.Priority { + equal = append(equal, candidate.ID) + } + } + if len(equal) > 1 { + return Decision{Kind: Frontier, Candidates: equal, Reason: "equally preferred admissible transitions require selection"} + } + if missing := missingAuthority(top, input.Available); len(missing) != 0 { + return Decision{Kind: Frontier, Transition: top.ID, Candidates: []string{top.ID}, Reason: fmt.Sprintf("transition requires unavailable capabilities: %v", missing)} + } + return Decision{Kind: Prescribed, Transition: top.ID, Reason: "canonical relation admitted one highest-priority transition"} +} + +func missingAuthority(candidate RelationCandidate, available []Capability) []Capability { + set := make(map[Capability]bool, len(available)) + for _, value := range available { + set[value] = true + } + missing := make([]Capability, 0, len(candidate.RequiredAll)+1) + for _, value := range candidate.RequiredAll { + if !set[value] { + missing = append(missing, value) + } + } + if len(candidate.RequiredAny) != 0 { + satisfied := false + for _, value := range candidate.RequiredAny { + if set[value] { + satisfied = true + break + } + } + if !satisfied { + missing = append(missing, candidate.RequiredAny...) + } + } + sort.Slice(missing, func(i, j int) bool { return missing[i] < missing[j] }) + return missing +} diff --git a/boatstack/kernel/relation_test.go b/boatstack/kernel/relation_test.go new file mode 100644 index 0000000..79b3e77 --- /dev/null +++ b/boatstack/kernel/relation_test.go @@ -0,0 +1,45 @@ +package kernel + +import "testing" + +func TestRelationOwnsSelectionAndAuthority(t *testing.T) { + candidates := []RelationCandidate{ + {ID: "low", Rank: 2, Priority: 1, Selectable: true}, + {ID: "top", Rank: 1, Priority: 1, Selectable: true, RequiredAll: []Capability{"execute"}}, + } + without := Relate(RelationInput{Candidates: candidates}) + if without.Kind != Frontier || without.Transition != "top" { + t.Fatalf("without authority = %#v", without) + } + with := Relate(RelationInput{Candidates: candidates, Available: []Capability{"execute"}}) + if with.Kind != Prescribed || with.Transition != "top" { + t.Fatalf("with authority = %#v", with) + } +} + +func TestRelationTargetedAndUntargetedUseSameCandidates(t *testing.T) { + candidates := []RelationCandidate{{ID: "advance", Priority: 1, Selectable: true}} + untargeted := Relate(RelationInput{Candidates: candidates}) + targeted := Relate(RelationInput{Requested: "advance", Candidates: candidates}) + if untargeted.Kind != Prescribed || targeted.Kind != Prescribed || untargeted.Transition != targeted.Transition { + t.Fatalf("untargeted/targeted = %#v/%#v", untargeted, targeted) + } + refused := Relate(RelationInput{Requested: "other", Candidates: candidates}) + if refused.Kind != Refused { + t.Fatalf("inadmissible target = %#v", refused) + } +} + +func TestRelationReportsEqualPreferenceAndMarkedState(t *testing.T) { + tied := Relate(RelationInput{Candidates: []RelationCandidate{ + {ID: "a", Priority: 1, Selectable: true}, + {ID: "b", Priority: 1, Selectable: true}, + }}) + if tied.Kind != Frontier || len(tied.Candidates) != 2 { + t.Fatalf("tie = %#v", tied) + } + marked := Relate(RelationInput{Marked: true}) + if marked.Kind != Marked { + t.Fatalf("marked = %#v", marked) + } +} diff --git a/boatstack/kernel/runtime.go b/boatstack/kernel/runtime.go new file mode 100644 index 0000000..7134b6b --- /dev/null +++ b/boatstack/kernel/runtime.go @@ -0,0 +1,558 @@ +package kernel + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "time" +) + +type Observation struct { + Fingerprint string `json:"fingerprint"` + Value json.RawMessage `json:"value"` +} + +func NewObservation(value any) (Observation, error) { + encoded, err := json.Marshal(value) + if err != nil { + return Observation{}, err + } + fingerprint, err := contentHash(json.RawMessage(encoded)) + return Observation{Fingerprint: fingerprint, Value: encoded}, err +} + +func (o Observation) Validate() error { + if len(o.Fingerprint) != 64 || len(o.Value) == 0 || !json.Valid(o.Value) { + return fmt.Errorf("domain observation requires canonical value and fingerprint") + } + fingerprint, err := contentHash(o.Value) + if err != nil || fingerprint != o.Fingerprint { + return fmt.Errorf("domain observation fingerprint mismatch") + } + return nil +} + +type Evaluation struct { + State ControlState + Observation Observation + Objective *Objective + Transition Transition +} + +type Domain interface { + Observe(context.Context, string) (Observation, error) + // Admissible must keep a declared recovery transition admissible while its + // matching RecoveryState is active, including after an earlier recovery + // attempt may already have changed the domain. + Admissible(context.Context, Evaluation) (bool, string, error) + Verify(context.Context, Evaluation, Effect, Observation) error +} + +type Operation struct { + InstanceID string + Transition Transition + Observation Observation + Objective *Objective + Capabilities []Capability +} + +type Operator interface { + Execute(context.Context, Operation) (Effect, error) +} + +// CapabilityClassifier is trusted mechanism configuration. Program +// declarations may strengthen its answer but can never weaken it. +type CapabilityClassifier interface { + RequiredCapabilities(Transition) ([]Capability, error) +} + +// Store owns the durable transaction boundary. BeginEffect must atomically +// persist an unresolved attempt before an operator can run. CommitTransition +// must atomically replace that attempt with the target state and its receipt: +// either both become visible or neither does. +type Store interface { + Load(context.Context, string) (ControlState, error) + BeginEffect(context.Context, uint64, ControlState) error + CommitTransition(context.Context, uint64, ControlState, Receipt) error +} + +type Lock interface{ Unlock() error } + +type Locker interface { + Acquire(context.Context, string) (Lock, error) +} + +type Clock interface{ Now() time.Time } + +type DecisionKind string + +const ( + Prescribed DecisionKind = "PRESCRIBED" + Marked DecisionKind = "MARKED" + Frontier DecisionKind = "FRONTIER" + Blocked DecisionKind = "BLOCKED" + Refused DecisionKind = "REFUSED" + Unresolved DecisionKind = "UNRESOLVED" +) + +type Decision struct { + Kind DecisionKind `json:"kind"` + Transition string `json:"transition,omitempty"` + Candidates []string `json:"candidates,omitempty"` + Reason string `json:"reason"` +} + +type Prescription struct { + SchemaVersion int `json:"schema_version"` + ID string `json:"id"` + TransitionID string `json:"transition_id"` + Freshness + ExpectedObjectiveBinding *ObjectiveBinding `json:"expected_objective_binding,omitempty"` + RequestedObjectiveBinding *ObjectiveBinding `json:"requested_objective_binding,omitempty"` + RequiredCapabilities []Capability `json:"required_capabilities"` +} + +type Resolution struct { + State ControlState `json:"state"` + Observation Observation `json:"observation"` + Decision Decision `json:"decision"` + Prescription *Prescription `json:"prescription,omitempty"` +} + +type ResolveRequest struct { + InstanceID string + Objective *Objective + Authority Authority + Requested string +} + +type ApplyRequest struct { + ResolveRequest + Prescription Prescription +} + +type Receipt struct { + SchemaVersion int `json:"schema_version"` + ID string `json:"id"` + InstanceID string `json:"instance_id"` + PrescriptionID string `json:"prescription_id"` + Program ProgramIdentity `json:"program"` + TransitionID string `json:"transition_id"` + PriorStateRevision uint64 `json:"prior_state_revision"` + AttemptStateRevision uint64 `json:"attempt_state_revision"` + ResultStateRevision uint64 `json:"result_state_revision"` + ObjectiveBinding *ObjectiveBinding `json:"objective_binding,omitempty"` + AuthorityFingerprint string `json:"authority_fingerprint"` + Capabilities []Capability `json:"capabilities"` + Effects []EffectFact `json:"effects"` + PriorObservation string `json:"prior_observation"` + ResultObservation string `json:"result_observation"` + Verification string `json:"verification"` + CommittedAt time.Time `json:"committed_at"` +} + +type Runtime struct { + program Program + domain Domain + operator Operator + classifier CapabilityClassifier + store Store + locker Locker + clock Clock +} + +func NewRuntime(program Program, domain Domain, operator Operator, classifier CapabilityClassifier, store Store, locker Locker, clock Clock) (Runtime, error) { + if err := program.Validate(); err != nil { + return Runtime{}, err + } + if domain == nil || operator == nil || classifier == nil || store == nil || locker == nil || clock == nil { + return Runtime{}, fmt.Errorf("kernel runtime requires domain, operator, capability classifier, transactional store, lock, and clock ports") + } + for _, transition := range program.Transitions { + if _, err := requiredCapabilities(classifier, transition); err != nil { + return Runtime{}, err + } + } + return Runtime{program: program, domain: domain, operator: operator, classifier: classifier, store: store, locker: locker, clock: clock}, nil +} + +func (r Runtime) Resolve(ctx context.Context, request ResolveRequest) (Resolution, error) { + state, err := r.store.Load(ctx, request.InstanceID) + if err != nil { + return Resolution{}, err + } + observation, err := r.domain.Observe(ctx, request.InstanceID) + if err != nil { + return Resolution{}, err + } + return r.resolve(ctx, state, observation, request) +} + +func (r Runtime) resolve(ctx context.Context, state ControlState, observation Observation, request ResolveRequest) (Resolution, error) { + base := Resolution{State: state, Observation: observation} + if err := state.Validate(); err != nil || observation.Validate() != nil || state.InstanceID != request.InstanceID || state.Program != r.program.Identity() { + base.Decision = Decision{Kind: Unresolved, Reason: "control state, domain observation, instance, or program identity is invalid"} + return base, nil + } + if r.program.Marked(state.Mode) && state.Recovery == nil && request.Requested == "" { + base.Decision = Decision{Kind: Marked, Reason: "program-defined marked state is established"} + return base, nil + } + authority, err := request.Authority.projection(r.clock.Now()) + if err != nil { + base.Decision = Decision{Kind: Refused, Reason: err.Error()} + return base, nil + } + transitions := map[string]Transition{} + var candidates []RelationCandidate + for _, transition := range r.program.Transitions { + if request.Requested != "" && transition.ID != request.Requested { + continue + } + if !contains(transition.SourceModes, state.Mode) { + continue + } + if state.Recovery != nil && !contains(transition.Recovers, state.Recovery.TransitionID) { + continue + } + if state.Recovery == nil && len(transition.Recovers) != 0 { + continue + } + objective, reason := objectiveFor(state, request.Objective, transition) + if reason != "" { + continue + } + allowed, _, evaluationErr := r.domain.Admissible(ctx, Evaluation{State: state, Observation: observation, Objective: objective, Transition: transition}) + if evaluationErr != nil { + return Resolution{}, evaluationErr + } + if allowed { + required, capabilityErr := requiredCapabilities(r.classifier, transition) + if capabilityErr != nil { + return Resolution{}, capabilityErr + } + transitions[transition.ID] = transition + candidates = append(candidates, RelationCandidate{ID: transition.ID, Priority: transition.Priority, Selectable: true, RequiredAll: required}) + } + } + base.Decision = Relate(RelationInput{Requested: request.Requested, Candidates: candidates, Available: authority.Capabilities}) + if base.Decision.Kind != Prescribed { + return base, nil + } + top := transitions[base.Decision.Transition] + required, err := requiredCapabilities(r.classifier, top) + if err != nil { + return Resolution{}, err + } + objective, _ := objectiveFor(state, request.Objective, top) + prescription, err := newPrescription(state, observation, top, objective, authority, required) + if err != nil { + return Resolution{}, err + } + base.Prescription = &prescription + return base, nil +} + +func (r Runtime) Apply(ctx context.Context, request ApplyRequest) (Receipt, error) { + lock, err := r.locker.Acquire(ctx, request.InstanceID) + if err != nil { + return Receipt{}, err + } + defer lock.Unlock() + state, err := r.store.Load(ctx, request.InstanceID) + if err != nil { + return Receipt{}, err + } + observation, err := r.domain.Observe(ctx, request.InstanceID) + if err != nil { + return Receipt{}, err + } + authority, err := request.Authority.projection(r.clock.Now()) + if err != nil { + return Receipt{}, err + } + if err := request.Prescription.validateCurrent(state, observation, authority); err != nil { + return Receipt{}, err + } + resolution, err := r.resolve(ctx, state, observation, request.ResolveRequest) + if err != nil { + return Receipt{}, err + } + if resolution.Decision.Kind != Prescribed || resolution.Prescription == nil { + return Receipt{}, fmt.Errorf("apply refused: %s", resolution.Decision.Reason) + } + if request.Prescription.ID != resolution.Prescription.ID { + return Receipt{}, StalePrescriptionError{Reason: "state, program, objective binding, observation, authority, or transition changed"} + } + transition, ok := r.program.Transition(request.Prescription.TransitionID) + if !ok { + return Receipt{}, StalePrescriptionError{Reason: "transition no longer belongs to the program"} + } + objective, _ := objectiveFor(state, request.Objective, transition) + required, err := requiredCapabilities(r.classifier, transition) + if err != nil { + return Receipt{}, err + } + attempt := state + attempt.Revision++ + if state.Recovery == nil { + attempt.Recovery = &RecoveryState{ + PrescriptionID: request.Prescription.ID, + TransitionID: request.Prescription.TransitionID, + Reason: "effect attempt began; outcome is unresolved", + } + } else { + recovery := *state.Recovery + attempt.Recovery = &recovery + } + if err := attempt.Validate(); err != nil { + return Receipt{}, fmt.Errorf("effect attempt state is invalid: %w", err) + } + if err := r.store.BeginEffect(ctx, state.Revision, attempt); err != nil { + return Receipt{}, fmt.Errorf("effect attempt did not begin: %w", err) + } + effect, err := r.operator.Execute(ctx, Operation{InstanceID: request.InstanceID, Transition: transition, Observation: observation, Objective: objective, Capabilities: required}) + if err != nil { + return Receipt{}, RecoveryRequiredError{Reason: fmt.Sprintf("operator outcome is unknown: %v", err)} + } + if err := validateEffects(transition, effect); err != nil { + return Receipt{}, RecoveryRequiredError{Reason: err.Error()} + } + sort.Slice(effect.Facts, func(i, j int) bool { + if effect.Facts[i].Facet != effect.Facts[j].Facet { + return effect.Facts[i].Facet < effect.Facts[j].Facet + } + if effect.Facts[i].Operation != effect.Facts[j].Operation { + return effect.Facts[i].Operation < effect.Facts[j].Operation + } + return effect.Facts[i].Fingerprint < effect.Facts[j].Fingerprint + }) + targetObservation, err := r.domain.Observe(ctx, request.InstanceID) + if err != nil { + return Receipt{}, RecoveryRequiredError{Reason: fmt.Sprintf("target observation failed: %v", err)} + } + if err := r.domain.Verify(ctx, Evaluation{State: state, Observation: observation, Objective: objective, Transition: transition}, effect, targetObservation); err != nil { + return Receipt{}, RecoveryRequiredError{Reason: fmt.Sprintf("verification failed: %v", err)} + } + target := attempt + target.Mode = transition.TargetMode + target.Revision++ + switch transition.ObjectiveMutation { + case BindObjectiveMutation: + binding, bindErr := BindObjective(*objective) + if bindErr != nil { + return Receipt{}, bindErr + } + target.ObjectiveBinding = &binding + case ClearObjectiveMutation: + target.ObjectiveBinding = nil + } + target.Recovery = nil + receipt := Receipt{SchemaVersion: ReceiptSchemaVersion, InstanceID: state.InstanceID, PrescriptionID: request.Prescription.ID, Program: state.Program, TransitionID: transition.ID, PriorStateRevision: state.Revision, AttemptStateRevision: attempt.Revision, ResultStateRevision: target.Revision, ObjectiveBinding: cloneBinding(target.ObjectiveBinding), AuthorityFingerprint: authority.Fingerprint, Capabilities: required, Effects: append([]EffectFact(nil), effect.Facts...), PriorObservation: observation.Fingerprint, ResultObservation: targetObservation.Fingerprint, Verification: "satisfied", CommittedAt: r.clock.Now().UTC()} + identity := receipt + identity.ID = "" + receipt.ID, err = contentHash(identity) + if err != nil { + return Receipt{}, err + } + receipt.ID = "rcp-" + receipt.ID + if err := receipt.Validate(); err != nil { + return Receipt{}, RecoveryRequiredError{Reason: "transition receipt is invalid: " + err.Error()} + } + if err := r.store.CommitTransition(ctx, attempt.Revision, target, receipt); err != nil { + return Receipt{}, RecoveryRequiredError{Reason: "state and receipt transaction did not commit: " + err.Error()} + } + return receipt, nil +} + +func (r Receipt) Validate() error { + identity := r + want := identity.ID + identity.ID = "" + got, err := contentHash(identity) + if err != nil || want != "rcp-"+got { + return fmt.Errorf("receipt content identity is invalid") + } + if r.SchemaVersion != ReceiptSchemaVersion || !semanticID.MatchString(r.InstanceID) || r.PrescriptionID == "" || !qualifiedSemanticID.MatchString(r.TransitionID) || r.PriorStateRevision == 0 || r.AttemptStateRevision != r.PriorStateRevision+1 || r.ResultStateRevision != r.AttemptStateRevision+1 || r.AuthorityFingerprint == "" || len(r.PriorObservation) != 64 || len(r.ResultObservation) != 64 || r.Verification != "satisfied" || r.CommittedAt.IsZero() { + return fmt.Errorf("receipt is missing exact instance, transition, revision, observation, authority, or verification facts") + } + if err := r.Program.Validate(); err != nil { + return err + } + if r.ObjectiveBinding != nil { + if err := r.ObjectiveBinding.Validate(); err != nil { + return err + } + } + if _, err := normalizeCapabilities(r.Capabilities); err != nil { + return err + } + if len(r.Effects) == 0 { + return fmt.Errorf("receipt contains no committed effect facts") + } + return nil +} + +type StalePrescriptionError struct{ Reason string } + +func (e StalePrescriptionError) Error() string { return "stale prescription: " + e.Reason } +func IsStale(err error) bool { var target StalePrescriptionError; return errors.As(err, &target) } + +type RecoveryRequiredError struct{ Reason string } + +func (e RecoveryRequiredError) Error() string { return "recovery required: " + e.Reason } +func IsRecoveryRequired(err error) bool { + var target RecoveryRequiredError + return errors.As(err, &target) +} + +func newPrescription(state ControlState, observation Observation, transition Transition, objective *Objective, authority authorityProjection, required []Capability) (Prescription, error) { + bindingFingerprint, err := Fingerprint(state.ObjectiveBinding) + if err != nil { + return Prescription{}, err + } + freshness, err := NewFreshness(state.InstanceID, state.Revision, state.Program.Fingerprint, observation.Fingerprint, bindingFingerprint, authority.Fingerprint) + if err != nil { + return Prescription{}, err + } + p := Prescription{SchemaVersion: PrescriptionSchemaVersion, TransitionID: transition.ID, Freshness: freshness, RequiredCapabilities: append([]Capability(nil), required...)} + p.ExpectedObjectiveBinding = cloneBinding(state.ObjectiveBinding) + if transition.ObjectiveMutation == BindObjectiveMutation { + binding, err := BindObjective(*objective) + if err != nil { + return Prescription{}, err + } + p.RequestedObjectiveBinding = &binding + } + identity := p + identity.ID = "" + id, err := contentHash(identity) + if err != nil { + return Prescription{}, err + } + p.ID = "prx-" + id + return p, nil +} + +func (p Prescription) validateCurrent(state ControlState, observation Observation, authority authorityProjection) error { + identity := p + want := identity.ID + identity.ID = "" + got, err := contentHash(identity) + if err != nil || want != "prx-"+got { + return StalePrescriptionError{Reason: "prescription content identity is invalid"} + } + bindingFingerprint, bindingErr := Fingerprint(state.ObjectiveBinding) + current, freshnessErr := NewFreshness(state.InstanceID, state.Revision, state.Program.Fingerprint, observation.Fingerprint, bindingFingerprint, authority.Fingerprint) + if p.SchemaVersion != PrescriptionSchemaVersion || bindingErr != nil || freshnessErr != nil || p.Freshness.Check(current) != nil || !equalBinding(p.ExpectedObjectiveBinding, state.ObjectiveBinding) { + return StalePrescriptionError{Reason: "instance, state, program, objective binding, observation, or authority changed"} + } + return nil +} + +func objectiveFor(state ControlState, supplied *Objective, transition Transition) (*Objective, string) { + if transition.ObjectiveMutation == BindObjectiveMutation { + if supplied == nil || supplied.Validate() != nil { + return nil, "objective binding requires one exact objective revision" + } + if state.ObjectiveBinding != nil && state.ObjectiveBinding.Matches(*supplied) { + return nil, "objective revision is already bound" + } + copy := *supplied + return ©, "" + } + if transition.ObjectiveMutation == ClearObjectiveMutation && state.ObjectiveBinding == nil { + return nil, "objective binding is already absent" + } + switch transition.ObjectiveScope { + case ObjectiveNone: + return nil, "" + case ObjectiveOptionalPreserve: + if state.ObjectiveBinding != nil && supplied != nil && state.ObjectiveBinding.Matches(*supplied) { + copy := *supplied + return ©, "" + } + // A command-scoped objective is not allowed to reinterpret durable + // supervisory state. Maintenance runs without an objective projection + // while its prescription remains bound to the exact existing binding. + return nil, "" + case ObjectiveBoundExact: + if state.ObjectiveBinding == nil || supplied == nil || !state.ObjectiveBinding.Matches(*supplied) { + return nil, "transition requires the exact bound objective revision" + } + copy := *supplied + return ©, "" + default: + return nil, "invalid objective scope" + } +} + +func validateEffects(transition Transition, effect Effect) error { + if len(effect.Facts) == 0 { + return fmt.Errorf("operator returned no committed effect facts") + } + owned := map[string]bool{} + for _, facet := range transition.OwnedFacets { + owned[facet] = true + } + for _, fact := range effect.Facts { + if !owned[fact.Facet] || !qualifiedSemanticID.MatchString(fact.Operation) || fact.Fingerprint == "" { + return fmt.Errorf("effect fact escapes transition-owned facets or is incomplete") + } + } + return nil +} + +func missingCapabilities(required, available []Capability) []Capability { + set := map[Capability]bool{} + for _, value := range available { + set[value] = true + } + var result []Capability + for _, value := range required { + if !set[value] { + result = append(result, value) + } + } + return result +} + +func requiredCapabilities(classifier CapabilityClassifier, transition Transition) ([]Capability, error) { + minimum, err := classifier.RequiredCapabilities(transition) + if err != nil { + return nil, err + } + return normalizeCapabilities(append(append([]Capability(nil), transition.RequiredCapabilities...), minimum...)) +} + +func contains(values []string, wanted string) bool { + for _, value := range values { + if value == wanted { + return true + } + } + return false +} +func containsCapability(values []Capability, wanted Capability) bool { + for _, value := range values { + if value == wanted { + return true + } + } + return false +} +func cloneBinding(value *ObjectiveBinding) *ObjectiveBinding { + if value == nil { + return nil + } + copy := *value + return © +} +func equalBinding(left, right *ObjectiveBinding) bool { + if left == nil || right == nil { + return left == nil && right == nil + } + return *left == *right +} diff --git a/boatstack/kernel/runtime_test.go b/boatstack/kernel/runtime_test.go new file mode 100644 index 0000000..c66ce19 --- /dev/null +++ b/boatstack/kernel/runtime_test.go @@ -0,0 +1,483 @@ +package kernel + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "testing" + "time" +) + +type integerDomain struct { + mu sync.Mutex + value int + incrementExecutions int + failAfterIncrement bool + panicAfterIncrement bool +} + +func (d *integerDomain) Observe(context.Context, string) (Observation, error) { + d.mu.Lock() + defer d.mu.Unlock() + return NewObservation(struct { + Value int `json:"value"` + }{d.value}) +} + +func (d *integerDomain) Admissible(_ context.Context, evaluation Evaluation) (bool, string, error) { + var observed struct { + Value int `json:"value"` + } + if err := json.Unmarshal(evaluation.Observation.Value, &observed); err != nil { + return false, "", err + } + switch evaluation.Transition.Operation { + case "objective.bind": + return evaluation.State.ObjectiveBinding == nil && evaluation.Objective != nil, "objective is not yet bound", nil + case "counter.increment": + if evaluation.Objective == nil { + return false, "exact objective required", nil + } + return observed.Value < 2, "value is below objective", nil + case "counter.inspect": + return true, "inspection is always available", nil + case "counter.reset": + return observed.Value > 0 || evaluation.State.Recovery != nil, "value is nonzero or recovery remains active", nil + default: + return false, "unknown transition", nil + } +} + +func (d *integerDomain) Verify(_ context.Context, evaluation Evaluation, effect Effect, target Observation) error { + var before, after struct { + Value int `json:"value"` + } + if err := json.Unmarshal(evaluation.Observation.Value, &before); err != nil { + return err + } + if err := json.Unmarshal(target.Value, &after); err != nil { + return err + } + switch evaluation.Transition.Operation { + case "objective.bind": + if before.Value != after.Value { + return fmt.Errorf("objective binding changed domain state") + } + case "counter.increment": + if after.Value != before.Value+1 { + return fmt.Errorf("increment postcondition failed") + } + case "counter.reset": + if after.Value != 0 { + return fmt.Errorf("reset postcondition failed") + } + } + return nil +} + +type integerOperator struct{ domain *integerDomain } + +func (o integerOperator) Execute(_ context.Context, operation Operation) (Effect, error) { + o.domain.mu.Lock() + defer o.domain.mu.Unlock() + switch operation.Transition.Operation { + case "objective.bind": + case "counter.increment": + o.domain.incrementExecutions++ + o.domain.value++ + if o.domain.panicAfterIncrement { + o.domain.panicAfterIncrement = false + panic("simulated process panic") + } + if o.domain.failAfterIncrement { + o.domain.failAfterIncrement = false + return Effect{}, fmt.Errorf("simulated interrupted operator") + } + case "counter.reset": + o.domain.value = 0 + case "counter.inspect": + default: + return Effect{}, fmt.Errorf("unknown operation") + } + facet := "counter.value" + if operation.Transition.Operation == "objective.bind" { + facet = "supervisor.objective" + } + return Effect{Facts: []EffectFact{{Facet: facet, Operation: operation.Transition.Operation, Fingerprint: fmt.Sprintf("value-%d", o.domain.value)}}}, nil +} + +type memoryStateStore struct { + mu sync.Mutex + state ControlState + receipts *memoryReceipts + commitFailures int +} + +func (s *memoryStateStore) Load(context.Context, string) (ControlState, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.state, nil +} +func (s *memoryStateStore) BeginEffect(_ context.Context, revision uint64, target ControlState) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.state.Revision != revision { + return fmt.Errorf("stale revision") + } + s.state = target + return nil +} +func (s *memoryStateStore) CommitTransition(_ context.Context, revision uint64, target ControlState, receipt Receipt) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.commitFailures > 0 { + s.commitFailures-- + return fmt.Errorf("simulated atomic transaction failure") + } + if s.state.Revision != revision { + return fmt.Errorf("stale revision") + } + s.state = target + s.receipts.values = append(s.receipts.values, receipt) + return nil +} + +type memoryReceipts struct{ values []Receipt } + +type memoryLock struct{ mu *sync.Mutex } + +func (l memoryLock) Unlock() error { l.mu.Unlock(); return nil } + +type memoryLocker struct{ mu sync.Mutex } + +func (l *memoryLocker) Acquire(context.Context, string) (Lock, error) { + l.mu.Lock() + return memoryLock{&l.mu}, nil +} + +type fixedClock struct{ now time.Time } + +func (c fixedClock) Now() time.Time { return c.now } + +type integerCapabilities struct{} + +func (integerCapabilities) RequiredCapabilities(transition Transition) ([]Capability, error) { + switch transition.Operation { + case "objective.bind": + return []Capability{"objective.bind"}, nil + case "counter.increment": + return []Capability{"counter.increment"}, nil + case "counter.reset": + return []Capability{"counter.reset"}, nil + default: + return nil, fmt.Errorf("unclassified operation %q", transition.Operation) + } +} + +func integerProgram(t *testing.T) Program { + t.Helper() + program, err := CompileProgram("integer-control", "1.0.0", "kernel-v1", "unbound", []string{"two"}, []Transition{ + {ID: "objective.bind", SourceModes: []string{"unbound"}, TargetMode: "zero", ObjectiveScope: ObjectiveNone, ObjectiveMutation: BindObjectiveMutation, RequiredCapabilities: []Capability{"objective.bind"}, OwnedFacets: []string{"supervisor.objective"}, Operation: "objective.bind", Priority: 5}, + {ID: "counter.increment-first", SourceModes: []string{"zero"}, TargetMode: "one", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, + {ID: "counter.increment-second", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, + {ID: "counter.reset", SourceModes: []string{"one", "two"}, TargetMode: "zero", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 20}, + {ID: "objective.recover", SourceModes: []string{"unbound"}, TargetMode: "unbound", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"objective.bind"}}, + {ID: "counter.recover", SourceModes: []string{"zero", "one", "two"}, TargetMode: "zero", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}}, + }) + if err != nil { + t.Fatal(err) + } + return program +} + +func newIntegerRuntime(t *testing.T, bound bool) (Runtime, *memoryStateStore, *memoryReceipts, *integerDomain, Objective, Authority) { + t.Helper() + program := integerProgram(t) + objective, err := NewObjective("reach-two", 1, map[string]int{"value": 2}) + if err != nil { + t.Fatal(err) + } + state := ControlState{InstanceID: "counter-fixture", Program: program.Identity(), Mode: "unbound", Revision: 1} + if bound { + binding, _ := BindObjective(objective) + state.ObjectiveBinding = &binding + state.Mode = "zero" + } + receipts, domain := &memoryReceipts{}, &integerDomain{} + states := &memoryStateStore{state: state, receipts: receipts} + now := time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC) + authority := Authority{Receipts: []AuthorityReceipt{{ID: "human-counter", Subject: "fixture", Fingerprint: "fixture-authority", Capabilities: []Capability{"objective.bind", "counter.increment", "counter.reset"}, IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)}}} + runtime, err := NewRuntime(program, domain, integerOperator{domain}, integerCapabilities{}, states, &memoryLocker{}, fixedClock{now}) + if err != nil { + t.Fatal(err) + } + return runtime, states, receipts, domain, objective, authority +} + +func TestDeterministicNonSoftwareProgramReachesMarkedState(t *testing.T) { + runtime, states, receipts, _, objective, authority := newIntegerRuntime(t, false) + ctx := context.Background() + for _, transition := range []string{"objective.bind", "counter.increment-first", "counter.increment-second"} { + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: transition}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("resolve %s: %#v %v", transition, resolution.Decision, err) + } + receipt, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: transition}, Prescription: *resolution.Prescription}) + if err != nil { + t.Fatal(err) + } + if receipt.Verification != "satisfied" || receipt.Program.Fingerprint == "" || receipt.AttemptStateRevision != receipt.PriorStateRevision+1 || receipt.ResultStateRevision != receipt.AttemptStateRevision+1 { + t.Fatalf("incomplete receipt: %#v", receipt) + } + } + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || resolution.Decision.Kind != Marked { + t.Fatalf("marked resolve: %#v %v", resolution.Decision, err) + } + if states.state.Revision != 7 || len(receipts.values) != 3 { + t.Fatalf("state/receipts = %d/%d", states.state.Revision, len(receipts.values)) + } +} + +func TestObjectiveRevisionInvalidatesPrescriptionBeforeEffects(t *testing.T) { + runtime, states, _, domain, objective, authority := newIntegerRuntime(t, true) + ctx := context.Background() + resolution, _ := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + revised, _ := NewObjective("reach-two", 2, map[string]int{"value": 3}) + binding, _ := BindObjective(revised) + states.state.ObjectiveBinding = &binding + _, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &revised, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + if !IsStale(err) || domain.value != 0 { + t.Fatalf("err/value = %v/%d", err, domain.value) + } +} + +func TestAuthorityDenialAndObjectiveAbsenceFailClosed(t *testing.T) { + runtime, _, _, domain, objective, _ := newIntegerRuntime(t, true) + resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Frontier { + t.Fatalf("decision/error = %#v/%v", resolution.Decision, err) + } + if domain.value != 0 { + t.Fatal("refused resolution mutated domain") + } +} + +func TestFutureAuthorityReceiptFailsClosedBeforeEffects(t *testing.T) { + runtime, _, _, domain, objective, authority := newIntegerRuntime(t, true) + authority.Receipts[0].IssuedAt = time.Date(2026, 8, 12, 10, 0, 1, 0, time.UTC) + resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Refused || domain.value != 0 || domain.incrementExecutions != 0 { + t.Fatalf("decision/value/executions/error = %#v/%d/%d/%v", resolution.Decision, domain.value, domain.incrementExecutions, err) + } +} + +func TestUntargetedAndTargetedResolutionShareOneRelation(t *testing.T) { + runtime, _, _, _, objective, authority := newIntegerRuntime(t, true) + ctx := context.Background() + untargeted, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || untargeted.Decision.Kind != Prescribed { + t.Fatalf("untargeted: %#v %v", untargeted.Decision, err) + } + targeted, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: untargeted.Decision.Transition}) + if err != nil || targeted.Decision.Kind != Prescribed || targeted.Prescription.ID != untargeted.Prescription.ID { + t.Fatalf("targeted: %#v %v", targeted.Decision, err) + } + if _, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: targeted.Decision.Transition}, Prescription: *targeted.Prescription}); err != nil { + t.Fatalf("prescribed transition was rejected by apply: %v", err) + } +} + +func TestObservationChangeMakesPrescriptionStaleBeforeOperator(t *testing.T) { + runtime, _, _, domain, objective, authority := newIntegerRuntime(t, true) + ctx := context.Background() + resolution, _ := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + domain.value = 1 + _, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + if !IsStale(err) || domain.value != 1 { + t.Fatalf("err/value = %v/%d", err, domain.value) + } +} + +type strictCapabilities struct{ integerCapabilities } + +func (strictCapabilities) RequiredCapabilities(transition Transition) ([]Capability, error) { + base, err := integerCapabilities{}.RequiredCapabilities(transition) + if transition.Operation == "counter.increment" { + base = append(base, "counter.audit") + } + return base, err +} + +func TestTrustedCapabilityClassifierCannotBeWeakenedByProgram(t *testing.T) { + program := integerProgram(t) + objective, _ := NewObjective("reach-two", 1, map[string]int{"value": 2}) + binding, _ := BindObjective(objective) + receipts, domain := &memoryReceipts{}, &integerDomain{} + states := &memoryStateStore{state: ControlState{InstanceID: "counter-fixture", Program: program.Identity(), ObjectiveBinding: &binding, Mode: "zero", Revision: 1}, receipts: receipts} + now := time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC) + authority := Authority{Receipts: []AuthorityReceipt{{ID: "program-declared-only", Subject: "fixture", Fingerprint: "authority", Capabilities: []Capability{"counter.increment"}, IssuedAt: now.Add(-time.Minute)}}} + runtime, err := NewRuntime(program, domain, integerOperator{domain}, strictCapabilities{}, states, &memoryLocker{}, fixedClock{now}) + if err != nil { + t.Fatal(err) + } + resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Frontier || domain.value != 0 { + t.Fatalf("decision/value/error = %#v/%d/%v", resolution.Decision, domain.value, err) + } +} + +func TestOptionalMaintenancePreservesObjectiveAbsence(t *testing.T) { + runtime, states, _, domain, _, authority := newIntegerRuntime(t, false) + states.state.Mode, domain.value = "one", 1 + commandObjective, _ := NewObjective("unbound-command", 1, map[string]int{"value": 0}) + resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &commandObjective, Authority: authority, Requested: "counter.reset"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("decision/error = %#v/%v", resolution.Decision, err) + } + _, err = runtime.Apply(context.Background(), ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &commandObjective, Authority: authority, Requested: "counter.reset"}, Prescription: *resolution.Prescription}) + if err != nil || states.state.ObjectiveBinding != nil { + t.Fatalf("maintenance synthesized objective: %#v %v", states.state.ObjectiveBinding, err) + } +} + +func TestOptionalMaintenancePreservesExactBinding(t *testing.T) { + runtime, states, _, domain, objective, authority := newIntegerRuntime(t, true) + domain.value = 1 + states.state.Mode = "one" + conflicting, _ := NewObjective("other", 1, map[string]int{"value": 0}) + before := *states.state.ObjectiveBinding + resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &conflicting, Authority: authority, Requested: "counter.reset"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("decision/error = %#v/%v", resolution.Decision, err) + } + _, err = runtime.Apply(context.Background(), ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &conflicting, Authority: authority, Requested: "counter.reset"}, Prescription: *resolution.Prescription}) + if err != nil { + t.Fatal(err) + } + if states.state.ObjectiveBinding == nil || *states.state.ObjectiveBinding != before { + t.Fatal("maintenance changed objective binding") + } + _ = objective +} + +func TestInterruptedOperatorRequiresAndCompletesExplicitRecovery(t *testing.T) { + runtime, states, _, domain, objective, authority := newIntegerRuntime(t, true) + domain.failAfterIncrement = true + ctx := context.Background() + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("resolve: %#v %v", resolution.Decision, err) + } + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + if !IsRecoveryRequired(err) || states.state.Recovery == nil || states.state.Revision != 2 || domain.value != 1 { + t.Fatalf("recovery state: %#v value=%d err=%v", states.state, domain.value, err) + } + recovery, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || recovery.Decision.Kind != Prescribed || recovery.Decision.Transition != "counter.recover" { + t.Fatalf("recovery resolve: %#v %v", recovery.Decision, err) + } + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.recover"}, Prescription: *recovery.Prescription}) + if err != nil || states.state.Recovery != nil || states.state.Revision != 4 || domain.value != 0 { + t.Fatalf("recovery result: %#v value=%d err=%v", states.state, domain.value, err) + } +} + +func TestAtomicTransactionFailureEntersRecoveryWithoutDuplicateEffect(t *testing.T) { + runtime, store, receipts, domain, objective, authority := newIntegerRuntime(t, true) + store.commitFailures = 1 + ctx := context.Background() + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("resolve: %#v %v", resolution.Decision, err) + } + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + if !IsRecoveryRequired(err) { + t.Fatalf("apply error = %v", err) + } + if store.state.Mode != "zero" || store.state.Recovery == nil || len(receipts.values) != 0 { + t.Fatalf("non-atomic store result: state=%#v receipts=%d", store.state, len(receipts.values)) + } + if domain.value != 1 || domain.incrementExecutions != 1 { + t.Fatalf("operator value/executions = %d/%d", domain.value, domain.incrementExecutions) + } + recovery, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || recovery.Decision.Kind != Prescribed || recovery.Decision.Transition != "counter.recover" { + t.Fatalf("recovery resolve: %#v %v", recovery.Decision, err) + } + if domain.incrementExecutions != 1 { + t.Fatalf("original operator ran %d times", domain.incrementExecutions) + } +} + +func TestProcessPanicLeavesDurableRecoveryBeforeEffectReplay(t *testing.T) { + runtime, store, _, domain, objective, authority := newIntegerRuntime(t, true) + domain.panicAfterIncrement = true + ctx := context.Background() + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("resolve: %#v %v", resolution.Decision, err) + } + var recovered any + func() { + defer func() { recovered = recover() }() + _, _ = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + }() + if recovered == nil || store.state.Recovery == nil || store.state.Recovery.TransitionID != "counter.increment-first" || store.state.Revision != 2 || domain.value != 1 || domain.incrementExecutions != 1 { + t.Fatalf("panic/recovery state: panic=%v state=%#v value=%d executions=%d", recovered, store.state, domain.value, domain.incrementExecutions) + } + next, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || next.Decision.Kind != Prescribed || next.Decision.Transition != "counter.recover" || domain.incrementExecutions != 1 { + t.Fatalf("post-panic resolution: %#v executions=%d error=%v", next.Decision, domain.incrementExecutions, err) + } +} + +func TestFailedRecoveryAttemptPreservesOriginalRecoveryObligation(t *testing.T) { + runtime, store, _, domain, objective, authority := newIntegerRuntime(t, true) + domain.failAfterIncrement = true + ctx := context.Background() + + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("resolve: %#v %v", resolution.Decision, err) + } + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + if !IsRecoveryRequired(err) || store.state.Recovery == nil { + t.Fatalf("initial recovery: state=%#v err=%v", store.state, err) + } + original := *store.state.Recovery + + recovery, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || recovery.Decision.Kind != Prescribed || recovery.Decision.Transition != "counter.recover" { + t.Fatalf("first recovery resolve: %#v %v", recovery.Decision, err) + } + store.commitFailures = 1 + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.recover"}, Prescription: *recovery.Prescription}) + if !IsRecoveryRequired(err) || store.state.Recovery == nil || *store.state.Recovery != original || domain.value != 0 { + t.Fatalf("failed recovery attempt: state=%#v value=%d err=%v", store.state, domain.value, err) + } + + retry, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) + if err != nil || retry.Decision.Kind != Prescribed || retry.Decision.Transition != "counter.recover" { + t.Fatalf("recovery retry resolve: %#v %v", retry.Decision, err) + } + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.recover"}, Prescription: *retry.Prescription}) + if err != nil || store.state.Recovery != nil || store.state.Mode != "zero" || store.state.Revision != 5 { + t.Fatalf("recovery retry result: state=%#v err=%v", store.state, err) + } +} + +func TestPrescriptionCannotReplayAcrossControlInstances(t *testing.T) { + runtime, store, _, domain, objective, authority := newIntegerRuntime(t, true) + ctx := context.Background() + resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) + if err != nil || resolution.Decision.Kind != Prescribed { + t.Fatalf("resolve: %#v %v", resolution.Decision, err) + } + store.state.InstanceID = "counter-other" + _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-other", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) + if !IsStale(err) || domain.value != 0 || domain.incrementExecutions != 0 { + t.Fatalf("cross-instance apply: value=%d executions=%d err=%v", domain.value, domain.incrementExecutions, err) + } +} diff --git a/boatstack/kernel/types.go b/boatstack/kernel/types.go new file mode 100644 index 0000000..694e8bc --- /dev/null +++ b/boatstack/kernel/types.go @@ -0,0 +1,249 @@ +// Package kernel implements Boatstack's domain-neutral supervisory-control +// mechanism. Domain packages supply observations, transition predicates, and +// operators; the kernel owns freshness, authority, selection, verification, +// state revision, and receipts. +package kernel + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "regexp" + "sort" + "time" +) + +const ( + ProgramSchemaVersion = 1 + PrescriptionSchemaVersion = 2 + ReceiptSchemaVersion = 3 +) + +var ( + semanticID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) + qualifiedSemanticID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*$`) +) + +// Objective is an external reference. It is not supervisory state. +type Objective struct { + ID string `json:"id"` + Revision uint64 `json:"revision"` + Fingerprint string `json:"fingerprint"` + Reference json.RawMessage `json:"reference"` +} + +func NewObjective(id string, revision uint64, reference any) (Objective, error) { + encoded, err := json.Marshal(reference) + if err != nil { + return Objective{}, err + } + objective := Objective{ID: id, Revision: revision, Reference: encoded} + fingerprint, err := contentHash(struct { + ID string `json:"id"` + Revision uint64 `json:"revision"` + Reference json.RawMessage `json:"reference"` + }{objective.ID, objective.Revision, objective.Reference}) + if err != nil { + return Objective{}, err + } + objective.Fingerprint = fingerprint + return objective, objective.Validate() +} + +func (o Objective) Validate() error { + if !semanticID.MatchString(o.ID) || o.Revision == 0 || len(o.Fingerprint) != 64 || len(o.Reference) == 0 || !json.Valid(o.Reference) { + return fmt.Errorf("objective requires semantic identity, positive revision, canonical reference, and fingerprint") + } + fingerprint, err := contentHash(struct { + ID string `json:"id"` + Revision uint64 `json:"revision"` + Reference json.RawMessage `json:"reference"` + }{o.ID, o.Revision, o.Reference}) + if err != nil || fingerprint != o.Fingerprint { + return fmt.Errorf("objective fingerprint does not identify its exact revision") + } + return nil +} + +// ObjectiveBinding is the only objective material retained in supervisory +// state. It binds an exact immutable objective revision. +type ObjectiveBinding struct { + ObjectiveID string `json:"objective_id"` + ObjectiveRevision uint64 `json:"objective_revision"` + ObjectiveFingerprint string `json:"objective_fingerprint"` +} + +func BindObjective(objective Objective) (ObjectiveBinding, error) { + if err := objective.Validate(); err != nil { + return ObjectiveBinding{}, err + } + return ObjectiveBinding{objective.ID, objective.Revision, objective.Fingerprint}, nil +} + +func (b ObjectiveBinding) Validate() error { + if !semanticID.MatchString(b.ObjectiveID) || b.ObjectiveRevision == 0 || len(b.ObjectiveFingerprint) != 64 { + return fmt.Errorf("objective binding requires exact identity, revision, and fingerprint") + } + return nil +} + +func (b ObjectiveBinding) Matches(objective Objective) bool { + return objective.Validate() == nil && b.ObjectiveID == objective.ID && b.ObjectiveRevision == objective.Revision && b.ObjectiveFingerprint == objective.Fingerprint +} + +type ProgramIdentity struct { + ID string `json:"id"` + Version string `json:"version"` + Fingerprint string `json:"fingerprint"` +} + +func (p ProgramIdentity) Validate() error { + if !semanticID.MatchString(p.ID) || p.Version == "" || len(p.Fingerprint) != 64 { + return fmt.Errorf("program identity requires id, version, and fingerprint") + } + return nil +} + +// ControlState is durable supervisory state. Domain state is deliberately not +// embedded here; it is supplied as a canonical observation by a Domain. +type ControlState struct { + InstanceID string `json:"instance_id"` + Program ProgramIdentity `json:"program"` + ObjectiveBinding *ObjectiveBinding `json:"objective_binding,omitempty"` + Mode string `json:"mode"` + Revision uint64 `json:"revision"` + Recovery *RecoveryState `json:"recovery,omitempty"` +} + +type RecoveryState struct { + PrescriptionID string `json:"prescription_id"` + TransitionID string `json:"transition_id"` + Reason string `json:"reason"` +} + +func (s ControlState) Validate() error { + if !semanticID.MatchString(s.InstanceID) || s.Mode == "" || s.Revision == 0 { + return fmt.Errorf("control state requires instance, mode, and positive revision") + } + if err := s.Program.Validate(); err != nil { + return err + } + if s.ObjectiveBinding != nil { + if err := s.ObjectiveBinding.Validate(); err != nil { + return err + } + } + if s.Recovery != nil && (s.Recovery.PrescriptionID == "" || !qualifiedSemanticID.MatchString(s.Recovery.TransitionID) || s.Recovery.Reason == "") { + return fmt.Errorf("recovery state is incomplete") + } + return nil +} + +type ObjectiveScope string + +const ( + ObjectiveNone ObjectiveScope = "none" + ObjectiveOptionalPreserve ObjectiveScope = "optional-preserve" + ObjectiveBoundExact ObjectiveScope = "bound-exact" +) + +func (s ObjectiveScope) Valid() bool { + return s == ObjectiveNone || s == ObjectiveOptionalPreserve || s == ObjectiveBoundExact +} + +type Capability string + +func (c Capability) Validate() error { + if !semanticID.MatchString(string(c)) { + return fmt.Errorf("capability %q is not a semantic identifier", c) + } + return nil +} + +type AuthorityReceipt struct { + ID string `json:"id"` + Subject string `json:"subject"` + Fingerprint string `json:"fingerprint"` + Capabilities []Capability `json:"capabilities"` + IssuedAt time.Time `json:"issued_at"` + ExpiresAt time.Time `json:"expires_at,omitempty"` +} + +func (r AuthorityReceipt) Validate(now time.Time) error { + if !semanticID.MatchString(r.ID) || r.Subject == "" || r.Fingerprint == "" || r.IssuedAt.IsZero() || r.IssuedAt.After(now) || (!r.ExpiresAt.IsZero() && !now.Before(r.ExpiresAt)) { + return fmt.Errorf("authority receipt %q is invalid or expired", r.ID) + } + if len(r.Capabilities) == 0 { + return fmt.Errorf("authority receipt %q grants no capabilities", r.ID) + } + _, err := normalizeCapabilities(r.Capabilities) + return err +} + +type Authority struct { + Receipts []AuthorityReceipt `json:"receipts"` +} + +func (a Authority) projection(now time.Time) (authorityProjection, error) { + receipts := append([]AuthorityReceipt(nil), a.Receipts...) + sort.Slice(receipts, func(i, j int) bool { return receipts[i].ID < receipts[j].ID }) + seen := map[string]bool{} + var capabilities []Capability + for _, receipt := range receipts { + if err := receipt.Validate(now); err != nil { + return authorityProjection{}, err + } + if seen[receipt.ID] { + return authorityProjection{}, fmt.Errorf("authority receipt %q is duplicated", receipt.ID) + } + seen[receipt.ID] = true + capabilities = append(capabilities, receipt.Capabilities...) + } + capabilities, err := normalizeCapabilities(capabilities) + if err != nil { + return authorityProjection{}, err + } + fingerprint, err := contentHash(receipts) + return authorityProjection{Fingerprint: fingerprint, Capabilities: capabilities}, err +} + +type authorityProjection struct { + Fingerprint string + Capabilities []Capability +} + +type EffectFact struct { + Facet string `json:"facet"` + Operation string `json:"operation"` + Fingerprint string `json:"fingerprint"` +} + +type Effect struct { + Facts []EffectFact `json:"facts"` +} + +func normalizeCapabilities(values []Capability) ([]Capability, error) { + seen := map[Capability]bool{} + for _, value := range values { + if err := value.Validate(); err != nil { + return nil, err + } + seen[value] = true + } + result := make([]Capability, 0, len(seen)) + for value := range seen { + result = append(result, value) + } + sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + return result, nil +} + +func contentHash(value any) (string, error) { + encoded, err := json.Marshal(value) + if err != nil { + return "", err + } + digest := sha256.Sum256(encoded) + return hex.EncodeToString(digest[:]), nil +} diff --git a/boatstack/program_effects.go b/boatstack/program_effects.go index 2271d3e..bf6a897 100644 --- a/boatstack/program_effects.go +++ b/boatstack/program_effects.go @@ -7,16 +7,16 @@ import ( "fmt" "io" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/effects" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type programEffectDriver struct { base ports.EffectDriver - program control.ControlProgram + program delivery.ControlProgram resolver ports.InvocationResolver clock ports.Clock } @@ -35,7 +35,7 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm } if transition.Origin.Kind == catalog.OriginControlProgram { flow := d.program.ProgramRuntime() - if flow.Manifest.RuntimeMode == control.ProgramRuntimeNative { + if flow.Manifest.RuntimeMode == delivery.ProgramRuntimeNative { return d.base.Prepare(ctx, admission, transition) } if flow.Runtime == nil { @@ -45,15 +45,15 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm if err != nil { return nil, err } - request := control.ProgramRuntimeRequest{ - ProtocolVersion: control.ProgramRuntimeProtocolVersion, ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version, + request := delivery.ProgramRuntimeRequest{ + ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version, ProgramFingerprint: admission.ExpectedProgramFingerprint, CorrelationID: admission.Invocation.Correlation, RepositoryRoot: admission.Invocation.InvokingPath, TransitionID: transition.ID, Parameters: parameters, Settings: flow.Manifest.Settings, - Capabilities: append([]control.Capability(nil), admission.EffectiveCapabilities...), + Capabilities: append([]delivery.Capability(nil), admission.EffectiveCapabilities...), } if transition.Class == catalog.EventOwnedExternal { prepared, err := effects.NewExtensionExternalPrepared(func(executionContext context.Context) (ports.EffectResult, error) { - request.Operation = control.ProgramExecuteExternalOperation + request.Operation = delivery.ProgramExecuteExternalOperation response, invokeErr := flow.Runtime.InvokeProgram(executionContext, request) if invokeErr != nil { return ports.EffectResult{}, invokeErr @@ -68,9 +68,9 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm } return effects.BindStateRevision(ctx, prepared, d.resolver, d.clock, admission, transition) } - operation := control.ProgramPlanLocalEffectOperation + operation := delivery.ProgramPlanLocalEffectOperation if transition.Class == catalog.EventRecovery { - operation = control.ProgramRecoverOperation + operation = delivery.ProgramRecoverOperation } request.Operation = operation response, invokeErr := flow.Runtime.InvokeProgram(ctx, request) @@ -97,16 +97,16 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm if err != nil { return nil, err } - baseRequest := control.ExtensionRequest{ - ProtocolVersion: control.ExtensionProtocolVersion, ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version, + baseRequest := delivery.ExtensionRequest{ + ProtocolVersion: delivery.ExtensionProtocolVersion, ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version, ProgramFingerprint: admission.ExpectedProgramFingerprint, CorrelationID: admission.Invocation.Correlation, RepositoryRoot: admission.Invocation.InvokingPath, TransitionID: transition.ID, Parameters: parameters, Settings: extension.Manifest.Settings, - Capabilities: append([]control.Capability(nil), admission.EffectiveCapabilities...), + Capabilities: append([]delivery.Capability(nil), admission.EffectiveCapabilities...), } if transition.Class == catalog.EventOwnedExternal { prepared, err := effects.NewExtensionExternalPrepared(func(executionContext context.Context) (ports.EffectResult, error) { request := baseRequest - request.Operation = control.ExtensionExecuteExternalOperation + request.Operation = delivery.ExtensionExecuteExternalOperation response, invokeErr := extension.Runtime.Invoke(executionContext, request) if invokeErr != nil { return ports.EffectResult{}, invokeErr @@ -121,9 +121,9 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm } return effects.BindStateRevision(ctx, prepared, d.resolver, d.clock, admission, transition) } - operation := control.ExtensionPlanLocalEffectOperation + operation := delivery.ExtensionPlanLocalEffectOperation if transition.Class == catalog.EventRecovery { - operation = control.ExtensionRecoverOperation + operation = delivery.ExtensionRecoverOperation } baseRequest.Operation = operation response, err := extension.Runtime.Invoke(ctx, baseRequest) @@ -143,7 +143,7 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm return effects.BindStateRevision(ctx, prepared, d.resolver, d.clock, admission, transition) } -func validateProgramWrites(program control.ControlProgram, transition catalog.Transition, owner string, writes []control.ResourceWrite) error { +func validateProgramWrites(program delivery.ControlProgram, transition catalog.Transition, owner string, writes []delivery.ResourceWrite) error { allowed := map[string]bool{} for _, resource := range transition.OwnedResources { allowed[resource] = true diff --git a/boatstack/program_observer.go b/boatstack/program_observer.go index e0dbd7a..521cf66 100644 --- a/boatstack/program_observer.go +++ b/boatstack/program_observer.go @@ -5,16 +5,16 @@ import ( "encoding/json" "fmt" - "github.com/operatorstack/boatstack/boatstack/control" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" + "github.com/operatorstack/boatstack/boatstack/delivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" ) type programObserver struct { base ports.Observer - program control.ControlProgram + program delivery.ControlProgram } // ComponentRuntimeError preserves a bounded protocol error classification and @@ -36,7 +36,7 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR return model.Observation{}, err } flow := o.program.ProgramRuntime() - if flow.Manifest.RuntimeMode == control.ProgramRuntimeProtocol { + if flow.Manifest.RuntimeMode == delivery.ProgramRuntimeProtocol { if flow.Runtime == nil { return model.Observation{}, fmt.Errorf("program runtime %q observer is unavailable", flow.Identity.ID) } @@ -49,8 +49,8 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if encodeErr != nil { return model.Observation{}, fmt.Errorf("encode bounded control-program observation: %w", encodeErr) } - response, invokeErr := flow.Runtime.InvokeProgram(ctx, control.ProgramRuntimeRequest{ - ProtocolVersion: control.ProgramRuntimeProtocolVersion, Operation: control.ProgramObserveOperation, + response, invokeErr := flow.Runtime.InvokeProgram(ctx, delivery.ProgramRuntimeRequest{ + ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, Operation: delivery.ProgramObserveOperation, ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version, ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation, RepositoryRoot: request.Invocation.InvokingPath, Snapshot: snapshot, Settings: flow.Manifest.Settings, @@ -59,7 +59,7 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if invokeErr != nil { return model.Observation{}, fmt.Errorf("program runtime %q observation failed: %w", flow.Identity.ID, invokeErr) } - if err := validateProgramRuntimeResponse(flow, control.ProgramObserveOperation, request.Invocation.Correlation, response); err != nil { + if err := validateProgramRuntimeResponse(flow, delivery.ProgramObserveOperation, request.Invocation.Correlation, response); err != nil { return model.Observation{}, err } declared := make(map[string]bool, len(flow.Manifest.Facts)) @@ -114,8 +114,8 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if err != nil { return model.Observation{}, fmt.Errorf("encode bounded extension observation: %w", err) } - response, err := extension.Runtime.Invoke(ctx, control.ExtensionRequest{ - ProtocolVersion: control.ExtensionProtocolVersion, Operation: control.ExtensionObserveOperation, + response, err := extension.Runtime.Invoke(ctx, delivery.ExtensionRequest{ + ProtocolVersion: delivery.ExtensionProtocolVersion, Operation: delivery.ExtensionObserveOperation, ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version, ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation, RepositoryRoot: request.Invocation.InvokingPath, Snapshot: snapshot, Settings: extension.Manifest.Settings, @@ -124,7 +124,7 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if err != nil { return model.Observation{}, fmt.Errorf("extension %q observation failed: %w", extension.Identity.ID, err) } - if err := validateExtensionResponse(extension, control.ExtensionObserveOperation, request.Invocation.Correlation, response); err != nil { + if err := validateExtensionResponse(extension, delivery.ExtensionObserveOperation, request.Invocation.Correlation, response); err != nil { return model.Observation{}, err } declared := make(map[string]bool, len(extension.Manifest.Facts)) @@ -156,7 +156,7 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR } if request.VerifyTransitionID != "" { transition, ok := o.program.RuntimeRegistry().Lookup(request.VerifyTransitionID) - if ok && transition.Origin.Kind == catalog.OriginControlProgram && flow.Manifest.RuntimeMode == control.ProgramRuntimeProtocol { + if ok && transition.Origin.Kind == catalog.OriginControlProgram && flow.Manifest.RuntimeMode == delivery.ProgramRuntimeProtocol { if err := requireComponentRuntimeCapabilities(request.Capabilities, flow.Manifest.Capabilities, flow.Identity.ID); err != nil { return model.Observation{}, err } @@ -164,8 +164,8 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if encodeErr != nil { return model.Observation{}, encodeErr } - response, invokeErr := flow.Runtime.InvokeProgram(ctx, control.ProgramRuntimeRequest{ - ProtocolVersion: control.ProgramRuntimeProtocolVersion, Operation: control.ProgramVerifyOperation, + response, invokeErr := flow.Runtime.InvokeProgram(ctx, delivery.ProgramRuntimeRequest{ + ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, Operation: delivery.ProgramVerifyOperation, ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version, ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation, RepositoryRoot: request.Invocation.InvokingPath, TransitionID: transition.ID, Snapshot: snapshot, Settings: flow.Manifest.Settings, @@ -174,7 +174,7 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if invokeErr != nil { return model.Observation{}, invokeErr } - if err := validateProgramRuntimeResponse(flow, control.ProgramVerifyOperation, request.Invocation.Correlation, response); err != nil { + if err := validateProgramRuntimeResponse(flow, delivery.ProgramVerifyOperation, request.Invocation.Correlation, response); err != nil { return model.Observation{}, err } if response.Verified == nil || !*response.Verified { @@ -193,8 +193,8 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if err != nil { return model.Observation{}, err } - response, err := extension.Runtime.Invoke(ctx, control.ExtensionRequest{ - ProtocolVersion: control.ExtensionProtocolVersion, Operation: control.ExtensionVerifyOperation, + response, err := extension.Runtime.Invoke(ctx, delivery.ExtensionRequest{ + ProtocolVersion: delivery.ExtensionProtocolVersion, Operation: delivery.ExtensionVerifyOperation, ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version, ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation, RepositoryRoot: request.Invocation.InvokingPath, TransitionID: transition.ID, Snapshot: snapshot, Settings: extension.Manifest.Settings, @@ -203,7 +203,7 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR if err != nil { return model.Observation{}, err } - if err := validateExtensionResponse(extension, control.ExtensionVerifyOperation, request.Invocation.Correlation, response); err != nil { + if err := validateExtensionResponse(extension, delivery.ExtensionVerifyOperation, request.Invocation.Correlation, response); err != nil { return model.Observation{}, err } if response.Verified == nil || !*response.Verified { @@ -232,12 +232,12 @@ func boundedComponentCapabilities(granted, declared []catalog.Capability) []cata return result.Sorted() } -func validateProgramRuntimeResponse(flow control.CompiledProgramRuntime, operation control.ProgramRuntimeOperation, correlation string, response control.ProgramRuntimeResponse) error { - if response.ProtocolVersion != control.ProgramRuntimeProtocolVersion || response.Operation != operation || +func validateProgramRuntimeResponse(flow delivery.CompiledProgramRuntime, operation delivery.ProgramRuntimeOperation, correlation string, response delivery.ProgramRuntimeResponse) error { + if response.ProtocolVersion != delivery.ProgramRuntimeProtocolVersion || response.Operation != operation || response.ProgramID != flow.Identity.ID || response.ProgramVersion != flow.Identity.Version || response.CorrelationID != correlation { return fmt.Errorf("program runtime %q returned a mismatched protocol response", flow.Identity.ID) } - if err := control.ValidateProgramRuntimeOperationResponse(operation, response); err != nil { + if err := delivery.ValidateProgramRuntimeOperationResponse(operation, response); err != nil { return fmt.Errorf("program runtime %q returned an invalid operation response: %w", flow.Identity.ID, err) } if response.ErrorClass != "" || response.Error != "" { @@ -246,13 +246,13 @@ func validateProgramRuntimeResponse(flow control.CompiledProgramRuntime, operati return nil } -func validateExtensionResponse(extension control.CompiledExtension, operation control.ExtensionOperation, correlation string, response control.ExtensionResponse) error { - if response.ProtocolVersion != control.ExtensionProtocolVersion || response.Operation != operation || +func validateExtensionResponse(extension delivery.CompiledExtension, operation delivery.ExtensionOperation, correlation string, response delivery.ExtensionResponse) error { + if response.ProtocolVersion != delivery.ExtensionProtocolVersion || response.Operation != operation || response.ExtensionID != extension.Identity.ID || response.ExtensionVersion != extension.Identity.Version || response.CorrelationID != correlation { return fmt.Errorf("extension %q returned a mismatched protocol response", extension.Identity.ID) } - if err := control.ValidateExtensionOperationResponse(operation, response); err != nil { + if err := delivery.ValidateExtensionOperationResponse(operation, response); err != nil { return fmt.Errorf("extension %q returned an invalid operation response: %w", extension.Identity.ID, err) } if response.ErrorClass != "" || response.Error != "" { diff --git a/boatstack/program_observer_test.go b/boatstack/program_observer_test.go index c56096e..153c666 100644 --- a/boatstack/program_observer_test.go +++ b/boatstack/program_observer_test.go @@ -8,11 +8,11 @@ import ( "testing" "time" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/flow/standard" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" ) type fixedObservation struct{ value model.Observation } @@ -22,11 +22,11 @@ func (o fixedObservation) Observe(context.Context, ports.ObservationRequest) (mo } func TestRuntimeErrorPreservesBoundedClassAndMessage(t *testing.T) { - extension := control.CompiledExtension{ - Identity: control.ComponentIdentity{ID: "example.runtime", Version: "1.0.0"}, + extension := delivery.CompiledExtension{ + Identity: delivery.ComponentIdentity{ID: "example.runtime", Version: "1.0.0"}, } - err := validateExtensionResponse(extension, control.ExtensionObserveOperation, "correlation", control.ExtensionResponse{ - ProtocolVersion: control.ExtensionProtocolVersion, Operation: control.ExtensionObserveOperation, + err := validateExtensionResponse(extension, delivery.ExtensionObserveOperation, "correlation", delivery.ExtensionResponse{ + ProtocolVersion: delivery.ExtensionProtocolVersion, Operation: delivery.ExtensionObserveOperation, ExtensionID: "example.runtime", ExtensionVersion: "1.0.0", CorrelationID: "correlation", ErrorClass: "temporary", Error: "provider response was incomplete", }) @@ -44,11 +44,11 @@ type isolatedObservationExtension struct { calls *int } -func (e isolatedObservationExtension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) { - manifest := control.ExtensionManifest{ - ID: e.id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion, +func (e isolatedObservationExtension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, error) { + manifest := delivery.ExtensionManifest{ + ID: e.id, Version: "1.0.0", ProtocolVersion: delivery.ExtensionProtocolVersion, SettingsSchema: json.RawMessage(`{"type":"object"}`), Facts: []string{e.id + ".fact"}, - Capabilities: []control.Capability{control.CapabilityCommandExecute}, + Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}, PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", } if e.executable { @@ -57,9 +57,9 @@ func (e isolatedObservationExtension) ExtensionManifest(context.Context) (contro return manifest, nil } -func (e isolatedObservationExtension) Runtime() control.ExtensionRuntime { return e } +func (e isolatedObservationExtension) Runtime() delivery.ExtensionRuntime { return e } -func (e isolatedObservationExtension) Invoke(_ context.Context, request control.ExtensionRequest) (control.ExtensionResponse, error) { +func (e isolatedObservationExtension) Invoke(_ context.Context, request delivery.ExtensionRequest) (delivery.ExtensionResponse, error) { if e.calls != nil { *e.calls++ } @@ -67,13 +67,13 @@ func (e isolatedObservationExtension) Invoke(_ context.Context, request control. ExtensionFacts map[string]json.RawMessage `json:"extension_facts"` } if err := json.Unmarshal(request.Snapshot, &projection); err != nil { - return control.ExtensionResponse{}, err + return delivery.ExtensionResponse{}, err } _, *e.sawFact = projection.ExtensionFacts[e.forbid] - return control.ExtensionResponse{ - ProtocolVersion: control.ExtensionProtocolVersion, Operation: request.Operation, + return delivery.ExtensionResponse{ + ProtocolVersion: delivery.ExtensionProtocolVersion, Operation: request.Operation, ExtensionID: e.id, ExtensionVersion: "1.0.0", CorrelationID: request.CorrelationID, - Facts: []control.ExtensionFact{{ID: e.id + ".fact", Status: control.FactKnown, Value: "observed", Fingerprint: e.id + "-fingerprint"}}, + Facts: []delivery.ExtensionFact{{ID: e.id + ".fact", Status: delivery.FactKnown, Value: "observed", Fingerprint: e.id + "-fingerprint"}}, }, nil } @@ -82,8 +82,8 @@ func TestExecutableExtensionObservationWaitsForVerifiedProgramBinding(t *testing var calls int var sawFact bool extension := isolatedObservationExtension{id: "example.external", sawFact: &sawFact, executable: true, calls: &calls} - program, err := control.Compile(context.Background(), control.CompileRequest{ - KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{extension}, + program, err := delivery.Compile(context.Background(), delivery.CompileRequest{ + KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), Extensions: []delivery.Extension{extension}, }) if err != nil { t.Fatal(err) @@ -94,7 +94,7 @@ func TestExecutableExtensionObservationWaitsForVerifiedProgramBinding(t *testing Configuration: model.Known(model.ConfigurationVerified, model.Evidence{Source: "test", Fingerprint: "configuration", ObservedAt: time.Unix(100, 0).UTC()}), } observer := programObserver{base: fixedObservation{value: base}, program: program} - observed, err := observer.Observe(context.Background(), ports.ObservationRequest{Invocation: invocation, Capabilities: []control.Capability{control.CapabilityCommandExecute}}) + observed, err := observer.Observe(context.Background(), ports.ObservationRequest{Invocation: invocation, Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}}) if err != nil { t.Fatal(err) } @@ -103,7 +103,7 @@ func TestExecutableExtensionObservationWaitsForVerifiedProgramBinding(t *testing } base.RecordedProgramFingerprint = program.Fingerprint() observer.base = fixedObservation{value: base} - observed, err = observer.Observe(context.Background(), ports.ObservationRequest{Invocation: invocation, Capabilities: []control.Capability{control.CapabilityCommandExecute}}) + observed, err = observer.Observe(context.Background(), ports.ObservationRequest{Invocation: invocation, Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}}) if err != nil { t.Fatal(err) } @@ -117,9 +117,9 @@ func TestExtensionObserversConsumeOneOrderIndependentProjection(t *testing.T) { var alphaSawBeta, betaSawAlpha bool alpha := isolatedObservationExtension{id: "example.alpha", forbid: "example.beta.fact", sawFact: &alphaSawBeta} beta := isolatedObservationExtension{id: "example.beta", forbid: "example.alpha.fact", sawFact: &betaSawAlpha} - program, err := control.Compile(context.Background(), control.CompileRequest{ + program, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), - Extensions: []control.Extension{beta, alpha}, + Extensions: []delivery.Extension{beta, alpha}, }) if err != nil { t.Fatal(err) @@ -128,7 +128,7 @@ func TestExtensionObserversConsumeOneOrderIndependentProjection(t *testing.T) { observed, err := (programObserver{ base: fixedObservation{value: model.Observation{Invocation: invocation, ObservedAt: time.Unix(100, 0).UTC()}}, program: program, - }).Observe(context.Background(), ports.ObservationRequest{Invocation: invocation, Capabilities: []control.Capability{control.CapabilityCommandExecute}}) + }).Observe(context.Background(), ports.ObservationRequest{Invocation: invocation, Capabilities: []delivery.Capability{delivery.CapabilityCommandExecute}}) if err != nil { t.Fatal(err) } diff --git a/boatstack/references/artifacts.md b/boatstack/references/artifacts.md index 793569c..38ab0b3 100644 --- a/boatstack/references/artifacts.md +++ b/boatstack/references/artifacts.md @@ -14,4 +14,4 @@ evidence to commit. They are partitioned by repository, clone, and worktree identity. An artifact is data, not authority. The kernel checks its fingerprint, source -snapshot, goal, transition, and authority receipt before use. +snapshot, objective, transition, and authority receipt before use. diff --git a/boatstack/references/config-schema.md b/boatstack/references/config-schema.md index a718442..2346051 100644 --- a/boatstack/references/config-schema.md +++ b/boatstack/references/config-schema.md @@ -2,7 +2,7 @@ Boatstack V2 accepts only `.boatstack/project.json` schema version 2. The normative Go decoder is -`internal/kernel/protocol.DecodeProjectConfig`; the public example is +`internal/softwaredelivery/protocol.DecodeProjectConfig`; the public example is `project.example.json`. Top-level keys are `schema_version`, `project`, `policy`, `hosts`, and optional diff --git a/boatstack/references/host-hook-contracts.md b/boatstack/references/host-hook-contracts.md index 80f4f5b..15c99c3 100644 --- a/boatstack/references/host-hook-contracts.md +++ b/boatstack/references/host-hook-contracts.md @@ -12,7 +12,7 @@ Example read request: "repository": "/absolute/worktree", "host": "codex", "correlation_id": "host-123", - "goal": { + "objective": { "id": "search-timeout", "kind": "verified-implementation", "delivery_id": "search-timeout" @@ -26,7 +26,7 @@ classifies raw text once, returns `guard.allowed`, and never writes the command to receipts or events. Hosts may render commands differently. They may not change the transition ID, -goal, source predicate, authority clauses, parameters, or expected +objective, source predicate, authority clauses, parameters, or expected postcondition. CLI, Cursor, Codex, Claude, Gemini, and MCP are capability labels, not controllers. diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index c6c3735..f425a2f 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -10,7 +10,7 @@ Event families: - invocation and engagement; - installation, runtime, and configuration; -- goal and plan; +- objective and plan; - workspace; - delivery gates and evidence; - publication; @@ -30,7 +30,7 @@ A normal verified-delivery path is: ```text installation.initialize -goal.configure +objective.bind engagement.begin plan.create -> plan.validate -> plan.approve -> plan.activate gate.build.record -> gate.test.record -> gate.review.record @@ -45,5 +45,5 @@ the exact workspace to become `landed`. parse a plan, persist a slice cursor, define PR cardinality, or advance a publication sequence. -Recovery outranks ordinary progress. Goal reconfiguration is explicit and may +Recovery outranks ordinary progress. Objective reconfiguration is explicit and may change an active delivery only with human or autonomy authority. diff --git a/boatstack/sdk/sdk.go b/boatstack/sdk/sdk.go index 240cd6e..23c7911 100644 --- a/boatstack/sdk/sdk.go +++ b/boatstack/sdk/sdk.go @@ -7,14 +7,14 @@ import ( "fmt" boatstack "github.com/operatorstack/boatstack/boatstack" - "github.com/operatorstack/boatstack/boatstack/control" "github.com/operatorstack/boatstack/boatstack/core" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/distribution" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" - "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor" - "github.com/operatorstack/boatstack/boatstack/internal/surfaces" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" ) const SchemaVersion = surfaces.SchemaVersion @@ -35,16 +35,16 @@ type Request = surfaces.Request type Response = surfaces.Response type DoctorReport = surfaces.DoctorReport type ProgramChange = surfaces.ProgramChange -type Goal = model.Goal -type GoalKind = model.GoalKind +type Objective = model.Objective +type ObjectiveKind = model.ObjectiveKind type StateFacet = model.StateFacet const ( - GoalApprovedPlan = model.GoalApprovedPlan - GoalVerified = model.GoalVerified - GoalOpenPR = model.GoalOpenPR - GoalMerged = model.GoalMerged - GoalAbandoned = model.GoalAbandoned + ObjectiveApprovedPlan = model.ObjectiveApprovedPlan + ObjectiveVerified = model.ObjectiveVerified + ObjectiveOpenPR = model.ObjectiveOpenPR + ObjectiveMerged = model.ObjectiveMerged + ObjectiveAbandoned = model.ObjectiveAbandoned StateFacetInstallation = model.StateFacetInstallation StateFacetProgram = model.StateFacetProgram @@ -54,12 +54,14 @@ const ( type TransitionID = catalog.TransitionID type Transition = catalog.Transition -type GoalScope = catalog.GoalScope +type ObjectiveScope = catalog.ObjectiveScope type AuthorityClass = catalog.AuthorityClass type Capability = catalog.Capability const ( - GoalScopeOptionalPreserve = catalog.GoalScopeOptionalPreserve + ObjectiveScopeNone = catalog.ObjectiveScopeNone + ObjectiveScopeOptionalPreserve = catalog.ObjectiveScopeOptionalPreserve + ObjectiveScopeBoundExact = catalog.ObjectiveScopeBoundExact AuthorityRepository = catalog.AuthorityRepository AuthorityHuman = catalog.AuthorityHuman @@ -110,13 +112,13 @@ const ( const HostIdentity = "sdk" type options struct { - runtime control.ProgramRuntimeDefinition - extensions []control.Extension + runtime delivery.ProgramRuntimeDefinition + extensions []delivery.Extension } type Option func(*options) error -func WithProgramRuntime(runtime control.ProgramRuntimeDefinition) Option { +func WithProgramRuntime(runtime delivery.ProgramRuntimeDefinition) Option { return func(configuration *options) error { if runtime == nil { return fmt.Errorf("SDK program runtime cannot be nil") @@ -129,7 +131,7 @@ func WithProgramRuntime(runtime control.ProgramRuntimeDefinition) Option { } } -func WithExtension(extension control.Extension) Option { +func WithExtension(extension delivery.Extension) Option { return func(configuration *options) error { if extension == nil { return fmt.Errorf("SDK extension cannot be nil") @@ -145,8 +147,8 @@ func WithExtension(extension control.Extension) Option { type Client struct { externalStateRoot string standard bool - runtime control.ProgramRuntimeDefinition - extensions []control.Extension + runtime delivery.ProgramRuntimeDefinition + extensions []delivery.Extension } // New assembles the standard Boatstack distribution. Options may add @@ -157,31 +159,31 @@ func New(externalStateRoot string, supplied ...Option) (Client, error) { return Client{}, err } if configuration.runtime != nil { - return Client{}, fmt.Errorf("sdk.New always uses StandardFlow; use sdk.NewKernel for an explicit flow") + return Client{}, fmt.Errorf("sdk.New always uses StandardFlow; use sdk.NewProgramClient for an explicit flow") } if _, err := distribution.StandardProgram(context.Background(), configuration.extensions...); err != nil { return Client{}, err } - return Client{externalStateRoot: externalStateRoot, standard: true, extensions: append([]control.Extension(nil), configuration.extensions...)}, nil + return Client{externalStateRoot: externalStateRoot, standard: true, extensions: append([]delivery.Extension(nil), configuration.extensions...)}, nil } -// NewKernel is the low-level composition API. It never inserts StandardFlow; +// NewProgramClient is the low-level composition API. It never inserts StandardFlow; // callers must supply exactly one WithProgramRuntime option. -func NewKernel(externalStateRoot string, supplied ...Option) (Client, error) { +func NewProgramClient(externalStateRoot string, supplied ...Option) (Client, error) { configuration, err := applyOptions(supplied) if err != nil { return Client{}, err } if configuration.runtime == nil { - return Client{}, fmt.Errorf("sdk.NewKernel requires an explicit ProgramRuntime") + return Client{}, fmt.Errorf("sdk.NewProgramClient requires an explicit ProgramRuntime") } - if _, err := control.Compile(context.Background(), control.CompileRequest{ + if _, err := delivery.Compile(context.Background(), delivery.CompileRequest{ KernelVersion: boatstack.Version, Core: core.System(), Runtime: configuration.runtime, Extensions: configuration.extensions, }); err != nil { return Client{}, err } - return Client{externalStateRoot: externalStateRoot, runtime: configuration.runtime, extensions: append([]control.Extension(nil), configuration.extensions...)}, nil + return Client{externalStateRoot: externalStateRoot, runtime: configuration.runtime, extensions: append([]delivery.Extension(nil), configuration.extensions...)}, nil } func applyOptions(supplied []Option) (options, error) { @@ -209,18 +211,18 @@ func (c Client) Do(ctx context.Context, request Request) (Response, error) { repositoryRequest.ConfigurationPath, _ = request.Parameters.Get("config_path") repositoryRequest.ConfigurationFingerprint, _ = request.Parameters.Get("config_sha256") } - var program control.ControlProgram + var program delivery.ControlProgram var err error if c.standard { program, err = distribution.StandardProgramForRepository(ctx, repositoryRequest) } else { - var configured []control.Extension + var configured []delivery.Extension var settings any configured, settings, err = distribution.ConfiguredExtensions(ctx, repositoryRequest) if err == nil { - extensions := append([]control.Extension(nil), c.extensions...) + extensions := append([]delivery.Extension(nil), c.extensions...) extensions = append(extensions, configured...) - program, err = control.Compile(ctx, control.CompileRequest{ + program, err = delivery.Compile(ctx, delivery.CompileRequest{ KernelVersion: boatstack.Version, Core: core.System(), Runtime: c.runtime, Extensions: extensions, Settings: settings, }) @@ -229,7 +231,7 @@ func (c Client) Do(ctx context.Context, request Request) (Response, error) { if err != nil { return Response{}, err } - kernel, err := boatstack.NewKernel(c.externalStateRoot, program) + kernel, err := boatstack.NewDeliveryController(c.externalStateRoot, program) if err != nil { return Response{}, err } diff --git a/boatstack/sdk/sdk_test.go b/boatstack/sdk/sdk_test.go index f0957f1..f9590ed 100644 --- a/boatstack/sdk/sdk_test.go +++ b/boatstack/sdk/sdk_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/operatorstack/boatstack/boatstack/control" + "github.com/operatorstack/boatstack/boatstack/delivery" "github.com/operatorstack/boatstack/boatstack/sdk" ) @@ -17,16 +17,16 @@ func TestPublicProtocolCanBeConstructedWithoutInternalPackages(t *testing.T) { Repository: t.TempDir(), Host: "mcp", CorrelationID: "correlation", - Goal: sdk.Goal{ID: "goal", Kind: sdk.GoalVerified, DeliveryID: "delivery"}, + Objective: sdk.Objective{ID: "objective", Kind: sdk.ObjectiveVerified, DeliveryID: "delivery"}, } - if request.Goal.Kind != sdk.GoalVerified || request.Operation != sdk.OperationResolve { + if request.Objective.Kind != sdk.ObjectiveVerified || request.Operation != sdk.OperationResolve { t.Fatalf("public V2 aliases lost protocol identity: %#v", request) } } func TestSDKPreservesCapabilityAdmissionProtocol(t *testing.T) { // control-law: SDK and CLI consume the same versioned prescription fields - raw := []byte(`{"schema_version":4,"operation":"apply","repository":"/repo","host":"sdk","correlation_id":"correlation","flow_id":"flow","transition_id":"program/write","prescription":{"schema_version":2,"id":"prx-test","transition_id":"program/write","expected_state_revision":7,"expected_program_fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expected_snapshot_fingerprint":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","authority_fingerprint":"auth-test","required_capabilities":["repository.write"],"effective_capabilities":["repository.write"]}}`) + raw := []byte(`{"schema_version":5,"operation":"apply","repository":"/repo","host":"sdk","correlation_id":"correlation","flow_id":"flow","transition_id":"program/write","prescription":{"schema_version":3,"id":"prx-test","transition_id":"program/write","expected_state_revision":7,"expected_program_fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expected_snapshot_fingerprint":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","expected_objective_binding_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","authority_fingerprint":"auth-test","required_capabilities":["repository.write"],"effective_capabilities":["repository.write"]}}`) var request sdk.Request if err := json.Unmarshal(raw, &request); err != nil { t.Fatal(err) @@ -37,7 +37,7 @@ func TestSDKPreservesCapabilityAdmissionProtocol(t *testing.T) { } func TestSDKSerializesTheSameDurableTransitionFactAsTheSurface(t *testing.T) { - raw := []byte(`{"schema_version":4,"operation":"apply","receipt":{"schema_version":7,"kind":"transition-committed","id":"trc-fact","flow_id":"flow","sequence":1,"program":{"id":"product-delivery","version":"1.0.0","fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"transition_id":"product-delivery/build.begin","transition_version":1,"prescription_id":"prx","admission_id":"adm","prior_state_revision":41,"resulting_state_revision":42,"goal_id":"goal","goal_kind":"verified","delivery_id":"delivery","goal_scope":"required","source_fingerprint":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","target_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","authority_fingerprint":"auth","authority_sources":[],"required_capabilities":["repository.write"],"granted_capabilities":["repository.write"],"committed_effects":[{"kind":"resource-mutation","effect_id":"build.begin","owner":"product-delivery","resource":"state","target":"/state","operation":"update","prior_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","resulting_fingerprint":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}],"changed_state_facets":["control","product"],"verification":{"verifier":"build-active","expected_postcondition":"active","result":"satisfied","evidence_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","verified_at":"2026-08-12T00:00:00Z"},"idempotency_key":"idem","terminal":"nonterminal","started_at":"2026-08-12T00:00:00Z","committed_at":"2026-08-12T00:00:01Z","duration_nanoseconds":1000000000}}`) + raw := []byte(`{"schema_version":5,"operation":"apply","receipt":{"schema_version":8,"kind":"transition-committed","id":"trc-fact","flow_id":"flow","sequence":1,"program":{"id":"product-delivery","version":"1.0.0","fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"transition_id":"product-delivery/build.begin","transition_version":1,"prescription_id":"prx","admission_id":"adm","prior_state_revision":41,"resulting_state_revision":42,"objective_id":"objective","objective_kind":"verified","delivery_id":"delivery","objective_scope":"bound-exact","objective_binding_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","source_fingerprint":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","target_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","authority_fingerprint":"auth","authority_sources":[],"required_capabilities":["repository.write"],"granted_capabilities":["repository.write"],"committed_effects":[{"kind":"resource-mutation","effect_id":"build.begin","owner":"product-delivery","resource":"state","target":"/state","operation":"update","prior_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","resulting_fingerprint":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}],"changed_state_facets":["control","product"],"verification":{"verifier":"build-active","expected_postcondition":"active","result":"satisfied","evidence_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","verified_at":"2026-08-12T00:00:00Z"},"idempotency_key":"idem","terminal":"nonterminal","started_at":"2026-08-12T00:00:00Z","committed_at":"2026-08-12T00:00:01Z","duration_nanoseconds":1000000000}}`) var response sdk.Response if err := json.Unmarshal(raw, &response); err != nil { t.Fatal(err) @@ -63,14 +63,14 @@ func TestSDKSerializesTheSameDurableTransitionFactAsTheSurface(t *testing.T) { func TestLowLevelSDKRequiresAndAcceptsExactlyOneNonStandardProgramRuntime(t *testing.T) { // control-law: low-level-sdk-never-inserts-or-multiplies-standard-flow - if _, err := sdk.NewKernel(""); err == nil { + if _, err := sdk.NewProgramClient(""); err == nil { t.Fatal("low-level SDK accepted a missing ProgramRuntime") } flow := syntheticFlow{} - if _, err := sdk.NewKernel("", sdk.WithProgramRuntime(flow)); err != nil { + if _, err := sdk.NewProgramClient("", sdk.WithProgramRuntime(flow)); err != nil { t.Fatalf("synthetic ProgramRuntime was rejected: %v", err) } - if _, err := sdk.NewKernel("", sdk.WithProgramRuntime(flow), sdk.WithProgramRuntime(flow)); err == nil { + if _, err := sdk.NewProgramClient("", sdk.WithProgramRuntime(flow), sdk.WithProgramRuntime(flow)); err == nil { t.Fatal("low-level SDK accepted two ProgramRuntimes") } if _, err := sdk.New("", sdk.WithProgramRuntime(flow)); err == nil { @@ -80,45 +80,45 @@ func TestLowLevelSDKRequiresAndAcceptsExactlyOneNonStandardProgramRuntime(t *tes type syntheticFlow struct{} -func (syntheticFlow) ProgramRuntime() control.ProgramRuntime { return syntheticRuntime{} } +func (syntheticFlow) ProgramRuntime() delivery.ProgramRuntime { return syntheticRuntime{} } -func (syntheticFlow) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) { +func (syntheticFlow) RuntimeManifest(context.Context) (delivery.ProgramRuntimeManifest, error) { const ( id = "synthetic.lifecycle" fact = "synthetic.lifecycle.stage" resource = "synthetic.lifecycle.state" ) - transition := func(id control.TransitionID, source, target string, priority int) control.Transition { - effect := control.EffectID(string(id) + "-effect") + transition := func(id delivery.TransitionID, source, target string, priority int) delivery.Transition { + effect := delivery.EffectID(string(id) + "-effect") verifier := string(id) + "-verifier" - return control.Transition{ - ID: id, Version: 1, SelectionClass: control.SelectionProgramProgress, Class: control.EventOwnedLocal, - SourcePhases: []control.ProtocolPhase{control.PhaseObserved, control.PhaseActive}, TargetPhases: []control.ProtocolPhase{control.PhaseObserved, control.PhaseActive}, - GoalKinds: []control.GoalKind{control.GoalVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"}, - Authority: []control.AuthorityClass{control.AuthorityRepository}, RequiredCapabilities: []control.Capability{control.CapabilityRepositoryWrite, control.CapabilityCommandExecute}, RequiredEvidence: []string{"snapshot", "goal", "facet:" + fact}, - OwnedResources: []string{resource}, Effect: effect, LocalEffects: []control.EffectID{effect}, Idempotent: true, - Prescription: control.Prescription{Operation: string(id), ExpectedPostcondition: target}, + return delivery.Transition{ + ID: id, Version: 1, SelectionClass: delivery.SelectionProgramProgress, Class: delivery.EventOwnedLocal, + SourcePhases: []delivery.ProtocolPhase{delivery.PhaseObserved, delivery.PhaseActive}, TargetPhases: []delivery.ProtocolPhase{delivery.PhaseObserved, delivery.PhaseActive}, + ObjectiveKinds: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"}, + Authority: []delivery.AuthorityClass{delivery.AuthorityRepository}, RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, RequiredEvidence: []string{"snapshot", "objective", "facet:" + fact}, + OwnedResources: []string{resource}, Effect: effect, LocalEffects: []delivery.EffectID{effect}, Idempotent: true, + Prescription: delivery.Prescription{Operation: string(id), ExpectedPostcondition: target}, SourcePredicate: "synthetic-source", AdmissionPredicate: "exact-admission", TargetPredicate: "synthetic-target", - SourceConditions: []control.FacetCondition{control.KnownCondition(control.FacetName(fact), source)}, - TargetConditions: []control.FacetCondition{control.KnownCondition(control.FacetName(fact), target)}, Verifier: verifier, - Interruption: control.InterruptionContract{ + SourceConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetName(fact), source)}, + TargetConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetName(fact), target)}, Verifier: verifier, + Interruption: delivery.InterruptionContract{ Points: []string{"after-effect"}, PartialState: []string{"namespaced-flow-state"}, Detection: "fresh-flow-observation", ResumeContract: "re-observe", RollbackContract: "restore-prior-bytes", CompensationContract: "not-required", Recovery: "recovery.escalate", RecoveryAuthority: "repository-policy", ResumptionPredicate: "fresh-flow-fact", }, - Reversibility: control.Reversible, TerminalEffect: "compiled-goal-contract", PrivacyClassification: "metadata-only", - TelemetryClassification: "transition-receipt", CostClass: "synthetic", Priority: priority, + Reversibility: delivery.Reversible, TerminalEffect: "compiled-objective-contract", PrivacyClassification: "metadata-only", + TelemetryClassification: "transition-receipt", CostClass: "synthetic", Policy: delivery.PolicyContract{ObjectiveScope: delivery.ObjectiveScopeBoundExact}, Priority: priority, } } verify := transition("synthetic.lifecycle.verify", "start", "verify", 1) finish := transition("synthetic.lifecycle.finish", "verify", "terminal", 2) - return control.ProgramRuntimeManifest{ - ID: id, Version: "1.0.0", ProtocolVersion: control.ProgramRuntimeProtocolVersion, RuntimeMode: control.ProgramRuntimeProtocol, - SupportedGoals: []control.GoalKind{control.GoalVerified}, - GoalContracts: []control.GoalContract{{GoalKind: control.GoalVerified, Conditions: []control.FacetCondition{control.KnownCondition(control.FacetName(fact), "terminal")}}}, - Transitions: []control.Transition{verify, finish}, Facts: []string{fact}, OwnedResources: []string{resource}, + return delivery.ProgramRuntimeManifest{ + ID: id, Version: "1.0.0", ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, RuntimeMode: delivery.ProgramRuntimeProtocol, + SupportedObjectives: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, + ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetName(fact), "terminal")}}}, + Transitions: []delivery.Transition{verify, finish}, Facts: []string{fact}, OwnedResources: []string{resource}, Effects: []string{string(verify.Effect), string(finish.Effect)}, Verifiers: []string{verify.Verifier, finish.Verifier}, - Capabilities: []control.Capability{control.CapabilityRepositoryWrite, control.CapabilityCommandExecute}, + Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, ConfigurationSchema: json.RawMessage(`{"type":"object"}`), PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", }, nil @@ -126,9 +126,9 @@ func (syntheticFlow) RuntimeManifest(context.Context) (control.ProgramRuntimeMan type syntheticRuntime struct{} -func (syntheticRuntime) InvokeProgram(_ context.Context, request control.ProgramRuntimeRequest) (control.ProgramRuntimeResponse, error) { - return control.ProgramRuntimeResponse{ - ProtocolVersion: control.ProgramRuntimeProtocolVersion, Operation: request.Operation, +func (syntheticRuntime) InvokeProgram(_ context.Context, request delivery.ProgramRuntimeRequest) (delivery.ProgramRuntimeResponse, error) { + return delivery.ProgramRuntimeResponse{ + ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, Operation: request.Operation, ProgramID: request.ProgramID, ProgramVersion: request.ProgramVersion, CorrelationID: request.CorrelationID, }, nil } diff --git a/boatstack/testdata/v2-scenarios/historical.json b/boatstack/testdata/v2-scenarios/historical.json index ce5db64..dace21a 100644 --- a/boatstack/testdata/v2-scenarios/historical.json +++ b/boatstack/testdata/v2-scenarios/historical.json @@ -39,8 +39,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-stale-worktree-runtime-selection", + "requested_objective": { + "id": "objective-stale-worktree-runtime-selection", "kind": "verified-implementation", "delivery_id": "delivery-stale-worktree-runtime-selection" }, @@ -96,8 +96,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-detached-bootstrap-hydration", + "requested_objective": { + "id": "objective-detached-bootstrap-hydration", "kind": "approved-plan", "delivery_id": "delivery-detached-bootstrap-hydration" }, @@ -153,8 +153,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-workspace-transition-deadlock", + "requested_objective": { + "id": "objective-workspace-transition-deadlock", "kind": "safely-abandoned", "delivery_id": "delivery-workspace-transition-deadlock" }, @@ -210,8 +210,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-cleanup-before-publication", + "requested_objective": { + "id": "objective-cleanup-before-publication", "kind": "open-or-updated-pr", "delivery_id": "delivery-cleanup-before-publication" }, @@ -268,8 +268,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-saved-plan-ambient-restriction", + "requested_objective": { + "id": "objective-saved-plan-ambient-restriction", "kind": "verified-implementation", "delivery_id": "delivery-saved-plan-ambient-restriction" }, @@ -325,8 +325,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-detached-command-admission-mismatch", + "requested_objective": { + "id": "objective-detached-command-admission-mismatch", "kind": "approved-plan", "delivery_id": "delivery-detached-command-admission-mismatch" }, @@ -382,8 +382,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-split-bootstrap-command-authority", + "requested_objective": { + "id": "objective-split-bootstrap-command-authority", "kind": "approved-plan", "delivery_id": "delivery-split-bootstrap-command-authority" }, @@ -439,8 +439,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-configuration-projection-drift", + "requested_objective": { + "id": "objective-configuration-projection-drift", "kind": "verified-implementation", "delivery_id": "delivery-configuration-projection-drift" }, @@ -496,8 +496,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-partial-delivery-vs-merged-projection", + "requested_objective": { + "id": "objective-partial-delivery-vs-merged-projection", "kind": "merged-delivery", "delivery_id": "delivery-partial-delivery-vs-merged-projection" }, @@ -554,8 +554,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-dormant-repository-interference", + "requested_objective": { + "id": "objective-dormant-repository-interference", "kind": "approved-plan", "delivery_id": "delivery-dormant-repository-interference" }, @@ -611,8 +611,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-configuration-mutation-self-invalidation", + "requested_objective": { + "id": "objective-configuration-mutation-self-invalidation", "kind": "verified-implementation", "delivery_id": "delivery-configuration-mutation-self-invalidation" }, @@ -668,8 +668,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-cross-shard-deterministic-resolution", + "requested_objective": { + "id": "objective-cross-shard-deterministic-resolution", "kind": "approved-plan", "delivery_id": "delivery-cross-shard-deterministic-resolution" }, @@ -725,8 +725,8 @@ "terminal": "nonterminal", "ref": "refs/heads/main" }, - "requested_goal": { - "id": "goal-unpublished-equal-main-not-landed", + "requested_objective": { + "id": "objective-unpublished-equal-main-not-landed", "kind": "merged-delivery", "delivery_id": "delivery-unpublished-equal-main-not-landed" }, @@ -782,8 +782,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-closed-unmerged-not-cleanup-eligible", + "requested_objective": { + "id": "objective-closed-unmerged-not-cleanup-eligible", "kind": "merged-delivery", "delivery_id": "delivery-closed-unmerged-not-cleanup-eligible" }, @@ -839,8 +839,8 @@ "terminal": "established", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-configured-merged-terminal", + "requested_objective": { + "id": "objective-bindd-merged-terminal", "kind": "merged-delivery", "delivery_id": "delivery-configured-merged-terminal" }, @@ -896,8 +896,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-active-workspace-preserved", + "requested_objective": { + "id": "objective-active-workspace-preserved", "kind": "merged-delivery", "delivery_id": "delivery-active-workspace-preserved" }, @@ -953,8 +953,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-amendment-deadlock", + "requested_objective": { + "id": "objective-amendment-deadlock", "kind": "verified-implementation", "delivery_id": "delivery-amendment-deadlock" }, @@ -1011,8 +1011,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-ambiguous-detached-controller-alias", + "requested_objective": { + "id": "objective-ambiguous-detached-controller-alias", "kind": "verified-implementation", "delivery_id": "delivery-ambiguous-detached-controller-alias" }, @@ -1069,8 +1069,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-activation-wrong-worktree-identity", + "requested_objective": { + "id": "objective-activation-wrong-worktree-identity", "kind": "verified-implementation", "delivery_id": "delivery-activation-wrong-worktree-identity" }, @@ -1126,8 +1126,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-runtime-publication-before-lock-release", + "requested_objective": { + "id": "objective-runtime-publication-before-lock-release", "kind": "open-or-updated-pr", "delivery_id": "delivery-runtime-publication-before-lock-release" }, @@ -1184,8 +1184,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-recovery-outranks-slice-position", + "requested_objective": { + "id": "objective-recovery-outranks-slice-position", "kind": "verified-implementation", "delivery_id": "delivery-recovery-outranks-slice-position" }, @@ -1241,8 +1241,8 @@ "terminal": "nonterminal", "ref": "refs/heads/feature" }, - "requested_goal": { - "id": "goal-unknown-provider-is-not-complete", + "requested_objective": { + "id": "objective-unknown-provider-is-not-complete", "kind": "open-or-updated-pr", "delivery_id": "delivery-unknown-provider-is-not-complete" }, diff --git a/docs/architecture/boatstack-v1-authority-inventory.md b/docs/architecture/boatstack-v1-authority-inventory.md index 9cd7438..eda61f8 100644 --- a/docs/architecture/boatstack-v1-authority-inventory.md +++ b/docs/architecture/boatstack-v1-authority-inventory.md @@ -26,7 +26,7 @@ boatstack/decision.go:49:func ResolvePlanDecision(input PlanDecisionInput) Decis boatstack/delivery_terminal.go:23:func normalizeDeliveryTerminal(value string) (DeliveryTerminal, bool) { boatstack/delivery_terminal.go:38:func configuredDeliveryTerminal(repo string) DeliveryTerminal { boatstack/delivery_terminal.go:51:func resolveDeliveryTerminal(repo, feature string) DeliveryTerminal { -boatstack/delivery_terminal.go:66:func deliveryGoalSnapshot(repo string) string { +boatstack/delivery_terminal.go:66:func deliveryObjectiveSnapshot(repo string) string { boatstack/engagement.go:57:func engagementLeasePath(repo string) (string, error) { boatstack/engagement.go:65:func dormantEngagement(reason string) EngagementStatus { boatstack/engagement.go:73:func ResolveEngagement(repoPath string, request EngagementRequest) EngagementStatus { diff --git a/docs/architecture/boatstack-v2-closure-report.md b/docs/architecture/boatstack-v2-closure-report.md index 51abb65..5280ee6 100644 --- a/docs/architecture/boatstack-v2-closure-report.md +++ b/docs/architecture/boatstack-v2-closure-report.md @@ -33,7 +33,7 @@ rewrite deletes: The conservative removed V1 managed-effect surface is therefore 120 sites. V2's static source inventory fails if an `os` writer exists outside -`internal/effects`, if a command boundary exists outside the exact plant/effect +`internal/softwaredelivery/effects`, if a command boundary exists outside the exact plant/effect allowlist, if a production file is unclassified, or if the deleted shadow controller is imported or recreated. @@ -70,7 +70,7 @@ required-visual terminal until revision-bound evidence exists. The historical corpus contains 22 typed fixtures. It covers every PR from #172 through #185 and the additional ambiguity, interruption, stale-runtime, -publication, workspace, configuration, and goal-terminal failure classes named +publication, workspace, configuration, and objective-terminal failure classes named in the V2 specification. Live integration tests exercise embedded and detached installation, attach and diff --git a/docs/architecture/boatstack-v2-kernel.md b/docs/architecture/boatstack-v2-kernel.md index f67bbcd..6cdbecd 100644 --- a/docs/architecture/boatstack-v2-kernel.md +++ b/docs/architecture/boatstack-v2-kernel.md @@ -21,14 +21,14 @@ V1 counts to the implemented V2 evidence. The existing implementation is projected into two minimal, jointly shipped slices. They are logical ownership boundaries, not rollout phases. -| Slice | Domain | Structure | Goal | Operator | Immediate value | +| Slice | Domain | Structure | Objective | Operator | Immediate value | | --- | --- | --- | --- | --- | --- | | 1. Compiled control law | Repository-local delivery control | CoreSystem plus one ProgramRuntime and zero or more conservative Extensions compiled into one immutable ControlProgram | Every managed state has a safe path to progress, recovery, authority frontier, or terminal | Compile, observe, resolve, admit, execute, verify, record, recover | Delivery policy can evolve without changing the mechanism that protects authority and effects | | 2. Product surfaces | Shipped CLI, hooks, SDK/MCP, hosts, and renderers | One adapter protocol projected from Kernel decisions and prescriptions | Every consumer observes and requests the same compiled semantics | Assemble, decode, invoke, render | Hosts stop acting as independent controllers while useful workflows remain available | Canonical form for slice 1: one domain, the `ControlProgram` and `Snapshot` -schemas, the configured `Goal`, and the `Kernel.Handle` operator. Canonical form for slice 2: one domain, -the `SurfaceRequest`/`SurfaceResponse` schema, the same goal, and the adapter +schemas, the configured `Objective`, and the `Kernel.Handle` operator. Canonical form for slice 2: one domain, +the `SurfaceRequest`/`SurfaceResponse` schema, the same objective, and the adapter projection operator. Known constraints are the flag-day cutover, explicit effectful identity, @@ -90,10 +90,10 @@ CoreSystem + ProgramRuntime + Extensions + RepositoryPolicy program runtime, extension implementation, CLI, SDK wrapper, or host renderer. - **CoreSystem** declares Boatstack operational capabilities: invocation and repository identity, engagement, runtime, configuration, installation, - generic goal identity, transactions, recovery, process events, and external + generic objective identity, transactions, recovery, process events, and external observations. - **ProgramRuntime** is one trusted in-process execution binding. It declares - goal contracts, facts, transitions, resources, effects, verifiers, recovery, + objective contracts, facts, transitions, resources, effects, verifiers, recovery, policy projection, and telemetry. The application selects it; repository configuration cannot select an arbitrary executable flow. - **StandardFlow** is the first-party complete Control Program preserving the familiar @@ -103,8 +103,8 @@ CoreSystem + ProgramRuntime + Extensions + RepositoryPolicy capabilities constrained by the compiler. Subprocess extensions are trusted executable boundaries using a strict bounded JSON protocol; they are not OS sandboxes. Extensions may add namespaced facts, resources, transitions, - recovery, and conjunctive goal obligations, but may not replace the flow, - weaken a goal contract, or mutate another owner's state. + recovery, and conjunctive objective obligations, but may not replace the flow, + weaken a objective contract, or mutate another owner's state. - **Surfaces** assemble or invoke a program and render typed results. They do not decide lifecycle, terminal state, authority, or recovery. @@ -114,10 +114,10 @@ CoreSystem + ProgramRuntime + Extensions + RepositoryPolicy zero or more extension manifests, and canonical program-affecting settings. It rejects missing or multiple flows, ID collisions, unnamespaced extension IDs, overlapping mutable-resource ownership, undeclared effects or verifiers, -missing recovery contracts, dependency cycles, and goal constraints that are +missing recovery contracts, dependency cycles, and objective constraints that are not conservative. -The result is immutable and contains one transition registry, one goal-contract +The result is immutable and contains one transition registry, one objective-contract set, one resource-ownership map, compiled handlers, origin metadata, and one content fingerprint. The registry is the only runtime graph. There is no core, flow, extension, terminal, or verification shadow graph. @@ -145,13 +145,13 @@ and identity, version, correlation, error classification, and operation type are checked at the Kernel boundary. `sdk.New(...)` assembles CoreSystem plus StandardFlow and repository-scoped -extensions. `sdk.NewKernel(..., sdk.WithProgramRuntime(runtime), sdk.WithExtension(...))` +extensions. `sdk.NewProgramClient(..., sdk.WithProgramRuntime(runtime), sdk.WithExtension(...))` requires exactly one explicit program runtime and never inserts StandardFlow. The fingerprint covers the Kernel version; CoreSystem ID, version, manifest, -and transitions; ProgramRuntime ID, version, manifest, goal contracts, and +and transitions; ProgramRuntime ID, version, manifest, objective contracts, and transitions; extension manifests, versions, executable SHA-256 values, -settings, goal constraints, and transitions; the compiled transition registry; +settings, objective constraints, and transitions; the compiled transition registry; resource ownership; verifier and recovery declarations; and canonical program-affecting repository policy. In this version that repository projection is exactly the checksum-bound extension composition; approval, host, visual, @@ -168,7 +168,7 @@ component declarations: | Owner | Families | Count | | --- | --- | ---: | -| CoreSystem | `engagement.*`, `invocation.*`, `repository.*`, `runtime.*`, `configuration.*`, `installation.*`, `catalog.*`, `goal.*`, `recovery.*`, `external.*` | 33 | +| CoreSystem | `engagement.*`, `invocation.*`, `repository.*`, `runtime.*`, `configuration.*`, `installation.*`, `catalog.*`, `objective.*`, `recovery.*`, `external.*` | 33 | | StandardFlow | `plan.*`, `workspace.*`, `gate.*`, `evidence.*`, `delivery.*`, `publication.*` | 30 | | Extensions in the default distribution | none | 0 | | **Compiled total** | one registry | **63** | @@ -181,7 +181,7 @@ verification facts without taking ownership of that boundary. Every transition records its origin, owner, manifest fingerprint, and bounded selection class: `SYSTEM_RECOVERY`, `PROGRAM_RECOVERY`, `EXTENSION_RECOVERY`, -`GOAL_REQUIRED`, `PROGRAM_PROGRESS`, `EXPLICIT_ONLY`, or `OBSERVED_EXTERNAL`. Third-party +`OBJECTIVE_REQUIRED`, `PROGRAM_PROGRESS`, `EXPLICIT_ONLY`, or `OBSERVED_EXTERNAL`. Third-party extensions cannot supply raw numeric priority. An extension becomes implicitly selectable only to discharge an active unmet extension obligation or its own recovery contract. @@ -199,7 +199,7 @@ semantic managed operation, and the compiled registry maps that operation to a transition through `PolicyContract.ManagedOperations`. A custom program that does not claim an operation does not inherit StandardFlow transition IDs. -The five software-delivery goal kinds remain closed. The ProgramRuntime supplies +The five software-delivery objective kinds remain closed. The ProgramRuntime supplies the base terminal contract. Extension obligations are conjoined with that contract, so for the same base state: @@ -241,7 +241,7 @@ forking Kernel and without parsing CLI output. ```go standardClient, err := sdk.New(stateRoot, sdk.WithExtension(extension)) -customClient, err := sdk.NewKernel( +customClient, err := sdk.NewProgramClient( stateRoot, sdk.WithProgramRuntime(programRuntime), sdk.WithExtension(extension), @@ -265,7 +265,7 @@ humans and coding agents. The agent writes software. Boatstack deterministically observes the delivery plant, retains explicit identity, establishes engagement, resolves legal managed events, binds authority, owns transactional effects, verifies postconditions, records receipts, recovers from interruption, and -establishes whether the configured goal is terminal. +establishes whether the configured objective is terminal. The repository owns policy and committed evidence. The kernel owns delivery decisions. CLI, hooks, Cursor, Codex, Claude Code, Gemini CLI, SDK, MCP, and future @@ -277,7 +277,7 @@ Observable behavior is classified only as follows: - **PRESERVE:** installation, initialization, update, doctor, embedded/detached/ hybrid operation, deterministic runtime hydration, explicit repository and worktree identity, planning and approval, autonomy, workspaces, build/test/ - review/change/journey gates, goal-driven run, interruption and resume, + review/change/journey gates, objective-driven run, interruption and resume, amendments, invalid-plan recovery, publication and correction, merged terminals, visual evidence, safety hooks, configuration, cleanup/reap, abandonment, portable host guidance, evidence, receipts, and passive @@ -321,7 +321,7 @@ structural classes carried into V2 are: - split transition, identity, configuration, and completion authority; - non-injective repository/worktree reverse lookup; - ambient engagement and saved-plan leakage; -- workspace, publication, Git ancestry, and goal-terminal conflation; +- workspace, publication, Git ancestry, and objective-terminal conflation; - stale or self-invalidating runtime/configuration mutation; - non-atomic multi-resource and externally uncertain effects; - surface, shell, and host prescription divergence; @@ -353,7 +353,7 @@ x_t = ( The read-only observer produces `o_t = H(x_t, evidence_t)`. Canonicalization produces the control-sufficient `z_t = P(o_t)`. Events are partitioned into controllable Boatstack events `Sigma_c` and uncontrollable observed plant events -`Sigma_u`. For goal `g` and authority set `a`, the supervisor returns the +`Sigma_u`. For objective `g` and authority set `a`, the supervisor returns the admissible set `S(z_t, g, a) subseteq Sigma_c`; deterministic policy selects at most one prescribed event. Execution is accepted only as: @@ -379,11 +379,11 @@ Normative properties within declared managed scope: 1. Safety: forbidden states and events are unreachable. 2. Inertness: ordinary repository work outside active managed scope is not blocked or mutated. -3. Coreachability: every reachable nonterminal managed state can reach the goal, +3. Coreachability: every reachable nonterminal managed state can reach the objective, a typed recovery path, an authority frontier, or safe abandonment/refusal. 4. Projection fidelity: `P(x1) = P(x2)` implies equal admissible controllable event sets. A distinguishing legal action requires a distinguishing facet. -5. Determinism: identical snapshot, goal, authority, and request yield identical +5. Determinism: identical snapshot, objective, authority, and request yield identical decisions and typed prescriptions. 6. Resource preservation: missing, stale, ambiguous, conflicting, or unknown evidence never grants delete, publish, overwrite, or advance authority. @@ -419,7 +419,7 @@ The executable catalog declares exactly 17 controlling facets: | Recovery info | exact transaction, cause, source phase, permitted exits, budget, resumption target | | Transaction info | exact transition, status, resource digests, external possibility | | Terminal | nonterminal, established, stale, unknown, conflicting | -| Goal | target kind, subject delivery, evidence predicate, frontier policy | +| Objective | target kind, subject delivery, evidence predicate, frontier policy | Every controlling fact is a `Fact[T]` containing value/status, evidence source, revision or fingerprint, observation time when freshness matters, and explicit @@ -462,7 +462,7 @@ activation, safety, publication, and adapters consume this snapshot rather than recomputing lifecycle subsets. The snapshot fingerprint covers every fact used by source predicates, authority, -admission, effects, postconditions, and goal termination. Display-only facts are +admission, effects, postconditions, and objective termination. Display-only facts are explicitly excluded and may not become controlling without a schema change. ## 6. Event vocabulary @@ -498,7 +498,7 @@ synchronized with this table. | Invocation and engagement | 6 | `engagement.begin`, `engagement.renew`, `engagement.release`, `invocation.rebind`, `repository.attach`, `repository.detach` | | Installation, runtime, configuration | 9 | `runtime.hydrate`, `runtime.replace`, `runtime.reconcile`, `configuration.initialize`, `configuration.mutate`, `configuration.reconcile`, `installation.initialize`, `installation.update`, `installation.reconcile-update` | | Catalog identity | 1 | `catalog.reconcile` | -| Goal and plan | 9 | `goal.configure`, `plan.create`, `plan.validate`, `plan.approve`, `plan.activate`, `plan.amend`, `plan.approve-amendment`, `plan.invalidate`, `plan.abandon` | +| Objective and plan | 9 | `objective.bind`, `plan.create`, `plan.validate`, `plan.approve`, `plan.activate`, `plan.amend`, `plan.approve-amendment`, `plan.invalidate`, `plan.abandon` | | Workspace | 8 | `workspace.cut`, `workspace.sync`, `workspace.activate`, `workspace.publish`, `workspace.cleanup`, `workspace.reap`, `workspace.abandon`, `workspace.reconcile` | | Delivery gates and evidence | 8 | `gate.build.record`, `gate.test.record`, `gate.review.record`, `gate.change.record`, `gate.journey.record`, `evidence.visual.attach`, `evidence.approval.revoke`, `delivery.slice.advance` | | Publication | 6 | `publication.preview`, `publication.execute`, `publication.observe`, `publication.reconcile`, `publication.correct`, `publication.abandon` | @@ -506,7 +506,7 @@ synchronized with this table. | Observed external | 13 | `external.files-changed`, `external.head-changed`, `external.branch-changed`, `external.runtime-disappeared`, `external.configuration-drifted`, `external.lease-expired`, `external.host-interrupted`, `external.ci-completed`, `external.pr-opened`, `external.pr-updated`, `external.pr-closed`, `external.pr-merged`, `external.provider-unavailable` | Every `Transition` declaration contains: ID and schema version; source predicate; -event class and controllability; goal relevance; required identity, authority, +event class and controllability; objective relevance; required identity, authority, evidence, and fingerprints; admission predicate; owned resources; local/external effects; idempotency binding; typed prescription; expected target predicate; independent verifier; interruption points; rollback/compensation; reversibility; @@ -528,12 +528,12 @@ it is not an independently maintained graph. ## 8. Supervisory control law -`supervisor.Resolve(snapshot, goal, authority, optionalObservedEvent)` is pure and +`supervisor.Resolve(snapshot, objective, authority, optionalObservedEvent)` is pure and deterministic. It evaluates the executable registry and returns exactly one: - `CANDIDATE`: one deterministic next transition still needs declared parameters; - `PRESCRIBED`: one exact next transition and prescription; -- `TERMINAL`: goal predicate established by current terminal evidence; +- `TERMINAL`: objective predicate established by current terminal evidence; - `FRONTIER`: a genuine human/reasoning authority decision is required; - `BLOCKED`: a known recoverable condition plus its registered recovery event; - `REFUSED`: the request is outside admissible managed behavior; @@ -558,7 +558,7 @@ Knowledge, precondition evidence, authority, and proof of effect are four separate objects. A content-addressed `Prescription` binds the exact transition, durable state revision, executable program fingerprint, and snapshot fingerprint. `Admission` binds that prescription plus transition ID/version, invocation -identity, goal and plan lock, observation/configuration fingerprints, source +identity, objective and plan lock, observation/configuration fingerprints, source revision, branch/worktree, authority receipt, provider preview, idempotency key, and expiry. @@ -677,9 +677,9 @@ decisions name the controlling reason and registered recovery or termination path. Repair budgets are monotonic and bounded; exhaustion produces `FRONTIER` or safe abandonment rather than an infinite retry loop. -## 13. Goal and terminal semantics +## 13. Objective and terminal semantics -`Goal` is configured before managed execution and identifies the subject delivery +`Objective` is configured before managed execution and identifies the subject delivery and one terminal predicate: approved plan, verified implementation, open/updated PR, merged delivery, or safely abandoned delivery. It also declares required evidence freshness and whether a frontier is acceptable as a stopped outcome. @@ -694,9 +694,9 @@ Terminal is evidence, not a local phase label. Examples: - abandonment requires explicit authority and a receipt proving resource policy. Local green tests, ancestry equality, workspace cleanup eligibility, saved plan -presence, or an agent's completion assertion cannot establish a goal. External +presence, or an agent's completion assertion cannot establish a objective. External unknown never establishes terminal. Once terminal, unrelated local projections -cannot resume the flow without a new goal or registered correction transition. +cannot resume the flow without a new objective or registered correction transition. ## 14. Package and dependency architecture @@ -706,20 +706,20 @@ Dependencies point downward in this table and are acyclic. | Package | Owns | Public boundary and verifier | Allowed dependencies | Forbidden dependencies | | --- | --- | --- | --- | --- | -| `internal/kernel/model` | typed facts, identity, snapshot, goal, fingerprints | constructors/canonical encoding; schema and invariant tests | standard library | plant, effects, surfaces, facade | +| `internal/softwaredelivery/model` | typed facts, identity, snapshot, objective, fingerprints | constructors/canonical encoding; schema and invariant tests | standard library | plant, effects, surfaces, facade | | `control` | stable CoreSystem, ProgramRuntime, Extension, and immutable ControlProgram compiler contracts | strict manifests, conservative extension compilation, fingerprints, ownership map | kernel contracts | concrete distribution or surfaces | | `core` | 32 operational-capability transition declarations | embedded strict declaration bytes through `CoreManifest` | control contracts | StandardFlow, extensions, surfaces | -| `flow/standard` | 30 first-party delivery transitions and five base goal contracts | `standard.Definition()` plus default-flow parity, historical, ownership, and completeness tests | control contracts and model vocabulary | Kernel mechanism, CLI, host rendering, SDK | +| `flow/standard` | 30 first-party delivery transitions and five base objective contracts | `standard.Definition()` plus default-flow parity, historical, ownership, and completeness tests | control contracts and model vocabulary | Kernel mechanism, CLI, host rendering, SDK | | `extension/*` | additive in-process and checksum-bound subprocess capabilities | strict extension manifests and bounded runtime protocol | control contracts | Kernel state, admissions, receipts, foreign resources | -| `internal/kernel/catalog` | transition, registry, and goal-contract mechanism and invariants | read-only registry; uniqueness and recovery-reference validation | model | CoreSystem or StandardFlow declarations, effects, surfaces | -| `internal/kernel/supervisor` | admissible-set and deterministic outcome law | pure `Resolve`; synthetic mechanism tests through the engine, with StandardFlow parity outside Kernel packages | model, catalog | I/O, effects, surfaces | -| `internal/kernel/protocol` | prescriptions, admission, receipts, recovery records | typed codecs and content identity verifier | model, catalog | concrete I/O and surfaces | -| `internal/kernel/durable` | strict machine-state and detached-binding codecs | canonical encode/decode and invariant validation | model, catalog | observation, effects, surfaces | -| `internal/kernel/ports` | observer, clock, lock, journal, local/external effect ports | compile-time narrow interfaces and fakes | model, protocol | concrete adapters | -| `internal/kernel/engine` | observe-resolve-admit-execute-reobserve-verify-record orchestration | `Resolve`, `Apply`, `Recover`; protocol/conformance tests | model, catalog, supervisor, protocol, ports | concrete surfaces and host logic | -| `internal/plant` | Git/worktree identity, layout, configuration, runtime, durable-state and journal observation | one read-only composite observer; fact/fingerprint fixtures | model, protocol, ports, durable codecs | engine decisions, mutating effects, surfaces | -| `internal/effects` | transactions, local/external effect drivers, trusted StandardFlow native state adapters, and recovery | port implementations; exhaustive admitted-reducer coverage; fault-injection/postcondition tests | model, catalog, durable, protocol, ports, shared supervisor command classifier | surfaces and any decision graph independent of the compiled registry | -| `internal/surfaces` | request decoding and decision/prescription rendering | CLI/hook/host/SDK/MCP adapter protocol; golden parity tests | model, protocol, engine facade interfaces | plant/effect implementations, lifecycle logic | +| `internal/softwaredelivery/catalog` | transition, registry, and objective-contract mechanism and invariants | read-only registry; uniqueness and recovery-reference validation | model | CoreSystem or StandardFlow declarations, effects, surfaces | +| `internal/softwaredelivery/supervisor` | admissible-set and deterministic outcome law | pure `Resolve`; synthetic mechanism tests through the engine, with StandardFlow parity outside Kernel packages | model, catalog | I/O, effects, surfaces | +| `internal/softwaredelivery/protocol` | prescriptions, admission, receipts, recovery records | typed codecs and content identity verifier | model, catalog | concrete I/O and surfaces | +| `internal/softwaredelivery/durable` | strict machine-state and detached-binding codecs | canonical encode/decode and invariant validation | model, catalog | observation, effects, surfaces | +| `internal/softwaredelivery/ports` | observer, clock, lock, journal, local/external effect ports | compile-time narrow interfaces and fakes | model, protocol | concrete adapters | +| `internal/softwaredelivery/engine` | observe-resolve-admit-execute-reobserve-verify-record orchestration | `Resolve`, `Apply`, `Recover`; protocol/conformance tests | model, catalog, supervisor, protocol, ports | concrete surfaces and host logic | +| `internal/softwaredelivery/plant` | Git/worktree identity, layout, configuration, runtime, durable-state and journal observation | one read-only composite observer; fact/fingerprint fixtures | model, protocol, ports, durable codecs | engine decisions, mutating effects, surfaces | +| `internal/softwaredelivery/effects` | transactions, local/external effect drivers, trusted StandardFlow native state adapters, and recovery | port implementations; exhaustive admitted-reducer coverage; fault-injection/postcondition tests | model, catalog, durable, protocol, ports, shared supervisor command classifier | surfaces and any decision graph independent of the compiled registry | +| `internal/softwaredelivery/surfaces` | request decoding and decision/prescription rendering | CLI/hook/host/SDK/MCP adapter protocol; golden parity tests | model, protocol, engine facade interfaces | plant/effect implementations, lifecycle logic | | top-level `boatstack` | stable Kernel facade over one explicit ControlProgram | dependency injection and public operations; end-to-end tests | control, engine, plant, effects, surfaces | StandardFlow, distribution assembly, independent durable state or alternate decisions | | `distribution` | Standard distribution composition and repository-scoped extension assembly | `StandardProgram` and `StandardProgramForRepository` | CoreSystem, StandardFlow, verified extensions, control | mutable global program state | | `cmd/boatstack-helper` | process startup and command parsing | parse -> facade request -> render; command tests | top-level facade/surfaces | direct plant writes or workflow decisions | @@ -739,12 +739,12 @@ All surfaces use the same versioned protocol: ```text SurfaceRequest { schema_version, operation(resolve|apply|recover|doctor|catalog|events|guard), - repository, host, correlation_id, flow_id?, goal?, transition_id?, + repository, host, correlation_id, flow_id?, objective?, transition_id?, authority?, parameters?, idempotency_key?, command? } SurfaceResponse { - schema_version, operation, goal?, snapshot?, decision?, admission?, receipt?, + schema_version, operation, objective?, snapshot?, decision?, admission?, receipt?, replayed?, catalog?, events?, doctor?, program_change?, guard?, error? } ``` @@ -756,7 +756,7 @@ never inspect state files to reconstruct policy. SDK and MCP expose the protocol, not internal Go packages. The facade resolves explicit repository/worktree and executing-runtime identity before observation; -hosts supply the repository, host, correlation, goal, transition, authority, and +hosts supply the repository, host, correlation, objective, transition, authority, and typed parameters. Cursor, Codex, Claude, Gemini, CLI, and MCP prescriptions are projections of one command AST plus host capability data. Host capability can affect rendering, never admissibility or target semantics. @@ -776,7 +776,7 @@ Receipts are the factual source. The facade exposes a passive JSONL reader, `boatstack events [--follow] --format jsonl`, over committed receipt projections. Telemetry is consumer-neutral and privacy-safe. -Allowlisted fields are schema version, flow ID, sequence, timestamp, goal ID, +Allowlisted fields are schema version, flow ID, sequence, timestamp, objective ID, transition ID, program and prescription identity, prior/resulting state revisions, source/target fingerprints, outcome, duration, recovery and authority classifications, terminal status, and controlled failure class. @@ -948,7 +948,7 @@ product operation needs an adapter, it targets the new facade/protocol directly. V2 is complete only when all criteria are evidenced at the exact final head. Architecture: one runtime kernel, catalog, observer, explicit identity, -admission path, receipt model, recovery model, and goal model own their respective +admission path, receipt model, recovery model, and objective model own their respective laws. The package graph is acyclic and the facade owns no independent durable state. @@ -979,7 +979,7 @@ merged. ## Appendix A. Historical control-law episodes and regression corpus -Each fixture contains initial plant facts, canonical observation, goal, event, +Each fixture contains initial plant facts, canonical observation, objective, event, expected admitted transition, expected postcondition, forbidden transition, source provenance, and failure class. Rows may share a stronger class fixture, but every cited PR has an explicit provenance edge. @@ -991,21 +991,21 @@ but every cited PR has an explicit provenance edge. | Hooks and malformed host events, PRs #42-#46 | Host-specific inputs diverged or bypassed policy | Hooks/hosts vs native controller | Typed surface request and one admission path | Malformed and replayed host request; remove host decisions | | Workspace/config foundation, PRs #51-#52 | Workspace and config projections lost topology/authority distinctions | Workspace lifecycle vs config writer | Composite facts with evidence and one observer | Detached/embedded/hybrid configuration fixtures | | Approval/grounding/worktrees, PRs #56-#59 | Authority or worktree identity was inferred from insufficient context | Approval artifacts and path lookup | Exact authority and `InvocationContext` binding | Ambiguous worktree/approval fingerprint fixtures | -| Deterministic plan and multi-delivery, PRs #61-#64 | One local slice or artifact could choose the wrong delivery | Plan/safety/workflow resolvers | Goal-scoped snapshot and deterministic supervisor | Two deliveries sharing artifacts; remove first-match selection | +| Deterministic plan and multi-delivery, PRs #61-#64 | One local slice or artifact could choose the wrong delivery | Plan/safety/workflow resolvers | Objective-scoped snapshot and deterministic supervisor | Two deliveries sharing artifacts; remove first-match selection | | Publication/config corrections, PRs #68-#78 | Publication, mutation, update, or correction could invalidate its own proof | External provider/config writers vs lifecycle | Preview/admit/execute/observe/reconcile and postcondition receipts | Unknown publication; post-publication correction; remove accepted unverified success | | Dual layout and state ledger, PRs #79, #89-#100 | Embedded/detached layouts and stale ledgers produced incompatible answers | Layout/path state vs delivery authority | Topology facts plus authoritative observation/canonicalization | Same logical plant in all topologies; remove path-as-authority | | Shadow flow model, PRs #101-#106 | Useful graph/oracle/trajectory existed but was not runtime authority | `internal/deliverycontrol` vs production functions | Executable catalog is runtime and formal model | Generated reachability parity; delete shadow graph | | Concurrency/worktree runtime, PRs #111-#123 | Stale runtime/worktree selection and destructive guards raced | Runtime launcher, worktree, cleanup, safety | Exact identity, source fingerprint, scoped lock, preservation on uncertainty | Stale runtime, shared aliases, branch/worktree combinations | | Recovery/denial/owners, PRs #124-#138 | Recovery or denial could be overridden or fail to name a path | Local status slices vs repair/ownership policy | Recovery precedence and typed denial with registered correction | Budget exhaustion and contradictory owner evidence | -| PR state and terminal, PRs #145-#150 | Open/closed/merged and ancestry were collapsed | GitHub projection vs Git graph vs goal | Multi-state publication and goal-specific terminal verifier | Open, closed-unmerged, merged, unavailable, published-not-landed | +| PR state and terminal, PRs #145-#150 | Open/closed/merged and ancestry were collapsed | GitHub projection vs Git graph vs objective | Multi-state publication and objective-specific terminal verifier | Open, closed-unmerged, merged, unavailable, published-not-landed | | Retro/readiness/visuals/insights, PRs #151-#159 | Ancillary evidence could leak into authority or lack freshness | Evidence services vs lifecycle | Separate services; managed writes cross effects, facts retain freshness | Stale evidence and privacy allowlist; remove evidence-presence authority | -| Update recovery and explicit goal, PRs #161-#163 | Update postconditions or local lifecycle ignored the requested terminal | Update writer/local phase vs goal | Independent verification and goal-first supervisor precedence | Configured PR vs merged terminals; self-invalidating update | +| Update recovery and explicit objective, PRs #161-#163 | Update postconditions or local lifecycle ignored the requested terminal | Update writer/local phase vs objective | Independent verification and objective-first supervisor precedence | Configured PR vs merged terminals; self-invalidating update | | Detached controller/privacy/cloud, PRs #164-#167 | Shared controller paths and external config/cloud facts were non-injective or sensitive | Detached registry/adapters vs repository identity | Explicit invocation plus evidence-source and privacy classifications | Two repos sharing controller alias; unknown external config | | Operation/shell/readiness, PRs #168-#170 | Operation drivers and shell guidance could encode different control decisions | Native code vs POSIX/PowerShell/host text | Typed prescription rendered per environment | Semantic shell/host parity; remove hand-authored workflow logic | | PR #172, deterministic worktree runtime launcher | Active worktree could select stale/wrong runtime | Launcher lookup vs worktree identity | Bind runtime source/version to explicit invocation | `stale-worktree-runtime-selection` | | PR #173, detached launcher hydration | Detached bootstrap lacked a verified runtime and could dead-end | Bootstrap vs detached runtime owner | `runtime.hydrate` recovery before managed execution | `detached-bootstrap-hydration` | | PR #174, workspace transition deadlock | Valid workspace states had no next transition | Workspace slice vs delivery resolver | Catalog coreachability and explicit recovery | `workspace-transition-deadlock` | -| PR #175-#176, cleanup/public lifecycle | Cleanup could act on weak completion/publication signals | Cleanup policy vs publication evidence | Cleanup requires goal/lifecycle predicate and explicit authority | `cleanup-before-publication`; remove cleanup-as-proof | +| PR #175-#176, cleanup/public lifecycle | Cleanup could act on weak completion/publication signals | Cleanup policy vs publication evidence | Cleanup requires objective/lifecycle predicate and explicit authority | `cleanup-before-publication`; remove cleanup-as-proof | | PR #177, saved plans are not active authority | Mere plan presence activated ambient restrictions | Filesystem presence vs engagement | Engagement fact/lease and command scope | `saved-plan-ambient-restriction` | | PR #178, detached command admission | Native and detached surfaces disagreed on command permission | Detached launcher vs controller admission | One surface request and admission protocol | `detached-command-admission-mismatch` | | PR #179, planning bootstrap authority | Bootstrap commands independently reconstructed planning authority | Helper command vs lifecycle | Map command to catalog ID; kernel resolves | `split-bootstrap-command-authority` | @@ -1014,7 +1014,7 @@ but every cited PR has an explicit provenance edge. | PR #182, explicit engagement | Dormant repositories were affected by ambient Boatstack state | Repository presence/plan vs engagement | Dormant/command/active/conflict facet | `dormant-repository-interference` | | PR #183, verified configuration mutation | Successful mutation could invalidate verification | Config writer vs verifier/runtime binding | Binding last, re-observe, independent target check | `configuration-mutation-self-invalidation` | | PR #184, test sharding | Large test topology exposed implicit shared assumptions | Test partitions vs hidden global state | Isolated catalog/plant/effect fixtures and deterministic seeds | Cross-shard/race parity; remove test-order authority | -| PR #185, preserve active workspaces | Branch equal to main or incomplete publication could be read as landed and cleaned | Git ancestry, publication, workspace, active delivery, configured goal | Durable publication evidence, active-delivery precedence, preserve on ambiguity | `unpublished-equal-main-not-landed`, `closed-unmerged-not-cleanup-eligible`, `configured-merged-terminal`, `active-workspace-preserved` | +| PR #185, preserve active workspaces | Branch equal to main or incomplete publication could be read as landed and cleaned | Git ancestry, publication, workspace, active delivery, configured objective | Durable publication evidence, active-delivery precedence, preserve on ambiguity | `unpublished-equal-main-not-landed`, `closed-unmerged-not-cleanup-eligible`, `configured-merged-terminal`, `active-workspace-preserved` | Additional class fixtures required even when covered by stronger rows are: activation from the wrong worktree identity; ambiguous detached controller alias; diff --git a/docs/architecture/boatstack-v2-locus-liveness.json b/docs/architecture/boatstack-v2-locus-liveness.json index 989b7ac..7fa6766 100644 --- a/docs/architecture/boatstack-v2-locus-liveness.json +++ b/docs/architecture/boatstack-v2-locus-liveness.json @@ -4,7 +4,7 @@ "subject": "Finite stable-phase abstraction generated from the compiled Boatstack ControlProgram registry. It contains one event for every runtime entry and expands each declared source and target phase set. The 18-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.", "evidence": [ { - "path": "boatstack/control/control.go", + "path": "boatstack/delivery/delivery.go", "note": "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry." }, { @@ -12,15 +12,15 @@ "note": "Generated readable projection from the same runtime registry." }, { - "path": "boatstack/internal/kernel/protocol/admission.go", + "path": "boatstack/internal/softwaredelivery/protocol/admission.go", "note": "Exact admission, authority, parameter, source-revision, provider-request, expiry, and stale-snapshot checks." }, { - "path": "boatstack/internal/kernel/engine/engine.go", + "path": "boatstack/internal/softwaredelivery/engine/engine.go", "note": "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery." }, { - "path": "boatstack/internal/effects/state_reducer.go", + "path": "boatstack/internal/softwaredelivery/effects/state_reducer.go", "note": "Admitted native effects reduce every controllable Standard distribution transition through one state adapter." }, { @@ -28,11 +28,11 @@ "note": "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests." }, { - "path": "boatstack/internal/kernel/engine/engine_test.go", + "path": "boatstack/internal/softwaredelivery/engine/engine_test.go", "note": "Exact-admission, stale-snapshot, postcondition, interruption, idempotency, and unknown-outcome tests." }, { - "path": "boatstack/internal/effects/prepared.go", + "path": "boatstack/internal/softwaredelivery/effects/prepared.go", "note": "Staged effect ordering, atomic resource application, rollback, and external settlement boundary." }, { @@ -239,31 +239,31 @@ "basis": "observed" }, { - "id": "goal.configure", + "id": "installation.initialize", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "installation.initialize", + "id": "installation.reconcile-update", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "installation.reconcile-update", + "id": "installation.update", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "installation.update", + "id": "invocation.rebind", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "invocation.rebind", + "id": "objective.bind", "controllable": true, "observable": true, "basis": "observed" @@ -3377,8 +3377,8 @@ "basis": "inferred" }, { - "from": "OBSERVED", - "event": "goal.configure", + "from": "DORMANT", + "event": "installation.initialize", "to": "OBSERVED", "evidence": [ 0, @@ -3389,8 +3389,8 @@ }, { "from": "OBSERVED", - "event": "goal.configure", - "to": "ACTIVE", + "event": "installation.initialize", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -3399,9 +3399,9 @@ "basis": "inferred" }, { - "from": "OBSERVED", - "event": "goal.configure", - "to": "FRONTIER", + "from": "DORMANT", + "event": "installation.reconcile-update", + "to": "DORMANT", "evidence": [ 0, 1, @@ -3411,7 +3411,7 @@ }, { "from": "DORMANT", - "event": "goal.configure", + "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ 0, @@ -3422,7 +3422,7 @@ }, { "from": "DORMANT", - "event": "goal.configure", + "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ 0, @@ -3433,7 +3433,7 @@ }, { "from": "DORMANT", - "event": "goal.configure", + "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ 0, @@ -3443,9 +3443,9 @@ "basis": "inferred" }, { - "from": "ACTIVE", - "event": "goal.configure", - "to": "OBSERVED", + "from": "DORMANT", + "event": "installation.reconcile-update", + "to": "TERMINAL", "evidence": [ 0, 1, @@ -3454,9 +3454,9 @@ "basis": "inferred" }, { - "from": "ACTIVE", - "event": "goal.configure", - "to": "ACTIVE", + "from": "DORMANT", + "event": "installation.reconcile-update", + "to": "ABANDONED", "evidence": [ 0, 1, @@ -3465,9 +3465,9 @@ "basis": "inferred" }, { - "from": "ACTIVE", - "event": "goal.configure", - "to": "FRONTIER", + "from": "OBSERVED", + "event": "installation.reconcile-update", + "to": "DORMANT", "evidence": [ 0, 1, @@ -3476,8 +3476,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "goal.configure", + "from": "OBSERVED", + "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ 0, @@ -3487,8 +3487,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "goal.configure", + "from": "OBSERVED", + "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ 0, @@ -3498,8 +3498,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "goal.configure", + "from": "OBSERVED", + "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ 0, @@ -3509,9 +3509,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "goal.configure", - "to": "OBSERVED", + "from": "OBSERVED", + "event": "installation.reconcile-update", + "to": "TERMINAL", "evidence": [ 0, 1, @@ -3520,9 +3520,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "goal.configure", - "to": "ACTIVE", + "from": "OBSERVED", + "event": "installation.reconcile-update", + "to": "ABANDONED", "evidence": [ 0, 1, @@ -3531,9 +3531,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "goal.configure", - "to": "FRONTIER", + "from": "ACTIVE", + "event": "installation.reconcile-update", + "to": "DORMANT", "evidence": [ 0, 1, @@ -3542,8 +3542,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "goal.configure", + "from": "ACTIVE", + "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ 0, @@ -3553,8 +3553,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "goal.configure", + "from": "ACTIVE", + "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ 0, @@ -3564,8 +3564,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "goal.configure", + "from": "ACTIVE", + "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ 0, @@ -3575,9 +3575,9 @@ "basis": "inferred" }, { - "from": "DORMANT", - "event": "installation.initialize", - "to": "OBSERVED", + "from": "ACTIVE", + "event": "installation.reconcile-update", + "to": "TERMINAL", "evidence": [ 0, 1, @@ -3586,9 +3586,9 @@ "basis": "inferred" }, { - "from": "OBSERVED", - "event": "installation.initialize", - "to": "OBSERVED", + "from": "ACTIVE", + "event": "installation.reconcile-update", + "to": "ABANDONED", "evidence": [ 0, 1, @@ -3597,7 +3597,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "DORMANT", "evidence": [ @@ -3608,7 +3608,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ @@ -3619,7 +3619,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ @@ -3630,7 +3630,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ @@ -3641,7 +3641,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "TERMINAL", "evidence": [ @@ -3652,7 +3652,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "ABANDONED", "evidence": [ @@ -3663,7 +3663,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "DORMANT", "evidence": [ @@ -3674,7 +3674,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ @@ -3685,7 +3685,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ @@ -3696,7 +3696,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ @@ -3707,7 +3707,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "TERMINAL", "evidence": [ @@ -3718,7 +3718,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "ABANDONED", "evidence": [ @@ -3729,7 +3729,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "DORMANT", "evidence": [ @@ -3740,7 +3740,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ @@ -3751,7 +3751,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ @@ -3762,7 +3762,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ @@ -3773,7 +3773,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "TERMINAL", "evidence": [ @@ -3784,7 +3784,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "ABANDONED", "evidence": [ @@ -3795,8 +3795,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "DORMANT", "evidence": [ 0, @@ -3806,8 +3806,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "OBSERVED", "evidence": [ 0, @@ -3817,8 +3817,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "ACTIVE", "evidence": [ 0, @@ -3828,8 +3828,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "FRONTIER", "evidence": [ 0, @@ -3839,8 +3839,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "TERMINAL", "evidence": [ 0, @@ -3850,8 +3850,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "ABANDONED", "evidence": [ 0, @@ -3861,8 +3861,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "DORMANT", "evidence": [ 0, @@ -3872,8 +3872,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "OBSERVED", "evidence": [ 0, @@ -3883,8 +3883,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "ACTIVE", "evidence": [ 0, @@ -3894,8 +3894,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "FRONTIER", "evidence": [ 0, @@ -3905,8 +3905,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "TERMINAL", "evidence": [ 0, @@ -3916,8 +3916,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "ABANDONED", "evidence": [ 0, @@ -3927,8 +3927,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "DORMANT", "evidence": [ 0, @@ -3938,8 +3938,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "OBSERVED", "evidence": [ 0, @@ -3949,8 +3949,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "ACTIVE", "evidence": [ 0, @@ -3960,8 +3960,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "FRONTIER", "evidence": [ 0, @@ -3971,8 +3971,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "TERMINAL", "evidence": [ 0, @@ -3982,8 +3982,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "ABANDONED", "evidence": [ 0, @@ -3993,7 +3993,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "DORMANT", "evidence": [ @@ -4004,7 +4004,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "OBSERVED", "evidence": [ @@ -4015,7 +4015,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "ACTIVE", "evidence": [ @@ -4026,7 +4026,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "FRONTIER", "evidence": [ @@ -4037,7 +4037,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "TERMINAL", "evidence": [ @@ -4048,7 +4048,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "ABANDONED", "evidence": [ @@ -4059,7 +4059,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "DORMANT", "evidence": [ @@ -4070,7 +4070,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "OBSERVED", "evidence": [ @@ -4081,7 +4081,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "ACTIVE", "evidence": [ @@ -4092,7 +4092,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "FRONTIER", "evidence": [ @@ -4103,7 +4103,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "TERMINAL", "evidence": [ @@ -4114,7 +4114,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "ABANDONED", "evidence": [ @@ -4125,7 +4125,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "DORMANT", "evidence": [ @@ -4136,7 +4136,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "OBSERVED", "evidence": [ @@ -4147,7 +4147,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "ACTIVE", "evidence": [ @@ -4158,7 +4158,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "FRONTIER", "evidence": [ @@ -4169,7 +4169,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "TERMINAL", "evidence": [ @@ -4180,7 +4180,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "ABANDONED", "evidence": [ @@ -4191,9 +4191,9 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", - "to": "DORMANT", + "from": "OBSERVED", + "event": "invocation.rebind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4202,8 +4202,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", + "from": "UNRESOLVED", + "event": "invocation.rebind", "to": "OBSERVED", "evidence": [ 0, @@ -4213,8 +4213,19 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", + "from": "OBSERVED", + "event": "objective.bind", + "to": "OBSERVED", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, + { + "from": "OBSERVED", + "event": "objective.bind", "to": "ACTIVE", "evidence": [ 0, @@ -4224,8 +4235,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", + "from": "OBSERVED", + "event": "objective.bind", "to": "FRONTIER", "evidence": [ 0, @@ -4235,9 +4246,9 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", - "to": "TERMINAL", + "from": "DORMANT", + "event": "objective.bind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4246,9 +4257,9 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", - "to": "ABANDONED", + "from": "DORMANT", + "event": "objective.bind", + "to": "ACTIVE", "evidence": [ 0, 1, @@ -4257,9 +4268,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", - "to": "DORMANT", + "from": "DORMANT", + "event": "objective.bind", + "to": "FRONTIER", "evidence": [ 0, 1, @@ -4268,8 +4279,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", + "from": "ACTIVE", + "event": "objective.bind", "to": "OBSERVED", "evidence": [ 0, @@ -4279,8 +4290,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", + "from": "ACTIVE", + "event": "objective.bind", "to": "ACTIVE", "evidence": [ 0, @@ -4290,8 +4301,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", + "from": "ACTIVE", + "event": "objective.bind", "to": "FRONTIER", "evidence": [ 0, @@ -4301,9 +4312,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", - "to": "TERMINAL", + "from": "FRONTIER", + "event": "objective.bind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4312,9 +4323,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", - "to": "ABANDONED", + "from": "FRONTIER", + "event": "objective.bind", + "to": "ACTIVE", "evidence": [ 0, 1, @@ -4323,9 +4334,9 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", - "to": "DORMANT", + "from": "FRONTIER", + "event": "objective.bind", + "to": "FRONTIER", "evidence": [ 0, 1, @@ -4334,8 +4345,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", + "from": "TERMINAL", + "event": "objective.bind", "to": "OBSERVED", "evidence": [ 0, @@ -4345,8 +4356,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", + "from": "TERMINAL", + "event": "objective.bind", "to": "ACTIVE", "evidence": [ 0, @@ -4356,8 +4367,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", + "from": "TERMINAL", + "event": "objective.bind", "to": "FRONTIER", "evidence": [ 0, @@ -4368,8 +4379,8 @@ }, { "from": "ABANDONED", - "event": "installation.update", - "to": "TERMINAL", + "event": "objective.bind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4379,19 +4390,8 @@ }, { "from": "ABANDONED", - "event": "installation.update", - "to": "ABANDONED", - "evidence": [ - 0, - 1, - 4 - ], - "basis": "inferred" - }, - { - "from": "OBSERVED", - "event": "invocation.rebind", - "to": "OBSERVED", + "event": "objective.bind", + "to": "ACTIVE", "evidence": [ 0, 1, @@ -4400,9 +4400,9 @@ "basis": "inferred" }, { - "from": "UNRESOLVED", - "event": "invocation.rebind", - "to": "OBSERVED", + "from": "ABANDONED", + "event": "objective.bind", + "to": "FRONTIER", "evidence": [ 0, 1, diff --git a/docs/architecture/boatstack-v2-locus-safety.json b/docs/architecture/boatstack-v2-locus-safety.json index 04b18d1..cd3fa9e 100644 --- a/docs/architecture/boatstack-v2-locus-safety.json +++ b/docs/architecture/boatstack-v2-locus-safety.json @@ -4,7 +4,7 @@ "subject": "Finite stable-phase abstraction generated from the compiled Boatstack ControlProgram registry. It contains one event for every runtime entry and expands each declared source and target phase set. The 18-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.", "evidence": [ { - "path": "boatstack/control/control.go", + "path": "boatstack/delivery/delivery.go", "note": "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry." }, { @@ -12,15 +12,15 @@ "note": "Generated readable projection from the same runtime registry." }, { - "path": "boatstack/internal/kernel/protocol/admission.go", + "path": "boatstack/internal/softwaredelivery/protocol/admission.go", "note": "Exact admission, authority, parameter, source-revision, provider-request, expiry, and stale-snapshot checks." }, { - "path": "boatstack/internal/kernel/engine/engine.go", + "path": "boatstack/internal/softwaredelivery/engine/engine.go", "note": "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery." }, { - "path": "boatstack/internal/effects/state_reducer.go", + "path": "boatstack/internal/softwaredelivery/effects/state_reducer.go", "note": "Admitted native effects reduce every controllable Standard distribution transition through one state adapter." }, { @@ -28,11 +28,11 @@ "note": "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests." }, { - "path": "boatstack/internal/kernel/engine/engine_test.go", + "path": "boatstack/internal/softwaredelivery/engine/engine_test.go", "note": "Exact-admission, stale-snapshot, postcondition, interruption, idempotency, and unknown-outcome tests." }, { - "path": "boatstack/internal/effects/prepared.go", + "path": "boatstack/internal/softwaredelivery/effects/prepared.go", "note": "Staged effect ordering, atomic resource application, rollback, and external settlement boundary." }, { @@ -242,31 +242,31 @@ "basis": "observed" }, { - "id": "goal.configure", + "id": "installation.initialize", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "installation.initialize", + "id": "installation.reconcile-update", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "installation.reconcile-update", + "id": "installation.update", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "installation.update", + "id": "invocation.rebind", "controllable": true, "observable": true, "basis": "observed" }, { - "id": "invocation.rebind", + "id": "objective.bind", "controllable": true, "observable": true, "basis": "observed" @@ -3380,8 +3380,8 @@ "basis": "inferred" }, { - "from": "OBSERVED", - "event": "goal.configure", + "from": "DORMANT", + "event": "installation.initialize", "to": "OBSERVED", "evidence": [ 0, @@ -3392,8 +3392,8 @@ }, { "from": "OBSERVED", - "event": "goal.configure", - "to": "ACTIVE", + "event": "installation.initialize", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -3402,9 +3402,9 @@ "basis": "inferred" }, { - "from": "OBSERVED", - "event": "goal.configure", - "to": "FRONTIER", + "from": "DORMANT", + "event": "installation.reconcile-update", + "to": "DORMANT", "evidence": [ 0, 1, @@ -3414,7 +3414,7 @@ }, { "from": "DORMANT", - "event": "goal.configure", + "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ 0, @@ -3425,7 +3425,7 @@ }, { "from": "DORMANT", - "event": "goal.configure", + "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ 0, @@ -3436,7 +3436,7 @@ }, { "from": "DORMANT", - "event": "goal.configure", + "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ 0, @@ -3446,9 +3446,9 @@ "basis": "inferred" }, { - "from": "ACTIVE", - "event": "goal.configure", - "to": "OBSERVED", + "from": "DORMANT", + "event": "installation.reconcile-update", + "to": "TERMINAL", "evidence": [ 0, 1, @@ -3457,9 +3457,9 @@ "basis": "inferred" }, { - "from": "ACTIVE", - "event": "goal.configure", - "to": "ACTIVE", + "from": "DORMANT", + "event": "installation.reconcile-update", + "to": "ABANDONED", "evidence": [ 0, 1, @@ -3468,9 +3468,9 @@ "basis": "inferred" }, { - "from": "ACTIVE", - "event": "goal.configure", - "to": "FRONTIER", + "from": "OBSERVED", + "event": "installation.reconcile-update", + "to": "DORMANT", "evidence": [ 0, 1, @@ -3479,8 +3479,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "goal.configure", + "from": "OBSERVED", + "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ 0, @@ -3490,8 +3490,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "goal.configure", + "from": "OBSERVED", + "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ 0, @@ -3501,8 +3501,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "goal.configure", + "from": "OBSERVED", + "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ 0, @@ -3512,9 +3512,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "goal.configure", - "to": "OBSERVED", + "from": "OBSERVED", + "event": "installation.reconcile-update", + "to": "TERMINAL", "evidence": [ 0, 1, @@ -3523,9 +3523,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "goal.configure", - "to": "ACTIVE", + "from": "OBSERVED", + "event": "installation.reconcile-update", + "to": "ABANDONED", "evidence": [ 0, 1, @@ -3534,9 +3534,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "goal.configure", - "to": "FRONTIER", + "from": "ACTIVE", + "event": "installation.reconcile-update", + "to": "DORMANT", "evidence": [ 0, 1, @@ -3545,8 +3545,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "goal.configure", + "from": "ACTIVE", + "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ 0, @@ -3556,8 +3556,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "goal.configure", + "from": "ACTIVE", + "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ 0, @@ -3567,8 +3567,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "goal.configure", + "from": "ACTIVE", + "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ 0, @@ -3578,9 +3578,9 @@ "basis": "inferred" }, { - "from": "DORMANT", - "event": "installation.initialize", - "to": "OBSERVED", + "from": "ACTIVE", + "event": "installation.reconcile-update", + "to": "TERMINAL", "evidence": [ 0, 1, @@ -3589,9 +3589,9 @@ "basis": "inferred" }, { - "from": "OBSERVED", - "event": "installation.initialize", - "to": "OBSERVED", + "from": "ACTIVE", + "event": "installation.reconcile-update", + "to": "ABANDONED", "evidence": [ 0, 1, @@ -3600,7 +3600,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "DORMANT", "evidence": [ @@ -3611,7 +3611,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ @@ -3622,7 +3622,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ @@ -3633,7 +3633,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ @@ -3644,7 +3644,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "TERMINAL", "evidence": [ @@ -3655,7 +3655,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.reconcile-update", "to": "ABANDONED", "evidence": [ @@ -3666,7 +3666,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "DORMANT", "evidence": [ @@ -3677,7 +3677,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ @@ -3688,7 +3688,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ @@ -3699,7 +3699,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ @@ -3710,7 +3710,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "TERMINAL", "evidence": [ @@ -3721,7 +3721,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.reconcile-update", "to": "ABANDONED", "evidence": [ @@ -3732,7 +3732,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "DORMANT", "evidence": [ @@ -3743,7 +3743,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "OBSERVED", "evidence": [ @@ -3754,7 +3754,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "ACTIVE", "evidence": [ @@ -3765,7 +3765,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "FRONTIER", "evidence": [ @@ -3776,7 +3776,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "TERMINAL", "evidence": [ @@ -3787,7 +3787,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.reconcile-update", "to": "ABANDONED", "evidence": [ @@ -3798,8 +3798,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "DORMANT", "evidence": [ 0, @@ -3809,8 +3809,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "OBSERVED", "evidence": [ 0, @@ -3820,8 +3820,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "ACTIVE", "evidence": [ 0, @@ -3831,8 +3831,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "FRONTIER", "evidence": [ 0, @@ -3842,8 +3842,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "TERMINAL", "evidence": [ 0, @@ -3853,8 +3853,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.reconcile-update", + "from": "DORMANT", + "event": "installation.update", "to": "ABANDONED", "evidence": [ 0, @@ -3864,8 +3864,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "DORMANT", "evidence": [ 0, @@ -3875,8 +3875,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "OBSERVED", "evidence": [ 0, @@ -3886,8 +3886,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "ACTIVE", "evidence": [ 0, @@ -3897,8 +3897,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "FRONTIER", "evidence": [ 0, @@ -3908,8 +3908,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "TERMINAL", "evidence": [ 0, @@ -3919,8 +3919,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.reconcile-update", + "from": "OBSERVED", + "event": "installation.update", "to": "ABANDONED", "evidence": [ 0, @@ -3930,8 +3930,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "DORMANT", "evidence": [ 0, @@ -3941,8 +3941,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "OBSERVED", "evidence": [ 0, @@ -3952,8 +3952,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "ACTIVE", "evidence": [ 0, @@ -3963,8 +3963,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "FRONTIER", "evidence": [ 0, @@ -3974,8 +3974,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "TERMINAL", "evidence": [ 0, @@ -3985,8 +3985,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.reconcile-update", + "from": "ACTIVE", + "event": "installation.update", "to": "ABANDONED", "evidence": [ 0, @@ -3996,7 +3996,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "DORMANT", "evidence": [ @@ -4007,7 +4007,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "OBSERVED", "evidence": [ @@ -4018,7 +4018,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "ACTIVE", "evidence": [ @@ -4029,7 +4029,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "FRONTIER", "evidence": [ @@ -4040,7 +4040,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "TERMINAL", "evidence": [ @@ -4051,7 +4051,7 @@ "basis": "inferred" }, { - "from": "DORMANT", + "from": "FRONTIER", "event": "installation.update", "to": "ABANDONED", "evidence": [ @@ -4062,7 +4062,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "DORMANT", "evidence": [ @@ -4073,7 +4073,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "OBSERVED", "evidence": [ @@ -4084,7 +4084,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "ACTIVE", "evidence": [ @@ -4095,7 +4095,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "FRONTIER", "evidence": [ @@ -4106,7 +4106,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "TERMINAL", "evidence": [ @@ -4117,7 +4117,7 @@ "basis": "inferred" }, { - "from": "OBSERVED", + "from": "TERMINAL", "event": "installation.update", "to": "ABANDONED", "evidence": [ @@ -4128,7 +4128,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "DORMANT", "evidence": [ @@ -4139,7 +4139,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "OBSERVED", "evidence": [ @@ -4150,7 +4150,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "ACTIVE", "evidence": [ @@ -4161,7 +4161,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "FRONTIER", "evidence": [ @@ -4172,7 +4172,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "TERMINAL", "evidence": [ @@ -4183,7 +4183,7 @@ "basis": "inferred" }, { - "from": "ACTIVE", + "from": "ABANDONED", "event": "installation.update", "to": "ABANDONED", "evidence": [ @@ -4194,9 +4194,9 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", - "to": "DORMANT", + "from": "OBSERVED", + "event": "invocation.rebind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4205,8 +4205,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", + "from": "UNRESOLVED", + "event": "invocation.rebind", "to": "OBSERVED", "evidence": [ 0, @@ -4216,8 +4216,19 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", + "from": "OBSERVED", + "event": "objective.bind", + "to": "OBSERVED", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, + { + "from": "OBSERVED", + "event": "objective.bind", "to": "ACTIVE", "evidence": [ 0, @@ -4227,8 +4238,8 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", + "from": "OBSERVED", + "event": "objective.bind", "to": "FRONTIER", "evidence": [ 0, @@ -4238,9 +4249,9 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", - "to": "TERMINAL", + "from": "DORMANT", + "event": "objective.bind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4249,9 +4260,9 @@ "basis": "inferred" }, { - "from": "FRONTIER", - "event": "installation.update", - "to": "ABANDONED", + "from": "DORMANT", + "event": "objective.bind", + "to": "ACTIVE", "evidence": [ 0, 1, @@ -4260,9 +4271,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", - "to": "DORMANT", + "from": "DORMANT", + "event": "objective.bind", + "to": "FRONTIER", "evidence": [ 0, 1, @@ -4271,8 +4282,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", + "from": "ACTIVE", + "event": "objective.bind", "to": "OBSERVED", "evidence": [ 0, @@ -4282,8 +4293,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", + "from": "ACTIVE", + "event": "objective.bind", "to": "ACTIVE", "evidence": [ 0, @@ -4293,8 +4304,8 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", + "from": "ACTIVE", + "event": "objective.bind", "to": "FRONTIER", "evidence": [ 0, @@ -4304,9 +4315,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", - "to": "TERMINAL", + "from": "FRONTIER", + "event": "objective.bind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4315,9 +4326,9 @@ "basis": "inferred" }, { - "from": "TERMINAL", - "event": "installation.update", - "to": "ABANDONED", + "from": "FRONTIER", + "event": "objective.bind", + "to": "ACTIVE", "evidence": [ 0, 1, @@ -4326,9 +4337,9 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", - "to": "DORMANT", + "from": "FRONTIER", + "event": "objective.bind", + "to": "FRONTIER", "evidence": [ 0, 1, @@ -4337,8 +4348,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", + "from": "TERMINAL", + "event": "objective.bind", "to": "OBSERVED", "evidence": [ 0, @@ -4348,8 +4359,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", + "from": "TERMINAL", + "event": "objective.bind", "to": "ACTIVE", "evidence": [ 0, @@ -4359,8 +4370,8 @@ "basis": "inferred" }, { - "from": "ABANDONED", - "event": "installation.update", + "from": "TERMINAL", + "event": "objective.bind", "to": "FRONTIER", "evidence": [ 0, @@ -4371,8 +4382,8 @@ }, { "from": "ABANDONED", - "event": "installation.update", - "to": "TERMINAL", + "event": "objective.bind", + "to": "OBSERVED", "evidence": [ 0, 1, @@ -4382,19 +4393,8 @@ }, { "from": "ABANDONED", - "event": "installation.update", - "to": "ABANDONED", - "evidence": [ - 0, - 1, - 4 - ], - "basis": "inferred" - }, - { - "from": "OBSERVED", - "event": "invocation.rebind", - "to": "OBSERVED", + "event": "objective.bind", + "to": "ACTIVE", "evidence": [ 0, 1, @@ -4403,9 +4403,9 @@ "basis": "inferred" }, { - "from": "UNRESOLVED", - "event": "invocation.rebind", - "to": "OBSERVED", + "from": "ABANDONED", + "event": "objective.bind", + "to": "FRONTIER", "evidence": [ 0, 1, diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md index ac553dd..1b5ed5b 100644 --- a/docs/architecture/boatstack-v2-transition-catalog.md +++ b/docs/architecture/boatstack-v2-transition-catalog.md @@ -3,72 +3,72 @@ Registry size: **63** transitions. Event classes: authority 9; owned-local 32; owned-external 2; recovery 7; observed-external 13. -Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `workspace`, `plan`, `configuration`, `configuration-policy`, `runtime`, `publication`, `verification`, `recovery`, `transaction`, `recovery-info`, `transaction-info`, `terminal`, `goal`. +Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `workspace`, `plan`, `configuration`, `configuration-policy`, `runtime`, `publication`, `verification`, `recovery`, `transaction`, `recovery-info`, `transaction-info`, `terminal`, `objective`. | Transition | Origin | Owner | Selection | Class | Source phases | Target phases | Authority | Required capabilities | Parameters | Owned resources | Verifier | Recovery | Cost | |---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `repository.write` | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` | -| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` | -| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` | -| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` | -| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | -| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | GOAL_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` | -| `engagement.release` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` | -| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | -| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | -| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | -| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` | -| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` | -| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` | -| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` | -| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` | -| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` | -| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` | -| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` | -| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` | -| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` | -| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` | -| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` | -| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` | -| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | -| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | -| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | -| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | -| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | -| `goal.configure` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` | -| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | -| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` | -| `installation.update` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | -| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | -| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | -| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | -| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | -| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | -| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | -| `plan.create` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | -| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | -| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | -| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | -| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | -| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | -| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | -| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | -| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | -| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` | -| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` | -| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` | -| `repository.attach` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` | -| `repository.detach` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | `repository.write` | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` | -| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` | -| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` | -| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`050e141a0e30430ae9e9340eb4722c93e5d76b3ddb41f58eb260dc59d4e291ef` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` | -| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | -| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | -| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | -| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | -| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | -| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | -| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `product.mutate`, `repository.write` | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | -| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`1df082e8e42dfcc8af9a60c2ecdb118e6d5c9d9420ec219d92ddc35935b161ef` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` | +| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `repository.write` | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` | +| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` | +| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` | +| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` | +| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | +| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` | +| `engagement.release` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` | +| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | +| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | +| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | +| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` | +| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` | +| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` | +| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` | +| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` | +| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` | +| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` | +| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` | +| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` | +| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` | +| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` | +| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` | +| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` | +| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | +| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | +| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | +| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | +| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | +| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | +| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` | +| `installation.update` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | +| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | +| `objective.bind` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `objective_kind*`, `delivery_id*` | `objective` | `verifier:fresh-observation:objective.bind` | `recovery.resume` | `declared-neutral` | +| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | +| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | +| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | +| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | +| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | +| `plan.create` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | +| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | +| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | +| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | +| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | +| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | +| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | +| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | +| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | +| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` | +| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` | +| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` | +| `repository.attach` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` | +| `repository.detach` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | `repository.write` | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` | +| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` | +| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` | +| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`1b894955c60436ed379944c908114a42b1216e4e8bc3882772bd1527236889aa` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` | +| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | +| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | +| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | +| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | +| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | +| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | +| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `product.mutate`, `repository.write` | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | +| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`f2ae5c5b91da3eb6a1687c5fc552a80921a755f0ba1110586aef4377fcf64c7e` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` | `*` marks a required parameter. OR authority is shown with `/`; mandatory authority clauses are shown with `AND`. Source and target facet predicates remain in the canonical JSON returned by `boatstack catalog --format json`. diff --git a/docs/architecture/boatstack-v2-transition-catalog.mmd b/docs/architecture/boatstack-v2-transition-catalog.mmd index 0b3b950..6f0d93b 100644 --- a/docs/architecture/boatstack-v2-transition-catalog.mmd +++ b/docs/architecture/boatstack-v2-transition-catalog.mmd @@ -15,7 +15,7 @@ flowchart TB t01["engagement.release
authority"] t02["engagement.renew
authority"] t03["evidence.approval.revoke
authority"] - t04["goal.configure
authority"] + t04["objective.bind
authority"] t05["plan.abandon
authority"] t06["plan.approve
authority"] t07["plan.approve-amendment
authority"] diff --git a/docs/architecture/capability-authority-boundary.md b/docs/architecture/capability-authority-boundary.md index efba3cb..9ec899f 100644 --- a/docs/architecture/capability-authority-boundary.md +++ b/docs/architecture/capability-authority-boundary.md @@ -29,7 +29,7 @@ a partial intersection. | --- | --- | | `repository.write` | Install or remove Boatstack-managed repository and controller resources. | | `command.execute` | Invoke a configured command or component runtime. | -| `product.mutate` | Change goal, plan, workspace, gate, evidence, delivery, or publication state. | +| `product.mutate` | Change objective, plan, workspace, gate, evidence, delivery, or publication state. | | `publication.prepare` | Create a publication preview artifact. | | `publication.publish` | Perform or correct an external publication. | | `human.approve` | Cross an admission policy that requires human approval. | diff --git a/docs/architecture/control-program-abi.md b/docs/architecture/control-program-abi.md index 8011735..8674a71 100644 --- a/docs/architecture/control-program-abi.md +++ b/docs/architecture/control-program-abi.md @@ -25,10 +25,10 @@ repository source | `requires_runtime` | compatibility | Exact `>=MAJOR.MINOR.PATCH` minimum; checked before registry construction and excluded from the executable fingerprint. | | `capabilities` | executable semantics | Exact, duplicate-free `effects`, `verifiers`, and `capability_surface` sets. The capability surface is the program's maximum intended effect surface; declaration does not grant authority. | | `owned_resources` | executable semantics | Exact, duplicate-free set of resources written by transitions; sorted canonically. | -| `goal_contracts` | executable semantics | Sorted by goal; conjunctive conditions and their set-valued members are sorted. | +| `objective_contracts` | executable semantics | Sorted by objective; conjunctive conditions and their set-valued members are sorted. | | `transitions` | executable semantics | Local declarations are normalized, program-qualified, validated, and sorted by complete ID for hashing. | -`goal_contracts` and `owned_resources` are required beyond the tentative six +`objective_contracts` and `owned_resources` are required beyond the tentative six fields because terminal resolution and effect ownership consume them directly. No repository state, runtime path, agent session, or granted authority belongs to this ABI. @@ -36,9 +36,9 @@ to this ABI. ## Ordering Explicit `selection_class` and `priority` carry selection semantics. Source -declaration order does not. Phase lists, goals, identities, authorities, +declaration order does not. Phase lists, objectives, identities, authorities, evidence, resources, parameters, conditions, interruption points, managed -operations, capabilities, and goal contracts are sets or name-keyed +operations, capabilities, and objective contracts are sets or name-keyed declarations and are normalized into canonical order. Prescription arguments retain source order because argument order is executable. @@ -65,7 +65,7 @@ those failures constructs a registry or reaches effects. The executable fingerprint excludes `program_version` and runtime compatibility because those are separate identities. It includes the complete -normalized transition graph, exact goal contracts, capability bindings, +normalized transition graph, exact objective contracts, capability bindings, resource ownership, and program-qualified identity. Thus representation-only changes remain stable while every kernel-observable control-law change changes the fingerprint. diff --git a/docs/architecture/general-supervisory-kernel.md b/docs/architecture/general-supervisory-kernel.md new file mode 100644 index 0000000..c3fe690 --- /dev/null +++ b/docs/architecture/general-supervisory-kernel.md @@ -0,0 +1,151 @@ +# General supervisory kernel + +Boatstack has one domain-neutral supervisory mechanism and one production +domain: software delivery. + +```text +external Objective + │ exact bind + ▼ +ControlState + Program + Domain Observation + Authority + │ + ▼ +canonical transition relation + │ + ▼ +Operator → Effect Facts → fresh observation → verification → receipt +``` + +The general mechanism is in `boatstack/kernel`. It has no Git, repository, +branch, worktree, plan, coding-host, test, review, publication, or pull-request +types. The software-delivery domain is in `boatstack/delivery`, +`boatstack/flow/standard`, and `boatstack/internal/softwaredelivery`. + +The dependency rule is: + +```text +kernel + ↑ +domain contracts + ↑ +software delivery +``` + +The general kernel never imports the software-delivery implementation. + +## Kernel-owned semantics + +- exact program identity and fingerprint; +- control-instance identity and monotonic state revision; +- external `Objective` and durable exact `ObjectiveBinding`; +- objective scopes: `none`, `optional-preserve`, and `bound-exact`; +- explicit objective bind and clear mutations; +- one transition relation used by resolve and apply; +- state, program, objective, observation, and authority freshness; +- trusted minimum-capability classification; +- transition-owned effect facets; +- operator-neutral execution and fresh postcondition verification; +- program-defined marked modes; +- explicit recovery state and recovery transitions; +- domain-neutral committed receipts. + +## Software-delivery-owned semantics + +- Git and repository identity; +- repository policy and coding-host configuration; +- plans, worktrees, builds, tests, reviews, and evidence; +- delivery and publication state; +- provider-authorized pull-request effects; +- software-specific objective kinds and terminal contracts. + +`DeliveryController` is the software-delivery facade. It retains the existing +transactional repository implementation as a domain executor. It projects the +compiled software Program ABI into one kernel `Program`, supplies admissible +domain candidates, and delegates ordering, targeted/untargeted selection, +marked-state recognition, ambiguity, and authority admission to +`kernel.Relate`. It does not own a second selector. + +The complete software manifest is hashed as the kernel Program's domain +contract fingerprint. The resulting kernel Program fingerprint is the one +identity used by software snapshots, prescriptions, admissions, and receipts. +Changing either generic transition data or any software-domain contract makes +prior prescriptions stale. + +## Objective law + +An objective is external reference data. Supervisory state stores only an +exact binding: + +```text +Objective = id + revision + fingerprint + reference +ObjectiveBinding = objective id + revision + fingerprint +``` + +A command-scoped objective cannot reinterpret an existing binding. Changing +intent creates a new objective revision. Prescriptions that bind an earlier +revision become stale before any operator effect. + +Maintenance transitions use `optional-preserve`: absent remains absent and a +known binding remains byte-for-byte exact. Product progress uses +`bound-exact`. Objective binding is an explicit, capability-gated transition. + +The software domain retains its typed objective projection so it can evaluate +delivery-specific terminal contracts. The kernel freshness envelope binds the +exact status/value fingerprint of that projection. Refreshed evidence alone +does not change the binding; any semantic objective change does. + +## Canonical relation + +Resolve filters the program by mode, recovery state, objective law, the domain +predicate, and authority. Apply reloads the state and observation under the +instance lock, verifies prescription freshness, and calls the same relation. +It cannot use a separate deterministic legality rule. + +Software delivery uses the same relation through its domain adapter. Its +advanced journal and reversible effect machinery remain domain-owned, while +the prescription uses the same `kernel.Freshness` CAS identity as the generic +runtime: state revision, Program fingerprint, snapshot fingerprint, objective +binding fingerprint, and authority fingerprint. + +Programs declare capabilities, but a trusted capability classifier supplies +the minimum for each concrete operation. The operator receives only that +admitted set. Effect facts must stay inside transition-owned facets. + +The generic `Store` is one durability boundary. `CommitTransition` atomically +persists the target control state and its verified receipt; neither may become +visible alone. If an operator may have changed domain state but that atomic +commit fails, `EnterRecovery` records recovery against the unchanged +pre-commit mode. Program compilation rejects any recovery mapping that cannot +run from every source mode of the transition it recovers. + +## Non-software proof fixture + +`boatstack/kernel/runtime_test.go` runs an integer control instance: + +```text +objective.bind → increment → increment → marked(value = 2) + ↘ interruption → reset recovery +``` + +It binds `reach-two@1`, invokes deterministic functions, verifies fresh integer +observations, advances revisions, commits receipts, rejects stale objective and +observation bindings, denies missing capability authority, and recovers an +interrupted operator. The fixture imports no software-delivery package and +requires no Git executable or repository. + +## Enforced properties + +1. Program determinacy: executable law is bound to one program fingerprint. +2. Prescription soundness: resolve and apply share one relation. +3. Freshness: state, program, objective binding, observation, and authority are exact. +4. Authority non-escalation: program and operator declarations grant nothing. +5. Effect containment: trusted capabilities and owned facets bound effects. +6. Objective separation: only an exact binding enters control state. +7. Objective preservation: unowned transitions cannot change the binding. +8. Domain isolation: the kernel has no software-delivery dependency. +9. State ownership: control and effect mutations have explicit owners. +10. Fact fidelity: receipts contain committed effects and verified observations. +11. Recovery liveness: uncertain outcomes enter explicit recovery state. +12. Marked-state generality: the program defines accepted modes. +13. Operator neutrality: the fixture uses deterministic functions, not an agent. +14. Domain substitution: the integer domain runs without kernel changes. diff --git a/docs/configuration.md b/docs/configuration.md index eb4856f..e4a6efe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -122,7 +122,7 @@ configuration source by first-match path discovery. ```sh boatstack attach --repo . --human alice \ - --goal-id bootstrap --goal-kind approved-plan --delivery bootstrap \ + --objective-id bootstrap --objective-kind approved-plan --delivery bootstrap \ --param topology=detached --param config_authority=external ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index caa9806..1246590 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,16 +12,16 @@ boatstack doctor --repo . --format text Windows users run `install.ps1` in PowerShell. The kernel creates `.boatstack/project.json`; review and commit that file before feature work. -## Configure one exact goal +## Configure one exact objective -Every managed delivery has a stable goal ID, delivery ID, and terminal kind. +Every managed delivery has a stable objective ID, delivery ID, and terminal kind. This example targets a verified implementation: ```sh -boatstack goal-configure --repo . \ - --goal-id search-timeout --goal-kind verified-implementation \ +boatstack objective-bind --repo . \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --human alice \ - --param goal_kind=verified-implementation \ + --param objective_kind=verified-implementation \ --param delivery_id=search-timeout ``` @@ -32,11 +32,11 @@ the current, independently hashed `.boatstack/project.json`. ```sh boatstack next --repo . --transition engagement.begin \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --repository-authority --format json boatstack apply --repo . --transition engagement.begin --flow search-timeout \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --repository-authority \ --correlation --prescription-id \ --expected-state-revision \ @@ -56,13 +56,13 @@ time; both are read-only. ```sh boatstack plan-create --repo . \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --human alice \ --param source_path=/absolute/path/to/plan.md \ --param delivery_id=search-timeout boatstack plan-validate --repo . \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --repository-authority ``` @@ -71,12 +71,12 @@ bytes: ```sh boatstack plan-approve --repo . \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --human alice \ --param plan_fingerprint= --param actor=alice boatstack plan-activate --repo . \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout --human alice ``` @@ -103,7 +103,7 @@ status installs the evidence and receipt. Command output is never persisted: ```sh boatstack record-build --repo . --repository-authority \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout \ --param source_revision="$(git rev-parse HEAD)" \ --param evidence_path=/absolute/path/to/build-evidence.json \ @@ -124,7 +124,7 @@ parks the source checkout: ```sh boatstack workspace-cut --repo . --human alice \ - --goal-id search-timeout --goal-kind verified-implementation \ + --objective-id search-timeout --objective-kind verified-implementation \ --delivery search-timeout \ --param branch=feature/search-timeout \ --param base_ref=origin/main \ diff --git a/docs/public-claims.json b/docs/public-claims.json index 13cfbe5..8157a42 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -12,13 +12,13 @@ "status": "verified", "readable_evidence": "architecture/boatstack-v2-kernel.md#14-package-and-dependency-architecture", "implementation": [ - "../boatstack/internal/kernel/engine/engine.go", - "../boatstack/control/control.go", - "../boatstack/kernel.go" + "../boatstack/internal/softwaredelivery/engine/engine.go", + "../boatstack/delivery/control.go", + "../boatstack/delivery_controller.go" ], "verification": [ "../boatstack/flow/standard/completeness_test.go", - "../boatstack/internal/kernel/engine/engine_test.go" + "../boatstack/internal/softwaredelivery/engine/engine_test.go" ], "last_verified_version": "v2.0.0" }, @@ -28,12 +28,12 @@ "status": "verified", "readable_evidence": "generated-files.md#machine-local-controller-state", "implementation": [ - "../boatstack/internal/plant/resolver.go", - "../boatstack/internal/effects/driver.go", - "../boatstack/internal/effects/command_boundary.go" + "../boatstack/internal/softwaredelivery/plant/resolver.go", + "../boatstack/internal/softwaredelivery/effects/driver.go", + "../boatstack/internal/softwaredelivery/effects/command_boundary.go" ], "verification": [ - "../boatstack/internal/effects/integration_test.go" + "../boatstack/internal/softwaredelivery/effects/integration_test.go" ], "last_verified_version": "v2.0.0" }, @@ -43,13 +43,13 @@ "status": "verified", "readable_evidence": "safety.md#transaction-boundary", "implementation": [ - "../boatstack/internal/effects/prepared.go", - "../boatstack/internal/effects/journal.go", - "../boatstack/internal/effects/recovery.go" + "../boatstack/internal/softwaredelivery/effects/prepared.go", + "../boatstack/internal/softwaredelivery/effects/journal.go", + "../boatstack/internal/softwaredelivery/effects/recovery.go" ], "verification": [ - "../boatstack/internal/effects/prepared_test.go", - "../boatstack/internal/effects/recovery_test.go" + "../boatstack/internal/softwaredelivery/effects/prepared_test.go", + "../boatstack/internal/softwaredelivery/effects/recovery_test.go" ], "last_verified_version": "v2.0.0" }, @@ -59,12 +59,12 @@ "status": "verified", "readable_evidence": "architecture/boatstack-v2-kernel.md#15-cli-hook-sdk-mcp-and-host-adapter-contracts", "implementation": [ - "../boatstack/internal/surfaces/protocol.go", - "../boatstack/internal/surfaces/render.go", + "../boatstack/internal/softwaredelivery/surfaces/protocol.go", + "../boatstack/internal/softwaredelivery/surfaces/render.go", "../boatstack/sdk/sdk.go" ], "verification": [ - "../boatstack/internal/surfaces/render_test.go", + "../boatstack/internal/softwaredelivery/surfaces/render_test.go", "../boatstack/sdk/sdk_test.go" ], "last_verified_version": "v2.0.0" @@ -75,13 +75,13 @@ "status": "verified", "readable_evidence": "safety.md#hook-guard", "implementation": [ - "../boatstack/internal/kernel/supervisor/guard.go", - "../boatstack/internal/kernel/supervisor/classify.go", - "../boatstack/internal/surfaces/guard.go" + "../boatstack/internal/softwaredelivery/supervisor/guard.go", + "../boatstack/internal/softwaredelivery/supervisor/classify.go", + "../boatstack/internal/softwaredelivery/surfaces/guard.go" ], "verification": [ "../boatstack/flow/standard/supervisor_parity_test.go", - "../boatstack/internal/surfaces/render_test.go" + "../boatstack/internal/softwaredelivery/surfaces/render_test.go" ], "last_verified_version": "v2.0.0" }, @@ -91,15 +91,15 @@ "status": "verified", "readable_evidence": "architecture/boatstack-v2-kernel.md#11-verification-and-receipt-model", "implementation": [ - "../boatstack/internal/kernel/protocol/admission.go", - "../boatstack/internal/effects/artifacts.go", - "../boatstack/internal/effects/command_boundary.go", - "../boatstack/internal/plant/observer.go" + "../boatstack/internal/softwaredelivery/protocol/admission.go", + "../boatstack/internal/softwaredelivery/effects/artifacts.go", + "../boatstack/internal/softwaredelivery/effects/command_boundary.go", + "../boatstack/internal/softwaredelivery/plant/observer.go" ], "verification": [ - "../boatstack/internal/effects/command_boundary_test.go", - "../boatstack/internal/effects/integration_test.go", - "../boatstack/internal/plant/observer_test.go" + "../boatstack/internal/softwaredelivery/effects/command_boundary_test.go", + "../boatstack/internal/softwaredelivery/effects/integration_test.go", + "../boatstack/internal/softwaredelivery/plant/observer_test.go" ], "last_verified_version": "v2.0.0" }, @@ -109,11 +109,11 @@ "status": "verified", "readable_evidence": "architecture/boatstack-v2-kernel.md#16-process-telemetry-contract", "implementation": [ - "../boatstack/internal/effects/receipts.go", - "../boatstack/kernel.go" + "../boatstack/internal/softwaredelivery/effects/receipts.go", + "../boatstack/delivery_controller.go" ], "verification": [ - "../boatstack/internal/effects/integration_test.go" + "../boatstack/internal/softwaredelivery/effects/integration_test.go" ], "last_verified_version": "v2.0.0" }, @@ -123,16 +123,16 @@ "status": "verified", "readable_evidence": "configuration.md#required-values", "implementation": [ - "../boatstack/internal/kernel/model/state.go", - "../boatstack/internal/kernel/supervisor/supervisor.go", - "../boatstack/internal/effects/state_reducer.go" + "../boatstack/internal/softwaredelivery/model/state.go", + "../boatstack/internal/softwaredelivery/supervisor/supervisor.go", + "../boatstack/internal/softwaredelivery/effects/state_reducer.go" ], "verification": [ "../boatstack/flow/standard/supervisor_parity_test.go", - "../boatstack/internal/kernel/protocol/policy_test.go", - "../boatstack/internal/plant/observer_test.go", - "../boatstack/internal/effects/state_reducer_test.go", - "../boatstack/internal/effects/integration_test.go" + "../boatstack/internal/softwaredelivery/protocol/policy_test.go", + "../boatstack/internal/softwaredelivery/plant/observer_test.go", + "../boatstack/internal/softwaredelivery/effects/state_reducer_test.go", + "../boatstack/internal/softwaredelivery/effects/integration_test.go" ], "last_verified_version": "v2.0.0" }, @@ -145,16 +145,16 @@ "architecture/boatstack-v2-transition-catalog.md", "architecture/boatstack-v2-locus-safety.json", "architecture/boatstack-v2-locus-liveness.json", - "../boatstack/control/control.go", + "../boatstack/delivery/control.go", "../boatstack/core/transitions.json", "../boatstack/flow/standard/transitions.json" ], "verification": [ "../boatstack/flow/standard/completeness_test.go", "../boatstack/flow/standard/historical_test.go", - "../boatstack/internal/surfaces/render_test.go", - "../boatstack/internal/kernel/engine/engine_test.go", - "../boatstack/internal/effects/integration_test.go" + "../boatstack/internal/softwaredelivery/surfaces/render_test.go", + "../boatstack/internal/softwaredelivery/engine/engine_test.go", + "../boatstack/internal/softwaredelivery/effects/integration_test.go" ], "last_verified_version": "v2.0.0" } diff --git a/docs/safety.md b/docs/safety.md index 18890ff..09a75f8 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -8,7 +8,7 @@ publish, overwrite, approve, or advance. ## Exact admission -Each managed effect binds the snapshot fingerprint, invocation, goal, +Each managed effect binds the snapshot fingerprint, invocation, objective, transition, parameters, authority receipts, configuration evidence, and expiry. The engine re-observes under the effect lock. Drift rejects the admission before mutation. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index dab5afb..d3790ff 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -35,7 +35,7 @@ authority is mandatory in addition to human/autonomy for external publication. ## A transition is REFUSED -The requested transition does not match the current source predicate or goal. +The requested transition does not match the current source predicate or objective. Run `status --format json` and `next --format json`. Do not edit state files or retry through another host. diff --git a/release-notes/2026-08-12-general-supervisory-kernel.md b/release-notes/2026-08-12-general-supervisory-kernel.md new file mode 100644 index 0000000..f71e920 --- /dev/null +++ b/release-notes/2026-08-12-general-supervisory-kernel.md @@ -0,0 +1,3 @@ +### Separate the general kernel from software delivery + +Boatstack now exposes a domain-neutral supervisory runtime with instance-bound prescriptions, exact objective bindings, canonical transition admission, operator-neutral execution, capability containment, write-ahead effect attempts, atomic state-and-receipt commits, complete recovery declarations, marked states, and time-valid authority receipts. Existing repository delivery runs through an explicit software-delivery domain boundary.