feat(forman): export field edges, editor markers and remote fragment exclusion - #72
Andrii Chumak (andriichumak) wants to merge 4 commits into
Conversation
…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>
There was a problem hiding this comment.
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
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/activeFieldEdgesand theFormanFieldEdgetype for normalized traversal of nested fields. - Add
x-editor/x-language/x-multilineJSON Schema markers withtoFormanSchemaround-trip support; extend types witheditor+language. - Add
excludeRemoteFragmentsoption to drop bare remote fragment strings from field lists and report them viaskippedPaths.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.
Jakub Stok (jakubstokcelonis)
left a comment
There was a problem hiding this comment.
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.
…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>

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.tsare the new markers appearing on fields that already carriededitor/multiline.What
fieldEdges(field)/activeFieldEdges(field, value)— one exported reader for every way a field reveals children: per-optionnestedin a plain array, astoreor an option group, per-option{ domain, store }objects,options.placeholder.nested(gated on''),options.nested, the field's ownnested, remote lists in every wrapper, and boolean toggles including the{ true, false }form andreversedNested. Each edge carries itsgate, targetdomain, and either staticchildrenor aremotereference.activeFieldEdgesreturns 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-multilinemarkers on the JSON Schema output, round-tripped bytoFormanSchema.languageadded toFormanSchemaField,'editor'to the type union.excludeRemoteFragmentsconversion option, next toexcludeAdvancedFields: drops barerpc://strings from field lists (a banner, a record schema) instead of emittingallOf: [{ $ref }], reporting them onskippedPaths.remoteFragments. A whole-list remote (nested: "rpc://…") keeps itsx-nested: { $ref }marker.FormanSchemaExtendedOptions.storeis optional and typed asFormanSchemaSelectOptionsStore | string. Both the converter and the validator already handle astore-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,activeFieldEdgesselection rules, and two consistency tests overtest/mocks/google-sheets-add-row.json: the namesfieldEdgesreaches per domain equal the property names the converter emits, and the live formactiveFieldEdgesbuilds for chosen values equals the validator'sresolvedSchemas.test/editor-markers.spec.ts— markers, serialization, round-trip, and their placement underallOf/if/thenon acode: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
fieldEdgeswould change behaviour where they disagree today — boolean toggles (validator gates, converter emitsx-nestedunconditionally),options.nestedvs field-levelnestedprecedence (each prefers the other), and per-option branches also repeated asx-nested. The consistency tests pinfieldEdgesto both so drift fails a test here; reconciling the two is a separate decision.🤖 Generated with Claude Code