Agent Skills › civitai/civitai › form-graph-port

form-graph-port

GitHub

指导将现有表单迁移至 form-graph 库,涵盖差异测试、状态映射与类型提取。通过构建 Oracle 验证器确保数据一致性,并规范分支声明与存储作用域设计。

.claude/skills/form-graph-port/SKILL.md civitai/civitai

Trigger Scenarios

需要迁移表单到 form-graph 重构表单逻辑以使用 form-graph

Install

npx skills add civitai/civitai --skill form-graph-port -g -y
More Options

Non-standard path

npx skills add https://github.com/civitai/civitai/tree/main/.claude/skills/form-graph-port -g -y

Use without installing

npx skills use civitai/civitai@form-graph-port

指定 Agent (Claude Code)

npx skills add civitai/civitai --skill form-graph-port -a claude-code -g -y

安装 repo 全部 skill

npx skills add civitai/civitai --all -g -y

预览 repo 内 skill

npx skills add civitai/civitai --list

SKILL.md

Frontmatter
{
    "name": "form-graph-port",
    "description": "Port an existing form (a data-graph graph, or a bespoke RHF+zod form like model training) to the form-graph library. Use when asked to move a form's field logic, branching, persistence, or server validation onto form-graph. Encodes the method proven by the generation-form port — oracle-first differential testing, scope mapping, staged cutover."
}

Porting a form to form-graph

The method that took the generation form (~45 family graphs, 4 output types, 7 standalone workflows, ~12k differential cases) onto form-graph, distilled so the next port (e.g. model training) doesn't rediscover it. The worked example is src/shared/form-graph/generation/ + docs/form-graph-port-plan.md; read the plan doc's phase structure before starting anything sizable.

0. Read the lib's own guidance first

C:\work\form-graph\CLAUDE.md carries the library's design invariants (one branch combinator, sync resolution, wire-named computedKeys, the prepack-after-every-edit rule for link: consumers). Don't design against an imagined API.

1. Identify the oracle, then build the harness FIRST

Nothing else starts until parity is measurable.

  • Oracle = whatever produces today's wire payload. For a data-graph form it's graph.safeParse. For a bespoke form (training: src/components/Training/Wizard + src/server/schema/training.schema.ts + the orchestrator validation) it's the submit payload builder — capture real input→payload fixtures if there's no parse function.
  • Differential = byte-identical wire. assertDifferential pattern: port parse vs oracle parse over generated cases, plus the parse-fixpoint pin (re-parse the port's own state → identical data; this is what makes whatIf/cost preview trustworthy).
  • Bound every generated-case driver — a fake that pages/loops must terminate on its own (see CLAUDE.md's microtask-loop warning; a hang is unreportable in vitest).
  • Divergences found by the harness are findings to record, not always bugs — v1 does have dead paths and quirks. Pin the deliberate deltas in a comment or the plan doc.

2. Structure: declare-then-dispatch

  • Discriminators are ordinary fields declared above the dispatch: .field('ecosystem', def) then .use(branch('ecosystem', [[keys, member], ...] as const)). Group related keys into one pair — arm count should equal family count, not key count.
  • State-only discriminators (never on the wire) are computeds with { emit: false } fed to the tagged branch(key, pick, members, { emit: false }) form.
  • Shared per-family plumbing goes in a shared.ts (familyScope, text-block factories, modelIdOf-style raw-or-parsed readers — store state holds RAW inputs, so anything reading ctx must accept both shapes).

3. Storage: map the old adapter groups to scopes

Translate the legacy storage-adapter groups (see the v1 createLocalStorageAdapter config in GenerationFormProvider.tsx for the pattern) into graph/field scope declarations: graph-level scope for family buckets, rootScope() to opt a field out to global memory, rootScope(workflow) for per-workflow buckets, relative [modelId] appends for per-variant refinements. One persisted record per form (persistedStorage('<key>')).

4. Types: extract, never re-declare

InferData / InferArm / InferLooseData from the graph type the handlers (EcosystemData<'X'> pattern in src/shared/form-graph/generation/types.ts). Zero as never; a residual cast marks a provably-dead path and says so. After type-level work, measure compiler cost against main (tsc --extendedDiagnostics, delete tsconfig.tsbuildinfo, NODE_OPTIONS=--max_old_space_size=12288 — default heap OOMs).

5. Stored-value migration (if old users' settings should survive)

Consumer-side module (migrate-v1-storage.ts is the template): read the old records, pick ONLY the fields worth carrying, build one address→raw-value record with scopedAddress, write it once iff the new key is absent. Values go in raw — the input schemas validate on first resolve, so stale garbage degrades to defaults. Never delete the old records while anything still reads them.

6. Cutover: ONE feature flag, always-on comparison

One feature flag (availability: ['mod'] first, widened via its Flipt key) gates the whole cutover per user: it swaps the form component on the client AND serves the port's parse on the server (read from the ctx the feature-flag system already threads — coerce it, a sparse record reads undefined). Every server parse runs BOTH engines regardless and records the comparison — outcomes counted (registerCounterWithLabels), divergence logged with diff keys only — never field values (user content must not reach logs; pin that with a test, one sentinel per emit path). Comparison noise is fine: it dies with the old engine.

Flag off must be byte-identical. The generation port briefly used three flags (separate shadow/serve Flipt switches) and collapsed them once the parity battery made independent server/client rollback unnecessary — start with one. Deleting the old engine is a separate change after the flag is fully widened.

Keeping parity during the dual-graph window

Until the old graph is deleted, EVERY merge from main needs: git diff HEAD...origin/main --stat -- src/shared/data-graph — then mirror each change into the port AND add a differential shape covering the changed path. The suites only catch drift where shapes exercise it: krea2's community-checkpoint fix (2026-09-03) passed parity under BOTH the old and new fallback because no shape used an unknown model id. A mirrored change without a new shape is unverified.

Gotchas that cost real time on the generation port

  • Cross-field coherence (a selection retargeting another selection) belongs in a RULE on the graph (.effect({...}) — gesture-aware, fires before resolution, covers every writer), NOT in transcribed v1 UI handlers. v1 kept it in handlers because data-graph had no rules; transcribing that architecture reintroduced a crash the graph could have prevented (see selector-coherence.ts).

  • The port parse composes as hub.parse(reconcileSelectors(raw).raw, ext) — selector reconciliation is part of the parse contract, not optional plumbing.

  • A generic helper over union arms hits TS's weak-type rule when one arm shares no properties — constrain T extends object and read loosely inside.

  • useForm must be typed to preserve the store's full type (Store extends FormStore<any, Ext, any, any>), or per-arm emits break DataOf≡Ctx at mounts.

  • After every form-graph lib edit: pnpm run prepack in the lib, or the link: consumer runs stale dist.

  • Pre-PR: publish the lib as ONE version, swap link: → ^x.y.z, remove any turbopack.root widening / tsconfig react paths pin added for the link, re-run the full battery.

Version History

  • c706944 Current 2026-09-23 10:05

Same Skill Collection

.claude/skills/add-ecosystem/SKILL.md
.claude/skills/add-generation-support/SKILL.md
.claude/skills/add-prompt-enhancement-guide/SKILL.md
.claude/skills/add-training-support/SKILL.md
.claude/skills/axiom/SKILL.md
.claude/skills/browser-automation/SKILL.md
.claude/skills/civitai-orchestration/SKILL.md
.claude/skills/civitai-review/SKILL.md
.claude/skills/cleanup/SKILL.md
.claude/skills/clickhouse-query/SKILL.md
.claude/skills/clickup/SKILL.md
.claude/skills/cloudflare/SKILL.md
.claude/skills/component-preview/SKILL.md
.claude/skills/deploy-status/SKILL.md
.claude/skills/dev-server/SKILL.md
.claude/skills/discord/SKILL.md
.claude/skills/feature-walkthrough/SKILL.md
.claude/skills/feedback-triage/SKILL.md
.claude/skills/flipt/SKILL.md
.claude/skills/freshdesk/SKILL.md
.claude/skills/generation-coverage/SKILL.md
.claude/skills/generation-gate-rules/SKILL.md
.claude/skills/generator-launch/SKILL.md
.claude/skills/listing-media/SKILL.md
.claude/skills/meilisearch-admin/SKILL.md
.claude/skills/metabase/SKILL.md
.claude/skills/mod-actions/SKILL.md
.claude/skills/moderator-page-migration/SKILL.md
.claude/skills/onboard-generator-model/SKILL.md
.claude/skills/postgres-query/SKILL.md
.claude/skills/quick-mockups/SKILL.md
.claude/skills/redis-inspect/SKILL.md
.claude/skills/retool-migration/SKILL.md
.claude/skills/retool-query/SKILL.md
.claude/skills/scaffold-civitai-app/SKILL.md
.claude/skills/stripe/SKILL.md
.claude/skills/svelte-review/SKILL.md
.claude/skills/ux-design/SKILL.md
.claude/skills/write-model-description/SKILL.md
.claude/skills/xguard-manager/SKILL.md
apps/event-engine/.claude/skills/agent-review/SKILL.md
apps/event-engine/.claude/skills/clickhouse-query/SKILL.md
apps/event-engine/.claude/skills/clickup/SKILL.md
apps/event-engine/.claude/skills/postgres-query/SKILL.md
apps/event-engine/.claude/skills/redis-inspect/SKILL.md
.claude/skills/add-trainer-model/SKILL.md
.claude/skills/app-capture/SKILL.md
.claude/skills/app-taste/SKILL.md
.claude/skills/ecosystem-seo-page/SKILL.md

Metadata

Files
0
Version
b735b6b
Hash
90a42a9e
Indexed
2026-09-23 10:05

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-29 02:00
浙ICP备14020137号-1