d365fo-agent-developer 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. d365fo_agent_developer-0.6.0/LICENSE +21 -0
  2. d365fo_agent_developer-0.6.0/PKG-INFO +171 -0
  3. d365fo_agent_developer-0.6.0/README.md +140 -0
  4. d365fo_agent_developer-0.6.0/pyproject.toml +57 -0
  5. d365fo_agent_developer-0.6.0/setup.cfg +4 -0
  6. d365fo_agent_developer-0.6.0/src/d365fo_agent/__init__.py +2 -0
  7. d365fo_agent_developer-0.6.0/src/d365fo_agent/aot_relations.py +147 -0
  8. d365fo_agent_developer-0.6.0/src/d365fo_agent/build.py +285 -0
  9. d365fo_agent_developer-0.6.0/src/d365fo_agent/cli.py +651 -0
  10. d365fo_agent_developer-0.6.0/src/d365fo_agent/data/aot-type-profiles.json +1836 -0
  11. d365fo_agent_developer-0.6.0/src/d365fo_agent/data/x++-methodology.md +152 -0
  12. d365fo_agent_developer-0.6.0/src/d365fo_agent/data/x++-rules.json +48 -0
  13. d365fo_agent_developer-0.6.0/src/d365fo_agent/entity_derive.py +176 -0
  14. d365fo_agent_developer-0.6.0/src/d365fo_agent/generator.py +1393 -0
  15. d365fo_agent_developer-0.6.0/src/d365fo_agent/graph_query.py +107 -0
  16. d365fo_agent_developer-0.6.0/src/d365fo_agent/graphify_runner.py +304 -0
  17. d365fo_agent_developer-0.6.0/src/d365fo_agent/index_store.py +465 -0
  18. d365fo_agent_developer-0.6.0/src/d365fo_agent/indexer.py +292 -0
  19. d365fo_agent_developer-0.6.0/src/d365fo_agent/knowledge.py +393 -0
  20. d365fo_agent_developer-0.6.0/src/d365fo_agent/knowledge_fetch.py +70 -0
  21. d365fo_agent_developer-0.6.0/src/d365fo_agent/linter.py +369 -0
  22. d365fo_agent_developer-0.6.0/src/d365fo_agent/mcp_server.py +842 -0
  23. d365fo_agent_developer-0.6.0/src/d365fo_agent/models.py +48 -0
  24. d365fo_agent_developer-0.6.0/src/d365fo_agent/packageslocal_export.py +253 -0
  25. d365fo_agent_developer-0.6.0/src/d365fo_agent/rules.py +42 -0
  26. d365fo_agent_developer-0.6.0/src/d365fo_agent/security_wiring.py +198 -0
  27. d365fo_agent_developer-0.6.0/src/d365fo_agent/specs.py +651 -0
  28. d365fo_agent_developer-0.6.0/src/d365fo_agent/sql_model.py +342 -0
  29. d365fo_agent_developer-0.6.0/src/d365fo_agent/type_profile.py +113 -0
  30. d365fo_agent_developer-0.6.0/src/d365fo_agent/validate.py +180 -0
  31. d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/PKG-INFO +171 -0
  32. d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/SOURCES.txt +44 -0
  33. d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/dependency_links.txt +1 -0
  34. d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/entry_points.txt +3 -0
  35. d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/requires.txt +8 -0
  36. d365fo_agent_developer-0.6.0/src/d365fo_agent_developer.egg-info/top_level.txt +1 -0
  37. d365fo_agent_developer-0.6.0/tests/test_aot_relations.py +131 -0
  38. d365fo_agent_developer-0.6.0/tests/test_build.py +222 -0
  39. d365fo_agent_developer-0.6.0/tests/test_catalog.py +268 -0
  40. d365fo_agent_developer-0.6.0/tests/test_cli.py +64 -0
  41. d365fo_agent_developer-0.6.0/tests/test_entity_derive.py +108 -0
  42. d365fo_agent_developer-0.6.0/tests/test_linter.py +145 -0
  43. d365fo_agent_developer-0.6.0/tests/test_mcp_toolkit.py +599 -0
  44. d365fo_agent_developer-0.6.0/tests/test_security_wiring.py +130 -0
  45. d365fo_agent_developer-0.6.0/tests/test_spec_pipeline.py +1775 -0
  46. d365fo_agent_developer-0.6.0/tests/test_sql_model.py +237 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Bru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: d365fo-agent-developer
3
+ Version: 0.6.0
4
+ Summary: Local D365 F&O knowledge toolkit: corpus indexing (SQLite FTS5), deterministic generation, and an MCP server that grounds coding agents (Claude Code, Codex) in real AOT facts.
5
+ Author-email: David Bru <dbru@fiveforty.fr>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dbru540/d365fo-agent
8
+ Project-URL: Repository, https://github.com/dbru540/d365fo-agent
9
+ Project-URL: Issues, https://github.com/dbru540/d365fo-agent/issues
10
+ Keywords: dynamics-365,d365fo,x++,aot,mcp,model-context-protocol,claude,claude-code,codex,ai,code-generation,rag
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Code Generators
20
+ Classifier: Topic :: Software Development :: Build Tools
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: graph
25
+ Requires-Dist: graphify; extra == "graph"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Requires-Dist: ruff; extra == "dev"
29
+ Requires-Dist: build; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # d365fo-agent — D365 F&O X++ knowledge for Claude Code & Codex
33
+
34
+ A local **MCP server** that gives an AI coding agent (Claude Code, Codex, any MCP host) a grounded
35
+ **knowledge base of Dynamics 365 Finance & Operations X++** — so you can develop for D365 quickly
36
+ **without re-feeding a whole repo for analysis every time**.
37
+
38
+ It grounds the agent in real AOT facts: it verifies that a class/table/EDT/enum/entity actually
39
+ exists (anti-hallucination), searches the corpus, walks extension/security relationships, serves an
40
+ X++ engineering methodology, validates generated XML against learned per-type structure, scaffolds
41
+ or deterministically generates artifacts, and (on a Windows D365 host) compiles with the real X++
42
+ compiler.
43
+
44
+ - **Pure Python standard library** — zero runtime dependencies, runs anywhere Python 3.11+ runs.
45
+ - **Vendor-neutral** — Claude Code, Codex, Gemini CLI, or any MCP-speaking client over stdio.
46
+ - The standard D365 corpus is the same for everyone, so it is indexed **once** and used as a
47
+ portable knowledge base; your own custom code is **optional**.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install d365fo-agent-developer
53
+ # or, isolated: pipx install d365fo-agent-developer
54
+ ```
55
+
56
+ This installs two commands: `d365fo-mcp` (the server) and `d365fo-agent` (the CLI).
57
+
58
+ ## Get the knowledge base (once)
59
+
60
+ Pick one. Both produce a local index at `~/.d365fo-agent/d365fo.db` that the server uses by default.
61
+
62
+ **A. Download the prebuilt standard-D365 index** (fastest, no D365 install needed):
63
+
64
+ ```bash
65
+ d365fo-agent fetch-knowledge # downloads + caches the standard knowledge index
66
+ ```
67
+
68
+ **B. Build it from your own D365 dev box** (no download; uses metadata you already have):
69
+
70
+ ```bash
71
+ d365fo-agent build-index \
72
+ --db ~/.d365fo-agent/d365fo.db \
73
+ --packages-root "C:/AOSService/PackagesLocalDirectory" \
74
+ --rebuild
75
+ ```
76
+
77
+ > The methodology, default lint rules, and a default learned type-profile ship **inside** the
78
+ > package, so validation and guidance work out of the box even before the index is built.
79
+
80
+ ## Wire it into your agent
81
+
82
+ **Claude Code** — add to `.mcp.json` (project) or `~/.claude.json` (global):
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "d365fo": { "command": "d365fo-mcp", "args": [] }
88
+ }
89
+ }
90
+ ```
91
+
92
+ …or one command: `claude mcp add d365fo d365fo-mcp`
93
+
94
+ **Codex** — add to `~/.codex/config.toml`:
95
+
96
+ ```toml
97
+ [mcp_servers.d365fo]
98
+ command = "d365fo-mcp"
99
+ args = []
100
+ ```
101
+
102
+ With no `--db`, the server uses the cached knowledge index automatically. That's it — ask the agent
103
+ to build something for D365 and it will verify elements, follow the methodology, and validate its
104
+ output instead of guessing.
105
+
106
+ ## Add your custom code (optional)
107
+
108
+ Point the server at your D365 source repo so your **custom** classes/tables/EDTs/enums/extensions
109
+ are indexed too, and so the rich tools can read real signatures and clone real examples:
110
+
111
+ ```toml
112
+ [mcp_servers.d365fo]
113
+ command = "d365fo-mcp"
114
+ args = ["--repo-root", "C:/path/to/your/D365Repo",
115
+ "--rules", "C:/path/to/your/rules.json",
116
+ "--packages-root", "C:/AOSService/PackagesLocalDirectory"]
117
+ ```
118
+
119
+ | Capability | Knowledge index only | + a PackagesLocalDirectory / repo |
120
+ |---|---|---|
121
+ | Verify an element exists, search, relations, methodology, validation, scaffolding by template | ✅ | ✅ |
122
+ | Read a real signature, clone a real example (`get_signature`, `find_similar_examples`, `scaffold_object`) | needs source files | ✅ |
123
+ | Compile with the real X++ compiler (`compile_model`) | — | ✅ (Windows D365 host) |
124
+
125
+ ## What the agent gets (MCP tools)
126
+
127
+ `element_exists`, `find_element`, `search_corpus`, `get_signature`, `get_extension_chain`,
128
+ `get_security_links`, `get_entity_exposure`, `find_similar_examples`, `scaffold_object`,
129
+ `find_references`, `find_reverse_references`, `analyze_spec`, `generate_from_spec`, `validate_xml`,
130
+ `lint_artifact`, `derive_entity`, `wire_security`, `compile_model`, `get_methodology`, `index_stats`.
131
+
132
+ See [docs/mcp-server.md](docs/mcp-server.md) for the verify-driven workflow and
133
+ [docs/x++-methodology.md](docs/x++-methodology.md) for the behavioural contract.
134
+
135
+ ## Maintainer: publish the knowledge index
136
+
137
+ The wheel stays tiny; the ~100 MB standard index is distributed as a downloadable asset.
138
+
139
+ ```bash
140
+ # 1. Build a STANDARD-only index from a PackagesLocalDirectory (no custom repo)
141
+ d365fo-agent build-index --db d365fo-standard.db --packages-root <PLD> --rebuild
142
+ # 2. (optional) learn type profiles to ship as the default
143
+ d365fo-agent build-type-profiles --db d365fo-standard.db --packages-root <PLD> \
144
+ --out src/d365fo_agent/data/aot-type-profiles.json
145
+ # 3. Compress and attach to a GitHub release
146
+ python -c "import gzip,shutil; shutil.copyfileobj(open('d365fo-standard.db','rb'), gzip.open('d365fo-standard.db.gz','wb'))"
147
+ # 4. Point users at it: set DEFAULT_KNOWLEDGE_URL in knowledge_fetch.py (or pass --url)
148
+ ```
149
+
150
+ > **Note:** the index holds factual AOT metadata (element names, types, packages, labels,
151
+ > relations) — not Microsoft source. Confirm your redistribution position before publishing a
152
+ > prebuilt standard index; option **B** above lets each user build their own with zero redistribution.
153
+
154
+ To publish the package itself: `python -m build` then `python -m twine upload dist/*` (PyPI account
155
+ required).
156
+
157
+ ## Develop / contribute
158
+
159
+ ```bash
160
+ pip install -e ".[dev]"
161
+ PYTHONPATH=src python -m unittest discover -s tests # full test suite
162
+ ruff check src/d365fo_agent tests
163
+ ```
164
+
165
+ Docs: [Architecture](docs/architecture.md) · [MCP Server](docs/mcp-server.md) ·
166
+ [X++ Methodology](docs/x++-methodology.md) · [Specification Contract](docs/specification-contract.md) ·
167
+ [Metadata Schema](docs/metadata-schema.md) · [Tool Catalog](docs/tool-catalog.md)
168
+
169
+ ## License
170
+
171
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,140 @@
1
+ # d365fo-agent — D365 F&O X++ knowledge for Claude Code & Codex
2
+
3
+ A local **MCP server** that gives an AI coding agent (Claude Code, Codex, any MCP host) a grounded
4
+ **knowledge base of Dynamics 365 Finance & Operations X++** — so you can develop for D365 quickly
5
+ **without re-feeding a whole repo for analysis every time**.
6
+
7
+ It grounds the agent in real AOT facts: it verifies that a class/table/EDT/enum/entity actually
8
+ exists (anti-hallucination), searches the corpus, walks extension/security relationships, serves an
9
+ X++ engineering methodology, validates generated XML against learned per-type structure, scaffolds
10
+ or deterministically generates artifacts, and (on a Windows D365 host) compiles with the real X++
11
+ compiler.
12
+
13
+ - **Pure Python standard library** — zero runtime dependencies, runs anywhere Python 3.11+ runs.
14
+ - **Vendor-neutral** — Claude Code, Codex, Gemini CLI, or any MCP-speaking client over stdio.
15
+ - The standard D365 corpus is the same for everyone, so it is indexed **once** and used as a
16
+ portable knowledge base; your own custom code is **optional**.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install d365fo-agent-developer
22
+ # or, isolated: pipx install d365fo-agent-developer
23
+ ```
24
+
25
+ This installs two commands: `d365fo-mcp` (the server) and `d365fo-agent` (the CLI).
26
+
27
+ ## Get the knowledge base (once)
28
+
29
+ Pick one. Both produce a local index at `~/.d365fo-agent/d365fo.db` that the server uses by default.
30
+
31
+ **A. Download the prebuilt standard-D365 index** (fastest, no D365 install needed):
32
+
33
+ ```bash
34
+ d365fo-agent fetch-knowledge # downloads + caches the standard knowledge index
35
+ ```
36
+
37
+ **B. Build it from your own D365 dev box** (no download; uses metadata you already have):
38
+
39
+ ```bash
40
+ d365fo-agent build-index \
41
+ --db ~/.d365fo-agent/d365fo.db \
42
+ --packages-root "C:/AOSService/PackagesLocalDirectory" \
43
+ --rebuild
44
+ ```
45
+
46
+ > The methodology, default lint rules, and a default learned type-profile ship **inside** the
47
+ > package, so validation and guidance work out of the box even before the index is built.
48
+
49
+ ## Wire it into your agent
50
+
51
+ **Claude Code** — add to `.mcp.json` (project) or `~/.claude.json` (global):
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "d365fo": { "command": "d365fo-mcp", "args": [] }
57
+ }
58
+ }
59
+ ```
60
+
61
+ …or one command: `claude mcp add d365fo d365fo-mcp`
62
+
63
+ **Codex** — add to `~/.codex/config.toml`:
64
+
65
+ ```toml
66
+ [mcp_servers.d365fo]
67
+ command = "d365fo-mcp"
68
+ args = []
69
+ ```
70
+
71
+ With no `--db`, the server uses the cached knowledge index automatically. That's it — ask the agent
72
+ to build something for D365 and it will verify elements, follow the methodology, and validate its
73
+ output instead of guessing.
74
+
75
+ ## Add your custom code (optional)
76
+
77
+ Point the server at your D365 source repo so your **custom** classes/tables/EDTs/enums/extensions
78
+ are indexed too, and so the rich tools can read real signatures and clone real examples:
79
+
80
+ ```toml
81
+ [mcp_servers.d365fo]
82
+ command = "d365fo-mcp"
83
+ args = ["--repo-root", "C:/path/to/your/D365Repo",
84
+ "--rules", "C:/path/to/your/rules.json",
85
+ "--packages-root", "C:/AOSService/PackagesLocalDirectory"]
86
+ ```
87
+
88
+ | Capability | Knowledge index only | + a PackagesLocalDirectory / repo |
89
+ |---|---|---|
90
+ | Verify an element exists, search, relations, methodology, validation, scaffolding by template | ✅ | ✅ |
91
+ | Read a real signature, clone a real example (`get_signature`, `find_similar_examples`, `scaffold_object`) | needs source files | ✅ |
92
+ | Compile with the real X++ compiler (`compile_model`) | — | ✅ (Windows D365 host) |
93
+
94
+ ## What the agent gets (MCP tools)
95
+
96
+ `element_exists`, `find_element`, `search_corpus`, `get_signature`, `get_extension_chain`,
97
+ `get_security_links`, `get_entity_exposure`, `find_similar_examples`, `scaffold_object`,
98
+ `find_references`, `find_reverse_references`, `analyze_spec`, `generate_from_spec`, `validate_xml`,
99
+ `lint_artifact`, `derive_entity`, `wire_security`, `compile_model`, `get_methodology`, `index_stats`.
100
+
101
+ See [docs/mcp-server.md](docs/mcp-server.md) for the verify-driven workflow and
102
+ [docs/x++-methodology.md](docs/x++-methodology.md) for the behavioural contract.
103
+
104
+ ## Maintainer: publish the knowledge index
105
+
106
+ The wheel stays tiny; the ~100 MB standard index is distributed as a downloadable asset.
107
+
108
+ ```bash
109
+ # 1. Build a STANDARD-only index from a PackagesLocalDirectory (no custom repo)
110
+ d365fo-agent build-index --db d365fo-standard.db --packages-root <PLD> --rebuild
111
+ # 2. (optional) learn type profiles to ship as the default
112
+ d365fo-agent build-type-profiles --db d365fo-standard.db --packages-root <PLD> \
113
+ --out src/d365fo_agent/data/aot-type-profiles.json
114
+ # 3. Compress and attach to a GitHub release
115
+ python -c "import gzip,shutil; shutil.copyfileobj(open('d365fo-standard.db','rb'), gzip.open('d365fo-standard.db.gz','wb'))"
116
+ # 4. Point users at it: set DEFAULT_KNOWLEDGE_URL in knowledge_fetch.py (or pass --url)
117
+ ```
118
+
119
+ > **Note:** the index holds factual AOT metadata (element names, types, packages, labels,
120
+ > relations) — not Microsoft source. Confirm your redistribution position before publishing a
121
+ > prebuilt standard index; option **B** above lets each user build their own with zero redistribution.
122
+
123
+ To publish the package itself: `python -m build` then `python -m twine upload dist/*` (PyPI account
124
+ required).
125
+
126
+ ## Develop / contribute
127
+
128
+ ```bash
129
+ pip install -e ".[dev]"
130
+ PYTHONPATH=src python -m unittest discover -s tests # full test suite
131
+ ruff check src/d365fo_agent tests
132
+ ```
133
+
134
+ Docs: [Architecture](docs/architecture.md) · [MCP Server](docs/mcp-server.md) ·
135
+ [X++ Methodology](docs/x++-methodology.md) · [Specification Contract](docs/specification-contract.md) ·
136
+ [Metadata Schema](docs/metadata-schema.md) · [Tool Catalog](docs/tool-catalog.md)
137
+
138
+ ## License
139
+
140
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "d365fo-agent-developer"
7
+ version = "0.6.0"
8
+ description = "Local D365 F&O knowledge toolkit: corpus indexing (SQLite FTS5), deterministic generation, and an MCP server that grounds coding agents (Claude Code, Codex) in real AOT facts."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "David Bru", email = "dbru@fiveforty.fr" }]
13
+ keywords = [
14
+ "dynamics-365", "d365fo", "x++", "aot", "mcp", "model-context-protocol",
15
+ "claude", "claude-code", "codex", "ai", "code-generation", "rag",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Software Development :: Code Generators",
27
+ "Topic :: Software Development :: Build Tools",
28
+ ]
29
+ # Standard library only — nothing to install at runtime, which keeps the MCP server portable.
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/dbru540/d365fo-agent"
34
+ Repository = "https://github.com/dbru540/d365fo-agent"
35
+ Issues = "https://github.com/dbru540/d365fo-agent/issues"
36
+
37
+ [project.optional-dependencies]
38
+ # The graph-building engine is the ONLY external dependency, and only the
39
+ # `run-graphify-staging` command needs it. Everything else (indexing, generation,
40
+ # MCP server, validation) is Python standard library only.
41
+ graph = ["graphify"]
42
+ dev = ["pytest", "ruff", "build"]
43
+
44
+ [project.scripts]
45
+ d365fo-agent = "d365fo_agent.cli:main"
46
+ d365fo-mcp = "d365fo_agent.mcp_server:main"
47
+
48
+ [tool.setuptools]
49
+ package-dir = { "" = "src" }
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.setuptools.package-data]
55
+ # Methodology, default lint rules, and a default learned type-profile ship inside the package so
56
+ # the server works after a plain `pip install` (no source tree needed).
57
+ d365fo_agent = ["data/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Local tooling for D365 F&O corpus curation and metadata extraction."""
2
+
@@ -0,0 +1,147 @@
1
+ """Extract AOT table relations — the D365 equivalent of foreign keys.
2
+
3
+ AX business tables define no SQL foreign keys; the relational graph lives in the AOT
4
+ metadata: every ``AxTable`` (and ``AxTableExtension``) XML carries a ``<Relations>`` block
5
+ with the related table, the relationship type (Association/Composition), both cardinalities,
6
+ and the EXACT join fields (``<AxTableRelationConstraintField>``: Field ↔ RelatedField, plus
7
+ fixed-value constraints). This module walks one or more corpus roots (PackagesLocalDirectory
8
+ and/or editable source trees), parses those blocks, and stores them next to the SQL data
9
+ model so ``find_relations``/``get_sql_model`` can explain table relationships with the same
10
+ authority a foreign key would. Standard library only.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sqlite3
16
+ import xml.etree.ElementTree as ET
17
+ from pathlib import Path
18
+ from typing import Iterator
19
+
20
+ _XSI_TYPE = "{http://www.w3.org/2001/XMLSchema-instance}type"
21
+ # Mirrors index_store._NON_AOT_DIRS: build outputs that contain compiled XML copies.
22
+ _SKIP_DIRS = {"bin", "Descriptor", "Resources", "XppMetadata", "AdditionalFiles", "obj"}
23
+
24
+ _SCHEMA = """
25
+ CREATE TABLE IF NOT EXISTS aot_relations(
26
+ table_name TEXT, relation_name TEXT, related_table TEXT,
27
+ relationship_type TEXT, cardinality TEXT, related_cardinality TEXT,
28
+ edt_relation INTEGER, source_element TEXT,
29
+ PRIMARY KEY(table_name, relation_name, source_element));
30
+ CREATE TABLE IF NOT EXISTS aot_relation_fields(
31
+ table_name TEXT, relation_name TEXT, kind TEXT,
32
+ field TEXT, related_field TEXT, fixed_value TEXT, source_edt TEXT);
33
+ CREATE INDEX IF NOT EXISTS ix_aot_rel_table ON aot_relations(table_name);
34
+ CREATE INDEX IF NOT EXISTS ix_aot_rel_related ON aot_relations(related_table);
35
+ """
36
+
37
+
38
+ def _text(node: ET.Element | None) -> str | None:
39
+ return node.text.strip() if node is not None and node.text else None
40
+
41
+
42
+ def _iter_table_files(root: Path) -> Iterator[tuple[Path, str]]:
43
+ """Yield (xml_path, element_kind) for every AxTable/AxTableExtension under a corpus root."""
44
+ for kind in ("AxTable", "AxTableExtension"):
45
+ for type_dir in root.rglob(kind):
46
+ if not type_dir.is_dir() or type_dir.name != kind:
47
+ continue
48
+ if any(part in _SKIP_DIRS for part in type_dir.relative_to(root).parts):
49
+ continue
50
+ for xml_file in type_dir.glob("*.xml"):
51
+ yield xml_file, kind
52
+
53
+
54
+ def parse_table_relations(xml_path: Path, kind: str) -> tuple[str, str, list[dict[str, object]]]:
55
+ """Parse one AxTable/AxTableExtension XML. Returns (table_name, source_element, relations).
56
+
57
+ For an extension ``CustTable.MyModel``, the relations belong to ``CustTable``.
58
+ """
59
+ tree = ET.parse(xml_path)
60
+ root = tree.getroot()
61
+ element_name = _text(root.find("Name")) or xml_path.stem
62
+ table_name = element_name.split(".")[0] if kind == "AxTableExtension" else element_name
63
+
64
+ relations: list[dict[str, object]] = []
65
+ for rel in root.iter("AxTableRelation"):
66
+ constraints: list[dict[str, object]] = []
67
+ for constraint in rel.iter("AxTableRelationConstraint"):
68
+ ctype = (constraint.get(_XSI_TYPE) or "AxTableRelationConstraintField")
69
+ constraints.append({
70
+ "kind": ctype.replace("AxTableRelationConstraint", "").lower() or "field",
71
+ "field": _text(constraint.find("Field")),
72
+ "related_field": _text(constraint.find("RelatedField")),
73
+ "fixed_value": _text(constraint.find("Value")),
74
+ "source_edt": _text(constraint.find("SourceEDT")),
75
+ })
76
+ relations.append({
77
+ "name": _text(rel.find("Name")),
78
+ "related_table": _text(rel.find("RelatedTable")),
79
+ "relationship_type": _text(rel.find("RelationshipType")),
80
+ "cardinality": _text(rel.find("Cardinality")),
81
+ "related_cardinality": _text(rel.find("RelatedTableCardinality")),
82
+ "edt_relation": 1 if _text(rel.find("EDTRelation")) == "Yes" else 0,
83
+ "constraints": constraints,
84
+ })
85
+ return table_name, element_name, relations
86
+
87
+
88
+ def extract_aot_relations(
89
+ roots: "list[str | Path]",
90
+ db_path: str | Path,
91
+ *,
92
+ progress: "callable[[str, int], None] | None" = None,
93
+ ) -> dict[str, int]:
94
+ """Walk every AxTable/AxTableExtension under ``roots`` and persist their relations."""
95
+ conn = sqlite3.connect(Path(db_path))
96
+ conn.executescript("DELETE FROM aot_relations; DELETE FROM aot_relation_fields;"
97
+ if conn.execute("SELECT 1 FROM sqlite_master WHERE name='aot_relations'").fetchone()
98
+ else "SELECT 1;")
99
+ conn.executescript(_SCHEMA)
100
+
101
+ files = relations_count = 0
102
+ rel_rows: list[tuple] = []
103
+ field_rows: list[tuple] = []
104
+ for root in roots:
105
+ root = Path(root)
106
+ for xml_file, kind in _iter_table_files(root):
107
+ try:
108
+ table, element, relations = parse_table_relations(xml_file, kind)
109
+ except OSError:
110
+ # Windows MAX_PATH: deep AOT paths exceed 260 chars — retry extended-length.
111
+ try:
112
+ table, element, relations = parse_table_relations(
113
+ Path("\\\\?\\" + str(xml_file.resolve())), kind)
114
+ except (OSError, ET.ParseError):
115
+ continue
116
+ except ET.ParseError:
117
+ continue
118
+ files += 1
119
+ for rel in relations:
120
+ if not rel["related_table"]:
121
+ continue
122
+ relations_count += 1
123
+ rel_rows.append((table, rel["name"], rel["related_table"],
124
+ rel["relationship_type"], rel["cardinality"],
125
+ rel["related_cardinality"], rel["edt_relation"], element))
126
+ for c in rel["constraints"]:
127
+ field_rows.append((table, rel["name"], c["kind"], c["field"],
128
+ c["related_field"], c["fixed_value"], c["source_edt"]))
129
+ if len(rel_rows) >= 5000:
130
+ conn.executemany("INSERT OR REPLACE INTO aot_relations VALUES(?,?,?,?,?,?,?,?)", rel_rows)
131
+ conn.executemany("INSERT INTO aot_relation_fields VALUES(?,?,?,?,?,?,?)", field_rows)
132
+ conn.commit()
133
+ rel_rows, field_rows = [], []
134
+ if progress:
135
+ progress(str(root), relations_count)
136
+ conn.executemany("INSERT OR REPLACE INTO aot_relations VALUES(?,?,?,?,?,?,?,?)", rel_rows)
137
+ conn.executemany("INSERT INTO aot_relation_fields VALUES(?,?,?,?,?,?,?)", field_rows)
138
+ conn.commit()
139
+ stats = {
140
+ "files_parsed": files,
141
+ "relations": conn.execute("SELECT COUNT(*) FROM aot_relations").fetchone()[0],
142
+ "constraint_fields": conn.execute("SELECT COUNT(*) FROM aot_relation_fields").fetchone()[0],
143
+ "tables_with_relations": conn.execute(
144
+ "SELECT COUNT(DISTINCT table_name) FROM aot_relations").fetchone()[0],
145
+ }
146
+ conn.close()
147
+ return stats