fix: align dynamic choice boundaries
This commit is contained in:
@@ -40,12 +40,21 @@ Final-fix RED artifact: `.omo/evidence/task-4-final-red.log`.
|
||||
- Python: 4/4 passed, including the new same-month/day-precision regression, confirming that
|
||||
the production behavior existed but previously lacked durable coverage.
|
||||
|
||||
Standards-axis RED artifact: `.omo/evidence/task-4-axis-red.log`.
|
||||
|
||||
- TypeScript: 19 tests, 2 expected failures. Server prompts of 121 and 240 characters were
|
||||
accepted by binder and service instead of failing before ID allocation and commit.
|
||||
- Python: 4/4 passed after replacing localized precision glyph assertions with numeric-boundary
|
||||
structure and normalized uniqueness checks.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `birth-time-dynamic-question-copy.ts` now contains only server-copy structural validation,
|
||||
NFKC/whitespace label normalization, the note-free opportunity-selection projection, and
|
||||
deterministic server-copy fingerprinting. The former note blacklist and substring
|
||||
grounding logic were removed.
|
||||
grounding logic were removed. Shared constants cap server questions at 120 characters and
|
||||
labels at 80 across the API adapter, public schema, internal model, persisted schema, and
|
||||
binding guard.
|
||||
- `birth-time-dynamic-question-validator.ts` accepts only strict selection objects. Binding
|
||||
resolves the selected server opportunity, validates the prompt and normalized uniqueness
|
||||
across every primary and reserved visible label, validates every matching private
|
||||
@@ -57,7 +66,9 @@ Final-fix RED artifact: `.omo/evidence/task-4-final-red.log`.
|
||||
- `dynamic_rectification_copy.py` owns localized contexts and the least detailed
|
||||
year/month/day range representation needed to distinguish visible windows. Cross-year
|
||||
ranges stay concise; same-year or same-month collisions gain month or day precision.
|
||||
`dynamic_rectification_opportunities.py` is again below the 250-pure-LOC boundary.
|
||||
Its precision discriminator is the exhaustive `Literal["year", "month", "day"]` domain;
|
||||
unknown precision cannot silently fall through. `dynamic_rectification_opportunities.py`
|
||||
remains below the 250-pure-LOC boundary.
|
||||
- The Mastra contract describes selection only and forbids prompt/options/labels/partition
|
||||
fields in Agent output.
|
||||
|
||||
@@ -70,24 +81,29 @@ through the Task 4 service. Task 5 persistence was not changed.
|
||||
|
||||
| Gate | Result | Artifact |
|
||||
| --- | --- | --- |
|
||||
| Final-fix RED | expected 2 TS failures; Python 4/4 | `.omo/evidence/task-4-final-red.log` |
|
||||
| Focused dynamic/guide TypeScript | 38/38 pass | `.omo/evidence/task-4-final-focused-ts.log` |
|
||||
| Focused Task 2 Python | 26/26 pass | `.omo/evidence/task-4-final-focused-python.log` |
|
||||
| Legacy Python rectification | 22/22 pass | `.omo/evidence/task-4-final-legacy-python.log` |
|
||||
| All birth-time TypeScript | 225/225 pass | `.omo/evidence/task-4-final-birth-time.log` |
|
||||
| Full frontend | 300/300 pass | `.omo/evidence/task-4-final-frontend-full.log` |
|
||||
| Cumulative changed TypeScript ESLint | pass, zero diagnostics | `.omo/evidence/task-4-final-eslint.log` |
|
||||
| Cumulative changed Python Ruff | pass | `.omo/evidence/task-4-final-ruff.log` |
|
||||
| Diff check and all changed TS/Python LOC | pass; every audited file <=250 | `.omo/evidence/task-4-final-quality.log` |
|
||||
| Full TypeScript check | only known unrelated `profile-persistence.test.ts:7` TS1501 | `.omo/evidence/task-4-final-tsc.log` |
|
||||
| Fresh final-fix review | CLEAR / APPROVE; no blockers or WATCH items | `.omo/evidence/task-4-final-fix-code-review.md` |
|
||||
| Standards-axis RED | expected 2 TS failures; Python 4/4 | `.omo/evidence/task-4-axis-red.log` |
|
||||
| Focused dynamic/guide TypeScript | 40/40 pass | `.omo/evidence/task-4-axis-focused-ts.log` |
|
||||
| Public dynamic-choice schema TypeScript | 7/7 pass | `.omo/evidence/task-4-axis-public-schema-ts.log` |
|
||||
| Dynamic adapter boundary TypeScript | 8/8 pass | `.omo/evidence/task-4-axis-adapter-ts.log` |
|
||||
| Focused Task 2 Python | 26/26 pass | `.omo/evidence/task-4-axis-focused-python.log` |
|
||||
| Legacy Python rectification | 22/22 pass | `.omo/evidence/task-4-axis-legacy-python.log` |
|
||||
| All birth-time TypeScript | 229/229 pass | `.omo/evidence/task-4-axis-birth-time.log` |
|
||||
| Full frontend | 304/304 pass | `.omo/evidence/task-4-axis-frontend-full.log` |
|
||||
| Cumulative changed TypeScript ESLint | pass, zero diagnostics | `.omo/evidence/task-4-axis-eslint.log` |
|
||||
| Cumulative changed Python Ruff | pass | `.omo/evidence/task-4-axis-ruff.log` |
|
||||
| Diff check and all changed TS/Python LOC | pass; every audited file <=250 | `.omo/evidence/task-4-axis-quality.log` |
|
||||
| Full TypeScript check | only known unrelated `profile-persistence.test.ts:7` TS1501 | `.omo/evidence/task-4-axis-tsc.log` |
|
||||
| Fresh standards-axis fix review | CLEAR / APPROVE; no blockers | `.omo/evidence/task-4-axis-fix-code-review.md` |
|
||||
|
||||
The TypeScript command remains non-zero solely because the pre-existing profile-persistence
|
||||
test uses a regular-expression flag newer than the configured target. No Task 4 file reports
|
||||
a type error.
|
||||
The earlier `.omo/evidence/task-4-selection-boundary-code-review.md` `CLEAR` is explicitly
|
||||
superseded by `.omo/evidence/task-4-final-review.md`; it is not cited as current acceptance.
|
||||
The final reviewer independently rechecked reserved-label normalization, zero allocation and
|
||||
commit, the packet-only prompt API, typed Python range-copy boundary, same-month day precision,
|
||||
prose-pin removal, and cumulative LOC. Both required programming language perspectives and the
|
||||
remove-slops perspective returned `CLEAR / APPROVE` with no remaining WATCH item.
|
||||
The earlier `.omo/evidence/task-4-final-fix-code-review.md` `CLEAR` is explicitly superseded by
|
||||
the standards-axis review and is not cited as current acceptance. The new tests contain no
|
||||
localized month/day or domain-word assertions; precision is verified through distinct normalized
|
||||
labels and the number of numeric range-boundary tokens.
|
||||
The fresh reviewer also verified the shared 120/80 boundary through the public schema, API
|
||||
adapter, internal and persisted schemas, and binding guard. Both programming language
|
||||
perspectives and the remove-slops perspective returned `CLEAR / APPROVE`.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { candidateResultSchema } from "./birth-time-evidence.ts";
|
||||
import {
|
||||
DYNAMIC_QUESTION_LABEL_MAX_LENGTH,
|
||||
DYNAMIC_QUESTION_PROMPT_MAX_LENGTH,
|
||||
} from "./birth-time-dynamic-question-limits.ts";
|
||||
import {
|
||||
publicChoiceKindSchema,
|
||||
publicDynamicChoiceQuestionSchema,
|
||||
@@ -111,7 +115,7 @@ export type DynamicControlState = {
|
||||
const evidencePartitionBaseSchema = z.object({
|
||||
partitionId: z.string().trim().min(1),
|
||||
descriptor: z.string().trim().min(1),
|
||||
fallbackLabel: z.string().trim().min(1).max(80),
|
||||
fallbackLabel: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
|
||||
}).strict();
|
||||
|
||||
export const evidencePartitionSchema = evidencePartitionBaseSchema.readonly();
|
||||
@@ -126,7 +130,7 @@ export const questionOpportunitySchema = z.object({
|
||||
neutralContext: z.string().trim().min(1),
|
||||
estimatedInformationGain: z.number().finite().nonnegative(),
|
||||
candidatePartitionFingerprint: z.string().trim().min(1),
|
||||
fallbackPrompt: z.string().trim().min(1).max(240),
|
||||
fallbackPrompt: z.string().trim().min(1).max(DYNAMIC_QUESTION_PROMPT_MAX_LENGTH),
|
||||
partitions: z.array(evidencePartitionSchema).min(2).max(4).readonly(),
|
||||
}).strict().readonly();
|
||||
|
||||
@@ -148,7 +152,7 @@ export const candidateDifferenceBuildSchema = z.object({
|
||||
|
||||
const persistedPrimaryChoiceSchema = z.object({
|
||||
optionId: z.string().trim().min(1),
|
||||
label: z.string().trim().min(1).max(80),
|
||||
label: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
|
||||
kind: z.literal("primary"),
|
||||
partitionId: z.string().trim().min(1),
|
||||
candidateScores: finiteScoresSchema,
|
||||
@@ -156,7 +160,7 @@ const persistedPrimaryChoiceSchema = z.object({
|
||||
|
||||
const persistedSpecialChoiceSchema = (kind: "unknown" | "unmatched") => z.object({
|
||||
optionId: z.string().trim().min(1),
|
||||
label: z.string().trim().min(1).max(80),
|
||||
label: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
|
||||
kind: z.literal(kind),
|
||||
partitionId: z.null(),
|
||||
candidateScores: z.null(),
|
||||
@@ -171,7 +175,7 @@ export const persistedDynamicChoiceQuestionSchema = z.object({
|
||||
source: z.enum(["agent", "fallback"]),
|
||||
questionFingerprint: z.string().trim().min(1),
|
||||
candidatePartitionFingerprint: z.string().trim().min(1),
|
||||
prompt: z.string().trim().min(1).max(240),
|
||||
prompt: z.string().trim().min(1).max(DYNAMIC_QUESTION_PROMPT_MAX_LENGTH),
|
||||
options: z.array(z.union([
|
||||
persistedPrimaryChoiceSchema,
|
||||
persistedSpecialChoiceSchema("unknown"),
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
DYNAMIC_QUESTION_LABEL_MAX_LENGTH,
|
||||
DYNAMIC_QUESTION_PROMPT_MAX_LENGTH,
|
||||
} from "./birth-time-dynamic-question-limits.ts";
|
||||
|
||||
const timeSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/);
|
||||
|
||||
@@ -18,7 +22,7 @@ export type TimeRange = {
|
||||
|
||||
export const publicDynamicChoiceOptionSchema = z.object({
|
||||
optionId: z.string().trim().min(1),
|
||||
label: z.string().trim().min(1).max(80),
|
||||
label: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
|
||||
kind: publicChoiceKindSchema,
|
||||
}).strict().readonly();
|
||||
|
||||
@@ -55,7 +59,7 @@ function validateOptionSet(
|
||||
|
||||
export const publicDynamicChoiceQuestionSchema = z.object({
|
||||
questionId: z.string().trim().min(1),
|
||||
prompt: z.string().trim().min(1).max(240),
|
||||
prompt: z.string().trim().min(1).max(DYNAMIC_QUESTION_PROMPT_MAX_LENGTH),
|
||||
options: z.array(publicDynamicChoiceOptionSchema).readonly(),
|
||||
}).strict().superRefine(validateOptionSet).readonly();
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
DYNAMIC_QUESTION_LABEL_MAX_LENGTH,
|
||||
DYNAMIC_QUESTION_PROMPT_MAX_LENGTH,
|
||||
} from "./birth-time-dynamic-question-limits.ts";
|
||||
import type { CandidateDifferencePacket } from "./birth-time-dynamic-choice-internal.ts";
|
||||
|
||||
const clockTimePattern = /(?:^|[^\d])(?:[01]?\d|2[0-3])\s*[::]\s*[0-5]\d(?:$|[^\d])/;
|
||||
|
||||
export function dynamicServerCopyIsSafe(value: string, question: boolean): boolean {
|
||||
const normalized = value.normalize("NFKC").trim();
|
||||
if (normalized.length > (question ? 240 : 80)) return false;
|
||||
if (normalized.length > (
|
||||
question ? DYNAMIC_QUESTION_PROMPT_MAX_LENGTH : DYNAMIC_QUESTION_LABEL_MAX_LENGTH
|
||||
)) return false;
|
||||
if (!/[\u3400-\u9fff]/u.test(normalized) || /[A-Za-z]/.test(normalized)) return false;
|
||||
if (clockTimePattern.test(normalized)) return false;
|
||||
return !question || (normalized.match(/[??]/g) ?? []).length === 1;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const DYNAMIC_QUESTION_PROMPT_MAX_LENGTH = 120;
|
||||
export const DYNAMIC_QUESTION_LABEL_MAX_LENGTH = 80;
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
dynamicChoiceScoringResultSchema,
|
||||
} from "./birth-time-dynamic-choice-internal.ts";
|
||||
import { candidateResultSchema } from "./birth-time-evidence.ts";
|
||||
import {
|
||||
DYNAMIC_QUESTION_LABEL_MAX_LENGTH,
|
||||
DYNAMIC_QUESTION_PROMPT_MAX_LENGTH,
|
||||
} from "./birth-time-dynamic-question-limits.ts";
|
||||
import type {
|
||||
CandidateDifferenceBuild,
|
||||
DynamicChoiceScoringResult,
|
||||
@@ -17,7 +21,7 @@ const apiRangeSchema = z.object({
|
||||
const partitionSchema = z.object({
|
||||
partition_id: z.string().trim().min(1),
|
||||
descriptor: z.string().trim().min(1),
|
||||
fallback_label: z.string().trim().min(1).max(80),
|
||||
fallback_label: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
|
||||
candidate_scores: z.record(candidateTimeSchema, z.number().finite().nonnegative()),
|
||||
}).strict();
|
||||
const opportunitySchema = z.object({
|
||||
@@ -26,7 +30,7 @@ const opportunitySchema = z.object({
|
||||
neutral_context: z.string().trim().min(1),
|
||||
estimated_information_gain: z.number().finite().nonnegative(),
|
||||
candidate_partition_fingerprint: z.string().trim().min(1),
|
||||
fallback_prompt: z.string().trim().min(1).max(240),
|
||||
fallback_prompt: z.string().trim().min(1).max(DYNAMIC_QUESTION_PROMPT_MAX_LENGTH),
|
||||
partitions: z.array(partitionSchema).min(2).max(4),
|
||||
}).strict().superRefine((value, context) => {
|
||||
if (new Set(value.partitions.map((item) => item.partition_id)).size !== value.partitions.length) {
|
||||
|
||||
@@ -3,7 +3,10 @@ import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { publicDynamicChoiceQuestionSchema } from "../src/lib/birth-time-dynamic-choice.ts";
|
||||
import { persistedDynamicChoiceQuestionSchema } from "../src/lib/birth-time-dynamic-choice-internal.ts";
|
||||
import {
|
||||
persistedDynamicChoiceQuestionSchema,
|
||||
toPublicDynamicChoiceQuestion,
|
||||
} from "../src/lib/birth-time-dynamic-choice-internal.ts";
|
||||
import { dynamicJourneyTurnStateSchema, journeyTurnStateSchema } from "../src/lib/birth-time-journey-turn-protocol.ts";
|
||||
|
||||
const internalQuestion = {
|
||||
@@ -74,6 +77,20 @@ test("public questions never expose partition ids", () => {
|
||||
}).success, false);
|
||||
});
|
||||
|
||||
test("public questions enforce the shared prompt limit", () => {
|
||||
const publicQuestion = toPublicDynamicChoiceQuestion(internalQuestion);
|
||||
const promptWithLength = (length: number) => `${"问".repeat(length - 1)}?`;
|
||||
|
||||
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
|
||||
...publicQuestion,
|
||||
prompt: promptWithLength(120),
|
||||
}).success, true);
|
||||
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
|
||||
...publicQuestion,
|
||||
prompt: promptWithLength(121),
|
||||
}).success, false);
|
||||
});
|
||||
|
||||
test("internal primary choices require a server partition", () => {
|
||||
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse(internalQuestion).success, true);
|
||||
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse({
|
||||
|
||||
@@ -213,6 +213,31 @@ test("reserved-label collisions propagate without commit or ID allocation", asyn
|
||||
}
|
||||
});
|
||||
|
||||
test("overlong server prompts propagate without commit or ID allocation", async () => {
|
||||
const opportunity = differenceBuild.packet.opportunities[0];
|
||||
if (!opportunity) throw new Error("missing test opportunity");
|
||||
for (const length of [121, 240]) {
|
||||
let allocations = 0;
|
||||
let commits = 0;
|
||||
const fallbackPrompt = `${"问".repeat(length - 1)}?`;
|
||||
|
||||
await assert.rejects(() => dynamicService({
|
||||
build: {
|
||||
...differenceBuild,
|
||||
packet: {
|
||||
...differenceBuild.packet,
|
||||
opportunities: [{ ...opportunity, fallbackPrompt }],
|
||||
},
|
||||
},
|
||||
generator: generatorFrom(() => JSON.stringify(validDynamicSelection)),
|
||||
createId: deterministicIds(() => { allocations += 1; }),
|
||||
onCommit: () => { commits += 1; },
|
||||
}).generateQuestion("owner-1", generationCommand), BirthTimeDynamicBindingError);
|
||||
assert.equal(allocations, 0, String(length));
|
||||
assert.equal(commits, 0, String(length));
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid server UUIDs propagate without committing a low result", async () => {
|
||||
let commits = 0;
|
||||
await assert.rejects(() => dynamicService({
|
||||
|
||||
@@ -139,6 +139,33 @@ test("primary labels cannot collide with either reserved visible choice", () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("server prompt accepts 120 characters but rejects longer copy before IDs", () => {
|
||||
const opportunity = dynamicPacket.opportunities[0];
|
||||
if (!opportunity) throw new Error("missing test opportunity");
|
||||
const promptWithLength = (length: number) => `${"问".repeat(length - 1)}?`;
|
||||
const buildWithPrompt = (length: number) => ({
|
||||
...differenceBuild,
|
||||
packet: {
|
||||
...dynamicPacket,
|
||||
opportunities: [{ ...opportunity, fallbackPrompt: promptWithLength(length) }],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
bindDynamicQuestion(validDynamicSelection, buildWithPrompt(120), deterministicIds()).prompt.length,
|
||||
120,
|
||||
);
|
||||
for (const length of [121, 240]) {
|
||||
let allocations = 0;
|
||||
assert.throws(() => bindDynamicQuestion(
|
||||
validDynamicSelection,
|
||||
buildWithPrompt(length),
|
||||
deterministicIds(() => { allocations += 1; }),
|
||||
), BirthTimeDynamicBindingError);
|
||||
assert.equal(allocations, 0, String(length));
|
||||
}
|
||||
});
|
||||
|
||||
test("repeated server semantics and partitions remain recoverable rejections", () => {
|
||||
const selection = parseDynamicQuestionOutput(validDynamicSelection, dynamicPacket);
|
||||
if (selection.kind !== "question") throw new Error("expected a selection");
|
||||
|
||||
@@ -85,6 +85,19 @@ test("difference packets separate public copy from private score vectors", () =>
|
||||
});
|
||||
});
|
||||
|
||||
test("difference packets enforce the public prompt limit at the API boundary", () => {
|
||||
const withPrompt = (length: number) => ({
|
||||
...apiPacket,
|
||||
opportunities: [{
|
||||
...apiPacket.opportunities[0],
|
||||
fallback_prompt: `${"问".repeat(length - 1)}?`,
|
||||
}],
|
||||
});
|
||||
|
||||
assert.equal(parseCandidateDifferenceBuild(withPrompt(120)).packet.opportunities[0]?.fallbackPrompt.length, 120);
|
||||
assert.throws(() => parseCandidateDifferenceBuild(withPrompt(121)));
|
||||
});
|
||||
|
||||
test("difference packets reject wrong versions, extra fields, and invalid score keys", () => {
|
||||
assert.throws(() => parseCandidateDifferenceBuild({
|
||||
...apiPacket, scoring_version: "birth-time-choice-scoring-v1",
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from datetime import date
|
||||
from typing import Final, TypedDict
|
||||
from typing import Final, Literal, TypedDict, assert_never
|
||||
|
||||
DIMENSION_CONTEXT: Final = {
|
||||
"education": "一次明显的升学、转学或学习方向变化",
|
||||
@@ -21,21 +21,28 @@ class DateRange(TypedDict):
|
||||
window_end: str
|
||||
|
||||
|
||||
def _range_label(item: DateRange, precision: str) -> str:
|
||||
RangePrecision = Literal["year", "month", "day"]
|
||||
|
||||
|
||||
def _range_label(item: DateRange, precision: RangePrecision) -> str:
|
||||
start = date.fromisoformat(item["window_start"])
|
||||
end = date.fromisoformat(item["window_end"])
|
||||
if precision == "year":
|
||||
return f"{start.year} 年" if start.year == end.year else f"{start.year}—{end.year} 年"
|
||||
if precision == "month":
|
||||
if start.year == end.year:
|
||||
return f"{start.year} 年 {start.month} 月—{end.month} 月"
|
||||
return f"{start.year} 年 {start.month} 月—{end.year} 年 {end.month} 月"
|
||||
if start.year == end.year and start.month == end.month:
|
||||
return f"{start.year} 年 {start.month} 月 {start.day} 日—{end.day} 日"
|
||||
return (
|
||||
f"{start.year} 年 {start.month} 月 {start.day} 日—"
|
||||
f"{end.year} 年 {end.month} 月 {end.day} 日"
|
||||
)
|
||||
match precision:
|
||||
case "year":
|
||||
return f"{start.year} 年" if start.year == end.year else f"{start.year}—{end.year} 年"
|
||||
case "month":
|
||||
if start.year == end.year:
|
||||
return f"{start.year} 年 {start.month} 月—{end.month} 月"
|
||||
return f"{start.year} 年 {start.month} 月—{end.year} 年 {end.month} 月"
|
||||
case "day":
|
||||
if start.year == end.year and start.month == end.month:
|
||||
return f"{start.year} 年 {start.month} 月 {start.day} 日—{end.day} 日"
|
||||
return (
|
||||
f"{start.year} 年 {start.month} 月 {start.day} 日—"
|
||||
f"{end.year} 年 {end.month} 月 {end.day} 日"
|
||||
)
|
||||
case unreachable:
|
||||
assert_never(unreachable)
|
||||
|
||||
|
||||
def visible_range_labels(items: Sequence[DateRange]) -> list[str]:
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.dynamic_rectification_opportunities import _dimension_opportunity
|
||||
@@ -44,7 +45,7 @@ def test_every_supported_dimension_emits_distinct_validator_safe_copy() -> None:
|
||||
assert re.search(r"[\u3400-\u9fff]", context)
|
||||
assert re.search(r"[A-Za-z]", context + prompt) is None
|
||||
assert prompt.endswith("?") and prompt.count("?") == 1
|
||||
assert len(labels) == len(set(label.replace(" ", "") for label in labels))
|
||||
assert len(labels) == len(set(re.sub(r"\s+", "", label) for label in labels))
|
||||
assert all(re.search(r"[\u3400-\u9fff]", label) for label in labels)
|
||||
|
||||
|
||||
@@ -78,7 +79,9 @@ def test_frontend_adapter_fixture_is_real_task2_opportunity_output() -> None:
|
||||
assert len(fixture["partitions"]) == len(opportunity["partitions"])
|
||||
assert re.search(r"[A-Za-z]", fixture["neutral_context"] + fixture["fallback_prompt"]) is None
|
||||
fixture_labels = [item["fallback_label"] for item in fixture["partitions"]]
|
||||
assert len(fixture_labels) == len(set(label.replace(" ", "") for label in fixture_labels))
|
||||
assert len(fixture_labels) == len(
|
||||
set(re.sub(r"\s+", "", label) for label in fixture_labels)
|
||||
)
|
||||
|
||||
|
||||
def test_same_year_windows_receive_distinct_visible_labels() -> None:
|
||||
@@ -99,8 +102,11 @@ def test_same_year_windows_receive_distinct_visible_labels() -> None:
|
||||
|
||||
assert opportunity is not None
|
||||
labels = [item["fallback_label"] for item in opportunity["partitions"]]
|
||||
assert len(labels) == len(set(labels))
|
||||
assert all("月" in label for label in labels)
|
||||
normalized = [
|
||||
re.sub(r"\s+", "", unicodedata.normalize("NFKC", label)) for label in labels
|
||||
]
|
||||
assert len(normalized) == len(set(normalized))
|
||||
assert all(len(re.findall(r"\d+", label)) >= 3 for label in labels)
|
||||
|
||||
|
||||
def test_same_month_windows_receive_distinct_day_precision_labels() -> None:
|
||||
@@ -121,5 +127,8 @@ def test_same_month_windows_receive_distinct_day_precision_labels() -> None:
|
||||
|
||||
assert opportunity is not None
|
||||
labels = [item["fallback_label"] for item in opportunity["partitions"]]
|
||||
assert len(labels) == len(set(labels))
|
||||
assert all("日" in label for label in labels)
|
||||
normalized = [
|
||||
re.sub(r"\s+", "", unicodedata.normalize("NFKC", label)) for label in labels
|
||||
]
|
||||
assert len(normalized) == len(set(normalized))
|
||||
assert all(len(re.findall(r"\d+", label)) >= 4 for label in labels)
|
||||
|
||||
Reference in New Issue
Block a user