Frontend drops 15 files no page reaches (issue #139, BUG-1480)

Unreachable from every app entry and frontend/scripts (dynamic imports
counted): chart-basics-tab, error-toast, seven unused shadcn ui files
(skeleton among them), three birth-time helpers only tests used, the motion
provider shell (it imported motion/react statically), and the two empty
people/account stores. Tests that only exercised them are removed or trimmed,
each with old/new/reason notes. Unreachable files 18 -> 3 (all kept on
purpose); npm test names drop exactly the 4 removed and 1 renamed test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jesse_Chen
2026-10-28 12:43:07 +08:00
co-authored by Claude Opus 5.5
parent b9a5415e71
commit 2fbce95536
23 changed files with 31 additions and 780 deletions
+5
View File
@@ -1,5 +1,10 @@
# 印度占星 Skill 更新日志
## 2026-10-28 — 前端删掉 15 个没人用的文件
- 删掉 15 个没有任何页面用到的前端文件(约 700 行),其中包括一个会把动效库带回首屏的旧外壳和一个与「不出现骨架屏」规定冲突的组件。用户看到的内容不变。
- Skill 版本不变(BUG-1480,issue #139)。
## 2026-10-28 — VedAstro 限流或不可用时,不再每轮重等
- VedAstro 被限流或暂时拿不到数据时,同一张盘当天只问一次;之后的提问直接说明「外部校验暂不可用」,不再每轮多等 0.4~1.5 秒。第二天会重新问。
+15
View File
@@ -20203,3 +20203,18 @@
- 关联:BUG-734(健康探测走缓存)、BUG-727 / BUG-301(前台快照)。
- 复发自:无
- 修复版本:分支 `codex/vedastro-negative-day-20261028`。
## BUG-1480 | 前端留着 15 个没有任何页面用到的文件
- 状态:resolved(代码 + 回归测试;待部署)
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检第 24 条 → #139)
- 影响面:`frontend/src/`;不影响任何页面(这些文件从任何入口都不可达)。
- 现象:从所有页面、布局、接口、错误页入口和 `frontend/scripts/` 出发(计入动态 import),有 18 个文件不可达。其中 `lib/motion/provider.tsx` 静态引用 `motion/react`,将来谁一引用 Motion 就回到首屏;`ui/skeleton` 与 DESIGN「揭幕后不得出现骨架」冲突,留着容易被误用。
- 根因:改版和组件库生成后留下的残余,只被测试引用或没人引用。
- 修复:删除 15 个:`components/chart-page/chart-basics-tab.tsx`、`components/error-toast.tsx`、`components/ui/{avatar,empty,field,kbd,separator,skeleton,switch}.tsx`、`lib/birth-time-choice-question-model.ts`、`lib/birth-time-guided-terminal.ts`、`lib/birth-time-guided-turn-identity.ts`、`lib/motion/provider.tsx`、`stores/account-store.ts`、`stores/people-store.ts`。保留 `app/forbidden.tsx`(Next 的特殊入口文件)以及进度记录里写过理由的 `reader-appendix-language.ts`、`personal-report-longform-download.ts`。删前对每个文件用 `git grep -- tests/ frontend/` 核对,Python 源码契约测试里没有引用。
- 测试变化(只少被删模块的用例):删除 `birth-time-guide-flow.test.ts`(1 条,只测 `guidedTurnIdentity`);`birth-time-choice-question.test.ts` 删 2 条(click intent submits primary and unknown options immediately、unmatched requests clarification without creating scoring evidence);`birth-time-guided-review-fixes.test.ts` 删 1 条(dynamic medium terminal preserves its candidate range without direct adoption),另 1 条去掉 `guidedTerminalPath` 断言;`birth-time-rectification-contract.test.ts` 1 条去掉 `guidedTerminalPath` 断言;`ui-kit-stores.test.ts` 去掉两个空骨架 store 的断言(Home 不引用它们的断言保留);`motion-presets.test.ts`「LazyMotion loads domAnimation asynchronously and honors reduced motion」改名为「the home and the layout never import motion or a motion shell statically」,只留首页与 layout 的静态引用约束。改动处都写了原值 / 新值 / 原因。
- 验证:不可达文件 18 → 3(上面保留的 3 个)。全量前端测试与 staging 逐条比对:少的名字正好是上面 4 条删除和 1 条改名,没有新增失败。tsc 0、lint 0 error。删的都是不可达文件,打包内容不变。
- 防复发:删除前跑可达性检查并 `git grep -- tests/ frontend/`;只被测试引用的模块连同测试一起删。
- 关联:前端死代码清理(frontend-dead-code 进度记录)。
- 复发自:无
- 修复版本:分支 `codex/frontend-dead-files-20261028`。
@@ -1,20 +0,0 @@
import type { ChartViewOk } from "@/lib/chart-view-contract";
import { formatDegree } from "./chart-format";
export function ChartBasicsTab({ view }: { view: ChartViewOk }) {
return (
<div className="chart-page-basics">
<div className="chart-page-basics-grid">
{view.vedic.planets.map((planet) => (
<article key={planet.name} className="chart-page-planet-card">
<h3>{planet.label}</h3>
<p>本命 {planet.signLabel} {formatDegree(planet.degreeInSign)} · 第{planet.house}宫</p>
<p>{planet.nakshatra} 第{planet.pada}足 · 星宿主 {planet.nakshatraLord}</p>
<p>D9 {planet.d9SignLabel ?? "未返回"}</p>
{planet.symbol ? <p className="chart-page-symbol">{planet.symbol}</p> : null}
</article>
))}
</div>
</div>
);
}
-17
View File
@@ -1,17 +0,0 @@
"use client";
import { useEffect } from "react";
import { notifyError } from "@/lib/notify";
/**
* An error a flow keeps in state, said once as a toast whenever it changes to
* a new message (2026-09-30: no inline error bars). The message is the toast's
* id, so a remount repeats nothing on screen. Renders nothing.
*/
export function ErrorToast({ message }: Readonly<{ message: string | null | undefined }>) {
useEffect(() => {
if (message) notifyError(message, { id: `error:${message}` });
}, [message]);
return null;
}
-108
View File
@@ -1,108 +0,0 @@
"use client"
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: AvatarPrimitive.Root.Props & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}
-103
View File
@@ -1,103 +0,0 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn("flex max-w-sm flex-col items-center gap-2", className)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn(
"text-sm font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}
-238
View File
@@ -1,238 +0,0 @@
"use client"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-sm font-normal text-destructive", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
-26
View File
@@ -1,26 +0,0 @@
import { cn } from "@/lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }
-24
View File
@@ -1,24 +0,0 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
-13
View File
@@ -1,13 +0,0 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }
-31
View File
@@ -1,31 +0,0 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
@@ -1,50 +0,0 @@
import type {
PublicDynamicChoiceQuestion,
TimeRange,
} from "./birth-time-dynamic-choice.ts";
type ChoiceOption = PublicDynamicChoiceQuestion["options"][number];
export type ChoiceQuestionGroups = {
readonly primary: readonly ChoiceOption[];
readonly unknown: ChoiceOption;
readonly unmatched: ChoiceOption;
};
export type ChoiceSelectionIntent = {
readonly kind: "submit";
readonly optionId: string;
readonly effective: boolean;
};
export class ChoiceQuestionModelError extends Error {
readonly name = "ChoiceQuestionModelError";
}
export function choiceQuestionGroups(
question: PublicDynamicChoiceQuestion,
): ChoiceQuestionGroups {
const primary = question.options.filter((option) => option.kind === "primary");
const unknown = question.options.find((option) => option.kind === "unknown");
const unmatched = question.options.find((option) => option.kind === "unmatched");
if (!unknown || !unmatched) {
throw new ChoiceQuestionModelError("Dynamic question is missing a reserved choice");
}
return { primary, unknown, unmatched };
}
export function choiceSelectionIntent(option: ChoiceOption): ChoiceSelectionIntent {
return {
kind: "submit",
optionId: option.optionId,
effective: option.kind === "primary",
};
}
export function normalizeUnmatchedNote(note: string): string {
return Array.from(note.trim()).slice(0, 240).join("");
}
export function rangeLabel(range: TimeRange): string {
return `${range.startTime}—${range.endTime}`;
}
@@ -1,29 +0,0 @@
import type { JourneyClientResponse } from "./birth-time-journey-response-schema.ts";
export type GuidedTerminalPath =
| {
readonly kind: "edit_birth_time_details";
readonly preservesCase: true;
readonly appliesCandidateTime: false;
}
| {
readonly kind: "complete_with_candidate";
readonly time: string;
readonly preservesCase: true;
readonly appliesCandidateTime: true;
};
export function guidedTerminalPath(journey: JourneyClientResponse): GuidedTerminalPath | null {
const kind = journey.nextAction.kind;
if (journey.journeyProtocol === "dynamic-choice-v2"
&& (kind === "present_low_result" || kind === "present_medium_result" || kind === "candidate_saved")) {
return {
kind: "edit_birth_time_details",
preservesCase: true,
appliesCandidateTime: false,
};
}
return kind === "present_low_result" || kind === "candidate_saved"
? { kind: "edit_birth_time_details", preservesCase: true, appliesCandidateTime: false }
: null;
}
@@ -1,3 +0,0 @@
export function guidedTurnIdentity(turnVersion: number, questionId: string): string {
return `${turnVersion}:${questionId}`;
}
-16
View File
@@ -1,16 +0,0 @@
"use client";
import { LazyMotion, MotionConfig } from "motion/react";
import type { ReactNode } from "react";
const loadDomAnimation = () =>
import("motion/react").then((module) => module.domAnimation);
/** Not mounted in K0. Callers use `m` from motion/react inside this tree. */
export function MotionProvider({ children }: { children: ReactNode }) {
return (
<LazyMotion features={loadDomAnimation} strict>
<MotionConfig reducedMotion="user">{children}</MotionConfig>
</LazyMotion>
);
}
-12
View File
@@ -1,12 +0,0 @@
"use client";
import { create } from "zustand";
/** Skeleton. Account state moves here in a later issue, not in K0. */
type AccountState = {
placeholder: true;
};
export const useAccountStore = create<AccountState>(() => ({
placeholder: true,
}));
-12
View File
@@ -1,12 +0,0 @@
"use client";
import { create } from "zustand";
/** Skeleton. People-archive state moves here in a later issue, not in K0. */
type PeopleState = {
placeholder: true;
};
export const usePeopleStore = create<PeopleState>(() => ({
placeholder: true,
}));
@@ -1,10 +1,5 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
choiceQuestionGroups,
choiceSelectionIntent,
normalizeUnmatchedNote,
} from "../src/lib/birth-time-choice-question-model.ts";
import type { PublicDynamicChoiceQuestion } from "../src/lib/birth-time-dynamic-choice.ts";
import {
advanceDynamicBirthTimePreview,
@@ -22,28 +17,6 @@ const question: PublicDynamicChoiceQuestion = {
],
};
test("click intent submits primary and unknown options immediately", () => {
const groups = choiceQuestionGroups(question);
assert.deepEqual(groups.primary.map((option) => option.optionId), ["early", "late"]);
assert.deepEqual(choiceSelectionIntent(groups.primary[0]), {
kind: "submit", optionId: "early", effective: true,
});
assert.deepEqual(choiceSelectionIntent(groups.unknown), {
kind: "submit", optionId: "unknown", effective: false,
});
});
test("unmatched requests clarification without creating scoring evidence", () => {
const groups = choiceQuestionGroups(question);
assert.deepEqual(choiceSelectionIntent(groups.unmatched), {
kind: "submit", optionId: "unmatched", effective: false,
});
assert.equal(normalizeUnmatchedNote(" 大约是在搬家之后 "), "大约是在搬家之后");
assert.equal(normalizeUnmatchedNote("甲".repeat(260)).length, 240);
});
test("preview choices expose real local interaction states without authentication", () => {
const start = dynamicBirthTimePreview();
const nextQuestion = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "earlier" });
@@ -1,11 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import { guidedTurnIdentity } from "../src/lib/birth-time-guided-turn-identity.ts";
test("persisted questions receive stable identities without leaking prior input", () => {
assert.equal(guidedTurnIdentity(3, "education_entry"), "3:education_entry");
assert.notEqual(
guidedTurnIdentity(3, "education_entry"),
guidedTurnIdentity(4, "relationship_entry"),
);
});
@@ -7,7 +7,6 @@ import {
scheduleCancellableStart,
} from "../src/lib/birth-time-guided-effect-coordinator.ts";
import { confirmReviewedBirthTimeDraft } from "../src/lib/birth-time-guided-draft-confirmation.ts";
import { guidedTerminalPath } from "../src/lib/birth-time-guided-terminal.ts";
import { parseJourneyResponse } from "../src/lib/birth-time-journey-client.ts";
import { dynamicBirthTimePreview } from "../src/lib/birth-time-dynamic-preview.ts";
import { guidedBirthTimePreview } from "../src/lib/birth-time-guided-preview.ts";
@@ -45,26 +44,11 @@ test("low without a result and saved medium both return to declared-time editing
const saved = guidedBirthTimePreview("birth-time-rectification-saved");
assert.equal(dynamicLow.candidateResult, null);
assert.deepEqual(guidedTerminalPath(nullResultLow), {
kind: "edit_birth_time_details",
preservesCase: true,
appliesCandidateTime: false,
});
assert.deepEqual(guidedTerminalPath(saved), {
kind: "edit_birth_time_details",
preservesCase: true,
appliesCandidateTime: false,
});
});
test("dynamic medium terminal preserves its candidate range without direct adoption", () => {
const medium = dynamicBirthTimePreview("medium");
assert.deepEqual(guidedTerminalPath(medium), {
kind: "edit_birth_time_details",
preservesCase: true,
appliesCandidateTime: false,
});
// 原值:另断言 guidedTerminalPath(nullResultLow / saved) 都回到改出生时间
// 新值:只断言这两种状态仍能解析
// 原因:issue #139 guidedTerminalPath 只被测试引用,产品里没有调用方,已删除
assert.equal(nullResultLow.nextAction.kind, "present_low_result");
assert.ok(saved.snapshot);
});
test("candidate completion hook cannot invoke the legacy endpoint for dynamic turns", () => {
@@ -4,7 +4,6 @@ import {
guidedBirthTimePreview,
isGuidedBirthTimePreview,
} from "../src/lib/birth-time-guided-preview.ts";
import { guidedTerminalPath } from "../src/lib/birth-time-guided-terminal.ts";
/* Legacy pre-dynamic-choice contract assertions intentionally omitted. */
/*
@@ -74,9 +73,5 @@ test("low-confidence preview mirrors the persisted dynamic terminal state", () =
assert.ok(low.candidateResult);
assert.equal(low.candidateResult.winningSegment?.representativeTime, "05:21");
assert.equal(low.nextAction.resultId, low.candidateResult.resultId);
assert.deepEqual(guidedTerminalPath(low), {
kind: "edit_birth_time_details",
preservesCase: true,
appliesCandidateTime: false,
});
// 原值:另断言 guidedTerminalPath(low) 回到改出生时间;原因:issue #139 该函数只被测试引用,已删除
});
+4 -9
View File
@@ -3,9 +3,7 @@ import { readFileSync } from "node:fs";
import test from "node:test";
import { easeOut, easeSpring, motionPresets } from "../src/lib/motion/presets.ts";
import { MotionProvider } from "../src/lib/motion/provider.tsx";
const providerSource = readFileSync(new URL("../src/lib/motion/provider.tsx", import.meta.url), "utf8");
const pageSource = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8");
const layoutSource = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
@@ -66,13 +64,10 @@ test("every site-motion row is a preset and nothing else is invented", () => {
}
});
test("LazyMotion loads domAnimation asynchronously and honors reduced motion", () => {
assert.equal(typeof MotionProvider, "function");
assert.match(providerSource, /LazyMotion/);
assert.match(providerSource, /MotionConfig/);
assert.match(providerSource, /reducedMotion="user"/);
assert.match(providerSource, /import\("motion\/react"\)\.then\(\(module\) => module\.domAnimation\)/);
assert.doesNotMatch(providerSource, /domMax/);
test("the home and the layout never import motion or a motion shell statically", () => {
// 原值:本条还断言 lib/motion/provider.tsx(LazyMotion 外壳)的写法
// 新值:该外壳没有任何产品引用,issue #139 已删除;只留首页与 layout 的静态引用约束
// 原因:留着它,将来一引用 Motion 就回到首屏
// 原值:首页不得引用 motion/react、动效外壳和任何一个 store。
// 新值:首页仍不得静态引用 motion 或动效外壳;可以引用会话 store,不得引用另外三个。
// 原因:K2 把对话状态放进会话 store。动效仍按需加载,不进首页同步包。
+1 -4
View File
@@ -2,8 +2,6 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { useAccountStore } from "../src/stores/account-store.ts";
import { usePeopleStore } from "../src/stores/people-store.ts";
import { useSessionStore } from "../src/stores/session-store.ts";
import { useUiStore } from "../src/stores/ui-store.ts";
@@ -15,8 +13,7 @@ test("K0 store skeletons exist and Home() does not move state into them", () =>
// 原因:K2 把对话状态放进会话 store,Home() 的 useState 从 33 降到 25。另外三个还没接上。
assert.equal(useSessionStore.getState().placeholder, true);
assert.equal(typeof useSessionStore.getState().setSessions, "function");
assert.equal(usePeopleStore.getState().placeholder, true);
assert.equal(useAccountStore.getState().placeholder, true);
// 原值:人物、账户两个 store 也是空骨架;新值:两者从未接上,issue #139 已删除
assert.equal(useUiStore.getState().placeholder, true);
assert.match(home, /useSessionStore/);
assert.doesNotMatch(home, /usePeopleStore|useAccountStore|useUiStore/);