コンテンツにスキップ
BlackOps1.xは試験的なバージョンです。Production Readyは2.xを予定しています。Releases
BlackOps
Esc
navigateopen⌘Jpreview
このページの内容

Quickstart and Skeleton

Repository main PreviewでGenerated Operation Object、Status、有限Wait、Database、Deferred Workerを確認する。

このPageはRepository mainのPreview Applicationを準備し、Header Authentication、Inline HTTP、Database Transaction、After Commit、Deferred Workerを一続きで確認します。Stable 1.1.0のInstallはInstallで先に完了してください。ここで説明するAuthentication/AuthorizationとDatabase/Transaction Exampleは未Release Surfaceです。Experimental Stable 1.1.0との差はReleasesで確認してください。

1. 実行Channelを選ぶ

Stable 1.1.0

公開済みSkeletonだけを試す場合はVersionを固定し、First OperationのStep 1〜3へ進みます。

composer create-project blackops/skeleton my-app 1.1.0

StableにはGlobal Middleware、Authentication、#[Authorize]がないためFrontend Operation Bridgeもありません。このPageのmain Preview Step 2以降へ進まず、InstallのHTTP 200確認後にFirst OperationのStep 1〜3(Generator、Value、Outcome)を実行してください。Stableの正確な提供範囲はStableとmainで確認してください。

Repository main Preview

以降の認証付きJourneyには、RepositoryのFramework SourceとQuickstartをLocal Path Repositoryで組み合わせたPreviewを使います。これはConsumer E2Eと同じsymlink: false/Version Mappingです。公開VersionのInstall手順ではありません。

git clone https://github.com/kubotak-is/blackops.git blackops-framework
cd blackops-framework

PREVIEW_DIR="$(pwd)/../blackops-preview"
mkdir -p "$PREVIEW_DIR"
cp -a examples/quickstart/. "$PREVIEW_DIR/"

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/framework:ro" -v "$PREVIEW_DIR:/app" -w /app composer:2 \
  composer config repositories.framework \
  '{"type":"path","url":"/framework","options":{"symlink":false,"versions":{"blackops/framework":"1.1.0"}}}'

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/framework:ro" -v "$PREVIEW_DIR:/app" -w /app composer:2 \
  composer install --no-interaction --prefer-dist

cd "$PREVIEW_DIR"
cp .env.example .env
mkdir -p var/build var/log

SAMPLE_API_TOKEN=local-example.env.exampleからLocal Previewへ入ります。未設定または空文字の場合、Sample Authenticatorは既知値へFallbackせずRuntime構成を失敗させます。以降はblackops-previewをProject Rootとして実行します。

接続できない、またはContainerの状態が分からない場合はTroubleshootingを参照してください。

2. Image、Artifact、Databaseを準備する

docker compose build app http
pnpm install --frozen-lockfile
docker compose run --rm app php blackops database:migrate
docker compose run --rm app php blackops build:compile
docker compose run --rm app php blackops database:seed
docker compose run --rm app php blackops frontend:generate
docker compose run --rm app php blackops frontend:check
pnpm test
docker compose up -d

BuildはSourceからOperation/HTTP/Frontend Contract ManifestとDI Containerを生成します。frontend:generateはContractからresources/js/blackops/へTypeScript ESMを生成し、frontend:checkは現在のContractとPath/Bytesが一致するかを非破壊で確認します。pnpm testはDOMなしStrict TypeScriptでGenerated SourceとApplication-owned Consumer Sourceを検査します。

Migration、Build、Seed、Frontend生成はHTTP起動時に暗黙実行されません。Install直後の空Root Seederも明示的に実行し、database:migrate -> build:compile -> database:seedのDeployment順序を確認します。docker compose up -dはHealthyなPostgreSQLとWorker Mode HTTPだけを起動し、Deferred WorkerやSchedulerを勝手に常駐させません。Frontendを使わないApplicationはpnpm以降のFrontend Stepを省略できます。Classic Modeはclassic-mode Profileの明示Fallbackです。

3. PHP Operationを先に確認する

Generated ClientはPHP OperationのContractを複製しません。Previewへ含まれるexamples/quickstartの実装を先に確認します。app/Feature/Welcome/ShowWelcome/ShowWelcome.phpGET /welcome、Operation Type welcome.showを持つInline Operationです。app/Feature/Welcome/ShowWelcome/WelcomeValue.phpは空のInput、WelcomeShown.phpmessage Outcomeを返します。

use App\Security\SampleUserAuthorizationPolicy;
use BlackOps\Core\Attribute\Authorize;

#[Route(method: 'GET', path: '/welcome')]
#[OperationType('welcome.show')]
#[Authorize(SampleUserAuthorizationPolicy::class)]
final readonly class ShowWelcome implements Operation
{
    public function handle(WelcomeValue $value): WelcomeShown
    {
        return new WelcomeShown('Welcome to BlackOps');
    }
}

これはRepository main Previewの/welcomeです。Stable Skeletonの/welcomeは匿名で、#[Authorize]とSample Token Headerを含みません。

final readonly class WelcomeValue implements OperationValue {}

final readonly class WelcomeShown implements Outcome
{
    public function __construct(public string $message) {}
}

app/Feature/Report/GenerateReport/GenerateReport.phpPOST /reports、Operation Type report.generateを持つDeferred Operationです。GenerateReportValuereportNameとwrite-only recipientEmailから、ReportGeneratedreportNamelocationを返します。

4. Generated Operation Objectから呼ぶ

PHP Operationを手書きでTypeScriptへ複製しません。まずProject Rootのtests/Frontend/try-client.tsへ、生成RootとNode 24のglobal fetchを使う最小Clientを書きます。追加Dependencyは不要です。

import { createBlackOpsClient } from '../../resources/js/blackops';

const blackops = createBlackOpsClient({
  baseUrl: 'http://127.0.0.1:8080',
  fetch,
  headers: { 'X-Sample-Token': 'local-example' },
});

async function main(): Promise<void> {
  const response = await blackops.ShowWelcome.fetch({});
  if (response.ok && response.kind === 'completed') {
    console.log(response.status, response.data);
    return;
  }

  throw new Error('Welcome request did not complete.');
}

main().catch(() => {
  throw new Error('try-client failed.');
});

HTTPを起動したProject Rootで実行します。

pnpm exec tsc --ignoreConfig --ignoreDeprecations 6.0 --strict --target ES2022 \
  --module CommonJS --moduleResolution Node10 --lib ES2022,DOM --rootDir . \
  --outDir .build tests/Frontend/try-client.ts
node tests/Frontend/write-runtime-package.mjs
node .build/tests/Frontend/try-client.js

期待結果は200 { message: 'Welcome to BlackOps' }です。SvelteKitではServer-side Requestの補足としてevent.fetchを渡せますが、汎用JavaScriptの前提にはしません。

生成RootからcreateBlackOpsClient()をImportし、Server RequestごとにBase URL、Fetch、Credentialを一度Bindingします。

import {
  createBlackOpsClient,
} from './resources/js/blackops';

const blackops = createBlackOpsClient({
  baseUrl: 'http://127.0.0.1:8080',
  fetch: event.fetch,
  headers: { 'X-Sample-Token': 'local-example' },
  credentials: 'same-origin',
});

このFactoryはServer-only Moduleで作ります。Browser BundleへPrivate Base URLやCredentialを含めず、ApplicationはSessionから安全なHeaderだけを組み立てます。

まず.url()へURLに必要なInputを渡します。Quickstartの4 OperationはPath/Query Parameterがないため引数は不要です。

const url = blackops.ShowWelcome.url();
/welcome

.toRequest()は送信せず、Inputと呼出単位のCredentialからRequestを作ります。SensitiveなrecipientEmailは送信するWrite-only Inputですが、Generated SourceやResultへ値を埋め込みません。

const request = blackops.GenerateReport.toRequest(
  {
    reportName: 'weekly',
    recipientEmail: 'reports@example.com',
  },
);
{
  "url": "http://127.0.0.1:8080/reports",
  "method": "POST",
  "headers": {
    "X-Sample-Token": "local-example",
    "Content-Type": "application/json"
  },
  "body": "{\"reportName\":\"weekly\",\"recipientEmail\":\"reports@example.com\"}"
}

.fetch()は同じBindingで実HTTPへ送り、okkindで判別できるResultを返します。入力と出力は次の対になります。Operation IDと時刻は実行ごとに変わります。

const welcome = await blackops.ShowWelcome.fetch({});
{"ok":true,"kind":"completed","status":200,"data":{"message":"Welcome to BlackOps"}}
const report = await blackops.GenerateReport.fetch(
  { reportName: 'weekly', recipientEmail: 'reports@example.com' },
  { idempotencyKey: 'report-weekly-001' },
);
{"ok":true,"kind":"accepted","status":202,"data":{"operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687697","acceptedAt":"2026-07-14T01:23:45.678901Z"}}
const validation = await blackops.GenerateReport.fetch(
  { reportName: '', recipientEmail: 'reports@example.com' },
);
{"ok":false,"kind":"validation","status":422,"error":{"code":"validation.failed","operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687698","violations":[{"field":"reportName","rule":"not_blank","code":"validation.not_blank"}]}}
const order = await blackops.CreateOrder.fetch(
  { reference: 'order-frontend-001' },
  { idempotencyKey: 'order-frontend-001' },
);
{"ok":true,"kind":"completed","status":200,"data":{"reference":"order-frontend-001","status":"created"}}
const internal = await blackops.TriggerFailure.fetch(
  { reference: 'incident-frontend-001', sensitiveNote: 'private note' },
);
{"ok":false,"kind":"internal","status":500,"error":{"code":"internal_error","operationId":"019f76f1-3fdc-7c18-9d62-b182d42df100"}}

Network Errorや到達不能はHTTP Statusを捏造せずTransport Resultになります。Thrown ErrorのMessageやRaw BodyはResultへ含みません。

const unavailable = createBlackOpsClient({
  baseUrl: 'http://127.0.0.1:8080',
  fetch: async () => {
    throw new Error('connection detail must stay private');
  },
});
const transport = await unavailable.ShowWelcome.fetch({});
{"ok":false,"kind":"transport","status":null,"error":{"code":"network_error"}}

MetadataはReadonly Literalとして同じObjectから参照できます。

blackops.ShowWelcome.type;     // 'welcome.show'
blackops.ShowWelcome.method;   // 'GET'
blackops.ShowWelcome.path;     // '/welcome'
blackops.ShowWelcome.strategy; // 'inline'

Generated ObjectはCallable/Thenableではありません。通信は.fetch()、一回の状態取得は.status()、有限待機は.wait()、Request参照は.toRequest()、URL参照は.url()と明示します。.fetch()は202後に自動Pollingしません。

202で得たOperation IDを使い、現在状態を一回だけ取得します。

const current = await blackops.GenerateReport.status(report.data.operationId);
{"ok":true,"kind":"accepted","status":200,"data":{"schemaVersion":1,"operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687697","operationType":"report.generate","state":"accepted"},"retryAfterSeconds":1}

BrowserでTerminal Stateまで待つ場合はnative AbortControllerと有限Deadlineを渡します。

const controller = new AbortController();
const terminal = await blackops.GenerateReport.wait(report.data.operationId, {
  signal: controller.signal,
  maxWaitMilliseconds: 15_000,
});

if (terminal.ok && terminal.kind === 'completed') {
  terminal.data.outcome.reportName;
  terminal.data.operationId;
  terminal.data.outcome.location;
}
{"ok":true,"kind":"completed","status":200,"data":{"schemaVersion":1,"operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687697","operationType":"report.generate","state":"completed","outcome":{"reportName":"weekly","location":"/reports/generated/weekly.json"}}}

Worker未起動の別Operationへ短いDeadlineを指定すると、無限に待たずpoll_timeoutで停止します。これはOperationのCancelではなく、Workerは後から同じOperationを処理できます。

const timedOut = await blackops.GenerateReport.wait(otherOperationId, {
  signal: new AbortController().signal,
  maxWaitMilliseconds: 150,
});
{"ok":false,"kind":"transport","status":null,"error":{"code":"poll_timeout"}}

5. Inline Operationをcurlで呼ぶ

curl -sS -H 'X-Sample-Token: local-example' http://127.0.0.1:8080/welcome
{"message":"Welcome to BlackOps"}

GET /welcomeはRequest内でShowWelcome::handle()を実行し、Typed OutcomeをHTTP 200へ変換します。

X-Sample-Tokenを省略するとOperation ID付き401、不正値を送るとOperation IDなし401になります。Header Credential自体はOperation ValueとJournalへ入りません。

curl -i http://127.0.0.1:8080/welcome
curl -i -H 'X-Sample-Token: invalid' http://127.0.0.1:8080/welcome

6. 失敗をOperation IDで調べる

Quickstartのdiagnostics.failure.triggerは、認証済みInline Operationを意図的に失敗させるLocal Exampleです。入力のreferenceはApplication Logの相関用、sensitiveNoteはSafe SurfaceのMask確認用です。

curl -sS -X POST -H 'Content-Type: application/json' \
  -H 'X-Sample-Token: local-example' \
  -d '{"reference":"incident-demo-001","sensitiveNote":"private diagnostic note"}' \
  http://127.0.0.1:8080/failures
{"status":"error","code":"internal_error","operationId":"019f76f1-3fdc-7c18-9d62-b182d42df100"}

HTTPはException MessageやSensitive Noteを返さず、調査の入口となるUUIDv7 Operation IDを返します。以下のIDと時刻はExampleであり、実行ごとに変わります。

Human形式でLifecycleを読みます。

docker compose run --rm app php blackops operation:inspect \
  019f76f1-3fdc-7c18-9d62-b182d42df100
Operation
  ID: 019f76f1-3fdc-7c18-9d62-b182d42df100
  Type: diagnostics.failure.trigger
  Strategy: inline
  Schema Version: 1
  Correlation ID: 019f76f1-3fdc-7c18-9d62-b182d42df100
  Causation ID: none
State
  Current: failed
  Terminal: yes
  Authority Source: journal
Availability
  Transport Payload: not_applicable
  Journal: available
  Outcome: not_applicable
  Dead Letter: not_applicable
Actors
  Origin: [masked] (user)
  Authorization: [masked] (user)
  Execution: [masked] (user)
Timeline
  #1 2026-07-18T20:35:49.867594Z operation.received | Attempt: none | Data: {"reference":"incident-demo-001","sensitiveNote":"[masked]"}
  #2 2026-07-18T20:35:49.869481Z attempt.started | Attempt: 019f76f1-3fed-76f4-bf86-338242667b74 (#1) | Data: {}
  #3 2026-07-18T20:35:49.872029Z attempt.failed | Attempt: 019f76f1-3fed-76f4-bf86-338242667b74 (#1) | Data: {"errorType":"RuntimeException","retryable":false}
  #4 2026-07-18T20:35:49.873693Z operation.failed | Attempt: 019f76f1-3fed-76f4-bf86-338242667b74 (#1) | Data: {"errorType":"RuntimeException","retryable":false}
Attempts
  #1 019f76f1-3fed-76f4-bf86-338242667b74 | Started At: 2026-07-18T20:35:49.869481Z | Sequences: 2, 3, 4
Outcome
  Availability: not_applicable
  Value: none

ScriptやSupport Toolから読む場合は同じIDをJSONで取得します。

docker compose run --rm app php blackops operation:inspect \
  019f76f1-3fdc-7c18-9d62-b182d42df100 --json
{
  "schemaVersion": 1,
  "status": "found",
  "operation": {
    "operationId": "019f76f1-3fdc-7c18-9d62-b182d42df100",
    "type": "diagnostics.failure.trigger",
    "schemaVersion": 1,
    "strategy": "inline",
    "correlationId": "019f76f1-3fdc-7c18-9d62-b182d42df100",
    "causationId": null,
    "actors": {
      "origin": {"id": "[masked]", "type": "user"},
      "authorization": {"id": "[masked]", "type": "user"},
      "execution": {"id": "[masked]", "type": "user"}
    }
  },
  "state": {"current": "failed", "terminal": true, "source": "journal"},
  "availability": {
    "transportPayload": "not_applicable",
    "journal": "available",
    "outcome": "not_applicable",
    "deadLetter": "not_applicable"
  },
  "timeline": [
    {"sequence": 1, "event": "operation.received", "occurredAt": "2026-07-18T20:35:49.867594Z", "attemptId": null, "attemptNumber": null, "data": {"reference": "incident-demo-001", "sensitiveNote": "[masked]"}},
    {"sequence": 2, "event": "attempt.started", "occurredAt": "2026-07-18T20:35:49.869481Z", "attemptId": "019f76f1-3fed-76f4-bf86-338242667b74", "attemptNumber": 1, "data": {}},
    {"sequence": 3, "event": "attempt.failed", "occurredAt": "2026-07-18T20:35:49.872029Z", "attemptId": "019f76f1-3fed-76f4-bf86-338242667b74", "attemptNumber": 1, "data": {"errorType": "RuntimeException", "retryable": false}},
    {"sequence": 4, "event": "operation.failed", "occurredAt": "2026-07-18T20:35:49.873693Z", "attemptId": "019f76f1-3fed-76f4-bf86-338242667b74", "attemptNumber": 1, "data": {"errorType": "RuntimeException", "retryable": false}}
  ],
  "attempts": [{"attemptId": "019f76f1-3fed-76f4-bf86-338242667b74", "number": 1, "startedAt": "2026-07-18T20:35:49.869481Z", "events": [2, 3, 4]}],
  "outcome": null
}

Docker-only Quickstartでは、Host BrowserからLocal Viewerを利用できません。PostgreSQLをHostへPublishしておらず、ApplicationのPOSTGRES_HOST=postgresはCompose Network内だけで解決します。また、Viewerは起動したCLI Processの127.0.0.1だけにBindするため、Containerの外から到達できません。この構成では、前述のdocker compose run --rm app php blackops operation:inspect ... Human/JSONを利用してください。Non-loopback Bindへ緩めて回避してはいけません。

Consumer E2EはViewerとHTTP Clientを同じnamed CLI Container、同じLocal Network Namespaceで動かして、Loopback限定のままToken/Session/Read-only動作を検証します。これはHost BrowserへViewerを公開する手順ではありません。

BrowserでViewerを使う場合は、Application/PHP CLI/PostgreSQL/Browserが同じLocal Network Namespaceから相互に到達できるNative Runtimeを準備します。Database HostもそのNative Runtimeから解決可能であることを確認したうえで、Project Rootから明示起動します。

php blackops operation:viewer
http://127.0.0.1:8082/?token=<one-time-bootstrap-token>

このURLを一度開くとSession Cookieへ交換し、/operations/019f76f1-3fdc-7c18-9d62-b182d42df100でHuman表示と同じFailed State、Timeline、Attempt、Mask済みValue/Actorを表示します。Tokenなしは404、POSTは405です。Bootstrap URLを貼り付けたり保存したりせず、調査後はProcessを終了します。

var/log/application.jsonlにはApplication RecordとFramework Failure Recordが同じOperation/Attempt/Correlation IDで残ります。referenceはApplication Contextにありますが、sensitiveNote、Exception Message、Credential、Raw Actor IDはありません。Canonical JournalはRestricted Dataのため、DatabaseのAccess Control、Encryption、RetentionをApplicationで管理してください。

7. Transactional OperationでOrderを作る

Install直後のOrder FeatureはRepository、Transactional Command、Transactional Operation、After Commit Serviceの関係を実行可能な形で示します。まずInputを送ります。

curl -sS -X POST -H 'Content-Type: application/json' \
  -H 'X-Sample-Token: local-example' \
  -d '{"reference":"order-001"}' \
  http://127.0.0.1:8080/orders

HTTP 200と次のTyped Outcomeが返ります。

{"reference":"order-001","status":"created"}

CreateOrder::handle()#[Transactional]が最外Transactionを開きます。CreateOrderCommand::execute()#[Transactional]ですが、同じDefault Connectionなので新しいDBAL Transactionを作らずNested Requiredで参加します。DoctrineOrderRepositoryはConstructor InjectionされたDefault DBAL ConnectionでParameterized SQLを実行します。

Business RowとAfter Commit Rowを入力と対にして確認します。

docker compose exec -T postgres psql -U blackops -d blackops -Atc "
SELECT reference FROM quickstart_orders WHERE reference = 'order-001';
SELECT reference FROM quickstart_order_commits WHERE reference = 'order-001';
"
order-001
order-001

RecordOrderCommit::record()はTransaction内で呼び出しますが、#[AfterCommit]により最外Commit後までQueueされます。そのため、二つ目のRowはOrder Rowと成功Terminal JournalのCommit後に追加されます。

Canonical JournalのTerminalも確認できます。

docker compose exec -T postgres psql -U blackops -d blackops -Atc "
SELECT event
FROM blackops.journal
WHERE operation_id = (
  SELECT operation_id
  FROM blackops.journal
  WHERE event = 'operation.received'
    AND convert_from(encoded_record, 'UTF8') LIKE '%order-001%'
  LIMIT 1
)
ORDER BY sequence;
"
operation.received
attempt.started
attempt.succeeded
operation.completed

After Commitは同期Best-effortで、Callback失敗やProcess Crashを越えた自動Retryを行いません。Email、Webhook、Message Publishなどのat-least-once DeliveryにはTransactional Outboxを使い、Relayの停止/再開、Retry、Dead Letter再開を明示的に運用します。詳しい保証差はTransactionを参照してください。

8. Deferred Operationを受け付ける

curl -sS -X POST -H 'Content-Type: application/json' \
  -H 'X-Sample-Token: local-example' \
  -d '{"reportName":"weekly","recipientEmail":"reports@example.com"}' \
  http://127.0.0.1:8080/reports
{"status":"accepted","operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687697","acceptedAt":"2026-07-14T01:23:45.678901Z"}

HTTP 202はHandler完了ではなく、ValueとContextをPostgreSQLへDurableに保存した合図です。operationIdacceptedAtは実行ごとに変わります。

同じCredentialでPublic Status Resourceを読むと、Worker未起動中はacceptedと正整数Retry-Afterを返します。

OPERATION_ID='<operation-id-from-accepted-response>'
curl -i -H 'X-Sample-Token: local-example' \
  "http://127.0.0.1:8080/operations/${OPERATION_ID}"
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store
Retry-After: 1

{"schemaVersion":1,"operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687697","operationType":"report.generate","state":"accepted"}

空のreportNameは宣言的Validationで受付前にHTTP 422となります。Inline/DeferredのどちらもValidation Failureを202にせず、Handlerを実行しません。

curl -sS -X POST -H 'Content-Type: application/json' \
  -H 'X-Sample-Token: local-example' \
  -d '{"reportName":"","recipientEmail":"reports@example.com"}' \
  http://127.0.0.1:8080/reports
{"status":"rejected","operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687698","category":"validation","code":"validation.failed","violations":[{"field":"reportName","rule":"not_blank","code":"validation.not_blank"}]}

9. Workerで完了させる

Sample Reportは一回目のAttemptでRetryを要求し、二回目で成功します。

docker compose run --rm app php blackops worker:run --iterations=1 --idle-sleep-milliseconds=1
sleep 2
docker compose run --rm app php blackops worker:run --iterations=1 --idle-sleep-milliseconds=1
Worker stopped. Processed claims: 0
Worker stopped. Processed claims: 1

Canonical Journalではauthorization Actorがquickstart-userのまま維持され、Worker Eventのexecution Actorだけがquickstart-worker-1systemになります。Workerは各Attemptで同じPolicyを再評価します。

var/log/journal.jsonlはHTTP ProcessのObserved Projectionです。Inline Welcomeと、HTTP内で完了するValidation Rejection等ではActor IDとrecipientEmail[masked]にしますが、Worker Eventは追記しません。Valid Deferred Reportの完了をJSONLで待たないでください。Header CredentialはMask対象として保存するのではなく、最初からValue/Transport/Journalへ含めません。

VALIDATION_OPERATION_ID='<operation-id-from-validation-response>'
grep "$VALIDATION_OPERATION_ID" var/log/journal.jsonl

Deferred Reportの受理から完了まではCanonical PostgreSQL Journalで確認します。

OPERATION_ID='<operation-id-from-accepted-response>'
docker compose exec -T postgres psql -U blackops -d blackops -Atc "
SELECT sequence || '|' || event || '|' ||
       (convert_from(encoded_record, 'UTF8')::jsonb #>> '{operation,actors,authorization,id}') || '|' ||
       (convert_from(encoded_record, 'UTF8')::jsonb #>> '{operation,actors,execution,id}')
FROM blackops.journal
WHERE operation_id = '${OPERATION_ID}'::uuid
ORDER BY sequence;
"
1|operation.received|quickstart-user|quickstart-user
2|operation.accepted|quickstart-user|quickstart-user
3|attempt.started|quickstart-user|quickstart-worker-1
4|attempt.failed|quickstart-user|quickstart-worker-1
5|retry.scheduled|quickstart-user|quickstart-worker-1
6|attempt.started|quickstart-user|quickstart-worker-1
7|attempt.succeeded|quickstart-user|quickstart-worker-1
8|operation.completed|quickstart-user|quickstart-worker-1

Canonical JournalはRaw Business ValueとActor IDを保持する監査正本です。暗号化、Access Control、RetentionをApplication/運用で構成してください。

Worker完了後は同じStatus ResourceがTyped Outcomeを返し、Terminal ResponseにRetry-Afterは付きません。

{"schemaVersion":1,"operationId":"019f32ab-2be0-7b38-a0a7-1ab2f9687697","operationType":"report.generate","state":"completed","outcome":{"reportName":"weekly","location":"/reports/generated/weekly.json"}}

QuickstartのSampleOperationStatusAuthorizerは、Current Actorと受付時のOrigin Actorがともにuserで、ID/Typeが完全一致するときだけAllowします。これはLocal Exampleであり、ProductionのTenant/Role/Resource Policyではありません。Header欠落はAnonymousのためUnknown/Denyと同じ404、不正TokenはSubject読取前の401です。Operation IDはSecretではありませんが、知っているだけでは参照権限を得ません。

PHP AdapterからOutcomeだけを直接読む場合はPublic OutcomeReaderを利用します。Pending、Terminal、Expiredを区別する主経路はStatus Resourceです。詳しくはOutcomeを参照してください。

10. 終了する

docker compose down

次はFirst Operationで、Generatorが作った3 FileへRoute、Value Validation、Deferred Strategyを追加します。起動、Token、Migration、Buildの問題はTroubleshootingへ戻ってください。