From 5012ff721264e17f8d2a03e725e687cc2dbbb880 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 15 Aug 2026 05:10:37 +0800 Subject: [PATCH] feat: add persistent personal report document v2 --- .../report-document.v2.schema.json | 827 ++++++++++++ docs/BUG_HISTORY.md | 6 +- ...0814025000_personal_report_document_v2.sql | 1 + ...20260814040000_personal_report_jobs_v2.sql | 1 + frontend/src/app/api/reports/route.ts | 27 +- .../generate-personal-report-button.tsx | 2 + .../personal-report-center.tsx | 6 +- .../personal-report-document-view.tsx | 308 ++++- .../personal-report/personal-report-page.tsx | 4 +- frontend/src/instrumentation.ts | 3 + .../personal-report-contract.server-core.ts | 27 +- frontend/src/lib/personal-report-contract.ts | 456 +++++-- .../src/lib/personal-report-generation.ts | 400 +++++- .../lib/personal-report-job-service-core.ts | 1007 ++++++++++++++ .../src/lib/personal-report-job-service.ts | 3 + frontend/src/lib/personal-report-job-state.ts | 203 +++ frontend/src/lib/personal-report-plan.ts | 188 +++ .../src/lib/personal-report-route-core.ts | 64 +- .../src/lib/personal-report-service-core.ts | 18 +- .../src/lib/personal-report-worker-core.ts | 384 ++++++ frontend/src/lib/personal-report-worker.ts | 240 ++++ frontend/src/mastra/personal-report.ts | 27 +- ...0814025000_personal_report_document_v2.sql | 42 + ...20260814040000_personal_report_jobs_v2.sql | 770 +++++++++++ .../tests/database-local-business.test.ts | 5 + .../database-personal-report-jobs.test.ts | 402 ++++++ frontend/tests/personal-report-api.test.ts | 158 ++- .../tests/personal-report-contract.test.ts | 130 ++ frontend/tests/personal-report-entry.test.ts | 3 +- frontend/tests/personal-report-export.test.ts | 20 + .../personal-report-generation-v2.test.ts | 531 ++++++++ .../tests/personal-report-generation.test.ts | 34 +- .../tests/personal-report-job-service.test.ts | 728 +++++++++++ .../tests/personal-report-job-state.test.ts | 180 +++ .../tests/personal-report-migration.test.ts | 109 +- frontend/tests/personal-report-plan.test.ts | 100 ++ .../tests/personal-report-service.test.ts | 50 +- frontend/tests/personal-report-view.test.ts | 113 +- frontend/tests/personal-report-worker.test.ts | 545 ++++++++ frontend/tests/skill-registry.test.ts | 8 +- scripts/personal_report_contract.py | 1163 +++++++++-------- skills/jyotish-personal-report/SKILL.md | 87 ++ .../references/report-contract.md | 92 ++ .../versions/1.0.0/SKILL.md | 87 ++ .../1.0.0/references/report-contract.md | 92 ++ skills/skill-package-registry.json | 8 + .../fixtures/personal_report_document.v2.json | 485 +++++++ ...t_jyotish_personal_report_skill_package.py | 89 ++ 48 files changed, 9301 insertions(+), 932 deletions(-) create mode 100644 contracts/personal-report/report-document.v2.schema.json create mode 120000 frontend/db/migrations/20260814025000_personal_report_document_v2.sql create mode 120000 frontend/db/migrations/20260814040000_personal_report_jobs_v2.sql create mode 100644 frontend/src/lib/personal-report-job-service-core.ts create mode 100644 frontend/src/lib/personal-report-job-service.ts create mode 100644 frontend/src/lib/personal-report-job-state.ts create mode 100644 frontend/src/lib/personal-report-plan.ts create mode 100644 frontend/src/lib/personal-report-worker-core.ts create mode 100644 frontend/src/lib/personal-report-worker.ts create mode 100644 frontend/supabase/migrations/20260814025000_personal_report_document_v2.sql create mode 100644 frontend/supabase/migrations/20260814040000_personal_report_jobs_v2.sql create mode 100644 frontend/tests/database-personal-report-jobs.test.ts create mode 100644 frontend/tests/personal-report-generation-v2.test.ts create mode 100644 frontend/tests/personal-report-job-service.test.ts create mode 100644 frontend/tests/personal-report-job-state.test.ts create mode 100644 frontend/tests/personal-report-plan.test.ts create mode 100644 frontend/tests/personal-report-worker.test.ts create mode 100644 skills/jyotish-personal-report/SKILL.md create mode 100644 skills/jyotish-personal-report/references/report-contract.md create mode 100644 skills/jyotish-personal-report/versions/1.0.0/SKILL.md create mode 100644 skills/jyotish-personal-report/versions/1.0.0/references/report-contract.md create mode 100644 tests/fixtures/personal_report_document.v2.json create mode 100644 tests/test_jyotish_personal_report_skill_package.py diff --git a/contracts/personal-report/report-document.v2.schema.json b/contracts/personal-report/report-document.v2.schema.json new file mode 100644 index 00000000..e4c5cdb4 --- /dev/null +++ b/contracts/personal-report/report-document.v2.schema.json @@ -0,0 +1,827 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://jyotisha.chat/contracts/personal-report/report-document.v2.schema.json", + "title": "ReportDocument v2", + "description": "Canonical server-issued personal astrology report document v2. The JSON Schema, frontend Zod contract, and stdlib Python validator align on all structural rules. Runtime validators additionally enforce: exactly one complete D1; unique chart, section, theme, and evidence ids; every requested theme is covered exactly once by either a thematic section or blockedConflictDisclosure; written career/wealth/marriage/education themes require D10/D2+D11/D9/D24 chart data respectively; all evidenceRefs close over evidenceAppendix; blocked content is non-deterministic; date claims require evidenceRefs; medical diagnoses, deterministic financial promises, HTML/CSS/executable content, internal paths, secrets, and tool traces are rejected; UTF-8 serialization is capped at 1572864 bytes. The server and Python validator recompute provenance.evidenceHash over canonical techniqueAudit, conflicts, and calculationEvidence. JSON object key order is not validated.", + "type": "object", + "definitions": { + "claimStatus": { + "type": "string", + "enum": [ + "multi_system_consensus", + "single_system_inference", + "parameter_sensitive", + "unclosed_divisional_chart", + "user_history_verification_required", + "blocked" + ] + }, + "evidenceId": { + "type": "string", + "pattern": "^ev-[a-z0-9_-]{1,63}$", + "minLength": 4, + "maxLength": 67 + }, + "sha256Hex": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "minLength": 64, + "maxLength": 64 + }, + "iso8601": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$", + "minLength": 20, + "maxLength": 40 + }, + "house": { + "type": "object", + "additionalProperties": false, + "properties": { + "houseNumber": { + "type": "integer", + "minimum": 1, + "maximum": 12 + }, + "sign": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "occupants": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 12 + } + }, + "required": [ + "houseNumber", + "sign", + "occupants" + ] + }, + "planet": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "sign": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "longitudeDegrees": { + "type": "number", + "minimum": 0, + "exclusiveMaximum": 360 + }, + "houseNumber": { + "type": "integer", + "minimum": 1, + "maximum": 12 + }, + "retrograde": { + "type": "boolean" + } + }, + "required": [ + "name", + "sign", + "longitudeDegrees", + "houseNumber", + "retrograde" + ] + }, + "chart": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "enum": [ + "D1", + "D2", + "D9", + "D10", + "D11", + "D24" + ] + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "houses": { + "type": "array", + "items": { + "$ref": "#/definitions/house" + }, + "maxItems": 12 + }, + "planets": { + "type": "array", + "items": { + "$ref": "#/definitions/planet" + }, + "maxItems": 12 + }, + "claimStatus": { + "$ref": "#/definitions/claimStatus" + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24, + "minItems": 1 + } + }, + "required": [ + "id", + "title", + "houses", + "claimStatus", + "evidenceRefs" + ] + }, + "thematicSection": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_-]{0,95}$", + "minLength": 1, + "maxLength": 96 + }, + "theme": { + "type": "string", + "pattern": "^[a-z][a-z0-9_.-]{0,95}$", + "minLength": 1, + "maxLength": 96 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "narrative": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "actions": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 12 + }, + "caveats": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 12 + }, + "claimStatus": { + "$ref": "#/definitions/claimStatus" + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24, + "minItems": 1 + } + }, + "required": [ + "id", + "theme", + "title", + "narrative", + "actions", + "caveats", + "claimStatus", + "evidenceRefs" + ] + }, + "techniqueAuditRow": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/evidenceId" + }, + "techniqueId": { + "type": "string", + "pattern": "^[a-z0-9_.-]{1,80}$", + "minLength": 1, + "maxLength": 80 + }, + "techniqueName": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "status": { + "type": "string", + "enum": [ + "verified", + "partial", + "blocked" + ] + }, + "used": { + "type": "boolean" + }, + "notes": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "id", + "techniqueId", + "techniqueName", + "status", + "used" + ] + }, + "conflictRow": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/evidenceId" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "impact": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "status": { + "type": "string", + "enum": [ + "unresolved", + "partial", + "resolved" + ] + } + }, + "required": [ + "id", + "description", + "impact", + "status" + ] + }, + "calculationEvidenceRow": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/definitions/evidenceId" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "id", + "label", + "value", + "source" + ] + }, + "actionNote": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_-]{0,95}$", + "minLength": 1, + "maxLength": 96 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "note": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "priority": { + "type": "string", + "enum": [ + "now", + "next", + "watch" + ] + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24, + "minItems": 1 + } + }, + "required": [ + "id", + "title", + "note", + "priority", + "evidenceRefs" + ] + }, + "blockedConflictDisclosure": { + "type": "object", + "additionalProperties": false, + "properties": { + "theme": { + "type": "string", + "pattern": "^[a-z][a-z0-9_.-]{0,95}$", + "minLength": 1, + "maxLength": 96 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "reason": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "missingEvidence": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 24, + "minItems": 1 + }, + "conflictNotes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "maxItems": 24 + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24 + }, + "claimStatus": { + "type": "string", + "const": "blocked" + } + }, + "required": [ + "theme", + "title", + "reason", + "missingEvidence", + "conflictNotes", + "evidenceRefs", + "claimStatus" + ] + } + }, + "additionalProperties": false, + "properties": { + "schemaVersion": { + "type": "string", + "const": "report_document.v2" + }, + "reportId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "reportType": { + "type": "string", + "enum": [ + "personal_full", + "personal_thematic" + ] + }, + "presentationMode": { + "type": "string", + "enum": [ + "default", + "research" + ] + }, + "depth": { + "type": "string", + "enum": [ + "concise", + "standard", + "deep", + "research" + ] + }, + "requestedThemes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_.-]{0,95}$", + "minLength": 1, + "maxLength": 96 + }, + "minItems": 1, + "maxItems": 12 + }, + "generatedAt": { + "$ref": "#/definitions/iso8601" + }, + "subject": { + "type": "object", + "additionalProperties": false, + "properties": { + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "birthTimeStatus": { + "type": "string", + "enum": [ + "reported", + "candidate", + "accepted", + "confirmed" + ] + }, + "birthPlaceLabel": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "displayName", + "birthTimeStatus", + "birthPlaceLabel" + ] + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "skillName": { + "type": "string", + "pattern": "^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$", + "minLength": 1, + "maxLength": 120 + }, + "skillVersion": { + "type": "string", + "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "minLength": 5, + "maxLength": 80 + }, + "skillSourceCommit": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-f]{40}$", + "minLength": 40, + "maxLength": 40 + }, + "skillSnapshotSha256": { + "$ref": "#/definitions/sha256Hex" + }, + "calculationHash": { + "$ref": "#/definitions/sha256Hex" + }, + "evidenceHash": { + "$ref": "#/definitions/sha256Hex" + }, + "reportContractVersion": { + "type": "string", + "const": "2" + } + }, + "required": [ + "skillName", + "skillVersion", + "skillSourceCommit", + "skillSnapshotSha256", + "calculationHash", + "evidenceHash", + "reportContractVersion" + ] + }, + "executiveSummary": { + "type": "object", + "additionalProperties": false, + "properties": { + "headline": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "priorities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "maxItems": 8 + }, + "overallClaimStatus": { + "$ref": "#/definitions/claimStatus" + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24, + "minItems": 1 + } + }, + "required": [ + "headline", + "summary", + "priorities", + "overallClaimStatus", + "evidenceRefs" + ] + }, + "natalFoundation": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "narrative": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "keyFactors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 12 + }, + "caveats": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 12 + }, + "claimStatus": { + "$ref": "#/definitions/claimStatus" + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24, + "minItems": 1 + } + }, + "required": [ + "title", + "narrative", + "keyFactors", + "caveats", + "claimStatus", + "evidenceRefs" + ] + }, + "currentPhase": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "phaseLabel": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "narrative": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "timingNotes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 12 + }, + "caveats": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxItems": 12 + }, + "claimStatus": { + "$ref": "#/definitions/claimStatus" + }, + "evidenceRefs": { + "type": "array", + "items": { + "$ref": "#/definitions/evidenceId" + }, + "maxItems": 24, + "minItems": 1 + } + }, + "required": [ + "title", + "phaseLabel", + "narrative", + "timingNotes", + "caveats", + "claimStatus", + "evidenceRefs" + ] + }, + { + "type": "null" + } + ] + }, + "actionNotes": { + "type": "array", + "minItems": 1, + "maxItems": 24, + "items": { + "$ref": "#/definitions/actionNote" + } + }, + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/chart" + }, + "minItems": 1, + "maxItems": 6 + }, + "thematicNarrative": { + "type": "array", + "items": { + "$ref": "#/definitions/thematicSection" + }, + "maxItems": 12 + }, + "blockedConflictDisclosure": { + "type": "array", + "items": { + "$ref": "#/definitions/blockedConflictDisclosure" + }, + "maxItems": 12 + }, + "evidenceAppendix": { + "type": "object", + "additionalProperties": false, + "properties": { + "expandedByDefault": { + "type": "boolean" + }, + "techniqueAudit": { + "type": "array", + "items": { + "$ref": "#/definitions/techniqueAuditRow" + }, + "maxItems": 100 + }, + "conflicts": { + "type": "array", + "items": { + "$ref": "#/definitions/conflictRow" + }, + "maxItems": 50 + }, + "calculationEvidence": { + "type": "array", + "items": { + "$ref": "#/definitions/calculationEvidenceRow" + }, + "maxItems": 100 + }, + "blockedTechniques": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "maxItems": 100 + } + }, + "required": [ + "expandedByDefault", + "techniqueAudit", + "conflicts", + "calculationEvidence", + "blockedTechniques" + ] + }, + "disclaimer": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": [ + "schemaVersion", + "reportId", + "reportType", + "presentationMode", + "depth", + "requestedThemes", + "generatedAt", + "subject", + "provenance", + "executiveSummary", + "natalFoundation", + "currentPhase", + "actionNotes", + "charts", + "thematicNarrative", + "blockedConflictDisclosure", + "evidenceAppendix", + "disclaimer" + ] +} diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 205accc8..090cde1a 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3192,8 +3192,8 @@ - 用户现象:完整报告即使请求多个主题也只执行一次 `general`,专题报告只使用首个主题;写作 Agent 只能获得缺少 Claim Card、Blocked Section 和执行账本的 `ReportEvidencePacket v1`,因此内容短、主题证据不足,且 accepted 时间曾通过零宽候选区间表达。 - 触发条件:创建包含多个 requested themes 的 `personal_full` 报告;请求 wealth 等需要专题分盘/技法但证据未闭合的报告;或使用 accepted 出生时间生成报告。 - 根因:报告路由把所有完整报告折叠到单次 `general` workflow,旧 packet 仅提供窄化盘面与技法状态,没有服务器持有的主题 Claim Graph、最低证据计划、blocked coverage、execution receipt 与规范化 hash;Agent 又被禁止自行推算未提供事实。 -- 修复:新增 `ReportEvidenceBundle v2`、主题最低证据计划、规范化 hash 与强引用校验;路由按 requested themes 分别运行 workflow,并把缺失专题证据表示为 Blocked Section,而不是伪造已执行技法或让部分 blocked 主题拖垮整份报告。Agent 仅接收安全投影后的 Bundle;服务器继续通过兼容层组装现有 ReportDocument v1。accepted 状态改为 `accepted_directional_only`,Bundle 不生成 `candidateRange`;缺失 D2/D11 等证据只能生成 `blocked + executed=false` receipt。 -- 验证:个人报告 API/生成聚焦回归 77 passed、0 failed,覆盖多主题运行、每主题 Claim Card/Blocked Section、部分 blocked 仍 ready、引用闭合、consensus 降级、accepted 无假 candidate range、wealth 缺 D2/D11 不得升级、Bundle hash 稳定、直接恶意 Bundle 自由文本绕过被拒绝与敏感信息不泄露;目标 ESLint 0 error/0 warning,`git diff --check` 通过。全库 `tsc --noEmit` 仅剩 5 个既有无关测试错误:`production-data-migration.test.ts` 两处 fixture 字段缺失、`staging-backend-workflows.test.ts` 三处低 target 正则 flag。 -- 防复发:每个 requested theme 必须且只能由 Claim Card 或 Blocked Section 覆盖;未执行或 blocked/partial 技法不得升级为 verified/consensus;Agent 不得接收 raw workflow、坐标、内部路径、secret、聊天历史或工具轨迹;Bundle hash 必须由服务器对规范化且排除自身 hash 的内容计算。 +- 修复:新增 `ReportEvidenceBundle v2`、主题最低证据计划、规范化 hash 与强引用校验;路由按 requested themes 分别运行 workflow,并把缺失专题证据表示为 Blocked Section,而不是伪造已执行技法或让部分 blocked 主题拖垮整份报告。服务器从 Bundle 生成确定性的 section plan,writer 只能填写计划允许且引用闭合的章节,再由服务器 guard 规范化为 `ReportDocument v2`;新建报告固定写入 v2,读取端保持 v1/v2 双读。生产创建路径不再依赖 Next.js `after()`,改为由数据库原子创建的持久化 job 与 lease worker 执行,覆盖 heartbeat、bounded retry、historical generating backfill、expired-lease recovery、幂等 request identity 和进程重启续跑;job migration 使用唯一版本 `20260814040000`。ready 与 failed 两种终态都通过 exact-live-lease RPC 在同一事务更新 report/job,且 complete/fail 在取得 job 行锁后才读取 lease 校验时间;final-attempt lease 过期会原子收敛为 `report=failed/job=failed` 并释放用户生成槽位。`scheduleRetry()` 耗尽预算时保留 live lease 交给失败 RPC 收敛;若 report 已 ready 而 job ready 对账遇到临时存储错误,则返回 `reconcile_deferred` 并保持 report ready、job running,绝不把 ready 报告降级为 failed。authenticated 对 job 保持 select-only,完成 RPC 仅授予 `service_role`;writer 与单次 repair retry 均接收 worker AbortSignal。accepted 状态改为 `accepted_directional_only`,Bundle 不生成 `candidateRange`;缺失 D2/D11 等证据只能生成 `blocked + executed=false` receipt。Web renderer 继续只消费结构化文档和真实 chart data,并使用浏览器原生打印,不接受模型生成 HTML/CSS/SVG。 +- 验证:个人报告合同、planner/writer、API、entry、job state/service/worker、renderer/export、migration 与 Skill registry 完整聚焦回归 247 passed、0 failed;Python 报告合同与 Skill package 41 passed、0 failed;目标 ESLint 0 error/0 warning,`git diff --check` 通过。真实本地 PostgreSQL 已成功应用两个 PR-6 migration,聚焦 smoke 1 passed、0 failed,覆盖 historical generating backfill、authenticated owner 删除拒绝、错误 lease 对 ready/failed 均不写入、正确 lease 原子完成 ready/failed、final-attempt 历史 ready/job-running split recovery、final-attempt generating/running 原子 failed/failed,以及失败后可创建新的 generating/queued 报告。migration 唯一性检查只剩基础分支既有的 `20260806010000_admin_rbac.sql` / `20260806010000_personal_reports.sql` 冲突;全库数据库业务测试仍在既有 Rectification V10 场景因 `service_role` 无权执行 `accept_agentic_rectification_candidate` 失败,该权限问题不属于本条报告修复。全库 `tsc --noEmit` 仅剩 5 个既有无关测试错误:`production-data-migration.test.ts` 两处 fixture 字段缺失、`staging-backend-workflows.test.ts` 三处低 target 正则 flag。pre-work 仅剩既有 fragment candidate 计数断言 `4 >= 0 + 23` 失败。 +- 防复发:每个 requested theme 必须且只能由 Claim Card 或 Blocked Section 覆盖;未执行或 blocked/partial 技法不得升级为 verified/consensus;Agent 不得接收 raw workflow、坐标、内部路径、secret、聊天历史或工具轨迹;Bundle hash 必须由服务器对规范化且排除自身 hash 的内容计算。报告文档、job 状态和日志不得写入用户资料、密钥、内部 URL 或模型自由生成的 HTML/CSS/SVG;不得把本地测试结果虚构为 staging 或生产验收。 - 相关记录:BUG-152、BUG-159 - 修复版本:本次功能分支提交(精确 SHA 以提交与远程分支核对结果为准;未合并 staging,未部署) diff --git a/frontend/db/migrations/20260814025000_personal_report_document_v2.sql b/frontend/db/migrations/20260814025000_personal_report_document_v2.sql new file mode 120000 index 00000000..60585c09 --- /dev/null +++ b/frontend/db/migrations/20260814025000_personal_report_document_v2.sql @@ -0,0 +1 @@ +../../supabase/migrations/20260814025000_personal_report_document_v2.sql \ No newline at end of file diff --git a/frontend/db/migrations/20260814040000_personal_report_jobs_v2.sql b/frontend/db/migrations/20260814040000_personal_report_jobs_v2.sql new file mode 120000 index 00000000..fb4884a8 --- /dev/null +++ b/frontend/db/migrations/20260814040000_personal_report_jobs_v2.sql @@ -0,0 +1 @@ +../../supabase/migrations/20260814040000_personal_report_jobs_v2.sql \ No newline at end of file diff --git a/frontend/src/app/api/reports/route.ts b/frontend/src/app/api/reports/route.ts index 98439cb2..a46f0650 100644 --- a/frontend/src/app/api/reports/route.ts +++ b/frontend/src/app/api/reports/route.ts @@ -1,4 +1,4 @@ -import { after, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { runConsultationWorkflow } from "@/mastra"; import { createPersonalReportAgent } from "@/mastra/personal-report"; import { loadLanguageModelCatalog } from "@/lib/model-catalog"; @@ -20,19 +20,20 @@ import { createSupabasePersonalReportService, type PersonalReportService, } from "@/lib/personal-report-service"; +import { createSupabasePersonalReportJobService } from "@/lib/personal-report-job-service"; import { createAdminSupabaseClient } from "@/lib/supabase/admin"; import { isSupabaseConfigurationError } from "@/lib/supabase/config"; import { createServerSupabaseClient } from "@/lib/supabase/server"; export const runtime = "nodejs"; export const maxDuration = 120; -const STALE_GENERATION_MS = 15 * 60 * 1000; const REPORT_LIST_COLUMNS = [ "id", "request_id", "report_type", "presentation_mode", + "depth", "requested_themes", "status", "failure_code", @@ -56,6 +57,7 @@ function listReportView(value: unknown) { requestId: typeof row.request_id === "string" ? row.request_id : "", reportType: typeof row.report_type === "string" ? row.report_type : "personal_full", presentationMode: typeof row.presentation_mode === "string" ? row.presentation_mode : "default", + depth: typeof row.depth === "string" ? row.depth : "standard", themes: Array.isArray(row.requested_themes) ? row.requested_themes.filter((theme): theme is string => typeof theme === "string") : [], @@ -113,25 +115,6 @@ export async function POST(request: Request) { const persistence: PersonalReportService = createSupabasePersonalReportService(admin); const adminDataClient = createPersonalReportDataClient(admin); - // `after()` is bounded by the route duration and can be interrupted by a - // process restart. Reclaim only rows far beyond that bound so a killed - // task never blocks the user's next explicit generation forever. - if (userId) { - const now = new Date(); - const staleBefore = new Date(now.getTime() - STALE_GENERATION_MS).toISOString(); - const { error } = await admin - .from("personal_reports") - .update({ - status: "failed", - failure_code: REPORT_STABLE_CODES.calculationUnavailable, - updated_at: now.toISOString(), - }) - .eq("user_id", userId) - .eq("status", "generating") - .lte("updated_at", staleBefore); - if (error) throw error; - } - let profile: unknown = null; let profileError: unknown = null; if (userId) { @@ -208,7 +191,7 @@ export async function POST(request: Request) { runWorkflow: (input) => runConsultationWorkflow(input), createAgent: (model) => createPersonalReportAgent(model as Parameters[0]), skillSnapshot: resolveSkillSnapshot(), - deferGeneration: (task) => after(task), + jobs: createSupabasePersonalReportJobService(admin), }; const response = await resolveReportCreate(deps); diff --git a/frontend/src/components/personal-report/generate-personal-report-button.tsx b/frontend/src/components/personal-report/generate-personal-report-button.tsx index 66c329de..7fc8ce42 100644 --- a/frontend/src/components/personal-report/generate-personal-report-button.tsx +++ b/frontend/src/components/personal-report/generate-personal-report-button.tsx @@ -26,6 +26,7 @@ export interface PersonalReportCreateRequest { requestId: string; reportType: "personal_full"; presentationMode: "default"; + depth: "standard"; themes: readonly string[]; } @@ -51,6 +52,7 @@ export function buildPersonalReportCreateRequest( requestId, reportType: "personal_full", presentationMode: "default", + depth: "standard", themes: [...DEFAULT_REPORT_THEMES], }; } diff --git a/frontend/src/components/personal-report/personal-report-center.tsx b/frontend/src/components/personal-report/personal-report-center.tsx index 1a2f0484..379bde07 100644 --- a/frontend/src/components/personal-report/personal-report-center.tsx +++ b/frontend/src/components/personal-report/personal-report-center.tsx @@ -11,6 +11,7 @@ type ReportListItem = Readonly<{ id: string; reportType: string; presentationMode: string; + depth: "concise" | "standard" | "deep" | "research"; themes: readonly string[]; status: "generating" | "ready" | "failed"; failureCode: string | null; @@ -44,6 +45,9 @@ function readReports(value: unknown): ReportListItem[] { id: row.id, reportType: typeof row.reportType === "string" ? row.reportType : "personal_full", presentationMode: typeof row.presentationMode === "string" ? row.presentationMode : "default", + depth: ["concise", "standard", "deep", "research"].includes(String(row.depth)) + ? row.depth as ReportListItem["depth"] + : "standard", themes: Array.isArray(row.themes) ? row.themes.filter((theme): theme is string => typeof theme === "string") : [], @@ -187,7 +191,7 @@ export function PersonalReportCenter() {

{report.reportType === "personal_thematic" ? "个人主题报告" : "个人完整报告"}

{copy.description}

- {formatDate(report.createdAt)} · {report.themes.join(" / ") || "综合主题"} + {formatDate(report.createdAt)} · {report.depth} · {report.themes.join(" / ") || "综合主题"} {report.status === "ready" ? (