Record the provenance producer triple on proposals and expose a board-authorized metadata endpoint - #2494
Conversation
…enance Adds nullable Provider/PromptVersion columns, stamps them from trusted capture-triage inputs, and exposes a proposal-scoped, board-authorized metadata endpoint so the Review provenance footnote can render server-recorded values. Refs #1987
…oint authorization Domain normalization/length rules, deterministic and live-provider stamping, origin-sentinel suppression, and owner/collaborator/stranger parity on the metadata endpoint. Refs #1987
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Review gate (Codex credits exhausted, SC-9): one fresh-context reviewer with an authz/trust/migration lens found the new endpoint byte-identical in gating to {id}/provenance (owner/collaborator 200, stranger parity incl. body-leak assertion, missing id 404), the [JsonIgnore] props unreachable from the single [FromBody] site (System.Text.Json only, no Newtonsoft in src), the migration purely additive nullable, and layering clean. Verdict SHIP. MEDIUM (pre-migration LLM rows with a real ModelId but null Provider now render silent) plus two LOWs (only capture triage stamps the triple; BOM on four test files) tracked in #2499. STATUS.md entry lands in the coordinator's block PR. Merging once CI at this head is green and main's in-progress run completes. |
Summary
Backend half of #1987: record the provenance triple (provider / model / promptVersion) on the proposal itself and expose it through a proposal-scoped, board-authorized endpoint, so the Paper Review provenance footnote and drawer can render server-recorded values instead of nothing.
Before this change the triple existed only on the capture payload.
ProposalProvenancestoredCorrelationId/ModelId/TotalTokensand nothing else, andGET /automation/proposals/{id}/provenancereturned row DTOs only — so PR #2310's frontend path could only reach producer metadata through the owner-only capture-detail endpoint, and Chat-origin proposals and non-owner reviewers got nothing at all.ProposalProvenancegains nullableProviderandPromptVersion(64 chars each, blank normalized to null, over-long rejected at the domain boundary).20260904030926_AddProposalProvenanceProducerTriple: two additive nullableTEXTcolumns onProposalProvenances. No rebuild, no backfill, no data change for existing rows.CreateProposalDtogainsProvenanceProvider/ProvenancePromptVersionas[JsonIgnore]init-only properties — the same trusted-input pattern asTrustedConfidence, so a client cannot label its own proposal as produced by a provider. OnlyCaptureTriageServicesets them, from the producer that actually ran (dispatched provider, or the deterministic extractor on fallback).AutomationProposalService.BuildCreationProvenancestamps them server-side.ProposalProvenanceMetadataDto+IProvenanceQueryService.GetProvenanceMetadataAsync+GET /automation/proposals/{id}/provenance/metadata, sharing the exact board read authorization and 404 parity of the sibling{id}/provenanceroute.UPGRADING.mdUnreleased entry:BREAKING: none.The contract fails closed.
Provideris null whenever no producer was recorded — legacy rows, and Chat/Manual origins whoseModelIdcarries only an origin sentinel (chat-tools,manual,queue).ModelandPromptVersionare reported only alongside a recorded provider, so an origin sentinel can never be rendered as a model name. A proposal with nothing recorded returns 200 with all-null fields, not an error: "not recorded" is an answer, and the surface renders it as no producer claim at all (#1963 — no claim beats a false claim).No frontend changes in this PR. The one-line source swap in
usePaperReviewSelectors.tsplus its API client and specs is deliberately deferred to a follow-up branch (issue-1987/provenance-frontend) to avoid colliding with the in-flight Review race-state work (#2455, #2457, #2458, #2460, #2461, #2464), which owns that file. That follow-up closes #1987 and #1284 AC4; this PR only makes it possible.Relationship to #2315
The new endpoint addresses two of the four states tracked there:
Test plan
Base
becd85fd4; head5a86e16f3(branchissue-1987/provenance-triple).dotnet build backend/Taskdeck.sln -c Releasedotnet ef migrations has-pending-model-changes --startup-project ../Taskdeck.Api/Taskdeck.Api.csprojdotnet test backend/tests/Taskdeck.Domain.Tests/Taskdeck.Domain.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~ProposalProvenanceTests"dotnet test backend/tests/Taskdeck.Application.Tests/Taskdeck.Application.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~CaptureTriage|FullyQualifiedName~Provenance|FullyQualifiedName~AutomationProposalService"dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~GetProposalProvenanceMetadata"dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~AutomationProposalsApiTests"dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~MigrationBootstrap"dotnet test backend/tests/Taskdeck.Architecture.Tests/Taskdeck.Architecture.Tests.csproj -c Release -m:1node scripts/check-docs-governance.mjsgit diff --check origin/main...HEADNew coverage: blank/over-long/trimmed producer values at the domain boundary; deterministic and live-provider stamping through
CreateProposalAsync; Chat-origin leaving the triple unrecorded; origin-sentinel suppression in the projection; owner 200, board-collaborator 200, stranger status equal to the sibling{id}/provenancestatus with no metadata leaked in the body, and unauthenticated rejection.Documentation
UPGRADING.md— Unreleased entry for the migration (BREAKING: none), perdocs/platform/EF_MIGRATION_WORKFLOW.md.docs/STATUS.md— intentionally untouched (coordinator-owned); shipped user-visible reality changes with the frontend follow-up, not with this contract.docs/IMPLEMENTATION_MASTERPLAN.md— no sequencing change.Risks
Downdrops them. Existing rows are untouched and read back as "not recorded". The pre-migration snapshot runs as usual and the copy cost is the normal per-schema-change one.AuthorizeProposalAsync(requireWriteAccess: false)verbatim and returns no capture, transcript, or card content — only three short recorded strings. Stranger parity with the sibling route is asserted in a test.CreateProposalDtoinputs are[JsonIgnore], so no HTTP caller can set them; the only writer is capture triage. Provenance remains server-stamped.Refs #1987
Refs #1284
Refs #2315