npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@form-engine-ts/core

v8.1.6

Published

Readme

@form-engine-ts/core

Framework-independent schemas, validation, submissions, analytics, CSV export, and adapter contracts for form-engine-ts.

Install

pnpm add @form-engine-ts/core

Quick start

import { type FormSchema, validateAnswers } from "@form-engine-ts/core";

const schema: FormSchema = {
  id: "contact",
  version: 1,
  title: "Contact",
  fields: [{ id: "name", type: "text", title: "Name", required: true }]
};

const result = validateAnswers(schema, { name: "Ada" });
if (!result.valid) console.error(result.issues);

AI authoring suggestions

Core keeps AI provider code outside the library. An application injects an AuthoringAssistantAdapter that returns operations, then previews and applies them only after policy and schema validation. Suggestions carry a deterministic baseSchemaHash; applying one after the form changed returns a stale-schema error. Added field and option IDs are created by the apply boundary, never by the provider.

import {
  applyAuthoringSuggestion,
  buildAuthoringContext,
  computeAuthoringSchemaHash,
  parseAuthoringSuggestion,
  previewAuthoringSuggestion,
  type AuthoringSuggestion
} from "@form-engine-ts/core";

const request = { intent: "add_questions", prompt } as const;
const suggestion: AuthoringSuggestion = parseAuthoringSuggestion(
  await adapter.generate({ ...request, context: buildAuthoringContext({ schema, request, policy }) })
);
const preview = previewAuthoringSuggestion(schema, suggestion, { policy });
// Re-preview a subset after the user unchecks an operation.
const selectedPreview = previewAuthoringSuggestion(schema, suggestion, { operationIds: ["question-1"], policy });
if (preview.valid) {
  const result = applyAuthoringSuggestion(schema, suggestion, ["question-1"], { policy });
  if (result.success) save(result.schema);
}

preview.operationPreviews contains operation-level validation plus before/after values for form, field, and option changes. Empty operationIds is a valid Core no-op; UI adapters disable Apply for that state. buildAuthoringContext returns only form metadata, content mode, policy limits, target-neighbor fields, and option labels, so answer data is not part of provider context. The React hook sends this bounded context and does not include the full schema in the provider request.

The MVP supports independent addField, updateField, updateForm, addOption, and updateOption operations. Removal, moving, pages, conditions, locale/translation, submission settings, and direct Azure/OpenAI SDK usage stay in the host application.

For a server-backed provider, keep credentials in the host server and inject a small HTTP adapter. The preview app includes createHttpAuthoringAssistantAdapter() as a client reference; the server endpoint should return an AuthoringSuggestion structured response and run parseAuthoringSuggestion() before returning it. Azure OpenAI, OpenAI, and other provider SDKs belong in that server endpoint or host application, not in Core.

Security responsibilities remain with the host application: do not put provider API keys in the browser, keep Azure OpenAI/OpenAI/Bedrock calls server-side, never let this package manage provider credentials, and always validate an AI response through previewAuthoringSuggestion/applyAuthoringSuggestion before persisting it.

Built-in templates

Core includes four purpose-oriented templates for survey, poll, and quiz creation. Select them by mode and locale, then pass the selected template to createSchemaFromTemplate with an application-issued ID and title:

import { createSchemaFromTemplate, getFormTemplates } from "@form-engine-ts/core";

const [template] = getFormTemplates({ mode: "survey", locale: "ja-JP" });
if (template) {
  const schema = createSchemaFromTemplate({ template, id: "feedback-2026", title: "春の満足度" });
}

Templates provide Japanese text for Japanese locales and English text otherwise. The returned templates and schemas are defensive copies, so changing one form does not change another. Core owns definitions and generation; the host application owns the new-form entry point, ID issuance, persistence, and navigation. Built-in open-text prompts ask respondents not to include personal information and do not promise a reply or a storage-specific anonymity guarantee. Use createInitialSchemaByMode when a blank form is preferred.

Selective imports

The root import remains supported. For smaller consumer module graphs, import a focused entry instead:

import { validateFormSchema } from "@form-engine-ts/core/schema";
import { validateSubmission } from "@form-engine-ts/core/validation";
import { EN_MESSAGES } from "@form-engine-ts/core/i18n/en";

Core also provides subpaths for analytics, aggregation, visibility, pipeline, submission, policy, translation, versioning, interaction comparison, optimization insights, and the other public top-level modules. Locale catalogs are available from @form-engine-ts/core/i18n/en and @form-engine-ts/core/i18n/ja without importing them from an application feature entry.

Multi-step, localization, analytics, and events

Interaction Telemetry

Interaction Telemetry is separate from submission analytics and records form behavior without answer content:

import { aggregateInteractionEvents, type FormTelemetryAdapter } from "@form-engine-ts/core";

const adapter: FormTelemetryAdapter = {
  track(event) {
    console.log(event);
  }
};

const analytics = aggregateInteractionEvents(events);

Events contain form, page, field, validation-code, timing, and funnel information, but do not include answer values, question labels, option labels, or validation messages. The adapter contract is vendor-neutral; the host application owns delivery and retention.

For explicitly defined pages, page.completed means that the page passed validation. The final page emits it before form.submitted or form.submit_failed; field duration prefers focus-to-completion and falls back to presented-to-completion.

Optimization Insights and Version Comparison

Optimization Insights are deterministic, metric-based diagnostics. They do not infer why respondents behaved a certain way and never include answer values, labels, question text, or validation messages.

import { aggregateInteractionEvents, analyzeInteractionAnalytics } from "@form-engine-ts/core";

const analytics = aggregateInteractionEvents(events);
const report = analyzeInteractionAnalytics(analytics, {
  minimumSamples: { field: 50 },
  thresholds: { lowFieldCompletionRate: 70, highValidationFailureRate: 20 }
});

The default minimum samples are 30 form-level observations, 20 page views, and 20 field presentations. Rate thresholds use percentages from 0 to 100. Defaults are 50% for low start rate, 50% for abandonment, 40% for page drop-off, 70% for field completion, 20% for validation friction, and 10% for submit failures. Page dropoffRate is defined as 100 - completionRate. Field-focus diagnostics are opt-in through lowFieldFocusRate because focus capture may be disabled or unavailable for custom renderers. Duration thresholds are opt-in milliseconds, so slow-completion insights are not emitted unless a duration threshold is supplied. Insight IDs are deterministic for a form version, type, scope, and target.

Compare two versions without assigning a success judgment:

import { compareInteractionAnalytics } from "@form-engine-ts/core";

const comparison = compareInteractionAnalytics(version3Analytics, version4Analytics);

Pages and fields are matched by ID. Added and removed entities are reported, rate changes are percentage-point deltas, and duration changes are millisecond deltas (after - before). The versions must belong to the same form ID.

Add pages to partition every field into an accessible wizard and use validatePageAnswers(schema, pageIndex, values) for step-scoped validation. Schemas without pages remain single-page forms.

Store authoring-time translations on forms, fields, options, and pages. resolveLocalizedSchema(schema, locale) applies them synchronously and returns the original schema when no locale is supplied, while populateSchemaTranslations fills them through an injected AsyncTranslationAdapter. Population defaults to overwrite: "missing-only", accepts per-slot shouldOverwrite and createMetadata callbacks, and returns { schema, report } with updated and skipped translation slots. The optional signal is forwarded to async adapters; continueOnError keeps successful slots and reports failed slots, cancellation, counts, and progress through TranslationReport and onProgress.

Forms, pages, fields, options, and submissions accept JSON-only metadata and per-locale/property translationMetadata. These extension values survive sanitization, localization, submission creation, and storage round-trips. completionMessage is localized with the rest of the form text.

transformFieldType changes a question's type without discarding source text, translations, conditions, or extension metadata. validateFormSchema(schema, { policy }) applies the framework-independent FormPolicy, including field, option, text, serialized-byte, allowed-type, locale, and per-field fieldConstraints rules. Rating bounds can be fixed or range-limited, text lengths can have a policy maximum, and required state can be fixed. allowedLocales constrains the default and supported locales, maxLocales limits their unique total, and contradictory required/allowed locale policies are reported. Required locales cover every source text that exists on the form, its fields, options, and pages.

collectSchemaLocales(schema) scans registrations plus every form/page/field/option translations and translationMetadata key. Validation reports unregistered translation locales and applies allowedLocales and maxLocales to the complete collected set. sanitizeSchema(schema, { policy }) also applies fixed field values and safe maximum-length corrections while purging unregistered locale content. Pass { policy: { allowedLocales, maxLocales } } to populateSchemaTranslations to reject inadmissible targets before the translation adapter runs. normalizeLocale(locale) returns the canonical BCP 47 tag, accepts underscore-separated compatibility input such as ja_JP, and returns null for invalid tags. Schema validation, sanitization, locale policy checks, and translation slot lookup use this same normalization so equivalent locale spellings cannot bypass constraints.

Submissions use values as their canonical answer property. FormSubmission<TMeta> carries application metadata types through creation and wire conversion. Applications that own storage contracts can use TypedFormStorageAdapter<TMeta> and TypedPagedSubmissionStorageAdapter<TMeta> to retain that type through persistence and pagination. toFormSubmissionWire and fromFormSubmissionWire preserve the optional submission locale across validated wire payloads. Use createFormSubmissionSchema({ metadata }) to generate a wire schema with application-owned metadata validation. Legacy payloads with answers must be migrated to the canonical values contract by the application. serializeSubmissionError and deserializeSubmissionError provide the JSON boundary for FormSubmissionError, while trpcSubmissionErrorAdapter handles tRPC data and shape.data boundaries. TrpcSubmissionErrorFormatter is structurally compatible with tRPC's standard formatter input; getTrpcSubmissionErrorData restores typed fieldErrors, formErrors, and PII payloads without an application cast. Use createTrpcSubmissionErrorIntegration() to obtain a server errorFormatter and matching client deserialize and getData functions without manually spreading a payload into tRPC data. runSubmissionPipeline and createSubmissionPipeline combine codec normalization, schema validation, PII confirmation, idempotent saving, and typed FormSubmissionError results.

createSubmissionId("ulid") and createSubmission's idFormat option use the same generator used by React's createSubmissionIdentity. Pass one identity object to a Controller and Renderer to share idFormat, scope, attemptStore, receiptStore, and the generated submission/attempt ID.

Fields can use a displayRule with nested all/any condition groups and show or hide actions. Supported operators include equality, containment, emptiness, and numeric comparisons; the legacy displayCondition and not_empty forms remain supported. submissionSettings can enable pre-submit confirmation and select its dialog, inline, or replace presentation.

Core supports text, textarea, number, rating, date, time, email, tel, url, select, radio, multi-select, and checkbox fields. Date/time fields use minDate/maxDate or minTime/maxTime; typed string fields receive format validation and native input semantics in the React renderer. Choice fields support deterministic shuffleOptions ordering, with FieldOption.pinned preserving selected positions.

For survey radio fields, set textInput: true on an option to accept an optional supplement. The submitted value is { optionId, text }; the legacy option ID string remains valid when no supplement is entered. Other field types and poll/quiz modes reject this setting.

submissionSettings supports openAt, closeAt, maxResponses, custom closed/not-yet-open messages, and honeypotFieldId. getFormAcceptanceStatus provides a pure decision, while the submission pipeline rechecks acceptance before saving. Storage adapters must implement countSubmissions(formId, formVersion?, { since, until }?); the pipeline uses that count directly instead of scanning submission pages. Core also exports vendor-neutral honeypot, challenge, and rate-limit guards.

collectTranslationSlots, computeSourceTextHash, and getTranslationStatus expose canonical translation targets and missing, translated, stale, or manual states for authoring tools. populateSchemaTranslations can populate stale and missing entries while preserving manual translations and reports skipped reasons.

Legacy translation metadata can be recognized with isManualTranslationMetadata and migrated with migrateSchemaTranslationMetadata; pass a custom migrator (directly or as { migrator }) when legacy fields need application-specific conversion. The migrator receives TranslationMigrationContext with the locale, JSON path, property, node kind, and node identifiers. PopulateTranslationsOptions is a compatibility alias for PopulateTranslationOptions, which also accepts custom manual-translation detection and metadata normalization callbacks. removeLocaleFromSchema(schema, locale) removes the locale from registrations and all form, page, field, option translation values and metadata; the default locale cannot be removed.

The official catalogs include JA_COMPARISON_MESSAGES for comparison-workspace labels. The Core translator returns an empty string for unresolved keys, including when a fallback resolver returns the key itself, so internal dotted keys are not rendered as user-facing labels.

The React builder uses canonical keys with legacy aliases when resolving UI translations:

| Canonical key | Legacy alias | | --- | --- | | builder.fields.typeText | builder.fieldType.text | | builder.fields.typeTextarea | builder.fieldType.textarea | | builder.fields.typeNumber | builder.fieldType.number | | builder.fields.typeRating | builder.fieldType.rating | | builder.fields.typeRadio | builder.fieldType.radio | | builder.fields.typeCheckbox | builder.fieldType.checkbox | | builder.fields.typeSelect | builder.fieldType.select | | builder.fields.typeMultiSelect | builder.fieldType.multi-select |

Adapters should return undefined or null for missing keys so the builder can try aliases and its default catalog. For example, an i18next adapter can avoid treating an unresolved key as translated:

const i18nextAdapter: TranslationAdapter = {
  translate: (key, locale, params) => {
    if (!i18n.exists(key, { lng: locale })) return undefined;
    return i18n.t(key, { ...params, lng: locale });
  }
};

Core also exports the fully typed FormEngineTranslationKey union, JA_MESSAGES, EN_MESSAGES, and createFormEngineTranslator. The built-in translator defaults to Japanese, falls back to English, accepts partial custom catalogs, formats {{placeholder}} values, and returns an empty string for an unresolved key instead of exposing the key itself.

Translation callbacks receive nodeMetadata and existingTranslationMetadata separately. The deprecated metadata slot property remains an alias for nodeMetadata during migration.

calculateCrossTabulation builds a two-question frequency matrix from submissions. dispatchWebhook posts typed response.submitted or schema.updated events with timeout handling, custom headers, and optional HMAC-SHA256 signing.

CSV export neutralizes string cells whose first non-whitespace character is =, +, -, or @. This is enabled by default; trusted callers can pass { neutralizeFormulas: false }. RFC 4180 quoting and the UTF-8 BOM remain unchanged. The columns are exactly submissionId, submittedAt, locale, followed by one column per field in schema order.

Storage adapters share inclusive ISO 8601 submission-range filtering:

import type { SubmissionQueryOptions } from "@form-engine-ts/core";

const range: SubmissionQueryOptions = {
  since: "2026-01-01T00:00:00.000Z",
  until: "2026-01-31T23:59:59.999Z"
};
const submissions = await storage.listSubmissions("contact", 1, range);

Results are ordered by submittedAt, then submission ID. Both boundaries are inclusive.

Submission metadata can be strongly typed at the application boundary:

import { createSubmission, validateSubmission } from "@form-engine-ts/core";

const submission = createSubmission<{ deckId: string; piiConfirmed: boolean }>({
  formId: "guide",
  formVersion: 1,
  answers: { title: "Welcome" },
  metadata: { deckId: "deck_123", piiConfirmed: false }
});
submission.metadata.deckId;
const validation = validateSubmission(schema, submission, { privacyEngine });

validateSubmission returns one serializable result containing field errors, form errors, and optional PII findings.

Versioning, incremental analytics, and paged storage

cloneVersionToDraft, asynchronous publishDraft, and deleteDraft implement revision-checked version transitions as pure functions. createCloneTransitionPlan, createPublishTransitionPlan, and createDeleteDraftTransitionPlan produce complete persistence plans with the next state, affected records, and immutable audit events. Publishing when state already identifies a Published version requires its matching currentPublishedRecord; omission or a version mismatch returns the typed missing_published_record error. The resulting archive preserves the original schema, creation timestamp, and metadata. Clone/delete operations accept expectedRevision; cloning rejects non-published sources, publish validation failures are returned as typed validation_failed issues, and successful publishing archives only a supplied actual published record, preserving its schema and metadata. createPublishTransitionPlan returns complete records plus expected/next revisions for storage adapters implementing VersionedFormStorageAdapter to commit atomically. Versioned adapters expose state/record reads and return a typed Result from commitVersionTransition, including the actual revision on concurrency conflicts. createResponseAccumulator incrementally counts choices, answered/unanswered values, and numeric summaries without retaining free-text bodies. In lenient mode, mismatched responses are skipped and exposed by addMany() and getReport() instead of being included silently. Independent accumulators for the same schema can be merged, and finalize() matches aggregateResponses.

exportResponsesToCsvStream accepts synchronous or asynchronous submissions and returns a byte ReadableStream that also remains async-iterable for compatibility. It emits the BOM/header and one chunk per response, and supports metadata-typed CsvColumnDefinition<TMeta> columns. Custom getters may be asynchronous and receive the submission, form version, and schema. Use pipeResponsesToCsvStream to write to a Web WritableStream or Node-compatible writable while honoring backpressure. Formula-injection neutralization applies to both default and custom columns.

Adapters implementing PagedSubmissionStorageAdapter expose listSubmissionPage(formId, options). The opaque Base64 cursor combines submittedAt and response ID, so equal timestamps do not produce gaps or duplicates. metadataFilters and filter are applied before page sizing. filter accepts a composable eq/in/range/exists and/or AST; adapters may push supported nodes to their native query language while preserving identical client-side semantics. Adapters that implement listTextAnswerPage expose stable cursor pagination over individual text answers. TextAnswerPageQueryOptions.fieldIds can select multiple free-text fields for item-level paging.

The paginateWithFilter helper in @form-engine-ts/storage continues fetching native pages until pageSize matching items are collected or the source is exhausted. totalScannedCount makes post-filter work observable and maxScanPages bounds low-density scans.

commitVersionTransition accepts a typed domainData value and carries it through optional before/after hooks and the persistence adapter without interpreting or modifying it.

iterateSubmissionPages(adapter, formId, query, options) safely traverses every page as an async generator. It supports pageSize, maxItems, and AbortSignal, continues through empty pages with a next cursor, and rejects missing or cyclic cursors instead of looping indefinitely. Publish transition validation also rejects mismatched form IDs, non-Published records, and an unexpected current record with typed errors.

See the project documentation for the complete schema and API guide.

Survey, poll and quiz content modes

FormContentMode, CustomFormMetadata, PollMetadata, QuizMetadata and QuizFieldMetadata are optional metadata contracts. getFormContentMode(metadata) returns survey for absent or unrecognized modes. validateFormSchema now also enforces mode-specific structure: polls have at least one radio/multi-select question with at least two options, and every quiz choice question must reference an existing correct option. getContentModeDiagnostics(schema) returns stable issue codes for localized UIs; validateContentMode(schema) retains its path/message result for editor and respondent diagnostics.

import { createInitialSchemaByMode, getContentModePolicy } from "@form-engine-ts/core";

const schema = createInitialSchemaByMode("poll", {
  id: "lunch-vote", title: "Lunch", locale: "en"
});
const policy = getContentModePolicy("poll", { maxOptionsPerField: 8 });

Presets are deterministic: version 1, default ID form-draft (supply a unique ID before persistence), no survey questions, or one required radio question with two options for poll/quiz. An empty survey is an editing draft and still fails the base validator until a question is added. Quiz presets include option-1 as a valid correct answer so they can be validated and rendered immediately. Poll allows radio / multi-select; the default poll has no maximum question count. quiz allows radio. Mode policy intersects allowed types and never raises a host limit. React's low-level builder applies it through policy; MuiFormBuilder applies it automatically for poll/quiz unless contentModeOptions.applyPolicy is false.

Use FormPolicy.contentMode to configure the same limits for validation, diagnostics, Builder and respondent evaluation. For example, getContentModePolicy("poll", { contentMode: { maxFields: 10, minOptionsPerField: 2 } }) produces the effective policy. Quiz formats that are not radio can provide contentMode.evaluateQuiz; the evaluator result is checked at the boundary before it is used.

mapField, mapOption, mapPage, mapSchema and createSchemaDomainCodec preserve unknown JSON properties, page/condition references and typed metadata while validating both conversion boundaries. aggregateForms adds locale, content-mode, metadata and optional quiz score groups without changing aggregateResponses.

Use TypedFormSchema<TMetadata, TTranslationMetadata> when an application owns metadata types. TypedFormField<TMetadata, TTranslationMetadata> and TypedFieldOption<TMetadata, TTranslationMetadata> provide the same types at field and choice boundaries; the legacy FormSchema shape remains available without type arguments.

contentMetadataToJson copies JSON metadata and rejects undefined, non-finite, cyclic and non-JSON data. readPollMetadata, readQuizMetadata and readQuizFieldMetadata provide typed reads; unknown metadata stays in the original schema. Store correct answers at field.metadata.quiz.correctOptionId. ResponseSummaryData and related neutral contracts describe display-ready analytics. toResponseSummary(summary, schemaOrVersion, locale) resolves localized form, question and option labels without a React or application-domain dependency. evaluateQuizLocally(schema, answers) rejects invalid quizzes and returns the transport-neutral QuizEvaluationResult used by respondent renderers. It scores visible questions, includes optional explanations and rewards supplied by a server. evaluateQuiz(schema, answers) remains a legacy-shaped adapter over the local result.

AI survey creation

SurveyCreationBrief and CreationAssistantAdapter provide a provider-neutral conversation layer for new survey creation. The adapter returns a parsed clarification or ready response; use evaluateCreationBriefReadiness and canGenerateCreationDraft to keep the conversation bounded. Pass the resulting brief through AuthoringRequest.context to the existing generate_form authoring flow. The authoring parser, preview, FormPolicy, and applyAuthoringSuggestion remain the only path to a FormSchema; AI responses never supply persistent field or option IDs.

The host application owns provider credentials. A minimal HTTP adapter keeps that boundary explicit:

const creationAdapter: CreationAssistantAdapter = {
  respond: async (request, signal) => {
    const response = await fetch("/api/form-assistant/conversation", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(request),
      signal
    });
    return response.json();
  }
};

The HTTP endpoint should parse and validate the provider result before returning it:

const response = parseCreationAssistantResponse(await provider.respond(request));
return Response.json(response);

Creation Assistant only organizes the SurveyCreationBrief; the existing authoring adapter remains responsible for generate_form, and parseAuthoringSuggestion/preview/apply plus FormPolicy are still the schema boundary. Keep submissions, answers, analytics, and provider credentials out of both adapter requests.

既存のauthoring adapterを置き換えず、SurveyCreationBriefを会話の構造化状態として使います。 providerのAPIキーはfrontendやCoreへ置かず、ホストアプリのbackendで管理してください。 Both reject invalid quizzes, score visible questions, uses 1 point by default and 0 for unanswered questions, and returns optional passed when passingScore is configured. Scores and thresholds are finite, non-negative numbers; the threshold cannot exceed the sum of configured points. Hidden questions are excluded from the earned and available score; the configured passing threshold is absolute and is not adjusted for visibility. When passingScore is omitted, callers can use the score for internal evaluation while respondent-facing result components should omit total-score and pass/fail summaries.

canShowPollResults(poll, { submitted, closed, canViewResults }) implements all four policies. private never exposes results to respondents, and authorization gates every policy. PollRuntimeAdapter<TSummary> injects result loading (with AbortSignal) and vote eligibility. The host must enforce authorization and atomic one-vote persistence; metadata and client-side checks alone are not server enforcement. Quiz answers are intentionally delivered to the browser for learning/entertainment.