Skip to content

Close the three deferred upstream-port items, drop the follow-up docs - #140

Merged
dynamics365ninja merged 2 commits into
mainfrom
feat/close-upstream-followups
Aug 5, 2026
Merged

Close the three deferred upstream-port items, drop the follow-up docs#140
dynamics365ninja merged 2 commits into
mainfrom
feat/close-upstream-followups

Conversation

@dynamics365ninja

Copy link
Copy Markdown
Owner

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 ObjectModifyEngine backing modify property, modify add-field, modify add-enum-value, and modify add-control — 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, so a model does not accumulate CustTable.Fleet next to CustTable.Extension.

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 also reaches the whole write surface now: 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 in the chain.
  • Any message containing the word "label" got label-creation advice, including "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-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 existing whole-form string renderers, which were private and reachable only by generating a form from scratch).

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. Refusals are reported under skipped with 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.

$ d365fo form-pattern repair broken.xml -o json
changed: True | fullyRepaired: True | dryRun: True
errors 3 -> 0
  + FP002 set-version   - Set PatternVersion to 1.1 (was 0.9).
  + FP009 set-property  - Set Design.Style to "SimpleList".
  + FP003 added         - Added required ActionPane "ActionPane" (ActionPane).
  + FP003 added         - Added required Grid "Grid" (Grid).

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 and ScaffoldFileWriter rejects abstract roots, so the command failed unconditionally at write time. It now pins the concrete AxEdt*Extension subtype from the target's indexed base type, and the guard accepts an abstract root carrying a concrete i:type — the same escape hatch the bridge already honours for <AxEdt i:type="AxEdtString">. There was no test over XppScaffolder.Extension at all, which is why this went unnoticed.
  • 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.

Second commit: MCP read-only mode leaked write tools

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 bridge and mutate live metadata. modify_object inherited the same hole when it was added, and journal_list was 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.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. 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, and object_patterns action=repair — 27 tools.

Docs

CAPABILITIES.md gains the modify surface + extension fallback, the knowledge/explain-error section, and form-pattern repair; MIGRATION_FROM_MCP.md gains the corrected LOCAL_TOOLS split. docs/followups/ is deleted.

🤖 Generated with Claude Code

dynamics365ninja and others added 2 commits August 5, 2026 11:13
…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>
@dynamics365ninja
dynamics365ninja merged commit add049b into main Aug 5, 2026
4 checks passed
@dynamics365ninja
dynamics365ninja deleted the feat/close-upstream-followups branch August 5, 2026 15:49
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.

1 participant