Skip to content

feat(forman): export field edges, editor markers and remote fragment exclusion - #72

Open
Andrii Chumak (andriichumak) wants to merge 4 commits into
mainfrom
feat/WM-4797-field-edges
Open

Andrii Chumak (andriichumak) wants to merge 4 commits into
mainfrom
feat/WM-4797-field-edges

Conversation

@andriichumak

Copy link
Copy Markdown

Jira: https://make.atlassian.net/browse/WM-4797

Additive, non-breaking. Nothing existing is renamed or changed in behaviour; the two snapshot updates and the one expectation change in test/test.spec.ts are the new markers appearing on fields that already carried editor/multiline.

What

  • fieldEdges(field) / activeFieldEdges(field, value) — one exported reader for every way a field reveals children: per-option nested in a plain array, a store or an option group, per-option { domain, store } objects, options.placeholder.nested (gated on ''), options.nested, the field's own nested, remote lists in every wrapper, and boolean toggles including the { true, false } form and reversedNested. Each edge carries its gate, target domain, and either static children or a remote reference. activeFieldEdges returns the edges a value reveals, following the validator: a matching gated edge replaces the unconditional ones, an out-of-options value falls back to them, an empty value reveals only the placeholder edge.
  • x-editor / x-language / x-multiline markers on the JSON Schema output, round-tripped by toFormanSchema. language added to FormanSchemaField, 'editor' to the type union.
  • excludeRemoteFragments conversion option, next to excludeAdvancedFields: drops bare rpc:// strings from field lists (a banner, a record schema) instead of emitting allOf: [{ $ref }], reporting them on skippedPaths.remoteFragments. A whole-list remote (nested: "rpc://…") keeps its x-nested: { $ref } marker.
  • FormanSchemaExtendedOptions.store is optional and typed as FormanSchemaSelectOptionsStore | string. Both the converter and the validator already handle a store-less wrapper and a partially grouped store at runtime; the type rejected schemas the library accepts.

Why

The MCP server host derives module configuration schemas, dynamic-field descriptors and required-field checks from app manifests and currently re-implements the child-reading grammar itself, missing three of the encodings above. This moves that grammar into the library that owns the DSL; the host will import it and delete its copies. Markers and fragment exclusion remove the two remaining reasons the host had to read raw Forman fields alongside the converter output.

Tests

  • test/field-edges.spec.ts — one case per encoding, activeFieldEdges selection rules, and two consistency tests over test/mocks/google-sheets-add-row.json: the names fieldEdges reaches per domain equal the property names the converter emits, and the live form activeFieldEdges builds for chosen values equals the validator's resolvedSchemas.
  • test/editor-markers.spec.ts — markers, serialization, round-trip, and their placement under allOf/if/then on a code:ExecuteCode-shaped form.
  • test/remote-fragments.spec.ts — collection, branch, unconditional and cross-domain lists, whole-list remotes left intact, reporting alongside the other skip reasons.

Not in this PR

The converter and validator still read children through their own helpers. Routing them through fieldEdges would change behaviour where they disagree today — boolean toggles (validator gates, converter emits x-nested unconditionally), options.nested vs field-level nested precedence (each prefers the other), and per-option branches also repeated as x-nested. The consistency tests pin fieldEdges to both so drift fails a test here; reconciling the two is a separate decision.

🤖 Generated with Claude Code

…exclusion

`fieldEdges(field)` normalizes every spelling of a field's children — per-option
`nested` (plain array, store, option group), `options.placeholder.nested`,
`options.nested`, the field's own `nested`, the boolean `{ true, false }` form
and the `{ store, domain }` wrapper — into one list of edges, each carrying its
gate, target domain and either static children or a remote reference.
`activeFieldEdges(field, value)` applies the validator's selection rules.

`editor` fields are stamped with `x-editor`/`x-language`, multiline text with
`x-multiline`; `toFormanSchema` reads them back.

`excludeRemoteFragments` drops bare `rpc://` strings from field lists and
reports them on `skippedPaths.remoteFragments`, so a static consumer never
receives a `$ref` it cannot resolve.

`FormanSchemaExtendedOptions.store` is optional and accepts a partially
grouped store, matching what the converter and validator already handle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 22, 2026 11:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

excludeRemoteFragments currently drops all string entries in field lists rather than only rpc:// fragments, which diverges from the documented behavior and can remove unintended $ref strings.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds new public utilities for reading Forman nested-field relationships, expands JSON Schema round-trip markers for editor/multiline fields, and introduces a conversion option to omit remote fragment strings from emitted JSON Schema—supporting downstream consumers that need a normalized “child-reading” model and cleaner schema output.

Changes:

  • Export fieldEdges / activeFieldEdges and the FormanFieldEdge type for normalized traversal of nested fields.
  • Add x-editor / x-language / x-multiline JSON Schema markers with toFormanSchema round-trip support; extend types with editor + language.
  • Add excludeRemoteFragments option to drop bare remote fragment strings from field lists and report them via skippedPaths.remoteFragments, with test coverage and docs.
File Description
test/​test.spec.ts Updates expectations for new editor marker + editor type round-trip.
test/​remote-fragments.spec.ts Adds tests for excludeRemoteFragments behavior and reporting.
test/​field-edges.spec.ts Adds tests for fieldEdges/activeFieldEdges and consistency checks vs converter/validator.
test/​editor-markers.spec.ts Adds tests for editor/multiline markers, serialization, and round-trip.
test/​composites/​__snapshots__/​udttype.spec.ts.snap Snapshot update to include x-multiline.
test/​composites/​__snapshots__/​udtspec.spec.ts.snap Snapshot update to include x-multiline.
src/​utils.ts Implements and exports fieldEdges/activeFieldEdges utilities.
src/​types.ts Adds editor type, language field, FormanFieldEdge, and excludeRemoteFragments typing.
src/​json.ts Reads x-editor/x-language/x-multiline markers back into Forman fields.
src/​index.ts Exports new type + utilities; extends skippedPaths wiring for remote fragments.
src/​forman.ts Adds editor/multiline marker emission and excludeRemoteFragments filtering/reporting.
README.md Documents the new APIs, markers, and remote fragment exclusion option.
AGENTS.md Updates project map and conversion option documentation to include new features.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/forman.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiles and the full suite passes on the branch. I reproduced each inline comment below with a probe spec rather than reading it off the diff.

The blocking part is activeFieldEdges: the PR and README say it follows the validator's rules, but in four shapes it gives the opposite answer (options.nested vs field nested, custom option value key, boolean holding an IML value, placeholder edge on a required/non-select field). A form walker built on it will reveal fields the validator rejects, or hide fields it accepts, and field-edges.spec.ts currently pins the divergent behaviour as correct. The remote-fragment comments are smaller and worth folding into the same revision.

Comment thread src/utils.ts Outdated
Comment thread src/utils.ts Outdated
Comment thread src/utils.ts Outdated
Comment thread src/utils.ts Outdated
Comment thread src/utils.ts Outdated
Comment thread src/forman.ts Outdated
Comment thread src/forman.ts Outdated
Comment thread src/forman.ts Outdated
Comment thread src/forman.ts Outdated
Comment thread README.md
…once, bump to 2.1.0

Addresses the review on #72.

- `fieldEdges` / `activeFieldEdges` move to `src/edges.ts` and reuse
  `resolveFormanFieldType`; options are matched on `options.value`, the
  field's own `nested` shadows `options.nested`, the placeholder edge exists
  only on a non-required select, and a boolean holding an IML value reveals
  its single-branch nested and nothing of the two-branch form
- remote fragments are filtered once in `extractNestedAndDomain`, reported
  at the declaring field through the `collectionPath` rule, and a list left
  empty emits no `x-nested` marker or `then` branch
- `x-multiline` is stamped on string-typed results only, matching what
  `toFormanSchema` reads back
- per-rule parity table pins `activeFieldEdges` to the validator's
  `resolvedSchemas`; package version bumped to match the README header

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants