Skip to content

feat: land knowledge-audit Phase 2 on main (stranded by a mis-targeted PR base) - #157

Merged
dynamics365ninja merged 6 commits into
mainfrom
feat/object-type-registry
Aug 6, 2026
Merged

feat: land knowledge-audit Phase 2 on main (stranded by a mis-targeted PR base)#157
dynamics365ninja merged 6 commits into
mainfrom
feat/object-type-registry

Conversation

@dynamics365ninja

Copy link
Copy Markdown
Owner

@

Why this exists

PR #155 ("knowledge-audit Phase 2") was opened with feat/object-type-registry as its base instead of main, and the stack then merged out of order:

time PR head → base effect
04:57 #154 feat/object-type-registrymain the branch landed on main
05:11 #155 feat/knowledge-audit-phase-2feat/object-type-registry Phase 2 landed on the branch 14 minutes after it had already been merged out

So #155 reads as MERGED, but its four commits only ever reached the feature branch. None of them are on main:

$ git cherry origin/main origin/feat/object-type-registry
+ 813e45f  feat: teach the contract catalog enum values and subtype resolution
+ cbb0846  fix: key the contract catalog by contract name, and rebuild the families it exposed
+ 5d7e1d3  feat: complete the report stack, deepen menu items, and refuse to write unreadable AOT
+ e74a3f2  feat: MCP parity, registry-driven prepare, and the three missing extension kinds

This PR merges current main into the branch and re-targets the whole of Phase 2 at main. No content is re-authored — it is the original four commits plus one merge commit.

What lands

  • Contract catalog keyed by contract name, with enum values and subtype resolution; the object families it exposes rebuilt (metadata-contracts.json, +9,076 lines).
  • Report stack completed, menu items deepened, and the writer now refuses to emit AOT the metadata reader would mangle rather than writing it silently.
  • MCP parity: prepare becomes registry-driven and the three missing extension kinds are covered, so the same request no longer yields a correct file via CLI and a lossy document via MCP.
  • Three new test files absent from main: ContractShapeGenerationTests, McpGenerationParityTests, MetadataContractsAotTests.

Merge notes

The only file changed on both sides since the merge base was docs/KNOWLEDGE_AUDIT_PLAN.md, and it auto-resolved — Phase 3 (#156) worked in different areas. No manual conflict resolution.

Verification

  • dotnet build — 0 errors.
  • dotnet test962 passed, 0 failed (351 CLI + 611 Core).

One caveat worth recording: on the first full-suite run WorkflowScaffolderTests.Scaffolded_workflow_objects_pass_the_pre_write_guards failed on a file count (expected 3, got 2). It passes in isolation and on re-run, and neither side of this merge touches WorkflowScaffolderTests.cs, WorkflowScaffolder.cs, or the ScaffoldFileWriter write path — so this is a pre-existing flake, not a regression from this merge. Likely cause is cross-test interference from the four Core test classes that mutate process-wide environment variables, which WriteCore reads via D365FoSettings.FromEnvironment(). Not addressed here; worth its own issue.

CI never ran on the branch head after #155 landed on it (the last run was at the merge base), so this is the first CI signal for Phase 2.

🤖 Generated with Claude Code
@

dynamics365ninja and others added 6 commits August 6, 2026 03:56
Two additions to the catalog derived in 1.3, both aimed at the same thing:
letting an offline check reach conclusions the live provider would.

Enum values (217 enums, 8,820 members unchanged). An unknown *member* is
dropped silently; an unknown *enum value* is worse — DataContractSerializer
throws and abandons the document, so the object is invisible to the entire
toolchain. Both instances this repo shipped looked entirely plausible:
`Style=TileSection` on a workspace group and `TabStyle=TOCList` on a
table-of-contents form. XML008 reports them offline, with the valid set.

Subtype resolution, split on whether the named contract can be instantiated.
An element named after an *abstract* contract must become something else, and
the serializer resolves that from the collection holding it — every shipped
form writes `<AxFormDataSource>` (abstract, five members) carrying an
`AxFormDataSourceRoot` (thirty). Ranking those against the base finds no
position for `AllowDelete` or `DataSourceLinks`, so they stay put and get
dropped on read. A *concrete* contract gets no such promotion: it is
instantiated as named and a subtype's member is simply not there, which is
how every data entity field lost `DataField` and `DataSource`.

That distinction was a hypothesis, so it was tested before it was trusted:
the shipped-file calibration passes at ten times its usual sample, meaning no
file Microsoft ships writes a subtype member under a concrete base name.
XML008 likewise finds nothing in shipped AOT — the discipline that killed
XML006, applied before shipping rather than after.

Offline lint now reproduces 7 of the 8 provider findings on the golden
catalog without a VM, which is what Phase 1.4 was asking for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5rweebVxabZzreN7N19rT
…ies it exposed

The catalog was keyed by CLR type name. Eighteen types serialize under a
different name, and the mismatch was not cosmetic: AxFormDataSourceRoot
contracts to <AxFormDataSource>, which is also a real abstract CLR type, so
every lookup of a form data source answered with the base's five members
instead of the root's thirty. AxMethodPropertyCollection writes as <Method>,
AxFormControlPropertyCollection as <Control>.

That single bug had been papered over. The subtype-substitution heuristic
added in the previous commit — "an element named after an abstract contract
resolves to the derived type accounting for most of its members" — was
reverse-engineered from the symptom. With correct keying it never fires, so
it is gone: an element names exactly the type the reader instantiates, and a
member the type lacks really is dropped. AcceptsMember no longer walks
subtypes, which is what let a data entity's <DataField> pass as acceptable.

Two more catalog gaps closed alongside it. Member value types are recorded,
so a walker can descend into <Grant> and <Design> — elements named after a
member, holding a type the name never mentions. And the Core assembly is
scanned too, where AccessGrant lives. Together they made the whole security
grant tree visible for the first time.

What that visibility found, each verified against the live provider:

- AxReport was largely fiction: <Datasets> (it is DataSets),
  <ReportParameters> (they live in DefaultParameterGroup), and a hand-rolled
  AxReportTablix/TablixBody tree that matches no AOT type. Everything was
  discarded on read — the report loaded with no datasets and no design.
  Rebuilt as AxReportAutoDesign with table data regions, the shape shipped
  reports use. RDL is deliberately not generated: it belongs to
  AxReportPrecisionDesign.Text, and a wrong RDL is far harder to detect than
  a wrong contract.
- AxDataEntityView had a <DataSources> member it does not have (they belong
  to the embedded ViewMetadata query), fields typed as the base
  AxDataEntityViewField so every DataField/DataSource mapping was dropped,
  and <IsMandatory> for what is Mandatory. Keys and EntityCategory added.
- Privileges wrote <AccessLevel>, which exists nowhere in the security model
  — access is six Allow/Deny permissions in <Grant>. Generated privileges
  granted nothing; the extractor read the same non-member, so every shipped
  privilege was indexed with a blank access level too.
- Form control methods were written under <DataControl> instead of <Control>
  and were dropped whole; data source links under <AxFormDataSourceLink>
  instead of <AxFormDataSourceRootLink>, same result.
- The workspace tile section claimed Style=TileSection, not a value of
  GroupStyle, which made the entire form unreadable. Shipped workspaces use
  the SectionTiles pattern on the tab page itself. FrameType was written on
  tab pages, where it belongs to groups.

Namespace handling is now derived rather than listed: a member whose contract
declares a different namespace starts a subtree in it. The hand-maintained
table of AxWorkflowApproval outcome members is gone, and the derivation
reproduces it exactly — while also catching AxReport's DefaultParameterGroup,
which was written in V2 and lost every report parameter.

Goldens: 43/51 readable-and-lossless -> 51/51, every one re-verified against
the provider before capture. Offline lint now reproduces all of it with no
VM, including two defects the bridge cannot see because SourceCode is
excluded from its comparison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5rweebVxabZzreN7N19rT
…te unreadable AOT

The contract catalog is now consulted on the way out. ScaffoldFileWriter runs
XML007/XML008 before writing anything, so a member the type does not declare
or a value outside its enum is an error at the point of the mistake rather
than a mystery on the AOS. Both rules are calibrated against shipped Microsoft
AOT, so this cannot start rejecting correct files without the calibration
failing first.

It found one immediately: `generate number-sequence` wrote
<NumberSequenceModule> onto an EDT, which has no such member. The association
is made in loadModule(), which the same command already generates — so the
EDT looked wired up and was not. Its golden could not have caught this: the
case captures the extension class, and the EDT is a sibling artifact nobody
was validating. The EDT root was also <AxEdtString> where every shipped EDT
and this repo's own Edt scaffolder write <AxEdt i:type="AxEdtString">.

Report stack (2.1) is now a stack. A report's DP declares and selects from
<DpClass>Tmp, and nothing created it — every generated report needed a table
written by hand before the model would compile. `generate report` now emits
the TempDB table per dataset, an SrsReportRunController, and an output menu
item pointing at it, each opt-out. Six files, all six verified lossless
against the live provider.

Menu items (2.2) gain EnumTypeParameter/EnumParameter, Parameters, Query,
ConfigurationKey and the linked-permission pair. NeededPermission is
deliberately not among them: the plan asked for it, but it is a form-control
member — a menu item has five independent *Permissions flags, so an access
level is expanded into those, cumulatively, the way shipped items use them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5rweebVxabZzreN7N19rT
…nsion kinds

MCP's gap turned out to be fidelity rather than coverage. The XML-only
handlers returned `doc.ToString()` — the raw scaffold, with none of the
contract namespace, member ordering or shape checking the file path applies.
The same request produced a correct file through the CLI and a document no
AOS would read through MCP, with nothing on disk afterwards to explain the
difference. Both surfaces now render through one emitter
(ScaffoldFileWriter.ToAotXml), which is the write path minus the write.

Coverage widened on top of that: generate_object gains menu-item, privilege,
duty, role, entity and extension. A new `validate` tool exposes xpp,
references and form-pattern — checks that existed only on the CLI, so an
agent could generate XML and had no way to ask whether it was sound — plus
`metadata-shape`, the offline half of `validate metadata`, which needs no
bridge because it judges against the contract catalog the AOT reader is
generated from.

`generate extension` gains view, query and dataEntityView. The query case is
the interesting one: the type is AxQuerySimpleExtension. AxQueryExtension
exists as neither a type nor a folder on any AOS — worth stating because the
first attempt at this "fixed" the registry to remove query extensions
entirely, on the strength of looking up the wrong name.

prepare's StrategiesFor gets bespoke advice for reports, entities and
security, and derives everything else from the registry: a kind with an
extension object now has it named, and a kind without one is told so, rather
than the old catch-all suggesting the agent go looking for an extension
mechanism that may not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5rweebVxabZzreN7N19rT
…ase-2

feat: knowledge-audit Phase 2 — generation depth, and the catalog bug that was hiding it
@dynamics365ninja
dynamics365ninja merged commit 4a9fa85 into main Aug 6, 2026
5 checks passed
@dynamics365ninja
dynamics365ninja deleted the feat/object-type-registry branch August 6, 2026 07:17
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