diff --git a/Documentation/Validation.md b/Documentation/Validation.md index 7d191dc..a0d426d 100644 --- a/Documentation/Validation.md +++ b/Documentation/Validation.md @@ -1,3 +1,9 @@ +# 1.6.0 validation · 2026-10-02 + +- 63 Swift tests pass on Xcode 27.0. New coverage: English II small/large tasks (scales 10/15, 100/150 words, own rubrics loaded from the bundle), kaoyan task order, and the grader prompt carrying the 【配图说明】 transcription rule with the 0–15 scale. +- An isolated QA copy restored a 68-question local package (2010–2026 English I/II small and large essays, 34 figures) through the existing backup restore: 17 per task, figures stored and shown in the library badge, preparation page and immersive view; English II large essay countdown is 30 minutes. The six kaoyan task tabs fit the preparation column. No user database was used. +- Past-paper text and figures are not bundled in the app or repository; they were imported only into the user's local question bank. + # 1.5.0 validation · 2026-10-02 - 62 Swift tests pass on Xcode 27.0 / macOS 27.0.1. New coverage: correction anchoring across curly quotes, full-width punctuation, whitespace and case; invented spans dropped without failing the review; DeepSeek streamed usage (including cache hits and reasoning tokens) and Codex turn-completion usage; required `expressions` in the Codex schema; single-judge quick review routing, aggregation and persistence; legacy reports decoding as full reviews; one exam time-up signal per crossing; clearing an untouched submitted draft; word/character revision diffs; parent-then-earlier revision baselines; built-in bank uniqueness and practice index; review-card sync idempotence and the 1/2/4/8/16-day schedule; backup export/restore round trip with images, cards and drafts, and a second restore that adds nothing. diff --git a/README.md b/README.md index 2c4ea83..b00a879 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ WriteBench 把练习收敛为一条清晰的路径:选题、作答、评阅、 | 考试 | 题型 | 评分尺度 | | :--- | :--- | :--- | -| **考研英语一** | 小作文 · 大作文 · 英译汉 | 10 分 · 20 分 · 10 分 | -| **考研英语二** | 英译汉段落翻译 | 15 分 | +| **考研英语一** | 小作文 · 大作文(图画 / 图表)· 英译汉 | 10 分 · 20 分 · 10 分 | +| **考研英语二** | 小作文 · 大作文(图表)· 英译汉段落翻译 | 10 分 · 15 分 · 15 分 | | **CET-6 六级** | 写作 · 汉译英 | 各 15 分练习尺度 | | **IELTS 雅思 Academic** | Task 1 · Task 2 | 单项任务 Band 9 | @@ -137,7 +137,7 @@ platforms/android/ Android Studio 项目与手机界面 scripts/ 构建、图标生成与显式联调脚本 ``` -macOS 自动化测试覆盖 62 个案例,Android 有 5 个领域测试与 2 个实际设备服务测试;Windows 通过评分、持久化、字段校验及实际 OCR 自检。GPT-6 Astra/MAX 已通过实际 Swift 子进程完成样例评卷;DeepSeek 已验证官方模型接口连接。后台、流式输出及版本路径已用隔离测试验证;本次未执行完整付费三评,需用户填入有效 Key 后使用。 +macOS 自动化测试覆盖 63 个案例,Android 有 5 个领域测试与 2 个实际设备服务测试;Windows 通过评分、持久化、字段校验及实际 OCR 自检。GPT-6 Astra/MAX 已通过实际 Swift 子进程完成样例评卷;DeepSeek 已验证官方模型接口连接。后台、流式输出及版本路径已用隔离测试验证;本次未执行完整付费三评,需用户填入有效 Key 后使用。
更多文档 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a1094ee..9f75408 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,10 @@ +# 1.6.0 · 考研英语二写作与图表题 + +- 新增 **英二小作文**(10 分,约 100 词)和 **英二大作文**(15 分,约 150 词,以图表为主),各有独立评分标准、考试限时(15 / 30 分钟)和原创练习题。考研题型标签改为“英一小作文 / 英一大作文 / 英二小作文 / 英二大作文 / 英一翻译 / 英二翻译”。 +- 图画和图表题:题目可附带“【配图说明】”,即配图的文字版(漫画内容、图表标题与全部数值)。三位评审以它为准核对作文中的描述与数据,误读或编造数据按任务完成问题处理;评审不会臆测图中没有的信息。评审提示词升级到 1.3.1。 +- 准备页的题目配图默认展开;题库列表标出“配图”。 +- 已有记录、草稿和题库不受影响;原“小作文 / 大作文”即英一小作文 / 英一大作文。 + # 1.5.0 · 练习闭环:限时、对比、复习 - **评分更稳**:评审引用原文时出现弯引号、全角标点、多余空格或大小写差异,会自动对齐到原文;个别找不到的修改建议只丢弃该条,不再让整份评阅失败。 diff --git a/WriteBench.xcodeproj/project.pbxproj b/WriteBench.xcodeproj/project.pbxproj index cf43afa..64fdd55 100644 --- a/WriteBench.xcodeproj/project.pbxproj +++ b/WriteBench.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ CB760C24CAE63253B9400E19 /* WriteBenchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 679F1D25813DBB4821B089EF /* WriteBenchTests.swift */; }; CCB636BAB11DE1B7DE6C1CA2 /* ImmersiveWritingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B600C2CBBB1B477ABB65C42 /* ImmersiveWritingView.swift */; }; CF9CA154E89BD40306ABC7D2 /* EssayFolderMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9C06884EE52989CB4491F /* EssayFolderMetadata.swift */; }; + D03424B806D55BF2BB9F11DC /* kaoyan_english2_large.md in Resources */ = {isa = PBXBuildFile; fileRef = 0506352B36FD805B5E9053D8 /* kaoyan_english2_large.md */; }; D3F29CFA674B891722D5B739 /* cet6_writing.md in Resources */ = {isa = PBXBuildFile; fileRef = 650B4D62F506EDDC71D6396A /* cet6_writing.md */; }; D4A1396AF9FFF2FF6C40C24D /* kaoyan_english2_translation.md in Resources */ = {isa = PBXBuildFile; fileRef = 56792E2C1501DBCE7B80739B /* kaoyan_english2_translation.md */; }; DB3DEEA68D82D9F8BECF108E /* ReviewOutline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B9132C06548AC9B8B44070 /* ReviewOutline.swift */; }; @@ -63,6 +64,7 @@ DCC333A3FAF19BC2ECC79BA0 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17802ADDF6589CB80F4B4F01 /* KeychainService.swift */; }; DEBA962949C4AADDFCB2C5D0 /* ielts_task1.md in Resources */ = {isa = PBXBuildFile; fileRef = 405A85F2A07E72B5AD277D96 /* ielts_task1.md */; }; DEF094BE47996A62C68AB985 /* CodexJudgeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27300883AE5AE3DD46C36924 /* CodexJudgeService.swift */; }; + E041B0138CF95145600B02C6 /* kaoyan_english2_small.md in Resources */ = {isa = PBXBuildFile; fileRef = 015B10F8076FB519F8FDAACD /* kaoyan_english2_small.md */; }; E0495C7F8744F99633CD2EF5 /* UsageCost.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B358A2E1043646F5A65FA4 /* UsageCost.swift */; }; E32387900468FFCA09760D27 /* kaoyan_english1_large.md in Resources */ = {isa = PBXBuildFile; fileRef = ACA34C8DE60D85ABFA45116F /* kaoyan_english1_large.md */; }; E7928726F27B1B2EB5341AEB /* EssaySession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8874F23C092B83269A6E56 /* EssaySession.swift */; }; @@ -84,8 +86,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 015B10F8076FB519F8FDAACD /* kaoyan_english2_small.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = kaoyan_english2_small.md; sourceTree = ""; }; 018639A9F1452A7617A5A0A2 /* AppLifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLifecycle.swift; sourceTree = ""; }; 029A81F1C48B2FAE8914576B /* ReviewTextExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewTextExporter.swift; sourceTree = ""; }; + 0506352B36FD805B5E9053D8 /* kaoyan_english2_large.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = kaoyan_english2_large.md; sourceTree = ""; }; 0A8874F23C092B83269A6E56 /* EssaySession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EssaySession.swift; sourceTree = ""; }; 0F9DB3748FBE72E213C6EB37 /* ReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewView.swift; sourceTree = ""; }; 100F5B726342F502CA8C4207 /* LocalProcessRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalProcessRunner.swift; sourceTree = ""; }; @@ -287,6 +291,8 @@ ACA34C8DE60D85ABFA45116F /* kaoyan_english1_large.md */, 6A7B637179FC6E271F31DBF4 /* kaoyan_english1_small.md */, AD3CEB6EC11C595CA1EA79D6 /* kaoyan_english1_translation.md */, + 0506352B36FD805B5E9053D8 /* kaoyan_english2_large.md */, + 015B10F8076FB519F8FDAACD /* kaoyan_english2_small.md */, 56792E2C1501DBCE7B80739B /* kaoyan_english2_translation.md */, ); path = Rubrics; @@ -508,6 +514,8 @@ E32387900468FFCA09760D27 /* kaoyan_english1_large.md in Resources */, 96EB80336D8E7D70E3839817 /* kaoyan_english1_small.md in Resources */, 4B7B1CDF7FADBE73B96E26DA /* kaoyan_english1_translation.md in Resources */, + D03424B806D55BF2BB9F11DC /* kaoyan_english2_large.md in Resources */, + E041B0138CF95145600B02C6 /* kaoyan_english2_small.md in Resources */, D4A1396AF9FFF2FF6C40C24D /* kaoyan_english2_translation.md in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -647,7 +655,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; @@ -662,7 +670,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -743,7 +751,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -764,7 +772,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; diff --git a/WriteBench/Components/ExamTaskBadge.swift b/WriteBench/Components/ExamTaskBadge.swift index ca9b5f6..7399de3 100644 --- a/WriteBench/Components/ExamTaskBadge.swift +++ b/WriteBench/Components/ExamTaskBadge.swift @@ -7,6 +7,10 @@ struct ExamTaskBadge: View { switch task { case .kaoyanSmall: "英语一 · 小作文" case .kaoyanLarge: "英语一 · 大作文" + case .kaoyan2Small: "英语二 · 小作文" + case .kaoyan2Large: "英语二 · 大作文" + case .kaoyanTranslation: "英语一 · 翻译" + case .kaoyan2Translation: "英语二 · 翻译" case .cet6Writing: "写作" case .ieltsTask1: "Task 1 · 小作文" case .ieltsTask2: "Task 2 · 大作文" diff --git a/WriteBench/Features/Writing/QuestionLibraryView.swift b/WriteBench/Features/Writing/QuestionLibraryView.swift index d5c6177..a4a57fe 100644 --- a/WriteBench/Features/Writing/QuestionLibraryView.swift +++ b/WriteBench/Features/Writing/QuestionLibraryView.swift @@ -93,6 +93,7 @@ struct QuestionLibraryView: View { HStack(spacing: 8) { Text(item.title).font(.system(size: 14, weight: .semibold)) if let year = item.year { Text(String(year)).font(.system(size: 11)).foregroundStyle(WB.blue) } + if item.image != nil { Label("配图", systemImage: "photo").font(.system(size: 11)).foregroundStyle(WB.secondary) } if !item.label.isEmpty { Text(item.label).font(.system(size: 11)).foregroundStyle(WB.secondary).padding(.horizontal, 6).padding(.vertical, 2).background(WB.tint, in: Capsule()) } Spacer() if let latest = attempts.last { diff --git a/WriteBench/Features/Writing/WritingView.swift b/WriteBench/Features/Writing/WritingView.swift index 9687df2..7e202f5 100644 --- a/WriteBench/Features/Writing/WritingView.swift +++ b/WriteBench/Features/Writing/WritingView.swift @@ -12,6 +12,7 @@ struct WritingView: View { @State private var ocrTask: Task? @State private var editingQuestion = false @State private var importingText = false + @State private var showQuestionImage = true private let clock = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { Group { @@ -109,7 +110,7 @@ struct WritingView: View { Text(store.question).font(.system(size: 16)).lineSpacing(7).textSelection(.enabled).frame(maxWidth: .infinity, alignment: .leading) } if let data = store.questionImage, let image = NSImage(data: data) { - DisclosureGroup("题目原图") { Image(nsImage: image).resizable().scaledToFit().frame(maxHeight: 230).padding(.top, 8) }.font(.system(size: 12)).foregroundStyle(WB.secondary) + DisclosureGroup("题目配图", isExpanded: $showQuestionImage) { Image(nsImage: image).resizable().scaledToFit().frame(maxHeight: 320).padding(.top, 8) }.font(.system(size: 12)).foregroundStyle(WB.secondary) } HStack { Text(store.task.fullTitle).font(.system(size: 11)).foregroundStyle(WB.secondary) diff --git a/WriteBench/Models/Exam.swift b/WriteBench/Models/Exam.swift index 3273974..2b39db1 100644 --- a/WriteBench/Models/Exam.swift +++ b/WriteBench/Models/Exam.swift @@ -5,28 +5,28 @@ enum Exam: String, CaseIterable, Identifiable, Codable, Sendable { var id: String { rawValue } var title: String { switch self { case .kaoyan: "考研英语"; case .cet6: "CET-6 六级"; case .ielts: "IELTS 雅思" } } var symbol: String { switch self { case .kaoyan: "graduationcap"; case .cet6: "book"; case .ielts: "globe" } } - var detail: String { switch self { case .kaoyan: "英语一写作 · 英语一 / 二翻译"; case .cet6: "写作 / 翻译"; case .ielts: "Academic · Task 1 / Task 2" } } + var detail: String { switch self { case .kaoyan: "英语一 / 二 · 大小作文与翻译"; case .cet6: "写作 / 翻译"; case .ielts: "Academic · Task 1 / Task 2" } } var tasks: [WritingTask] { WritingTask.allCases.filter { $0.exam == self } } } enum WritingTask: String, CaseIterable, Identifiable, Codable, Sendable { - case kaoyanSmall, kaoyanLarge, kaoyanTranslation, kaoyan2Translation, cet6Writing, cet6Translation, ieltsTask1, ieltsTask2 + case kaoyanSmall, kaoyanLarge, kaoyan2Small, kaoyan2Large, kaoyanTranslation, kaoyan2Translation, cet6Writing, cet6Translation, ieltsTask1, ieltsTask2 var id: String { rawValue } var exam: Exam { - switch self { case .kaoyanSmall, .kaoyanLarge, .kaoyanTranslation, .kaoyan2Translation: .kaoyan; case .cet6Writing, .cet6Translation: .cet6; case .ieltsTask1, .ieltsTask2: .ielts } + switch self { case .kaoyanSmall, .kaoyanLarge, .kaoyan2Small, .kaoyan2Large, .kaoyanTranslation, .kaoyan2Translation: .kaoyan; case .cet6Writing, .cet6Translation: .cet6; case .ieltsTask1, .ieltsTask2: .ielts } } var title: String { - switch self { case .kaoyanSmall: "小作文"; case .kaoyanLarge: "大作文"; case .cet6Writing: "Writing"; case .ieltsTask1: "Task 1"; case .ieltsTask2: "Task 2"; case .kaoyanTranslation: "英语一翻译"; case .kaoyan2Translation: "英语二翻译"; case .cet6Translation: "翻译" } + switch self { case .kaoyanSmall: "英一小作文"; case .kaoyanLarge: "英一大作文"; case .kaoyan2Small: "英二小作文"; case .kaoyan2Large: "英二大作文"; case .cet6Writing: "Writing"; case .ieltsTask1: "Task 1"; case .ieltsTask2: "Task 2"; case .kaoyanTranslation: "英一翻译"; case .kaoyan2Translation: "英二翻译"; case .cet6Translation: "翻译" } } var isTranslation: Bool { self == .kaoyanTranslation || self == .kaoyan2Translation || self == .cet6Translation } var targetLanguage: String { (self == .kaoyanTranslation || self == .kaoyan2Translation) ? "Simplified Chinese" : "English" } var fullTitle: String { "\(exam.title) · \(title)" } - var maxScore: Double { switch self { case .kaoyanSmall, .kaoyanTranslation: 10; case .kaoyanLarge: 20; case .cet6Writing, .cet6Translation, .kaoyan2Translation: 15; case .ieltsTask1, .ieltsTask2: 9 } } - var targetWords: Int { switch self { case .kaoyanSmall: 100; case .kaoyanLarge: 200; case .cet6Writing: 180; case .ieltsTask1: 150; case .ieltsTask2: 250; case .kaoyanTranslation, .kaoyan2Translation, .cet6Translation: 0 } } - var wordGuidance: String { switch self { case .kaoyanSmall: "About 100 words"; case .kaoyanLarge: "160–200 words"; case .cet6Writing: "150–200 words"; case .ieltsTask1: "At least 150 words"; case .ieltsTask2: "At least 250 words"; case .kaoyanTranslation: "英语一 · 英译汉 · 完整翻译指定句子"; case .kaoyan2Translation: "英语二 · 英译汉 · 完整翻译段落"; case .cet6Translation: "汉译英 · 完整翻译原文" } } - var suggestedMinutes: Int { switch self { case .kaoyanSmall: 15; case .kaoyanTranslation, .kaoyan2Translation: 20; case .kaoyanLarge, .cet6Writing, .cet6Translation: 30; case .ieltsTask1: 20; case .ieltsTask2: 40 } } + var maxScore: Double { switch self { case .kaoyanSmall, .kaoyan2Small, .kaoyanTranslation: 10; case .kaoyanLarge: 20; case .kaoyan2Large, .cet6Writing, .cet6Translation, .kaoyan2Translation: 15; case .ieltsTask1, .ieltsTask2: 9 } } + var targetWords: Int { switch self { case .kaoyanSmall, .kaoyan2Small: 100; case .kaoyanLarge: 200; case .kaoyan2Large: 150; case .cet6Writing: 180; case .ieltsTask1: 150; case .ieltsTask2: 250; case .kaoyanTranslation, .kaoyan2Translation, .cet6Translation: 0 } } + var wordGuidance: String { switch self { case .kaoyanSmall, .kaoyan2Small: "About 100 words"; case .kaoyanLarge: "160–200 words"; case .kaoyan2Large: "About 150 words"; case .cet6Writing: "150–200 words"; case .ieltsTask1: "At least 150 words"; case .ieltsTask2: "At least 250 words"; case .kaoyanTranslation: "英语一 · 英译汉 · 完整翻译指定句子"; case .kaoyan2Translation: "英语二 · 英译汉 · 完整翻译段落"; case .cet6Translation: "汉译英 · 完整翻译原文" } } + var suggestedMinutes: Int { switch self { case .kaoyanSmall, .kaoyan2Small: 15; case .kaoyan2Large: 30; case .kaoyanTranslation, .kaoyan2Translation: 20; case .kaoyanLarge, .cet6Writing, .cet6Translation: 30; case .ieltsTask1: 20; case .ieltsTask2: 40 } } var rubricFile: String { - switch self { case .kaoyanSmall: "kaoyan_english1_small"; case .kaoyanLarge: "kaoyan_english1_large"; case .cet6Writing: "cet6_writing"; case .ieltsTask1: "ielts_task1"; case .ieltsTask2: "ielts_task2"; case .kaoyanTranslation: "kaoyan_english1_translation"; case .kaoyan2Translation: "kaoyan_english2_translation"; case .cet6Translation: "cet6_translation" } + switch self { case .kaoyanSmall: "kaoyan_english1_small"; case .kaoyanLarge: "kaoyan_english1_large"; case .kaoyan2Small: "kaoyan_english2_small"; case .kaoyan2Large: "kaoyan_english2_large"; case .cet6Writing: "cet6_writing"; case .ieltsTask1: "ielts_task1"; case .ieltsTask2: "ielts_task2"; case .kaoyanTranslation: "kaoyan_english1_translation"; case .kaoyan2Translation: "kaoyan_english2_translation"; case .cet6Translation: "cet6_translation" } } var sampleQuestion: String { switch self { @@ -34,6 +34,10 @@ enum WritingTask: String, CaseIterable, Identifiable, Codable, Sendable { "You are organising a lecture on Chinese culture at your university. Write a letter to your foreign friend Alex, inviting them to attend. Include the following details:\n1. The time and place\n2. The topic of the lecture\n3. Why you think they would enjoy it\nWrite about 100 words. Use “Li Ming” instead of your own name." case .kaoyanLarge: "A drawing shows two students approaching the same mountain. One looks only at its height; the other starts climbing one step at a time. Write an essay in which you describe the drawing, interpret its message, and give your comments. Write 160–200 words." + case .kaoyan2Small: + "Suppose your university is organising a weekend volunteer trip to a rural primary school. Write an email to Jack, an international student, to\n1) invite him to join the trip, and\n2) tell him about the arrangements.\nWrite your answer in about 100 words.\nDo not use your own name in your email. Use “Li Ming” instead." + case .kaoyan2Large: + "Write an essay based on the chart below. In your essay, you should\n1) describe and interpret the chart, and\n2) give your comments.\nWrite your answer in about 150 words.\n\n【图表数据】某高校学生每周课外阅读时长调查(2020 年与 2024 年,占受访学生比例)\n少于 1 小时:2020 年 42%,2024 年 28%\n1–3 小时:2020 年 38%,2024 年 41%\n3 小时以上:2020 年 20%,2024 年 31%" case .cet6Writing: "For this part, you are allowed 30 minutes to write an essay on the importance of developing independent thinking at university. Support your view with reasons and examples. Write at least 150 words but no more than 200 words." case .ieltsTask1: diff --git a/WriteBench/Models/QuestionBank.swift b/WriteBench/Models/QuestionBank.swift index b6eb71a..91759ac 100644 --- a/WriteBench/Models/QuestionBank.swift +++ b/WriteBench/Models/QuestionBank.swift @@ -36,6 +36,18 @@ enum QuestionBank { ("图画 · 照亮夜路的书灯", "A drawing shows a person walking along a dark forest path at night. They are holding up an open book that gives off light like a lamp, illuminating the path ahead. The caption reads “Light the way”.\nWrite an essay of 160–200 words in which you should\n1) describe the drawing briefly,\n2) explain its intended meaning, and\n3) give your comments."), ("图画 · 两条起跑线", "A drawing shows two runners on a track. One stands at the start line and keeps looking back at the crowd. The other is already running, eyes on the road ahead. A sign beside the track reads “The race is your own”.\nWrite an essay of 160–200 words in which you should\n1) describe the drawing briefly,\n2) explain its intended meaning, and\n3) give your comments.") ] + case .kaoyan2Small: [ + ("通知 · 二手书交换", "Suppose your class is going to hold a second-hand book exchange on campus. Write a notice to\n1) introduce the event, and\n2) encourage students to take part.\nWrite your answer in about 100 words.\nDo not use your own name in the notice. Use “Li Ming” instead."), + ("邮件 · 推迟小组讨论", "Suppose you have to postpone a group discussion you arranged with your classmate Tom. Write him an email to\n1) explain the reason, and\n2) suggest a new time.\nWrite your answer in about 100 words.\nDo not use your own name in your email. Use “Li Ming” instead."), + ("邮件 · 推荐周末去处", "Your friend Lucy, an exchange student, wants to spend a weekend outside the city. Write her an email to\n1) recommend a place, and\n2) give her some practical tips.\nWrite your answer in about 100 words.\nDo not use your own name in your email. Use “Li Ming” instead."), + ("邮件 · 感谢导师指导", "Suppose your research report won a prize with the help of Professor Brown. Write him an email to\n1) express your thanks, and\n2) tell him about your plan for further study.\nWrite your answer in about 100 words.\nDo not use your own name in your email. Use “Li Ming” instead.") + ] + case .kaoyan2Large: [ + ("图表 · 外卖使用频率", "Write an essay based on the chart below. In your essay, you should\n1) describe and interpret the chart, and\n2) give your comments.\nWrite your answer in about 150 words.\n\n【图表数据】某市大学生每周点外卖次数调查(占受访学生比例)\n0 次:12%\n1–2 次:35%\n3–5 次:38%\n6 次及以上:15%"), + ("图表 · 城市共享单车", "Write an essay based on the chart below. In your essay, you should\n1) describe and interpret the chart, and\n2) give your comments.\nWrite your answer in about 150 words.\n\n【图表数据】某市共享单车日均骑行量(单位:万次)\n2019 年:45\n2020 年:38\n2021 年:52\n2022 年:61\n2023 年:70"), + ("图表 · 选择工作的考虑因素", "Write an essay based on the chart below. In your essay, you should\n1) describe and interpret the chart, and\n2) give your comments.\nWrite your answer in about 150 words.\n\n【图表数据】某高校毕业生选择工作时最看重的因素\n个人发展空间:36%\n薪资待遇:29%\n工作地点:18%\n工作稳定性:12%\n其他:5%"), + ("图表 · 博物馆线上展览", "Write an essay based on the chart below. In your essay, you should\n1) describe and interpret the chart, and\n2) give your comments.\nWrite your answer in about 150 words.\n\n【图表数据】某省博物馆线下参观与线上观展人次(单位:万人次)\n2021 年:线下 320,线上 150\n2022 年:线下 290,线上 260\n2023 年:线下 410,线上 380") + ] case .cet6Writing: [ ("终身学习", "For this part, you are allowed 30 minutes to write an essay on the importance of lifelong learning. You can start your essay with the sentence “Learning is a treasure that will follow its owner everywhere.” You should write at least 150 words but no more than 200 words."), ("线上与线下学习", "For this part, you are allowed 30 minutes to write an essay on how college students can strike a balance between online and offline learning. You should write at least 150 words but no more than 200 words."), diff --git a/WriteBench/Rubrics/kaoyan_english2_large.md b/WriteBench/Rubrics/kaoyan_english2_large.md new file mode 100644 index 0000000..2c83d56 --- /dev/null +++ b/WriteBench/Rubrics/kaoyan_english2_large.md @@ -0,0 +1,6 @@ +# Kaoyan English II — large essay +Version: 2026.09-v1 +Scale: 0–15, half-point estimates. Typical length: about 150 words. App-owned practice rubric, not official examiner guidance. +English II large essays are usually based on a chart or table. Assess: accurate description of the key data and its overall trend or comparison, using the figures actually given; a reasonable interpretation of causes or meaning; relevant comments; coherent paragraphs; language range and accuracy. Selecting the main features matters more than listing every number. Misreading or inventing data is a task-completion error. Rely only on the figure transcription in the question; never invent unseen values. +Anchors: 13–15 complete, accurate data description with developed, relevant comment and varied accurate language; 10–12 generally complete and coherent despite some errors or thin development; 7–9 basic relevant response with weak data use or noticeable errors; 4–6 significant omissions, misread data or language problems; 1–3 minimal relevant communication; 0 blank or wholly unrelated. +Avoid rigid template rewards. Report overall score on 15, and diagnostics on 10. diff --git a/WriteBench/Rubrics/kaoyan_english2_small.md b/WriteBench/Rubrics/kaoyan_english2_small.md new file mode 100644 index 0000000..601fa1c --- /dev/null +++ b/WriteBench/Rubrics/kaoyan_english2_small.md @@ -0,0 +1,6 @@ +# Kaoyan English II — small essay +Version: 2026.09-v1 +Scale: 0–10, half-point estimates. Around 100 words. This is an app-owned practice rubric, not official examiner guidance. +Priorities: every bullet the prompt asks for and the communicative purpose; correct email/letter/notice format; tone suited to the recipient; clear order; accurate, natural language. English II small essays are practical and everyday; reward clear, appropriate communication over ornate wording. Evaluate underlength through missing content, not an invented fixed penalty. Follow the prompt's signature and address requirements. +Anchors: 9–10 covers all requirements clearly with appropriate format and very few language errors; 7–8 mostly complete and coherent with occasional errors; 5–6 basic purpose achieved but uneven coverage or frequent errors; 3–4 incomplete purpose and meaning often obstructed; 1–2 very limited relevant communication; 0 blank or wholly unrelated. +A missing requested bullet, wrong recipient or wrong text type is a task-completion issue. Keep diagnostics on 0–10 separately from the overall score. diff --git a/WriteBench/Services/DeepSeek/DeepSeekClient.swift b/WriteBench/Services/DeepSeek/DeepSeekClient.swift index 6e2aff5..2e0568d 100644 --- a/WriteBench/Services/DeepSeek/DeepSeekClient.swift +++ b/WriteBench/Services/DeepSeek/DeepSeekClient.swift @@ -124,7 +124,7 @@ extension GraderPrompt { Apply this rubric (version \(RubricLoader.version)): \(input.rubric) Overall score is on 0–\(input.task.maxScore), in increments of 0.5. Diagnostic taskCompletion, language, coherence, register are 0–10; these diagnostics are not a replacement for the exam rubric. Never conflate the two scales. - Give concise, specific explanations in Simplified Chinese. Keep corrected text and improvedVersion in \(input.task.targetLanguage). Original spans must be copied verbatim from the student answer. \(input.task.isTranslation ? "Assess translation fidelity against the source, completeness, logical relationships and natural target-language expression. Do not require essay arguments or penalize valid alternative translations. Distinguish omissions, additions and mistranslations. improvedVersion must be a complete faithful translation, not an essay." : "Preserve the student's meaning.") Do not invent prompt facts or data missing from a diagram. If essential information is missing, clearly explain the limitation in summary and taskCompletion. + Give concise, specific explanations in Simplified Chinese. Keep corrected text and improvedVersion in \(input.task.targetLanguage). Original spans must be copied verbatim from the student answer. \(input.task.isTranslation ? "Assess translation fidelity against the source, completeness, logical relationships and natural target-language expression. Do not require essay arguments or penalize valid alternative translations. Distinguish omissions, additions and mistranslations. improvedVersion must be a complete faithful translation, not an essay." : "Preserve the student's meaning.") Do not invent prompt facts or data missing from a diagram. If the question contains a section marked 【配图说明】, 【图画内容】 or 【图表数据】, treat it as the authoritative transcription of the picture, chart or table the student saw: check the essay's description and data against it, and penalize misread or invented figures. If essential information is missing, clearly explain the limitation in summary and taskCompletion. Return JSON only with exactly this schema; every field is required. Write summary FIRST so it can be previewed while the rest streams. Give a concise verdict in summary; do not expose private reasoning. strengths identifies 1–3 specific things done well, weaknesses identifies 1–3 scoring-relevant problems, and improvements gives 1–3 concrete next-rewrite actions. Do not invent praise; arrays may be empty when there is no supported observation. {"summary": "specific examiner verdict", "strengths": ["what works, with evidence"], "weaknesses": ["what loses marks, with evidence"], "improvements": ["specific next-rewrite action"], "score": 0.0, "taskCompletion": 0.0, "language": 0.0, "coherence": 0.0, "register": 0.0, diff --git a/WriteBench/Services/Grading/GradingService.swift b/WriteBench/Services/Grading/GradingService.swift index 4f2dbfa..01ea8f6 100644 --- a/WriteBench/Services/Grading/GradingService.swift +++ b/WriteBench/Services/Grading/GradingService.swift @@ -104,4 +104,4 @@ enum RubricLoader { return try String(contentsOf: url, encoding: .utf8) } } -enum GraderPrompt { static let version = "1.3.0" } +enum GraderPrompt { static let version = "1.3.1" } diff --git a/WriteBenchTests/PracticeLoopTests.swift b/WriteBenchTests/PracticeLoopTests.swift index bdeda47..be74c05 100644 --- a/WriteBenchTests/PracticeLoopTests.swift +++ b/WriteBenchTests/PracticeLoopTests.swift @@ -230,3 +230,15 @@ private actor RecordingGrader: EssayGradingService { #expect(again.added == 0 && again.skipped == 5) #expect(throws: (any Error).self) { try BackupService.restore(Data("{}".utf8), into: target) } } + +// MARK: English II writing + +@Test func englishTwoWritingTasksHaveTheirOwnScalesRubricsAndFigureRule() throws { + #expect(Exam.kaoyan.tasks == [.kaoyanSmall, .kaoyanLarge, .kaoyan2Small, .kaoyan2Large, .kaoyanTranslation, .kaoyan2Translation]) + #expect(WritingTask.kaoyan2Small.maxScore == 10 && WritingTask.kaoyan2Large.maxScore == 15 && WritingTask.kaoyanLarge.maxScore == 20) + #expect(WritingTask.kaoyan2Large.targetWords == 150 && !WritingTask.kaoyan2Large.isTranslation) + for task in [WritingTask.kaoyan2Small, .kaoyan2Large] { #expect(try RubricLoader.load(task).contains("English II")) } + let input = GradingInput(task: .kaoyan2Large, question: "Write an essay based on the chart.\n【配图说明】看电视 90.8%", essay: "TV is popular.", rubric: try RubricLoader.load(.kaoyan2Large)) + let prompt = GraderPrompt.system(judge: .a, input: input) + #expect(prompt.contains("【配图说明】") && prompt.contains("0–15.0")) +} diff --git a/project.yml b/project.yml index 75d4c3a..f7c7a39 100644 --- a/project.yml +++ b/project.yml @@ -11,8 +11,8 @@ settings: CODE_SIGN_IDENTITY: "-" ENABLE_HARDENED_RUNTIME: YES GENERATE_INFOPLIST_FILE: YES - MARKETING_VERSION: "1.5.0" - CURRENT_PROJECT_VERSION: "8" + MARKETING_VERSION: "1.6.0" + CURRENT_PROJECT_VERSION: "9" targets: WriteBench: type: application