S529 lane B — what requirement does the review surface serve?
Task: id-417 D2/D3 (D3 blocked on D2). Question asked: what requirement does the review surface serve, is that requirement still live, and what should the surface therefore be. Posture: read-only on code; every claim below carries a file:line, a quoted passage, or a command and its output.
Headline. The built split does not mis-cut the two axes the record_lifecycle
migration declares — it serves a third axis the facet never modelled. The
quality-score has no persistence home, a hard ceiling of 65/100, and flags every expired
document regardless of quality. And the requirement the owner attributes to the client
feedback file is real and live, but it is not in that file — it is in the
gap-analysis pair, where it carries an explicit pre-launch owner decision.
1. What exists
Section titled “1. What exists”1.1 There are three axes, not two
Section titled “1.1 There are three axes, not two”The migration declares two and I do not contradict it — but a third exists on the typed
records and it is the one /review is built on.
| Axis | Columns | Home | Owner kinds |
|---|---|---|---|
| A — Review/Governance | governance_review_status, governance_review_due, governance_reviewer_id, verified_at, verified_by, content_owner_id | record_lifecycle | both {source_document, q_a_pair} |
| B — Freshness/expiry/cadence | freshness, lifecycle_type, expiry_date, next_review_date, review_cadence_days | record_lifecycle | source_document only |
| C — Publication | publication_status | source_documents and q_a_pairs — not record_lifecycle | both |
Axes A and B are the migration’s own, verbatim:
-- Review/Governance axis — spans {source_document, q_a_pair} (BI-20).—supabase/migrations/20260628190000_id131_record_lifecycle_facet.sql:33
-- Freshness/expiry/review-cadence axis — source_document-ONLY (D7, BI-22).— same file,:40, enforced byrecord_lifecycle_freshness_axis_chkat:58
Axis C is measured, not asserted:
select table_name, column_name from information_schema.columnswhere table_schema='public' and column_name='publication_status';-- → q_a_pairs.publication_status, source_documents.publication_status-- (record_lifecycle: no row)1.2 The surfaces, mapped to the axes
Section titled “1.2 The surfaces, mapped to the axes”| Surface | Axis served | Owner kinds reached | Evidence |
|---|---|---|---|
/review UI + /api/review/queue | C (publication + verification) | source_document only | queries .from('source_documents'), app/api/review/queue/route.ts:274; tabs are drafts / pending / verified-review / verified-audit / all / unclassified / awaiting-publication, components/review/review-tabs.tsx:30-34,106-142 |
/api/review/cadence | B | source_document only | .eq('owner_kind','source_document'), app/api/review/cadence/route.ts:101 |
/api/review/assignments | B | source_document only | same, app/api/review/assignments/route.ts:153 |
/api/review/action | A | both | resolveReviewItemOwner(...), app/api/review/action/route.ts:81-85; writes .eq('owner_kind', ownerKind), :144-145 |
/api/governance/review | A | source_document only | .eq('owner_kind','source_document') at :78, :94, :166, :234, :260 |
/promotion-gate + /api/governance/promotion-candidates/{accept,edit,reject} | knowledge-admission (R2) | q_a_pair only | app/api/governance/promotion-candidates/[extractionId]/accept/route.ts:17-29 |
Crons — all three write the same axis-A column, and all three are source_document-only:
| Cron | Schedule | Derives from | Writes |
|---|---|---|---|
freshness-transitions | daily 03:15 (vercel.json) | axis B | governance_review_status:'pending', route.ts:555-556 |
review-cadence | daily 03:45 | axis B | governance_review_status:'review_overdue', route.ts:210-211 |
quality-score | Sundays 05:00 | a score that is 46% axis B (§2) | governance_review_status:'pending', route.ts:376-378 |
1.3 Where the built split contradicts the schema
Section titled “1.3 Where the built split contradicts the schema”(i) The governance axis spans both kinds; every governance read surface is pinned to
one. /api/governance/review filters owner_kind='source_document' in all five of its
queries. But /api/review/action implements the full polymorphic write — and its own
comment says why the read side never followed:
“Previously hardcoded to source_documents in every branch, so /library’s Bulk Verify (q_a_pairs-only …) 404’d for every pair.
owner_kindis optional on the request body — that hook is OUT of this Subtask’s file-ownership boundary and still omits it” —app/api/review/action/route.ts:76-80
Net effect: the platform can act on a Q&A governance review that it cannot list. The
only caller that reaches the q_a_pair branch is /library’s bulk-verify hook, which is
not a review surface.
(ii) The axis separation the DDL enforces is dissolved in the cron layer. D7 makes
freshness structurally impossible on a q_a_pair (CHECK at :58). Yet two of the three
writers of the governance column derive their trigger entirely from freshness. The
migration separates the axes in the schema; the crons re-merge them at runtime.
(iii) /review serves axis C, which record_lifecycle does not model at all. So the
/review vs /api/governance/review split is not a mis-cut of axes A and B — it is a
surface for a third axis the facet never absorbed. This is a refinement of the
dispatch’s anchor, not a contradiction of it: the anchor’s claim that the route split
matches neither axis holds, and the measured reason is that /review’s actual concern is
outside the facet entirely.
(iv) An axis-A action writes an axis-B column. Approving a governance review advances
next_review_date:
governance_review_status: 'approved', … ...(nextReviewDate && { next_review_date: nextReviewDate })— app/api/governance/review/route.ts:198-204
2. Quality-score, answered
Section titled “2. Quality-score, answered”The owner’s question was literal, so here is the literal answer.
Where it is computed. lib/quality/quality-score.ts:178 calculateQualityScore.
It has exactly one call site in the entire tree — the cron:
$ grep -rn "calculateQualityScore\|calculateAndRoundQualityScore" app/ lib/ | grep -v lib/quality/quality-score.tsapp/api/cron/quality-score/route.ts:17 (import)app/api/cron/quality-score/route.ts:243 (call)bunx gitnexus impact calculateQualityScore --direction upstream → impactedCount: 3,
risk: "LOW", direct: 2, processes_affected: 1 (the cron GET).
What it scores, declared vs live. Declared: five weighted components — freshness 30,
confidence 20, completeness 20, summary 15, citations 15 (quality-score.ts:52-58). Live:
the cron passes only freshness, classification_confidence, summary, and hardcodes
citation_count: 0 (route.ts:243-251). It never passes brief/detail/reference —
those columns were dropped at M3 (BI-11), per the cron’s own header at :41-47.
Measured ceiling (executed probe against the real module, cron-identical input shape):
BEST POSSIBLE (fresh + conf 1.0 + summary) = 65 [fresh 30, conf 20, complete 0, summ 15, cite 0]same inputs with brief/detail/reference/citations = 100unreachable weight = completeness 20 + citations 15 = 35 of 100freshness share of the reachable range = 30/65 = 46.2%What it is compared against. governance_config.quality_score_threshold per domain,
falling back to DEFAULT_THRESHOLD = 40 (route.ts:36). Live config, measured:
select domain, quality_score_threshold, auto_flag_on_quality_drop from public.governance_config;-- → e2e-seeded-domain | 40 | false (one row)The code default is fail-open: domainConfig?.auto_flag_on_quality_drop ?? true
(route.ts:268) — any domain with no config row auto-flags on.
Whether it persists. It does not. Measured:
select table_schema, table_name, column_name from information_schema.columnswhere column_name in ('quality_score','previous_quality_score','quality_score_updated_at') and table_schema in ('public','api');-- → [] (zero rows)The cron agrees (route.ts:137-141, totalUpdated hardcoded 0), and the read surface
has already given up on it: /api/review/queue returns quality_score: null literally
(route.ts:832) and its quality_score_asc sort is “a documented no-op” (:348-349,
:530).
What it actually discriminates on. Executed enumeration of the full live input space (4 freshness states × 5 confidence values × summary present/absent) against threshold 40:
combinations below threshold: 28/40of the 'expired' combinations: 10/10 below threshold
max score attainable per freshness state (conf=1.0, summary present): fresh = 65 aging = 53 stale = 44 expired = 35 <-- BELOW THRESHOLD EVEN AT PERFECT QUALITYDirect answer. It scores freshness (46% of the reachable range), classification
confidence, and whether a summary string is non-empty — against a threshold of 40 that
exactly one synthetic domain row configures. The result is discarded; its only effects are
a notification and an auto-flag onto the governance column. An expired document is
flagged as a quality failure no matter how good it is. It is a freshness restatement
wearing a quality label, and it writes that restatement onto the other axis.
Is there a requirement behind it? There was, and it was a fix-or-remove ask that was never actioned. The client raised completeness scoring specifically:
“Completeness scoring sitting at 0/20 on most items suggests the metric is not populated — fix or replace” —
kb-hub-gap-analysis-liam.md:390“Fix completeness scoring or remove it” — same file,:405
Neither branch was taken. The metric has since degraded from unpopulated to structurally unreachable. The requirement behind the quality-score as built is not live.
The governing spec does not exist. docs/specs/p0-document-control-lifecycle-spec.md
is cited by 10 files — including the cadence penalty schedule (quality-score.ts:15), the
review-cadence cron (:31), lib/governance/review-input-statuses.ts:14 and
lib/governance/cadence-renewal.ts:9. Measured:
$ find <canonical> <docs-site> <client-archive> -name "p0-document-control*"(no output)$ git log --oneline --all -- "**/p0-document-control-lifecycle-spec.md"(no output)It is absent from every checkout and was never in git history.
3. The requirements, extracted
Section titled “3. The requirements, extracted”| Req | Source | Live? |
|---|---|---|
| R-DOC — document control: version, changelog, owner, next-review-date | kb-hub-gap-analysis-liam.md:216-221 §7.6 | LIVE, partly built |
| R-QA-REVIEW — human sign-off on promoted Q&A diffs | corpus-reframe-review.html:341 (R2) + DR-026 | LIVE, built in a separate place |
| R-GOV — governance review | DR-034 (explicit RETAIN) | LIVE |
| R-SCOPE — hard scope tags | kh-client-feedback.md:31-33 | LIVE, routed to id-71 (S528 E2) |
| R-QSCORE — quality score as built | kb-hub-gap-analysis-liam.md:390,405 | NOT LIVE (see §2) |
R-DOC — and a correction to the attribution
Section titled “R-DOC — and a correction to the attribution”The dispatch (and the S528 board) attribute the document-lifecycle framing to
kh-client-content-archive/docs/client-briefs/kh-client-feedback.md. I read all 38 lines
of that file. The framing is not in it. Its three items are: (1) list_user_workspaces
returns an invalid response shape; (2) MCP search underperforming — five answered
questions returned as gaps; (3) scope tags and enforced citation. No document-lifecycle
content appears.
The framing is real and the owner’s memory of it is sound — the source is a different
document. It is kb-hub-gap-analysis-liam.md §7.6:
“The refined mind map specified version, change history, ownership, review cadence, especially for Company/Corporate and Bid Detail content. The build has quality scoring and freshness tracking but document control (version number, changelog, owner, next-review-date) is not visible.” —
:216-221
and it carries an explicit recorded owner decision:
“Liam decision: Required. Cross-reference with Roadmap item ‘5. Document Control & Lifecycle’ — intention is to prioritise that whole section to be completed pre-launch too.” —
kb-hub-gap-analysis-action-tracker.md:186-190
This matters for the ruling: R-DOC is the only requirement in this report with a recorded pre-launch owner decision attached, and it is stronger evidence than the file the board cited.
Delivery status against the tracker’s own five-phase plan (action-tracker.md:210-226):
Phase 1 (columns) and Phase 2 (cron) shipped and now live on record_lifecycle; Phase 5
(cadence in the scorer) shipped as cadenceCompliancePenalty. Unbuilt: Phase 3’s
/review “overdue reviews” filter, and — critically — version and changelog, the two
fields §7.6 names first, have no column on record_lifecycle at all (migration
:25-49).
R-QA-REVIEW — already a review queue, just not called one
Section titled “R-QA-REVIEW — already a review queue, just not called one”R2, verbatim:
“The authoritative gate is knowledge admission: promotion + dedup review + confidence gate for records; ontology linter + publish gate for concepts. Authority is earned at sign-off, not inherited from a folder.” —
corpus-reframe-review.html:341
Built as /promotion-gate over the awaiting_review bucket. The accept route states its
own job:
“ACCEPT applies the extraction’s OWN carried fields … onto the published pair — the ‘apply the diff’ action DR-026 blocks from auto-firing; a human now confirms it per-item.” —
accept/route.ts:24-29
That is a review queue by every property except its name and its position in the IA — it
is a separate Governance-zone nav entry from Review (components/shell/nav-config.ts:133
vs :146).
4. Proposed shape
Section titled “4. Proposed shape”Binding constraint on all three options. id-71 {71.9} whats_in_my_queue is done and
shipped — a faceted queue (content_quality | governance | all) over lib/attention.ts,
whose test strategy explicitly asserts “/review + /api/governance/review routes
unchanged” (tasks/id-71.md:192, restated :103). Its TECH is equally explicit that the
cron writers and formatGovernanceQueue “are unchanged producers; only the MCP read
surface consolidates.” The facet enum is the composition seam. Any option that changes
those routes lands as an amendment to {71.9}‘s enum plus its fixture — never as a silent
break.
Option 1 — RECOMMENDED: one queue, faceted by the axes as they actually are
Section titled “Option 1 — RECOMMENDED: one queue, faceted by the axes as they actually are”/review becomes the single human queue, with facets matching the three measured axes:
publication(axis C — both kinds) — absorbs today’s drafts / pending / awaiting-publication tabs.governance(axis A — both kinds) — absorbs/api/governance/reviewand the promotion-candidateawaiting_reviewbucket, which is a governance sign-off on a proposed diff.freshness(axis B — source_document only) — absorbs cadence + assignments.
Each facet is owner-kind-polymorphic exactly where its axis is. /api/review/action
already implements the polymorphic axis-A write — this makes the read side match the
write side that already exists (§1.3(i)).
Composes with {71.9}: the MCP facet enum widens content_quality|governance|all →
publication|governance|freshness|all — one enum plus one fixture edit. The
lib/attention.ts producers stay the producers, precisely as {71.9}‘s TECH specifies.
Makes possible: reviewing promoted Q&As in the same place as everything else (the owner’s steer); the Q&A half of the governance axis becomes listable; freshness stops masquerading as quality.
Costs: three tab-groups collapse into one faceted surface — real front-end work in
review-content.tsx (733 lines) and review-tabs.tsx. /promotion-gate loses its own nav
entry, which is a change to the Governance zone’s entry set and therefore needs an
owner nav ruling under DR-041 — even though DR-041 makes no claim about concern
boundaries.
Option 2 — fix the axis violations only, keep the surfaces
Section titled “Option 2 — fix the axis violations only, keep the surfaces”Leave /review, /promotion-gate and /api/governance/review where they are. Retire the
quality-score cron; stop deriving axis A from axis B; add the q_a_pair read leg to
/api/governance/review.
Makes possible: the schema contradictions close with no UX work; ships in a fraction of the time; zero DR-041 exposure.
Costs: does not deliver the owner’s “review promoted Q&As in the review surface” steer.
Leaves three surfaces a reviewer must learn, and leaves /review serving an axis the facet
does not model — so the next task still opens with archaeology, which is the precise cost
id-417 exists to stop (“every task starts with archaeology”).
Option 3 — document-lifecycle-first
Section titled “Option 3 — document-lifecycle-first”Take R-DOC literally: /review becomes a document-control surface — version, changelog,
owner, next-review-date, review status — with Q&A review left as a separate promotion
surface.
Makes possible: the one requirement carrying an explicit pre-launch owner decision, delivered whole.
Costs: version and changelog have no schema home (migration :25-49), so this option
needs a migration before it needs a UI. It also hard-splits document review from Q&A
review — the opposite of the owner’s steer, and it re-cuts against axis A deliberately
spanning both kinds. Recommend only if version + changelog prove to be the binding client
commitment (see §6).
5. What must be retired, and what that unblocks
Section titled “5. What must be retired, and what that unblocks”The quality-score cron and lib/quality/quality-score.ts. No persistence home,
35/100 structurally unreachable, expired always flagged, and its governance bridge
duplicates freshness-transitions’ bridge onto the same column. Blast radius measured as
LOW (gitnexus, §2) — one call site.
Unblocks: {71.9}‘s content_quality facet stops pointing at a producer with no live
requirement; governance_config.quality_score_threshold / auto_flag_on_quality_drop /
the quality_flag notification type lose their only writer; id-71 {71.29}‘s “dead
‘quality’ ExposureLayerKey member” cleanup gains a coherent story; and
/api/review/queue’s hardcoded quality_score: null plus its no-op sort can be deleted
rather than perpetually explained.
Caveat, stated plainly: retiring it removes the only level-triggered governance
backstop. freshness-transitions flags only on a transition (newTransitions filter,
route.ts:494-496), so a document that went expired before the flag existed is never
re-flagged by it. If a level-triggered sweep is wanted it belongs on axis B as a freshness
sweep — not on a quality score.
lib/mcp/formatters/briefing.ts:23,35-36,145,165-167 — still formats quality_score
and previous_quality_score deltas for values that have no column anywhere. Retires with
the cron.
The 10 p0-document-control-lifecycle-spec.md citations. Either the spec is restored
from wherever it actually lives, or the citations are repointed at the real authority.
Until then cadenceCompliancePenalty’s schedule and ALLOWED_REVIEW_INPUT_STATUSES are
unfalsifiable — no reviewer can check them against anything.
The /api/review/ vs /api/governance/review path split — under Option 1 only.
6. UNDECIDABLE
Section titled “6. UNDECIDABLE”Questions carried verbatim.
- “Is the cadence-compliance penalty schedule (0 / −10 / −15 / −25 / −40,
quality-score.ts:101-110) the ratified one?” — Its only cited source,p0-document-control-lifecycle-spec.md§9.3, exists in no checkout and has no git history. No named requirement plus no current source. - “Should
record_lifecyclegain a publication axis, or should publication stay on the typed records?” — The migration models two axes and publication is neither; nothing I read rules on whether that was deliberate. It decides whether Option 1’spublicationfacet reads one table or two. - “Does
governance_config’s per-domain policy survive if the quality-score cron retires?” — Three of its columns exist only for that cron. Whether per-domain governance policy is a live requirement independent of it is settled nowhere I read. - “Are version + changelog (§7.6’s first two named fields) still a client
commitment?” — The recorded decision is “Required … prioritise pre-launch”
(
action-tracker.md:186-190), dated to the S188 era. No later source I read either reaffirms or retires it, and it has no schema home. This one gates Option 3.
7. Coverage gaps
Section titled “7. Coverage gaps”- Not read in full:
app/review/review-content.tsx(733 lines) andapp/api/review/queue/route.ts(837). I measured their query shapes, filters and tab definitions by targeted grep, not their rendering logic — current-queue UX detail is under-specified here, which matters for costing Option 1’s front-end work. - MCP side not measured. I did not read
lib/mcp/tools/review.tsFACET_BY_TYPE,whats_in_my_queue’s live shape, orformatGovernanceQueue. Option 1’s claim that the facet enum is a one-edit seam derives from {71.9}‘s task record and TECH text, not from reading the tool file. Verify before committing to that estimate. - Not read:
app/api/review/history,publication-bulk-action,stats, and theclassification-qualitycron. - No test run. No behavioural verification that retiring
quality-scoreleaves the suite green;__tests__/api/cron/quality-score.test.tsexists and I did not read it. - Evidence hygiene. No consumer-counting, population, or emptiness claim is used as
evidence anywhere above. The single population figure quoted (
governance_confighas one synthetic row) establishes only which threshold value the code path takes — it is not an argument that the surface is unused.