Finish #164 (R5 form-engine extras), and what #160 still needs - #167
Merged
Conversation
#164, R5) Every catalog this tool answers from is generated from one platform version and then committed: the form-pattern registry from Microsoft.Dynamics.AX.Metadata.Patterns.dll, the contract catalog from Microsoft.Dynamics.AX.Metadata.dll, the object-type registry by hand. That makes each of them right when it was made and silent about drift ever after — the tool keeps answering confidently from a catalog that no longer covers what is on disk. `d365fo index cross-check` asks the installation instead. Run against the box this was written on it immediately found one real gap and one instructive false positive. **Real:** three shipped forms use the `HubPartGrid` pattern and the registry has no entry for it, so `form-pattern validate` and `repair` cannot judge them. **False positive, now suppressed:** `Custom` is the fourth most common `<Pattern>` value on the installation (915 indexed forms). It is not a pattern — it is the AOT's marker for "this form deliberately has none", and of 143 sampled forms carrying it, every single one has no `PatternVersion`, which a real pattern always does. Left in, it would have been the largest "gap" in the report and it is not a gap at all. The two finding types are kept apart rather than ranked, because they mean different things. A gap is where the tool will be *wrong*. An uncovered AOT family is where it is merely *narrow* — 40 of the 83 AOT folders present on a real installation are families this tool was never built to handle, and mixing those into the same list would have buried the one finding that mattered. Uncovered is off by default and never fails the command; gaps exit 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuTvn5ymx3HAUtGjsxBuaA
The last of R5's form-engine material. Starting from a Microsoft form that already has the pattern, the control tree and the wiring right beats any template, and it is what a developer does by hand anyway. String-level, and this repo has the evidence for it. An AxForm is a V6 contract whose Design subtree is written in the empty namespace, carries i:type on every control and <FormControlExtension i:nil="true" /> on all of them; loading that into an XDocument and writing it back rewrites namespace declarations and prefixes, which is exactly why FormPatternTemplates renders forms as strings rather than building them as documents. A cloner that round-tripped would return a form differing from the original in ways nobody asked for, on every clone. So every edit is anchored and narrow: the root <Name>, the class declaration, formStr() self-references, and — on --rebind — the table, the datasource named after it, and every control pointing at that datasource. What it deliberately does not do is a blind replace of the old form name: names are short and appear inside unrelated identifiers, and a global replace would quietly rename Grid_CustGroupId along with the form. The ordering is load-bearing and pinned by a test. A form, its datasource and its table are all commonly called the same thing, so the form is renamed *before* the rebind runs — otherwise the rebind renames the form itself to the new table's name. Verified against ApplicationSuite\Foundation\AxForm\CustGroup.xml on a live installation: a 16 KB form where the clone differs on exactly the intended lines, is byte-identical everywhere else, and passes `validate xpp` with zero findings. Also fixes a latent false positive in the property-honesty report found by this command: --from names a file, and reconciling a path against the document reports one gap per path segment (AosService, PackagesLocalDirectory, CustGroup.xml…), burying everything that matters. Values that are rooted or carry a file extension are no longer reconciled. Rooted-or-extensioned rather than "contains a separator", because `/` is meaningful in real option values — `--constrained Header/Line` nests a policy's constrained-table tree and every segment of it does have to reach the document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuTvn5ymx3HAUtGjsxBuaA
This was referenced Aug 6, 2026
jackyclever
pushed a commit
to jackyclever/d365fo-cli
that referenced
this pull request
Aug 7, 2026
…ynamics365ninja#164) The `eval` CI job has been red since dynamics365ninja#167 merged: `eval coverage --check` reported COVERAGE_DRIFT because dynamics365ninja#167 added the `form-clone` capability without regenerating eval/COVERAGE.md. (dynamics365ninja#165's red build-test and knowledge-audit were an Actions outage — "Failed to resolve action download info" — not code, and are already green on main.) Writing the missing eval case surfaced a real defect in the capability the stale row was hiding. `--rebind` renamed the datasource and repointed the control-level <DataSource> nodes, but left the design's own <DataSource> and <TitleDataSource> naming the datasource the clone no longer has — a form that compiles and fails at runtime. The cause is that those two are first-level children of <Design>, so they are written as <DataSource xmlns="">, and the pattern was anchored on the bare "<DataSource>"; the control-level ones nest under <Controls xmlns="">, inherit the namespace, appear bare, and so were the only ones matched. <JoinSource> was not handled at all, so a joined datasource kept pointing at its parent's old name. The element list is grounded on the live installation rather than guessed: across 300 shipped ApplicationSuite forms, DataSource, TitleDataSource, WorkflowDataSource, PresenceDataSource and JoinSource are the only elements whose value is a datasource name — the rest of the *DataSource* family is either a container or holds something else ( DataSourceChangeGroupMode is an enum, DataSourceRelation names a relation). Matching now tolerates attributes and backreferences the closing tag, so it cannot bleed from DataSource into DataSourceLinks. The unit fixture's <Design> went straight to <AxFormControl> and so never had the nodes that break — it now carries the design properties and a joined datasource, which is what makes the four new tests fail before the fix. L2-form-clone-basic clones the two-datasource details-transaction form: 19 KB in, and the clone differs on exactly the root <Name> and the class declaration and is byte-identical everywhere else, which is the guarantee worth locking for string surgery over a document this code does not own. It is a plain clone rather than a rebind because MiniAot has only FmVehicle and FmVehicleLine: rebinding the simple form onto FmVehicleLine binds a <DataField>Make</DataField> that table does not have, and the richer form already uses both, so either golden would enshrine a form nobody should ship. The rebind path is covered by the unit tests instead. A fixture table would let it graduate to an eval case; left as follow-up. Verified locally against all four CI jobs: build clean, 1178 tests pass, knowledge audit clean, 52/52 eval cases with `coverage --check` green, and no skills drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuTvn5ymx3HAUtGjsxBuaA
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.
Closes the two issues left open by #166. Full suite green (1174 tests, three consecutive clean runs).
ea61000,6731160#164 — the last two R5 items
crossCheck(ea61000) —d365fo index cross-check. Every catalog this tool answers from is generated from one platform version and then committed, which makes it right when it was made and silent about drift ever after. This asks the installation instead.Run against a live install it immediately found one real gap and one instructive false positive:
HubPartGridpattern and the registry has no entry, soform-pattern validateandrepaircannot judge them.Customis the fourth most common<Pattern>value (915 indexed forms). It is not a pattern — it is the AOT's marker for "this form deliberately has none", and of 143 sampled forms carrying it, every one has noPatternVersion. Left in, it would have been the largest "gap" in the report.Gaps and uncovered families are kept apart rather than ranked: a gap is where the tool will be wrong; an uncovered AOT family is where it is merely narrow. 40 of the 83 AOT folders on a real installation are families this tool was never built to handle, and mixing those in would have buried the one finding that mattered. Uncovered is off by default and never fails the command; gaps exit 2.
formCloner(6731160) —d365fo generate form-clone. String-level by design: an AxForm's Design subtree is the empty namespace withi:typeon every control, so a round-trip throughXDocumentrewrites namespace declarations nobody asked to change. Verified against a shipped 16 KBCustGroup— the clone differs on exactly the intended lines, is byte-identical elsewhere, and passesvalidate xppclean.The ordering is load-bearing and pinned: a form, its datasource and its table are all commonly named the same, so the form is renamed before
--rebindruns, or the rebind renames the form to the table's name.R3 (RDL precision design) is declined, not deferred. A wrong RDL renders wrongly rather than failing to load, so absorbing it needs a rendering oracle — a different class of evidence from everything else here, and one this repo has no way to build. That was already the issue's own conclusion; this records it as the decision.
Also fixed
A latent false positive in the property-honesty report, surfaced by
form-clone:--fromnames a file, and reconciling a path against the document reports one gap per path segment. Values that are rooted or carry a file extension are no longer reconciled — rooted-or-extensioned rather than "contains a separator", because/is meaningful in--constrained Header/Line. The same noise was latent on--out-*,--add-toand--into-role.#160 — one blocker removed, one constraint remains
Not closed. Two findings recorded on the issue:
/unattendedsuppresses the debugger prompt.SysTestConsole.execallsWaitForDebugger()before it parses arguments and throws on a redirected stdin, which I reported earlier as blocking any headless harness. It does not: with/unattendedthe runner proceeds toExecuting test(s). That removes the blocker.What remains is a deliberate constraint, not a technical one. Scoring the catalog needs tests to actually run, and the runner creates a partition to execute in. The environment owner has asked that no partition be created, so the final step — provisioning a case's fixtures into a real model and filling
EvalScoreCard.RuntimeCleanfrom a live run — is not something to do unasked. The parser and the scorecard slot landed in #166; the oracle needs that decision first.For the record, nothing in this branch or its investigation changed the environment: the probes died at argument validation, and
SELECT PARTITIONKEY FROM PARTITIONSstill returns onlyinitial.🤖 Generated with Claude Code
https://claude.ai/code/session_01SuTvn5ymx3HAUtGjsxBuaA