@frt-platform/report-core 1.2.1 β†’ 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,695 +1,500 @@
1
1
  # `@frt-platform/report-core`
2
2
 
3
- Core engine for building, validating, and migrating **report templates** – plus validating **report responses** and exporting **JSON Schema**.
3
+ Core engine for defining, validating, migrating, diffing, and serializing **dynamic report templates**.
4
4
 
5
5
  This package is:
6
6
 
7
- * **Framework-agnostic** (no React)
8
- * **Storage-agnostic** (no DB assumptions)
9
- * **Runtime-safe** (Zod-based validation)
7
+ * 🧠 **Framework-agnostic**
8
+ * 🧩 **UI-agnostic**
9
+ * πŸ”’ **Safe by design (Zod-based)**
10
+ * 🧱 **The foundation of the FRT incident & reporting platform**
10
11
 
11
- It gives you:
12
-
13
- * A **typed schema** for report templates (templates β†’ sections β†’ fields)
14
- * **Zod-based validation** and parsing from unknown / JSON input
15
- * **Legacy schema migration** (flat `fields` β†’ `sections`, old type names, etc.)
16
- * **Normalization** helpers to ensure safe IDs & consistent structure
17
- * **Field defaults** and **ID utilities** you can use to build your own UI
18
- * **Response validation** based on templates (`validateReportResponse`)
19
- * **Conditional logic** for fields (`visibleIf` / `requiredIf`)
20
- * **Template diffing** (`diffTemplates`) to compare template versions (including ordering)
21
- * **Type-level response inference** (`InferResponse`) for fully typed responses
22
- * **Field registry** (`FieldRegistry`) for custom / extensible field types
23
- * **JSON Schema export** (`exportJSONSchema`) for integration with OpenAPI / external validators
24
-
25
- It’s the core that powers a flexible incident/report builder, but it’s generic enough to be reused in any app.
12
+ It contains **no React**, **no database code**, and **no styling**.
13
+ You can use it in **Node**, **Next.js**, **backend services**, or custom form engines.
26
14
 
27
15
  ---
28
16
 
29
- ## Installation
17
+ # ✨ Features
30
18
 
31
- ```bash
32
- # npm
33
- npm install @frt-platform/report-core zod
19
+ ### πŸ“„ Template Schema
34
20
 
35
- # yarn
36
- yarn add @frt-platform/report-core zod
21
+ * Sections β†’ fields
22
+ * Field IDs, labels, descriptions, placeholders
23
+ * Built-in field types:
37
24
 
38
- # pnpm
39
- pnpm add @frt-platform/report-core zod
40
- ```
25
+ * `shortText`, `longText`
26
+ * `number`
27
+ * `date`
28
+ * `checkbox`
29
+ * `singleSelect`, `multiSelect`
30
+ * `repeatGroup` (nested fieldsets)
41
31
 
42
- `zod` is a peer dependency and is used for schema validation & parsing.
32
+ ### πŸŽ› Field Constraints
43
33
 
44
- ---
34
+ * min/max length (`shortText`, `longText`)
35
+ * min/max value (`number`)
36
+ * min/max selections (`multiSelect`)
37
+ * allowed options
38
+ * checkbox required semantics
39
+ * default values
45
40
 
46
- ## Concepts
41
+ ### πŸ”€ Conditional Logic
47
42
 
48
- The core model looks like this:
43
+ * `visibleIf`
44
+ * `requiredIf`
45
+ * Supports:
46
+ `equals`, `any`, `all`, `not`
47
+ * Fully integrated into validation.
49
48
 
50
- * A **template** contains multiple **sections**
51
- * A **section** contains multiple **fields**
52
- * A **field** has a `type` (short text, long text, number, select, etc.) and optional constraints
53
- * Fields can have **conditional logic** (`visibleIf`, `requiredIf`)
49
+ ### πŸ”₯ Validation Engine
54
50
 
55
- Supported field types:
51
+ * Build response schema dynamically with conditions
52
+ * DX helper: `buildResponseSchema(template)` (no response needed)
53
+ * Throwing API: `validateReportResponse()`
54
+ * Non-throwing API: `validateReportResponseDetailed()`
55
+ * Rich `ResponseFieldError` objects with:
56
56
 
57
- ```ts
58
- import { REPORT_TEMPLATE_FIELD_TYPES } from "@frt-platform/report-core";
59
-
60
- console.log(REPORT_TEMPLATE_FIELD_TYPES);
61
- // [
62
- // "shortText",
63
- // "longText",
64
- // "number",
65
- // "date",
66
- // "checkbox",
67
- // "singleSelect",
68
- // "multiSelect",
69
- // "repeatGroup", // reserved for repeating groups / fieldsets
70
- // ]
71
- ```
57
+ * section title
58
+ * field label
59
+ * error code
60
+ * full message
61
+ * nested repeatGroup row context
72
62
 
73
- > `repeatGroup` is reserved for repeating fieldsets (e.g. multiple injured people); the core schema knows about it and provides defaults, and the full runtime semantics can be layered on top in your app/React package.
63
+ ### πŸ”„ Template Migration & Normalization
74
64
 
75
- ---
65
+ * Legacy format migration (`fields β†’ sections`)
66
+ * Automatic ID normalization & uniqueness enforcement
67
+ * Stable `"lowercase_with_underscores"`-style IDs
68
+ * Fallback IDs for missing values: `section_1`, `field_1`, etc.
76
69
 
77
- ## Quick Start
70
+ ### πŸ” Schema Diffing
78
71
 
79
- ### 1. Define a template in JSON
72
+ Detect:
80
73
 
81
- You can define templates statically or load them from DB / API:
74
+ * Added / removed / reordered sections
75
+ * Added / removed / reordered fields
76
+ * Modified fields
77
+ * Nested diffs inside repeat groups
82
78
 
83
- ```ts
84
- import type { ReportTemplateSchema } from "@frt-platform/report-core";
79
+ ### πŸ“¦ JSON Schema Export
85
80
 
86
- const templateJson: ReportTemplateSchema = {
87
- version: 1,
88
- title: "Incident follow-up",
89
- description: "Collect a quick summary after an incident has been resolved.",
90
- sections: [
91
- {
92
- id: "section-overview",
93
- title: "Incident overview",
94
- fields: [
95
- {
96
- id: "summary",
97
- type: "longText",
98
- label: "What happened?",
99
- required: true,
100
- minLength: 10,
101
- },
102
- {
103
- id: "status",
104
- type: "singleSelect",
105
- label: "Incident status",
106
- required: true,
107
- options: ["Pending review", "Resolved", "Escalated"],
108
- defaultValue: "Resolved",
109
- },
110
- ],
111
- },
112
- ],
113
- };
114
- ```
81
+ * Export a template as a valid JSON Schema (2020-12 draft)
82
+ * Includes vendor extensions:
115
83
 
116
- ### 2. Validate and normalize the template
84
+ * `x-frt-visibleIf`
85
+ * `x-frt-requiredIf`
86
+ * `x-frt-minIf` / `x-frt-maxIf` for repeatGroup
87
+ * placeholders
88
+ * Useful for OpenAPI, Postman, or other backend runtimes.
117
89
 
118
- Use the helpers to safely parse and normalize unknown input:
90
+ ### πŸ— Field Registry
119
91
 
120
- ```ts
121
- import {
122
- parseReportTemplateSchema,
123
- normalizeReportTemplateSchema,
124
- } from "@frt-platform/report-core";
92
+ Extend the system at runtime:
125
93
 
126
- const raw = /* from DB, file, API, etc. */ templateJson;
94
+ * Add custom types (`richText`, `fileUpload`, etc.)
95
+ * Override validation logic
96
+ * Provide metadata for UI packages
97
+ * Unknown/unregistered field types safely fall back to `z.any()` so they never break the core engine.
127
98
 
128
- // 1) parse + validate (throws on invalid input)
129
- const parsed = parseReportTemplateSchema(raw);
99
+ ### 🧬 Type Inference
130
100
 
131
- // 2) optional extra normalization step (IDs, trimming, de-duplication, etc.)
132
- const normalized = normalizeReportTemplateSchema(parsed);
101
+ Get a fully typed response type from a template:
133
102
 
134
- console.log(normalized.sections[0].fields[0].id);
135
- // always a non-empty, safe identifier
136
- ```
103
+ ```ts
104
+ type MyResponse = InferResponse<typeof template>;
105
+ ````
137
106
 
138
- ### 3. Serialize for storage
107
+ ### 🧾 Serialization Helpers
139
108
 
140
- ```ts
141
- import { serializeReportTemplateSchema } from "@frt-platform/report-core";
109
+ Deterministic JSON output with sorting options:
142
110
 
143
- const jsonString = serializeReportTemplateSchema(normalized);
144
- // ready to store in DB, file, etc.
111
+ ```ts
112
+ serializeReportTemplateSchema(template, {
113
+ pretty: true,
114
+ sortSectionsById: true,
115
+ sortFieldsById: true,
116
+ });
145
117
  ```
146
118
 
147
- ---
148
-
149
- ## Parsing from a JSON string
119
+ Perfect for Git diffs and storage.
150
120
 
151
- If you have a JSON string (e.g. from a form textarea):
121
+ ---
152
122
 
153
- ```ts
154
- import { parseReportTemplateSchemaFromString } from "@frt-platform/report-core";
123
+ # πŸ“¦ Installation
155
124
 
156
- try {
157
- const template = parseReportTemplateSchemaFromString(userInputString);
158
- // valid template here
159
- } catch (err) {
160
- // invalid JSON or schema – show error to user
161
- }
125
+ ```bash
126
+ npm install @frt-platform/report-core zod
162
127
  ```
163
128
 
164
- ---
129
+ `zod` is a peer dependency.
165
130
 
166
- ## Validating report responses
131
+ ---
167
132
 
168
- Once you have a template, you can validate **user-submitted responses** against it.
133
+ # 🧹 Parsing & Normalization
169
134
 
170
- ### Runtime validation
135
+ The core exposes helpers for safely **parsing**, **migrating**, and **normalizing** templates.
171
136
 
172
137
  ```ts
173
- import { validateReportResponse } from "@frt-platform/report-core";
174
-
175
- const template = /* a valid ReportTemplateSchema */ normalized;
138
+ import {
139
+ parseReportTemplateSchema,
140
+ parseReportTemplateSchemaFromString,
141
+ } from "@frt-platform/report-core";
176
142
 
177
- const responseData = {
178
- summary: "Student slipped in the hallway, no serious injuries.",
179
- status: "Resolved",
180
- };
143
+ // From a raw JS object (e.g. loaded from DB, file, etc.)
144
+ const template = parseReportTemplateSchema(rawTemplateObject);
181
145
 
182
- // Validate (throws ZodError on failure)
183
- const safeResponse = validateReportResponse(template, responseData);
146
+ // From a JSON string
147
+ const templateFromString = parseReportTemplateSchemaFromString(jsonString);
184
148
  ```
185
149
 
186
- `validateReportResponse` respects:
150
+ Under the hood, `parseReportTemplateSchema` does:
187
151
 
188
- * `required` flags
189
- * `minLength` / `maxLength` (short/long text)
190
- * `minValue` / `maxValue` (numbers)
191
- * `minSelections` / `maxSelections` (multiSelect)
192
- * `options` (singleSelect + multiSelect)
193
- * `checkbox` semantics (required checkbox must be `true`)
194
- * **Conditional logic**:
152
+ 1. **Legacy migration**
195
153
 
196
- * `visibleIf`: if false, the field is treated as invisible and removed from the parsed output
197
- * `requiredIf`: if true, the field is treated as required for this specific response
154
+ * Accepts both the new `{ sections: [...] }` format and legacy flat:
198
155
 
199
- You can still build a raw schema yourself using the conditional builder:
156
+ ```ts
157
+ {
158
+ version: 1,
159
+ fields: [ /* ... */ ]
160
+ }
161
+ ```
200
162
 
201
- ```ts
202
- import { buildResponseSchemaWithConditions } from "@frt-platform/report-core";
163
+ * Legacy `fields` are automatically wrapped into a single `section_1`.
203
164
 
204
- const responseSchema = buildResponseSchemaWithConditions(template, responseData);
205
- const safeResponse = responseSchema.parse(responseData);
206
- ```
165
+ 2. **Schema validation**
207
166
 
208
- ---
167
+ * Uses a Zod validator for `ReportTemplateSchema` to ensure the template is structurally valid.
209
168
 
210
- ## Conditional logic (`visibleIf` / `requiredIf`)
169
+ 3. **Normalization**
211
170
 
212
- Fields can declare conditions that refer to other field values.
171
+ * Section and field IDs are normalized to **lowercase** with spaces β†’ underscores:
213
172
 
214
- Example:
173
+ * `"My Field!"` β†’ `"my_field"`
215
174
 
216
- ```json
217
- {
218
- "id": "injury_details",
219
- "type": "longText",
220
- "label": "Describe the injury",
221
- "visibleIf": { "equals": { "injured": true } },
222
- "requiredIf": { "equals": { "severity": "High" } }
223
- }
224
- ```
175
+ * Invalid characters are stripped (only `[a-z0-9_-]` are kept).
225
176
 
226
- Supported condition shapes:
177
+ * IDs are made **unique per namespace** by appending `-1`, `-2`, … as needed:
227
178
 
228
- * `{"equals": { "fieldId": value } }`
229
- * `{"any": [cond1, cond2, ...] }`
230
- * `{"all": [cond1, cond2, ...] }`
231
- * `{"not": cond }`
179
+ * `my_field`, `my_field-1`, `my_field-2`, …
232
180
 
233
- At runtime:
181
+ * Missing IDs get deterministic fallbacks:
234
182
 
235
- * If `visibleIf` is **false** β†’ field is treated as **invisible**, becomes optional, and is stripped from the validated response.
236
- * If `requiredIf` is **true** β†’ field is treated as **required** for that specific response.
183
+ * Sections: `section_1`, `section_2`, …
184
+ * Fields: `field_1`, `field_2`, …
185
+
186
+ This makes templates safe to store, diff, and round-trip in a stable way.
237
187
 
238
188
  ---
239
189
 
240
- ## Type-level response inference (`InferResponse`)
190
+ # πŸš€ Quickstart
241
191
 
242
- If you define templates statically (`as const`), you can get a fully-typed response type from the template:
192
+ ## 1. Define a template
243
193
 
244
194
  ```ts
245
- import type { InferResponse } from "@frt-platform/report-core";
195
+ import type { ReportTemplateSchema } from "@frt-platform/report-core";
246
196
 
247
- const incidentTemplate = {
197
+ const template: ReportTemplateSchema = {
248
198
  version: 1,
249
199
  sections: [
250
200
  {
251
- id: "section-overview",
201
+ id: "general",
202
+ title: "General Info",
252
203
  fields: [
253
204
  {
254
- id: "summary",
255
- type: "longText",
256
- label: "What happened?",
205
+ id: "title",
206
+ type: "shortText",
207
+ label: "Incident title",
257
208
  required: true,
258
- minLength: 10,
259
209
  },
260
210
  {
261
- id: "status",
211
+ id: "severity",
262
212
  type: "singleSelect",
263
- label: "Status",
213
+ label: "Severity",
264
214
  required: true,
265
- options: ["Pending", "Resolved", "Escalated"] as const,
266
- },
267
- {
268
- id: "follow_up_date",
269
- type: "date",
270
- label: "Follow-up date",
215
+ options: ["Low", "Medium", "High"],
271
216
  },
272
217
  {
273
- id: "tags",
274
- type: "multiSelect",
275
- label: "Tags",
276
- options: ["Safety", "Medical", "Security"] as const,
277
- },
278
- {
279
- id: "confirmed",
280
- type: "checkbox",
281
- label: "I confirm this report is accurate",
282
- required: true,
218
+ id: "details",
219
+ type: "longText",
220
+ label: "Details",
221
+ minLength: 10,
283
222
  },
284
223
  ],
285
224
  },
286
225
  ],
287
- } as const;
288
-
289
- type IncidentResponse = InferResponse<typeof incidentTemplate>;
290
- ```
291
-
292
- `IncidentResponse` will be inferred as:
293
-
294
- ```ts
295
- type IncidentResponse = {
296
- summary: string; // required
297
- status: "Pending" | "Resolved" | "Escalated"; // required
298
- follow_up_date?: string; // optional
299
- tags?: ("Safety" | "Medical" | "Security")[]; // optional
300
- confirmed: boolean; // required checkbox
301
226
  };
302
227
  ```
303
228
 
304
- This is extremely useful for:
305
-
306
- * API handlers (`(body: IncidentResponse) => { ... }`)
307
- * DB layers that store `data` blobs
308
- * Frontend forms with type-safe state
309
-
310
229
  ---
311
230
 
312
- ## Using field defaults
313
-
314
- The package includes core defaults for each field type (no UI, no icons):
231
+ ## 2. Validate a response (throwing API)
315
232
 
316
233
  ```ts
317
- import {
318
- CORE_FIELD_DEFAULTS,
319
- DEFAULT_FIELD_LABEL,
320
- type ReportTemplateFieldType,
321
- } from "@frt-platform/report-core";
322
-
323
- function createDefaultField(type: ReportTemplateFieldType) {
324
- const defaults = CORE_FIELD_DEFAULTS[type] ?? {};
325
- return {
326
- id: "your-id-here",
327
- type,
328
- label: (defaults.label as string) ?? DEFAULT_FIELD_LABEL,
329
- ...defaults,
330
- };
331
- }
234
+ import { validateReportResponse } from "@frt-platform/report-core";
332
235
 
333
- const field = createDefaultField("shortText");
236
+ const parsed = validateReportResponse(template, {
237
+ title: "Broken fire alarm",
238
+ severity: "High",
239
+ details: "Triggered after smoke test",
240
+ });
334
241
  ```
335
242
 
336
- `CORE_FIELD_DEFAULTS` includes basic defaults for:
337
-
338
- * `shortText`, `longText`, `number`, `date`, `checkbox`, `singleSelect`, `multiSelect`
339
- * `repeatGroup` (with an empty `fields` array; your builder populates nested fields)
340
-
341
- This is especially useful in a form builder UI when the user adds a new field of a given type.
243
+ If invalid β†’ throws a ZodError.
342
244
 
343
245
  ---
344
246
 
345
- ## Field Registry (`FieldRegistry`)
346
-
347
- You can register **custom field types** or override core types at runtime:
247
+ ## 3. Validate without throwing (UI-friendly)
348
248
 
349
249
  ```ts
350
- import { FieldRegistry } from "@frt-platform/report-core";
351
- import { z } from "zod";
352
-
353
- // Simple example: a "richText" field that behaves like a string
354
- FieldRegistry.register("richText", {
355
- defaults: {
356
- label: "Details",
357
- placeholder: "Write here...",
358
- },
359
- buildResponseSchema: (field) => {
360
- let schema = z.string();
250
+ import { validateReportResponseDetailed } from "@frt-platform/report-core";
361
251
 
362
- if (typeof field.minLength === "number") {
363
- schema = schema.min(field.minLength);
364
- }
365
- if (typeof field.maxLength === "number") {
366
- schema = schema.max(field.maxLength);
367
- }
368
-
369
- return field.required ? schema : schema.optional();
370
- },
252
+ const result = validateReportResponseDetailed(template, {
253
+ title: "",
254
+ severity: "High",
371
255
  });
372
- ```
373
256
 
374
- At validation time, `validateReportResponse` and `buildBaseFieldSchema` will:
375
-
376
- 1. Check the registry for a custom handler.
377
- 2. Use the built-in fallback if none is registered.
257
+ if (!result.success) {
258
+ console.log(result.errors);
259
+ }
260
+ ```
378
261
 
379
- Use this for:
262
+ Produces:
380
263
 
381
- * App-specific field types (e.g. `fileUpload`, `signature`, `mapLocation`)
382
- * Tenant-specific extensions in a multi-tenant platform
383
- * Gradually introducing new field behaviors without changing the core package
264
+ ```ts
265
+ [
266
+ {
267
+ fieldId: "title",
268
+ sectionId: "general",
269
+ sectionTitle: "General Info",
270
+ label: "Incident title",
271
+ code: "field.too_small",
272
+ message:
273
+ 'Section "General Info" β†’ Field "Incident title": String must contain at least 1 character(s).'
274
+ }
275
+ ]
276
+ ```
384
277
 
385
278
  ---
386
279
 
387
- ## Generating unique IDs
388
-
389
- The core package exposes a generic `createUniqueId` helper:
280
+ # πŸ”€ Conditional Logic Example
390
281
 
391
282
  ```ts
392
- import { createUniqueId } from "@frt-platform/report-core";
393
-
394
- const existingSectionIds = ["section-1", "section-2"];
395
- const newId = createUniqueId("section", existingSectionIds);
396
- // "section-3" (or the next available)
283
+ {
284
+ id: "follow_up_notes",
285
+ type: "longText",
286
+ label: "Follow-up notes",
287
+ visibleIf: { equals: { follow_up_required: true } },
288
+ requiredIf: { equals: { follow_up_required: true } },
289
+ }
397
290
  ```
398
291
 
399
- You can use this for both section and field IDs when building templates dynamically.
400
-
401
- ---
402
-
403
- ## Legacy schema migration
292
+ Behavior:
404
293
 
405
- If you had older templates that:
294
+ * If `follow_up_required = false` β†’ field is **hidden** and **ignored**
295
+ * If `true` β†’ field becomes **required**
406
296
 
407
- * Used a flat `fields` array instead of `sections`, or
408
- * Used older field type names (`text`, `textarea`, `dropdown`, `multiselect`, …)
297
+ ---
409
298
 
410
- you can pass them through the migration pipeline:
299
+ # πŸ” Repeat Group Example
411
300
 
412
301
  ```ts
413
- import {
414
- migrateLegacySchema,
415
- parseReportTemplateSchema,
416
- } from "@frt-platform/report-core";
417
-
418
- const legacy = {
419
- title: "Old template",
420
- description: "Using flat fields",
302
+ {
303
+ id: "injured",
304
+ type: "repeatGroup",
305
+ label: "Injured people",
306
+ min: 1,
307
+ max: 5,
421
308
  fields: [
422
- { id: "summary", type: "textarea", label: "Summary" },
423
- { id: "status", type: "dropdown", label: "Status", options: ["Open", "Closed"] },
424
- ],
425
- };
426
-
427
- const migrated = migrateLegacySchema(legacy);
428
- const parsed = parseReportTemplateSchema(migrated);
429
-
430
- console.log(parsed.sections.length); // 1
431
- console.log(parsed.sections[0].fields[0].type); // "longText"
309
+ { id: "name", type: "shortText", label: "Name", required: true },
310
+ { id: "injury", type: "longText", label: "Injury description" }
311
+ ]
312
+ }
432
313
  ```
433
314
 
434
- You don’t normally need to call `migrateLegacySchema` yourself – `parseReportTemplateSchema` already does:
315
+ Response shape:
435
316
 
436
317
  ```ts
437
- // internally:
438
- const migrated = migrateLegacySchema(raw);
439
- const parsed = ReportTemplateSchemaValidator.parse(migrated);
318
+ injured: Array<{ name: string; injury?: string }>;
440
319
  ```
441
320
 
442
- ---
321
+ ### πŸ“ repeatGroup behavior & limitations
443
322
 
444
- ## Diffing templates
323
+ * **Base constraints**
445
324
 
446
- When you version templates (e.g. editing in a UI), you might want to see what changed between versions. The core exposes a diff utility:
325
+ * `min` / `max` are always enforced on the row array.
326
+ * Each row is an object keyed by nested field IDs.
447
327
 
448
- ```ts
449
- import { diffTemplates } from "@frt-platform/report-core";
328
+ * **Conditional `minIf` / `maxIf`**
450
329
 
451
- const beforeTemplate = /* old template */;
452
- const afterTemplate = /* updated template */;
330
+ * If `minIf` / `maxIf` are present, they are evaluated against the **current response**.
331
+ * When the condition is `true`, the conditional value overrides the static `min` / `max` for that validation pass.
332
+ * When the condition is `false`, the engine falls back to the static `min` / `max` (if any).
453
333
 
454
- const diff = diffTemplates(beforeTemplate, afterTemplate);
334
+ * **Conditional logic inside rows**
455
335
 
456
- console.log(diff.addedSections);
457
- console.log(diff.removedSections);
458
- console.log(diff.reorderedSections);
459
- console.log(diff.modifiedSections);
336
+ * Nested fields in a repeatGroup support the same `visibleIf` / `requiredIf` semantics as top-level fields.
337
+ * Hidden nested fields are treated as **optional** and are **stripped** from the parsed response, just like hidden top-level fields.
338
+ * For now, row-level conditions see the **full response object**, not just the row. This matches top-level behavior and keeps the logic model simple.
460
339
 
461
- console.log(diff.addedFields);
462
- console.log(diff.removedFields);
463
- console.log(diff.reorderedFields);
464
- console.log(diff.modifiedFields);
465
- ```
340
+ * **JSON Schema export**
466
341
 
467
- `diff.modifiedFields` contains entries like:
468
-
469
- ```ts
470
- {
471
- sectionId: "section-overview",
472
- fieldId: "summary",
473
- before: { label: "What happened?", required: false, ... },
474
- after: { label: "What happened exactly?", required: true, ... },
475
- changes: [
476
- { key: "label", before: "What happened?", after: "What happened exactly?" },
477
- { key: "required", before: false, after: true },
478
- ],
479
- }
480
- ```
481
-
482
- Use this for:
483
-
484
- * β€œChanges” tabs in your UI
485
- * Migration & compatibility warnings
486
- * Audit trails
487
- * Showing ordered changes (section/field reorder is explicitly tracked)
342
+ * repeatGroup constraints and conditions are exported via `x-frt-*` vendor extensions (e.g. `x-frt-minIf`, `x-frt-maxIf`, `x-frt-visibleIf`, `x-frt-requiredIf`), so you can mirror this behavior in other runtimes.
488
343
 
489
344
  ---
490
345
 
491
- ## JSON Schema export
492
-
493
- You can export a template as a **JSON Schema** object for use with OpenAPI, Swagger, Postman, or other runtimes that don’t speak TypeScript/Zod.
346
+ # 🧩 Field Registry (Custom Types)
494
347
 
495
348
  ```ts
496
- import { exportJSONSchema } from "@frt-platform/report-core";
349
+ import { FieldRegistry } from "@frt-platform/report-core";
350
+ import { z } from "zod";
497
351
 
498
- const jsonSchema = exportJSONSchema(template);
352
+ FieldRegistry.register("richText", {
353
+ defaults: { label: "Details" },
354
+ buildResponseSchema(field) {
355
+ let schema = z.string();
356
+ if (field.minLength) schema = schema.min(field.minLength);
357
+ return field.required ? schema : schema.optional();
358
+ },
359
+ });
499
360
  ```
500
361
 
501
- Shape (simplified):
362
+ Now templates may include fields like:
502
363
 
503
- ```json
504
- {
505
- "$schema": "https://json-schema.org/draft/2020-12/schema",
506
- "type": "object",
507
- "properties": {
508
- "summary": { "type": "string", "minLength": 10 },
509
- "status": { "type": "string", "enum": ["Pending", "Resolved", "Escalated"] }
510
- },
511
- "required": ["summary", "status"],
512
- "additionalProperties": false,
513
- "x-frt-templateTitle": "Incident follow-up",
514
- "x-frt-version": 1
515
- }
364
+ ```ts
365
+ { id: "body", type: "richText", label: "Report body" }
516
366
  ```
517
367
 
518
- Key points:
519
-
520
- * String/number/boolean/array mapping matches the runtime validation semantics.
521
- * `required` only includes **unconditionally required** fields (no `visibleIf` / `requiredIf`), but we expose conditional logic as vendor extensions.
522
- * Extra metadata is available under `x-frt-*` keys, e.g. `x-frt-placeholder`, `x-frt-dataClassification`, `x-frt-visibleIf`, `x-frt-requiredIf`.
368
+ If a template uses a field type that is **not** registered and not one of the built-in core types, the engine safely falls back to `z.any()` so unknown types never crash validation.
523
369
 
524
370
  ---
525
371
 
526
- ## API Overview
527
-
528
- ### Types
372
+ # 🧾 JSON Schema Export
529
373
 
530
374
  ```ts
531
- import type {
532
- ReportTemplateSchema,
533
- ReportTemplateSection,
534
- ReportTemplateField,
535
- ReportTemplateFieldType,
536
- InferResponse,
537
- } from "@frt-platform/report-core";
538
- ```
539
-
540
- * `ReportTemplateFieldType` – union of all field type strings
541
- * `ReportTemplateField` – one question/field in a section
542
- * `ReportTemplateSection` – a logical grouping of fields
543
- * `ReportTemplateSchema` – full template
544
- * `InferResponse<TTemplate>` – builds a typed response object from a static template
545
-
546
- ### Constants
375
+ import { exportJSONSchema } from "@frt-platform/report-core";
547
376
 
548
- ```ts
549
- import {
550
- REPORT_TEMPLATE_VERSION,
551
- REPORT_TEMPLATE_FIELD_TYPES,
552
- DEFAULT_FIELD_LABEL,
553
- CORE_FIELD_DEFAULTS,
554
- } from "@frt-platform/report-core";
377
+ const jsonSchema = exportJSONSchema(template);
555
378
  ```
556
379
 
557
- * `REPORT_TEMPLATE_VERSION: number`
558
- * `REPORT_TEMPLATE_FIELD_TYPES: readonly ReportTemplateFieldType[]`
559
- * `DEFAULT_FIELD_LABEL: string`
560
- * `CORE_FIELD_DEFAULTS: Record<ReportTemplateFieldType, Record<string, unknown>>`
380
+ Produces JSON Schema with:
561
381
 
562
- ### Validation & parsing
382
+ * field types
383
+ * enums
384
+ * min/max constraints
385
+ * default values
386
+ * conditional logic preserved as custom `x-frt-*` properties
563
387
 
564
- ```ts
565
- import {
566
- ReportTemplateFieldSchema,
567
- ReportTemplateSectionSchema,
568
- ReportTemplateSchemaValidator,
569
- parseReportTemplateSchema,
570
- parseReportTemplateSchemaFromString,
571
- normalizeReportTemplateSchema,
572
- serializeReportTemplateSchema,
573
- } from "@frt-platform/report-core";
574
- ```
388
+ ---
575
389
 
576
- Use the Zod schemas directly if you want:
390
+ # πŸ” Diff Templates
577
391
 
578
392
  ```ts
579
- import { ReportTemplateSchemaValidator } from "@frt-platform/report-core";
393
+ import { diffTemplates } from "@frt-platform/report-core";
580
394
 
581
- const result = ReportTemplateSchemaValidator.safeParse(raw);
582
- if (!result.success) {
583
- // handle validation errors
584
- }
395
+ const diff = diffTemplates(oldTemplate, newTemplate);
585
396
  ```
586
397
 
587
- ### Migration
398
+ Detects:
588
399
 
589
- ```ts
590
- import { migrateLegacySchema } from "@frt-platform/report-core";
591
- ```
400
+ * added/removed/reordered sections
401
+ * added/removed/reordered fields
402
+ * modified fields
403
+ * nested diffs for repeat groups
592
404
 
593
- Takes `unknown` legacy input and returns a shape better suited for current schema validation.
405
+ Perfect for:
594
406
 
595
- ### Response validation
407
+ * Version history
408
+ * Audit logs
409
+ * Template editing UI
596
410
 
597
- ```ts
598
- import {
599
- validateReportResponse,
600
- buildResponseSchemaWithConditions,
601
- type InferResponse,
602
- } from "@frt-platform/report-core";
603
- ```
411
+ ---
412
+
413
+ # 🧬 Type Inference
604
414
 
605
- ### Template diffing
415
+ Given a template:
606
416
 
607
417
  ```ts
608
- import { diffTemplates } from "@frt-platform/report-core";
418
+ export const myTemplate = {
419
+ version: 1,
420
+ sections: [
421
+ {
422
+ id: "s",
423
+ fields: [
424
+ { id: "title", type: "shortText", required: true },
425
+ { id: "tags", type: "multiSelect", options: ["A", "B"] },
426
+ ]
427
+ }
428
+ ]
429
+ } as const;
609
430
  ```
610
431
 
611
- ### JSON Schema export
432
+ Infer response type:
612
433
 
613
434
  ```ts
614
- import { exportJSONSchema } from "@frt-platform/report-core";
435
+ type MyResponse = InferResponse<typeof myTemplate>;
615
436
  ```
616
437
 
617
- ### Field registry
438
+ Produces:
618
439
 
619
440
  ```ts
620
- import { FieldRegistry } from "@frt-platform/report-core";
441
+ type MyResponse = {
442
+ title: string;
443
+ tags?: ("A" | "B")[];
444
+ };
621
445
  ```
622
446
 
623
447
  ---
624
448
 
625
- ## Integration Ideas
626
-
627
- This package is intentionally minimal. You can layer it into:
449
+ # 🧾 Serialization
628
450
 
629
- * **Next.js / React** apps for:
630
-
631
- * Template builders
632
- * Dynamic report forms
633
- * Admin panels for managing report templates
634
- * **Node/Edge backends** for:
635
-
636
- * Validating templates on save
637
- * Migrating old template shapes
638
- * Ensuring consistent data before persisting to a DB
639
- * Validating report responses before saving them
640
- * Serving JSON Schema to other services
641
-
642
- Typical pattern in a full stack app:
643
-
644
- 1. **Frontend**
645
-
646
- * Build a visual editor for templates
647
- * Send template JSON to server
648
-
649
- 2. **Backend**
451
+ ```ts
452
+ import { serializeReportTemplateSchema } from "@frt-platform/report-core";
650
453
 
651
- * `parseReportTemplateSchema` to validate templates
652
- * Store `ReportTemplateSchema` JSON in your DB (e.g. Mongo via Prisma)
653
- * On response submission:
454
+ const json = serializeReportTemplateSchema(template, {
455
+ pretty: true,
456
+ sortSectionsById: true,
457
+ sortFieldsById: true,
458
+ });
459
+ ```
654
460
 
655
- * Load template from DB
656
- * `validateReportResponse(template, data)` before insert
461
+ Useful for deterministic output in Git and stable diffs across environments.
657
462
 
658
- 3. **Runtime**
463
+ ---
659
464
 
660
- * Load templates from DB
661
- * Use them to render dynamic forms
662
- * Use `validateReportResponse` / `buildResponseSchemaWithConditions` to validate and normalize responses
663
- * Use `exportJSONSchema` for interoperability with other stacks
465
+ # 🧱 Roadmap
664
466
 
665
- ---
467
+ ### Phase 1 β€” Core Maturation (βœ”οΈ COMPLETE)
666
468
 
667
- ## Roadmap / Extensions (ideas)
469
+ * Validation
470
+ * Conditional logic
471
+ * Diffing
472
+ * Field Registry
473
+ * Error helpers
474
+ * Serialization features
475
+ * Parsing & normalization helpers
668
476
 
669
- Things you might add on top (in a separate package, e.g. `@frt-platform/report-react`):
477
+ ### Phase 2 β€” Advanced Field System (IN PROGRESS)
670
478
 
671
- * React components for:
479
+ * Richer repeatGroup UX
480
+ * Computed fields (design)
481
+ * RichText / FileUpload via registry
672
482
 
673
- * Template builder UI
674
- * Field editors/renderers (short text, selects, etc.)
675
- * Auto-generated `<ReportForm />` that consumes a template
676
- * More advanced field types:
483
+ ### Phase 3 β€” Reactions & Analytics (Planned)
677
484
 
678
- * File uploads
679
- * Signature pads
680
- * Matrix / grid fields
681
- * Fully-featured `repeatGroup` UI & runtime
682
- * Richer conditional logic:
485
+ * Scoring rules
486
+ * Auto-tagging
487
+ * Suggested outcomes
683
488
 
684
- * `visibleIf`
685
- * `requiredIf`
686
- * multi-step branching
687
- * Multi-language labels & descriptions
489
+ ### Phase 4 β€” React UI Package (Planned)
688
490
 
689
- The core stays simple: **schema + validation + conditional logic + migration + diffing + registry + JSON Schema export**.
491
+ * Form renderer
492
+ * Template builder
493
+ * Field palette
494
+ * Full ShadCN integration
690
495
 
691
496
  ---
692
497
 
693
- ## License
498
+ # πŸ“„ License
694
499
 
695
- MIT – use it in your projects, commercial or not. Contributions welcome.
500
+ MIT β€” feel free to use, extend, or contribute.