feat(qa-ops): 按确认口径补齐质量比例与分母明细
build / build (push) Successful in 1m6s

使用完整筛选范围的当前有效结论计算覆盖率和正确率,人工优先,失效与对应待确认/未评估/不计入排除;零分母返回null,界面展示原始分子分母和排除构成,前端拒绝不守恒或比例不一致的响应。

验证:make verify退出码0,Go658及前端257通过,91项声明可选集成跳过;隔离PG、三包竞态及合成/真实只读源浏览器验证通过。无新增迁移,不接AI模型,不改绩效,不写源库;本批未推送或部署,8088运行版保持0.1.21。
This commit is contained in:
姚章浩
2026-10-18 02:00:31 +08:00
parent 4b3aaff321
commit 828c2c5fa0
13 changed files with 367 additions and 11 deletions
+3 -1
View File
@@ -4,8 +4,10 @@
### Added
- 问答运营按已确认口径显示知识覆盖率、回答正确率及原始分子/分母、排除数量与构成。使用完整筛选范围和当前有效人工优先结论,精确规则命中的未覆盖可进入覆盖分母;部分覆盖不算完全覆盖,待确认/未评估/失效/不计入按对应指标排除,零分母显示“暂无可计算数据”。返回独立规则版本,AI 模型与绩效范围仍未接入。
- 问答详情支持治理侧人工复核及不可变审计:覆盖、正确性、产品分类、问题类型、根因与理由由管理员明确填写,操作人取登录身份;保存后独立读回复核历史。保留源端判断和自动规则,输入变化使旧人工结论失效,已观测的内容回退不自动恢复旧结论;并发修改冲突、重复请求不重复留档。
- 新增覆盖复检历史、已采集输入快照和完整筛选范围质量数量。批量统计只传指纹与规则命中元数据,原文在读取问答/复核时采集,未采集版本明确提示而非伪造空快照;源事务时点阻止延迟旧扫描回退新输入。仅提供数量,不接 AI 模型、不定义质量率或绩效分母、不回写 DobotAI 原问答。
- 新增覆盖复检历史、已采集输入快照和完整筛选范围质量数量。批量统计只传指纹与规则命中元数据,原文在读取问答/复核时采集,未采集版本明确提示而非伪造空快照;源事务时点阻止延迟旧扫描回退新输入。基础数量与本期经确认新增的质量比例分开显示;不接 AI 模型、不定义绩效分母、不回写 DobotAI 原问答。
- 「使用统计 → 个人月明细」按北京时间自然月及截止时点展示当前应用可见名册的人员用量(包含本源零记录),支持部门含子部门、区域/待核对状态、姓名/完整部门路径和已有提问/零记录筛选。稳定身份关联去重、不可关联问答单列、过期组织版本阻断翻页;源端姓名可缺失并提示重新同步,不按姓名匹配身份。姓名筛选通过有界只读 POST 请求体传递、不进入 URL,原始员工标识不返回浏览器;名册不作为在职/考核分母,当月进度不作为绩效结论。
+1 -1
View File
@@ -101,7 +101,7 @@
},
"/api/v1/qa-ops/usage/people": {"post": {"tags": ["QA operations"], "operationId": "queryQAOperationsMonthlyPeople", "summary": "按北京时间自然月列出当前名册人员的2.0用量(含零记录),仅管理员,不返回原始员工标识,不作为在职/考核名单", "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}, "409": {"description": "完整名册缺失或目录/区域版本变化,请同步/重新查询,不返回伪零"}}, "description": "只读查询,不写治理库或生产库;姓名筛选置于 JSON 请求体,不进入 URL;整名册指标不随人员筛选变化。第2页起须带首页目录、区域版本及 end 截止。", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "required": ["month"], "properties": {"month": {"type": "string", "pattern": "^(?!0000)[0-9]{4}-(0[1-9]|1[0-2])$"}, "cutoff": {"type": "string", "format": "date-time"}, "region": {"type": "string", "enum": ["国内工业", "APAC", "EMEA", "America", "unassigned", "cross_region"]}, "department_id": {"type": "string"}, "activity": {"type": "string", "enum": ["all", "used", "zero"]}, "search": {"type": "string", "maxLength": 100}, "page": {"type": "integer", "minimum": 1, "maximum": 100000, "default": 1}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 30}, "directory_id": {"type": "string"}, "revision_id": {"type": "string"}}}}}}, "parameters": [{"name": "X-Governance-Action", "in": "header", "required": true, "schema": {"type": "string", "enum": ["qa-usage-read"]}}]}},
"/api/v1/qa-ops/usage": {"get": {"tags": ["QA operations"], "operationId": "getQAOperationsUsage", "summary": "完整筛选范围内按当前通讯录和区域版本回看2.0问答使用;固定租户、管理员访问,不输出绩效结论", "parameters": [{"name": "start", "in": "query", "required": true, "schema": {"type": "string", "format": "date-time"}}, {"name": "end", "in": "query", "required": true, "schema": {"type": "string", "format": "date-time"}}, {"name": "keyword", "in": "query", "schema": {"type": "string", "maxLength": 200}}], "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}}}},
"/api/v1/qa-ops/quality/query": {"post": {"operationId": "queryQAQualityCounts", "summary": "按完整时间/原问题字面关键词读取指纹及规则命中元数据并留档统计,原文仅在读取问答/复核时采集;不计算质量率,最多5000条", "parameters": [{"name": "X-Governance-Action", "in": "header", "required": true, "schema": {"type": "string", "enum": ["qa-quality"]}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "properties": {"start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "keyword": {"type": "string", "maxLength": 200}}, "required": ["start", "end"]}}}}, "tags": ["QA operations"], "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}, "409": {"description": "输入或复核版本变化/重复操作载荷不一致,未覆盖原结论"}, "404": {"description": "当前授权范围内未找到记录/历史输入"}}}},
"/api/v1/qa-ops/quality/query": {"post": {"operationId": "queryQAQualityCounts", "summary": "按完整筛选范围读取指纹/规则元数据并留档统计,最多5000条;summary.rates 返回 quality-rates-human-first-v1、原始分子/分母、百分比与分母构成。覆盖率=covered/(covered+uncovered+partial),正确率=covered_correct/(covered_correct+covered_incorrect);失效/不计入/相应待确认与未评估排除,零分母percent=null。原文仅在读取问答/复核时采集", "parameters": [{"name": "X-Governance-Action", "in": "header", "required": true, "schema": {"type": "string", "enum": ["qa-quality"]}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "properties": {"start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "keyword": {"type": "string", "maxLength": 200}}, "required": ["start", "end"]}}}}, "tags": ["QA operations"], "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}, "409": {"description": "输入或复核版本变化/重复操作载荷不一致,未覆盖原结论"}, "404": {"description": "当前授权范围内未找到记录/历史输入"}}}},
"/api/v1/qa-ops/records/{id}/reviews": {"post": {"operationId": "saveQAQualityReview", "summary": "治理库追加人工复核,登录身份审计,源端只读,输入/复核乐观锁和nonce幂等;正确时根因为空,不计入须说明原因", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "X-Governance-Action", "in": "header", "required": true, "schema": {"type": "string", "enum": ["qa-quality"]}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "properties": {"input_hash": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, "input_epoch": {"type": "integer", "minimum": 1}, "base_revision": {"type": "integer", "minimum": 0}, "nonce": {"type": "string", "pattern": "^[A-Za-z0-9_-]{16,128}$"}, "fields": {"type": "object", "additionalProperties": false, "properties": {"coverage": {"type": "string", "enum": ["covered", "uncovered", "partial", "pending", "excluded"]}, "correctness": {"type": "string", "enum": ["correct", "incorrect", "pending"]}, "product_category": {"type": "string", "maxLength": 128}, "problem_type": {"type": "string", "maxLength": 128}, "root_cause": {"type": "string", "maxLength": 2000}, "exclusion_reason": {"type": "string", "maxLength": 2000}}, "required": ["coverage", "correctness"]}, "reason": {"type": "string", "minLength": 1, "maxLength": 4000}}, "required": ["input_hash", "input_epoch", "base_revision", "nonce", "fields", "reason"]}}}}, "tags": ["QA operations"], "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}, "409": {"description": "输入或复核版本变化/重复操作载荷不一致,未覆盖原结论"}, "404": {"description": "当前授权范围内未找到记录/历史输入"}}}},
"/api/v1/qa-ops/records/{id}/quality/history": {"get": {"operationId": "listQAQualityHistory", "summary": "按当前固定租户查询不可变复检/人工历史,分别倒序分页,各20条", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "review_before", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "check_before", "in": "query", "schema": {"type": "integer", "minimum": 1}}], "tags": ["QA operations"], "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}, "409": {"description": "输入或复核版本变化/重复操作载荷不一致,未覆盖原结论"}, "404": {"description": "当前授权范围内未找到记录/历史输入"}}}},
"/api/v1/qa-ops/records/{id}/quality/inputs/{epoch}": {"get": {"operationId": "getQAQualityInputSnapshot", "summary": "管理员读取已采集的历史质量输入快照,不另存人员关联/反馈字段;元数据留档但未采集原文返回409,不编造空快照", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "epoch", "in": "path", "required": true, "schema": {"type": "integer", "minimum": 1}}], "tags": ["QA operations"], "responses": {"200": {"$ref": "#/components/responses/Envelope"}, "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"description": "需要登录"}, "403": {"description": "仅管理员访问"}, "503": {"description": "独立只读源或治理组织存储不可用,不返回伪造的零值"}, "500": {"$ref": "#/components/responses/InternalError"}, "409": {"description": "该输入版本仅有指纹/规则元数据、原文未采集"}, "404": {"description": "当前授权范围内未找到记录/历史输入"}}}},
+8
View File
@@ -117,6 +117,14 @@ func TestQAQualityDetailListAndWholeScopeCounts(t *testing.T) {
if w.Code != 200 || !strings.Contains(w.Body.String(), `"unreviewed":2`) || source.scopeCalls != 1 || source.tenant != "fixed" || store.calls < 2 {
t.Fatal("quality summary estimated from page instead of scope", w.Code, w.Body.String())
}
var counts struct {
Data struct {
Summary qaopsquality.Summary `json:"summary"`
} `json:"data"`
}
if json.Unmarshal(w.Body.Bytes(), &counts) != nil || counts.Data.Summary.Rates.Coverage.Denominator != 2 || counts.Data.Summary.Rates.Coverage.Numerator != 0 || counts.Data.Summary.Rates.Correctness.Percent != nil {
t.Fatal("confirmed quality metrics were not computed for the whole scope")
}
store.fail = true
w = qualityRequest(h, "GET", "/api/v1/qa-ops/records/fixture-turn", "quality-admin", "")
if w.Code != 503 || !strings.Contains(w.Body.String(), "quality_unavailable") {
+1 -1
View File
@@ -104,7 +104,7 @@ func Project(hash string, epoch, revision int64, automatic Check, review *Review
return view
}
func Summarize(views map[string]View) Summary {
result := Summary{Total: len(views)}
result := Summary{Total: len(views), Rates: ComputeRates(views)}
for _, v := range views {
switch v.State {
case "reviewed":
+7 -6
View File
@@ -64,12 +64,13 @@ type History struct {
NextCheck int64 `json:"next_check"`
}
type Summary struct {
Total int `json:"total"`
Reviewed int `json:"reviewed"`
Unreviewed int `json:"unreviewed"`
Stale int `json:"stale"`
PendingConfirmation int `json:"pending_confirmation"`
RuleUncovered int `json:"rule_uncovered"`
Rates Rates `json:"rates"`
Total int `json:"total"`
Reviewed int `json:"reviewed"`
Unreviewed int `json:"unreviewed"`
Stale int `json:"stale"`
PendingConfirmation int `json:"pending_confirmation"`
RuleUncovered int `json:"rule_uncovered"`
}
type Receipt struct {
View View `json:"view"`
+72
View File
@@ -0,0 +1,72 @@
package qaopsquality
type Rate struct {
Numerator int `json:"numerator"`
Denominator int `json:"denominator"`
Percent *float64 `json:"percent"`
}
type RateBreakdown struct {
Covered int `json:"covered"`
Uncovered int `json:"uncovered"`
Partial int `json:"partial"`
Pending int `json:"pending"`
Unassessed int `json:"unassessed"`
Excluded int `json:"excluded"`
Stale int `json:"stale"`
CoveredCorrect int `json:"covered_correct"`
CoveredIncorrect int `json:"covered_incorrect"`
CoveredPending int `json:"covered_pending"`
CoveredUnassessed int `json:"covered_unassessed"`
}
type Rates struct {
PolicyVersion string `json:"policy_version"`
Coverage Rate `json:"coverage"`
Correctness Rate `json:"correctness"`
Breakdown RateBreakdown `json:"breakdown"`
}
func ComputeRates(views map[string]View) Rates {
out := Rates{PolicyVersion: "quality-rates-human-first-v1"}
b := &out.Breakdown
for _, v := range views {
if v.State == "stale" {
b.Stale++
continue
}
switch v.Effective.Coverage {
case "covered":
b.Covered++
switch v.Effective.Correctness {
case "correct":
b.CoveredCorrect++
case "incorrect":
b.CoveredIncorrect++
case "pending":
b.CoveredPending++
default:
b.CoveredUnassessed++
}
case "uncovered":
b.Uncovered++
case "partial":
b.Partial++
case "pending":
b.Pending++
case "excluded":
b.Excluded++
default:
b.Unassessed++
}
}
out.Coverage = makeRate(b.Covered, b.Covered+b.Uncovered+b.Partial)
out.Correctness = makeRate(b.CoveredCorrect, b.CoveredCorrect+b.CoveredIncorrect)
return out
}
func makeRate(numerator, denominator int) Rate {
r := Rate{Numerator: numerator, Denominator: denominator}
if denominator > 0 {
p := 100 * float64(numerator) / float64(denominator)
r.Percent = &p
}
return r
}
+44
View File
@@ -0,0 +1,44 @@
package qaopsquality
import (
"encoding/json"
"knowledge-governance/internal/dobotaiops"
"math"
"strings"
"testing"
)
func TestQualityRatesUseEffectiveCurrentConclusionsAndExplicitDenominators(t *testing.T) {
views := map[string]View{}
for i, fields := range []Fields{{Coverage: "covered", Correctness: "correct"}, {Coverage: "covered", Correctness: "incorrect"}, {Coverage: "covered", Correctness: "pending"}, {Coverage: "uncovered"}, {Coverage: "partial", Correctness: "correct"}, {Coverage: "pending", Correctness: "correct"}, {}, {Coverage: "excluded", Correctness: "pending"}, {Coverage: "covered", Correctness: "correct"}, {Coverage: "covered"}} {
state := "reviewed"
if i == 3 || i == 6 {
state = "unreviewed"
}
if i == 8 {
state = "stale"
}
views[string(rune('a'+i))] = View{State: state, Effective: fields, Automatic: Check{Result: dobotaiops.CheckAnswerCoverage("暂无已发布")}}
}
r := ComputeRates(views)
if r.PolicyVersion != "quality-rates-human-first-v1" || r.Coverage.Numerator != 4 || r.Coverage.Denominator != 6 || r.Correctness.Numerator != 1 || r.Correctness.Denominator != 2 {
t.Fatal("effective decisions, partial or exclusion scopes incorrect", r)
}
if r.Coverage.Percent == nil || math.Abs(*r.Coverage.Percent-100*float64(4)/6) > 1e-10 || r.Correctness.Percent == nil || *r.Correctness.Percent != 50 {
t.Fatal("ratio value disagrees with raw counts")
}
b := r.Breakdown
if b.Covered+b.Uncovered+b.Partial+b.Pending+b.Unassessed+b.Excluded+b.Stale != len(views) || b.CoveredCorrect+b.CoveredIncorrect+b.CoveredPending+b.CoveredUnassessed != b.Covered {
t.Fatal("denominator breakdown not conserved")
}
if got := Summarize(views); got.Rates.Coverage.Denominator != 6 {
t.Fatal("scope summary did not consume the one shared calculation")
}
}
func TestQualityRatesEmptyScopeHasNullPercent(t *testing.T) {
r := ComputeRates(map[string]View{})
raw, _ := json.Marshal(r)
if r.Coverage.Percent != nil || r.Correctness.Percent != nil || strings.Count(string(raw), `"percent":null`) != 2 {
t.Fatal("zero denominator fabricated zero or full rate")
}
}
+53
View File
@@ -41,3 +41,56 @@ describe('manual quality operations', () => {
expect(ok).toContain('AI 评估未接入')
})
})
it('displays approved rates with raw denominators and exclusions', () => {
const html = renderToStaticMarkup(
<QAQualitySummaryView data={parseQualitySummary(summaryFixture)} error="" loading={false} onRefresh={() => {}} />
)
for (const value of [
'知识覆盖率',
'回答正确率',
'50.0%',
'100.0%',
'分子',
'分母',
'排除',
'部分覆盖',
'quality-rates-human-first-v1',
])
expect(html).toContain(value)
expect(html).not.toContain('不计算比例')
})
it('does not turn zero denominators into 0% or 100%', () => {
const v = parseQualitySummary(structuredClone(summaryFixture))
v.summary = {
total: 0,
reviewed: 0,
unreviewed: 0,
stale: 0,
pending_confirmation: 0,
rule_uncovered: 0,
rates: {
policy_version: 'quality-rates-human-first-v1',
coverage: { numerator: 0, denominator: 0, percent: null },
correctness: { numerator: 0, denominator: 0, percent: null },
breakdown: {
covered: 0,
uncovered: 0,
partial: 0,
pending: 0,
unassessed: 0,
excluded: 0,
stale: 0,
covered_correct: 0,
covered_incorrect: 0,
covered_pending: 0,
covered_unassessed: 0,
},
},
} as typeof v.summary
const html = renderToStaticMarkup(
<QAQualitySummaryView data={parseQualitySummary(v)} error="" loading={false} onRefresh={() => {}} />
)
expect(html).toContain('暂无可计算数据')
expect(html).not.toContain('0.0%')
})
+40 -1
View File
@@ -421,8 +421,47 @@ export function QAQualitySummaryView({ data, loading, error, onRefresh }: Summar
待重新核对 {data.summary.stale} · 人工待确认 {data.summary.pending_confirmation} · 规则命中未覆盖{' '}
{data.summary.rule_uncovered}
</p>
<div className="qa-quality-rates" aria-label="质量比例">
{(
[
['知识覆盖率', data.summary.rates.coverage],
['回答正确率', data.summary.rates.correctness],
] as const
).map(([label, rate]) => (
<div key={label}>
<strong>
{label}:{rate.percent === null ? '暂无可计算数据' : `${rate.percent.toFixed(1)}%`}
</strong>
<p>
分子 {rate.numerator} / 分母 {rate.denominator};排除 {data.summary.total - rate.denominator} 条
</p>
</div>
))}
</div>
<details className="qa-ops-organization">
<summary>查看比例口径和分母构成</summary>
<p>
覆盖分母:已覆盖 {data.summary.rates.breakdown.covered} + 未覆盖 {data.summary.rates.breakdown.uncovered}{' '}
+ 部分覆盖 {data.summary.rates.breakdown.partial};部分覆盖不计完全覆盖。
</p>
<p>
覆盖分母排除:待确认 {data.summary.rates.breakdown.pending}、未评估{' '}
{data.summary.rates.breakdown.unassessed}、不计入 {data.summary.rates.breakdown.excluded}、待重新核对{' '}
{data.summary.rates.breakdown.stale}。
</p>
<p>
正确性分母仅含已覆盖且判断明确:正确 {data.summary.rates.breakdown.covered_correct} + 错误{' '}
{data.summary.rates.breakdown.covered_incorrect};已覆盖中的待确认{' '}
{data.summary.rates.breakdown.covered_pending}、未评估 {data.summary.rates.breakdown.covered_unassessed}{' '}
不计入,其余未覆盖/部分覆盖等记录也不计入。
</p>
<p>
当前有效人工结论优先;精确规则命中的未覆盖可进入覆盖分母,但不证明回答错误。失效记录暂不参与两项比例,原始问答运行成功也不代表答案正确。
</p>
<p className="qa-ops-muted">统计规则:{data.summary.rates.policy_version}</p>
</details>
<p className="qa-ops-muted">
AI 评估未接入;人工待确认是已复核中的子集,规则命中可与人工结论不同。不计算比例。读取:{stamp(data.as_of)}
AI 评估未接入;人工待确认是已复核中的子集,规则命中可与人工结论不同。读取:{stamp(data.as_of)}
</p>
</>
) : !error && !loading ? (
+14
View File
@@ -394,3 +394,17 @@
grid-template-columns: 100px 1fr;
}
}
.qa-quality-rates {
display: flex;
flex-wrap: wrap;
gap: 12px 32px;
margin: 12px 0;
}
.qa-quality-rates > div {
min-width: 0;
overflow-wrap: anywhere;
}
.qa-quality-rates p {
margin: 4px 0 0;
}
+26 -1
View File
@@ -40,7 +40,32 @@ export const qualityFixture = {
effective: fields,
}
export const summaryFixture = {
summary: { total: 3, reviewed: 1, unreviewed: 1, stale: 1, pending_confirmation: 0, rule_uncovered: 2 },
summary: {
total: 3,
reviewed: 1,
unreviewed: 1,
stale: 1,
pending_confirmation: 0,
rule_uncovered: 2,
rates: {
policy_version: 'quality-rates-human-first-v1',
coverage: { numerator: 1, denominator: 2, percent: 50 },
correctness: { numerator: 1, denominator: 1, percent: 100 },
breakdown: {
covered: 1,
uncovered: 1,
partial: 0,
pending: 0,
unassessed: 0,
excluded: 0,
stale: 1,
covered_correct: 1,
covered_incorrect: 0,
covered_pending: 0,
covered_unassessed: 0,
},
},
},
start: '2026-10-14T00:00:00Z',
end: '2026-10-15T00:00:00Z',
keyword: '',
+23
View File
@@ -49,3 +49,26 @@ describe('quality evidence contracts', () => {
).toHaveLength(1)
})
})
describe('confirmed quality rate contract', () => {
it('retains raw counts, null rates, scope breakdown and policy version', () => {
const result = parseQualitySummary(summaryFixture)
expect(result.summary.rates.coverage.numerator).toBe(1)
expect(result.summary.rates.coverage.denominator).toBe(2)
expect(result.summary.rates.correctness.percent).toBe(100)
})
it('rejects invalid or inconsistent numerator, denominator, percent and category sums', () => {
for (const change of [
(r: any) => (r.coverage.numerator = 3),
(r: any) => (r.coverage.denominator = 3),
(r: any) => (r.coverage.percent = 99),
(r: any) => (r.breakdown.stale = 0),
(r: any) => (r.correctness.percent = null),
(r: any) => (r.policy_version = 'unknown'),
]) {
const v = structuredClone(summaryFixture)
change(v.summary.rates)
expect(() => parseQualitySummary(v)).toThrow()
}
})
})
+75
View File
@@ -45,6 +45,29 @@ export interface QualitySnapshot {
created_at: string
payload: { id: string; question: string; answer: string; status: string; references: unknown; media: unknown }
}
export interface QualityRate {
numerator: number
denominator: number
percent: number | null
}
export interface QualityRates {
policy_version: 'quality-rates-human-first-v1'
coverage: QualityRate
correctness: QualityRate
breakdown: {
covered: number
uncovered: number
partial: number
pending: number
unassessed: number
excluded: number
stale: number
covered_correct: number
covered_incorrect: number
covered_pending: number
covered_unassessed: number
}
}
export interface QualitySummary {
summary: {
total: number
@@ -53,6 +76,7 @@ export interface QualitySummary {
stale: number
pending_confirmation: number
rule_uncovered: number
rates: QualityRates
}
start: string
end: string
@@ -195,6 +219,56 @@ export function parseQAQuality(value: unknown): QAQuality {
for (const key of Object.keys(expected) as (keyof QualityFields)[]) if (expected[key] !== effective[key]) return bad()
return { input_hash, input_epoch, revision, state: state as QAQuality['state'], automatic, latest_review, effective }
}
function qualityRate(value: unknown): QualityRate {
const v = obj(value),
numerator = num(v.numerator),
denominator = num(v.denominator)
if (numerator > denominator) return bad()
if (denominator === 0) {
if (v.percent !== null) return bad()
return { numerator, denominator, percent: null }
}
if (
typeof v.percent !== 'number' ||
!Number.isFinite(v.percent) ||
v.percent < 0 ||
v.percent > 100 ||
Math.abs(v.percent - (100 * numerator) / denominator) > 1e-8
)
return bad()
return { numerator, denominator, percent: v.percent }
}
function qualityRates(value: unknown, total: number, stale: number): QualityRates {
const v = obj(value),
r = obj(v.breakdown)
const b = {
covered: num(r.covered),
uncovered: num(r.uncovered),
partial: num(r.partial),
pending: num(r.pending),
unassessed: num(r.unassessed),
excluded: num(r.excluded),
stale: num(r.stale),
covered_correct: num(r.covered_correct),
covered_incorrect: num(r.covered_incorrect),
covered_pending: num(r.covered_pending),
covered_unassessed: num(r.covered_unassessed),
}
const coverage = qualityRate(v.coverage),
correctness = qualityRate(v.correctness)
if (
v.policy_version !== 'quality-rates-human-first-v1' ||
b.stale !== stale ||
num(b.covered + b.uncovered + b.partial + b.pending + b.unassessed + b.excluded + b.stale) !== total ||
num(b.covered_correct + b.covered_incorrect + b.covered_pending + b.covered_unassessed) !== b.covered ||
coverage.numerator !== b.covered ||
coverage.denominator !== b.covered + b.uncovered + b.partial ||
correctness.numerator !== b.covered_correct ||
correctness.denominator !== b.covered_correct + b.covered_incorrect
)
return bad()
return { policy_version: 'quality-rates-human-first-v1', coverage, correctness, breakdown: b }
}
export function parseQualitySummary(value: unknown): QualitySummary {
const v = obj(value),
s = obj(v.summary),
@@ -205,6 +279,7 @@ export function parseQualitySummary(value: unknown): QualitySummary {
stale: num(s.stale),
pending_confirmation: num(s.pending_confirmation),
rule_uncovered: num(s.rule_uncovered),
rates: qualityRates(s.rates, num(s.total), num(s.stale)),
}
const start = stamp(v.start),
end = stamp(v.end)