fix(polish): gate test-build style generation at 1,250 chars and cap export at 5,000

Remove the test-mode unlimited personal-style corpus bypass and replace it with a real 1,250-character minimum gate. Production stays at 2,500. Training-corpus export and the diagnostics view now use a 5,000-character upper bound so the user's full private history never leaves the device in a single export.

- Add PolishStyleLearningCorpusBuilder.testBuildEffectiveCharacterCount (1,250) and trainingExtractionMaximumCharacterCount (5,000); trainingWindow(from:maximumCharacterCount:) accepts a custom cap.
- PolishStylesView: drop bypassesStyleLearningCharacterGate in favor of styleLearningMinimumCharacterCount, unify the ready/remaining text, remove the obsolete polishStyles.learn.testBuildReady key.
- PolishStyleCorpusExportStore: pass the 5,000 cap to trainingWindow and expose trainingExtractionMaximumCharacterCount in the export schema.
- FlowDiagnosticsSettingsView: show the export-sized (5,000-cap) corpus so the diagnostics match what the export ZIP actually contains.
- Tests: rewrite the export cap test for 5,000, add below-cap coverage, add trainingWindow(maximumCharacterCount:) unit coverage, and convert the old UI test into a regression test for the 1,250 test-build minimum.
- L10n: drop the testBuildReady string in en + zh-Hans, update diagnostics.corpus.ready to mention 5,000.
- CHANGELOG: bilingual entry under [Unreleased].
This commit is contained in:
Rocky
2026-08-30 12:09:06 +08:00
parent eac44daaf4
commit 3033ca5b33
11 changed files with 175 additions and 35 deletions
@@ -15,6 +15,10 @@ struct PolishStyleCorpusExport: Codable, Equatable {
let appBuild: String
let effectiveCharacterCount: Int
let requiredEffectiveCharacterCount: Int
/// Maximum effective characters included in this export. Caps the
/// training-corpus window even when the user has accumulated far
/// more history than the production 2,500-character unlock gate.
let trainingExtractionMaximumCharacterCount: Int
let examples: [Example]
struct Example: Codable, Equatable {
@@ -55,7 +59,10 @@ final class PolishStyleCorpusExportStore {
let eligibleCorpus = PolishStyleLearningCorpusBuilder.build(from: history)
guard !eligibleCorpus.examples.isEmpty else { return nil }
let corpus = PolishStyleLearningCorpusBuilder.trainingWindow(
from: eligibleCorpus.examples
from: eligibleCorpus.examples,
maximumCharacterCount:
PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount
)
let examples = corpus.examples
@@ -77,6 +84,9 @@ final class PolishStyleCorpusExportStore {
effectiveCharacterCount: corpus.effectiveCharacterCount,
requiredEffectiveCharacterCount:
PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount,
trainingExtractionMaximumCharacterCount:
PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount,
examples: examples
)
}
@@ -135,7 +135,10 @@ struct FlowDiagnosticsSettingsView: View {
from: historyStore.snapshot()
)
return PolishStyleLearningCorpusBuilder.trainingWindow(
from: eligibleCorpus.examples
from: eligibleCorpus.examples,
maximumCharacterCount:
PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount
)
}
+19 -11
View File
@@ -55,7 +55,8 @@ struct PolishStylesView: View {
outputLanguage: language,
minimumEffectiveCharacterCount:
AppDistributionChannel.allowsInternalTools
? 0
? PolishStyleLearningCorpusBuilder
.testBuildEffectiveCharacterCount
: PolishStyleLearningCorpusBuilder
.requiredEffectiveCharacterCount
)
@@ -158,17 +159,20 @@ struct PolishStylesView: View {
PolishStyleLearningCorpusBuilder.build(from: history.snapshot())
}
/// Debug and TestFlight builds may exercise the complete generation
/// pipeline before enough personal corpus exists. App Store builds keep
/// the production 2,500-character gate.
private var bypassesStyleLearningCharacterGate: Bool {
/// Minimum effective character count required to unlock personal style
/// generation. Debug + TestFlight builds use a lower 1,250-character
/// gate so internal testers can still exercise the pipeline; the
/// previous 0 / "unlimited" bypass has been removed.
private var styleLearningMinimumCharacterCount: Int {
AppDistributionChannel.allowsInternalTools
? PolishStyleLearningCorpusBuilder.testBuildEffectiveCharacterCount
: PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount
}
private func isEligibleForStyleGeneration(
_ corpus: PolishStyleLearningCorpus
) -> Bool {
bypassesStyleLearningCharacterGate || corpus.isReady
corpus.effectiveCharacterCount >= styleLearningMinimumCharacterCount
}
private var learnedStylePack: PolishStylePack? {
@@ -185,7 +189,7 @@ struct PolishStylesView: View {
private var styleLearningCard: some View {
let corpus = styleLearningCorpus
let required = PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount
let required = styleLearningMinimumCharacterCount
let reachedLimit = catalog.entries.count >= PolishStyleLimits.maximumUserPacks
let isActionAvailable = isEligibleForStyleGeneration(corpus) && !reachedLimit
let canGenerate = isActionAvailable && !isGeneratingLearnedStyle
@@ -240,13 +244,17 @@ struct PolishStylesView: View {
Spacer()
Text(
bypassesStyleLearningCharacterGate && !corpus.isReady
? AppL10n.string("polishStyles.learn.testBuildReady")
: corpus.isReady
isEligibleForStyleGeneration(corpus)
? AppL10n.string("polishStyles.learn.ready")
: AppL10n.format(
"polishStyles.learn.remaining",
Int64(corpus.remainingCharacterCount)
Int64(
max(
0,
styleLearningMinimumCharacterCount
- corpus.effectiveCharacterCount
)
)
)
)
.font(TypeStyle.caption2)
@@ -134,7 +134,8 @@ private struct PolishStylesServiceUITestHarness: View {
outputLanguage: language,
minimumEffectiveCharacterCount:
AppDistributionChannel.allowsInternalTools
? 0
? PolishStyleLearningCorpusBuilder
.testBuildEffectiveCharacterCount
: PolishStyleLearningCorpusBuilder
.requiredEffectiveCharacterCount
)
+4 -2
View File
@@ -128,6 +128,7 @@
"common.continue" = "Continue";
"common.reset" = "Reset";
"common.cancel" = "Cancel";
"common.dismiss" = "Dismiss";
"common.save" = "Save";
"common.clear" = "Clear";
"common.delete" = "Delete";
@@ -294,7 +295,7 @@
"settings.diagnostics.corpus.section" = "Personal Style Training Corpus";
"settings.diagnostics.corpus.count" = "%d eligible samples · %d effective characters";
"settings.diagnostics.corpus.empty" = "No eligible paired dictations are available.";
"settings.diagnostics.corpus.ready" = "Newest complete samples through the 2,500-character target.";
"settings.diagnostics.corpus.ready" = "Newest complete samples through the 5,000-character export cap.";
"settings.diagnostics.corpus.export" = "Export Training Corpus";
"settings.diagnostics.corpus.privacy" = "Contains dictated text and prior polish prompts. Nothing is uploaded automatically. Share only with trusted devices or services.";
"settings.diagnostics.privacy.section" = "Privacy";
@@ -683,7 +684,6 @@
"polishStyles.learn.progress" = "%lld / %lld characters";
"polishStyles.learn.remaining" = "%lld to go";
"polishStyles.learn.ready" = "Ready";
"polishStyles.learn.testBuildReady" = "Test build: 2,500-character limit disabled";
"polishStyles.learn.action" = "Generate Style";
"polishStyles.learn.generating" = "Generating…";
"polishStyles.learn.generated.description" = "Your personal style learned from reviewed evidence.";
@@ -959,6 +959,8 @@
"account.referral.status.rewarded" = "Rewarded";
"account.referral.status.ineligible" = "Ineligible";
"account.referral.pendingAfterSignIn" = "Your invitation is saved and will be redeemed after sign-in.";
"account.referral.rewardGranted" = "You received %@ credits from the invitation.";
"account.referral.rewardGrantedGeneric" = "Your invitation reward has been added to your balance.";
"account.security.section" = "Account controls";
"account.signOut.action" = "Sign Out";
"account.signOut.confirmTitle" = "Sign out?";
@@ -128,6 +128,7 @@
"common.continue" = "继续";
"common.reset" = "重置";
"common.cancel" = "取消";
"common.dismiss" = "关闭";
"common.save" = "保存";
"common.clear" = "清空";
"common.delete" = "删除";
@@ -294,7 +295,7 @@
"settings.diagnostics.corpus.section" = "个人风格训练语料";
"settings.diagnostics.corpus.count" = "%d 条可用样本 · %d 个有效字符";
"settings.diagnostics.corpus.empty" = "暂无符合条件的成对听写语料。";
"settings.diagnostics.corpus.ready" = "选取最新完整样本,累积至至少 2,500 个有效字符。";
"settings.diagnostics.corpus.ready" = "选取最新完整样本,导出时最多累积 5,000 个有效字符。";
"settings.diagnostics.corpus.export" = "导出训练语料";
"settings.diagnostics.corpus.privacy" = "文件包含口述文本和历史润色提示词,不会自动上传。请仅分享给可信设备或服务。";
"settings.diagnostics.privacy.section" = "隐私";
@@ -682,7 +683,6 @@
"polishStyles.learn.progress" = "%lld / %lld 字";
"polishStyles.learn.remaining" = "还差 %lld 字";
"polishStyles.learn.ready" = "可以生成";
"polishStyles.learn.testBuildReady" = "测试版本:已暂时取消 2500 字限制";
"polishStyles.learn.action" = "生成风格";
"polishStyles.learn.generating" = "生成中…";
"polishStyles.learn.generated.description" = "根据经过确认的证据学习得到的个人表达风格。";
@@ -958,6 +958,8 @@
"account.referral.status.rewarded" = "已奖励";
"account.referral.status.ineligible" = "不符合条件";
"account.referral.pendingAfterSignIn" = "邀请已暂存,登录后会自动兑换。";
"account.referral.rewardGranted" = "恭喜获得 %@ 积分";
"account.referral.rewardGrantedGeneric" = "邀请奖励已发放,积分已到账。";
"account.security.section" = "账号操作";
"account.signOut.action" = "退出登录";
"account.signOut.confirmTitle" = "退出登录?";