Skip to main content
Glama
Formly Agent Contracts

Formly Agent Contracts

by dills122

Formly Contract

Formly Contract 将 Angular Formly 字段配置转换为稳定、带版本号的 JSON,使 E2E 测试编写者或编码代理无需猜测表单结构即可理解。

给定一个 FormlyFieldConfig[],该适配器描述:

  • 表单中的控件、展示内容、分组和可重复模板;

  • 每个字段的模型路径、Formly 类型、标签、约束和选项;

  • 已知的可见性、必填、只读、禁用和动态选项行为;

  • 精确的或由应用推导出的测试定位器,如 data-testid、data-test-id 和 data-cy;

  • 哪些内容直接来自配置,哪些由受控的 Formly 构建解析得到,哪些仍然未知;以及

  • 对无法安全表示的行为的稳定诊断信息。

结果是确定性的 Form Contract,具有严格的运行时验证、规范化序列化和内容哈希。该契约旨在作为 Cypress/Playwright 测试规划和未来代理工具链的可靠输入。它不是 Formly 运行时对象的转储。

当前已有内容

本仓库目前提供 schema v0.3 和两个工作区包:

包

用途

@formly-contract/contract-schema

契约 DTO、运行时验证、规范化 JSON 和 SHA-256 内容哈希

@formly-contract/formly-adapter

针对 Formly 6.1 的安全声明式提取和可信场景编译

此外还包括:

  • 使用合成黄金表单的确定性 CLI 演示;

  • 一个浏览器渲染的 Angular 测试应用,包含十二个合成 Formly 夹具;以及

  • 针对固定的 Angular 20.3.29 和 Formly 6.1.8 组合的兼容性覆盖。

解析器和契约是当前的产品。生产级 MCP 服务器、自动 Playwright 生成、浏览器观察和应用源码发现是未来的层次,不包含在此 MVP 中。

Related MCP server: dbt-assay

在你自己的 Angular/Formly 代码库中使用

该包作为构建/测试工具运行在你的 Angular 应用旁边。它不需要被添加到应用的浏览器 bundle 中。典型的采用流程是:

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

1. 添加包

这些包尚未发布到 npm。在首次发布之前,请将此仓库克隆到消费应用旁边,并构建这两个包:

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

然后从消费应用的 package.json 中链接它们(根据你的检出位置调整相对路径):

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

在消费应用中运行 pnpm install。该应用必须已经提供兼容的 Angular 和 Formly 对等依赖;当前测试的组合是 Angular 20.3.29 搭配 Formly 6.1.8。包发布后,常规的带版本号 pnpm add --save-dev 依赖将取代这些本地链接。

2. 选择要暴露的表单

应用源码发现刻意不设为自动。创建一个小的、应用自有的注册表,只导入你希望契约生成器检查的表单工厂:

// 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() },
];

每个工厂应返回一棵全新的字段树。如果工厂需要应用输入,请将其包装在带有合成值的闭包中,这些值在本地开发和 CI 中使用是安全的。

3. 生成契约产物

在应用仓库中添加一个构建时脚本:

// 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`,
  );
}

使用消费仓库已使用的 TypeScript 运行器运行此文件,或将其编译为面向 Node 的工具项目的一部分。生成的 JSON 可以提交以供审查、作为 CI 产物上传,或由下游测试编写工具读取。由于它是规范化的且带内容哈希,意外的表单契约变更会在源码控制或 CI 中可见。

这条声明式路径是最好的起点。它捕获静态结构,并将表达式回调记录为动态元数据,而无需执行任意应用代码。

4. 在 Playwright 中使用契约

在信任存储的 JSON 之前先验证它,找到你需要的语义节点,并使用其精确的定位器候选之一。对于标准的 data-testid 定位器:

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');

实际消费者通常会将递归节点查找和定位器选择放在共享的 Playwright 或 Cypress 辅助函数中。复合控件可以暴露多个定位器目标,因此辅助函数应按 target 选择,而不是假设一个 Formly 节点总是映射到一个 DOM 元素。空定位器数组和诊断信息必须作为证据缺失来处理,而不是用发明的选择器来替代。

5. 在需要时解析动态行为

如果表达式决定可见性、必填/只读状态或选项列表,请添加合成场景并调用 compileFormContractScenario。在配置了应用真实 Formly 模块和自定义类型的可信 Angular 测试/构建环境中运行该 API。为每个有意义的场景生成一个产物,仅使用合成模型和表单状态数据。

合成兼容性测试工具 展示了获取 FormlyFormBuilder 的完整 Angular TestBed 设置。下面的详细 API 示例展示了场景调用。

为什么这很有用

大型 Formly 表单通常由嵌套分组、共享片段、自定义字段类型、表达式、动态选项和应用约定组装而成。反复阅读这些源码很慢,而从渲染页面猜测会导致脆弱的测试。

本项目创建了一个小而明确的边界:

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

消费者可以检查一个契约来回答以下问题:

  • 存在哪些控件,顺序如何?

  • 每个控件编辑哪个模型值?

  • 哪些值和验证边界是已知的?

  • 选项列表是空的、静态的、动态的还是异步的?

  • 哪些字段可能被隐藏、必填、只读或禁用?

  • 哪些 data-*、role、label、placeholder 或 DOM-ID 定位器候选可用?

  • 哪些事实是精确的、推导的、为某个场景解析的,或仍然未知的?

试用本仓库

前置要求:

  • Node.js 22.22.1

  • pnpm 10.23.0

pnpm install --frozen-lockfile
pnpm demo

pnpm demo 构建包切片并打印一个规范化 JSON 契约。使用以下命令运行完整的仓库门禁:

pnpm check

该命令运行 lint、所有测试、包和 Angular 生产构建、演示冒烟测试以及文档检查。

提取声明的表单结构

当你拥有 Formly 配置并希望在不运行回调的情况下检查它时,使用 extractFormContract:

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,
});

此路径是纯函数且非变异的。它不调用表达式函数、不订阅 Observables、不运行验证器,也不渲染 Angular 组件。可识别的回调成为动态规则元数据;不支持的行为成为显式诊断信息。返回的节点具有稳定 ID example.profile::path:s_profile.s_name、模型路径 ['profile', 'name']、其必填约束以及精确的 data-testid 定位器。

解析合成场景

当必填、只读、禁用、隐藏、选项或定位器属性依赖于 Formly 表达式回调时,使用 compileFormContractScenario:

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 },
});

这是一个可信的构建/CI API。它使用应用配置的 FormlyFormBuilder,因此应用和 Formly 回调可能会运行。模型和表单状态必须是可结构化克隆的;两者在字段工厂或构建器运行之前都会被克隆。

构建后的字段树仍然经过与声明式提取相同的白名单。例如,动态选项被缩减为公开的 label/value/disabled 记录,而不是从应用对象复制任意属性。

不要直接从 MCP 或其他不受信任的请求处理器中暴露此编译器。查询层应读取先前生成的契约产物。

测试定位器

每个节点都有一个有序的 locators 数组。适配器自动从 props.attributes 中读取这些常见属性:

  • data-testid

  • data-test-id

  • data-test

  • data-cy

  • data-pw

它还可以保留显式的 role、可访问名称、placeholder 和 Formly 字段 ID 候选。空数组意味着未找到可靠的定位器;适配器从不发明 CSS 或 XPath。

具有自己命名约定的应用可以设置 testIdAttributes 并提供确定性的 deriveLocators 回调。该回调只接收冻结的身份数据,而不是实时的 Formly 字段。它可以为复合小部件(如日期范围)返回多个命名目标;其输出标记为 confidence: "derived"。完整的契约和示例请参阅 v0.3 定位器规范。

证据模型

契约将三个证据级别分开:

证据

含义

当前可用?

declared

从提供的 Formly 配置中安全读取

是

resolved

从针对一个合成场景的受控 Formly 构建中读取

是

observed

在真实渲染的浏览器 DOM 中看到

Schema 已就绪;捕获层尚未实现

解析的定位器不会静默地呈现为浏览器观察到的。同样,不透明或异步行为会被报告而不是猜测。

支持的契约信息

Schema v0.3 可以表示:

  • 有序的控件、分组、仅展示节点和数组模板;

  • 稳定的语义节点 ID 和累积模型路径;

  • Formly 和常见语义控件类型;

  • 标签、描述、placeholder、JSON 安全的默认值和包装器;

  • 必填、最小/最大、长度、字符串模式和命名约束;

  • 静态和解析的公开选项,以及动态/异步选项源元数据;

  • 字符串/布尔条件和回调/异步动态规则元数据;

  • 解析的隐藏、只读和禁用状态;

  • 精确和推导的定位器候选,包括多个命名目标;以及

  • 确定性诊断、规范化 JSON 和内容哈希。

有意的限制

  • 表单必须显式提供;适配器不发现任意的 TypeScript 导出或应用路由。

  • 声明式提取从不评估函数或函数源码。

  • 场景编译器执行初始的受控 Formly 构建,但不等待远程选项或生命周期驱动的浏览器行为。

  • Formly RegExp 模式会被诊断;v0.3 仅表示字符串模式。

  • 自定义小部件操作和值编解码器尚未建模。

  • 项目当前不生成或执行 Cypress/Playwright 测试。

  • 不包含生产级 MCP 服务器或浏览器观察层。

  • 兼容性已针对 Angular 20.3.29 搭配 Formly 6.1.8 验证,而非所有 Angular/Formly 组合。

  • npm 发布和发布自动化尚未包含。

合成测试应用

Angular 测试应用包含十二个虚构表单,涵盖原生和自定义字段、包装器、验证器、扩展、预设、表达式、验证、重复器、不透明行为以及遗留的 Formly v6 别名。

pnpm app:serve

打开 http://127.0.0.1:4200/ 并从目录中选择一个夹具。

工作场所的表单和数据应保留在私有工作仓库中。私有夹具模块可以实现 TestFormDefinition 并通过 TEST_FORM_GROUPS 注册一个分组,而无需将工作场所的标签、标识符、选项或规则复制到本公开项目中。

仓库布局

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

路线图

预期的交付路径是:

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

未来的层应消费不可变契约。它们不应将 Angular 执行、任意回调评估或选择器发明移入常规代理请求中。

文档

贡献与安全

欢迎贡献。参与前请阅读 CONTRIBUTING.md 和行为准则。请通过 SECURITY.md 中描述的私有流程报告安全问题。

本项目基于 MIT 许可证 提供。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides AI coding agents with accurate OpenAPI contract details to prevent hallucinated API calls, supporting multi-version pinning, endpoint discovery, and request validation.
    42 npm
    Apache 2.0
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI coding agents to query a live, type-aware, intent-aware cross-repo index of an organization's TypeScript services, answering semantic questions about functions, endpoints, types, and behavior across repositories. It also supports validation of cross-repo API mismatches and same-repo checks.
    7
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables agents to discover offers, retrieve schemas, validate JSON blueprints, preview plugins, and compile deterministic plugin packages as portable artifacts without publishing or mutating third-party systems.
    -