Skip to content

Resolve docs/followups: scaffold validation, targeted upstream fixes, connect, View/Map writers, bulk labels - #133

Merged
dynamics365ninja merged 5 commits into
mainfrom
fix/followups-targeted-fixes-2026-08
Aug 5, 2026
Merged

Resolve docs/followups: scaffold validation, targeted upstream fixes, connect, View/Map writers, bulk labels#133
dynamics365ninja merged 5 commits into
mainfrom
fix/followups-targeted-fixes-2026-08

Conversation

@dynamics365ninja

Copy link
Copy Markdown
Owner

Works through docs/followups, resolving everything in it except three items that are whole subsystems rather than fixes (listed at the bottom). Resolved entries are removed from the follow-up docs as they land; the two docs now list open work and nothing else.

Rebased on current main, so #112 (modify method), #113 (journal/undo) and the #114 HTTP transport are already accounted for rather than duplicated. Two things were found to be partly done and were not rebuilt: the MCP labels tool already had bulk create (only the CLI side was missing), and the modify command family now exists (only the wider write surface is left).

Scaffold validation — issue #70 follow-ups

  • ScaffoldFileWriter.EnsureValueShapes runs from both Write overloads: requires the XMLSchema-instance declaration on the roots whose fields are polymorphic (AxEnum, AxTable, AxView, AxMap, AxEdt*), and rejects anything outside true/false/1/0 in CLR-bool elements (IsExtensible) — exactly what XmlConvert.ToBoolean accepts, so both the NoYes spelling and garbage are caught. Deliberately not a blanket rule: AxClass/AxMenuItem/AxQuery are written without the declaration today and read back fine.
  • --verify on GenerateSettings reads an artefact back through the Metadata API after writing. Skipped (never fails) when the runtime is absent, so offline generation keeps working; only --install-to is verifiable, because the provider resolves objects by name inside the packages paths and an arbitrary --out path would either be missed or matched against a different same-named object.

Targeted upstream fixes

Fix Change
Label where-used ResolveTargetPaths recognises @WAX2194, @LabelFile:LabelId, /Labels/@… and emits the exact /Labels/@<ref> path instead of a bare name matching nothing
[ExtensionOf] casing ExtensionOfRx gains IgnoreCase — X++ casing varies freely on the Str intrinsic, so class extensions were silently missed by CoC indexing
Search ranking All 16 object-level Search* methods order by m.IsCustom DESC first, so custom/ISV objects are neither buried nor truncated away by LIMIT
generate table New --configuration-key / --form-ref; omitted when unset so AOT defaults still apply
Inherited members GetClassDetails walks the ExtendsName chain (recursive CTE, bounded at 32 hops) and reports inherited methods in a separate list, each stamped with its DeclaringClass
OLS coverage GetSecurityCoverage reports row-level (XDS) state explicitly: Constrained / NotConstrained / Unknown / NotApplicable

The OLS item was flagged "verify, do not assume" — it turned out to be a stronger gap than upstream's. The CLI indexes XDS policies (SecurityPolicies.ConstrainedTable) but GetSecurityCoverage never read them, so get security --type Table X returned a roles-only answer that reads as "no row-level security" when the CLI simply never looked. Unknown covers the case where the index holds no policies at all, so zero matches is never mistaken for an assurance.

New features from the deferred list

  • d365fo connect <url> — probes GET /health first, so a mistyped URL reports SERVER_UNREACHABLE instead of producing a config that yields no tools (indistinguishable from inside an editor). Echoes the resolved MCP_SERVER_MODE back, warning when it is not full. Only the named entry is written: other servers and top-level keys survive, JSONC is tolerated, an unparseable config is refused rather than overwritten, and replacing an entry needs --force.
  • generate view / generate mapViewScaffolder and MapScaffolder. Honesty constraints upstream had to retrofit are built in: no <Ranges> and no invented <Title*> on views, a computed field without an explicit type is rejected rather than guessed, and a mapping connecting a field the map does not declare is rejected rather than compiled into something that silently never matches.
  • label create --entry KEY=VALUE — per-key error isolation mirroring the MCP handler's payload. Partial success stays ok:true (the writes that landed are real and journaled; gate on data.failed == 0); BATCH_FAILED when nothing could be written. The positional form keeps its exact previous behaviour, including the hard KEY_EXISTS scripts rely on.

Grounded against a real AOT, not guessed

View and Map shapes were verified against shipped standard-model files on the local AOS (196 models) rather than inferred. That settled two things a reasonable guess gets wrong: bound view fields are always AxViewFieldBound with no primitive suffix (21 846 occurrences, no exceptions) while only computed fields carry a type; and map fields use the same suffix vocabulary as AxTableField, so both now share XppScaffolder.ConcreteFieldSuffix and cannot disagree on one EDT.

A round-trip through the CLI's own index surfaced a real pre-existing bug. ParseView searched Descendants() for <Fields> and matched the empty one nested in the first <AxTableFieldGroup>, which the AOT emits before the view's real <Fields>. Every shipped view therefore indexed with zero fields. Fixed to read direct children like every other parser here — the same fix already applied table-side.

Verification

647 tests pass in Release (262 CLI + 385 Core), 60 of them new. Beyond unit tests: connect was exercised against a live d365fo-mcp --http instance including both failure paths, and generate view/generate map were driven end to end through index extractget view / find usages. The [ExtensionOf] regression test was confirmed to fail without its fix.

Two gaps worth knowing: the label where-used fix has no unit testD365FO.Bridge targets net48 and no test project references it, so it is build-verified only; and get map still has no CLI command (the repository method exists), which is a separate read-surface gap I did not widen into.

Still open

Three items remain across both docs, all multi-day subsystems rather than fixes: the rest of the modify surface (add-field, add-control, modify-property, extension writers), the knowledge-base subsystem, and the TRUDUtils generators / form auto-repair.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FfAAnxQDnPHRB1ne5H724n

dynamics365ninja and others added 5 commits August 5, 2026 05:44
Closes the two issue #70 scaffold-validation follow-ups and the seven
"targeted fixes" catalogued in the 2026-08 upstream port pass.

Scaffold validation (issue #70):
- ScaffoldFileWriter.EnsureValueShapes runs from both Write overloads:
  requires the XMLSchema-instance declaration on AxEnum/AxTable/AxEdt*
  roots, and rejects anything but true/false/1/0 in CLR-bool elements
  (IsExtensible), which is exactly what XmlConvert.ToBoolean accepts.
- New --verify flag on GenerateSettings reads an artefact back through
  the Metadata API after writing. Skipped (never fails) when the runtime
  is absent, so offline generation keeps working; only --install-to is
  verifiable because the provider resolves objects by name.

Targeted upstream fixes:
- XrefRepository.ResolveTargetPaths recognises label targets (@WAX2194,
  @LabelFile:LabelId, /Labels/@...) and emits the exact /Labels/@<ref>
  path instead of a bare name that matched nothing.
- ExtensionOfRx gains IgnoreCase: X++ casing varies freely on the Str
  intrinsic, so class extensions were silently missed by CoC indexing.
- All 16 object-level Search* methods order by m.IsCustom DESC first, so
  custom/ISV objects are not buried under Microsoft's — and, since the
  ordering precedes LIMIT, no longer truncated away entirely.
- generate table gains --configuration-key and --form-ref; both elements
  are omitted when unset so the AOT defaults still apply.
- GetClassDetails walks the ExtendsName chain (recursive CTE, bounded at
  32 hops) and reports inherited methods in a separate list, each stamped
  with its DeclaringClass.
- GetSecurityCoverage reports row-level (XDS) coverage with an explicit
  state instead of omitting it: Constrained / NotConstrained / Unknown /
  NotApplicable. Unknown covers "the index holds no policies at all", so
  zero matches is never mistaken for an assurance.

Resolved items are removed from docs/followups; the issue-70 doc held
only these two and is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfAAnxQDnPHRB1ne5H724n
…faf38)

`label create` could only write one key per invocation (fanned out across
locales). The MCP `labels action=create` tool already accepted a
`labels:[{key,value}]` batch with per-item error isolation; the CLI had no
equivalent, so scripted label seeding meant one process per key.

Adds a repeatable `--entry <KEY>=<VALUE>`, splitting on the first '=' so
values may contain one. The positional <KEY> <VALUE> form becomes optional
and keeps its exact previous behaviour, including the hard KEY_EXISTS
failure that scripts rely on.

Batch semantics mirror the MCP handler's payload: each key is written
independently, so a key that already exists (or a file that will not take
it) costs only itself. Partial success stays ok:true with the failures
repeated as warnings — the writes that landed are real and journaled, and
callers gate on `data.failed == 0`. When nothing at all could be written
the command fails with BATCH_FAILED so CI still sees a non-zero exit.

Duplicate keys and malformed entries are rejected up front, before any
write, rather than letting the last value silently win.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfAAnxQDnPHRB1ne5H724n
Follow-on to the #114 HTTP transport. With a server deployed there was still
no CLI-side way to point an MCP client at it; the documented procedure was
"write this JSON by hand", which is exactly how the other MCP entries in a
shared .mcp.json get clobbered.

  d365fo connect https://d365fo-mcp.example.com                  # .mcp.json
  d365fo connect https://d365fo-mcp.example.com --editor vscode  # .vscode/mcp.json

Probes GET /health before writing, so a mistyped URL reports
SERVER_UNREACHABLE instead of producing a config that yields no tools --
the two failures are indistinguishable from inside an editor. --force writes
anyway (a cold-starting instance may answer later), --no-probe skips it. The
resolved MCP_SERVER_MODE is echoed back, with a warning when it is not
"full", so connecting to a read-only instance and then hunting for
generate_object is a one-line answer.

Merge semantics, tested directly because this is the part that can destroy
work: only the named entry is written, every other server and top-level key
survives, JSONC comments and trailing commas are tolerated (VS Code writes
them), a config that will not parse is refused rather than overwritten, and
an existing entry needs --force. --name adds a second entry so prod and
local can coexist.

Base-URL normalisation accepts the /mcp endpoint people actually paste.
--api-key is written in plain text and says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfAAnxQDnPHRB1ne5H724n
The CLI could read and index views and maps but had no writer for either, so
the upstream view/map fixes catalogued in the 2026-08 follow-up had nothing to
land on. Adds ViewScaffolder / MapScaffolder and `generate view` /
`generate map`.

Both shapes are ground-truthed against shipped standard-model files on the
local AOS rather than guessed, which settled two things a reasonable guess
gets wrong:
- bound view fields are always i:type="AxViewFieldBound" (21 846 of them on
  the reference AOT, none with a primitive suffix); only computed fields carry
  a type, as AxViewFieldComputed<Type> plus a <ViewMethod>.
- map fields are <AxMapBaseField i:type="AxMapField<Suffix>"> over the same
  primitive vocabulary as AxTableField, so both now share
  XppScaffolder.ConcreteFieldSuffix and cannot disagree on one EDT.

Honesty constraints upstream had to retrofit are built in: no <Ranges> and no
invented <Title*> on views (ranges belong on the backing query); a computed
field without an explicit type is rejected rather than guessed; a mapping that
connects a field the map does not declare is rejected rather than written to
compile into something that silently never matches.

AxView and AxMap join the writer's xmlns:i-required set — their fields are
polymorphic, so the declaration is load-bearing. Both commands write straight
to disk: the bridge's createObject only accepts class|table|edt|enum|form, and
routing through it would report a misleading "Metadata API rejected" warning
on every install (`generate query` already has this shape).

Also fixes a real pre-existing bug the round-trip surfaced: ParseView searched
Descendants() for <Fields> and matched the empty one nested in the first
<AxTableFieldGroup>, which the AOT emits BEFORE the view's real <Fields>. Every
shipped view therefore indexed with zero fields. Now reads direct children like
every other parser here — the same fix already applied table-side.

Verified end to end: generate → index extract → get view returns all three
fields, and the map indexes and resolves through find usages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfAAnxQDnPHRB1ne5H724n
Main has moved since these were written: #112/#113 (modify method +
modification journal) landed, and this branch adds `connect`, the View/Map
writers and CLI bulk label creation.

Removes the items that are now done and rewrites the two that only partly
were, so the docs list open work and nothing else:
- 2026-08 gains a short "resolved so far" note in the intro and drops the
  connect, View/Map and bulk-label entries; the modify item is rewritten
  around what actually remains (add-field, add-control, modify-property,
  extension writers — the bridge still takes only class|table|edt|enum|form).
- 2026-07 drops View/Map and bulk labels, marks modify as partially resolved,
  and points at the 2026-08 doc as the current view of each remaining topic.

Three items are left across both docs: the rest of the modify surface, the
knowledge-base subsystem, and the TRUDUtils generators / form auto-repair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfAAnxQDnPHRB1ne5H724n
@dynamics365ninja
dynamics365ninja merged commit 3f90c6f into main Aug 5, 2026
4 checks passed
@dynamics365ninja
dynamics365ninja deleted the fix/followups-targeted-fixes-2026-08 branch August 5, 2026 07:24
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