feat: land knowledge-audit Phase 2 on main (stranded by a mis-targeted PR base) - #157
Merged
Conversation
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
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.
@
Why this exists
PR #155 ("knowledge-audit Phase 2") was opened with
feat/object-type-registryas its base instead ofmain, and the stack then merged out of order:feat/object-type-registry→ mainfeat/knowledge-audit-phase-2→feat/object-type-registrySo #155 reads as MERGED, but its four commits only ever reached the feature branch. None of them are on
main:This PR merges current
maininto the branch and re-targets the whole of Phase 2 atmain. No content is re-authored — it is the original four commits plus one merge commit.What lands
metadata-contracts.json, +9,076 lines).preparebecomes 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.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 test— 962 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_guardsfailed on a file count (expected 3, got 2). It passes in isolation and on re-run, and neither side of this merge touchesWorkflowScaffolderTests.cs,WorkflowScaffolder.cs, or theScaffoldFileWriterwrite 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, whichWriteCorereads viaD365FoSettings.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
@