@derwinjs/db 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +79 -0
- package/dist/agent-findings-ingestor.d.ts +40 -0
- package/dist/agent-findings-ingestor.d.ts.map +1 -0
- package/dist/agent-findings-ingestor.js +154 -0
- package/dist/agent-findings-ingestor.js.map +1 -0
- package/dist/classification-trust-store.d.ts +31 -0
- package/dist/classification-trust-store.d.ts.map +1 -0
- package/dist/classification-trust-store.js +154 -0
- package/dist/classification-trust-store.js.map +1 -0
- package/dist/coverage-gap-reporter.d.ts +35 -0
- package/dist/coverage-gap-reporter.d.ts.map +1 -0
- package/dist/coverage-gap-reporter.js +84 -0
- package/dist/coverage-gap-reporter.js.map +1 -0
- package/dist/fix-policy.d.ts +46 -0
- package/dist/fix-policy.d.ts.map +1 -0
- package/dist/fix-policy.js +162 -0
- package/dist/fix-policy.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/learning-health-reporter.d.ts +37 -0
- package/dist/learning-health-reporter.d.ts.map +1 -0
- package/dist/learning-health-reporter.js +141 -0
- package/dist/learning-health-reporter.js.map +1 -0
- package/dist/prisma.d.ts +31 -0
- package/dist/prisma.d.ts.map +1 -0
- package/dist/prisma.js +31 -0
- package/dist/prisma.js.map +1 -0
- package/dist/qa-fix-attempt-store.d.ts +28 -0
- package/dist/qa-fix-attempt-store.d.ts.map +1 -0
- package/dist/qa-fix-attempt-store.js +258 -0
- package/dist/qa-fix-attempt-store.js.map +1 -0
- package/dist/qa-pattern-store.d.ts +32 -0
- package/dist/qa-pattern-store.d.ts.map +1 -0
- package/dist/qa-pattern-store.js +123 -0
- package/dist/qa-pattern-store.js.map +1 -0
- package/dist/qa-revert-store.d.ts +24 -0
- package/dist/qa-revert-store.d.ts.map +1 -0
- package/dist/qa-revert-store.js +139 -0
- package/dist/qa-revert-store.js.map +1 -0
- package/dist/qa-run-store.d.ts +46 -0
- package/dist/qa-run-store.d.ts.map +1 -0
- package/dist/qa-run-store.js +201 -0
- package/dist/qa-run-store.js.map +1 -0
- package/dist/qa-ticket-store.d.ts +35 -0
- package/dist/qa-ticket-store.d.ts.map +1 -0
- package/dist/qa-ticket-store.js +293 -0
- package/dist/qa-ticket-store.js.map +1 -0
- package/dist/qa-uniformity-store.d.ts +41 -0
- package/dist/qa-uniformity-store.d.ts.map +1 -0
- package/dist/qa-uniformity-store.js +288 -0
- package/dist/qa-uniformity-store.js.map +1 -0
- package/dist/scripts/smoke-auto-fix.d.ts +37 -0
- package/dist/scripts/smoke-auto-fix.d.ts.map +1 -0
- package/dist/scripts/smoke-auto-fix.js +270 -0
- package/dist/scripts/smoke-auto-fix.js.map +1 -0
- package/dist/scripts/smoke-learning-loop.d.ts +21 -0
- package/dist/scripts/smoke-learning-loop.d.ts.map +1 -0
- package/dist/scripts/smoke-learning-loop.js +375 -0
- package/dist/scripts/smoke-learning-loop.js.map +1 -0
- package/dist/scripts/smoke-orchestration.d.ts +35 -0
- package/dist/scripts/smoke-orchestration.d.ts.map +1 -0
- package/dist/scripts/smoke-orchestration.js +215 -0
- package/dist/scripts/smoke-orchestration.js.map +1 -0
- package/dist/scripts/smoke-qa-ticket-store.d.ts +18 -0
- package/dist/scripts/smoke-qa-ticket-store.d.ts.map +1 -0
- package/dist/scripts/smoke-qa-ticket-store.js +233 -0
- package/dist/scripts/smoke-qa-ticket-store.js.map +1 -0
- package/package.json +69 -0
- package/prisma/migrations/20260501165631_init/migration.sql +407 -0
- package/prisma/migrations/20260503051425_0002_qap018b_qaticket_crosslink_fields/migration.sql +6 -0
- package/prisma/migrations/20260504231316_add_project_repofullname_and_webhooksecret/migration.sql +12 -0
- package/prisma/migrations/20260504232851_add_qaticket_resolvedby/migration.sql +2 -0
- package/prisma/migrations/20260505042646_add_qapattern_qarevert/migration.sql +77 -0
- package/prisma/migrations/20260505055826_add_qauniformity_and_agent_trigger/migration.sql +35 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +748 -0
- package/prisma/seed.ts +181 -0
- package/prisma-client/default.d.ts +1 -0
- package/prisma-client/default.js +1 -0
- package/prisma-client/edge.d.ts +1 -0
- package/prisma-client/edge.js +631 -0
- package/prisma-client/index-browser.js +615 -0
- package/prisma-client/index.d.ts +34509 -0
- package/prisma-client/index.js +660 -0
- package/prisma-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/prisma-client/libquery_engine-linux-arm64-openssl-3.0.x.so.node +0 -0
- package/prisma-client/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/prisma-client/package.json +97 -0
- package/prisma-client/runtime/edge-esm.js +31 -0
- package/prisma-client/runtime/edge.js +31 -0
- package/prisma-client/runtime/index-browser.d.ts +365 -0
- package/prisma-client/runtime/index-browser.js +13 -0
- package/prisma-client/runtime/library.d.ts +3403 -0
- package/prisma-client/runtime/library.js +143 -0
- package/prisma-client/runtime/react-native.js +80 -0
- package/prisma-client/runtime/wasm.js +32 -0
- package/prisma-client/schema.prisma +748 -0
- package/prisma-client/wasm.d.ts +1 -0
- package/prisma-client/wasm.js +615 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Derwin — Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vize LLC. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its associated documentation files (the "Software") are
|
|
6
|
+
proprietary and confidential. No part of the Software may be copied, modified,
|
|
7
|
+
distributed, sublicensed, sold, or otherwise transferred to any third party
|
|
8
|
+
without the prior written consent of the copyright holder.
|
|
9
|
+
|
|
10
|
+
Unauthorized use, reproduction, or distribution of the Software is prohibited
|
|
11
|
+
and may result in civil and criminal penalties.
|
|
12
|
+
|
|
13
|
+
This license may be superseded by an alternative license (e.g., MIT or
|
|
14
|
+
Apache 2.0) at a future date if a decision is made to release the Software
|
|
15
|
+
as open source. Such a decision will be recorded as a superseding ADR in
|
|
16
|
+
docs/adrs/ and will apply only to the version of the Software in effect at
|
|
17
|
+
the time of the decision.
|
|
18
|
+
|
|
19
|
+
The Software is provided "as is", without warranty of any kind, express or
|
|
20
|
+
implied, including but not limited to the warranties of merchantability,
|
|
21
|
+
fitness for a particular purpose, and non-infringement. In no event shall the
|
|
22
|
+
authors or copyright holders be liable for any claim, damages, or other
|
|
23
|
+
liability, whether in an action of contract, tort, or otherwise, arising
|
|
24
|
+
from, out of, or in connection with the Software or the use or other dealings
|
|
25
|
+
in the Software.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @derwinjs/db
|
|
2
|
+
|
|
3
|
+
Prisma schema + migrations for Derwin's own Postgres. **14 models, project-namespaced.** Per ADR-0005.
|
|
4
|
+
|
|
5
|
+
This package owns the platform's persistence layer. Consumer DBs (Lifeline's Postgres, Side Piece's Postgres) are NOT shared with this — Derwin reads from them via Ticket / Document adapters; it stores its own state here.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Set up Postgres locally (one option: Docker)
|
|
11
|
+
docker run -d --name derwin-pg \
|
|
12
|
+
-e POSTGRES_USER=derwin \
|
|
13
|
+
-e POSTGRES_PASSWORD=derwin \
|
|
14
|
+
-e POSTGRES_DB=derwin \
|
|
15
|
+
-p 5432:5432 \
|
|
16
|
+
postgres:16
|
|
17
|
+
|
|
18
|
+
# 2. Configure connection
|
|
19
|
+
cp packages/db/.env.example packages/db/.env
|
|
20
|
+
# Edit DATABASE_URL if needed
|
|
21
|
+
|
|
22
|
+
# 3. Generate Prisma client + apply migrations
|
|
23
|
+
pnpm --filter @derwinjs/db prisma:generate
|
|
24
|
+
pnpm --filter @derwinjs/db prisma:migrate
|
|
25
|
+
|
|
26
|
+
# 4. Seed dev fixture (Lifeline project + Profile + default Policy)
|
|
27
|
+
pnpm --filter @derwinjs/db db:seed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
After seeding, you can connect with `psql` and inspect:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
docker exec -it derwin-pg psql -U derwin -d derwin -c "SELECT slug, type, mode FROM projects;"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Expected: one row, `lifeline | ERP | AUTHOR`.
|
|
37
|
+
|
|
38
|
+
## Production: Supabase
|
|
39
|
+
|
|
40
|
+
Per ADR-0005, the production deployment uses Supabase (parity with Lifeline). Connection string format:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The pgvector extension gets enabled in QAP-036 (Sprint 3 — DB deployment). The RAGCorpus.embedding column is `Bytes?` for v1; QAP-076 (Sprint 7) switches it to `vector(1536)` once pgvector is available.
|
|
47
|
+
|
|
48
|
+
## Schema conventions
|
|
49
|
+
|
|
50
|
+
- **Project-namespaced.** Every project-scoped table has a `projectId` FK to `Project`. Cross-project leakage is the leading regression risk; queries MUST filter on `projectId`.
|
|
51
|
+
- **Cascading deletes.** Deleting a Project cleanly removes all project-scoped data (cascades through 12 of 14 models).
|
|
52
|
+
- **Append-only logs.** `ProfileEvolutionLog`, `ProjectModeLog`, `SpendLedger` are append-only — never updated, never deleted (compliance requirement per product brief §11.3).
|
|
53
|
+
- **Audit retention.** `AuditArtifact.retentionUntil` is computed at write time from project compliance mode; a separate retention cron prunes expired artifacts (lands in Sprint 7).
|
|
54
|
+
- **Indices on every read pattern.** Per technical spec §3 — `(projectId, status)`, `(projectId, finalBucket)`, `(projectId, createdAt DESC)`, etc.
|
|
55
|
+
|
|
56
|
+
## Models (14)
|
|
57
|
+
|
|
58
|
+
| # | Model | Purpose | Layer |
|
|
59
|
+
| --- | --------------------- | --------------------------------------------------------- | ------------- |
|
|
60
|
+
| 1 | `Project` | Multi-tenant root | A (Profile) |
|
|
61
|
+
| 2 | `ProjectProfile` | Domain ontology, risk weights, critical flows, glossary | A |
|
|
62
|
+
| 3 | `IngestedDoc` | Source docs that fed the Profile | A |
|
|
63
|
+
| 4 | `ProfileEvolutionLog` | Append-only audit of every Profile change | A |
|
|
64
|
+
| 5 | `QARun` | One discovery pass | B/C |
|
|
65
|
+
| 6 | `RawSignal` | What a Discovery route emits before triage | B/C |
|
|
66
|
+
| 7 | `QATicket` | The structured bug ticket | D |
|
|
67
|
+
| 8 | `QAFixAttempt` | One Claude dispatch + outcome | E |
|
|
68
|
+
| 9 | `ClassificationTrust` | Per-classification rolling 30-day trust score | F |
|
|
69
|
+
| 10 | `RAGCorpus` | Successful past fixes for in-context examples | F |
|
|
70
|
+
| 11 | `AuditArtifact` | Persistent evidence (screenshots, videos, prompts, diffs) | cross-cutting |
|
|
71
|
+
| 12 | `Policy` | Risk-tier rules, classification overrides, thresholds | safety |
|
|
72
|
+
| 13 | `ProjectModeLog` | Append-only mode-change audit | safety |
|
|
73
|
+
| 14 | `SpendLedger` | Per-event $ spend (Anthropic, storage, etc.) | cost |
|
|
74
|
+
|
|
75
|
+
Detail: see [`prisma/schema.prisma`](./prisma/schema.prisma) and technical spec §3.
|
|
76
|
+
|
|
77
|
+
## Status
|
|
78
|
+
|
|
79
|
+
Scaffolded in QAP-005 (Sprint 1). Schema is complete; the Prisma client gets imported by `@derwinjs/core` and `@derwinjs/api` starting in Sprint 2.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentFindingsIngestor — Prisma-backed ingestor that projects an inbound
|
|
3
|
+
* QA-agent envelope into a QARun + N RawSignals.
|
|
4
|
+
*
|
|
5
|
+
* QAP-019F (Sprint 2 Group D-3, Commit 2). Implements the
|
|
6
|
+
* AgentFindingsIngestor contract from @derwinjs/sdk by composing
|
|
7
|
+
* QARunStore.createRun under the hood (Group D-3 plan Decision 2).
|
|
8
|
+
*
|
|
9
|
+
* Translation under Option β:
|
|
10
|
+
* - Envelope → QARun(triggerType='AGENT', triggeredBy=agentVersion ?? 'qa-agent',
|
|
11
|
+
* scope = non-finding fields).
|
|
12
|
+
* - Each finding → RawSignal(routeName='agent', signalType='agent_finding',
|
|
13
|
+
* rawData = {sev, area, msg, file, tags, proposedFix}).
|
|
14
|
+
* - Findings with sev='info' are dropped (Lifeline parity); counted
|
|
15
|
+
* separately as droppedInfoCount in the response.
|
|
16
|
+
* - **No inline ticket creation.** The orchestrator's Triage stage owns
|
|
17
|
+
* ticket creation (ADR-0008); this ingestor only writes signals.
|
|
18
|
+
*
|
|
19
|
+
* FK violations from the underlying QARunStore (unknown projectId) bubble
|
|
20
|
+
* up as QAStoreError('fk_violation', ...) — we re-throw as
|
|
21
|
+
* AgentFindingsIngestorError('fk_violation', ...) so the route handler
|
|
22
|
+
* has a single typed error class to switch on.
|
|
23
|
+
*/
|
|
24
|
+
import type { QARunStore } from '@derwinjs/sdk';
|
|
25
|
+
import { type AgentFindingsIngestor } from '@derwinjs/sdk';
|
|
26
|
+
/**
|
|
27
|
+
* The ingestor composes QARunStore.createRun, so config takes a runStore
|
|
28
|
+
* rather than a Prisma client. Consumers that need a custom run store
|
|
29
|
+
* (e.g., a write-through cache, a dual-write to legacy Lifeline) can pass
|
|
30
|
+
* their own.
|
|
31
|
+
*
|
|
32
|
+
* configureQAPlatform() wires `runs: createPrismaQARunStore({ prisma })`
|
|
33
|
+
* and then `agentFindings: createPrismaAgentFindingsIngestor({ runStore: runs })`.
|
|
34
|
+
*/
|
|
35
|
+
export interface PrismaAgentFindingsIngestorConfig {
|
|
36
|
+
/** A QARunStore implementation. Most consumers pass createPrismaQARunStore output. */
|
|
37
|
+
runStore: QARunStore;
|
|
38
|
+
}
|
|
39
|
+
export declare function createPrismaAgentFindingsIngestor(config: PrismaAgentFindingsIngestorConfig): AgentFindingsIngestor;
|
|
40
|
+
//# sourceMappingURL=agent-findings-ingestor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-findings-ingestor.d.ts","sourceRoot":"","sources":["../src/agent-findings-ingestor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAML,KAAK,qBAAqB,EAG3B,MAAM,eAAe,CAAC;AAIvB;;;;;;;;GAQG;AACH,MAAM,WAAW,iCAAiC;IAChD,sFAAsF;IACtF,QAAQ,EAAE,UAAU,CAAC;CACtB;AAID,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,iCAAiC,GACxC,qBAAqB,CAwDvB"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentFindingsIngestor — Prisma-backed ingestor that projects an inbound
|
|
3
|
+
* QA-agent envelope into a QARun + N RawSignals.
|
|
4
|
+
*
|
|
5
|
+
* QAP-019F (Sprint 2 Group D-3, Commit 2). Implements the
|
|
6
|
+
* AgentFindingsIngestor contract from @derwinjs/sdk by composing
|
|
7
|
+
* QARunStore.createRun under the hood (Group D-3 plan Decision 2).
|
|
8
|
+
*
|
|
9
|
+
* Translation under Option β:
|
|
10
|
+
* - Envelope → QARun(triggerType='AGENT', triggeredBy=agentVersion ?? 'qa-agent',
|
|
11
|
+
* scope = non-finding fields).
|
|
12
|
+
* - Each finding → RawSignal(routeName='agent', signalType='agent_finding',
|
|
13
|
+
* rawData = {sev, area, msg, file, tags, proposedFix}).
|
|
14
|
+
* - Findings with sev='info' are dropped (Lifeline parity); counted
|
|
15
|
+
* separately as droppedInfoCount in the response.
|
|
16
|
+
* - **No inline ticket creation.** The orchestrator's Triage stage owns
|
|
17
|
+
* ticket creation (ADR-0008); this ingestor only writes signals.
|
|
18
|
+
*
|
|
19
|
+
* FK violations from the underlying QARunStore (unknown projectId) bubble
|
|
20
|
+
* up as QAStoreError('fk_violation', ...) — we re-throw as
|
|
21
|
+
* AgentFindingsIngestorError('fk_violation', ...) so the route handler
|
|
22
|
+
* has a single typed error class to switch on.
|
|
23
|
+
*/
|
|
24
|
+
import { AgentFindingsIngestorError, QARunStoreError, } from '@derwinjs/sdk';
|
|
25
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
26
|
+
export function createPrismaAgentFindingsIngestor(config) {
|
|
27
|
+
const { runStore } = config;
|
|
28
|
+
return {
|
|
29
|
+
async ingestAgentFindings(input) {
|
|
30
|
+
validateInput(input);
|
|
31
|
+
const { projectId, envelope } = input;
|
|
32
|
+
// Filter findings: drop info-severity. Lifeline rule — info-level
|
|
33
|
+
// findings are diagnostic noise, not signal.
|
|
34
|
+
const droppedInfoCount = envelope.findings.filter((f) => f.sev === 'info').length;
|
|
35
|
+
const keptFindings = envelope.findings.filter((f) => f.sev !== 'info');
|
|
36
|
+
// Build the signals payload. routeName is a synthetic constant
|
|
37
|
+
// 'agent' — Lifeline's RawSignal.routeName is required + non-empty
|
|
38
|
+
// and the agent has no per-finding route concept.
|
|
39
|
+
const signals = keptFindings.map((f) => ({
|
|
40
|
+
routeName: 'agent',
|
|
41
|
+
signalType: 'agent_finding',
|
|
42
|
+
rawData: agentFindingToRawData(f),
|
|
43
|
+
rawArtifactRefs: [],
|
|
44
|
+
}));
|
|
45
|
+
// Compose QARunStore.createRun. Triage stage may later promote some
|
|
46
|
+
// signals to QATickets; that's not this ingestor's job.
|
|
47
|
+
let detail;
|
|
48
|
+
try {
|
|
49
|
+
detail = await runStore.createRun({
|
|
50
|
+
projectId,
|
|
51
|
+
triggeredBy: envelope.agentVersion ?? 'qa-agent',
|
|
52
|
+
triggerType: 'AGENT',
|
|
53
|
+
scope: buildScopeFromEnvelope(envelope),
|
|
54
|
+
status: 'COMPLETED',
|
|
55
|
+
completedAt: new Date(),
|
|
56
|
+
signals,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof QARunStoreError && err.code === 'fk_violation') {
|
|
61
|
+
throw new AgentFindingsIngestorError('fk_violation', `AgentFindingsIngestor: QARunStore reports FK violation: ${err.message}`, { projectId });
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
qaRunId: detail.run.id,
|
|
67
|
+
status: detail.run.status,
|
|
68
|
+
signalsRaised: detail.run.signalsRaised,
|
|
69
|
+
droppedInfoCount,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Project a single agent finding to RawSignal.rawData. Optional fields
|
|
77
|
+
* are omitted (rather than written as null) so JSON column reads stay
|
|
78
|
+
* tight — Lifeline parity.
|
|
79
|
+
*/
|
|
80
|
+
function agentFindingToRawData(f) {
|
|
81
|
+
const data = {
|
|
82
|
+
sev: f.sev,
|
|
83
|
+
area: f.area,
|
|
84
|
+
msg: f.msg,
|
|
85
|
+
};
|
|
86
|
+
if (f.file !== undefined && f.file !== null)
|
|
87
|
+
data.file = f.file;
|
|
88
|
+
if (f.tags !== undefined && f.tags.length > 0)
|
|
89
|
+
data.tags = [...f.tags];
|
|
90
|
+
if (f.proposedFix !== undefined && f.proposedFix !== null)
|
|
91
|
+
data.proposedFix = f.proposedFix;
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Pack the non-finding envelope fields into QARun.scope so the dashboard
|
|
96
|
+
* can render the agent's summary metadata without an extra query. Findings
|
|
97
|
+
* intentionally omitted — they live as RawSignal rows.
|
|
98
|
+
*/
|
|
99
|
+
function buildScopeFromEnvelope(envelope) {
|
|
100
|
+
const scope = {
|
|
101
|
+
source: envelope.source,
|
|
102
|
+
generatedAt: envelope.generatedAt,
|
|
103
|
+
platform: { ...envelope.platform },
|
|
104
|
+
};
|
|
105
|
+
if (envelope.agentVersion !== undefined)
|
|
106
|
+
scope.agentVersion = envelope.agentVersion;
|
|
107
|
+
if (envelope.healthScore !== undefined)
|
|
108
|
+
scope.healthScore = envelope.healthScore;
|
|
109
|
+
if (envelope.summary !== undefined)
|
|
110
|
+
scope.summary = envelope.summary;
|
|
111
|
+
if (envelope.sprintPlan !== undefined)
|
|
112
|
+
scope.sprintPlan = envelope.sprintPlan;
|
|
113
|
+
if (envelope.moduleHealth !== undefined)
|
|
114
|
+
scope.moduleHealth = envelope.moduleHealth;
|
|
115
|
+
if (envelope.issuesByArea !== undefined)
|
|
116
|
+
scope.issuesByArea = envelope.issuesByArea;
|
|
117
|
+
return scope;
|
|
118
|
+
}
|
|
119
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
120
|
+
function validateInput(input) {
|
|
121
|
+
if (typeof input.projectId !== 'string' || input.projectId.length === 0) {
|
|
122
|
+
throw new AgentFindingsIngestorError('invalid_input', 'AgentFindingsIngestor: projectId is required');
|
|
123
|
+
}
|
|
124
|
+
const envelopeValue = input.envelope;
|
|
125
|
+
if (typeof envelopeValue !== 'object' || envelopeValue === null) {
|
|
126
|
+
throw new AgentFindingsIngestorError('invalid_input', 'AgentFindingsIngestor: envelope is required');
|
|
127
|
+
}
|
|
128
|
+
// Cast through unknown to bypass the literal-type narrowing — this guard
|
|
129
|
+
// catches untyped JSON from the route layer where source could be anything.
|
|
130
|
+
const sourceValue = input.envelope.source;
|
|
131
|
+
if (sourceValue !== 'qa-agent') {
|
|
132
|
+
throw new AgentFindingsIngestorError('invalid_input', `AgentFindingsIngestor: envelope.source must be 'qa-agent' (got '${String(sourceValue)}')`);
|
|
133
|
+
}
|
|
134
|
+
const findingsValue = input.envelope.findings;
|
|
135
|
+
if (!Array.isArray(findingsValue)) {
|
|
136
|
+
throw new AgentFindingsIngestorError('invalid_input', 'AgentFindingsIngestor: envelope.findings must be an array');
|
|
137
|
+
}
|
|
138
|
+
input.envelope.findings.forEach((f, i) => {
|
|
139
|
+
validateFinding(f, i);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function validateFinding(f, idx) {
|
|
143
|
+
const validSevs = ['critical', 'high', 'medium', 'low', 'info'];
|
|
144
|
+
if (typeof f.sev !== 'string' || !validSevs.includes(f.sev)) {
|
|
145
|
+
throw new AgentFindingsIngestorError('invalid_input', `AgentFindingsIngestor: findings[${String(idx)}].sev must be one of ${validSevs.join('|')}`);
|
|
146
|
+
}
|
|
147
|
+
if (typeof f.area !== 'string' || f.area.length === 0) {
|
|
148
|
+
throw new AgentFindingsIngestorError('invalid_input', `AgentFindingsIngestor: findings[${String(idx)}].area is required`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof f.msg !== 'string' || f.msg.length === 0) {
|
|
151
|
+
throw new AgentFindingsIngestorError('invalid_input', `AgentFindingsIngestor: findings[${String(idx)}].msg is required`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=agent-findings-ingestor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-findings-ingestor.js","sourceRoot":"","sources":["../src/agent-findings-ingestor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClassificationTrustStore — Prisma implementation against Derwin's
|
|
3
|
+
* ClassificationTrust table.
|
|
4
|
+
*
|
|
5
|
+
* QAP-015 (Sprint 2). Implements the ClassificationTrustStore contract from
|
|
6
|
+
* @derwinjs/sdk (see packages/sdk/src/types/classification-trust-store.ts)
|
|
7
|
+
* against the @derwinjs/db Prisma client. Mirrors createPrismaQAFixAttemptStore
|
|
8
|
+
* in structure: a factory taking `{ prisma }` so callers inject the client
|
|
9
|
+
* (per the architecture pivot's DI seam — @derwinjs/db owns persistence; the
|
|
10
|
+
* orchestrator wires it in via configureQAPlatform()).
|
|
11
|
+
*
|
|
12
|
+
* Tenant isolation: every method scopes by projectId in the WHERE clause and
|
|
13
|
+
* uses findFirst (not findUnique) for reads so wrong-tenant lookups return
|
|
14
|
+
* null instead of leaking the (classification, surface) tuple's existence
|
|
15
|
+
* cross-project. App-layer guards in Sprint 2 ahead of the full Supabase RLS
|
|
16
|
+
* migration (Sprint 3 / QAP-024).
|
|
17
|
+
*
|
|
18
|
+
* Composes with TrustScoreUpdater (@derwinjs/core, Layer F): the updater
|
|
19
|
+
* recomputes whole rows from scratch on each cron tick (rolling 30-day
|
|
20
|
+
* window aggregation). It does NOT supply lastComputedAt — this store
|
|
21
|
+
* always overwrites it with `new Date()` on both upsert branches per the
|
|
22
|
+
* SDK contract.
|
|
23
|
+
*/
|
|
24
|
+
import { type PrismaClient } from './prisma.js';
|
|
25
|
+
import { type ClassificationTrustStore } from '@derwinjs/sdk';
|
|
26
|
+
export interface PrismaClassificationTrustStoreConfig {
|
|
27
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
28
|
+
prisma: PrismaClient;
|
|
29
|
+
}
|
|
30
|
+
export declare function createPrismaClassificationTrustStore(config: PrismaClassificationTrustStoreConfig): ClassificationTrustStore;
|
|
31
|
+
//# sourceMappingURL=classification-trust-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classification-trust-store.d.ts","sourceRoot":"","sources":["../src/classification-trust-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAEL,KAAK,wBAAwB,EAG9B,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,oCAAoC;IACnD,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAID,wBAAgB,oCAAoC,CAClD,MAAM,EAAE,oCAAoC,GAC3C,wBAAwB,CA6F1B"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClassificationTrustStore — Prisma implementation against Derwin's
|
|
3
|
+
* ClassificationTrust table.
|
|
4
|
+
*
|
|
5
|
+
* QAP-015 (Sprint 2). Implements the ClassificationTrustStore contract from
|
|
6
|
+
* @derwinjs/sdk (see packages/sdk/src/types/classification-trust-store.ts)
|
|
7
|
+
* against the @derwinjs/db Prisma client. Mirrors createPrismaQAFixAttemptStore
|
|
8
|
+
* in structure: a factory taking `{ prisma }` so callers inject the client
|
|
9
|
+
* (per the architecture pivot's DI seam — @derwinjs/db owns persistence; the
|
|
10
|
+
* orchestrator wires it in via configureQAPlatform()).
|
|
11
|
+
*
|
|
12
|
+
* Tenant isolation: every method scopes by projectId in the WHERE clause and
|
|
13
|
+
* uses findFirst (not findUnique) for reads so wrong-tenant lookups return
|
|
14
|
+
* null instead of leaking the (classification, surface) tuple's existence
|
|
15
|
+
* cross-project. App-layer guards in Sprint 2 ahead of the full Supabase RLS
|
|
16
|
+
* migration (Sprint 3 / QAP-024).
|
|
17
|
+
*
|
|
18
|
+
* Composes with TrustScoreUpdater (@derwinjs/core, Layer F): the updater
|
|
19
|
+
* recomputes whole rows from scratch on each cron tick (rolling 30-day
|
|
20
|
+
* window aggregation). It does NOT supply lastComputedAt — this store
|
|
21
|
+
* always overwrites it with `new Date()` on both upsert branches per the
|
|
22
|
+
* SDK contract.
|
|
23
|
+
*/
|
|
24
|
+
import { ClassificationTrustStoreError, } from '@derwinjs/sdk';
|
|
25
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
26
|
+
export function createPrismaClassificationTrustStore(config) {
|
|
27
|
+
const { prisma } = config;
|
|
28
|
+
return {
|
|
29
|
+
async upsertTrust(input) {
|
|
30
|
+
validateUpsertInput(input);
|
|
31
|
+
// The lastComputedAt timestamp is owned by this store, not the caller —
|
|
32
|
+
// the SDK contract documents it as "Sets lastComputedAt to now on both
|
|
33
|
+
// paths." Generate once so create + update branches agree.
|
|
34
|
+
const lastComputedAt = new Date();
|
|
35
|
+
const dataShared = {
|
|
36
|
+
attemptsLast30d: input.attemptsLast30d,
|
|
37
|
+
mergedClean: input.mergedClean,
|
|
38
|
+
mergedWithEdits: input.mergedWithEdits,
|
|
39
|
+
regressed: input.regressed,
|
|
40
|
+
reverted: input.reverted,
|
|
41
|
+
successScore: input.successScore,
|
|
42
|
+
trustPercent: input.trustPercent,
|
|
43
|
+
autoMergeEligible: input.autoMergeEligible,
|
|
44
|
+
lastComputedAt,
|
|
45
|
+
};
|
|
46
|
+
const upserted = await prisma.classificationTrust.upsert({
|
|
47
|
+
where: {
|
|
48
|
+
projectId_classification_surface: {
|
|
49
|
+
projectId: input.projectId,
|
|
50
|
+
classification: input.classification,
|
|
51
|
+
surface: input.surface,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
create: {
|
|
55
|
+
projectId: input.projectId,
|
|
56
|
+
classification: input.classification,
|
|
57
|
+
surface: input.surface,
|
|
58
|
+
...dataShared,
|
|
59
|
+
},
|
|
60
|
+
update: dataShared,
|
|
61
|
+
});
|
|
62
|
+
return mapRow(upserted);
|
|
63
|
+
},
|
|
64
|
+
async getTrust(key) {
|
|
65
|
+
// findFirst (not findUnique) — leaking the (classification, surface)
|
|
66
|
+
// composite's existence cross-tenant via P2025 vs null is the exact
|
|
67
|
+
// enumeration vector ADR-0009 / Pattern D guards against. With
|
|
68
|
+
// projectId in the WHERE, missing-row + wrong-tenant collapse to
|
|
69
|
+
// identical null returns.
|
|
70
|
+
const row = await prisma.classificationTrust.findFirst({
|
|
71
|
+
where: {
|
|
72
|
+
projectId: key.projectId,
|
|
73
|
+
classification: key.classification,
|
|
74
|
+
surface: key.surface,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return row ? mapRow(row) : null;
|
|
78
|
+
},
|
|
79
|
+
async listTrust(filter) {
|
|
80
|
+
if (!filter.projectId) {
|
|
81
|
+
throw new ClassificationTrustStoreError('invalid_input', 'projectId is required for listTrust — there is no cross-tenant list query');
|
|
82
|
+
}
|
|
83
|
+
const where = {
|
|
84
|
+
projectId: filter.projectId,
|
|
85
|
+
};
|
|
86
|
+
if (filter.surface !== undefined) {
|
|
87
|
+
where.surface = Array.isArray(filter.surface) ? { in: filter.surface } : filter.surface;
|
|
88
|
+
}
|
|
89
|
+
if (filter.minTrustPercent !== undefined) {
|
|
90
|
+
where.trustPercent = { gte: filter.minTrustPercent };
|
|
91
|
+
}
|
|
92
|
+
if (filter.autoMergeEligible !== undefined) {
|
|
93
|
+
where.autoMergeEligible = filter.autoMergeEligible;
|
|
94
|
+
}
|
|
95
|
+
const limit = Math.max(1, Math.min(200, filter.limit ?? 50));
|
|
96
|
+
const rows = await prisma.classificationTrust.findMany({
|
|
97
|
+
where,
|
|
98
|
+
orderBy: { trustPercent: 'desc' },
|
|
99
|
+
take: limit,
|
|
100
|
+
});
|
|
101
|
+
return { trusts: rows.map(mapRow) };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
106
|
+
/** Convert a Prisma ClassificationTrust row to the SDK value type. */
|
|
107
|
+
function mapRow(row) {
|
|
108
|
+
return {
|
|
109
|
+
id: row.id,
|
|
110
|
+
projectId: row.projectId,
|
|
111
|
+
classification: row.classification,
|
|
112
|
+
surface: row.surface,
|
|
113
|
+
attemptsLast30d: row.attemptsLast30d,
|
|
114
|
+
mergedClean: row.mergedClean,
|
|
115
|
+
mergedWithEdits: row.mergedWithEdits,
|
|
116
|
+
regressed: row.regressed,
|
|
117
|
+
reverted: row.reverted,
|
|
118
|
+
successScore: row.successScore,
|
|
119
|
+
trustPercent: row.trustPercent,
|
|
120
|
+
autoMergeEligible: row.autoMergeEligible,
|
|
121
|
+
lastComputedAt: row.lastComputedAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function validateUpsertInput(input) {
|
|
125
|
+
if (!input.projectId) {
|
|
126
|
+
throw new ClassificationTrustStoreError('invalid_input', 'projectId is required');
|
|
127
|
+
}
|
|
128
|
+
if (!input.classification) {
|
|
129
|
+
throw new ClassificationTrustStoreError('invalid_input', 'classification is required');
|
|
130
|
+
}
|
|
131
|
+
// Bucket counts: rolling 30-day aggregates that can never be negative.
|
|
132
|
+
const counts = [
|
|
133
|
+
['attemptsLast30d', input.attemptsLast30d],
|
|
134
|
+
['mergedClean', input.mergedClean],
|
|
135
|
+
['mergedWithEdits', input.mergedWithEdits],
|
|
136
|
+
['regressed', input.regressed],
|
|
137
|
+
['reverted', input.reverted],
|
|
138
|
+
];
|
|
139
|
+
for (const [name, value] of counts) {
|
|
140
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
141
|
+
throw new ClassificationTrustStoreError('invalid_input', `${name} must be a non-negative finite number`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// successScore: derived ratio, bounded [-1, 1] per the schema docstring.
|
|
145
|
+
if (!Number.isFinite(input.successScore) || input.successScore < -1 || input.successScore > 1) {
|
|
146
|
+
throw new ClassificationTrustStoreError('invalid_input', 'successScore must be in [-1, 1]');
|
|
147
|
+
}
|
|
148
|
+
// trustPercent: integer percent for operator UI; range + integer enforced
|
|
149
|
+
// here so the DB never stores a fractional or out-of-range percent.
|
|
150
|
+
if (!Number.isInteger(input.trustPercent) || input.trustPercent < 0 || input.trustPercent > 100) {
|
|
151
|
+
throw new ClassificationTrustStoreError('invalid_input', 'trustPercent must be an integer in [0, 100]');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=classification-trust-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classification-trust-store.js","sourceRoot":"","sources":["../src/classification-trust-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EACL,6BAA6B,GAI9B,MAAM,eAAe,CAAC;AASvB,4EAA4E;AAE5E,MAAM,UAAU,oCAAoC,CAClD,MAA4C;IAE5C,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAE1B,OAAO;QACL,KAAK,CAAC,WAAW,CAAC,KAAK;YACrB,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAE3B,wEAAwE;YACxE,uEAAuE;YACvE,2DAA2D;YAC3D,MAAM,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC;YAElC,MAAM,UAAU,GAAG;gBACjB,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;gBAC1C,cAAc;aACf,CAAC;YAEF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;gBACvD,KAAK,EAAE;oBACL,gCAAgC,EAAE;wBAChC,SAAS,EAAE,KAAK,CAAC,SAAS;wBAC1B,cAAc,EAAE,KAAK,CAAC,cAAc;wBACpC,OAAO,EAAE,KAAK,CAAC,OAAO;qBACvB;iBACF;gBACD,MAAM,EAAE;oBACN,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,GAAG,UAAU;iBACd;gBACD,MAAM,EAAE,UAAU;aACnB,CAAC,CAAC;YAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,GAAG;YAChB,qEAAqE;YACrE,oEAAoE;YACpE,+DAA+D;YAC/D,iEAAiE;YACjE,0BAA0B;YAC1B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC;gBACrD,KAAK,EAAE;oBACL,SAAS,EAAE,GAAG,CAAC,SAAS;oBACxB,cAAc,EAAE,GAAG,CAAC,cAAc;oBAClC,OAAO,EAAE,GAAG,CAAC,OAAO;iBACrB;aACF,CAAC,CAAC;YACH,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClC,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,MAAM;YACpB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBACtB,MAAM,IAAI,6BAA6B,CACrC,eAAe,EACf,2EAA2E,CAC5E,CAAC;YACJ,CAAC;YAED,MAAM,KAAK,GAAyC;gBAClD,SAAS,EAAE,MAAM,CAAC,SAAS;aAC5B,CAAC;YAEF,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBACjC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;YAC1F,CAAC;YACD,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;gBACzC,KAAK,CAAC,YAAY,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,eAAe,EAAE,CAAC;YACvD,CAAC;YACD,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;gBAC3C,KAAK,CAAC,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC;YACrD,CAAC;YAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;YAE7D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,QAAQ,CAAC;gBACrD,KAAK;gBACL,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE;gBACjC,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;YAEH,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAE5E,sEAAsE;AACtE,SAAS,MAAM,CACb,GAAgE;IAEhE,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,cAAc,EAAE,GAAG,CAAC,cAAc;KACnC,CAAC;AACJ,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAqC;IAChE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACrB,MAAM,IAAI,6BAA6B,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1B,MAAM,IAAI,6BAA6B,CAAC,eAAe,EAAE,4BAA4B,CAAC,CAAC;IACzF,CAAC;IAED,uEAAuE;IACvE,MAAM,MAAM,GAAuB;QACjC,CAAC,iBAAiB,EAAE,KAAK,CAAC,eAAe,CAAC;QAC1C,CAAC,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC;QAClC,CAAC,iBAAiB,EAAE,KAAK,CAAC,eAAe,CAAC;QAC1C,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC;QAC9B,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;KAC7B,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,6BAA6B,CACrC,eAAe,EACf,GAAG,IAAI,uCAAuC,CAC/C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QAC9F,MAAM,IAAI,6BAA6B,CAAC,eAAe,EAAE,iCAAiC,CAAC,CAAC;IAC9F,CAAC;IAED,0EAA0E;IAC1E,oEAAoE;IACpE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,IAAI,KAAK,CAAC,YAAY,GAAG,GAAG,EAAE,CAAC;QAChG,MAAM,IAAI,6BAA6B,CACrC,eAAe,EACf,6CAA6C,CAC9C,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoverageGapReporter — Prisma-backed report listing tickets joined with
|
|
3
|
+
* their test-coverage signal status.
|
|
4
|
+
*
|
|
5
|
+
* QAP-019F (Sprint 2 Group D-3, Commit 3). Implements the
|
|
6
|
+
* CoverageGapReporter contract from @derwinjs/sdk under **Option β**.
|
|
7
|
+
*
|
|
8
|
+
* **Reframe vs Lifeline**: Lifeline's coverage-gap was a hardcoded list of
|
|
9
|
+
* dashboard ticket IDs joined LEFT against QARunResult to find tickets
|
|
10
|
+
* without a Playwright spec. Derwin doesn't ship dashboard-ticket parity
|
|
11
|
+
* (Sprint 4+ owns Profile-driven ticket inventory) and doesn't have
|
|
12
|
+
* QARunResult under Option β — per-test envelopes live as RawSignal records
|
|
13
|
+
* with `signalType='test_failure'`.
|
|
14
|
+
*
|
|
15
|
+
* The reframed query (Decision 3): a ticket "has coverage" if there exists
|
|
16
|
+
* at least one RawSignal with `signalType='test_failure'` and
|
|
17
|
+
* `qaTicketId=ticket.id`. Same dashboard intent ("tickets we track but
|
|
18
|
+
* lack signal evidence"), different mechanic.
|
|
19
|
+
*
|
|
20
|
+
* The query is two findMany calls:
|
|
21
|
+
* 1. List all QATickets in the project (id + display fields).
|
|
22
|
+
* 2. List the distinct qaTicketIds from RawSignals where
|
|
23
|
+
* signalType='test_failure' in this project.
|
|
24
|
+
* Then we LEFT JOIN in memory: hasSignal = signalIds.has(ticket.id).
|
|
25
|
+
*
|
|
26
|
+
* Tenant isolation: every query scopes by projectId.
|
|
27
|
+
*/
|
|
28
|
+
import type { PrismaClient } from './prisma.js';
|
|
29
|
+
import { type CoverageGapReporter } from '@derwinjs/sdk';
|
|
30
|
+
export interface PrismaCoverageGapReporterConfig {
|
|
31
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
32
|
+
prisma: PrismaClient;
|
|
33
|
+
}
|
|
34
|
+
export declare function createPrismaCoverageGapReporter(config: PrismaCoverageGapReporterConfig): CoverageGapReporter;
|
|
35
|
+
//# sourceMappingURL=coverage-gap-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage-gap-reporter.d.ts","sourceRoot":"","sources":["../src/coverage-gap-reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAIL,KAAK,mBAAmB,EAEzB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,+BAA+B;IAC9C,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAID,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,+BAA+B,GACtC,mBAAmB,CAuDrB"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoverageGapReporter — Prisma-backed report listing tickets joined with
|
|
3
|
+
* their test-coverage signal status.
|
|
4
|
+
*
|
|
5
|
+
* QAP-019F (Sprint 2 Group D-3, Commit 3). Implements the
|
|
6
|
+
* CoverageGapReporter contract from @derwinjs/sdk under **Option β**.
|
|
7
|
+
*
|
|
8
|
+
* **Reframe vs Lifeline**: Lifeline's coverage-gap was a hardcoded list of
|
|
9
|
+
* dashboard ticket IDs joined LEFT against QARunResult to find tickets
|
|
10
|
+
* without a Playwright spec. Derwin doesn't ship dashboard-ticket parity
|
|
11
|
+
* (Sprint 4+ owns Profile-driven ticket inventory) and doesn't have
|
|
12
|
+
* QARunResult under Option β — per-test envelopes live as RawSignal records
|
|
13
|
+
* with `signalType='test_failure'`.
|
|
14
|
+
*
|
|
15
|
+
* The reframed query (Decision 3): a ticket "has coverage" if there exists
|
|
16
|
+
* at least one RawSignal with `signalType='test_failure'` and
|
|
17
|
+
* `qaTicketId=ticket.id`. Same dashboard intent ("tickets we track but
|
|
18
|
+
* lack signal evidence"), different mechanic.
|
|
19
|
+
*
|
|
20
|
+
* The query is two findMany calls:
|
|
21
|
+
* 1. List all QATickets in the project (id + display fields).
|
|
22
|
+
* 2. List the distinct qaTicketIds from RawSignals where
|
|
23
|
+
* signalType='test_failure' in this project.
|
|
24
|
+
* Then we LEFT JOIN in memory: hasSignal = signalIds.has(ticket.id).
|
|
25
|
+
*
|
|
26
|
+
* Tenant isolation: every query scopes by projectId.
|
|
27
|
+
*/
|
|
28
|
+
import { CoverageGapReporterError, } from '@derwinjs/sdk';
|
|
29
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
30
|
+
export function createPrismaCoverageGapReporter(config) {
|
|
31
|
+
const { prisma } = config;
|
|
32
|
+
return {
|
|
33
|
+
async getCoverageGap(filter) {
|
|
34
|
+
validateFilter(filter);
|
|
35
|
+
// 1. All tickets in the project — minimal fields the contract returns.
|
|
36
|
+
const tickets = await prisma.qATicket.findMany({
|
|
37
|
+
where: { projectId: filter.projectId },
|
|
38
|
+
orderBy: { createdAt: 'desc' },
|
|
39
|
+
select: {
|
|
40
|
+
id: true,
|
|
41
|
+
title: true,
|
|
42
|
+
classification: true,
|
|
43
|
+
status: true,
|
|
44
|
+
createdAt: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (tickets.length === 0) {
|
|
48
|
+
return { tickets: [] };
|
|
49
|
+
}
|
|
50
|
+
// 2. Distinct qaTicketIds with at least one test_failure signal.
|
|
51
|
+
//
|
|
52
|
+
// RawSignal.qaTicketId is FK-scoped to QATicket; tenant-isolation
|
|
53
|
+
// happens via the parent ticket projectId filter when we join.
|
|
54
|
+
// We restrict the lookup to this project's ticket ids to keep the
|
|
55
|
+
// query bounded.
|
|
56
|
+
const ticketIds = tickets.map((t) => t.id);
|
|
57
|
+
const signalRows = await prisma.rawSignal.findMany({
|
|
58
|
+
where: {
|
|
59
|
+
signalType: 'test_failure',
|
|
60
|
+
qaTicketId: { in: ticketIds },
|
|
61
|
+
},
|
|
62
|
+
select: { qaTicketId: true },
|
|
63
|
+
distinct: ['qaTicketId'],
|
|
64
|
+
});
|
|
65
|
+
const ticketIdsWithSignal = new Set(signalRows.map((r) => r.qaTicketId).filter((id) => id !== null));
|
|
66
|
+
const rows = tickets.map((t) => ({
|
|
67
|
+
id: t.id,
|
|
68
|
+
title: t.title,
|
|
69
|
+
classification: t.classification,
|
|
70
|
+
status: t.status,
|
|
71
|
+
createdAt: t.createdAt,
|
|
72
|
+
hasSignal: ticketIdsWithSignal.has(t.id),
|
|
73
|
+
}));
|
|
74
|
+
return { tickets: rows };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
79
|
+
function validateFilter(filter) {
|
|
80
|
+
if (typeof filter.projectId !== 'string' || filter.projectId.length === 0) {
|
|
81
|
+
throw new CoverageGapReporterError('invalid_input', 'CoverageGapReporter: projectId is required');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=coverage-gap-reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage-gap-reporter.js","sourceRoot":"","sources":["../src/coverage-gap-reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EACL,wBAAwB,GAKzB,MAAM,eAAe,CAAC;AASvB,4EAA4E;AAE5E,MAAM,UAAU,+BAA+B,CAC7C,MAAuC;IAEvC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAE1B,OAAO;QACL,KAAK,CAAC,cAAc,CAAC,MAAyB;YAC5C,cAAc,CAAC,MAAM,CAAC,CAAC;YAEvB,uEAAuE;YACvE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAC7C,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE;gBACtC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;gBAC9B,MAAM,EAAE;oBACN,EAAE,EAAE,IAAI;oBACR,KAAK,EAAE,IAAI;oBACX,cAAc,EAAE,IAAI;oBACpB,MAAM,EAAE,IAAI;oBACZ,SAAS,EAAE,IAAI;iBAChB;aACF,CAAC,CAAC;YAEH,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACzB,CAAC;YAED,iEAAiE;YACjE,EAAE;YACF,kEAAkE;YAClE,+DAA+D;YAC/D,kEAAkE;YAClE,iBAAiB;YACjB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC3C,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;gBACjD,KAAK,EAAE;oBACL,UAAU,EAAE,cAAc;oBAC1B,UAAU,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;iBAC9B;gBACD,MAAM,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;gBAC5B,QAAQ,EAAE,CAAC,YAAY,CAAC;aACzB,CAAC,CAAC;YACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CACjC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC,CAC9E,CAAC;YAEF,MAAM,IAAI,GAA2B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvD,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,cAAc,EAAE,CAAC,CAAC,cAAc;gBAChC,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,SAAS,EAAE,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;aACzC,CAAC,CAAC,CAAC;YAEJ,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAE5E,SAAS,cAAc,CAAC,MAAyB;IAC/C,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,wBAAwB,CAChC,eAAe,EACf,4CAA4C,CAC7C,CAAC;IACJ,CAAC;AACH,CAAC"}
|