diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/code-generation/code-generation-plan.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/code-generation/code-generation-plan.md new file mode 100644 index 0000000000..24c275b40a --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/code-generation/code-generation-plan.md @@ -0,0 +1,32 @@ +# Code Generation Plan — autonomy-review-observability + +## スコープと追跡元 + +U4 `autonomy-review-observability`(Issue #2067)だけを `self-feature` として実装する。正本は U4 の Functional / NFR Design と U3 が提供する immutable autonomy projection である。active / completed Intent の自動判断一覧・詳細・未レビューqueue、人間本人による accept / flag、completed seal を保持する限定append、status、Event Registry / OTel provenance を一つの harness-neutral Core で閉じる。 + +Claude Code、Codex、Cursor、OpenCode、Kimi Code は同じ Core を利用し、eligibility、redaction、review authorization、remediation分類をharness別に複製しない。配布物は package generator から生成し、`dist/` を直接編集しない。 + +## 実装計画 + +- [x] **Step 1 — decision read model**: explicit Intent UUID と lifecycle から U3 の canonical projection を読み、eligible decision の一覧・詳細・未レビューqueueを構築する。decision IDからIntentを逆引きせず、raw evidence / prompt / secretを公開しない。 +- [x] **Step 2 — snapshot-bound pagination**: query fingerprint、target audit revision、completed review extension head、projection event-set digestへcursorを束縛し、page間driftを `CONFLICT(cursorSnapshot)` で拒否する。 +- [x] **Step 3 — real-human authorization**: canonical `HUMAN_TURN` とreview command bindingをsource Intentから直接再検証する。active targetは同一Intent、completed targetは現在のactive Intentをsource contextとし、synthetic / missing / stale provenanceを拒否する。 +- [x] **Step 4 — terminal review append**: accept / flagを `AUTO_DECISION_REVIEWED` としてappendし、same choiceをidempotent、different choiceをterminal conflictにする。completed targetではcompletion sealを変更せず、denseなreview extension chainだけを進める。 +- [x] **Step 5 — safe remediation**: flagは分類に応じて `self-fix`、`self-feature`、または `self-fix-with-feature-alternative` を提案するだけに留める。rollback、effect replay、grant変更、新規Intent生成を行わない。 +- [x] **Step 6 — persistence / replay**: canonical payload digest、event identity、transaction、projection revision、extension chainを検証してsnapshotをreloadし、session / process / compaction / clone後もqueueとterminal receiptを再構築する。改変はfail-closedにする。 +- [x] **Step 7 — status / telemetry**: human / machine statusを同じprojectionから組み立て、completed workflowをterminalのまま保持する。`AUTO_DECISION_REVIEWED` を canonical audit vocabulary の85件目としてEvent Registry、OTel attribute、audit formatへ同期する。 +- [x] **Step 8 — shared projection**: Coreとregistry変更を package generator で全7生成treeへ反映し、現行5 self-install harnessへpromoteする。5 harness contractではexactly-once cohortと共通algorithmを検証する。 +- [x] **Step 9 — verification**: focused test、Event Registry drift、typecheck、Biome、coverage、全体 `test:ci`、重いsuiteの個別再実行、package / promote drift、`git diff --check` を実測する。 +- [x] **Step 10 — convergence follow-up**: 親再検証で検出された `t81` のaudit event count assertion漏れを統合側で85へ修正し、U4成果物に事実を記録する。 + +## 非目標 + +U5 の credential-attested terminal live completion、workflow completion判定、rollback、自動remediation実行、自動Intent生成、PR / merge / GitHub semantics、外部runner / supervisor、harness固有Core、U3 projectionの再実装は対象外とする。レビューqueueは中断点を増やさず、既存autonomy flowを途中でblockingしない。 + +## 検証基準 + +- focused U4 behaviorとEvent Registry driftがgreenである。 +- 新規Coreのcoverageを計測し、主要なlist / detail / review / reload / status経路を通す。 +- `bun run typecheck`、対象Biome check、`bun scripts/package.ts --check`、`bun run promote:self:check` がgreenである。 +- default timeoutで失敗した重い5ファイルは120秒timeoutで個別再実行し、U4由来failureか環境・wall-clock由来かを切り分ける。 +- 実装commitと親統合側の追補修正を区別し、未修正事項を隠さない。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/code-generation/code-summary.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/code-generation/code-summary.md new file mode 100644 index 0000000000..ae22b94b0c --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/code-generation/code-summary.md @@ -0,0 +1,60 @@ +# Code Summary — autonomy-review-observability + +## 実装結果 + +U4 `autonomy-review-observability`(Issue #2067)を、U3 immutable autonomy projectionを消費する harness-neutral Core として実装した。active / completed Intent の自動判断を同じread modelで一覧・詳細表示し、未レビューqueue、real-human accept / flag、completed sealを保持する限定review extension、status / telemetry、永続snapshot reloadを追加した。実装commitは `6cdfcae8005c9d33d825f53ff506fa80a9f4fdc9`(`feat(autonomy): add decision review observability`)である。 + +## 主な変更 + +- `packages/framework/core/tools/amadeus-autonomy-review.ts` + - decision list / detail、redaction、review eligibility、snapshot-bound cursorを実装した。 + - canonical human turnをsource Intentから再検証し、active targetの同一Intent reviewとcompleted targetのactive-source reviewを認可する。 + - accept / flagのterminal append、same-choice idempotency、different-choice conflictを実装した。 + - completed completion sealを変更せず、review eventだけを別extension chainへappendする。 + - snapshot export / reload時にpayload digest、event identity、projection revision、extension headを検証し、改変を拒否する。 + - human / machine status、safe Event Registry fields、safe OTel attributes、5 harness共通contractを公開する。 +- `packages/framework/core/tools/amadeus-audit.ts`、`packages/framework/core/otel/event-registry.ts`、`packages/framework/core/knowledge/amadeus-shared/audit-format.md` + - `AUTO_DECISION_REVIEWED` を canonical 85件目のaudit eventとして追加し、event name、required / optional attributes、audit formatを同期した。 +- `tests/unit/t433-autonomy-review-observability.test.ts` + - active / completed queue、cursor drift、redacted detail、real-human provenance、completed seal保持、tamper reject、synthetic reject、idempotency / conflict、completed status、safe telemetry、exact 5 harness cohortを検証した。 +- package generatorにより Claude Code、Codex、Cursor、OpenCode、Kimi Code、Kiro、Kiro IDE の全7生成treeを同期し、現行5 self-install harnessへ共通Coreをpromoteした。 + +## 安全境界と非目標 + +- flagは `self-fix` / `self-feature` の提案だけを返し、rollback、選択effectの再実行、grant変更、新規Intent生成を行わない。`createdIntentCount` は常に0である。 +- completed review後もworkflowはcompleted / terminalのままであり、completion sealを再計算しない。 +- review queueは観測・事後レビュー用であり、既存autonomy flowへ新しいmid-flow gateを追加しない。 +- PR / merge / GitHub semantics、外部runner / supervisor、U5 terminal live completionはCoreへ含めていない。 +- U3のdecision projectionを入力正本とし、U4に別のdecision authorityを作っていない。 + +## 検証結果 + +- focused U4 test: 10 tests / 29 expects、全件pass。 +- Event Registry driftを含むfocused suite: 36 tests / 579 expects、全件pass。 +- focused coverage: 新規 `amadeus-autonomy-review.ts` は Functions 91.18%、Lines 87.24%。依存するU3 moduleを含む合算値ではなく、新規U4 module単体の値である。 +- `bun run typecheck`: pass。 +- 新規source / testへのtargeted Biome check: warning / errorともに0。 +- `bun run lint`: exit 0。repository既存baselineとして398 warnings / 23 infosが出たが、新規U4 fileにはwarningなし。 +- `bun scripts/package.ts --check`: Claude / Codex / Cursor / Kimi / Kiro / Kiro IDE / OpenCode の全7生成treeでpass。 +- `bun run promote:self:check`: Claude Code / Codex / Cursor / OpenCode / Kimi Code の現行5 self-install harnessでpass。 +- `git diff --check`: pass。 + +## 全体CIとdefault timeoutの切り分け + +`bun run test:ci` は765 test filesを完走したが、default timeout / wall-clock driftとして次の5ファイル、合計7 assertionsが失敗した。 + +- `tests/integration/t-codex-hooks-migration.test.ts` +- `tests/integration/t-solo-standing-grant-opencode-mint.test.ts` +- `tests/integration/t225-upstream-v2-migration-preflight.test.ts` +- `tests/smoke/t05-run-tests-parallel.test.ts` +- `tests/unit/t17.test.ts` + +この5ファイルを `bun test --timeout 120000` でまとめて再実行した結果は 212 pass / 1 skip / 0 fail、2,302 expects だった。skipはinvalid UTF-8 path bytesを扱う環境依存caseである。したがってdefault runの5件はU4 behaviorの回帰ではなく、既知の重いsuiteの時間制約として切り分けた。AWS credentials無効時のlive SDK / substrate testもrunner規則どおりskipされた。 + +## 親再検証で判明した追補修正 + +U4実装commitでは `t81` のaudit event count説明を85へ更新した一方、実assertion `expect(count).toBe(84)` の更新が漏れていた。親の再検証がこの不整合を検出し、親統合commit `5a1851593ed297f0942b63affac81a4be8bc8903`(`fix(autonomy-review): update audit count assertion`)で `expect(count).toBe(85)` へ修正済みである。本Boltでは親commitを取り込まず、コードを追加変更せず、収束事実だけを記録する。 + +## 残作業 + +U4の実装残はない。後続のU5はU4のreview / status projectionを消費するため、U4統合後に開始する。U5のterminal completion実装や検証はこの成果物の対象外である。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/business-logic-model.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/business-logic-model.md new file mode 100644 index 0000000000..61394b82e8 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/business-logic-model.md @@ -0,0 +1,888 @@ +# Business Logic Model — autonomy-review-observability + +## 上流入力と設計範囲 + +本設計は`units-generation/unit-of-work.md`、`units-generation/unit-of-work-story-map.md`、`requirements-analysis/requirements.md`、`application-design/components.md`、`application-design/component-methods.md`、`application-design/services.md`を正本とする。対象はU4 `autonomy-review-observability`、FR-OBS-001〜007、2067-AC18〜21、およびU4へ割り当てられたstatus / privacy / telemetry contractである。 + +実装範囲はM05 decision record / query refinement、M07 read model・protected review append・status・Event Registry / OTel、M06 CLI projection / UX、M09の5 harness contract snapshotである。U3のimmutable `AutoDecisionRecord`を消費し、Intent reopen、rollback、過去event変更、新Intent自動作成、terminal live completionを所有しない。 + +## Public contract refinement + +| Contract | U4 refinement | Owner | +|---|---|---| +| `DecisionQuery` | explicit Intent UUID、lifecycle、review state、stable cursorを束縛 | M05 / M07 | +| `DecisionDetail` | question / options / selected / decider / basis / grant / redacted evidence / degradation / reviewを返す | M05 | +| `DecisionReviewCommand` | target Intent、decision、choice、flag metadataを同じreal human turnへ束縛 | M06 / M07 | +| `DecisionReviewProjection` | unreviewed / accepted / flaggedとreview event identityを保持 | M05 / M07 | +| `PostSealReviewExtension` | completed sealを変更せずreview eventだけをhash-chain append | M07 | +| `RemediationSuggestion` | self-fix / self-featureの非実行提案を返す | M06 | +| `MachineStatus` | mode / workflow / grant / stop / resume / unreviewed countをstrict schemaで返す | M07 | + +U4は上流の概念的APIを次のclosed public schemaへ具体化する。`StableId`、`Sha256Digest`、`AutonomyMode`、`WorkflowExecutionState`、`GrantState`、`VerifiedHumanTurn`、`AuditEventPlan`、`CanonicalAuditEvent`、`AuditCommitReceipt`、`AutoDecisionRecord`、`AutonomyProjection`、`WorkflowResult`、`ResumeCondition`、`ContractError`、`ContractResult`は上流M00 / M04 / M05 / M07の定義をそのまま使う。既存`AutoDecisionRecord`はprotected reducer inputに留め、public read APIからraw payloadを返さない。 + +```ts +type DecisionSourceKind = AutoDecisionRecord["decider"]; +type DecisionReviewState = AutoDecisionRecord["reviewState"]; +type ReviewChoice = "accept" | "flag"; +type IntentLifecycle = "active" | "completed"; +type RedactionStatus = "redacted" | "withheld"; + +type SafeDecisionOption = { + optionId: StableId; + safeLabel: string | null; + labelDigest: Sha256Digest | null; + redactionStatus: RedactionStatus; +}; +type SafeEvidenceReference = { + evidenceFingerprint: Sha256Digest | null; + safeKind: StableId; + redactionStatus: RedactionStatus; +}; +type SafeSubjectReference = { + subjectRef: StableId | null; + redactionStatus: RedactionStatus; +}; +type DecisionReviewReceipt = { + reviewId: StableId; + reviewEventId: StableId; + auditTransactionId: StableId; + committedEventIdentities: readonly [StableId]; + stateProjectionRevision: number; + state: "accepted" | "flagged"; + remediation: "self-fix" | "self-feature" | "self-fix-with-feature-alternative" | null; +}; +type DecisionSummary = { + intentUuid: StableId; + decisionId: StableId; + questionId: StableId; + occurrenceId: StableId; + safeQuestion: string | null; + questionDigest: Sha256Digest | null; + selectedOptionId: StableId; + decisionSource: DecisionSourceKind; + safeBasisDigest: Sha256Digest | null; + decisionPrincipal: SafeSubjectReference; + decisionActor: SafeSubjectReference; + grantId: StableId | null; + evidenceFingerprint: Sha256Digest | null; + degradedCapability: StableId | null; + reviewState: DecisionReviewState; + redactionStatus: RedactionStatus; +}; +type DecisionCursor = { + intentUuid: StableId; + queryFingerprint: Sha256Digest; + targetAuditRevision: number; + reviewExtensionHead: StableId | null; + projectionEventSetDigest: Sha256Digest; + lastOccurrenceId: StableId; + lastDecisionId: StableId; + cursorDigest: Sha256Digest; +}; +type DecisionQuery = { + intentUuid: StableId; + lifecycle: IntentLifecycle | "either"; + reviewState?: DecisionReviewState; + pageSize: number; + cursor?: DecisionCursor; +}; +type DecisionPage = { + items: readonly DecisionSummary[]; + nextCursor: DecisionCursor | null; + queryFingerprint: Sha256Digest; +}; +type DecisionDetail = DecisionSummary & { + options: readonly SafeDecisionOption[]; + evidence: readonly SafeEvidenceReference[]; + graphRevision: Sha256Digest; + auditEventId: StableId; + reviewReceipt: DecisionReviewReceipt | null; +}; + +type HumanReviewCommandBinding = { + sourceIntentUuid: StableId; + targetIntentUuid: StableId; + decisionId: StableId; + choice: ReviewChoice; + commandOccurrenceId: StableId; + flagClassification: "contract-defect" | "specification-change" | "unspecified" | null; + safeNoteDigest: Sha256Digest | null; +}; +type HumanReviewTurnBindingPayloadV1 = HumanReviewCommandBinding & { + schemaVersion: "1"; + sourceHumanTurnId: StableId; + commandBindingDigest: Sha256Digest; +}; +type HumanReviewAuthorizationInput = { + command: HumanReviewCommandBinding; + sourceHumanTurnId: StableId; + sourceHumanTurnEventId: StableId; +}; +type HumanReviewAuthorizationReceipt = HumanReviewCommandBinding & { + sourceHumanTurnId: StableId; + sourceHumanTurnEventId: StableId; + sourceAuditRevision: number; + principalId: StableId; + reviewActorId: StableId; + commandBindingDigest: Sha256Digest; +}; +type CanonicalReviewSourceSnapshot = { + sourceIntentUuid: StableId; + lifecycle: "active"; + sourceProjectionRevision: number; + humanTurn: VerifiedHumanTurn; + humanTurnEvent: CanonicalAuditEvent; + humanTurnCommitReceipt: AuditCommitReceipt; +}; +type CanonicalReviewSourceReader = { + readActiveSource( + sourceIntentUuid: StableId, + sourceHumanTurnId: StableId, + sourceHumanTurnEventId: StableId, + ): ContractResult; +}; +type RegisteredDecisionActor = { + actorRef: StableId; + actorRegistryEntryId: StableId; + kind: "core-engine" | "harness-adapter"; +}; +type DecisionActorRegistryReader = { + readRegisteredActor(actorRegistryEntryId: StableId): ContractResult; +}; +type CanonicalAutonomyProvenanceSnapshot = { + intentUuid: StableId; + audit: readonly CanonicalAuditEvent[]; + auditRevision: number; + stateProjectionRevision: number; +}; +type CanonicalAutonomyProvenanceReader = { + readIntentSnapshot(intentUuid: StableId): ContractResult; +}; +type AutoDecisionPrincipalAuthorizationReceipt = + | { + schemaVersion: "1"; + kind: "mode-semi"; + intentUuid: StableId; + decisionId: StableId; + occurrenceId: StableId; + principalRef: StableId; + principalProvenanceEventId: StableId; + sourceAuditRevision: number; + modeProjectionRevision: number; + gateOccurrenceId: StableId; + grantId: null; + grantExerciseId: null; + receiptDigest: Sha256Digest; + } + | { + schemaVersion: "1"; + kind: "full-grant"; + intentUuid: StableId; + decisionId: StableId; + occurrenceId: StableId; + principalRef: StableId; + principalProvenanceEventId: StableId; + sourceAuditRevision: number; + modeProjectionRevision: number; + gateOccurrenceId: null; + grantId: StableId; + grantExerciseId: StableId; + receiptDigest: Sha256Digest; + }; +type AutoDecisionPrincipalAuthorizationRequest = { + intentUuid: StableId; + decisionId: StableId; + occurrenceId: StableId; + authorization: AutoDecisionCommitAuthorization; +}; +type AutoDecisionPrincipalAuthorizer = { + authorize( + request: AutoDecisionPrincipalAuthorizationRequest, + ): ContractResult; + verify( + receipt: AutoDecisionPrincipalAuthorizationReceipt, + ): ContractResult; +}; +type AutoDecisionSubjectInput = { + intentUuid: StableId; + decisionId: StableId; + actorRegistryEntryId: StableId; +}; +type AutoDecisionSubjectPayloadV1 = { + schemaVersion: "1"; + authorizationKind: AutoDecisionPrincipalAuthorizationReceipt["kind"]; + principalRef: StableId; + principalProvenanceEventId: StableId; + principalAuthorizationReceiptDigest: Sha256Digest; + sourceAuditRevision: number; + actorRef: StableId; + actorRegistryEntryId: StableId; + actorKind: RegisteredDecisionActor["kind"]; +}; +type AutoDecisionCommitAuthorization = + | { + kind: "mode-semi"; + intentUuid: StableId; + modeProjectionRevision: number; + modeProvenanceEventId: StableId; + gateOccurrenceId: StableId; + } + | { + kind: "full-grant"; + exercise: ReservedGrantExercise; + }; +type AutoDecisionCommitInput = { + decision: AutoDecisionRecord; + principalAuthorization: AutoDecisionPrincipalAuthorizationReceipt; + subject: AutoDecisionSubjectInput; +}; +type AutoDecisionCommitPlan = AuditEventPlan & { + eventType: "AUTO_DECIDED"; + eventIdentity: StableId; + intentUuid: StableId; + fields: { + decision_v1: string; + subject_v1: string; + }; +}; +type AutoDecisionCommitPlanner = { + plan(input: AutoDecisionCommitInput): ContractResult; +}; +type DecisionReviewCommand = { + targetIntentUuid: StableId; + decisionId: StableId; + choice: ReviewChoice; + expectedTargetAuditRevision: number; + expectedCompletionSealDigest: Sha256Digest | null; + humanAuthorization: HumanReviewAuthorizationReceipt; +}; +type AutoDecisionReviewedPayloadV1 = { + schemaVersion: "1"; + targetIntentUuid: StableId; + decisionId: StableId; + reviewId: StableId; + choice: ReviewChoice; + reviewPrincipalRef: StableId; + reviewActorRef: StableId; + decisionPrincipalRef: StableId | null; + decisionPrincipalStatus: RedactionStatus; + decisionActorRef: StableId | null; + decisionActorStatus: RedactionStatus; + decisionSource: DecisionSourceKind; + safeBasisDigest: Sha256Digest | null; + grantId: StableId | null; + sourceIntentUuid: StableId; + sourceHumanTurnId: StableId; + sourceHumanTurnEventId: StableId; + commandOccurrenceId: StableId; + commandBindingDigest: Sha256Digest; + auditTransactionId: StableId; + receiptProjectionRevision: number; + lifecycleAtReview: IntentLifecycle; + remediation: DecisionReviewReceipt["remediation"]; + flagClassification: HumanReviewCommandBinding["flagClassification"]; + safeNoteDigest: Sha256Digest | null; + redactionStatus: RedactionStatus; +}; +type AutoDecisionReviewedEventPlan = { + eventType: "AUTO_DECISION_REVIEWED"; + eventIdentity: StableId; + intentUuid: StableId; + payload: AutoDecisionReviewedPayloadV1; + payloadDigest: Sha256Digest; +}; +type PostSealReviewExtension = { + schemaVersion: "1"; + targetIntentUuid: StableId; + completionSealDigest: Sha256Digest; + previousExtensionId: StableId | null; + reviewEventId: StableId; + reviewPayloadDigest: Sha256Digest; + auditTransactionId: StableId; + extensionRevision: number; + extensionId: StableId; +}; + +type DecisionReviewCounts = { + total: number; + unreviewed: number; + accepted: number; + flagged: number; +}; +type SafeGrantScopeSummary = { + scopeFingerprint: Sha256Digest; + selfScopeId: StableId; + allowedInteractionKinds: readonly ("stage-gate" | "phase-gate" | "walking-skeleton" | "question")[]; +}; +type SafeMigrationDiagnostic = { + status: "legacy-non-authoritative"; + legacyGrantIds: readonly StableId[]; + recommendedHumanAction: "select-none" | "select-semi" | "issue-full"; +}; +type ReviewStatusInput = { + intentUuid: StableId; + lifecycle: IntentLifecycle; + autonomy: AutonomyProjection; + workflowResult: WorkflowResult | null; + currentGrantScope: SafeGrantScopeSummary | null; + decisionPolicyCount: number; + decisionCounts: DecisionReviewCounts; + reviewExtensionHead: StableId | null; + legacyDiagnostic: SafeMigrationDiagnostic | null; +}; +type GrantStatusView = { + grantId: StableId; + state: GrantState; + scope: SafeGrantScopeSummary; +}; +type MachineStatus = { + intentUuid: StableId; + lifecycle: IntentLifecycle; + autonomyMode: AutonomyMode; + workflowExecutionState: WorkflowExecutionState; + grant: GrantStatusView | null; + decisionPolicyCount: number; + decisionCount: number; + unreviewedDecisionCount: number; + acceptedDecisionCount: number; + flaggedDecisionCount: number; + suspendedReason: WorkflowResult["reasonCode"]; + stopReason: WorkflowResult["reasonCode"]; + resumeCondition: ResumeCondition | null; + legacyDiagnostic: SafeMigrationDiagnostic | null; +}; + +function listAutoDecisions(query: DecisionQuery): ContractResult; +function getAutoDecision(intentUuid: StableId, decisionId: StableId): ContractResult; +function authorizeHumanReview(input: HumanReviewAuthorizationInput): ContractResult; +function createAutoDecisionCommitPlanner(deps: { + actorRegistry: DecisionActorRegistryReader; + principalAuthorizer: AutoDecisionPrincipalAuthorizer; +}): AutoDecisionCommitPlanner; +function createAutoDecisionPrincipalAuthorizer(deps: { + provenanceReader: CanonicalAutonomyProvenanceReader; +}): AutoDecisionPrincipalAuthorizer; +function serializeHumanReviewTurnBinding( + payload: HumanReviewTurnBindingPayloadV1, +): ContractResult; +function appendDecisionReview(input: DecisionReviewCommand): ContractResult; +function serializeAutoDecisionReviewed(plan: AutoDecisionReviewedEventPlan): ContractResult; +function validatePostSealReviewExtension(value: unknown): ContractResult; +function projectHumanStatus(input: ReviewStatusInput): ContractResult; +function projectMachineStatus(input: ReviewStatusInput): ContractResult; +``` + +`CanonicalReviewSourceReader`はM07内部のstore dependencyであり、command / adapter inputではない。`authorizeHumanReview`はcallerからaudit / commit receipt / lifecycleを受け取らない。このreaderが`sourceIntentUuid`からcurrent projection、source audit、commit receiptsを直接取得し、source lifecycle=active、`HUMAN_TURN` event identity / turn ID / Intent / principal、event fieldsのcommand occurrence / binding digest、そのeventを含むcommit receipt / source audit revisionをexact matchする。`appendDecisionReview`もreceiptをbearer tokenとして信用せず、同じM07 readerでsource referenceを再検証する。 + +UserPromptSubmit adapterはreal human turnを記録する同じ`HUMAN_TURN` eventのreserved field `review_command_v1`へ、`HumanReviewTurnBindingPayloadV1`を型宣言順canonical JSONで保存する。bindingはflag classificationとsafe note digestをexplicit nullを含めて保持する。M07はunknown / missing field、binding digest mismatch、turn / source mismatchを拒否する。このfieldはreview commandが明示されたturnだけに存在し、通常のhuman turn schemaをreview authorizationへ暗黙変換しない。 + +新規`AUTO_DECIDED` producerの実入口はM05-owned `AutoDecisionCommitPlanner.plan`だけである。その前段でM06はM04-owned `AutoDecisionPrincipalAuthorizer.authorize`へdecision identityとclosed authorization unionを渡す。authorizerはcaller projectionを受けず、M04-owned `CanonicalAutonomyProvenanceReader` portを通じてclosed `CanonicalAutonomyProvenanceSnapshot`を読み、snapshot.auditから`replayAutonomy`とgrant exercise replayを内部実行する。M07 adapterがこのportをM00 eventとnumberだけで実装するため、M04からM07へのimportはない。 + +M07 adapterの`readIntentSnapshot`はcanonical multi-shard audit、`appendProtectedEvents`の`AuditTransaction.expectedRevision`に使うauthoritative audit lock revision、最新`AuditCommitReceipt.stateProjectionRevision`を同じread lock / snapshot transaction内で返す。event件数、shard sequence最大値、timestampからrevisionを合成しない。authorizerの`sourceAuditRevision`はsnapshot.auditRevision、`modeProjectionRevision`はsnapshot.stateProjectionRevisionを正本とし、request側値と違えば拒否する。 + +authorizerは`mode-semi`ではgrant不要のmode revision / mode provenance event / gate occurrenceとprincipalをcanonical auditから解決し、`full-grant`ではreserved exercise / active grant / grant issuance principalを解決するclosed receiptを返す。receiptはdecision / occurrence / principal / provenance event / source audit revision / mode revision / nullable grant / exerciseへ束縛したcanonical digestを持つ。semi variantはdecision.grantId=null、full variantはdecision / exercise candidate / grant一致を必須にするため、grantのないsemiへfull reservationを捏造しない。入力にprincipal refやprovenance eventを受けず、M06がそれらを組み立てる余地を残さない。 + +M06は取得したreceiptとactor registry entry IDだけをM05 plannerへ渡す。M05は許可済みM04 public interfaceの`AutoDecisionPrincipalAuthorizer.verify`を呼び、authorizerは新しい同一snapshot readでreceipt digest、principal event、audit / projection revision、mode / grant / exerciseを再検証する。revisionが変わっていれば古いreceiptを返さず`CONFLICT`とし、M06はauthorizeからやり直す。actorはM05-owned `DecisionActorRegistryReader` portをM07 canonical Event Registry adapterが実装してplanner生成時に注入し、M05からM07をimportしない。plannerはportからactor ref / kindを取得し、caller提供actor payloadを信用しない。decision / receipt / subjectのIntent・decision一致とcanonical registry rowを検証し、同じ完全な`AuditEventPlan`のclosed fieldsへ`decision_v1`と`subject_v1`を必須生成する。 + +`subject_v1`はauthorization kind、principal ref / provenance event、receipt digest、source audit revision、actor ref / registry entry / kindをclosed schemaで保存する。M06はM04 exercise event、M05 decision event、workflow effect eventを集約する`AuditTransaction.expectedRevision`へreceipt.sourceAuditRevisionをそのまま設定する。M07はappend lock内のauthoritative audit revisionとCASし、driftなら全eventを拒否する。これによりauthorize / verify後のmode・grant・provenance変更もcommitされない。 + +`AutoDecisionCommitPlan`は上流`AuditEventPlan`を満たし、event type / event identity / Intent / fieldsを欠かさない。event identityは後述`amadeus.auto-decided-event.v1` tupleから決定し、M06は返却planを変換せずM07 transactionへ渡す。subject欠落・不一致・registry mismatchは`PROVENANCE_REQUIRED`でevent planを返さない。別のsubject無しproducer overload、partial plan serializer、caller提供Registry snapshotは公開しない。field導入前のreplayだけをnull / withheld legacy projectionとする。 + +`DecisionReviewCommand`はclassification / noteの自由入力を持たない。M06 / M07は再検証済み`HumanReviewAuthorizationReceipt`に束縛された値だけからremediationと`AutoDecisionReviewedPayloadV1.flagClassification / safeNoteDigest`を生成する。`accept`は両fieldをexplicit nullに限定し、`flag`だけがclassificationとsafe note digestを利用できる。これによりhuman turn commit後の差替えはbinding digest不一致として拒否される。 + +review commandのprincipalとactorは別fieldだが、生成規則は`reviewPrincipalRef = receipt.principalId`、`reviewActorRef = receipt.reviewActorId = receipt.principalId`である。real human本人がreview actionを実行するためであり、代理actorを創作しない。decision principal / actorはcanonical `AUTO_DECIDED.subject_v1`だけから投影する。field導入前eventに限り各ref=null、status=withheldとし、`AutoDecisionRecord`から推測しない。 + +`DecisionSummary.decisionSource`は上流`AutoDecisionRecord.decider`を値変更なしで写す。`basis`はfree-form raw textを公開せず、共通redactor成功時だけcanonical value digestを返す。redaction失敗時はnullと`redactionStatus=withheld`にし、raw basisからqueue eligibilityを導出しない。 + +### Canonical identity encoding v1 + +すべてのU4 identity / digestは曖昧な文字列連結を禁止し、`canonical-tuple-v1`を使う。tupleはASCII domain tagと順序付きatomからなる。atomは`tag`と`text | null` variantを持つ。byte encodingは次で固定する。 + +```text +u32be(domainUtf8Length) || domainUtf8 || +u32be(atomCount) || +for each atom: + u32be(tagUtf8Length) || tagUtf8 || + variantByte(0x00=null, 0x01=text) || + if text: u64be(valueUtf8Length) || valueUtf8 +``` + +値は上流canonical ID / enum、または共通redactorがNFCで返したsafe textだけをUTF-8 encodeする。hashはSHA-256、表示は`sha256:`である。domain、atom順、tag、null variantを含むため、field境界の異なるtupleは同じpreimageにならない。 + +| Identity | Domain | Ordered atoms | +|---|---|---| +| auto decision event ID | `amadeus.auto-decided-event.v1` | intent、decision、occurrence、graph-revision、decision-payload-digest、subject-payload-digest | +| principal authorization receipt | `amadeus.auto-decision-principal-authorization.v1` | kind、intent、decision、occurrence、principal、provenance-event、source-audit-revision、mode-revision、gate-occurrence(null可)、grant(null可)、exercise(null可) | +| command binding | `amadeus.review-command-binding.v1` | source-intent、target-intent、decision、choice、command-occurrence、flag-classification(null可)、safe-note-digest(null可)、human-turn | +| review ID | `amadeus.decision-review.v1` | target-intent、decision、source-human-turn-event、choice | +| review transaction ID | `amadeus.decision-review-transaction.v1` | target-intent、review、target-audit-revision、completion-seal(null可) | +| review event ID | `amadeus.decision-review-event.v1` | review、command-occurrence、target-audit-revision、review-payload-digest | +| extension ID | `amadeus.review-extension.v1` | completion-seal、previous-extension(null可)、review-event、review-payload-digest、transaction、extension-revision | +| query fingerprint | `amadeus.decision-query.v1` | target-intent、lifecycle、review-state(null可)、page-size | +| projection event entry digest | `amadeus.decision-projection-event.v1` | event-type、event-id、decision-payload-digest(null可)、subject-payload-digest(null可)、review-payload-digest(null可) | +| projection event-set digest | `amadeus.decision-projection-event-set.v1` | target-intent、event-count、event-entry-digestの反復 | +| cursor digest | `amadeus.decision-cursor.v1` | query-fingerprint、target-audit-revision、review-extension-head(null可)、projection-event-set-digest、last-occurrence、last-decision | +| safe basis / note digest | `amadeus.redacted-value.v1` | value-kind、NFC redacted value | + +projection event-setの対象はexplicit target Intent partition内でclosed parserを通過した`AUTO_DECIDED`と`AUTO_DECISION_REVIEWED`だけであり、その他のevent typeは集合へ含めない。entryはevent typeのclosed order(`AUTO_DECIDED`、`AUTO_DECISION_REVIEWED`)、次にcanonical event IDのUTF-8 byte昇順でsortする。`AUTO_DECIDED` entryはdecision / subject payload digestをnon-null、review payload digestをnullとし、`AUTO_DECISION_REVIEWED` entryは前2つをnull、review payload digestをnon-nullとする。同じevent IDかつ同じentry digestは1件へdedupeし、同じevent IDでentry digestが違う場合は`ContractError(code=CONFLICT,locus=projectionEventSet)`とする。対象eventのunknown / missing field、invalid ID / payload digest、Intent mismatchはdigestを生成せず`ContractError(code=MALFORMED,locus=projectionEventSet)`へ閉じる。set tupleはtarget Intent、dedupe後の件数、sort済みentry digestを同じ`event` atom tagで順にencodeする。 + +5 harnessはprojection event entry / setについてempty、decision-only、decision+review、同一duplicate、衝突duplicate、順序違いのgolden byte / digest vectorを含む同じbyte encoder fixtureを共有し、native JSON stringifyや区切り文字連結でidentityを作らない。 + +### Canonical contract value encoding v1 + +contract success値のdigestはidentity tupleとは別の`canonical-value-v1`で生成する。各schemaは`HumanReviewAuthorizationReceipt / DecisionPage / DecisionDetail / DecisionReviewReceipt / MachineStatus / PersistenceReloadObservation / HumanStatusString / ContractError`のclosed IDを持つ。encoding grammarは次で固定する。 + +| Value | Byte encoding | +|---|---| +| null | `0x00` | +| false / true | `0x01 0x00` / `0x01 0x01` | +| non-negative integer | `0x02 || u64be(value)` | +| string / enum / ID / digest | `0x03 || u64be(utf8Length) || utf8` | +| array | `0x04 || u32be(count) || encoded items in declared order` | +| object | `0x05 || u32be(fieldCount) || each(field-tag length + field-tag + encoded value)` | + +object field順は上記public type宣言順、nullable fieldも必ずnullをencodeし、unknown / missing fieldを拒否する。optionsはcanonical question order、DecisionPage itemsはoccurrence / decision order、evidenceはsafeKind→nullable fingerprint、terminal review receiptsはreview ID昇順とする。`DecisionPage.nextCursor`のgolden vectorは`DecisionCursor`の宣言順でsnapshot 3値を含み、projection event-setのgolden vectorを参照して5 harnessでexact bytesを共有する。integerはsafe non-negative range外を拒否する。human status stringはCRLF / CRをLFへ変換し、Unicode NFC、末尾改行exactly oneにしてencodeする。 + +digest preimageは`u32be(domainUtf8Length) || domainUtf8 || u64be(valueByteLength) || canonicalValueBytes`とし、domain=`amadeus.contract-value..v1`で分離してSHA-256する。fixtureはschema IDとgolden byte vectorを持ち、全5 harnessが同じnested object / array / number / human string digestを得ることを先に検証する。 + +## 1. Decision read model + +M07はexplicit `intentUuid`からcanonical auditを読み、M05 reducerでU3の`AUTO_DECIDED`とU4の`AUTO_DECISION_REVIEWED`を同じIntent partition内だけに投影する。decision IDからIntentを逆引きしない。 + +### List contract + +1. target Intentの存在とlifecycle=`active | completed`を検証する。 +2. `lifecycle=active | completed | either` filterと実際のlifecycleを照合する。 +3. review queueでは上流`AutoDecisionRecord.decider=solo-election | agent-recommendation`かつ`reviewState=unreviewed`だけを返す。`confirmed-policy / norm-history`はqueue外であり、display文やfree-form basisから判定しない。 +4. history表示ではmode / grant gate、confirmed policy、norm、historyを含む全decisionを返せるが、これらは`not-applicable`でqueue件数へ加えない。 +5. 並び順はcanonical occurrence sequence、同順位はdecision ID昇順とする。query fingerprintはIntent / lifecycle / review state / page sizeだけから導出して全pageで不変とする。cursorがないfirst pageではtarget audit revision、nullable review extension head、canonical decision / review event-set digest、itemsをM07の同一read snapshotから取得する。cursor digestはquery fingerprint、snapshot 3値、last occurrence / decision IDから別domainで導出し、時刻や表示文へ依存しない。cursor内fingerprintとcurrent filter fingerprintが違えば`MALFORMED`、subsequent pageでauthoritative snapshot 3値のいずれかがcursorと違えばitemsを返さず`ContractError(code=CONFLICT,locus=cursorSnapshot)`で拒否する。historical snapshotを推測して再構成せず、callerはfirst pageから再取得する。 + +### Detail contract + +detailはquestion / gate identity、redacted question、canonical option ID / safe label、selected option、safe principal / actor reference、decision source、safe basis digest、nullable grant ID、nullable evidence fingerprint、degraded capability、decision / graph revision、review projectionを返す。credential、secret、raw provider prompt、未redact host / tool payloadは返さない。redactionに失敗したfieldは値とdigestをnullにして`redaction-status=withheld`を表示し、raw fallbackを禁止する。 + +## 2. Review eligibilityとhuman provenance + +`accept / flag`の対象はsolo electionまたはagent recommendationにより作られ、current review stateが`unreviewed`のdecisionだけである。policy / norm / history / gate decisionは履歴として閲覧できるがreview command対象ではない。 + +M06は`DecisionReviewCommand`にtarget Intent UUID、decision ID、choice、expected revision / seal、`HumanReviewAuthorizationReceipt`を要求する。command occurrence、flag classification、safe note digestはreceipt内のhuman-bound値だけを使い、command側から上書きできない。receiptはreal `VerifiedHumanTurn`からだけ生成し、headless / agent / harness / legacy eventからsynthetic human turnを作らない。 + +active targetではsource Intent=target Intentとし、通常のUserPromptSubmit経路がreal `HUMAN_TURN`をtarget auditへcommitした後にreviewをappendする。completed targetでは、現在のactive Intentをsource review contextとして使う。source側のreal `HUMAN_TURN` command payload digestはcompleted target UUID、decision ID、choice、command occurrence IDを明示的に束縛する。M07はsource canonical auditとcommit receiptを検証して`HumanReviewAuthorizationReceipt`を作り、target側eventはそのstable referenceを保存する。source turnからtargetを推測せず、active source Intentがなければ既存`ContractError(code=PROVENANCE_REQUIRED,locus=sourceIntentUuid)`でstateを変えない。 + +source turn commitとtarget review appendはcross-Intent原子transactionにしない。先にcommitされたsource human turnはauthorization evidenceであってtarget stateを変えず、target append失敗時は同じreceiptでidempotent retryできる。target側でreviewがterminal化した後は再利用できない。 + +検証順序は次のとおりである。 + +1. explicit target Intentとactive / completed lifecycleを解決する。 +2. decisionがそのIntent partitionに実在することを検証する。別Intentの同一形式IDは存在を開示せず`ContractError(code=CONFLICT,locus=decisionId)`とする。 +3. decisionがreview eligibleかつunreviewedであることを検証する。 +4. active source auditのreal human turn、command binding digest、target / decision / choice / flag classification / safe note digest / principalを検証し、authorization receiptを作る。 +5. canonical tupleからreview ID / transaction IDを作り、`receiptProjectionRevision=expectedTargetAuditRevision+1`をpayloadへ束縛する。payloadのcanonical-value digestを計算後、そのdigestを含むtupleからevent IDを作る。 +6. activeなら通常のprotected append、completedなら次節のcompleted-only validatorでcommitする。 + +同じreview IDの再送はpayloadから`DecisionReviewReceipt(reviewId, reviewEventId, auditTransactionId, [reviewEventId], receiptProjectionRevision, state, remediation)`を再構築して同じ値を返す。accepted / flagged済みdecisionへの同じchoiceはこの既存receipt、異なるchoiceは`ContractError(code=CONFLICT,locus=reviewState)`で拒否し、過去reviewを上書きしない。 + +## 3. Completed Intentの限定追記 + +completed Intentのlifecycle sealとartifact digestはimmutableである。M07の`CompletedDecisionReviewValidator`だけが次をすべて満たす場合に`AUTO_DECISION_REVIEWED`をappendできる。 + +- event typeがexactly `AUTO_DECISION_REVIEWED`である。 +- explicit target Intentがcompletedであり、decisionがそのsealed auditに存在する。 +- `HumanReviewAuthorizationReceipt`のsource Intentがactiveで、source human turn / audit revision / command binding digestがcanonical auditと一致する。 +- event payloadがclosed `AutoDecisionReviewedPayloadV1`のrequired / explicit-null fieldsだけを含み、review principal / actor、decision principal / actor / source / basisを別fieldで保持する。 +- expected completion seal digestとcurrent review-extension headが一致する。 +- transaction内にlifecycle / artifact / grant / workflow / decision mutation eventがない。 + +original completion sealは書き換えない。post-seal review extensionは`amadeus.review-extension.v1` tupleでIDを作る別hash chainであり、各entryはoriginal seal digest、nullable previous head、review event、dense extension revisionへ束縛される。canonical audit readはoriginal sealed historyとvalidated extension chainを合成するが、completion identity、artifact digest、workflow lifecycleは変えない。一般append例外へ再利用できるAPIを公開しない。 + +`AUTO_DECISION_REVIEWED`のwire表現は`AuditEventPlan.fields`をexactly one key `payload_v1`に限定し、その値を`AutoDecisionReviewedPayloadV1`のfixed-key-order canonical JSON(型宣言のfield順、UTF-8、whitespaceなし、全nullable fieldをexplicit null)とする。runtime parserはrequired key、closed enum、nullabilityを検証し、unknown / missing keyを拒否する。payload digestは`canonical-value-v1(auto-decision-reviewed-payload)`から再計算し、review event IDへ含める。`PostSealReviewExtension`も型宣言順のfixed-key-order canonical JSON規則を使い、current head一致、`extensionRevision=current+1`、event type / ID / payload digest / transaction一致を検証する。 + +## 4. Accept / flagとremediation提案 + +`accept`はdecision review stateをacceptedへ投影するだけで、decision effectを再実行しない。 + +`flag`はreview stateをflaggedへ投影するが、active / completedを問わずrollback、Intent reopen、grant変更、成果物変更を行わない。M06は次の非実行`RemediationSuggestion`をstatus / receiptへ返す。 + +- existing requirement / contract / declared verificationへの違反としてflagした場合は`self-fix`をprimary提案とする。 +- 新しいbehavior、scope追加、既存仕様変更としてflagした場合は`self-feature`をprimary提案とする。 +- classification未指定時は安全側の`self-fix`をprimary、`self-feature`を「仕様変更なら選択」のalternativeとして表示する。 + +optional classification / safe noteは`review_command_v1`でreal human turnへ束縛されたsafe enum / digestだけをeventへ保存する。提案はscope commandを実行せず、新Intentを作らず、現在Intentのstateを変更しない。 + +## 5. Statusとresult projection + +human-readable statusは主要語彙を「自律レベル、grant、grant scope、事前裁定方針、自動裁定、grant行使、停止理由」に限定し、次を表示する。 + +- target Intentとlifecycle +- autonomy mode、workflow execution state +- nullable grant ID / state / scopeとpolicy count +- suspended reason、stop reason、resume condition +- decision count、unreviewed decision count、accepted / flagged count +- legacy diagnostic、mode別grant説明 + +machine-readable statusは同じ値をstable enum / nullable fieldで返し、illegal combinationをparse時に拒否する。completedはworkflow=null、current grant=null、unreviewed queueは存在可能であり、後続reviewでcompletion resultを変更しない。 + +M07は`ReviewStatusInput`だけをstatus projectorへ渡す。`currentGrantScope`はgrant issuance eventから再生し、active fullでは`autonomy.currentGrant.scopeFingerprint`とのexact matchを検証する。`decisionPolicyCount`もgrant eventのconfirmed policy setから再生し、active fullではcurrent grant、completedではterminal completed grant、none / semiでfull grant historyがなければ0へ対応させる。`decisionCounts(total/unreviewed/accepted/flagged)`はdecision reducerから渡す明示fieldである。lifecycle、scope、policy source、stop / resumeを含む必須入力が欠ける場合は推測せず`MALFORMED / ILLEGAL_STATE`を返す。human / machine formatterは同じvalidated inputを消費する。 + +## 6. Event RegistryとOTel + +`AUTO_DECISION_REVIEWED`を既存Event Registryへ登録し、event identity、Intent UUID、decision ID、review ID、choice、review principal / actor、decision principal / actor / source / basis、source human turn reference、nullable grant ID、lifecycle-at-review、remediation kind、trace correlationを既存schemaへ射影する。 + +OTelはsafe stable ID / enum / digestだけをattributeにし、question text、option raw payload、evidence raw payload、credentialを載せない。decision trace、review trace、grant exercise、stop / resumeはIntent UUID / decision ID / audit transaction IDで相互参照する。別telemetry store / schemaを作らない。 + +| Registry / OTel attribute | Cardinality | Value / redaction | +|---|---|---| +| `amadeus.intent.id`、`amadeus.decision.id`、`amadeus.review.id` | 1 | stable safe ID | +| `amadeus.review.choice`、`amadeus.review.lifecycle` | 1 | closed enum | +| `amadeus.review.principal_ref`、`amadeus.review.source_turn_ref` | 1 | pseudonymous stable reference | +| `amadeus.review.actor_ref` | 1 | reviewを実行したhuman actor reference | +| `amadeus.decision.principal_ref`、`amadeus.decision.actor_ref` | 0..1 | safe projection、withheld時はomission | +| `amadeus.decision.source` | 1 | `AutoDecisionRecord.decider` closed enum | +| `amadeus.decision.basis_digest` | 0..1 | redaction成功後のsafe digest | +| `amadeus.grant.id` | 0..1 | stable ID、grantなしはattribute omission | +| `amadeus.review.note_digest` | 0..1 | redaction後canonical noteのdigestのみ | +| `amadeus.redaction.status` | 1 | `redacted / withheld` | +| `amadeus.audit.transaction_id` | 1 | review appendのstable transaction ID | +| `amadeus.trace.id`、`amadeus.span.id` | 1 | existing OTel correlation format | + +redaction / access control / retentionはevent計画前とspan attribute生成前の共通safe projectionで一度だけ適用する。redaction失敗時は`amadeus.redaction.status=withheld`とID / enum correlationだけを残し、note / question / evidence digestも生成しない。 + +## 7. Five harness contract + +Claude Code、Codex、Cursor、OpenCode、Kimi Codeは同じCore list / detail / review / status fixtureとhuman / machine snapshotを実行する。native adapterは既存CLI / skill entrypointへ引数と表示を投影するだけで、eligibility、seal validator、redaction、remediation classificationを複製しない。 + +```ts +type ReviewHarnessId = "claude" | "codex" | "cursor" | "opencode" | "kimi"; +type RequiredReviewHarnesses = readonly ["claude", "codex", "cursor", "opencode", "kimi"]; +const REQUIRED_REVIEW_HARNESSES: RequiredReviewHarnesses = + ["claude", "codex", "cursor", "opencode", "kimi"]; + +type ExpectedContractResult = + | { outcome: "success"; value: T } + | { outcome: "error"; error: ContractError }; +type ContractValueSchemaId = + | "human-review-authorization-receipt" + | "auto-decision-reviewed-payload" + | "post-seal-review-extension" + | "decision-page" + | "decision-detail" + | "decision-review-receipt" + | "machine-status" + | "persistence-reload-observation" + | "human-status-string" + | "contract-error"; +type CanonicalValueGoldenVector = { + vectorId: StableId; + schemaId: ContractValueSchemaId; + value: unknown; + expectedBytesHex: string; + expectedDigest: Sha256Digest; +}; +type ContractCaseResult = { + caseId: StableId; + passed: boolean; + observedOutcome: "success" | "error"; + observedValueDigest: Sha256Digest | null; + observedError: ContractError | null; +}; +type PersistenceBoundary = "session" | "process" | "compaction" | "clone"; +type PersistenceReloadInput = { + checkpointId: StableId; + boundary: PersistenceBoundary; + sourceIntentUuid: StableId; + targetIntentUuid: StableId; +}; +type PersistenceReloadObservation = { + checkpointId: StableId; + boundary: PersistenceBoundary; + sourceAuditRevision: number; + targetAuditRevision: number; + reviewExtensionHead: StableId | null; + queue: DecisionPage; + terminalReviewReceipts: readonly DecisionReviewReceipt[]; +}; +type ReviewHarnessContractFixture = { + fixtureId: StableId; + contractRevision: Sha256Digest; + requiredHarnesses: RequiredReviewHarnesses; + canonicalValueVectors: readonly CanonicalValueGoldenVector[]; + sourceIntentAudit: readonly CanonicalAuditEvent[]; + targetIntentAudit: readonly CanonicalAuditEvent[]; + authorizationCases: readonly { + caseId: StableId; + input: HumanReviewAuthorizationInput; + expected: ExpectedContractResult; + }[]; + listCases: readonly { + caseId: StableId; + query: DecisionQuery; + expected: ExpectedContractResult; + }[]; + detailCases: readonly { + caseId: StableId; + intentUuid: StableId; + decisionId: StableId; + expected: ExpectedContractResult; + }[]; + reviewCases: readonly { + caseId: StableId; + command: DecisionReviewCommand; + expected: ExpectedContractResult; + }[]; + statusCases: readonly { + caseId: StableId; + input: ReviewStatusInput; + expectedMachine: ExpectedContractResult; + expectedHuman: ExpectedContractResult; + }[]; + persistenceCases: readonly { + caseId: StableId; + afterCaseId: StableId; + input: PersistenceReloadInput; + expected: ExpectedContractResult; + }[]; +}; +type ReviewHarnessContractResult = { + harnessId: ReviewHarnessId; + fixtureId: StableId; + contractRevision: Sha256Digest; + passed: boolean; + caseResults: readonly ContractCaseResult[]; +}; +type ReviewHarnessAdapter = { + harnessId: ReviewHarnessId; + invokeAuthorize(input: HumanReviewAuthorizationInput): Promise>; + invokeList(query: DecisionQuery): Promise>; + invokeDetail(intentUuid: StableId, decisionId: StableId): Promise>; + invokeReview(command: DecisionReviewCommand): Promise>; + invokeMachineStatus(input: ReviewStatusInput): Promise>; + invokeHumanStatus(input: ReviewStatusInput): Promise>; + reload(input: PersistenceReloadInput): Promise>; +}; +type ReviewHarnessSuiteResult = { + fixtureId: StableId; + contractRevision: Sha256Digest; + requiredHarnesses: RequiredReviewHarnesses; + receipts: readonly ReviewHarnessContractResult[]; + passed: boolean; +}; +function runReviewHarnessContract( + adapter: ReviewHarnessAdapter, + fixture: ReviewHarnessContractFixture, +): Promise>; +function canonicalContractValueDigest( + schemaId: ContractValueSchemaId, + value: unknown, +): ContractResult; +function evaluateReviewHarnessSuite( + fixture: ReviewHarnessContractFixture, + receipts: readonly ReviewHarnessContractResult[], +): ContractResult; +``` + +fixtureはsource human turn、target review queue、active / completed両方を含む。`canonicalValueVectors`を最初に検証し、1件でもbyte / digestが違えばbehavior caseを実行しない。`persistenceCases.afterCaseId`はどのreview後にreloadするかを固定し、session / process / compaction / cloneごとにcanonical revisions、extension head、queue page、full terminal review receiptsを再読する。negative casesはcross-Intent detail / reviewを`CONFLICT`、missing / mismatched humanを`PROVENANCE_REQUIRED`、terminal choice競合を`CONFLICT`、malformed cursor / statusを`MALFORMED | ILLEGAL_STATE`のexact `ContractError`として固定する。 + +suite evaluatorはfixtureのrequired tupleと同じ5 harness IDがexactly once存在し、全receiptのfixture ID / contract revisionが一致し、全caseがpassした場合だけ`passed=true`を返す。runnerはsuccessならcanonical value digestとerror=null、failureならvalue digest=nullとexact `ContractError(code,locus,detail)`を`ContractCaseResult`へ保存してexpectedと比較する。欠損、重複、未知harness、success/error取り違え、error code / locus / detail差、machine / human digest差、reload後revision / queue / receipt差はfailである。表示成功だけでpassにしない。 + +将来harnessはdescriptor registry row、adapter projection、同じcontract fixtureの追加で閉じる。U4はcredential-attested live completionを有効化せず、Kiro / Kiro IDEを今回のlive対応済みと扱わない。 + +## 8. Verification scenarios + +| Scenario | Oracle | +|---|---| +| active queue list | election / recommendationのunreviewedだけ、stable order | +| completed queue list | completionを変更せずunreviewedを表示 | +| history list | policy / norm / history / gateをnot-applicableとして表示、queue外 | +| detail | question/options/selected/decider/basis/grant/safe evidence/degradation | +| cross-Intent ID | explicit target partitionでnot found、逆引きなし | +| missing / synthetic human | review eventなし、state不変 | +| active accept | protected `AUTO_DECISION_REVIEWED`、acceptedへ投影 | +| completed flag | original seal不変、extension chainへreviewだけappend | +| duplicate review | same choiceはsame receipt、conflicting choiceは拒否 | +| review payload tamper | payload / event / extension digest mismatchでreplay拒否 | +| flag contract defect | rollbackなし、self-fix提案のみ | +| flag specification change | rollbackなし、self-feature提案のみ | +| redaction failure | raw fallbackなし、withheld表示 | +| completed status | workflow/grant terminalを維持しqueue count更新 | +| compaction / clone reload | full receipt、queue、extension headが同一 | +| 5 harness snapshot | byte-equivalent machine projection、同じhuman用語 | + +## 要件・AC追跡 + +| Design behavior | Requirement / AC | +|---|---| +| active / completed list-detail、queue | FR-DEC-007、FR-OBS-001〜002、2067-AC18 | +| real human accept / flag、protected append | FR-OBS-003〜004、NFR-SAF-002、2067-AC19 | +| no rollback、remediation proposal | FR-OBS-005、2067-AC20 | +| human / machine status | FR-STP-005〜006、FR-OBS-006、NFR-UX-001〜003、2067-AC21 | +| registry / telemetry | FR-GRT-008、FR-OBS-007、NFR-OBS-001、2067-AC16 secondary | +| privacy / replay / harness | FR-HAR-004、NFR-DET-001〜002、NFR-REL-001〜003、NFR-PRV-001〜002 | + +## 非目標 + +- Intent reopen、過去decision / effect変更、rollback、成果物変更。 +- self-fix / self-feature Intentの自動作成またはscope command自動実行。 +- 一般的なcompleted audit append例外。 +- PR / GitHub / merge / convergence、runner / scheduler、常駐supervisor。 +- terminal live completion、credential保存、raw evidence / host payload表示。 + +## Historical Review Cycle 1 — Iteration 1 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T13:34:56Z +- **Iteration:** 1 +- **Scope decision:** none + +completed Intentのhuman provenanceと、query・status・5 harness検証の公開契約が閉じておらず、FR-OBS-001〜006および2067-AC18〜21を推測なしに実装できない。 + +### Findings + +- BLOCKER | completed reviewのreal human provenanceが成立しない。sealed targetへAUTO_DECISION_REVIEWED以外を追記できず、別active sourceのturnとcompleted target review commandを結ぶ型・検証規則がない。 +- BLOCKER | list/detail契約が上流公開APIと不整合である。cursor/page size、DecisionDetail、redaction境界、queue判定fieldを具体的な公開契約へ反映する必要がある。 +- BLOCKER | status projectionの要求値を上流関数入力から生成できない。lifecycle、scope、policy count、stop reason、decision/review countsをpublic input/outputへ追加する必要がある。 +- BLOCKER | 5 harness contract snapshotの検証面が公開されていない。decision query、review receipt、machine/human statusをfixture/result schemaへ含める必要がある。 +- FOLLOW-UP | Event Registry / OTelのsafe属性名、cardinality、redaction/retention適用点とredaction失敗時の残存情報を固定すると検証可能性が上がる。 + +## Historical Review Cycle 1 — Iteration 2 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T13:42:41Z +- **Iteration:** 2 +- **Scope decision:** none + +Iteration 1の4 BLOCKERはいずれも公開型またはfixture schema上で未解消であり、completed review、query/detail、status、5 harness検証を推測なしに実装できない。 + +### Findings + +- BLOCKER | completed Intent reviewのhuman provenance生成契約が閉じていない。HumanReviewAuthorizationInputとsource audit / commit receipt / command bindingの検証入力が未定義である。 +- BLOCKER | query/detail公開契約が依然として未完である。DecisionSummary、DecisionCursor、DecisionReviewReceipt、safe option/evidence型、DigestとbasisKind変換規則が未定義である。 +- BLOCKER | status入力から要求出力を生成できない。safe grant scope、decisionPolicyCount、DecisionReviewCountsの入力契約が未定義である。 +- BLOCKER | 5 harness contract fixtureはfixture/harness identity、必須5 harness集合、negative ContractError oracleを表現できない。 +- FOLLOW-UP | OTel表へaudit transaction IDとtrace/spanの具体的attribute名・cardinalityを追加するとNFR-OBS-001をschema testできる。 + +## Historical Review Cycle 2 — Iteration 1 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T13:54:27Z +- **Iteration:** 1 +- **Scope decision:** none + +主要契約は具体化されたが、human provenanceの信頼境界、review event・extensionのclosed schema、redaction失敗表現、決定的identity encodingに実装不能な欠落が残る。 + +### Findings + +- BLOCKER | caller提供のaudit配列とreceiptだけではsynthetic sourceを排除できず、M07-owned canonical readまたは偽造不能trusted snapshotが必要。 +- BLOCKER | AUTO_DECISION_REVIEWED payloadとPostSealReviewExtensionのclosed public schema、canonical serializationが未定義。 +- BLOCKER | redaction failureでdigestを生成しない規則とnon-null evidenceFingerprintが矛盾する。 +- BLOCKER | H(a+b+...)にlength prefix / type tag / canonical tuple encodingがなくidentity collision境界が曖昧。 +- FOLLOW-UP | DecisionDetailへsafe principal/actor projection、OTelへactor keyを追加するとFR-GRT-008をschema testできる。 +- FOLLOW-UP | 5-harness fixtureへsession/process/clone reload stepsとobserved success/errorを追加するとFR-HAR-004を検証できる。 + +## Historical Review Cycle 2 — Iteration 2 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:04:11Z +- **Iteration:** 2 +- **Scope decision:** none + +主要なIteration 1指摘は具体化されたが、主体情報の生成元、5 harness成功値digest、compaction永続化の契約が未閉鎖であり実装判断が残る。 + +### Findings + +- BLOCKER | reviewActorRefとdecision principal/actorのcanonical生成元が未定義でclosed payloadとRegistry射影を構築できない。 +- BLOCKER | DecisionPage/Detail/MachineStatus/配列/数値/human stringのschema別canonical success value encodingがなく5 harness digestを一致させられない。 +- BLOCKER | PersistenceBoundaryにcompactionがなくFR-HAR-004の明示要件を検証できない。 + +## Historical Review Cycle 3 — Iteration 1 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:19:53Z +- **Iteration:** 1 +- **Scope decision:** none + +review主体・canonical-value-v1・compaction列挙は改善されたが、主体生成、pagination identity、receipt永続化、post-seal integrityが未閉鎖であり実装可能ではない。 + +### Findings + +- BLOCKER | decision principal/actorの新規AUTO_DECIDED producer入力と生成規則がなくRegistry射影を満たせない。 +- BLOCKER | query fingerprintとcursor digestが同一identity定義でpage位置の包含有無が矛盾する。 +- BLOCKER | AUTO_DECISION_REVIEWEDからtransaction receiptを再構築できず境界越しsame receiptを満たせない。 +- BLOCKER | post-seal extension identityがclosed review payload digestへ束縛されず内容改変を検知できない。 + +## Historical Review Cycle 3 — Iteration 2 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:28:16Z +- **Iteration:** 2 +- **Scope decision:** none + +query/cursorのdomain分離、receipt再構築、payload digest付きchainは解消したが、AUTO_DECIDED主体のproducer配線とreview付随情報のhuman provenanceが未閉鎖である。 + +### Findings + +- BLOCKER | planAutoDecisionSubject payloadを実producer planAutoDecisionCommit / event-planへ渡してAUTO_DECIDED.subject_v1を必須commitする経路がない。 +- BLOCKER | flagClassificationとsafeNoteDigestがreview_command_v1 human bindingに含まれずhuman認可後の差替えを防げない。 + +## Historical Review Cycle 4 — Iteration 1 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:35:49Z +- **Iteration:** 1 +- **Scope decision:** none + +U4のreview・seal・query契約は詳細化されているが、AUTO_DECIDEDの主体情報追加経路に3件の実装不能な不整合がある。 + +### Findings + +- BLOCKER | `AutoDecisionSubjectInput.authorizationKind`は`mode-semi`を許容する一方、唯一のproducerである`planAutoDecisionCommit`は常に`ReservedGrantExercise`を要求する。requirements.mdのFR-AUT-005/FR-GRT-004はsemiのgrantをnullとし、component-methods.mdの`ReservedGrantExercise`はfull grantの予約でしか生成できないため、semiの自動裁定へ必須`subject_v1`を記録する入力を構築できない。semi用の非grant認可証跡をclosed unionとしてcommit入力へ追加するか、mode別producer契約を定義する必要がある。 +- BLOCKER | `planAutoDecisionCommit`が返す`AutoDecisionCommitPlan`には`eventIdentity`がなく、M00の`AuditEventPlan`必須schemaを満たさない。別のserializerまたはevent identity生成関数も公開されていない一方、この関数以外のAUTO_DECIDED producerは禁止されているため、M06は生成結果をM07 transactionへappendできない。戻り値を完全な`AuditEventPlan`へするか、決定的event identityを生成して変換するclosed APIが必要である。 +- BLOCKER | 設計はM05が`actorRegistryEntryId`の実在・一致を再検証すると規定するが、`AutoDecisionSubjectInput`にはcaller提供の`RegisteredDecisionActor`しかなく、Event Registryをcanonicalに読むportやtrusted receiptがない。component-methods.mdはM07がM05を一方向importし、domain moduleからM07への逆importを禁止しているため、M05が直接Registryを読む実装は循環依存となり、入力を信用する実装ではregistry mismatchを検出できない。非循環なregistry lookup portまたはM07発行の再検証可能receiptを定義する必要がある。 + +## Historical Review Cycle 4 — Iteration 2 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:38:51Z +- **Iteration:** 2 +- **Scope decision:** none + +前回のevent plan欠落とRegistry循環は解消されたが、semi/full双方のcanonical principal provenanceを生成・再検証する公開経路がなく、AUTO_DECIDED producerを実装できない。 + +### Findings + +- BLOCKER | `AutoDecisionCommitAuthorization`のsemi branchは`modeProjectionRevision / modeProvenanceEventId`を要求し、full branchの`ReservedGrantExercise`にもprincipal provenanceは含まれないが、M04の公開契約にはこれらとcanonical principalを生成するproducerがない。`AutonomyProjection`もmode/grantのprincipal・provenance event・projection revisionを保持せず、`AutoDecisionCommitPlanner`へ注入されるdependencyはactor用`DecisionActorRegistryReader`だけである。そのためM06はsemi authorizationを型どおり構築できず、semi/fullともcaller提供`subject.principalRef / principalProvenanceEventId`をM05がcanonical M04 stateへ再検証できない。M04-owned authorization producerまたは非循環なcanonical provenance reader/receiptを定義し、planner入力とprincipalを一意に生成・検証可能にする必要がある。 + +## Review — Iteration 1 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:45:53Z +- **Iteration:** 1 +- **Scope decision:** none + +M04 authorizer/receiptとM05再検証経路は成立したが、receiptをM07のlock revisionへ束縛するcanonical read契約が不足している。 + +### Findings + +- BLOCKER | `CanonicalAutonomyProvenanceReader.readIntentAudit`は`CanonicalAuditEvent[]`だけを返す一方、M04 receiptは`sourceAuditRevision / modeProjectionRevision`を必須とし、M07は`receipt.sourceAuditRevision`をtransactionの`expectedRevision`と一致させる。M00の`CanonicalAuditEvent`には`stateProjectionRevision`やlock revisionがなく、複数shardのevent列からM07の現在revisionを一意に導出できない。audit件数を代用することもNFR-DET-003に反する。このためauthorizerは検証可能なrevisionをreceiptへ格納できず、authorize時に読んだauditとM07 lockの同一snapshot性も保証できない。M07 adapterのread結果を、canonical auditとM07のauthoritative lock/state projection revisionを同一snapshotで返すclosed型へ変更し、そのrevisionをauthorize・verify・append CASで一貫して使用する必要がある。 + +## Review — Iteration 2 + +- **Verdict:** READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T14:47:36Z +- **Iteration:** 2 +- **Scope decision:** none + +前回BLOCKERは解消済み。canonical multi-shard audit、authoritative audit lock revision、state projection revisionを同一snapshotで取得し、M04 authorize・M05経由の再verify・M07 append CASまで同じaudit revisionへ束縛される。revision driftはCONFLICTまたはtransaction全体拒否となり、実装可能性・依存方向・原子性に未解決BLOCKERはない。 + +### Findings + +- None diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/business-rules.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/business-rules.md new file mode 100644 index 0000000000..809ef16b33 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/business-rules.md @@ -0,0 +1,136 @@ +# Business Rules — autonomy-review-observability + +## 上流入力と適用範囲 + +本規則は`units-generation/unit-of-work.md`、`units-generation/unit-of-work-story-map.md`、`requirements-analysis/requirements.md`、`application-design/components.md`、`application-design/component-methods.md`、`application-design/services.md`からU4 `autonomy-review-observability`の不変条件を抽出する。対象はread / review / status / telemetryであり、decision effect、Intent completion、rollback、新Intent作成を含めない。 + +## Query規則 + +| ID | Rule | Violation | +|---|---|---| +| OBS-Q01 | list / detailはexplicit target Intent UUIDを要求し、decision IDからIntentを逆引きしない | target required | +| OBS-Q02 | target lifecycleはactive / completedだけ、query filterと一致させる | lifecycle mismatch | +| OBS-Q03 | queueはsolo election / agent recommendationのunreviewedだけ | projection error | +| OBS-Q04 | policy / norm / history / gateはhistory表示可能だがnot-applicableかつqueue非対象 | projection error | +| OBS-Q05 | ordering / cursorはoccurrence / decision ID / query fingerprintとimmutable read snapshot identityから決定し、時刻や文面に依存しない。page間snapshot driftは`CONFLICT(cursorSnapshot)`とする | malformed / stale cursor | +| OBS-Q06 | cross-Intent decision IDをnot foundとして拒否し、他Intentの存在を漏らさない | not found | + +## Detail・privacy規則 + +- question / options / selected option / principal / decider / actor / basis / nullable grant / evidence / degradation / review stateを返す。 +- safe label、redacted value、canonical digestだけを表示・保存する。 +- credential、secret、bearer token、raw provider prompt、未redact host / tool payloadを返さない。 +- redaction failure時はraw fallbackせず`withheld`を表示する。 +- access control / retentionは既存Intent audit、Event Registry、OTel contractを継承する。 + +## Review規則 + +| ID | Rule | Violation | +|---|---|---| +| OBS-R01 | accept / flagはeligible unreviewed decisionだけ | review not eligible | +| OBS-R02 | active source Intentのreal VerifiedHumanTurnをtarget Intent、decision、choice、command occurrenceへ束縛したreceiptとして検証する | provenance error | +| OBS-R03 | synthetic human、proxy principal、headless auto reviewを認めない | fail-closed | +| OBS-R04 | review IDはIntent / decision / human turn / choiceから決定する | identity error | +| OBS-R05 | same review / choiceはsame receipt、terminal choice競合は拒否する | terminal conflict | +| OBS-R06 | reviewはdecision effectを再実行せず、過去decision eventを変更しない | transaction reject | +| OBS-R07 | `AUTO_DECISION_REVIEWED`はchoice、principal、human turn、safe remediation metadataを記録する | schema reject | +| OBS-R08 | active targetはsource=target、completed targetは現在のactive source Intentを使い、target sealへHUMAN_TURNを追記しない | context required | +| OBS-R09 | caller提供audit / receipt / lifecycleを認可せず、M07がcanonical storeからactive sourceとhuman turn commitを直接readしてappend時にも再検証する | provenance error | +| OBS-R10 | review principal / actorはreal human principalと同一、decision principal / actorはcanonical AUTO_DECIDED safe fieldだけから投影し、field導入前eventはnull / withheld | projection error | +| OBS-R11 | 新規AUTO_DECIDEDはsubject無しoverloadを持たないM05 plannerだけから生成する。M04 authorizerがcanonical audit / authoritative lock revision / state projection revisionの同一snapshotからsemi mode eventまたはfull grant issuance / exerciseを検証してprincipal receiptを発行し、M05が新snapshotで同receiptを再検証する。canonical Registry portで解決したM06 actorとreceipt digest / source revisionを完全なAuditEventPlanのsubject_v1へ必須保存し、source revisionをM07 append CASへそのまま使う。欠落・revision drift時はplan / commitしない。null / withheldはfield導入前replayだけ | provenance error | +| OBS-R12 | flag classification / safe note digestはreview_command_v1のhuman bindingとdigestへexplicit nullを含めて束縛し、review command側の自由入力やhuman turn後の差替えを認めない | provenance error | + +## Completed seal規則 + +- completed Intentでは専用`CompletedDecisionReviewValidator`だけを使う。 +- event typeはexactly `AUTO_DECISION_REVIEWED`とし、同じtransactionの他eventを拒否する。 +- decisionがexplicit target sealed auditに存在し、active source auditのhuman turn receiptがtarget / decision / choice / occurrenceと一致することを必須にする。 +- original completion seal、artifact digest、workflow lifecycle、grant、decisionを変更しない。 +- post-seal review extensionはcompletion seal digest、previous extension、review eventからhash chainを作る。 +- extension chain mismatch、expected revision mismatch、unknown eventはfail-closedする。 +- completed-only pathを一般append APIとして公開しない。 +- event fieldsはexactly `payload_v1`だけとし、closed `AutoDecisionReviewedPayloadV1`のfixed-key-order canonical JSONを保存する。 +- post-seal extensionはclosed v1 schema、dense revision、current head、canonical extension identityを検証する。 +- review event / extension identityはcanonical review payload digestとaudit transaction IDを含み、payload改変でchain identityが変わる。 +- review payloadはaudit transaction IDとreceipt projection revisionを持ち、replayでfull `DecisionReviewReceipt`を再構築する。 + +## Identity・redaction規則 + +- U4 identityはdomain tag、ordered field tag、null/text variant、u32/u64 length prefix、UTF-8 valueを持つ`canonical-tuple-v1`だけからSHA-256生成する。 +- delimiter連結、native JSON stringify、display text、時刻をidentity preimageにしない。 +- redaction失敗時はquestion / label / basis / note / evidenceのvalueとdigestをnullにし、`withheld`だけを返す。 +- safe principal / actorは別nullable referenceとして投影し、review principal / actorとdecision principal / actorを混同しない。 +- contract success digestはschema ID別canonical-value-v1でnested object / array / integer / normalized human stringをencodeし、`DecisionPage.nextCursor`ではtarget audit revision、nullable review extension head、projection event-set digestをpublic type宣言順に含むgolden byte vectorで5 harness一致を検証する。 +- projection event-set digestはtarget Intent内のvalid `AUTO_DECIDED / AUTO_DECISION_REVIEWED`だけをclosed payload digest付きentryへ変換し、event type / event ID順にsortしてexact duplicateだけをdedupeする。同一event IDの内容衝突や対象eventのinvalid schemaはcursorを作らずfail-closedする。 + +## Flag remediation規則 + +| Classification | Primary proposal | Effect | +|---|---|---| +| existing contract defect | self-fix | proposal only | +| specification addition / change | self-feature | proposal only | +| unspecified | self-fix primary、self-feature conditional alternative | proposal only | + +flagはactive / completedをrollback / reopenせず、grant / workflow / artifactsを変更しない。提案からscope commandを実行せず、新Intentを自動作成しない。 + +## Status規則 + +| ID | Rule | +|---|---| +| OBS-S01 | human / machine statusは同じcanonical projectionを使う | +| OBS-S02 | mode、workflow、nullable grant / scope、policy count、suspended / stop reason、resume condition、unreviewed countを表示する | +| OBS-S03 | completedはworkflow=null / current grant=nullを維持し、review queueを保持できる | +| OBS-S04 | review appendはcompletion result / identityを変更しない | +| OBS-S05 | REPAIR_STALLEDとlegacy diagnosticは既定のmode別説明を維持する | +| OBS-S06 | illegal enum / combinationをmachine schemaで拒否する | +| OBS-S07 | projector inputはlifecycle、safe grant scope、policy count、stop / resume、decision / accepted / flagged countsを明示的に持ち、欠落値を推測しない | + +## Public API・harness fixture規則 + +- `listAutoDecisions`はpage items、snapshot-bound next cursor、query fingerprintを返し、bounded page sizeを受ける。first pageはsnapshot identityとitemsを同一readで取得し、継続時のsnapshot driftは`CONFLICT(cursorSnapshot)`で拒否する。 +- `getAutoDecision`はraw recordでなくredacted `DecisionDetail`を返す。 +- queue判定は上流`AutoDecisionRecord.decider / reviewState`のclosed enumを使い、表示文やfree-form basisへ依存しない。 +- `ReviewStatusInput`と`MachineStatus`はlifecycle、safe grant scope、policy count、stop reason、decision / unreviewed / accepted / flagged countを持つ。 +- 5 harness fixtureはfixture ID / contract revision / exact harness tuple、canonical-value-v1およびprojection event entry / setのgolden vectors、authorization / list / detail / review / status、source / target audit、success / exact `ContractError` oracleを持つ。 +- contract resultはharness IDを束縛し、exactly-once 5件、nested value / human stringのcanonical digest、session / process / compaction / clone後のqueue / receipt persistence、negative caseのerror code / locusを検証する。 + +## Registry・OTel規則 + +- `AUTO_DECISION_REVIEWED`と`amadeus.intent.id / decision.id / review.id / review.choice / review.lifecycle / review.principal_ref / review.actor_ref / review.source_turn_ref / decision.principal_ref / decision.actor_ref / decision.source / decision.basis_digest / grant.id / review.note_digest / redaction.status / audit.transaction_id / trace.id / span.id`を既存Event Registryへ登録する。 +- principal / decider / actor / basisを別fieldで保持する。 +- Intent / decision / review / grant / audit transaction / trace identityを相互参照可能にする。 +- raw question / evidence / credentialをspan attributeへ載せない。 +- redaction failure時はID / enum correlationと`redaction.status=withheld`だけを残す。 +- 別telemetry schema / storeを作らない。 +- 5 harness adapterへreview / redaction / seal algorithmを複製しない。 + +## Failure classification + +| Failure | Classification | Effect | Recovery | +|---|---|---|---| +| target missing / lifecycle mismatch | query error | state不変 | explicit target correction | +| cross-Intent decision | not found | existence非開示 | correct target | +| ineligible decision | review boundary | state不変 | history閲覧のみ | +| invalid human turn | authorization error | review eventなし | real human command | +| duplicate same review | idempotent replay | same receipt | none | +| conflicting terminal review | conflict | past review不変 | history確認 | +| completed extension mismatch | seal conflict | appendなし | replay / reload | +| redaction failure | privacy boundary | value withheld | protected source correction | + +## 要件・AC追跡 + +| Rule group | Requirement / AC | +|---|---| +| query / detail / queue | FR-DEC-007、FR-OBS-001〜002、2067-AC18 | +| review / completed seal | FR-OBS-003〜004、NFR-SAF-002、2067-AC19 | +| flag proposal | FR-OBS-005、2067-AC20 | +| status | FR-STP-005〜006、FR-OBS-006、NFR-UX-001〜003、2067-AC21 | +| registry / OTel / privacy | FR-OBS-007、NFR-OBS-001、NFR-PRV-001〜002、2067-AC16 secondary | +| persistence / harness | FR-HAR-004、NFR-DET / REL / MNT | + +## 非目標 + +- rollback、Intent reopen、decision effect再実行、artifact変更。 +- self-fix / self-featureの自動起動、新Intent自動作成。 +- terminal live completion、PR / GitHub、runner / supervisor。 +- credential / raw evidenceの保存・表示。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/domain-entities.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/domain-entities.md new file mode 100644 index 0000000000..d99c540a9a --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/domain-entities.md @@ -0,0 +1,201 @@ +# Domain Entities — autonomy-review-observability + +## 上流入力とmodel境界 + +本modelは`units-generation/unit-of-work.md`、`units-generation/unit-of-work-story-map.md`、`requirements-analysis/requirements.md`、`application-design/components.md`、`application-design/component-methods.md`、`application-design/services.md`のU4契約を具体化する。U3 `AutoDecisionRecord`とcanonical Intent auditを正本とし、別review store、PR、runner、harness固有projectionを正本にしない。 + +## Aggregate map + +```mermaid +flowchart LR + AD["Auto Decision Record"] --> DP["Decision Projection"] + DP --> Q["Review Queue"] + HC["Human Review Command"] --> RP["Review Plan"] + Q --> RP + RP --> RE["AUTO_DECISION_REVIEWED"] + RE --> DR["Decision Review Projection"] + CS["Completion Seal"] --> PE["Post-Seal Review Extension"] + RE --> PE + DR --> ST["Status / Telemetry"] + DR --> RS["Remediation Suggestion"] +``` + +テキスト代替: U3 decisionをIntent内read modelへ投影し、eligible unreviewed decisionだけをqueueにする。real human review commandがreview eventを計画し、active auditまたはcompleted post-seal extensionへappendする。review projectionからstatus、telemetry、非実行remediation提案を作る。 + +## Entity catalog + +| Entity / Value Object | Kind | Identity | Owner | Persistence | +|---|---|---|---|---| +| `DecisionProjection` | Read Model | Intent UUID + decision ID | M05 / M07 | canonical audit projection | +| `DecisionQuery` | Value Object | query fingerprint | M05 | transient / cursor | +| `DecisionCursor` | Value Object | query + immutable read snapshot + last occurrence | M05 | client token | +| `DecisionDetail` | Value Object | Intent + decision | M05 | derived | +| `DecisionReviewCommand` | Command | command occurrence | M06 | human turn reference | +| `HumanReviewAuthorizationReceipt` | Value Object | source turn + command binding | M06 / M07 | source audit reference | +| `AutoDecisionSubjectPayloadV1` | Value Object | decision + provenance / actor registry | M04 / M05 / M06 | `AUTO_DECIDED.subject_v1` | +| `AutoDecisionCommitAuthorization` | Value Object | mode revisionまたはgrant exercise | M04 / M05 / M06 | transient validation input | +| `AutoDecisionPrincipalAuthorizationReceipt` | Value Object | decision + canonical principal provenance | M04 | audit-revalidated receipt | +| `CanonicalAutonomyProvenanceSnapshot` | Value Object | Intent UUID + audit revision | M04 / M07 | atomic audit / revision read | +| `CanonicalAutonomyProvenanceReader` | Domain Port | Intent UUID | M04、M07 adapter | canonical audit + lock / projection revision snapshot | +| `DecisionActorRegistryReader` | Domain Port | registry entry ID | M05、M07 adapter | canonical Event Registry read | +| `DecisionReviewPlan` | Value Object | review ID | M06 / M07 | audit plan | +| `DecisionReviewProjection` | Entity | Intent + decision | M05 / M07 | canonical audit projection | +| `PostSealReviewExtension` | Entity | extension ID | M07 | completed audit extension | +| `RemediationSuggestion` | Value Object | review ID + kind | M06 | derived / event safe metadata | +| `ReviewStatusProjection` | Read Model | Intent UUID + audit revision | M07 | derived status | +| `ReviewHarnessContractFixture` | Value Object | fixture ID | M09 | contract test input | + +## DecisionProjection + +| Attribute | Invariant | +|---|---| +| `intentUuid / decisionId` | decision IDはtarget partition内で解決 | +| `interactionIdentity` | question / gate + occurrence + graph revision | +| `question` | redacted text / safe digest | +| `options` | canonical ID + safe label + selected flag | +| `principal / actor` | safe nullable reference + redaction status | +| `decisionSource / safeBasisDigest` | upstream decider enum + nullable redacted basis digest | +| `grantId` | full decisionだけnon-null | +| `evidence` | safe identity / digest / redaction status | +| `degradedCapability` | recommendation degradation、通常null | +| `reviewState` | `not-applicable / unreviewed / accepted / flagged` | +| `review` | terminal review ref、未確認はnull | + +`not-applicable`はmode-semi、grant-gate、confirmed-policy、norm、historyに限定する。`unreviewed`はsolo-election / agent-recommendationだけである。accepted / flaggedから別stateへ遷移しない。 + +## DecisionQueryとDecisionCursor + +`DecisionQuery`はexplicit Intent UUID、`active / completed / either` lifecycle、optional review state、page sizeを持つ。`queryFingerprint`はcanonical filterから決定する。 + +query fingerprintはIntent UUID、lifecycle、review state、page sizeだけを`amadeus.decision-query.v1`でencodeし、page位置を含めない。`DecisionCursor`はその不変query fingerprint、同一read snapshotから得たtarget audit revision、nullable review extension head、canonical decision / review event-set digest、last occurrence、last decision ID、`amadeus.decision-cursor.v1` digestを持つ。別Intent / filterでの再利用、unknown field、改ざんを拒否する。 + +public `listAutoDecisions`は`DecisionPage(items, nextCursor, queryFingerprint)`を返す。page sizeはbounded positive integerであり、cursorがない最初のpageとcursor付き継続pageを同じquery fingerprintとsnapshot 3値へ束縛する。継続時にauthoritative snapshotが変わっていれば`CONFLICT(cursorSnapshot)`で拒否し、最初のpageから再取得させる。 + +## DecisionDetail + +detailはlist projectionに加え、canonical option set、selected option、evidence references、degraded capability reason、graph revision、audit event identity、review receiptを返す。raw provider prompt / tool payload / credentialは型に持たず、redacted textまたはsafe digestだけを持つ。 + +public `getAutoDecision`はprotected `AutoDecisionRecord`そのものではなく`DecisionDetail`を返す。queue判定は上流`AutoDecisionRecord.decider`のclosed enumとreview stateを使い、display labelやfree-form `basis`から推測しない。`confirmed-policy / norm-history`はnot-applicable、`solo-election / agent-recommendation`だけがunreviewedになり得る。 + +## HumanReviewAuthorizationReceipt + +| Attribute | Invariant | +|---|---| +| `sourceIntentUuid` | review時にactiveなIntent | +| `sourceHumanTurnEventId / sourceAuditRevision` | source canonical auditのreal turn commit receiptと一致 | +| `principalId` | source turn principal | +| `reviewActorId` | principalIdとexactly同一 | +| `commandOccurrenceId / commandBindingDigest` | target Intent / decision / choice / flag classification / safe note digestを含むhuman command | +| `targetIntentUuid / decisionId / choice` | review commandとexact match | + +active targetではsource=targetである。completed targetではsourceは現在のactive Intentであり、targetのcompletion sealへ`HUMAN_TURN`を追記しない。receiptはsource auditから再検証可能なstable referenceで、permission token / bearer credentialではない。 + +authorization inputはcommand binding、source human-turn ID / event IDだけを持つ。real `HUMAN_TURN`のreserved `review_command_v1` fieldはsource / target / decision / choice / occurrence / flag classification / safe note digest / turn / binding digestのclosed v1 payloadを持つ。M07-owned canonical readerがsource Intentのactive projection、audit、commit receiptsをstoreから直接取得し、event payload、principal、commit receipt内event identity、source audit revisionをexact matchする。caller提供のaudit / receipt / lifecycleを受けず、`appendDecisionReview`時にもcanonical sourceを再検証する。 + +## DecisionReviewCommandとPlan + +| Attribute | Invariant | +|---|---| +| `intentUuid` | explicit target、逆引き禁止 | +| `decisionId` | target audit内に存在 | +| `choice` | `accept / flag` | +| `humanAuthorization.commandOccurrenceId` | human turnと同じreview command | +| `humanAuthorization` | validated `HumanReviewAuthorizationReceipt` | +| `humanAuthorization.flagClassification` | `contract-defect / specification-change / unspecified / null`、human turnへ束縛 | +| `humanAuthorization.safeNoteDigest` | optional、human turnへ束縛、raw note非保存 | + +review ID、review event IDはそれぞれ`amadeus.decision-review.v1`、`amadeus.decision-review-event.v1`の型tag・長さ接頭辞付きcanonical tupleから作る。planはtarget lifecycle、expected revision / completion seal、closed `AutoDecisionReviewedPayloadV1`、remediation suggestionを持つ。caller提供の`isEligible` booleanを受けず、M05 / M07 projectionから内部検証する。 + +## DecisionReviewProjection + +| Attribute | Invariant | +|---|---| +| `decisionId / reviewId` | owning Intent内でunique | +| `choice / state` | accept→accepted、flag→flagged | +| `principalId / reviewActorId` | 同じreal human principal、別field | +| `sourceIntentUuid / sourceHumanTurnEventId` | canonical human provenance reference | +| `reviewEventId / transactionId` | append receiptと一致 | +| `lifecycleAtReview` | active / completed | +| `reviewedAtAuditPosition` | display用、identityには不使用 | +| `remediation` | acceptはnull、flagはproposal | + +同じreview ID / event identityのreplayは同じprojectionを返す。別choiceのeventを同じdecisionへ適用すると`ContractError(code=CONFLICT,locus=reviewState)`でfail-closedする。 + +review actorはreal human principalと同一である。M04-owned `AutoDecisionPrincipalAuthorizer`はM07 adapter実装の`CanonicalAutonomyProvenanceReader` portからcanonical multi-shard audit、authoritative lock revision、state projection revisionを同一snapshotで読み、semiではmode event、fullではgrant issuance / exerciseからprincipal provenanceを解決したclosed receiptを発行・再検証する。event件数やshard sequenceをrevisionへ代用しない。M06 / callerはprincipal refやprovenance eventを入力できない。receiptはsnapshotのaudit / projection revisionとcanonical digestを持ち、M06がaudit revisionを`AuditTransaction.expectedRevision`へそのまま設定する。M07 append lock内CASと一致しなければtransaction全体を拒否する。 + +新規decision producerの唯一の入口`AutoDecisionCommitPlanner.plan`はdecision / M04 receipt / `AutoDecisionSubjectInput`を同時に受ける。M05はM04 public verifierでreceiptをcanonical auditへ再検証し、M05-owned `DecisionActorRegistryReader` portでM06 execution actorのcanonical rowを読む。callerはregistry entry IDだけを渡し、principal / actor ref / kindを注入できない。plannerは完全な`AuditEventPlan`を返し、決定的event identityと同じeventの必須`subject_v1` fieldにreceipt digest / source revisionを含める。subject無しoverloadはなく、入力欠落・kind / decision不一致は`PROVENANCE_REQUIRED`でplanを返さない。field導入前のeventだけは各ref=null / status=withheldとする。free-form basis、grant ID、未登録harness identityから主体を推測しない。 + +## PostSealReviewExtension + +| Attribute | Invariant | +|---|---| +| `intentUuid` | completed target | +| `completionSealDigest` | original seal exact match | +| `previousExtensionId` | nullまたはcurrent head | +| `reviewEventId` | exactly `AUTO_DECISION_REVIEWED` | +| `reviewPayloadDigest / auditTransactionId` | event payload / receiptとexact match | +| `extensionId` | payload digestを含む`amadeus.review-extension.v1` canonical tuple digest | +| `extensionRevision` | dense、review extension内だけ | + +extensionはschemaVersion、target Intent、completion seal、nullable previous head、review event ID、review payload digest、audit transaction ID、dense revision、extension IDのclosed schemaであり、completion seal / lifecycle / artifact digestの一部を置換しない。canonical readはsealed historyを検証後、payload bytesを再hashし、event / transaction / extension tupleを再計算して別chainとして検証・合成する。unknown field / event、payload改変、gap、fork、seal mismatchを拒否する。 + +## RemediationSuggestion + +closed unionは次のとおりである。 + +- `self-fix-proposal`: existing contract / requirement defect。実行commandなし。 +- `self-feature-proposal`: specification addition / change。実行commandなし。 +- `self-fix-with-feature-alternative`: classification unspecified。両方の選択条件を表示し、実行しない。 + +全variantはsource review ID、target Intent、decision ID、reason code、安全な説明を持つ。new Intent ID、scope grant、automatic commandを持たない。 + +## ReviewStatusProjection + +| Attribute | Invariant | +|---|---| +| `intentUuid / lifecycle` | explicit target | +| `autonomyMode` | none / semi / full | +| `workflowExecutionState` | active=running/suspended、completed=null | +| `grant` | legal U3 projection、completed=null | +| `decisionPolicyCount` | non-negative | +| `decision / unreviewed / accepted / flagged counts` | reducerで導出 | +| `suspendedReason / stopReason / resumeCondition` | U3 resultと一致 | +| `legacyDiagnostic` | optional、authorizationには不使用 | + +human / machineは同じprojectionから生成する。formattingやlocaleをcanonical identityへ含めない。 + +projector inputはlifecycle、U3 `AutonomyProjection`、workflow result、current grant scope、decision policy count、decision / review counts、review extension head、legacy diagnosticを必須にする。current grant scopeはM07がgrant issuance eventから再生した`SafeGrantScopeSummary`であり、`AutonomyProjection.currentGrant.scopeFingerprint`とexact matchしなければ`ILLEGAL_STATE`とする。none / semi / completedでは両方null、fullでは両方non-nullを要求する。出力はdecision total / unreviewed / accepted / flagged、policy count、suspended / stop reasonを欠落させない。 + +## ReviewHarnessContractFixture + +fixtureはfixture ID、contract revision、exact 5 harness tuple、canonical-value-v1 golden vectors、source / target canonical audit、authorization / list / detail / review / statusのsuccess / exact `ContractError` cases、session / process / compaction / clone reload checkpointsを持つ。reload observationはsource / target revision、extension head、queue、terminal review receiptsを返す。active / completed、cross-Intent reject、missing / synthetic human reject、terminal conflict、永続化を同じCore oracleで検証する。adapterはharness IDとentrypoint / reload呼出しだけを担当し、expected値やreducerをharness別に持たない。 + +## Audit event projection + +| Event fact | Projection effect | +|---|---| +| `AUTO_DECIDED` | decision historyとeligible queueを生成 | +| `AUTO_DECISION_REVIEWED(accept)` | unreviewed→accepted、queue count減算 | +| `AUTO_DECISION_REVIEWED(flag)` | unreviewed→flagged、queue count減算、proposal表示 | +| completion seal | completed lifecycleとoriginal sealを固定 | +| post-seal review extension | reviewだけをvalidated chainへ追加 | + +`AUTO_DECISION_REVIEWED.payload_v1`はaudit transaction IDとreceipt projection revisionを持つ。replay reducerはcanonical event identityとpayloadから`DecisionReviewReceipt`全fieldを再構築し、session / process / compaction / clone後もsame-choiceへ同じreceiptを返す。transaction情報をprocess-local cacheへ依存させない。 + +M05 reducerはdecision / review stateを所有し、M07はcanonical read / append / seal validation / status assemblyを所有する。M06はcommand / UX orchestrationを所有し、state reducerやseal例外を実装しない。 + +## Event Registry / OTel projection + +registry entryはevent name、Intent / decision / review / transaction IDs、choice、review principal / actor、decision principal / actor / source / safe basis digest、source Intent / human turn reference、nullable grant、lifecycle-at-review、remediation kindを持つ。OTel attributesは`amadeus.intent.id`、`amadeus.decision.id`、`amadeus.review.id`、choice / lifecycle / decision source enum、optional basis digest、pseudonymous review/decision subject references、optional grant ID / note digest、redaction status、audit transaction / trace / span IDに限定する。redaction失敗時はcorrelation ID / enumと`withheld`だけを残す。 + +## Verification invariants + +- explicit targetなしにlist / detail / reviewを構築できない。 +- cross-Intent decision IDが別Intentの存在やdetailを漏らさない。 +- not-applicable decisionがqueue / review対象にならない。 +- synthetic human、duplicate conflicting review、unknown eventがstateを変えない。 +- completed target reviewがactive source Intentのreal human turn receiptを検証し、target sealへHUMAN_TURNを追加しない。 +- completed review後もcompletion seal、artifact digest、workflow / grant terminal stateが同一である。 +- same review replayが同じreceipt / projection / extension headを返す。 +- flagがrollback、Intent reopen、new Intent creationを生成しない。 +- 5 harnessが同じfixtureからbyte-equivalentなmachine status / review receiptを返す。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/functional-design-questions.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/functional-design-questions.md new file mode 100644 index 0000000000..2d0f918df4 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/functional-design/functional-design-questions.md @@ -0,0 +1,28 @@ +# Functional Design Questions — autonomy-review-observability + +## 上流入力 + +`units-generation/unit-of-work.md`、`units-generation/unit-of-work-story-map.md`、`requirements-analysis/requirements.md`、`application-design/components.md`、`application-design/component-methods.md`、`application-design/services.md`を照合した。 + +## 確認結果 + +初稿レビューで、completed Intentのsealへ`HUMAN_TURN`を追加できない一方、reviewには新しいreal human turnが必要という上流契約の抜けが判明した。 + +人間選択は「active Intent参照」で確定した。completed reviewでは現在のactive Intentへreal `HUMAN_TURN`をcanonical commitし、completed target / decision / choice / command occurrenceへ束縛したreceiptをtargetの`AUTO_DECISION_REVIEWED`から参照する。completed sealのappend例外は広げない。 + +fresh reviewでsource auditの信頼境界が未決と判明し、人間選択は「M07直接read」で確定した。callerはaudit配列、commit receipt、lifecycleを渡さず、M07自身がcanonical storeからactive source、real human turn、commit receiptを取得・再検証する。 + +次のreviewer上限で主体生成元が未決と判明し、人間選択は「人間principal兼actor」で確定した。review principal / actorはreal human principalの同一safe referenceとし、decision eventにsafe主体fieldがなければdecision principal / actorはnull / withheldとする。 + +上記以外に人間へ追加確認すべきIssue外の矛盾・抜け漏れはない。 + +- active / completed Intentのdecision list / detailを明示target Intentで取得する。 +- queue対象はsolo election / agent recommendationの`unreviewed`だけとし、policy / norm / history / gate decisionは履歴表示だけにする。 +- `accept / flag`はreal `HUMAN_TURN`を必須とし、`AUTO_DECISION_REVIEWED`をcanonical auditへ記録する。 +- completed Intentではこのeventだけをprotected post-seal appendとして許可し、過去event、成果物、workflow lifecycle、completion sealを変更しない。 +- `flag`はrollbackせず、既存contract不備なら`self-fix`、仕様追加・変更なら`self-feature`を提案するだけで、新Intentを自動作成しない。 +- raw credential / evidence / host payloadは表示・保存せず、redacted valueまたはsafe digestだけを使う。 + +## Optional artifact判断 + +`frontend-components.md`は生成しない。U4のsurfaceは既存CLI status / replay / read commandとmachine-readable projectionであり、新規frontend componentを含まない。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/logical-components.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/logical-components.md new file mode 100644 index 0000000000..da2d92b869 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/logical-components.md @@ -0,0 +1,39 @@ +# Logical Components — autonomy-review-observability + +## 入力と境界 + +本設計は`functional-design/business-logic-model.md`を正本とする。`performance-requirements.md`、`security-requirements.md`、`scalability-requirements.md`、`reliability-requirements.md`、`tech-stack-decisions.md`はexpected absenceである。 + +U4はdecision review / observability projectionを所有する。Intent reopen、rollback、過去event変更、新Intent作成、PR、runner、terminal live completionを所有しない。 + +## Component inventory + +| Component | Owns | Isolation | +| --- | --- | --- | +| `DecisionReadModel` (M05/M07) | list / detail、queue eligibility、snapshot-bound cursor、review projection | explicit Intent partitionだけを読み、page間driftを拒否 | +| `SafeDecisionProjector` | question / option / subject / evidence redaction | raw fallback禁止、withheldをclosed表現 | +| `HumanReviewAuthorizer` (M07) | active source canonical read、human turn / binding / receipt検証 | caller payloadをauthorityにしない | +| `DecisionReviewCoordinator` (M06) | accept / flag command、classification、remediation suggestion | suggestionを実行しない | +| `ProtectedReviewAppender` (M07) | active review atomic append、idempotent receipt | decision / effectを変更しない | +| `CompletedReviewValidator` (M07) | seal / head CAS、review-only extension chain | general post-seal mutationへ再利用不可 | +| `ReviewStatusProjector` (M07) | human / machine status from one validated input | formatterごとの再読を禁止 | +| `ReviewTelemetryProjector` | Event Registry / OTel safe attributes | shared redaction後metadataだけ | +| `ReviewHarnessVerifier` (M09) | 5harness canonical vectors、behavior / reload fixture | native adapterへeligibilityを複製しない | + +## Dependency direction + +`Canonical audit → ReadModel / Authorizer → Coordinator → protected appender → Review projection / Status / Telemetry`の方向に限定する。M05はM07を直接importせずreader / registry portを使う。Coordinatorはseal bypassやraw audit append APIを持たない。 + +ReadModel adapterはaudit revision、extension head、event-set digestを同じread snapshotから返す。M05はそのopaque snapshot identityをcursorへ束縛し、次pageのM07再読がdriftを検出した場合はitemsを返さず`CONFLICT(cursorSnapshot)`を伝播する。 + +## Blast radius + +- source authorization不備: target reviewを変更しない。 +- redaction失敗: 対象fieldをwithheldにし、raw payloadを公開しない。 +- completed extension conflict: target completionは維持し、review appendだけを拒否する。 +- flag: remediation suggestionだけを返し、現Intent / grant / artifactを変更しない。 +- harness mismatch: suiteをfailにするがCore review stateを変更しない。 + +## Test seams + +source / target audit reader、human-turn verifier、redactor、actor registry、active appender、completed validator、canonical encoder、status / telemetry sink、persistence reload、harness adapterをport化する。active / completed、tamper、crash、clone、redactionをclosed fixtureで検証する。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/nfr-design-questions.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/nfr-design-questions.md new file mode 100644 index 0000000000..10bf78ff34 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/nfr-design-questions.md @@ -0,0 +1,18 @@ +# NFR Design 質問 — autonomy-review-observability + +## 裁定結果 + +追加のユーザー裁定は不要である。`performance-requirements.md`、`security-requirements.md`、`scalability-requirements.md`、`reliability-requirements.md`、`tech-stack-decisions.md`はexpected absenceであり、`functional-design/business-logic-model.md`のFR-OBS / 2067-AC18〜21契約をNFRオラクルとする。 + +## 矛盾・抜け漏れ確認 + +completed Intent reviewはoriginal completion sealを変更せず、別extension chainへreview eventだけをappendする。flagはrollbackやIntent reopenをせずself-fix / self-featureを提案するだけ、というIssue境界を維持する。 + +## Reviewer上限到達後の進め方 + +- A. Functional Designの`DecisionCursor`公開型・identity tuple・5ハーネスcanonical vectorをsnapshot-bound契約へ同期し、新しいレビューサイクルで再確認する(推奨) +- B. autonomy-review-observability UnitのNFR Designを最初からやり直す +- C. ここで停止する +- X. その他 + +[Answer]: A — Functional Designの公開契約をsnapshot-bound cursorへ同期し、新しいレビューサイクルで再確認する。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/performance-design.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/performance-design.md new file mode 100644 index 0000000000..38b38fd917 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/performance-design.md @@ -0,0 +1,63 @@ +# Performance Design — autonomy-review-observability + +## 入力と性能オラクル + +本設計は`functional-design/business-logic-model.md`を正本とする。`performance-requirements.md`、`security-requirements.md`、`scalability-requirements.md`、`reliability-requirements.md`、`tech-stack-decisions.md`はexpected absenceであり、数値レイテンシSLOを追加しない。 + +性能オラクルはexplicit Intent partition、stable pagination、review-state index、incremental extension headであり、list / detail / statusのたびに全Intent・全audit・全artifactをscanしないことである。 + +## Queryとpagination + +M07はIntent + lifecycle + review stateでdecision projectionをindexし、canonical occurrence sequence + decision IDでsort keyを固定する。query fingerprintはfilterとpage size、cursor digestはquery fingerprintとlast occurrence / decisionから導出する。 + +NFR実装ではpublic `DecisionCursor`を`targetAuditRevision`、nullable `reviewExtensionHead`、`projectionEventSetDigest`でrefineする。first pageはM07の単一read snapshotからこの3値とitemsを取得し、next cursorへ固定する。cursor digestはquery fingerprint、snapshot 3値、last occurrence / decisionを別domainでhashする。 + +subsequent pageは同じIntentのauthoritative audit revision、extension head、canonical decision / review event-set digestを再読し、cursorのsnapshotとexact matchした場合だけ続行する。event-setはvalid `AUTO_DECIDED / AUTO_DECISION_REVIEWED`をclosed payload digest付きentryへ変換し、closed event-type orderとevent ID byte順でsortし、exact duplicateだけをdedupeして`amadeus.decision-projection-event-set.v1`でhashする。1つでもdriftした場合は旧cursorを適用せず`ContractError(code=CONFLICT,locus=cursorSnapshot)`を返し、callerはfirst pageから再取得する。historical snapshotを推測して再構成しない。 + +`pageSize`は正の整数かつCore-owned `reviewQueryLimits.maxPageSize`以下を要求する。上限値をharnessごとに変えない。cursor mismatch、上限超過、unknown lifecycleは全量fallbackせず`MALFORMED`へ閉じる。 + +detailはdecision IDをtarget Intent partition内でexact lookupする。decision IDからIntentを逆引きせず、cross-Intent probeで存在有無を開示しない。 + +## Review projectionとencoding + +review reducerは`AUTO_DECIDED`と`AUTO_DECISION_REVIEWED`をcontent identityで畳み込み、decisionごとにterminal review state / receiptを1件だけ投影する。same review ID再送は保存済みreceiptを返し、再encodingやextension再appendを行わない。 + +canonical-tuple-v1 / canonical-value-v1 encodingはpayload byte数へ線形、event-set生成は対象event件数に対するsortを含む`O(E log E)`、追加memoryは`O(E + pageSize)`にする。event ID衝突、unknown / missing fieldを汎用JSON objectとして保持せずparse時に拒否する。 + +## Statusとtelemetry + +statusはvalidated `ReviewStatusInput`からdecision counts、grant summary、stop / resumeを1回投影する。human / machine formatterは同じ入力を使い、別々にauditを読むことを禁止する。 + +OTel属性はevent計画時に生成済みのsafe ID / enum / digestを再利用し、raw question / evidenceをspanごとに再redactしない。redaction失敗はwithheldとしてbounded metadataだけを出す。 + +## Verification + +page境界、cursor tamper、large decision history、review済み再送、extension chain成長、redaction failureを検査する。成功した同一snapshot traversalでは各pageのquery / snapshot fingerprint不変、items重複 / 欠落0、same reviewの追加append 0、list read setがtarget Intent partitionへ限定されることを要求する。page間のnew decision、review-state変更、clone merge、completed extension appendは旧cursorをexact `CONFLICT(cursorSnapshot)`へし、first page再取得後の新snapshotで完全なqueueを返す。 + +## Review — Iteration 1 + +- **Verdict:** NOT-READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T15:44:01Z +- **Iteration:** 1 +- **Scope decision:** none + +cursorの公開型・identity tuple・drift処理は成果物間で統一されたが、clone変化検出の基礎となるprojectionEventSetDigest自体のcanonical生成規則がなく、5 harnessで同一cursorを実装できない。 + +### Findings + +- BLOCKER | projectionEventSetDigestのcanonical contractが未定義である。各成果物はdecision/review event-setのdigestをcursorへ含めるが、対象event、invalid eventの扱い、identityかpayload digestのどちらを入力にするか、sort/dedupe順、domain tag、byte encodingを規定していない。このdigestはaudit revisionやextension headだけでは検出できないclone mergeを判定し、amadeus.decision-cursor.v1と5 harness golden bytesへ直接入るため、実装差により同じevent setから異なるcursor digestまたはdrift判定が生成され得る。canonical event-set tuple/encodingとgolden vectorを定義するか、既存の一意なcanonical snapshot digestを明示的に参照する必要がある。 + +## Review — Iteration 2 + +- **Verdict:** READY +- **Reviewer:** amadeus-architecture-reviewer-agent +- **Date:** 2026-08-03T15:45:56Z +- **Iteration:** 2 +- **Scope decision:** none + +projectionEventSetDigestは対象event、closed entry、payload digest、sort・dedupe、衝突/不正時の閉じ方、domain-separated encoding、golden vectorsまで一意に定義された。cursor、reload、clone drift、5 harness contractとの整合も取れており、未解決BLOCKERや具体的な循環依存はない。 + +### Findings + +- None diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/reliability-design.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/reliability-design.md new file mode 100644 index 0000000000..138affdd1a --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/reliability-design.md @@ -0,0 +1,37 @@ +# Reliability Design — autonomy-review-observability + +## 入力とimmutability原則 + +本設計は`functional-design/business-logic-model.md`を正本とする。`performance-requirements.md`、`security-requirements.md`、`scalability-requirements.md`、`reliability-requirements.md`、`tech-stack-decisions.md`はexpected absenceである。 + +reviewはimmutable decision / effectを変更せず、terminal review projectionだけをappendする。accept / flagでeffect再実行、rollback、Intent reopen、grant変更、artifact変更を行わない。 + +## Active review transaction + +M07はauthorization sourceを再読し、target decisionがeligible / unreviewed、expected target revision一致をappend lock内で検証する。`AUTO_DECISION_REVIEWED` payload、event identity、transaction identity、receipt projection revisionを同一planから生成してcommitする。 + +same review ID / same choiceはcanonical eventから同じ`DecisionReviewReceipt`を再構築する。異choiceまたは異payloadは`CONFLICT`とし、既存reviewを上書きしない。append前crashは未review、append後crashはterminal reviewとして再生する。 + +## Completed extension transaction + +completed reviewはexpected seal、target revision、current extension headをCASし、review eventと`PostSealReviewExtension` successorを1 transactionでcommitする。extension IDはseal、previous head、review event / payload digest、transaction、revisionへ束縛する。 + +review eventだけ成功、extensionだけ成功、headだけ更新という部分状態を可視化しない。replayはoriginal sealed historyとvalid extension chainだけを合成し、invalid / forked / digest mismatch extensionをcompletion truthへ入れない。 + +## Cross-Intent authorization recovery + +completed targetではsource human turnが先にcommitされる。source commit後・target append前のcrashはtarget stateを変えず、same source referenceでtarget transactionを再試行する。target terminal review後はreceipt再利用を拒否する。 + +source lifecycle、human event、binding digest、commit receipt、source revisionがdriftした場合はtarget appendを拒否する。cross-Intent原子性を偽装せず、source evidenceとtarget effectの責務を分離する。 + +## Canonical encodingとreload + +identityはlength-prefixed `canonical-tuple-v1`、contract valueはclosed-schema `canonical-value-v1`を使う。field順、explicit null、array order、integer range、NFC / newlineを固定し、native JSON stringifyへ依存しない。 + +DecisionCursorはtarget audit revision、nullable extension head、projection event-set digestを含むread snapshot identityへ束縛する。event-set digestはtarget Intent内のvalid `AUTO_DECIDED / AUTO_DECISION_REVIEWED`だけをclosed entryへ変換し、event type / event ID順にsort、exact duplicateをdedupeした`amadeus.decision-projection-event-set.v1` tupleから生成する。同一event IDの内容衝突は`CONFLICT(projectionEventSet)`、対象eventのschema / Intent不正は`MALFORMED(projectionEventSet)`とし、cursorを生成しない。page間でdecision / review event、clone merge、extension headが変わった場合は旧cursorを`CONFLICT(cursorSnapshot)`として拒否し、mutable projection上で継続しない。したがって成功したmulti-page traversalはexactly one snapshotだけを観測する。 + +session / process / compaction / clone reload後にcanonical revisions、extension head、queue、terminal receiptsを比較する。reload結果の一部欠落をpassにせずexact `ContractError`へ閉じる。 + +## Failure injection + +source turn、authorization、active append、completed seal/head CAS、review event / extension commit、page間decision / review / clone mutation、redaction、Registry / OTel、各reload境界へcrash / driftを注入する。duplicate terminal review 0、partial extension 0、completion seal / artifact digest差分0、raw fallback 0、same review receipt差分0、stale cursor成功0を要求する。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/scalability-design.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/scalability-design.md new file mode 100644 index 0000000000..30ae5beca8 --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/scalability-design.md @@ -0,0 +1,29 @@ +# Scalability Design — autonomy-review-observability + +## 入力とpartition + +本設計は`functional-design/business-logic-model.md`を正本とする。`performance-requirements.md`、`security-requirements.md`、`scalability-requirements.md`、`reliability-requirements.md`、`tech-stack-decisions.md`はexpected absenceである。中央review serviceを追加せず、Intent audit partitionとreview extension chainを使う。 + +decision list / detail / review / statusはexplicit target Intent UUIDでpartitionする。completed reviewのsource human turnは別active source partitionに存在するが、target mutationはtarget review transactionだけで行う。 + +## Active / completed growth + +active Intentは通常protected append、completed Intentはsealごとのextension chainへappendする。extension entryはprevious headとdense revisionへ束縛し、review数に比例してchainを成長させるがoriginal sealed historyを再hashしない。 + +source turn commitとtarget appendをcross-Intent distributed transactionにしない。source evidenceはtarget stateを変更しないため、target append失敗時に同じauthorizationでidempotent retryできる。terminal review後は再利用を拒否する。 + +## Cloneとpersistence + +session / process / compaction / clone reloadはcanonical target revision、extension head、queue page、full terminal receiptsを再読する。同一review identityを畳み込み、同じprevious headへ異なるsuccessorがある場合は物理順で選ばず`CONFLICT`とする。 + +paginationはsnapshot revision / head / event-set digestへ束縛するため、clone mergeやreview appendがpage間に入った traversalを継続しない。旧cursorは明示的conflictとなり、新しいfirst pageから新snapshotを読む。これによりwriterをglobal lockで長時間止めず、successful traversalの一貫性を保つ。 + +## Harness growth + +Claude Code、Codex、Cursor、OpenCode、Kimi Codeは同じCore list / detail / review / status / reload fixtureとcanonical byte vectorsを使う。native adapterは引数と表示だけを投影し、eligibility、seal validator、redaction、remediation分類を複製しない。 + +将来harnessはdescriptor registry row、adapter、同じfixture receiptの追加で閉じる。required cohortはregistry-derivedに移行可能なownerをM09に保ち、M05 / M07へharness分岐を追加しない。 + +## Verification + +多数Intent、active / completed混在、長いextension chain、clone fork、4 persistence boundary、harness cohort追加をfixture化する。target partition isolation、same event setの同一queue / receipt / head、unknown / duplicate harnessのfail-closedを要求する。 diff --git a/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/security-design.md b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/security-design.md new file mode 100644 index 0000000000..54601d860e --- /dev/null +++ b/amadeus/spaces/default/intents/260803-intent-autonomy/construction/autonomy-review-observability/nfr-design/security-design.md @@ -0,0 +1,32 @@ +# Security Design — autonomy-review-observability + +## 入力とtrust boundary + +本設計は`functional-design/business-logic-model.md`を正本とする。`performance-requirements.md`、`security-requirements.md`、`scalability-requirements.md`、`reliability-requirements.md`、`tech-stack-decisions.md`はexpected absenceである。 + +trust boundaryはactive source human turn、target decision lookup、completed-only append validator、redaction、Event Registry / OTel projectionである。caller提供audit、receipt、principal、actor、lifecycleをauthorityとして信用しない。 + +## Human review authorization + +`CanonicalReviewSourceReader`はM07内部でsource Intentのcanonical audit、commit receipt、audit revisionを同じsnapshotから読む。`authorizeHumanReview`はsource lifecycle=active、real `HUMAN_TURN`、`review_command_v1` binding、target / decision / choice / classification / safe note digest、principalをexact matchする。 + +active targetではsource=target、completed targetではexplicit active source Intentを要求する。sourceがなければ`PROVENANCE_REQUIRED`で状態を変えない。source turnからtargetを推測せず、別targetへreceiptをbearer tokenとして再利用しない。 + +review principalとactorはreal human principalへ一致させる。decision principal / actorはcanonical `AUTO_DECIDED.subject_v1`だけから投影し、legacy eventではnull / withheldにして推測しない。 + +## Completed seal protection + +`CompletedDecisionReviewValidator`だけがcompleted targetへexactly `AUTO_DECISION_REVIEWED`をappendできる。transactionにlifecycle、artifact、grant、workflow、decision mutationが含まれた場合は全体を拒否する。 + +expected completion seal digest、current extension head、target audit revisionをappend lock内で再検証する。original sealとartifact digestは不変で、review eventは別hash chainにだけ追加する。このAPIを一般的なpost-seal appendへ公開しない。 + +## Privacyとredaction + +public list / detail、status、Registry、OTelは共通safe projectionだけを使う。credential、raw prompt、host / tool payload、raw evidence、未redact noteを返さない。redaction失敗時は値とdigestをnull、status=`withheld`にし、raw fallbackを禁止する。 + +OTelはstable ID / closed enum / safe digestだけを属性にする。grant IDやpseudonymous subject refは既存access / retention policyに従い、別telemetry storeを作らない。 + +## Security verification + +synthetic human、caller-forged audit、cross-Intent receipt、binding tamper、terminal choice conflict、seal / head drift、mutation event混入、redaction failure、raw telemetry leakageをred fixtureにする。すべてでtarget review mutationまたはsecret出力を0件にする。 + diff --git a/docs/reference/12-state-machine.md b/docs/reference/12-state-machine.md index 553c4a7092..9da29469f4 100644 --- a/docs/reference/12-state-machine.md +++ b/docs/reference/12-state-machine.md @@ -395,6 +395,7 @@ The Loop Monitor commits delivery observations, cycle triggers, Judge reservatio | `LOOP_MONITOR_EVENT_SET_COMMITTED` | `tools/amadeus-loop-monitor-replay.ts` | One atomic Loop Monitor delivery, Judge, or latch transition committed | | `QUALITY_REPAIR_TRANSACTION_COMMITTED` | `tools/amadeus-quality-repair-replay.ts` | One quality snapshot, progress, replan, stall, or resume transaction and its generic Monitor effects committed atomically | | `INTENT_AUTONOMY_TRANSACTION_COMMITTED` | `tools/amadeus-intent-autonomy-replay.ts` | One Intent-scoped mode, grant, decision, workflow-effect, park, resume, or invocation-failure transaction committed atomically | +| `AUTO_DECISION_REVIEWED` | `tools/amadeus-autonomy-review-production.ts` | A human accepted or flagged one immutable auto decision on the review surface (append-only; never replays the decided effect) | ### Swarm diff --git a/packages/framework/core/knowledge/amadeus-shared/audit-format.md b/packages/framework/core/knowledge/amadeus-shared/audit-format.md index 1d099ac6ba..d0d78f06eb 100644 --- a/packages/framework/core/knowledge/amadeus-shared/audit-format.md +++ b/packages/framework/core/knowledge/amadeus-shared/audit-format.md @@ -29,7 +29,7 @@ tools read them from. Neither lists the record envelope — every record carries (the park pair, the practices events) show it in the table as the attribute it is. -## Event Registry (88 events, 21 categories) +## Event Registry (89 events, 21 categories) ### Workflow Lifecycle (7 events) @@ -247,7 +247,7 @@ Emitted by stage-protocol §13 (Learnings Ritual). The runtime-graph compile emi | `RULE_LEARNED` | The learning gate persisted a kept learning as a practice line under the routed heading in `{project,team}.md` | Stage, Candidate-ID, Destination, Heading, Source | — | `tools/amadeus-learnings.ts persist` | | `SENSOR_PROPOSED` | The learning gate scaffolded a project-tier sensor manifest and bound it to the originating stage's `sensors:` frontmatter | Stage, Candidate-ID, Sensor ID, Manifest path, Matches, Destinations, Source | — | `tools/amadeus-learnings.ts persist` | -### Loop Monitor, Quality Repair, and Intent Autonomy (3 events) +### Loop Monitor, Quality Repair, and Intent Autonomy (4 events) The event set is the atomic canonical stream for delivery observation, cycle trigger, Judge reservation/result, closed route application, and latch transitions. The per-clone Replay Index is a repairable secondary projection and never replaces this audit source of truth. @@ -256,6 +256,7 @@ The event set is the atomic canonical stream for delivery observation, cycle tri | `LOOP_MONITOR_EVENT_SET_COMMITTED` | One atomic Loop Monitor delivery/Judge/latch transition commits | Partition Key, Event Set Id, Event Set | — | `tools/amadeus-loop-monitor-replay.ts` | | `QUALITY_REPAIR_TRANSACTION_COMMITTED` | One Quality snapshot/progress/replan/stall/resume transaction and its generic Monitor effects commit atomically | Quality Scope Id, Transaction Id, Transaction | — | `tools/amadeus-quality-repair-replay.ts` | | `INTENT_AUTONOMY_TRANSACTION_COMMITTED` | One Intent-scoped mode/grant/decision/effect/park transaction commits atomically | Intent Uuid, Transaction Id, Transaction Digest, Transaction | Principal, Decider, Actor, Basis | `tools/amadeus-intent-autonomy-replay.ts` | +| `AUTO_DECISION_REVIEWED` | A real human accepts or flags one immutable automatic decision; completed Intent reviews extend the review chain without changing the completion seal | Intent Uuid, Decision Id, Review Id, Choice, Lifecycle, Review Principal, Review Actor, Source Human Turn, Audit Transaction Id, Payload Digest, Payload V1 | Decision Principal, Decision Actor, Decision Source, Basis Digest, Grant Id, Remediation, Note Digest, Redaction Status, Event Identity, Projection Revision, Trace Id, Span Id | `tools/amadeus-autonomy-review-production.ts` | ### Swarm (7 events) diff --git a/packages/framework/core/otel/event-registry.ts b/packages/framework/core/otel/event-registry.ts index 2b41a2c4dc..349dcf5847 100644 --- a/packages/framework/core/otel/event-registry.ts +++ b/packages/framework/core/otel/event-registry.ts @@ -1,6 +1,6 @@ // event-registry.ts — the typed Event Registry (FR-EVT-1). // -// The canonical half of the registry covers the full 88-event audit +// The canonical half of the registry covers the full 89-event audit // vocabulary (#1672; VALID_EVENT_TYPES in tools/amadeus-audit.ts) — every // canonical name maps 1:1 onto the EXISTING v1 audit event vocabulary so the // current readers understand the records unchanged. The telemetry half @@ -76,7 +76,7 @@ export type EventDef = { // The canonical cardinality (#1672). The drift guard pins this so an emptied // or truncated registry fails instead of passing vacuously. -export const EXPECTED_CANONICAL_COUNT = 88; +export const EXPECTED_CANONICAL_COUNT = 89; // The OTel semantic-convention span event name produced by recordException(). // Registered as telemetry (FR-EVT-7): it rides the span record, never the @@ -224,6 +224,40 @@ export const REGISTERED_EVENTS = [ optionalAttributes: ["Principal", "Decider", "Actor", "Basis"], schemaVersion: 1, }, + { + name: "amadeus.auto_decision.reviewed", + auditEvent: "AUTO_DECISION_REVIEWED", + durability: "canonical", + category: "grant", + requiredAttributes: [ + "Intent Uuid", + "Decision Id", + "Review Id", + "Choice", + "Lifecycle", + "Review Principal", + "Review Actor", + "Source Human Turn", + "Audit Transaction Id", + "Payload Digest", + "Payload V1", + ], + optionalAttributes: [ + "Decision Principal", + "Decision Actor", + "Decision Source", + "Basis Digest", + "Grant Id", + "Remediation", + "Note Digest", + "Redaction Status", + "Event Identity", + "Projection Revision", + "Trace Id", + "Span Id", + ], + schemaVersion: 1, + }, // --- Phase Lifecycle (4) --- { name: "amadeus.phase.started", diff --git a/packages/framework/core/tools/amadeus-audit.ts b/packages/framework/core/tools/amadeus-audit.ts index d3336e47b1..539055c5f6 100644 --- a/packages/framework/core/tools/amadeus-audit.ts +++ b/packages/framework/core/tools/amadeus-audit.ts @@ -58,7 +58,7 @@ export type AppendAuditResult = | { appended: true; event: string; timestamp: string } | { appended: false; reason: "intent-complete" | "fatal-latch"; event: string; timestamp: string }; -// --- Canonical event types (84) --- +// --- Canonical event types (85) --- // See docs/reference/12-state-machine.md for the state transitions that emit each event. const VALID_EVENT_TYPES = new Set([ @@ -93,6 +93,7 @@ const VALID_EVENT_TYPES = new Set([ "LOOP_MONITOR_EVENT_SET_COMMITTED", "QUALITY_REPAIR_TRANSACTION_COMMITTED", "INTENT_AUTONOMY_TRANSACTION_COMMITTED", + "AUTO_DECISION_REVIEWED", // Session events (hook-owned) "SESSION_STARTED", "SESSION_RESUMED", @@ -237,6 +238,7 @@ export const EVENT_HEADINGS: Record = { LOOP_MONITOR_EVENT_SET_COMMITTED: "Loop Monitor Event Set Committed", QUALITY_REPAIR_TRANSACTION_COMMITTED: "Quality Repair Transaction Committed", INTENT_AUTONOMY_TRANSACTION_COMMITTED: "Intent Autonomy Transaction Committed", + AUTO_DECISION_REVIEWED: "Auto Decision Reviewed", UNIT_POOL_EVENT_SET_COMMITTED: "Unit Pool Event Set Committed", SESSION_STARTED: "Session Start", SESSION_RESUMED: "Session Resume", diff --git a/packages/framework/core/tools/amadeus-autonomy-review-production.ts b/packages/framework/core/tools/amadeus-autonomy-review-production.ts new file mode 100644 index 0000000000..7406261995 --- /dev/null +++ b/packages/framework/core/tools/amadeus-autonomy-review-production.ts @@ -0,0 +1,484 @@ +// Production audit adapter for Intent autonomy review (#2067). +// +// The domain review service remains storage-neutral. This adapter projects its +// immutable snapshot from the target Intent audit and is the only writer of the +// protected AUTO_DECISION_REVIEWED append path. + +import { + autonomyReviewStableId, + bindHumanReviewCommand, + canonicalContractValueDigest, + canonicalTupleDigest, + createMemoryAutonomyReviewService, + nextReviewExtensionHead, + reviewAuditFields, + type AutonomyReviewPersistenceSnapshot, + type AutoDecisionReviewedEvent, + type DecisionDetail, + type DecisionPage, + type DecisionReviewReceipt, + type HumanReviewCommandBinding, + type HumanReviewTurnSeed, + type IntentLifecycle, + type ReviewChoice, + type ReviewIntentSeed, + normalizeReviewFlagMetadata, + reviewCommandContentDigest, +} from "./amadeus-autonomy-review.ts"; +import { autonomyDigest, autonomyIsRecord } from "./amadeus-intent-autonomy.ts"; +import { readProductionAutonomyProjection } from "./amadeus-intent-autonomy-production.ts"; +import { + activeIntent, + activeSpace, + auditBlockField, + findAllEvents, + listIntents, + readAllAuditShards, + splitAuditRecords, + withAuditLock, +} from "./amadeus-lib.ts"; +import { emitAuditEventGuarded } from "../otel/audit-emit.ts"; + +type ReviewTarget = { + readonly space: string; + readonly dirName: string; + readonly intentUuid: string; + readonly lifecycle: IntentLifecycle; +}; + +function resolveReviewTarget(projectDir: string, selector?: string): ReviewTarget | null { + const space = activeSpace(projectDir); + const activeDir = activeIntent(projectDir, space); + const candidates = listIntents(projectDir, space).filter((candidate) => candidate.dirName !== null); + const selected = selector === undefined + ? candidates.find((candidate) => candidate.dirName === activeDir) + : candidates.find((candidate) => + candidate.uuid === selector || candidate.dirName === selector || candidate.slug === selector + ); + if (selected?.dirName === null || selected?.dirName === undefined || selected.uuid === "") return null; + return { + space, + dirName: selected.dirName, + intentUuid: selected.uuid, + lifecycle: selected.status === "complete" || selected.status === "archived" ? "completed" : "active", + }; +} + +function completionSeal(audit: string, target: ReviewTarget): string | null { + if (target.lifecycle === "active") return null; + const rows = findAllEvents(audit, "INTENT_COMPLETION_TRANSACTION_COMMITTED"); + return rows.length === 0 ? null : auditBlockField(rows.at(-1)!.block, "Completion Seal Digest"); +} + +function currentSeed(projectDir: string, target: ReviewTarget): ReviewIntentSeed { + const autonomy = readProductionAutonomyProjection(projectDir, target.dirName, target.space); + if (autonomy === null) throw new Error("intent-autonomy-projection-not-found"); + const audit = readAllAuditShards(projectDir, target.dirName, target.space); + const seal = completionSeal(audit, target); + if (target.lifecycle === "completed" && seal === null) throw new Error("completed-intent-seal-not-found"); + return { + intentUuid: target.intentUuid, + lifecycle: target.lifecycle, + autonomy, + auditRevision: splitAuditRecords(audit).length, + completionSealDigest: seal, + }; +} + +type ReviewAuditPayload = Record & { + readonly targetIntentUuid: string; + readonly decisionId: string; + readonly reviewId: string; + readonly choice: ReviewChoice; + readonly auditTransactionId: string; + readonly receiptProjectionRevision: number; + readonly lifecycleAtReview: IntentLifecycle; +}; + +function parseReviewPayload(payloadV1: string, payloadDigest: string): ReviewAuditPayload { + const parsed: unknown = JSON.parse(payloadV1); + if (!autonomyIsRecord(parsed)) throw new Error("invalid-review-audit-payload"); + const payload = parsed; + const observedDigest = canonicalContractValueDigest("auto-decision-reviewed-payload", payload); + const valid = [ + observedDigest.ok && observedDigest.value === payloadDigest, + typeof payload.targetIntentUuid === "string", + typeof payload.decisionId === "string", + typeof payload.reviewId === "string", + payload.choice === "accept" || payload.choice === "flag", + typeof payload.auditTransactionId === "string", + typeof payload.receiptProjectionRevision === "number", + payload.lifecycleAtReview === "active" || payload.lifecycleAtReview === "completed", + typeof payload.reviewPrincipalRef === "string", + typeof payload.reviewActorRef === "string", + typeof payload.decisionPrincipalRef === "string", + typeof payload.decisionActorRef === "string", + typeof payload.decisionSource === "string", + typeof payload.safeBasisDigest === "string", + payload.grantId === null || typeof payload.grantId === "string", + typeof payload.sourceIntentUuid === "string", + typeof payload.sourceHumanTurnId === "string", + typeof payload.sourceHumanTurnEventId === "string", + typeof payload.commandOccurrenceId === "string", + typeof payload.commandBindingDigest === "string", + payload.remediation === null || typeof payload.remediation === "string", + payload.flagClassification === null || typeof payload.flagClassification === "string", + payload.safeNoteDigest === null || typeof payload.safeNoteDigest === "string", + payload.redactionStatus === "redacted" || payload.redactionStatus === "withheld", + ["deterministic-engine", "confirmed-policy", "norm-derivation", "human-ruling-history", "solo-election", "agent-recommendation"].includes(String(payload.decisionSource)), + payload.remediation === null || ["self-fix", "self-feature", "self-fix-with-feature-alternative"].includes(String(payload.remediation)), + payload.flagClassification === null || ["contract-defect", "specification-change", "unspecified"].includes(String(payload.flagClassification)), + ].every(Boolean); + if (!valid) throw new Error("invalid-review-audit-payload"); + return payload as ReviewAuditPayload; +} + +function parsedReviewEvent(block: string): AutoDecisionReviewedEvent { + const payloadV1 = auditBlockField(block, "Payload V1"); + const payloadDigest = auditBlockField(block, "Payload Digest"); + if (payloadV1 === null || payloadDigest === null) throw new Error("invalid-review-audit-payload"); + const payload = parseReviewPayload(payloadV1, payloadDigest); + const revision = payload.receiptProjectionRevision; + const eventIdentity = autonomyReviewStableId("review-event", canonicalTupleDigest("amadeus.decision-review-event.v1", [ + { tag: "review", value: payload.reviewId }, + { tag: "command-occurrence", value: String(payload.commandOccurrenceId) }, + { tag: "target-audit-revision", value: String(revision - 1) }, + { tag: "review-payload-digest", value: payloadDigest }, + ])); + const remediation = payload.remediation as AutoDecisionReviewedEvent["remediation"]; + return { + eventType: "AUTO_DECISION_REVIEWED", + eventIdentity, + transactionId: payload.auditTransactionId, + payloadDigest, + payloadV1, + targetIntentUuid: payload.targetIntentUuid, + decisionId: payload.decisionId, + choice: payload.choice, + principalId: payload.reviewPrincipalRef as string, + actorId: payload.reviewActorRef as string, + decisionPrincipalId: payload.decisionPrincipalRef as string, + decisionActorId: payload.decisionActorRef as string, + decisionSource: payload.decisionSource as AutoDecisionReviewedEvent["decisionSource"], + safeBasisDigest: payload.safeBasisDigest as string, + grantId: payload.grantId as string | null, + sourceIntentUuid: payload.sourceIntentUuid as string, + sourceHumanTurnId: payload.sourceHumanTurnId as string, + sourceHumanTurnEventId: payload.sourceHumanTurnEventId as string, + commandOccurrenceId: payload.commandOccurrenceId as string, + commandBindingDigest: payload.commandBindingDigest as string, + lifecycleAtReview: payload.lifecycleAtReview, + remediation, + flagClassification: payload.flagClassification as AutoDecisionReviewedEvent["flagClassification"], + safeNoteDigest: payload.safeNoteDigest as string | null, + redactionStatus: payload.redactionStatus as "redacted" | "withheld", + projectionRevision: revision, + receipt: { + reviewId: payload.reviewId, + reviewEventId: eventIdentity, + auditTransactionId: payload.auditTransactionId, + committedEventIdentities: [eventIdentity], + stateProjectionRevision: revision, + state: payload.choice === "accept" ? "accepted" : "flagged", + remediation, + }, + }; +} + +function readStoredReviews(projectDir: string, target: ReviewTarget): readonly AutoDecisionReviewedEvent[] { + const rows = findAllEvents( + readAllAuditShards(projectDir, target.dirName, target.space), + "AUTO_DECISION_REVIEWED", + ); + // Identical event identities dedupe across ALL journals so a row replayed + // into a second shard stays one event and cannot inflate the extension chain. + const seen = new Set(); + const own: AutoDecisionReviewedEvent[] = []; + for (const row of rows) { + const event = parsedReviewEvent(row.block); + if (seen.has(event.eventIdentity)) continue; + seen.add(event.eventIdentity); + own.push(event); + } + if (target.lifecycle === "active") return own; + // A completed target's journal is sealed, so post-seal review rows live on + // whichever ACTIVE intent recorded them (OBS-R08). Union every sibling + // intent's shards and re-attribute by the payload's target uuid. A sibling + // row that fails to parse says nothing about THIS target unless it names the + // target's uuid — only that case stays fail-closed; other siblings' garbage + // must not take this target's review surface down. + const extension: AutoDecisionReviewedEvent[] = []; + for (const sibling of listIntents(projectDir, target.space)) { + if (sibling.dirName === null || sibling.dirName === target.dirName) continue; + for (const row of findAllEvents( + readAllAuditShards(projectDir, sibling.dirName, target.space), + "AUTO_DECISION_REVIEWED", + )) { + let event: AutoDecisionReviewedEvent; + try { + event = parsedReviewEvent(row.block); + } catch (cause) { + if (row.block.includes(target.intentUuid)) throw cause; + continue; + } + if (event.targetIntentUuid !== target.intentUuid || seen.has(event.eventIdentity)) continue; + seen.add(event.eventIdentity); + extension.push(event); + } + } + return [...own, ...extension]; +} + +function reviewExtension( + target: ReviewTarget, + seal: string | null, + reviews: readonly AutoDecisionReviewedEvent[], +): { readonly head: string | null; readonly revision: number } { + if (target.lifecycle === "active") return { head: null, revision: 0 }; + let head: string | null = null; + let revision = 0; + for (const event of reviews) { + revision += 1; + head = nextReviewExtensionHead({ + completionSealDigest: seal, + previousExtensionHead: head, + eventIdentity: event.eventIdentity, + payloadDigest: event.payloadDigest, + transactionId: event.transactionId, + revision, + }); + } + return { head, revision }; +} + +function refreshedSnapshot( + projectDir: string, + target: ReviewTarget, + source?: ReviewTarget, + turn?: HumanReviewTurnSeed, +): AutonomyReviewPersistenceSnapshot { + const targetSeed = currentSeed(projectDir, target); + const reviews = readStoredReviews(projectDir, target); + const extension = reviewExtension(target, targetSeed.completionSealDigest, reviews); + const targetState = { + ...targetSeed, + reviews, + reviewExtensionHead: extension.head, + reviewExtensionRevision: extension.revision, + }; + const sourceStates = source === undefined || source.intentUuid === target.intentUuid + ? [] + : [{ ...currentSeed(projectDir, source), reviews: [], reviewExtensionHead: null, reviewExtensionRevision: 0 }]; + const nextValue = { + schemaVersion: 1 as const, + intents: [targetState, ...sourceStates], + humanTurns: turn === undefined ? [] : [turn], + }; + const digest = canonicalContractValueDigest("autonomy-review-persistence", nextValue); + if (!digest.ok) throw new Error(digest.error.detail); + return { value: nextValue, digest: digest.value }; +} + +// The ONE production redactor: read projections and the commit path must +// disclose identically, so both service constructions share this instance. +const productionRedactor = { + redact: (_kind: string, value: string): { value: string; status: "redacted" } => ({ value: value.normalize("NFC"), status: "redacted" }), +}; + +function reviewService(projectDir: string, target: ReviewTarget) { + return createMemoryAutonomyReviewService({ + snapshot: refreshedSnapshot(projectDir, target), + redactor: productionRedactor, + }); +} + +interface ListProductionAutoDecisionsInput { + readonly projectDir: string; + readonly intent?: string; + readonly reviewState?: "not-applicable" | "unreviewed" | "accepted" | "flagged"; + readonly pageSize?: number; +} + +export function listProductionAutoDecisions(input: ListProductionAutoDecisionsInput): { readonly ok: true; readonly page: DecisionPage } | { readonly ok: false; readonly error: string } { + try { + const target = resolveReviewTarget(input.projectDir, input.intent); + if (target === null) return { ok: false, error: "review-target-not-found" }; + const result = reviewService(input.projectDir, target).listAutoDecisions({ + intentUuid: target.intentUuid, + lifecycle: target.lifecycle, + reviewState: input.reviewState, + pageSize: input.pageSize ?? 100, + }); + return result.ok ? { ok: true, page: result.value } : { ok: false, error: `${result.error.code}:${result.error.locus}` }; + } catch (cause) { + return { ok: false, error: cause instanceof Error ? cause.message : String(cause) }; + } +} + +interface GetProductionAutoDecisionInput { + readonly projectDir: string; + readonly intent?: string; + readonly decisionId: string; +} + +export function getProductionAutoDecision(input: GetProductionAutoDecisionInput): { readonly ok: true; readonly detail: DecisionDetail } | { readonly ok: false; readonly error: string } { + try { + const target = resolveReviewTarget(input.projectDir, input.intent); + if (target === null) return { ok: false, error: "review-target-not-found" }; + const result = reviewService(input.projectDir, target).getAutoDecision(target.intentUuid, input.decisionId); + return result.ok ? { ok: true, detail: result.value } : { ok: false, error: `${result.error.code}:${result.error.locus}` }; + } catch (cause) { + return { ok: false, error: cause instanceof Error ? cause.message : String(cause) }; + } +} + +type ProductionDecisionReviewInput = { + readonly projectDir: string; + readonly intent?: string; + readonly decisionId: string; + readonly choice: ReviewChoice; + readonly flagClassification?: HumanReviewCommandBinding["flagClassification"]; + readonly note?: string; + readonly confirmedContentDigest: string; +}; + +type ProductionDecisionReviewResult = { readonly ok: true; readonly receipt: DecisionReviewReceipt } | + { readonly ok: false; readonly error: string }; + +function commitDecisionReviewLocked( + input: ProductionDecisionReviewInput, + target: ReviewTarget, + source: ReviewTarget, +): ProductionDecisionReviewResult { + const sourceAudit = readAllAuditShards(input.projectDir, source.dirName, source.space); + const turns = findAllEvents(sourceAudit, "HUMAN_TURN"); + // Timestamps are second-precision, so a turn's identity is its timestamp + // PLUS its ordinal among same-timestamp turns in the chronologically sorted + // journal — appends only ever add later (or same-second, higher-position) + // rows, so an already-recorded identity never re-points at a different turn. + const turnKeys: string[] = []; + const sameTimestampSeen = new Map(); + for (const turn of turns) { + const ordinal = sameTimestampSeen.get(turn.timestamp) ?? 0; + sameTimestampSeen.set(turn.timestamp, ordinal + 1); + turnKeys.push(`${turn.timestamp}#${ordinal}`); + } + const latestPriorReview = readStoredReviews(input.projectDir, target) + .filter((event) => event.sourceIntentUuid === source.intentUuid) + .at(-1); + const consumedTurnIndex = latestPriorReview === undefined + ? -1 + : turnKeys.lastIndexOf(latestPriorReview.sourceHumanTurnEventId); + if (latestPriorReview !== undefined && consumedTurnIndex < 0) { + return { ok: false, error: "PROVENANCE_REQUIRED" }; + } + const latestTurnIndex = turns.length - 1; + if (latestTurnIndex <= consumedTurnIndex) return { ok: false, error: "PROVENANCE_REQUIRED" }; + const latestTurnKey = turnKeys[latestTurnIndex]; + // OBS-R09/OBS-R12: the commit recomputes the digest the preview displayed + // and refuses a mismatch, and the fresh human turn must postdate the last + // consumed one. This is the same trust geometry as set-autonomy's + // confirmed-display-digest: the turn precedes the commit and the presented + // digest matches the previewed content. It does NOT cryptographically bind + // the turn to a rendered display; that stronger attestation needs the turn + // itself to carry the displayed digest (a HUMAN_TURN vocabulary change). + const flagMetadata = normalizeReviewFlagMetadata({ + choice: input.choice, + flagClassification: input.flagClassification, + noteDigest: input.note !== undefined ? autonomyDigest(input.note) : null, + }); + const expectedContentDigest = reviewCommandContentDigest({ + targetIntentUuid: target.intentUuid, + decisionId: input.decisionId, + choice: input.choice, + flagClassification: flagMetadata.flagClassification, + safeNoteDigest: flagMetadata.safeNoteDigest, + }); + if (input.confirmedContentDigest !== expectedContentDigest) { + return { ok: false, error: "PROVENANCE_REQUIRED:confirmed-content-digest-mismatch" }; + } + const binding = bindHumanReviewCommand({ + sourceIntentUuid: source.intentUuid, + targetIntentUuid: target.intentUuid, + decisionId: input.decisionId, + choice: input.choice, + commandOccurrenceId: `review-${input.choice}-${input.decisionId}-${latestTurnKey}`, + flagClassification: flagMetadata.flagClassification, + safeNoteDigest: flagMetadata.safeNoteDigest, + sourceHumanTurnId: latestTurnKey, + }); + const turn: HumanReviewTurnSeed = { + sourceIntentUuid: source.intentUuid, + lifecycle: "active", + sourceAuditRevision: splitAuditRecords(sourceAudit).length, + sourceHumanTurnId: latestTurnKey, + sourceHumanTurnEventId: latestTurnKey, + principalId: "local-human", + binding, + }; + const service = createMemoryAutonomyReviewService({ + snapshot: refreshedSnapshot(input.projectDir, target, source, turn), + redactor: productionRedactor, + }); + const targetState = service.readIntent(target.intentUuid); + if (targetState === null) return { ok: false, error: "review-target-not-found" }; + const authorization = service.authorizeHumanReview({ + command: binding.command, + sourceHumanTurnId: turn.sourceHumanTurnId, + sourceHumanTurnEventId: turn.sourceHumanTurnEventId, + }); + if (!authorization.ok) return { ok: false, error: `${authorization.error.code}:${authorization.error.locus}` }; + const reviewed = service.appendDecisionReview({ + targetIntentUuid: target.intentUuid, + decisionId: input.decisionId, + choice: input.choice, + expectedTargetAuditRevision: targetState.auditRevision, + expectedCompletionSealDigest: targetState.completionSealDigest, + humanAuthorization: authorization.value, + }); + if (!reviewed.ok) return { ok: false, error: `${reviewed.error.code}:${reviewed.error.locus}` }; + const event = service.readReviewEvents(target.intentUuid).at(-1); + if (event === undefined) return { ok: false, error: "review-event-not-produced" }; + // OBS-R08: a completed target's journal is sealed (#1248) — post-seal review + // rows land on the ACTIVE SOURCE journal instead. The payload carries the + // target intent uuid, so readStoredReviews re-attributes them on read. + const ledger = target.lifecycle === "completed" ? source : target; + emitAuditEventGuarded( + "AUTO_DECISION_REVIEWED", + { ...reviewAuditFields(event) }, + input.projectDir, + ledger.dirName, + ledger.space, + ); + return { ok: true, receipt: reviewed.value }; +} + +export function commitProductionDecisionReview( + input: ProductionDecisionReviewInput, +): ProductionDecisionReviewResult { + try { + const target = resolveReviewTarget(input.projectDir, input.intent); + const source = resolveReviewTarget(input.projectDir); + if (target === null || source === null || source.lifecycle !== "active") { + return { ok: false, error: "active-source-and-review-target-required" }; + } + // The commit reads the SOURCE journal (turn consumption) and writes to the + // source (completed target) or the target (active target), so BOTH ledgers + // are locked. dirName order is the deterministic acquisition order — two + // concurrent commits over any pair of intents cannot deadlock, and two + // completed-target reviews serialise on the shared source lock. + const commit = (): ProductionDecisionReviewResult => commitDecisionReviewLocked(input, target, source); + if (target.dirName === source.dirName) { + return withAuditLock(input.projectDir, commit, target.dirName, target.space); + } + const [outer, inner] = [target, source].sort((left, right) => left.dirName.localeCompare(right.dirName)); + return withAuditLock( + input.projectDir, + () => withAuditLock(input.projectDir, commit, inner.dirName, inner.space), + outer.dirName, + outer.space, + ); + } catch (cause) { + return { ok: false, error: cause instanceof Error ? cause.message : String(cause) }; + } +} diff --git a/packages/framework/core/tools/amadeus-autonomy-review.ts b/packages/framework/core/tools/amadeus-autonomy-review.ts new file mode 100644 index 0000000000..47d97a970f --- /dev/null +++ b/packages/framework/core/tools/amadeus-autonomy-review.ts @@ -0,0 +1,1273 @@ +// Harness-neutral review and observability for immutable Intent autonomy decisions (#2067). +// +// Reviews are an append-only projection. They never replay a selected effect, +// mutate a grant, reopen an Intent, or create a remediation Intent. + +import { createHash } from "node:crypto"; + +import { + assertLegalAutonomyProjection, + type AutoDecisionRecord, + type AutonomyProjection, + type IntentGrantState, + type ResumeCondition, + type WorkflowResult, +} from "./amadeus-intent-autonomy.ts"; +import { + SELF_INSTALL_HARNESS_IDS, + type SelfInstallHarnessId, +} from "./amadeus-harness-registry.ts"; + +export const AUTO_DECISION_REVIEWED_EVENT = "AUTO_DECISION_REVIEWED"; + +export type ContractErrorCode = "MALFORMED" | "CONFLICT" | "PROVENANCE_REQUIRED" | "ILLEGAL_STATE"; +export interface ContractError { + readonly code: ContractErrorCode; + readonly locus: string; + readonly detail: string; +} +export type ContractResult = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: ContractError }; +export type IntentLifecycle = "active" | "completed"; +export type DecisionReviewState = "not-applicable" | "unreviewed" | "accepted" | "flagged"; +export type ReviewChoice = "accept" | "flag"; +export type RedactionStatus = "redacted" | "withheld"; +export type RemediationKind = "self-fix" | "self-feature" | "self-fix-with-feature-alternative"; + +function success(value: T): ContractResult { + return { ok: true, value }; +} + +function failure(code: ContractErrorCode, locus: string, detail: string): ContractResult { + return { ok: false, error: { code, locus, detail } }; +} + +function compareUtf8(left: string, right: string): number { + return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8")); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function sha256(value: Uint8Array): string { + return `sha256:${createHash("sha256").update(value).digest("hex")}`; +} + +function u32(value: number): Buffer { + const bytes = Buffer.alloc(4); + bytes.writeUInt32BE(value); + return bytes; +} + +function u64(value: number): Buffer { + const bytes = Buffer.alloc(8); + bytes.writeBigUInt64BE(BigInt(value)); + return bytes; +} + +export interface CanonicalTupleAtom { + readonly tag: string; + readonly value: string | null; +} + +export function encodeCanonicalTuple(domain: string, atoms: readonly CanonicalTupleAtom[]): Uint8Array { + const domainBytes = Buffer.from(domain, "utf8"); + const chunks: Buffer[] = [u32(domainBytes.length), domainBytes, u32(atoms.length)]; + for (const atom of atoms) { + const tag = Buffer.from(atom.tag, "utf8"); + chunks.push(u32(tag.length), tag); + if (atom.value === null) { + chunks.push(Buffer.from([0])); + continue; + } + const value = Buffer.from(atom.value, "utf8"); + chunks.push(Buffer.from([1]), u64(value.length), value); + } + return Buffer.concat(chunks); +} + +export function canonicalTupleDigest(domain: string, atoms: readonly CanonicalTupleAtom[]): string { + return sha256(encodeCanonicalTuple(domain, atoms)); +} + +export function autonomyReviewStableId(prefix: string, digest: string): string { + return `${prefix}-${digest.slice("sha256:".length, "sha256:".length + 32)}`; +} + +interface NextReviewExtensionHeadInput { + readonly completionSealDigest: string | null; + readonly previousExtensionHead: string | null; + readonly eventIdentity: string; + readonly payloadDigest: string; + readonly transactionId: string; + readonly revision: number; +} + +export function nextReviewExtensionHead(input: NextReviewExtensionHeadInput): string { + return autonomyReviewStableId("review-extension", canonicalTupleDigest("amadeus.review-extension.v1", [ + { tag: "completion-seal", value: input.completionSealDigest }, + { tag: "previous-extension", value: input.previousExtensionHead }, + { tag: "review-event", value: input.eventIdentity }, + { tag: "review-payload-digest", value: input.payloadDigest }, + { tag: "transaction", value: input.transactionId }, + { tag: "extension-revision", value: String(input.revision) }, + ])); +} + +function encodeCanonicalValue(value: unknown): Buffer { + if (value === null) return Buffer.from([0]); + if (typeof value === "boolean") return Buffer.from([1, value ? 1 : 0]); + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) throw new Error("canonical-value-integer-out-of-range"); + return Buffer.concat([Buffer.from([2]), u64(value)]); + } + if (typeof value === "string") { + const bytes = Buffer.from(value.normalize("NFC"), "utf8"); + return Buffer.concat([Buffer.from([3]), u64(bytes.length), bytes]); + } + if (Array.isArray(value)) { + return Buffer.concat([Buffer.from([4]), u32(value.length), ...value.map(encodeCanonicalValue)]); + } + if (!isRecord(value)) throw new Error("unsupported-canonical-value"); + const entries = Object.entries(value).sort(([left], [right]) => compareUtf8(left, right)); + const chunks: Buffer[] = [Buffer.from([5]), u32(entries.length)]; + for (const [field, fieldValue] of entries) { + if (fieldValue === undefined) throw new Error("undefined-canonical-value-field"); + const tag = Buffer.from(field, "utf8"); + chunks.push(u32(tag.length), tag, encodeCanonicalValue(fieldValue)); + } + return Buffer.concat(chunks); +} + +export function canonicalContractValueDigest(schemaId: string, value: unknown): ContractResult { + try { + const domain = Buffer.from(`amadeus.contract-value.${schemaId}.v1`, "utf8"); + const encoded = encodeCanonicalValue(value); + return success(sha256(Buffer.concat([u32(domain.length), domain, u64(encoded.length), encoded]))); + } catch (error) { + return failure("MALFORMED", "canonicalValue", error instanceof Error ? error.message : String(error)); + } +} + +export interface SafeSubjectReference { + readonly subjectRef: string | null; + readonly redactionStatus: RedactionStatus; +} +export interface SafeDecisionOption { + readonly optionId: string; + readonly safeLabel: string | null; + readonly labelDigest: string | null; + readonly redactionStatus: RedactionStatus; +} +export interface SafeEvidenceReference { + readonly evidenceFingerprint: string | null; + readonly safeKind: string; + readonly redactionStatus: RedactionStatus; +} +export interface DecisionReviewReceipt { + readonly reviewId: string; + readonly reviewEventId: string; + readonly auditTransactionId: string; + readonly committedEventIdentities: readonly [string]; + readonly stateProjectionRevision: number; + readonly state: "accepted" | "flagged"; + readonly remediation: RemediationKind | null; +} +export interface DecisionSummary { + readonly intentUuid: string; + readonly decisionId: string; + readonly questionId: string; + readonly occurrenceId: string; + readonly safeQuestion: string | null; + readonly questionDigest: string | null; + readonly selectedOptionId: string; + readonly decisionSource: AutoDecisionRecord["decider"]; + readonly safeBasisDigest: string | null; + readonly decisionPrincipal: SafeSubjectReference; + readonly decisionActor: SafeSubjectReference; + readonly grantId: string | null; + readonly evidenceFingerprint: string | null; + readonly degradedCapability: string | null; + readonly reviewState: DecisionReviewState; + readonly redactionStatus: RedactionStatus; +} +export interface DecisionDetail extends DecisionSummary { + readonly options: readonly SafeDecisionOption[]; + readonly evidence: readonly SafeEvidenceReference[]; + readonly graphRevision: string; + readonly auditEventId: string; + readonly reviewReceipt: DecisionReviewReceipt | null; +} +export interface DecisionCursor { + readonly intentUuid: string; + readonly queryFingerprint: string; + readonly targetAuditRevision: number; + readonly reviewExtensionHead: string | null; + readonly projectionEventSetDigest: string; + readonly lastOccurrenceId: string; + readonly lastDecisionId: string; + readonly cursorDigest: string; +} +export interface DecisionQuery { + readonly intentUuid: string; + readonly lifecycle: IntentLifecycle | "either"; + readonly reviewState?: DecisionReviewState; + readonly pageSize: number; + readonly cursor?: DecisionCursor; +} +export interface DecisionPage { + readonly items: readonly DecisionSummary[]; + readonly nextCursor: DecisionCursor | null; + readonly queryFingerprint: string; +} + +export interface HumanReviewCommandBinding { + readonly sourceIntentUuid: string; + readonly targetIntentUuid: string; + readonly decisionId: string; + readonly choice: ReviewChoice; + readonly commandOccurrenceId: string; + readonly flagClassification: "contract-defect" | "specification-change" | "unspecified" | null; + readonly safeNoteDigest: string | null; +} +export interface BoundHumanReviewCommand { + readonly schemaVersion: "1"; + readonly sourceHumanTurnId: string; + readonly command: HumanReviewCommandBinding; + readonly commandBindingDigest: string; +} + +// The pre-turn confirmation digest (OBS-R09/OBS-R12): binds WHAT is being +// confirmed — target, decision, choice, and the flag metadata with explicit +// nulls — before any turn exists. The commit recomputes this digest and +// refuses a mismatch; combined with the fresh-turn requirement this gives the +// set-autonomy confirmed-display-digest trust geometry, not a cryptographic +// attestation that a human saw a rendered display. Occurrence and turn ids +// are deliberately excluded: they are minted at commit time and would make +// the digest unobtainable at preview time. +interface ReviewCommandContentDigestInput { + readonly targetIntentUuid: string; + readonly decisionId: string; + readonly choice: ReviewChoice; + readonly flagClassification: HumanReviewCommandBinding["flagClassification"]; + readonly safeNoteDigest: string | null; +} + +// The ONE normalization of flag metadata (explicit nulls for accept, the +// "unspecified" default for an unclassified flag, the note collapsed to its +// digest). Preview, expected-value recomputation, and command binding all call +// this, so the displayed digest and the verified digest cannot drift. +interface ReviewFlagMetadataInput { + readonly choice: ReviewChoice; + readonly flagClassification?: HumanReviewCommandBinding["flagClassification"] | undefined; + readonly noteDigest?: string | null; +} + +interface NormalizedReviewFlagMetadata { + readonly flagClassification: HumanReviewCommandBinding["flagClassification"]; + readonly safeNoteDigest: string | null; +} + +export function normalizeReviewFlagMetadata(input: ReviewFlagMetadataInput): NormalizedReviewFlagMetadata { + if (input.choice !== "flag") return { flagClassification: null, safeNoteDigest: null }; + return { + flagClassification: input.flagClassification ?? "unspecified", + safeNoteDigest: input.noteDigest ?? null, + }; +} + +export function reviewCommandContentDigest(input: ReviewCommandContentDigestInput): string { + return canonicalTupleDigest("amadeus.review-command-content.v1", [ + { tag: "target-intent", value: input.targetIntentUuid }, + { tag: "decision", value: input.decisionId }, + { tag: "choice", value: input.choice }, + { tag: "flag-classification", value: input.flagClassification }, + { tag: "safe-note-digest", value: input.safeNoteDigest }, + ]); +} + +export function bindHumanReviewCommand(input: HumanReviewCommandBinding & { readonly sourceHumanTurnId: string }): BoundHumanReviewCommand { + const command: HumanReviewCommandBinding = { + sourceIntentUuid: input.sourceIntentUuid, + targetIntentUuid: input.targetIntentUuid, + decisionId: input.decisionId, + choice: input.choice, + commandOccurrenceId: input.commandOccurrenceId, + flagClassification: input.flagClassification, + safeNoteDigest: input.safeNoteDigest, + }; + if (command.choice === "accept" && (command.flagClassification !== null || command.safeNoteDigest !== null)) { + throw new Error("accept-review-cannot-carry-flag-metadata"); + } + const commandBindingDigest = canonicalTupleDigest("amadeus.review-command-binding.v1", [ + { tag: "source-intent", value: command.sourceIntentUuid }, + { tag: "target-intent", value: command.targetIntentUuid }, + { tag: "decision", value: command.decisionId }, + { tag: "choice", value: command.choice }, + { tag: "command-occurrence", value: command.commandOccurrenceId }, + { tag: "flag-classification", value: command.flagClassification }, + { tag: "safe-note-digest", value: command.safeNoteDigest }, + { tag: "human-turn", value: input.sourceHumanTurnId }, + ]); + return { schemaVersion: "1", sourceHumanTurnId: input.sourceHumanTurnId, command, commandBindingDigest }; +} + +export interface HumanReviewAuthorizationInput { + readonly command: HumanReviewCommandBinding; + readonly sourceHumanTurnId: string; + readonly sourceHumanTurnEventId: string; +} +export interface HumanReviewAuthorizationReceipt extends HumanReviewCommandBinding { + readonly sourceHumanTurnId: string; + readonly sourceHumanTurnEventId: string; + readonly sourceAuditRevision: number; + readonly principalId: string; + readonly reviewActorId: string; + readonly commandBindingDigest: string; +} +export interface DecisionReviewCommand { + readonly targetIntentUuid: string; + readonly decisionId: string; + readonly choice: ReviewChoice; + readonly expectedTargetAuditRevision: number; + readonly expectedCompletionSealDigest: string | null; + readonly humanAuthorization: HumanReviewAuthorizationReceipt; +} + +export interface ReviewIntentSeed { + readonly intentUuid: string; + readonly lifecycle: IntentLifecycle; + readonly autonomy: AutonomyProjection; + readonly auditRevision: number; + readonly completionSealDigest: string | null; +} +export interface HumanReviewTurnSeed { + readonly sourceIntentUuid: string; + readonly lifecycle: "active"; + readonly sourceAuditRevision: number; + readonly sourceHumanTurnId: string; + readonly sourceHumanTurnEventId: string; + readonly principalId: string; + readonly binding: BoundHumanReviewCommand; +} +export interface SafeDecisionRedactor { + redact(kind: "question", value: string): { readonly value: string; readonly status: "redacted" } | { readonly value: null; readonly status: "withheld" }; +} + +export interface AutoDecisionReviewedEvent { + readonly eventType: typeof AUTO_DECISION_REVIEWED_EVENT; + readonly eventIdentity: string; + readonly transactionId: string; + readonly payloadDigest: string; + readonly payloadV1: string; + readonly targetIntentUuid: string; + readonly decisionId: string; + readonly choice: ReviewChoice; + readonly principalId: string; + readonly actorId: string; + readonly decisionPrincipalId: string; + readonly decisionActorId: string; + readonly decisionSource: AutoDecisionRecord["decider"]; + readonly safeBasisDigest: string; + readonly grantId: string | null; + readonly sourceIntentUuid: string; + readonly sourceHumanTurnId: string; + readonly sourceHumanTurnEventId: string; + readonly commandOccurrenceId: string; + readonly commandBindingDigest: string; + readonly lifecycleAtReview: IntentLifecycle; + readonly remediation: RemediationKind | null; + readonly flagClassification: HumanReviewCommandBinding["flagClassification"]; + readonly safeNoteDigest: string | null; + readonly redactionStatus: RedactionStatus; + readonly projectionRevision: number; + readonly receipt: DecisionReviewReceipt; +} + +interface IntentState extends ReviewIntentSeed { + readonly reviews: readonly AutoDecisionReviewedEvent[]; + readonly reviewExtensionHead: string | null; + readonly reviewExtensionRevision: number; +} + +export interface ReadIntentObservation extends ReviewIntentSeed { + readonly reviewExtensionHead: string | null; + readonly reviewExtensionRevision: number; +} + +export interface AutonomyReviewPersistenceState extends ReviewIntentSeed { + readonly reviews: readonly AutoDecisionReviewedEvent[]; + readonly reviewExtensionHead: string | null; + readonly reviewExtensionRevision: number; +} +export interface AutonomyReviewPersistenceValue { + readonly schemaVersion: 1; + readonly intents: readonly AutonomyReviewPersistenceState[]; + readonly humanTurns: readonly HumanReviewTurnSeed[]; +} +export interface AutonomyReviewPersistenceSnapshot { + readonly value: AutonomyReviewPersistenceValue; + readonly digest: string; +} + +function validatePersistedReviewEvent( + persisted: AutonomyReviewPersistenceState, + event: AutoDecisionReviewedEvent, + expectedProjectionRevision: number, +): void { + let payload: unknown; + try { + payload = JSON.parse(event.payloadV1); + } catch { + throw new Error("invalid-autonomy-review-persistence-payload"); + } + const payloadDigest = canonicalContractValueDigest("auto-decision-reviewed-payload", payload); + if (!payloadDigest.ok || payloadDigest.value !== event.payloadDigest || !isRecord(payload)) { + throw new Error("invalid-autonomy-review-persistence-payload"); + } + const fieldsMatch = [ + payload.targetIntentUuid === persisted.intentUuid, + payload.decisionId === event.decisionId, + payload.choice === event.choice, + payload.auditTransactionId === event.transactionId, + payload.receiptProjectionRevision === expectedProjectionRevision, + event.projectionRevision === expectedProjectionRevision, + payload.remediation === event.remediation, + payload.flagClassification === event.flagClassification, + payload.grantId === event.grantId, + payload.lifecycleAtReview === event.lifecycleAtReview, + payload.reviewPrincipalRef === event.principalId, + payload.reviewActorRef === event.actorId, + payload.safeNoteDigest === event.safeNoteDigest, + payload.commandBindingDigest === event.commandBindingDigest, + ].every(Boolean); + if (!fieldsMatch) { + throw new Error("invalid-autonomy-review-persistence-payload"); + } + const expectedIdentity = autonomyReviewStableId("review-event", canonicalTupleDigest("amadeus.decision-review-event.v1", [ + { tag: "review", value: event.receipt.reviewId }, + { tag: "command-occurrence", value: event.commandOccurrenceId }, + { tag: "target-audit-revision", value: String(expectedProjectionRevision - 1) }, + { tag: "review-payload-digest", value: event.payloadDigest }, + ])); + if (event.eventIdentity !== expectedIdentity || event.receipt.reviewEventId !== expectedIdentity || + event.receipt.auditTransactionId !== event.transactionId || + event.receipt.stateProjectionRevision !== expectedProjectionRevision) { + throw new Error("invalid-autonomy-review-persistence-identity"); + } +} + +// A sealed (#1248) completed target cannot grow its own journal, so post-seal +// reviews land on a SOURCE journal and legitimately carry a projection revision +// past the target's audit revision — that overflow is exactly what the review +// extension chain fingerprints. Active targets keep the strict in-journal bound. +function withinPersistedRevisionBound(persisted: AutonomyReviewPersistenceState, revision: number): boolean { + if (persisted.lifecycle === "completed" && persisted.completionSealDigest !== null) return true; + return revision <= persisted.auditRevision; +} + +function validatePersistedReviewState(persisted: AutonomyReviewPersistenceState): void { + const decisionIds = new Set(persisted.autonomy.autoDecisions.map((decision) => decision.decisionId)); + const reviewed = new Set(); + let priorReviewRevision = 0; + let extensionHead: string | null = null; + let extensionRevision = 0; + for (const event of persisted.reviews) { + if (event.eventType !== AUTO_DECISION_REVIEWED_EVENT || event.targetIntentUuid !== persisted.intentUuid || + !decisionIds.has(event.decisionId) || reviewed.has(event.decisionId) || + event.projectionRevision <= priorReviewRevision || !withinPersistedRevisionBound(persisted, event.projectionRevision)) { + throw new Error("invalid-autonomy-review-persistence-events"); + } + reviewed.add(event.decisionId); + priorReviewRevision = event.projectionRevision; + validatePersistedReviewEvent(persisted, event, event.projectionRevision); + if (persisted.lifecycle !== "completed") continue; + extensionRevision += 1; + extensionHead = nextReviewExtensionHead({ + completionSealDigest: persisted.completionSealDigest, + previousExtensionHead: extensionHead, + eventIdentity: event.eventIdentity, + payloadDigest: event.payloadDigest, + transactionId: event.transactionId, + revision: extensionRevision, + }); + } + if (persisted.lifecycle === "active") { + if (persisted.reviewExtensionHead !== null || persisted.reviewExtensionRevision !== 0) { + throw new Error("invalid-autonomy-review-extension-snapshot"); + } + return; + } + if (persisted.reviewExtensionHead !== extensionHead || persisted.reviewExtensionRevision !== extensionRevision) { + throw new Error("invalid-autonomy-review-extension-snapshot"); + } +} + +function reviewEligible(decision: AutoDecisionRecord): boolean { + return decision.reviewState === "unreviewed" && + (decision.decider === "solo-election" || decision.decider === "agent-recommendation"); +} + +function reviewState(state: IntentState, decision: AutoDecisionRecord): DecisionReviewState { + const terminal = state.reviews.find((event) => event.decisionId === decision.decisionId); + if (terminal !== undefined) return terminal.choice === "accept" ? "accepted" : "flagged"; + return reviewEligible(decision) ? "unreviewed" : "not-applicable"; +} + +function queryFingerprint(query: DecisionQuery): string { + return canonicalTupleDigest("amadeus.decision-query.v1", [ + { tag: "target-intent", value: query.intentUuid }, + { tag: "lifecycle", value: query.lifecycle }, + { tag: "review-state", value: query.reviewState ?? null }, + { tag: "page-size", value: String(query.pageSize) }, + ]); +} + +// The set contract sorts entries by the CLOSED event-type order (AUTO_DECIDED, +// then AUTO_DECISION_REVIEWED), then canonical event id — never by entry-digest +// bytes — dedupes identical (id, digest) pairs, and closes a same-id +// different-digest pair as a CONFLICT rather than encoding both. +function eventSetDigest(state: IntentState): ContractResult { + const decisions: Array<{ typeOrder: number; eventId: string; digest: string }> = []; + for (const decision of state.autonomy.autoDecisions) { + const payloadDigest = canonicalContractValueDigest("auto-decision", decision); + if (!payloadDigest.ok) { + return failure("MALFORMED", "projectionEventSet", `decision ${decision.decisionId} payload digest failed: ${payloadDigest.error.detail}`); + } + decisions.push({ typeOrder: 0, eventId: decision.decisionId, digest: canonicalTupleDigest("amadeus.decision-projection-event.v1", [ + { tag: "event-type", value: "AUTO_DECIDED" }, + { tag: "event-id", value: decision.decisionId }, + { tag: "decision-payload-digest", value: payloadDigest.value }, + { tag: "subject-payload-digest", value: canonicalTupleDigest("amadeus.decision-subject.v1", [ + { tag: "principal", value: decision.principalId }, + { tag: "actor", value: decision.actorId }, + ]) }, + { tag: "review-payload-digest", value: null }, + ]) }); + } + const reviews = state.reviews.map((review) => ({ typeOrder: 1, eventId: review.eventIdentity, digest: canonicalTupleDigest("amadeus.decision-projection-event.v1", [ + { tag: "event-type", value: AUTO_DECISION_REVIEWED_EVENT }, + { tag: "event-id", value: review.eventIdentity }, + { tag: "decision-payload-digest", value: null }, + { tag: "subject-payload-digest", value: null }, + { tag: "review-payload-digest", value: review.payloadDigest }, + ]) })); + const ordered = [...decisions, ...reviews].sort( + (left, right) => (left.typeOrder - right.typeOrder) || compareUtf8(left.eventId, right.eventId), + ); + const deduped: typeof ordered = []; + for (const entry of ordered) { + const previous = deduped.at(-1); + if (previous !== undefined && previous.typeOrder === entry.typeOrder && previous.eventId === entry.eventId) { + if (previous.digest !== entry.digest) { + return failure("CONFLICT", "projectionEventSet", `duplicate event id ${entry.eventId} with diverging entry digests`); + } + continue; + } + deduped.push(entry); + } + return success(canonicalTupleDigest("amadeus.decision-projection-event-set.v1", [ + { tag: "target-intent", value: state.intentUuid }, + { tag: "event-count", value: String(deduped.length) }, + ...deduped.map((entry) => ({ tag: "event", value: entry.digest })), + ])); +} + +function cursorDigest(cursor: Omit): string { + return canonicalTupleDigest("amadeus.decision-cursor.v1", [ + { tag: "query-fingerprint", value: cursor.queryFingerprint }, + { tag: "target-audit-revision", value: String(cursor.targetAuditRevision) }, + { tag: "review-extension-head", value: cursor.reviewExtensionHead }, + { tag: "projection-event-set-digest", value: cursor.projectionEventSetDigest }, + { tag: "last-occurrence", value: cursor.lastOccurrenceId }, + { tag: "last-decision", value: cursor.lastDecisionId }, + ]); +} + +function validateCursorSnapshot( + query: DecisionQuery, + state: IntentState, + fingerprint: string, + projectionDigest: string, +): ContractResult { + if (query.cursor === undefined) return success(null); + const { cursorDigest: suppliedDigest, ...unsigned } = query.cursor; + if (query.cursor.intentUuid !== query.intentUuid || query.cursor.queryFingerprint !== fingerprint || + cursorDigest(unsigned) !== suppliedDigest) { + return failure("MALFORMED", "cursor", "cursor does not match query"); + } + if (query.cursor.targetAuditRevision !== state.auditRevision || + query.cursor.reviewExtensionHead !== state.reviewExtensionHead || + query.cursor.projectionEventSetDigest !== projectionDigest) { + return failure("CONFLICT", "cursorSnapshot", "decision snapshot changed"); + } + return success(null); +} + +function pageStart(query: DecisionQuery, summaries: readonly DecisionSummary[]): ContractResult { + if (query.cursor === undefined) return success(0); + const index = summaries.findIndex((item) => + item.occurrenceId === query.cursor?.lastOccurrenceId && item.decisionId === query.cursor.lastDecisionId + ); + return index < 0 + ? failure("CONFLICT", "cursorSnapshot", "cursor item is absent from snapshot") + : success(index + 1); +} + +function remediationFor(receipt: HumanReviewAuthorizationReceipt): RemediationKind | null { + if (receipt.choice === "accept") return null; + if (receipt.flagClassification === "specification-change") return "self-feature"; + if (receipt.flagClassification === "contract-defect") return "self-fix"; + return "self-fix-with-feature-alternative"; +} + +interface PlannedDecisionReview { + readonly event: AutoDecisionReviewedEvent; + readonly receipt: DecisionReviewReceipt; + readonly reviewExtensionHead: string | null; + readonly reviewExtensionRevision: number; +} + +type ResolvedReviewTarget = + | { readonly kind: "ready"; readonly decision: AutoDecisionRecord } + | { readonly kind: "terminal"; readonly result: ContractResult }; + +function resolveReviewTarget(state: IntentState, input: DecisionReviewCommand): ResolvedReviewTarget { + const decision = state.autonomy.autoDecisions.find((candidate) => candidate.decisionId === input.decisionId); + if (decision === undefined) { + return { kind: "terminal", result: failure("CONFLICT", "decisionId", "decision not found in target intent") }; + } + const terminal = state.reviews.find((review) => review.decisionId === input.decisionId); + if (terminal !== undefined) { + const result = terminal.choice === input.choice + ? success(terminal.receipt) + : failure("CONFLICT", "reviewState", "decision already has a different terminal review"); + return { kind: "terminal", result }; + } + if (!reviewEligible(decision)) { + return { kind: "terminal", result: failure("CONFLICT", "reviewState", "decision is not review eligible") }; + } + return { kind: "ready", decision }; +} + +function planDecisionReview( + state: IntentState, + decision: AutoDecisionRecord, + input: DecisionReviewCommand, +): ContractResult { + const authorization = input.humanAuthorization; + const reviewId = autonomyReviewStableId("review", canonicalTupleDigest("amadeus.decision-review.v1", [ + { tag: "target-intent", value: state.intentUuid }, + { tag: "decision", value: decision.decisionId }, + { tag: "source-human-turn-event", value: authorization.sourceHumanTurnEventId }, + { tag: "choice", value: input.choice }, + ])); + const transactionId = autonomyReviewStableId("review-transaction", canonicalTupleDigest("amadeus.decision-review-transaction.v1", [ + { tag: "target-intent", value: state.intentUuid }, + { tag: "review", value: reviewId }, + { tag: "target-audit-revision", value: String(state.auditRevision) }, + { tag: "completion-seal", value: state.completionSealDigest }, + ])); + const remediation = remediationFor(authorization); + const projectionRevision = state.auditRevision + 1; + const payload = { + schemaVersion: "1", + targetIntentUuid: state.intentUuid, + decisionId: decision.decisionId, + reviewId, + choice: input.choice, + reviewPrincipalRef: authorization.principalId, + reviewActorRef: authorization.reviewActorId, + decisionPrincipalRef: decision.principalId, + decisionActorRef: decision.actorId, + decisionSource: decision.decider, + safeBasisDigest: decision.basisFingerprint, + grantId: decision.grantId, + sourceIntentUuid: authorization.sourceIntentUuid, + sourceHumanTurnId: authorization.sourceHumanTurnId, + sourceHumanTurnEventId: authorization.sourceHumanTurnEventId, + commandOccurrenceId: authorization.commandOccurrenceId, + commandBindingDigest: authorization.commandBindingDigest, + auditTransactionId: transactionId, + receiptProjectionRevision: projectionRevision, + lifecycleAtReview: state.lifecycle, + remediation, + flagClassification: authorization.flagClassification, + safeNoteDigest: authorization.safeNoteDigest, + redactionStatus: "redacted" as const, + }; + const payloadDigest = canonicalContractValueDigest("auto-decision-reviewed-payload", payload); + if (!payloadDigest.ok) return payloadDigest; + const eventIdentity = autonomyReviewStableId("review-event", canonicalTupleDigest("amadeus.decision-review-event.v1", [ + { tag: "review", value: reviewId }, + { tag: "command-occurrence", value: authorization.commandOccurrenceId }, + { tag: "target-audit-revision", value: String(state.auditRevision) }, + { tag: "review-payload-digest", value: payloadDigest.value }, + ])); + const receipt: DecisionReviewReceipt = { + reviewId, + reviewEventId: eventIdentity, + auditTransactionId: transactionId, + committedEventIdentities: [eventIdentity], + stateProjectionRevision: projectionRevision, + state: input.choice === "accept" ? "accepted" : "flagged", + remediation, + }; + const event: AutoDecisionReviewedEvent = { + eventType: "AUTO_DECISION_REVIEWED", + eventIdentity, + transactionId, + payloadDigest: payloadDigest.value, + payloadV1: JSON.stringify(payload), + targetIntentUuid: state.intentUuid, + decisionId: decision.decisionId, + choice: input.choice, + principalId: authorization.principalId, + actorId: authorization.reviewActorId, + decisionPrincipalId: decision.principalId, + decisionActorId: decision.actorId, + decisionSource: decision.decider, + safeBasisDigest: decision.basisFingerprint, + grantId: decision.grantId, + sourceIntentUuid: authorization.sourceIntentUuid, + sourceHumanTurnId: authorization.sourceHumanTurnId, + sourceHumanTurnEventId: authorization.sourceHumanTurnEventId, + commandOccurrenceId: authorization.commandOccurrenceId, + commandBindingDigest: authorization.commandBindingDigest, + lifecycleAtReview: state.lifecycle, + remediation, + flagClassification: authorization.flagClassification, + safeNoteDigest: authorization.safeNoteDigest, + redactionStatus: "redacted", + projectionRevision, + receipt, + }; + if (state.lifecycle === "active") { + return success({ event, receipt, reviewExtensionHead: null, reviewExtensionRevision: 0 }); + } + const reviewExtensionRevision = state.reviewExtensionRevision + 1; + const reviewExtensionHead = nextReviewExtensionHead({ + completionSealDigest: state.completionSealDigest, + previousExtensionHead: state.reviewExtensionHead, + eventIdentity, + payloadDigest: payloadDigest.value, + transactionId, + revision: reviewExtensionRevision, + }); + return success({ event, receipt, reviewExtensionHead, reviewExtensionRevision }); +} + +function sameCommand(left: HumanReviewCommandBinding, right: HumanReviewCommandBinding): boolean { + const leftDigest = canonicalContractValueDigest("review-command", left); + const rightDigest = canonicalContractValueDigest("review-command", right); + return leftDigest.ok && rightDigest.ok && leftDigest.value === rightDigest.value; +} + +export interface AutonomyReviewService { + readonly createdIntentCount: 0; + listAutoDecisions(query: DecisionQuery): ContractResult; + getAutoDecision(intentUuid: string, decisionId: string): ContractResult; + authorizeHumanReview(input: HumanReviewAuthorizationInput): ContractResult; + appendDecisionReview(input: DecisionReviewCommand): ContractResult; + readIntent(intentUuid: string): ReadIntentObservation | null; + readReviewEvents(intentUuid: string): readonly AutoDecisionReviewedEvent[]; + exportSnapshot(): AutonomyReviewPersistenceSnapshot; + replaceIntent(seed: ReviewIntentSeed): void; +} + +interface MemoryAutonomyReviewServiceOptions { + readonly intents?: readonly ReviewIntentSeed[]; + readonly humanTurns?: readonly HumanReviewTurnSeed[]; + readonly redactor?: SafeDecisionRedactor; + readonly snapshot?: AutonomyReviewPersistenceSnapshot; +} + +export function createMemoryAutonomyReviewService(options: MemoryAutonomyReviewServiceOptions = {}): AutonomyReviewService { + const intents = new Map(); + const humanTurns = new Map(); + const redactor = options.redactor ?? { redact: (): { value: null; status: "withheld" } => ({ value: null, status: "withheld" }) }; + + function put(seed: ReviewIntentSeed): void { + assertLegalAutonomyProjection(seed.autonomy); + if (seed.intentUuid !== seed.autonomy.intentUuid || !Number.isSafeInteger(seed.auditRevision) || seed.auditRevision < 0 || + (seed.lifecycle === "completed") !== (seed.completionSealDigest !== null)) { + throw new Error("invalid-review-intent-seed"); + } + const current = intents.get(seed.intentUuid); + // Retained reviews must still name decisions the REPLACEMENT projection + // carries — otherwise the runtime state would report a terminal review for + // a decision the snapshot validator (validatePersistedReviewState) rejects. + const decisionIds = new Set(seed.autonomy.autoDecisions.map((decision) => decision.decisionId)); + for (const review of current?.reviews ?? []) { + if (!decisionIds.has(review.decisionId)) throw new Error("invalid-review-intent-seed"); + } + intents.set(seed.intentUuid, { + ...seed, + reviews: current?.reviews ?? [], + reviewExtensionHead: current?.reviewExtensionHead ?? null, + reviewExtensionRevision: current?.reviewExtensionRevision ?? 0, + }); + } + + if (options.snapshot !== undefined) { + const observedDigest = canonicalContractValueDigest("autonomy-review-persistence", options.snapshot.value); + if (!observedDigest.ok || observedDigest.value !== options.snapshot.digest || options.snapshot.value.schemaVersion !== 1) { + throw new Error("invalid-autonomy-review-persistence-snapshot"); + } + for (const persisted of options.snapshot.value.intents) { + put(persisted); + validatePersistedReviewState(persisted); + intents.set(persisted.intentUuid, { ...persisted }); + } + for (const turn of options.snapshot.value.humanTurns) { + humanTurns.set(`${turn.sourceIntentUuid}\0${turn.sourceHumanTurnId}\0${turn.sourceHumanTurnEventId}`, turn); + } + } + for (const seed of options.intents ?? []) put(seed); + for (const turn of options.humanTurns ?? []) humanTurns.set(`${turn.sourceIntentUuid}\0${turn.sourceHumanTurnId}\0${turn.sourceHumanTurnEventId}`, turn); + + function summarize(state: IntentState, decision: AutoDecisionRecord): DecisionSummary { + const redactedQuestion = redactor.redact("question", decision.question); + const questionDigest = redactedQuestion.value === null ? null : canonicalTupleDigest("amadeus.redacted-value.v1", [ + { tag: "value-kind", value: "question" }, + { tag: "value", value: redactedQuestion.value }, + ]); + return { + intentUuid: state.intentUuid, + decisionId: decision.decisionId, + questionId: decision.occurrenceId, + occurrenceId: decision.occurrenceId, + safeQuestion: redactedQuestion.value, + questionDigest, + selectedOptionId: decision.selectedOptionId, + decisionSource: decision.decider, + safeBasisDigest: decision.basisFingerprint, + decisionPrincipal: { subjectRef: decision.principalId, redactionStatus: "redacted" }, + decisionActor: { subjectRef: decision.actorId, redactionStatus: "redacted" }, + grantId: decision.grantId, + evidenceFingerprint: decision.basisFingerprint, + degradedCapability: decision.degradedCapability?.capability ?? null, + reviewState: reviewState(state, decision), + redactionStatus: redactedQuestion.status, + }; + } + + function listAutoDecisions(query: DecisionQuery): ContractResult { + if (!Number.isSafeInteger(query.pageSize) || query.pageSize < 1 || query.pageSize > 100) { + return failure("MALFORMED", "pageSize", "page size must be between 1 and 100"); + } + const state = intents.get(query.intentUuid); + if (state === undefined) return failure("CONFLICT", "intentUuid", "target intent not found"); + if (query.lifecycle !== "either" && query.lifecycle !== state.lifecycle) { + return failure("CONFLICT", "lifecycle", "target lifecycle does not match query"); + } + const fingerprint = queryFingerprint(query); + const projectionSet = eventSetDigest(state); + if (!projectionSet.ok) return projectionSet; + const projectionDigest = projectionSet.value; + const validCursor = validateCursorSnapshot(query, state, fingerprint, projectionDigest); + if (!validCursor.ok) return validCursor; + const summaries = state.autonomy.autoDecisions.map((decision) => summarize(state, decision)) + .filter((item) => query.reviewState === undefined || item.reviewState === query.reviewState) + .sort((left, right) => compareUtf8(left.occurrenceId, right.occurrenceId) || compareUtf8(left.decisionId, right.decisionId)); + const resolvedStart = pageStart(query, summaries); + if (!resolvedStart.ok) return resolvedStart; + const start = resolvedStart.value; + const items = summaries.slice(start, start + query.pageSize); + const last = items.at(-1); + let nextCursor: DecisionCursor | null = null; + if (last !== undefined && start + items.length < summaries.length) { + const unsigned = { + intentUuid: query.intentUuid, + queryFingerprint: fingerprint, + targetAuditRevision: state.auditRevision, + reviewExtensionHead: state.reviewExtensionHead, + projectionEventSetDigest: projectionDigest, + lastOccurrenceId: last.occurrenceId, + lastDecisionId: last.decisionId, + }; + nextCursor = { ...unsigned, cursorDigest: cursorDigest(unsigned) }; + } + return success({ items, nextCursor, queryFingerprint: fingerprint }); + } + + function getAutoDecision(intentUuid: string, decisionId: string): ContractResult { + const state = intents.get(intentUuid); + const decision = state?.autonomy.autoDecisions.find((candidate) => candidate.decisionId === decisionId); + if (state === undefined || decision === undefined) { + return failure("CONFLICT", "decisionId", "decision not found in target intent"); + } + const summary = summarize(state, decision); + const terminal = state.reviews.find((review) => review.decisionId === decisionId); + return success({ + ...summary, + options: decision.optionIds.map((optionId) => ({ optionId, safeLabel: null, labelDigest: null, redactionStatus: "withheld" })), + evidence: [{ evidenceFingerprint: decision.basisFingerprint, safeKind: decision.basisKind, redactionStatus: "redacted" }], + graphRevision: state.autonomy.modeProvenance.kind === "human-command" + ? state.autonomy.modeProvenance.confirmedDisplayDigest + : canonicalTupleDigest("amadeus.legacy-graph-revision.v1", [{ tag: "decision", value: decision.decisionId }]), + auditEventId: decision.decisionId, + reviewReceipt: terminal?.receipt ?? null, + }); + } + + function authorizeHumanReview(input: HumanReviewAuthorizationInput): ContractResult { + const key = `${input.command.sourceIntentUuid}\0${input.sourceHumanTurnId}\0${input.sourceHumanTurnEventId}`; + const turn = humanTurns.get(key); + const sourceIntent = turn === undefined ? undefined : intents.get(turn.sourceIntentUuid); + if (turn === undefined || turn.lifecycle !== "active" || sourceIntent?.lifecycle !== "active" || + sourceIntent.auditRevision !== turn.sourceAuditRevision) { + return failure("PROVENANCE_REQUIRED", "sourceHumanTurnId", "real active human turn not found"); + } + const rebound = bindHumanReviewCommand({ ...input.command, sourceHumanTurnId: input.sourceHumanTurnId }); + if (!sameCommand(turn.binding.command, input.command) || turn.binding.commandBindingDigest !== rebound.commandBindingDigest || + turn.binding.sourceHumanTurnId !== input.sourceHumanTurnId) { + return failure("PROVENANCE_REQUIRED", "commandBindingDigest", "human turn command binding mismatch"); + } + return success({ + ...input.command, + sourceHumanTurnId: input.sourceHumanTurnId, + sourceHumanTurnEventId: input.sourceHumanTurnEventId, + sourceAuditRevision: turn.sourceAuditRevision, + principalId: turn.principalId, + reviewActorId: turn.principalId, + commandBindingDigest: turn.binding.commandBindingDigest, + }); + } + + function validateCanonicalReviewAuthorization( + state: IntentState, + input: DecisionReviewCommand, + ): ContractResult { + const authorization = input.humanAuthorization; + if (authorization.targetIntentUuid !== input.targetIntentUuid || authorization.decisionId !== input.decisionId || + authorization.choice !== input.choice) { + return failure("PROVENANCE_REQUIRED", "humanAuthorization", "authorization does not bind this review command"); + } + const sourceKey = `${authorization.sourceIntentUuid}\0${authorization.sourceHumanTurnId}\0${authorization.sourceHumanTurnEventId}`; + const source = humanTurns.get(sourceKey); + const sourceIntent = source === undefined ? undefined : intents.get(source.sourceIntentUuid); + if (source === undefined || source.lifecycle !== "active" || sourceIntent?.lifecycle !== "active" || + sourceIntent.auditRevision !== source.sourceAuditRevision || source.sourceAuditRevision !== authorization.sourceAuditRevision || + source.binding.commandBindingDigest !== authorization.commandBindingDigest || source.principalId !== authorization.principalId) { + return failure("PROVENANCE_REQUIRED", "sourceIntentUuid", "human authorization is no longer canonical"); + } + if (state.lifecycle === "active" && authorization.sourceIntentUuid !== state.intentUuid) { + return failure("PROVENANCE_REQUIRED", "sourceIntentUuid", "active target must use its own human turn"); + } + return success(null); + } + + function validateReviewExpectation(state: IntentState, input: DecisionReviewCommand): ContractResult { + if (input.expectedTargetAuditRevision !== state.auditRevision) { + return failure("CONFLICT", "targetAuditRevision", "target audit revision changed"); + } + if (state.lifecycle === "completed" && input.expectedCompletionSealDigest !== state.completionSealDigest) { + return failure("CONFLICT", "completionSealDigest", "completion seal changed"); + } + if (state.lifecycle === "active" && input.expectedCompletionSealDigest !== null) { + return failure("MALFORMED", "completionSealDigest", "active review cannot carry a completion seal"); + } + return success(null); + } + + function appendDecisionReview(input: DecisionReviewCommand): ContractResult { + const state = intents.get(input.targetIntentUuid); + if (state === undefined) return failure("CONFLICT", "targetIntentUuid", "target intent not found"); + const target = resolveReviewTarget(state, input); + if (target.kind === "terminal") return target.result; + const canonicalAuthorization = validateCanonicalReviewAuthorization(state, input); + if (!canonicalAuthorization.ok) return canonicalAuthorization; + const expectation = validateReviewExpectation(state, input); + if (!expectation.ok) return expectation; + const plan = planDecisionReview(state, target.decision, input); + if (!plan.ok) return plan; + intents.set(state.intentUuid, { + ...state, + auditRevision: plan.value.receipt.stateProjectionRevision, + reviews: [...state.reviews, plan.value.event], + reviewExtensionHead: plan.value.reviewExtensionHead, + reviewExtensionRevision: plan.value.reviewExtensionRevision, + }); + return success(plan.value.receipt); + } + + return { + createdIntentCount: 0, + listAutoDecisions, + getAutoDecision, + authorizeHumanReview, + appendDecisionReview, + readIntent(intentUuid) { + const state = intents.get(intentUuid); + if (state === undefined) return null; + return { + intentUuid: state.intentUuid, + lifecycle: state.lifecycle, + autonomy: state.autonomy, + auditRevision: state.auditRevision, + completionSealDigest: state.completionSealDigest, + reviewExtensionHead: state.reviewExtensionHead, + reviewExtensionRevision: state.reviewExtensionRevision, + }; + }, + readReviewEvents(intentUuid) { + return [...(intents.get(intentUuid)?.reviews ?? [])]; + }, + exportSnapshot() { + const value: AutonomyReviewPersistenceValue = { + schemaVersion: 1, + intents: [...intents.values()].sort((left, right) => compareUtf8(left.intentUuid, right.intentUuid)), + humanTurns: [...humanTurns.values()].sort((left, right) => + compareUtf8(left.sourceIntentUuid, right.sourceIntentUuid) || + compareUtf8(left.sourceHumanTurnEventId, right.sourceHumanTurnEventId) + ), + }; + const digest = canonicalContractValueDigest("autonomy-review-persistence", value); + if (!digest.ok) throw new Error(digest.error.detail); + return { value, digest: digest.value }; + }, + replaceIntent: put, + }; +} + +export function reviewAuditFields(event: AutoDecisionReviewedEvent): Readonly> { + const fields: Record = { + "Intent Uuid": event.targetIntentUuid, + "Decision Id": event.decisionId, + "Review Id": event.receipt.reviewId, + Choice: event.choice, + Lifecycle: event.lifecycleAtReview, + "Review Principal": event.principalId, + "Review Actor": event.actorId, + "Source Human Turn": event.sourceHumanTurnId, + "Audit Transaction Id": event.transactionId, + "Event Identity": event.eventIdentity, + "Projection Revision": String(event.projectionRevision), + "Payload Digest": event.payloadDigest, + "Payload V1": event.payloadV1, + }; + fields["Decision Principal"] = event.decisionPrincipalId; + fields["Decision Actor"] = event.decisionActorId; + fields["Decision Source"] = event.decisionSource; + fields["Basis Digest"] = event.safeBasisDigest; + fields["Redaction Status"] = event.redactionStatus; + if (event.grantId !== null) fields["Grant Id"] = event.grantId; + if (event.remediation !== null) fields.Remediation = event.remediation; + if (event.safeNoteDigest !== null) fields["Note Digest"] = event.safeNoteDigest; + return fields; +} + +export interface DecisionReviewCounts { + readonly total: number; + readonly unreviewed: number; + readonly accepted: number; + readonly flagged: number; +} +export interface SafeGrantScopeSummary { + readonly scopeFingerprint: string; + readonly selfScopeId: string; + readonly allowedInteractionKinds: readonly ("stage-gate" | "phase-gate" | "walking-skeleton" | "question")[]; +} +export interface SafeMigrationDiagnostic { + readonly status: "legacy-non-authoritative"; + readonly legacyGrantIds: readonly string[]; + readonly recommendedHumanAction: "select-none" | "select-semi" | "issue-full"; +} +export interface ReviewStatusInput { + readonly intentUuid: string; + readonly lifecycle: IntentLifecycle; + readonly autonomy: AutonomyProjection; + readonly workflowResult: WorkflowResult | null; + readonly currentGrantScope: SafeGrantScopeSummary | null; + readonly decisionPolicyCount: number; + readonly decisionCounts: DecisionReviewCounts; + readonly reviewExtensionHead: string | null; + readonly legacyDiagnostic: SafeMigrationDiagnostic | null; +} +export interface GrantStatusView { + readonly grantId: string; + readonly state: IntentGrantState; + readonly scope: SafeGrantScopeSummary; +} +export interface MachineReviewStatus { + readonly intentUuid: string; + readonly lifecycle: IntentLifecycle; + readonly autonomyMode: AutonomyProjection["mode"]; + readonly workflowExecutionState: AutonomyProjection["workflowExecutionState"]; + readonly grant: GrantStatusView | null; + readonly decisionPolicyCount: number; + readonly decisionCount: number; + readonly unreviewedDecisionCount: number; + readonly acceptedDecisionCount: number; + readonly flaggedDecisionCount: number; + readonly suspendedReason: WorkflowResult["reasonCode"]; + readonly stopReason: WorkflowResult["reasonCode"]; + readonly resumeCondition: ResumeCondition | null; + readonly legacyDiagnostic: SafeMigrationDiagnostic | null; +} + +function validateStatusCounts(input: ReviewStatusInput): ContractResult { + const counts = input.decisionCounts; + const values = [input.decisionPolicyCount, counts.total, counts.unreviewed, counts.accepted, counts.flagged]; + if (input.intentUuid !== input.autonomy.intentUuid || values.some((value) => !Number.isSafeInteger(value) || value < 0) || + counts.unreviewed + counts.accepted + counts.flagged > counts.total) { + return failure("MALFORMED", "statusInput", "status counts or intent identity are invalid"); + } + return success(null); +} + +function validateStatusGrant(input: ReviewStatusInput): ContractResult { + const grant = input.autonomy.currentGrant; + if (input.lifecycle === "completed") { + return grant === null && input.currentGrantScope === null && input.workflowResult?.outcome === "completed" + ? success(null) + : failure("ILLEGAL_STATE", "lifecycle", "completed status must keep workflow and grant terminal"); + } + if (input.autonomy.mode === "full") { + return grant !== null && input.currentGrantScope !== null && + grant.scope.scopeFingerprint === input.currentGrantScope.scopeFingerprint + ? success(null) + : failure("ILLEGAL_STATE", "grantScope", "full mode requires the canonical current grant scope"); + } + return grant === null && input.currentGrantScope === null + ? success(null) + : failure("ILLEGAL_STATE", "grantScope", "none and semi modes cannot project a current grant"); +} + +export function projectMachineReviewStatus(input: ReviewStatusInput): ContractResult { + try { + assertLegalAutonomyProjection(input.autonomy); + } catch (error) { + return failure("ILLEGAL_STATE", "autonomy", error instanceof Error ? error.message : String(error)); + } + const validCounts = validateStatusCounts(input); + if (!validCounts.ok) return validCounts; + const validGrant = validateStatusGrant(input); + if (!validGrant.ok) return validGrant; + const counts = input.decisionCounts; + const grant = input.autonomy.currentGrant; + const grantView = grant === null || input.currentGrantScope === null ? null : { + grantId: grant.grantId, + state: grant.state, + scope: input.currentGrantScope, + }; + return success({ + intentUuid: input.intentUuid, + lifecycle: input.lifecycle, + autonomyMode: input.autonomy.mode, + workflowExecutionState: input.autonomy.workflowExecutionState, + grant: grantView, + decisionPolicyCount: input.decisionPolicyCount, + decisionCount: counts.total, + unreviewedDecisionCount: counts.unreviewed, + acceptedDecisionCount: counts.accepted, + flaggedDecisionCount: counts.flagged, + suspendedReason: input.autonomy.parkEnvelope?.reason ?? null, + stopReason: input.workflowResult?.reasonCode ?? null, + resumeCondition: input.workflowResult?.resumeCondition ?? null, + legacyDiagnostic: input.legacyDiagnostic, + }); +} + +export function projectHumanReviewStatus(input: ReviewStatusInput): ContractResult { + const machine = projectMachineReviewStatus(input); + if (!machine.ok) return machine; + const status = machine.value; + return success([ + `Intent: ${status.intentUuid} (${status.lifecycle})`, + `自律レベル: ${status.autonomyMode}`, + `ワークフロー: ${status.workflowExecutionState}`, + `grant: ${status.grant?.grantId ?? "なし"}`, + `grant scope: ${status.grant?.scope.selfScopeId ?? "なし"}`, + `事前裁定方針: ${status.decisionPolicyCount}`, + `自動裁定: ${status.decisionCount} (未確認 ${status.unreviewedDecisionCount}, accept ${status.acceptedDecisionCount}, flag ${status.flaggedDecisionCount})`, + `停止理由: ${status.stopReason ?? status.suspendedReason ?? "なし"}`, + "", + ].join("\n")); +} + +export interface ReviewTelemetryInput { + readonly intentUuid: string; + readonly decisionId: string; + readonly reviewId: string; + readonly choice: ReviewChoice; + readonly lifecycleAtReview: IntentLifecycle; + readonly reviewPrincipalRef: string; + readonly reviewActorRef: string; + readonly sourceHumanTurnId: string; + readonly decisionPrincipalRef: string | null; + readonly decisionActorRef: string | null; + readonly decisionSource: AutoDecisionRecord["decider"]; + readonly safeBasisDigest: string | null; + readonly grantId: string | null; + readonly safeNoteDigest: string | null; + readonly redactionStatus: RedactionStatus; + readonly auditTransactionId: string; + readonly traceId: string; + readonly spanId: string; +} + +export function projectReviewTelemetry(input: ReviewTelemetryInput): Readonly> { + const attributes: Record = { + "amadeus.intent.id": input.intentUuid, + "amadeus.decision.id": input.decisionId, + "amadeus.review.id": input.reviewId, + "amadeus.review.choice": input.choice, + "amadeus.review.lifecycle": input.lifecycleAtReview, + "amadeus.review.principal_ref": input.reviewPrincipalRef, + "amadeus.review.actor_ref": input.reviewActorRef, + "amadeus.review.source_turn_ref": input.sourceHumanTurnId, + "amadeus.decision.source": input.decisionSource, + "amadeus.redaction.status": input.redactionStatus, + "amadeus.audit.transaction_id": input.auditTransactionId, + "amadeus.trace.id": input.traceId, + "amadeus.span.id": input.spanId, + }; + if (input.redactionStatus === "redacted") { + if (input.decisionPrincipalRef !== null) attributes["amadeus.decision.principal_ref"] = input.decisionPrincipalRef; + if (input.decisionActorRef !== null) attributes["amadeus.decision.actor_ref"] = input.decisionActorRef; + if (input.safeBasisDigest !== null) attributes["amadeus.decision.basis_digest"] = input.safeBasisDigest; + if (input.safeNoteDigest !== null) attributes["amadeus.review.note_digest"] = input.safeNoteDigest; + if (input.grantId !== null) attributes["amadeus.grant.id"] = input.grantId; + } + return attributes; +} + +export type ReviewHarnessId = SelfInstallHarnessId; +export const REQUIRED_REVIEW_HARNESSES = SELF_INSTALL_HARNESS_IDS; +export interface ReviewHarnessContractResult { + readonly harnessId: ReviewHarnessId; + readonly fixtureId: string; + readonly contractRevision: string; + readonly passed: boolean; +} +export interface ReviewHarnessSuiteResult { + readonly fixtureId: string; + readonly contractRevision: string; + readonly requiredHarnesses: typeof REQUIRED_REVIEW_HARNESSES; + readonly receipts: readonly ReviewHarnessContractResult[]; + readonly passed: boolean; +} + +export function evaluateReviewHarnessSuite( + fixtureId: string, + contractRevision: string, + receipts: readonly ReviewHarnessContractResult[], +): ContractResult { + const observed = receipts.map((receipt) => receipt.harnessId).sort(compareUtf8); + const required = [...REQUIRED_REVIEW_HARNESSES].sort(compareUtf8); + if (observed.length !== required.length || observed.some((harness, index) => harness !== required[index])) { + return failure("CONFLICT", "requiredHarnesses", "each required review harness must report exactly once"); + } + if (receipts.some((receipt) => receipt.fixtureId !== fixtureId || receipt.contractRevision !== contractRevision)) { + return failure("CONFLICT", "contractRevision", "harness receipt does not match the fixture"); + } + return success({ + fixtureId, + contractRevision, + requiredHarnesses: REQUIRED_REVIEW_HARNESSES, + receipts, + passed: receipts.every((receipt) => receipt.passed), + }); +} diff --git a/packages/framework/core/tools/amadeus-bolt.ts b/packages/framework/core/tools/amadeus-bolt.ts index 0931fc083d..c62034a9a9 100644 --- a/packages/framework/core/tools/amadeus-bolt.ts +++ b/packages/framework/core/tools/amadeus-bolt.ts @@ -66,6 +66,12 @@ import { resumeProductionQuality, type ProductionQuestionDecisionInput, } from "./amadeus-intent-autonomy-production.ts"; +import { + commitProductionDecisionReview, + getProductionAutoDecision, + listProductionAutoDecisions, +} from "./amadeus-autonomy-review-production.ts"; +import { normalizeReviewFlagMetadata, reviewCommandContentDigest } from "./amadeus-autonomy-review.ts"; import { autonomyDigest, type DecisionPolicyInput } from "./amadeus-intent-autonomy.ts"; import { emitAuditEventGuarded } from "../otel/audit-emit.ts"; import { observeSubprocessSpan } from "../otel/subprocess-span.ts"; @@ -952,6 +958,96 @@ function handleResumeQuality(args: string[], explicitProjectDir?: string): void console.log(JSON.stringify(result)); } +function handleListAutoDecisions(args: string[], explicitProjectDir?: string): void { + const flags = parseFlags(args); + const state = flags.state; + if (state !== undefined && !["not-applicable", "unreviewed", "accepted", "flagged"].includes(state)) { + error(`Invalid --state: ${state}`); + } + const result = listProductionAutoDecisions({ + projectDir: resolveBoltProjectDir(explicitProjectDir), + intent: flags.intent, + reviewState: state as "not-applicable" | "unreviewed" | "accepted" | "flagged" | undefined, + }); + if (!result.ok) error(`Auto-decision list failed: ${result.error}`); + console.log(JSON.stringify(result.page)); +} + +// Preview-side confirmation digest. The flag-metadata normalization lives in +// ONE exported helper (normalizeReviewFlagMetadata) that the commit path's +// expected-value recomputation also calls, so the displayed digest and the +// verified digest cannot drift. +function reviewConfirmationDigest( + targetIntentUuid: string, + decisionId: string, + choice: "accept" | "flag", + classification: "contract-defect" | "specification-change" | "unspecified" | undefined, + note: string | undefined, +): string { + const metadata = normalizeReviewFlagMetadata({ + choice, + flagClassification: classification ?? undefined, + noteDigest: note !== undefined ? autonomyDigest(note) : null, + }); + return reviewCommandContentDigest({ + targetIntentUuid, + decisionId, + choice, + flagClassification: metadata.flagClassification, + safeNoteDigest: metadata.safeNoteDigest, + }); +} + +function handleGetAutoDecision(args: string[], explicitProjectDir?: string): void { + const flags = parseFlags(args); + if (!flags.decision) error("Missing --decision "); + const result = getProductionAutoDecision({ + projectDir: resolveBoltProjectDir(explicitProjectDir), + intent: flags.intent, + decisionId: flags.decision, + }); + if (!result.ok) error(`Auto-decision detail failed: ${result.error}`); + if (flags.choice !== undefined && flags.choice !== "accept" && flags.choice !== "flag") { + error(`Invalid --choice: ${flags.choice}. Must be 'accept' or 'flag'.`); + } + if (flags.choice === "accept" || flags.choice === "flag") { + const digest = reviewConfirmationDigest( + result.detail.intentUuid, + flags.decision, + flags.choice, + flags.classification as "contract-defect" | "specification-change" | "unspecified" | undefined, + flags.note, + ); + console.log(JSON.stringify({ ...result.detail, confirmedReviewDigest: digest })); + return; + } + console.log(JSON.stringify(result.detail)); +} + +function handleReviewAutoDecision(args: string[], explicitProjectDir?: string): void { + const flags = parseFlags(args); + if (!flags.decision) error("Missing --decision "); + if (flags.choice !== "accept" && flags.choice !== "flag") error("Missing --choice "); + const classification = flags.classification; + if (classification !== undefined && !["contract-defect", "specification-change", "unspecified"].includes(classification)) { + error(`Invalid --classification: ${classification}`); + } + if (!flags["confirmed-review-digest"]) { + error("Missing --confirmed-review-digest (preview it with get-auto-decision --choice)"); + } + const result = commitProductionDecisionReview({ + projectDir: resolveBoltProjectDir(explicitProjectDir), + intent: flags.intent, + decisionId: flags.decision, + choice: flags.choice, + flagClassification: classification as "contract-defect" | "specification-change" | "unspecified" | undefined, + note: flags.note, + confirmedContentDigest: flags["confirmed-review-digest"], + }); + if (!result.ok) error(`Auto-decision review failed: ${result.error}`); + console.log(JSON.stringify(result.receipt)); +} + function handleSetAutonomy(args: string[], explicitProjectDir?: string): void { const flags = parseFlags(args); if (!flags.mode) error("Missing --mode "); @@ -1102,7 +1198,7 @@ export function handleBoltCommand( return; default: error( - `Unknown subcommand: ${subcommand}. Valid: start, complete, fail, abort, preview-autonomy, set-autonomy, decide-question, observe-quality, resume-quality, approve-batch, dispatch-event, hold-merge, release-merge`, + `Unknown subcommand: ${subcommand}. Valid: start, complete, fail, abort, preview-autonomy, set-autonomy, decide-question, observe-quality, resume-quality, list-auto-decisions, get-auto-decision, review-auto-decision, approve-batch, dispatch-event, hold-merge, release-merge`, explicitProjectDir, ); } @@ -1119,6 +1215,9 @@ function handleAutonomySupportCommand( "decide-question": handleDecideQuestion, "observe-quality": handleObserveQuality, "resume-quality": handleResumeQuality, + "list-auto-decisions": handleListAutoDecisions, + "get-auto-decision": handleGetAutoDecision, + "review-auto-decision": handleReviewAutoDecision, }; const handler = subcommand === undefined ? undefined : handlers[subcommand]; if (handler === undefined) return false; diff --git a/packages/framework/core/tools/amadeus-harness-registry.ts b/packages/framework/core/tools/amadeus-harness-registry.ts new file mode 100644 index 0000000000..43eafc1b22 --- /dev/null +++ b/packages/framework/core/tools/amadeus-harness-registry.ts @@ -0,0 +1,65 @@ +// Canonical harness capability registry. This Core location is projected +// byte-for-byte into every harness distribution. + +export interface HarnessDescriptor { + readonly id: string; + readonly displayName: string; + readonly packageFace: boolean; + readonly selfInstallFace: boolean; + readonly autonomyContract: boolean; + readonly autonomyLive: boolean; + readonly native: { + readonly liveAuthorization: "credential-attested" | "unavailable"; + readonly judgeReplay: "invoke-once" | "unavailable"; + readonly liveCommandEnv: string | null; + }; +} + +export const HARNESS_REGISTRY = [ + { id: "claude", displayName: "Claude Code", packageFace: true, selfInstallFace: true, + autonomyContract: true, autonomyLive: true, + native: { liveAuthorization: "credential-attested", judgeReplay: "invoke-once", + liveCommandEnv: "AMADEUS_CLAUDE_LIVE_COMMAND_JSON" } }, + { id: "codex", displayName: "Codex", packageFace: true, selfInstallFace: true, + autonomyContract: true, autonomyLive: true, + native: { liveAuthorization: "credential-attested", judgeReplay: "invoke-once", + liveCommandEnv: "AMADEUS_CODEX_LIVE_COMMAND_JSON" } }, + { id: "cursor", displayName: "Cursor", packageFace: true, selfInstallFace: true, + autonomyContract: true, autonomyLive: true, + native: { liveAuthorization: "credential-attested", judgeReplay: "invoke-once", + liveCommandEnv: "AMADEUS_CURSOR_LIVE_COMMAND_JSON" } }, + { id: "kiro", displayName: "Kiro CLI", packageFace: true, selfInstallFace: false, + autonomyContract: true, autonomyLive: false, + native: { liveAuthorization: "unavailable", judgeReplay: "unavailable", liveCommandEnv: null } }, + { id: "kiro-ide", displayName: "Kiro IDE", packageFace: true, selfInstallFace: false, + autonomyContract: true, autonomyLive: false, + native: { liveAuthorization: "unavailable", judgeReplay: "unavailable", liveCommandEnv: null } }, + { id: "opencode", displayName: "OpenCode", packageFace: true, selfInstallFace: true, + autonomyContract: true, autonomyLive: true, + native: { liveAuthorization: "credential-attested", judgeReplay: "invoke-once", + liveCommandEnv: "AMADEUS_OPENCODE_LIVE_COMMAND_JSON" } }, + { id: "kimi", displayName: "Kimi Code", packageFace: true, selfInstallFace: true, + autonomyContract: true, autonomyLive: true, + native: { liveAuthorization: "credential-attested", judgeReplay: "invoke-once", + liveCommandEnv: "AMADEUS_KIMI_LIVE_COMMAND_JSON" } }, + { id: "pi", displayName: "Pi Coding Agent", packageFace: true, selfInstallFace: false, + autonomyContract: true, autonomyLive: false, + native: { liveAuthorization: "unavailable", judgeReplay: "unavailable", liveCommandEnv: null } }, +] as const satisfies readonly HarnessDescriptor[]; + +type RegistryRow = (typeof HARNESS_REGISTRY)[number]; +export type HarnessId = RegistryRow["id"]; +export type PackageHarnessId = RegistryRow extends infer Row + ? Row extends { readonly packageFace: true; readonly id: infer Id extends string } ? Id : never + : never; +export type SelfInstallHarnessId = RegistryRow extends infer Row + ? Row extends { readonly selfInstallFace: true; readonly id: infer Id extends string } ? Id : never + : never; + +export const PACKAGE_HARNESS_IDS: readonly PackageHarnessId[] = HARNESS_REGISTRY + .filter((descriptor): descriptor is RegistryRow & { readonly packageFace: true } => descriptor.packageFace) + .map((descriptor) => descriptor.id as PackageHarnessId); + +export const SELF_INSTALL_HARNESS_IDS: readonly SelfInstallHarnessId[] = HARNESS_REGISTRY + .filter((descriptor): descriptor is RegistryRow & { readonly selfInstallFace: true } => descriptor.selfInstallFace) + .map((descriptor) => descriptor.id as SelfInstallHarnessId); diff --git a/tests/.coverage-patch-allowlist.json b/tests/.coverage-patch-allowlist.json index c626e79cea..605a5687da 100644 --- a/tests/.coverage-patch-allowlist.json +++ b/tests/.coverage-patch-allowlist.json @@ -6031,5 +6031,71 @@ }, "reason": "The status projection read is driven in-process by t435; this residual line is the defensive catch arm for an unreadable audit projection, not reachable from a healthy record.", "expiry": "remove when the residual arm gains a deterministic in-process driver, or Bun merges child-process coverage" + }, + { + "file": "packages/framework/core/tools/amadeus-bolt.ts", + "selector": { + "function": "handleListAutoDecisions", + "fingerprint": "sha256:64c19e102994ef34f709fdd374045dd92b7872452bc0f73cfabfb80d662f39c0", + "anchorLines": 1, + "targetLines": "1" + }, + "reason": "The verb is driven in-process by t435 and the line executes on that path; bun's union-merged lcov stamps this flag-read line 0 in the parent report (loaded-only chunk artifact), the bun-inbody attribution class.", + "expiry": "remove when bun's union merge attributes the line, or the handler gains a dedicated in-process lcov driver" + }, + { + "file": "packages/framework/core/tools/amadeus-bolt.ts", + "selector": { + "function": "handleReviewAutoDecision", + "fingerprint": "sha256:af6e749d8b625ed51ffa6e5011ecd8ecc17c89e59360144c57325b4b103ba71e", + "anchorLines": 1, + "targetLines": "1" + }, + "reason": "The verb is driven in-process by t435 and the line executes on that path; bun's union-merged lcov stamps this flag-read line 0 in the parent report (loaded-only chunk artifact), the bun-inbody attribution class.", + "expiry": "remove when bun's union merge attributes the line, or the handler gains a dedicated in-process lcov driver" + }, + { + "file": "packages/framework/core/tools/amadeus-autonomy-review-production.ts", + "selector": { + "function": "commitProductionDecisionReview", + "fingerprint": "sha256:6596f0e61c1718154f1ecd81dee83126576ee17ec85fb0dd6c47c4bfadf2fc12", + "anchorLines": 2, + "targetLines": "1-2" + }, + "reason": "Defensive catch converting an unexpected lock/journal throw into a typed error; the review commit's reachable failure arms are typed returns pinned by t433, so this arm only fires on infrastructure faults not reproducible in-process.", + "expiry": "remove when the residual arm gains a deterministic in-process driver, or Bun merges child-process coverage" + }, + { + "file": "packages/framework/core/tools/amadeus-bolt.ts", + "selector": { + "function": "handleReviewAutoDecision", + "fingerprint": "sha256:8cd42ca44cfc1e429ac54c895629dd38fa0a41a7d0d0c03a852d7f99d0bddfd4", + "anchorLines": 1, + "targetLines": "1" + }, + "reason": "CLI flag-validation arm: error() exits the process, so the missing-digest branch is spawn-only; the confirmed path is driven in-process by t435.", + "expiry": "remove when the residual arm gains a deterministic in-process driver, or Bun merges child-process coverage" + }, + { + "file": "packages/framework/core/tools/amadeus-autonomy-review.ts", + "selector": { + "function": "put", + "fingerprint": "sha256:9f058a96ce21445c8a2dc2e68dcd6418400bf158dc4df47155c076b91c6e81b1", + "anchorLines": 1, + "targetLines": "1" + }, + "reason": "Defensive arm: put() is only reachable from the constructor seeds today, where retained reviews are always empty; the guard exists so a future reseed API cannot orphan a terminal review.", + "expiry": "remove when the residual arm gains a deterministic in-process driver, or Bun merges child-process coverage" + }, + { + "file": "packages/framework/core/tools/amadeus-bolt.ts", + "selector": { + "function": "handleGetAutoDecision", + "fingerprint": "sha256:f91b9204be7a636743ec5e5e16bb0c4dc5cad748f4dd49a9ee8b9a7355eaab43", + "anchorLines": 1, + "targetLines": "1" + }, + "reason": "CLI flag-validation arm: error() exits the process, so the invalid --choice branch is spawn-only; the valid preview path is driven in-process by t435.", + "expiry": "remove when the residual arm gains a deterministic in-process driver, or Bun merges child-process coverage" } ] diff --git a/tests/.coverage-ratchet.json b/tests/.coverage-ratchet.json index 2aa2e36780..2c9da7a6ec 100644 --- a/tests/.coverage-ratchet.json +++ b/tests/.coverage-ratchet.json @@ -2,7 +2,7 @@ "note": "Committed baseline: covered-unit count per class. The --check ratchet fails CI if any class's covered count DROPS below these numbers without a reviewed deferred entry. Monotonic anti-regression: you can cover more, never silently less. Regenerate with: bun tests/gen-coverage-registry.ts", "coveredByClass": { "function": 175, - "audit": 42, + "audit": 43, "scope": 15, "stage": 8, "hook": 14, diff --git a/tests/.coverage-registry.json b/tests/.coverage-registry.json index 8aef13b05a..f648c3df9c 100644 --- a/tests/.coverage-registry.json +++ b/tests/.coverage-registry.json @@ -20,19 +20,19 @@ "render-surface": "tui" }, "counts": { - "total": 600, + "total": 604, "enumeratedByClass": { "function": 339, - "audit": 88, + "audit": 89, "scope": 15, "stage": 32, "hook": 14, - "subcommand": 105, + "subcommand": 108, "render-surface": 7 }, "coveredByClass": { "function": 175, - "audit": 42, + "audit": 43, "scope": 15, "stage": 8, "hook": 14, @@ -86,6 +86,18 @@ ], "status": "covered" }, + { + "unitClass": "audit", + "unitId": "AUTO_DECISION_REVIEWED", + "minMechanism": "none", + "coveredBy": [ + { + "file": "tests/integration/t433-autonomy-review-observability.test.ts", + "mechanism": "cli" + } + ], + "status": "covered" + }, { "unitClass": "audit", "unitId": "AUTONOMY_MODE_SET", @@ -5688,6 +5700,13 @@ ], "status": "covered" }, + { + "unitClass": "subcommand", + "unitId": "amadeus-bolt get-auto-decision", + "minMechanism": "cli", + "coveredBy": [], + "status": "UNCOVERED" + }, { "unitClass": "subcommand", "unitId": "amadeus-bolt hold-merge", @@ -5700,6 +5719,13 @@ ], "status": "covered" }, + { + "unitClass": "subcommand", + "unitId": "amadeus-bolt list-auto-decisions", + "minMechanism": "cli", + "coveredBy": [], + "status": "UNCOVERED" + }, { "unitClass": "subcommand", "unitId": "amadeus-bolt observe-quality", @@ -5733,6 +5759,13 @@ "coveredBy": [], "status": "UNCOVERED" }, + { + "unitClass": "subcommand", + "unitId": "amadeus-bolt review-auto-decision", + "minMechanism": "cli", + "coveredBy": [], + "status": "UNCOVERED" + }, { "unitClass": "subcommand", "unitId": "amadeus-bolt set-autonomy", diff --git a/tests/integration/event-registry-drift.test.ts b/tests/integration/event-registry-drift.test.ts index cc812fe1d9..cb1eae3754 100644 --- a/tests/integration/event-registry-drift.test.ts +++ b/tests/integration/event-registry-drift.test.ts @@ -47,11 +47,11 @@ describe("registry internal consistency (VER-1)", () => { expect(() => assertRegistryConsistent()).not.toThrow(); }); - test("canonical cardinality is pinned at 88 (#1672, #1602, #1919, #2095, #2096, #2067) — vacuous equality banned", () => { - expect(EXPECTED_CANONICAL_COUNT).toBe(88); - expect(canonicalAuditEvents().length).toBe(88); - expect(SETS.registryCanonical.size).toBe(88); - expect(SETS.auditVocabulary.size).toBe(88); + test("canonical cardinality is pinned at 89 (#1672, #1602, #1919, #2095, #2096, #2067) — vacuous equality banned", () => { + expect(EXPECTED_CANONICAL_COUNT).toBe(89); + expect(canonicalAuditEvents().length).toBe(89); + expect(SETS.registryCanonical.size).toBe(89); + expect(SETS.auditVocabulary.size).toBe(89); }); test("canonical defs always map to the audit journal; telemetry defs never do (FR-EXP-4)", () => { @@ -189,7 +189,7 @@ describe("extraction parity with the t28 vocabulary guard", () => { test("extractAuditVocabulary agrees with the t28 sed-range rule on the shipped amadeus-audit.ts", () => { const body = readFileSync(join(AMADEUS_SRC, "tools", "amadeus-audit.ts"), "utf-8"); const vocab = extractAuditVocabulary(body); - expect(vocab.length).toBe(88); + expect(vocab.length).toBe(89); expect(vocab).toEqual([...SETS.auditVocabulary].sort()); }); }); diff --git a/tests/integration/t-coverage-mechanism-ratchet.test.ts b/tests/integration/t-coverage-mechanism-ratchet.test.ts index eb04767598..0955009f42 100644 --- a/tests/integration/t-coverage-mechanism-ratchet.test.ts +++ b/tests/integration/t-coverage-mechanism-ratchet.test.ts @@ -190,6 +190,7 @@ describe("repository-wide mechanism honesty ratchets", () => { "integration/t427-goal-reconciliation-completion.integration.test.ts", "integration/t428-goal-revision-authority.integration.test.ts", "integration/t429-legacy-goal-migration.integration.test.ts", + "integration/t433-autonomy-review-observability.test.ts", "integration/t45.test.ts", "integration/t49.test.ts", "integration/t51.test.ts", diff --git a/tests/integration/t433-autonomy-review-observability.test.ts b/tests/integration/t433-autonomy-review-observability.test.ts new file mode 100644 index 0000000000..a80cde034b --- /dev/null +++ b/tests/integration/t433-autonomy-review-observability.test.ts @@ -0,0 +1,1221 @@ +// covers: file:packages/framework/core/tools/amadeus-autonomy-review.ts, file:packages/framework/core/tools/amadeus-autonomy-review-production.ts +// covers: audit:AUTO_DECISION_REVIEWED + +import { describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { appendFileSync, readFileSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; + +import { + bindHumanReviewCommand, + canonicalContractValueDigest, + canonicalTupleDigest, + createMemoryAutonomyReviewService, + evaluateReviewHarnessSuite, + projectHumanReviewStatus, + projectMachineReviewStatus, + projectReviewTelemetry, + REQUIRED_REVIEW_HARNESSES, + reviewAuditFields, + normalizeReviewFlagMetadata, + reviewCommandContentDigest, + type HumanReviewCommandBinding, + type ReviewIntentSeed, + type ReviewStatusInput, +} from "../../packages/framework/core/tools/amadeus-autonomy-review.ts"; +import { + autonomyDigest, + createAutonomyProjection, + grantIssuanceDisplayDigest, + normalizeDecisionPolicies, + planHumanAutonomyCommand, + type AutoDecisionRecord, + type GrantScopeDescriptor, +} from "../../packages/framework/core/tools/amadeus-intent-autonomy.ts"; +import { + commitProductionQuestionDecision, + applyProductionAutonomyMode, + previewProductionAutonomyGrant, +} from "../../packages/framework/core/tools/amadeus-intent-autonomy-production.ts"; +import { + commitProductionDecisionReview, + getProductionAutoDecision, + listProductionAutoDecisions, +} from "../../packages/framework/core/tools/amadeus-autonomy-review-production.ts"; +import { + readAllAuditShards, + transitionIntentStatusLocked, + withLockedIntentRegistry, +} from "../../packages/framework/core/tools/amadeus-lib.ts"; +import { emitAuditEventGuarded } from "../../packages/framework/core/otel/audit-emit.ts"; +import { + resetObservabilityConfigCache, + resolveObservabilityConfig, +} from "../../packages/framework/core/tools/amadeus-observability.ts"; +import { + cleanupTestProject, + DEFAULT_INTENT_UUID, + DEFAULT_RECORD_DIR, + setupIntegrationProject, +} from "../harness/fixtures.ts"; +import { resetOtelPerProject } from "../harness/otel-reset.ts"; + +const ACTIVE = "019fc5ac-f0bb-7a5f-8a64-c944b6f76ead"; +const COMPLETED = "019fc5ac-f0bb-7a5f-8a64-c944b6f76aee"; +const OTHER_ACTIVE = "019fc5ac-f0bb-7a5f-8a64-c944b6f76aef"; + +function decision(intentUuid: string, suffix: string, reviewable = true): AutoDecisionRecord { + return { + decisionId: `decision-${suffix}`, + occurrenceId: `occurrence-${suffix}`, + question: `Choose ${suffix}?`, + optionIds: ["accept", "reject"], + selectedOptionId: "accept", + decider: reviewable ? "solo-election" : "deterministic-engine", + basisKind: reviewable ? "solo-election" : "confirmed-policy", + basisFingerprint: autonomyDigest([intentUuid, suffix]), + principalId: "principal-1", + actorId: "core-engine-1", + grantId: reviewable ? "grant-1" : null, + degradedCapability: null, + reviewState: reviewable ? "unreviewed" : "not-applicable", + }; +} + +function seed( + intentUuid: string, + lifecycle: "active" | "completed", + decisions: readonly AutoDecisionRecord[], + auditRevision = 4, +): ReviewIntentSeed { + const autonomy = createAutonomyProjection({ intentUuid }); + return { + intentUuid, + lifecycle, + autonomy: { ...autonomy, autoDecisions: decisions, projectionRevision: auditRevision }, + auditRevision, + completionSealDigest: lifecycle === "completed" ? autonomyDigest([intentUuid, "seal"]) : null, + }; +} + +function binding(choice: "accept" | "flag" = "accept", classification: HumanReviewCommandBinding["flagClassification"] = null) { + return bindHumanReviewCommand({ + sourceIntentUuid: ACTIVE, + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice, + commandOccurrenceId: `review-${choice}`, + flagClassification: classification, + safeNoteDigest: choice === "flag" ? autonomyDigest("safe-note") : null, + sourceHumanTurnId: `turn-${choice}`, + }); +} + +function activeStatusInput(overrides: Partial = {}): ReviewStatusInput { + return { + intentUuid: ACTIVE, + lifecycle: "active", + autonomy: createAutonomyProjection({ intentUuid: ACTIVE }), + workflowResult: null, + currentGrantScope: null, + decisionPolicyCount: 0, + decisionCounts: { total: 3, unreviewed: 1, accepted: 1, flagged: 1 }, + reviewExtensionHead: null, + legacyDiagnostic: null, + ...overrides, + }; +} + +function fullStatusInput(): ReviewStatusInput { + const initial = createAutonomyProjection({ intentUuid: ACTIVE }); + const scope: GrantScopeDescriptor = { + intentUuid: ACTIVE, + scopeId: "self-feature", + scopeFingerprint: autonomyDigest("self-feature"), + normFingerprint: autonomyDigest("norm-v1"), + allowedInteractionKinds: ["stage-gate", "phase-gate", "walking-skeleton", "question"], + permissionBoundaryFingerprint: autonomyDigest("host-policy"), + prohibitedEffects: ["new-permission", "irreversible", "scope-out", "norm-waiver", "quality-waiver"], + }; + const policies = normalizeDecisionPolicies({ + grantIdentitySeed: "grant-seed", + scopeFingerprint: scope.scopeFingerprint, + humanTurnId: "human-turn-1", + policies: [{ sourceText: "Prefer the accepted option", selector: "selector-1", optionId: "accept" }], + }); + const confirmedDisplayDigest = grantIssuanceDisplayDigest({ + intentUuid: ACTIVE, + principalId: "principal-1", + scope, + policies, + }); + const plan = planHumanAutonomyCommand(initial, { kind: "issue-full", scope, policies }, { + targetIntentUuid: ACTIVE, + principalId: "principal-1", + humanTurn: { verified: true, eventType: "HUMAN_TURN", actor: "human", turnId: "human-turn-1" }, + commandOccurrenceId: "command-1", + expectedProjectionRevision: initial.projectionRevision, + confirmedDisplayDigest, + }); + if (!plan.ok) throw new Error(plan.code); + return activeStatusInput({ + autonomy: plan.after, + currentGrantScope: { + scopeFingerprint: scope.scopeFingerprint, + selfScopeId: scope.scopeId, + allowedInteractionKinds: scope.allowedInteractionKinds, + }, + decisionPolicyCount: policies.length, + }); +} + +describe("autonomy decision read model", () => { + test("lists only eligible unreviewed decisions for active and completed intents", () => { + const service = createMemoryAutonomyReviewService({ + intents: [ + seed(ACTIVE, "active", [decision(ACTIVE, "active"), decision(ACTIVE, "policy", false)]), + seed(COMPLETED, "completed", [decision(COMPLETED, "completed")]), + ], + }); + + const active = service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", reviewState: "unreviewed", pageSize: 10 }); + const completed = service.listAutoDecisions({ intentUuid: COMPLETED, lifecycle: "completed", reviewState: "unreviewed", pageSize: 10 }); + expect(active.ok && active.value.items.map((item) => item.decisionId)).toEqual(["decision-active"]); + expect(completed.ok && completed.value.items.map((item) => item.decisionId)).toEqual(["decision-completed"]); + }); + + test("binds cursors to one immutable snapshot and rejects drift", () => { + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [decision(ACTIVE, "a"), decision(ACTIVE, "b")])], + }); + const first = service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", reviewState: "unreviewed", pageSize: 1 }); + if (!first.ok) throw new Error(JSON.stringify(first.error)); + expect(first.value.nextCursor).not.toBeNull(); + if (first.value.nextCursor === null) throw new Error("expected a next cursor"); + const second = service.listAutoDecisions({ + intentUuid: ACTIVE, + lifecycle: "active", + reviewState: "unreviewed", + pageSize: 1, + cursor: first.value.nextCursor, + }); + expect(second.ok && second.value.items.map((item) => item.decisionId)).toEqual(["decision-b"]); + expect(service.listAutoDecisions({ + intentUuid: ACTIVE, + lifecycle: "active", + reviewState: "unreviewed", + pageSize: 1, + cursor: { ...first.value.nextCursor, cursorDigest: "tampered" }, + })).toMatchObject({ ok: false, error: { code: "MALFORMED", locus: "cursor" } }); + expect(service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", pageSize: 0 })).toMatchObject({ + ok: false, + error: { code: "MALFORMED", locus: "pageSize" }, + }); + expect(service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "completed", pageSize: 10 })).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "lifecycle" }, + }); + expect(service.getAutoDecision(ACTIVE, "decision-missing")).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "decisionId" }, + }); + service.replaceIntent(seed(ACTIVE, "active", [decision(ACTIVE, "a"), decision(ACTIVE, "b"), decision(ACTIVE, "c")])); + const stale = service.listAutoDecisions({ + intentUuid: ACTIVE, + lifecycle: "active", + reviewState: "unreviewed", + pageSize: 1, + cursor: first.value.nextCursor, + }); + expect(stale).toEqual({ ok: false, error: { code: "CONFLICT", locus: "cursorSnapshot", detail: "decision snapshot changed" } }); + }); + + test("returns redacted detail without exposing a raw evidence payload", () => { + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [decision(ACTIVE, "active")])], + redactor: { + redact: (_kind, value) => ({ value: value.normalize("NFC"), status: "redacted" }), + }, + }); + const detail = service.getAutoDecision(ACTIVE, "decision-active"); + expect(detail.ok && detail.value.safeQuestion).toBe("Choose active?"); + expect(detail.ok && detail.value.evidence[0]?.evidenceFingerprint).toBe(autonomyDigest([ACTIVE, "active"])); + expect(detail.ok && JSON.stringify(detail.value)).not.toContain("rawPayload"); + }); +}); + +describe("real-human review append", () => { + test("accepts an active decision only from a real turn in the same Intent", () => { + const activeBinding = bindHumanReviewCommand({ + sourceIntentUuid: ACTIVE, + targetIntentUuid: ACTIVE, + decisionId: "decision-active", + choice: "accept", + commandOccurrenceId: "review-active", + flagClassification: null, + safeNoteDigest: null, + sourceHumanTurnId: "turn-active", + }); + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [decision(ACTIVE, "active")], 7)], + humanTurns: [{ + sourceIntentUuid: ACTIVE, + lifecycle: "active", + sourceAuditRevision: 7, + sourceHumanTurnId: "turn-active", + sourceHumanTurnEventId: "human-event-active", + principalId: "human-1", + binding: activeBinding, + }], + }); + const authorization = service.authorizeHumanReview({ + command: activeBinding.command, + sourceHumanTurnId: "turn-active", + sourceHumanTurnEventId: "human-event-active", + }); + if (!authorization.ok) throw new Error(authorization.error.code); + expect(service.appendDecisionReview({ + targetIntentUuid: ACTIVE, + decisionId: "decision-active", + choice: "accept", + expectedTargetAuditRevision: 7, + expectedCompletionSealDigest: autonomyDigest("unexpected-active-seal"), + humanAuthorization: authorization.value, + })).toMatchObject({ ok: false, error: { code: "MALFORMED", locus: "completionSealDigest" } }); + const reviewed = service.appendDecisionReview({ + targetIntentUuid: ACTIVE, + decisionId: "decision-active", + choice: "accept", + expectedTargetAuditRevision: 7, + expectedCompletionSealDigest: null, + humanAuthorization: authorization.value, + }); + expect(reviewed).toMatchObject({ ok: true, value: { state: "accepted", remediation: null } }); + expect(service.readIntent(ACTIVE)).toMatchObject({ auditRevision: 8, reviewExtensionHead: null }); + }); + + test("flags a completed decision while preserving its seal and only suggesting self-fix", () => { + const reviewBinding = binding("flag", "contract-defect"); + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [], 7), seed(COMPLETED, "completed", [decision(COMPLETED, "completed")])], + humanTurns: [{ + sourceIntentUuid: ACTIVE, + lifecycle: "active", + sourceAuditRevision: 7, + sourceHumanTurnId: "turn-flag", + sourceHumanTurnEventId: "human-event-flag", + principalId: "human-1", + binding: reviewBinding, + }], + }); + const authorization = service.authorizeHumanReview({ + command: reviewBinding.command, + sourceHumanTurnId: "turn-flag", + sourceHumanTurnEventId: "human-event-flag", + }); + expect(authorization.ok).toBe(true); + if (!authorization.ok) return; + const before = service.readIntent(COMPLETED); + const reviewed = service.appendDecisionReview({ + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice: "flag", + expectedTargetAuditRevision: 4, + expectedCompletionSealDigest: before?.completionSealDigest ?? null, + humanAuthorization: authorization.value, + }); + const after = service.readIntent(COMPLETED); + expect(reviewed.ok && reviewed.value.state).toBe("flagged"); + expect(reviewed.ok && reviewed.value.remediation).toBe("self-fix"); + expect(after?.completionSealDigest).toBe(before?.completionSealDigest); + expect(after?.lifecycle).toBe("completed"); + expect(after?.reviewExtensionHead).not.toBeNull(); + expect(service.createdIntentCount).toBe(0); + + const snapshot = service.exportSnapshot(); + const reloaded = createMemoryAutonomyReviewService({ snapshot }); + expect(reloaded.getAutoDecision(COMPLETED, "decision-completed")).toMatchObject({ + ok: true, + value: { reviewState: "flagged", reviewReceipt: reviewed.ok ? reviewed.value : null }, + }); + expect(reloaded.readIntent(COMPLETED)?.completionSealDigest).toBe(before?.completionSealDigest); + + const events = service.readReviewEvents(COMPLETED); + expect(events).toHaveLength(1); + expect(reviewAuditFields(events[0]!)).toMatchObject({ + "Intent Uuid": COMPLETED, + "Decision Id": "decision-completed", + Choice: "flag", + Lifecycle: "completed", + "Review Principal": "human-1", + "Review Actor": "human-1", + "Source Human Turn": "turn-flag", + "Decision Principal": "principal-1", + "Decision Actor": "core-engine-1", + "Decision Source": "solo-election", + "Grant Id": "grant-1", + Remediation: "self-fix", + "Note Digest": autonomyDigest("safe-note"), + }); + + const tamperedValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, reviews: intent.reviews.map((event) => ({ ...event, payloadV1: "{}" })) } + : intent), + }; + const signedSnapshot = (value: typeof snapshot.value) => { + const digest = canonicalContractValueDigest("autonomy-review-persistence", value); + if (!digest.ok) throw new Error(digest.error.code); + return { value, digest: digest.value }; + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(tamperedValue) })) + .toThrow("invalid-autonomy-review-persistence-payload"); + + const invalidJsonValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, reviews: intent.reviews.map((event) => ({ ...event, payloadV1: "{" })) } + : intent), + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(invalidJsonValue) })) + .toThrow("invalid-autonomy-review-persistence-payload"); + + const invalidIdentityValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, reviews: intent.reviews.map((event) => ({ ...event, eventIdentity: "review-event-tampered" })) } + : intent), + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(invalidIdentityValue) })) + .toThrow("invalid-autonomy-review-persistence-identity"); + + const fieldMismatchValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, reviews: intent.reviews.map((event) => ({ ...event, remediation: "self-feature" as const })) } + : intent), + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(fieldMismatchValue) })) + .toThrow("invalid-autonomy-review-persistence-payload"); + + const duplicateEventValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, reviews: [...intent.reviews, ...intent.reviews] } + : intent), + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(duplicateEventValue) })) + .toThrow("invalid-autonomy-review-persistence-events"); + + const activeExtensionValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, lifecycle: "active" as const, completionSealDigest: null } + : intent), + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(activeExtensionValue) })) + .toThrow("invalid-autonomy-review-extension-snapshot"); + + const completedExtensionValue = { + ...snapshot.value, + intents: snapshot.value.intents.map((intent) => intent.intentUuid === COMPLETED + ? { ...intent, reviewExtensionHead: autonomyDigest("wrong-extension-head") } + : intent), + }; + expect(() => createMemoryAutonomyReviewService({ snapshot: signedSnapshot(completedExtensionValue) })) + .toThrow("invalid-autonomy-review-extension-snapshot"); + }); + + test("rejects synthetic provenance and leaves the target unreviewed", () => { + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [], 7), seed(COMPLETED, "completed", [decision(COMPLETED, "completed")])], + }); + const reviewBinding = binding(); + const result = service.authorizeHumanReview({ + command: reviewBinding.command, + sourceHumanTurnId: "synthetic-turn", + sourceHumanTurnEventId: "synthetic-event", + }); + expect(result.ok).toBe(false); + expect(service.getAutoDecision(COMPLETED, "decision-completed")).toMatchObject({ + ok: true, + value: { reviewState: "unreviewed" }, + }); + }); + + test("rejects a human turn from another active Intent for an active target", () => { + const crossIntentBinding = bindHumanReviewCommand({ + sourceIntentUuid: OTHER_ACTIVE, + targetIntentUuid: ACTIVE, + decisionId: "decision-active", + choice: "accept", + commandOccurrenceId: "review-cross-intent", + flagClassification: null, + safeNoteDigest: null, + sourceHumanTurnId: "turn-cross-intent", + }); + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [decision(ACTIVE, "active")], 7), seed(OTHER_ACTIVE, "active", [], 7)], + humanTurns: [{ + sourceIntentUuid: OTHER_ACTIVE, + lifecycle: "active", + sourceAuditRevision: 7, + sourceHumanTurnId: "turn-cross-intent", + sourceHumanTurnEventId: "human-event-cross-intent", + principalId: "human-1", + binding: crossIntentBinding, + }], + }); + const authorization = service.authorizeHumanReview({ + command: crossIntentBinding.command, + sourceHumanTurnId: "turn-cross-intent", + sourceHumanTurnEventId: "human-event-cross-intent", + }); + if (!authorization.ok) throw new Error(authorization.error.code); + expect(service.appendDecisionReview({ + targetIntentUuid: ACTIVE, + decisionId: "decision-active", + choice: "accept", + expectedTargetAuditRevision: 7, + expectedCompletionSealDigest: null, + humanAuthorization: authorization.value, + })).toMatchObject({ ok: false, error: { code: "PROVENANCE_REQUIRED", locus: "sourceIntentUuid" } }); + }); + + test("is idempotent for the same choice and rejects a conflicting terminal choice", () => { + const acceptBinding = binding("accept"); + const service = createMemoryAutonomyReviewService({ + intents: [ + seed(ACTIVE, "active", [], 7), + seed(COMPLETED, "completed", [decision(COMPLETED, "completed"), decision(COMPLETED, "policy", false)]), + ], + humanTurns: [{ + sourceIntentUuid: ACTIVE, + lifecycle: "active", + sourceAuditRevision: 7, + sourceHumanTurnId: "turn-accept", + sourceHumanTurnEventId: "human-event-accept", + principalId: "human-1", + binding: acceptBinding, + }], + }); + const authorization = service.authorizeHumanReview({ + command: acceptBinding.command, + sourceHumanTurnId: "turn-accept", + sourceHumanTurnEventId: "human-event-accept", + }); + if (!authorization.ok) throw new Error(authorization.error.code); + const command = { + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice: "accept" as const, + expectedTargetAuditRevision: 4, + expectedCompletionSealDigest: service.readIntent(COMPLETED)?.completionSealDigest ?? null, + humanAuthorization: authorization.value, + }; + expect(service.authorizeHumanReview({ + command: { ...acceptBinding.command, choice: "flag" }, + sourceHumanTurnId: "turn-accept", + sourceHumanTurnEventId: "human-event-accept", + })).toMatchObject({ ok: false, error: { code: "PROVENANCE_REQUIRED", locus: "commandBindingDigest" } }); + expect(service.appendDecisionReview({ ...command, decisionId: "decision-missing" })).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "decisionId" }, + }); + expect(service.appendDecisionReview({ ...command, decisionId: "decision-policy" })).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "reviewState" }, + }); + expect(service.appendDecisionReview({ ...command, choice: "flag" })).toMatchObject({ + ok: false, + error: { code: "PROVENANCE_REQUIRED", locus: "humanAuthorization" }, + }); + expect(service.appendDecisionReview({ ...command, expectedTargetAuditRevision: 3 })).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "targetAuditRevision" }, + }); + expect(service.appendDecisionReview({ + ...command, + expectedCompletionSealDigest: autonomyDigest("wrong-completion-seal"), + })).toMatchObject({ ok: false, error: { code: "CONFLICT", locus: "completionSealDigest" } }); + expect(service.appendDecisionReview({ + ...command, + humanAuthorization: { ...authorization.value, sourceAuditRevision: 8 }, + })).toMatchObject({ ok: false, error: { code: "PROVENANCE_REQUIRED", locus: "sourceIntentUuid" } }); + const first = service.appendDecisionReview(command); + const again = service.appendDecisionReview(command); + expect(first).toMatchObject({ ok: true, value: { state: "accepted" } }); + expect(first).toEqual(again); + const conflict = service.appendDecisionReview({ ...command, choice: "flag" }); + expect(conflict).toMatchObject({ ok: false, error: { code: "CONFLICT", locus: "reviewState" } }); + }); + + test("maps an unspecified flag to an explicit repair-or-feature decision", () => { + const reviewBinding = binding("flag", "unspecified"); + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [], 7), seed(COMPLETED, "completed", [decision(COMPLETED, "completed")])], + humanTurns: [{ + sourceIntentUuid: ACTIVE, + lifecycle: "active", + sourceAuditRevision: 7, + sourceHumanTurnId: "turn-flag", + sourceHumanTurnEventId: "human-event-unspecified", + principalId: "human-1", + binding: reviewBinding, + }], + }); + const authorization = service.authorizeHumanReview({ + command: reviewBinding.command, + sourceHumanTurnId: "turn-flag", + sourceHumanTurnEventId: "human-event-unspecified", + }); + if (!authorization.ok) throw new Error(authorization.error.code); + expect(service.appendDecisionReview({ + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice: "flag", + expectedTargetAuditRevision: 4, + expectedCompletionSealDigest: service.readIntent(COMPLETED)?.completionSealDigest ?? null, + humanAuthorization: authorization.value, + })).toMatchObject({ + ok: true, + value: { state: "flagged", remediation: "self-fix-with-feature-alternative" }, + }); + }); + + test("rejects noncanonical values and accept commands carrying flag metadata", () => { + expect(canonicalContractValueDigest("unsupported", Symbol("not-canonical"))).toMatchObject({ + ok: false, + error: { code: "MALFORMED", locus: "canonicalValue" }, + }); + expect(() => bindHumanReviewCommand({ + sourceIntentUuid: ACTIVE, + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice: "accept", + commandOccurrenceId: "review-invalid-accept", + flagClassification: "contract-defect", + safeNoteDigest: null, + sourceHumanTurnId: "turn-invalid-accept", + })).toThrow("accept-review-cannot-carry-flag-metadata"); + }); + + test("rejects malformed persistence boundaries and exports human turns deterministically", () => { + expect(() => createMemoryAutonomyReviewService({ + intents: [{ ...seed(ACTIVE, "active", []), intentUuid: COMPLETED }], + })).toThrow("invalid-review-intent-seed"); + + const emptySnapshot = createMemoryAutonomyReviewService().exportSnapshot(); + expect(() => createMemoryAutonomyReviewService({ + snapshot: { ...emptySnapshot, digest: "tampered" }, + })).toThrow("invalid-autonomy-review-persistence-snapshot"); + + const bindingA = bindHumanReviewCommand({ + sourceIntentUuid: ACTIVE, + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice: "accept", + commandOccurrenceId: "review-a", + flagClassification: null, + safeNoteDigest: null, + sourceHumanTurnId: "turn-a", + }); + const bindingB = bindHumanReviewCommand({ + sourceIntentUuid: ACTIVE, + targetIntentUuid: COMPLETED, + decisionId: "decision-completed", + choice: "accept", + commandOccurrenceId: "review-b", + flagClassification: null, + safeNoteDigest: null, + sourceHumanTurnId: "turn-b", + }); + const snapshot = createMemoryAutonomyReviewService({ + humanTurns: [ + { + sourceIntentUuid: ACTIVE, + lifecycle: "active", + sourceAuditRevision: 1, + sourceHumanTurnId: "turn-b", + sourceHumanTurnEventId: "event-b", + principalId: "human-1", + binding: bindingB, + }, + { + sourceIntentUuid: ACTIVE, + lifecycle: "active", + sourceAuditRevision: 1, + sourceHumanTurnId: "turn-a", + sourceHumanTurnEventId: "event-a", + principalId: "human-1", + binding: bindingA, + }, + ], + }).exportSnapshot(); + expect(snapshot.value.humanTurns.map((turn) => turn.sourceHumanTurnEventId)).toEqual(["event-a", "event-b"]); + }); +}); + +describe("status, telemetry and harness projection", () => { + test("keeps completed workflow terminal while reporting an unreviewed queue", () => { + const autonomy = createAutonomyProjection({ intentUuid: COMPLETED }); + const result = projectMachineReviewStatus({ + intentUuid: COMPLETED, + lifecycle: "completed", + autonomy, + workflowResult: { + outcome: "completed", + reasonCode: null, + retryable: false, + intentUuid: COMPLETED, + autonomyMode: "none", + grant: null, + evidenceFingerprint: null, + resumeCondition: null, + failureRef: null, + }, + currentGrantScope: null, + decisionPolicyCount: 0, + decisionCounts: { total: 1, unreviewed: 1, accepted: 0, flagged: 0 }, + reviewExtensionHead: null, + legacyDiagnostic: null, + }); + expect(result).toMatchObject({ + ok: true, + value: { lifecycle: "completed", workflowExecutionState: "running", grant: null, unreviewedDecisionCount: 1 }, + }); + }); + + test("rejects malformed status counts, lifecycle, grant scope, and autonomy state", () => { + const malformed = [ + activeStatusInput({ intentUuid: COMPLETED }), + activeStatusInput({ decisionPolicyCount: -1 }), + activeStatusInput({ decisionCounts: { total: 1, unreviewed: 1, accepted: 1, flagged: 0 } }), + ]; + for (const input of malformed) { + expect(projectMachineReviewStatus(input)).toMatchObject({ + ok: false, + error: { code: "MALFORMED", locus: "statusInput" }, + }); + } + + expect(projectMachineReviewStatus(activeStatusInput({ + autonomy: { ...createAutonomyProjection({ intentUuid: ACTIVE }), mode: "full" }, + }))).toMatchObject({ ok: false, error: { code: "ILLEGAL_STATE", locus: "autonomy" } }); + expect(projectMachineReviewStatus(activeStatusInput({ lifecycle: "completed" }))).toMatchObject({ + ok: false, + error: { code: "ILLEGAL_STATE", locus: "lifecycle" }, + }); + + const full = fullStatusInput(); + expect(projectMachineReviewStatus({ ...full, currentGrantScope: null })).toMatchObject({ + ok: false, + error: { code: "ILLEGAL_STATE", locus: "grantScope" }, + }); + expect(projectMachineReviewStatus(activeStatusInput({ currentGrantScope: full.currentGrantScope }))).toMatchObject({ + ok: false, + error: { code: "ILLEGAL_STATE", locus: "grantScope" }, + }); + }); + + test("renders the human status from a valid full grant without leaking unsafe detail", () => { + const input = fullStatusInput(); + const machine = projectMachineReviewStatus(input); + expect(machine).toMatchObject({ + ok: true, + value: { + intentUuid: ACTIVE, + lifecycle: "active", + autonomyMode: "full", + grant: { state: "active", scope: { selfScopeId: "self-feature" } }, + decisionPolicyCount: 1, + decisionCount: 3, + unreviewedDecisionCount: 1, + acceptedDecisionCount: 1, + flaggedDecisionCount: 1, + }, + }); + + const human = projectHumanReviewStatus(input); + expect(human).toMatchObject({ ok: true }); + if (!human.ok) return; + expect(human.value).toContain(`Intent: ${ACTIVE} (active)`); + expect(human.value).toContain("自律レベル: full"); + expect(human.value).toContain("ワークフロー: running"); + expect(human.value).toContain("grant scope: self-feature"); + expect(human.value).toContain("事前裁定方針: 1"); + expect(human.value).toContain("自動裁定: 3 (未確認 1, accept 1, flag 1)"); + expect(human.value).toContain("停止理由: なし"); + expect(human.value).not.toContain("Prefer the accepted option"); + + expect(projectHumanReviewStatus({ ...input, decisionPolicyCount: -1 })).toMatchObject({ + ok: false, + error: { code: "MALFORMED", locus: "statusInput" }, + }); + }); + + test("projects only safe registry/OTel provenance", () => { + const attributes = projectReviewTelemetry({ + intentUuid: COMPLETED, + decisionId: "decision-completed", + reviewId: "review-1", + choice: "flag", + lifecycleAtReview: "completed", + reviewPrincipalRef: "human-1", + reviewActorRef: "human-1", + sourceHumanTurnId: "turn-1", + decisionPrincipalRef: "principal-1", + decisionActorRef: "core-engine-1", + decisionSource: "solo-election", + safeBasisDigest: autonomyDigest("basis"), + grantId: "grant-1", + safeNoteDigest: autonomyDigest("note"), + redactionStatus: "redacted", + auditTransactionId: "transaction-1", + traceId: "trace-1", + spanId: "span-1", + }); + expect(attributes["amadeus.review.choice"]).toBe("flag"); + expect(JSON.stringify(attributes)).not.toContain("question"); + expect(JSON.stringify(attributes)).not.toContain("credential"); + }); + + test("requires each current harness exactly once without harness-specific algorithms", () => { + const receipts = REQUIRED_REVIEW_HARNESSES.map((harnessId) => ({ + harnessId, + fixtureId: "fixture-1", + contractRevision: autonomyDigest("contract-v1"), + passed: true, + })); + expect(evaluateReviewHarnessSuite("fixture-1", autonomyDigest("contract-v1"), receipts)).toMatchObject({ + ok: true, + value: { passed: true, requiredHarnesses: REQUIRED_REVIEW_HARNESSES }, + }); + expect(evaluateReviewHarnessSuite("fixture-1", autonomyDigest("contract-v1"), receipts.slice(1))).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "requiredHarnesses" }, + }); + expect(evaluateReviewHarnessSuite("fixture-2", autonomyDigest("contract-v1"), receipts)).toMatchObject({ + ok: false, + error: { code: "CONFLICT", locus: "contractRevision" }, + }); + }); +}); + +describe("review flag metadata normalization", () => { + test("accept forces explicit nulls; flag defaults the classification and keeps the note digest", () => { + expect(normalizeReviewFlagMetadata({ choice: "accept", flagClassification: "contract-defect", noteDigest: autonomyDigest("n") })) + .toEqual({ flagClassification: null, safeNoteDigest: null }); + expect(normalizeReviewFlagMetadata({ choice: "flag" })) + .toEqual({ flagClassification: "unspecified", safeNoteDigest: null }); + expect(normalizeReviewFlagMetadata({ choice: "flag", flagClassification: "contract-defect", noteDigest: autonomyDigest("n") })) + .toEqual({ flagClassification: "contract-defect", safeNoteDigest: autonomyDigest("n") }); + }); +}); + +describe("projection event-set digest contract", () => { + test("orders entries by closed event type then event id and dedupes identical pairs", () => { + // Two decisions whose ids invert insertion order; the digest embedded in + // the page cursor must equal the CONTRACT ordering (type order first, then + // event id bytes) recomputed here — never a sort of the entry digests. + const first = decision(ACTIVE, "zz"); + const second = decision(ACTIVE, "aa"); + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [first, second])], + }); + const page = service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", pageSize: 1 }); + if (!page.ok) throw new Error(JSON.stringify(page.error)); + if (page.value.nextCursor === null) throw new Error("expected a next cursor"); + const entry = (record: AutoDecisionRecord) => { + const payloadDigest = canonicalContractValueDigest("auto-decision", record); + return canonicalTupleDigest("amadeus.decision-projection-event.v1", [ + { tag: "event-type", value: "AUTO_DECIDED" }, + { tag: "event-id", value: record.decisionId }, + { tag: "decision-payload-digest", value: payloadDigest.ok ? payloadDigest.value : null }, + { tag: "subject-payload-digest", value: canonicalTupleDigest("amadeus.decision-subject.v1", [ + { tag: "principal", value: record.principalId }, + { tag: "actor", value: record.actorId }, + ]) }, + { tag: "review-payload-digest", value: null }, + ]); + }; + const expected = canonicalTupleDigest("amadeus.decision-projection-event-set.v1", [ + { tag: "target-intent", value: ACTIVE }, + { tag: "event-count", value: "2" }, + { tag: "event", value: entry(second) }, + { tag: "event", value: entry(first) }, + ]); + expect(page.value.nextCursor.projectionEventSetDigest).toBe(expected); + }); + + test("closes a decision whose canonical digest fails as a projection-set MALFORMED", () => { + const undigestable = { ...decision(ACTIVE, "bad"), degradedCapability: undefined } as unknown as AutoDecisionRecord; + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [undigestable])], + }); + const page = service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", pageSize: 10 }); + expect(page.ok).toBe(false); + if (page.ok) return; + expect(page.error).toMatchObject({ code: "MALFORMED", locus: "projectionEventSet" }); + }); + + test("dedupes a duplicated event id with identical payloads to one entry", () => { + const twin = decision(ACTIVE, "dup"); + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [decision(ACTIVE, "dup"), twin])], + }); + const page = service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", pageSize: 1 }); + if (!page.ok) throw new Error(JSON.stringify(page.error)); + if (page.value.nextCursor === null) throw new Error("expected a next cursor"); + const record = decision(ACTIVE, "dup"); + const payloadDigest = canonicalContractValueDigest("auto-decision", record); + const entry = canonicalTupleDigest("amadeus.decision-projection-event.v1", [ + { tag: "event-type", value: "AUTO_DECIDED" }, + { tag: "event-id", value: record.decisionId }, + { tag: "decision-payload-digest", value: payloadDigest.ok ? payloadDigest.value : null }, + { tag: "subject-payload-digest", value: canonicalTupleDigest("amadeus.decision-subject.v1", [ + { tag: "principal", value: record.principalId }, + { tag: "actor", value: record.actorId }, + ]) }, + { tag: "review-payload-digest", value: null }, + ]); + const expected = canonicalTupleDigest("amadeus.decision-projection-event-set.v1", [ + { tag: "target-intent", value: ACTIVE }, + { tag: "event-count", value: "1" }, + { tag: "event", value: entry }, + ]); + expect(page.value.nextCursor.projectionEventSetDigest).toBe(expected); + }); + + test("closes a duplicated event id with diverging payloads as a projection-set CONFLICT", () => { + const clash = { ...decision(ACTIVE, "dup"), principalId: "principal-2" }; + const service = createMemoryAutonomyReviewService({ + intents: [seed(ACTIVE, "active", [decision(ACTIVE, "dup"), clash])], + }); + const page = service.listAutoDecisions({ intentUuid: ACTIVE, lifecycle: "active", pageSize: 10 }); + expect(page.ok).toBe(false); + if (page.ok) return; + expect(page.error).toMatchObject({ code: "CONFLICT", locus: "projectionEventSet" }); + }); +}); + +describe("production review projection", () => { + test("rejects a stored review whose consumed HUMAN_TURN is missing", () => { + const projectDir = setupIntegrationProject({ withState: "state-construction.md", stripEnvScope: true }); + try { + const payload = { + targetIntentUuid: DEFAULT_INTENT_UUID, + decisionId: "decision-prior", + reviewId: "review-prior", + choice: "accept", + auditTransactionId: "review-transaction-prior", + receiptProjectionRevision: 1, + lifecycleAtReview: "active", + reviewPrincipalRef: "human-1", + reviewActorRef: "human-1", + decisionPrincipalRef: "principal-1", + decisionActorRef: "core-engine-1", + decisionSource: "solo-election", + safeBasisDigest: autonomyDigest("prior-review-basis"), + grantId: null, + sourceIntentUuid: DEFAULT_INTENT_UUID, + sourceHumanTurnId: "missing-human-turn", + sourceHumanTurnEventId: "missing-human-event", + commandOccurrenceId: "review-accept-prior", + commandBindingDigest: autonomyDigest("prior-review-binding"), + remediation: null, + flagClassification: null, + safeNoteDigest: null, + redactionStatus: "redacted", + }; + const payloadDigest = canonicalContractValueDigest("auto-decision-reviewed-payload", payload); + if (!payloadDigest.ok) throw new Error(payloadDigest.error.code); + emitAuditEventGuarded("AUTO_DECISION_REVIEWED", { + "Intent Uuid": DEFAULT_INTENT_UUID, + "Decision Id": payload.decisionId, + "Review Id": payload.reviewId, + Choice: payload.choice, + Lifecycle: payload.lifecycleAtReview, + "Review Principal": payload.reviewPrincipalRef, + "Review Actor": payload.reviewActorRef, + "Source Human Turn": payload.sourceHumanTurnId, + "Audit Transaction Id": payload.auditTransactionId, + "Payload Digest": payloadDigest.value, + "Payload V1": JSON.stringify(payload), + }, projectDir, DEFAULT_RECORD_DIR, "default"); + + expect(commitProductionDecisionReview({ + projectDir, + decisionId: "decision-next", + choice: "accept", + confirmedContentDigest: reviewCommandContentDigest({ + targetIntentUuid: DEFAULT_INTENT_UUID, + decisionId: "decision-next", + choice: "accept", + flagClassification: null, + safeNoteDigest: null, + }), + })).toEqual({ ok: false, error: "PROVENANCE_REQUIRED" }); + } finally { + resetOtelPerProject(); + cleanupTestProject(projectDir); + } + }); + + test("replays a completed review by stable selectors and fails closed on malformed audit payloads", () => { + const projectDir = setupIntegrationProject({ withState: "state-construction.md", stripEnvScope: true }); + try { + emitAuditEventGuarded("HUMAN_TURN", {}, projectDir, DEFAULT_RECORD_DIR, "default"); + const preview = previewProductionAutonomyGrant({ projectDir, stateContent: "" }); + if (!preview.ok) throw new Error(preview.error); + expect(applyProductionAutonomyMode({ + projectDir, + stateContent: "", + mode: "full", + confirmedDisplayDigest: preview.preview.displayDigest, + })).toMatchObject({ ok: true, projection: { mode: "full" } }); + + const decided = commitProductionQuestionDecision({ + projectDir, + stage: "code-generation", + phase: "construction", + graphRevision: `sha256:${"a".repeat(64)}`, + questionId: "review-question", + selector: "review-selector", + question: "Which reviewed option should be selected?", + optionIds: ["accept", "reject"], + recommendedOptionId: "accept", + election: { optionId: "accept", evidenceFingerprint: `sha256:${"b".repeat(64)}` }, + }); + expect(decided.kind).toBe("decided"); + const decidedLater = commitProductionQuestionDecision({ + projectDir, + stage: "code-generation", + phase: "construction", + graphRevision: `sha256:${"a".repeat(64)}`, + questionId: "post-seal-review-question", + selector: "post-seal-review-selector", + question: "Which post-seal option should be selected?", + optionIds: ["accept", "reject"], + recommendedOptionId: "accept", + election: { optionId: "accept", evidenceFingerprint: `sha256:${"d".repeat(64)}` }, + }); + expect(decidedLater.kind).toBe("decided"); + + const unreviewed = listProductionAutoDecisions({ projectDir, reviewState: "unreviewed" }); + if (!unreviewed.ok) throw new Error(unreviewed.error); + const decisionId = unreviewed.page.items.find((item) => item.safeQuestion?.includes("reviewed option"))?.decisionId; + const laterDecisionId = unreviewed.page.items.find((item) => item.safeQuestion?.includes("post-seal option"))?.decisionId; + if (decisionId === undefined || laterDecisionId === undefined) throw new Error("expected two production decisions"); + + // OBS-R09/OBS-R12: a fresh human turn does not authorize arbitrary + // caller-supplied content — the confirmation digest must match. + expect(commitProductionDecisionReview({ + projectDir, + decisionId, + choice: "accept", + confirmedContentDigest: `sha256:${"9".repeat(64)}`, + })).toEqual({ ok: false, error: "PROVENANCE_REQUIRED:confirmed-content-digest-mismatch" }); + expect(commitProductionDecisionReview({ + projectDir, + decisionId, + choice: "accept", + confirmedContentDigest: reviewCommandContentDigest({ + targetIntentUuid: DEFAULT_INTENT_UUID, + decisionId, + choice: "accept", + flagClassification: null, + safeNoteDigest: null, + }), + })).toMatchObject({ + ok: true, + receipt: { state: "accepted", remediation: null }, + }); + + const completionSealDigest = `sha256:${"c".repeat(64)}`; + // Raw shard append: INTENT_COMPLETION_TRANSACTION_COMMITTED is Bolt 5 + // vocabulary the registry does not carry yet; the review production path + // only READS the seal off the shards, so the fixture seeds the row + // directly instead of routing it through the registry-guarded emitter. + appendFileSync( + join(projectDir, "amadeus", "spaces", "default", "intents", DEFAULT_RECORD_DIR, "audit", "review-completion-seed.jsonl"), + `${JSON.stringify({ + schemaVersion: 1, + seq: 1, + cloneId: "review-completion-seed", + intentId: DEFAULT_RECORD_DIR, + timestamp: new Date().toISOString(), + heading: "Intent Completion Transaction Committed", + event: "INTENT_COMPLETION_TRANSACTION_COMMITTED", + fields: { + "Intent Uuid": DEFAULT_INTENT_UUID, + "Transaction Id": "review-completion-transaction", + "Evidence Id": "review-completion-evidence", + "Evidence Digest": completionSealDigest, + "Completion Seal Digest": completionSealDigest, + Transaction: "{}", + }, + })}\n`, + ); + expect(withLockedIntentRegistry( + projectDir, + (context) => transitionIntentStatusLocked(context, DEFAULT_RECORD_DIR, "complete"), + "default", + )).toBe(true); + + // A completed intent is no longer an active review SOURCE: the command + // must fail closed rather than mint a review from a sealed lifecycle. + expect(commitProductionDecisionReview({ + projectDir, + decisionId, + choice: "accept", + confirmedContentDigest: reviewCommandContentDigest({ + targetIntentUuid: DEFAULT_INTENT_UUID, + decisionId, + choice: "accept", + flagClassification: null, + safeNoteDigest: null, + }), + })).toEqual({ + ok: false, + error: "active-source-and-review-target-required", + }); + + // OBS-R08: with a NEW active source intent, the completed target stays + // reviewable — the row lands on the SOURCE journal (the target's is + // sealed by #1248) and the union read re-attributes it to the target. + const birth = spawnSync( + process.execPath, + [join(projectDir, ".claude", "tools", "amadeus-utility.ts"), "intent-birth", "--scope", "feature", "--project-dir", projectDir], + { cwd: projectDir, encoding: "utf8", env: { ...process.env } }, + ); + expect(birth.status).toBe(0); + const sourceDir = readFileSync( + join(projectDir, "amadeus", "spaces", "default", "intents", "active-intent"), + "utf8", + ).trim(); + expect(sourceDir).not.toBe(DEFAULT_RECORD_DIR); + emitAuditEventGuarded("HUMAN_TURN", {}, projectDir, sourceDir, "default"); + const targetShardBefore = readAllAuditShards(projectDir, DEFAULT_RECORD_DIR, "default"); + expect(commitProductionDecisionReview({ + projectDir, + intent: DEFAULT_INTENT_UUID, + decisionId: laterDecisionId, + choice: "accept", + confirmedContentDigest: reviewCommandContentDigest({ + targetIntentUuid: DEFAULT_INTENT_UUID, + decisionId: laterDecisionId, + choice: "accept", + flagClassification: null, + safeNoteDigest: null, + }), + })).toMatchObject({ ok: true, receipt: { state: "accepted" } }); + expect(readAllAuditShards(projectDir, DEFAULT_RECORD_DIR, "default")).toBe(targetShardBefore); + expect( + readAllAuditShards(projectDir, sourceDir, "default").includes("AUTO_DECISION_REVIEWED"), + ).toBe(true); + + // A sibling journal's garbage review row that does NOT name this target + // says nothing about it — the union read skips it and the surface stays + // up. A malformed row that DOES name the target stays fail-closed. + const malformedSeed = (marker: string) => `${JSON.stringify({ + schemaVersion: 1, + seq: 99, + cloneId: `malformed-${marker}`, + intentId: sourceDir, + timestamp: new Date().toISOString(), + heading: "Auto Decision Reviewed", + event: "AUTO_DECISION_REVIEWED", + fields: { "Payload Digest": autonomyDigest(marker), "Payload V1": `{"marker":"${marker}"}` }, + })}\n`; + appendFileSync( + join(projectDir, "amadeus", "spaces", "default", "intents", sourceDir, "audit", "malformed-foreign.jsonl"), + malformedSeed("foreign-target"), + ); + expect(listProductionAutoDecisions({ + projectDir, + intent: DEFAULT_INTENT_UUID, + reviewState: "accepted", + })).toMatchObject({ ok: true }); + appendFileSync( + join(projectDir, "amadeus", "spaces", "default", "intents", sourceDir, "audit", "malformed-named.jsonl"), + malformedSeed(`names-${DEFAULT_INTENT_UUID}`), + ); + expect(listProductionAutoDecisions({ + projectDir, + intent: DEFAULT_INTENT_UUID, + reviewState: "accepted", + })).toMatchObject({ ok: false, error: "invalid-review-audit-payload" }); + unlinkSync(join(projectDir, "amadeus", "spaces", "default", "intents", sourceDir, "audit", "malformed-named.jsonl")); + expect(listProductionAutoDecisions({ + projectDir, + intent: DEFAULT_INTENT_UUID, + reviewState: "accepted", + })).toMatchObject({ + ok: true, + page: { items: [expect.objectContaining({ decisionId }), expect.objectContaining({ decisionId: laterDecisionId })] }, + }); + + expect(getProductionAutoDecision({ projectDir, intent: DEFAULT_RECORD_DIR, decisionId })).toMatchObject({ + ok: true, + detail: { decisionId, reviewState: "accepted", reviewReceipt: { state: "accepted" } }, + }); + expect(listProductionAutoDecisions({ projectDir, intent: "fixture", reviewState: "accepted" })) + .toMatchObject({ ok: true, page: { items: [expect.objectContaining({ decisionId }), expect.objectContaining({ decisionId: laterDecisionId })] } }); + + expect(withLockedIntentRegistry( + projectDir, + (context) => transitionIntentStatusLocked(context, DEFAULT_RECORD_DIR, "archive"), + "default", + )).toBe(true); + expect(withLockedIntentRegistry( + projectDir, + (context) => transitionIntentStatusLocked(context, DEFAULT_RECORD_DIR, "unarchive"), + "default", + )).toBe(true); + emitAuditEventGuarded("AUTO_DECISION_REVIEWED", { + "Intent Uuid": DEFAULT_INTENT_UUID, + "Decision Id": decisionId, + "Review Id": "review-malformed", + Choice: "accept", + Lifecycle: "active", + "Review Principal": "human-1", + "Review Actor": "human-1", + "Source Human Turn": "turn-malformed", + "Audit Transaction Id": "review-transaction-malformed", + "Payload Digest": autonomyDigest("malformed-review-payload"), + "Payload V1": "{", + }, projectDir, DEFAULT_RECORD_DIR, "default"); + expect(listProductionAutoDecisions({ projectDir, intent: DEFAULT_INTENT_UUID })).toMatchObject({ ok: false }); + expect(getProductionAutoDecision({ projectDir, intent: DEFAULT_INTENT_UUID, decisionId })).toMatchObject({ ok: false }); + expect(commitProductionDecisionReview({ + projectDir, + decisionId, + choice: "accept", + confirmedContentDigest: reviewCommandContentDigest({ + targetIntentUuid: DEFAULT_INTENT_UUID, + decisionId, + choice: "accept", + flagClassification: null, + safeNoteDigest: null, + }), + })).toMatchObject({ ok: false }); + } finally { + resetOtelPerProject(); + cleanupTestProject(projectDir); + } + }); +}); + +describe("observability configuration boundary", () => { + test("fails closed when an untyped caller supplies an unreadable project root", () => { + resetObservabilityConfigCache(); + expect(resolveObservabilityConfig(null as unknown as string)).toEqual({ + enabled: false, + localExport: false, + redactionOptIn: [], + }); + resetObservabilityConfigCache(); + }); +}); diff --git a/tests/integration/t435-intent-autonomy-production.integration.test.ts b/tests/integration/t435-intent-autonomy-production.integration.test.ts index 5450b461ee..3819b81bc8 100644 --- a/tests/integration/t435-intent-autonomy-production.integration.test.ts +++ b/tests/integration/t435-intent-autonomy-production.integration.test.ts @@ -19,6 +19,8 @@ import { resumeProductionQuality, } from "../../packages/framework/core/tools/amadeus-intent-autonomy-production.ts"; import { main as boltMain } from "../../packages/framework/core/tools/amadeus-bolt.ts"; +import { listProductionAutoDecisions } from "../../packages/framework/core/tools/amadeus-autonomy-review-production.ts"; +import { reviewCommandContentDigest } from "../../packages/framework/core/tools/amadeus-autonomy-review.ts"; import { runUtilityMain } from "../../packages/framework/core/tools/amadeus-utility.ts"; const BUN = process.execPath; @@ -789,6 +791,30 @@ describe("Intent-scoped autonomy production path", () => { })); boltMain(["--project-dir", projectDir, "observe-quality", "--input", observationPath]); expect(readProductionAutonomyProjection(projectDir)?.workflowExecutionState).toBe("running"); + + boltMain(["--project-dir", projectDir, "list-auto-decisions", "--state", "unreviewed"]); + const unreviewed = listProductionAutoDecisions({ projectDir, reviewState: "unreviewed" }); + if (!unreviewed.ok) throw new Error(unreviewed.error); + const decisionId = unreviewed.page.items[0]?.decisionId; + if (decisionId === undefined) throw new Error("expected an unreviewed production decision"); + boltMain(["--project-dir", projectDir, "get-auto-decision", "--decision", decisionId]); + boltMain(["--project-dir", projectDir, "get-auto-decision", "--decision", decisionId, "--choice", "accept"]); + appendLedgerEvent(projectDir, "HUMAN_TURN"); + const confirmed = reviewCommandContentDigest({ + targetIntentUuid: readProductionAutonomyProjection(projectDir)!.intentUuid, + decisionId, + choice: "accept", + flagClassification: null, + safeNoteDigest: null, + }); + boltMain([ + "--project-dir", projectDir, + "review-auto-decision", "--decision", decisionId, + "--choice", "accept", "--confirmed-review-digest", confirmed, + ]); + const accepted = listProductionAutoDecisions({ projectDir, reviewState: "accepted" }); + if (!accepted.ok) throw new Error(accepted.error); + expect(accepted.page.items.map((item) => item.decisionId)).toContain(decisionId); }, 60_000); test("utility status renders the autonomy projection in-process", () => { diff --git a/tests/no-silent-drop/baseline.json b/tests/no-silent-drop/baseline.json index 6bf65343df..d77175920e 100644 --- a/tests/no-silent-drop/baseline.json +++ b/tests/no-silent-drop/baseline.json @@ -2,10 +2,10 @@ "schemaVersion": 1, "direction": "shrink-only", "generatedFrom": { - "revision": "ddb8ae1cd8291b07b3883ca7acad3db047bcb280", + "revision": "7034aac35acfac1d00b98bc8d6fd8e3c1b2804c6", "censusDigest": "b6bf74ffc7810e2a619424bf3992eeeab321b511f9fe8cce9876808b5ea34c2c", "approvalDigest": "0e92854570e9dacf4664fd19ff7074b8538b32fd59283d1d48252925015ca6df", - "previousDigest": "01fb53ee19d64aa302568693ad94d27aba2f2a6ec1155bd0849f5b89e9dd228b" + "previousDigest": "1041a18e3d1ceb0f624d2131f585945bf27933585421c8291e3688bde5e60c08" }, "entries": [ { diff --git a/tests/no-silent-drop/exemptions.json b/tests/no-silent-drop/exemptions.json index fb42447a1c..02fed07c83 100644 --- a/tests/no-silent-drop/exemptions.json +++ b/tests/no-silent-drop/exemptions.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, - "previousDigest": "ef6b61d3966f0fc878b610a2e3c562ff93b51b427010b69f0979268a5cd86408", + "previousDigest": "e686c8f6b5ffac9a3abe37141b1310d501ac2f5dd3a3a05c6324eb691bd6e280", "entries": [] } diff --git a/tests/unit/t28-audit-event-sync.test.ts b/tests/unit/t28-audit-event-sync.test.ts index f4567a87f0..9b18734b8b 100644 --- a/tests/unit/t28-audit-event-sync.test.ts +++ b/tests/unit/t28-audit-event-sync.test.ts @@ -77,7 +77,8 @@ const AUDIT_MD = join(AMADEUS_SRC, "knowledge", "amadeus-shared", "audit-format. // +LOOP_MONITOR_EVENT_SET_COMMITTED (#2095) takes it to 86; // +QUALITY_REPAIR_TRANSACTION_COMMITTED (#2096) takes it to 87; // +INTENT_AUTONOMY_TRANSACTION_COMMITTED (#2067) takes it to 88. -const CANONICAL_COUNT = 88; +// +AUTO_DECISION_REVIEWED (#2067 review surface) takes it to 89. +const CANONICAL_COUNT = 89; /** Slice the lines of `text` BETWEEN the first line matching `start` and the * next line matching `end` (inclusive of both), reproducing `sed -n @@ -180,7 +181,7 @@ describe("t28 audit event-type sync (migrated from t28-audit-event-sync.sh, plan // .sh test 7: assert_eq TS_COUNT - the canonical baseline pin, bumped when // events are added or removed. (#367 added WORKFLOW_PARKED/UNPARKED -> 69; // #369 removed TEST_RUN_MODE_ENABLED -> 68; HUMAN_TURN took it to 69; the adaptive composer added RECOMPOSED -> 70; #671 added DELEGATED_APPROVAL -> 71; #685 added DELEGATED_REJECTION -> 72; #499/#848 added GUARD_EXEMPTED -> 73; #1125 added GRANT_ISSUED + GRANT_REVOKED -> 75; lifecycle transactions add two -> 77.) - test("VALID_EVENT_TYPES.size === 88 (baseline pin) [.sh test 7]", () => { + test("VALID_EVENT_TYPES.size === 89 (baseline pin) [.sh test 7]", () => { expect(TS_EVENTS.length).toBe(CANONICAL_COUNT); }); }); diff --git a/tests/unit/t81.test.ts b/tests/unit/t81.test.ts index 1146326a00..1954f6c699 100644 --- a/tests/unit/t81.test.ts +++ b/tests/unit/t81.test.ts @@ -225,7 +225,8 @@ describe("t81 amadeus-state practices-event — bolt-plan-marker-conflict overri // plus LOOP_MONITOR_EVENT_SET_COMMITTED (durable Loop Monitor stream, +1) = 82, // plus the four Goal Lifecycle events (+4) = 86, // plus QUALITY_REPAIR_TRANSACTION_COMMITTED (#2096, +1) = 87, - // plus INTENT_AUTONOMY_TRANSACTION_COMMITTED (#2067, +1) = 88. + // plus INTENT_AUTONOMY_TRANSACTION_COMMITTED (#2067, +1) = 88, + // plus AUTO_DECISION_REVIEWED (#2067 review surface, +1) = 89. const auditSrc = readFileSync( join(REPO_ROOT, "dist", "claude", ".claude", "tools", "amadeus-audit.ts"), "utf-8", @@ -233,7 +234,7 @@ describe("t81 amadeus-state practices-event — bolt-plan-marker-conflict overri const block = auditSrc.match(/const VALID_EVENT_TYPES = new Set\(\[([\s\S]*?)\]\)/); expect(block).not.toBeNull(); const count = (block ? block[1].match(/"[A-Z0-9_]+"/g) : null)?.length ?? -1; - expect(count).toBe(88); + expect(count).toBe(89); }); // --- Test 4: milestone 8 write-failure path coexists (different Reason value) ---