dills122

Formly Contract

Community dills122
Updated

Compile Angular Formly configurations into deterministic, agent-readable contracts.

Formly Contract

Formly Contract turns Angular Formly field configuration into stable,versioned JSON that an E2E test author or coding agent can understand withoutguessing how a form is structured.

Given a FormlyFieldConfig[], the adapter describes:

  • the controls, display content, groups, and repeatable templates in the form;
  • each field's model path, Formly type, label, constraints, and choices;
  • known visibility, required, readonly, disabled, and dynamic-option behavior;
  • exact or application-derived test locators such as data-testid,data-test-id, and data-cy;
  • what came directly from configuration, what was resolved by a controlledFormly build, and what remains unknown; and
  • stable diagnostics for behavior that cannot be represented safely.

The result is a deterministic Form Contract with strict runtime validation,canonical serialization, and a content hash. The contract is intended to be areliable input for Cypress/Playwright test planning and future agent tooling. Itis not a dump of Formly's live runtime objects.

What exists today

This repository currently provides schema v0.3 and two workspace packages:

Package Purpose
@formly-contract/contract-schema Contract DTOs, runtime validation, canonical JSON, and SHA-256 content hashing
@formly-contract/formly-adapter Safe declared extraction and trusted scenario compilation for Formly 6.1

It also includes:

  • a deterministic CLI demo using a synthetic golden form;
  • a browser-rendered Angular test application with twelve synthetic Formlyfixtures; and
  • compatibility coverage for the pinned Angular 20.3.29 and Formly 6.1.8combination.

The parser and contract are the current product. A production MCP server,automatic Playwright generation, browser observation, and application-sourcediscovery are future layers and are not shipped by this MVP.

Use it in your own Angular/Formly codebase

The package runs as build/test tooling beside your Angular application. It doesnot need to be added to the application's browser bundle. A typical adoptionflow is:

application-owned Formly factories
              |
     generation script or CI job
              |
       versioned contract JSON
              |
 Playwright / Cypress / agent tooling

1. Add the packages

The packages are not published to npm yet. Until the first release, clone thisrepository next to the consuming application and build the two packages:

git clone https://github.com/dills122/formly-contract.git
cd formly-contract
pnpm install --frozen-lockfile
pnpm --filter @formly-contract/contract-schema build
pnpm --filter @formly-contract/formly-adapter build

Then link them from the consuming application's package.json (adjust therelative path for your checkout):

{
  "devDependencies": {
    "@formly-contract/contract-schema": "link:../formly-contract/packages/contract-schema",
    "@formly-contract/formly-adapter": "link:../formly-contract/packages/formly-adapter"
  }
}

Run pnpm install in the consuming application. The application must alreadyprovide compatible Angular and Formly peer dependencies; the currently testedcombination is Angular 20.3.29 with Formly 6.1.8. Once the packages arepublished, normal versioned pnpm add --save-dev dependencies will replacethese local links.

2. Select the forms to expose

Application-source discovery is deliberately not automatic. Create a small,application-owned registry that imports only the form factories you want thecontract generator to inspect:

// tools/contract-forms.ts
import type { FormlyFieldConfig } from '@ngx-formly/core';
import { createClaimFields } from '../src/app/claims/claim.fields';
import { createCustomerFields } from '../src/app/customers/customer.fields';

export interface ContractFormTarget {
  id: string;
  createFields: () => FormlyFieldConfig[];
}

export const contractForms: ContractFormTarget[] = [
  { id: 'claims.create', createFields: () => createClaimFields() },
  { id: 'customers.edit', createFields: () => createCustomerFields() },
];

Each factory should return a fresh field tree. If a factory needs applicationinputs, wrap it in a closure with synthetic values that are safe to use inlocal development and CI.

3. Generate contract artifacts

Add a build-time script in the application repository:

// tools/generate-form-contracts.ts
import { mkdir, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { canonicalStringify } from '@formly-contract/contract-schema';
import { extractFormContract } from '@formly-contract/formly-adapter';
import { contractForms } from './contract-forms';

const outputDirectory = resolve('artifacts/form-contracts');
await mkdir(outputDirectory, { recursive: true });

for (const target of contractForms) {
  const { contract, diagnostics } = extractFormContract({
    formId: target.id,
    fields: target.createFields(),
  });

  await writeFile(
    resolve(outputDirectory, `${target.id}.json`),
    `${canonicalStringify(contract)}\n`,
  );

  console.log(
    `${target.id}: ${contract.nodes.length} root nodes, ${diagnostics.length} diagnostics`,
  );
}

Run this file with the TypeScript runner already used by the consumingrepository, or compile it as part of a Node-targeted tooling project. Theresulting JSON can be committed for review, uploaded as a CI artifact, or readby downstream test-authoring tools. Because it is canonical and content-hashed,an unexpected form-contract change is visible in source control or CI.

This declared path is the best starting point. It captures static structure andrecords expression callbacks as dynamic metadata without executing arbitraryapplication code.

4. Use a contract in Playwright

Validate stored JSON before trusting it, find the semantic node you need, anduse one of its exact locator candidates. For a standard data-testid locator:

import { readFile } from 'node:fs/promises';
import {
  parseFormContract,
  type ContractNode,
  type ModelPathSegment,
} from '@formly-contract/contract-schema';

function findNodeByPath(
  nodes: readonly ContractNode[],
  modelPath: readonly ModelPathSegment[],
): ContractNode | undefined {
  for (const node of nodes) {
    if (
      node.modelPath.length === modelPath.length &&
      node.modelPath.every((segment, index) => segment === modelPath[index])
    ) {
      return node;
    }

    const nested = findNodeByPath(
      node.arrayTemplate
        ? [...node.children, node.arrayTemplate]
        : node.children,
      modelPath,
    );
    if (nested) return nested;
  }
}

const contract = parseFormContract(
  JSON.parse(
    await readFile('artifacts/form-contracts/claims.create.json', 'utf8'),
  ),
);

const claimantName = findNodeByPath(contract.nodes, ['claimant', 'name']);

const testId = claimantName?.locators.find(
  (locator) =>
    locator.strategy === 'testId' && locator.attribute === 'data-testid',
);

if (!claimantName || !testId) {
  throw new Error('claimant.name has no exact data-testid locator');
}

await page.getByTestId(testId.value).fill('Ada Lovelace');

Real consumers will normally put recursive node lookup and locator selection ina shared Playwright or Cypress helper. Composite controls can expose severallocator targets, so helpers should select by target rather than assuming oneFormly node always maps to one DOM element. Empty locator arrays and diagnosticsmust be handled as missing evidence, not replaced with invented selectors.

5. Resolve dynamic behavior when needed

If expressions determine visibility, required/readonly state, or option lists,add synthetic scenarios and call compileFormContractScenario. Run that API ina trusted Angular test/build environment configured with the application's realFormly modules and custom types. Generate one artifact per meaningful scenario,using only synthetic model and form-state data.

The synthetic compatibility harnessshows the complete Angular TestBed setup for obtaining aFormlyFormBuilder. The detailed API example below shows the scenario call.

Why this is useful

Large Formly forms are often assembled from nested groups, shared fragments,custom field types, expressions, dynamic choices, and application conventions.Reading that source repeatedly is slow, and guessing from a rendered page leadsto brittle tests.

This project creates a small, explicit boundary:

Formly fields + synthetic scenario
                |
       safe contract projection
                |
   deterministic versioned JSON
                |
 E2E planning / agent inspection

Consumers can inspect one contract to answer questions such as:

  • Which controls exist, and in what order?
  • What model value does each control edit?
  • Which values and validation boundaries are known?
  • Is a choice list empty, static, dynamic, or asynchronous?
  • Which fields may be hidden, required, readonly, or disabled?
  • Which data-*, role, label, placeholder, or DOM-ID locator candidates areavailable?
  • Which facts are exact, derived, resolved for one scenario, or still unknown?

Try this repository

Prerequisites:

  • Node.js 22.22.1
  • pnpm 10.23.0
pnpm install --frozen-lockfile
pnpm demo

pnpm demo builds the package slice and prints one canonical JSON contract.Run the complete repository gate with:

pnpm check

That command runs lint, all tests, package and Angular production builds, thedemo smoke test, and documentation checks.

Extract declared form structure

Use extractFormContract when you have Formly configuration and want to inspectit without running callbacks:

import { extractFormContract } from '@formly-contract/formly-adapter';
import type { FormlyFieldConfig } from '@ngx-formly/core';

const fields: FormlyFieldConfig[] = [
  {
    key: 'profile.name',
    type: 'input',
    props: {
      label: 'Name',
      required: true,
      attributes: { 'data-testid': 'profile-name' },
    },
  },
];

const { contract, diagnostics } = extractFormContract({
  formId: 'example.profile',
  fields,
});

This path is pure and non-mutating. It does not call expression functions,subscribe to Observables, run validators, or render Angular components.Recognized callbacks become dynamic-rule metadata; unsupported behavior becomesan explicit diagnostic. The returned node has the stable IDexample.profile::path:s_profile.s_name, model path ['profile', 'name'], itsrequired constraint, and an exact data-testid locator.

Resolve a synthetic scenario

Use compileFormContractScenario when required, readonly, disabled, hidden,options, or locator attributes depend on Formly expression callbacks:

import { inject } from '@angular/core';
import { FormlyFormBuilder } from '@ngx-formly/core';
import { compileFormContractScenario } from '@formly-contract/formly-adapter';

const builder = inject(FormlyFormBuilder);
const { contract, diagnostics } = compileFormContractScenario({
  formId: 'example.profile',
  builder,
  createFields: () => createProfileFields(),
  model: { contactMethod: 'email' },
  formState: { readonly: false },
});

This is a trusted build/CI API. It uses the application's configuredFormlyFormBuilder, so application and Formly callbacks may run. The model andform state must be structured-cloneable; both are cloned before the fieldfactory or builder runs.

The built field tree still passes through the same allowlist as declaredextraction. For example, dynamic options are reduced to publiclabel/value/disabled records rather than copying arbitrary properties fromapplication objects.

Do not expose this compiler directly from an MCP or other untrusted requesthandler. Query layers should read previously generated contract artifacts.

Test locators

Every node has an ordered locators array. The adapter automatically readsthese common attributes from props.attributes:

  • data-testid
  • data-test-id
  • data-test
  • data-cy
  • data-pw

It can also retain explicit role, accessible name, placeholder, and Formlyfield-ID candidates. An empty array means no reliable locator was found; theadapter never invents CSS or XPath.

Applications with their own naming convention can set testIdAttributes andprovide a deterministic deriveLocators callback. The callback receives onlyfrozen identity data, not the live Formly field. It may return several namedtargets for a composite widget such as a date range; its output is markedconfidence: "derived". See thev0.3 locator specification for the completecontract and example.

Evidence model

The contract keeps three evidence levels separate:

Evidence Meaning Available now?
declared Read safely from supplied Formly configuration Yes
resolved Read from a controlled Formly build for one synthetic scenario Yes
observed Seen in a real rendered browser DOM Schema-ready; capture layer not implemented

A resolved locator is not silently presented as browser-observed. Likewise,opaque or asynchronous behavior is reported rather than guessed.

Supported contract information

Schema v0.3 can represent:

  • ordered controls, groups, display-only nodes, and array templates;
  • stable semantic node IDs and cumulative model paths;
  • Formly and common semantic control types;
  • labels, descriptions, placeholders, JSON-safe defaults, and wrappers;
  • required, min/max, length, string-pattern, and named constraints;
  • static and resolved public options plus dynamic/async option-source metadata;
  • string/boolean conditions and callback/async dynamic-rule metadata;
  • resolved hidden, readonly, and disabled state;
  • exact and derived locator candidates, including multiple named targets; and
  • deterministic diagnostics, canonical JSON, and content hashing.

Intentional limitations

  • Forms must be supplied explicitly; the adapter does not discover arbitraryTypeScript exports or application routes.
  • Declared extraction never evaluates functions or function source.
  • The scenario compiler performs the initial controlled Formly build but doesnot wait for remote options or lifecycle-driven browser behavior.
  • Formly RegExp patterns are diagnosed; v0.3 represents string patterns only.
  • Custom widget actions and value codecs are not yet modeled.
  • The project does not currently generate or execute Cypress/Playwright tests.
  • No production MCP server or browser-observation layer is included.
  • Compatibility is proven for Angular 20.3.29 with Formly 6.1.8, not forevery Angular/Formly combination.
  • npm publication and release automation are not included yet.

Synthetic test application

The Angular test application contains twelve invented forms covering nativeand custom fields, wrappers, validators, extensions, presets, expressions,validation, repeaters, opaque behavior, and legacy Formly v6 aliases.

pnpm app:serve

Open http://127.0.0.1:4200/ and choose a fixture from the catalog.

Workplace forms and data should remain in a private work repository. A privatefixture module can implement TestFormDefinition and register a group throughTEST_FORM_GROUPS without copying workplace labels, identifiers, options, orrules into this public project.

Repository layout

packages/
  contract-schema/   Versioned DTOs, validation, canonical JSON, and hashing
  formly-adapter/     Declared extraction and trusted Formly scenario builds
fixtures/
  synthetic-form/    Public golden form and real-builder compatibility fixture
apps/
  demo-cli/          Prints the deterministic golden contract
  formly-test-app/   Browser-rendered Angular/Formly fixture catalog
docs/                Specifications, ADRs, delivery plans, and evidence

Roadmap

The intended delivery path is:

Form Contract packages (current)
              |
      read-only MCP queries
              |
        typed E2E intent
              |
 deterministic Playwright/Cypress drivers
              |
 browser observation and parity checks

Future layers should consume immutable contracts. They should not move Angularexecution, arbitrary callback evaluation, or selector invention into routineagent requests.

Documentation

  • Architecture overview
  • MVP specification
  • v0.2 real-world semantics specification
  • v0.3 test locator specification
  • Formly test application specification
  • Implementation plan
  • Architecture decisions

Contributing and security

Contributions are welcome. Read CONTRIBUTING.md and theCode of Conduct before participating. Report securityissues through the private process described in SECURITY.md.

This project is available under the MIT License.

MCP Server ยท Populars

MCP Server ยท New

    PSU3D0

    agent-spreadsheet

    MCP server for spreadsheet analysis and editing. Slim, token-efficient tool surface designed for LLM agents.

    Community PSU3D0
    pitiflautico

    NeoBrowser

    MCP server that drives real Chrome with your real logged-in sessions โ€” genuine fingerprint (passes bot.sannysoft), human-like input, bot-wall aware. 43 tools, single static Rust binary.

    Community pitiflautico
    aeonfun

    Aeon MCP Server

    The most autonomous AI agent framework: runs unattended on GitHub Actions, self-healing skills, drives Claude Code, Grok, Codex & more. No approval loops. Configure once, forget forever.

    Community aeonfun
    nhadaututtheky

    NeuralMemory

    NeuralMemory stores experiences as interconnected neurons and recalls them through spreading activation, mimicking how the human brain works. Instead of searching a database, memories are retrieved through associative recall - activating related concepts until the relevant memory emerges.

    Community nhadaututtheky
    norrietaylor

    Distillery

    Team knowledge evaporates daily โ€” pairing sessions, debugging context, architectural rationale lost to Slack. Distillery captures it at the point of creation, connects it into a living graph, and surfaces it conversationally. It monitors feeds, tracks what matters to your projects, and alerts you before you know to ask. A team brain that learns.

    Community norrietaylor