Skip to content

feat: 006 finding occurrence and lineage lifecycle 구현 - #271

Merged
goodtu02 merged 4 commits into
devfrom
feat/270-006-finding-lineage
Jul 30, 2026
Merged

feat: 006 finding occurrence and lineage lifecycle 구현#271
goodtu02 merged 4 commits into
devfrom
feat/270-006-finding-lineage

Conversation

@goodtu02

@goodtu02 goodtu02 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🎋 작업 중인 브랜치 및 이슈 - 브랜치: feat/270-006-finding-lineage - 이슈: #270 - 선행 PR: #269 ## 🔎 주요 변경 사항 - sast-finding-lineage-v1, target-context, rename-attestation, T039-compatible lifecycle coverage 계약과 canonical digest/shape validator를 shared package에 추가했습니다. - T036 fingerprinted handoff와 tenant/repository/scan/attempt/scanner/plan/artifact/profile/retention binding을 service와 Prisma receiving boundary에서 재검증합니다. - tenant·repository·capability·fingerprint-version·stable-fingerprint 단위로 하나의 durable lineage를 유지하고, 동일 fingerprint의 반복 관측도 producer ordinal별 별도 occurrence와 normalized-finding row로 모두 보존합니다. - observation batch/source scanner-run replay는 전체 immutable batch와 ordered occurrence ledger가 정확히 일치할 때만 idempotent하게 반환하며 변경·누락·추가·cross-scope replay를 거부합니다. - rename은 검증된 fixed-commit/fixed-target one-to-one canonical attestation과 기존 predecessor alias가 있을 때만 continuity를 부여하고, rename-back도 alias 중복 없이 append-only RENAMED event로 기록합니다. - lifecycle을 tenant/repository/target context별 OPEN | FIXED 상태로 분리하고 legacy policy/triage status와 독립시켰습니다. - FIXED/REOPENED는 T039 호환 gate가 검증한 COMPLETE, non-stale, comparable decision과 현재 scan의 zero-finding batch를 포함한 전체 observation digest set이 정확히 일치할 때만 적용합니다. - global reconciliation sequence는 연속적으로 유지하면서 새로 생성되거나 다시 eligible해진 state가 non-future fence에서 안전하게 따라잡도록 처리했습니다. - serializable transaction, 5초 acquisition wait, 120초 deadline, 최대 3회 P2034/P2002 retry로 concurrent lineage/alias/lifecycle race를 결정적으로 처리합니다. - rolling-safe Prisma migration에 lineage/alias/batch/occurrence/state/reconciliation/event 모델과 tenant·repository·target-context composite FK, unique, check, index를 추가했습니다. - exact/repeated/replay/tamper/concurrency/rename/rename-back/ambiguous/UNKNOWN/context isolation/fixed/reopen/partial/stale/zero-batch/zero-leak 회귀 테스트를 추가했습니다. - ScanPlaneModule은 T037 lineage service만 다음 내부 stage에 export하고 correlation, coverage calculation, evidence, policy, publication, AI authority와 사용자 route는 계속 차단했습니다. - 006 contracts/data-model/research/threat-model/quality-gates/spec/plan/quickstart/tasks 및 active-feature guard를 T037 완료와 T038 다음 진입점으로 동기화했습니다. ## ✅ 컨벤션 확인 - [x] 브랜치명이 type/issue-number-short-feature 형식을 따르나요? - [x] 이슈 제목과 PR 제목을 동일하게 작성했나요? - [x] 커밋 메시지가 <type>: <description> 형식을 따르나요? ## Check List - [x] Assignees 등록을 하였나요? - [x] 라벨(Label) 등록을 하였나요? - [x] PR 머지 전 반드시 CI가 정상적으로 작동하는지 확인했나요? ## 검증 - corepack pnpm lint - corepack pnpm test (API 88 suites / 540 tests, Web 15 files / 54 tests, Shared 75 tests, GitHub/runtime contract tests 통과) - corepack pnpm typecheck - corepack pnpm build - corepack pnpm --filter @aegisai/api prisma:validate - node --test test/runtime/*.test.mjs (1 test) - focused T037 finding-lineage API 38 tests - focused T037 shared lineage 9 tests - git diff --check

  • 자동 리뷰 inline 10개 스레드 모두 해결: 9개 수정·회귀 검증, 1개 T037 비적용 근거 기록; review-body 품질 제안 13개 반영 ## 006 진행 상태 - T037 완료 - 다음 작업: T038 authority-aware cross-tool correlation with full provenance preservation Closes feat: 006 finding occurrence and lineage lifecycle 구현 #270 ## Summary by CodeRabbit * New Features * Added durable SAST finding lineage and occurrence tracking across scans. * Added lifecycle history for findings, including created, renamed, fixed, and reopened states. * Added deterministic replay handling and verified rename continuity. * Added fail-closed validation for incomplete, stale, mismatched, or unavailable scan coverage. * Documentation * Updated production SAST documentation, readiness status, quality gates, and implementation milestones to reflect lineage and lifecycle support.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change implements T037 SAST finding lineage and lifecycle persistence. It adds shared contracts, Prisma models and constraints, serializable observation/reconciliation flows, rename verification, fail-closed coverage gates, module wiring, tests, and synchronized production-runtime design documentation.

Changes

SAST finding lineage lifecycle

Layer / File(s) Summary
Shared lineage contracts and validation
packages/shared/src/types/sast-finding-lineage.ts, packages/shared/src/index.ts, packages/shared/test/*
Adds lineage, rename, coverage, observation, reconciliation, rejection, canonicalization, digest, authority, and audit metadata contracts with validation tests.
Lineage and lifecycle persistence model
apps/api/prisma/schema.prisma, apps/api/prisma/migrations/..., apps/api/scripts/apply-online-sast-runtime-schema.mjs
Adds lineage, alias, observation, occurrence, lifecycle, reconciliation, and event tables with indexes, foreign keys, and integrity constraints.
Observation and reconciliation services
apps/api/src/scan-plane/sast-finding-lineage*, apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts
Implements durable scope validation, exact replay, repeated occurrence storage, rename handling, serializable retries, lifecycle transitions, and bounded rejection results.
Runtime wiring and behavioral coverage
apps/api/src/scan-plane/scan-plane.module.ts, apps/api/test/scan-plane/*, apps/api/test/support/*
Registers the lineage service and fallback gates, and tests observation, replay, concurrency, rename, reconciliation, scope isolation, and fail-closed behavior.
T037 design and completion gates
specs/006-production-sast-runtime-design/*, test/github-actions/active-feature.test.mjs, README.md
Synchronizes contracts, data model, requirements, threat model, quality gates, quickstart, task completion, active-feature checks, and readiness wording.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScannerHandoff
  participant SastFindingLineageService
  participant PrismaSastFindingLineageStore
  participant PrismaDatabase
  participant LifecycleCoverageGate

  ScannerHandoff->>SastFindingLineageService: submit fingerprinted finding batch
  SastFindingLineageService->>PrismaSastFindingLineageStore: validate scope and persist observation
  PrismaSastFindingLineageStore->>PrismaDatabase: serializable lineage, occurrence, and event writes
  LifecycleCoverageGate-->>SastFindingLineageService: verify complete comparable coverage
  SastFindingLineageService->>PrismaSastFindingLineageStore: reconcile lifecycle state
  PrismaSastFindingLineageStore->>PrismaDatabase: append FIXED or REOPENED transition
Loading

Possibly related PRs

Suggested labels: ✨ feat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the T037 lineage, occurrence, rename, lifecycle, migration, tests, and docs called for in #270.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; the extra docs, tests, and guards all support the T037 scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: implementing finding occurrence and lineage lifecycle work for 006/T037.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd839544f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts
Comment thread apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (13)
packages/shared/src/types/sast-finding-lineage.ts (2)

963-976: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate authority literal.

canonicalAuthority() re-declares the exact object already frozen in AUTHORITY (Lines 288-300). Derive it instead so the two can't drift.

♻️ Derive from the frozen constant
 function canonicalAuthority(): SastFindingLineageAuthority {
-  return {
-    normalizedFindingPersistenceAuthority: true,
-    occurrenceAuthority: true,
-    lifecycleAuthority: true,
-    renameAuthority: true,
-    correlationAuthority: false,
-    coverageCalculationAuthority: false,
-    evidenceAuthority: false,
-    policyAuthority: false,
-    publicationAuthority: false,
-    aiPayloadEligible: false
-  };
+  return { ...AUTHORITY };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/sast-finding-lineage.ts` around lines 963 - 976,
Update canonicalAuthority() to derive its return value from the existing frozen
AUTHORITY constant instead of redeclaring the authority object literal. Preserve
the same SastFindingLineageAuthority shape and values while ensuring future
changes to AUTHORITY are reflected automatically.

1156-1158: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the TextEncoder.

utf8ByteLength is on the hot path (every reference, path, and canonical field) and allocates a new encoder per call.

♻️ Module-level encoder
+const UTF8_ENCODER = new TextEncoder();
+
 function utf8ByteLength(value: string): number {
-  return new TextEncoder().encode(value).byteLength;
+  return UTF8_ENCODER.encode(value).byteLength;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/sast-finding-lineage.ts` around lines 1156 - 1158,
Hoist the TextEncoder allocation out of utf8ByteLength by creating a
module-level encoder and reusing it for each call. Update utf8ByteLength to
encode with that shared instance while preserving its byte-length result.
packages/shared/test/sast-finding-lineage.test.mjs (1)

5-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for isSastFindingLineageObservationResultShapeValid or toSastFindingLineageAuditMetadata.

The import list omits both. The observation-result validator is the primary ingestion gate for this contract (occurrence/finding count equality, the created + exact + renamed === distinct algebra, authority validation), and toSastFindingLineageAuditMetadata is the fail-closed dispatcher that throws on invalid outcomes. Neither is exercised anywhere in this suite.

Want me to draft the missing cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/test/sast-finding-lineage.test.mjs` around lines 5 - 25,
Extend the test imports and suite to cover
isSastFindingLineageObservationResultShapeValid and
toSastFindingLineageAuditMetadata. Add cases verifying the observation
validator’s occurrence/finding count equality, created + exact + renamed ===
distinct algebra, and authority validation, plus dispatcher cases confirming
toSastFindingLineageAuditMetadata returns valid metadata and throws for invalid
outcomes.
apps/api/test/scan-plane/prisma-sast-finding-lineage.store.e2e-spec.ts (3)

1117-1123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate digest helper.

batchIndependentDigest is byte-identical to fixtureDigest already exported from ../support/sast-finding-lineage-fixtures (imported in this file's sibling specs). Reuse the shared helper to keep fixture digests consistent across the suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/scan-plane/prisma-sast-finding-lineage.store.e2e-spec.ts`
around lines 1117 - 1123, Remove the duplicate batchIndependentDigest helper and
import and reuse fixtureDigest from ../support/sast-finding-lineage-fixtures in
the affected test code. Update references to batchIndependentDigest to use
fixtureDigest, preserving the existing digest behavior.

682-682: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This assertion is tautological.

reconciliationTransaction never defines normalizedFinding, so 'normalizedFinding' in transaction is false regardless of what the store does. The intended invariant (reconciliation never touches policy status) isn't actually exercised. Adding a normalizedFinding mock to the transaction and asserting its methods were not called would test the store instead of the fixture.

♻️ Suggested change
-      expect('normalizedFinding' in transaction).toBe(false);
+      expect(
+         transaction.normalizedFinding.updateMany
+      ).not.toHaveBeenCalled();

Add to reconciliationTransaction:

normalizedFinding: {
  updateMany: jest.fn(),
  createMany: jest.fn()
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/scan-plane/prisma-sast-finding-lineage.store.e2e-spec.ts` at
line 682, Update the reconciliationTransaction fixture to include a
normalizedFinding object with mocked updateMany and createMany methods, then
replace the tautological property-existence assertion with expectations that
both methods were not called. Keep the test focused on verifying reconciliation
does not modify policy status.

212-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Guard the mock-call destructure.

calls[0]?.[0].data still throws a TypeError when createMany was never invoked, masking the real assertion failure with a confusing stack. Assert the call count first (or use ?.[0]?.data).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/scan-plane/prisma-sast-finding-lineage.store.e2e-spec.ts`
around lines 212 - 217, Update the occurrenceRows setup around the mocked
sastFindingOccurrence.createMany call to safely handle an uninvoked mock. Assert
that the mock was called before accessing its arguments, or make the argument
access fully optional with ?. [0]?.data, so missing calls produce the intended
assertion failure rather than a TypeError.
apps/api/test/scan-plane/sast-finding-lineage-persistence.e2e-spec.ts (2)

76-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Whitespace-exact schema assertion will break on prisma format.

'lastObservedAt DateTime?' hardcodes the column-alignment padding that prisma format recomputes whenever a longer field name is added to the model. Match on a whitespace-tolerant regex instead.

♻️ Suggested change
-    expect(schema).toContain(
-      'lastObservedAt             DateTime?'
-    );
+    expect(schema).toMatch(/lastObservedAt\s+DateTime\?/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/scan-plane/sast-finding-lineage-persistence.e2e-spec.ts` around
lines 76 - 81, Update the schema assertion in the SAST finding lineage
persistence test to match the lastObservedAt field with a whitespace-tolerant
regular expression instead of hardcoded alignment spaces. Keep the migration
assertion unchanged and continue validating the DateTime? type.

174-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Negative regex is broader than intended.

/\b(calculate|publish|override)\s*\(/ also matches these words inside comments, string literals, or unrelated local helpers, so an innocuous edit to the gate file can fail this gate while a method named e.g. computeCoverage slips through. Anchor on the declaration form you actually forbid (e.g. method definitions in the class body).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/scan-plane/sast-finding-lineage-persistence.e2e-spec.ts` around
lines 174 - 176, Refine the coverageGate assertion in the SAST lineage
persistence test to match only forbidden method declarations in the class body,
rather than occurrences in comments, strings, or unrelated helpers. Preserve
detection of calculate, publish, and override declarations while avoiding false
positives such as call sites or textual references.
apps/api/test/support/sast-finding-lineage-fixtures.ts (1)

244-322: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

as OpenGrepRedactedFinding disables the type check this fixture most needs.

The cast means a drift in the shared SastSecretRedactedFindingCandidate contract (renamed/added required field) compiles cleanly here and surfaces only as a confusing runtime fixture rejection in fingerprintedFindingBatch. Building the base object with satisfies OpenGrepRedactedFinding before applying overrides would keep compile-time coverage of the contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/support/sast-finding-lineage-fixtures.ts` around lines 244 -
322, Replace the direct `as OpenGrepRedactedFinding` cast in the finding fixture
with a `satisfies OpenGrepRedactedFinding` validation on the base object before
applying `overrides`. Preserve the existing override and redaction
decision-digest calculation flow while ensuring required contract changes are
caught at compile time.
apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts (3)

2446-2458: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive scanner capabilities from SAST_SCANNER_RESPONSIBILITIES instead of re-hardcoding them.

SAST_SCANNER_RESPONSIBILITIES is already imported and used at Line 1567. This local copy of the OPENGREP/TRIVY capability lists will silently diverge if the shared contract changes, causing readCapabilities to reject legitimately persisted batches.

♻️ Proposed refactor
 function authoritativeScannerCapabilities(
   scanner: string
 ): FindingCapability[] {
-  if (scanner === 'OPENGREP') return ['SAST'];
-  if (scanner === 'TRIVY') {
-    return [
-      'DEPENDENCY_VULNERABILITY',
-      'SECRET_DETECTION',
-      'IAC_MISCONFIGURATION'
-    ];
-  }
-  throw new SastFindingLineageDurableScopeError();
+  if (!isFindingScannerKind(scanner)) {
+    throw new SastFindingLineageDurableScopeError();
+  }
+  return FINDING_CAPABILITIES.filter((capability) =>
+    (
+      SAST_SCANNER_RESPONSIBILITIES[scanner]
+        .authoritativeCapabilities as readonly string[]
+    ).includes(capability)
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts` around lines
2446 - 2458, Update authoritativeScannerCapabilities to derive each scanner’s
capabilities from the imported SAST_SCANNER_RESPONSIBILITIES contract instead of
hardcoded OPENGREP and TRIVY lists. Preserve the existing unknown-scanner error
behavior and return the responsibility-defined capability values used by
readCapabilities.

2574-2586: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

JSON.stringify equality makes context comparison key-order sensitive.

sameObservationContext/sameReconciliationContext compare serialized objects. Both sides are currently produced by readObservationContext/readReconciliationContext, so key order matches today — but SastFindingLineageScanContext.source has optional ruleBundleDigest/vulnerabilityDatabaseDigest that are conditionally spread (Lines 1667-1675), so any future reordering or a context that round-trips through another producer will fail-closed with a confusing DurableScopeError. A field-wise comparison (you already have observationBatchMatchesContext as a template) is more robust.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts` around lines
2574 - 2586, The sameObservationContext and sameReconciliationContext helpers
are sensitive to object key order because they compare JSON strings. Replace
both with explicit field-wise comparisons, including all required nested source
fields and optional ruleBundleDigest/vulnerabilityDatabaseDigest values,
following observationBatchMatchesContext as the comparison pattern.

1774-1792: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Serializable retries have no backoff.

On P2034/P2002 the loop immediately re-enters a 120s-timeout serializable transaction. Under concurrent ingestion for the same scanner run this maximises contention and repeated full re-reads. A small jittered delay between attempts would shed contention cheaply.

Also note throw lastError after exhausting attempts is typed unknown; that's fine at runtime but the caller only distinguishes the domain error classes, so exhausted retries surface as FINDING_LINEAGE_PERSISTENCE_FAILED.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts` around lines
1774 - 1792, Add a small jittered backoff delay between retryable failures in
the serializable transaction retry loop, before the next iteration of the
operation guarded by SERIALIZABLE_ATTEMPTS. Keep the existing immediate rethrow
for non-retryable errors and preserve the final throw of lastError so exhausted
retries retain their current caller behavior.
apps/api/src/scan-plane/sast-finding-lineage.store.ts (1)

177-182: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the unused durable SAST type aliases.

DurableSastFingerprintedFinding, DurableSastFindingObservationResult, and DurableSastFindingReconciliationResult are only imported and exist as aliases, so callers can import the shared types directly instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/scan-plane/sast-finding-lineage.store.ts` around lines 177 -
182, Remove the unused type aliases DurableSastFingerprintedFinding,
DurableSastFindingObservationResult, and DurableSastFindingReconciliationResult
from the SAST lineage store. Update any imports or references to use
SastFingerprintedFinding, SastFindingLineageObservationResult, and
SastFindingLifecycleReconciliationResult directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/api/prisma/migrations/20260730160000_sast_finding_lineage_lifecycle/migration.sql`:
- Around line 23-32: Update the migration and occurrence projection around
createOccurrences so SAST non-FILE findings cannot expose null filePath,
lineStart, or lineEnd to existing consumers before making those columns
nullable. Preserve the existing non-null location contract for AI inference and
report PDF consumers, or guard and exclude unsupported occurrences before
applying the ALTER TABLE changes.

In `@apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts`:
- Around line 1957-1999: The rename-candidate derivation and comparison logic is
duplicated between the persistence store and service, risking mismatched
validation. Extract the shared derivation and ordering into a helper in
packages/shared, then update the store validation block and service function
prepareRenameCandidates to use it. Replace localeCompare-based ordering with
deterministic plain key < and > comparisons so both boundaries produce identical
JSON ordering.
- Around line 634-671: Scope both lineage lookup sites to the current tenant and
repository binding. In the sastFindingLineage.findMany existence probe around
lines 634-671, add tenantId and repositoryBindingId to the where clause; in the
OR branches around lines 1073-1094, add tenantId and repositoryBindingId to the
id and coverageDecisionDigest branches to match the existing third branch.

In `@apps/api/src/scan-plane/sast-finding-lineage.service.ts`:
- Line 115: Remove the upfront yieldForFindingBatch(batch.findings.length) call
from the batch loop and move yielding into the per-finding iteration used by
prepareRenameCandidates, yielding every yieldFindingInterval processed findings.
Ensure the heavy context, rename-derivation, and persistence work is interleaved
with event-loop yields, and apply the same adjustment to the additional
finding-processing path.
- Around line 102-112: Add a Number.isFinite(retentionExpiresAt) guard
immediately after parsing it in the retention validation flow, alongside the
existing firstReferenceTime check, and reject with the appropriate
invalid-retention result when parsing fails. Ensure both retention gates fail
closed for unparseable expiration values.

In `@packages/shared/src/types/sast-finding-lineage.ts`:
- Around line 848-876: Update the rejection payload validation around
reasonCodes to reject arrays longer than the 16 supported rejection codes, using
the existing canonical-code collection for the cap. Compute
orderSastFindingLineageRejectionReasons(reasonCodes) once before validating
ordering, then compare the original array against that hoisted result; use the
resulting length equality to detect unknown or duplicate codes and remove the
redundant per-element membership check.
- Around line 715-727: Update the validation chain for the lineage result around
findingCount and distinctFingerprintCount to require distinctFingerprintCount to
be positive whenever findingCount is greater than zero. Preserve the existing
zero-count behavior and all current upper-bound and lineage-sum invariants.

In `@packages/shared/test/sast-finding-lineage.test.mjs`:
- Around line 263-290: Update the negative-case mutations in
packages/shared/test/sast-finding-lineage.test.mjs at lines 263-290 and 129-154
to spread the undigested core object, then recompute decisionDigest with
canonicalizeSastFindingLifecycleCoverageDecision or attestationDigest with
canonicalizeSastFindingRenameAttestation before validation. Preserve each
mutation’s intended structural violation so the tests independently exercise the
relevant shape rules rather than failing only on stale digests.

---

Nitpick comments:
In `@apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts`:
- Around line 2446-2458: Update authoritativeScannerCapabilities to derive each
scanner’s capabilities from the imported SAST_SCANNER_RESPONSIBILITIES contract
instead of hardcoded OPENGREP and TRIVY lists. Preserve the existing
unknown-scanner error behavior and return the responsibility-defined capability
values used by readCapabilities.
- Around line 2574-2586: The sameObservationContext and
sameReconciliationContext helpers are sensitive to object key order because they
compare JSON strings. Replace both with explicit field-wise comparisons,
including all required nested source fields and optional
ruleBundleDigest/vulnerabilityDatabaseDigest values, following
observationBatchMatchesContext as the comparison pattern.
- Around line 1774-1792: Add a small jittered backoff delay between retryable
failures in the serializable transaction retry loop, before the next iteration
of the operation guarded by SERIALIZABLE_ATTEMPTS. Keep the existing immediate
rethrow for non-retryable errors and preserve the final throw of lastError so
exhausted retries retain their current caller behavior.

In `@apps/api/src/scan-plane/sast-finding-lineage.store.ts`:
- Around line 177-182: Remove the unused type aliases
DurableSastFingerprintedFinding, DurableSastFindingObservationResult, and
DurableSastFindingReconciliationResult from the SAST lineage store. Update any
imports or references to use SastFingerprintedFinding,
SastFindingLineageObservationResult, and
SastFindingLifecycleReconciliationResult directly.

In `@apps/api/test/scan-plane/prisma-sast-finding-lineage.store.e2e-spec.ts`:
- Around line 1117-1123: Remove the duplicate batchIndependentDigest helper and
import and reuse fixtureDigest from ../support/sast-finding-lineage-fixtures in
the affected test code. Update references to batchIndependentDigest to use
fixtureDigest, preserving the existing digest behavior.
- Line 682: Update the reconciliationTransaction fixture to include a
normalizedFinding object with mocked updateMany and createMany methods, then
replace the tautological property-existence assertion with expectations that
both methods were not called. Keep the test focused on verifying reconciliation
does not modify policy status.
- Around line 212-217: Update the occurrenceRows setup around the mocked
sastFindingOccurrence.createMany call to safely handle an uninvoked mock. Assert
that the mock was called before accessing its arguments, or make the argument
access fully optional with ?. [0]?.data, so missing calls produce the intended
assertion failure rather than a TypeError.

In `@apps/api/test/scan-plane/sast-finding-lineage-persistence.e2e-spec.ts`:
- Around line 76-81: Update the schema assertion in the SAST finding lineage
persistence test to match the lastObservedAt field with a whitespace-tolerant
regular expression instead of hardcoded alignment spaces. Keep the migration
assertion unchanged and continue validating the DateTime? type.
- Around line 174-176: Refine the coverageGate assertion in the SAST lineage
persistence test to match only forbidden method declarations in the class body,
rather than occurrences in comments, strings, or unrelated helpers. Preserve
detection of calculate, publish, and override declarations while avoiding false
positives such as call sites or textual references.

In `@apps/api/test/support/sast-finding-lineage-fixtures.ts`:
- Around line 244-322: Replace the direct `as OpenGrepRedactedFinding` cast in
the finding fixture with a `satisfies OpenGrepRedactedFinding` validation on the
base object before applying `overrides`. Preserve the existing override and
redaction decision-digest calculation flow while ensuring required contract
changes are caught at compile time.

In `@packages/shared/src/types/sast-finding-lineage.ts`:
- Around line 963-976: Update canonicalAuthority() to derive its return value
from the existing frozen AUTHORITY constant instead of redeclaring the authority
object literal. Preserve the same SastFindingLineageAuthority shape and values
while ensuring future changes to AUTHORITY are reflected automatically.
- Around line 1156-1158: Hoist the TextEncoder allocation out of utf8ByteLength
by creating a module-level encoder and reusing it for each call. Update
utf8ByteLength to encode with that shared instance while preserving its
byte-length result.

In `@packages/shared/test/sast-finding-lineage.test.mjs`:
- Around line 5-25: Extend the test imports and suite to cover
isSastFindingLineageObservationResultShapeValid and
toSastFindingLineageAuditMetadata. Add cases verifying the observation
validator’s occurrence/finding count equality, created + exact + renamed ===
distinct algebra, and authority validation, plus dispatcher cases confirming
toSastFindingLineageAuditMetadata returns valid metadata and throws for invalid
outcomes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fdf574-3f69-4aed-8851-99d3fb329855

📥 Commits

Reviewing files that changed from the base of the PR and between 865d468 and fd83954.

📒 Files selected for processing (28)
  • README.md
  • apps/api/prisma/migrations/20260730160000_sast_finding_lineage_lifecycle/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/scripts/apply-online-sast-runtime-schema.mjs
  • apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts
  • apps/api/src/scan-plane/sast-finding-lifecycle-coverage.gate.ts
  • apps/api/src/scan-plane/sast-finding-lineage.service.ts
  • apps/api/src/scan-plane/sast-finding-lineage.store.ts
  • apps/api/src/scan-plane/sast-finding-rename-attestation.verifier.ts
  • apps/api/src/scan-plane/scan-plane.module.ts
  • apps/api/test/scan-plane/prisma-sast-finding-lineage.store.e2e-spec.ts
  • apps/api/test/scan-plane/sast-finding-lineage-persistence.e2e-spec.ts
  • apps/api/test/scan-plane/sast-finding-lineage.e2e-spec.ts
  • apps/api/test/support/sast-finding-lineage-fixtures.ts
  • packages/shared/src/index.ts
  • packages/shared/src/types/sast-finding-lineage.ts
  • packages/shared/test/sast-finding-lineage.test.mjs
  • packages/shared/test/shared-contract-exports.test.mjs
  • specs/006-production-sast-runtime-design/contracts/sast-runtime.md
  • specs/006-production-sast-runtime-design/data-model.md
  • specs/006-production-sast-runtime-design/plan.md
  • specs/006-production-sast-runtime-design/quality-gates.md
  • specs/006-production-sast-runtime-design/quickstart.md
  • specs/006-production-sast-runtime-design/research.md
  • specs/006-production-sast-runtime-design/spec.md
  • specs/006-production-sast-runtime-design/tasks.md
  • specs/006-production-sast-runtime-design/threat-model.md
  • test/github-actions/active-feature.test.mjs

Comment thread apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts
Comment thread apps/api/src/scan-plane/prisma-sast-finding-lineage.store.ts Outdated
Comment thread apps/api/src/scan-plane/sast-finding-lineage.service.ts
Comment thread apps/api/src/scan-plane/sast-finding-lineage.service.ts Outdated
Comment thread packages/shared/src/types/sast-finding-lineage.ts
Comment thread packages/shared/src/types/sast-finding-lineage.ts
Comment thread packages/shared/test/sast-finding-lineage.test.mjs Outdated
@goodtu02
goodtu02 merged commit a017d4c into dev Jul 30, 2026
2 checks passed
@goodtu02
goodtu02 deleted the feat/270-006-finding-lineage branch July 30, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 006 finding occurrence and lineage lifecycle 구현

1 participant