## Summary - add a request-scoped derived-state store with deterministic keying and freshness controls - adopt the shared contract in ArtifactTruthPresenter, OperationUxPresenter, and RelatedNavigationResolver plus the covered Filament consumers - add spec, plan, contracts, guardrails, and focused memoization and freshness test coverage for spec 167 ## Verification - vendor/bin/sail artisan test --compact tests/Feature/078/RelatedLinksOnDetailTest.php - vendor/bin/sail artisan test --compact tests/Feature/078/ tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Verification/VerificationAuthorizationTest.php tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php tests/Feature/Verification/VerificationReportRedactionTest.php tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php tests/Feature/OpsUx/FailureSanitizationTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php - vendor/bin/sail bin pint --dirty --format agent ## Notes - Livewire v4.0+ compliance preserved - provider registration remains in bootstrap/providers.php - no Filament assets or panel registration changes - no global-search behavior changes - no destructive action behavior changes in this PR Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #198
572 lines
19 KiB
YAML
572 lines
19 KiB
YAML
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
|
|
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
|
|
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
|