Skip to content

Webapp product walkthrough

Composition · Canonical English日本語

This is the canonical first-use walkthrough for creating a Web application with Composition. Follow it from top to bottom if you are new to this repository; you do not need to read the Composition architecture first.

The example product is Task Ledger. The minimal reference product below provides a browser UI for creating, listing, completing or reopening, deleting, and filtering tasks, persistent storage, an independently supported HTTP JSON API, and a small list / export CLI. Its API also supports title updates, but the minimal browser UI does not claim browser title editing.

This walkthrough defines its own deliberately small reference-product scope. Optional task notes and a browser title-edit control are normal consumer-owned extensions, not completion requirements for this walkthrough. If you add either feature, update the consumer-owned contracts, implementation, tests, and evidence together.

Composition supplies contracts, managed validation material, and a deterministic lifecycle. It does not choose the product framework, database, API implementation, deployment platform, or product test system. Python and SQLite appear later only as concrete Task Ledger product decisions.

Completion path at a glance

Use this short path as the completion gate for the full walkthrough. The numbered sections below retain the detailed product example; this list makes the lifecycle milestone explicit before the details begin.

  1. Doctor — run the installed Composition doctor and resolve local bootstrap blockers.
  2. Inspect — inspect the target repository before any mutation.
  3. Plan — create the read-only Composition plan.
  4. Review — check the target, resolved components, actions, and conflicts.
  5. Apply — apply exactly the reviewed plan to materialize the scaffold.
  6. Validate scaffold — run Composition validate; initial VALID is the scaffold milestone only. Define truthful planning evidence before crossing into product coding.
  7. Create planning checkpoint — when lifecycle checkpoints are selected, follow lifecycle.next_actions and execute its next_action_command.argv rather than reconstructing checkpoint CLI syntax from prose. Record the validated planning state before product coding begins.
  8. Implement product — edit consumer-owned contracts and ordinary product code. Product code alone is not the implemented-product milestone.
  9. Populate product evidence — change implementation evidence from planning/template to truthful product evidence with current records, proofs, commands, and gates.
  10. Run product verifier — execute the authoritative product verifier and retain its result as evidence.
  11. Validate product state — run Composition validation again and follow the machine-readable lifecycle.next_actions. If evidence is still planning or template, continue; do not stop at scaffold VALID.
  12. Create product checkpoint — when lifecycle checkpoints are selected, follow lifecycle.next_actions and execute its projected next_action_command.argv to close the validated planning-to-product transition before release-readiness evaluation.
  13. Check release readiness — run the exact release-readiness operation. Any required deferred browser proof means NOT READY, even when implementation and ordinary validation pass.

The initial VALID result is a scaffold milestone, not an implemented-product or release-ready claim. When lifecycle checkpoints are selected, the planning checkpoint is the hard boundary before product coding and the product checkpoint is the hard boundary before release-readiness evaluation. The implementation milestone requires truthful product evidence and a passing product verifier. The release milestone is separate and remains NOT READY while required proof is missing, deferred, failed, or not yet evaluated.

0. What this walkthrough will produce

You will create a separate product repository named task-ledger. Do not clone TakashiSasaki/templates and start implementing Task Ledger inside it; normal Composition consumption does not require a templates checkout. The normal relationship is:

TakashiSasaki/templates
        |
        | provides the Composition tooling and contracts
        v
your separate task-ledger product repository

By the first milestone you will have:

a separate product repository
        ↓
Composition installed outside that repository
        ↓
composition.json
        ↓
inspect → plan → review → apply → validate
        ↓
a valid Composition scaffold
        ↓
a clear editing boundary and product-development starting point

That first VALID scaffold is intentionally not a claim that the Web application has been implemented or product-tested. The later sections take the same repository through real product code, product verification, implementation evidence, optional Policy adoption, and normal Composition maintenance.

Command examples below use POSIX shell syntax and absolute placeholder paths such as /absolute/path/to/task-ledger. On another shell or operating system, use the equivalent directory-creation commands, but keep the shown Python runner argument semantics. In particular, use absolute paths for the canonical first-use --repository and --config values so their resolution is unambiguous.

1. Create the separate product repository

Choose a normal development location outside any provider-authority checkout.

Run

mkdir /absolute/path/to/task-ledger
cd /absolute/path/to/task-ledger
git init

Expected

  • /absolute/path/to/task-ledger exists as its own Git repository.
  • It does not yet contain .template-composition/lock.json.

Repository change

Yes. This creates the product repository itself. No Composition material has been added yet. Git is used here because Task Ledger is an ordinary version-controlled product repository; Git is not a prerequisite of the Composition consumer runner.

What this means

Task Ledger is the consumer repository. TakashiSasaki/templates remains the provider of Composition and Policy authorities; it is not the application repository you are about to implement and does not need to be cloned for normal use.

Next

Check the Composition runner prerequisite.

2. Check prerequisites

Normal Composition consumption requires CPython 3.11, 3.12, 3.13, or 3.14. Git is not required by the Composition runner.

Run

python --version

Expected

  • Python reports 3.11 through 3.14.

Repository change

None.

What this means

The local machine can run the stdlib-only immutable Composition installer and bootstrap the selected full-SHA source archive without a templates checkout. Cold runner execution requires HTTPS access to GitHub; a missing Python runtime cache can also require access to the configured Python package source. In a sandbox or CI environment whose normal user cache is not writable, set COMPOSITION_RUNTIME_CACHE and COMPOSITION_VALIDATION_CACHE to writable directories outside the product repository before the first runner invocation; the full cache guidance is in Using Composition.

Next

Install the published Composition skill outside Task Ledger.

3. Install Composition

Normal consumers install the Composition skill through the reviewed immutable installer. Pick an installation directory outside the product repository; this walkthrough uses /absolute/path/to/agent-skills/composition. The stable release publishes both the installer full-SHA identity and its SHA-256 digest, so verify the downloaded bytes before writing or executing them.

Run

python -I -c '
import hashlib
import pathlib
import subprocess
import sys
import tempfile
import urllib.request

url = "https://raw.githubusercontent.com/TakashiSasaki/templates/c328fbe2bf733cf32cea54c1054570a94afa693a/scripts/install_composition_skill.py"
expected = "d5422e28b29aaf015c14ffe4d17ae4a0478e0d108a98c951f978f7016f90e607"
data = urllib.request.urlopen(url, timeout=30).read()
actual = hashlib.sha256(data).hexdigest()
if actual != expected:
    raise SystemExit(f"installer SHA-256 mismatch: expected {expected}, got {actual}")
print(f"Verified Composition installer SHA-256: {actual}")
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as handle:
    handle.write(data)
    installer = pathlib.Path(handle.name)
try:
    subprocess.run([sys.executable, "-I", str(installer), *sys.argv[1:]], check=True)
finally:
    installer.unlink(missing_ok=True)
' /absolute/path/to/agent-skills/composition

A digest mismatch exits before installer bytes are written or an installer process is launched. The printed verified digest is useful audit evidence. If that destination already contains an installed Composition skill, append --replace; replacement remains guarded by the installer and is accepted only for an existing directory identified as this skill.

Expected

/absolute/path/to/agent-skills/composition/scripts/run.py exists as the installed repository-facing runner.

Repository change

None in Task Ledger. The skill is installed at the separate destination you selected. Later runtime and validator cache creation also occurs outside the product repository. The selected Composition source itself is an ephemeral full-SHA archive snapshot and is deleted after the invocation.

What this means

You now have the normal consumer entry point. The full-SHA installer URL and the published SHA-256 serve different purposes: the SHA identifies the reviewed source revision, while the digest verifies the bytes actually received before execution. You do not need to understand the installer/skill/toolchain SHA roles before continuing; see Using Composition when you need that trust detail.

Before first Composer execution, you may run the installed skill's read-only local doctor:

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  doctor

A fresh installation normally reports source acquisition as ephemeral-full-sha-archive, Git as not required, and runtime acquisition as required unless a matching validated runtime cache already exists. READY means locally observable prerequisites do not block the runner; it is not Composition validation and does not probe GitHub/package-index availability.

Next

Create Task Ledger's Composition intent file in the product repository.

4. Create composition.json

Task Ledger deliberately supports three caller-visible concerns beyond the Webapp baseline:

Requirement Selection Why
Browser product UI webapp recipe baseline The Webapp artifact resolves the shared Web foundation and defines application-specific surfaces, route behavior, visible states, and Web-specific validation.
Python process and execution commands capability.runtime The product has a maintained application runtime.
Independent HTTP JSON API capability.service Non-browser callers may use the API without the browser UI.
Maintained list / export CLI capability.cli The CLI is a supported caller-visible interface.

A shared process or port does not merge those caller-visible contracts. Conversely, do not select capabilities merely because implementation code happens to use a process, route, or library internally.

Create /absolute/path/to/task-ledger/composition.json with exactly this initial intent:

{
  "schema_version": 1,
  "recipe": "webapp",
  "components": {
    "include": [
      "capability.cli",
      "capability.runtime",
      "capability.service"
    ],
    "exclude": []
  },
  "parameters": {}
}

The same machine-checked example is stored in examples/onboarding/task-ledger/composition.json in the Composition authority. Recipe dependency closure adds required lifecycle components; do not duplicate those required components in include merely to document the closure.

Expected

composition.json is present at the root of the Task Ledger product repository.

Repository change

Yes. composition.json is consumer intent that you created. Composition has still not materialized any scaffold files.

What this means

You have stated what kind of artifact and externally supported capabilities you want. You have not yet asked Composition to mutate the repository.

Next

Inspect the target state.

5. Inspect the repository

Run

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  inspect

Expected

Because you just created the directory and no Composition lock exists, the JSON output contains:

{
  "state": "unmanaged"
}

The real output also includes the absolute target. If you had run inspect before creating the directory, absent would also be a normal new-target state.

Repository change

None. inspect is read-only.

What this means

Composition does not currently manage this repository. That is the expected first-use state.

If you instead see managed-valid, managed-invalid, or managed-interrupted, stop treating this as a fresh initial composition. Use the state-specific workflow in Using Composition; an interrupted repository must be recovered rather than re-initialized.

Next

Plan the initial materialization using the configuration you just created.

6. Plan the initial materialization

For the canonical example, use an absolute --config path.

Run

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  plan --config /absolute/path/to/task-ledger/composition.json

Expected

The JSON plan contains:

  • operation: "initial";
  • the normalized intent;
  • the resolved components;
  • an actions list, normally dominated by create for a fresh repository;
  • a conflicts list, which should be empty before you proceed; and
  • a lock_preview showing the state that would be recorded.

A byte-identical pre-existing destination may be reported as adopt-identical rather than create.

Repository change

None. Initial planning is read-only. It does not create the lock or scaffold.

What this means

You are looking at the complete deterministic mutation proposal before allowing it to run.

--config has an important path rule: a relative path is resolved from the process current working directory, not from --repository. The absolute path above deliberately avoids requiring you to infer that relationship. The same rule applies to a new upgrade that accepts --config.

Next

Review the plan. Do not jump directly from configuration authoring to apply.

7. Review the plan

Check the actions and conflicts fields from the previous command.

Proceed when:

  • the target is /absolute/path/to/task-ledger;
  • the recipe and component intent are the ones you selected;
  • every action is understood (create or an intentional adopt-identical on a fresh target); and
  • conflicts is empty.

If a conflict exists, resolve why the destination already contains different bytes before applying. Do not rename or delete Composition metadata to make the conflict disappear.

Repository change

None. Reviewing a plan is a human decision point, not a mutation step.

What this means

plan is the fail-closed safety boundary between intent and mutation.

Next

Apply exactly the reviewed intent.

8. Apply the scaffold

Run

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  apply --config /absolute/path/to/task-ledger/composition.json

Expected

The JSON result reports status: "applied", operation: "initial", created/adopted destinations, and lock: ".template-composition/lock.json".

Repository change

Yes. This is the first Composition command in the walkthrough that materializes the scaffold. Composition writes .template-composition/lock.json last, after the planned files have been installed and source-state validation succeeds.

What this means

Task Ledger is now a Composition-managed consumer repository. Ownership for each materialized file is recorded in the lock.

Next

Validate the scaffold before starting product implementation.

9. Validate the scaffold

Run

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  validate

Expected

The public JSON result has status: "valid". Selected-component checks include the Webapp and lifecycle validators required by the resolved component set. Because implementation evidence starts in template mode, the implementation-evidence check is deferred rather than asserted as a product claim.

Repository change

No product-repository content is intentionally changed by validation. A cold validation may create or reuse an isolated cache outside the repository.

What this means

Composition validation: VALID means the resolved Composition state and template contracts are valid. It does not mean that Task Ledger is implemented, product-tested, deployed, or release-ready.

This distinction is the boundary between a safe scaffold and a finished product.

Next

Inspect ownership before editing anything generated by Composition.

10. Inspect the generated tree and editing boundary

Read .template-composition/lock.json. Do not edit the lock itself. It records each materialized file's component owner, ownership mode, and materialized digest.

For this Task Ledger configuration, concrete examples are:

File Ownership What you should do
README.md seed Edit it. Replace scaffold wording with Task Ledger-specific documentation.
TEMPLATE.md seed Edit it. Specialize the Webapp product contract.
RUNTIME.md seed Edit it. Record the actual Task Ledger runtime decisions.
CLI_INTERFACE.md seed Edit it. Define the supported list / export behavior.
SERVICE_INTERFACE.md seed Edit it. Define the independently supported JSON API.
contracts/routes.json, contracts/application-routes.json, contracts/surfaces.json, contracts/ui-states.json, contracts/viewports.json seed Edit them. Keep shared route identity/navigation in routes.json and application behavior in application-routes.json, and make all browser contracts truthful for Task Ledger.
contracts/cli-interface.json seed Edit it when the selected CLI becomes a product claim. Keep it in template mode until the caller-visible CLI and executable proof exist.
contracts/implementation-evidence.json seed Edit later, after real proofs exist. It initially remains in template mode.
contracts/manifest.json generated Do not hand-edit it. Composition regenerates it deterministically.
schemas/*.schema.json managed Do not hand-edit them. They remain Composition-owned.
.github/workflows/validate-webapp.yml managed Do not hand-edit it. It is Composition-owned validation wiring.
scripts/validate_contracts.py, scripts/scaffold_webapp_evidence.py and other scaffold validators managed Do not hand-edit them. Use them as provided.
.template-composition/validate.py and other .template-composition validator material managed Do not hand-edit them.
.template-composition/lock.json Composer state Do not hand-edit it. Lifecycle operations own it.
new files such as task_ledger/cli.py or tests/test_task_ledger.py ordinary consumer content Create and edit them normally. They are product implementation, not Composition-owned material.

The generic rule is: seed transfers to consumer ownership after initial materialization; managed and generated remain Composition-owned; a path absent from the lock is ordinary consumer content unless another repository-local authority says otherwise.

Do not copy a managed schema or validator into a product-owned variant merely to bypass validation.

Next

Turn the editable seeds into truthful Task Ledger contracts, then implement the product in ordinary consumer files.

11. Replace template assumptions with the actual product contract

Keep only contract items the product really implements.

Browser contract

A small Task Ledger inventory can use:

Contract Product decision
surface primary: Task Ledger browser UI, local-product audience, non-diagnostic
shared route home at /: canonical, deep-linkable task-list path with the generated main-heading focus target
application route bind home to primary; keep the chosen authentication/history/access-failure behavior and declare ready, empty, and error as the observable Task Ledger states
viewport retain or revise the responsive lower bound and input/zoom behavior to match tested behavior

contracts/routes.json is shared Web foundation authority: it declares the semantic route ID, path, canonical/alias/deep-link properties, and generic accessibility expectations. Do not put Webapp surface, authentication, access-failure, history, or state behavior into that document. contracts/application-routes.json is the Webapp-owned join: its routeId references the shared route and attaches the application surface, authentication/access-failure behavior, history behavior, and route states.

For this reference product, retain the required "minWidthPx": 0 coverage-start sentinel on the base entry in contracts/viewports.json; the browser proof below exercises a representative 320px narrow browser viewport. Keep the generated home shared route focus target as main-heading; Section 12 makes that heading programmatically focusable and focuses it on route entry.

For this reference product, set the home application-route record's states array in contracts/application-routes.json to ["ready", "empty", "error"]. In contracts/ui-states.json, retain ready and add route-scoped empty and error items. Use category: "content" for empty, category: "error" for error, announcement: "polite" for both because #message is a status region, and focusStrategy: "preserve" for all three. The implementation below renders No tasks yet. for empty, renders Could not load tasks. on a failed list refresh while leaving the existing content in place, restores focus to the replacement task action after completion, and moves focus to the status-filter fallback after deleting the focused task. Do not omit observable states from the application-route inventory merely to reduce evidence requirements.

Do not add authentication, administration, role-based authorization, touch support, multiple breakpoints, or diagnostic surfaces merely because a larger application might need them.

Runtime contract

Concretize RUNTIME.md with consumer decisions. For this example:

Implementation ecosystem: CPython 3.11+
Persistence: SQLite
Server command: python -m task_ledger.cli --database task-ledger.db serve --host 127.0.0.1 --port 8080
Distribution: source execution for this example

These are product decisions, not Composition defaults.

Service contract

Concretize SERVICE_INTERFACE.md because the JSON API is independently supported. A small contract can include:

GET    /api/tasks?status=all|open|completed
GET    /api/tasks/{id}
POST   /api/tasks
PATCH  /api/tasks/{id}
DELETE /api/tasks/{id}
GET    /healthz

Specify request validation, result/error semantics, size limits, authentication/exposure decisions, readiness/liveness behavior, restart handling, and the relationship to the browser UI. Sharing one process/listener with the UI does not remove those service obligations.

Because capability.service is selected, replace the editable machine seed contracts/service-interface.json after these operations exist and are executable:

{
  "$schema": "../schemas/service-interface.schema.json",
  "schemaVersion": 2,
  "mode": "product",
  "protocol": "http-json",
  "operations": [
    {
      "id": "list-tasks",
      "invocation": "GET /api/tasks?status=all|open|completed",
      "success": "200 JSON task array for a valid status filter",
      "negative": "400 JSON error for an invalid status filter"
    },
    {
      "id": "get-task",
      "invocation": "GET /api/tasks/{id}",
      "success": "200 JSON task for an existing id",
      "negative": "404 JSON error for a missing id"
    },
    {
      "id": "create-task",
      "invocation": "POST /api/tasks",
      "success": "201 JSON task for a non-empty title",
      "negative": "400 JSON error for an empty title"
    },
    {
      "id": "update-task",
      "invocation": "PATCH /api/tasks/{id}",
      "success": "200 JSON updated task for an existing id",
      "negative": "404 JSON error for a missing id"
    },
    {
      "id": "delete-task",
      "invocation": "DELETE /api/tasks/{id}",
      "success": "204 for an existing id",
      "negative": "404 JSON error when the id no longer exists"
    },
    {
      "id": "health",
      "invocation": "GET /healthz",
      "success": "200 JSON status ok",
      "negative": "404 JSON error for an unknown service path"
    }
  ]
}

Do not switch the service contract to product because a listener starts or because source routes exist. Section 12 executes every declared operation through the HTTP boundary, including a negative path for each operation, and Section 15 links each service_interface/operation/<id> target to integration-test evidence.

CLI contract

Concretize CLI_INTERFACE.md, for example:

python -m task_ledger.cli --database task-ledger.db list --status all
python -m task_ledger.cli --database task-ledger.db export

Document stdout/stderr, exit status, invalid arguments, persistence-target selection, and whether CLI operations have semantics equivalent to corresponding API operations.

Because capability.cli is selected, also replace the editable machine seed contracts/cli-interface.json with the caller-visible product contract after the implementation exists:

{
  "$schema": "../schemas/cli-interface.schema.json",
  "schemaVersion": 2,
  "mode": "product",
  "entrypoints": [
    {
      "id": "task-ledger",
      "command": ["python", "-m", "task_ledger.cli", "--database", "task-ledger.db"],
      "workingDirectory": ".",
      "helpArguments": ["--help"],
      "versionArguments": ["--version"],
      "structuredOutput": {
        "arguments": ["export"],
        "format": "json",
        "contractVersionField": "contractVersion"
      },
      "exitCodes": {
        "success": 0,
        "negativeResult": 1,
        "invalidInput": 2,
        "unavailable": 3,
        "refused": 4,
        "internalFailure": 5,
        "additionalInputRequired": 6
      }
    }
  ]
}

Do not switch this contract to product merely because the CLI source file exists. The --help, --version, structured export, and invalid-input paths below are executed by the product verifier, and Section 15 links those executable checks to a cli_interface/entrypoint/task-ledger evidence record.

12. Create the minimal consumer-owned implementation and tests

Do not stop at a hypothetical tree. The commands below create a small but executable Python/SQLite implementation, browser UI, product tests, and the verifier that Section 13 runs. All of these paths are ordinary consumer content: none is present in the Composition lock.

From /absolute/path/to/task-ledger, create the directories first:

mkdir -p task_ledger/static tests scripts
touch task_ledger/__init__.py

Create task_ledger/cli.py:

from __future__ import annotations

import argparse
import json
import sqlite3
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse


def connect(database: str) -> sqlite3.Connection:
    connection = sqlite3.connect(database)
    connection.row_factory = sqlite3.Row
    connection.execute(
        "CREATE TABLE IF NOT EXISTS tasks ("
        "id INTEGER PRIMARY KEY, title TEXT NOT NULL, completed INTEGER NOT NULL DEFAULT 0)"
    )
    connection.commit()
    return connection


def task_dict(row: sqlite3.Row) -> dict[str, object]:
    return {"id": row["id"], "title": row["title"], "completed": bool(row["completed"])}


def list_tasks(database: str, status: str = "all") -> list[dict[str, object]]:
    if status not in {"all", "open", "completed"}:
        raise ValueError("status must be all, open, or completed")
    query = "SELECT id, title, completed FROM tasks"
    parameters: tuple[object, ...] = ()
    if status != "all":
        query += " WHERE completed = ?"
        parameters = (1 if status == "completed" else 0,)
    query += " ORDER BY id"
    with connect(database) as connection:
        return [task_dict(row) for row in connection.execute(query, parameters)]


def create_task(database: str, title: object) -> dict[str, object]:
    if not isinstance(title, str) or not title.strip():
        raise ValueError("title must be a non-empty string")
    with connect(database) as connection:
        cursor = connection.execute("INSERT INTO tasks(title) VALUES (?)", (title.strip(),))
        row = connection.execute(
            "SELECT id, title, completed FROM tasks WHERE id = ?", (cursor.lastrowid,)
        ).fetchone()
    assert row is not None
    return task_dict(row)


def get_task(database: str, task_id: int) -> dict[str, object] | None:
    with connect(database) as connection:
        row = connection.execute(
            "SELECT id, title, completed FROM tasks WHERE id = ?", (task_id,)
        ).fetchone()
    return task_dict(row) if row is not None else None


def update_task(database: str, task_id: int, changes: dict[str, object]) -> dict[str, object] | None:
    current = get_task(database, task_id)
    if current is None:
        return None
    title = changes.get("title", current["title"])
    completed = changes.get("completed", current["completed"])
    if not isinstance(title, str) or not title.strip() or not isinstance(completed, bool):
        raise ValueError("title must be non-empty and completed must be boolean")
    with connect(database) as connection:
        connection.execute(
            "UPDATE tasks SET title = ?, completed = ? WHERE id = ?",
            (title.strip(), int(completed), task_id),
        )
    return get_task(database, task_id)


def delete_task(database: str, task_id: int) -> bool:
    with connect(database) as connection:
        cursor = connection.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
    return cursor.rowcount == 1


def make_server(database: str, host: str, port: int) -> ThreadingHTTPServer:
    static_root = Path(__file__).with_name("static")

    class Handler(BaseHTTPRequestHandler):
        def send_json(self, status: int, value: object) -> None:
            body = json.dumps(value).encode()
            self.send_response(status)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        def read_json(self) -> dict[str, object]:
            length = int(self.headers.get("Content-Length", "0"))
            value = json.loads(self.rfile.read(length) or b"{}")
            if not isinstance(value, dict):
                raise ValueError("JSON body must be an object")
            return value

        def task_id(self, path: str) -> int | None:
            parts = path.strip("/").split("/")
            if len(parts) == 3 and parts[:2] == ["api", "tasks"] and parts[2].isdigit():
                return int(parts[2])
            return None

        def do_GET(self) -> None:
            parsed = urlparse(self.path)
            if parsed.path == "/healthz":
                self.send_json(HTTPStatus.OK, {"status": "ok"})
                return
            if parsed.path == "/api/tasks":
                status = parse_qs(parsed.query).get("status", ["all"])[0]
                try:
                    self.send_json(HTTPStatus.OK, list_tasks(database, status))
                except ValueError as exc:
                    self.send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)})
                return
            task_id = self.task_id(parsed.path)
            if task_id is not None:
                task = get_task(database, task_id)
                self.send_json(HTTPStatus.OK, task) if task else self.send_json(
                    HTTPStatus.NOT_FOUND, {"error": "task not found"}
                )
                return
            if parsed.path == "/":
                body = (static_root / "index.html").read_bytes()
                self.send_response(HTTPStatus.OK)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                self.wfile.write(body)
                return
            self.send_json(HTTPStatus.NOT_FOUND, {"error": "not found"})

        def do_POST(self) -> None:
            if urlparse(self.path).path != "/api/tasks":
                self.send_json(HTTPStatus.NOT_FOUND, {"error": "not found"})
                return
            try:
                self.send_json(HTTPStatus.CREATED, create_task(database, self.read_json().get("title")))
            except (ValueError, json.JSONDecodeError) as exc:
                self.send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)})

        def do_PATCH(self) -> None:
            task_id = self.task_id(urlparse(self.path).path)
            if task_id is None:
                self.send_json(HTTPStatus.NOT_FOUND, {"error": "not found"})
                return
            try:
                task = update_task(database, task_id, self.read_json())
            except (ValueError, json.JSONDecodeError) as exc:
                self.send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)})
                return
            self.send_json(HTTPStatus.OK, task) if task else self.send_json(
                HTTPStatus.NOT_FOUND, {"error": "task not found"}
            )

        def do_DELETE(self) -> None:
            task_id = self.task_id(urlparse(self.path).path)
            if task_id is None or not delete_task(database, task_id):
                self.send_json(HTTPStatus.NOT_FOUND, {"error": "task not found"})
                return
            self.send_response(HTTPStatus.NO_CONTENT)
            self.end_headers()

        def log_message(self, format: str, *args: object) -> None:
            return

    return ThreadingHTTPServer((host, port), Handler)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--version", action="version", version="Task Ledger 1.0")
    parser.add_argument("--database", required=True)
    subcommands = parser.add_subparsers(dest="command", required=True)
    list_parser = subcommands.add_parser("list")
    list_parser.add_argument("--status", choices=("all", "open", "completed"), default="all")
    subcommands.add_parser("export")
    serve_parser = subcommands.add_parser("serve")
    serve_parser.add_argument("--host", default="127.0.0.1")
    serve_parser.add_argument("--port", type=int, default=8080)
    args = parser.parse_args()

    if args.command == "list":
        for task in list_tasks(args.database, args.status):
            marker = "x" if task["completed"] else " "
            print(f"{task['id']}\t[{marker}]\t{task['title']}")
        return 0
    if args.command == "export":
        print(
            json.dumps(
                {"contractVersion": "1", "tasks": list_tasks(args.database)},
                ensure_ascii=False,
                indent=2,
            )
        )
        return 0

    server = make_server(args.database, args.host, args.port)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Create task_ledger/static/index.html:

<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Task Ledger</title>
<style>li span { overflow-wrap: anywhere; }</style>
<h1 id="main-heading" tabindex="-1">Task Ledger</h1>
<form id="new-task"><input id="title" required><button>Add task</button></form>
<label>Show <select id="status"><option>all</option><option>open</option><option>completed</option></select></label>
<ul id="tasks"></ul>
<p id="message" role="status"></p>
<script>
const heading = document.querySelector('#main-heading');
heading.focus();
const tasks = document.querySelector('#tasks');
const message = document.querySelector('#message');
async function request(path, options = {}) {
  const response = await fetch(path, {headers: {'Content-Type': 'application/json'}, ...options});
  if (!response.ok && response.status !== 204) throw new Error(await response.text());
  return response.status === 204 ? null : response.json();
}
async function load({focusTaskId = null, focusAction = null} = {}) {
  try {
    const values = await request('/api/tasks?status=' + document.querySelector('#status').value);
    tasks.replaceChildren();
    if (!values.length) message.textContent = 'No tasks yet.'; else message.textContent = '';
    for (const task of values) {
      const item = document.createElement('li');
      item.dataset.taskId = String(task.id);
      const label = document.createElement('span');
      label.textContent = task.title + (task.completed ? ' (completed)' : '');
      const toggle = document.createElement('button');
      toggle.dataset.action = 'toggle';
      toggle.textContent = task.completed ? 'Reopen' : 'Complete';
      toggle.onclick = async () => {
        await request('/api/tasks/' + task.id, {method: 'PATCH', body: JSON.stringify({completed: !task.completed})});
        await load({focusTaskId: task.id, focusAction: 'toggle'});
      };
      const remove = document.createElement('button');
      remove.dataset.action = 'delete';
      remove.textContent = 'Delete';
      remove.onclick = async () => {
        await request('/api/tasks/' + task.id, {method: 'DELETE'});
        await load();
        document.querySelector('#status').focus();
      };
      item.append(label, ' ', toggle, ' ', remove); tasks.append(item);
    }
    if (focusTaskId !== null && focusAction) {
      document.querySelector(
        `#tasks li[data-task-id="${focusTaskId}"] button[data-action="${focusAction}"]`
      )?.focus();
    }
  } catch (error) {
    message.textContent = 'Could not load tasks.';
    throw error;
  }
}
document.querySelector('#new-task').onsubmit = async event => {
  event.preventDefault();
  const input = document.querySelector('#title');
  await request('/api/tasks', {method: 'POST', body: JSON.stringify({title: input.value})});
  input.value = ''; await load();
};
document.querySelector('#status').onchange = () => { load().catch(() => {}); };
load().catch(() => {});
</script>

Create tests/test_task_ledger.py:

from __future__ import annotations

import json
import subprocess
import sys
import tempfile
import threading
import unittest
import urllib.error
import urllib.request
from pathlib import Path

from task_ledger.cli import create_task, list_tasks, make_server, update_task


class TaskLedgerTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temporary = tempfile.TemporaryDirectory()
        self.database = str(Path(self.temporary.name) / "tasks.db")

    def tearDown(self) -> None:
        self.temporary.cleanup()

    def test_crud_filter_and_persistence(self) -> None:
        first = create_task(self.database, "write docs")
        self.assertEqual([task["title"] for task in list_tasks(self.database, "open")], ["write docs"])
        update_task(self.database, int(first["id"]), {"title": "write guide", "completed": True})
        self.assertEqual([task["title"] for task in list_tasks(self.database, "completed")], ["write guide"])
        self.assertEqual(list_tasks(self.database), list_tasks(self.database))

    def test_cli_export_uses_selected_database(self) -> None:
        create_task(self.database, "export me")
        result = subprocess.run(
            [sys.executable, "-m", "task_ledger.cli", "--database", self.database, "export"],
            text=True,
            capture_output=True,
            check=True,
        )
        payload = json.loads(result.stdout)
        self.assertEqual(payload["contractVersion"], "1")
        self.assertEqual(payload["tasks"][0]["title"], "export me")

        help_result = subprocess.run(
            [sys.executable, "-m", "task_ledger.cli", "--database", self.database, "--help"],
            text=True,
            capture_output=True,
            check=False,
        )
        self.assertEqual(help_result.returncode, 0, help_result.stderr)
        self.assertIn("export", help_result.stdout)

        version_result = subprocess.run(
            [sys.executable, "-m", "task_ledger.cli", "--database", self.database, "--version"],
            text=True,
            capture_output=True,
            check=False,
        )
        self.assertEqual(version_result.returncode, 0, version_result.stderr)
        self.assertEqual(version_result.stdout.strip(), "Task Ledger 1.0")

        invalid = subprocess.run(
            [
                sys.executable,
                "-m",
                "task_ledger.cli",
                "--database",
                self.database,
                "list",
                "--status",
                "invalid",
            ],
            text=True,
            capture_output=True,
            check=False,
        )
        self.assertEqual(invalid.returncode, 2)
        self.assertIn("invalid choice", invalid.stderr)

    def test_http_api_positive_and_negative_paths(self) -> None:
        server = make_server(self.database, "127.0.0.1", 0)
        thread = threading.Thread(target=server.serve_forever, daemon=True)
        thread.start()
        base = f"http://127.0.0.1:{server.server_port}"

        def request(method: str, path: str, payload: dict | None = None):
            data = None if payload is None else json.dumps(payload).encode()
            headers = {} if payload is None else {"Content-Type": "application/json"}
            return urllib.request.urlopen(
                urllib.request.Request(base + path, data=data, headers=headers, method=method)
            )

        try:
            health = json.load(request("GET", "/healthz"))
            self.assertEqual(health, {"status": "ok"})
            with self.assertRaises(urllib.error.HTTPError) as missing_health:
                request("GET", "/not-a-service-route")
            self.assertEqual(missing_health.exception.code, 404)

            created = json.load(request("POST", "/api/tasks", {"title": "from api"}))
            with self.assertRaises(urllib.error.HTTPError) as invalid_create:
                request("POST", "/api/tasks", {"title": ""})
            self.assertEqual(invalid_create.exception.code, 400)

            open_tasks = json.load(request("GET", "/api/tasks?status=open"))
            self.assertEqual([task["id"] for task in open_tasks], [created["id"]])
            with self.assertRaises(urllib.error.HTTPError) as invalid_filter:
                request("GET", "/api/tasks?status=invalid")
            self.assertEqual(invalid_filter.exception.code, 400)

            fetched = json.load(request("GET", f"/api/tasks/{created['id']}"))
            self.assertEqual(fetched["title"], "from api")
            with self.assertRaises(urllib.error.HTTPError) as missing_get:
                request("GET", "/api/tasks/999999")
            self.assertEqual(missing_get.exception.code, 404)

            updated = json.load(request("PATCH", f"/api/tasks/{created['id']}", {"completed": True}))
            self.assertTrue(updated["completed"])
            with self.assertRaises(urllib.error.HTTPError) as missing_patch:
                request("PATCH", "/api/tasks/999999", {"completed": True})
            self.assertEqual(missing_patch.exception.code, 404)

            deleted = request("DELETE", f"/api/tasks/{created['id']}")
            self.assertEqual(deleted.status, 204)
            deleted.close()
            with self.assertRaises(urllib.error.HTTPError) as missing_delete:
                request("DELETE", f"/api/tasks/{created['id']}")
            self.assertEqual(missing_delete.exception.code, 404)
        finally:
            server.shutdown()
            server.server_close()
            thread.join()



if __name__ == "__main__":
    unittest.main()

Finally create the authoritative product verifier scripts/verify.sh and make it executable:

cat > scripts/verify.sh <<'SH'
#!/bin/sh
set -eu
python -m unittest discover -s tests -v
SH
chmod +x scripts/verify.sh

At this point the verifier exists before the walkthrough asks you to run it. You can also start the application manually with:

python -m task_ledger.cli --database task-ledger.db serve --host 127.0.0.1 --port 8080

Then open http://127.0.0.1:8080/ and exercise create, complete/reopen, delete, and filter behavior. The reference browser contract intentionally does not claim browser title editing. PATCH /api/tasks/{id} remains part of the independently supported API; adding a browser edit control is an ordinary consumer-owned extension that also requires matching browser contract and proof updates. The service and CLI remain independently callable.

Add the real-browser viewport and keyboard proof

The Webapp evidence validator requires real positive and negative browser-level proof for the declared viewports/base and input-capability/keyboard targets. HTTP reachability and the unit/integration tests above do not satisfy that requirement.

Use a matching Chrome or Chrome for Testing binary and ChromeDriver. If they are not already installed, download the matching browser and driver archives from the official Chrome for Testing availability dashboard and extract them outside the product repository. Put chromedriver on PATH, or set CHROMEWEBDRIVER to its absolute path. When Chrome is not on the normal platform path, set CHROME_BINARY to the extracted browser executable.

Check

"${CHROME_BINARY:-google-chrome}" --version
"${CHROMEWEBDRIVER:-chromedriver}" --version

Acquire the reviewed standard-library WebDriver proof into the consumer-owned test directory while preserving the exact received bytes. The full-SHA URL is immutable and the script has no Python package dependency. Hash the in-memory bytes before writing them; do not use text reserialization, newline normalization, or a later reconstruction as the digest input:

python -c '
import hashlib
import pathlib
import urllib.request

data = urllib.request.urlopen("https://raw.githubusercontent.com/TakashiSasaki/templates/7e1352a527cdfa6a20ac5df1a81b404b4a6699b3/examples/onboarding/task-ledger/browser_proof.py", timeout=30).read()
expected = "7921d0308850aeefdb71332c5f089bf6a5d2ed1e50bf5f77b5d3d40eda53030b"
actual = hashlib.sha256(data).hexdigest()
if actual != expected:
    raise SystemExit(f"browser proof SHA-256 mismatch: expected {expected}, got {actual}")
destination = pathlib.Path("tests/test_task_ledger_browser.py")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(data)
print(f"Verified browser proof SHA-256: {actual} ({len(data)} bytes)")
'

The proof starts Task Ledger with a temporary SQLite database and drives it through a real headless Chrome session. It covers:

  • positive responsive behavior at narrow and landscape viewports;
  • negative page-wide horizontal-overflow and zoom-lock checks;
  • genuine 200% browser page-scale operability;
  • positive keyboard create, complete, filter, and delete paths;
  • negative empty-title keyboard submission; and
  • an unknown-route browser negative path.

Add the browser proof to the authoritative verifier:

cat >> scripts/verify.sh <<'SH'
python tests/test_task_ledger_browser.py
SH

Repository change

Yes. The files above are ordinary consumer-owned implementation and verification material. They do not modify Composition-managed/generated paths.

Next

Run the product verifier you just created.

13. Define and run authoritative product verification

Composition does not choose the product test runner. Task Ledger now has one independently runnable consumer-owned command.

Run

./scripts/verify.sh

Expected

The consumer-owned unit/integration checks pass and the command exits successfully. The tests exercise SQLite persistence across independent connections, filtering/update behavior, CLI help/version/structured export plus an invalid-argument exit-2 path, an independently reachable JSON API, health, and a negative invalid-filter case.

Repository change

The verifier does not rewrite Composition-owned material.

What this means

You now have product-behavior evidence that is separate from Composition's structural/contract validation. Before claiming browser edit behavior, either add the corresponding UI control and browser-facing proof or narrow the browser contract so it describes only the behavior actually exposed by the UI.

Next

Derive the exact evidence targets from the current contracts rather than inventing target IDs.

14. Generate the current evidence worklist

The Webapp scaffold includes a read-only deterministic generator.

Run

python scripts/scaffold_webapp_evidence.py > /tmp/webapp-evidence-worklist.json

Expected

A JSON worklist is written to the selected output file. contracts/implementation-evidence.json is unchanged.

Repository change

None from the generator itself. The redirected worklist above is outside the repository.

What this means

The Webapp-owned target set comes from the actual current surface, application-route, state, and viewport contracts, plus the browser-identity proof family. Shared routes.json remains foundation authority and is joined by application-routes.json; do not invent a separate Webapp evidence target for the shared route document.

Next

For every current target, identify the implementation boundary, at least one positive proof, at least one negative proof, the authoritative command that produces those proofs, and a release gate that executes the referenced command.

Multiple records may reuse one command/gate when one suite genuinely proves multiple targets; do not manufacture one command per record.

15. Make incomplete product evidence explicit

The initial contracts/implementation-evidence.json is intentionally in template mode with no product implementation claim. Once Task Ledger has concrete caller-visible requirements, implemented boundaries, and real proof definitions, switch to product mode and enumerate every requirement with a stable requirement ID, linked recordIds, and a non-empty requiredPositiveProofKinds declaration. Do not add a synthetic catch-all requirement merely to satisfy the schema.

The unit/integration portion of the Section 12 verifier is not browser-level proof by itself. The downloaded tests/test_task_ledger_browser.py defines real positive and negative end-to-end-test paths for the viewport and keyboard targets. If that proof exists but Chrome/ChromeDriver or another required execution environment is temporarily unavailable, keep the product claim machine-visible and mark the affected proof deferred. A deferred proof may remain structurally valid, but it is unfinished evidence and blocks release readiness. If the proof definition or locator itself does not yet exist, do not fabricate it; remain in template mode until the product evidence graph can be stated truthfully.

Do not relabel source inspection, HTTP reachability, or unit tests as browser proof. requiredPositiveProofKinds records the minimum acceptable positive proof class for each requirement; for browser interaction use end-to-end-test and/or accessibility-test, while executable CLI behavior can require integration-test.

A command and gate can look like:

{
  "commands": [
    {
      "id": "verify-product",
      "command": "./scripts/verify.sh",
      "purpose": "Run Task Ledger product verification."
    }
  ],
  "releaseGates": [
    {
      "id": "product-verification",
      "purpose": "Require the authoritative product verification command.",
      "commandIds": ["verify-product"]
    }
  ]
}

Each record still needs its exact worklist target, verified implementation-boundary locator, verified positive/negative proof locators, expected results, and selected gate. Do not copy a sample target from this guide; the authoritative target set belongs to the consumer repository.

For the generated viewports/base and input-capability/keyboard records, use tests/test_task_ledger_browser.py as the positive and negative proof locator, end-to-end-test as the proof kind, and verify-product as the command ID. The expected results must describe the corresponding successful interaction and rejected/absent invalid behavior rather than merely saying that the file exists.

Because capability.service is selected, add one contract-item / service_interface / operation / <id> record for every operation declared in contracts/service-interface.json. Use task_ledger/cli.py as the implementation boundary, tests/test_task_ledger.py as positive/negative proof locator, and integration-test as the proof kind. Each operation gets a stable requirement whose requiredPositiveProofKinds contains integration-test; the expanded HTTP test above executes both the documented success and negative path for all six operations. A selected service contract left in template mode, or service records backed only by source inspection/unit-only proof, must keep Composition validation invalid.

Because capability.cli is selected, add one further record whose target is contract-item / cli_interface / entrypoint / task-ledger. Its implementation boundary is task_ledger/cli.py; its positive and negative proof locator is tests/test_task_ledger.py; and its proof kind is integration-test. Link that record from a stable CLI requirement whose requiredPositiveProofKinds contains integration-test. The positive path covers help/version/structured export, while the negative path covers the invalid-argument exit code. A selected CLI contract left in template mode, or a CLI record backed only by source inspection/unit-only proof, must keep Composition validation invalid.

Run the structural validation whenever product evidence changes. Run the stricter release-readiness check before claiming that the evidence can approve a release.

Run

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  validate
python .template-composition/validators/validate_implementation_evidence.py \
  . --release-readiness

When every required proof is available, also run the authoritative product verifier:

./scripts/verify.sh

Expected

  • Composition validation returns status: "valid" with implementation evidence executed rather than template-deferred;
  • the release-readiness command exits successfully only when every required proof, including browser-sensitive proof, is verified; and
  • the authoritative product verification command passes before the implemented-product milestone is claimed.

If Chrome/ChromeDriver is unavailable, a truthful product document may still contain deferred browser proof. In that state the worklist must continue to show the remaining evidence, release readiness must remain NOT READY, and the implemented-product/release-ready milestone must not be claimed.

What this means

Task Ledger can represent both complete and incomplete product evidence without confusing either state with a valid scaffold. product mode means that concrete product requirements and implementation claims exist; release readiness is the stronger statement that every required proof has actually been verified.

16. Optionally adopt coding-agent Policy

Policy is a separate authority, not a Composition capability. Do not add a fictitious capability.policy to composition.json.

If coding agents will maintain Task Ledger, follow the Policy getting-started workflow after Composition has materialized its seeds and transferred those seeds to consumer ownership:

Composition initial
  → consumer-owned seed/product implementation
  → explicit Policy adoption
  → Composition validation + Policy validation/check + product verification

Composition does not own .agent-policy.yml, .agent-policy.lock, or .agent-policy/**. Use the published Policy getting-started guide for the Policy-owned adoption commands rather than copying those semantics into this Composition tutorial.

17. Make ordinary product changes normally

Adding a Task Ledger feature, changing SQLite queries, editing consumer-owned seed contracts, or adding product tests is ordinary repository work. It does not require a Composition update merely because the product changed.

After a product change:

  1. update consumer-owned contracts/evidence truthfully;
  2. run ./scripts/verify.sh;
  3. run Composition validate;
  4. run Policy validation/check as well if Policy is adopted.

Use Composition lifecycle operations only when the Composition source/intent itself changes.

18. Update or upgrade Composition later

When the installed runner selects a newer reviewed Composition revision, inspect first.

For unchanged intent and no compatibility-boundary change:

Run

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  inspect
python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  plan --mode update

Review the read-only plan. If acceptable:

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  apply --mode update

Consumer-owned seed changes are preserved; clean managed/generated material may be replaced or removed according to the reviewed plan. The runner verifies the old lock revision to selected revision ancestry through GitHub's compare API without needing local Git history.

If the plan reports COMPONENT_VERSION_UPGRADE_REQUIRED, or if Task Ledger intentionally changes recipe/components/parameters, make that boundary explicit:

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  plan --mode upgrade --config /absolute/path/to/task-ledger/composition.json

python /absolute/path/to/agent-skills/composition/scripts/run.py \
  --repository /absolute/path/to/task-ledger \
  apply --mode upgrade --config /absolute/path/to/task-ledger/composition.json

Then rerun product verification and Composition validation. Do not edit lock metadata to turn an update/upgrade conflict into apparent success.

Completion checklist

At the first-use scaffold milestone, you have succeeded when:

  • Task Ledger is a separate product repository;
  • the Composition skill is installed outside it and normal Composition use required no templates checkout;
  • composition.json states the intended Webapp/capability selection;
  • inspect → plan → review → apply → validate was followed in order;
  • the plan was understood as read-only before mutation;
  • Composition validation is valid; and
  • you can identify concrete files that are editable seeds, Composition-owned managed/generated material, and ordinary product code.

The implemented-product milestone is stronger. It additionally requires:

  • consumer-owned contracts describe the real product rather than template assumptions;
  • product source and tests exist;
  • the authoritative product verification command passes;
  • implementation evidence is in product mode with complete current-target coverage;
  • when capability.cli is selected, contracts/cli-interface.json is in truthful product mode and every declared CLI entrypoint has executable positive/negative evidence;
  • every caller-visible product requirement has a stable requirement ID, linked records, and a non-empty requiredPositiveProofKinds declaration;
  • real positive/negative proofs satisfy those declared proof kinds, including browser-level proof for browser-sensitive requirements, with no required proof left deferred;
  • Composition validation passes with implementation evidence executed rather than template-deferred;
  • release-readiness validation passes; and
  • optional Policy state is independently valid if Policy was adopted.

If you reached the first milestone, you no longer need to infer what to do next: edit the consumer-owned Task Ledger contracts, add ordinary product source/tests, and proceed through Sections 11–15. Architecture, exact ownership rules, managed recovery, and immutable-source details remain available in Using Composition and the Composer reference when you need them.