bc-atlas 0.11.0 → 0.13.0

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 (56) hide show
  1. package/.bca.example.json +0 -2
  2. package/CHANGELOG.md +121 -0
  3. package/README.md +22 -23
  4. package/THIRD_PARTY_NOTICES.md +23 -6
  5. package/package.json +10 -4
  6. package/src/analyzer.js +221 -13
  7. package/src/app/service.js +16 -2
  8. package/src/app/snapshot.js +4 -1
  9. package/src/app/ui/app.css +93 -7
  10. package/src/app/ui/app.js +425 -79
  11. package/src/app/workspace.js +62 -19
  12. package/src/blueprint/components.js +14 -4
  13. package/src/blueprint/evaluate.js +64 -18
  14. package/src/blueprint/index.js +3 -1
  15. package/src/blueprint/report.js +68 -3
  16. package/src/blueprint/resolutions.js +72 -0
  17. package/src/capabilities.js +10 -5
  18. package/src/commands/export.js +13 -5
  19. package/src/commands/refactor.js +4 -1
  20. package/src/commands/report.js +8 -18
  21. package/src/context-analysis.js +719 -0
  22. package/src/contract-analysis.js +372 -0
  23. package/src/d2.js +127 -81
  24. package/src/diagram/collapse.js +94 -0
  25. package/src/diagram/geometry.js +70 -0
  26. package/src/diagram/index.js +59 -0
  27. package/src/diagram/layout.js +191 -0
  28. package/src/diagram/metrics.js +4 -0
  29. package/src/diagram/pdf-font.js +133 -0
  30. package/src/diagram/pdf.js +205 -0
  31. package/src/diagram/png.js +36 -0
  32. package/src/diagram/svg.js +88 -0
  33. package/src/diagram/text.js +49 -0
  34. package/src/docs/export.js +75 -12
  35. package/src/docs/render/csv.js +6 -1
  36. package/src/docs/render/html.js +27 -13
  37. package/src/docs/task.js +70 -28
  38. package/src/graph/lenses/dependency.js +20 -3
  39. package/src/health.js +84 -13
  40. package/src/insights.js +107 -4
  41. package/src/knowledge/graph.js +213 -0
  42. package/src/knowledge/index.js +65 -0
  43. package/src/knowledge/ontology.js +105 -0
  44. package/src/knowledge/serialize.js +131 -0
  45. package/src/knowledge/vocabulary.js +210 -0
  46. package/src/output.js +12 -35
  47. package/src/refactor/move.js +14 -2
  48. package/src/relation-aggregation.js +18 -1
  49. package/src/resolver.js +1 -0
  50. package/src/sarif.js +48 -6
  51. package/src/views.js +10 -1
  52. package/vendor/README.md +9 -0
  53. package/vendor/fonts/LICENSE-LiberationSans.txt +101 -0
  54. package/vendor/fonts/LiberationSans-Bold.ttf +0 -0
  55. package/vendor/fonts/LiberationSans-Regular.ttf +0 -0
  56. package/src/svg.js +0 -67
package/.bca.example.json CHANGED
@@ -50,8 +50,6 @@
50
50
  },
51
51
  "details": false,
52
52
  "noExternal": false,
53
- "layout": "elk",
54
- "theme": 0,
55
53
  "sourceUrl": "https://github.com/example/repo/blob/{ref}/{file}#L{line}",
56
54
  "sourceRef": "main",
57
55
  "sourcePathPrefix": "apps/Sales",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,127 @@ All notable changes to BC Atlas will be documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
5
5
  follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 0.13.0 - 2026-11-04
8
+
9
+ ### Added
10
+
11
+ - `bca check` flags interface methods whose enum implementation can complete
12
+ silently: an empty body, a constant `true` return, or a conditional path
13
+ with no error (`contract-fallback`). Without configuration these are
14
+ informational; `check.contractExpectations` names the interface methods
15
+ that must not fall back silently and turns them into warnings. Unresolved
16
+ delegation, symbol-only bodies, and ambiguous signatures stay unknown
17
+ instead of counting as safe. See [Configuration](./docs/configuration.md).
18
+ - `check.contexts` summarizes the responsibilities of selected objects:
19
+ reads, persistent and temporary writes, commits, resolved calls, and global
20
+ state, direct and up to `maxCallDepth` calls away. `roles`,
21
+ `lazyReadMethods`, and `forbid` turn them into policy warnings; bounded or
22
+ unresolved evidence is reported with its limitations.
23
+ - Architecture component selectors accept `file:` (one file, globs allowed),
24
+ more specific than a namespace, folder, or ID range.
25
+ - Enum bindings keep `DefaultImplementation` and `UnknownValueImplementation`
26
+ as evidence, and calls keep their arguments.
27
+
28
+ ### Changed
29
+
30
+ - Health and architecture findings carry their basis (`configured-policy`,
31
+ `observed-code-fact`, or `heuristic` for a proposed architecture), a
32
+ primary location, all evidence, and limitations. Text and Markdown show up
33
+ to five locations; JSON and SARIF keep everything.
34
+ - Component cycles are explained by a representative path with the
35
+ dependencies behind each hop, and, where several providers call back into
36
+ the same orchestrator procedure, suggest extracting a shared service.
37
+ - `bca refactor move` defaults to the common parent folder of a component's
38
+ objects instead of the folder most of them are in.
39
+ - Records of pages and tables with `SourceTableTemporary` count as temporary,
40
+ and record parameters of unknown temporariness are no longer reported as
41
+ persistent writes.
42
+ - Release: one *Release* workflow publishes the npm package and the VS Code
43
+ extension under one version, after testing both, and attaches the `.vsix`
44
+ to the GitHub release.
45
+
46
+ ### Fixed
47
+
48
+ - Health reports no longer print `architecture origin: undefined`, and
49
+ Markdown evidence cells with `|` no longer break the table.
50
+
51
+ ## 0.12.0 - 2026-10-29
52
+
53
+ ### Added
54
+
55
+ - `bca export --view knowledge-graph` writes the project as RDF with the BC
56
+ Atlas OWL ontology, for triple stores, graph databases, and ontology
57
+ editors: Turtle (`.ttl`), RDF/XML (`.rdf`, `.owl`), N-Triples (`.nt`), or
58
+ JSON-LD (`.jsonld`). Apps, namespaces, objects with fields, keys, and
59
+ procedures, every relation as a property (`bca:calls`, `bca:writes`, ...)
60
+ and as a `bca:Relation` with its evidence, objects outside the workspace,
61
+ architecture components, and violations. Stable IRIs (`urn:bc-atlas:...`, or
62
+ `--base-iri`), and `SOURCE_DATE_EPOCH` for reproducible files. See
63
+ [Knowledge graph](./docs/knowledge-graph.md).
64
+ - Knowledge graph: `--no-evidence` writes only the direct relation triples,
65
+ without a `bca:Relation` per relation; the Base Application's graph drops
66
+ from 4.4 million triples (270 MB of Turtle) to 1.1 million (78 MB).
67
+ - The BC Atlas ontology is published at its IRI,
68
+ `https://schulzoli.github.io/BC-Atlas/ontology`: a page with every class
69
+ and property (each term's IRI opens its row), and the ontology as
70
+ `ontology.ttl`, `.owl`, `.rdf`, `.nt`, and `.jsonld` next to it, for
71
+ Protégé and other tools that resolve IRIs. Exports point to it with
72
+ `rdfs:seeAlso`.
73
+
74
+ ### Changed
75
+
76
+ - App and VS Code panel: Diagrams and Docs work the same way. On the left, a
77
+ filterable list (the views; the overview, use cases, and scenarios) with
78
+ the settings below it; on the right, the selected item with its tools.
79
+ `J`/`K` and **‹**/**›** step through the list, **Enter** in the filter opens
80
+ the first match, and the address keeps the selection and the settings.
81
+ - Diagrams: views are listed with what they show, choices apply at once,
82
+ **PNG** saves the diagram besides **SVG** and **D2**, and a click on an
83
+ object opens its page.
84
+ - Diagrams are drawn by BC Atlas itself instead of D2: an ELK layered layout
85
+ with groups, right-angled routes (curves beyond 120 relations), labels, and
86
+ a legend, as SVG, PNG, or vector PDF. `--format png` and `--format pdf` no
87
+ longer need the `d2` executable, and every drawing still writes its D2
88
+ source next to it. Text is sized with the metrics of the bundled Liberation
89
+ Sans (Arial and Helvetica widths), so boxes fit in all three formats. Large
90
+ views are faster and far less tall (a 200-object project view: 5 instead of
91
+ 19 seconds, 7499 × 6811 instead of 6112 × 25030 pixels).
92
+ - Diagrams page: hover an object to light up its relations and neighbors,
93
+ click it to open its page, find objects by name (**Enter** jumps to the next
94
+ match), and click a legend entry to hide that kind of relation.
95
+ - The npm package no longer installs `@terrastruct/d2` (58 MB); it uses
96
+ `elkjs` and `@resvg/resvg-wasm`. The VS Code extension shrinks from about 9
97
+ to 4.5 MB. The `layout` and `theme` settings of `.bca.json` (D2 options)
98
+ have no effect any more.
99
+ - Docs: the guides are part of the page instead of a page in a frame, one
100
+ overview, use case, or scenario at a time. Switch between user guide and
101
+ test case and between English and German; **Open test** opens the AL test
102
+ at its procedure; **Markdown**, **DITA**, **BPMN**, and **SVG** save the
103
+ page, **All as HTML** the whole documentation. Links between guides and
104
+ from a use case's process to its scenarios stay on the page.
105
+ - Large diagrams as an overview of their groups: every closed group is one
106
+ box with its object count and main object types, and the relations into,
107
+ out of, and between closed groups are bundled (one route per pair of groups
108
+ with the number of relations, per kind towards single objects). On the
109
+ Diagrams page, views with more than 120 objects or 250 relations start this
110
+ way; click a box to open its group, a group's name to close it, or use
111
+ **Open groups** and **Close groups**. Files of views with more than 1,500
112
+ objects are drawn as the overview instead of only writing the D2 source, so
113
+ the Base Application's project view (8,083 objects) now has a diagram.
114
+ - Diagrams page and the flows on the Docs page: on a dark theme the diagrams
115
+ turn dark too (saved files stay light).
116
+ - PDF diagrams with text outside Windows-1252 (Polish, Czech, Greek,
117
+ Cyrillic, ...) embed Liberation Sans, so the text shows and can be copied
118
+ and searched; other PDFs keep using Helvetica and stay small.
119
+ - The README's diagram images are rendered from the example SVGs by
120
+ `npm run examples`, and CI checks them with the other generated examples.
121
+
122
+ ### Security
123
+
124
+ - The Azure DevOps test-case CSV (`--export ado-csv`) writes cells that start
125
+ with `=`, `+`, `-`, or `@` with a leading apostrophe, so Excel and Google
126
+ Sheets do not run them as formulas.
127
+
7
128
  ## 0.11.0 - 2026-10-29
8
129
 
9
130
  ### Changed
package/README.md CHANGED
@@ -41,10 +41,10 @@ In VS Code, install the **BC Atlas** extension and open an AL project:
41
41
  | **Get started** | `bca setup` | Guided setup in the terminal or through your AI agent (Claude Code, Copilot, Cursor): a starter `.bca.json`, features, Husky or Git hooks, GitHub Actions or Azure Pipelines, cron schedule, and sync. |
42
42
  | **Analyze** | `bca check` | Health findings - cycles, forbidden dependencies, architecture violations, hot spots, diagnostics, and AL compiler and CodeCop/UICop/AppSourceCop findings - with a CI exit code, also as SARIF for code scanning. |
43
43
  | | `bca architecture` | Find a target architecture (components, layers), list every dependency that blocks it with how to resolve it, and enforce it with a shrinking baseline. [Guide](./docs/clean-architecture.md) |
44
- | **Export** | `bca export` | Everything the extension shows, as files: the BC Atlas pages as one offline HTML file to share (default), the Markdown report ([example](./examples/report/README.md)), the 3D explorer, one Markdown page per object, any of ten diagram views as D2, SVG, PNG, PDF, or JSON, with `--watch`. |
44
+ | **Export** | `bca export` | Everything the extension shows, as files: the BC Atlas pages as one offline HTML file to share (default), the Markdown report ([example](./examples/report/README.md)), the 3D explorer, one Markdown page per object, any of ten diagram views as D2, SVG, PNG, PDF, or JSON, with `--watch`, and the project as a knowledge graph in RDF with an OWL ontology ([Turtle, RDF/XML, OWL, N-Triples, JSON-LD](./docs/knowledge-graph.md)). |
45
45
  | | `bca docs` | User guides and test cases from AL UI tests (`[SCENARIO]`, `[GIVEN]`, `[WHEN]`, `[THEN]`) as Markdown, HTML, DITA 1.3, or Azure DevOps CSV, with real captions and translations, a flow diagram per scenario, and BPMN 2.0 processes per use case. [Example](./examples/docs/index.md) |
46
46
  | **Integrate** | `bca run` | One entry point for Git hooks, schedules, and pipelines; verifies, stages, or publishes generated files. Also available as a [GitHub Action](./docs/automation-and-ci.md#github-actions). |
47
- | | `bca update` | "BC Atlas 0.12.0 is available": release notes, one-step update, and regenerated hooks, tasks, and pipelines. Dependabot pull requests on request. |
47
+ | | `bca update` | "BC Atlas 0.14.0 is available": release notes, one-step update, and regenerated hooks, tasks, and pipelines. Dependabot pull requests on request. |
48
48
  | | `bca mcp` | A Model Context Protocol server for AI agents; `bca help --format json` is the versioned command contract. |
49
49
 
50
50
  Run `bca` for the same overview in your terminal and `bca help <command>` for
@@ -65,7 +65,7 @@ options and examples.
65
65
  | `ui` | How do pages, actions, and source tables connect? | `bca export --view ui --focus "Sales Order" -o ui.svg` |
66
66
  | `workflow` | What happens after an action or entry point? | `bca export --view workflow --entry action:Release -o flow.svg` |
67
67
 
68
- In VS Code, the Diagrams page of the BC Atlas panel renders every view with zoom and pan.
68
+ In VS Code, the Diagrams page of the BC Atlas panel renders every view with zoom and pan; click an object to open it.
69
69
 
70
70
  ### Clean architecture
71
71
 
@@ -208,7 +208,7 @@ and update-check tools. `bca setup agent --agent claude|copilot|cursor` adds a
208
208
  -> AL compiler: calls and diagnostics (src/al, when the toolchain is installed)
209
209
  -> resolved model with evidence (resolver, call analysis, semantics)
210
210
  -> view projection and insights (views, workflow, health)
211
- -> D2 / SVG / JSON / Markdown / HTML (renderers)
211
+ -> SVG / PNG / PDF / D2 / JSON / Markdown / HTML (renderers)
212
212
  -> the pages: one workspace, in VS Code (vscode/) or exported as one file
213
213
  ```
214
214
 
@@ -259,27 +259,25 @@ lists planned work.
259
259
 
260
260
  ### Publishing
261
261
 
262
- Releases are published manually through the `Publish npm package` workflow.
263
- Update and commit the version in `package.json` and `package-lock.json`, then
264
- enter that exact version in the workflow form. The workflow rejects mismatched
265
- or already published versions, runs all checks, and publishes with the selected
266
- dist-tag. It then tags the commit as `v<version>`, so
262
+ Releases are published manually through the `Release` workflow
263
+ (`.github/workflows/publish-npm.yml`). Set the version of both packages with
264
+ `npm run version:set -- <version>`, merge it, then run the workflow on `main`
265
+ with that exact version. It rejects mismatched, already published, or already
266
+ tagged versions, and tests the npm package and the VS Code extension, the
267
+ packaged `.vsix` included, before anything is published. Then it publishes the
268
+ npm package with the selected dist-tag and, unless switched off, the extension
269
+ to the Visual Studio Marketplace and, with the secret `OVSX_PAT`, to Open VSX.
270
+ Last, it tags the commit as `v<version>`, so
267
271
  `uses: SchulzOli/BC-Atlas@v<version>` resolves, and creates a GitHub release
268
- with the version's `CHANGELOG.md` section. npm authenticates through
272
+ with the version's `CHANGELOG.md` section and the `.vsix`.
273
+
274
+ No token is stored for either registry. npm authenticates through
269
275
  [trusted publishing](https://docs.npmjs.com/trusted-publishers): the package's
270
276
  trusted publisher on npmjs.com is this repository's `publish-npm.yml`
271
- workflow, so no npm token is stored in GitHub.
272
-
273
-
274
- The VS Code extension in `vscode/` is published by the `Publish VS Code
275
- extension` workflow: set `version` in `vscode/package.json`, then run the
276
- workflow with that version. It tests and packages the extension with the
277
- current engine and publishes it to the Visual Studio Marketplace as the
278
- publisher `SchulzOli`, signed in with Microsoft Entra ID through GitHub's OIDC
279
- token (no Marketplace token is stored; see the
280
- [launch checklist](https://github.com/SchulzOli/BC-Atlas/blob/main/vscode/marketing/README.md#launch-checklist)), and, with
281
- the secret `OVSX_PAT`, to Open VSX. Every CI run uploads the `.vsix` as the
282
- `bc-atlas-vsix` artifact.
277
+ workflow. The Marketplace publisher `SchulzOli` is signed in with Microsoft
278
+ Entra ID through GitHub's OIDC token in the `marketplace` environment (see the
279
+ [launch checklist](https://github.com/SchulzOli/BC-Atlas/blob/main/vscode/marketing/README.md#launch-checklist)).
280
+ Every CI run uploads the `.vsix` as the `bc-atlas-vsix` artifact.
283
281
 
284
282
  ## Contributing and support
285
283
 
@@ -291,5 +289,6 @@ Release notes are in [CHANGELOG.md](./CHANGELOG.md).
291
289
  ## License
292
290
 
293
291
  [MIT](./LICENSE). Dependencies retain their own licenses: `tree-sitter-al` and
294
- `web-tree-sitter` are MIT, and D2 is MPL-2.0. See
292
+ `web-tree-sitter` are MIT, elkjs is EPL-2.0, resvg is MPL-2.0, and the
293
+ Liberation Sans font is OFL-1.1. See
295
294
  [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
@@ -13,14 +13,31 @@ project.
13
13
  Copyright Tree-sitter contributors. Licensed under the MIT License.
14
14
  Source: <https://github.com/tree-sitter/tree-sitter>
15
15
 
16
- ## D2
16
+ ## ELK (elkjs)
17
17
 
18
- Copyright Terrastruct, Inc. and contributors. Licensed under the Mozilla Public
19
- License 2.0. Source: <https://github.com/terrastruct/d2>
18
+ Copyright Kiel University and others. Licensed under the Eclipse Public License
19
+ 2.0 (or, at your option, GPL-3.0-or-later). Source: <https://github.com/kieler/elkjs>
20
20
 
21
- The official D2 WebAssembly package is redistributed as an npm dependency for
22
- SVG rendering. The CLI can optionally invoke a separately installed D2
23
- executable for PNG and PDF output.
21
+ `elkjs` lays out the diagram views (layered layout, routes, and labels).
22
+
23
+ ## resvg (@resvg/resvg-wasm)
24
+
25
+ Copyright the resvg authors. Licensed under the Mozilla Public License 2.0.
26
+ Source: <https://github.com/yisibl/resvg-js>
27
+
28
+ The resvg WebAssembly package is an npm dependency that turns diagrams into
29
+ PNG files.
30
+
31
+ ## Liberation Sans
32
+
33
+ Digitized data copyright (c) 2010 Google Corporation; copyright (c) 2012 Red
34
+ Hat, Inc. Licensed under the SIL Open Font License 1.1. Source:
35
+ <https://github.com/liberationfonts/liberation-fonts>
36
+
37
+ `vendor/fonts/LiberationSans-Regular.ttf` and `LiberationSans-Bold.ttf` are
38
+ redistributed (license in `vendor/fonts/LICENSE-LiberationSans.txt`): PNG files
39
+ are rendered with them, and their glyph widths size the boxes of every
40
+ diagram.
24
41
 
25
42
  ## 3d-force-graph and three.js
26
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bc-atlas",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "BC Atlas maps Microsoft Dynamics 365 Business Central AL projects: architecture diagrams, health checks, reports, and documentation generated from source.",
5
5
  "homepage": "https://github.com/SchulzOli/BC-Atlas#readme",
6
6
  "bugs": {
@@ -38,7 +38,8 @@
38
38
  "site:build": "vitepress build website && node ./scripts/site-assets.js",
39
39
  "site:preview": "vitepress preview website",
40
40
  "version:set": "node ./scripts/set-version.js",
41
- "update:graph-vendor": "node ./scripts/build-graph-vendor.js"
41
+ "update:graph-vendor": "node ./scripts/build-graph-vendor.js",
42
+ "update:font-metrics": "node ./scripts/build-font-metrics.js"
42
43
  },
43
44
  "keywords": [
44
45
  "al",
@@ -62,7 +63,8 @@
62
63
  },
63
64
  "dependencies": {
64
65
  "@modelcontextprotocol/server": "^2.4.0",
65
- "@terrastruct/d2": "^0.1.33",
66
+ "@resvg/resvg-wasm": "^2.6.2",
67
+ "elkjs": "^0.12.1",
66
68
  "fflate": "^0.8.3",
67
69
  "graphology": "^0.26.0",
68
70
  "graphology-communities-louvain": "^2.0.2",
@@ -71,10 +73,14 @@
71
73
  "zod": "^4.6.7"
72
74
  },
73
75
  "devDependencies": {
74
- "3d-force-graph": "^1.80.0",
75
76
  "@modelcontextprotocol/client": "^2.4.0",
77
+ "3d-force-graph": "^1.80.0",
76
78
  "bpmn-moddle": "^10.3.1",
77
79
  "esbuild": "^0.28.2",
80
+ "jsonld": "^9.0.0",
81
+ "n3": "^2.13.12",
82
+ "opentype.js": "^2.0.0",
83
+ "rdfxml-streaming-parser": "^3.3.0",
78
84
  "three": "^0.186.1",
79
85
  "vitepress": "2.0.0-alpha.20"
80
86
  }
package/src/analyzer.js CHANGED
@@ -293,10 +293,18 @@ function enclosingMember(node, source, types) {
293
293
  return undefined;
294
294
  }
295
295
 
296
- function implementationRelations(value, node, source, add) {
296
+ function implementationRelations(property, value, node, source, add) {
297
+ const bindingKind = {
298
+ implementation: "explicit",
299
+ defaultimplementation: "default",
300
+ unknownvalueimplementation: "unknownValue"
301
+ }[property];
302
+ if (!bindingKind) return;
297
303
  const identifier = String.raw`(?:"((?:""|[^"])*)"|([A-Za-z_][\w.]*))`;
298
304
  const pattern = new RegExp(`${identifier}\\s*=\\s*${identifier}`, "gu");
299
- const member = enclosingMember(node, source, ["enum_value_declaration"]);
305
+ const member = bindingKind === "explicit"
306
+ ? enclosingMember(node, source, ["enum_value_declaration"])
307
+ : undefined;
300
308
  for (const match of value.matchAll(pattern)) {
301
309
  const contract = (match[1] ?? match[2]).replaceAll('""', '"');
302
310
  const target = (match[3] ?? match[4]).replaceAll('""', '"');
@@ -304,6 +312,7 @@ function implementationRelations(value, node, source, add) {
304
312
  target,
305
313
  targetType: "codeunit",
306
314
  kind: "selects",
315
+ bindingKind,
307
316
  contract,
308
317
  member
309
318
  });
@@ -496,8 +505,8 @@ function referencesFor(objectNode, source, file) {
496
505
  if (name === "includedpermissionsets") {
497
506
  includedPermissionSetRelations(value, node, add);
498
507
  }
499
- if (name === "implementation") {
500
- implementationRelations(value, node, source, add);
508
+ if (["implementation", "defaultimplementation", "unknownvalueimplementation"].includes(name)) {
509
+ implementationRelations(name, value, node, source, add);
501
510
  }
502
511
  if (name === "runobject") {
503
512
  const match = value.match(
@@ -533,6 +542,9 @@ function callsFor(node, source, file) {
533
542
  ? functionNode.childForFieldName("object")
534
543
  : undefined;
535
544
  const argumentsNode = callNode.childForFieldName("arguments");
545
+ const argumentsList = argumentsNode?.namedChildren.map((argument) =>
546
+ source.slice(argument.startIndex, argument.endIndex).trim()
547
+ ) ?? [];
536
548
  return {
537
549
  name: cleanName(source.slice(
538
550
  (memberNode ?? functionNode).startIndex,
@@ -543,6 +555,7 @@ function callsFor(node, source, file) {
543
555
  : undefined,
544
556
  receiverCall: receiverNode?.type === "call_expression" ? describe(receiverNode) : undefined,
545
557
  arity: argumentsNode?.namedChildCount ?? 0,
558
+ ...(argumentsList.length ? { arguments: argumentsList } : {}),
546
559
  scope: node.startPosition.row + 1,
547
560
  location: locationFor(callNode, file)
548
561
  };
@@ -610,6 +623,149 @@ function variableType(node, source) {
610
623
  return declaredType(type);
611
624
  }
612
625
 
626
+ function fallbackStatements(node, source, file, conditional = false) {
627
+ if (!node) return [];
628
+ if (node.type === "statement_block") {
629
+ return node.namedChildren.flatMap((child) =>
630
+ fallbackStatements(child, source, file, conditional)
631
+ );
632
+ }
633
+ if (node.type === "if_statement") {
634
+ return [{
635
+ kind: "conditional",
636
+ then: fallbackStatements(node.childForFieldName("then_branch"), source, file, true),
637
+ else: node.childForFieldName("else_branch")
638
+ ? fallbackStatements(node.childForFieldName("else_branch"), source, file, true)
639
+ : null,
640
+ location: locationFor(node, file)
641
+ }];
642
+ }
643
+ if (node.type === "case_statement") {
644
+ const body = node.childForFieldName("body");
645
+ const branches = (body?.namedChildren ?? [])
646
+ .filter((child) => child.type === "case_branch")
647
+ .map((branch) => fallbackStatements(
648
+ branch.childForFieldName("body"),
649
+ source,
650
+ file,
651
+ true
652
+ ));
653
+ const elseBranch = node.namedChildren.find((child) => child.type === "case_else_branch");
654
+ if (elseBranch) {
655
+ branches.push(elseBranch.namedChildren
656
+ .filter((child) => child.type !== "else_keyword")
657
+ .flatMap((child) => fallbackStatements(child, source, file, true)));
658
+ }
659
+ return [{
660
+ kind: "conditional",
661
+ branches,
662
+ hasImplicitEmptyPath: !elseBranch,
663
+ location: locationFor(node, file)
664
+ }];
665
+ }
666
+ if (["while_statement", "for_statement", "repeat_statement"].includes(node.type)) {
667
+ const body = node.childForFieldName("body");
668
+ return [{
669
+ kind: "conditional",
670
+ branches: [fallbackStatements(body, source, file, true)],
671
+ hasImplicitEmptyPath: node.type !== "repeat_statement",
672
+ location: locationFor(node, file)
673
+ }];
674
+ }
675
+ if (node.type === "call_expression") {
676
+ const functionNode = node.childForFieldName("function");
677
+ const memberNode = functionNode?.type === "member_expression"
678
+ ? functionNode.childForFieldName("member")
679
+ : undefined;
680
+ const receiverNode = functionNode?.type === "member_expression"
681
+ ? functionNode.childForFieldName("object")
682
+ : undefined;
683
+ const target = functionNode
684
+ ? cleanName(source.slice(
685
+ (memberNode ?? functionNode).startIndex,
686
+ (memberNode ?? functionNode).endIndex
687
+ ))
688
+ : undefined;
689
+ const argumentsNode = node.childForFieldName("arguments");
690
+ return [{
691
+ kind: "call",
692
+ target,
693
+ receiver: receiverNode
694
+ ? cleanName(source.slice(receiverNode.startIndex, receiverNode.endIndex))
695
+ : undefined,
696
+ arity: argumentsNode?.namedChildCount ?? 0,
697
+ directError: !receiverNode && target?.toLocaleLowerCase("en-US") === "error",
698
+ conditional,
699
+ location: locationFor(node, file)
700
+ }];
701
+ }
702
+ if (node.type === "exit_statement") {
703
+ const returnNode = node.childForFieldName("return_value");
704
+ return [{
705
+ kind: "return",
706
+ value: returnNode ? source.slice(returnNode.startIndex, returnNode.endIndex).trim() : undefined,
707
+ constant: Boolean(returnNode && [
708
+ "boolean",
709
+ "integer",
710
+ "decimal",
711
+ "string_literal"
712
+ ].includes(returnNode.type)),
713
+ conditional,
714
+ location: locationFor(node, file)
715
+ }];
716
+ }
717
+ if (node.type === "assignment_statement") {
718
+ const targetNode = node.childForFieldName("left");
719
+ return [{
720
+ kind: "assignment",
721
+ target: targetNode
722
+ ? cleanName(source.slice(targetNode.startIndex, targetNode.endIndex))
723
+ : undefined,
724
+ conditional,
725
+ location: locationFor(node, file)
726
+ }];
727
+ }
728
+ return [{
729
+ kind: "other",
730
+ conditional,
731
+ location: locationFor(node, file)
732
+ }];
733
+ }
734
+
735
+ function fallbackFactsFor(node, source, file, hasBody) {
736
+ if (!hasBody) return undefined;
737
+ const codeBlock = node.namedChildren.find((child) =>
738
+ ["code_block", "statement_block"].includes(child.type)
739
+ );
740
+ const body = codeBlock?.childForFieldName("body") ??
741
+ codeBlock?.namedChildren.find((child) => child.type === "statement_block");
742
+ return {
743
+ complete: !node.hasError && !codeBlock?.hasError,
744
+ statements: fallbackStatements(body, source, file),
745
+ ...((node.hasError || codeBlock?.hasError)
746
+ ? { limitations: ["The AL parser reported errors in this procedure body."] }
747
+ : {})
748
+ };
749
+ }
750
+
751
+ function resolveFallbackCalls(statements, procedures) {
752
+ for (const statement of statements) {
753
+ if (statement.kind === "conditional") {
754
+ if (statement.then) resolveFallbackCalls(statement.then, procedures);
755
+ if (statement.else) resolveFallbackCalls(statement.else, procedures);
756
+ for (const branch of statement.branches ?? []) {
757
+ resolveFallbackCalls(branch, procedures);
758
+ }
759
+ continue;
760
+ }
761
+ if (statement.kind !== "call" || statement.directError) continue;
762
+ const candidates = procedures.get(`${statement.target?.toLocaleLowerCase("en-US")}:${statement.arity}`) ?? [];
763
+ const target = !statement.receiver && candidates.length === 1 ? candidates[0] : undefined;
764
+ statement.resolved = Boolean(target?.fallbackFacts?.complete);
765
+ if (target) statement.targetSignature = target.signature;
766
+ }
767
+ }
768
+
613
769
  function membersFor(objectNode, source, file) {
614
770
  const procedures = [];
615
771
  const fields = [];
@@ -624,18 +780,24 @@ function membersFor(objectNode, source, file) {
624
780
  const name = fieldText(node, "name", source);
625
781
  if (name) {
626
782
  const returnTypeNode = node.childForFieldName("return_type");
627
- const returnType = returnTypeNode
628
- ? declaredType(source.slice(returnTypeNode.startIndex, returnTypeNode.endIndex).trim())
629
- : {};
783
+ const header = procedureHeader(node, source);
784
+ const returnTypeText = returnTypeNode
785
+ ? source.slice(returnTypeNode.startIndex, returnTypeNode.endIndex).trim()
786
+ : header.match(/\)\s*:\s*([^;\r\n]+)$/u)?.[1]?.trim();
787
+ const returnType = declaredType(returnTypeText);
630
788
  const attributes = [
631
789
  ...node.namedChildren
632
790
  .filter((child) => child.type === "attribute_item")
633
791
  .map((child) => source.slice(child.startIndex, child.endIndex)),
634
792
  ...attributesFor(node, source)
635
793
  ];
794
+ const hasBody = node.namedChildren.some((child) =>
795
+ ["code_block", "statement_block"].includes(child.type)
796
+ );
797
+ const fallbackFacts = fallbackFactsFor(node, source, file, hasBody);
636
798
  procedures.push({
637
799
  name,
638
- header: procedureHeader(node, source),
800
+ header,
639
801
  visibility: node.type === "trigger_declaration"
640
802
  ? undefined
641
803
  : fieldText(node, "modifier", source)?.toLowerCase() ?? "public",
@@ -647,6 +809,8 @@ function membersFor(objectNode, source, file) {
647
809
  : attributes.some((attribute) => /\[(?:Integration|Business)Event\b/iu.test(attribute))
648
810
  ? "event"
649
811
  : "procedure",
812
+ hasBody,
813
+ ...(fallbackFacts ? { fallbackFacts } : {}),
650
814
  location: locationFor(node, file),
651
815
  calls: callsFor(node, source, file),
652
816
  attributes,
@@ -825,6 +989,18 @@ function membersFor(objectNode, source, file) {
825
989
  .map(({ order, location }) => ({ kind: "commit", order, location }));
826
990
  procedure.transactionSegments = procedure.transactionBoundaries.length + 1;
827
991
  }
992
+ const localProcedures = new Map();
993
+ for (const procedure of procedures) {
994
+ const key = `${procedure.name.toLocaleLowerCase("en-US")}:${procedure.arity}`;
995
+ const candidates = localProcedures.get(key) ?? [];
996
+ candidates.push(procedure);
997
+ localProcedures.set(key, candidates);
998
+ }
999
+ for (const procedure of procedures) {
1000
+ if (procedure.fallbackFacts) {
1001
+ resolveFallbackCalls(procedure.fallbackFacts.statements, localProcedures);
1002
+ }
1003
+ }
828
1004
  const transactionBoundaries = procedures.flatMap((procedure) =>
829
1005
  procedure.transactionBoundaries.map((boundary) => ({
830
1006
  ...boundary,
@@ -854,8 +1030,8 @@ function dataOperationRelations(members, object) {
854
1030
  .map((variable) => [variable.name.toLowerCase(), variable])
855
1031
  );
856
1032
  if (sourceTable) {
857
- variables.set("rec", { target: sourceTable, temporary: false });
858
- variables.set("xrec", { target: sourceTable, temporary: false });
1033
+ variables.set("rec", { target: sourceTable, temporary: object.sourceTableTemporary });
1034
+ variables.set("xrec", { target: sourceTable, temporary: object.sourceTableTemporary });
859
1035
  }
860
1036
  let transactionSegment = 1;
861
1037
  for (const call of procedure.calls) {
@@ -874,7 +1050,10 @@ function dataOperationRelations(members, object) {
874
1050
  kind,
875
1051
  relationClass: "runtime",
876
1052
  operation: call.name,
877
- temporary: variable.temporary,
1053
+ recordVariable: call.receiver,
1054
+ temporary: variable.parameter && variable.temporary !== true
1055
+ ? undefined
1056
+ : variable.temporary,
878
1057
  member: procedure.signature,
879
1058
  sourceProcedure: procedure.signature,
880
1059
  transactionSegment,
@@ -885,6 +1064,24 @@ function dataOperationRelations(members, object) {
885
1064
  return relations;
886
1065
  }
887
1066
 
1067
+ function subscriberRelations(procedures) {
1068
+ const relations = [];
1069
+ for (const procedure of procedures) {
1070
+ for (const attribute of procedure.attributes ?? []) {
1071
+ const match = attribute.match(/^\[\s*EventSubscriber\s*\(\s*ObjectType::(Codeunit|Table|Page|Report|XmlPort)\s*,\s*(?:Codeunit|Database|Page|Report|XmlPort)::\s*(?:"((?:""|[^"])*)"|([^,\]\s]+))/iu);
1072
+ if (!match) continue;
1073
+ relations.push({
1074
+ target: (match[2] ?? match[3]).replaceAll('""', '"'),
1075
+ targetType: match[1].toLowerCase(),
1076
+ kind: "subscribes",
1077
+ member: procedure.name,
1078
+ location: procedure.location
1079
+ });
1080
+ }
1081
+ }
1082
+ return relations;
1083
+ }
1084
+
888
1085
  function helperDataOperationRelations(members, directRelations) {
889
1086
  const procedures = new Map();
890
1087
  for (const procedure of members.procedures) {
@@ -971,7 +1168,18 @@ async function objectsFromSource(source, file) {
971
1168
  });
972
1169
  }
973
1170
  const members = membersFor(node, source, file);
974
- const dataRelations = dataOperationRelations(members, { type, name, base, relations });
1171
+ const properties = propertiesFor(node, source);
1172
+ const sourceTableTemporary = /^true$/iu.test(
1173
+ propertyValueFor(node, source, "sourcetabletemporary") ?? "false"
1174
+ );
1175
+ relations.push(...subscriberRelations(members.procedures));
1176
+ const dataRelations = dataOperationRelations(members, {
1177
+ type,
1178
+ name,
1179
+ base,
1180
+ relations,
1181
+ sourceTableTemporary
1182
+ });
975
1183
  relations.push(...dataRelations, ...helperDataOperationRelations(members, dataRelations));
976
1184
 
977
1185
  const assignableValue = type === "permissionset"
@@ -987,7 +1195,7 @@ async function objectsFromSource(source, file) {
987
1195
  namespace,
988
1196
  usings,
989
1197
  file,
990
- properties: propertiesFor(node, source),
1198
+ properties,
991
1199
  relations,
992
1200
  procedures: members.procedures,
993
1201
  fields: members.fields,