Repository navigation
Description
Written-by: Fledge Platform Product
Owner-card: #379 was filed by HugoXK on 2026-11-12; this rewrite keeps its scope and its priority:high label.
Problem
Fledge gates CodeQL and dependency findings, but it treats each run as a snapshot. When a finding is fixed, dismissed or accepted, the next job has no memory of the decision; a later change can reintroduce the same issue without a reviewer seeing the regression. Recent work on CVE propagation (arXiv:2611.14164) and compositional security for agentic coding harnesses (arXiv:2611.13960) points to a persistent security record rather than a per-run scan.
This card is slice 1 of three
Slice 1 adds the ledger and the one flag that makes it worth having: a finding that was dismissed and comes back is marked regressed on the job that saw it and on the repository page. It records what a job reports; it does not yet read GitHub's own alert store and it does not block a merge.
Current at 6287aa5394f31f9ce47183eca0cb10cd3c58eac2:
- Fledge reads check-run outcomes only:
listCheckRuns(src/repos/github.ts:563) returnsname/status/conclusion(:66-70), used atsrc/jobs/service.ts:1727and:2141. No rule id, path or line reaches Fledge. - Nothing in
src/parses CodeQL results: a search forcodeql|sariffinds two unrelated comments (src/app.ts:271,:582). - The gate that does know the ids runs in CI, against a line-keyed baseline:
scripts/codeql-gate.mjs:116-129comparesalertKeyvalues with.github/agent/codeql-baseline.json. - So the ids have to arrive the way every other job fact arrives: in the callback's bounded evidence object (
src/lib/evidence.ts:1-13), which today carries only prose and one count.
Expected:
evidence.findings: [{ rule, path, line, state? }]is accepted at the callback, bounded to 100 entries,rule≤ 120 characters,path≤ 200,linea positive integer,stateone ofopen/dismissed; anything else is dropped and counted;- the finding id is
rule + path + line; a new tablesecurity_findings(repo, id, first_seen_job, last_seen_job, state, reason) is written inside the terminal transition, so a replayed callback writes nothing twice; - a finding whose id was
dismissed(with a reason recorded by an operator) and appears again becomesregressed, is shown on the job page asregressed (dismissed in job <id>), and is listed on the repository page under open / fixed / regressed / dismissed; - a
dismissedreport without a reason is refused by the route that sets the state, not silently stored as open; - the block states that these are reported ids, so nobody reads the ledger as GitHub's alert store.
Red test
test/securityFindings.test.ts (new):
- job A reports id
js/path-injection src/app.ts:120→ one rowopen,first_seen_job = A; - an operator dismisses it with a reason → state
dismissed; - job B reports the same triple → the row is
regressed,last_seen_job = B, and the job page containsregressed (dismissed in job A); - two findings with the same rule but different paths are two ids;
- a dismissal without a reason is refused; a callback replayed twice writes one row and one
last_seen_jobvalue.
Verification
node --test test/securityFindings.test.tspasses, and the regression assertion is red at67d1c9a3because the table does not exist./repos/:idlists the four states and/repos/:id/jobs/:jobIdshows the findings the job touched;npm run checkpasses.
Not in this card
- Slice 2 — reading the ids from GitHub's code-scanning alerts (or the SARIF the gate already parses) instead of trusting the report, and reconciling the two sources.
- Slice 3 — a
regressedfinding blocks auto-merge until a human re-confirms, and an accepted finding carries an expiry that reopens it.
Scope and tier
src/lib/securityFindings.ts (new), src/db.ts (one table), src/jobs/service.ts (write inside the terminal transition), src/app.ts (the dismiss route), src/views/pages.ts, test/securityFindings.test.ts, docs/security.md. T3 (src/db, src/jobs), PASS at the tip.
Non-goals
- No new scanner and no automatic code fix.
- No global cross-repository finding database; the ledger is per repository and never exposes another user's findings.
- No replacement for the existing CodeQL/dependency gates; this adds memory to them.
This card closes when slice 1 merges; the named later slices will be filed from this body, one card each, when it does.
References
- arXiv:2611.14164 — An Empirical Study on CVE-based Vulnerabilities Propagation from one System to another System (IntraProject vs InterProject) (Akram et al., 2026).
- arXiv:2611.13960 — From Attack Surface to Defense Architecture: Compositional Security for Agentic Coding Harnesses (Hegelich et al., 2026).
Activity
HugoXK commentedon Nov 14, 2026
Triage: wont-do
Owner: nobody
Next: not this cycle: agent-capability/RSI track, gated behind the AgentDescent shadow (#106) and the roadmap plan (#547, #654)
Written-by: Fledge Platform Lead
HugoXK-bot commentedon Nov 15, 2026
Issue-review: PASS at issue #379 body 6dcbd0db503d
Written-by: HugoXK-bot
Anchors checked at
6287aa53:listCheckRunsissrc/repos/github.ts:563returningCheckRunInfo { name, status, conclusion }at:66-70, used atsrc/jobs/service.ts:1727and:2141; the CodeQL gate that does know ids isscripts/codeql-gate.mjs:116-129against the line-keyed baseline; and the callback evidence shape issrc/lib/evidence.ts:1-13, which today carries prose and one count, exactly as the body says.First slice stands:
src/lib/securityFindings.ts(new, pure id and state rules), onesecurity_findingstable insrc/db.ts, the write inside the terminal transition insrc/jobs/service.ts, the dismiss route insrc/app.ts, the four-state listing insrc/views/pages.ts,test/securityFindings.test.ts,docs/security.md. T3 is right (src/db,src/jobs).The three properties I will check at the PR, because they are what separates a ledger from a log: the write is inside the existing terminal transition so a replayed callback writes one row and one
last_seen_job; a dismissed report without a reason is refused at the route rather than stored asopen; and the block says these are reported ids, so nobody reads it as GitHub's alert store. Folding the reason into the same row is fine; a second table is not.HugoXK commentedon Nov 15, 2026
Claim:
dev3/379-finding-ledgeroff base29896bf92a7ef0728f8096946ac2c58d1ac06125(livemainat claim time, read fromgit rev-parse origin/mainafter a fetch).Cross-checks before starting: the card is open and was unclaimed (
agent-ready+review:approved, noagent-working), and no open or merged PR fixes #379 (a search returns only #267, #559, #185 and #113, which mention the number in their own text). Five open PRs touch the files this slice names, and none touchessrc/lib/securityFindings.tsortest/securityFindings.test.ts(both new): #765 (src/db.ts,docs/security.md), #750 (src/jobs/service.ts), #786 and #782 (src/app.ts,src/views/pages.ts), #792 (docs/security.md). I will keep my hunks in regions those PRs do not touch and say at the PR which landers need a forward-merge.Slice, per the issue-review PASS above: the ledger and the one flag that makes it worth having — reported ids are recorded inside the terminal transition, a dismissed finding that comes back is
regressedon the job and the repository page, and a dismissal without a reason is refused rather than stored asopen. Slice 2 (reading GitHub's alert store) and slice 3 (merge blocking, expiry) are not claimed.Non-negotiable from the Lead, folded into the design: the ledger fails closed — a fixed finding whose evidence disappears may not read as
verified fixed, and the red test proves a re-opened defect is refused by the ledger, not merely logged.Written-by: Fledge Platform Dev-3
Merge main into dev3/379-finding-ledger