202 lines
8.3 KiB
Markdown
202 lines
8.3 KiB
Markdown
# QuestionGraph
|
|
|
|
A questionnaire authoring format, compiler, evaluation engine, and React renderer.
|
|
The claims questionnaire is a worked example. `/docs` includes a smaller event
|
|
registration example and an expression playground.
|
|
|
|
## Run
|
|
|
|
Use Node.js 20.19+ or 22.12+ (24 recommended).
|
|
|
|
```sh
|
|
npm install
|
|
npm run dev
|
|
npm test
|
|
npm run build
|
|
```
|
|
|
|
## Authoring
|
|
|
|
```json
|
|
{
|
|
"$schema": "./questionnaire.schema.json",
|
|
"schemaVersion": 1,
|
|
"id": "registration",
|
|
"version": "1",
|
|
"title": "Registration",
|
|
"pages": [
|
|
{
|
|
"id": "details",
|
|
"title": "Your details",
|
|
"fields": [
|
|
{
|
|
"id": "name",
|
|
"type": "text",
|
|
"label": "Your name",
|
|
"required": true,
|
|
"constraints": { "minLength": 2, "maxLength": 100 }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Pages and fields appear in array order. Answer fields have globally unique IDs.
|
|
`when` on pages, groups, and fields controls applicability; ancestor conditions
|
|
apply to every descendant. Groups contain `fields` and optionally `title` and
|
|
`description`; they do not create answer values.
|
|
|
|
Types describe answers: `text`, `number`, `date`, `boolean`, `select`,
|
|
`multiselect`, and `files`. Optional `ui.widget` chooses presentation without
|
|
changing the answer contract. Constraints are type-specific and engine-enforced.
|
|
`required` defaults to false; use `requiredWhen` for conditional requirements,
|
|
never both. A required boolean accepts false. `constraints.mustBeTrue` requires
|
|
acceptance, including when the answer is missing.
|
|
|
|
Use `validation` expressions for cross-field or dynamic constraints:
|
|
|
|
```json
|
|
{
|
|
"rule": "value <= answer('hoursBefore')",
|
|
"message": "Current hours cannot exceed previous hours."
|
|
}
|
|
```
|
|
|
|
Use `route: { cases: [{ when, to }], otherwise }` only for explicit jumps. First
|
|
matching case wins. Targets must be later page IDs or `END`; fallback is mandatory.
|
|
Skipped pages do not execute routes. Ordinary pages need no navigation properties.
|
|
|
|
## Answer semantics
|
|
|
|
- Drafts retain inactive answers, restoring them when their branch applies again.
|
|
- `answer('id')` reads effective answers: inactive, empty, or structurally invalid
|
|
values return null. Business constraint failures remain visible for correction.
|
|
- Whitespace-only text and empty arrays are unanswered; false and zero are answers.
|
|
- Validation and submission include only applicable fields.
|
|
- Null comparisons (`<`, `<=`, `>`, `>=`) are false; arithmetic with null is null.
|
|
- Conditions must return boolean. Equality is `===` / `!==`.
|
|
- Dates are valid `YYYY-MM-DD` calendar dates; month arithmetic clamps the day.
|
|
- Sessions capture one date. Supply `{ today: "2026-09-12" }` to `createSession`
|
|
for a deterministic business date on the server or in tests.
|
|
|
|
## Architecture
|
|
|
|
`Author JSON → schema validation → compilation → evaluated session → React`
|
|
|
|
- `src/lib/questionnaire/questionnaire.schema.json`: formal editor/runtime JSON Schema.
|
|
- `src/lib/questionnaire/questionnaire-schema.ts`: discriminated author types and answer types.
|
|
- `src/lib/questionnaire/questionnaire-compiler.ts`: document validation, lookup tables, expression
|
|
reference/type checks, forward routing validation, and dependency-cycle detection.
|
|
- `src/lib/questionnaire/dsl.ts`: bounded lexer/parser, AST, helper signatures, evaluator, dependencies.
|
|
- `src/lib/questionnaire/questionnaire-engine.ts`: immutable answer snapshots, applicability,
|
|
derived values, validation, navigation, outcomes, and evaluated page state.
|
|
- `src/lib/react/QuestionnaireRenderer.tsx`: inputs, nested groups, touched/error display,
|
|
focus, and save-event integration.
|
|
- `src/demo/claim-questionnaire.json`: full migrated claim questionnaire.
|
|
- `src/demo/example-questionnaire.json`: small standalone authoring example.
|
|
- `tests/questionnaire.test.ts`: compiler, semantics, branches, constraints,
|
|
draft resume, save previews, and all four claim journey scenarios.
|
|
|
|
Conditions cannot depend on their own answers, directly or through groups,
|
|
pages, routing, or derived values. The compiler detects these dependency cycles.
|
|
`value` is only available inside field validation and `requiredWhen`.
|
|
The format has no compatibility layer for the previous nodes/questions document.
|
|
|
|
## Using a custom component library
|
|
|
|
`src/lib` contains the reusable questionnaire library; `src/demo` contains the
|
|
claims example and inspector application. `QuestionnaireRenderer` owns questionnaire state, visibility, validation,
|
|
navigation, resume behavior, and save events. The default HTML controls can be
|
|
replaced with application components through `components`:
|
|
|
|
```tsx
|
|
<QuestionnaireRenderer
|
|
definition={definition}
|
|
components={{
|
|
text: ({ id, value, accessibility, onChange }) => (
|
|
<DesignSystemTextInput
|
|
id={id}
|
|
value={String(value ?? "")}
|
|
{...accessibility}
|
|
onChange={event => onChange(event.target.value)}
|
|
/>
|
|
),
|
|
select: MySelectField
|
|
}}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
```
|
|
|
|
Components receive the evaluated question, current value, required state,
|
|
validation results, accessibility attributes, and an `onChange` callback. Use
|
|
`renderField` when the host application needs to own the complete field markup,
|
|
including labels and error presentation. In that case the custom renderer
|
|
should provide an element with the field's `${id}-question` convention if it
|
|
wants the built-in resume-focus behavior.
|
|
|
|
## Production persistence
|
|
|
|
Pass an async `onSave` handler to send draft patches to the host application.
|
|
The library does not know the API URL, authentication mechanism, or HTTP client.
|
|
The existing `onSubmit` callback may also be async and is called with effective
|
|
answers and evaluated outcomes:
|
|
|
|
```tsx
|
|
<QuestionnaireRenderer
|
|
definition={definition}
|
|
resumedDraft={draft}
|
|
onSave={request => api.patchDraft({
|
|
...request,
|
|
headers: { "If-Match": `"${request.revision}"` }
|
|
})}
|
|
onUpload={async (questionId, files) => {
|
|
// Send the actual File bytes; return metadata and a real uploadId per file.
|
|
return api.uploadFiles(questionId, files);
|
|
}}
|
|
onSubmit={async (answers, outcomes) => {
|
|
await api.submitQuestionnaire({ answers, outcomes });
|
|
}}
|
|
onSaveEvent={event => telemetry.record(event)}
|
|
/>
|
|
```
|
|
|
|
`onSave` is serialized by the library and must return the server's new
|
|
`revision`. A rejected save is reported as a blocked save event. The host
|
|
application should use the revision for optimistic concurrency and handle
|
|
conflicts or retries according to its API policy.
|
|
|
|
Changes are acknowledged only after a successful save. A later save retries
|
|
unacknowledged answers using the last confirmed revision. Continue waits for
|
|
the queue and stays on the page when saving fails; Submit waits for draft saving
|
|
before calling the host. Inputs and navigation are disabled during uploads and
|
|
Continue/Submit operations. Submission failures are displayed for retry.
|
|
|
|
Without an `onSave` handler the renderer operates in memory, emits `Changed`
|
|
events, and does not advance a server revision. With a handler it emits `Saved`
|
|
after success or `Blocked` on failure. Events contain transport-independent
|
|
`data` (changes, resume position, revision, trigger), not HTTP requests.
|
|
`SaveResponse` contains only the confirmed revision.
|
|
|
|
The built-in file input requires `onUpload(questionId, files)` for nonempty
|
|
selections. It validates metadata before uploading and requires an uploaded
|
|
reference for each file. Custom file controls can use `onFilesSelected` or pass
|
|
already uploaded references through `onChange`. Save and submit use those same
|
|
references; the library never generates upload IDs. Configure persistence when
|
|
mounting the renderer; remount it to switch between in-memory and server modes.
|
|
|
|
## Boundaries
|
|
|
|
The demo uses explicit mock handlers in `src/demo/mock-persistence.ts`; they
|
|
perform no file transfer or durable persistence. The backend must validate
|
|
the exact questionnaire version independently and resolve authorized upload IDs.
|
|
|
|
Expressions cannot access JavaScript globals, properties, assignments, loops,
|
|
or arbitrary functions. Length/token/nesting limits bound parsing. Native regular
|
|
expressions have a pattern-length limit but no execution-time guarantee; review
|
|
published patterns or replace that implementation before accepting untrusted rules.
|
|
|
|
Repeatable groups, reusable option libraries, localized messages, real uploads,
|
|
and persistence are not implemented. Currency is currently a numeric presentation
|
|
hint, not a decimal-money storage type.
|