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.4 and three packages:

Package Purpose
@formly-contract/schema Contract DTOs, runtime validation, canonical JSON, and SHA-256 content hashing
@formly-contract/compiler Safe declared extraction and trusted scenario compilation for Formly 6.x
@formly-contract/workspace Experimental trusted config loading, strict root/project/source descriptors, policy resolution, and deterministic multi-project artifact generation

It also includes:

  • a deterministic CLI demo using a synthetic golden form;
  • a browser-rendered Angular test application with twelve synthetic Formlyfixtures;
  • two consumer-shaped monorepo anchors: a deep Angular CLI behavior corpus anda real Nx workspace with an app, base Formly setup, reusable custom-fieldlibrary, consuming feature library, project graph, and cached build;
  • a supported usage target of Angular 20 or newer with Formly 6.x; and
  • deep compatibility coverage for the pinned Angular 20.3.29 and Formly6.1.8 reference combination.

The parser, contract, programmatic workspace runner, and pilot generate CLIare the current product. A production MCP server, automatic Playwrightgeneration, browser observation, Angular-assisted application-sourcediscovery, and the remaining generic CLI commands are future layers and are notshipped by this MVP.

Evaluating the current product in a private application repository? Start withthe workplace pilot guide. It provides one completesetup, configuration, generation, troubleshooting, privacy, and feedback pathfor a fresh coding agent or maintainer.

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 three packages:

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

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

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

Run pnpm install in the consuming application. The supported usage target isAngular 20 or newer with Formly 6.x. The compatibility suite is pinned toAngular 20.3.29 with Formly 6.1.8, so that exact pairing has the strongesttest evidence. Other Angular major and Formly minor or patch combinations maywork, but validate them in the consuming application because they are notcovered to the same depth. Once the packages are published, normal versionedpnpm add --save-dev dependencies will replace these 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.

For a repository-aware pilot, follow theworkplace pilot guide, using theworkspace configuration reference when youneed the full configuration semantics, then run:

pnpm exec formly-contracts generate

The command discovers every configured project and bulk source, writescontent-addressed contracts, and publishes workspace-index.json last. Themanual script below remains useful for a single-package or one-off extraction.

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/schema';
import { extractFormContract } from '@formly-contract/compiler';
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/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,release metadata and tarball checks, the demo smoke test, and documentationchecks. Maintainers can find the tag, npm trusted-publishing, and first-releaseprocedure in Releasing.

Extract declared form structure

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

import { extractFormContract } from '@formly-contract/compiler';
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.

For application-owned custom fields, pass the canonicalfieldTypeProfiles bundle from resolved project configuration. Mapped nodesthen include reviewed roles, parts, interaction operations, driver identity,possible values when safely enumerable, and provenance. A custom type orwrapper that is not registered remains visible but non-operable and produces astable diagnostic. Named variants use the data-only root field metadataformlyContract: { profileVariant: 'variant-name' }; fields cannot embedarbitrary interaction overrides.

Static radio/select options and boolean controls expose complete value domains.Unresolved option functions, expressions, and asynchronous sources remaindynamic and are never executed by declared extraction. Trusted scenarioextraction may expose the scenario-specific resolved domain.

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/compiler';

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. Field IDs are derived hints because acustom type may render the ID on a wrapper. IDs declared inside fieldArraytemplates are omitted with an UNRELIABLE_DOM_ID diagnostic because runtimerows may omit or index them.

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.4 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;
  • explicit finite, dynamic, and unknown value domains plus resolvedinteraction-profile 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.4 represents string patterns only.
  • Custom widget interaction metadata requires an application-supplied,serializable field-type profile registry. Profile-aware extraction isimplemented; executable application drivers remain a later integration.
  • The project does not currently generate or execute Cypress/Playwright tests.
  • No production MCP server or browser-observation layer is included.
  • The supported usage target is Angular 20 or newer with Formly 6.x, butcompatibility testing is concentrated on Angular 20.3.29 with Formly6.1.8. Mileage may vary across other major, minor, and patch combinations.
  • The packages are not on npm yet. Their @formly-contract/* names are settled,but the one-time npm bootstrap release still needs maintainer approval; theautomated release path is ready and documented inReleasing.

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/
  schema/            Versioned DTOs, validation, canonical JSON, and hashing
  compiler/          Declared extraction and trusted Formly scenario builds
  workspace/         Config loading, discovery, artifact runner, index, and CLI
fixtures/
  synthetic-form/    Public golden form and real-builder compatibility fixture
  angular-monorepo/  Deep six-form Angular CLI discovery/interaction corpus
  nx-workspace/      Real four-project Nx graph, build, and cache anchor
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

  • Workplace pilot guide
  • Workspace configuration reference
  • Architecture overview
  • MVP specification
  • v0.2 real-world semantics specification
  • v0.3 test locator specification
  • Formly test application specification
  • Parser MVP implementation plan
  • Workspace discovery implementationplan
  • 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

    chatmcp

    3802

    directory for Awesome MCP Servers

    Community chatmcp
    Morningstar202604

    AgentSeed

    Anti-hallucination gate for AI coding agents โ€” 8 MCP tools catch invented APIs (17 languages), fake "all tests pass" claims, and slopsquatting packages before they ship. Zero-dependency Agent Plugins 1.0.0 plugin (Skill + MCP server + CLI + CI gate) for Claude Code, Cursor, VS Code, Copilot.

    Community Morningstar202604
    skarn-security

    Skarn guard: agent plugins

    Skarn plugins for Claude Code, Codex CLI, Gemini CLI, Grok Build, and Antigravity: audit skills, guard hooks, and MCP declarations that find leaked secrets and credentials in AI coding sessions, locally and redacted.

    Community skarn-security
    punkpeye

    mcp-remote

    Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server.

    Community punkpeye
    HiAi-gg

    DocsMint

    Self-hosted AI-native knowledge workspace and installable PWA with hybrid search, GraphRAG, REST, SDK, CLI, and MCP access for people and AI agents.

    Community HiAi-gg