Close the three deferred upstream-port items, drop the follow-up docs - #140
Merged
Conversation
…p docs
docs/followups/ tracked three items deferred across the 2026-07 and 2026-08
upstream port waves. All three are implemented here, so both docs are removed
rather than re-dated.
1. Object modification beyond method bodies (ObjectModifyEngine)
`modify property`, `modify add-field`, `modify add-enum-value`, and
`modify add-control`, on the same contract as `modify method`: read the live
object as XML through the bridge, edit the parsed XDocument, write back
through IMetadataProvider, no on-disk fallback.
Two things the method path did not have:
- Extension fallback. A write whose target model is not in
D365FO_CUSTOM_MODELS is redirected to the <Target>.<Suffix> extension in a
custom model — created if absent — instead of editing a Microsoft or ISV
object in place, and says so in `warnings`. --extension forces it,
--require-extension refuses the in-place path. The suffix defaults to
whatever an existing extension of the object already uses. This is what
the new tableExtension/formExtension/edtExtension/enumExtension/
viewExtension/queryExtension/dataEntityViewExtension bridge kinds are for;
view/map/query/dataEntityView were added alongside them, and the two
literal restatements of the old five-kind allow-list now derive from the
map itself.
- Journaling. `modify method` was the one write path in the CLI that read a
pre-image and then discarded it, so `d365fo undo` could not revert a
method edit. Every modify now journals, including that one.
`--verify` reaches the whole write surface: kinds without a typed read verb
fall back to the generic readObjectXml instead of silently skipping.
2. Knowledge lookup + scored build-error hints
`knowledge list|get|search` serves the verified skills/_source corpus,
embedded in the binary and sliced by '##' section, so an agent without
skill-file support can ground itself and a fact still has exactly one home.
XppcDiagnostics.FixHint was an ordered `Contains` chain, so the first
loosely-worded rule won: "the label @sys12345 does not exist" got generic
identifier advice (the identifier rule tests "does not exist" and sat
higher), and any message containing the word "label" got label-creation
advice. Replaced with XppcFixHints, where each rule declares required and
disqualifying tokens plus a weight, all rules are scored, and an
unrecognised message gets no hint rather than the nearest-looking one.
`explain-error` exposes it over a pasted log — no VM, no index.
3. Form control expander + auto-repair
FormControlFactory emits one AOT-valid AxFormControl at a time (the XElement
counterpart of the whole-form string renderers). FormPatternRepairer
consumes the validator's own remediations for the violations with a single
correct outcome — FP003 missing control, FP005 order, FP002 version, FP009
pattern defaults, FP006 unambiguous sub-pattern, FP010 adopt a pattern — and
refuses the rest (FP004 would delete someone's control; FP007/FP008 and
multi-candidate FP006 are design decisions), reporting them under `skipped`
rather than dropping them. Surfaced as `form-pattern repair`, dry-run unless
--apply/--out.
Three latent defects surfaced while testing this and are fixed:
- `generate extension edt <Target>` could never be written: the scaffolder
emitted the abstract <AxEdtExtension> root and ScaffoldFileWriter rejects
abstract roots. It now pins the concrete AxEdt*Extension subtype from the
target's indexed base type, and the guard accepts an abstract root that
carries a concrete i:type — the same escape hatch the bridge already honours
for <AxEdt i:type="AxEdtString">.
- `generate form --pattern Workspace --section X` failed its own write gate:
the section's filter group declared CustomAndQuickFilters over an empty
<Controls />, and that sub-pattern requires a QuickFilterControl child.
- `generate form --pattern Dialog --section X` likewise: sections were wrapped
in a Tab inside the dialog body, whose FieldsFieldGroups sub-pattern allows
only input controls and one level of Group. They are field groups now.
The golden gate in FormPatternValidatorTests now also runs every pattern with
sections supplied, which is what would have caught the latter two.
MCP parity: modify_object, get_knowledge, explain_build_error, and
object_patterns action=repair (27 tools).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ServerModeConfig.LocalTools carried only the four filesystem-touching tools, so a read-only deployment advertised and accepted modify_method and undo_last_modification — both of which round-trip through the local D365FO.Bridge process and mutate live metadata. modify_object inherited the same hole when it was added. journal_list was reachable too, on an instance whose journal directory is always empty. All four are local: the modify tools and undo need the bridge (and therefore the on-disk package tree), and journal_list reads <index-dir>/journal/, which is only populated where the writes happened. Adding them also gives the write-only companion the whole write -> inspect -> undo loop instead of just generate_object and labels, which is what that mode is for. The invariant is now asserted over ToolCatalog.WriteTools rather than a hand-kept list, so a future write tool that is not classified fails the test instead of shipping in a shared deployment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
docs/followups/tracked three items deferred across the 2026-07 and 2026-08 upstream port waves. All three are implemented here, so both docs are removed rather than re-dated.783 tests pass (277 CLI + 506 Core), build clean.
1. Object modification beyond method bodies
New
ObjectModifyEnginebackingmodify property,modify add-field,modify add-enum-value, andmodify add-control— same contract asmodify method: read the live object as XML through the bridge, edit the parsedXDocument, write back throughIMetadataProvider, no on-disk fallback.Two things the method path did not have:
Extension fallback. A write whose target model is not in
D365FO_CUSTOM_MODELSis redirected to the<Target>.<Suffix>extension in a custom model — created if absent — instead of editing a Microsoft or ISV object in place, and says so inwarnings.--extensionforces it,--require-extensionrefuses the in-place path. The suffix defaults to whatever an existing extension of the object already uses, so a model does not accumulateCustTable.Fleetnext toCustTable.Extension.This is what the new
tableExtension/formExtension/edtExtension/enumExtension/viewExtension/queryExtension/dataEntityViewExtensionbridge kinds are for;view/map/query/dataEntityViewwere added alongside them, and the two literal restatements of the old five-kind allow-list now derive from the map itself.Journaling.
modify methodwas the one write path in the CLI that read a pre-image and then discarded it, sod365fo undocould not revert a method edit. Every modify now journals, including that one.--verifyalso reaches the whole write surface now: kinds without a typed read verb fall back to the genericreadObjectXmlinstead of silently skipping.2. Knowledge lookup + scored build-error hints
knowledge list|get|searchserves the verifiedskills/_sourcecorpus, embedded in the binary and sliced by##section, so an agent without skill-file support can ground itself and a fact still has exactly one home.XppcDiagnostics.FixHintwas an orderedContainschain, so the first loosely-worded rule won:"does not exist"and sat higher in the chain."Control 'Label' must be bound to a data source property".Replaced with
XppcFixHints, where each rule declares required and disqualifying tokens plus a weight, all rules are scored, and an unrecognised message gets no hint rather than the nearest-looking one.explain-errorexposes it over a pasted log — no VM, no index.3. Form control expander + auto-repair
FormControlFactoryemits one AOT-validAxFormControlat a time (theXElementcounterpart of the existing whole-form string renderers, which were private and reachable only by generating a form from scratch).FormPatternRepairerconsumes the validator's own remediations for the violations with a single correct outcome — FP003 missing control, FP005 order, FP002 version, FP009 pattern defaults, FP006 unambiguous sub-pattern, FP010 adopt a pattern — and refuses the rest: FP004 would delete someone's control; FP007/FP008 and multi-candidate FP006 are design decisions. Refusals are reported underskippedwith the reason rather than dropped, so "repaired" is always distinguishable from "repaired what it could".Surfaced as
form-pattern repair, dry-run unless--apply/--out.Latent defects fixed along the way
Three of these surfaced while testing the above:
generate extension edt <Target>could never be written. The scaffolder emitted the abstract<AxEdtExtension>root andScaffoldFileWriterrejects abstract roots, so the command failed unconditionally at write time. It now pins the concreteAxEdt*Extensionsubtype from the target's indexed base type, and the guard accepts an abstract root carrying a concretei:type— the same escape hatch the bridge already honours for<AxEdt i:type="AxEdtString">. There was no test overXppScaffolder.Extensionat all, which is why this went unnoticed.generate form --pattern Workspace --section Xfailed its own write gate. The section's filter group declaredCustomAndQuickFiltersover an empty<Controls />, and that sub-pattern requires aQuickFilterControlchild.generate form --pattern Dialog --section Xlikewise. Sections were wrapped in aTabinside the dialog body, whoseFieldsFieldGroupssub-pattern allows only input controls and one level ofGroup. They are field groups now.The golden gate in
FormPatternValidatorTestsnow also runs every pattern with sections supplied, which is what would have caught the latter two.Second commit: MCP read-only mode leaked write tools
ServerModeConfig.LocalToolscarried only the four filesystem-touching tools, so a read-only deployment advertised and acceptedmodify_methodandundo_last_modification— both of which round-trip through the local bridge and mutate live metadata.modify_objectinherited the same hole when it was added, andjournal_listwas reachable on an instance whose journal directory is always empty.All four are local and now classified as such. The invariant is asserted over
ToolCatalog.WriteToolsrather than a hand-kept list, so a future write tool that is not classified fails the test instead of shipping in a shared deployment. Side benefit: the write-only companion now carries the whole write → inspect → undo loop, which is what that mode is for.MCP parity
modify_object,get_knowledge,explain_build_error, andobject_patterns action=repair— 27 tools.Docs
CAPABILITIES.mdgains the modify surface + extension fallback, the knowledge/explain-errorsection, andform-pattern repair;MIGRATION_FROM_MCP.mdgains the correctedLOCAL_TOOLSsplit.docs/followups/is deleted.🤖 Generated with Claude Code