npm package: @frt-platform/report-core
Description: Core report template engine: schema, validation, normalization, and migration.
Author: Sebastian Mostert
Latest version: 1.5.1

# `@frt-platform/report-core`

Core engine for defining, validating, migrating, diffing, and serializing **dynamic report templates**.

This package is:

* 🧠 **Framework-agnostic**
* 🧩 **UI-agnostic**
* 🔒 **Safe by design (Zod-based)**
* 🧱 **The foundation of the FRT incident & reporting platform**

It contains **no React**, **no database code**, and **no styling**.  
You can use it in **Node**, **Next.js**, **backend services**, or custom form engines.

---

# ✨ Features

### 📄 Template Schema

* Sections → fields
* Field IDs, labels, descriptions, placeholders
* Built-in field types:

  * `shortText`, `longText`
  * `number`
  * `date`
  * `checkbox`
  * `singleSelect`, `multiSelect`
  * `repeatGroup` (nested fieldsets)

### 🎛 Field Constraints

* min/max length (`shortText`, `longText`)
* min/max value (`number`)
* min/max selections (`multiSelect`)
* allowed options
* checkbox required semantics
* default values

### 🔀 Conditional Logic

* `visibleIf`
* `requiredIf`
* Supports:
  `equals`, `any`, `all`, `not`
* Fully integrated into validation.

### 🔥 Validation Engine

* Build response schema dynamically with conditions
* DX helper: `buildResponseSchema(template)` (no response needed)
* Throwing API: `validateReportResponse()`
* Non-throwing API: `validateReportResponseDetailed()`
* Rich `ResponseFieldError` objects with:

  * section title
  * field label
  * error code
  * full message
  * nested repeatGroup row context

### 🔄 Template Migration & Normalization

* Legacy format migration (`fields → sections`)
* Automatic ID normalization & uniqueness enforcement
* Stable `"lowercase_with_underscores"`-style IDs
* Fallback IDs for missing values: `section_1`, `field_1`, etc.

### 🔍 Schema Diffing

Detect:

* Added / removed / reordered sections
* Added / removed / reordered fields
* Modified fields
* Nested diffs inside repeat groups

### 📦 JSON Schema Export

* Export a template as a valid JSON Schema (2020-12 draft)
* Includes vendor extensions:

  * `x-frt-visibleIf`
  * `x-frt-requiredIf`
  * `x-frt-minIf` / `x-frt-maxIf` for repeatGroup
  * placeholders
* Useful for OpenAPI, Postman, or other backend runtimes.

### 🏗 Field Registry

Extend the system at runtime:

* Add custom types (`richText`, `fileUpload`, etc.)
* Override validation logic
* Provide metadata for UI packages
* Unknown/unregistered field types safely fall back to `z.any()` so they never break the core engine.

### 🧬 Type Inference

Get a fully typed response type from a template:

```ts
type MyResponse = InferResponse<typeof template>;
````

### 🧾 Serialization Helpers

Deterministic JSON output with sorting options:

```ts
serializeReportTemplateSchema(template, {
  pretty: true,
  sortSectionsById: true,
  sortFieldsById: true,
});
```

Perfect for Git diffs and storage.

---

# 📦 Installation

```bash
npm install @frt-platform/report-core zod
```

`zod` is a peer dependency.

---

# 🧹 Parsing & Normalization

The core exposes helpers for safely **parsing**, **migrating**, and **normalizing** templates.

```ts
import {
  parseReportTemplateSchema,
  parseReportTemplateSchemaFromString,
} from "@frt-platform/report-core";

// From a raw JS object (e.g. loaded from DB, file, etc.)
const template = parseReportTemplateSchema(rawTemplateObject);

// From a JSON string
const templateFromString = parseReportTemplateSchemaFromString(jsonString);
```

Under the hood, `parseReportTemplateSchema` does:

1. **Legacy migration**

   * Accepts both the new `{ sections: [...] }` format and legacy flat:

     ```ts
     {
       version: 1,
       fields: [ /* ... */ ]
     }
     ```

   * Legacy `fields` are automatically wrapped into a single `section_1`.

2. **Schema validation**

   * Uses a Zod validator for `ReportTemplateSchema` to ensure the template is structurally valid.

3. **Normalization**

   * Section and field IDs are normalized to **lowercase** with spaces → underscores:

     * `"My Field!"` → `"my_field"`

   * Invalid characters are stripped (only `[a-z0-9_-]` are kept).

   * IDs are made **unique per namespace** by appending `-1`, `-2`, … as needed:

     * `my_field`, `my_field-1`, `my_field-2`, …

   * Missing IDs get deterministic fallbacks:

     * Sections: `section_1`, `section_2`, …
     * Fields: `field_1`, `field_2`, …

This makes templates safe to store, diff, and round-trip in a stable way.

---

# 🚀 Quickstart

## 1. Define a template

```ts
import type { ReportTemplateSchema } from "@frt-platform/report-core";

const template: ReportTemplateSchema = {
  version: 1,
  sections: [
    {
      id: "general",
      title: "General Info",
      fields: [
        {
          id: "title",
          type: "shortText",
          label: "Incident title",
          required: true,
        },
        {
          id: "severity",
          type: "singleSelect",
          label: "Severity",
          required: true,
          options: ["Low", "Medium", "High"],
        },
        {
          id: "details",
          type: "longText",
          label: "Details",
          minLength: 10,
        },
      ],
    },
  ],
};
```

---

## 2. Validate a response (throwing API)

```ts
import { validateReportResponse } from "@frt-platform/report-core";

const parsed = validateReportResponse(template, {
  title: "Broken fire alarm",
  severity: "High",
  details: "Triggered after smoke test",
});
```

If invalid → throws a ZodError.

---

## 3. Validate without throwing (UI-friendly)

```ts
import { validateReportResponseDetailed } from "@frt-platform/report-core";

const result = validateReportResponseDetailed(template, {
  title: "",
  severity: "High",
});

if (!result.success) {
  console.log(result.errors);
}
```

Produces:

```ts
[
  {
    fieldId: "title",
    sectionId: "general",
    sectionTitle: "General Info",
    label: "Incident title",
    code: "field.too_small",
    message:
      'Section "General Info" → Field "Incident title": String must contain at least 1 character(s).'
  }
]
```

---

# 🔀 Conditional Logic Example

```ts
{
  id: "follow_up_notes",
  type: "longText",
  label: "Follow-up notes",
  visibleIf: { equals: { follow_up_required: true } },
  requiredIf: { equals: { follow_up_required: true } },
}
```

Behavior:

* If `follow_up_required = false` → field is **hidden** and **ignored**
* If `true` → field becomes **required**

---

# 🔁 Repeat Group Example

```ts
{
  id: "injured",
  type: "repeatGroup",
  label: "Injured people",
  min: 1,
  max: 5,
  fields: [
    { id: "name", type: "shortText", label: "Name", required: true },
    { id: "injury", type: "longText", label: "Injury description" }
  ]
}
```

Response shape:

```ts
injured: Array<{ name: string; injury?: string }>;
```

### 📝 repeatGroup behavior & limitations

* **Base constraints**

  * `min` / `max` are always enforced on the row array.
  * Each row is an object keyed by nested field IDs.

* **Conditional `minIf` / `maxIf`**

  * If `minIf` / `maxIf` are present, they are evaluated against the **current response**.
  * When the condition is `true`, the conditional value overrides the static `min` / `max` for that validation pass.
  * When the condition is `false`, the engine falls back to the static `min` / `max` (if any).

* **Conditional logic inside rows**

  * Nested fields in a repeatGroup support the same `visibleIf` / `requiredIf` semantics as top-level fields.
  * Hidden nested fields are treated as **optional** and are **stripped** from the parsed response, just like hidden top-level fields.
  * 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.

* **JSON Schema export**

  * 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.

---

# 🧩 Field Registry (Custom Types)

```ts
import { FieldRegistry } from "@frt-platform/report-core";
import { z } from "zod";

FieldRegistry.register("richText", {
  defaults: { label: "Details" },
  buildResponseSchema(field) {
    let schema = z.string();
    if (field.minLength) schema = schema.min(field.minLength);
    return field.required ? schema : schema.optional();
  },
});
```

Now templates may include fields like:

```ts
{ id: "body", type: "richText", label: "Report body" }
```

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.

---

# 🧾 JSON Schema Export

```ts
import { exportJSONSchema } from "@frt-platform/report-core";

const jsonSchema = exportJSONSchema(template);
```

Produces JSON Schema with:

* field types
* enums
* min/max constraints
* default values
* conditional logic preserved as custom `x-frt-*` properties

---

# 🔍 Diff Templates

```ts
import { diffTemplates } from "@frt-platform/report-core";

const diff = diffTemplates(oldTemplate, newTemplate);
```

Detects:

* added/removed/reordered sections
* added/removed/reordered fields
* modified fields
* nested diffs for repeat groups

Perfect for:

* Version history
* Audit logs
* Template editing UI

---

# 🧬 Type Inference

Given a template:

```ts
export const myTemplate = {
  version: 1,
  sections: [
    {
      id: "s",
      fields: [
        { id: "title", type: "shortText", required: true },
        { id: "tags", type: "multiSelect", options: ["A", "B"] },
      ]
    }
  ]
} as const;
```

Infer response type:

```ts
type MyResponse = InferResponse<typeof myTemplate>;
```

Produces:

```ts
type MyResponse = {
  title: string;
  tags?: ("A" | "B")[];
};
```

---

# 🧾 Serialization

```ts
import { serializeReportTemplateSchema } from "@frt-platform/report-core";

const json = serializeReportTemplateSchema(template, {
  pretty: true,
  sortSectionsById: true,
  sortFieldsById: true,
});
```

Useful for deterministic output in Git and stable diffs across environments.

---

# 🧱 Roadmap

### Phase 1 — Core Maturation (✔️ COMPLETE)

* Validation
* Conditional logic
* Diffing
* Field Registry
* Error helpers
* Serialization features
* Parsing & normalization helpers

### Phase 2 — Advanced Field System (IN PROGRESS)

* Richer repeatGroup UX
* Computed fields (design)
* RichText / FileUpload via registry

### Phase 3 — Reactions & Analytics (Planned)

* Scoring rules
* Auto-tagging
* Suggested outcomes

### Phase 4 — React UI Package (Planned)

* Form renderer
* Template builder
* Field palette
* Full ShadCN integration

---

# 📄 License

MIT — feel free to use, extend, or contribute.