openapi: 3.1.0
info:
  title: Request-Scoped Derived State Logical Contract
  version: 0.1.0
  summary: Logical contract for resolving, reusing, and invalidating deterministic derived state within one request.
  description: |
    This contract is logical rather than transport-prescriptive. It documents the
    expected behavior of the internal request-scoped derived-state store used by
    existing presenter and resolver families. It does not add new external APIs
    and does not imply cross-request caching.
    Resolution and invalidation payloads use camelCase transport keys in this
    contract, while the runtime data model and JSON schema keep their internal
    snake_case field names. Implementations must normalize between the two
    shapes rather than treating them as separate contracts.
    Every future presenter or resolver family that wants to use the shared store
    must document its family key, scope-sensitive inputs, access pattern, and
    freshness policy through the consumer-validation contract before adoption.
servers:
  - url: https://tenantpilot.local
x-derived-state-consumers:
  - surface: reviews.register.table
    family: artifact_truth
    variant: tenant_review
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Pages/Reviews/ReviewRegister.php
    requiredMarkers:
      - 'private function reviewTruth(TenantReview $record, bool $fresh = false): ArtifactTruthEnvelope'
      - '$this->reviewTruth($record)'
    maxOccurrences:
      - needle: '->forTenantReview('
        max: 1
      - needle: '->forTenantReviewFresh('
        max: 1
  - surface: monitoring.evidence_overview.table
    family: artifact_truth
    variant: evidence_snapshot
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Pages/Monitoring/EvidenceOverview.php
    requiredMarkers:
      - 'private function snapshotTruth(EvidenceSnapshot $snapshot, bool $fresh = false): ArtifactTruthEnvelope'
      - '$this->snapshotTruth($snapshot)'
    maxOccurrences:
      - needle: '->forEvidenceSnapshot('
        max: 1
      - needle: '->forEvidenceSnapshotFresh('
        max: 1
  - surface: tenant.evidence_snapshots.table
    family: artifact_truth
    variant: evidence_snapshot
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/EvidenceSnapshotResource.php
    requiredMarkers:
      - 'private static function truthEnvelope(EvidenceSnapshot $record, bool $fresh = false): ArtifactTruthEnvelope'
      - 'static::truthEnvelope($record)'
      - 'fresh: true'
    maxOccurrences:
      - needle: '->forEvidenceSnapshot('
        max: 1
      - needle: '->forEvidenceSnapshotFresh('
        max: 1
  - surface: tenant.tenant_reviews.table
    family: artifact_truth
    variant: tenant_review
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/TenantReviewResource.php
    requiredMarkers:
      - 'private static function truthEnvelope(TenantReview $record, bool $fresh = false): ArtifactTruthEnvelope'
      - 'static::truthEnvelope($record)'
      - 'static::truthEnvelope($review->refresh(), fresh: true);'
    maxOccurrences:
      - needle: '->forTenantReview('
        max: 1
      - needle: '->forTenantReviewFresh('
        max: 1
  - surface: tenant.review_packs.table
    family: artifact_truth
    variant: review_pack
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/ReviewPackResource.php
    requiredMarkers:
      - 'private static function truthEnvelope(ReviewPack $record, bool $fresh = false): ArtifactTruthEnvelope'
      - 'static::truthEnvelope($record)'
      - 'static::truthEnvelope($reviewPack->refresh(), fresh: true);'
    maxOccurrences:
      - needle: '->forReviewPack('
        max: 1
      - needle: '->forReviewPackFresh('
        max: 1
  - surface: admin.baseline_snapshots.truth
    family: artifact_truth
    variant: baseline_snapshot
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/BaselineSnapshotResource.php
    requiredMarkers:
      - 'private static function truthEnvelope(BaselineSnapshot $snapshot, bool $fresh = false): ArtifactTruthEnvelope'
      - 'self::truthEnvelope($record)'
    maxOccurrences:
      - needle: '->forBaselineSnapshot('
        max: 1
      - needle: '->forBaselineSnapshotFresh('
        max: 1
  - surface: admin.baseline_snapshots.primary_navigation
    family: related_navigation_primary
    variant: baseline_snapshot
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - user_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/BaselineSnapshotResource.php
    requiredMarkers:
      - 'private static function primaryRelatedEntry(BaselineSnapshot $record): ?RelatedContextEntry'
      - 'static::primaryRelatedEntry($record)'
    maxOccurrences:
      - needle: '->primaryListAction(CrossResourceNavigationMatrix::SOURCE_BASELINE_SNAPSHOT, $record)'
        max: 1
  - surface: tenant.findings.primary_navigation
    family: related_navigation_primary
    variant: finding
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
      - active_tenant_id
      - user_id
      - route_name
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/FindingResource.php
    requiredMarkers:
      - 'private static function primaryRelatedEntry(Finding $record, bool $fresh = false): ?RelatedContextEntry'
      - 'static::primaryRelatedEntry($record)'
    maxOccurrences:
      - needle: '->primaryListAction(CrossResourceNavigationMatrix::SOURCE_FINDING, $record)'
        max: 1
      - needle: '->primaryListActionFresh(CrossResourceNavigationMatrix::SOURCE_FINDING, $record)'
        max: 1
      - needle: 'primaryRelatedEntryCache'
        max: 0
  - surface: tenant.policy_versions.header_navigation
    family: related_navigation_primary
    variant: policy_version
    accessPattern: page_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
      - active_tenant_id
      - user_id
      - route_name
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php
    requiredMarkers:
      - 'private function primaryRelatedEntry(bool $fresh = false): ?RelatedContextEntry'
      - '$this->primaryRelatedEntry()'
    maxOccurrences:
      - needle: '->primaryListAction(CrossResourceNavigationMatrix::SOURCE_POLICY_VERSION, $this->getRecord())'
        max: 1
      - needle: '->primaryListActionFresh(CrossResourceNavigationMatrix::SOURCE_POLICY_VERSION, $this->getRecord())'
        max: 1
  - surface: admin.operations.table_guidance
    family: operation_ux_guidance
    variant: surface_guidance
    accessPattern: row_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/OperationRunResource.php
    requiredMarkers:
      - 'private static function surfaceGuidance(OperationRun $record, bool $fresh = false): ?string'
      - 'private static function lifecycleAttentionSummary(OperationRun $record, bool $fresh = false): ?string'
      - 'static::surfaceGuidance($record)'
    maxOccurrences:
      - needle: 'OperationUxPresenter::surfaceGuidance('
        max: 1
      - needle: 'OperationUxPresenter::surfaceGuidanceFresh('
        max: 1
      - needle: 'OperationUxPresenter::lifecycleAttentionSummary('
        max: 1
      - needle: 'OperationUxPresenter::lifecycleAttentionSummaryFresh('
        max: 1
  - surface: admin.operations.detail_related_context
    family: related_navigation_detail
    variant: operation_run
    accessPattern: page_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
      - active_tenant_id
      - user_id
      - route_name
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Resources/OperationRunResource.php
    requiredMarkers:
      - 'private static function relatedContextEntries(OperationRun $record, bool $fresh = false): array'
      - 'CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN'
    maxOccurrences:
      - needle: '->detailEntries(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $record)'
        max: 1
      - needle: '->detailEntriesFresh(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $record)'
        max: 1
  - surface: admin.operations.viewer_explanation
    family: operation_ux_explanation
    variant: governance_operator_explanation
    accessPattern: page_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Pages/Operations/TenantlessOperationRunViewer.php
    requiredMarkers:
      - 'private function governanceOperatorExplanation(): ?OperatorExplanationPattern'
      - 'OperationUxPresenter::governanceOperatorExplanation($this->run);'
    maxOccurrences:
      - needle: 'OperationUxPresenter::governanceOperatorExplanation('
        max: 1
      - needle: 'ArtifactTruthPresenter::class)->forOperationRun('
        max: 0
  - surface: admin.operations.viewer_related_links
    family: related_navigation_detail
    variant: operation_run
    accessPattern: page_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
      - active_tenant_id
      - user_id
      - route_name
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Pages/Operations/TenantlessOperationRunViewer.php
    requiredMarkers:
      - 'private function relatedLinks(bool $fresh = false): array'
      - '$resolver->operationLinks($this->run, $this->relatedLinksTenant())'
    maxOccurrences:
      - needle: '->operationLinks($this->run, $this->relatedLinksTenant())'
        max: 1
      - needle: '->operationLinksFresh($this->run, $this->relatedLinksTenant())'
        max: 1
  - surface: tenant.dashboard.baseline_governance
    family: tenant_governance_aggregate
    variant: tenant_governance_summary
    accessPattern: widget_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Widgets/Dashboard/BaselineCompareNow.php
    requiredMarkers:
      - 'private function governanceAggregate(Tenant $tenant): TenantGovernanceAggregate'
      - '$this->governanceAggregate($tenant)'
      - 'summaryAssessment'
    maxOccurrences:
      - needle: 'BaselineCompareStats::forTenant('
        max: 0
  - surface: tenant.banner.baseline_compare_coverage
    family: tenant_governance_aggregate
    variant: tenant_governance_summary
    accessPattern: widget_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Widgets/Tenant/BaselineCompareCoverageBanner.php
    requiredMarkers:
      - 'private function governanceAggregate(Tenant $tenant): TenantGovernanceAggregate'
      - '$this->governanceAggregate($tenant)'
      - 'nextActionUrl'
    maxOccurrences:
      - needle: 'BaselineCompareStats::forTenant('
        max: 0
  - surface: tenant.page.baseline_compare_landing
    family: tenant_governance_aggregate
    variant: tenant_governance_summary
    accessPattern: page_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Pages/BaselineCompareLanding.php
    requiredMarkers:
      - 'private function governanceAggregate(Tenant $tenant, BaselineCompareStats $stats): TenantGovernanceAggregate'
      - '$this->governanceAggregate($tenant, $stats)'
      - 'Compare now'
    maxOccurrences:
      - needle: 'BaselineCompareStats::forTenant('
        max: 1
      - needle: '$stats->summaryAssessment()'
        max: 0
  - surface: tenant.dashboard.needs_attention
    family: tenant_governance_aggregate
    variant: tenant_governance_summary
    accessPattern: widget_safe
    scopeInputs:
      - record_class
      - record_key
      - workspace_id
      - tenant_id
    freshnessPolicy: invalidate_after_mutation
    guardScope:
      - app/Filament/Widgets/Dashboard/NeedsAttention.php
    requiredMarkers:
      - 'private function governanceAggregate(Tenant $tenant): TenantGovernanceAggregate'
      - '$this->governanceAggregate($tenant)'
      - 'Baseline compare posture'
    maxOccurrences:
      - needle: 'Finding::query()'
        max: 0
      - needle: 'BaselineCompareStats::forTenant('
        max: 0
paths:
  /contracts/derived-state/resolve:
    post:
      summary: Resolve or reuse one deterministic derived-state result within the current request
      operationId: resolveDerivedState
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DerivedStateResolutionRequest'
      responses:
        '200':
          description: Derived-state value resolved, reused, or intentionally bypassed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DerivedStateResolutionResponse'
  /contracts/derived-state/invalidate:
    post:
      summary: Invalidate one or more request-local derived-state entries after a covered mutation
      operationId: invalidateDerivedState
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DerivedStateInvalidationRequest'
      responses:
        '200':
          description: Matching request-local entries invalidated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DerivedStateInvalidationResponse'
  /contracts/derived-state/validate-consumer:
    post:
      summary: Validate one UI consumer against the supported family, keying, and freshness rules
      description: |
        Use this logical validation step before onboarding a new presenter or
        resolver family or before replacing an existing local cache pattern.
        The consumer must declare its access pattern, scope inputs, and
        freshness policy so unsupported reuse never becomes implicit.
        The automated Pest guard for derived-state adoption should report
        violations from this validation step with file and snippet context.
      operationId: validateDerivedStateConsumer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DerivedStateConsumerValidationRequest'
      responses:
        '200':
          description: Consumer validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DerivedStateConsumerValidationResponse'
components:
  schemas:
    DerivedStateResolutionRequest:
      type: object
      additionalProperties: false
      required:
        - family
        - recordClass
        - recordKey
        - variant
      properties:
        family:
          type: string
          enum:
            - artifact_truth
            - operation_ux_guidance
            - operation_ux_explanation
            - related_navigation_primary
            - related_navigation_detail
            - related_navigation_header
        recordClass:
          type: string
          example: App\\Models\\TenantReview
        recordKey:
          type: string
          example: '42'
        variant:
          type: string
          example: list_row
        workspaceId:
          type:
            - integer
            - 'null'
        tenantId:
          type:
            - integer
            - 'null'
        contextHash:
          type:
            - string
            - 'null'
        allowNegativeResultCache:
          type: boolean
          default: true
        freshnessPolicy:
          type: string
          enum:
            - request_stable
            - invalidate_after_mutation
            - no_reuse
          default: request_stable
    DerivedStateResolutionResponse:
      type: object
      additionalProperties: false
      required:
        - cacheStatus
        - family
        - variant
        - negativeResult
        - freshnessPolicy
      properties:
        cacheStatus:
          type: string
          enum:
            - miss_resolved
            - hit_reused
            - bypassed
        family:
          type: string
        variant:
          type: string
        negativeResult:
          type: boolean
        freshnessPolicy:
          type: string
          enum:
            - request_stable
            - invalidate_after_mutation
            - no_reuse
        scopeFingerprint:
          type:
            - string
            - 'null'
        notes:
          type:
            - string
            - 'null'
    DerivedStateInvalidationRequest:
      type: object
      additionalProperties: false
      properties:
        family:
          type:
            - string
            - 'null'
        recordClass:
          type:
            - string
            - 'null'
        recordKey:
          type:
            - string
            - 'null'
        variant:
          type:
            - string
            - 'null'
        workspaceId:
          type:
            - integer
            - 'null'
        tenantId:
          type:
            - integer
            - 'null'
        reason:
          type: string
      required:
        - reason
    DerivedStateInvalidationResponse:
      type: object
      additionalProperties: false
      required:
        - invalidatedCount
      properties:
        invalidatedCount:
          type: integer
          minimum: 0
    DerivedStateConsumerValidationRequest:
      type: object
      description: |
        Adoption request for one UI consumer. New consumers should not use the
        shared store until family support, scope-sensitive inputs, access
        pattern, and freshness behavior are all explicit.
      additionalProperties: false
      required:
        - surface
        - family
        - variant
        - accessPattern
        - scopeInputs
        - freshnessPolicy
        - guardScope
      properties:
        surface:
          type: string
          example: reviews.register.table
        family:
          type: string
          description: Supported derived-state family name, or a proposed family under review for adoption.
        variant:
          type: string
          description: Stable variant identifier for the consumer path, such as `list_row` or `detail_page`.
        accessPattern:
          type: string
          enum:
            - row_safe
            - page_safe
            - direct_once
            - widget_safe
        scopeInputs:
          type: array
          description: Scope or capability inputs that affect the result for this consumer.
          items:
            type: string
        guardScope:
          type: array
          description: Source paths or helper seams the automated guard scans when validating this consumer.
          items:
            type: string
        mutationSensitive:
          type: boolean
          description: Advisory hint for the guard when post-mutation state changes require explicit freshness handling; does not replace `freshnessPolicy`.
          default: false
        capabilitySensitive:
          type: boolean
          description: Advisory hint for the guard when capability context changes the result; does not replace `scopeInputs`.
          default: false
        freshnessPolicy:
          type: string
          enum:
            - request_stable
            - invalidate_after_mutation
            - no_reuse
          default: request_stable
    DerivedStateConsumerValidationResponse:
      type: object
      additionalProperties: false
      required:
        - valid
        - violations
      properties:
        valid:
          type: boolean
        violations:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - code
              - message
            properties:
              code:
                type: string
                enum:
                  - missing_scope_context
                  - unsupported_family
                  - mutation_freshness_gap
                  - ad_hoc_local_cache
                  - unstable_variant_key
                  - missing_guard_scope
                  - missing_freshness_policy
              message:
                type: string
