Repository navigation
feat(forms): forms are valid against their destination; a draft saves with its findings, publishing needs none (decisions 179, 189) - #1099
Open
rubenvdlinde wants to merge 10 commits into
Open
rubenvdlinde wants to merge 10 commits into
rubenvdlinde wants to merge 10 commits into
Conversation
Decision 179 (Ruben, 10 Oct 2026): a form submits into its destination object, with no in-between intake layer, and is valid against that object's schema when built. Cross-app change: hydra openspec/changes/form-submits-into-its-destination-object (ADR-117, hydra#748). Spec only.
…181) Ruben answered Q9: no report-only release. A buildiq form with findings is not saved; every finding shows on its field.
…pp publish (decisions 179, 189)
…fields; publishing needs none (decisions 179, 189)
…save over the destination check
…pmd and the copilot fixture follow the destination key
Contributor
Quality Report — ConductionNL/buildiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 109/109 | |||
| npm | ✅ | ✅ 742/742 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ❌ |
Quality workflow — 2026-10-10 21:50 UTC
Download the full PDF report from the workflow artifacts.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Decision 179 (no intake layer) and decision 189 (forms can be draft): buildiq's half of change
forms-are-valid-against-their-destination. Spec from #1097 (merged in at a0bac45) and amended here for decision 189.Depends on openregister#4563 (form destination validator,
FormDestinationDependentsEvent). Until it lands, every destination check answersdestination-unchecked: a draft still saves (with that finding), publishing is refused. Paired with ConductionNL/nextcloud-vue #1461 (CnFormPage marks a 422findings[]per field and renders the honeypot).What an author can now do
draftsaves with its destination findings, which come back in the save answer and are marked on each field and preset row; findings on no row (a required property nothing fills) are listed under them. The page designer asksPOST /api/forms/destination-checkafter every save and marks each finding on its form field.publishedwith findings is refused 422 with every finding. Publishing an app is refused 422 while a form page in its live manifest has findings. There is no report-only mode (decision 181).honeypot: "_hp".POST /api/forms/destination-checktakes{ appSlug, manifest }and answers 403 unless the caller is an owner or editor of that app (or an admin): the findings name the destination schema's properties.FormDestinationDependentsListenerannounces buildiq's published registration forms to OpenRegister; a form a schema change breaks is set back todraft.config.destination { register, schema }, filled from or intoregister/schema/submitEndpoint.Open point: drafts (decision 189) and why not
@self.statusThe coordinator relayed decision 189: drafts save with findings, publish refuses. Built that way, and the spec amended in this PR (new requirement "A draft form saves with its findings"; the publish scenarios say publish, not save). buildiq keeps its forms' own
status(draft/published) rather than openregister#4566's@self.statusdraft, because that status is left only through a form submit (FormSubmitService), and an author publishes a form, not submits it. Form pages are drafts while the app is unpublished.How it calls OpenRegister
FormDestinationCheckresolves the schema inside the form's register (two registers may each hold acase) and hands the entity to OpenRegister'sFormDestinationValidator, the service behindPOST /api/forms/validate, through the container. Same rules and findings as the endpoint, without the bare-slug ambiguity. A schema that cannot be read, a register without it, or an OpenRegister without the validator is a finding (destination-unchecked), never a silent pass.Registration form fields render their property's own enum and bounds (the
registrationFormschema gives a field no options or limits), soenum-unconstrainedandconstraint-looseron such a field are left out; every other code stands.New classes and their tests (coverage guard)
Service/FormDestinationChecktests/Unit/Service/FormDestinationCheckTest(each refusal, zero-findings control, real validator scenario)Service/ManifestFormDestinationstests/Unit/Service/ManifestFormDestinationsTest(pages checked, mapping, public audience, real type mismatch)Controller/FormDestinationControllertests/Unit/Controller/FormDestinationControllerTestListener/FormDestinationDependentsListenertests/Unit/Listener/FormDestinationDependentsListenerTestException/FormDestinationRefusedExceptiontests/Unit/Exception/FormDestinationRefusedExceptionTestChanged:
RegistrationFormTargetWarnings(now the validator's caller,RegistrationFormTargetWarningsTestwith the spec scenarios against the real validator),RegistrationFormAuthoringService(draft saves, publish refuses),RegistrationFormValidator(rules only),RegistrationFormController(422 with findings),ApplicationPublishController(publish refusal,testPublishIsRefusedWhileAFormPageHasFindings),ManifestPageShape(destination), frontendformDestinationCheck.js(tests/services/formDestinationCheck.spec.js),FormFieldBuilder,FormPageEditor,PageDesigner(Host),RegistrationForm{List,Editor},FormLayoutBuilder,FormPresetsBuilder,ExternalFormAccessDialog. E2e:tests/e2e/form-destination.spec.ts(API level, skips without the validator).The real-validator tests skip in CI until openregister#4563 is on development; locally they ran against its branch.
Spec amendments
BqPaginaOntwerperFoutendraws a request log, not findings on fields, so- Design backlog: BqFormulierBevindingen (decision 157)is added (also in the lane's design-backlog.md). The UI is built from the existing inline marks; the board refines layout only.forms-per-case-typeREQ-OBRF-005 "Saving SHALL warn" replaced by a pointer here;journey-designertask 3 notes the shared validator.register+schemaalready are its destination, so no second copy is stored.Verified
All through with-slot.sh, logs in the lane directory (
build-round/d179/).COMPOSER_PROCESS_TIMEOUT=0 composer check:strict, once, at f124ab0: exit 1. lint and phpstan clean, phpcs 0 errors. NEW findings, all fixed after the run: phpmd (static access, one complexity) inManifestFormDestinations, psalmUndefinedClasson the OpenRegister event (now a guarded psalm stub,tests/stubs/openregister-form.stub.php), one PHPUnit failure (CopilotServiceTestfixture now carries the newdestinationkey, which the canonical manifest validator accepts:tests/vitest/copilotRealPlan.spec.jsgreen), gate-7 on the new destination-check endpoint (now an editor guard).composer phpmdexit 0,composer psalmexit 0 ("No errors found!"), fullphpunit --no-coverageexit 0 (1404 tests, 3 skipped, all inherited skips), touched vitest specs exit 0 (60 tests), earlier touched vitest runs exit 0 (73 and 113 tests).npm run lint: exit 0 (0 errors, 154 warnings, none on lines this PR adds).prettier --checkon changed files: clean.npm run test:l10nandcheck:l10n-js: exit 0.--require-full-coverage, base origin/development, this body): 84 of 84 applicable gates ran, 5 fail, all inherited: gate-22 and gate-53 (src/manifest.json, assembled manifest), gate-64 (AppHost autoload prelude), gate-104 and gate-107 (Reports menu section). This PR touches no manifest or menu file.openspec validate --strict:forms-are-valid-against-their-destination,forms-per-case-type,journey-designervalid.Still owed
FormDestinationDependentsEvent(they live in app manifests; unpublishing one means unpublishing its app). Follow-up.caseTypeshows the finding; publishing it answers 422; the designer marks aproperty-unknownon its field; external access refuses a form with findings; a breaking change to thecaseschema puts a published form back in draft.