Skip to content

Finish #164 (R5 form-engine extras), and what #160 still needs - #167

Merged
dynamics365ninja merged 2 commits into
mainfrom
feat/finish-164-160
Aug 6, 2026
Merged

Finish #164 (R5 form-engine extras), and what #160 still needs#167
dynamics365ninja merged 2 commits into
mainfrom
feat/finish-164-160

Conversation

@dynamics365ninja

Copy link
Copy Markdown
Owner

Closes the two issues left open by #166. Full suite green (1174 tests, three consecutive clean runs).

Issue State Commit
#164 R3 (RDL) + R5 (form engine extras) closes ea61000, 6731160
#160 L4 runtime oracle stays open — one blocker removed, see below

#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:

  • Real: three shipped forms use the HubPartGrid pattern and the registry has no entry, so form-pattern validate and repair cannot judge them.
  • False positive, suppressed: Custom is 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 no PatternVersion. 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 with i:type on every control, so a round-trip through XDocument rewrites namespace declarations nobody asked to change. Verified against a shipped 16 KB CustGroup — the clone differs on exactly the intended lines, is byte-identical elsewhere, and passes validate xpp clean.

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 --rebind runs, 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: --from names 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-to and --into-role.

#160 — one blocker removed, one constraint remains

Not closed. Two findings recorded on the issue:

/unattended suppresses the debugger prompt. SysTestConsole.exe calls WaitForDebugger() before it parses arguments and throws on a redirected stdin, which I reported earlier as blocking any headless harness. It does not: with /unattended the runner proceeds to Executing 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.RuntimeClean from 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 PARTITIONS still returns only initial.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SuTvn5ymx3HAUtGjsxBuaA

dynamics365ninja and others added 2 commits August 6, 2026 19:22
#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
@dynamics365ninja
dynamics365ninja merged commit dc5f7a9 into main Aug 6, 2026
@dynamics365ninja
dynamics365ninja deleted the feat/finish-164-160 branch August 7, 2026 05:18
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
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