Skip to content

feat(forms): create draft content entries from form submissions - #2099

Open
rmakk wants to merge 10 commits into
emdash-cms:mainfrom
rmakk:forms-content-mapping
Open

rmakk wants to merge 10 commits into
emdash-cms:mainfrom
rmakk:forms-content-mapping

Conversation

@rmakk

@rmakk rmakk commented Jul 17, 2026 •

Copy link
Copy Markdown

What does this PR do?

Adds an optional contentMapping setting to the forms plugin. When a form has one, each successful submission also creates a draft entry in a chosen collection. The submission is still stored in the forms inbox as it is today, so the mapping only adds to what a submission does.

Feature Discussion: #1672

contentMapping?: {
  collection: string;                 // target collection slug, e.g. "events"
  fieldMappings: Record<string, string | {
    field: string;
    transform?: "portableText" | "string" | "number" | "date";
  }>;
  metadata?: Record<string, unknown>; // fixed values on every entry, e.g. { source: "form" }
};

Drafts only. Entries are always created as drafts. @masonjames suggested defaulting to drafts, or only allowing them. This PR only allows drafts, since anonymous visitors should not be able to publish content. A status option can be added later if there is a need for one.

Checked on save. Creating, updating or duplicating a form checks its mapping against the target collection. The collection and every mapped field must exist, and metadata keys must be fields in the collection. Each form field, with its transform, must produce a value the target field accepts, so a text field cannot feed a number field without the number transform, and a select field's options must all be options of the target. A field cannot be set by both a mapping and metadata, and file fields cannot be mapped. As @MA2153 asked, every required field in the collection must be covered by a non-empty metadata value or a required form field that is always shown. An edit to a form's pages that would break its mapping is rejected as well.

Submissions are never lost. The entry is created after the submission is stored, in after() like the webhook, so the visitor gets their confirmation without waiting for it. If creating the entry fails, the error is logged.

Slugs. Entries get a unique slug from their title or name field, the same way entries created in the admin do. An earlier version of this PR added a slugFrom option and changes to the plugin content API in core. Those are gone: main's plugin create path already generates slugs, so this PR no longer changes emdash core.

Capabilities. The plugin now declares content:write and schema:read. It uses them only for forms that have a contentMapping, to look up the target collection and to create the entry.

Not in this PR. There is no admin UI for the mapping yet. It is set through the forms/create and forms/update plugin routes. I plan to follow up with a mapping editor in the admin once this shape lands.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes (lint:json reports 0 diagnostics)
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. (Not applicable: no admin UI changes.)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package) (@emdash-cms/plugin-forms: minor)
  • New features link to an approved Discussion: Create content entries from form submissions (forms plugin) #1672
  • I have included screenshots below if this PR changes the UI (Not applicable: no UI changes.)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Code (Claude Fable 5, Claude Opus 5.5 and Claude Fable 5.1), reviewed by me

Screenshots / test output

Not applicable for screenshots, since there are no UI changes.

The tests cover the cases from the discussion: a form with no mapping behaves as before, a mapped submission creates an entry and keeps the inbox submission, an invalid mapping is rejected when the form is saved, and a failed create does not lose the submission. Each transform also has its own tests.

packages/plugins/forms   Test Files  8 passed (8)
                              Tests  91 passed (91)

pnpm build, pnpm typecheck and pnpm lint:json (0 diagnostics) are clean on the merged branch.

🤖 Generated with Claude Code

Implements the contentMapping proposal from discussion emdash-cms#1672: an
optional per-form setting that creates a draft entry in a target
collection from each successful submission. Mappings are validated
against the target collection at form-save time (including required
field coverage), and the inbox submission is preserved even when
content creation fails.

Adds getCollection and REST-parity slug generation to the plugin
content API in core to support this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 75835fb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@emdash-cms/plugin-forms Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,061 lines across 14 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@rmakk

rmakk commented Jul 17, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jul 17, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR implements the approved discussion well: it adds an optional contentMapping form setting, keeps submissions in the inbox, creates draft-only entries, validates mappings at form-save time, and preserves submissions when content creation fails. The core additions (ctx.content.getCollection and slug generation in ctx.content.create) are additive and mirror the REST path. I read the diff, the changed files, ContentRepository, the REST handleContentCreate, and the forms validation code, and checked against AGENTS.md.

The implementation is generally solid and well-tested, but I found a few gaps worth fixing before merge:

  1. AGENTS.md violation: module-scope mutable singleton. content-mapping.ts keeps a let keyCounter = 0 counter and mutates it from generateKey(). AGENTS.md requires module-scope singletons to live on globalThis because Vite can duplicate modules across SSR chunks. Use a Symbol.for key on globalThis.
  2. Metadata constant validation is incomplete. validateContentMapping only checks that a metadata key is a real collection field. A null/undefined constant for a required target field will pass validation but fail every submit-time insert against the NOT NULL column. The validator should reject required fields covered by nullish metadata.
  3. Optional form fields can satisfy required collection fields. A required collection field mapped from an optional form field (or a transform that can return undefined) may produce an entry with a default/empty value, or, when combined with a failing transform, a logged create failure. Requiring the source form field to be required when the target collection field is required would close the loophole.
  4. Form duplication skips validation. formsDuplicateHandler spreads the existing definition (including contentMapping) into a new form without re-running validateContentMapping. If the target collection or fields changed since the original was saved, the duplicate’s mapping is invalid from the start.
  5. Plugin slug generation ignores site locale. ctx.content.create calls generateUniqueSlug(...) without a locale, while the REST create path scopes uniqueness to the effective locale and creates entries in that locale. Plugin-created entries therefore default to en and use global slug uniqueness, which will behave differently on non-English default sites.

None of these are merge blockers on their own, but (1) is a convention violation and (2)/(3) weaken the save-time validation that is a headline feature of this PR.


Findings

  • [needs fixing] packages/plugins/forms/src/content-mapping.ts:183-189

    Module-scope mutable state (let keyCounter = 0) is used for Portable Text _key generation. AGENTS.md says module-scope singletons must live on globalThis because Vite can duplicate modules across SSR chunks, so two copies of this module could share/reset the counter and produce duplicate keys.

    const KEY_COUNTER = Symbol.for("emdash-forms.content-mapping.key-counter");
    
    function nextKeyCounter(): number {
    	const current = ((globalThis as Record<symbol, number>)[KEY_COUNTER] ?? 0) + 1;
    	(globalThis as Record<symbol, number>)[KEY_COUNTER] = current;
    	return current;
    }
    
    /** Generate a Portable Text `_key`, unique within this process */
    function generateKey(): string {
    	return `form-${nextKeyCounter().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
    }
    
  • [needs fixing] packages/plugins/forms/src/content-mapping.ts:69-76

    The metadata key check verifies the key exists as a collection field, but it does not verify the constant value. A null or undefined metadata constant for a required target field will pass validation and then fail every submit-time insert on the NOT NULL column. Reject required fields covered by nullish metadata constants at save time.

    	for (const key of metadataKeys) {
    		const collectionField = collectionFields.get(key);
    		if (!collectionField) {
    			throw PluginRouteError.badRequest(
    				`Content mapping metadata targets unknown field "${key}" in collection "${mapping.collection}"`,
    			);
    		}
    		const value = mapping.metadata?.[key];
    		if (collectionField.required && (value === undefined || value === null)) {
    			throw PluginRouteError.badRequest(
    				`Content mapping metadata for required field "${key}" in collection "${mapping.collection}" must not be null or undefined`,
    			);
    		}
    	}
    
  • [suggestion] packages/plugins/forms/src/content-mapping.ts:46-82

    The required-field coverage check guarantees that a required collection field is referenced by a mapping or metadata, but not that a field mapping will actually supply a value. If an optional form field is mapped to a required collection field and the submitter leaves it empty, the entry will receive the column default (empty string/0/etc.) or a transform will skip it and a logged create failure occurs. Consider requiring the source form field to be required when it backs a required target field that isn’t covered by metadata.

    	const requiredFormFields = new Set(
    		pages.flatMap((page) => page.fields.filter((field) => field.required).map((field) => field.name)),
    	);
    
    	const mappedTargets = new Set<string>();
    	const targetsFromOptionalField = new Set<string>();
    	for (const [formField, target] of Object.entries(mapping.fieldMappings)) {
    		if (!formFieldNames.has(formField)) {
    			throw PluginRouteError.badRequest(
    				`Content mapping references unknown form field "${formField}"`,
    			);
    		}
    		const targetField = typeof target === "string" ? target : target.field;
    		const collectionField = collectionFields.get(targetField);
    		if (!collectionField) {
    			throw PluginRouteError.badRequest(
    				`Content mapping targets unknown field "${targetField}" in collection "${mapping.collection}"`,
    			);
    		}
    		if (collectionField.required && !requiredFormFields.has(formField)) {
    			targetsFromOptionalField.add(targetField);
    		}
    		mappedTargets.add(targetField);
    	}
    
    	// ...
    
    	for (const field of collection.fields) {
    		if (field.required && !mappedTargets.has(field.slug) && !metadataKeys.has(field.slug)) {
    			throw PluginRouteError.badRequest(
    				`Content mapping does not map required field "${field.slug}" in collection "${mapping.collection}"`,
    			);
    		}
    		if (field.required && targetsFromOptionalField.has(field.slug) && !metadataKeys.has(field.slug)) {
    			throw PluginRouteError.badRequest(
    				`Content mapping maps required field "${field.slug}" in collection "${mapping.collection}" from an optional form field; mark the source form field as required or cover it with metadata`,
    			);
    		}
    	}
    
  • [suggestion] packages/plugins/forms/src/handlers/forms.ts:203-235

    formsDuplicateHandler creates a brand-new form by spreading the existing definition, including contentMapping, but never re-runs validateContentMapping. If the target collection or its fields changed since the original form was saved, the duplicated form will contain an invalid mapping from the moment it is written. Add a validation call before forms(ctx).put(id, duplicate) so duplication behaves like the other save paths.

  • [suggestion] packages/core/src/plugins/context.ts:441

    Plugin ctx.content.create derives a slug without passing a locale, while handleContentCreate scopes uniqueness to the effective locale and inserts the entry in that locale. This means plugin-created entries default to en and use global slug uniqueness, which diverges from the REST/admin path on non-English sites. Consider threading ctx.site.locale (or adding locale support to ContentWriteInput) so the plugin API matches the native create behavior.

- Move the Portable Text _key counter to a globalThis singleton
- Reject nullish metadata constants for required collection fields
- Require required collection fields to be mapped from required form
  fields (or covered by metadata)
- Re-validate the mapping when duplicating a form
- Thread the site default locale through plugin content.create so the
  entry locale and slug uniqueness match the REST create path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview New commits since the last review overlap and removed review/needs-review No maintainer or bot review yet labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@swissky swissky added the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This re-review confirms the PR is in good shape. The author has addressed all five findings from the previous review:

  1. Module-scope key counter is now stored on globalThis via Symbol.for (packages/plugins/forms/src/content-mapping.ts).
  2. Metadata constants for required fields now reject null/undefined at save time (packages/plugins/forms/src/content-mapping.ts).
  3. Required collection fields mapped from optional form fields are rejected (targetsWithRequiredSource check in packages/plugins/forms/src/content-mapping.ts).
  4. Form duplication now re-runs validateContentMapping before saving the duplicate (packages/plugins/forms/src/handlers/forms.ts).
  5. Plugin slug generation now uses the site's configured default locale and locale-scoped uniqueness, matching the REST create path (packages/core/src/plugins/context.ts).

The feature continues to follow the approved discussion: mappings are validated on form save, entries are always created as drafts, the inbox submission is preserved even if content creation fails, and the forms plugin correctly declares content:write. The core additions (ctx.content.getCollection and slug generation in ctx.content.create) are additive and mirror the REST path. Tests cover validation, duplicate revalidation, submit-time failures, transforms, and locale-scoped slug behavior. There are no admin UI changes, so Lingui/RTL concerns do not apply; changesets are present for both affected packages.

No remaining blockers or regressions were found.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview New commits since the last review labels Jul 19, 2026
@masonjames

Copy link
Copy Markdown
Contributor

locally smoke-tested and ready, pending GitHub checks

@emdashbot emdashbot Bot added review/approved Approved; no new commits since and removed bot:review Trigger an emdashbot code review on this PR review/needs-rereview New commits since the last review labels Sep 30, 2026

@masonjames masonjames left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adapting this to the current plugin APIs. I reviewed 4b389c2 and reran the promised checks in an isolated checkout: the root build, package typecheck, quick/type-aware lint (0 diagnostics), and formatting of the eight changed files pass. All 77 forms tests and 115 existing core capability/locale/deferred-work tests pass. The prior synchronous-create and comment findings are addressed.

I reproduced two additional save-time validation gaps, detailed inline. Three temporary regression checks use real SQLite and EmDashRuntime.handleContentCreate: each mapping is accepted on save, but its resulting entry is rejected with VALIDATION_ERROR. The inbox remains protected; the missing draft is the problem. These would be useful regression tests alongside the metadata/field collision already identified by the latest bot review.

Hosted validation is still pending: the CI run for this commit is action_required with zero jobs. A maintainer needs to approve the workflows before merge; ci:rerun cannot clear that approval gate. Local results do not replace those checks.

I removed my old manually applied review/ready-for-maintainer label from July while these items are outstanding. The workflow-managed review, area, size, overlap, and CLA labels can remain.

Review assisted by Codex; the reproductions and checks above were run against the stated commit.

Comment on lines +57 to +58
if (source.required) {
targetsWithRequiredSource.add(targetField);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Required source fields can still be absent from the mapped data. This counts every source.required field as guaranteed coverage, but validateSubmission skips a required field when its condition is false and excludes file fields from result.data even when an upload satisfies their required check. buildContentEntry(mapping, result.data) consequently omits the target.

For example, mapping a conditionally displayed required text field to required collection title saves successfully; submitting the branch where that field is hidden is a valid submission, but the deferred content create fails with VALIDATION_ERROR for the missing title. File sources have the same omission. I reproduced both through real SQLite/core content validation.

Count only sources that are guaranteed to appear in the mapping input toward required-target coverage. A conditional source needs an unconditional source or valid metadata fallback; file sources should be rejected until their uploaded values are explicitly supported. Please add regression coverage for both cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17ee6ba. Only required form fields without a condition count toward required-field coverage, and file fields cannot be mapped at all. Both have regression tests.

`Content mapping metadata targets unknown field "${key}" in collection "${mapping.collection}"`,
);
}
if (collectionField.required && (metadata[key] === undefined || metadata[key] === null)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Empty metadata can satisfy a required field without supplying a valid value. Only null/undefined are rejected here. With a required text title, { fieldMappings: { details: "body" }, metadata: { title: "" } } passes save-time validation, and the later required-field loop treats title as covered. Every submission then builds an empty title and the real runtime rejects the draft with VALIDATION_ERROR.

I reproduced this against SQLite and EmDashRuntime.handleContentCreate. Reject empty string constants for required string fields, consistent with core content validation, and add a save-time regression test. This is separate from the metadata/field collision flagged by the bot.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17ee6ba. Empty metadata for a required field is rejected on save. This also rejects whitespace-only strings, which core accepts, since a blank title is never useful.

@masonjames masonjames added the bot:review Trigger an emdashbot code review on this PR label Sep 30, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still a sensible, well-scoped implementation of the approved Discussion. I verified the two earlier review items are resolved: the content-entry create now runs in after() so it does not block the anonymous submission response, and the new step comment is unnumbered. The contentMapping schema, save-time validation, merge logic, and submit-time deferred create all fit the plugin patterns.

However, the one remaining issue from the previous review is still outstanding: validateContentMapping does not reject a metadata key that collides with a fieldMappings target. Because buildContentEntry spreads metadata first and then overwrites colliding keys with any non-empty mapped form value, the changeset's claim that metadata sets "fixed values on every entry" is not reliable in practice.

Please add the collision check to validateContentMapping and a regression test that asserts the collision is rejected. Once that is fixed, this PR should be clean to merge.

Comment on lines +64 to +76
for (const key of metadataKeys) {
const collectionField = collectionFields.get(key);
if (!collectionField) {
throw PluginRouteError.badRequest(
`Content mapping metadata targets unknown field "${key}" in collection "${mapping.collection}"`,
);
}
if (collectionField.required && (metadata[key] === undefined || metadata[key] === null)) {
throw PluginRouteError.badRequest(
`Content mapping metadata for required field "${key}" in collection "${mapping.collection}" must not be null`,
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The metadata key loop validates that each key exists in the target collection, but it never checks whether the same key is also a fieldMappings target. buildContentEntry spreads mapping.metadata first and then overwrites any colliding key with a transformed form value whenever the mapped field is non-empty, so metadata is a fallback rather than the "fixed value on every entry" documented in the PR description and changeset.

Reject the collision at save time so the stored mapping is unambiguous and the changeset stays accurate:

Suggested change
for (const key of metadataKeys) {
const collectionField = collectionFields.get(key);
if (!collectionField) {
throw PluginRouteError.badRequest(
`Content mapping metadata targets unknown field "${key}" in collection "${mapping.collection}"`,
);
}
if (collectionField.required && (metadata[key] === undefined || metadata[key] === null)) {
throw PluginRouteError.badRequest(
`Content mapping metadata for required field "${key}" in collection "${mapping.collection}" must not be null`,
);
}
}
for (const key of metadataKeys) {
if (mappedTargets.has(key)) {
throw PluginRouteError.badRequest(
`Content mapping metadata field "${key}" in collection "${mapping.collection}" is also mapped from a form field`,
);
}
const collectionField = collectionFields.get(key);
if (!collectionField) {
throw PluginRouteError.badRequest(
`Content mapping metadata targets unknown field "${key}" in collection "${mapping.collection}"`,
);
}
if (collectionField.required && (metadata[key] === undefined || metadata[key] === null)) {
throw PluginRouteError.badRequest(
`Content mapping metadata for required field "${key}" in collection "${mapping.collection}" must not be null`,
);
}
}

Add a matching regression test in packages/plugins/forms/tests/content-mapping.test.ts that asserts this collision is rejected by the save handlers.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 30, 2026
A mapping is now rejected when a metadata key is also a mapping target,
when a required field's metadata value is empty, when a required field is
covered only by a conditional form field, when a file field is mapped, or
when a form field, transform or metadata value cannot produce a value the
target field accepts. Each of these saved before but failed at submit time
with VALIDATION_ERROR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@emdashbot emdashbot Bot added the review/needs-rereview New commits since the last review label Oct 3, 2026
@rmakk

rmakk commented Oct 3, 2026

Copy link
Copy Markdown
Author

@masonjames thanks for the reproductions. 17ee6ba fixes the three gaps and goes one step further, since they share a cause: the mapping saved, but the entry failed core validation at submit time.

  • Metadata keys that are also mapping targets are rejected.
  • Empty metadata for a required field is rejected. This also rejects whitespace-only strings, which core accepts.
  • Only required form fields without a condition count toward required-field coverage. File fields cannot be mapped.
  • New: each form field and transform must produce a value the target field accepts. I probed EmDashRuntime.handleContentCreate for every collection field type against the value shapes a form can produce and wrote the rules from that. A text field onto a number field without the number transform, a date field onto a datetime field without the date transform, a select whose options are not all options of the target, and image or reference targets are now rejected on save. Metadata values go through the same check.

Each case has a regression test; 91 forms tests pass, with the root build, typecheck and lint clean.

Two gaps remain that I could not close from the form side: a number form field onto an integer collection field passes but a decimal entry fails at submit, since forms have no integer field type, and a metadata string for a url field is not URL-validated. Happy to tighten either if you prefer.

The workflow approval for the CI run is on your side.

@emdashbot emdashbot Bot removed the review/approved Approved; no new commits since label Oct 3, 2026
@rmakk
rmakk requested a review from masonjames October 3, 2026 16:07
@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet needs-rebase and removed review/needs-rereview New commits since the last review labels Oct 9, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is waiting on its author because it has merge conflicts with main, and it has had no activity for 14 days. It will be closed in 7 days unless there is activity: push an update or leave a comment to keep it open.

@github-actions github-actions Bot added stale and removed stale labels Oct 20, 2026

@masonjames masonjames left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the feature against main 0eefcfebc and published the reviewed update branch at 39c9d4832, preserving the contributor's commits. The branch includes main 0eefcfebc (freshly fetched before publication), resolves both handler conflicts, preserves main's digest and notification fixes, and closes mapping/input validation gaps. Required blank checkbox selections are rejected, optional blank selections no longer become numeric zero, and the minimum supported core version is corrected to 0.42.0 for compatible URL validation. Documentation now describes the existing admin editor limitation for multi-page or titled forms.

Verification on the prepared revision: root build and package typecheck pass; formatting completed; quick and type-aware lint report zero diagnostics; 190 forms tests and 124 core/runtime/capability/deferred-work tests pass. Eleven new real SQLite runtime tests cover unpublished drafts, default locale, unique slugs, typed mappings, rejected metadata, required selections, and inbox preservation on failure. A controlled pending-create test proves confirmation does not wait for content creation. Claude Opus 5.5 performed a complete tool-disabled adversarial review plus follow-ups and approved the final fixes.

An existing main race remains outside this diff: writing submission counters from a whole-form snapshot can restore concurrently changed settings, including a cleared mapping or paused status. Opus accepted a separate follow-up because a reliable fix requires atomic updates or separate counter storage. Expanded-year dates also remain a documented nonblocking runtime limitation.

This verification applies to the prepared update, not GitHub's existing 75835fb39 head. Once someone with write access applies the update, hosted CI and human maintainer review are still required.

~ 🤖 Codex (GPT-6)

@emdashbot emdashbot Bot added review/needs-review No maintainer or bot review yet and removed review/needs-review No maintainer or bot review yet labels Oct 23, 2026
@github-actions github-actions Bot removed the stale label Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants