bc-atlas 0.8.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,94 @@ 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.10.0 - 2026-10-29
8
+
9
+ The npm package skips 0.9.0 so it shares its version with the VS Code
10
+ extension, whose previous release was 0.9.0.
11
+
12
+ ### Added
13
+
14
+ - Shared core: objects that many components use but none owns (setup
15
+ tables, master data) form a component of their own instead of gluing the
16
+ others together.
17
+ - App candidates and namespaces: components that are coupled more to each
18
+ other than to the rest could become one app together. Every proposed
19
+ component gets a role (app, component, shared core) and a suggested
20
+ namespace, in the JSON (`proposal.recommendations`), the reports, the
21
+ component's description, and the Architecture page.
22
+ - `id:50100..50199` selects objects by ID range; proposals use it for code
23
+ without namespaces or functional folders.
24
+ - A page per component (click it on the Architecture page): its role, layer,
25
+ the components it may use, suggested namespace, app candidate, and
26
+ selectors; the groups inside it (sub-domains clustered by coupling and
27
+ named after their namespace, folder, or shared words); a graph of its
28
+ objects with one island per group and, on request, its links to other
29
+ components in green (allowed) and red (wrong way); per other component
30
+ whether it may be used and how often it is; every object; and its work
31
+ items. Also in the exported app.
32
+ - Dependencies between components, should vs. is: a map of the components in
33
+ their layers with green arrows for allowed and red arrows for wrong-way
34
+ dependencies, and a matrix that tints every pair by the rules and counts
35
+ today's object links in it.
36
+ - `bca docs` turns more TestPage code into steps: `Field.Value :=`,
37
+ `Filter.SetFilter`, `GoToKey`, the `OK`, `Cancel`, `Yes`, and `No` buttons,
38
+ `First`/`Last`/`Next`/`Previous`, `Lookup`, `Drilldown`, `AssistEdit`,
39
+ `Page.Trap()` ("The ... page opens."), `asserterror` ("Business Central
40
+ shows an error message."), calls over several lines, and the pages and
41
+ confirmations a test answers in `[ModalPageHandler]`, `[PageHandler]`,
42
+ `[RequestPageHandler]`, and `[ConfirmHandler]` procedures ("If the ... page
43
+ opens, ...").
44
+
45
+ ### Changed
46
+
47
+ - The architecture proposal comes from the relations, not from namespaces.
48
+ Relations are typed (structural, data, behavioral, loose, administrative)
49
+ and weighted; inside each app or module, the objects that are coupled more
50
+ to each other than to the rest form a component (the best of several
51
+ Louvain runs, refined object by object, with today's namespaces and
52
+ folders competing as candidates). It works the same for apps without
53
+ namespaces (type folders, ID blocks) and for a mix. `--group-by coupling`
54
+ is now the same as `auto`; `namespace` and `folder` let the structure
55
+ decide and place objects without one by their relations.
56
+ - A page or query and its source table are a relation of their own
57
+ (`source`, weight 4) instead of a read.
58
+ - VS Code and `bca export --view app` keep the parse results of every AL file
59
+ on disk (`BCA_CACHE_DIR`, default `<temp>/bc-atlas/parse`) and across
60
+ changes, so reopening a project or saving a file parses only what changed.
61
+ The Docs page and the explorer reuse them instead of parsing the app again.
62
+ On a 2,931-object app the overview opens in about 8 seconds instead of 15,
63
+ and the Docs page in about 11 instead of 25.
64
+ - The npm package and the VS Code extension are released under one version
65
+ number from now on, starting with 0.10.0; `npm run version:set -- <version>`
66
+ sets both, and the tests and publish workflows refuse a mismatch.
67
+ - Dependencies: zod 4.6.7, web-tree-sitter 0.27.1, and the MCP SDK 2.4.0.
68
+
69
+ ### Fixed
70
+
71
+ - The report no longer writes an absolute path into README.md when a view has
72
+ too many nodes for the bundled SVG renderer: it keeps the view as D2, links
73
+ it relative to the README, removes an SVG left by an earlier run, and the
74
+ MCP report tool marks it `d2Only`.
75
+ - Opening a scenario or use case on the Docs page no longer leaves the page
76
+ blank in VS Code (or loads the whole app into the frame in an exported
77
+ app): the guide is embedded through `srcdoc`, where `#links` resolve
78
+ against the host page, so it now scrolls to the target itself.
79
+ - `bca docs` reads AL tests written the way Microsoft writes the Business
80
+ Central tests: `[SCENARIO 218101]` with a work item number (kept as a
81
+ reference), `[FEATURE] [Sales] [Invoice]` lists (`[UI]` and `[UT]` are left
82
+ out), the codeunit's `[FEATURE]` in its `OnRun` trigger, and `[VERIFY]` as
83
+ an expected result. Such test codeunits used to give no scenarios at all.
84
+ - A documented test ends where its procedure ends: tags and TestPage calls of
85
+ helper procedures after it (and, for the last test, the codeunit's global
86
+ variables) no longer leak into its guide and its fingerprint.
87
+ - Directory runs of `bca docs` leave out `[SCENARIO]` tests without any UI
88
+ step (unless they have a `[DOC-ID]`) and report how many; they used to
89
+ become empty guides.
90
+ - Guides for descriptive scenarios ("Users can post ...") no longer read
91
+ "Use this guide to users can ...", empty values no longer show as `****`,
92
+ the note about example values only appears when the steps show any, and
93
+ test case tables keep the context of loops and dialogs in one row.
94
+
7
95
  ## 0.8.0 - 2026-10-29
8
96
 
9
97
  ### Added
package/README.md CHANGED
@@ -44,7 +44,7 @@ In VS Code, install the **BC Atlas** extension and open an AL project:
44
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`. |
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.8.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.11.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bc-atlas",
3
- "version": "0.8.0",
3
+ "version": "0.10.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": {
@@ -36,7 +36,8 @@
36
36
  "update:grammar": "node ./scripts/update-grammar.js",
37
37
  "site:dev": "vitepress dev website",
38
38
  "site:build": "vitepress build website && node ./scripts/site-assets.js",
39
- "site:preview": "vitepress preview website"
39
+ "site:preview": "vitepress preview website",
40
+ "version:set": "node ./scripts/set-version.js"
40
41
  },
41
42
  "keywords": [
42
43
  "al",
@@ -59,18 +60,18 @@
59
60
  "node": ">=22"
60
61
  },
61
62
  "dependencies": {
62
- "@modelcontextprotocol/server": "^2.3.0",
63
+ "@modelcontextprotocol/server": "^2.4.0",
63
64
  "@terrastruct/d2": "^0.1.33",
64
65
  "3d-force-graph": "^1.80.0",
65
66
  "fflate": "^0.8.3",
66
67
  "graphology": "^0.26.0",
67
68
  "graphology-communities-louvain": "^2.0.2",
68
69
  "graphology-metrics": "^2.4.1",
69
- "web-tree-sitter": "^0.27.0",
70
- "zod": "^4.6.5"
70
+ "web-tree-sitter": "^0.27.1",
71
+ "zod": "^4.6.7"
71
72
  },
72
73
  "devDependencies": {
73
- "@modelcontextprotocol/client": "^2.3.0",
74
+ "@modelcontextprotocol/client": "^2.4.0",
74
75
  "bpmn-moddle": "^10.3.1",
75
76
  "vitepress": "2.0.0-alpha.20"
76
77
  }
@@ -25,6 +25,11 @@ export function createService(workspace) {
25
25
  overview: () => workspace.overview(),
26
26
  health: () => workspace.healthReport(),
27
27
  architecture: (params) => workspace.architecture(architectureSettings(params)),
28
+ component: async ({ name, ...params } = {}) => {
29
+ const component = await workspace.component(String(name ?? ""), architectureSettings(params));
30
+ if (!component) throw new Error("component not found");
31
+ return component;
32
+ },
28
33
  objects: () => workspace.objects(),
29
34
  object: async ({ id } = {}) => {
30
35
  const object = await workspace.object(String(id ?? ""));
@@ -39,9 +39,10 @@ function compactDetails(index) {
39
39
 
40
40
  export async function createSnapshot(workspace, { log = () => {}, history = false, last, title } = {}) {
41
41
  const overview = { ...await workspace.overview(), ...(title ? { title } : {}) };
42
- const [health, architecture, index, docsHtml] = await Promise.all([
42
+ const [health, architecture, components, index, docsHtml] = await Promise.all([
43
43
  workspace.healthReport(),
44
44
  workspace.architecture(),
45
+ workspace.components(),
45
46
  workspace.objectIndex(),
46
47
  workspace.docsHtml()
47
48
  ]);
@@ -66,6 +67,7 @@ export async function createSnapshot(workspace, { log = () => {}, history = fals
66
67
  overview: { ...overview, live: false, root: undefined, projectRoot: undefined },
67
68
  health,
68
69
  architecture,
70
+ components,
69
71
  objects: index.list,
70
72
  nodes,
71
73
  details,
@@ -214,6 +214,63 @@ details.steps ol, details.steps ul { margin: 6px 0 2px; padding-left: 18px; }
214
214
  .legend .bad::before { background: var(--error); }
215
215
  .controls { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
216
216
 
217
+ /* Dependencies between components: green where allowed, red where against the architecture */
218
+ .dependency-map { overflow: auto; margin-top: 8px; }
219
+ .dependency-map svg { width: 100%; height: auto; display: block; margin: 0 auto; }
220
+ .dependency-map .dep { fill: none; opacity: 0.75; transition: opacity 0.15s; }
221
+ .dependency-map .dep.allowed, .dependency-map .tip.allowed { stroke: var(--ok); fill: var(--ok); }
222
+ .dependency-map .dep.allowed { fill: none; }
223
+ .dependency-map .dep.wrong, .dependency-map .tip.wrong { stroke: var(--error); fill: var(--error); }
224
+ .dependency-map .dep.wrong { fill: none; }
225
+ .dependency-map svg.focused .dep { opacity: 0.06; }
226
+ .dependency-map svg.focused .dep.on { opacity: 1; }
227
+ .dependency-map.hide-allowed .dep.allowed, .dependency-map.hide-wrong .dep.wrong { display: none; }
228
+ .dependency-map .dep-node { cursor: pointer; }
229
+ .dependency-map .dep-node rect { fill: var(--surface); stroke: var(--border-strong); }
230
+ .dependency-map .dep-node.extractable rect { stroke: var(--ok); }
231
+ .dependency-map .dep-node.blocked rect { stroke: var(--error); }
232
+ .dependency-map .dep-node:hover rect { stroke-width: 2; }
233
+ .dependency-map .dep-node text { fill: var(--text); font-size: 12.5px; font-weight: 600; }
234
+ .dependency-map .dep-node .count { fill: var(--muted); font-weight: 400; }
235
+ .dependency-map .layer-name { fill: var(--muted); font-size: 12px; }
236
+ table.matrix { border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
237
+ table.matrix th { font-weight: 600; color: var(--muted); padding: 4px; text-align: center; position: sticky; top: 0; background: var(--surface); z-index: 1; }
238
+ table.matrix th.row-head { text-align: left; white-space: nowrap; position: sticky; left: 0; padding-right: 10px; z-index: 0; }
239
+ table.matrix td { min-width: 34px; height: 26px; text-align: center; border: 1px solid var(--surface); }
240
+ table.matrix td.allowed { background: color-mix(in srgb, var(--ok) 16%, transparent); }
241
+ table.matrix td.forbidden { background: color-mix(in srgb, var(--error) 6%, transparent); color: var(--muted); }
242
+ table.matrix td.wrong { background: color-mix(in srgb, var(--error) 26%, transparent); color: var(--error); font-weight: 700; }
243
+ table.matrix td.self { background: var(--surface-2); }
244
+ .matrix-legend .swatch { margin-left: 4px; }
245
+ .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
246
+ .swatch.allowed { background: color-mix(in srgb, var(--ok) 40%, transparent); }
247
+ .swatch.forbidden { background: color-mix(in srgb, var(--error) 14%, transparent); }
248
+ .swatch.wrong { background: color-mix(in srgb, var(--error) 60%, transparent); }
249
+
250
+ /* Component page */
251
+ .group-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
252
+ .group-chips .swatch { margin-right: 0; }
253
+ .toolbar.padded { padding: 10px 18px 6px; }
254
+ .canvas.component-canvas { height: 66vh; min-height: 420px; background: var(--surface-2); }
255
+ .component-graph .link { stroke: var(--muted); stroke-opacity: 0.28; stroke-width: 0.8; }
256
+ .component-graph .ext { stroke-opacity: 0.55; }
257
+ .component-graph .ext.allowed { stroke: var(--ok); }
258
+ .component-graph .ext.wrong { stroke: var(--error); stroke-opacity: 0.75; }
259
+ .component-graph .ext.in { stroke-dasharray: 5 4; }
260
+ .component-graph .obj circle { stroke: var(--surface); stroke-width: 1.2; }
261
+ .component-graph .obj circle.flagged { stroke: var(--error); stroke-width: 2.5; }
262
+ .component-graph .obj:hover circle { stroke: var(--text); stroke-width: 2; }
263
+ .component-graph .obj text { fill: var(--text); font-size: 10px; pointer-events: none; }
264
+ .component-graph .island { fill-opacity: 0.07; stroke-opacity: 0.35; stroke-width: 1; stroke-dasharray: 4 4; }
265
+ .component-graph .group-name { font-size: 15px; font-weight: 700; paint-order: stroke; stroke: var(--surface-2); stroke-width: 4px; pointer-events: none; }
266
+ .component-graph .group-name .count { fill: var(--muted); font-weight: 400; }
267
+ .component-graph .other circle { fill: var(--surface); stroke-width: 2.5; }
268
+ .component-graph .other.allowed circle { stroke: var(--ok); }
269
+ .component-graph .other.wrong circle { stroke: var(--error); }
270
+ .component-graph .other text { fill: var(--text); font-size: 12px; font-weight: 600; paint-order: stroke; stroke: var(--surface); stroke-width: 3px; }
271
+ .component-graph.focused [data-group], .component-graph.focused [data-groups] { opacity: 0.12; }
272
+ .component-graph.focused .on { opacity: 1; }
273
+
217
274
  /* Diagrams */
218
275
  .diagram-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 18px; align-items: start; }
219
276
  .diagram-form { display: grid; gap: 12px; position: sticky; top: 90px; }